impel-cli 0.18.2 → 0.18.4
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 +13 -0
- package/package.json +1 -1
- package/src/apps.js +16 -0
- package/src/commands/apps.js +5 -2
- package/src/commands/converge.js +14 -2
- package/src/commands/setup.js +8 -1
- package/src/installRecovery/tools.js +16 -4
- package/src/prompt.js +24 -4
- package/src/windowsApps.js +239 -50
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.
|
|
@@ -324,6 +329,14 @@ run a real package manager, or modify native personal profiles.
|
|
|
324
329
|
CI runs on Ubuntu, macOS, and Windows with supported Node versions. See
|
|
325
330
|
[RELEASING.md](RELEASING.md) for the npm release process.
|
|
326
331
|
|
|
332
|
+
The separate Windows vendor contract runs only on a disposable GitHub-hosted
|
|
333
|
+
Windows VM. It installs the exact newer Claude fixture that preceded the
|
|
334
|
+
current compatibility pin, invokes the production downloader and installer to
|
|
335
|
+
downgrade to the exact pinned MSIX, verifies the registered AppX identity,
|
|
336
|
+
version, architecture, executable, and idempotent second run, then removes the
|
|
337
|
+
package. Docker/Wine remains useful for Windows path and process semantics, but
|
|
338
|
+
cannot validate AppX deployment, packaged services, trust, or Store behavior.
|
|
339
|
+
|
|
327
340
|
The macOS config-contract job downloads the exact ChatGPT archive declared in
|
|
328
341
|
`PINNED_VENDOR_APPS`, verifies its checksum, signature, app version, and
|
|
329
342
|
embedded Codex version, then validates a deterministic generated profile with
|
package/package.json
CHANGED
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",
|
package/src/commands/apps.js
CHANGED
|
@@ -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(
|
|
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) {
|
package/src/commands/converge.js
CHANGED
|
@@ -91,13 +91,18 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
91
91
|
// answer (not unref'd) so the auto-decline reliably fires even when the
|
|
92
92
|
// prompt is the only thing keeping the event loop alive.
|
|
93
93
|
const allowed = confirmed(await new Promise((resolve) => {
|
|
94
|
-
const
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
controller.abort();
|
|
97
|
+
resolve("");
|
|
98
|
+
}, io.vendorPromptTimeoutMs ?? 60_000);
|
|
95
99
|
const settle = (answer) => {
|
|
96
100
|
clearTimeout(timer);
|
|
97
101
|
resolve(answer);
|
|
98
102
|
};
|
|
99
103
|
Promise.resolve(io.promptText(
|
|
100
104
|
`Allow the verified ${label} vendor ${context.mode === "update" ? "update" : "installation"}? [y/N] (auto-N in 60s) `,
|
|
105
|
+
{ signal: controller.signal },
|
|
101
106
|
)).then(settle, () => settle(""));
|
|
102
107
|
}));
|
|
103
108
|
vendorAppDecisions.set(target, allowed);
|
|
@@ -272,7 +277,14 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
272
277
|
|| product === target
|
|
273
278
|
|| (target === "codex" && product === "chatgpt")
|
|
274
279
|
));
|
|
275
|
-
|
|
280
|
+
const installed = vendorAppTargetReady(retryReport, target);
|
|
281
|
+
return {
|
|
282
|
+
installed,
|
|
283
|
+
error: installed
|
|
284
|
+
? null
|
|
285
|
+
: retryReport?.tenants?.[0]?.errors?.join("; ")
|
|
286
|
+
|| `The ${target} vendor app did not reach a verified state.`,
|
|
287
|
+
};
|
|
276
288
|
},
|
|
277
289
|
},
|
|
278
290
|
}, overrides.recoveryOverrides || {});
|
package/src/commands/setup.js
CHANGED
|
@@ -453,7 +453,14 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
453
453
|
|| product === target
|
|
454
454
|
|| (target === "codex" && product === "chatgpt")
|
|
455
455
|
));
|
|
456
|
-
|
|
456
|
+
const installed = vendorAppTargetReady(retryReport, target);
|
|
457
|
+
return {
|
|
458
|
+
installed,
|
|
459
|
+
error: installed
|
|
460
|
+
? null
|
|
461
|
+
: retryReport?.tenants?.[0]?.errors?.join("; ")
|
|
462
|
+
|| `The ${target} vendor app did not reach a verified state.`,
|
|
463
|
+
};
|
|
457
464
|
},
|
|
458
465
|
},
|
|
459
466
|
}, overrides.recoveryOverrides || {});
|
|
@@ -500,10 +500,22 @@ async function installVendorApp(input, context) {
|
|
|
500
500
|
if (typeof context.installVendorApp !== "function") {
|
|
501
501
|
return result("failed", "Vendor app installation is unavailable in this recovery context.");
|
|
502
502
|
}
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
?
|
|
506
|
-
:
|
|
503
|
+
const outcome = await context.installVendorApp(input.target);
|
|
504
|
+
const installed = typeof outcome === "object" && outcome !== null
|
|
505
|
+
? outcome.installed !== false
|
|
506
|
+
: outcome !== false;
|
|
507
|
+
if (installed) {
|
|
508
|
+
return result("succeeded", `The ${input.target} vendor app installation completed.`);
|
|
509
|
+
}
|
|
510
|
+
const detail = typeof outcome === "object" && outcome?.error
|
|
511
|
+
? redactInstallRecoveryText(outcome.error).slice(0, 1_000)
|
|
512
|
+
: null;
|
|
513
|
+
return result(
|
|
514
|
+
"failed",
|
|
515
|
+
detail
|
|
516
|
+
? `The ${input.target} vendor app did not install cleanly: ${detail}`
|
|
517
|
+
: `The ${input.target} vendor app did not install cleanly.`,
|
|
518
|
+
);
|
|
507
519
|
}
|
|
508
520
|
|
|
509
521
|
function repairUserPath(input, context, io) {
|
package/src/prompt.js
CHANGED
|
@@ -40,12 +40,32 @@ export function promptSecret(question) {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function promptText(question
|
|
43
|
+
export function promptText(question, {
|
|
44
|
+
signal = null,
|
|
45
|
+
input = process.stdin,
|
|
46
|
+
output = process.stdout,
|
|
47
|
+
} = {}) {
|
|
44
48
|
return new Promise((resolve) => {
|
|
45
|
-
const rl = readline.createInterface({ input
|
|
46
|
-
|
|
49
|
+
const rl = readline.createInterface({ input, output });
|
|
50
|
+
let settled = false;
|
|
51
|
+
const finish = (answer, aborted = false) => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
signal?.removeEventListener("abort", abort);
|
|
47
55
|
rl.close();
|
|
48
|
-
|
|
56
|
+
// A cancelled readline prompt otherwise leaves the next status line on
|
|
57
|
+
// the same terminal row as its unanswered question.
|
|
58
|
+
if (aborted && output?.isTTY) output.write("\n");
|
|
59
|
+
resolve(String(answer || "").trim());
|
|
60
|
+
};
|
|
61
|
+
const abort = () => finish("", true);
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
abort();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
67
|
+
rl.question(question, (answer) => {
|
|
68
|
+
finish(answer);
|
|
49
69
|
});
|
|
50
70
|
});
|
|
51
71
|
}
|
package/src/windowsApps.js
CHANGED
|
@@ -3,14 +3,18 @@ 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;
|
|
17
|
+
const WINDOWS_WINGET_UPDATE_NOT_APPLICABLE = 0x8A15002B;
|
|
14
18
|
|
|
15
19
|
export function windowsClaudeUserData(environment = process.env, tenantId = null) {
|
|
16
20
|
const localAppData = environmentValue(environment, "LOCALAPPDATA")
|
|
@@ -31,6 +35,16 @@ function isFile(filePath) {
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
// A CLI launched from PowerShell 7 inherits its PSModulePath. Passing that
|
|
39
|
+
// value into Windows PowerShell 5.1 makes legacy modules such as
|
|
40
|
+
// Microsoft.PowerShell.Security visible but unloadable. Omit the variable so
|
|
41
|
+
// powershell.exe reconstructs its own version-correct default module path.
|
|
42
|
+
function windowsPowerShellEnvironment(environment = process.env) {
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
Object.entries(environment).filter(([key]) => key.toLowerCase() !== "psmodulepath"),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
function versionedSquirrelCandidates(root, readDirectory = fs.readdirSync) {
|
|
35
49
|
try {
|
|
36
50
|
return readDirectory(root, { withFileTypes: true })
|
|
@@ -45,24 +59,6 @@ function versionedSquirrelCandidates(root, readDirectory = fs.readdirSync) {
|
|
|
45
59
|
}
|
|
46
60
|
}
|
|
47
61
|
|
|
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
62
|
function installedMsixChatGPT(environment, run = spawnSync) {
|
|
67
63
|
const script = [
|
|
68
64
|
`$package = Get-AppxPackage -Name '${WINDOWS_CHATGPT_PACKAGE_NAME}' -ErrorAction SilentlyContinue`,
|
|
@@ -76,7 +72,7 @@ function installedMsixChatGPT(environment, run = spawnSync) {
|
|
|
76
72
|
try {
|
|
77
73
|
const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
78
74
|
encoding: "utf8",
|
|
79
|
-
env: environment,
|
|
75
|
+
env: windowsPowerShellEnvironment(environment),
|
|
80
76
|
stdio: ["ignore", "pipe", "ignore"],
|
|
81
77
|
windowsHide: true,
|
|
82
78
|
});
|
|
@@ -108,11 +104,12 @@ export function windowsClaudeAppCandidates(environment = process.env, dependenci
|
|
|
108
104
|
}
|
|
109
105
|
|
|
110
106
|
export function findWindowsClaudeApp(environment = process.env, dependencies = {}) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
if (dependencies.queryMsix) {
|
|
108
|
+
const msix = dependencies.queryMsix(environment, dependencies.run);
|
|
109
|
+
const exists = dependencies.isFile || isFile;
|
|
110
|
+
return msix && exists(msix) ? path.win32.normalize(msix) : null;
|
|
111
|
+
}
|
|
112
|
+
return pinnedWindowsClaudeApp(environment, dependencies);
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
/** Candidate paths for an unpackaged OpenAI ChatGPT/Codex desktop install. */
|
|
@@ -166,37 +163,210 @@ function wingetInvocation(action, environment, { packageId, source = "winget", s
|
|
|
166
163
|
);
|
|
167
164
|
}
|
|
168
165
|
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
function windowsClaudeArchitecture(environment, fallback = process.arch) {
|
|
167
|
+
const detected = environmentValue(environment, "PROCESSOR_ARCHITEW6432")
|
|
168
|
+
|| environmentValue(environment, "PROCESSOR_ARCHITECTURE")
|
|
169
|
+
|| fallback;
|
|
170
|
+
if (/arm64/iu.test(detected)) return "arm64";
|
|
171
|
+
if (/amd64|x64/iu.test(detected)) return "x64";
|
|
172
|
+
return fallback;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pinnedWindowsClaudeApp(environment = process.env, dependencies = {}) {
|
|
171
176
|
const io = {
|
|
172
|
-
environment: process.env,
|
|
173
|
-
find: findWindowsClaudeApp,
|
|
174
177
|
run: spawnSync,
|
|
178
|
+
isFile,
|
|
179
|
+
pin: PINNED_VENDOR_APPS.claude.windows,
|
|
180
|
+
architecture: windowsClaudeArchitecture(environment),
|
|
175
181
|
...dependencies,
|
|
176
182
|
};
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
let result;
|
|
183
|
+
const expectedArchitecture = io.architecture === "arm64" ? "Arm64" : "X64";
|
|
184
|
+
const script = [
|
|
185
|
+
"$package = Get-AppxPackage -Name 'Claude' -ErrorAction SilentlyContinue",
|
|
186
|
+
"$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",
|
|
187
|
+
"if ($package) { Join-Path $package.InstallLocation $env:IMPEL_CLAUDE_PACKAGE_EXECUTABLE }",
|
|
188
|
+
].join("; ");
|
|
184
189
|
try {
|
|
185
|
-
result = io.run(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
const result = io.run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
env: {
|
|
193
|
+
...windowsPowerShellEnvironment(environment),
|
|
194
|
+
IMPEL_CLAUDE_PACKAGE_VERSION: io.pin.packageVersion,
|
|
195
|
+
IMPEL_CLAUDE_PACKAGE_PUBLISHER: io.pin.publisher,
|
|
196
|
+
IMPEL_CLAUDE_PACKAGE_ARCHITECTURE: expectedArchitecture,
|
|
197
|
+
IMPEL_CLAUDE_PACKAGE_EXECUTABLE: io.pin.executable,
|
|
198
|
+
},
|
|
199
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
200
|
+
windowsHide: true,
|
|
189
201
|
});
|
|
202
|
+
const binary = result?.status === 0 ? String(result.stdout || "").trim() : "";
|
|
203
|
+
return binary && io.isFile(binary) ? path.win32.normalize(binary) : null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function downloadWindowsClaudeMsix(url, destination, fetchImpl, timeoutMs) {
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
212
|
+
try {
|
|
213
|
+
const response = await fetchImpl(url, { signal: controller.signal, redirect: "follow" });
|
|
214
|
+
if (!response?.ok || !response.body) {
|
|
215
|
+
throw new Error(`Claude MSIX download failed (HTTP ${response?.status ?? "error"})`);
|
|
216
|
+
}
|
|
217
|
+
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(destination, { flags: "wx" }));
|
|
190
218
|
} catch (error) {
|
|
191
|
-
|
|
219
|
+
if (error?.name === "AbortError") {
|
|
220
|
+
throw new Error(`Claude MSIX download timed out after ${timeoutMs}ms`);
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
} finally {
|
|
224
|
+
clearTimeout(timeout);
|
|
192
225
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const INSTALL_PINNED_CLAUDE_MSIX = String.raw`
|
|
229
|
+
$ErrorActionPreference = 'Stop'
|
|
230
|
+
$packagePath = $env:IMPEL_CLAUDE_MSIX_PATH
|
|
231
|
+
if (-not (Test-Path -LiteralPath $packagePath -PathType Leaf)) {
|
|
232
|
+
throw 'the verified Claude MSIX is missing'
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
$signature = Get-AuthenticodeSignature -LiteralPath $packagePath
|
|
236
|
+
if ($signature.Status.ToString() -cne 'Valid') {
|
|
237
|
+
throw "Claude MSIX signature is not valid ($($signature.Status))"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
241
|
+
$archive = [System.IO.Compression.ZipFile]::OpenRead($packagePath)
|
|
242
|
+
try {
|
|
243
|
+
$manifestEntry = $archive.GetEntry('AppxManifest.xml')
|
|
244
|
+
if ($null -eq $manifestEntry) { throw 'Claude MSIX has no AppxManifest.xml' }
|
|
245
|
+
$reader = [System.IO.StreamReader]::new($manifestEntry.Open())
|
|
246
|
+
try { [xml]$manifest = $reader.ReadToEnd() } finally { $reader.Dispose() }
|
|
247
|
+
$identity = $manifest.Package.Identity
|
|
248
|
+
if ([string]$identity.Name -cne $env:IMPEL_CLAUDE_PACKAGE_NAME) {
|
|
249
|
+
throw "unexpected Claude MSIX package name: $($identity.Name)"
|
|
250
|
+
}
|
|
251
|
+
if ([string]$identity.Version -cne $env:IMPEL_CLAUDE_PACKAGE_VERSION) {
|
|
252
|
+
throw "unexpected Claude MSIX version: $($identity.Version)"
|
|
253
|
+
}
|
|
254
|
+
if ([string]$identity.Publisher -cne $env:IMPEL_CLAUDE_PACKAGE_PUBLISHER) {
|
|
255
|
+
throw 'unexpected Claude MSIX publisher'
|
|
256
|
+
}
|
|
257
|
+
if ([string]$identity.ProcessorArchitecture -cne $env:IMPEL_CLAUDE_MANIFEST_ARCHITECTURE) {
|
|
258
|
+
throw "unexpected Claude MSIX architecture: $($identity.ProcessorArchitecture)"
|
|
259
|
+
}
|
|
260
|
+
$application = $manifest.Package.Applications.Application | Where-Object Id -ceq 'Claude' | Select-Object -First 1
|
|
261
|
+
if ($null -eq $application -or [string]$application.Executable -cne $env:IMPEL_CLAUDE_PACKAGE_EXECUTABLE) {
|
|
262
|
+
throw 'Claude MSIX has an unexpected application entry point'
|
|
263
|
+
}
|
|
264
|
+
foreach ($entryName in @('app/claude.exe', 'app/resources/app.asar', 'AppxSignature.p7x')) {
|
|
265
|
+
if ($null -eq $archive.GetEntry($entryName)) {
|
|
266
|
+
throw "Claude MSIX is missing required entry $entryName"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
$archive.Dispose()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
Add-AppxPackage -Path $packagePath -ForceApplicationShutdown -ForceUpdateFromAnyVersion
|
|
274
|
+
`;
|
|
275
|
+
|
|
276
|
+
function installPinnedWindowsClaudeMsix(packagePath, environment, pin, architecture, run = spawnSync) {
|
|
277
|
+
const result = run(
|
|
278
|
+
"powershell.exe",
|
|
279
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", INSTALL_PINNED_CLAUDE_MSIX],
|
|
280
|
+
{
|
|
281
|
+
encoding: "utf8",
|
|
282
|
+
env: {
|
|
283
|
+
...windowsPowerShellEnvironment(environment),
|
|
284
|
+
IMPEL_CLAUDE_MSIX_PATH: packagePath,
|
|
285
|
+
IMPEL_CLAUDE_PACKAGE_NAME: pin.packageName,
|
|
286
|
+
IMPEL_CLAUDE_PACKAGE_VERSION: pin.packageVersion,
|
|
287
|
+
IMPEL_CLAUDE_PACKAGE_PUBLISHER: pin.publisher,
|
|
288
|
+
IMPEL_CLAUDE_PACKAGE_EXECUTABLE: pin.executable,
|
|
289
|
+
IMPEL_CLAUDE_MANIFEST_ARCHITECTURE: architecture,
|
|
290
|
+
},
|
|
291
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
292
|
+
windowsHide: true,
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
if (result?.status !== 0 && !result?.error) {
|
|
296
|
+
const detail = String(result?.stderr || "").trim().split(/\r?\n/u).find(Boolean);
|
|
297
|
+
if (detail) return { ...result, error: new Error(`Claude MSIX installation failed: ${detail}`) };
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function windowsClaudeCacheRoot(homeDir, environment, version, architecture) {
|
|
303
|
+
const appsRoot = environmentValue(environment, "IMPEL_APP_HOME")
|
|
304
|
+
|| path.join(homeDir, ".config", "impel", "apps");
|
|
305
|
+
return path.join(appsRoot, "vendor-cache", "claude", version, architecture);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Download, verify, and install Anthropic's exact signed per-user MSIX. */
|
|
309
|
+
export async function ensureWindowsClaudeApp(_options = {}, dependencies = {}) {
|
|
310
|
+
const io = {
|
|
311
|
+
environment: process.env,
|
|
312
|
+
architecture: null,
|
|
313
|
+
downloadTimeoutMs: WINDOWS_CLAUDE_DOWNLOAD_TIMEOUT_MS,
|
|
314
|
+
fetchImpl: fetch,
|
|
315
|
+
findPinned: pinnedWindowsClaudeApp,
|
|
316
|
+
homeDir: os.homedir(),
|
|
317
|
+
install: installPinnedWindowsClaudeMsix,
|
|
318
|
+
logger: console,
|
|
319
|
+
pin: PINNED_VENDOR_APPS.claude,
|
|
320
|
+
run: spawnSync,
|
|
321
|
+
...dependencies,
|
|
199
322
|
};
|
|
323
|
+
const architecture = io.architecture || windowsClaudeArchitecture(io.environment);
|
|
324
|
+
const windowsPin = io.pin?.windows;
|
|
325
|
+
const release = windowsPin?.downloads?.[architecture];
|
|
326
|
+
if (!windowsPin || !release) {
|
|
327
|
+
const error = new Error(`Claude: no verified Windows MSIX is available for ${architecture}`);
|
|
328
|
+
return { binary: null, action: "install-failed", result: { status: null, error } };
|
|
329
|
+
}
|
|
330
|
+
const findPinned = () => io.findPinned(io.environment, {
|
|
331
|
+
architecture,
|
|
332
|
+
pin: windowsPin,
|
|
333
|
+
run: io.run,
|
|
334
|
+
});
|
|
335
|
+
const existing = findPinned();
|
|
336
|
+
if (existing) return { binary: existing, action: "existing", result: null };
|
|
337
|
+
|
|
338
|
+
const cacheRoot = windowsClaudeCacheRoot(io.homeDir, io.environment, io.pin.version, architecture);
|
|
339
|
+
const archive = path.join(cacheRoot, `Claude-${io.pin.version}-${architecture}.msix`);
|
|
340
|
+
const temporary = `${archive}.download-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
|
341
|
+
try {
|
|
342
|
+
fs.mkdirSync(cacheRoot, { recursive: true, mode: 0o700 });
|
|
343
|
+
if (fs.existsSync(archive) && sha256File(archive) !== release.sha256) fs.rmSync(archive, { force: true });
|
|
344
|
+
if (!fs.existsSync(archive)) {
|
|
345
|
+
io.logger.log(`Claude: downloading verified vendor app ${io.pin.version} for Windows ${architecture}…`);
|
|
346
|
+
await downloadWindowsClaudeMsix(release.url, temporary, io.fetchImpl, io.downloadTimeoutMs);
|
|
347
|
+
if (sha256File(temporary) !== release.sha256) {
|
|
348
|
+
throw new Error(`Claude MSIX integrity check failed for ${io.pin.version} (${architecture})`);
|
|
349
|
+
}
|
|
350
|
+
fs.renameSync(temporary, archive);
|
|
351
|
+
}
|
|
352
|
+
let result = io.install(archive, io.environment, windowsPin, architecture, io.run);
|
|
353
|
+
const binary = result?.status === 0 && !result?.error ? findPinned() : null;
|
|
354
|
+
if (result?.status === 0 && !result?.error && !binary) {
|
|
355
|
+
result = {
|
|
356
|
+
...result,
|
|
357
|
+
error: new Error(`Claude ${io.pin.version} did not appear after its verified MSIX was installed`),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
binary,
|
|
362
|
+
action: binary ? "installed" : "install-failed",
|
|
363
|
+
result,
|
|
364
|
+
};
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return { binary: null, action: "install-failed", result: { status: null, error } };
|
|
367
|
+
} finally {
|
|
368
|
+
fs.rmSync(temporary, { force: true });
|
|
369
|
+
}
|
|
200
370
|
}
|
|
201
371
|
|
|
202
372
|
/** Install/update OpenAI's official ChatGPT/Codex Store package. */
|
|
@@ -204,13 +374,17 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
|
|
|
204
374
|
const io = {
|
|
205
375
|
environment: process.env,
|
|
206
376
|
find: findWindowsChatGPTApp,
|
|
377
|
+
logger: console,
|
|
207
378
|
run: spawnSync,
|
|
208
379
|
...dependencies,
|
|
209
380
|
};
|
|
210
381
|
const before = io.find(io.environment);
|
|
211
382
|
if (before && !update) return { binary: before, action: "existing", result: null };
|
|
212
383
|
|
|
213
|
-
|
|
384
|
+
// `impel update` also repairs missing surfaces. A missing Store package must
|
|
385
|
+
// be installed, not sent to `winget upgrade` (which returns 0x8A150014).
|
|
386
|
+
const shouldUpdate = Boolean(update && before);
|
|
387
|
+
const invocation = wingetInvocation(shouldUpdate ? "update" : "install", io.environment, {
|
|
214
388
|
packageId: WINDOWS_CHATGPT_PACKAGE,
|
|
215
389
|
source: "msstore",
|
|
216
390
|
scope: null,
|
|
@@ -226,10 +400,25 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
|
|
|
226
400
|
result = { status: null, error };
|
|
227
401
|
}
|
|
228
402
|
const binary = io.find(io.environment) || before;
|
|
229
|
-
|
|
403
|
+
// winget uses an error HRESULT when `upgrade` finds an installed package
|
|
404
|
+
// with no newer Store release. That is a healthy idempotent update result,
|
|
405
|
+
// not an installation failure (APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE).
|
|
406
|
+
const alreadyUpToDate = Boolean(
|
|
407
|
+
shouldUpdate
|
|
408
|
+
&& binary
|
|
409
|
+
&& !result?.error
|
|
410
|
+
&& Number.isInteger(result?.status)
|
|
411
|
+
&& (result.status >>> 0) === WINDOWS_WINGET_UPDATE_NOT_APPLICABLE,
|
|
412
|
+
);
|
|
413
|
+
if (alreadyUpToDate) {
|
|
414
|
+
io.logger.log("ChatGPT/Codex: Microsoft Store package already up to date.");
|
|
415
|
+
}
|
|
416
|
+
const succeeded = (result?.status === 0 && !result?.error) || alreadyUpToDate;
|
|
230
417
|
return {
|
|
231
418
|
binary,
|
|
232
|
-
action:
|
|
419
|
+
action: alreadyUpToDate
|
|
420
|
+
? "existing"
|
|
421
|
+
: succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
|
|
233
422
|
result,
|
|
234
423
|
};
|
|
235
424
|
}
|