impel-cli 0.18.1 → 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 +29 -15
- package/package.json +1 -1
- package/src/apps.js +29 -4
- package/src/cli.js +2 -0
- package/src/commands/apps.js +5 -2
- package/src/macDeveloperTools.js +65 -0
- package/src/windowsApps.js +210 -48
package/README.md
CHANGED
|
@@ -22,6 +22,11 @@ npm install --global impel-cli
|
|
|
22
22
|
|
|
23
23
|
Node.js 18 or newer is required.
|
|
24
24
|
|
|
25
|
+
On macOS, setup and update verify Apple's Xcode Command Line Tools before any
|
|
26
|
+
profile, app, or skills work begins. If they are missing, Impel opens the
|
|
27
|
+
system installer with `xcode-select --install` and stops safely; finish that
|
|
28
|
+
installation, then rerun the same Impel command.
|
|
29
|
+
|
|
25
30
|
Then run setup and paste an Impel Personal Access Token when prompted:
|
|
26
31
|
|
|
27
32
|
```sh
|
|
@@ -35,17 +40,19 @@ surfaces for each tenant. You do not need to repeat setup tenant by tenant.
|
|
|
35
40
|
|
|
36
41
|
`impel setup`:
|
|
37
42
|
|
|
38
|
-
1.
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
1. On macOS, verifies the Xcode Command Line Tools and opens Apple's installer
|
|
44
|
+
before doing any other setup work when they are missing.
|
|
45
|
+
2. Stores the PAT in the private Impel config file.
|
|
46
|
+
3. Fetches the complete live tenant list.
|
|
47
|
+
4. Preserves the current CLI tenant when it is still accessible, or selects the
|
|
41
48
|
control-plane default.
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
5. Creates isolated Claude and Codex CLI profiles for every supported tenant.
|
|
50
|
+
6. On Windows machines without git, installs a pinned, checksum-verified
|
|
44
51
|
MinGit into `~/.config/impel/tools/git` (no admin rights, no system PATH
|
|
45
52
|
change) so Claude and Codex can install the Impel skills marketplace.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
7. Installs or repairs supported tenant desktop apps on macOS and Windows.
|
|
54
|
+
8. Registers tenant apps with Finder/Spotlight or Windows Start/Search.
|
|
55
|
+
9. Verifies every supported tenant surface and prints a tenant-sorted summary.
|
|
49
56
|
|
|
50
57
|
Use `--tenant` only to choose the default for CLI launches. It does not limit
|
|
51
58
|
which tenants setup prepares:
|
|
@@ -117,13 +124,15 @@ impel update
|
|
|
117
124
|
|
|
118
125
|
Update:
|
|
119
126
|
|
|
120
|
-
1.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
1. On macOS, verifies the Xcode Command Line Tools and opens Apple's installer
|
|
128
|
+
before doing any other update work when they are missing.
|
|
129
|
+
2. Checks and installs the latest `impel-cli` package from npm.
|
|
130
|
+
3. Continues under the freshly installed build.
|
|
131
|
+
4. Fetches the current live tenant list.
|
|
132
|
+
5. Fully prepares tenants added since the previous run.
|
|
133
|
+
6. Repairs and upgrades existing tenants in place.
|
|
134
|
+
7. Re-registers operating-system launch entries.
|
|
135
|
+
8. Reports local tenants that are no longer accessible without deleting them.
|
|
127
136
|
|
|
128
137
|
An update preserves histories, browser state, preferences, permissions, stable
|
|
129
138
|
app identity, and user-authored settings outside Impel-managed blocks. Desktop
|
|
@@ -246,6 +255,11 @@ compatibility for one release, but no command can enable native routing.
|
|
|
246
255
|
### Windows
|
|
247
256
|
|
|
248
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.
|
|
249
263
|
- Signed vendor desktop executables remain unchanged.
|
|
250
264
|
- Tenant profiles and Start entries are isolated per tenant.
|
|
251
265
|
- Shared package-manager/vendor preparation runs once per product.
|
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",
|
|
@@ -726,7 +742,12 @@ export function installManagedAppFiles({
|
|
|
726
742
|
if (resolvedVendorPaths[target] && isVendableVendorApp(target, resolvedVendorPaths[target])) {
|
|
727
743
|
assertPinnedVendorAppVersion(target, resolvedVendorPaths[target]);
|
|
728
744
|
}
|
|
729
|
-
if (target === "chatgpt") writeVendoredChatGPTBundle(
|
|
745
|
+
if (target === "chatgpt") writeVendoredChatGPTBundle(
|
|
746
|
+
paths,
|
|
747
|
+
resolvedVendorPaths.chatgpt,
|
|
748
|
+
config.gatewayUrl,
|
|
749
|
+
homeDir,
|
|
750
|
+
);
|
|
730
751
|
else writeVendoredClaudeBundle(
|
|
731
752
|
paths,
|
|
732
753
|
resolvedVendorPaths.claude,
|
|
@@ -1561,7 +1582,7 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
|
|
|
1561
1582
|
}
|
|
1562
1583
|
}
|
|
1563
1584
|
|
|
1564
|
-
function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
1585
|
+
function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1565
1586
|
if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
|
|
1566
1587
|
const definition = APP_DEFINITIONS.chatgpt;
|
|
1567
1588
|
const executableName = definition.executableNames.find((name) => (
|
|
@@ -1600,7 +1621,7 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
1600
1621
|
const asarHash = asarHeaderHash(runtimeAsarPath);
|
|
1601
1622
|
const vendorExecutable = path.join(vendorBundle, "Contents", "MacOS", executableName);
|
|
1602
1623
|
const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
|
|
1603
|
-
const launchHost = ensureChatGPTLaunchHost();
|
|
1624
|
+
const launchHost = ensureChatGPTLaunchHost({ cacheDir: chatGPTLaunchHostCacheDir(homeDir) });
|
|
1604
1625
|
writeAtomic(
|
|
1605
1626
|
path.join(staging, "Contents", "Info.plist"),
|
|
1606
1627
|
vendoredChatGPTWrapperPlist(vendorPath, asarHash, paths.chatgpt, launchHost ? "launch-host" : "launch"),
|
|
@@ -2083,7 +2104,7 @@ export function ensureChatGPTLaunchHost({
|
|
|
2083
2104
|
platform = process.platform,
|
|
2084
2105
|
osRelease = os.release(),
|
|
2085
2106
|
compiler = "/usr/bin/cc",
|
|
2086
|
-
cacheDir =
|
|
2107
|
+
cacheDir = chatGPTLaunchHostCacheDir(),
|
|
2087
2108
|
warn = console.warn,
|
|
2088
2109
|
} = {}) {
|
|
2089
2110
|
if (platform !== "darwin") return null;
|
|
@@ -2127,6 +2148,10 @@ export function ensureChatGPTLaunchHost({
|
|
|
2127
2148
|
}
|
|
2128
2149
|
}
|
|
2129
2150
|
|
|
2151
|
+
export function chatGPTLaunchHostCacheDir(homeDir = os.homedir()) {
|
|
2152
|
+
return path.join(homeDir, ".config", "impel", "cache", "chatgpt-launch-host");
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2130
2155
|
function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity, executableName = "launch") {
|
|
2131
2156
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2132
2157
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
package/src/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { cmdUpdate } from "./commands/update.js";
|
|
|
20
20
|
import { cmdNuke } from "./commands/nuke.js";
|
|
21
21
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
22
22
|
import { cmdConverge } from "./commands/converge.js";
|
|
23
|
+
import { ensureMacDeveloperTools } from "./macDeveloperTools.js";
|
|
23
24
|
import { refuseElevatedMacExecution } from "./privileges.js";
|
|
24
25
|
|
|
25
26
|
const HELP = `impel — isolated Impel workspaces for every tenant
|
|
@@ -82,6 +83,7 @@ function normalizeTarget(token) {
|
|
|
82
83
|
|
|
83
84
|
export async function main(argv) {
|
|
84
85
|
if (refuseElevatedMacExecution(argv)) return;
|
|
86
|
+
if (!ensureMacDeveloperTools(argv)) return;
|
|
85
87
|
const [cmd, ...rest] = argv;
|
|
86
88
|
|
|
87
89
|
switch (cmd) {
|
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) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const XCODE_SELECT = "/usr/bin/xcode-select";
|
|
4
|
+
|
|
5
|
+
function hasHelpFlag(argv) {
|
|
6
|
+
return argv[0] === "help" || argv.slice(1).some((arg) => ["--help", "-h"].includes(arg));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Commands that may clone a skills marketplace or build a managed macOS app.
|
|
11
|
+
* Read-only commands deliberately stay available while Apple's installer is
|
|
12
|
+
* pending, and `update --check` keeps its documented no-mutation contract.
|
|
13
|
+
*/
|
|
14
|
+
export function commandRequiresMacDeveloperTools(argv = []) {
|
|
15
|
+
const [command, subcommand] = argv;
|
|
16
|
+
if (!command || hasHelpFlag(argv)) return false;
|
|
17
|
+
|
|
18
|
+
if (["setup", "_converge"].includes(command)) return true;
|
|
19
|
+
if (["update", "upgrade"].includes(command)) {
|
|
20
|
+
return !argv.includes("--check") && !argv.includes("--refresh-cache");
|
|
21
|
+
}
|
|
22
|
+
if (["skill", "skills"].includes(command)) return !subcommand || subcommand === "sync";
|
|
23
|
+
if (["app", "apps"].includes(command)) {
|
|
24
|
+
return ["install", "update", "open", "refresh"].includes(subcommand);
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ensure Apple's Command Line Tools are available before Impel mutates any
|
|
31
|
+
* lifecycle state that depends on git or the native compiler. The Apple
|
|
32
|
+
* installer runs asynchronously, so a newly requested install must stop this
|
|
33
|
+
* invocation and let the operator rerun it after installation finishes.
|
|
34
|
+
*/
|
|
35
|
+
export function ensureMacDeveloperTools(argv = [], dependencies = {}) {
|
|
36
|
+
const io = {
|
|
37
|
+
platform: process.platform,
|
|
38
|
+
spawn: spawnSync,
|
|
39
|
+
log: (message) => console.log(message),
|
|
40
|
+
error: (message) => console.error(message),
|
|
41
|
+
setExitCode: (code) => {
|
|
42
|
+
process.exitCode = code;
|
|
43
|
+
},
|
|
44
|
+
...dependencies,
|
|
45
|
+
};
|
|
46
|
+
if (io.platform !== "darwin" || !commandRequiresMacDeveloperTools(argv)) return true;
|
|
47
|
+
|
|
48
|
+
const selected = io.spawn(XCODE_SELECT, ["-p"], { stdio: "ignore" });
|
|
49
|
+
if (!selected.error && selected.status === 0) return true;
|
|
50
|
+
|
|
51
|
+
io.log("Xcode Command Line Tools are required; opening Apple's installer…");
|
|
52
|
+
const requested = io.spawn(XCODE_SELECT, ["--install"], { stdio: "inherit" });
|
|
53
|
+
if (requested.error) {
|
|
54
|
+
io.error(`impel: could not start the Xcode Command Line Tools installer (${requested.error.message || requested.error}).`);
|
|
55
|
+
io.error(" Run `xcode-select --install`, finish the system installation, then rerun this Impel command.");
|
|
56
|
+
} else if (requested.status !== 0) {
|
|
57
|
+
io.error("impel: the Xcode Command Line Tools installer did not start successfully.");
|
|
58
|
+
io.error(" Run `xcode-select --install`, finish the system installation, then rerun this Impel command.");
|
|
59
|
+
} else {
|
|
60
|
+
io.error("impel: finish installing the Xcode Command Line Tools in the system dialog, then rerun this command.");
|
|
61
|
+
io.error(" No Impel profiles, apps, or skills were changed.");
|
|
62
|
+
}
|
|
63
|
+
io.setExitCode(1);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
package/src/windowsApps.js
CHANGED
|
@@ -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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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 ? (
|
|
394
|
+
action: succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
|
|
233
395
|
result,
|
|
234
396
|
};
|
|
235
397
|
}
|