From 416265c9d5319c6f12ff9657fb77134ec3d7344f Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Wed, 5 Aug 2026 19:19:30 +0700 Subject: [PATCH 1/7] Print the manual elevated-PowerShell command instead of staging a .cmd Field result: endpoint protection also quarantines the .ps1 when the self-elevating Install-CoDev-Office.cmd launches it. The only launch mode it tolerates - proven repeatedly - is the user typing the command in an elevated PowerShell themselves. The Windows flow now downloads everything and prints exactly that: open an elevated PowerShell (with the use-a-different-account UAC guidance), then copy-paste two lines - cd into the staging folder and the powershell -ExecutionPolicy Bypass -File command with the profile-safe -ModulesDir/-SkillsRoot paths baked in, quoted where they contain spaces. No wrapper file, no Explorer window, no spawn. Co-Authored-By: Claude Fable 5 --- src/lib/office.ts | 123 +++++++++++++------------------------ tests/lib/download.test.ts | 81 ++++++++---------------- 2 files changed, 67 insertions(+), 137 deletions(-) diff --git a/src/lib/office.ts b/src/lib/office.ts index dfd54f1..f73c1e6 100644 --- a/src/lib/office.ts +++ b/src/lib/office.ts @@ -6,7 +6,6 @@ import { readdirSync, renameSync, rmSync, - writeFileSync, } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; @@ -205,24 +204,28 @@ function manualRunCommand(platform: OfficePlatform, script: string): string { : `bash ${script}`; } -// Windows installs are handed to the user as a right-click .cmd instead of -// being spawned from codevhub: endpoint protection (Kaspersky Endpoint -// Security in the field) silently kills a powershell child of node.exe -// mid-install, while the very same script from the very same folder finishes -// when the user launches it themselves. The wrapper reproduces that -// verified-good ancestry (explorer -> cmd -> powershell) and needs nothing -// typed. Exported for tests. -export function officeWrapperName(uninstall: boolean): string { - return uninstall ? "Uninstall-CoDev-Office.cmd" : "Install-CoDev-Office.cmd"; +// The exact command the user types in an ELEVATED PowerShell — after field +// testing every alternative (spawning from codevhub, a self-elevating .cmd), +// this is the only launch mode the endpoint protection tolerates: it kills +// installers codevhub launches and quarantines the .ps1 a .cmd launches, +// while the same command typed interactively runs to "Verification passed". +// Exported for tests. +export function officeManualWindowsCommand( + script: string, + args: string[], +): string { + const argStr = args.map((a) => ` ${/\s/.test(a) ? `"${a}"` : a}`).join(""); + return `powershell -ExecutionPolicy Bypass -File .\\${script}${argStr}`; } -// Paths baked into the wrapper so a UAC elevation with a DIFFERENT admin -// account cannot strand the install on the admin's profile: the JS module -// tree goes to the shared, account-independent %PUBLIC% dir, and the skills -// root is pinned to the REAL user's profile — codevhub runs unelevated as -// that user, so homedir() is authoritative here. Cross-platform staging -// (--platform windows from another OS) cannot know the target machine's -// user, so only the modules dir is baked there. Exported for tests. +// Paths baked into the printed command so a UAC elevation with a DIFFERENT +// admin account cannot strand the install on the admin's profile: the JS +// module tree goes to the shared, account-independent %PUBLIC% dir, and the +// skills root is pinned to the REAL user's profile — codevhub runs +// unelevated as that user, so homedir() is authoritative here. +// Cross-platform staging (--platform windows from another OS) cannot know +// the target machine's user, so only the modules dir is baked there. +// Exported for tests. export function officeWrapperBakedArgs(hostIsWindows: boolean): string[] { const publicDir = process.env.PUBLIC ?? "C:\\Users\\Public"; const args = ["-ModulesDir", `${publicDir}\\codev-office\\node_modules`]; @@ -271,36 +274,6 @@ export function migrateLegacyOfficeDir(fromDir: string, toDir: string): void { } } -export function officeWrapperContent(script: string, args: string[]): string { - // Self-elevating so a plain DOUBLE-CLICK is enough (some environments strip - // "Run as administrator" from the context menu): when not elevated, the - // .cmd relaunches itself elevated via a UAC prompt. If elevation is - // declined or unavailable (non-admin account), it continues non-elevated — - // the setup script supports that: each component installer raises its own - // permission prompt, and a declined one only skips that component (fatal - // for the .NET SDK alone). - // %~dp0 = the .cmd's own folder (an elevated relaunch starts in System32); - // `pause` keeps the window open so the closing "Verification passed" (or a - // [FAIL] line) stays readable. - const argStr = args.map((a) => ` ${/\s/.test(a) ? `"${a}"` : a}`).join(""); - return [ - "@echo off", - 'cd /d "%~dp0"', - "net session >nul 2>&1", - "if not errorlevel 1 goto :run", - "echo Requesting administrator rights - choose Yes in the prompt...", - "powershell -NoProfile -Command \"Start-Process -FilePath '%~f0' -Verb RunAs\" >nul 2>&1", - "if not errorlevel 1 exit /b 0", - "echo Continuing without administrator rights - each installer will ask for permission separately.", - "echo.", - ":run", - `powershell -ExecutionPolicy Bypass -File ".\\${script}"${argStr}`, - "echo.", - "pause", - "", - ].join("\r\n"); -} - export function installerArgs( parsed: OfficeArgs, platform: OfficePlatform, @@ -461,53 +434,41 @@ export async function runSkillOffice( ? uninstallerArgs(parsed, platform) : installerArgs(parsed, platform); - // Windows: stage a right-click wrapper and stop — see officeWrapperName. + // Windows: never run (or wrap) the installer from codevhub. Endpoint + // protection (Kaspersky Endpoint Security in the field) kills a powershell + // child of node.exe mid-install AND quarantines the .ps1 when a staged + // .cmd launches it — the only launch it tolerates is the user typing the + // command in an elevated PowerShell themselves, which is field-proven to + // run to "Verification passed". Print that exact command, with the + // profile-safe paths baked in so an elevation under a different admin + // account still installs to the real user's profile. if (platform === "windows") { - const wrapper = officeWrapperName(parsed.uninstall); - writeFileSync( - join(dir, wrapper), - officeWrapperContent(script, [ - ...scriptArgs, - ...officeWrapperBakedArgs(hostPlatform === "windows"), - ]), - ); const verb = parsed.uninstall ? "uninstaller" : "installer"; + const commandLine = officeManualWindowsCommand(script, [ + ...scriptArgs, + ...officeWrapperBakedArgs(hostPlatform === "windows"), + ]); console.error(`\nFiles are in ${dir}.`); console.error( `codevhub does not auto-run the Windows ${verb}: endpoint protection ` + - "(e.g. Kaspersky) is known to silently kill installers it launches. Instead:", - ); - console.error( - ` 1. Open that folder in File Explorer (opened for you if possible)`, + "(e.g. Kaspersky) is known to kill installers it launches. Run it " + + "yourself in an ELEVATED PowerShell:", ); - console.error(` 2. Double-click ${wrapper}`); console.error( - ` 3. Choose "Yes" when Windows asks for administrator permission`, + ' 1. Press the Windows key, type "powershell", then press Ctrl+Shift+Enter', ); console.error( - ` (no admin rights? choose "No" - the install continues and each component asks for permission separately)`, + " (in the UAC dialog: More choices -> Use a different account -> enter the ADMIN username and password)", ); + console.error(" 2. Copy-paste these two lines into that window:"); + console.error(` cd "${dir}"`); + console.error(` ${commandLine}`); console.error( - ` 4. Wait for the closing message - the window stays open when done`, + ' 3. Wait for the green "Verification passed" closing message', ); - if (hostPlatform === "windows") { - try { - const explorer = spawn("explorer.exe", [dir], { - detached: true, - stdio: "ignore", - }); - // Best-effort convenience: spawn failures surface as an async - // "error" event (which would crash the process if unhandled), - // not as a throw — the printed path is enough either way. - explorer.on("error", () => {}); - explorer.unref(); - } catch { - // Same best-effort stance for synchronous spawn failures. - } - } - logInfo("office windows handoff staged", { + logInfo("office windows manual handoff", { action: parsed.uninstall ? "office.uninstall" : "office.install", - extra: { platform, dir, wrapper }, + extra: { platform, dir }, }); return 0; } diff --git a/tests/lib/download.test.ts b/tests/lib/download.test.ts index 0120b2d..8a0eae0 100644 --- a/tests/lib/download.test.ts +++ b/tests/lib/download.test.ts @@ -18,9 +18,8 @@ import { ensureStagingDir, installerArgs, migrateLegacyOfficeDir, + officeManualWindowsCommand, officeWrapperBakedArgs, - officeWrapperContent, - officeWrapperName, runSkillOffice, uninstallerArgs, } from "@/lib/office.js"; @@ -399,10 +398,10 @@ describe("runSkillOffice", () => { expect(existsSync(join(dir, "codev-office-windows.zip"))).toBe(true); }); - test("windows staging writes the right-click wrapper, flags baked in, never spawns", async () => { - // Endpoint protection (KES) silently kills installers spawned by - // codevhub on Windows, so the windows flow must never spawn - it - // stages a .cmd the user runs via right-click -> Run as administrator. + test("windows staging stages files only - no wrapper, no spawn", async () => { + // Endpoint protection kills installers codevhub launches AND quarantines + // the .ps1 a staged .cmd launches - the windows flow must only download + // and print the manual elevated-PowerShell command. objects.set("/codev-office-windows.zip", BUNDLE); objects.set("/codev-office-windows-setup.ps1", SCRIPT); const dir = join(tempDir, "office"); @@ -424,26 +423,27 @@ describe("runSkillOffice", () => { ); expect(code).toBe(0); expect(spawns).toEqual([]); - const wrapper = readFileSync(join(dir, "Install-CoDev-Office.cmd"), "utf8"); - expect(wrapper).toContain( - 'powershell -ExecutionPolicy Bypass -File ".\\codev-office-windows-setup.ps1" -SkipVerify -ForceSkills', + expect(existsSync(join(dir, "codev-office-windows.zip"))).toBe(true); + expect(existsSync(join(dir, "Install-CoDev-Office.cmd"))).toBe(false); + }); + + test("the manual command carries flags and baked paths, quoted", () => { + const line = officeManualWindowsCommand("codev-office-windows-setup.ps1", [ + "-SkipVerify", + ...officeWrapperBakedArgs(false), + "-SkillsRoot", + "C:\\Users\\Van Phong\\.config\\codev\\skills", + ]); + expect(line).toContain( + "powershell -ExecutionPolicy Bypass -File .\\codev-office-windows-setup.ps1 -SkipVerify", ); - expect(wrapper).toContain("pause"); - expect(wrapper).toContain('cd /d "%~dp0"'); - // Self-elevation: a plain double-click must request admin rights itself - // (some environments strip "Run as administrator" from the context - // menu) and fall back to a non-elevated run when declined. - expect(wrapper).toContain("net session"); - expect(wrapper).toContain("Start-Process -FilePath '%~f0' -Verb RunAs"); - expect(wrapper).toContain(":run"); - expect(wrapper).toContain("Continuing without administrator rights"); - // Profile-safe path baking: the shared modules dir always, but no - // -SkillsRoot on cross-platform staging - this host's homedir says - // nothing about the target machine's user. - expect(wrapper).toContain( + expect(line).toContain( "-ModulesDir C:\\Users\\Public\\codev-office\\node_modules", ); - expect(wrapper).not.toContain("-SkillsRoot"); + // Space-containing paths are quoted so copy-paste survives them. + expect(line).toContain( + '-SkillsRoot "C:\\Users\\Van Phong\\.config\\codev\\skills"', + ); }); test("baked args pin the real user's skills root on a Windows host", () => { @@ -460,16 +460,6 @@ describe("runSkillOffice", () => { expect(onWindows[3]).toContain(".config"); }); - test("wrapper quotes arguments containing spaces", () => { - const content = officeWrapperContent("s.ps1", [ - "-SkillsRoot", - "C:\\Users\\Van Phong\\.config\\codev\\skills", - ]); - expect(content).toContain( - '-SkillsRoot "C:\\Users\\Van Phong\\.config\\codev\\skills"', - ); - }); - test("ensureStagingDir falls back when the preferred dir is unwritable", () => { const locked = join(tempDir, "locked"); mkdirSync(locked, { recursive: true }); @@ -506,20 +496,6 @@ describe("runSkillOffice", () => { migrateLegacyOfficeDir(join(tempDir, "nope"), to); }); - test("wrapper name and content cover the uninstall flow", () => { - expect(officeWrapperName(false)).toBe("Install-CoDev-Office.cmd"); - expect(officeWrapperName(true)).toBe("Uninstall-CoDev-Office.cmd"); - const content = officeWrapperContent("codev-office-windows-uninstall.ps1", [ - "-Yes", - "-SkillsOnly", - ]); - expect(content).toContain( - 'powershell -ExecutionPolicy Bypass -File ".\\codev-office-windows-uninstall.ps1" -Yes -SkillsOnly', - ); - // CRLF line endings - the file must open cleanly in cmd.exe. - expect(content).toContain("\r\n"); - }); - test("always refetches the setup script, but reuses a finished bundle", async () => { const dir = join(tempDir, "office"); mkdirSync(dir, { recursive: true }); @@ -555,11 +531,7 @@ describe("runSkillOffice", () => { // The PowerShell branch is unreachable from a non-Windows host — a // cross-platform --platform forces download-only — so the host is stubbed to // keep the argv shape pinned on the Linux/macOS machines that run this suite. - test("a Windows host stages the right-click wrapper instead of spawning", async () => { - // Endpoint protection (KES in the field) silently kills a powershell - // child of node.exe mid-install, so the installer must never be - // spawned from codevhub on Windows — the user launches the staged - // .cmd via right-click -> Run as administrator instead. + test("a Windows host never spawns the installer", async () => { objects.set("/codev-office-windows.zip", BUNDLE); objects.set("/codev-office-windows-setup.ps1", SCRIPT); const dir = join(tempDir, "office"); @@ -576,10 +548,7 @@ describe("runSkillOffice", () => { ); expect(code).toBe(0); expect(spawned).toBeNull(); - const wrapper = readFileSync(join(dir, "Install-CoDev-Office.cmd"), "utf8"); - expect(wrapper).toContain( - 'powershell -ExecutionPolicy Bypass -File ".\\codev-office-windows-setup.ps1" -SkipVerify', - ); + expect(existsSync(join(dir, "Install-CoDev-Office.cmd"))).toBe(false); }); test("exits 1 on an OS with no bundle and downloads nothing", async () => { From 27796d885ea51975cb0d8598edbc06fc6d6cee8e Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Wed, 5 Aug 2026 19:24:10 +0700 Subject: [PATCH 2/7] Standardize on Start-Process powershell -Verb RunAs for elevation Co-Authored-By: Claude Fable 5 --- src/lib/office.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/office.ts b/src/lib/office.ts index f73c1e6..7f9d9e2 100644 --- a/src/lib/office.ts +++ b/src/lib/office.ts @@ -454,9 +454,8 @@ export async function runSkillOffice( "(e.g. Kaspersky) is known to kill installers it launches. Run it " + "yourself in an ELEVATED PowerShell:", ); - console.error( - ' 1. Press the Windows key, type "powershell", then press Ctrl+Shift+Enter', - ); + console.error(" 1. In this (or any) PowerShell window, run:"); + console.error(" Start-Process powershell -Verb RunAs"); console.error( " (in the UAC dialog: More choices -> Use a different account -> enter the ADMIN username and password)", ); From 47545136595ea5b99010d2aecbec3266ae8ed847 Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Wed, 5 Aug 2026 19:42:58 +0700 Subject: [PATCH 3/7] Remove every .cmd/wrapper reference from the office flow officeWrapperBakedArgs -> officeBakedPathArgs, comments reworded to describe the launch constraint without the retired staged-launcher history, and the .cmd-absence test assertions dropped. Co-Authored-By: Claude Fable 5 --- src/lib/office.ts | 30 ++++++++++++++---------------- tests/lib/download.test.ts | 18 ++++++++---------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/lib/office.ts b/src/lib/office.ts index 7f9d9e2..74ef052 100644 --- a/src/lib/office.ts +++ b/src/lib/office.ts @@ -205,11 +205,10 @@ function manualRunCommand(platform: OfficePlatform, script: string): string { } // The exact command the user types in an ELEVATED PowerShell — after field -// testing every alternative (spawning from codevhub, a self-elevating .cmd), -// this is the only launch mode the endpoint protection tolerates: it kills -// installers codevhub launches and quarantines the .ps1 a .cmd launches, -// while the same command typed interactively runs to "Verification passed". -// Exported for tests. +// testing every alternative launcher, this is the only launch mode the +// endpoint protection tolerates: anything codevhub starts (directly or via a +// staged launcher) gets killed or quarantined, while the same command typed +// interactively runs to "Verification passed". Exported for tests. export function officeManualWindowsCommand( script: string, args: string[], @@ -226,7 +225,7 @@ export function officeManualWindowsCommand( // Cross-platform staging (--platform windows from another OS) cannot know // the target machine's user, so only the modules dir is baked there. // Exported for tests. -export function officeWrapperBakedArgs(hostIsWindows: boolean): string[] { +export function officeBakedPathArgs(hostIsWindows: boolean): string[] { const publicDir = process.env.PUBLIC ?? "C:\\Users\\Public"; const args = ["-ModulesDir", `${publicDir}\\codev-office\\node_modules`]; if (hostIsWindows) { @@ -434,19 +433,18 @@ export async function runSkillOffice( ? uninstallerArgs(parsed, platform) : installerArgs(parsed, platform); - // Windows: never run (or wrap) the installer from codevhub. Endpoint - // protection (Kaspersky Endpoint Security in the field) kills a powershell - // child of node.exe mid-install AND quarantines the .ps1 when a staged - // .cmd launches it — the only launch it tolerates is the user typing the - // command in an elevated PowerShell themselves, which is field-proven to - // run to "Verification passed". Print that exact command, with the - // profile-safe paths baked in so an elevation under a different admin - // account still installs to the real user's profile. + // Windows: never launch the installer from codevhub. Endpoint protection + // (Kaspersky Endpoint Security in the field) kills or quarantines any + // launch codevhub initiates — the only mode it tolerates is the user + // typing the command in an elevated PowerShell themselves, which is + // field-proven to run to "Verification passed". Print that exact command, + // with the profile-safe paths baked in so an elevation under a different + // admin account still installs to the real user's profile. if (platform === "windows") { const verb = parsed.uninstall ? "uninstaller" : "installer"; const commandLine = officeManualWindowsCommand(script, [ ...scriptArgs, - ...officeWrapperBakedArgs(hostPlatform === "windows"), + ...officeBakedPathArgs(hostPlatform === "windows"), ]); console.error(`\nFiles are in ${dir}.`); console.error( @@ -482,7 +480,7 @@ export async function runSkillOffice( console.error( `\nRunning the ${parsed.uninstall ? "uninstaller" : "installer"} (${script})...\n`, ); - // Windows returned above with the right-click wrapper — only the bash + // Windows returned above with the printed manual command — only the bash // platforms reach the spawn. const command = "bash"; const args = [join(dir, script), ...scriptArgs]; diff --git a/tests/lib/download.test.ts b/tests/lib/download.test.ts index 8a0eae0..3cd6c96 100644 --- a/tests/lib/download.test.ts +++ b/tests/lib/download.test.ts @@ -18,8 +18,8 @@ import { ensureStagingDir, installerArgs, migrateLegacyOfficeDir, + officeBakedPathArgs, officeManualWindowsCommand, - officeWrapperBakedArgs, runSkillOffice, uninstallerArgs, } from "@/lib/office.js"; @@ -398,10 +398,10 @@ describe("runSkillOffice", () => { expect(existsSync(join(dir, "codev-office-windows.zip"))).toBe(true); }); - test("windows staging stages files only - no wrapper, no spawn", async () => { - // Endpoint protection kills installers codevhub launches AND quarantines - // the .ps1 a staged .cmd launches - the windows flow must only download - // and print the manual elevated-PowerShell command. + test("windows staging stages files only and never spawns", async () => { + // Endpoint protection kills or quarantines anything codevhub launches - + // the windows flow must only download and print the manual + // elevated-PowerShell command. objects.set("/codev-office-windows.zip", BUNDLE); objects.set("/codev-office-windows-setup.ps1", SCRIPT); const dir = join(tempDir, "office"); @@ -424,13 +424,12 @@ describe("runSkillOffice", () => { expect(code).toBe(0); expect(spawns).toEqual([]); expect(existsSync(join(dir, "codev-office-windows.zip"))).toBe(true); - expect(existsSync(join(dir, "Install-CoDev-Office.cmd"))).toBe(false); }); test("the manual command carries flags and baked paths, quoted", () => { const line = officeManualWindowsCommand("codev-office-windows-setup.ps1", [ "-SkipVerify", - ...officeWrapperBakedArgs(false), + ...officeBakedPathArgs(false), "-SkillsRoot", "C:\\Users\\Van Phong\\.config\\codev\\skills", ]); @@ -447,11 +446,11 @@ describe("runSkillOffice", () => { }); test("baked args pin the real user's skills root on a Windows host", () => { - expect(officeWrapperBakedArgs(false)).toEqual([ + expect(officeBakedPathArgs(false)).toEqual([ "-ModulesDir", "C:\\Users\\Public\\codev-office\\node_modules", ]); - const onWindows = officeWrapperBakedArgs(true); + const onWindows = officeBakedPathArgs(true); expect(onWindows.slice(0, 2)).toEqual([ "-ModulesDir", "C:\\Users\\Public\\codev-office\\node_modules", @@ -548,7 +547,6 @@ describe("runSkillOffice", () => { ); expect(code).toBe(0); expect(spawned).toBeNull(); - expect(existsSync(join(dir, "Install-CoDev-Office.cmd"))).toBe(false); }); test("exits 1 on an OS with no bundle and downloads nothing", async () => { From 4af563c48715b74549c487fa528f1d7609f75d7e Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Wed, 5 Aug 2026 19:57:56 +0700 Subject: [PATCH 4/7] Stop baking -ModulesDir - the setup script's default is now the shared path Companion to codev-scripts#23: %PUBLIC%\codev-office\node_modules is the script's own default, so the printed command carries only -SkillsRoot (the one per-machine value). Cross-platform staging now bakes nothing. Co-Authored-By: Claude Fable 5 --- src/lib/office.ts | 22 +++++++++------------- tests/lib/download.test.ts | 19 +++++-------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/lib/office.ts b/src/lib/office.ts index 74ef052..689cd0b 100644 --- a/src/lib/office.ts +++ b/src/lib/office.ts @@ -217,21 +217,17 @@ export function officeManualWindowsCommand( return `powershell -ExecutionPolicy Bypass -File .\\${script}${argStr}`; } -// Paths baked into the printed command so a UAC elevation with a DIFFERENT -// admin account cannot strand the install on the admin's profile: the JS -// module tree goes to the shared, account-independent %PUBLIC% dir, and the -// skills root is pinned to the REAL user's profile — codevhub runs -// unelevated as that user, so homedir() is authoritative here. -// Cross-platform staging (--platform windows from another OS) cannot know -// the target machine's user, so only the modules dir is baked there. +// The one path baked into the printed command: the skills root, pinned to +// the REAL user's profile so a UAC elevation with a DIFFERENT admin account +// cannot strand the skills on the admin's profile — codevhub runs unelevated +// as that user, so homedir() is authoritative here. (The JS module tree +// needs no baking: the setup script's default is already the shared +// %PUBLIC% dir.) Cross-platform staging (--platform windows from another OS) +// cannot know the target machine's user, so nothing is baked there. // Exported for tests. export function officeBakedPathArgs(hostIsWindows: boolean): string[] { - const publicDir = process.env.PUBLIC ?? "C:\\Users\\Public"; - const args = ["-ModulesDir", `${publicDir}\\codev-office\\node_modules`]; - if (hostIsWindows) { - args.push("-SkillsRoot", `${homedir()}\\.config\\codev\\skills`); - } - return args; + if (!hostIsWindows) return []; + return ["-SkillsRoot", `${homedir()}\\.config\\codev\\skills`]; } // One-time migration: move files staged under the old per-user dot-folder diff --git a/tests/lib/download.test.ts b/tests/lib/download.test.ts index 3cd6c96..0e3ec4d 100644 --- a/tests/lib/download.test.ts +++ b/tests/lib/download.test.ts @@ -429,16 +429,12 @@ describe("runSkillOffice", () => { test("the manual command carries flags and baked paths, quoted", () => { const line = officeManualWindowsCommand("codev-office-windows-setup.ps1", [ "-SkipVerify", - ...officeBakedPathArgs(false), "-SkillsRoot", "C:\\Users\\Van Phong\\.config\\codev\\skills", ]); expect(line).toContain( "powershell -ExecutionPolicy Bypass -File .\\codev-office-windows-setup.ps1 -SkipVerify", ); - expect(line).toContain( - "-ModulesDir C:\\Users\\Public\\codev-office\\node_modules", - ); // Space-containing paths are quoted so copy-paste survives them. expect(line).toContain( '-SkillsRoot "C:\\Users\\Van Phong\\.config\\codev\\skills"', @@ -446,17 +442,12 @@ describe("runSkillOffice", () => { }); test("baked args pin the real user's skills root on a Windows host", () => { - expect(officeBakedPathArgs(false)).toEqual([ - "-ModulesDir", - "C:\\Users\\Public\\codev-office\\node_modules", - ]); + // The modules dir is the setup script's own default now — nothing to + // bake; cross-platform staging bakes nothing at all. + expect(officeBakedPathArgs(false)).toEqual([]); const onWindows = officeBakedPathArgs(true); - expect(onWindows.slice(0, 2)).toEqual([ - "-ModulesDir", - "C:\\Users\\Public\\codev-office\\node_modules", - ]); - expect(onWindows[2]).toBe("-SkillsRoot"); - expect(onWindows[3]).toContain(".config"); + expect(onWindows[0]).toBe("-SkillsRoot"); + expect(onWindows[1]).toContain(".config"); }); test("ensureStagingDir falls back when the preferred dir is unwritable", () => { From 00ff2ba9297d8c280388722f52b5bb9d7df4a6fe Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Wed, 5 Aug 2026 20:23:38 +0700 Subject: [PATCH 5/7] Print the bare install command - the script now self-detects the user Companion to codev-scripts#23: the setup script resolves the signed-in user's skills root itself when elevated as a different account, and the modules dir default is the shared %PUBLIC% path. Nothing per-machine remains to bake, so the printed command is the same bare line the docs show - one command everywhere. Co-Authored-By: Claude Fable 5 --- src/lib/office.ts | 19 +------------------ tests/lib/download.test.ts | 10 ---------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/src/lib/office.ts b/src/lib/office.ts index 689cd0b..8582cd2 100644 --- a/src/lib/office.ts +++ b/src/lib/office.ts @@ -7,7 +7,6 @@ import { renameSync, rmSync, } from "node:fs"; -import { homedir } from "node:os"; import { join } from "node:path"; import { OFFICE_DOWNLOADS_URL } from "@/lib/const.js"; import { downloadFile } from "@/lib/download.js"; @@ -217,19 +216,6 @@ export function officeManualWindowsCommand( return `powershell -ExecutionPolicy Bypass -File .\\${script}${argStr}`; } -// The one path baked into the printed command: the skills root, pinned to -// the REAL user's profile so a UAC elevation with a DIFFERENT admin account -// cannot strand the skills on the admin's profile — codevhub runs unelevated -// as that user, so homedir() is authoritative here. (The JS module tree -// needs no baking: the setup script's default is already the shared -// %PUBLIC% dir.) Cross-platform staging (--platform windows from another OS) -// cannot know the target machine's user, so nothing is baked there. -// Exported for tests. -export function officeBakedPathArgs(hostIsWindows: boolean): string[] { - if (!hostIsWindows) return []; - return ["-SkillsRoot", `${homedir()}\\.config\\codev\\skills`]; -} - // One-time migration: move files staged under the old per-user dot-folder // into the new Public dir, so multi-GB bundles are not re-downloaded just // because the staging folder moved. Same-volume renames; anything locked or @@ -438,10 +424,7 @@ export async function runSkillOffice( // admin account still installs to the real user's profile. if (platform === "windows") { const verb = parsed.uninstall ? "uninstaller" : "installer"; - const commandLine = officeManualWindowsCommand(script, [ - ...scriptArgs, - ...officeBakedPathArgs(hostPlatform === "windows"), - ]); + const commandLine = officeManualWindowsCommand(script, scriptArgs); console.error(`\nFiles are in ${dir}.`); console.error( `codevhub does not auto-run the Windows ${verb}: endpoint protection ` + diff --git a/tests/lib/download.test.ts b/tests/lib/download.test.ts index 0e3ec4d..a3539b0 100644 --- a/tests/lib/download.test.ts +++ b/tests/lib/download.test.ts @@ -18,7 +18,6 @@ import { ensureStagingDir, installerArgs, migrateLegacyOfficeDir, - officeBakedPathArgs, officeManualWindowsCommand, runSkillOffice, uninstallerArgs, @@ -441,15 +440,6 @@ describe("runSkillOffice", () => { ); }); - test("baked args pin the real user's skills root on a Windows host", () => { - // The modules dir is the setup script's own default now — nothing to - // bake; cross-platform staging bakes nothing at all. - expect(officeBakedPathArgs(false)).toEqual([]); - const onWindows = officeBakedPathArgs(true); - expect(onWindows[0]).toBe("-SkillsRoot"); - expect(onWindows[1]).toContain(".config"); - }); - test("ensureStagingDir falls back when the preferred dir is unwritable", () => { const locked = join(tempDir, "locked"); mkdirSync(locked, { recursive: true }); From bd63b48a3ed7d4e6b44d60681008ee4aa3951e1f Mon Sep 17 00:00:00 2001 From: minhn4 Date: Wed, 5 Aug 2026 21:47:38 +0700 Subject: [PATCH 6/7] Drop the stale baked-paths wording from the office comment and test Nothing is baked into the printed command any more - the setup script itself defaults to the shared %PUBLIC% dirs and detects the console user. Co-Authored-By: Claude Fable 5 --- src/lib/office.ts | 5 +++-- tests/lib/download.test.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/office.ts b/src/lib/office.ts index 8582cd2..45a34b6 100644 --- a/src/lib/office.ts +++ b/src/lib/office.ts @@ -420,8 +420,9 @@ export async function runSkillOffice( // launch codevhub initiates — the only mode it tolerates is the user // typing the command in an elevated PowerShell themselves, which is // field-proven to run to "Verification passed". Print that exact command, - // with the profile-safe paths baked in so an elevation under a different - // admin account still installs to the real user's profile. + // bare: the setup script itself defaults to profile-safe paths (shared + // %PUBLIC% dirs, console-user detection), so an elevation under a + // different admin account still installs to the real user's profile. if (platform === "windows") { const verb = parsed.uninstall ? "uninstaller" : "installer"; const commandLine = officeManualWindowsCommand(script, scriptArgs); diff --git a/tests/lib/download.test.ts b/tests/lib/download.test.ts index a3539b0..5119f84 100644 --- a/tests/lib/download.test.ts +++ b/tests/lib/download.test.ts @@ -425,7 +425,7 @@ describe("runSkillOffice", () => { expect(existsSync(join(dir, "codev-office-windows.zip"))).toBe(true); }); - test("the manual command carries flags and baked paths, quoted", () => { + test("the manual command carries flags, quoting space-containing values", () => { const line = officeManualWindowsCommand("codev-office-windows-setup.ps1", [ "-SkipVerify", "-SkillsRoot", From 39264e8dd049fde8b78f2be6ed2b417747307dce Mon Sep 17 00:00:00 2001 From: minhn4 Date: Wed, 5 Aug 2026 22:17:12 +0700 Subject: [PATCH 7/7] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5772110..5960bc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codev-ai", - "version": "0.5.11", + "version": "0.5.12", "description": "CoDev — AI Coding Agent Hub. Install, configure, and manage multiple AI coding agents.", "keywords": [ "ai",