impel-cli 0.9.1 → 0.9.2
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 +16 -13
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/commands/apps.js +115 -37
package/README.md
CHANGED
|
@@ -154,8 +154,8 @@ impel app update [target] [--tenant <org>] Refresh that tenant's apps, configs
|
|
|
154
154
|
impel app refresh [target] [--tenant <org>] Configs/catalog/skills only; safe while apps run
|
|
155
155
|
(--stale-only: no-op unless 6h+ since last sync)
|
|
156
156
|
impel app status [target] [--tenant <org>] Show tenant launcher/vendor status
|
|
157
|
-
impel app open [target] [--tenant <org>]
|
|
158
|
-
|
|
157
|
+
impel app open [target] [--tenant <org>] Install/configure a tenant on first use, then launch
|
|
158
|
+
it (later opens are immediate when nothing changed;
|
|
159
159
|
a detached stale-only refresh keeps things current)
|
|
160
160
|
impel app uninstall [target] [--tenant <org>] [--keep-data]
|
|
161
161
|
Remove one tenant's Impel-managed app files
|
|
@@ -292,9 +292,13 @@ On macOS, desktop app state is isolated under
|
|
|
292
292
|
bundles in `~/Applications`, with names such as `Impel Claude (Acme)` and
|
|
293
293
|
`Impel ChatGPT (Acme)`, stable tenant-specific bundle identifiers, and fixed
|
|
294
294
|
tenant profile/auth paths. Different tenants can stay open concurrently.
|
|
295
|
-
`impel app open --tenant <org>`
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
`impel app open --tenant <org>` installs and configures a missing tenant variant
|
|
296
|
+
on first use, then launches it directly without changing the globally selected
|
|
297
|
+
tenant. Later opens use the existing isolated app and profile. Switching tenants
|
|
298
|
+
never deletes local history or projects; each tenant's preserved profile reopens
|
|
299
|
+
independently. The first-use setup still requires an authenticated user PAT and
|
|
300
|
+
live membership in the requested organization; it does not create a PAT per
|
|
301
|
+
tenant.
|
|
298
302
|
Windows Claude Desktop keeps each
|
|
299
303
|
tenant under `%LOCALAPPDATA%\Claude-3p\Impel\<org>` and launches the unchanged
|
|
300
304
|
signed vendor executable with that profile selected. Windows ChatGPT keeps its
|
|
@@ -460,8 +464,9 @@ the selected tenant's 3P gateway configuration beneath Anthropic's reserved
|
|
|
460
464
|
normal Claude profile unchanged. `impel app install codex` installs/updates
|
|
461
465
|
OpenAI's `OpenAI.Codex` Store package, stages its unchanged signed app payload
|
|
462
466
|
inside Impel's shared app cache, and writes separate Codex and Chromium
|
|
463
|
-
profiles. `impel app open claude` and `impel app open codex`
|
|
464
|
-
|
|
467
|
+
profiles. `impel app open claude` and `impel app open codex` install a missing
|
|
468
|
+
official app and configure the selected tenant on first use, then launch the
|
|
469
|
+
isolated profile without changing either normal vendor profile.
|
|
465
470
|
|
|
466
471
|
For Claude, the Impel PAT is supplied only to the launched child process as an
|
|
467
472
|
authentication token and disappears when that process exits; it is never
|
|
@@ -602,10 +607,8 @@ Codex CLI and the Codex IDE extension / desktop app share the same
|
|
|
602
607
|
### Windows Claude and ChatGPT Desktop
|
|
603
608
|
|
|
604
609
|
```powershell
|
|
605
|
-
impel app install claude
|
|
606
610
|
impel app open claude
|
|
607
611
|
impel app status claude
|
|
608
|
-
impel app install codex
|
|
609
612
|
impel app open codex
|
|
610
613
|
impel app status codex
|
|
611
614
|
```
|
|
@@ -630,7 +633,8 @@ app-target names.
|
|
|
630
633
|
|
|
631
634
|
### macOS Claude and ChatGPT
|
|
632
635
|
|
|
633
|
-
`impel app
|
|
636
|
+
The first `impel app open --tenant <org>` (or an explicit `impel app install`)
|
|
637
|
+
creates tenant-specific apps such as
|
|
634
638
|
`Impel Claude (acme).app` and `Impel ChatGPT (acme).app` in `~/Applications`.
|
|
635
639
|
Their visible bundle names use the organization display name, for example
|
|
636
640
|
`Impel Claude (Acme)` and `Impel ChatGPT (Acme)`. Each tenant/product pair has
|
|
@@ -657,11 +661,10 @@ a stable, unique bundle identifier and gateway configuration:
|
|
|
657
661
|
- Neither app changes `~/.claude`, `~/.claude.json`, `~/.codex`, or the
|
|
658
662
|
normal vendor app's browser/profile data.
|
|
659
663
|
|
|
660
|
-
|
|
664
|
+
Open several tenants without changing the current selection. A missing variant
|
|
665
|
+
is installed and configured automatically before it opens:
|
|
661
666
|
|
|
662
667
|
```sh
|
|
663
|
-
impel app install all --tenant acme
|
|
664
|
-
impel app install all --tenant globex
|
|
665
668
|
impel app open all --tenant acme
|
|
666
669
|
impel app open all --tenant globex
|
|
667
670
|
```
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ Get started:
|
|
|
29
29
|
Run:
|
|
30
30
|
impel claude [args...] Launch Claude Code with an isolated Impel profile
|
|
31
31
|
impel codex [args...] Launch Codex with an isolated Impel profile
|
|
32
|
-
impel app open [target] [--tenant <org>]
|
|
32
|
+
impel app open [target] [--tenant <org>] Install/configure on first use, then open the app(s)
|
|
33
33
|
impel status Tenant, PAT, per-tool mode, gateway reachability
|
|
34
34
|
impel doctor [--tenant <org>|--all-tenants] Synthetic provider, routing, and latency checks
|
|
35
35
|
|
package/src/commands/apps.js
CHANGED
|
@@ -207,13 +207,26 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
207
207
|
claudeUserData: actionUserData,
|
|
208
208
|
tenantName: config.tenantName,
|
|
209
209
|
});
|
|
210
|
+
const profileWasInstalled = {
|
|
211
|
+
claude: fs.existsSync(path.join(
|
|
212
|
+
actionPaths.claude.userData,
|
|
213
|
+
"configLibrary",
|
|
214
|
+
`${CLAUDE_CONFIG_ID}.json`,
|
|
215
|
+
)),
|
|
216
|
+
chatgpt: fs.existsSync(path.join(actionPaths.chatgpt.codexHome, "config.toml")),
|
|
217
|
+
};
|
|
210
218
|
const vendorPaths = {};
|
|
211
219
|
for (const target of targets) {
|
|
212
220
|
const isClaude = target === "claude";
|
|
213
221
|
const find = isClaude ? io.findClaudeApp : io.findChatGPTApp;
|
|
214
222
|
const ensure = isClaude ? io.ensureClaudeApp : io.ensureChatGPTApp;
|
|
215
223
|
let binary = find(io.environment);
|
|
216
|
-
|
|
224
|
+
const shouldEnsureVendor = !flags["skip-vendor"] && (
|
|
225
|
+
action === "install"
|
|
226
|
+
|| action === "update"
|
|
227
|
+
|| (action === "open" && !binary)
|
|
228
|
+
);
|
|
229
|
+
if (shouldEnsureVendor) {
|
|
217
230
|
const vendor = ensure({ update: action === "update" }, { environment: io.environment });
|
|
218
231
|
binary = vendor.binary;
|
|
219
232
|
console.log(`${target}: vendor app ${vendor.action}`);
|
|
@@ -252,9 +265,12 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
252
265
|
claudeUserData: userData,
|
|
253
266
|
tenantName: config.tenantName,
|
|
254
267
|
});
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
268
|
+
const configuredTargets = action === "open"
|
|
269
|
+
? targets.filter((target) => !profileWasInstalled[target])
|
|
270
|
+
: targets;
|
|
271
|
+
if (action === "install" || action === "update" || action === "refresh" || configuredTargets.length > 0) {
|
|
272
|
+
const verb = action === "update" ? "Updated" : action === "refresh" ? "Refreshed" : "Installed";
|
|
273
|
+
for (const target of configuredTargets) {
|
|
258
274
|
const profile = target === "claude" ? tenantPaths.claude.userData : tenantPaths.chatgpt.root;
|
|
259
275
|
console.log(`${verb} Impel ${target === "claude" ? "Claude" : "ChatGPT"} profile at ${profile}`);
|
|
260
276
|
const { client, env, label } = appSkillTarget(target, tenantPaths);
|
|
@@ -358,43 +374,12 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
358
374
|
|
|
359
375
|
const config = await selectedAppConfig(targets, flags.tenant || null);
|
|
360
376
|
const statuses = appStatus(targets, os.homedir(), config.tenantId, config.tenantName);
|
|
361
|
-
|
|
362
|
-
for (const status of statuses) {
|
|
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
|
-
}
|
|
366
|
-
if (!status.vendorPath) throw new Error(`${status.label} vendor app is unavailable; reinstall the vendor app first`);
|
|
367
|
-
}
|
|
368
|
-
let catalog;
|
|
369
|
-
try {
|
|
370
|
-
catalog = await fetchGatewayModels(config);
|
|
371
|
-
} catch (error) {
|
|
372
|
-
throw new Error(
|
|
373
|
-
`tenant model catalog is unavailable (${redactSecretText(error.message)}); the selected tenant app was not opened`,
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
// Only stale bundles pay the clone + codesign rebuild; a bundle swap needs
|
|
377
|
-
// its app closed, and quitting is fine here since we relaunch right after.
|
|
378
|
-
const staleBundleTargets = statuses
|
|
379
|
-
.filter((status) => !bundleIsCurrent(status))
|
|
380
|
-
.map((status) => status.target);
|
|
381
|
-
if (staleBundleTargets.length > 0) {
|
|
382
|
-
await quitBlockingApps(staleBundleTargets, {
|
|
383
|
-
tenantId: config.tenantId,
|
|
384
|
-
tenantName: config.tenantName,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
installManagedAppFiles({
|
|
377
|
+
await provisionAndOpenManagedApps({
|
|
388
378
|
config,
|
|
389
379
|
targets,
|
|
390
|
-
|
|
380
|
+
statuses,
|
|
391
381
|
homeDir: os.homedir(),
|
|
392
|
-
vendorPaths: Object.fromEntries(statuses.map((status) => [status.target, status.vendorPath])),
|
|
393
|
-
writeBundles: staleBundleTargets,
|
|
394
382
|
});
|
|
395
|
-
for (const status of appStatus(targets, os.homedir(), config.tenantId, config.tenantName)) {
|
|
396
|
-
spawnSync("/usr/bin/open", ["-n", status.launcher], { stdio: "inherit" });
|
|
397
|
-
}
|
|
398
383
|
return;
|
|
399
384
|
}
|
|
400
385
|
if (action !== "install" && action !== "update") {
|
|
@@ -445,6 +430,99 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
445
430
|
}
|
|
446
431
|
}
|
|
447
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Complete the slow `app open` path. A first open is intentionally equivalent
|
|
435
|
+
* to an install for the selected tenant: verify or install the pinned vendor,
|
|
436
|
+
* build the isolated bundle/profile, sync skills, and only then launch it.
|
|
437
|
+
* Existing current installs never reach this helper because fastOpenLaunchers
|
|
438
|
+
* handles them without network or rebuild work.
|
|
439
|
+
*/
|
|
440
|
+
export async function provisionAndOpenManagedApps({
|
|
441
|
+
config,
|
|
442
|
+
targets,
|
|
443
|
+
statuses,
|
|
444
|
+
homeDir = os.homedir(),
|
|
445
|
+
}, overrides = {}) {
|
|
446
|
+
const io = {
|
|
447
|
+
fetchModels: fetchGatewayModels,
|
|
448
|
+
ensureVendor: ensureVendorApp,
|
|
449
|
+
bundleCurrent: bundleIsCurrent,
|
|
450
|
+
quitApps: quitBlockingApps,
|
|
451
|
+
installFiles: installManagedAppFiles,
|
|
452
|
+
syncSkills: syncSkillsSafe,
|
|
453
|
+
secureCodexHome: secureManagedCodexHome,
|
|
454
|
+
openLauncher: (launcher) => spawnSync("/usr/bin/open", ["-n", launcher], { stdio: "inherit" }),
|
|
455
|
+
log: (message) => console.log(message),
|
|
456
|
+
...overrides,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// Resolve the live tenant catalog before installing or changing anything.
|
|
460
|
+
// This keeps unavailable/unauthorized tenants fail-closed on first use.
|
|
461
|
+
let catalog;
|
|
462
|
+
try {
|
|
463
|
+
catalog = await io.fetchModels(config);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`tenant model catalog is unavailable (${redactSecretText(error?.message || error)}); the selected tenant app was not opened`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const vendorPaths = {};
|
|
471
|
+
for (const status of statuses) {
|
|
472
|
+
let vendorPath = status.vendorPath;
|
|
473
|
+
if (!vendorPath) {
|
|
474
|
+
const result = io.ensureVendor(status.target, { homeDir });
|
|
475
|
+
vendorPath = result.path;
|
|
476
|
+
io.log(`${status.target}: vendor app ${result.action}${result.note ? ` (${result.note})` : ""}`);
|
|
477
|
+
if (!vendorPath) {
|
|
478
|
+
throw new Error(`${status.target} verified vendor app is unavailable; retry the pinned download`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
vendorPaths[status.target] = vendorPath;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Only stale or absent bundles pay the clone + codesign rebuild. The model
|
|
485
|
+
// catalog and vendor checks above finish before any running apps are closed.
|
|
486
|
+
const staleBundleTargets = statuses
|
|
487
|
+
.filter((status) => !io.bundleCurrent(status))
|
|
488
|
+
.map((status) => status.target);
|
|
489
|
+
if (staleBundleTargets.length > 0) {
|
|
490
|
+
await io.quitApps(staleBundleTargets, {
|
|
491
|
+
tenantId: config.tenantId,
|
|
492
|
+
tenantName: config.tenantName,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const installed = io.installFiles({
|
|
497
|
+
config,
|
|
498
|
+
targets,
|
|
499
|
+
models: catalog.models,
|
|
500
|
+
homeDir,
|
|
501
|
+
vendorPaths,
|
|
502
|
+
writeBundles: staleBundleTargets,
|
|
503
|
+
});
|
|
504
|
+
const newlyInstalled = new Set(
|
|
505
|
+
statuses.filter((status) => !status.launcherInstalled).map((status) => status.target),
|
|
506
|
+
);
|
|
507
|
+
for (const item of installed) {
|
|
508
|
+
if (newlyInstalled.has(item.target)) {
|
|
509
|
+
io.log(`Installed and configured ${item.launcher} for tenant ${config.tenantId}.`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Match explicit install setup without penalizing repeat opens: this helper
|
|
514
|
+
// only runs when the zero-network fast path has detected drift or first use.
|
|
515
|
+
const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
|
|
516
|
+
const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
|
|
517
|
+
for (const item of installed) {
|
|
518
|
+
const { client, env, label } = appSkillTarget(item.target, paths);
|
|
519
|
+
await io.syncSkills({ client, gatewayUrl, env, label });
|
|
520
|
+
if (item.target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
|
|
521
|
+
}
|
|
522
|
+
for (const item of installed) await io.openLauncher(item.launcher);
|
|
523
|
+
return installed;
|
|
524
|
+
}
|
|
525
|
+
|
|
448
526
|
const REFRESH_TTL_MS = 6 * 60 * 60 * 1000;
|
|
449
527
|
|
|
450
528
|
/**
|