impel-cli 0.18.2 → 0.18.3

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.
package/README.md CHANGED
@@ -255,6 +255,11 @@ compatibility for one release, but no command can enable native routing.
255
255
  ### Windows
256
256
 
257
257
  - Missing official Claude Code and Codex CLIs can be installed during setup.
258
+ - Claude Desktop is downloaded from Anthropic as the exact x64 or arm64 MSIX
259
+ pinned by this CLI. Its SHA-256, Authenticode signature, package identity,
260
+ version, architecture, and required layout are checked before installation.
261
+ - ChatGPT/Codex uses the Microsoft Store package: a missing package is installed
262
+ during either setup or update, while an existing package is upgraded.
258
263
  - Signed vendor desktop executables remain unchanged.
259
264
  - Tenant profiles and Start entries are isolated per tenant.
260
265
  - Shared package-manager/vendor preparation runs once per product.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -53,6 +53,22 @@ export const PINNED_VENDOR_APPS = Object.freeze({
53
53
  claudeCodeVersion: "2.1.209",
54
54
  claudeCodeCommit: "0fe048596fd45e79e99353cbe2c3d1b1ac069568",
55
55
  bundleName: "Claude.app",
56
+ windows: Object.freeze({
57
+ packageName: "Claude",
58
+ packageVersion: "1.20186.9.0",
59
+ publisher: 'CN="Anthropic, PBC", O="Anthropic, PBC", L=San Francisco, S=California, C=US, SERIALNUMBER=4860621, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US',
60
+ executable: "app\\Claude.exe",
61
+ downloads: Object.freeze({
62
+ arm64: Object.freeze({
63
+ url: "https://downloads.claude.ai/releases/win32/arm64/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.msix",
64
+ sha256: "9a0591a0d9e298dea92d5547ee259f35e818ce0b1982512ad1550f5ed2744c39",
65
+ }),
66
+ x64: Object.freeze({
67
+ url: "https://downloads.claude.ai/releases/win32/x64/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.msix",
68
+ sha256: "55a0b8f8c3c8d46cd64a03d3f42818da28f312cd4be904265cd61baee7ea68a5",
69
+ }),
70
+ }),
71
+ }),
56
72
  downloads: Object.freeze({
57
73
  arm64: Object.freeze({
58
74
  url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
@@ -402,7 +402,7 @@ export async function reconcileWindowsTenantApps({
402
402
  fail(target, `${label} vendor ${mode === "update" ? "update" : "installation"} requires confirmation`);
403
403
  continue;
404
404
  }
405
- const vendor = ensure({ update: mode === "update" }, { environment });
405
+ const vendor = await ensure({ update: mode === "update" }, { environment, homeDir });
406
406
  binary = vendor.binary;
407
407
  if (!binary) {
408
408
  fail(target, `${label} vendor installation failed (${windowsProcessFailure(vendor.result)})`);
@@ -741,7 +741,10 @@ export async function cmdWindowsApps(argv, overrides = {}) {
741
741
  || (action === "open" && !binary)
742
742
  );
743
743
  if (shouldEnsureVendor) {
744
- const vendor = ensure({ update: action === "update" }, { environment: io.environment });
744
+ const vendor = await ensure(
745
+ { update: action === "update" },
746
+ { environment: io.environment, homeDir: io.homeDir },
747
+ );
745
748
  binary = vendor.binary;
746
749
  console.log(`${target}: vendor app ${vendor.action}`);
747
750
  if (!binary) {
@@ -3,14 +3,17 @@ import crypto from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { Readable } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
6
8
 
9
+ import { PINNED_VENDOR_APPS } from "./apps.js";
7
10
  import { environmentValue, nativeCommandInvocation } from "./nativeProcess.js";
8
11
  import { normalizeTenantId } from "./tenants.js";
9
12
 
10
- export const WINDOWS_CLAUDE_PACKAGE = "Anthropic.Claude";
11
13
  export const WINDOWS_CHATGPT_PACKAGE = "9PLM9XGG6VKS";
12
14
  export const WINDOWS_CHATGPT_PACKAGE_NAME = "OpenAI.Codex";
13
15
  export const WINDOWS_CHATGPT_PUBLISHER_ID = "2p2nqsd0c76g0";
16
+ const WINDOWS_CLAUDE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
14
17
 
15
18
  export function windowsClaudeUserData(environment = process.env, tenantId = null) {
16
19
  const localAppData = environmentValue(environment, "LOCALAPPDATA")
@@ -45,24 +48,6 @@ function versionedSquirrelCandidates(root, readDirectory = fs.readdirSync) {
45
48
  }
46
49
  }
47
50
 
48
- function installedMsixClaude(environment, run = spawnSync) {
49
- const script = [
50
- "$package = Get-AppxPackage -Name Claude | Sort-Object Version -Descending | Select-Object -First 1",
51
- "if ($package) { Join-Path $package.InstallLocation 'app\\Claude.exe' }",
52
- ].join("; ");
53
- try {
54
- const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
55
- encoding: "utf8",
56
- env: environment,
57
- stdio: ["ignore", "pipe", "ignore"],
58
- windowsHide: true,
59
- });
60
- return result?.status === 0 ? String(result.stdout || "").trim() || null : null;
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
51
  function installedMsixChatGPT(environment, run = spawnSync) {
67
52
  const script = [
68
53
  `$package = Get-AppxPackage -Name '${WINDOWS_CHATGPT_PACKAGE_NAME}' -ErrorAction SilentlyContinue`,
@@ -108,11 +93,12 @@ export function windowsClaudeAppCandidates(environment = process.env, dependenci
108
93
  }
109
94
 
110
95
  export function findWindowsClaudeApp(environment = process.env, dependencies = {}) {
111
- const exists = dependencies.isFile || isFile;
112
- const unpackaged = windowsClaudeAppCandidates(environment, dependencies).find((candidate) => exists(candidate));
113
- if (unpackaged) return unpackaged;
114
- const msix = (dependencies.queryMsix || installedMsixClaude)(environment, dependencies.run);
115
- return msix && exists(msix) ? path.win32.normalize(msix) : null;
96
+ if (dependencies.queryMsix) {
97
+ const msix = dependencies.queryMsix(environment, dependencies.run);
98
+ const exists = dependencies.isFile || isFile;
99
+ return msix && exists(msix) ? path.win32.normalize(msix) : null;
100
+ }
101
+ return pinnedWindowsClaudeApp(environment, dependencies);
116
102
  }
117
103
 
118
104
  /** Candidate paths for an unpackaged OpenAI ChatGPT/Codex desktop install. */
@@ -166,37 +152,210 @@ function wingetInvocation(action, environment, { packageId, source = "winget", s
166
152
  );
167
153
  }
168
154
 
169
- /** Install/update Anthropic's signed per-user app without modifying its files. */
170
- export function ensureWindowsClaudeApp({ update = false } = {}, dependencies = {}) {
155
+ function windowsClaudeArchitecture(environment, fallback = process.arch) {
156
+ const detected = environmentValue(environment, "PROCESSOR_ARCHITEW6432")
157
+ || environmentValue(environment, "PROCESSOR_ARCHITECTURE")
158
+ || fallback;
159
+ if (/arm64/iu.test(detected)) return "arm64";
160
+ if (/amd64|x64/iu.test(detected)) return "x64";
161
+ return fallback;
162
+ }
163
+
164
+ function pinnedWindowsClaudeApp(environment = process.env, dependencies = {}) {
171
165
  const io = {
172
- environment: process.env,
173
- find: findWindowsClaudeApp,
174
166
  run: spawnSync,
167
+ isFile,
168
+ pin: PINNED_VENDOR_APPS.claude.windows,
169
+ architecture: windowsClaudeArchitecture(environment),
175
170
  ...dependencies,
176
171
  };
177
- const before = io.find(io.environment);
178
- if (before && !update) return { binary: before, action: "existing", result: null };
179
-
180
- const invocation = wingetInvocation(update ? "update" : "install", io.environment, {
181
- packageId: WINDOWS_CLAUDE_PACKAGE,
182
- });
183
- let result;
172
+ const expectedArchitecture = io.architecture === "arm64" ? "Arm64" : "X64";
173
+ const script = [
174
+ "$package = Get-AppxPackage -Name 'Claude' -ErrorAction SilentlyContinue",
175
+ "$package = $package | Where-Object { $_.Version.ToString() -ceq $env:IMPEL_CLAUDE_PACKAGE_VERSION -and $_.Publisher -ceq $env:IMPEL_CLAUDE_PACKAGE_PUBLISHER -and $_.Architecture.ToString() -ieq $env:IMPEL_CLAUDE_PACKAGE_ARCHITECTURE } | Select-Object -First 1",
176
+ "if ($package) { Join-Path $package.InstallLocation $env:IMPEL_CLAUDE_PACKAGE_EXECUTABLE }",
177
+ ].join("; ");
184
178
  try {
185
- result = io.run(invocation.command, invocation.args, {
186
- env: io.environment,
187
- stdio: "inherit",
188
- windowsVerbatimArguments: invocation.windowsVerbatimArguments,
179
+ const result = io.run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
180
+ encoding: "utf8",
181
+ env: {
182
+ ...environment,
183
+ IMPEL_CLAUDE_PACKAGE_VERSION: io.pin.packageVersion,
184
+ IMPEL_CLAUDE_PACKAGE_PUBLISHER: io.pin.publisher,
185
+ IMPEL_CLAUDE_PACKAGE_ARCHITECTURE: expectedArchitecture,
186
+ IMPEL_CLAUDE_PACKAGE_EXECUTABLE: io.pin.executable,
187
+ },
188
+ stdio: ["ignore", "pipe", "ignore"],
189
+ windowsHide: true,
189
190
  });
191
+ const binary = result?.status === 0 ? String(result.stdout || "").trim() : "";
192
+ return binary && io.isFile(binary) ? path.win32.normalize(binary) : null;
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ async function downloadWindowsClaudeMsix(url, destination, fetchImpl, timeoutMs) {
199
+ const controller = new AbortController();
200
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
201
+ try {
202
+ const response = await fetchImpl(url, { signal: controller.signal, redirect: "follow" });
203
+ if (!response?.ok || !response.body) {
204
+ throw new Error(`Claude MSIX download failed (HTTP ${response?.status ?? "error"})`);
205
+ }
206
+ await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(destination, { flags: "wx" }));
190
207
  } catch (error) {
191
- result = { status: null, error };
208
+ if (error?.name === "AbortError") {
209
+ throw new Error(`Claude MSIX download timed out after ${timeoutMs}ms`);
210
+ }
211
+ throw error;
212
+ } finally {
213
+ clearTimeout(timeout);
192
214
  }
193
- const binary = io.find(io.environment) || before;
194
- const succeeded = result?.status === 0 && !result?.error;
195
- return {
196
- binary,
197
- action: succeeded ? (update ? "updated" : "installed") : (before ? "existing" : "install-failed"),
198
- result,
215
+ }
216
+
217
+ const INSTALL_PINNED_CLAUDE_MSIX = String.raw`
218
+ $ErrorActionPreference = 'Stop'
219
+ $packagePath = $env:IMPEL_CLAUDE_MSIX_PATH
220
+ if (-not (Test-Path -LiteralPath $packagePath -PathType Leaf)) {
221
+ throw 'the verified Claude MSIX is missing'
222
+ }
223
+
224
+ $signature = Get-AuthenticodeSignature -LiteralPath $packagePath
225
+ if ($signature.Status.ToString() -cne 'Valid') {
226
+ throw "Claude MSIX signature is not valid ($($signature.Status))"
227
+ }
228
+
229
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
230
+ $archive = [System.IO.Compression.ZipFile]::OpenRead($packagePath)
231
+ try {
232
+ $manifestEntry = $archive.GetEntry('AppxManifest.xml')
233
+ if ($null -eq $manifestEntry) { throw 'Claude MSIX has no AppxManifest.xml' }
234
+ $reader = [System.IO.StreamReader]::new($manifestEntry.Open())
235
+ try { [xml]$manifest = $reader.ReadToEnd() } finally { $reader.Dispose() }
236
+ $identity = $manifest.Package.Identity
237
+ if ([string]$identity.Name -cne $env:IMPEL_CLAUDE_PACKAGE_NAME) {
238
+ throw "unexpected Claude MSIX package name: $($identity.Name)"
239
+ }
240
+ if ([string]$identity.Version -cne $env:IMPEL_CLAUDE_PACKAGE_VERSION) {
241
+ throw "unexpected Claude MSIX version: $($identity.Version)"
242
+ }
243
+ if ([string]$identity.Publisher -cne $env:IMPEL_CLAUDE_PACKAGE_PUBLISHER) {
244
+ throw 'unexpected Claude MSIX publisher'
245
+ }
246
+ if ([string]$identity.ProcessorArchitecture -cne $env:IMPEL_CLAUDE_MANIFEST_ARCHITECTURE) {
247
+ throw "unexpected Claude MSIX architecture: $($identity.ProcessorArchitecture)"
248
+ }
249
+ $application = $manifest.Package.Applications.Application | Where-Object Id -ceq 'Claude' | Select-Object -First 1
250
+ if ($null -eq $application -or [string]$application.Executable -cne $env:IMPEL_CLAUDE_PACKAGE_EXECUTABLE) {
251
+ throw 'Claude MSIX has an unexpected application entry point'
252
+ }
253
+ foreach ($entryName in @('app/claude.exe', 'app/resources/app.asar', 'AppxSignature.p7x')) {
254
+ if ($null -eq $archive.GetEntry($entryName)) {
255
+ throw "Claude MSIX is missing required entry $entryName"
256
+ }
257
+ }
258
+ } finally {
259
+ $archive.Dispose()
260
+ }
261
+
262
+ Add-AppxPackage -Path $packagePath -ForceApplicationShutdown -ForceUpdateFromAnyVersion
263
+ `;
264
+
265
+ function installPinnedWindowsClaudeMsix(packagePath, environment, pin, architecture, run = spawnSync) {
266
+ const result = run(
267
+ "powershell.exe",
268
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", INSTALL_PINNED_CLAUDE_MSIX],
269
+ {
270
+ encoding: "utf8",
271
+ env: {
272
+ ...environment,
273
+ IMPEL_CLAUDE_MSIX_PATH: packagePath,
274
+ IMPEL_CLAUDE_PACKAGE_NAME: pin.packageName,
275
+ IMPEL_CLAUDE_PACKAGE_VERSION: pin.packageVersion,
276
+ IMPEL_CLAUDE_PACKAGE_PUBLISHER: pin.publisher,
277
+ IMPEL_CLAUDE_PACKAGE_EXECUTABLE: pin.executable,
278
+ IMPEL_CLAUDE_MANIFEST_ARCHITECTURE: architecture,
279
+ },
280
+ stdio: ["ignore", "pipe", "pipe"],
281
+ windowsHide: true,
282
+ },
283
+ );
284
+ if (result?.status !== 0 && !result?.error) {
285
+ const detail = String(result?.stderr || "").trim().split(/\r?\n/u).find(Boolean);
286
+ if (detail) return { ...result, error: new Error(`Claude MSIX installation failed: ${detail}`) };
287
+ }
288
+ return result;
289
+ }
290
+
291
+ function windowsClaudeCacheRoot(homeDir, environment, version, architecture) {
292
+ const appsRoot = environmentValue(environment, "IMPEL_APP_HOME")
293
+ || path.join(homeDir, ".config", "impel", "apps");
294
+ return path.join(appsRoot, "vendor-cache", "claude", version, architecture);
295
+ }
296
+
297
+ /** Download, verify, and install Anthropic's exact signed per-user MSIX. */
298
+ export async function ensureWindowsClaudeApp(_options = {}, dependencies = {}) {
299
+ const io = {
300
+ environment: process.env,
301
+ architecture: null,
302
+ downloadTimeoutMs: WINDOWS_CLAUDE_DOWNLOAD_TIMEOUT_MS,
303
+ fetchImpl: fetch,
304
+ findPinned: pinnedWindowsClaudeApp,
305
+ homeDir: os.homedir(),
306
+ install: installPinnedWindowsClaudeMsix,
307
+ logger: console,
308
+ pin: PINNED_VENDOR_APPS.claude,
309
+ run: spawnSync,
310
+ ...dependencies,
199
311
  };
312
+ const architecture = io.architecture || windowsClaudeArchitecture(io.environment);
313
+ const windowsPin = io.pin?.windows;
314
+ const release = windowsPin?.downloads?.[architecture];
315
+ if (!windowsPin || !release) {
316
+ const error = new Error(`Claude: no verified Windows MSIX is available for ${architecture}`);
317
+ return { binary: null, action: "install-failed", result: { status: null, error } };
318
+ }
319
+ const findPinned = () => io.findPinned(io.environment, {
320
+ architecture,
321
+ pin: windowsPin,
322
+ run: io.run,
323
+ });
324
+ const existing = findPinned();
325
+ if (existing) return { binary: existing, action: "existing", result: null };
326
+
327
+ const cacheRoot = windowsClaudeCacheRoot(io.homeDir, io.environment, io.pin.version, architecture);
328
+ const archive = path.join(cacheRoot, `Claude-${io.pin.version}-${architecture}.msix`);
329
+ const temporary = `${archive}.download-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
330
+ try {
331
+ fs.mkdirSync(cacheRoot, { recursive: true, mode: 0o700 });
332
+ if (fs.existsSync(archive) && sha256File(archive) !== release.sha256) fs.rmSync(archive, { force: true });
333
+ if (!fs.existsSync(archive)) {
334
+ io.logger.log(`Claude: downloading verified vendor app ${io.pin.version} for Windows ${architecture}…`);
335
+ await downloadWindowsClaudeMsix(release.url, temporary, io.fetchImpl, io.downloadTimeoutMs);
336
+ if (sha256File(temporary) !== release.sha256) {
337
+ throw new Error(`Claude MSIX integrity check failed for ${io.pin.version} (${architecture})`);
338
+ }
339
+ fs.renameSync(temporary, archive);
340
+ }
341
+ let result = io.install(archive, io.environment, windowsPin, architecture, io.run);
342
+ const binary = result?.status === 0 && !result?.error ? findPinned() : null;
343
+ if (result?.status === 0 && !result?.error && !binary) {
344
+ result = {
345
+ ...result,
346
+ error: new Error(`Claude ${io.pin.version} did not appear after its verified MSIX was installed`),
347
+ };
348
+ }
349
+ return {
350
+ binary,
351
+ action: binary ? "installed" : "install-failed",
352
+ result,
353
+ };
354
+ } catch (error) {
355
+ return { binary: null, action: "install-failed", result: { status: null, error } };
356
+ } finally {
357
+ fs.rmSync(temporary, { force: true });
358
+ }
200
359
  }
201
360
 
202
361
  /** Install/update OpenAI's official ChatGPT/Codex Store package. */
@@ -210,7 +369,10 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
210
369
  const before = io.find(io.environment);
211
370
  if (before && !update) return { binary: before, action: "existing", result: null };
212
371
 
213
- const invocation = wingetInvocation(update ? "update" : "install", io.environment, {
372
+ // `impel update` also repairs missing surfaces. A missing Store package must
373
+ // be installed, not sent to `winget upgrade` (which returns 0x8A150014).
374
+ const shouldUpdate = Boolean(update && before);
375
+ const invocation = wingetInvocation(shouldUpdate ? "update" : "install", io.environment, {
214
376
  packageId: WINDOWS_CHATGPT_PACKAGE,
215
377
  source: "msstore",
216
378
  scope: null,
@@ -229,7 +391,7 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
229
391
  const succeeded = result?.status === 0 && !result?.error;
230
392
  return {
231
393
  binary,
232
- action: succeeded ? (update ? "updated" : "installed") : (before ? "existing" : "install-failed"),
394
+ action: succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
233
395
  result,
234
396
  };
235
397
  }