diff --git a/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts b/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts index 9813b8c4..12a6e867 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts +++ b/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts @@ -161,20 +161,14 @@ export function createDeveloperToolsController() { settings.developer.setAmicodePath(DEFAULT_AMICODE_PATH) } } - // Don't persist enabled=false — the marketplace build doesn't render - // Developer Tools at all, and persisting false prevents the dev build - // from showing it after a bash-script bootstrap without toggle interaction. + settings.developer.setEnabled(value) if (value) { - settings.developer.setEnabled(value) // Toggle ON: trigger a full rebuild (shows "Rebuilding..." status) rebuild("local") } else { - // Toggle OFF: show switching status, then the extension restores + reloads. - // Don't persist false — the reload brings up the marketplace build which - // doesn't have this section anyway. + // Toggle OFF: restore marketplace build and reload. setRebuildState("rebuilding") setRebuildError(undefined) - // Send enabled=false explicitly (can't rely on the signal since we didn't persist it) if (!inAmicode()) return setPending(true) setStatus(undefined) diff --git a/packages/app-bundle/overlay/packages/app/src/context/settings.tsx b/packages/app-bundle/overlay/packages/app/src/context/settings.tsx index 25fb8be8..299cbc27 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/settings.tsx +++ b/packages/app-bundle/overlay/packages/app/src/context/settings.tsx @@ -216,7 +216,7 @@ const defaultSettings: Settings = { configDir: "", }, developer: { - enabled: true, + enabled: false, opencodePath: "", amicodePath: "", }, diff --git a/packages/extension/.gitignore b/packages/extension/.gitignore index b3fe3184..ac059ade 100644 --- a/packages/extension/.gitignore +++ b/packages/extension/.gitignore @@ -2,3 +2,4 @@ dev/brand_harness/main.js dev/pulseplot_harness/main.js scripts/benchmark/out/ scripts/benchmark/traces/ +dist.marketplace-backup/ diff --git a/packages/extension/src/chat_bridge.ts b/packages/extension/src/chat_bridge.ts index d1469af7..fe3b1756 100644 --- a/packages/extension/src/chat_bridge.ts +++ b/packages/extension/src/chat_bridge.ts @@ -258,34 +258,40 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean }; if (!enabled) { - // Toggle OFF: clear overrides, restore marketplace extension, and reload. + // Toggle OFF: clear overrides, reinstall the marketplace extension, and reload. void vscode.workspace.getConfiguration("amicode").update("opencodeBinary", "", vscode.ConfigurationTarget.Global); void vscode.workspace.getConfiguration("amicode").update("devAssetRoot", "", vscode.ConfigurationTarget.Global); - // Restore the marketplace extension dist if a backup exists + // Guard: write a temporary marker so onboarding won't re-trigger after + // the reinstall. The marker is consumed (deleted) on next activation. + // A manual uninstall by the user does NOT write this marker, so + // onboarding correctly re-triggers for genuine fresh installs. + try { + const { writeDevtoolsRestoreMarker } = require("./substrate/vault_store") as typeof import("./substrate/vault_store"); + writeDevtoolsRestoreMarker(); + } catch { /* non-critical — worst case onboarding re-shows */ } + + // Reinstall from the marketplace to restore the user's current release. + // The old backup approach was fragile (went stale on extension updates). + // Uninstall+install is the only reliable way to restore a clean dist — + // `--force` alone says "already installed" for the same version. const installedExt = vscode.extensions.getExtension("harmoniqs.amicode"); if (installedExt) { - const backupDist = path.join(installedExt.extensionPath, "dist.marketplace-backup"); - const installedDist = path.join(installedExt.extensionPath, "dist"); - if (fs.existsSync(backupDist)) { - try { - const backupFiles = fs.readdirSync(backupDist).filter(f => f.endsWith(".js") || f.endsWith(".js.map")); - for (const f of backupFiles) { - fs.copyFileSync(path.join(backupDist, f), path.join(installedDist, f)); - } - console.log("[amicode/bridge] restored marketplace dist from backup"); - } catch (restoreErr) { - console.warn("[amicode/bridge] marketplace dist restore failed:", restoreErr); + const { exec } = require("child_process") as typeof import("child_process"); + const extId = "harmoniqs.amicode"; + exec(`code --uninstall-extension ${extId} && code --install-extension ${extId}`, { timeout: 60_000 }, (err) => { + if (err) { + console.warn("[amicode/bridge] marketplace reinstall failed:", err.message); + } else { + console.log("[amicode/bridge] reinstalled marketplace extension"); } - } + void vscode.commands.executeCommand("workbench.action.reloadWindow"); + }); + } else { + void vscode.commands.executeCommand("workbench.action.reloadWindow"); } - // Don't restart server separately — reloading the window does it. - // Don't send reloadNeeded — the auto-reload handles it silently. io.postToWebview(reply); - setTimeout(() => { - void vscode.commands.executeCommand("workbench.action.reloadWindow"); - }, 300); return true; } diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 8a111445..eb6b973f 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -26,6 +26,7 @@ import { readMemoryIndexLines, hasOnboardingCompleted, onboardingDir, + consumeDevtoolsRestoreMarker, } from "./substrate/vault_store"; import { resolveMountStack, personalMount, type Mount, type MountStack } from "./substrate/mount_store"; import { @@ -614,6 +615,7 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro vaultDir !== "" && readProfileMd(vaultDir) === "" && !hasOnboardingCompleted(onboardingDir()) && + !consumeDevtoolsRestoreMarker() && !profileHasIdentity(); // the welcome WIZARD already collected identity — don't re-interview if (shouldOnboard && overture && score0) { // Chained: ONE compiled section, ONE manifest (id `overture`, stages = diff --git a/packages/extension/src/substrate/vault_store.ts b/packages/extension/src/substrate/vault_store.ts index a9a02ef2..8e64aaab 100644 --- a/packages/extension/src/substrate/vault_store.ts +++ b/packages/extension/src/substrate/vault_store.ts @@ -107,3 +107,28 @@ export function hasOnboardingCompleted(onboardingStreamDir: string): boolean { } return false; } + +// ─── Devtools restore marker ───────────────────────────────────────────────── +// A temporary file that tells the next activation "this reinstall was triggered +// by the devtools toggle, not a manual user action — skip onboarding." The +// marker is consumed (deleted) on read so it only suppresses once. + +const DEVTOOLS_RESTORE_MARKER = ".devtools-restore"; + +/** Write the devtools restore marker. Called by toggle-OFF before uninstall. */ +export function writeDevtoolsRestoreMarker(opsDir: string = amicodeOpsDir()): void { + fs.mkdirSync(opsDir, { recursive: true }); + fs.writeFileSync(path.join(opsDir, DEVTOOLS_RESTORE_MARKER), String(Date.now())); +} + +/** Check and consume the devtools restore marker. Returns true if it existed + * (meaning this activation follows a toggle-OFF reinstall, not a fresh install). */ +export function consumeDevtoolsRestoreMarker(opsDir: string = amicodeOpsDir()): boolean { + const markerPath = path.join(opsDir, DEVTOOLS_RESTORE_MARKER); + try { + fs.unlinkSync(markerPath); + return true; + } catch { + return false; + } +} diff --git a/packages/extension/test/onboarding_routing.test.ts b/packages/extension/test/onboarding_routing.test.ts index 46b070e0..ed400d2f 100644 --- a/packages/extension/test/onboarding_routing.test.ts +++ b/packages/extension/test/onboarding_routing.test.ts @@ -19,6 +19,12 @@ import { writeWelcomeShown, } from "../src/onboarding_routing"; +import { + hasOnboardingCompleted, + writeDevtoolsRestoreMarker, + consumeDevtoolsRestoreMarker, +} from "../src/substrate/vault_store"; + // ─── AC10: Routing predicate (pure function, table-driven) ─────────────────── describe("resolveOnboardingAction — routing predicate (AC10)", () => { @@ -237,3 +243,36 @@ describe("welcome_shown flag semantics (AC6)", () => { expect(readWelcomeShown(file)).toBe(true); }); }); + +// ─── devtools restore marker — toggle-OFF guard ───────────────────────────── + +describe("devtools restore marker — toggle-OFF guard", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devtools-marker-")); + }); + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("consumeDevtoolsRestoreMarker returns false when no marker exists", () => { + expect(consumeDevtoolsRestoreMarker(tmpDir)).toBe(false); + }); + + it("writeDevtoolsRestoreMarker + consumeDevtoolsRestoreMarker returns true and deletes the marker", () => { + writeDevtoolsRestoreMarker(tmpDir); + expect(consumeDevtoolsRestoreMarker(tmpDir)).toBe(true); + // Second consume returns false (marker was deleted) + expect(consumeDevtoolsRestoreMarker(tmpDir)).toBe(false); + }); + + it("does not interfere with onboarding completion state", () => { + // Marker exists but onboarding events.jsonl does not + writeDevtoolsRestoreMarker(tmpDir); + expect(hasOnboardingCompleted(tmpDir)).toBe(false); + // Consuming the marker doesn't create onboarding_completed + consumeDevtoolsRestoreMarker(tmpDir); + expect(hasOnboardingCompleted(tmpDir)).toBe(false); + }); +});