impel-cli 0.18.12 → 0.18.14-beta.0
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 +43 -2
- package/package.json +1 -1
- package/src/apps.js +41 -8
- package/src/commands/apps.js +23 -1
- package/src/commands/converge.js +20 -2
- package/src/commands/launch.js +49 -0
- package/src/commands/setup.js +60 -46
- package/src/commands/status.js +18 -3
- package/src/platformSetup.js +23 -3
- package/src/providerReadiness.js +122 -0
- package/src/provisioning.js +11 -0
- package/src/shellEntries.js +26 -2
- package/src/windowsCapabilities.js +334 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ impel update
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
-
|
|
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
|
|
|
@@ -103,6 +117,12 @@ impel claude
|
|
|
103
117
|
impel codex
|
|
104
118
|
```
|
|
105
119
|
|
|
120
|
+
Before spawning the vendor CLI, Impel verifies that the selected tenant's live
|
|
121
|
+
model catalogue has a healthy, routable provider pool. A short-lived successful
|
|
122
|
+
check is reused for one minute. If a provider is unavailable, the command stops
|
|
123
|
+
with an actionable gateway message instead of opening a client that cannot
|
|
124
|
+
complete inference.
|
|
125
|
+
|
|
106
126
|
Arguments after the command are passed to the vendor CLI:
|
|
107
127
|
|
|
108
128
|
```sh
|
|
@@ -122,6 +142,12 @@ Run one command for routine maintenance:
|
|
|
122
142
|
impel update
|
|
123
143
|
```
|
|
124
144
|
|
|
145
|
+
On Windows PowerShell, keep the command shim explicit:
|
|
146
|
+
|
|
147
|
+
```powershell
|
|
148
|
+
impel.cmd update
|
|
149
|
+
```
|
|
150
|
+
|
|
125
151
|
Update:
|
|
126
152
|
|
|
127
153
|
1. On macOS, verifies the Xcode Command Line Tools and opens Apple's installer
|
|
@@ -255,6 +281,21 @@ compatibility for one release, but no command can enable native routing.
|
|
|
255
281
|
### Windows
|
|
256
282
|
|
|
257
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 and Code remain available through the per-user managed profile.
|
|
290
|
+
Cowork is enabled only when a read-only check confirms Virtual Machine
|
|
291
|
+
Platform, the HCS services and `vfpext` driver, nested virtualization when
|
|
292
|
+
applicable, and machine-wide provisioning of the exact pinned Claude MSIX.
|
|
293
|
+
- When those Cowork prerequisites are missing or cannot be read, Impel hides
|
|
294
|
+
the Cowork tab and reports the reason and remediation in setup, update, and
|
|
295
|
+
status. Impel never enables Windows features, restarts Windows, provisions a
|
|
296
|
+
machine-wide package, or elevates itself.
|
|
297
|
+
- Machine-wide deployment requirements are documented by Anthropic in
|
|
298
|
+
[Deploy Claude Desktop for Windows](https://support.claude.com/en/articles/12622703-deploy-claude-desktop-for-windows).
|
|
258
299
|
- Claude Desktop is downloaded from Anthropic as the exact x64 or arm64 MSIX
|
|
259
300
|
pinned by this CLI. Its SHA-256, Authenticode signature, package identity,
|
|
260
301
|
version, architecture, and required layout are checked before installation.
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -57,6 +57,7 @@ export const PINNED_VENDOR_APPS = Object.freeze({
|
|
|
57
57
|
packageName: "Claude",
|
|
58
58
|
packageVersion: "1.24012.9.0",
|
|
59
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
|
+
publisherId: "pzs8sxrjxfjjc",
|
|
60
61
|
executable: "app\\Claude.exe",
|
|
61
62
|
downloads: Object.freeze({
|
|
62
63
|
arm64: Object.freeze({
|
|
@@ -232,7 +233,9 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
232
233
|
// 22: refresh GPT-5.6 model windows from the pinned ChatGPT Codex catalog.
|
|
233
234
|
// 23: select Codex's unelevated native Windows sandbox for managed profiles so
|
|
234
235
|
// enterprise machines do not enter the administrator-only setup flow.
|
|
235
|
-
|
|
236
|
+
// 24: thread the inspected Windows Cowork host capability into Claude config
|
|
237
|
+
// and the tenant manifest so unsupported hosts keep Chat and Code usable.
|
|
238
|
+
export const CURRENT_CONFIG_VERSION = 24;
|
|
236
239
|
|
|
237
240
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
238
241
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -710,6 +713,7 @@ export function installManagedAppFiles({
|
|
|
710
713
|
writeTokenHelperFile = true,
|
|
711
714
|
claudeUserData = null,
|
|
712
715
|
chatgptInvocations = null,
|
|
716
|
+
windowsCoworkCapability = null,
|
|
713
717
|
generatedAt = new Date().toISOString(),
|
|
714
718
|
}) {
|
|
715
719
|
// true = rebuild every target's bundle; false = configs only; an array
|
|
@@ -768,7 +772,7 @@ export function installManagedAppFiles({
|
|
|
768
772
|
migrateLegacyClaudeAppSessions(paths.claude.userData, homeDir);
|
|
769
773
|
writeClaudeNative3PSelection(paths);
|
|
770
774
|
writeClaudeCodeSettings(paths);
|
|
771
|
-
writeClaudeConfig(paths, config, models);
|
|
775
|
+
writeClaudeConfig(paths, config, models, windowsCoworkCapability);
|
|
772
776
|
if (RUNTIME_BRAND.features.sessions) ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
|
|
773
777
|
}
|
|
774
778
|
else {
|
|
@@ -789,8 +793,33 @@ export function installManagedAppFiles({
|
|
|
789
793
|
});
|
|
790
794
|
}
|
|
791
795
|
|
|
796
|
+
const manifestCoworkCapability = windowsCoworkCapability
|
|
797
|
+
? {
|
|
798
|
+
schemaVersion: 1,
|
|
799
|
+
status: ["available", "unavailable", "unknown"].includes(windowsCoworkCapability.status)
|
|
800
|
+
? windowsCoworkCapability.status
|
|
801
|
+
: "unknown",
|
|
802
|
+
enabled: windowsCoworkCapability.enabled === true,
|
|
803
|
+
checks: {
|
|
804
|
+
virtualMachinePlatform: windowsCoworkCapability.checks?.virtualMachinePlatform ?? "unknown",
|
|
805
|
+
restartPending: windowsCoworkCapability.checks?.restartPending ?? null,
|
|
806
|
+
vmcompute: windowsCoworkCapability.checks?.vmcompute ?? "unknown",
|
|
807
|
+
hns: windowsCoworkCapability.checks?.hns ?? "unknown",
|
|
808
|
+
vfpext: windowsCoworkCapability.checks?.vfpext ?? "unknown",
|
|
809
|
+
machineProvisioned: windowsCoworkCapability.checks?.machineProvisioned ?? null,
|
|
810
|
+
virtualMachine: windowsCoworkCapability.checks?.virtualMachine ?? null,
|
|
811
|
+
nestedVirtualization: windowsCoworkCapability.checks?.nestedVirtualization ?? null,
|
|
812
|
+
},
|
|
813
|
+
reasonCodes: Array.isArray(windowsCoworkCapability.reasonCodes)
|
|
814
|
+
? [...windowsCoworkCapability.reasonCodes]
|
|
815
|
+
: ["inspector-malformed-output"],
|
|
816
|
+
remediation: Array.isArray(windowsCoworkCapability.remediation)
|
|
817
|
+
? [...windowsCoworkCapability.remediation]
|
|
818
|
+
: [],
|
|
819
|
+
}
|
|
820
|
+
: null;
|
|
792
821
|
writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
|
|
793
|
-
schemaVersion:
|
|
822
|
+
schemaVersion: 5,
|
|
794
823
|
configVersion: CURRENT_CONFIG_VERSION,
|
|
795
824
|
modelCatalogVersion: 3,
|
|
796
825
|
gatewayUrl: config.gatewayUrl,
|
|
@@ -803,6 +832,7 @@ export function installManagedAppFiles({
|
|
|
803
832
|
models,
|
|
804
833
|
config,
|
|
805
834
|
),
|
|
835
|
+
...(manifestCoworkCapability ? { capabilities: { claudeCowork: manifestCoworkCapability } } : {}),
|
|
806
836
|
models: models.map((model) => model.id),
|
|
807
837
|
updatedAt: generatedAt,
|
|
808
838
|
}, null, 2) + "\n", 0o600);
|
|
@@ -1029,7 +1059,7 @@ export function removeManagedApps(targets, {
|
|
|
1029
1059
|
}
|
|
1030
1060
|
}
|
|
1031
1061
|
|
|
1032
|
-
function writeClaudeConfig(paths, config, models) {
|
|
1062
|
+
function writeClaudeConfig(paths, config, models, windowsCoworkCapability = null) {
|
|
1033
1063
|
const dir = path.join(paths.claude.userData, "configLibrary");
|
|
1034
1064
|
const experimental = crossAppModelsEnabled(config);
|
|
1035
1065
|
const claudeModels = experimental
|
|
@@ -1044,10 +1074,13 @@ function writeClaudeConfig(paths, config, models) {
|
|
|
1044
1074
|
inferenceGatewayAuthScheme: "bearer",
|
|
1045
1075
|
modelDiscoveryEnabled: false,
|
|
1046
1076
|
inferenceModels: claudeModels,
|
|
1047
|
-
// These are Anthropic's native managed 3P surface controls.
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
|
|
1077
|
+
// These are Anthropic's native managed 3P surface controls. Windows
|
|
1078
|
+
// enables Cowork only after a separate read-only host inspection proves
|
|
1079
|
+
// the VMP/HCS/machine-provisioning prerequisites. Other platforms and
|
|
1080
|
+
// legacy callers without a Windows observation retain the reviewed true.
|
|
1081
|
+
coworkTabEnabled: windowsCoworkCapability
|
|
1082
|
+
? windowsCoworkCapability.enabled === true
|
|
1083
|
+
: true,
|
|
1051
1084
|
isClaudeCodeForDesktopEnabled: true,
|
|
1052
1085
|
// Chat is the only opt-in surface in the pinned build.
|
|
1053
1086
|
chatTabEnabled: true,
|
package/src/commands/apps.js
CHANGED
|
@@ -60,6 +60,11 @@ import {
|
|
|
60
60
|
windowsManagedChatGPTRoot,
|
|
61
61
|
} from "../windowsApps.js";
|
|
62
62
|
import { RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
63
|
+
import {
|
|
64
|
+
formatWindowsClaudeCoworkCapability,
|
|
65
|
+
inspectWindowsClaudeCoworkCapability,
|
|
66
|
+
unknownWindowsClaudeCoworkCapability,
|
|
67
|
+
} from "../windowsCapabilities.js";
|
|
63
68
|
|
|
64
69
|
const CLAUDE_KEYCHAIN_NOTICE = "Claude Keychain: enter your Mac login password and choose Always Allow on the first prompt for this tenant; Allow is temporary.";
|
|
65
70
|
|
|
@@ -319,6 +324,7 @@ function configureWindowsApps(config, targets, vendorPaths, catalog, io) {
|
|
|
319
324
|
writeTokenHelperFile: false,
|
|
320
325
|
claudeUserData: userData,
|
|
321
326
|
chatgptInvocations: { auth, mcp },
|
|
327
|
+
windowsCoworkCapability: io.windowsCoworkCapability || null,
|
|
322
328
|
});
|
|
323
329
|
return { catalog, userData };
|
|
324
330
|
}
|
|
@@ -353,7 +359,10 @@ export async function reconcileWindowsTenantApps({
|
|
|
353
359
|
confirmVendorInstall = async () => true,
|
|
354
360
|
homeDir = os.homedir(),
|
|
355
361
|
environment = process.env,
|
|
362
|
+
windowsCoworkCapability = null,
|
|
356
363
|
} = {}, overrides = {}) {
|
|
364
|
+
const effectiveCoworkCapability = windowsCoworkCapability
|
|
365
|
+
|| unknownWindowsClaudeCoworkCapability();
|
|
357
366
|
const io = {
|
|
358
367
|
findClaudeApp: findWindowsClaudeApp,
|
|
359
368
|
ensureClaudeApp: ensureWindowsClaudeApp,
|
|
@@ -435,6 +444,7 @@ export async function reconcileWindowsTenantApps({
|
|
|
435
444
|
...io,
|
|
436
445
|
homeDir,
|
|
437
446
|
environment,
|
|
447
|
+
windowsCoworkCapability: effectiveCoworkCapability,
|
|
438
448
|
});
|
|
439
449
|
}
|
|
440
450
|
const agentTargets = [];
|
|
@@ -628,6 +638,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
628
638
|
syncSkills: syncSkillsSafe,
|
|
629
639
|
syncAgents: syncAgentProfilesSafe,
|
|
630
640
|
selectedConfig: selectedAppConfig,
|
|
641
|
+
inspectCoworkCapability: inspectWindowsClaudeCoworkCapability,
|
|
631
642
|
...overrides,
|
|
632
643
|
};
|
|
633
644
|
// Preserve the original Windows-Claude override names used by embedders and
|
|
@@ -656,6 +667,10 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
656
667
|
console.log(` profile: ${fs.existsSync(claudeConfigPath) ? paths.claude.userData : "not installed"}`);
|
|
657
668
|
console.log(` vendor: ${io.findClaudeApp(io.environment) || "not installed"}`);
|
|
658
669
|
console.log(" open: impel app open claude");
|
|
670
|
+
const manifest = localTenantId ? readTenantManifest(io.homeDir, localTenantId) : null;
|
|
671
|
+
for (const line of formatWindowsClaudeCoworkCapability(manifest?.capabilities?.claudeCowork)) {
|
|
672
|
+
console.log(` ${line}`);
|
|
673
|
+
}
|
|
659
674
|
}
|
|
660
675
|
if (targets.includes("chatgpt")) {
|
|
661
676
|
console.log("Impel ChatGPT:");
|
|
@@ -777,7 +792,14 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
777
792
|
managedChatGPT = io.stageChatGPTApp(vendorPaths.chatgpt, actionPaths.root);
|
|
778
793
|
}
|
|
779
794
|
|
|
780
|
-
const
|
|
795
|
+
const windowsCoworkCapability = io.inspectCoworkCapability({
|
|
796
|
+
environment: io.environment,
|
|
797
|
+
platform: "win32",
|
|
798
|
+
});
|
|
799
|
+
const { userData } = configureWindowsApps(config, actionTargets, vendorPaths, catalog, {
|
|
800
|
+
...io,
|
|
801
|
+
windowsCoworkCapability,
|
|
802
|
+
});
|
|
781
803
|
const tenantPaths = appPaths(io.homeDir, config.tenantId, {
|
|
782
804
|
claudeUserData: userData,
|
|
783
805
|
tenantName: config.tenantName,
|
package/src/commands/converge.js
CHANGED
|
@@ -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(`
|
|
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
|
|
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);
|
package/src/commands/launch.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
normalizeGatewayUrl,
|
|
13
13
|
redactSecretText,
|
|
14
14
|
resolveDefaultGateway,
|
|
15
|
+
saveConfig,
|
|
15
16
|
} from "../config.js";
|
|
16
17
|
import { impelClaudeBaseUrl, impelCrossAppClaudeBaseUrl } from "../claudeSetup.js";
|
|
17
18
|
import { withGitEnvironment } from "../skills.js";
|
|
@@ -22,6 +23,12 @@ import {
|
|
|
22
23
|
resolveNativeBinary,
|
|
23
24
|
} from "../nativeProcess.js";
|
|
24
25
|
import { RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
26
|
+
import {
|
|
27
|
+
cacheProviderReadiness,
|
|
28
|
+
cachedProviderReadiness,
|
|
29
|
+
probeGatewayProviderReadiness,
|
|
30
|
+
providerAvailable,
|
|
31
|
+
} from "../providerReadiness.js";
|
|
25
32
|
|
|
26
33
|
const CLAUDE_DIRECT_AUTH_ENV = [
|
|
27
34
|
"ANTHROPIC_API_KEY",
|
|
@@ -81,6 +88,32 @@ function childExitCode(code, signal) {
|
|
|
81
88
|
return Number.isInteger(signalNumber) ? 128 + signalNumber : 1;
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
function localInformationRequest(argv) {
|
|
92
|
+
return argv.length === 1 && ["--help", "-h", "--version", "-V", "-v", "help"].includes(argv[0]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function assertLiveProviderReadiness({ config, gatewayUrl, credential, tenantId, tool, crossAppModels }) {
|
|
96
|
+
let readiness = cachedProviderReadiness(config, tenantId);
|
|
97
|
+
if (!readiness) {
|
|
98
|
+
readiness = await probeGatewayProviderReadiness(gatewayUrl, credential, tenantId);
|
|
99
|
+
if (cacheProviderReadiness(config, tenantId, readiness)) saveConfig(config);
|
|
100
|
+
}
|
|
101
|
+
const label = tool === "claude" ? "Claude" : "Codex";
|
|
102
|
+
if (!readiness.reachable) {
|
|
103
|
+
throw new Error(`could not verify live ${label} availability for tenant "${tenantId}" (${readiness.error || "gateway unreachable"})`);
|
|
104
|
+
}
|
|
105
|
+
if (readiness.rejected) throw new Error(`gateway credential was rejected (HTTP ${readiness.status})`);
|
|
106
|
+
if (!readiness.healthy) {
|
|
107
|
+
throw new Error(`could not verify live ${label} availability for tenant "${tenantId}" (${readiness.error || `HTTP ${readiness.status}`})`);
|
|
108
|
+
}
|
|
109
|
+
if (!providerAvailable(tool, readiness.providers, { crossAppModels })) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`${label} is currently unavailable for tenant "${tenantId}": the gateway advertises no ${label} models. `
|
|
112
|
+
+ `Ask a gateway administrator to restore the ${label} provider pool, then rerun \`${RUNTIME_BRAND.cli.command} setup\`.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
84
117
|
function runNativeCli(tool, argv, environment) {
|
|
85
118
|
const binary = resolveNativeBinary(tool, environment);
|
|
86
119
|
let invocation;
|
|
@@ -141,6 +174,22 @@ export async function cmdLaunch(tool, argv) {
|
|
|
141
174
|
return;
|
|
142
175
|
}
|
|
143
176
|
const gatewayCredential = tenantCredential(config.pat, tenantId);
|
|
177
|
+
if (!localInformationRequest(argv)) {
|
|
178
|
+
try {
|
|
179
|
+
await assertLiveProviderReadiness({
|
|
180
|
+
config,
|
|
181
|
+
gatewayUrl,
|
|
182
|
+
credential: gatewayCredential,
|
|
183
|
+
tenantId,
|
|
184
|
+
tool,
|
|
185
|
+
crossAppModels,
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(`${RUNTIME_BRAND.cli.command} ${tool}: ${redactSecretText(error?.message || error)}`);
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
144
193
|
const environment = { ...process.env };
|
|
145
194
|
environment.IMPEL_TENANT_ID = tenantId;
|
|
146
195
|
let agentProfile;
|
package/src/commands/setup.js
CHANGED
|
@@ -23,6 +23,11 @@ import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
|
23
23
|
import { refreshUpdateCache, updateNoticeLine } from "../updates.js";
|
|
24
24
|
import { restoreNativeProfiles } from "./use.js";
|
|
25
25
|
import { brandedEnvironmentName, brandedText, RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
26
|
+
import {
|
|
27
|
+
cacheProviderReadiness,
|
|
28
|
+
probeGatewayProviderReadiness,
|
|
29
|
+
providerAvailable,
|
|
30
|
+
} from "../providerReadiness.js";
|
|
26
31
|
|
|
27
32
|
const HELP = brandedText(`impel setup - prepare every accessible Impel tenant
|
|
28
33
|
|
|
@@ -52,52 +57,33 @@ export function resolveTenantChoice(listing, { requested = null, answer = null,
|
|
|
52
57
|
return selectDefaultTenant(listing, { currentTenantId });
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
async function probeGatewayOnce(gatewayUrl, pat, tenantId, fetchImpl, timeoutMs) {
|
|
56
|
-
const controller = new AbortController();
|
|
57
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
58
|
-
try {
|
|
59
|
-
const response = await fetchImpl(`${gatewayUrl}/v1/models`, {
|
|
60
|
-
headers: {
|
|
61
|
-
accept: "application/json",
|
|
62
|
-
authorization: `Bearer ${tenantCredential(pat, tenantId)}`,
|
|
63
|
-
},
|
|
64
|
-
signal: controller.signal,
|
|
65
|
-
});
|
|
66
|
-
return {
|
|
67
|
-
reachable: true,
|
|
68
|
-
healthy: response.ok,
|
|
69
|
-
status: response.status,
|
|
70
|
-
rejected: [401, 403].includes(response.status),
|
|
71
|
-
};
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return {
|
|
74
|
-
reachable: false,
|
|
75
|
-
error: error?.name === "AbortError"
|
|
76
|
-
? `timed out after ${Math.round(timeoutMs / 1000)}s`
|
|
77
|
-
: redactSecretText(error?.message || error),
|
|
78
|
-
};
|
|
79
|
-
} finally {
|
|
80
|
-
clearTimeout(timeout);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// A single short probe misreports a cold gateway (serverless cold start plus
|
|
85
|
-
// long-haul latency) as unreachable, and that transient verdict gets baked
|
|
86
|
-
// into the convergence failure message. Retry once with a longer deadline
|
|
87
|
-
// before concluding the gateway is down.
|
|
88
60
|
export async function probeGateway(gatewayUrl, pat, tenantId, fetchImpl = fetch, timeoutsMs = [5_000, 10_000]) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
61
|
+
return probeGatewayProviderReadiness(
|
|
62
|
+
gatewayUrl,
|
|
63
|
+
tenantCredential(pat, tenantId),
|
|
64
|
+
tenantId,
|
|
65
|
+
fetchImpl,
|
|
66
|
+
timeoutsMs,
|
|
67
|
+
);
|
|
95
68
|
}
|
|
96
69
|
|
|
97
|
-
function markProbeFailures(report, probes) {
|
|
70
|
+
function markProbeFailures(report, probes, { crossAppModels = false } = {}) {
|
|
98
71
|
probes.forEach((probe, index) => {
|
|
99
|
-
if (probe.reachable && !probe.rejected && probe.healthy !== false) return;
|
|
100
72
|
const tenant = report.tenants[index];
|
|
73
|
+
if (probe.reachable && !probe.rejected && probe.healthy !== false) {
|
|
74
|
+
if (!Array.isArray(probe.providers)) return;
|
|
75
|
+
for (const client of ["claude", "codex"]) {
|
|
76
|
+
if (tenant.clients[client].cli !== "ready") continue;
|
|
77
|
+
if (!providerAvailable(client, probe.providers, { crossAppModels })) {
|
|
78
|
+
tenant.clients[client].cli = "unavailable";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const states = [tenant.clients.claude.cli, tenant.clients.codex.cli];
|
|
82
|
+
tenant.cli = states.includes("failed")
|
|
83
|
+
? "failed"
|
|
84
|
+
: states.includes("ready") ? "ready" : "unavailable";
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
101
87
|
tenant.cli = "failed";
|
|
102
88
|
tenant.status = "failed";
|
|
103
89
|
for (const client of Object.values(tenant.clients)) {
|
|
@@ -267,6 +253,7 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
267
253
|
return allowed;
|
|
268
254
|
};
|
|
269
255
|
|
|
256
|
+
const cliInstallDecisions = new Map();
|
|
270
257
|
let sharedFailure = null;
|
|
271
258
|
const prepareSharedClis = async ({ confirmedTools = null, inspectOnly = false } = {}) => {
|
|
272
259
|
try {
|
|
@@ -285,11 +272,20 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
285
272
|
for (const tool of inspected.missingAfter) {
|
|
286
273
|
if (!inspected.installCommands?.[tool]) continue;
|
|
287
274
|
const label = tool === "claude" ? "Claude Code" : "Codex";
|
|
288
|
-
if (confirmed(await io.promptText(`
|
|
275
|
+
if (confirmed(await io.promptText(`Impel will download, verify, and install the official ${label} CLI for the current user. Continue? [y/N] `))) {
|
|
289
276
|
installTools.push(tool);
|
|
277
|
+
cliInstallDecisions.set(tool, "approved");
|
|
278
|
+
} else {
|
|
279
|
+
cliInstallDecisions.set(tool, "declined");
|
|
290
280
|
}
|
|
291
281
|
}
|
|
282
|
+
} else {
|
|
283
|
+
for (const tool of inspected.missingAfter) {
|
|
284
|
+
if (inspected.installCommands?.[tool]) cliInstallDecisions.set(tool, "confirmation-required");
|
|
285
|
+
}
|
|
292
286
|
}
|
|
287
|
+
} else {
|
|
288
|
+
for (const tool of installTools) cliInstallDecisions.set(tool, "approved");
|
|
293
289
|
}
|
|
294
290
|
if (installTools.length) {
|
|
295
291
|
prepared = await io.preparePlatformClis({
|
|
@@ -302,7 +298,13 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
302
298
|
}
|
|
303
299
|
}
|
|
304
300
|
if (prepared.missingAfter.length) {
|
|
305
|
-
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
|
+
});
|
|
306
308
|
return prepared;
|
|
307
309
|
}
|
|
308
310
|
sharedFailure = null;
|
|
@@ -344,7 +346,12 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
344
346
|
) => {
|
|
345
347
|
const nextReport = await runConvergence(includeApps, tenants, confirmInstall);
|
|
346
348
|
const probes = await Promise.all(tenants.map((tenant) => io.probe(gatewayUrl, pat, tenant.id)));
|
|
347
|
-
|
|
349
|
+
let cacheChanged = false;
|
|
350
|
+
probes.forEach((probe, index) => {
|
|
351
|
+
cacheChanged = cacheProviderReadiness(config, tenants[index].id, probe) || cacheChanged;
|
|
352
|
+
});
|
|
353
|
+
if (cacheChanged) io.saveConfig(config);
|
|
354
|
+
return markProbeFailures(nextReport, probes, { crossAppModels: config.experimental?.crossAppModels === true });
|
|
348
355
|
};
|
|
349
356
|
let report = await verifyConvergence();
|
|
350
357
|
if (sharedFailure) report.passed = false;
|
|
@@ -476,10 +483,17 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
476
483
|
printReconciliationSummary(report);
|
|
477
484
|
console.log(`CLI tenant: ${selected.id}`);
|
|
478
485
|
if (!RUNTIME_BRAND.tenant.defaultId) console.log("Change it: impel tenant use <tenant>");
|
|
479
|
-
|
|
486
|
+
const selectedReport = report.tenants.find((tenant) => tenant.tenantId === selected.id);
|
|
487
|
+
const launchable = ["claude", "codex"]
|
|
488
|
+
.filter((client) => selectedReport?.clients?.[client]?.cli === "ready")
|
|
489
|
+
.map((client) => `${RUNTIME_BRAND.cli.command} ${client}`);
|
|
490
|
+
console.log(launchable.length
|
|
491
|
+
? `Launch: ${launchable.join(" | ")}`
|
|
492
|
+
: "Launch: no provider CLI is currently available for this tenant");
|
|
480
493
|
if (!report.passed || sharedFailure) {
|
|
481
494
|
if (sharedFailure) console.error(`Shared setup failure: ${sharedFailure}`);
|
|
482
|
-
|
|
495
|
+
const setupCommand = io.platform === "win32" ? "impel.cmd setup" : "impel setup";
|
|
496
|
+
console.error(`Setup is incomplete. Fix the reported issue or rerun \`${setupCommand}\`.`);
|
|
483
497
|
process.exitCode = 1;
|
|
484
498
|
return report;
|
|
485
499
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { appPaths, CLAUDE_CONFIG_ID, readTenantManifest } from "../apps.js";
|
|
6
|
-
import { loadConfig, maskSecret } from "../config.js";
|
|
6
|
+
import { crossAppModelsEnabled, loadConfig, maskSecret } from "../config.js";
|
|
7
7
|
import { environmentValue, findNativeBinary } from "../nativeProcess.js";
|
|
8
8
|
import { tenantCliClientReadiness } from "../provisioning.js";
|
|
9
9
|
import { windowsTenantShortcutName } from "../shellEntries.js";
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { installedVersion, maybePrintUpdateNotice } from "../updates.js";
|
|
17
17
|
import { windowsClaudeUserData } from "../windowsApps.js";
|
|
18
18
|
import { RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
19
|
+
import { cachedProviderReadiness, providerAvailable } from "../providerReadiness.js";
|
|
20
|
+
import { formatWindowsClaudeCoworkCapability } from "../windowsCapabilities.js";
|
|
19
21
|
|
|
20
22
|
function readShellManifest(paths) {
|
|
21
23
|
try {
|
|
@@ -26,7 +28,7 @@ function readShellManifest(paths) {
|
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
function desktopReadiness(tenant, {
|
|
31
|
+
export function desktopReadiness(tenant, {
|
|
30
32
|
platform = process.platform,
|
|
31
33
|
homeDir = os.homedir(),
|
|
32
34
|
environment = process.env,
|
|
@@ -36,6 +38,7 @@ function desktopReadiness(tenant, {
|
|
|
36
38
|
return {
|
|
37
39
|
claude: { app: "unavailable", shell: "unavailable" },
|
|
38
40
|
codex: { app: "unavailable", shell: "unavailable" },
|
|
41
|
+
cowork: null,
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
const claudeUserData = platform === "win32" ? windowsClaudeUserData(environment, tenant.id) : null;
|
|
@@ -69,11 +72,13 @@ function desktopReadiness(tenant, {
|
|
|
69
72
|
shell: shellReady ? "ready" : "missing",
|
|
70
73
|
};
|
|
71
74
|
}
|
|
75
|
+
states.cowork = manifest?.capabilities?.claudeCowork || null;
|
|
72
76
|
return states;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
function cliState({ supported, profileReady, binaryReady }) {
|
|
79
|
+
function cliState({ supported, available = true, profileReady, binaryReady }) {
|
|
76
80
|
if (!supported) return "unsupported";
|
|
81
|
+
if (!available) return "unavailable";
|
|
77
82
|
return profileReady && binaryReady ? "ready" : "missing";
|
|
78
83
|
}
|
|
79
84
|
|
|
@@ -123,27 +128,37 @@ export async function cmdStatus(overrides = {}) {
|
|
|
123
128
|
};
|
|
124
129
|
console.log("Tenant readiness:");
|
|
125
130
|
let incomplete = false;
|
|
131
|
+
let currentCowork = null;
|
|
132
|
+
let fallbackCowork = null;
|
|
126
133
|
for (const tenant of [...selected.tenants].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
127
134
|
const profiles = io.cliReadiness(tenant.id);
|
|
128
135
|
const desktop = io.desktopReadiness(tenant, overrides);
|
|
136
|
+
const providerReadiness = cachedProviderReadiness(config, tenant.id);
|
|
129
137
|
const claudeCli = cliState({
|
|
130
138
|
supported: !scopeKnown || scopes.has(PAT_SCOPE_CLAUDE),
|
|
139
|
+
available: !providerReadiness || providerAvailable("claude", providerReadiness.providers, {
|
|
140
|
+
crossAppModels: crossAppModelsEnabled(config),
|
|
141
|
+
}),
|
|
131
142
|
profileReady: profiles.claude,
|
|
132
143
|
binaryReady: binaries.claude,
|
|
133
144
|
});
|
|
134
145
|
const codexCli = cliState({
|
|
135
146
|
supported: !scopeKnown || scopes.has(PAT_SCOPE_CODEX),
|
|
147
|
+
available: !providerReadiness || providerAvailable("codex", providerReadiness.providers),
|
|
136
148
|
profileReady: profiles.codex,
|
|
137
149
|
binaryReady: binaries.codex,
|
|
138
150
|
});
|
|
139
151
|
const states = [claudeCli, codexCli, desktop.claude.app, desktop.codex.app, desktop.claude.shell, desktop.codex.shell];
|
|
140
152
|
if (states.includes("missing")) incomplete = true;
|
|
153
|
+
fallbackCowork ||= desktop.cowork || null;
|
|
154
|
+
if (tenant.id === selected.tenantId) currentCowork = desktop.cowork || null;
|
|
141
155
|
console.log(
|
|
142
156
|
` ${tenant.id}${tenant.id === selected.tenantId ? " (current)" : ""}: `
|
|
143
157
|
+ `Claude CLI ${claudeCli}, Codex CLI ${codexCli}, `
|
|
144
158
|
+ `Claude app ${desktop.claude.app}, ChatGPT app ${desktop.codex.app}`,
|
|
145
159
|
);
|
|
146
160
|
}
|
|
161
|
+
for (const line of formatWindowsClaudeCoworkCapability(currentCowork || fallbackCowork)) console.log(line);
|
|
147
162
|
if (incomplete) {
|
|
148
163
|
console.log(RUNTIME_BRAND.tenant.defaultId
|
|
149
164
|
? `Repair or finish missing tenant surfaces with: ${RUNTIME_BRAND.cli.command} setup`
|
package/src/platformSetup.js
CHANGED
|
@@ -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
|
-
:
|
|
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}
|
|
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
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { redactSecretText } from "./config.js";
|
|
2
|
+
|
|
3
|
+
const PROVIDERS = new Set(["claude", "codex"]);
|
|
4
|
+
const POOL_STATES = new Set(["healthy", "exhausted", "rate_limited", "expired", "no_seat"]);
|
|
5
|
+
export const PROVIDER_READINESS_CACHE_MS = 60_000;
|
|
6
|
+
|
|
7
|
+
function providersFromPayload(payload) {
|
|
8
|
+
if (!Array.isArray(payload?.data)) return { error: "gateway model catalog has no model data array" };
|
|
9
|
+
const statuses = {};
|
|
10
|
+
if (Object.hasOwn(payload, "provider_status")) {
|
|
11
|
+
const raw = payload.provider_status;
|
|
12
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
13
|
+
return { error: "gateway model catalog has malformed provider status metadata" };
|
|
14
|
+
}
|
|
15
|
+
for (const provider of PROVIDERS) {
|
|
16
|
+
if (!Object.hasOwn(raw, provider)) continue;
|
|
17
|
+
const status = raw[provider];
|
|
18
|
+
if (!status
|
|
19
|
+
|| typeof status !== "object"
|
|
20
|
+
|| Array.isArray(status)
|
|
21
|
+
|| !POOL_STATES.has(status.state)
|
|
22
|
+
|| typeof status.routable !== "boolean") {
|
|
23
|
+
return { error: "gateway model catalog has malformed provider status metadata" };
|
|
24
|
+
}
|
|
25
|
+
statuses[provider] = status;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const providers = [...new Set(payload.data
|
|
29
|
+
.filter((model) => model?.available !== false && model?.ready !== false && model?.readiness?.ready !== false)
|
|
30
|
+
.map((model) => model?.provider)
|
|
31
|
+
.filter((provider) => PROVIDERS.has(provider))
|
|
32
|
+
.filter((provider) => !statuses[provider]
|
|
33
|
+
|| (statuses[provider].state === "healthy" && statuses[provider].routable === true)))]
|
|
34
|
+
.sort();
|
|
35
|
+
return { providers };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function probeGatewayOnce(gatewayUrl, credential, tenantId, fetchImpl, timeoutMs) {
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetchImpl(`${gatewayUrl}/v1/models`, {
|
|
43
|
+
headers: {
|
|
44
|
+
accept: "application/json",
|
|
45
|
+
authorization: `Bearer ${credential}`,
|
|
46
|
+
},
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
});
|
|
49
|
+
const result = {
|
|
50
|
+
reachable: true,
|
|
51
|
+
healthy: response.ok,
|
|
52
|
+
status: response.status,
|
|
53
|
+
rejected: [401, 403].includes(response.status),
|
|
54
|
+
};
|
|
55
|
+
if (!response.ok || typeof response.json !== "function") return result;
|
|
56
|
+
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
payload = await response.json();
|
|
60
|
+
} catch {
|
|
61
|
+
return { ...result, healthy: false, error: "gateway model catalog was not valid JSON" };
|
|
62
|
+
}
|
|
63
|
+
const catalog = providersFromPayload(payload);
|
|
64
|
+
if (catalog.error) {
|
|
65
|
+
return { ...result, healthy: false, error: catalog.error };
|
|
66
|
+
}
|
|
67
|
+
if (payload.org_id && payload.org_id !== tenantId) {
|
|
68
|
+
return { ...result, healthy: false, error: "gateway model catalog tenant did not match the selected tenant" };
|
|
69
|
+
}
|
|
70
|
+
return { ...result, providers: catalog.providers };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return {
|
|
73
|
+
reachable: false,
|
|
74
|
+
error: error?.name === "AbortError"
|
|
75
|
+
? `timed out after ${Math.round(timeoutMs / 1000)}s`
|
|
76
|
+
: redactSecretText(error?.message || error),
|
|
77
|
+
};
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Retry one cold/unreachable gateway probe with a longer deadline, but never
|
|
84
|
+
// retry an authoritative HTTP or model-catalog response.
|
|
85
|
+
export async function probeGatewayProviderReadiness(
|
|
86
|
+
gatewayUrl,
|
|
87
|
+
credential,
|
|
88
|
+
tenantId,
|
|
89
|
+
fetchImpl = fetch,
|
|
90
|
+
timeoutsMs = [5_000, 10_000],
|
|
91
|
+
) {
|
|
92
|
+
let probe = { reachable: false, error: "not probed" };
|
|
93
|
+
for (const timeoutMs of timeoutsMs) {
|
|
94
|
+
probe = await probeGatewayOnce(gatewayUrl, credential, tenantId, fetchImpl, timeoutMs);
|
|
95
|
+
if (probe.reachable) return probe;
|
|
96
|
+
}
|
|
97
|
+
return probe;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function cacheProviderReadiness(config, tenantId, probe, checkedAt = new Date()) {
|
|
101
|
+
if (!probe?.healthy || !Array.isArray(probe.providers)) return false;
|
|
102
|
+
config.providerReadiness ||= {};
|
|
103
|
+
config.providerReadiness[tenantId] = {
|
|
104
|
+
checkedAt: checkedAt.toISOString(),
|
|
105
|
+
providers: [...new Set(probe.providers.filter((provider) => PROVIDERS.has(provider)))].sort(),
|
|
106
|
+
};
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function cachedProviderReadiness(config, tenantId, now = Date.now()) {
|
|
111
|
+
const record = config?.providerReadiness?.[tenantId];
|
|
112
|
+
const checkedAt = Date.parse(record?.checkedAt || "");
|
|
113
|
+
if (!Number.isFinite(checkedAt) || checkedAt > now || now - checkedAt > PROVIDER_READINESS_CACHE_MS) return null;
|
|
114
|
+
if (!Array.isArray(record.providers) || record.providers.some((provider) => !PROVIDERS.has(provider))) return null;
|
|
115
|
+
return { healthy: true, reachable: true, providers: [...new Set(record.providers)].sort(), cached: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function providerAvailable(tool, providers, { crossAppModels = false } = {}) {
|
|
119
|
+
if (!Array.isArray(providers)) return false;
|
|
120
|
+
if (tool === "claude" && crossAppModels) return providers.length > 0;
|
|
121
|
+
return providers.includes(tool);
|
|
122
|
+
}
|
package/src/provisioning.js
CHANGED
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
} from "./tenants.js";
|
|
18
18
|
import { reconcileTenantApps } from "./commands/apps.js";
|
|
19
19
|
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
20
|
+
import {
|
|
21
|
+
formatWindowsClaudeCoworkCapability,
|
|
22
|
+
inspectWindowsClaudeCoworkCapability,
|
|
23
|
+
} from "./windowsCapabilities.js";
|
|
20
24
|
|
|
21
25
|
export function selectDefaultTenant(listing, { requested = null, currentTenantId = null } = {}) {
|
|
22
26
|
const normalized = requested ? normalizeTenantId(requested) : null;
|
|
@@ -185,6 +189,7 @@ export async function reconcileAllTenants({
|
|
|
185
189
|
reconcileApps: reconcileTenantApps,
|
|
186
190
|
registerShellEntries: registerTenantShellEntries,
|
|
187
191
|
knownTenantIds: locallyKnownTenantIds,
|
|
192
|
+
inspectCoworkCapability: inspectWindowsClaudeCoworkCapability,
|
|
188
193
|
...overrides,
|
|
189
194
|
};
|
|
190
195
|
// Dependency-injection compatibility for older embedders/tests. Production
|
|
@@ -208,6 +213,9 @@ export async function reconcileAllTenants({
|
|
|
208
213
|
claude: io.findBinary("claude", environment, platform),
|
|
209
214
|
codex: io.findBinary("codex", environment, platform),
|
|
210
215
|
};
|
|
216
|
+
const windowsCoworkCapability = includeApps && platform === "win32"
|
|
217
|
+
? io.inspectCoworkCapability({ environment, platform })
|
|
218
|
+
: null;
|
|
211
219
|
const results = new Map(liveTenants.map((tenant) => [tenant.id, {
|
|
212
220
|
tenantId: tenant.id,
|
|
213
221
|
tenantName: tenant.name,
|
|
@@ -262,6 +270,7 @@ export async function reconcileAllTenants({
|
|
|
262
270
|
homeDir,
|
|
263
271
|
environment,
|
|
264
272
|
confirmVendorInstall,
|
|
273
|
+
windowsCoworkCapability,
|
|
265
274
|
}, overrides.appOverrides || {});
|
|
266
275
|
supported = new Set(installed.targets || []);
|
|
267
276
|
const failedApps = installed.failed || [];
|
|
@@ -333,6 +342,7 @@ export async function reconcileAllTenants({
|
|
|
333
342
|
selectedTenantId: config.tenantId,
|
|
334
343
|
tenants: values,
|
|
335
344
|
inaccessible,
|
|
345
|
+
...(windowsCoworkCapability ? { capabilities: { claudeCowork: windowsCoworkCapability } } : {}),
|
|
336
346
|
passed: values.every((result) => (
|
|
337
347
|
(result.cli === "ready" || result.cli === "unavailable")
|
|
338
348
|
&& (result.apps === "ready" || result.apps === "unavailable")
|
|
@@ -366,6 +376,7 @@ export function printReconciliationSummary(report, log = console.log) {
|
|
|
366
376
|
if (report.inaccessible.length) {
|
|
367
377
|
log(` Retained local data for inaccessible tenants: ${report.inaccessible.join(", ")}`);
|
|
368
378
|
}
|
|
379
|
+
for (const line of formatWindowsClaudeCoworkCapability(report.capabilities?.claudeCowork)) log(line);
|
|
369
380
|
}
|
|
370
381
|
|
|
371
382
|
export function tenantCliPathsReady(tenantId, existsSync = fs.existsSync) {
|
package/src/shellEntries.js
CHANGED
|
@@ -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 {
|
|
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({
|
|
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
|
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
|
|
4
|
+
import { PINNED_VENDOR_APPS } from "./apps.js";
|
|
5
|
+
import { environmentValue, nativeCommandInvocation } from "./nativeProcess.js";
|
|
6
|
+
|
|
7
|
+
export const WINDOWS_COWORK_REMEDIATION = Object.freeze({
|
|
8
|
+
enableVirtualMachinePlatform: "Enable the Windows Virtual Machine Platform optional feature, then restart Windows.",
|
|
9
|
+
restartWindows: "Restart Windows to finish Virtual Machine Platform and service provisioning.",
|
|
10
|
+
restoreHcs: "Ask an administrator or endpoint manager to restore the vmcompute, hns, and vfpext Windows components.",
|
|
11
|
+
provisionClaude: "Ask an administrator or endpoint manager to machine-provision the exact pinned Claude MSIX with Add-AppxProvisionedPackage.",
|
|
12
|
+
enableNestedVirtualization: "Enable nested virtualization for this VM, or use a physical Windows host for Claude Cowork.",
|
|
13
|
+
inspectHost: "Ask an administrator to verify Virtual Machine Platform, HCS services, vfpext, and machine-wide Claude provisioning.",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const WINDOWS_PACKAGE_BASE32 = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
17
|
+
|
|
18
|
+
/** Derive the stable Package Family Name publisher id from the exact DN. */
|
|
19
|
+
export function windowsPackagePublisherId(publisher) {
|
|
20
|
+
const hash = crypto.createHash("sha256").update(Buffer.from(String(publisher), "utf16le")).digest();
|
|
21
|
+
let bits = "";
|
|
22
|
+
for (const byte of hash.subarray(0, 8)) bits += byte.toString(2).padStart(8, "0");
|
|
23
|
+
bits += "0";
|
|
24
|
+
let publisherId = "";
|
|
25
|
+
for (let offset = 0; offset < 65; offset += 5) {
|
|
26
|
+
publisherId += WINDOWS_PACKAGE_BASE32[Number.parseInt(bits.slice(offset, offset + 5), 2)];
|
|
27
|
+
}
|
|
28
|
+
return publisherId;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function windowsPackageFullName({ name, version, architecture, publisherId }) {
|
|
32
|
+
return `${name}_${version}_${String(architecture).toLowerCase()}__${publisherId}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function windowsProvisionedPackageIdentityMatches(candidate, expected) {
|
|
36
|
+
if (!candidate || !expected) return false;
|
|
37
|
+
const expectedFullName = windowsPackageFullName(expected);
|
|
38
|
+
return candidate.displayName === expected.name
|
|
39
|
+
&& candidate.version === expected.version
|
|
40
|
+
&& String(candidate.architecture).toLowerCase() === String(expected.architecture).toLowerCase()
|
|
41
|
+
&& candidate.packageName === expectedFullName;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fixed, read-only host observation. Expected package metadata is passed only
|
|
45
|
+
// through fixed environment keys; no tenant, user, or path is interpolated
|
|
46
|
+
// into PowerShell source.
|
|
47
|
+
export const WINDOWS_COWORK_INSPECTION_SCRIPT = String.raw`
|
|
48
|
+
$ErrorActionPreference = 'Stop'
|
|
49
|
+
function Read-ServiceState([string]$Name) {
|
|
50
|
+
try {
|
|
51
|
+
$service = Get-Service -Name $Name -ErrorAction SilentlyContinue
|
|
52
|
+
if ($null -eq $service) { return 'missing' }
|
|
53
|
+
if ($service.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Running) { return 'present-running' }
|
|
54
|
+
return 'present-stopped'
|
|
55
|
+
} catch { return 'unknown' }
|
|
56
|
+
}
|
|
57
|
+
function Read-DriverState([string]$Name) {
|
|
58
|
+
try {
|
|
59
|
+
$driver = Get-CimInstance -ClassName Win32_SystemDriver -Filter "Name='$Name'" -ErrorAction SilentlyContinue
|
|
60
|
+
if ($null -eq $driver) { return 'missing' }
|
|
61
|
+
if ($driver.State -eq 'Running') { return 'present-running' }
|
|
62
|
+
return 'present-stopped'
|
|
63
|
+
} catch { return 'unknown' }
|
|
64
|
+
}
|
|
65
|
+
$vmp = 'unknown'
|
|
66
|
+
try {
|
|
67
|
+
$feature = Get-CimInstance -ClassName Win32_OptionalFeature -Filter "Name='VirtualMachinePlatform'" -ErrorAction Stop
|
|
68
|
+
if ($null -eq $feature) { $vmp = 'absent' }
|
|
69
|
+
elseif ($feature.InstallState -eq 1) { $vmp = 'enabled' }
|
|
70
|
+
elseif ($feature.InstallState -eq 2) { $vmp = 'disabled' }
|
|
71
|
+
elseif ($feature.InstallState -eq 3) { $vmp = 'absent' }
|
|
72
|
+
} catch { $vmp = 'unknown' }
|
|
73
|
+
$restartPending = $null
|
|
74
|
+
try {
|
|
75
|
+
$cbs = Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
|
|
76
|
+
$wu = Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
|
|
77
|
+
$restartPending = [bool]($cbs -or $wu)
|
|
78
|
+
} catch { $restartPending = $null }
|
|
79
|
+
$machineProvisioned = $null
|
|
80
|
+
try {
|
|
81
|
+
$expectedName = $env:IMPEL_COWORK_EXPECTED_PACKAGE_NAME
|
|
82
|
+
$expectedVersion = $env:IMPEL_COWORK_EXPECTED_PACKAGE_VERSION
|
|
83
|
+
$expectedArchitecture = $env:IMPEL_COWORK_EXPECTED_PACKAGE_ARCHITECTURE
|
|
84
|
+
$expectedPublisherId = $env:IMPEL_COWORK_EXPECTED_PACKAGE_PUBLISHER_ID
|
|
85
|
+
$expectedPackageName = $expectedName + '_' + $expectedVersion + '_' + $expectedArchitecture.ToLowerInvariant() + '__' + $expectedPublisherId
|
|
86
|
+
$packages = Get-AppxProvisionedPackage -Online -ErrorAction Stop
|
|
87
|
+
$exact = $packages | Where-Object {
|
|
88
|
+
$displayMatches = ([string]$_.DisplayName -ceq $expectedName)
|
|
89
|
+
$versionMatches = ([string]$_.Version -ceq $expectedVersion)
|
|
90
|
+
$architectureMatches = ([string]$_.Architecture -ieq $expectedArchitecture)
|
|
91
|
+
$packageNameMatches = ([string]$_.PackageName -ceq $expectedPackageName)
|
|
92
|
+
$displayMatches -and $versionMatches -and $architectureMatches -and $packageNameMatches
|
|
93
|
+
} | Select-Object -First 1
|
|
94
|
+
$machineProvisioned = [bool]($null -ne $exact)
|
|
95
|
+
} catch { $machineProvisioned = $null }
|
|
96
|
+
$virtualMachine = $null
|
|
97
|
+
$nestedVirtualization = $null
|
|
98
|
+
try {
|
|
99
|
+
$computer = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
|
|
100
|
+
$identity = (([string]$computer.Manufacturer) + ' ' + ([string]$computer.Model))
|
|
101
|
+
$virtualMachine = [bool]($identity -match 'Virtual|VMware|KVM|HVM|Hyper-V|Parallels|Xen')
|
|
102
|
+
if ($virtualMachine) {
|
|
103
|
+
$processors = @(Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop)
|
|
104
|
+
$nestedVirtualization = [bool]($processors.Count -gt 0 -and ($processors | Where-Object {
|
|
105
|
+
-not ($_.VirtualizationFirmwareEnabled -and $_.VMMonitorModeExtensions)
|
|
106
|
+
}).Count -eq 0)
|
|
107
|
+
} else {
|
|
108
|
+
$nestedVirtualization = $true
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
$virtualMachine = $null
|
|
112
|
+
$nestedVirtualization = $null
|
|
113
|
+
}
|
|
114
|
+
[ordered]@{
|
|
115
|
+
schemaVersion = 1
|
|
116
|
+
virtualMachinePlatform = $vmp
|
|
117
|
+
restartPending = $restartPending
|
|
118
|
+
vmcompute = Read-ServiceState 'vmcompute'
|
|
119
|
+
hns = Read-ServiceState 'hns'
|
|
120
|
+
vfpext = Read-DriverState 'vfpext'
|
|
121
|
+
machineProvisioned = $machineProvisioned
|
|
122
|
+
virtualMachine = $virtualMachine
|
|
123
|
+
nestedVirtualization = $nestedVirtualization
|
|
124
|
+
} | ConvertTo-Json -Compress -Depth 3
|
|
125
|
+
`.trim();
|
|
126
|
+
|
|
127
|
+
const VMP_STATES = new Set(["enabled", "disabled", "absent", "unknown"]);
|
|
128
|
+
const COMPONENT_STATES = new Set(["present-running", "present-stopped", "missing", "unknown"]);
|
|
129
|
+
|
|
130
|
+
function emptyChecks() {
|
|
131
|
+
return {
|
|
132
|
+
virtualMachinePlatform: "unknown",
|
|
133
|
+
restartPending: null,
|
|
134
|
+
vmcompute: "unknown",
|
|
135
|
+
hns: "unknown",
|
|
136
|
+
vfpext: "unknown",
|
|
137
|
+
machineProvisioned: null,
|
|
138
|
+
virtualMachine: null,
|
|
139
|
+
nestedVirtualization: null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizedFailure(reasonCode) {
|
|
144
|
+
return {
|
|
145
|
+
schemaVersion: 1,
|
|
146
|
+
status: "unknown",
|
|
147
|
+
enabled: false,
|
|
148
|
+
checks: emptyChecks(),
|
|
149
|
+
reasonCodes: [reasonCode],
|
|
150
|
+
remediation: [WINDOWS_COWORK_REMEDIATION.inspectHost],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function unknownWindowsClaudeCoworkCapability(reasonCode = "inspector-not-run") {
|
|
155
|
+
return normalizedFailure(reasonCode);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function nullableBoolean(value) {
|
|
159
|
+
return value === null || typeof value === "boolean";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function normalizeWindowsClaudeCoworkCapability(observation) {
|
|
163
|
+
if (
|
|
164
|
+
!observation
|
|
165
|
+
|| observation.schemaVersion !== 1
|
|
166
|
+
|| !VMP_STATES.has(observation.virtualMachinePlatform)
|
|
167
|
+
|| !COMPONENT_STATES.has(observation.vmcompute)
|
|
168
|
+
|| !COMPONENT_STATES.has(observation.hns)
|
|
169
|
+
|| !COMPONENT_STATES.has(observation.vfpext)
|
|
170
|
+
|| !nullableBoolean(observation.restartPending)
|
|
171
|
+
|| !nullableBoolean(observation.machineProvisioned)
|
|
172
|
+
|| !nullableBoolean(observation.virtualMachine)
|
|
173
|
+
|| !nullableBoolean(observation.nestedVirtualization)
|
|
174
|
+
) return normalizedFailure("inspector-malformed-output");
|
|
175
|
+
|
|
176
|
+
const checks = {
|
|
177
|
+
virtualMachinePlatform: observation.virtualMachinePlatform,
|
|
178
|
+
restartPending: observation.restartPending,
|
|
179
|
+
vmcompute: observation.vmcompute,
|
|
180
|
+
hns: observation.hns,
|
|
181
|
+
vfpext: observation.vfpext,
|
|
182
|
+
machineProvisioned: observation.machineProvisioned,
|
|
183
|
+
virtualMachine: observation.virtualMachine,
|
|
184
|
+
nestedVirtualization: observation.nestedVirtualization,
|
|
185
|
+
};
|
|
186
|
+
const reasonCodes = [];
|
|
187
|
+
const knownUnavailable = [];
|
|
188
|
+
const unknown = [];
|
|
189
|
+
const addKnown = (reason) => {
|
|
190
|
+
reasonCodes.push(reason);
|
|
191
|
+
knownUnavailable.push(reason);
|
|
192
|
+
};
|
|
193
|
+
const addUnknown = (reason) => {
|
|
194
|
+
reasonCodes.push(reason);
|
|
195
|
+
unknown.push(reason);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (checks.virtualMachinePlatform === "disabled") addKnown("virtual-machine-platform-disabled");
|
|
199
|
+
else if (checks.virtualMachinePlatform === "absent") addKnown("virtual-machine-platform-absent");
|
|
200
|
+
else if (checks.virtualMachinePlatform === "unknown") addUnknown("virtual-machine-platform-unknown");
|
|
201
|
+
|
|
202
|
+
if (checks.virtualMachinePlatform === "enabled") {
|
|
203
|
+
for (const component of ["vmcompute", "hns", "vfpext"]) {
|
|
204
|
+
if (checks[component] === "missing") addKnown(`${component}-missing`);
|
|
205
|
+
else if (checks[component] === "unknown") addUnknown(`${component}-unknown`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const componentsReady = [checks.vmcompute, checks.hns, checks.vfpext]
|
|
209
|
+
.every((state) => state.startsWith("present-"));
|
|
210
|
+
if (checks.restartPending === true && (
|
|
211
|
+
checks.virtualMachinePlatform !== "enabled" || !componentsReady
|
|
212
|
+
)) addKnown("restart-required");
|
|
213
|
+
else if (checks.restartPending === null && (
|
|
214
|
+
checks.virtualMachinePlatform !== "enabled" || !componentsReady
|
|
215
|
+
)) addUnknown("restart-state-unknown");
|
|
216
|
+
|
|
217
|
+
if (checks.machineProvisioned === false) addKnown("claude-machine-provisioning-missing");
|
|
218
|
+
else if (checks.machineProvisioned === null) addUnknown("claude-machine-provisioning-unknown");
|
|
219
|
+
|
|
220
|
+
if (checks.virtualMachine === null) addUnknown("virtualization-environment-unknown");
|
|
221
|
+
else if (checks.virtualMachine && checks.nestedVirtualization === false) addKnown("nested-virtualization-unavailable");
|
|
222
|
+
else if (checks.virtualMachine && checks.nestedVirtualization === null) addUnknown("nested-virtualization-unknown");
|
|
223
|
+
|
|
224
|
+
const enabled = checks.virtualMachinePlatform === "enabled"
|
|
225
|
+
&& componentsReady
|
|
226
|
+
&& checks.machineProvisioned === true
|
|
227
|
+
&& checks.virtualMachine !== null
|
|
228
|
+
&& (!checks.virtualMachine || checks.nestedVirtualization === true);
|
|
229
|
+
if (enabled) {
|
|
230
|
+
return {
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
status: "available",
|
|
233
|
+
enabled: true,
|
|
234
|
+
checks,
|
|
235
|
+
reasonCodes: [],
|
|
236
|
+
remediation: [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const remediation = [];
|
|
241
|
+
const addRemediation = (message) => {
|
|
242
|
+
if (!remediation.includes(message)) remediation.push(message);
|
|
243
|
+
};
|
|
244
|
+
if (reasonCodes.some((reason) => reason.startsWith("virtual-machine-platform-"))) {
|
|
245
|
+
addRemediation(WINDOWS_COWORK_REMEDIATION.enableVirtualMachinePlatform);
|
|
246
|
+
}
|
|
247
|
+
if (reasonCodes.includes("restart-required")) addRemediation(WINDOWS_COWORK_REMEDIATION.restartWindows);
|
|
248
|
+
if (reasonCodes.some((reason) => /^(?:vmcompute|hns|vfpext)-missing$/u.test(reason))) {
|
|
249
|
+
addRemediation(WINDOWS_COWORK_REMEDIATION.restoreHcs);
|
|
250
|
+
}
|
|
251
|
+
if (reasonCodes.includes("claude-machine-provisioning-missing")) {
|
|
252
|
+
addRemediation(WINDOWS_COWORK_REMEDIATION.provisionClaude);
|
|
253
|
+
}
|
|
254
|
+
if (reasonCodes.includes("nested-virtualization-unavailable")) {
|
|
255
|
+
addRemediation(WINDOWS_COWORK_REMEDIATION.enableNestedVirtualization);
|
|
256
|
+
}
|
|
257
|
+
if (unknown.length) addRemediation(WINDOWS_COWORK_REMEDIATION.inspectHost);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
schemaVersion: 1,
|
|
261
|
+
status: knownUnavailable.length ? "unavailable" : "unknown",
|
|
262
|
+
enabled: false,
|
|
263
|
+
checks,
|
|
264
|
+
reasonCodes,
|
|
265
|
+
remediation,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function inspectWindowsClaudeCoworkCapability({
|
|
270
|
+
environment = process.env,
|
|
271
|
+
platform = process.platform,
|
|
272
|
+
run = spawnSync,
|
|
273
|
+
architecture = process.arch,
|
|
274
|
+
} = {}) {
|
|
275
|
+
if (platform !== "win32") return normalizedFailure("inspector-not-windows");
|
|
276
|
+
const expectedArchitecture = /arm64/iu.test(
|
|
277
|
+
environmentValue(environment, "PROCESSOR_ARCHITEW6432")
|
|
278
|
+
|| environmentValue(environment, "PROCESSOR_ARCHITECTURE")
|
|
279
|
+
|| architecture,
|
|
280
|
+
) ? "arm64" : "x64";
|
|
281
|
+
const pin = PINNED_VENDOR_APPS.claude.windows;
|
|
282
|
+
if (windowsPackagePublisherId(pin.publisher) !== pin.publisherId) {
|
|
283
|
+
return normalizedFailure("inspector-pinned-publisher-invalid");
|
|
284
|
+
}
|
|
285
|
+
let invocation;
|
|
286
|
+
try {
|
|
287
|
+
invocation = nativeCommandInvocation("powershell", [
|
|
288
|
+
"-NoLogo",
|
|
289
|
+
"-NoProfile",
|
|
290
|
+
"-NonInteractive",
|
|
291
|
+
"-ExecutionPolicy",
|
|
292
|
+
"Bypass",
|
|
293
|
+
"-Command",
|
|
294
|
+
WINDOWS_COWORK_INSPECTION_SCRIPT,
|
|
295
|
+
], environment, "win32");
|
|
296
|
+
} catch {
|
|
297
|
+
return normalizedFailure("inspector-powershell-failed");
|
|
298
|
+
}
|
|
299
|
+
let result;
|
|
300
|
+
try {
|
|
301
|
+
result = run(invocation.command, invocation.args, {
|
|
302
|
+
encoding: "utf8",
|
|
303
|
+
env: {
|
|
304
|
+
...environment,
|
|
305
|
+
IMPEL_COWORK_EXPECTED_PACKAGE_NAME: pin.packageName,
|
|
306
|
+
IMPEL_COWORK_EXPECTED_PACKAGE_VERSION: pin.packageVersion,
|
|
307
|
+
IMPEL_COWORK_EXPECTED_PACKAGE_ARCHITECTURE: expectedArchitecture,
|
|
308
|
+
IMPEL_COWORK_EXPECTED_PACKAGE_PUBLISHER_ID: pin.publisherId,
|
|
309
|
+
},
|
|
310
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
311
|
+
windowsHide: true,
|
|
312
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
313
|
+
});
|
|
314
|
+
} catch {
|
|
315
|
+
return normalizedFailure("inspector-powershell-failed");
|
|
316
|
+
}
|
|
317
|
+
if (result?.error || result?.status !== 0) return normalizedFailure("inspector-powershell-failed");
|
|
318
|
+
try {
|
|
319
|
+
return normalizeWindowsClaudeCoworkCapability(JSON.parse(String(result.stdout || "").trim()));
|
|
320
|
+
} catch {
|
|
321
|
+
return normalizedFailure("inspector-malformed-output");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function formatWindowsClaudeCoworkCapability(capability) {
|
|
326
|
+
if (!capability || capability.enabled === true) return [];
|
|
327
|
+
const reasons = Array.isArray(capability.reasonCodes) && capability.reasonCodes.length
|
|
328
|
+
? capability.reasonCodes.join(", ")
|
|
329
|
+
: "host-prerequisites-unknown";
|
|
330
|
+
return [
|
|
331
|
+
`Claude Cowork ${capability.status || "unknown"} (${reasons}). Chat and Code remain available.`,
|
|
332
|
+
...(Array.isArray(capability.remediation) ? capability.remediation.map((message) => ` ${message}`) : []),
|
|
333
|
+
];
|
|
334
|
+
}
|