impel-cli 0.18.4 → 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 +12 -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/launch.js +12 -8
- package/src/commands/pat.js +46 -37
- package/src/commands/setup.js +11 -8
- 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/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 +7 -6
package/src/commands/launch.js
CHANGED
|
@@ -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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
package/src/commands/pat.js
CHANGED
|
@@ -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
|
|
21
|
-
const
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
361
|
-
console.error(
|
|
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(
|
|
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(
|
|
388
|
+
fail(`${PAT_COMMAND} revoke: unexpected argument "${redactSecretText(positionals[1])}".`);
|
|
380
389
|
}
|
|
381
390
|
if (flags.yes !== true) {
|
|
382
|
-
fail(
|
|
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(
|
|
401
|
+
fail(`${PAT_COMMAND} create: unexpected argument "${redactSecretText(positionals[0])}".`);
|
|
393
402
|
}
|
|
394
403
|
|
|
395
404
|
const subject = requestedSubject(flags);
|
package/src/commands/setup.js
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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();
|
package/src/commands/status.js
CHANGED
|
@@ -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",
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/token.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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 =
|
|
21
|
-
export const DEFAULT_APP_URL =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
100
|
-
const
|
|
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,
|
|
108
|
-
.replace(PAT_RE,
|
|
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
|
+
}
|
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,
|