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/README.md
CHANGED
|
@@ -121,6 +121,9 @@ impel update [--check] [--skip-apps] Update EVERYTHING: reinstall the CL
|
|
|
121
121
|
cascade to `app update all` and `skills sync all`
|
|
122
122
|
impel auth [--pat <pat>] [--gateway <url>] [--app <url>]
|
|
123
123
|
Store your Impel PAT + URLs
|
|
124
|
+
impel pat create --label <label> [options] Mint a personal PAT through the control plane
|
|
125
|
+
impel pat create --agent <agent-id> [options]
|
|
126
|
+
Mint a PAT associated with an org agent
|
|
124
127
|
impel token [--tenant <org>] Print a selected-tenant bearer
|
|
125
128
|
impel mcp Run the local MCP bridge used by Claude/Codex
|
|
126
129
|
impel claude [claude args...] Launch isolated Impel Claude Code
|
|
@@ -145,18 +148,22 @@ impel off [claude|codex|all] Alias for `impel use account`
|
|
|
145
148
|
impel skills sync [claude|codex|all] Sync Bifrost shared skills into managed clients (default: all)
|
|
146
149
|
|
|
147
150
|
impel status Launcher readiness + native mode + gateway reachability
|
|
148
|
-
impel app install [
|
|
149
|
-
impel app update [
|
|
150
|
-
impel app refresh [
|
|
151
|
+
impel app install [target] [--tenant <org>] Install isolated apps for one tenant
|
|
152
|
+
impel app update [target] [--tenant <org>] Refresh that tenant's apps, configs, and models
|
|
153
|
+
impel app refresh [target] [--tenant <org>] Configs/catalog/skills only; safe while apps run
|
|
151
154
|
(--stale-only: no-op unless 6h+ since last sync)
|
|
152
|
-
impel app status [
|
|
153
|
-
impel app open [
|
|
155
|
+
impel app status [target] [--tenant <org>] Show tenant launcher/vendor status
|
|
156
|
+
impel app open [target] [--tenant <org>] Launch isolated instances (fast path: opens
|
|
154
157
|
immediately when nothing changed since last open;
|
|
155
158
|
a detached stale-only refresh keeps things current)
|
|
156
|
-
impel app uninstall [target] [--keep-data]
|
|
159
|
+
impel app uninstall [target] [--tenant <org>] [--keep-data]
|
|
160
|
+
Remove one tenant's Impel-managed app files
|
|
157
161
|
impel help Show help
|
|
158
162
|
impel --version Show version
|
|
159
163
|
|
|
164
|
+
For app commands, `target` is `claude`, `chatgpt`/`codex`, or `all` (the
|
|
165
|
+
default). `--tenant` defaults to the currently selected organization.
|
|
166
|
+
|
|
160
167
|
# `impel setup` no longer takes a per-tool target and never flips native
|
|
161
168
|
# configs; flip a native tool explicitly with `impel use gateway [claude|codex]`.
|
|
162
169
|
```
|
|
@@ -179,6 +186,35 @@ Stores your PAT, gateway base URL, and app/control-plane URL in `~/.config/impel
|
|
|
179
186
|
impel auth --pat impel_pat_xxxxxxxxxxxx --gateway https://gateway.useimpel.com --app https://www.useimpel.com
|
|
180
187
|
```
|
|
181
188
|
|
|
189
|
+
### `impel pat create ...` (alias `impel pat mint ...`)
|
|
190
|
+
|
|
191
|
+
Mints a new PAT through the app/control-plane API, authenticated by the PAT
|
|
192
|
+
already stored by `impel auth`. Identity remains the minting authority: the
|
|
193
|
+
control plane verifies the current owner, target-org membership, requested
|
|
194
|
+
scopes, product access, and (when applicable) agent access before it requests a
|
|
195
|
+
new secret. The gateway never mints or verifies PATs.
|
|
196
|
+
|
|
197
|
+
Use `--label` for a personal token or `--agent` for a token associated with an
|
|
198
|
+
org agent. Agent PATs are still personal credentials: they act as you, are
|
|
199
|
+
org-scoped, and use the same stable `Agent · <agent-id>` association as the agent
|
|
200
|
+
setup UI.
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
impel pat create --label "Rahul's laptop"
|
|
204
|
+
impel pat create --agent support-agent --org acme
|
|
205
|
+
impel pat create --label ci-automation --scopes codex-gateway,tasks --ttl-days 30 --json
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`--org` defaults to the selected tenant (`--tenant` is an alias). `--scopes`
|
|
209
|
+
accepts `claude-code-gateway`, `codex-gateway`, and `tasks`; when omitted, the
|
|
210
|
+
control plane chooses the defaults allowed by your access tier. `--ttl-days`
|
|
211
|
+
accepts 1–365 and defaults to 90 days.
|
|
212
|
+
|
|
213
|
+
The new secret is returned once on stdout and is deliberately **not** written to
|
|
214
|
+
the config file. To rotate this CLI onto it, explicitly run
|
|
215
|
+
`impel auth --pat <new-pat>` after copying it. `--json` includes the raw secret,
|
|
216
|
+
so treat redirected output as a credential.
|
|
217
|
+
|
|
182
218
|
### `impel update`
|
|
183
219
|
|
|
184
220
|
One command brings everything current, in dependency order: the CLI itself
|
|
@@ -237,12 +273,14 @@ not bleed between tenants. `impel tenant use <org> --launch claude|codex|apps`
|
|
|
237
273
|
selects and launches in one command.
|
|
238
274
|
|
|
239
275
|
On macOS, desktop app state is isolated under
|
|
240
|
-
`~/.config/impel/apps/tenants/<org>/`.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
276
|
+
`~/.config/impel/apps/tenants/<org>/`. Every installed tenant keeps its own app
|
|
277
|
+
bundles in `~/Applications`, with names such as `Impel Claude (Acme)` and
|
|
278
|
+
`Impel ChatGPT (Acme)`, stable tenant-specific bundle identifiers, and fixed
|
|
279
|
+
tenant profile/auth paths. Different tenants can stay open concurrently.
|
|
280
|
+
`impel app open --tenant <org>` refreshes and launches a tenant directly without
|
|
281
|
+
changing the globally selected tenant. Switching tenants never deletes local
|
|
282
|
+
history or projects; each tenant's preserved profile reopens independently.
|
|
283
|
+
Windows Claude Desktop keeps each
|
|
246
284
|
tenant under `%LOCALAPPDATA%\Claude-3p\Impel\<org>` and launches the unchanged
|
|
247
285
|
signed vendor executable with that profile selected. Windows ChatGPT keeps its
|
|
248
286
|
Codex and browser state under the same tenant-scoped Impel apps root used on
|
|
@@ -577,21 +615,24 @@ app-target names.
|
|
|
577
615
|
|
|
578
616
|
### macOS Claude and ChatGPT
|
|
579
617
|
|
|
580
|
-
`impel app install` creates
|
|
581
|
-
|
|
582
|
-
|
|
618
|
+
`impel app install` creates tenant-specific apps such as
|
|
619
|
+
`Impel Claude (acme).app` and `Impel ChatGPT (acme).app` in `~/Applications`.
|
|
620
|
+
Their visible bundle names use the organization display name, for example
|
|
621
|
+
`Impel Claude (Acme)` and `Impel ChatGPT (Acme)`. Each tenant/product pair has
|
|
622
|
+
a stable, unique bundle identifier and gateway configuration:
|
|
583
623
|
|
|
584
624
|
- Impel Claude is an APFS-cloned vendored copy of the official app with its own
|
|
585
625
|
bundle and helper identities. Its LaunchServices environment sets the
|
|
586
|
-
vendor-supported `CLAUDE_USER_DATA_DIR` and writes the 3P gateway
|
|
587
|
-
`~/.config/impel/apps/claude`. A narrowly
|
|
588
|
-
connects Claude's native plan-usage meter
|
|
589
|
-
Claude subscription pool exposed by the Impel
|
|
626
|
+
vendor-supported `CLAUDE_USER_DATA_DIR` and writes the 3P gateway
|
|
627
|
+
`configLibrary` below `~/.config/impel/apps/tenants/<org>/claude`. A narrowly
|
|
628
|
+
version-checked compatibility patch connects Claude's native plan-usage meter
|
|
629
|
+
to the authenticated aggregate Claude subscription pool exposed by the Impel
|
|
630
|
+
gateway.
|
|
590
631
|
- Impel ChatGPT wraps an APFS-cloned official app while preserving the nested
|
|
591
632
|
OpenAI-signed bundle and executable. Its outer resource root contains the
|
|
592
633
|
narrowly patched renderer ASAR used at runtime. The wrapper sets `CODEX_HOME`,
|
|
593
634
|
passes a separate Chromium `--user-data-dir`, and writes its provider config
|
|
594
|
-
and model catalog below `~/.config/impel/apps/chatgpt`. It also supplies a
|
|
635
|
+
and model catalog below `~/.config/impel/apps/tenants/<org>/chatgpt`. It also supplies a
|
|
595
636
|
process-scoped, Codex-compatible wrapper around the Impel PAT so native
|
|
596
637
|
`/status` requests can read the organization's aggregated subscription pool
|
|
597
638
|
from the gateway. Its generated Codex config uses the same snapshot and
|
|
@@ -601,9 +642,22 @@ configuration:
|
|
|
601
642
|
- Neither app changes `~/.claude`, `~/.claude.json`, `~/.codex`, or the
|
|
602
643
|
normal vendor app's browser/profile data.
|
|
603
644
|
|
|
645
|
+
Install or open several tenants without changing the current selection:
|
|
646
|
+
|
|
647
|
+
```sh
|
|
648
|
+
impel app install all --tenant acme
|
|
649
|
+
impel app install all --tenant globex
|
|
650
|
+
impel app open all --tenant acme
|
|
651
|
+
impel app open all --tenant globex
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Updates and uninstalls touch only the named tenant variant. The older global
|
|
655
|
+
`Impel Claude.app` / `Impel ChatGPT.app` launcher is removed after the first
|
|
656
|
+
successful tenant-specific rebuild.
|
|
657
|
+
|
|
604
658
|
The official Claude and ChatGPT apps remain separate installations and keep
|
|
605
659
|
their normal accounts, updater, profiles, and bundle identities. Each CLI
|
|
606
|
-
release pins exact, tested vendor builds (v0.
|
|
660
|
+
release pins exact, tested vendor builds (v0.9.0 pins Claude `1.20186.9` and
|
|
607
661
|
ChatGPT `26.707.72221`) and their archive SHA-256 digests. The installer reuses
|
|
608
662
|
an existing official app only when its version exactly matches; otherwise it
|
|
609
663
|
downloads the pinned signed archive into Impel's managed vendor cache under
|
|
@@ -715,3 +769,15 @@ OpenAI Responses front door for SDK/Eve traffic and may normalize the body
|
|
|
715
769
|
- https://developers.openai.com/codex/config-advanced
|
|
716
770
|
- https://developers.openai.com/codex/auth
|
|
717
771
|
- https://developers.openai.com/codex/ide
|
|
772
|
+
|
|
773
|
+
## Sources consulted for concurrent tenant apps
|
|
774
|
+
|
|
775
|
+
- Electron v43.1.0 release: https://github.com/electron/electron/releases/tag/v43.1.0
|
|
776
|
+
- Electron applies `--user-data-dir` before app JavaScript:
|
|
777
|
+
https://github.com/electron/electron/blob/b5c102b3f0f7e9c5be064ba337a3547a83cf7d09/shell/app/electron_main_delegate.cc#L247-L256
|
|
778
|
+
- Electron scopes its single-instance lock to the resolved user-data directory:
|
|
779
|
+
https://github.com/electron/electron/blob/b5c102b3f0f7e9c5be064ba337a3547a83cf7d09/shell/browser/api/electron_api_app.cc#L1049-L1073
|
|
780
|
+
- Electron macOS bundle rebranding requirements:
|
|
781
|
+
https://github.com/electron/electron/blob/b5c102b3f0f7e9c5be064ba337a3547a83cf7d09/docs/tutorial/application-distribution.md#L79-L96
|
|
782
|
+
- Apple `CFBundleIdentifier` reference:
|
|
783
|
+
https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -21,11 +21,13 @@ const CLI_VERSION = JSON.parse(
|
|
|
21
21
|
const APP_DEFINITIONS = {
|
|
22
22
|
claude: {
|
|
23
23
|
label: "Impel Claude",
|
|
24
|
+
bundleIdentifier: "com.useimpel.claude",
|
|
24
25
|
names: ["Claude.app"],
|
|
25
26
|
executableNames: ["Claude"],
|
|
26
27
|
},
|
|
27
28
|
chatgpt: {
|
|
28
29
|
label: "Impel ChatGPT",
|
|
30
|
+
bundleIdentifier: "com.useimpel.chatgpt",
|
|
29
31
|
names: ["ChatGPT.app", "Codex.app"],
|
|
30
32
|
executableNames: ["ChatGPT", "Codex"],
|
|
31
33
|
},
|
|
@@ -103,9 +105,44 @@ export function normalizeAppTarget(value) {
|
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
function tenantAppName(value, fallback) {
|
|
109
|
+
const name = redactSecretText(String(value || fallback))
|
|
110
|
+
.normalize("NFC")
|
|
111
|
+
.replace(/[/:]/gu, "-")
|
|
112
|
+
.replace(/\s+/gu, " ")
|
|
113
|
+
.trim()
|
|
114
|
+
.slice(0, 128);
|
|
115
|
+
return name || fallback;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Stable macOS name and LaunchServices identity for one tenant-bound app. */
|
|
119
|
+
export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
120
|
+
const definition = APP_DEFINITIONS[target];
|
|
121
|
+
if (!definition) throw new Error(`unknown app target "${redactSecretText(target)}"`);
|
|
122
|
+
if (!tenantId) {
|
|
123
|
+
return {
|
|
124
|
+
displayName: definition.label,
|
|
125
|
+
launcherName: `${definition.label}.app`,
|
|
126
|
+
bundleIdentifier: definition.bundleIdentifier,
|
|
127
|
+
helperBundleIdentifier: `${definition.bundleIdentifier}.helper`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const normalizedTenantId = normalizeTenantId(tenantId);
|
|
131
|
+
const displayName = `${definition.label} (${tenantAppName(tenantName, normalizedTenantId)})`;
|
|
132
|
+
const tenantKey = crypto.createHash("sha256").update(normalizedTenantId, "utf8").digest("hex").slice(0, 20);
|
|
133
|
+
return {
|
|
134
|
+
displayName,
|
|
135
|
+
// Keep the on-disk path stable if an organization is renamed; Finder,
|
|
136
|
+
// Launchpad, and the Dock use CFBundleDisplayName below for the human name.
|
|
137
|
+
launcherName: `${definition.label} (${normalizedTenantId}).app`,
|
|
138
|
+
bundleIdentifier: `${definition.bundleIdentifier}.tenant.${tenantKey}`,
|
|
139
|
+
helperBundleIdentifier: `${definition.bundleIdentifier}.tenant.${tenantKey}.helper`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
// Bump when the written config/manifest schema changes; a mismatch forces the
|
|
107
144
|
// slow open path (and thus a full config rewrite) after a CLI update.
|
|
108
|
-
export const CURRENT_CONFIG_VERSION =
|
|
145
|
+
export const CURRENT_CONFIG_VERSION = 9;
|
|
109
146
|
|
|
110
147
|
/** Parse the tenant's install manifest, or null when absent/corrupt. */
|
|
111
148
|
export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
|
|
@@ -144,6 +181,8 @@ export function bundleIsCurrent(status) {
|
|
|
144
181
|
compatibility
|
|
145
182
|
&& compatibility.cliVersion === CLI_VERSION
|
|
146
183
|
&& compatibility.vendorVersion === status.vendorVersion
|
|
184
|
+
&& compatibility.bundleIdentifier === status.bundleIdentifier
|
|
185
|
+
&& compatibility.displayName === status.label
|
|
147
186
|
&& compatibility.profileRoot === status.profileRoot
|
|
148
187
|
&& (status.target !== "chatgpt" || (
|
|
149
188
|
compatibility.codexHome === status.codexHome
|
|
@@ -182,24 +221,28 @@ function managedBundleMatchesCompatibility(status, compatibility) {
|
|
|
182
221
|
// and codesign failures.
|
|
183
222
|
|
|
184
223
|
/** osascript application names whose bundles install/update will touch. */
|
|
185
|
-
export function blockingAppNames(targets) {
|
|
224
|
+
export function blockingAppNames(targets, { tenantId = null, tenantName = null } = {}) {
|
|
186
225
|
const names = [];
|
|
187
226
|
for (const target of targets) {
|
|
188
227
|
const definition = APP_DEFINITIONS[target];
|
|
189
228
|
for (const bundleName of definition.names) names.push(bundleName.replace(/\.app$/u, ""));
|
|
190
|
-
names.push(
|
|
229
|
+
names.push(managedAppIdentity(target, tenantId, tenantName).launcherName.replace(/\.app$/u, ""));
|
|
230
|
+
// The pre-v0.8 launcher was global. Close it too while a tenant-specific
|
|
231
|
+
// bundle replaces/migrates it, but never close another tenant's variant.
|
|
232
|
+
if (tenantId) names.push(definition.label);
|
|
191
233
|
}
|
|
192
|
-
return names;
|
|
234
|
+
return [...new Set(names)];
|
|
193
235
|
}
|
|
194
236
|
|
|
195
237
|
/**
|
|
196
238
|
* pgrep/pkill -f pattern for one app's processes. Anchored on
|
|
197
|
-
* "/<Name>.app
|
|
198
|
-
*
|
|
239
|
+
* "/<Name>.app/.../Contents/MacOS/" so "Claude" never matches the
|
|
240
|
+
* space-prefixed tenant launcher (and vice versa), while nested Electron
|
|
241
|
+
* executables and helpers still count as processes belonging to the wrapper.
|
|
199
242
|
*/
|
|
200
243
|
export function appProcessPattern(name) {
|
|
201
244
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
202
|
-
return `/${escaped}\\.app/Contents/MacOS/`;
|
|
245
|
+
return `/${escaped}\\.app/(.*/)?Contents/MacOS/`;
|
|
203
246
|
}
|
|
204
247
|
|
|
205
248
|
const QUIT_POLL_MS = 500;
|
|
@@ -210,6 +253,8 @@ const QUIT_POLL_MS = 500;
|
|
|
210
253
|
* telling the user to close it themselves if even that fails.
|
|
211
254
|
*/
|
|
212
255
|
export async function quitBlockingApps(targets, {
|
|
256
|
+
tenantId = null,
|
|
257
|
+
tenantName = null,
|
|
213
258
|
log = console.log,
|
|
214
259
|
isRunning = (name) =>
|
|
215
260
|
spawnSync("/usr/bin/pgrep", ["-f", appProcessPattern(name)], { stdio: "ignore" }).status === 0,
|
|
@@ -234,7 +279,7 @@ export async function quitBlockingApps(targets, {
|
|
|
234
279
|
return !isRunning(name);
|
|
235
280
|
}
|
|
236
281
|
|
|
237
|
-
for (const name of blockingAppNames(targets)) {
|
|
282
|
+
for (const name of blockingAppNames(targets, { tenantId, tenantName })) {
|
|
238
283
|
if (!isRunning(name)) continue;
|
|
239
284
|
log(`${name} is running; closing it before touching app bundles…`);
|
|
240
285
|
requestQuit(name);
|
|
@@ -246,26 +291,33 @@ export async function quitBlockingApps(targets, {
|
|
|
246
291
|
}
|
|
247
292
|
}
|
|
248
293
|
|
|
249
|
-
export function appPaths(homeDir = os.homedir(), tenantId = null, {
|
|
294
|
+
export function appPaths(homeDir = os.homedir(), tenantId = null, {
|
|
295
|
+
claudeUserData = null,
|
|
296
|
+
tenantName = null,
|
|
297
|
+
} = {}) {
|
|
250
298
|
const root = process.env.IMPEL_APP_HOME || path.join(homeDir, ".config", "impel", "apps");
|
|
251
299
|
const tenantRoot = tenantId
|
|
252
300
|
? path.join(root, "tenants", normalizeTenantId(tenantId))
|
|
253
301
|
: root;
|
|
302
|
+
const claudeIdentity = managedAppIdentity("claude", tenantId, tenantName);
|
|
303
|
+
const chatgptIdentity = managedAppIdentity("chatgpt", tenantId, tenantName);
|
|
254
304
|
return {
|
|
255
305
|
root,
|
|
256
306
|
tenantRoot,
|
|
257
307
|
tokenHelper: path.join(tenantRoot, "bin", "token"),
|
|
258
308
|
claude: {
|
|
309
|
+
...claudeIdentity,
|
|
259
310
|
root: claudeUserData || path.join(tenantRoot, "claude"),
|
|
260
311
|
userData: claudeUserData || path.join(tenantRoot, "claude", "user-data"),
|
|
261
|
-
launcher: path.join(homeDir, "Applications",
|
|
312
|
+
launcher: path.join(homeDir, "Applications", claudeIdentity.launcherName),
|
|
262
313
|
},
|
|
263
314
|
chatgpt: {
|
|
315
|
+
...chatgptIdentity,
|
|
264
316
|
root: path.join(tenantRoot, "chatgpt"),
|
|
265
317
|
codexHome: path.join(tenantRoot, "chatgpt", "codex-home"),
|
|
266
318
|
browserData: path.join(tenantRoot, "chatgpt", "browser-data"),
|
|
267
319
|
catalog: path.join(tenantRoot, "chatgpt", "codex-home", "models.json"),
|
|
268
|
-
launcher: path.join(homeDir, "Applications",
|
|
320
|
+
launcher: path.join(homeDir, "Applications", chatgptIdentity.launcherName),
|
|
269
321
|
},
|
|
270
322
|
};
|
|
271
323
|
}
|
|
@@ -309,7 +361,10 @@ export function installManagedAppFiles({
|
|
|
309
361
|
// true = rebuild every target's bundle; false = configs only; an array
|
|
310
362
|
// limits the expensive rebuild to just the stale targets.
|
|
311
363
|
const bundleTargets = new Set(writeBundles === true ? targets : writeBundles || []);
|
|
312
|
-
const paths = appPaths(homeDir, config.tenantId, {
|
|
364
|
+
const paths = appPaths(homeDir, config.tenantId, {
|
|
365
|
+
claudeUserData,
|
|
366
|
+
tenantName: config.tenantName,
|
|
367
|
+
});
|
|
313
368
|
if (targets.includes("chatgpt")) secureAllManagedCodexHomes({ appsRoot: paths.root });
|
|
314
369
|
fs.mkdirSync(paths.tenantRoot, { recursive: true, mode: 0o700 });
|
|
315
370
|
if (writeTokenHelperFile) writeTokenHelper(paths.tokenHelper, config.tenantId);
|
|
@@ -334,16 +389,22 @@ export function installManagedAppFiles({
|
|
|
334
389
|
}
|
|
335
390
|
if (target === "chatgpt") writeVendoredChatGPTBundle(paths, resolvedVendorPaths.chatgpt, config.gatewayUrl);
|
|
336
391
|
else writeVendoredClaudeBundle(paths, resolvedVendorPaths.claude, config.gatewayUrl);
|
|
392
|
+
if (config.tenantId) {
|
|
393
|
+
// A successful tenant-specific rebuild supersedes the old global
|
|
394
|
+
// launcher. Other tenant launchers have distinct names and survive.
|
|
395
|
+
fs.rmSync(appPaths(homeDir)[target].launcher, { recursive: true, force: true });
|
|
396
|
+
}
|
|
337
397
|
}
|
|
338
398
|
installed.push({ target, launcher: paths[target].launcher });
|
|
339
399
|
}
|
|
340
400
|
|
|
341
401
|
writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
|
|
342
|
-
schemaVersion:
|
|
402
|
+
schemaVersion: 4,
|
|
343
403
|
configVersion: CURRENT_CONFIG_VERSION,
|
|
344
404
|
modelCatalogVersion: 3,
|
|
345
405
|
gatewayUrl: config.gatewayUrl,
|
|
346
406
|
tenantId: config.tenantId,
|
|
407
|
+
tenantName: config.tenantName || config.tenantId || null,
|
|
347
408
|
experiments: { crossAppModels: crossAppModelsEnabled(config) },
|
|
348
409
|
targets,
|
|
349
410
|
models: models.map((model) => model.id),
|
|
@@ -518,18 +579,22 @@ export function ensureVendorApp(target, { homeDir = os.homedir() } = {}) {
|
|
|
518
579
|
return { path: installed, action: "installed", note: `verified ${pin.version}` };
|
|
519
580
|
}
|
|
520
581
|
|
|
521
|
-
export function appStatus(targets, homeDir = os.homedir(), tenantId = null) {
|
|
522
|
-
const
|
|
582
|
+
export function appStatus(targets, homeDir = os.homedir(), tenantId = null, tenantName = null) {
|
|
583
|
+
const manifestName = tenantId && !tenantName
|
|
584
|
+
? readTenantManifest(homeDir, tenantId)?.tenantName
|
|
585
|
+
: null;
|
|
586
|
+
const paths = appPaths(homeDir, tenantId, { tenantName: tenantName || manifestName });
|
|
523
587
|
return targets.map((target) => {
|
|
524
588
|
const vendorPath = findVendorApp(target, homeDir);
|
|
525
589
|
return {
|
|
526
590
|
target,
|
|
527
|
-
label:
|
|
591
|
+
label: paths[target].displayName,
|
|
528
592
|
launcher: paths[target].launcher,
|
|
529
593
|
launcherInstalled: fs.existsSync(paths[target].launcher),
|
|
594
|
+
bundleIdentifier: paths[target].bundleIdentifier,
|
|
595
|
+
profileRoot: target === "claude" ? paths.claude.userData : paths.chatgpt.root,
|
|
530
596
|
vendorPath,
|
|
531
597
|
vendorVersion: vendorPath ? bundleVersion(vendorPath) : null,
|
|
532
|
-
profileRoot: target === "claude" ? paths.claude.userData : paths.chatgpt.root,
|
|
533
598
|
codexHome: target === "chatgpt" ? paths.chatgpt.codexHome : null,
|
|
534
599
|
browserData: target === "chatgpt" ? paths.chatgpt.browserData : null,
|
|
535
600
|
configPath: target === "claude"
|
|
@@ -543,9 +608,10 @@ export function removeManagedApps(targets, {
|
|
|
543
608
|
homeDir = os.homedir(),
|
|
544
609
|
keepData = false,
|
|
545
610
|
tenantId = null,
|
|
611
|
+
tenantName = null,
|
|
546
612
|
claudeUserData = null,
|
|
547
613
|
} = {}) {
|
|
548
|
-
const paths = appPaths(homeDir, tenantId, { claudeUserData });
|
|
614
|
+
const paths = appPaths(homeDir, tenantId, { claudeUserData, tenantName });
|
|
549
615
|
for (const target of targets) {
|
|
550
616
|
fs.rmSync(paths[target].launcher, { recursive: true, force: true });
|
|
551
617
|
if (!keepData) fs.rmSync(paths[target].root, { recursive: true, force: true });
|
|
@@ -852,13 +918,14 @@ function writeTokenHelper(target, tenantId) {
|
|
|
852
918
|
const node = shellQuote(process.execPath);
|
|
853
919
|
const cli = shellQuote(cliPath);
|
|
854
920
|
const tenantArgs = tenantId ? ` --tenant ${shellQuote(tenantId)}` : "";
|
|
921
|
+
const refreshTenantArgs = tenantId ? ` --tenant ${shellQuote(tenantId)}` : "";
|
|
855
922
|
// The apps invoke this helper whenever they need auth, which makes it the
|
|
856
923
|
// one hook that fires on Finder/Dock launches too. Piggyback a detached,
|
|
857
924
|
// TTL-gated config/catalog/skills refresh (`app refresh --stale-only`) so
|
|
858
925
|
// the managed apps keep themselves current without touching stdout — the
|
|
859
926
|
// caller consumes stdout as the token.
|
|
860
927
|
const script = `#!/bin/sh
|
|
861
|
-
(${node} ${cli} app refresh --stale-only </dev/null >/dev/null 2>&1 &)
|
|
928
|
+
(${node} ${cli} app refresh --stale-only${refreshTenantArgs} </dev/null >/dev/null 2>&1 &)
|
|
862
929
|
exec ${node} ${cli} token${tenantArgs}
|
|
863
930
|
`;
|
|
864
931
|
writeAtomic(target, script, 0o700);
|
|
@@ -900,27 +967,28 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl) {
|
|
|
900
967
|
const newAsarHash = asarHeaderHash(asarPath);
|
|
901
968
|
|
|
902
969
|
updateBundleIdentity(plistPath, {
|
|
903
|
-
displayName:
|
|
970
|
+
displayName: paths.claude.displayName,
|
|
904
971
|
executable: executableName,
|
|
905
|
-
identifier:
|
|
906
|
-
name:
|
|
972
|
+
identifier: paths.claude.bundleIdentifier,
|
|
973
|
+
name: paths.claude.displayName,
|
|
907
974
|
});
|
|
908
975
|
updateAsarIntegrityPlist(plistPath, oldAsarHash, newAsarHash);
|
|
909
976
|
setPlistEnvironmentString(plistPath, "CLAUDE_USER_DATA_DIR", paths.claude.userData);
|
|
910
977
|
rebrandElectronHelpers(staging, {
|
|
911
978
|
fromName: "Claude",
|
|
912
|
-
toName:
|
|
979
|
+
toName: paths.claude.displayName,
|
|
913
980
|
fromIdentifier: "com.anthropic.claudefordesktop.helper",
|
|
914
|
-
toIdentifier:
|
|
981
|
+
toIdentifier: paths.claude.helperBundleIdentifier,
|
|
915
982
|
});
|
|
916
983
|
|
|
917
984
|
const macos = path.join(staging, "Contents", "MacOS");
|
|
918
985
|
writeAtomic(path.join(staging, "Contents", "Resources", "impel-compatibility.json"), JSON.stringify({
|
|
919
|
-
schemaVersion:
|
|
986
|
+
schemaVersion: 2,
|
|
920
987
|
cliVersion: CLI_VERSION,
|
|
921
988
|
vendorVersion: bundleVersion(vendorPath),
|
|
922
989
|
sourceBundle: vendorPath,
|
|
923
|
-
bundleIdentifier:
|
|
990
|
+
bundleIdentifier: paths.claude.bundleIdentifier,
|
|
991
|
+
displayName: paths.claude.displayName,
|
|
924
992
|
profileRoot: paths.claude.userData,
|
|
925
993
|
launchMode: "LSEnvironment",
|
|
926
994
|
patches: {
|
|
@@ -984,7 +1052,7 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
984
1052
|
const asarHash = asarHeaderHash(runtimeAsarPath);
|
|
985
1053
|
writeAtomic(
|
|
986
1054
|
path.join(staging, "Contents", "Info.plist"),
|
|
987
|
-
vendoredChatGPTWrapperPlist(vendorPath, asarHash),
|
|
1055
|
+
vendoredChatGPTWrapperPlist(vendorPath, asarHash, paths.chatgpt),
|
|
988
1056
|
0o644,
|
|
989
1057
|
);
|
|
990
1058
|
writeAtomic(
|
|
@@ -993,10 +1061,12 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
993
1061
|
0o755,
|
|
994
1062
|
);
|
|
995
1063
|
writeAtomic(path.join(staging, "Contents", "Resources", "impel-compatibility.json"), JSON.stringify({
|
|
996
|
-
schemaVersion:
|
|
1064
|
+
schemaVersion: 2,
|
|
997
1065
|
cliVersion: CLI_VERSION,
|
|
998
1066
|
vendorVersion: bundleVersion(vendorPath),
|
|
999
1067
|
sourceBundle: vendorPath,
|
|
1068
|
+
bundleIdentifier: paths.chatgpt.bundleIdentifier,
|
|
1069
|
+
displayName: paths.chatgpt.displayName,
|
|
1000
1070
|
profileRoot: paths.chatgpt.root,
|
|
1001
1071
|
codexHome: paths.chatgpt.codexHome,
|
|
1002
1072
|
browserData: paths.chatgpt.browserData,
|
|
@@ -1265,15 +1335,15 @@ function rebrandElectronHelpers(bundle, { fromName, toName, fromIdentifier, toId
|
|
|
1265
1335
|
}
|
|
1266
1336
|
}
|
|
1267
1337
|
|
|
1268
|
-
function vendoredChatGPTWrapperPlist(vendorPath, asarHash) {
|
|
1338
|
+
function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity) {
|
|
1269
1339
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1270
1340
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1271
1341
|
<plist version="1.0"><dict>
|
|
1272
|
-
<key>CFBundleDisplayName</key><string
|
|
1342
|
+
<key>CFBundleDisplayName</key><string>${xmlEscape(identity.displayName)}</string>
|
|
1273
1343
|
<key>CFBundleExecutable</key><string>launch</string>
|
|
1274
1344
|
<key>CFBundleIconFile</key><string>electron.icns</string>
|
|
1275
|
-
<key>CFBundleIdentifier</key><string
|
|
1276
|
-
<key>CFBundleName</key><string
|
|
1345
|
+
<key>CFBundleIdentifier</key><string>${xmlEscape(identity.bundleIdentifier)}</string>
|
|
1346
|
+
<key>CFBundleName</key><string>${xmlEscape(identity.displayName)}</string>
|
|
1277
1347
|
<key>CFBundlePackageType</key><string>APPL</string>
|
|
1278
1348
|
<key>CFBundleShortVersionString</key><string>${xmlEscape(bundleVersion(vendorPath) || "1")}</string>
|
|
1279
1349
|
<key>ElectronAsarIntegrity</key><dict><key>Resources/app.asar</key><dict><key>algorithm</key><string>SHA256</string><key>hash</key><string>${xmlEscape(asarHash)}</string></dict></dict>
|
|
@@ -1372,9 +1442,14 @@ function signVendoredClaudeCapabilities(bundle) {
|
|
|
1372
1442
|
try {
|
|
1373
1443
|
const frameworks = path.join(bundle, "Contents", "Frameworks");
|
|
1374
1444
|
const helpers = fs.readdirSync(frameworks, { withFileTypes: true })
|
|
1375
|
-
.filter((entry) =>
|
|
1445
|
+
.filter((entry) => (
|
|
1446
|
+
entry.isDirectory()
|
|
1447
|
+
&& entry.name.startsWith("Impel Claude")
|
|
1448
|
+
&& entry.name.includes(" Helper")
|
|
1449
|
+
&& entry.name.endsWith(".app")
|
|
1450
|
+
));
|
|
1376
1451
|
for (const helper of helpers) {
|
|
1377
|
-
const entitlements = helper.name
|
|
1452
|
+
const entitlements = helper.name.endsWith(" Helper.app") ? fullPath : jitPath;
|
|
1378
1453
|
const result = codesign([
|
|
1379
1454
|
"--force", "--deep", "--options", "runtime", "--entitlements", entitlements, "--sign", "-",
|
|
1380
1455
|
path.join(frameworks, helper.name),
|
package/src/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
4
|
import { cmdAuth } from "./commands/auth.js";
|
|
5
|
+
import { cmdPat } from "./commands/pat.js";
|
|
5
6
|
import { cmdToken } from "./commands/token.js";
|
|
6
7
|
import { cmdUse } from "./commands/use.js";
|
|
7
8
|
import { cmdStatus } from "./commands/status.js";
|
|
@@ -28,12 +29,14 @@ Get started:
|
|
|
28
29
|
Run:
|
|
29
30
|
impel claude [args...] Launch Claude Code with an isolated Impel profile
|
|
30
31
|
impel codex [args...] Launch Codex with an isolated Impel profile
|
|
31
|
-
impel app open [
|
|
32
|
+
impel app open [target] [--tenant <org>] Open that tenant's isolated desktop app(s)
|
|
32
33
|
impel status Tenant, PAT, per-tool mode, gateway reachability
|
|
33
34
|
impel doctor [--tenant <org>|--all-tenants] Synthetic provider, routing, and latency checks
|
|
34
35
|
|
|
35
36
|
Manage:
|
|
36
37
|
impel auth [--pat <pat>] Store your PAT + URLs (--gateway/--app <url>)
|
|
38
|
+
impel pat create --label <label> Mint a personal PAT (--org/--scopes/--ttl-days)
|
|
39
|
+
impel pat create --agent <agent-id> Mint an org-scoped PAT associated with an agent
|
|
37
40
|
impel tenant list|current|use <org> List or select the active organization
|
|
38
41
|
impel tasks list|get|create|update|delete CRUD Impel tickets (aliases: task, ticket[s])
|
|
39
42
|
impel token [--tenant <org>] Print the selected-tenant bearer
|
|
@@ -41,11 +44,13 @@ Manage:
|
|
|
41
44
|
impel skills sync [claude|codex|all] Sync shared skills into managed clients
|
|
42
45
|
|
|
43
46
|
Desktop apps (macOS and Windows):
|
|
44
|
-
|
|
45
|
-
impel app
|
|
46
|
-
impel app
|
|
47
|
-
impel app
|
|
48
|
-
impel app
|
|
47
|
+
target is claude, chatgpt/codex, or all (default: all)
|
|
48
|
+
impel app install [target] [--tenant <org>] Install that tenant's isolated app/profile
|
|
49
|
+
impel app update [target] [--tenant <org>] Update vendor apps and managed profiles
|
|
50
|
+
impel app refresh [target] [--tenant <org>] Configs/catalog/skills only; safe while apps run
|
|
51
|
+
impel app status [target] [--tenant <org>] Show isolated app and vendor versions
|
|
52
|
+
impel app uninstall [target] [--tenant <org>] [--keep-data]
|
|
53
|
+
Remove only that tenant's managed apps/data
|
|
49
54
|
|
|
50
55
|
Native profile switching (advanced; setup never touches native tools):
|
|
51
56
|
impel use gateway [claude|codex|all] Route native Claude Code/Codex through the gateway
|
|
@@ -57,7 +62,7 @@ Native profile switching (advanced; setup never touches native tools):
|
|
|
57
62
|
|
|
58
63
|
Env:
|
|
59
64
|
IMPEL_GATEWAY_URL Default gateway URL when --gateway isn't passed.
|
|
60
|
-
IMPEL_APP_URL Default app/control-plane URL for setup, auth, and tasks.
|
|
65
|
+
IMPEL_APP_URL Default app/control-plane URL for setup, auth, PATs, and tasks.
|
|
61
66
|
IMPEL_SKIP_UPDATE_CHECK=1 Silence launch-time update notices.
|
|
62
67
|
|
|
63
68
|
Config file:
|
|
@@ -99,6 +104,10 @@ export async function main(argv) {
|
|
|
99
104
|
case "auth":
|
|
100
105
|
return cmdAuth(rest);
|
|
101
106
|
|
|
107
|
+
case "pat":
|
|
108
|
+
case "pats":
|
|
109
|
+
return cmdPat(rest);
|
|
110
|
+
|
|
102
111
|
case "token":
|
|
103
112
|
return cmdToken(rest);
|
|
104
113
|
|