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.
@@ -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 installed = await context.installVendorApp(input.target);
504
- return installed === false
505
- ? result("failed", `The ${input.target} vendor app did not install cleanly.`)
506
- : result("succeeded", `The ${input.target} vendor app installation completed.`);
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: process.stdin, output: process.stdout });
46
- rl.question(question, (answer) => {
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
- resolve(answer.trim());
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
  }
@@ -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", "impel", "cli", "tenants"),
66
- path.join(homeDir, ".config", "impel", "apps", "tenants"),
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
+ }
@@ -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 = fileURLToPath(new URL("../bin/impel.js", import.meta.url));
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 GLOBAL_INSTALL_RE = /[\\/]node_modules[\\/]impel-cli[\\/]bin[\\/]impel\.js$/iu;
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, "Impel", "bin", "impel-entry.cjs");
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. */
@@ -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" ? "Impel Claude" : "Impel ChatGPT";
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", "Impel");
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,
@@ -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 impel-cli — stable Impel entry point.
31
+ // Managed by ${RUNTIME_BRAND.cli.command}-cli — stable ${RUNTIME_BRAND.product.displayName} entry point.
28
32
  //
29
- // Vendor apps (Impel Claude, Impel ChatGPT) persist absolute invocations of
30
- // this file in their managed profiles. It resolves the current impel-cli
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("node_modules", "impel-cli", "bin", "impel.js");
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.IMPEL_ENTRYPOINT_WAIT_MS || "", 10);
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, "src", "cli.js"))) return false;
77
- return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).name === "impel-cli";
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
- "impel-entry: no impel-cli install found (checked " + candidateCliPaths().join("; ") + "). " +
110
- "Run \`npm install -g impel-cli\`, then \`impel setup\`.\\n"
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
- "impel-entry: could not start " + cli + " (" +
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
- "impel-entry: " + (error && error.message ? error.message : String(error)) + "\\n"
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
- `impel: could not refresh the stable Windows entry point (${redactSecretText(error?.message || error)})`
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 = "impel_tenant_";
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("impel_pat_")) {
89
- throw new Error("an Impel PAT is required to create a tenant credential");
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("not authenticated; run `impel setup` (or `impel auth`) first");
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("not authenticated; run `impel setup` (or `impel auth`) first");
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 {
@@ -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
- "Impel",
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, "IMPEL_CLAUDE_APP_BIN");
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, "IMPEL_CHATGPT_APP_BIN")
107
- || environmentValue(environment, "IMPEL_CODEX_APP_BIN");
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, "IMPEL_APP_HOME")
293
- || path.join(homeDir, ".config", "impel", "apps");
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
- const succeeded = result?.status === 0 && !result?.error;
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: succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
420
+ action: alreadyUpToDate
421
+ ? "existing"
422
+ : succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
395
423
  result,
396
424
  };
397
425
  }