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.
@@ -91,13 +91,18 @@ export async function cmdConverge(argv = [], overrides = {}) {
91
91
  // answer (not unref'd) so the auto-decline reliably fires even when the
92
92
  // prompt is the only thing keeping the event loop alive.
93
93
  const allowed = confirmed(await new Promise((resolve) => {
94
- const timer = setTimeout(() => resolve(""), io.vendorPromptTimeoutMs ?? 60_000);
94
+ const controller = new AbortController();
95
+ const timer = setTimeout(() => {
96
+ controller.abort();
97
+ resolve("");
98
+ }, io.vendorPromptTimeoutMs ?? 60_000);
95
99
  const settle = (answer) => {
96
100
  clearTimeout(timer);
97
101
  resolve(answer);
98
102
  };
99
103
  Promise.resolve(io.promptText(
100
104
  `Allow the verified ${label} vendor ${context.mode === "update" ? "update" : "installation"}? [y/N] (auto-N in 60s) `,
105
+ { signal: controller.signal },
101
106
  )).then(settle, () => settle(""));
102
107
  }));
103
108
  vendorAppDecisions.set(target, allowed);
@@ -272,7 +277,14 @@ export async function cmdConverge(argv = [], overrides = {}) {
272
277
  || product === target
273
278
  || (target === "codex" && product === "chatgpt")
274
279
  ));
275
- return vendorAppTargetReady(retryReport, target);
280
+ const installed = vendorAppTargetReady(retryReport, target);
281
+ return {
282
+ installed,
283
+ error: installed
284
+ ? null
285
+ : retryReport?.tenants?.[0]?.errors?.join("; ")
286
+ || `The ${target} vendor app did not reach a verified state.`,
287
+ };
276
288
  },
277
289
  },
278
290
  }, overrides.recoveryOverrides || {});
@@ -21,6 +21,7 @@ import {
21
21
  nativeSpawnInvocation,
22
22
  resolveNativeBinary,
23
23
  } from "../nativeProcess.js";
24
+ import { RUNTIME_BRAND } from "../runtimeBrand.js";
24
25
 
25
26
  const CLAUDE_DIRECT_AUTH_ENV = [
26
27
  "ANTHROPIC_API_KEY",
@@ -53,6 +54,7 @@ const IMPEL_CODEX_RUNTIME_OVERRIDES = [
53
54
  ];
54
55
 
55
56
  export function impelLaunchArguments(tool, argv) {
57
+ if (!RUNTIME_BRAND.features.agents) return [...argv];
56
58
  if (tool === "claude") {
57
59
  return ["--append-system-prompt", IMPEL_SPECIALIST_DELEGATION_INSTRUCTIONS, ...argv];
58
60
  }
@@ -120,7 +122,7 @@ export async function cmdLaunch(tool, argv) {
120
122
  process.exitCode = 1;
121
123
  return;
122
124
  }
123
- maybePrintUpdateNotice();
125
+ if (RUNTIME_BRAND.cli.packageName === "impel-cli") maybePrintUpdateNotice();
124
126
 
125
127
  const gatewayUrl = normalizeGatewayUrl(config.gatewayUrl || resolveDefaultGateway());
126
128
  const crossAppModels = tool === "claude" && crossAppModelsEnabled(config);
@@ -168,13 +170,15 @@ export async function cmdLaunch(tool, argv) {
168
170
 
169
171
  // Refresh at most every six hours. A network/catalog failure must not block
170
172
  // the isolated client; the last successfully generated agents remain usable.
171
- await syncAgentProfilesSafe({
172
- profiles: [agentProfile],
173
- gatewayUrl,
174
- credential: gatewayCredential,
175
- tenantId,
176
- staleOnly: true,
177
- });
173
+ if (RUNTIME_BRAND.features.agents) {
174
+ await syncAgentProfilesSafe({
175
+ profiles: [agentProfile],
176
+ gatewayUrl,
177
+ credential: gatewayCredential,
178
+ tenantId,
179
+ staleOnly: true,
180
+ });
181
+ }
178
182
 
179
183
  // Both vendor CLIs shell out to `git` for plugin/marketplace operations; on
180
184
  // a fresh Windows machine the installed git (typically the Impel-managed
@@ -12,18 +12,21 @@ import {
12
12
  PAT_SCOPE_CODEX,
13
13
  PAT_SCOPE_TASKS,
14
14
  } from "../tenants.js";
15
+ import { RUNTIME_BRAND } from "../runtimeBrand.js";
15
16
 
16
17
  const AGENT_PAT_LABEL_PREFIX = "Agent · ";
17
18
  const MAX_PAT_LABEL_LENGTH = 120;
18
19
  const MAX_PAT_TTL_DAYS = 365;
19
20
  const PAT_COLLECTION_PATH = "/api/cli/pats";
20
- const PAT_TOKEN_RE = /^impel_pat_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u;
21
- const PAT_TOKEN_INPUT_RE = /^impel_pat_([A-Za-z0-9_-]{8,128})\.[A-Za-z0-9_-]+$/u;
21
+ const escapedPATPrefix = RUNTIME_BRAND.auth.patPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
22
+ const PAT_TOKEN_RE = new RegExp(`^${escapedPATPrefix}[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$`, "u");
23
+ const PAT_TOKEN_INPUT_RE = new RegExp(`^${escapedPATPrefix}([A-Za-z0-9_-]{8,128})\\.[A-Za-z0-9_-]+$`, "u");
22
24
  const PAT_TOKEN_ID_RE = /^[A-Za-z0-9_-]{8,128}$/u;
23
25
  const AGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,111}$/u;
24
26
  const CONTROL_CHARACTER_RE = /[\u0000-\u001F\u007F-\u009F]/u;
25
27
  const PAT_SCOPES = [PAT_SCOPE_CLAUDE, PAT_SCOPE_CODEX, PAT_SCOPE_TASKS];
26
28
  const PAT_SCOPE_SET = new Set(PAT_SCOPES);
29
+ const PAT_COMMAND = `${RUNTIME_BRAND.cli.command} pat`;
27
30
  const FLAG_NAMES = new Set([
28
31
  "agent",
29
32
  "app",
@@ -50,16 +53,19 @@ const CREATE_FLAG_NAMES = new Set([
50
53
  ]);
51
54
  const REVOKE_FLAG_NAMES = new Set(["app", "help", "json", "yes"]);
52
55
 
53
- const HELP = `impel pat - manage personal access tokens through Impel
56
+ const AGENT_PAT_HELP = RUNTIME_BRAND.capabilities.agentPats
57
+ ? `\n ${RUNTIME_BRAND.cli.command} pat create --agent <agent-id> [options]`
58
+ : "";
59
+ const HELP = `${RUNTIME_BRAND.cli.command} pat - manage personal access tokens through ${RUNTIME_BRAND.product.displayName}
54
60
 
55
61
  Usage:
56
- impel pat create --label <label> [options]
57
- impel pat create --agent <agent-id> [options]
58
- impel pat revoke <token-id> --yes [options]
62
+ ${RUNTIME_BRAND.cli.command} pat create --label <label> [options]
63
+ ${AGENT_PAT_HELP}
64
+ ${RUNTIME_BRAND.cli.command} pat revoke <token-id> --yes [options]
59
65
 
60
66
  Aliases:
61
- impel pats ...
62
- impel pat mint ...
67
+ ${RUNTIME_BRAND.cli.command} pats ...
68
+ ${RUNTIME_BRAND.cli.command} pat mint ...
63
69
 
64
70
  Options:
65
71
  --org <org> Target org. Defaults to the selected tenant.
@@ -71,7 +77,7 @@ Options:
71
77
  --json Print the result as JSON.
72
78
  --yes Confirm irreversible PAT revocation.
73
79
 
74
- The stored PAT authenticates this request. The control plane verifies the owner,
80
+ The stored ${RUNTIME_BRAND.product.displayName} PAT authenticates this request. The control plane verifies the owner,
75
81
  organization membership, requested scopes, and agent access before identity mints
76
82
  the new secret or revokes the selected token. New PATs are shown once and are not
77
83
  stored automatically. Revoke accepts a token id or a full PAT and never echoes a secret.
@@ -101,14 +107,14 @@ function flagSpec() {
101
107
  function rejectUnsupportedFlags(flags, allowed, action) {
102
108
  const unsupported = Object.keys(flags).find((name) => !allowed.has(name));
103
109
  if (unsupported) {
104
- fail(`impel pat ${action}: option --${redactSecretText(unsupported)} is not supported.`);
110
+ fail(`${PAT_COMMAND} ${action}: option --${redactSecretText(unsupported)} is not supported.`);
105
111
  }
106
112
  }
107
113
 
108
114
  function requireConfig(flags) {
109
115
  const config = loadConfig();
110
116
  if (!config?.pat) {
111
- fail("impel pat: not authenticated. Run `impel setup` (or `impel auth`) first.");
117
+ fail(`${RUNTIME_BRAND.cli.command} pat: not authenticated. Run \`${RUNTIME_BRAND.cli.command} setup\` (or \`${RUNTIME_BRAND.cli.command} auth\`) first.`);
112
118
  }
113
119
 
114
120
  const appUrl = normalizeGatewayUrl(flags.app || config.appUrl || resolveDefaultAppUrl());
@@ -116,7 +122,7 @@ function requireConfig(flags) {
116
122
  try {
117
123
  parsed = new URL(appUrl);
118
124
  } catch {
119
- fail("impel pat: --app must be an HTTP or HTTPS URL.");
125
+ fail(`${PAT_COMMAND}: --app must be an HTTP or HTTPS URL.`);
120
126
  }
121
127
  if (
122
128
  (parsed.protocol !== "http:" && parsed.protocol !== "https:")
@@ -126,7 +132,7 @@ function requireConfig(flags) {
126
132
  || parsed.search
127
133
  || parsed.hash
128
134
  ) {
129
- fail("impel pat: --app must be a bare HTTP or HTTPS origin without credentials, a path, query, or fragment.");
135
+ fail(`${PAT_COMMAND}: --app must be a bare HTTP or HTTPS origin without credentials, a path, query, or fragment.`);
130
136
  }
131
137
  return { appUrl, config };
132
138
  }
@@ -138,11 +144,11 @@ function requestedScopes(value) {
138
144
  .map((scope) => scope.trim())
139
145
  .filter(Boolean);
140
146
  if (requested.length === 0) {
141
- fail("impel pat: --scopes must contain at least one scope.");
147
+ fail(`${PAT_COMMAND}: --scopes must contain at least one scope.`);
142
148
  }
143
149
  for (const scope of requested) {
144
150
  if (!PAT_SCOPE_SET.has(scope)) {
145
- fail(`impel pat: unknown scope "${redactSecretText(scope)}".`);
151
+ fail(`${PAT_COMMAND}: unknown scope "${redactSecretText(scope)}".`);
146
152
  }
147
153
  }
148
154
  const requestedSet = new Set(requested);
@@ -152,23 +158,23 @@ function requestedScopes(value) {
152
158
  function requestedTtlDays(value) {
153
159
  if (value === undefined) return undefined;
154
160
  if (!/^\d+$/u.test(String(value))) {
155
- fail(`impel pat: --ttl-days must be an integer from 1 to ${MAX_PAT_TTL_DAYS}.`);
161
+ fail(`${PAT_COMMAND}: --ttl-days must be an integer from 1 to ${MAX_PAT_TTL_DAYS}.`);
156
162
  }
157
163
  const days = Number(value);
158
164
  if (!Number.isSafeInteger(days) || days < 1 || days > MAX_PAT_TTL_DAYS) {
159
- fail(`impel pat: --ttl-days must be an integer from 1 to ${MAX_PAT_TTL_DAYS}.`);
165
+ fail(`${PAT_COMMAND}: --ttl-days must be an integer from 1 to ${MAX_PAT_TTL_DAYS}.`);
160
166
  }
161
167
  return days;
162
168
  }
163
169
 
164
170
  function requestedTokenId(value) {
165
171
  if (value === undefined) {
166
- fail("impel pat revoke: missing token id.");
172
+ fail(`${PAT_COMMAND} revoke: missing token id.`);
167
173
  }
168
174
  const requested = String(value).trim();
169
175
  const tokenId = PAT_TOKEN_INPUT_RE.exec(requested)?.[1] || requested;
170
176
  if (!PAT_TOKEN_ID_RE.test(tokenId)) {
171
- fail("impel pat revoke: expected a valid token id or full Impel PAT.");
177
+ fail(`${PAT_COMMAND} revoke: expected a valid token id or full ${RUNTIME_BRAND.product.displayName} PAT.`);
172
178
  }
173
179
  return tokenId;
174
180
  }
@@ -177,31 +183,34 @@ function requestedSubject(flags) {
177
183
  const agentId = flags.agent === undefined ? null : String(flags.agent).trim();
178
184
  const personalLabel = flags.label === undefined ? null : String(flags.label).trim();
179
185
  if (agentId !== null && personalLabel !== null) {
180
- fail("impel pat create: use either --label for a personal PAT or --agent for an agent PAT, not both.");
186
+ fail(`${PAT_COMMAND} create: use either --label for a personal PAT or --agent for an agent PAT, not both.`);
181
187
  }
182
188
  if (agentId !== null) {
189
+ if (!RUNTIME_BRAND.capabilities.agentPats) {
190
+ fail(`${PAT_COMMAND} create: agent PATs are not available in this branded CLI.`);
191
+ }
183
192
  if (!AGENT_ID_RE.test(agentId)) {
184
- fail("impel pat create: --agent must be a valid agent id (letters, numbers, dots, dashes, or underscores).");
193
+ fail(`${PAT_COMMAND} create: --agent must be a valid agent id (letters, numbers, dots, dashes, or underscores).`);
185
194
  }
186
195
  const label = `${AGENT_PAT_LABEL_PREFIX}${agentId}`;
187
196
  if (label.length > MAX_PAT_LABEL_LENGTH) {
188
- fail(`impel pat create: the generated agent label exceeds ${MAX_PAT_LABEL_LENGTH} characters.`);
197
+ fail(`${PAT_COMMAND} create: the generated agent label exceeds ${MAX_PAT_LABEL_LENGTH} characters.`);
189
198
  }
190
199
  return { agentId, kind: "agent", label };
191
200
  }
192
201
 
193
202
  if (!personalLabel) {
194
- fail("impel pat create: --label is required for a personal PAT; use --agent <id> for an agent PAT.");
203
+ fail(`${PAT_COMMAND} create: --label is required for a personal PAT; use --agent <id> for an agent PAT.`);
195
204
  }
196
205
  if (personalLabel.length > MAX_PAT_LABEL_LENGTH || CONTROL_CHARACTER_RE.test(personalLabel)) {
197
- fail(`impel pat create: --label must be 1 to ${MAX_PAT_LABEL_LENGTH} characters without control characters.`);
206
+ fail(`${PAT_COMMAND} create: --label must be 1 to ${MAX_PAT_LABEL_LENGTH} characters without control characters.`);
198
207
  }
199
208
  return { agentId: null, kind: "personal", label: personalLabel };
200
209
  }
201
210
 
202
211
  async function requestedOrgId(flags, config, appUrl) {
203
212
  if (flags.org !== undefined && flags.tenant !== undefined && flags.org !== flags.tenant) {
204
- fail("impel pat: --org and --tenant must name the same org when both are passed.");
213
+ fail(`${PAT_COMMAND}: --org and --tenant must name the same org when both are passed.`);
205
214
  }
206
215
  const requested = flags.org ?? flags.tenant;
207
216
  try {
@@ -210,7 +219,7 @@ async function requestedOrgId(flags, config, appUrl) {
210
219
  const listing = await fetchTenants({ ...config, appUrl });
211
220
  return listing.defaultTenantId;
212
221
  } catch (error) {
213
- fail(`impel pat: could not resolve the target org: ${redactSecretText(error?.message || error)}`);
222
+ fail(`${PAT_COMMAND}: could not resolve the target org: ${redactSecretText(error?.message || error)}`);
214
223
  }
215
224
  }
216
225
 
@@ -270,7 +279,7 @@ async function requestControlPlane({ appUrl, currentPat, path, method, body, fea
270
279
  });
271
280
  } catch (error) {
272
281
  const message = error?.name === "AbortError" ? "request timed out" : error?.message || error;
273
- fail(`impel pat: could not reach ${appUrl}: ${redactSecretText(message)}`);
282
+ fail(`${PAT_COMMAND}: could not reach ${appUrl}: ${redactSecretText(message)}`);
274
283
  } finally {
275
284
  clearTimeout(timeout);
276
285
  }
@@ -284,10 +293,10 @@ async function requestControlPlane({ appUrl, currentPat, path, method, body, fea
284
293
  }
285
294
  if (!response.ok) {
286
295
  if (response.status === 404 && !payload?.error) {
287
- fail(`impel pat: PAT ${feature} is not available on ${appUrl}; update the Impel control plane and try again.`);
296
+ fail(`${PAT_COMMAND}: PAT ${feature} is not available on ${appUrl}; update the ${RUNTIME_BRAND.product.displayName} control plane and try again.`);
288
297
  }
289
298
  const message = redactSecretText(payload?.error || `${response.status} ${response.statusText}`.trim());
290
- fail(`impel pat: ${message}`);
299
+ fail(`${PAT_COMMAND}: ${message}`);
291
300
  }
292
301
  return payload;
293
302
  }
@@ -303,7 +312,7 @@ async function createPat({ appUrl, currentPat, body }) {
303
312
  });
304
313
  const created = normalizeCreatedPat(payload, body);
305
314
  if (!created) {
306
- fail("impel pat: the control plane returned an invalid PAT creation response; the secret was not displayed.");
315
+ fail(`${PAT_COMMAND}: the control plane returned an invalid PAT creation response; the secret was not displayed.`);
307
316
  }
308
317
  return created;
309
318
  }
@@ -338,7 +347,7 @@ function printCreatedPat(created, subject, json) {
338
347
  console.log(`Scopes: ${created.scopes.join(", ") || "none"}`);
339
348
  console.log(`Expires: ${created.expiresAt === null ? "never" : new Date(created.expiresAt).toISOString()}`);
340
349
  console.log("");
341
- console.log("Copy this token now. Impel cannot show the secret again:");
350
+ console.log(`Copy this token now. ${RUNTIME_BRAND.product.displayName} cannot show the secret again:`);
342
351
  console.log(created.token);
343
352
  }
344
353
 
@@ -357,8 +366,8 @@ export async function cmdPat(argv) {
357
366
  return;
358
367
  }
359
368
  if (action !== "create" && action !== "mint" && action !== "revoke") {
360
- console.error(`impel pat: unknown subcommand "${redactSecretText(action)}".`);
361
- console.error(" Use `impel pat create --label <label>`, `impel pat create --agent <agent-id>`, or `impel pat revoke <token-id> --yes`.");
369
+ console.error(`${PAT_COMMAND}: unknown subcommand "${redactSecretText(action)}".`);
370
+ console.error(` Use \`${PAT_COMMAND} create --label <label>\`, \`${PAT_COMMAND} create --agent <agent-id>\`, or \`${PAT_COMMAND} revoke <token-id> --yes\`.`);
362
371
  process.exitCode = 1;
363
372
  return;
364
373
  }
@@ -370,16 +379,16 @@ export async function cmdPat(argv) {
370
379
  return;
371
380
  }
372
381
  const unknownFlag = Object.keys(flags).find((name) => !FLAG_NAMES.has(name));
373
- if (unknownFlag) fail(`impel pat: unknown option --${redactSecretText(unknownFlag)}.`);
382
+ if (unknownFlag) fail(`${PAT_COMMAND}: unknown option --${redactSecretText(unknownFlag)}.`);
374
383
 
375
384
  if (action === "revoke") {
376
385
  rejectUnsupportedFlags(flags, REVOKE_FLAG_NAMES, "revoke");
377
386
  const tokenId = requestedTokenId(positionals[0]);
378
387
  if (positionals.length > 1) {
379
- fail(`impel pat revoke: unexpected argument "${redactSecretText(positionals[1])}".`);
388
+ fail(`${PAT_COMMAND} revoke: unexpected argument "${redactSecretText(positionals[1])}".`);
380
389
  }
381
390
  if (flags.yes !== true) {
382
- fail("impel pat revoke: refusing to revoke without --yes.");
391
+ fail(`${PAT_COMMAND} revoke: refusing to revoke without --yes.`);
383
392
  }
384
393
  const { appUrl, config } = requireConfig(flags);
385
394
  const revoked = await revokePat({ appUrl, currentPat: config.pat, tokenId });
@@ -389,7 +398,7 @@ export async function cmdPat(argv) {
389
398
 
390
399
  rejectUnsupportedFlags(flags, CREATE_FLAG_NAMES, "create");
391
400
  if (positionals.length > 0) {
392
- fail(`impel pat create: unexpected argument "${redactSecretText(positionals[0])}".`);
401
+ fail(`${PAT_COMMAND} create: unexpected argument "${redactSecretText(positionals[0])}".`);
393
402
  }
394
403
 
395
404
  const subject = requestedSubject(flags);
@@ -22,8 +22,9 @@ import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
22
22
  import { runInstallRecovery } from "../installRecovery/engine.js";
23
23
  import { refreshUpdateCache, updateNoticeLine } from "../updates.js";
24
24
  import { restoreNativeProfiles } from "./use.js";
25
+ import { brandedEnvironmentName, brandedText, RUNTIME_BRAND } from "../runtimeBrand.js";
25
26
 
26
- const HELP = `impel setup - prepare every accessible Impel tenant
27
+ const HELP = brandedText(`impel setup - prepare every accessible Impel tenant
27
28
 
28
29
  Stores your Personal Access Token, discovers every tenant you can access,
29
30
  prepares each tenant's isolated Claude and Codex CLI profiles, and installs
@@ -37,7 +38,7 @@ Usage:
37
38
  impel setup --skip-apps Skip desktop apps
38
39
  impel setup --skip-clis Do not install missing vendor CLIs
39
40
  impel setup --no-recovery Disable automatic install recovery
40
- `;
41
+ `);
41
42
 
42
43
  /** Kept as a pure compatibility helper for callers/tests. */
43
44
  export function resolveTenantChoice(listing, { requested = null, answer = null, currentTenantId = null } = {}) {
@@ -187,7 +188,7 @@ export async function cmdSetup(argv, overrides = {}) {
187
188
  ? (await io.promptSecret(`Token (${maskSecret(existing.pat)} stored; Enter keeps it): `)) || existing.pat
188
189
  : existing.pat;
189
190
  } else if (!pat && io.isTTY) {
190
- pat = await io.promptSecret("Impel Personal Access Token (impel_pat_...): ");
191
+ pat = await io.promptSecret(`${RUNTIME_BRAND.product.displayName} Personal Access Token (${RUNTIME_BRAND.auth.patPrefix}...): `);
191
192
  }
192
193
  if (!pat) {
193
194
  console.error("impel setup: no token provided; pass --pat or run interactively.");
@@ -230,7 +231,7 @@ export async function cmdSetup(argv, overrides = {}) {
230
231
  // over the network, and the registry fetch is bounded (10s) and best-effort.
231
232
  // TTY-gated like maybePrintUpdateNotice: the nudge is for a human who can
232
233
  // stop and update, not for scripted/CI setups.
233
- if (io.isTTY && !["1", "true"].includes(io.environment.IMPEL_SKIP_UPDATE_CHECK)) {
234
+ if (RUNTIME_BRAND.cli.packageName === "impel-cli" && io.isTTY && !["1", "true"].includes(io.environment[brandedEnvironmentName("SKIP_UPDATE_CHECK")])) {
234
235
  try {
235
236
  const updateCache = await io.refreshUpdateCache();
236
237
  const updateNotice = io.updateNoticeLine({ cache: updateCache });
@@ -242,10 +243,12 @@ export async function cmdSetup(argv, overrides = {}) {
242
243
  // Never block setup on the update check.
243
244
  }
244
245
  }
245
- try {
246
- io.restoreNativeProfiles({ quiet: true });
247
- } catch (error) {
248
- console.warn(`Legacy native-profile cleanup was skipped safely (${redactSecretText(error?.message || error)}).`);
246
+ if (RUNTIME_BRAND.product.id === "impel") {
247
+ try {
248
+ io.restoreNativeProfiles({ quiet: true });
249
+ } catch (error) {
250
+ console.warn(`Legacy native-profile cleanup was skipped safely (${redactSecretText(error?.message || error)}).`);
251
+ }
249
252
  }
250
253
 
251
254
  const vendorAppDecisions = new Map();
@@ -453,7 +456,14 @@ export async function cmdSetup(argv, overrides = {}) {
453
456
  || product === target
454
457
  || (target === "codex" && product === "chatgpt")
455
458
  ));
456
- return vendorAppTargetReady(retryReport, target);
459
+ const installed = vendorAppTargetReady(retryReport, target);
460
+ return {
461
+ installed,
462
+ error: installed
463
+ ? null
464
+ : retryReport?.tenants?.[0]?.errors?.join("; ")
465
+ || `The ${target} vendor app did not reach a verified state.`,
466
+ };
457
467
  },
458
468
  },
459
469
  }, overrides.recoveryOverrides || {});
@@ -15,6 +15,7 @@ import {
15
15
  } from "../tenants.js";
16
16
  import { installedVersion, maybePrintUpdateNotice } from "../updates.js";
17
17
  import { windowsClaudeUserData } from "../windowsApps.js";
18
+ import { RUNTIME_BRAND } from "../runtimeBrand.js";
18
19
 
19
20
  function readShellManifest(paths) {
20
21
  try {
@@ -58,7 +59,7 @@ function desktopReadiness(tenant, {
58
59
  if (platform === "win32" && appData) {
59
60
  const shortcutPath = path.win32.join(
60
61
  appData,
61
- "Microsoft", "Windows", "Start Menu", "Programs", "Impel",
62
+ "Microsoft", "Windows", "Start Menu", "Programs", RUNTIME_BRAND.apps.windowsStartMenuFolder,
62
63
  windowsTenantShortcutName(product, tenant.id, tenant.name),
63
64
  );
64
65
  shellReady = appReady && existsSync(shortcutPath);
@@ -90,10 +91,11 @@ export async function cmdStatus(overrides = {}) {
90
91
  ...overrides,
91
92
  };
92
93
  const config = io.loadConfig();
93
- console.log(`impel-cli: v${io.installedVersion() || "?"}`);
94
+ const version = process.env.IMPEL_CLI_EXTENSION_VERSION || io.installedVersion() || "?";
95
+ console.log(`${RUNTIME_BRAND.cli.command}-cli: v${version}`);
94
96
  console.log(`Authentication: ${config?.pat ? `configured (${maskSecret(config.pat)})` : "not configured - run `impel setup`"}`);
95
97
  if (!config?.pat) {
96
- io.maybePrintUpdateNotice();
98
+ if (RUNTIME_BRAND.cli.packageName === "impel-cli") io.maybePrintUpdateNotice();
97
99
  return;
98
100
  }
99
101
 
@@ -105,7 +107,7 @@ export async function cmdStatus(overrides = {}) {
105
107
  console.log(`Authentication check: unavailable (${error?.message || error})`);
106
108
  console.log(`Current CLI tenant: ${config.tenantId || "not selected"}`);
107
109
  console.log("Run `impel setup` to refresh authentication, then `impel update` to repair local tenants.");
108
- io.maybePrintUpdateNotice();
110
+ if (RUNTIME_BRAND.cli.packageName === "impel-cli") io.maybePrintUpdateNotice();
109
111
  return;
110
112
  }
111
113
 
@@ -141,5 +143,5 @@ export async function cmdStatus(overrides = {}) {
141
143
  );
142
144
  }
143
145
  if (incomplete) console.log("Repair or finish missing tenant surfaces with: impel update");
144
- io.maybePrintUpdateNotice();
146
+ if (RUNTIME_BRAND.cli.packageName === "impel-cli") io.maybePrintUpdateNotice();
145
147
  }
@@ -1,6 +1,7 @@
1
1
  import { loadConfig } from "../config.js";
2
2
  import { parseFlags } from "../args.js";
3
3
  import { ensureTenantSelection, normalizeTenantId, tenantCredential } from "../tenants.js";
4
+ import { RUNTIME_BRAND } from "../runtimeBrand.js";
4
5
 
5
6
  // This is the `apiKeyHelper` / auth-command contract: stdout (and only
6
7
  // stdout) must be exactly the bearer token, nothing else. Both Claude Code's
@@ -9,7 +10,7 @@ export async function cmdToken(argv = []) {
9
10
  const { flags } = parseFlags(argv, { tenant: { type: "string" } });
10
11
  const config = loadConfig();
11
12
  if (!config?.pat) {
12
- process.stderr.write("impel: not authenticated. Run `impel setup` (or `impel auth`) first.\n");
13
+ process.stderr.write(`${RUNTIME_BRAND.cli.command}: not authenticated. Run \`${RUNTIME_BRAND.cli.command} setup\` (or \`${RUNTIME_BRAND.cli.command} auth\`) first.\n`);
13
14
  process.exitCode = 1;
14
15
  return;
15
16
  }
@@ -19,7 +20,7 @@ export async function cmdToken(argv = []) {
19
20
  : (await ensureTenantSelection(config)).tenantId;
20
21
  process.stdout.write(`${tenantCredential(config.pat, tenantId)}\n`);
21
22
  } catch (error) {
22
- process.stderr.write(`impel: ${error.message}\n`);
23
+ process.stderr.write(`${RUNTIME_BRAND.cli.command}: ${error.message}\n`);
23
24
  process.exitCode = 1;
24
25
  }
25
26
  }
package/src/config.js CHANGED
@@ -15,13 +15,15 @@ import fs from "node:fs";
15
15
  import os from "node:os";
16
16
  import path from "node:path";
17
17
 
18
+ import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
18
19
  import { renameWithWindowsRetry } from "./windowsFs.js";
19
20
 
20
- export const DEFAULT_GATEWAY_URL = "https://gateway.useimpel.com";
21
- export const DEFAULT_APP_URL = "https://www.useimpel.com";
21
+ export const DEFAULT_GATEWAY_URL = RUNTIME_BRAND.gateway.defaultOrigin;
22
+ export const DEFAULT_APP_URL = RUNTIME_BRAND.controlPlane.defaultOrigin;
22
23
  const LEGACY_GATEWAY_URLS = new Set(["https://gateway.useimpel.ai"]);
23
24
 
24
- export const CONFIG_DIR = path.join(os.homedir(), ".config", "impel");
25
+ export const CONFIG_DIR = process.env[brandedEnvironmentName("CONFIG_DIR")]
26
+ || path.join(os.homedir(), ".config", RUNTIME_BRAND.cli.configNamespace);
25
27
  export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
26
28
 
27
29
  /** Hidden, default-off experiment. Only the exact boolean true enables it. */
@@ -31,12 +33,12 @@ export function crossAppModelsEnabled(config) {
31
33
 
32
34
  /** Resolve the gateway URL to use when nothing is stored yet: env var, else the canonical gateway. */
33
35
  export function resolveDefaultGateway() {
34
- return normalizeGatewayUrl(process.env.IMPEL_GATEWAY_URL || DEFAULT_GATEWAY_URL);
36
+ return normalizeGatewayUrl(process.env[brandedEnvironmentName("GATEWAY_URL")] || DEFAULT_GATEWAY_URL);
35
37
  }
36
38
 
37
39
  /** Resolve the app/control-plane URL used by task APIs. */
38
40
  export function resolveDefaultAppUrl() {
39
- return normalizeGatewayUrl(process.env.IMPEL_APP_URL || DEFAULT_APP_URL);
41
+ return normalizeGatewayUrl(process.env[brandedEnvironmentName("APP_URL")] || DEFAULT_APP_URL);
40
42
  }
41
43
 
42
44
  /** Strip trailing slashes so `${gatewayUrl}/anthropic` never ends up with a double slash. */
@@ -60,7 +62,7 @@ export function loadConfig() {
60
62
  return config;
61
63
  } catch {
62
64
  throw new Error(
63
- `${CONFIG_PATH} exists but isn't valid JSON. Fix or delete it, then run \`impel auth\` again.`
65
+ `${CONFIG_PATH} exists but isn't valid JSON. Fix or delete it, then run \`${RUNTIME_BRAND.cli.command} auth\` again.`
64
66
  );
65
67
  }
66
68
  }
@@ -96,16 +98,18 @@ export function maskSecret(secret) {
96
98
  return `${secret.slice(0, 10)}...${secret.slice(-4)}`;
97
99
  }
98
100
 
99
- const TENANT_CREDENTIAL_RE = /impel_tenant_[A-Za-z0-9_-]+\.impel_pat_[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?/gu;
100
- const PAT_RE = /impel_pat_[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?/gu;
101
+ const escapedPATPrefix = RUNTIME_BRAND.auth.patPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
102
+ const escapedTenantPrefix = RUNTIME_BRAND.auth.tenantPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
103
+ const TENANT_CREDENTIAL_RE = new RegExp(`${escapedTenantPrefix}[A-Za-z0-9_-]+\\.${escapedPATPrefix}[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)?`, "gu");
104
+ const PAT_RE = new RegExp(`${escapedPATPrefix}[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)?`, "gu");
101
105
  const ANSI_ESCAPE_RE = /\u001B(?:\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/gu;
102
106
  const TERMINAL_CONTROL_RE = /[\u0000-\u001F\u007F-\u009F]/gu;
103
107
 
104
108
  /** Redact Impel bearer credentials without changing JSON/NDJSON whitespace. */
105
109
  export function redactCredentialText(value) {
106
110
  return String(value ?? "")
107
- .replace(TENANT_CREDENTIAL_RE, "[REDACTED IMPEL CREDENTIAL]")
108
- .replace(PAT_RE, "[REDACTED IMPEL CREDENTIAL]");
111
+ .replace(TENANT_CREDENTIAL_RE, `[REDACTED ${RUNTIME_BRAND.product.displayName.toUpperCase()} CREDENTIAL]`)
112
+ .replace(PAT_RE, `[REDACTED ${RUNTIME_BRAND.product.displayName.toUpperCase()} CREDENTIAL]`);
109
113
  }
110
114
 
111
115
  /** Remove credentials and terminal control sequences from untrusted text. */
@@ -0,0 +1,81 @@
1
+ import path from "node:path";
2
+
3
+ import { main as upstreamMain } from "../cli.js";
4
+ import { brandedText, RUNTIME_BRAND, validateRuntimeBrand } from "../runtimeBrand.js";
5
+
6
+ const ALIASES = Object.freeze({ apps: "app", pats: "pat" });
7
+
8
+ function help(version) {
9
+ const command = RUNTIME_BRAND.cli.command;
10
+ const product = RUNTIME_BRAND.product.displayName;
11
+ const enabled = new Set(RUNTIME_BRAND.capabilities.commands);
12
+ const lines = [`${command} — ${product} managed apps and isolated CLI profiles`, ""];
13
+ if (enabled.has("setup")) lines.push(` ${command} setup Configure and prepare ${product} CLI/app profiles`);
14
+ if (enabled.has("auth")) lines.push(` ${command} auth Store an existing ${product} PAT`);
15
+ if (enabled.has("pat")) lines.push(` ${command} pat create|revoke Mint or revoke a ${product} PAT`);
16
+ if (enabled.has("app")) lines.push(` ${command} app install|update|open ... Manage isolated desktop apps`);
17
+ if (enabled.has("claude")) lines.push(` ${command} claude [args...] Launch isolated Claude Code`);
18
+ if (enabled.has("codex")) lines.push(` ${command} codex [args...] Launch isolated Codex`);
19
+ if (enabled.has("status")) lines.push(` ${command} status Show local readiness`);
20
+ lines.push("", `Version: ${version}`);
21
+ return `${lines.join("\n")}\n`;
22
+ }
23
+
24
+ async function withBrandedConsole(run) {
25
+ const original = { log: console.log, error: console.error, warn: console.warn };
26
+ const rewrite = (method) => (...values) => original[method](...values.map((value) => (
27
+ typeof value === "string" ? brandedText(value) : value
28
+ )));
29
+ console.log = rewrite("log");
30
+ console.error = rewrite("error");
31
+ console.warn = rewrite("warn");
32
+ try { return await run(); }
33
+ finally {
34
+ console.log = original.log;
35
+ console.error = original.error;
36
+ console.warn = original.warn;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Create a strict branded CLI using the canonical Impel command implementations.
42
+ * The brand must be installed in IMPEL_CLI_RUNTIME_BRAND before this module is
43
+ * imported so path and self-invocation constants are initialized atomically.
44
+ */
45
+ export function createImpelCliExtension({ brand, entrypoint, version }) {
46
+ const expected = validateRuntimeBrand(brand);
47
+ if (JSON.stringify(expected) !== JSON.stringify(RUNTIME_BRAND)) {
48
+ throw new Error("runtime brand does not match IMPEL_CLI_RUNTIME_BRAND");
49
+ }
50
+ if (!path.isAbsolute(String(entrypoint || ""))) throw new Error("extension entrypoint must be absolute");
51
+ if (process.env.IMPEL_CLI_EXTENSION_ENTRYPOINT !== entrypoint) {
52
+ throw new Error("IMPEL_CLI_EXTENSION_ENTRYPOINT must identify the branded executable");
53
+ }
54
+ if (!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/u.test(String(version || ""))) {
55
+ throw new Error("extension version is invalid");
56
+ }
57
+ process.env.IMPEL_CLI_EXTENSION_VERSION = String(version);
58
+ const allowed = new Set(RUNTIME_BRAND.capabilities.commands);
59
+
60
+ return Object.freeze({
61
+ async main(argv = []) {
62
+ const [rawCommand] = argv;
63
+ if (rawCommand === undefined || ["help", "--help", "-h"].includes(rawCommand)) {
64
+ process.stdout.write(help(version));
65
+ return;
66
+ }
67
+ if (["--version", "-v"].includes(rawCommand)) {
68
+ process.stdout.write(`${version}\n`);
69
+ return;
70
+ }
71
+ const command = ALIASES[rawCommand] || rawCommand;
72
+ const hiddenAppCommand = rawCommand === "_app-launch";
73
+ if ((!hiddenAppCommand && !allowed.has(command)) || (hiddenAppCommand && !allowed.has("app"))) {
74
+ process.stderr.write(`${RUNTIME_BRAND.cli.command}: command ${JSON.stringify(rawCommand)} is not available\n`);
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ return withBrandedConsole(() => upstreamMain(argv));
79
+ },
80
+ });
81
+ }