impel-cli 0.18.13 → 0.18.14-beta.1

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
@@ -14,7 +14,7 @@ impel update
14
14
 
15
15
  ## Install
16
16
 
17
- Install the public npm package once per machine:
17
+ On macOS and Linux, install the public npm package once per machine:
18
18
 
19
19
  ```sh
20
20
  npm install --global impel-cli
@@ -22,17 +22,31 @@ npm install --global impel-cli
22
22
 
23
23
  Node.js 18 or newer is required.
24
24
 
25
+ On Windows PowerShell, invoke npm's command shims explicitly:
26
+
27
+ ```powershell
28
+ npm.cmd install --global impel-cli
29
+ impel.cmd setup
30
+ ```
31
+
32
+ The explicit `.cmd` suffix prevents PowerShell from selecting npm's `.ps1`
33
+ shim when script execution is restricted. Impel does not change the machine's
34
+ or current user's PowerShell execution policy.
35
+
25
36
  On macOS, setup and update verify Apple's Xcode Command Line Tools before any
26
37
  profile, app, or skills work begins. If they are missing, Impel opens the
27
38
  system installer with `xcode-select --install` and stops safely; finish that
28
39
  installation, then rerun the same Impel command.
29
40
 
30
- Then run setup and paste an Impel Personal Access Token when prompted:
41
+ Then run setup and paste an Impel Personal Access Token when prompted. On
42
+ macOS and Linux:
31
43
 
32
44
  ```sh
33
45
  impel setup
34
46
  ```
35
47
 
48
+ On Windows PowerShell, use `impel.cmd setup` as shown above.
49
+
36
50
  Setup discovers every tenant available to the PAT and prepares all supported
37
51
  surfaces for each tenant. You do not need to repeat setup tenant by tenant.
38
52
 
@@ -128,6 +142,12 @@ Run one command for routine maintenance:
128
142
  impel update
129
143
  ```
130
144
 
145
+ On Windows PowerShell, keep the command shim explicit:
146
+
147
+ ```powershell
148
+ impel.cmd update
149
+ ```
150
+
131
151
  Update:
132
152
 
133
153
  1. On macOS, verifies the Xcode Command Line Tools and opens Apple's installer
@@ -261,6 +281,15 @@ compatibility for one release, but no command can enable native routing.
261
281
  ### Windows
262
282
 
263
283
  - Missing official Claude Code and Codex CLIs can be installed during setup.
284
+ - Each CLI prompt explains that Impel will download, verify, and install the
285
+ official vendor CLI for the current user; each defaults to No.
286
+ - Run `impel.cmd setup` or `impel.cmd update` from PowerShell so restricted
287
+ script policy cannot select the npm-generated `impel.ps1` shim. Impel does
288
+ not modify persistent PowerShell execution policy.
289
+ - Claude Chat, Code, and Cowork remain enabled through the per-user managed
290
+ profile. Impel does not infer Cowork runtime support during setup or update.
291
+ - If Cowork reports a problem, open Claude Desktop's **Help > Troubleshooting >
292
+ Show Logs** and use the vendor diagnostics for that exact runtime failure.
264
293
  - Claude Desktop is downloaded from Anthropic as the exact x64 or arm64 MSIX
265
294
  pinned by this CLI. Its SHA-256, Authenticode signature, package identity,
266
295
  version, architecture, and required layout are checked before installation.
@@ -344,6 +373,15 @@ run a real package manager, or modify native personal profiles.
344
373
  CI runs on Ubuntu, macOS, and Windows with supported Node versions. See
345
374
  [RELEASING.md](RELEASING.md) for the npm release process.
346
375
 
376
+ The pinned macOS CLI installer contract follows the official Claude and Codex
377
+ installer redirects with the production downloader, then verifies their
378
+ reviewed final origins, size, shell-script marker, and SHA-256 without executing
379
+ either script or touching a local profile. Run it explicitly with:
380
+
381
+ ```sh
382
+ IMPEL_VALIDATE_PINNED_MAC_CLI_INSTALLERS=1 node --test test/mac-installer-contract.test.js
383
+ ```
384
+
347
385
  The separate Windows vendor contract runs only on a disposable GitHub-hosted
348
386
  Windows VM. It installs the exact newer Claude fixture that preceded the
349
387
  current compatibility pin, invokes the production downloader and installer to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.13",
3
+ "version": "0.18.14-beta.1",
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
@@ -232,7 +232,11 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
232
232
  // 22: refresh GPT-5.6 model windows from the pinned ChatGPT Codex catalog.
233
233
  // 23: select Codex's unelevated native Windows sandbox for managed profiles so
234
234
  // enterprise machines do not enter the administrator-only setup flow.
235
- export const CURRENT_CONFIG_VERSION = 23;
235
+ // 24: thread the inspected Windows Cowork host capability into Claude config
236
+ // and the tenant manifest so unsupported hosts keep Chat and Code usable.
237
+ // 25: remove the unproven Windows Cowork capability gate from beta.0 profiles
238
+ // and restore the reviewed always-enabled managed policy.
239
+ export const CURRENT_CONFIG_VERSION = 25;
236
240
 
237
241
  // Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
238
242
  // helper rebranding, and signing. A vendored bundle is rebuilt only when this
@@ -790,7 +794,7 @@ export function installManagedAppFiles({
790
794
  }
791
795
 
792
796
  writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
793
- schemaVersion: 4,
797
+ schemaVersion: 6,
794
798
  configVersion: CURRENT_CONFIG_VERSION,
795
799
  modelCatalogVersion: 3,
796
800
  gatewayUrl: config.gatewayUrl,
@@ -777,7 +777,9 @@ export async function cmdWindowsApps(argv, overrides = {}) {
777
777
  managedChatGPT = io.stageChatGPTApp(vendorPaths.chatgpt, actionPaths.root);
778
778
  }
779
779
 
780
- const { userData } = configureWindowsApps(config, actionTargets, vendorPaths, catalog, io);
780
+ const { userData } = configureWindowsApps(config, actionTargets, vendorPaths, catalog, {
781
+ ...io,
782
+ });
781
783
  const tenantPaths = appPaths(io.homeDir, config.tenantId, {
782
784
  claudeUserData: userData,
783
785
  tenantName: config.tenantName,
@@ -123,6 +123,7 @@ export async function cmdConverge(argv = [], overrides = {}) {
123
123
  confirmVendorInstall: confirmInstall,
124
124
  }, overrides.reconcileOverrides || {});
125
125
 
126
+ const cliInstallDecisions = new Map();
126
127
  let sharedFailure = null;
127
128
  const prepareSharedClis = async ({ confirmedTools = null, inspectOnly = false } = {}) => {
128
129
  try {
@@ -141,11 +142,20 @@ export async function cmdConverge(argv = [], overrides = {}) {
141
142
  for (const tool of inspected.missingAfter) {
142
143
  if (!inspected.installCommands?.[tool]) continue;
143
144
  const label = tool === "claude" ? "Claude Code" : "Codex";
144
- if (confirmed(await io.promptText(`Install the official ${label} CLI for this user? [y/N] `))) {
145
+ if (confirmed(await io.promptText(`Impel will download, verify, and install the official ${label} CLI for the current user. Continue? [y/N] `))) {
145
146
  installTools.push(tool);
147
+ cliInstallDecisions.set(tool, "approved");
148
+ } else {
149
+ cliInstallDecisions.set(tool, "declined");
146
150
  }
147
151
  }
152
+ } else {
153
+ for (const tool of inspected.missingAfter) {
154
+ if (inspected.installCommands?.[tool]) cliInstallDecisions.set(tool, "confirmation-required");
155
+ }
148
156
  }
157
+ } else {
158
+ for (const tool of installTools) cliInstallDecisions.set(tool, "approved");
149
159
  }
150
160
  if (installTools.length) {
151
161
  prepared = await io.preparePlatformClis({
@@ -157,7 +167,15 @@ export async function cmdConverge(argv = [], overrides = {}) {
157
167
  });
158
168
  }
159
169
  }
160
- sharedFailure = prepared.missingAfter.length ? describeCliFailure(prepared) : null;
170
+ sharedFailure = prepared.missingAfter.length
171
+ ? describeCliFailure(prepared, {
172
+ commandName: "update",
173
+ cliExecutable: io.platform === "win32" ? "impel.cmd" : "impel",
174
+ isTTY: io.isTTY,
175
+ decisions: cliInstallDecisions,
176
+ skipClis,
177
+ })
178
+ : null;
161
179
  return prepared;
162
180
  } catch (error) {
163
181
  sharedFailure = redactSecretText(error?.message || error);
@@ -253,6 +253,7 @@ export async function cmdSetup(argv, overrides = {}) {
253
253
  return allowed;
254
254
  };
255
255
 
256
+ const cliInstallDecisions = new Map();
256
257
  let sharedFailure = null;
257
258
  const prepareSharedClis = async ({ confirmedTools = null, inspectOnly = false } = {}) => {
258
259
  try {
@@ -271,11 +272,20 @@ export async function cmdSetup(argv, overrides = {}) {
271
272
  for (const tool of inspected.missingAfter) {
272
273
  if (!inspected.installCommands?.[tool]) continue;
273
274
  const label = tool === "claude" ? "Claude Code" : "Codex";
274
- if (confirmed(await io.promptText(`Install the official ${label} CLI for this user? [y/N] `))) {
275
+ if (confirmed(await io.promptText(`Impel will download, verify, and install the official ${label} CLI for the current user. Continue? [y/N] `))) {
275
276
  installTools.push(tool);
277
+ cliInstallDecisions.set(tool, "approved");
278
+ } else {
279
+ cliInstallDecisions.set(tool, "declined");
276
280
  }
277
281
  }
282
+ } else {
283
+ for (const tool of inspected.missingAfter) {
284
+ if (inspected.installCommands?.[tool]) cliInstallDecisions.set(tool, "confirmation-required");
285
+ }
278
286
  }
287
+ } else {
288
+ for (const tool of installTools) cliInstallDecisions.set(tool, "approved");
279
289
  }
280
290
  if (installTools.length) {
281
291
  prepared = await io.preparePlatformClis({
@@ -288,7 +298,13 @@ export async function cmdSetup(argv, overrides = {}) {
288
298
  }
289
299
  }
290
300
  if (prepared.missingAfter.length) {
291
- sharedFailure = describeCliFailure(prepared);
301
+ sharedFailure = describeCliFailure(prepared, {
302
+ commandName: "setup",
303
+ cliExecutable: io.platform === "win32" ? "impel.cmd" : "impel",
304
+ isTTY: io.isTTY,
305
+ decisions: cliInstallDecisions,
306
+ skipClis: flags["skip-clis"],
307
+ });
292
308
  return prepared;
293
309
  }
294
310
  sharedFailure = null;
@@ -476,7 +492,8 @@ export async function cmdSetup(argv, overrides = {}) {
476
492
  : "Launch: no provider CLI is currently available for this tenant");
477
493
  if (!report.passed || sharedFailure) {
478
494
  if (sharedFailure) console.error(`Shared setup failure: ${sharedFailure}`);
479
- console.error("Setup is incomplete. Fix the reported issue or rerun `impel setup`.");
495
+ const setupCommand = io.platform === "win32" ? "impel.cmd setup" : "impel setup";
496
+ console.error(`Setup is incomplete. Fix the reported issue or rerun \`${setupCommand}\`.`);
480
497
  process.exitCode = 1;
481
498
  return report;
482
499
  }
@@ -27,7 +27,7 @@ function readShellManifest(paths) {
27
27
  }
28
28
  }
29
29
 
30
- function desktopReadiness(tenant, {
30
+ export function desktopReadiness(tenant, {
31
31
  platform = process.platform,
32
32
  homeDir = os.homedir(),
33
33
  environment = process.env,
package/src/macSetup.js CHANGED
@@ -20,13 +20,21 @@ const MAC_SIGNING_TEAMS = Object.freeze({ claude: "Q6L2SF6YDW", codex: "2DC432GL
20
20
  export const MAC_CLI_INSTALLERS = Object.freeze({
21
21
  claude: Object.freeze({
22
22
  url: "https://claude.ai/install.sh",
23
- sha256: "b3f79015b54c751440a6488f07b1b64f9088742b9052bc1bd356d13108320d2a",
23
+ trustedOrigins: Object.freeze([
24
+ "https://claude.ai",
25
+ "https://downloads.claude.ai",
26
+ ]),
27
+ sha256: "cde4f1702d3b1695f92b73d26888364e17bca476e17f0fd676484c951d36c125",
24
28
  command: "curl -fsSL https://claude.ai/install.sh | bash -s stable",
25
29
  args: Object.freeze(["stable"]),
26
30
  environment: Object.freeze({}),
27
31
  }),
28
32
  codex: Object.freeze({
29
33
  url: "https://chatgpt.com/codex/install.sh",
34
+ trustedOrigins: Object.freeze([
35
+ "https://chatgpt.com",
36
+ "https://releases.openai.com",
37
+ ]),
30
38
  sha256: "1154e9daf713aacd1534efca8042bfd6665ad24bc1d1dfd86b8f439fe60a7a5d",
31
39
  command: "curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 CODEX_RELEASE=0.144.6 sh",
32
40
  args: Object.freeze([]),
@@ -75,7 +83,10 @@ function detectMacClis(find, environment, verify) {
75
83
  return { claude: detect("claude"), codex: detect("codex") };
76
84
  }
77
85
 
78
- async function downloadPinnedInstaller(tool, { fetchImpl, fsImpl, installers }) {
86
+ export async function fetchVerifiedMacCliInstaller(tool, {
87
+ fetchImpl = fetch,
88
+ installers = MAC_CLI_INSTALLERS,
89
+ } = {}) {
79
90
  const installer = installers[tool];
80
91
  if (!installer) throw new Error(`unsupported macOS CLI installer: ${tool}`);
81
92
  const controller = new AbortController();
@@ -87,9 +98,14 @@ async function downloadPinnedInstaller(tool, { fetchImpl, fsImpl, installers })
87
98
  clearTimeout(timeout);
88
99
  }
89
100
  if (!response.ok) throw new Error(`official ${tool} installer returned HTTP ${response.status}`);
90
- const finalUrl = new URL(response.url || installer.url);
91
- if (finalUrl.protocol !== "https:" || !["claude.ai", "chatgpt.com"].includes(finalUrl.hostname)) {
92
- throw new Error(`official ${tool} installer redirected to an untrusted origin`);
101
+ let finalUrl;
102
+ try {
103
+ finalUrl = new URL(response.url || installer.url);
104
+ } catch {
105
+ throw new Error(`official ${tool} installer returned an invalid final URL`);
106
+ }
107
+ if (finalUrl.protocol !== "https:" || !installer.trustedOrigins?.includes(finalUrl.origin)) {
108
+ throw new Error(`official ${tool} installer redirected to untrusted origin ${finalUrl.origin}`);
93
109
  }
94
110
  const bytes = Buffer.from(await response.arrayBuffer());
95
111
  if (bytes.length === 0 || bytes.length > MAX_INSTALLER_BYTES) {
@@ -101,6 +117,11 @@ async function downloadPinnedInstaller(tool, { fetchImpl, fsImpl, installers })
101
117
  }
102
118
  const source = bytes.toString("utf8");
103
119
  if (!source.startsWith("#!")) throw new Error(`official ${tool} installer was not a shell script`);
120
+ return { bytes, finalUrl: finalUrl.href, sha256: digest, status: response.status };
121
+ }
122
+
123
+ async function downloadPinnedInstaller(tool, { fetchImpl, fsImpl, installers }) {
124
+ const { bytes } = await fetchVerifiedMacCliInstaller(tool, { fetchImpl, installers });
104
125
  const directory = fsImpl.mkdtempSync(path.join(os.tmpdir(), `impel-${tool}-installer-`));
105
126
  const script = path.join(directory, "install.sh");
106
127
  fsImpl.writeFileSync(script, bytes, { mode: 0o700 });
@@ -31,19 +31,39 @@ export async function preparePlatformClis(options = {}, dependencies = {}) {
31
31
  };
32
32
  }
33
33
 
34
- export function describeCliFailure(prepared) {
34
+ export function describeCliFailure(prepared, {
35
+ commandName = "setup",
36
+ cliExecutable = "impel",
37
+ isTTY = true,
38
+ decisions = {},
39
+ skipClis = false,
40
+ } = {}) {
35
41
  const labels = { claude: "Claude Code", codex: "Codex" };
36
42
  const details = [];
37
43
  for (const tool of prepared.missingAfter || []) {
38
44
  const installation = prepared.installations?.[tool];
39
45
  const failure = installation?.failure;
46
+ const decision = decisions instanceof Map ? decisions.get(tool) : decisions?.[tool];
47
+ const attempted = Boolean(
48
+ installation?.attempted
49
+ || failure
50
+ || installation?.succeeded === false,
51
+ );
40
52
  const reason = failure?.message || failure?.code
41
53
  || (Number.isInteger(failure?.status)
42
54
  ? `exit ${failure.status}`
43
- : installation ? "not discoverable after installation" : "not installed or discoverable");
55
+ : attempted
56
+ ? "not discoverable after installation"
57
+ : decision === "declined"
58
+ ? "automatic installation was declined"
59
+ : skipClis
60
+ ? "installation was skipped by --skip-clis"
61
+ : !isTTY && prepared.installCommands?.[tool]
62
+ ? `interactive confirmation required; rerun \`${cliExecutable} ${commandName}\` interactively (or use \`--skip-clis\` to leave it uninstalled)`
63
+ : "not installed or discoverable");
44
64
  details.push(`${labels[tool] || tool}: ${redactSecretText(reason)}`);
45
65
  const command = installation?.command || prepared.installCommands?.[tool];
46
- if (command) details.push(`manual ${tool} installer: ${redactSecretText(command)}`);
66
+ if (attempted && command) details.push(`manual ${tool} recovery command: ${redactSecretText(command)}`);
47
67
  }
48
68
  return details.join("; ") || `missing vendor CLIs: ${(prepared.missingAfter || []).join(", ")}`;
49
69
  }
@@ -8,7 +8,11 @@ import { redactSecretText } from "./config.js";
8
8
  import { environmentValue, nativeCommandInvocation, resolveNativeBinary } from "./nativeProcess.js";
9
9
  import { managedCliEntrypoint } from "./selfInvocation.js";
10
10
  import { normalizeTenantId } from "./tenants.js";
11
- import { windowsClaudeUserData } from "./windowsApps.js";
11
+ import {
12
+ findManagedWindowsChatGPTApp,
13
+ findWindowsClaudeApp,
14
+ windowsClaudeUserData,
15
+ } from "./windowsApps.js";
12
16
  import { RUNTIME_BRAND } from "./runtimeBrand.js";
13
17
 
14
18
  const LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
@@ -151,6 +155,8 @@ export function registerTenantShellEntries({
151
155
  run = spawnSync,
152
156
  writeManifest = writeShellEntryManifest,
153
157
  shortcut = registerWindowsTenantShortcut,
158
+ findClaudeApp = findWindowsClaudeApp,
159
+ findChatGPTApp = findManagedWindowsChatGPTApp,
154
160
  } = {}) {
155
161
  const id = normalizeTenantId(tenantId);
156
162
  const paths = appPaths(homeDir, id, {
@@ -175,9 +181,27 @@ export function registerTenantShellEntries({
175
181
  };
176
182
  for (const product of targets) {
177
183
  if (!available[product]) throw new Error(`managed ${product} profile is missing for tenant ${id}`);
184
+ let iconExecutable;
185
+ try {
186
+ iconExecutable = product === "claude"
187
+ ? findClaudeApp(environment)
188
+ : findChatGPTApp(paths.root);
189
+ } catch (error) {
190
+ throw new Error(`could not resolve ${product} shortcut icon (${redactSecretText(error?.message || String(error))})`);
191
+ }
192
+ if (!iconExecutable || !path.win32.isAbsolute(iconExecutable)) {
193
+ throw new Error(`managed ${product} shortcut icon executable is unavailable for tenant ${id}`);
194
+ }
178
195
  registered.push({
179
196
  product,
180
- path: shortcut({ product, tenantId: id, tenantName, environment, run }),
197
+ path: shortcut({
198
+ product,
199
+ tenantId: id,
200
+ tenantName,
201
+ environment,
202
+ run,
203
+ iconPath: `${path.win32.normalize(iconExecutable)},0`,
204
+ }),
181
205
  status: "ready",
182
206
  });
183
207
  }