impel-cli 0.20.55 → 0.20.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { spawnSync } from "node:child_process";
5
5
  import {
6
6
  environmentValue,
7
7
  findNativeBinary,
8
+ nativeSpawnInvocation,
8
9
  resolveNativeBinary,
9
10
  } from "./nativeProcess.js";
10
11
  import { PINNED_VENDOR_CLI_VERSIONS } from "./vendorCliVersions.js";
@@ -99,9 +100,100 @@ export function verifyReviewedMacVendorCli(
99
100
  }
100
101
  }
101
102
 
103
+ /** Run `<binary> --version` through the platform-safe spawn and parse it. */
104
+ export function windowsVendorCliVersion(binary, environment = process.env, { run = spawnSync } = {}) {
105
+ try {
106
+ const invocation = nativeSpawnInvocation(binary, ["--version"], environment, "win32");
107
+ const result = run(invocation.command, invocation.args, {
108
+ encoding: "utf8",
109
+ env: environment,
110
+ stdio: ["ignore", "pipe", "pipe"],
111
+ timeout: 15_000,
112
+ windowsHide: true,
113
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
114
+ });
115
+ if (result?.status !== 0 || result?.error) return null;
116
+ return parseVendorCliVersion(`${result.stdout || ""}\n${result.stderr || ""}`);
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
102
122
  /**
103
- * Locate the reviewed macOS vendor CLI. On other platforms this intentionally
104
- * preserves the existing native resolution behavior.
123
+ * Accept only the exact reviewed release on Windows. Windows ships no
124
+ * in-box codesign equivalent for these unsigned-shim installs, so the gate
125
+ * is the exact version contract; explicit binary overrides remain
126
+ * authoritative for development and recovery, as on macOS.
127
+ */
128
+ export function verifyReviewedWindowsVendorCli(
129
+ tool,
130
+ binary,
131
+ environment = process.env,
132
+ {
133
+ run = spawnSync,
134
+ versions = PINNED_VENDOR_CLI_VERSIONS,
135
+ environmentPrefix = "IMPEL",
136
+ } = {},
137
+ ) {
138
+ if (!versions[tool]) return false;
139
+ try {
140
+ const override = vendorOverrideName(tool, environmentPrefix);
141
+ const overriddenBinary = override ? environmentValue(environment, override) : null;
142
+ if (overriddenBinary && path.resolve(overriddenBinary) === path.resolve(binary)) return true;
143
+ return windowsVendorCliVersion(binary, environment, { run }) === versions[tool];
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ function reviewedWindowsClaudeFallback(environment, environmentPrefix, accept, { find, realpath, stat }) {
150
+ // The native Windows installer keeps releases side by side under
151
+ // .local/share/claude/versions/<release> and its auto-updater replaces the
152
+ // .local/bin launcher in place. When the launcher drifts past the reviewed
153
+ // release, resolve the exact reviewed release directly instead of failing
154
+ // the launch; every candidate still passes the version verification.
155
+ const candidates = [];
156
+ const drifted = find("claude", environment, "win32", environmentPrefix);
157
+ if (drifted) {
158
+ try {
159
+ candidates.push(path.win32.join(
160
+ path.win32.dirname(realpath(drifted)),
161
+ "..", "share", "claude", "versions", PINNED_VENDOR_CLI_VERSIONS.claude,
162
+ ));
163
+ } catch {
164
+ // An unresolvable launcher only forfeits the derived candidate.
165
+ }
166
+ }
167
+ // USERPROFILE gates the fixed-root candidates: it is only set on real
168
+ // Windows sessions, so cross-platform tests injecting fake finders never
169
+ // fall through to a genuine macOS side-by-side install. HOME is probed too
170
+ // because the vendor updater follows HOME when it diverges from USERPROFILE.
171
+ const userProfile = environmentValue(environment, "USERPROFILE");
172
+ if (userProfile) {
173
+ const homes = [userProfile];
174
+ const home = environmentValue(environment, "HOME");
175
+ if (home && home !== userProfile) homes.push(home);
176
+ for (const root of homes) {
177
+ candidates.push(path.win32.join(root, ".local", "share", "claude", "versions", PINNED_VENDOR_CLI_VERSIONS.claude));
178
+ }
179
+ }
180
+ for (const base of candidates) {
181
+ // The release file is stored bare on every platform, but probe the .exe
182
+ // sibling too in case the installer ever normalizes Windows names.
183
+ for (const candidate of [base, `${base}.exe`]) {
184
+ try {
185
+ if (stat(candidate).isFile() && accept(candidate)) return candidate;
186
+ } catch {
187
+ // A missing side-by-side release keeps resolution fail-closed.
188
+ }
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+
194
+ /**
195
+ * Locate the reviewed vendor CLI on macOS and Windows. Other platforms
196
+ * intentionally preserve the existing native resolution behavior.
105
197
  */
106
198
  export function findReviewedVendorCliBinary(
107
199
  tool,
@@ -110,13 +202,17 @@ export function findReviewedVendorCliBinary(
110
202
  environmentPrefix = "IMPEL",
111
203
  {
112
204
  find = findNativeBinary,
113
- verify = verifyReviewedMacVendorCli,
205
+ verify = null,
114
206
  realpath = fs.realpathSync,
115
207
  stat = fs.statSync,
116
208
  } = {},
117
209
  ) {
118
- if (platform !== "darwin") return find(tool, environment, platform, environmentPrefix);
119
- const accept = (binary) => verify(tool, binary, environment, { environmentPrefix });
210
+ if (platform !== "darwin" && platform !== "win32") {
211
+ return find(tool, environment, platform, environmentPrefix);
212
+ }
213
+ const verifier = verify
214
+ || (platform === "win32" ? verifyReviewedWindowsVendorCli : verifyReviewedMacVendorCli);
215
+ const accept = (binary) => verifier(tool, binary, environment, { environmentPrefix });
120
216
  const reviewed = find(tool, environment, platform, environmentPrefix, accept);
121
217
  if (reviewed || tool !== "claude") return reviewed;
122
218
  // An explicit override remains authoritative even while unusable: callers
@@ -124,6 +220,10 @@ export function findReviewedVendorCliBinary(
124
220
  const override = vendorOverrideName(tool, environmentPrefix);
125
221
  if (override && environmentValue(environment, override)) return null;
126
222
 
223
+ if (platform === "win32") {
224
+ return reviewedWindowsClaudeFallback(environment, environmentPrefix, accept, { find, realpath, stat });
225
+ }
226
+
127
227
  // The standalone Claude installer keeps releases side by side under
128
228
  // versions/<release> and its auto-updater only moves the launcher symlink.
129
229
  // When the symlink drifts past the reviewed release, resolve the exact
@@ -153,9 +253,9 @@ export function findReviewedVendorCliBinary(
153
253
  }
154
254
 
155
255
  /**
156
- * Resolve a launch target. macOS deliberately returns null when only an
157
- * unreviewed or version-drifted binary exists so callers can request repair
158
- * instead of silently launching it.
256
+ * Resolve a launch target. macOS and Windows deliberately return null when
257
+ * only an unreviewed or version-drifted binary exists so callers can request
258
+ * repair instead of silently launching it.
159
259
  */
160
260
  export function resolveReviewedVendorCliBinary(
161
261
  tool,
@@ -163,7 +263,7 @@ export function resolveReviewedVendorCliBinary(
163
263
  platform = process.platform,
164
264
  environmentPrefix = "IMPEL",
165
265
  ) {
166
- if (platform !== "darwin") {
266
+ if (platform !== "darwin" && platform !== "win32") {
167
267
  return resolveNativeBinary(tool, environment, platform, environmentPrefix);
168
268
  }
169
269
  return findReviewedVendorCliBinary(tool, environment, platform, environmentPrefix);
package/src/windowsFs.js CHANGED
@@ -42,3 +42,26 @@ export function renameWithWindowsRetry(from, to, {
42
42
  }
43
43
  }
44
44
  }
45
+
46
+ // chmod on Windows only toggles the FAT-era read-only attribute — it cannot
47
+ // deliver the owner-only guarantee the POSIX callers rely on, and it fails
48
+ // EPERM whenever the file is momentarily held by antivirus or carries an
49
+ // inherited deny ACL (observed in the field: `setup.shared_vendor_clis`
50
+ // aborting a whole tenant convergence over chmod of a managed models.json).
51
+ // On POSIX the mode bits are a real security property, so failures there
52
+ // still throw unless the caller explicitly opts into best-effort.
53
+
54
+ /** chmod that swallows Windows failures; POSIX failures throw unless bestEffort. */
55
+ export function chmodWithPlatformPolicy(target, mode, {
56
+ platform = process.platform,
57
+ chmod = fs.chmodSync,
58
+ bestEffort = false,
59
+ } = {}) {
60
+ try {
61
+ chmod(target, mode);
62
+ return true;
63
+ } catch (error) {
64
+ if (platform === "win32" || bestEffort) return false;
65
+ throw error;
66
+ }
67
+ }
@@ -5,9 +5,13 @@ import { ensureImpelClaudeProfile, ensureImpelCodexProfile } from "./cliProfiles
5
5
  import {
6
6
  findNativeBinary,
7
7
  nativeCommandInvocation,
8
- nativeSpawnInvocation,
9
8
  } from "./nativeProcess.js";
10
9
  import { syncSkillsSafe } from "./skills.js";
10
+ import {
11
+ findReviewedVendorCliBinary,
12
+ verifyReviewedWindowsVendorCli,
13
+ windowsVendorCliVersion,
14
+ } from "./vendorCliBinaries.js";
11
15
  import { PINNED_VENDOR_CLI_VERSIONS } from "./vendorCliVersions.js";
12
16
  import { provisionWindowsGit } from "./windowsGit.js";
13
17
 
@@ -47,37 +51,49 @@ export function windowsCliInstallCommands(tools = Object.keys(WINDOWS_CLI_INSTAL
47
51
  }));
48
52
  }
49
53
 
50
- function verifyWindowsCli(_tool, binary, environment) {
51
- try {
52
- const invocation = nativeSpawnInvocation(binary, ["--version"], environment, "win32");
53
- const result = spawnSync(invocation.command, invocation.args, {
54
- encoding: "utf8",
55
- env: environment,
56
- stdio: ["ignore", "pipe", "pipe"],
57
- timeout: 15_000,
58
- windowsHide: true,
59
- windowsVerbatimArguments: invocation.windowsVerbatimArguments,
60
- });
61
- return result?.status === 0 && !result?.error;
62
- } catch {
63
- return false;
64
- }
54
+ function verifyWindowsCli(tool, binary, environment) {
55
+ // The exact reviewed release, matching the macOS contract: a runnable but
56
+ // version-drifted vendor CLI must count as missing so setup/update repair
57
+ // it, instead of launching an unreviewed build against the gateway
58
+ // (observed in the field: Windows Claude auto-updated past the pin and the
59
+ // gateway rejected the session while macOS stayed on the reviewed release).
60
+ return verifyReviewedWindowsVendorCli(tool, binary, environment);
65
61
  }
66
62
 
67
63
  function detectWindowsClis(find, environment, verify) {
68
- const detect = (tool) => find(
69
- tool,
70
- environment,
71
- "win32",
72
- "IMPEL",
73
- (binary) => verify(tool, binary, environment),
74
- );
64
+ // Detection must share the launcher's reviewed resolution (including the
65
+ // side-by-side fallback after vendor auto-update drift); a raw PATH probe
66
+ // here would classify a drifted-but-launchable install as missing and
67
+ // reinstall it on every setup/update pass.
68
+ const detect = (tool) => findReviewedVendorCliBinary(tool, environment, "win32", "IMPEL", {
69
+ find,
70
+ verify: (candidateTool, binary, candidateEnvironment) =>
71
+ verify(candidateTool, binary, candidateEnvironment),
72
+ });
75
73
  return {
76
74
  claude: detect("claude"),
77
75
  codex: detect("codex"),
78
76
  };
79
77
  }
80
78
 
79
+ /**
80
+ * Pin-agnostic probe for the drift diagnostic: which build is actually on
81
+ * this machine when the reviewed detection came back empty.
82
+ */
83
+ function detectDriftedWindowsClis(find, environment, binaries, { versionOf = windowsVendorCliVersion } = {}) {
84
+ const drifted = {};
85
+ for (const tool of ["claude", "codex"]) {
86
+ if (binaries[tool]) continue;
87
+ const binary = find(tool, environment, "win32", "IMPEL");
88
+ if (!binary) continue;
89
+ const version = versionOf(binary, environment);
90
+ if (version && version !== PINNED_VENDOR_CLI_VERSIONS[tool]) {
91
+ drifted[tool] = { binary, version, pinned: PINNED_VENDOR_CLI_VERSIONS[tool] };
92
+ }
93
+ }
94
+ return drifted;
95
+ }
96
+
81
97
  function installCli(tool, { environment, run }) {
82
98
  const installer = WINDOWS_CLI_INSTALLERS[tool];
83
99
  if (!installer) throw new Error(`unsupported Windows CLI installer: ${tool}`);
@@ -127,11 +143,13 @@ export async function prepareWindowsClis({
127
143
  skipInstall = false,
128
144
  inspectOnly = false,
129
145
  installTools = ["claude", "codex"],
146
+ crossAppModels = false,
130
147
  } = {}, dependencies = {}) {
131
148
  const io = {
132
149
  environment: process.env,
133
150
  find: findNativeBinary,
134
151
  verify: verifyWindowsCli,
152
+ versionOf: windowsVendorCliVersion,
135
153
  run: spawnSync,
136
154
  ensureClaudeProfile: ensureImpelClaudeProfile,
137
155
  ensureCodexProfile: ensureImpelCodexProfile,
@@ -164,6 +182,7 @@ export async function prepareWindowsClis({
164
182
  binaries: before,
165
183
  missingBefore,
166
184
  missingAfter: [...missingBefore],
185
+ drifted: detectDriftedWindowsClis(io.find, io.environment, before, { versionOf: io.versionOf }),
167
186
  installAttempted: false,
168
187
  installSucceeded: null,
169
188
  installFailure: null,
@@ -219,8 +238,9 @@ export async function prepareWindowsClis({
219
238
  const binaries = installAttempted
220
239
  ? detectWindowsClis(io.find, io.environment, io.verify)
221
240
  : before;
222
- const claudeProfile = io.ensureClaudeProfile(gatewayUrl, tenantId);
223
- const codexProfile = io.ensureCodexProfile(gatewayUrl, tenantId);
241
+ const drifted = detectDriftedWindowsClis(io.find, io.environment, binaries, { versionOf: io.versionOf });
242
+ const claudeProfile = io.ensureClaudeProfile(gatewayUrl, tenantId, { crossAppModels });
243
+ const codexProfile = io.ensureCodexProfile(gatewayUrl, tenantId, { crossAppModels });
224
244
 
225
245
  if (binaries.claude) {
226
246
  await io.syncSkills({
@@ -248,6 +268,7 @@ export async function prepareWindowsClis({
248
268
  binaries,
249
269
  missingBefore,
250
270
  missingAfter: Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool),
271
+ drifted,
251
272
  installAttempted,
252
273
  installSucceeded,
253
274
  installFailure,