impel-cli 0.18.3 → 0.18.5
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 +20 -3
- package/package.json +3 -2
- package/src/apps.js +42 -35
- package/src/cliProfiles.js +36 -33
- package/src/commands/apps.js +35 -28
- package/src/commands/auth.js +6 -4
- package/src/commands/converge.js +14 -2
- package/src/commands/launch.js +12 -8
- package/src/commands/pat.js +46 -37
- package/src/commands/setup.js +19 -9
- package/src/commands/status.js +7 -5
- package/src/commands/token.js +3 -2
- package/src/config.js +14 -10
- package/src/extension/index.js +81 -0
- package/src/installRecovery/tools.js +16 -4
- package/src/prompt.js +24 -4
- package/src/provisioning.js +5 -4
- package/src/runtimeBrand.js +140 -0
- package/src/selfInvocation.js +7 -3
- package/src/shellEntries.js +5 -2
- package/src/stableEntrypoint.js +18 -12
- package/src/tenants.js +15 -7
- package/src/windowsApps.js +39 -11
|
@@ -500,10 +500,22 @@ async function installVendorApp(input, context) {
|
|
|
500
500
|
if (typeof context.installVendorApp !== "function") {
|
|
501
501
|
return result("failed", "Vendor app installation is unavailable in this recovery context.");
|
|
502
502
|
}
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
?
|
|
506
|
-
:
|
|
503
|
+
const outcome = await context.installVendorApp(input.target);
|
|
504
|
+
const installed = typeof outcome === "object" && outcome !== null
|
|
505
|
+
? outcome.installed !== false
|
|
506
|
+
: outcome !== false;
|
|
507
|
+
if (installed) {
|
|
508
|
+
return result("succeeded", `The ${input.target} vendor app installation completed.`);
|
|
509
|
+
}
|
|
510
|
+
const detail = typeof outcome === "object" && outcome?.error
|
|
511
|
+
? redactInstallRecoveryText(outcome.error).slice(0, 1_000)
|
|
512
|
+
: null;
|
|
513
|
+
return result(
|
|
514
|
+
"failed",
|
|
515
|
+
detail
|
|
516
|
+
? `The ${input.target} vendor app did not install cleanly: ${detail}`
|
|
517
|
+
: `The ${input.target} vendor app did not install cleanly.`,
|
|
518
|
+
);
|
|
507
519
|
}
|
|
508
520
|
|
|
509
521
|
function repairUserPath(input, context, io) {
|
package/src/prompt.js
CHANGED
|
@@ -40,12 +40,32 @@ export function promptSecret(question) {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function promptText(question
|
|
43
|
+
export function promptText(question, {
|
|
44
|
+
signal = null,
|
|
45
|
+
input = process.stdin,
|
|
46
|
+
output = process.stdout,
|
|
47
|
+
} = {}) {
|
|
44
48
|
return new Promise((resolve) => {
|
|
45
|
-
const rl = readline.createInterface({ input
|
|
46
|
-
|
|
49
|
+
const rl = readline.createInterface({ input, output });
|
|
50
|
+
let settled = false;
|
|
51
|
+
const finish = (answer, aborted = false) => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
signal?.removeEventListener("abort", abort);
|
|
47
55
|
rl.close();
|
|
48
|
-
|
|
56
|
+
// A cancelled readline prompt otherwise leaves the next status line on
|
|
57
|
+
// the same terminal row as its unanswered question.
|
|
58
|
+
if (aborted && output?.isTTY) output.write("\n");
|
|
59
|
+
resolve(String(answer || "").trim());
|
|
60
|
+
};
|
|
61
|
+
const abort = () => finish("", true);
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
abort();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
67
|
+
rl.question(question, (answer) => {
|
|
68
|
+
finish(answer);
|
|
49
69
|
});
|
|
50
70
|
});
|
|
51
71
|
}
|
package/src/provisioning.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
tenantCredential,
|
|
17
17
|
} from "./tenants.js";
|
|
18
18
|
import { reconcileTenantApps } from "./commands/apps.js";
|
|
19
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
19
20
|
|
|
20
21
|
export function selectDefaultTenant(listing, { requested = null, currentTenantId = null } = {}) {
|
|
21
22
|
const normalized = requested ? normalizeTenantId(requested) : null;
|
|
@@ -62,8 +63,8 @@ export function buildTenantInventory(listing, {
|
|
|
62
63
|
|
|
63
64
|
export function locallyKnownTenantIds({ homeDir = os.homedir(), readDirectory = fs.readdirSync } = {}) {
|
|
64
65
|
const roots = [
|
|
65
|
-
path.join(homeDir, ".config",
|
|
66
|
-
path.join(homeDir, ".config",
|
|
66
|
+
path.join(homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "cli", "tenants"),
|
|
67
|
+
path.join(homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "apps", "tenants"),
|
|
67
68
|
];
|
|
68
69
|
const ids = new Set();
|
|
69
70
|
for (const root of roots) {
|
|
@@ -118,7 +119,7 @@ async function prepareTenantCli(config, tenant, io, binaries) {
|
|
|
118
119
|
};
|
|
119
120
|
continue;
|
|
120
121
|
}
|
|
121
|
-
await io.syncSkills({
|
|
122
|
+
if (RUNTIME_BRAND.features.skills) await io.syncSkills({
|
|
122
123
|
client,
|
|
123
124
|
gatewayUrl,
|
|
124
125
|
env: definition.env(profile),
|
|
@@ -143,7 +144,7 @@ async function prepareTenantCli(config, tenant, io, binaries) {
|
|
|
143
144
|
root: value.root,
|
|
144
145
|
label: `Impel ${client === "claude" ? "Claude" : "Codex"} CLI (${tenant.id})`,
|
|
145
146
|
}));
|
|
146
|
-
if (agentProfiles.length) {
|
|
147
|
+
if (RUNTIME_BRAND.features.agents && agentProfiles.length) {
|
|
147
148
|
try {
|
|
148
149
|
await io.syncAgents({
|
|
149
150
|
profiles: agentProfiles,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
const SAFE_ID = /^[a-z][a-z0-9-]{1,63}$/u;
|
|
4
|
+
const SAFE_NAMESPACE = /^[a-z][a-z0-9_-]{1,63}$/u;
|
|
5
|
+
const SAFE_PREFIX = /^[a-z][a-z0-9_]{2,63}_$/u;
|
|
6
|
+
const SAFE_ENV = /^[A-Z][A-Z0-9_]{1,63}$/u;
|
|
7
|
+
const SAFE_TENANT = /^[A-Za-z0-9_.-]{1,128}$/u;
|
|
8
|
+
const SAFE_PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]{0,62}\/)?[a-z0-9][a-z0-9._-]{0,126}$/u;
|
|
9
|
+
const SAFE_BUNDLE_PREFIX = /^[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)+$/u;
|
|
10
|
+
const CONTROL_RE = /[\u0000-\u001F\u007F-\u009F]/u;
|
|
11
|
+
const SUPPORTED_COMMANDS = new Set(["setup", "auth", "pat", "app", "claude", "codex", "status", "token"]);
|
|
12
|
+
|
|
13
|
+
const DEFAULT = Object.freeze({
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
product: Object.freeze({ id: "impel", displayName: "Impel" }),
|
|
16
|
+
cli: Object.freeze({
|
|
17
|
+
command: "impel",
|
|
18
|
+
packageName: "impel-cli",
|
|
19
|
+
configNamespace: "impel",
|
|
20
|
+
providerId: "impel",
|
|
21
|
+
managedMarker: "impel-cli",
|
|
22
|
+
environmentPrefix: "IMPEL",
|
|
23
|
+
}),
|
|
24
|
+
auth: Object.freeze({ patPrefix: "impel_pat_", tenantPrefix: "impel_tenant_" }),
|
|
25
|
+
tenant: Object.freeze({ defaultId: null, displayName: null }),
|
|
26
|
+
gateway: Object.freeze({ defaultOrigin: "https://gateway.useimpel.com" }),
|
|
27
|
+
controlPlane: Object.freeze({ defaultOrigin: "https://www.useimpel.com" }),
|
|
28
|
+
apps: Object.freeze({
|
|
29
|
+
displayPrefix: "Impel",
|
|
30
|
+
bundleIdentifierPrefix: "com.useimpel",
|
|
31
|
+
windowsStartMenuFolder: "Impel",
|
|
32
|
+
}),
|
|
33
|
+
capabilities: Object.freeze({ commands: Object.freeze([...SUPPORTED_COMMANDS]), agentPats: true }),
|
|
34
|
+
features: Object.freeze({ sessions: true, mcp: true, skills: true, agents: true }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function text(name, value, pattern = null) {
|
|
38
|
+
if (typeof value !== "string" || !value || value.trim() !== value || value.length > 128 || CONTROL_RE.test(value)) {
|
|
39
|
+
throw new Error(`impel-cli runtime brand ${name} is invalid`);
|
|
40
|
+
}
|
|
41
|
+
if (pattern && !pattern.test(value)) throw new Error(`impel-cli runtime brand ${name} is invalid`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function origin(name, value) {
|
|
46
|
+
const parsed = new URL(text(name, value));
|
|
47
|
+
const local = parsed.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
|
|
48
|
+
if ((parsed.protocol !== "https:" && !local) || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
49
|
+
throw new Error(`impel-cli runtime brand ${name} must be a bare HTTPS origin`);
|
|
50
|
+
}
|
|
51
|
+
return parsed.toString().replace(/\/$/u, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pathSegment(name, value) {
|
|
55
|
+
const result = text(name, value);
|
|
56
|
+
if (result === "." || result === ".." || /[\\/:]/u.test(result)) {
|
|
57
|
+
throw new Error(`impel-cli runtime brand ${name} is invalid`);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validateRuntimeBrand(input) {
|
|
63
|
+
if (!input || Array.isArray(input) || typeof input !== "object" || input.schemaVersion !== 1) {
|
|
64
|
+
throw new Error("impel-cli runtime brand schemaVersion must be 1");
|
|
65
|
+
}
|
|
66
|
+
const command = text("cli.command", input.cli?.command, SAFE_ID);
|
|
67
|
+
const packageName = text("cli.packageName", input.cli?.packageName || command, SAFE_PACKAGE);
|
|
68
|
+
if (path.isAbsolute(packageName)) throw new Error("impel-cli runtime brand cli.packageName is invalid");
|
|
69
|
+
const defaultTenant = input.tenant?.defaultId == null ? null : text("tenant.defaultId", input.tenant.defaultId, SAFE_TENANT);
|
|
70
|
+
const requestedCommands = input.capabilities?.commands;
|
|
71
|
+
if (!Array.isArray(requestedCommands) || requestedCommands.length === 0) {
|
|
72
|
+
throw new Error("impel-cli runtime brand capabilities.commands must be a non-empty array");
|
|
73
|
+
}
|
|
74
|
+
const commands = [...new Set(requestedCommands.map((commandName) => text("capabilities.commands", commandName, SAFE_ID)))];
|
|
75
|
+
if (commands.some((commandName) => !SUPPORTED_COMMANDS.has(commandName))) {
|
|
76
|
+
throw new Error("impel-cli runtime brand capabilities.commands contains an unsupported command");
|
|
77
|
+
}
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
product: Object.freeze({
|
|
81
|
+
id: text("product.id", input.product?.id, SAFE_ID),
|
|
82
|
+
displayName: text("product.displayName", input.product?.displayName),
|
|
83
|
+
}),
|
|
84
|
+
cli: Object.freeze({
|
|
85
|
+
command,
|
|
86
|
+
packageName,
|
|
87
|
+
configNamespace: text("cli.configNamespace", input.cli?.configNamespace, SAFE_NAMESPACE),
|
|
88
|
+
providerId: text("cli.providerId", input.cli?.providerId, SAFE_NAMESPACE),
|
|
89
|
+
managedMarker: text("cli.managedMarker", input.cli?.managedMarker, SAFE_NAMESPACE),
|
|
90
|
+
environmentPrefix: text("cli.environmentPrefix", input.cli?.environmentPrefix, SAFE_ENV),
|
|
91
|
+
}),
|
|
92
|
+
auth: Object.freeze({
|
|
93
|
+
patPrefix: text("auth.patPrefix", input.auth?.patPrefix, SAFE_PREFIX),
|
|
94
|
+
tenantPrefix: text("auth.tenantPrefix", input.auth?.tenantPrefix, SAFE_PREFIX),
|
|
95
|
+
}),
|
|
96
|
+
tenant: Object.freeze({
|
|
97
|
+
defaultId: defaultTenant,
|
|
98
|
+
displayName: input.tenant?.displayName == null ? defaultTenant : text("tenant.displayName", input.tenant.displayName),
|
|
99
|
+
}),
|
|
100
|
+
gateway: Object.freeze({ defaultOrigin: origin("gateway.defaultOrigin", input.gateway?.defaultOrigin) }),
|
|
101
|
+
controlPlane: Object.freeze({ defaultOrigin: origin("controlPlane.defaultOrigin", input.controlPlane?.defaultOrigin) }),
|
|
102
|
+
apps: Object.freeze({
|
|
103
|
+
displayPrefix: pathSegment("apps.displayPrefix", input.apps?.displayPrefix || input.product?.displayName),
|
|
104
|
+
bundleIdentifierPrefix: text("apps.bundleIdentifierPrefix", input.apps?.bundleIdentifierPrefix, SAFE_BUNDLE_PREFIX),
|
|
105
|
+
windowsStartMenuFolder: pathSegment("apps.windowsStartMenuFolder", input.apps?.windowsStartMenuFolder || input.product?.displayName),
|
|
106
|
+
}),
|
|
107
|
+
capabilities: Object.freeze({
|
|
108
|
+
commands: Object.freeze(commands),
|
|
109
|
+
agentPats: input.capabilities?.agentPats === true,
|
|
110
|
+
}),
|
|
111
|
+
features: Object.freeze({
|
|
112
|
+
sessions: input.features?.sessions === true,
|
|
113
|
+
mcp: input.features?.mcp === true,
|
|
114
|
+
skills: input.features?.skills === true,
|
|
115
|
+
agents: input.features?.agents === true,
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function loadRuntimeBrand() {
|
|
121
|
+
const encoded = process.env.IMPEL_CLI_RUNTIME_BRAND;
|
|
122
|
+
if (!encoded) return DEFAULT;
|
|
123
|
+
let parsed;
|
|
124
|
+
try { parsed = JSON.parse(encoded); }
|
|
125
|
+
catch { throw new Error("IMPEL_CLI_RUNTIME_BRAND must contain valid JSON"); }
|
|
126
|
+
return validateRuntimeBrand(parsed);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const RUNTIME_BRAND = loadRuntimeBrand();
|
|
130
|
+
|
|
131
|
+
export function brandedEnvironmentName(suffix) {
|
|
132
|
+
return `${RUNTIME_BRAND.cli.environmentPrefix}_${suffix}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function brandedText(value) {
|
|
136
|
+
return String(value)
|
|
137
|
+
.replaceAll("impel-cli", `${RUNTIME_BRAND.cli.command}-cli`)
|
|
138
|
+
.replaceAll("Impel", RUNTIME_BRAND.product.displayName)
|
|
139
|
+
.replaceAll("impel", RUNTIME_BRAND.cli.command);
|
|
140
|
+
}
|
package/src/selfInvocation.js
CHANGED
|
@@ -2,9 +2,11 @@ import path from "node:path";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
4
|
import { environmentValue } from "./nativeProcess.js";
|
|
5
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
5
6
|
|
|
6
7
|
/** The running package's own bin script, used directly for live child spawns. */
|
|
7
|
-
export const IMPEL_CLI_ENTRYPOINT =
|
|
8
|
+
export const IMPEL_CLI_ENTRYPOINT = process.env.IMPEL_CLI_EXTENSION_ENTRYPOINT
|
|
9
|
+
|| fileURLToPath(new URL("../bin/impel.js", import.meta.url));
|
|
8
10
|
|
|
9
11
|
// Matches a real package install (npm/pnpm/volta all place the package under
|
|
10
12
|
// node_modules/impel-cli; an unsupported project-local install matches too,
|
|
@@ -12,7 +14,9 @@ export const IMPEL_CLI_ENTRYPOINT = fileURLToPath(new URL("../bin/impel.js", imp
|
|
|
12
14
|
// install). A git checkout or worktree never matches, so development runs
|
|
13
15
|
// keep baking their own path instead of repointing the machine-wide stable
|
|
14
16
|
// entry at a checkout.
|
|
15
|
-
const
|
|
17
|
+
const escapedPackageName = RUNTIME_BRAND.cli.packageName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&").replaceAll("/", "[\\\\/]");
|
|
18
|
+
const escapedCommand = RUNTIME_BRAND.cli.command.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
19
|
+
const GLOBAL_INSTALL_RE = new RegExp(`[\\\\/]node_modules[\\\\/]${escapedPackageName}[\\\\/]bin[\\\\/]${escapedCommand}\\.js$`, "iu");
|
|
16
20
|
|
|
17
21
|
export function runningFromGlobalInstall(entrypoint = IMPEL_CLI_ENTRYPOINT) {
|
|
18
22
|
return GLOBAL_INSTALL_RE.test(entrypoint);
|
|
@@ -22,7 +26,7 @@ export function runningFromGlobalInstall(entrypoint = IMPEL_CLI_ENTRYPOINT) {
|
|
|
22
26
|
export function windowsStableEntrypointPath(environment = process.env) {
|
|
23
27
|
const localAppData = environmentValue(environment, "LOCALAPPDATA");
|
|
24
28
|
if (typeof localAppData !== "string" || !localAppData.trim()) return null;
|
|
25
|
-
return path.join(localAppData,
|
|
29
|
+
return path.join(localAppData, RUNTIME_BRAND.apps.windowsStartMenuFolder, "bin", `${RUNTIME_BRAND.cli.command}-entry.cjs`);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/** The directory `impel nuke` removes to erase the stable entry point. */
|
package/src/shellEntries.js
CHANGED
|
@@ -9,6 +9,7 @@ import { environmentValue, nativeCommandInvocation, resolveNativeBinary } from "
|
|
|
9
9
|
import { managedCliEntrypoint } from "./selfInvocation.js";
|
|
10
10
|
import { normalizeTenantId } from "./tenants.js";
|
|
11
11
|
import { windowsClaudeUserData } from "./windowsApps.js";
|
|
12
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
12
13
|
|
|
13
14
|
const LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
14
15
|
|
|
@@ -42,7 +43,9 @@ export function windowsShortcutArgument(value) {
|
|
|
42
43
|
export function windowsTenantShortcutName(product, tenantId, tenantName) {
|
|
43
44
|
const id = normalizeTenantId(tenantId);
|
|
44
45
|
const name = safeShortcutSegment(tenantName, id);
|
|
45
|
-
const label = product === "claude"
|
|
46
|
+
const label = product === "claude"
|
|
47
|
+
? `${RUNTIME_BRAND.apps.displayPrefix} Claude`
|
|
48
|
+
: `${RUNTIME_BRAND.apps.displayPrefix} ChatGPT`;
|
|
46
49
|
return `${label} (${name} · ${id}).lnk`;
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -59,7 +62,7 @@ export function registerWindowsTenantShortcut({
|
|
|
59
62
|
}) {
|
|
60
63
|
const appData = environmentValue(environment, "APPDATA");
|
|
61
64
|
if (!appData) throw new Error("Windows APPDATA is unavailable; Start menu entry was not created");
|
|
62
|
-
const shortcutDirectory = path.win32.join(appData, "Microsoft", "Windows", "Start Menu", "Programs",
|
|
65
|
+
const shortcutDirectory = path.win32.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", RUNTIME_BRAND.apps.windowsStartMenuFolder);
|
|
63
66
|
mkdirSync(shortcutDirectory, { recursive: true });
|
|
64
67
|
const shortcutPath = path.win32.join(
|
|
65
68
|
shortcutDirectory,
|
package/src/stableEntrypoint.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
windowsStableEntrypointPath,
|
|
8
8
|
} from "./selfInvocation.js";
|
|
9
9
|
import { redactSecretText } from "./config.js";
|
|
10
|
+
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Render the stable Windows entry point (`%LOCALAPPDATA%\Impel\bin\impel-entry.cjs`).
|
|
@@ -22,12 +23,15 @@ import { redactSecretText } from "./config.js";
|
|
|
22
23
|
* supported Node (>=18). Output is deterministic for a given baked path.
|
|
23
24
|
*/
|
|
24
25
|
export function renderStableEntrypoint(cliEntrypoint = IMPEL_CLI_ENTRYPOINT) {
|
|
26
|
+
const packageParts = RUNTIME_BRAND.cli.packageName.split("/");
|
|
27
|
+
const cliSuffixParts = ["node_modules", ...packageParts, "bin", `${RUNTIME_BRAND.cli.command}.js`];
|
|
28
|
+
const completeInstallFile = RUNTIME_BRAND.cli.packageName === "impel-cli" ? path.join("src", "cli.js") : null;
|
|
25
29
|
return `#!/usr/bin/env node
|
|
26
30
|
"use strict";
|
|
27
|
-
// Managed by
|
|
31
|
+
// Managed by ${RUNTIME_BRAND.cli.command}-cli — stable ${RUNTIME_BRAND.product.displayName} entry point.
|
|
28
32
|
//
|
|
29
|
-
// Vendor apps (
|
|
30
|
-
// this file in their managed profiles. It resolves the current
|
|
33
|
+
// Vendor apps (${RUNTIME_BRAND.apps.displayPrefix} Claude, ${RUNTIME_BRAND.apps.displayPrefix} ChatGPT) persist absolute invocations of
|
|
34
|
+
// this file in their managed profiles. It resolves the current ${RUNTIME_BRAND.cli.packageName}
|
|
31
35
|
// global install at run time, so replacing or moving the npm global package
|
|
32
36
|
// never strands the invocations baked into those profiles.
|
|
33
37
|
|
|
@@ -37,10 +41,12 @@ const { pathToFileURL } = require("node:url");
|
|
|
37
41
|
const { spawnSync } = require("node:child_process");
|
|
38
42
|
|
|
39
43
|
const BAKED_CLI = ${JSON.stringify(cliEntrypoint)};
|
|
40
|
-
const CLI_SUFFIX = path.join(
|
|
44
|
+
const CLI_SUFFIX = path.join(${cliSuffixParts.map((part) => JSON.stringify(part)).join(", ")});
|
|
45
|
+
const PACKAGE_NAME = ${JSON.stringify(RUNTIME_BRAND.cli.packageName)};
|
|
46
|
+
const REQUIRED_FILE = ${JSON.stringify(completeInstallFile)};
|
|
41
47
|
|
|
42
48
|
function waitBudgetMs() {
|
|
43
|
-
const raw = Number.parseInt(process.env
|
|
49
|
+
const raw = Number.parseInt(process.env.${brandedEnvironmentName("ENTRYPOINT_WAIT_MS")} || "", 10);
|
|
44
50
|
if (Number.isFinite(raw) && raw >= 0) return Math.min(raw, 60000);
|
|
45
51
|
return 8000;
|
|
46
52
|
}
|
|
@@ -73,8 +79,8 @@ function isFile(candidate) {
|
|
|
73
79
|
function isCompleteInstall(cliPath) {
|
|
74
80
|
try {
|
|
75
81
|
const root = path.dirname(path.dirname(cliPath));
|
|
76
|
-
if (!isFile(cliPath) || !isFile(path.join(root,
|
|
77
|
-
return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).name ===
|
|
82
|
+
if (!isFile(cliPath) || (REQUIRED_FILE && !isFile(path.join(root, REQUIRED_FILE)))) return false;
|
|
83
|
+
return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).name === PACKAGE_NAME;
|
|
78
84
|
} catch {
|
|
79
85
|
return false;
|
|
80
86
|
}
|
|
@@ -106,8 +112,8 @@ async function main() {
|
|
|
106
112
|
if (!cli) cli = candidateCliPaths().find(isFile) || null;
|
|
107
113
|
if (!cli) {
|
|
108
114
|
process.stderr.write(
|
|
109
|
-
"
|
|
110
|
-
"Run \`npm install -g
|
|
115
|
+
"${RUNTIME_BRAND.cli.command}-entry: no ${RUNTIME_BRAND.cli.packageName} install found (checked " + candidateCliPaths().join("; ") + "). " +
|
|
116
|
+
"Run \`npm install -g ${RUNTIME_BRAND.cli.packageName}\`, then \`${RUNTIME_BRAND.cli.command} setup\`.\\n"
|
|
111
117
|
);
|
|
112
118
|
process.exit(1);
|
|
113
119
|
}
|
|
@@ -125,7 +131,7 @@ async function main() {
|
|
|
125
131
|
});
|
|
126
132
|
if (typeof rerun.status === "number") process.exit(rerun.status);
|
|
127
133
|
process.stderr.write(
|
|
128
|
-
"
|
|
134
|
+
"${RUNTIME_BRAND.cli.command}-entry: could not start " + cli + " (" +
|
|
129
135
|
(error && error.message ? error.message : String(error)) + ")\\n"
|
|
130
136
|
);
|
|
131
137
|
process.exit(1);
|
|
@@ -134,7 +140,7 @@ async function main() {
|
|
|
134
140
|
|
|
135
141
|
main().catch((error) => {
|
|
136
142
|
process.stderr.write(
|
|
137
|
-
"
|
|
143
|
+
"${RUNTIME_BRAND.cli.command}-entry: " + (error && error.message ? error.message : String(error)) + "\\n"
|
|
138
144
|
);
|
|
139
145
|
process.exit(1);
|
|
140
146
|
});
|
|
@@ -187,7 +193,7 @@ export function ensureWindowsStableEntrypointQuietly(options = {}) {
|
|
|
187
193
|
return ensureWindowsStableEntrypoint(options);
|
|
188
194
|
} catch (error) {
|
|
189
195
|
console.error(
|
|
190
|
-
|
|
196
|
+
`${RUNTIME_BRAND.cli.command}: could not refresh the stable Windows entry point (${redactSecretText(error?.message || error)})`
|
|
191
197
|
);
|
|
192
198
|
return null;
|
|
193
199
|
}
|
package/src/tenants.js
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
resolveDefaultAppUrl,
|
|
5
5
|
saveConfig,
|
|
6
6
|
} from "./config.js";
|
|
7
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
7
8
|
|
|
8
|
-
export const TENANT_CREDENTIAL_PREFIX =
|
|
9
|
+
export const TENANT_CREDENTIAL_PREFIX = RUNTIME_BRAND.auth.tenantPrefix;
|
|
9
10
|
export const PRODUCT_ACCESS_WORKSPACE = "workspace";
|
|
10
11
|
export const PRODUCT_ACCESS_IDENTITY = "identity";
|
|
11
12
|
export const PRODUCT_ACCESS_GATEWAY = "gateway";
|
|
@@ -85,15 +86,15 @@ function normalizeTenantName(value, fallback) {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export function tenantCredential(pat, tenantId) {
|
|
88
|
-
if (!String(pat || "").startsWith(
|
|
89
|
-
throw new Error(
|
|
89
|
+
if (!String(pat || "").startsWith(RUNTIME_BRAND.auth.patPrefix)) {
|
|
90
|
+
throw new Error(`a ${RUNTIME_BRAND.product.displayName} PAT is required to create a tenant credential`);
|
|
90
91
|
}
|
|
91
92
|
const encodedTenant = Buffer.from(normalizeTenantId(tenantId), "utf8").toString("base64url");
|
|
92
93
|
return `${TENANT_CREDENTIAL_PREFIX}${encodedTenant}.${pat}`;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
export async function fetchTenants(config, fetchImpl = fetch) {
|
|
96
|
-
if (!config?.pat) throw new Error(
|
|
97
|
+
if (!config?.pat) throw new Error(`not authenticated; run \`${RUNTIME_BRAND.cli.command} setup\` (or \`${RUNTIME_BRAND.cli.command} auth\`) first`);
|
|
97
98
|
const appUrl = normalizeGatewayUrl(config.appUrl || resolveDefaultAppUrl());
|
|
98
99
|
const controller = new AbortController();
|
|
99
100
|
const timeout = setTimeout(() => controller.abort(), 7_000);
|
|
@@ -141,9 +142,16 @@ export async function fetchTenants(config, fetchImpl = fetch) {
|
|
|
141
142
|
if (!tenants.some((tenant) => tenant.id === defaultTenantId)) {
|
|
142
143
|
throw new Error("tenant API default is not in the available tenant list");
|
|
143
144
|
}
|
|
145
|
+
const fixedTenant = RUNTIME_BRAND.tenant.defaultId;
|
|
146
|
+
const visibleTenants = fixedTenant
|
|
147
|
+
? tenants.filter((tenant) => tenant.id === fixedTenant || tenant.slug === fixedTenant)
|
|
148
|
+
: tenants;
|
|
149
|
+
if (fixedTenant && visibleTenants.length !== 1) {
|
|
150
|
+
throw new Error(`the required ${RUNTIME_BRAND.product.displayName} tenant is not available`);
|
|
151
|
+
}
|
|
144
152
|
return {
|
|
145
|
-
tenants,
|
|
146
|
-
defaultTenantId,
|
|
153
|
+
tenants: visibleTenants,
|
|
154
|
+
defaultTenantId: fixedTenant ? visibleTenants[0].id : defaultTenantId,
|
|
147
155
|
patTenantId: payload.patTenantId || null,
|
|
148
156
|
productAccess,
|
|
149
157
|
scopes,
|
|
@@ -151,7 +159,7 @@ export async function fetchTenants(config, fetchImpl = fetch) {
|
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
export async function ensureTenantSelection(config, { refresh = false } = {}) {
|
|
154
|
-
if (!config?.pat) throw new Error(
|
|
162
|
+
if (!config?.pat) throw new Error(`not authenticated; run \`${RUNTIME_BRAND.cli.command} setup\` (or \`${RUNTIME_BRAND.cli.command} auth\`) first`);
|
|
155
163
|
if (config.tenantId && !refresh) {
|
|
156
164
|
const tenantId = normalizeTenantId(config.tenantId);
|
|
157
165
|
return {
|
package/src/windowsApps.js
CHANGED
|
@@ -9,11 +9,13 @@ import { pipeline } from "node:stream/promises";
|
|
|
9
9
|
import { PINNED_VENDOR_APPS } from "./apps.js";
|
|
10
10
|
import { environmentValue, nativeCommandInvocation } from "./nativeProcess.js";
|
|
11
11
|
import { normalizeTenantId } from "./tenants.js";
|
|
12
|
+
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
12
13
|
|
|
13
14
|
export const WINDOWS_CHATGPT_PACKAGE = "9PLM9XGG6VKS";
|
|
14
15
|
export const WINDOWS_CHATGPT_PACKAGE_NAME = "OpenAI.Codex";
|
|
15
16
|
export const WINDOWS_CHATGPT_PUBLISHER_ID = "2p2nqsd0c76g0";
|
|
16
17
|
const WINDOWS_CLAUDE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
|
|
18
|
+
const WINDOWS_WINGET_UPDATE_NOT_APPLICABLE = 0x8A15002B;
|
|
17
19
|
|
|
18
20
|
export function windowsClaudeUserData(environment = process.env, tenantId = null) {
|
|
19
21
|
const localAppData = environmentValue(environment, "LOCALAPPDATA")
|
|
@@ -21,7 +23,7 @@ export function windowsClaudeUserData(environment = process.env, tenantId = null
|
|
|
21
23
|
return path.win32.join(
|
|
22
24
|
localAppData,
|
|
23
25
|
"Claude-3p",
|
|
24
|
-
|
|
26
|
+
RUNTIME_BRAND.apps.windowsStartMenuFolder,
|
|
25
27
|
normalizeTenantId(tenantId || "default"),
|
|
26
28
|
);
|
|
27
29
|
}
|
|
@@ -34,6 +36,16 @@ function isFile(filePath) {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
// A CLI launched from PowerShell 7 inherits its PSModulePath. Passing that
|
|
40
|
+
// value into Windows PowerShell 5.1 makes legacy modules such as
|
|
41
|
+
// Microsoft.PowerShell.Security visible but unloadable. Omit the variable so
|
|
42
|
+
// powershell.exe reconstructs its own version-correct default module path.
|
|
43
|
+
function windowsPowerShellEnvironment(environment = process.env) {
|
|
44
|
+
return Object.fromEntries(
|
|
45
|
+
Object.entries(environment).filter(([key]) => key.toLowerCase() !== "psmodulepath"),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
function versionedSquirrelCandidates(root, readDirectory = fs.readdirSync) {
|
|
38
50
|
try {
|
|
39
51
|
return readDirectory(root, { withFileTypes: true })
|
|
@@ -61,7 +73,7 @@ function installedMsixChatGPT(environment, run = spawnSync) {
|
|
|
61
73
|
try {
|
|
62
74
|
const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
63
75
|
encoding: "utf8",
|
|
64
|
-
env: environment,
|
|
76
|
+
env: windowsPowerShellEnvironment(environment),
|
|
65
77
|
stdio: ["ignore", "pipe", "ignore"],
|
|
66
78
|
windowsHide: true,
|
|
67
79
|
});
|
|
@@ -74,7 +86,7 @@ function installedMsixChatGPT(environment, run = spawnSync) {
|
|
|
74
86
|
/** Candidate paths for Anthropic's signed per-user Windows desktop install. */
|
|
75
87
|
export function windowsClaudeAppCandidates(environment = process.env, dependencies = {}) {
|
|
76
88
|
const io = { readDirectory: fs.readdirSync, ...dependencies };
|
|
77
|
-
const overridden = environmentValue(environment, "
|
|
89
|
+
const overridden = environmentValue(environment, brandedEnvironmentName("CLAUDE_APP_BIN"));
|
|
78
90
|
const localAppData = environmentValue(environment, "LOCALAPPDATA")
|
|
79
91
|
|| path.win32.join(environmentValue(environment, "USERPROFILE") || os.homedir(), "AppData", "Local");
|
|
80
92
|
const programFiles = environmentValue(environment, "ProgramFiles");
|
|
@@ -103,8 +115,8 @@ export function findWindowsClaudeApp(environment = process.env, dependencies = {
|
|
|
103
115
|
|
|
104
116
|
/** Candidate paths for an unpackaged OpenAI ChatGPT/Codex desktop install. */
|
|
105
117
|
export function windowsChatGPTAppCandidates(environment = process.env) {
|
|
106
|
-
const overridden = environmentValue(environment, "
|
|
107
|
-
|| environmentValue(environment, "
|
|
118
|
+
const overridden = environmentValue(environment, brandedEnvironmentName("CHATGPT_APP_BIN"))
|
|
119
|
+
|| environmentValue(environment, brandedEnvironmentName("CODEX_APP_BIN"));
|
|
108
120
|
const localAppData = environmentValue(environment, "LOCALAPPDATA")
|
|
109
121
|
|| path.win32.join(environmentValue(environment, "USERPROFILE") || os.homedir(), "AppData", "Local");
|
|
110
122
|
const programFiles = environmentValue(environment, "ProgramFiles");
|
|
@@ -179,7 +191,7 @@ function pinnedWindowsClaudeApp(environment = process.env, dependencies = {}) {
|
|
|
179
191
|
const result = io.run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
180
192
|
encoding: "utf8",
|
|
181
193
|
env: {
|
|
182
|
-
...environment,
|
|
194
|
+
...windowsPowerShellEnvironment(environment),
|
|
183
195
|
IMPEL_CLAUDE_PACKAGE_VERSION: io.pin.packageVersion,
|
|
184
196
|
IMPEL_CLAUDE_PACKAGE_PUBLISHER: io.pin.publisher,
|
|
185
197
|
IMPEL_CLAUDE_PACKAGE_ARCHITECTURE: expectedArchitecture,
|
|
@@ -269,7 +281,7 @@ function installPinnedWindowsClaudeMsix(packagePath, environment, pin, architect
|
|
|
269
281
|
{
|
|
270
282
|
encoding: "utf8",
|
|
271
283
|
env: {
|
|
272
|
-
...environment,
|
|
284
|
+
...windowsPowerShellEnvironment(environment),
|
|
273
285
|
IMPEL_CLAUDE_MSIX_PATH: packagePath,
|
|
274
286
|
IMPEL_CLAUDE_PACKAGE_NAME: pin.packageName,
|
|
275
287
|
IMPEL_CLAUDE_PACKAGE_VERSION: pin.packageVersion,
|
|
@@ -289,8 +301,8 @@ function installPinnedWindowsClaudeMsix(packagePath, environment, pin, architect
|
|
|
289
301
|
}
|
|
290
302
|
|
|
291
303
|
function windowsClaudeCacheRoot(homeDir, environment, version, architecture) {
|
|
292
|
-
const appsRoot = environmentValue(environment, "
|
|
293
|
-
|| path.join(homeDir, ".config",
|
|
304
|
+
const appsRoot = environmentValue(environment, brandedEnvironmentName("APP_HOME"))
|
|
305
|
+
|| path.join(homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "apps");
|
|
294
306
|
return path.join(appsRoot, "vendor-cache", "claude", version, architecture);
|
|
295
307
|
}
|
|
296
308
|
|
|
@@ -363,6 +375,7 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
|
|
|
363
375
|
const io = {
|
|
364
376
|
environment: process.env,
|
|
365
377
|
find: findWindowsChatGPTApp,
|
|
378
|
+
logger: console,
|
|
366
379
|
run: spawnSync,
|
|
367
380
|
...dependencies,
|
|
368
381
|
};
|
|
@@ -388,10 +401,25 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
|
|
|
388
401
|
result = { status: null, error };
|
|
389
402
|
}
|
|
390
403
|
const binary = io.find(io.environment) || before;
|
|
391
|
-
|
|
404
|
+
// winget uses an error HRESULT when `upgrade` finds an installed package
|
|
405
|
+
// with no newer Store release. That is a healthy idempotent update result,
|
|
406
|
+
// not an installation failure (APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE).
|
|
407
|
+
const alreadyUpToDate = Boolean(
|
|
408
|
+
shouldUpdate
|
|
409
|
+
&& binary
|
|
410
|
+
&& !result?.error
|
|
411
|
+
&& Number.isInteger(result?.status)
|
|
412
|
+
&& (result.status >>> 0) === WINDOWS_WINGET_UPDATE_NOT_APPLICABLE,
|
|
413
|
+
);
|
|
414
|
+
if (alreadyUpToDate) {
|
|
415
|
+
io.logger.log("ChatGPT/Codex: Microsoft Store package already up to date.");
|
|
416
|
+
}
|
|
417
|
+
const succeeded = (result?.status === 0 && !result?.error) || alreadyUpToDate;
|
|
392
418
|
return {
|
|
393
419
|
binary,
|
|
394
|
-
action:
|
|
420
|
+
action: alreadyUpToDate
|
|
421
|
+
? "existing"
|
|
422
|
+
: succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
|
|
395
423
|
result,
|
|
396
424
|
};
|
|
397
425
|
}
|