impel-cli 0.7.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -21
- package/package.json +1 -1
- package/src/apps.js +109 -34
- package/src/cli.js +16 -7
- package/src/commands/apps.js +127 -32
- package/src/commands/auth.js +4 -0
- package/src/commands/pat.js +331 -0
- package/src/commands/setup.js +1 -0
- package/src/commands/update.js +5 -1
- package/src/config.js +1 -0
- package/src/tenants.js +11 -0
- package/src/updates.js +7 -2
package/src/commands/apps.js
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
refreshUpdateCache,
|
|
16
16
|
spawnDetachedAppRefresh,
|
|
17
17
|
} from "../updates.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
assertProviderScopes,
|
|
20
|
+
ensureTenantSelection,
|
|
21
|
+
fetchTenants,
|
|
22
|
+
normalizeTenantId,
|
|
23
|
+
tenantCredential,
|
|
24
|
+
} from "../tenants.js";
|
|
19
25
|
import {
|
|
20
26
|
CURRENT_CONFIG_VERSION,
|
|
21
27
|
CLAUDE_CONFIG_ID,
|
|
@@ -111,6 +117,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
111
117
|
"skip-vendor": { type: "boolean" },
|
|
112
118
|
"keep-data": { type: "boolean" },
|
|
113
119
|
"stale-only": { type: "boolean" },
|
|
120
|
+
tenant: { type: "string" },
|
|
114
121
|
});
|
|
115
122
|
const io = {
|
|
116
123
|
environment: process.env,
|
|
@@ -140,13 +147,20 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
140
147
|
if (overrides.launchApp) io.launchClaudeApp = overrides.launchApp;
|
|
141
148
|
if (overrides.userData) io.claudeUserData = overrides.userData;
|
|
142
149
|
const stored = loadConfig();
|
|
143
|
-
const
|
|
144
|
-
|
|
150
|
+
const localTenantId = flags.tenant
|
|
151
|
+
? normalizeTenantId(flags.tenant)
|
|
152
|
+
: stored?.tenantId || null;
|
|
153
|
+
const localTenantName = localTenantId === stored?.tenantId ? stored?.tenantName : null;
|
|
154
|
+
const storedUserData = io.claudeUserData(io.environment, localTenantId);
|
|
155
|
+
const paths = appPaths(io.homeDir, localTenantId, {
|
|
156
|
+
claudeUserData: storedUserData,
|
|
157
|
+
tenantName: localTenantName,
|
|
158
|
+
});
|
|
145
159
|
const claudeConfigPath = path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`);
|
|
146
160
|
const chatgptConfigPath = path.join(paths.chatgpt.codexHome, "config.toml");
|
|
147
161
|
|
|
148
162
|
if (action === "status") {
|
|
149
|
-
console.log(`Tenant: ${
|
|
163
|
+
console.log(`Tenant: ${localTenantId || "not selected"}`);
|
|
150
164
|
if (targets.includes("claude")) {
|
|
151
165
|
console.log("Impel Claude:");
|
|
152
166
|
console.log(` profile: ${fs.existsSync(claudeConfigPath) ? paths.claude.userData : "not installed"}`);
|
|
@@ -167,7 +181,8 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
167
181
|
io.removeApps(targets, {
|
|
168
182
|
homeDir: io.homeDir,
|
|
169
183
|
keepData: Boolean(flags["keep-data"]),
|
|
170
|
-
tenantId:
|
|
184
|
+
tenantId: localTenantId,
|
|
185
|
+
tenantName: localTenantName,
|
|
171
186
|
claudeUserData: storedUserData,
|
|
172
187
|
});
|
|
173
188
|
if (targets.includes("chatgpt")) io.removeManagedChatGPTApp(paths.root);
|
|
@@ -186,9 +201,12 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
186
201
|
|
|
187
202
|
try {
|
|
188
203
|
if (action === "open") maybePrintUpdateNotice();
|
|
189
|
-
const config = await io.selectedConfig(targets);
|
|
204
|
+
const config = await io.selectedConfig(targets, flags.tenant || null);
|
|
190
205
|
const actionUserData = io.claudeUserData(io.environment, config.tenantId);
|
|
191
|
-
const actionPaths = appPaths(io.homeDir, config.tenantId, {
|
|
206
|
+
const actionPaths = appPaths(io.homeDir, config.tenantId, {
|
|
207
|
+
claudeUserData: actionUserData,
|
|
208
|
+
tenantName: config.tenantName,
|
|
209
|
+
});
|
|
192
210
|
const vendorPaths = {};
|
|
193
211
|
for (const target of targets) {
|
|
194
212
|
const isClaude = target === "claude";
|
|
@@ -230,7 +248,10 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
230
248
|
}
|
|
231
249
|
|
|
232
250
|
const { userData } = configureWindowsApps(config, targets, vendorPaths, catalog, io);
|
|
233
|
-
const tenantPaths = appPaths(io.homeDir, config.tenantId, {
|
|
251
|
+
const tenantPaths = appPaths(io.homeDir, config.tenantId, {
|
|
252
|
+
claudeUserData: userData,
|
|
253
|
+
tenantName: config.tenantName,
|
|
254
|
+
});
|
|
234
255
|
if (action === "install" || action === "update" || action === "refresh") {
|
|
235
256
|
const verb = action === "install" ? "Installed" : action === "update" ? "Updated" : "Refreshed";
|
|
236
257
|
for (const target of targets) {
|
|
@@ -282,17 +303,25 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
282
303
|
"skip-vendor": { type: "boolean" },
|
|
283
304
|
"keep-data": { type: "boolean" },
|
|
284
305
|
"stale-only": { type: "boolean" },
|
|
306
|
+
tenant: { type: "string" },
|
|
285
307
|
});
|
|
286
308
|
|
|
287
|
-
if (action === "status") return printStatus(targets);
|
|
309
|
+
if (action === "status") return printStatus(targets, flags.tenant || null);
|
|
288
310
|
if (action === "refresh") {
|
|
289
|
-
return refreshApps(targets, {
|
|
311
|
+
return refreshApps(targets, {
|
|
312
|
+
staleOnly: Boolean(flags["stale-only"]),
|
|
313
|
+
tenantId: flags.tenant || null,
|
|
314
|
+
});
|
|
290
315
|
}
|
|
291
316
|
if (action === "uninstall") {
|
|
292
317
|
const config = loadConfig();
|
|
318
|
+
const tenantId = flags.tenant
|
|
319
|
+
? normalizeTenantId(flags.tenant)
|
|
320
|
+
: config?.tenantId || null;
|
|
293
321
|
removeManagedApps(targets, {
|
|
294
322
|
keepData: Boolean(flags["keep-data"]),
|
|
295
|
-
tenantId
|
|
323
|
+
tenantId,
|
|
324
|
+
tenantName: tenantId === config?.tenantId ? config?.tenantName : null,
|
|
296
325
|
});
|
|
297
326
|
for (const target of targets) console.log(`Removed Impel ${target} app${flags["keep-data"] ? " (kept isolated data)" : " and isolated data"}.`);
|
|
298
327
|
return;
|
|
@@ -304,19 +333,36 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
304
333
|
// credentials, there is nothing to fetch or rebuild — launch immediately
|
|
305
334
|
// and let a detached stale-only refresh keep configs/catalog/skills
|
|
306
335
|
// converging in the background.
|
|
307
|
-
const
|
|
336
|
+
const stored = loadConfig();
|
|
337
|
+
const tenantId = flags.tenant
|
|
338
|
+
? normalizeTenantId(flags.tenant)
|
|
339
|
+
: stored?.tenantId || null;
|
|
340
|
+
const manifest = tenantId ? readTenantManifest(os.homedir(), tenantId) : null;
|
|
341
|
+
const openConfig = stored && tenantId
|
|
342
|
+
? {
|
|
343
|
+
...stored,
|
|
344
|
+
tenantId,
|
|
345
|
+
tenantName: tenantId === stored.tenantId
|
|
346
|
+
? manifest?.tenantName || stored.tenantName || tenantId
|
|
347
|
+
: manifest?.tenantName || tenantId,
|
|
348
|
+
}
|
|
349
|
+
: stored;
|
|
350
|
+
const fastLaunchers = fastOpenLaunchers(targets, { config: openConfig });
|
|
308
351
|
if (fastLaunchers) {
|
|
309
|
-
spawnDetachedAppRefresh();
|
|
352
|
+
spawnDetachedAppRefresh(tenantId);
|
|
310
353
|
for (const launcher of fastLaunchers) {
|
|
311
354
|
spawnSync("/usr/bin/open", ["-n", launcher], { stdio: "inherit" });
|
|
312
355
|
}
|
|
313
356
|
return;
|
|
314
357
|
}
|
|
315
358
|
|
|
316
|
-
const config = await selectedAppConfig(targets);
|
|
317
|
-
const statuses = appStatus(targets, os.homedir(), config.tenantId);
|
|
359
|
+
const config = await selectedAppConfig(targets, flags.tenant || null);
|
|
360
|
+
const statuses = appStatus(targets, os.homedir(), config.tenantId, config.tenantName);
|
|
361
|
+
const legacyPaths = appPaths(os.homedir());
|
|
318
362
|
for (const status of statuses) {
|
|
319
|
-
if (!status.launcherInstalled
|
|
363
|
+
if (!status.launcherInstalled && !fs.existsSync(legacyPaths[status.target].launcher)) {
|
|
364
|
+
throw new Error(`${status.label} is not installed; run \`impel app install ${status.target}\``);
|
|
365
|
+
}
|
|
320
366
|
if (!status.vendorPath) throw new Error(`${status.label} vendor app is unavailable; reinstall the vendor app first`);
|
|
321
367
|
}
|
|
322
368
|
let catalog;
|
|
@@ -332,7 +378,12 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
332
378
|
const staleBundleTargets = statuses
|
|
333
379
|
.filter((status) => !bundleIsCurrent(status))
|
|
334
380
|
.map((status) => status.target);
|
|
335
|
-
if (staleBundleTargets.length > 0)
|
|
381
|
+
if (staleBundleTargets.length > 0) {
|
|
382
|
+
await quitBlockingApps(staleBundleTargets, {
|
|
383
|
+
tenantId: config.tenantId,
|
|
384
|
+
tenantName: config.tenantName,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
336
387
|
installManagedAppFiles({
|
|
337
388
|
config,
|
|
338
389
|
targets,
|
|
@@ -341,7 +392,7 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
341
392
|
vendorPaths: Object.fromEntries(statuses.map((status) => [status.target, status.vendorPath])),
|
|
342
393
|
writeBundles: staleBundleTargets,
|
|
343
394
|
});
|
|
344
|
-
for (const status of appStatus(targets, os.homedir(), config.tenantId)) {
|
|
395
|
+
for (const status of appStatus(targets, os.homedir(), config.tenantId, config.tenantName)) {
|
|
345
396
|
spawnSync("/usr/bin/open", ["-n", status.launcher], { stdio: "inherit" });
|
|
346
397
|
}
|
|
347
398
|
return;
|
|
@@ -350,11 +401,14 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
350
401
|
throw new Error(`unknown app action "${action}"; use install, update, refresh, status, open, or uninstall`);
|
|
351
402
|
}
|
|
352
403
|
|
|
353
|
-
const config = await selectedAppConfig(targets);
|
|
404
|
+
const config = await selectedAppConfig(targets, flags.tenant || null);
|
|
354
405
|
console.log(`Tenant: ${config.tenantId} (desktop history is isolated per tenant).`);
|
|
355
406
|
|
|
356
407
|
// A running vendor or Impel app makes the pinned vendor install / bundle swap fail.
|
|
357
|
-
await quitBlockingApps(targets
|
|
408
|
+
await quitBlockingApps(targets, {
|
|
409
|
+
tenantId: config.tenantId,
|
|
410
|
+
tenantName: config.tenantName,
|
|
411
|
+
});
|
|
358
412
|
|
|
359
413
|
const vendorPaths = {};
|
|
360
414
|
if (!flags["skip-vendor"]) {
|
|
@@ -382,7 +436,7 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
382
436
|
|
|
383
437
|
// Best-effort: sync the Bifrost shared skills into each installed isolated app
|
|
384
438
|
// profile. Never fails the install/update.
|
|
385
|
-
const paths = appPaths(os.homedir(), config.tenantId);
|
|
439
|
+
const paths = appPaths(os.homedir(), config.tenantId, { tenantName: config.tenantName });
|
|
386
440
|
const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
|
|
387
441
|
for (const item of installed) {
|
|
388
442
|
const { client, env, label } = appSkillTarget(item.target, paths);
|
|
@@ -415,7 +469,12 @@ export function fastOpenLaunchers(targets, { homeDir = os.homedir(), config = lo
|
|
|
415
469
|
return null;
|
|
416
470
|
}
|
|
417
471
|
const launchers = [];
|
|
418
|
-
for (const status of statuses ?? appStatus(
|
|
472
|
+
for (const status of statuses ?? appStatus(
|
|
473
|
+
targets,
|
|
474
|
+
homeDir,
|
|
475
|
+
config.tenantId,
|
|
476
|
+
config.tenantName || manifest.tenantName,
|
|
477
|
+
)) {
|
|
419
478
|
if (!bundleIsCurrent(status)) return null;
|
|
420
479
|
if (!fs.existsSync(status.configPath)) return null;
|
|
421
480
|
if (status.target === "claude" && !claudeConfigIsCurrent(status.configPath, config, gatewayUrl)) {
|
|
@@ -445,19 +504,29 @@ function claudeConfigIsCurrent(configPath, config, gatewayUrl) {
|
|
|
445
504
|
// run (config writes only, never a bundle swap, never quits anything) and
|
|
446
505
|
// silent on any failure: it is invoked detached from the apps' token helper,
|
|
447
506
|
// so there is no user to talk to and nothing may leak to stdout.
|
|
448
|
-
async function refreshApps(targets, { staleOnly = false } = {}) {
|
|
507
|
+
async function refreshApps(targets, { staleOnly = false, tenantId = null } = {}) {
|
|
449
508
|
const stored = loadConfig();
|
|
450
509
|
if (!stored?.pat) return;
|
|
451
|
-
const
|
|
510
|
+
const requestedTenantId = tenantId
|
|
511
|
+
? normalizeTenantId(tenantId)
|
|
512
|
+
: stored.tenantId || null;
|
|
513
|
+
const manifest = requestedTenantId
|
|
514
|
+
? readTenantManifest(os.homedir(), requestedTenantId)
|
|
515
|
+
: null;
|
|
516
|
+
const paths = appPaths(os.homedir(), requestedTenantId, {
|
|
517
|
+
tenantName: requestedTenantId === stored.tenantId
|
|
518
|
+
? stored.tenantName || manifest?.tenantName
|
|
519
|
+
: manifest?.tenantName,
|
|
520
|
+
});
|
|
452
521
|
if (staleOnly && manifestIsFresh(paths, stored)) return;
|
|
453
522
|
|
|
454
523
|
let config;
|
|
455
524
|
try {
|
|
456
|
-
config = await selectedAppConfig(targets);
|
|
525
|
+
config = await selectedAppConfig(targets, tenantId ? requestedTenantId : null);
|
|
457
526
|
} catch {
|
|
458
527
|
return;
|
|
459
528
|
}
|
|
460
|
-
const statuses = appStatus(targets, os.homedir(), config.tenantId);
|
|
529
|
+
const statuses = appStatus(targets, os.homedir(), config.tenantId, config.tenantName);
|
|
461
530
|
const installedTargets = statuses.filter((status) => status.launcherInstalled);
|
|
462
531
|
if (installedTargets.length === 0) return;
|
|
463
532
|
|
|
@@ -476,7 +545,7 @@ async function refreshApps(targets, { staleOnly = false } = {}) {
|
|
|
476
545
|
writeBundles: false,
|
|
477
546
|
});
|
|
478
547
|
|
|
479
|
-
const tenantPaths = appPaths(os.homedir(), config.tenantId);
|
|
548
|
+
const tenantPaths = appPaths(os.homedir(), config.tenantId, { tenantName: config.tenantName });
|
|
480
549
|
const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
|
|
481
550
|
for (const status of installedTargets) {
|
|
482
551
|
const { client, env, label } = appSkillTarget(status.target, tenantPaths);
|
|
@@ -505,10 +574,28 @@ function manifestIsFresh(paths, config, now = Date.now()) {
|
|
|
505
574
|
}
|
|
506
575
|
}
|
|
507
576
|
|
|
508
|
-
async function selectedAppConfig(targets) {
|
|
577
|
+
async function selectedAppConfig(targets, tenantOverride = null) {
|
|
509
578
|
const config = loadConfig();
|
|
510
579
|
if (!config?.pat || !config?.gatewayUrl) throw new Error("run `impel auth` before using Impel apps");
|
|
511
|
-
|
|
580
|
+
let selected;
|
|
581
|
+
if (tenantOverride) {
|
|
582
|
+
const requested = normalizeTenantId(tenantOverride);
|
|
583
|
+
const listing = await fetchTenants(config);
|
|
584
|
+
const tenant = listing.tenants.find((candidate) => (
|
|
585
|
+
candidate.id === requested || candidate.slug === requested
|
|
586
|
+
));
|
|
587
|
+
if (!tenant) throw new Error(`tenant "${requested}" is not available to this user`);
|
|
588
|
+
selected = {
|
|
589
|
+
config,
|
|
590
|
+
tenantId: tenant.id,
|
|
591
|
+
tenantName: tenant.name,
|
|
592
|
+
tenant,
|
|
593
|
+
productAccess: listing.productAccess,
|
|
594
|
+
scopes: listing.scopes,
|
|
595
|
+
};
|
|
596
|
+
} else {
|
|
597
|
+
selected = await ensureTenantSelection(config, { refresh: true });
|
|
598
|
+
}
|
|
512
599
|
if (!selected.productAccess) {
|
|
513
600
|
throw new Error("the control plane did not return a live product access entitlement; retry after it is upgraded");
|
|
514
601
|
}
|
|
@@ -520,16 +607,24 @@ async function selectedAppConfig(targets) {
|
|
|
520
607
|
return {
|
|
521
608
|
...config,
|
|
522
609
|
tenantId,
|
|
610
|
+
tenantName: selected.tenantName || selected.tenant?.name || tenantId,
|
|
523
611
|
productAccess: selected.productAccess,
|
|
524
612
|
scopes: selected.scopes,
|
|
525
613
|
pat: tenantCredential(config.pat, tenantId),
|
|
526
614
|
};
|
|
527
615
|
}
|
|
528
616
|
|
|
529
|
-
function printStatus(targets) {
|
|
617
|
+
function printStatus(targets, tenantOverride = null) {
|
|
530
618
|
const config = loadConfig();
|
|
531
|
-
|
|
532
|
-
|
|
619
|
+
const tenantId = tenantOverride
|
|
620
|
+
? normalizeTenantId(tenantOverride)
|
|
621
|
+
: config?.tenantId || null;
|
|
622
|
+
const manifest = tenantId ? readTenantManifest(os.homedir(), tenantId) : null;
|
|
623
|
+
const tenantName = tenantId === config?.tenantId
|
|
624
|
+
? manifest?.tenantName || config?.tenantName
|
|
625
|
+
: manifest?.tenantName;
|
|
626
|
+
if (tenantId) console.log(`Tenant: ${tenantId}${tenantName && tenantName !== tenantId ? ` (${tenantName})` : ""}`);
|
|
627
|
+
for (const status of appStatus(targets, os.homedir(), tenantId, tenantName)) {
|
|
533
628
|
console.log(`${status.label}:`);
|
|
534
629
|
console.log(` launcher: ${status.launcherInstalled ? status.launcher : "not installed"}`);
|
|
535
630
|
console.log(` vendor: ${status.vendorPath || "not installed"}${status.vendorVersion ? ` (${status.vendorVersion})` : ""}`);
|
package/src/commands/auth.js
CHANGED
|
@@ -53,6 +53,8 @@ export async function cmdAuth(argv) {
|
|
|
53
53
|
? existing.tenantId
|
|
54
54
|
: listing.defaultTenantId
|
|
55
55
|
);
|
|
56
|
+
config.tenantName = listing.tenants.find((tenant) => tenant.id === config.tenantId)?.name
|
|
57
|
+
|| config.tenantId;
|
|
56
58
|
if (listing.productAccess) config.productAccess = listing.productAccess;
|
|
57
59
|
else delete config.productAccess;
|
|
58
60
|
if (listing.scopes) config.scopes = listing.scopes;
|
|
@@ -62,8 +64,10 @@ export async function cmdAuth(argv) {
|
|
|
62
64
|
if (flags.tenant) throw error;
|
|
63
65
|
if (existing?.tenantId && existing.pat === pat) {
|
|
64
66
|
config.tenantId = existing.tenantId;
|
|
67
|
+
config.tenantName = existing.tenantName || existing.tenantId;
|
|
65
68
|
} else {
|
|
66
69
|
delete config.tenantId;
|
|
70
|
+
delete config.tenantName;
|
|
67
71
|
delete config.productAccess;
|
|
68
72
|
delete config.scopes;
|
|
69
73
|
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { parseFlags } from "../args.js";
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
normalizeGatewayUrl,
|
|
5
|
+
redactSecretText,
|
|
6
|
+
resolveDefaultAppUrl,
|
|
7
|
+
} from "../config.js";
|
|
8
|
+
import {
|
|
9
|
+
fetchTenants,
|
|
10
|
+
normalizeTenantId,
|
|
11
|
+
PAT_SCOPE_CLAUDE,
|
|
12
|
+
PAT_SCOPE_CODEX,
|
|
13
|
+
PAT_SCOPE_TASKS,
|
|
14
|
+
} from "../tenants.js";
|
|
15
|
+
|
|
16
|
+
const AGENT_PAT_LABEL_PREFIX = "Agent · ";
|
|
17
|
+
const MAX_PAT_LABEL_LENGTH = 120;
|
|
18
|
+
const MAX_PAT_TTL_DAYS = 365;
|
|
19
|
+
const PAT_CREATE_PATH = "/api/cli/pats";
|
|
20
|
+
const PAT_TOKEN_RE = /^impel_pat_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u;
|
|
21
|
+
const PAT_TOKEN_ID_RE = /^[A-Za-z0-9_-]{8,128}$/u;
|
|
22
|
+
const AGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,111}$/u;
|
|
23
|
+
const CONTROL_CHARACTER_RE = /[\u0000-\u001F\u007F-\u009F]/u;
|
|
24
|
+
const PAT_SCOPES = [PAT_SCOPE_CLAUDE, PAT_SCOPE_CODEX, PAT_SCOPE_TASKS];
|
|
25
|
+
const PAT_SCOPE_SET = new Set(PAT_SCOPES);
|
|
26
|
+
const FLAG_NAMES = new Set([
|
|
27
|
+
"agent",
|
|
28
|
+
"app",
|
|
29
|
+
"help",
|
|
30
|
+
"json",
|
|
31
|
+
"label",
|
|
32
|
+
"org",
|
|
33
|
+
"scopes",
|
|
34
|
+
"tenant",
|
|
35
|
+
"ttl-days",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const HELP = `impel pat - mint a personal access token through Impel
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
impel pat create --label <label> [options]
|
|
42
|
+
impel pat create --agent <agent-id> [options]
|
|
43
|
+
|
|
44
|
+
Aliases:
|
|
45
|
+
impel pats ...
|
|
46
|
+
impel pat mint ...
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--org <org> Target org. Defaults to the selected tenant.
|
|
50
|
+
--tenant <org> Alias for --org.
|
|
51
|
+
--scopes <csv> Requested scopes: ${PAT_SCOPES.join(", ")}.
|
|
52
|
+
Defaults are chosen by the control plane for your access tier.
|
|
53
|
+
--ttl-days <days> Expiry from 1 to ${MAX_PAT_TTL_DAYS} days. Default: 90.
|
|
54
|
+
--app <url> Override the app/control-plane URL for this command.
|
|
55
|
+
--json Print the created PAT and metadata as JSON.
|
|
56
|
+
|
|
57
|
+
The stored PAT authenticates this request. The control plane verifies the owner,
|
|
58
|
+
organization membership, requested scopes, and agent access before identity mints
|
|
59
|
+
the new secret. The new PAT is shown once and is not stored automatically.
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
class PatCommandError extends Error {}
|
|
63
|
+
|
|
64
|
+
function fail(message) {
|
|
65
|
+
throw new PatCommandError(message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function flagSpec() {
|
|
69
|
+
return {
|
|
70
|
+
agent: { type: "string" },
|
|
71
|
+
app: { type: "string" },
|
|
72
|
+
help: { type: "boolean" },
|
|
73
|
+
json: { type: "boolean" },
|
|
74
|
+
label: { type: "string" },
|
|
75
|
+
org: { type: "string" },
|
|
76
|
+
scopes: { type: "string" },
|
|
77
|
+
tenant: { type: "string" },
|
|
78
|
+
"ttl-days": { type: "string" },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requireConfig(flags) {
|
|
83
|
+
const config = loadConfig();
|
|
84
|
+
if (!config?.pat) {
|
|
85
|
+
fail("impel pat: not authenticated. Run `impel setup` (or `impel auth`) first.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const appUrl = normalizeGatewayUrl(flags.app || config.appUrl || resolveDefaultAppUrl());
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = new URL(appUrl);
|
|
92
|
+
} catch {
|
|
93
|
+
fail("impel pat: --app must be an HTTP or HTTPS URL.");
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
(parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
97
|
+
|| parsed.username
|
|
98
|
+
|| parsed.password
|
|
99
|
+
|| parsed.pathname !== "/"
|
|
100
|
+
|| parsed.search
|
|
101
|
+
|| parsed.hash
|
|
102
|
+
) {
|
|
103
|
+
fail("impel pat: --app must be a bare HTTP or HTTPS origin without credentials, a path, query, or fragment.");
|
|
104
|
+
}
|
|
105
|
+
return { appUrl, config };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requestedScopes(value) {
|
|
109
|
+
if (value === undefined) return undefined;
|
|
110
|
+
const requested = String(value)
|
|
111
|
+
.split(",")
|
|
112
|
+
.map((scope) => scope.trim())
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
if (requested.length === 0) {
|
|
115
|
+
fail("impel pat: --scopes must contain at least one scope.");
|
|
116
|
+
}
|
|
117
|
+
for (const scope of requested) {
|
|
118
|
+
if (!PAT_SCOPE_SET.has(scope)) {
|
|
119
|
+
fail(`impel pat: unknown scope "${redactSecretText(scope)}".`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const requestedSet = new Set(requested);
|
|
123
|
+
return PAT_SCOPES.filter((scope) => requestedSet.has(scope));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function requestedTtlDays(value) {
|
|
127
|
+
if (value === undefined) return undefined;
|
|
128
|
+
if (!/^\d+$/u.test(String(value))) {
|
|
129
|
+
fail(`impel pat: --ttl-days must be an integer from 1 to ${MAX_PAT_TTL_DAYS}.`);
|
|
130
|
+
}
|
|
131
|
+
const days = Number(value);
|
|
132
|
+
if (!Number.isSafeInteger(days) || days < 1 || days > MAX_PAT_TTL_DAYS) {
|
|
133
|
+
fail(`impel pat: --ttl-days must be an integer from 1 to ${MAX_PAT_TTL_DAYS}.`);
|
|
134
|
+
}
|
|
135
|
+
return days;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function requestedSubject(flags) {
|
|
139
|
+
const agentId = flags.agent === undefined ? null : String(flags.agent).trim();
|
|
140
|
+
const personalLabel = flags.label === undefined ? null : String(flags.label).trim();
|
|
141
|
+
if (agentId !== null && personalLabel !== null) {
|
|
142
|
+
fail("impel pat create: use either --label for a personal PAT or --agent for an agent PAT, not both.");
|
|
143
|
+
}
|
|
144
|
+
if (agentId !== null) {
|
|
145
|
+
if (!AGENT_ID_RE.test(agentId)) {
|
|
146
|
+
fail("impel pat create: --agent must be a valid agent id (letters, numbers, dots, dashes, or underscores).");
|
|
147
|
+
}
|
|
148
|
+
const label = `${AGENT_PAT_LABEL_PREFIX}${agentId}`;
|
|
149
|
+
if (label.length > MAX_PAT_LABEL_LENGTH) {
|
|
150
|
+
fail(`impel pat create: the generated agent label exceeds ${MAX_PAT_LABEL_LENGTH} characters.`);
|
|
151
|
+
}
|
|
152
|
+
return { agentId, kind: "agent", label };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!personalLabel) {
|
|
156
|
+
fail("impel pat create: --label is required for a personal PAT; use --agent <id> for an agent PAT.");
|
|
157
|
+
}
|
|
158
|
+
if (personalLabel.length > MAX_PAT_LABEL_LENGTH || CONTROL_CHARACTER_RE.test(personalLabel)) {
|
|
159
|
+
fail(`impel pat create: --label must be 1 to ${MAX_PAT_LABEL_LENGTH} characters without control characters.`);
|
|
160
|
+
}
|
|
161
|
+
return { agentId: null, kind: "personal", label: personalLabel };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function requestedOrgId(flags, config, appUrl) {
|
|
165
|
+
if (flags.org !== undefined && flags.tenant !== undefined && flags.org !== flags.tenant) {
|
|
166
|
+
fail("impel pat: --org and --tenant must name the same org when both are passed.");
|
|
167
|
+
}
|
|
168
|
+
const requested = flags.org ?? flags.tenant;
|
|
169
|
+
try {
|
|
170
|
+
if (requested !== undefined) return normalizeTenantId(requested);
|
|
171
|
+
if (config.tenantId) return normalizeTenantId(config.tenantId);
|
|
172
|
+
const listing = await fetchTenants({ ...config, appUrl });
|
|
173
|
+
return listing.defaultTenantId;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
fail(`impel pat: could not resolve the target org: ${redactSecretText(error?.message || error)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function timestamp(value, { nullable = false } = {}) {
|
|
180
|
+
if (nullable && value === null) return null;
|
|
181
|
+
if (!Number.isSafeInteger(value) || Number.isNaN(new Date(value).getTime())) return undefined;
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeCreatedPat(payload, expected) {
|
|
186
|
+
const value = payload?.pat && typeof payload.pat === "object" ? payload.pat : payload;
|
|
187
|
+
if (!value || typeof value !== "object") return null;
|
|
188
|
+
const scopes = Array.isArray(value.scopes)
|
|
189
|
+
&& value.scopes.every((scope) => typeof scope === "string" && PAT_SCOPE_SET.has(scope))
|
|
190
|
+
? [...new Set(value.scopes)]
|
|
191
|
+
: null;
|
|
192
|
+
const createdAt = timestamp(value.createdAt);
|
|
193
|
+
const expiresAt = timestamp(value.expiresAt, { nullable: true });
|
|
194
|
+
if (
|
|
195
|
+
typeof value.token !== "string"
|
|
196
|
+
|| !PAT_TOKEN_RE.test(value.token)
|
|
197
|
+
|| typeof value.tokenId !== "string"
|
|
198
|
+
|| !PAT_TOKEN_ID_RE.test(value.tokenId)
|
|
199
|
+
|| value.orgId !== expected.orgId
|
|
200
|
+
|| value.label !== expected.label
|
|
201
|
+
|| !scopes
|
|
202
|
+
|| createdAt === undefined
|
|
203
|
+
|| expiresAt === undefined
|
|
204
|
+
) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
token: value.token,
|
|
209
|
+
tokenId: value.tokenId,
|
|
210
|
+
orgId: value.orgId,
|
|
211
|
+
label: value.label,
|
|
212
|
+
scopes,
|
|
213
|
+
createdAt,
|
|
214
|
+
expiresAt,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function createPat({ appUrl, currentPat, body }) {
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
221
|
+
let response;
|
|
222
|
+
try {
|
|
223
|
+
response = await fetch(new URL(PAT_CREATE_PATH, appUrl), {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
accept: "application/json",
|
|
227
|
+
authorization: `Bearer ${currentPat}`,
|
|
228
|
+
"content-type": "application/json",
|
|
229
|
+
},
|
|
230
|
+
body: JSON.stringify(body),
|
|
231
|
+
signal: controller.signal,
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const message = error?.name === "AbortError" ? "request timed out" : error?.message || error;
|
|
235
|
+
fail(`impel pat: could not reach ${appUrl}: ${redactSecretText(message)}`);
|
|
236
|
+
} finally {
|
|
237
|
+
clearTimeout(timeout);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const text = await response.text();
|
|
241
|
+
let payload = null;
|
|
242
|
+
try {
|
|
243
|
+
payload = text ? JSON.parse(text) : null;
|
|
244
|
+
} catch {
|
|
245
|
+
payload = null;
|
|
246
|
+
}
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
if (response.status === 404) {
|
|
249
|
+
fail(`impel pat: PAT minting is not available on ${appUrl}; update the Impel control plane and try again.`);
|
|
250
|
+
}
|
|
251
|
+
const message = redactSecretText(payload?.error || `${response.status} ${response.statusText}`.trim());
|
|
252
|
+
fail(`impel pat: ${message}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const created = normalizeCreatedPat(payload, body);
|
|
256
|
+
if (!created) {
|
|
257
|
+
fail("impel pat: the control plane returned an invalid PAT creation response; the secret was not displayed.");
|
|
258
|
+
}
|
|
259
|
+
return created;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function printCreatedPat(created, subject, json) {
|
|
263
|
+
if (json) {
|
|
264
|
+
console.log(JSON.stringify({
|
|
265
|
+
kind: subject.kind,
|
|
266
|
+
...(subject.agentId ? { agentId: subject.agentId } : {}),
|
|
267
|
+
...created,
|
|
268
|
+
}, null, 2));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (subject.kind === "agent") {
|
|
273
|
+
console.log(`Created an agent PAT for ${subject.agentId} in ${created.orgId}.`);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(`Created personal PAT ${JSON.stringify(created.label)} in ${created.orgId}.`);
|
|
276
|
+
}
|
|
277
|
+
console.log(`Scopes: ${created.scopes.join(", ") || "none"}`);
|
|
278
|
+
console.log(`Expires: ${created.expiresAt === null ? "never" : new Date(created.expiresAt).toISOString()}`);
|
|
279
|
+
console.log("");
|
|
280
|
+
console.log("Copy this token now. Impel cannot show the secret again:");
|
|
281
|
+
console.log(created.token);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function cmdPat(argv) {
|
|
285
|
+
const [action, ...rest] = argv;
|
|
286
|
+
if (!action || action === "help" || action === "--help" || action === "-h") {
|
|
287
|
+
console.log(HELP);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (action !== "create" && action !== "mint") {
|
|
291
|
+
console.error(`impel pat: unknown subcommand "${redactSecretText(action)}".`);
|
|
292
|
+
console.error(" Use `impel pat create --label <label>` or `impel pat create --agent <agent-id>`.");
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const { flags, positionals } = parseFlags(rest, flagSpec());
|
|
298
|
+
try {
|
|
299
|
+
if (flags.help) {
|
|
300
|
+
console.log(HELP);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const unknownFlag = Object.keys(flags).find((name) => !FLAG_NAMES.has(name));
|
|
304
|
+
if (unknownFlag) fail(`impel pat: unknown option --${redactSecretText(unknownFlag)}.`);
|
|
305
|
+
if (positionals.length > 0) {
|
|
306
|
+
fail(`impel pat create: unexpected argument "${redactSecretText(positionals[0])}".`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const subject = requestedSubject(flags);
|
|
310
|
+
const scopes = requestedScopes(flags.scopes);
|
|
311
|
+
const ttlDays = requestedTtlDays(flags["ttl-days"]);
|
|
312
|
+
const { appUrl, config } = requireConfig(flags);
|
|
313
|
+
const orgId = await requestedOrgId(flags, config, appUrl);
|
|
314
|
+
const body = {
|
|
315
|
+
orgId,
|
|
316
|
+
label: subject.label,
|
|
317
|
+
...(subject.agentId ? { agentId: subject.agentId } : {}),
|
|
318
|
+
...(scopes ? { scopes } : {}),
|
|
319
|
+
...(ttlDays ? { ttlDays } : {}),
|
|
320
|
+
};
|
|
321
|
+
const created = await createPat({ appUrl, currentPat: config.pat, body });
|
|
322
|
+
printCreatedPat(created, subject, flags.json === true);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (error instanceof PatCommandError) {
|
|
325
|
+
console.error(error.message);
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
package/src/commands/setup.js
CHANGED
|
@@ -214,6 +214,7 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
config.tenantId = tenant.id;
|
|
217
|
+
config.tenantName = tenant.name;
|
|
217
218
|
if (listing.productAccess) config.productAccess = listing.productAccess;
|
|
218
219
|
else delete config.productAccess;
|
|
219
220
|
if (listing.scopes) config.scopes = listing.scopes;
|
package/src/commands/update.js
CHANGED
|
@@ -118,8 +118,12 @@ function anyAppInstalled(homeDir = os.homedir()) {
|
|
|
118
118
|
|| fs.existsSync(path.join(paths.chatgpt.codexHome, "config.toml"))
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
|
+
const legacyPaths = appPaths(homeDir);
|
|
121
122
|
return (
|
|
122
|
-
fs.existsSync(paths.claude.launcher)
|
|
123
|
+
fs.existsSync(paths.claude.launcher)
|
|
124
|
+
|| fs.existsSync(paths.chatgpt.launcher)
|
|
125
|
+
|| fs.existsSync(legacyPaths.claude.launcher)
|
|
126
|
+
|| fs.existsSync(legacyPaths.chatgpt.launcher)
|
|
123
127
|
);
|
|
124
128
|
}
|
|
125
129
|
|