impel-cli 0.7.0 → 0.7.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 +94 -74
- package/package.json +1 -1
- package/src/apps.js +287 -59
- package/src/commands/apps.js +5 -6
- package/src/commands/setup.js +2 -2
- package/src/commands/update.js +1 -1
package/README.md
CHANGED
|
@@ -25,78 +25,91 @@ of the protected Store directory and launches it with tenant-specific Codex and
|
|
|
25
25
|
browser profiles. Each approach keeps the user's normal app profile and
|
|
26
26
|
signed-in account untouched.
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Start here
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
You do not need GitHub access to install Impel. You need an Impel account and a
|
|
31
|
+
computer with Node.js installed. Node.js includes the small installer tool
|
|
32
|
+
called `npm` that installs Impel for you.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
npm install --global impel-cli
|
|
35
|
-
```
|
|
34
|
+
### 1. Install Node.js and npm
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
CLI—no credentials, service secrets, or customer data. Installing it grants no
|
|
40
|
-
Impel access: every protected operation still requires an Impel PAT and the
|
|
41
|
-
gateway/control plane re-checks the user's live product access, scopes, and
|
|
42
|
-
tenant membership.
|
|
36
|
+
If `node --version` and `npm --version` already print version numbers, skip to
|
|
37
|
+
the next step.
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
the
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
- **Mac or Windows:** visit [nodejs.org/download](https://nodejs.org/en/download),
|
|
40
|
+
choose the **LTS** installer, and accept the default installer options.
|
|
41
|
+
- **Linux:** follow Node's [official package-manager instructions](https://nodejs.org/en/download/package-manager)
|
|
42
|
+
for your distribution.
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
After installing Node.js, close and reopen Terminal (Mac/Linux) or PowerShell
|
|
45
|
+
(Windows), then check that it worked:
|
|
51
46
|
|
|
52
47
|
```sh
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
node --version
|
|
49
|
+
npm --version
|
|
55
50
|
```
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
Both commands should print a version number. Impel needs Node.js 18 or newer.
|
|
53
|
+
|
|
54
|
+
### 2. Install Impel
|
|
60
55
|
|
|
61
|
-
|
|
56
|
+
In Terminal or PowerShell, run:
|
|
62
57
|
|
|
63
58
|
```sh
|
|
64
|
-
|
|
65
|
-
impel claude # launch Claude Code with an isolated Impel profile
|
|
66
|
-
impel codex # launch Codex with an isolated Impel profile
|
|
67
|
-
impel update # later: update the CLI, apps, and skills in one command
|
|
59
|
+
npm install --global impel-cli
|
|
68
60
|
```
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
This downloads the public Impel command-line app. It does **not** give anyone
|
|
63
|
+
access to your Impel account, workspace, or data.
|
|
64
|
+
|
|
65
|
+
### 3. Connect your Impel account
|
|
66
|
+
|
|
67
|
+
1. Open [Impel gateway settings](https://www.useimpel.com/settings/gateway).
|
|
68
|
+
2. Create a Personal Access Token and copy it somewhere safe.
|
|
69
|
+
3. Back in Terminal or PowerShell, run:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
impel setup
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
4. Paste the token when asked. The input is hidden, and Impel stores it only in
|
|
76
|
+
your local account configuration.
|
|
77
|
+
5. Choose your organization when prompted and let the setup finish.
|
|
78
|
+
|
|
79
|
+
Your token is the key to your account. Never paste it into a chat, ticket, or
|
|
80
|
+
shared document.
|
|
81
|
+
|
|
82
|
+
### 4. Start working
|
|
77
83
|
|
|
78
84
|
```sh
|
|
79
|
-
impel
|
|
80
|
-
impel
|
|
81
|
-
impel
|
|
82
|
-
impel
|
|
83
|
-
impel codex # launch Codex with an isolated Impel profile
|
|
84
|
-
impel status # isolated-launcher readiness, native mode, and reachability
|
|
85
|
-
impel doctor # make one synthetic request to each provider and measure routing/latency
|
|
86
|
-
|
|
87
|
-
impel app install # install both isolated desktop app profiles
|
|
88
|
-
impel app update # update vendor apps, configs, and supported models
|
|
89
|
-
impel app open claude # open Claude Desktop with its isolated Impel profile
|
|
90
|
-
impel app open codex # open ChatGPT/Codex Desktop with its isolated Impel profile
|
|
91
|
-
|
|
92
|
-
# The normal `claude` and `codex` commands keep using your personal profiles.
|
|
85
|
+
impel claude # open an isolated Impel Claude Code session
|
|
86
|
+
impel codex # open an isolated Impel Codex session
|
|
87
|
+
impel status # check your connection and configuration
|
|
88
|
+
impel update # install the latest Impel CLI later
|
|
93
89
|
```
|
|
94
90
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
`impel setup` keeps your normal Claude and Codex profiles separate. On Mac and
|
|
92
|
+
Windows it also prepares the supported isolated desktop experiences. On Windows,
|
|
93
|
+
it installs missing official vendor command-line tools when needed. On Linux,
|
|
94
|
+
it prepares the isolated command-line workflow.
|
|
95
|
+
|
|
96
|
+
### If something does not work
|
|
97
|
+
|
|
98
|
+
- **`npm` or `node` is not recognized:** install Node.js from the link above,
|
|
99
|
+
then close and reopen Terminal or PowerShell.
|
|
100
|
+
- **`impel` is not recognized:** close and reopen the terminal, then run
|
|
101
|
+
`npm install --global impel-cli` again.
|
|
102
|
+
- **Your token is rejected:** create a fresh token in Impel gateway settings and
|
|
103
|
+
run `impel setup` again.
|
|
104
|
+
- **You need a different organization:** run `impel tenant list`, then
|
|
105
|
+
`impel tenant use <organization>`.
|
|
106
|
+
|
|
107
|
+
The npm package is public so Impel users do not need access to the private
|
|
108
|
+
GitHub repository. The package contains no account credentials or service
|
|
109
|
+
secrets; the Impel gateway and control plane still enforce your token, product
|
|
110
|
+
access, scopes, and organization membership for every protected action.
|
|
98
111
|
|
|
99
|
-
##
|
|
112
|
+
## Command reference
|
|
100
113
|
|
|
101
114
|
```
|
|
102
115
|
impel setup [--pat <pat>] [--tenant <org>] [--skip-apps] [--skip-clis]
|
|
@@ -171,7 +184,7 @@ impel auth --pat impel_pat_xxxxxxxxxxxx --gateway https://gateway.useimpel.com -
|
|
|
171
184
|
One command brings everything current, in dependency order: the CLI itself
|
|
172
185
|
(`npm install -g impel-cli@latest` from npm, with no GitHub credentials),
|
|
173
186
|
then — re-executing the freshly installed build — `impel app update all`
|
|
174
|
-
(close running apps,
|
|
187
|
+
(close running apps, install the CLI-pinned vendor builds, rebuild the vendored Impel apps)
|
|
175
188
|
and `impel skills sync all` across every managed profile.
|
|
176
189
|
|
|
177
190
|
Update discovery compares the installed package version with npm's public
|
|
@@ -572,12 +585,14 @@ configuration:
|
|
|
572
585
|
`~/.config/impel/apps/claude`. A narrowly version-checked compatibility patch
|
|
573
586
|
connects Claude's native plan-usage meter to the authenticated aggregate
|
|
574
587
|
Claude subscription pool exposed by the Impel gateway.
|
|
575
|
-
- Impel ChatGPT
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
588
|
+
- Impel ChatGPT wraps an APFS-cloned official app while preserving the nested
|
|
589
|
+
OpenAI-signed bundle and executable. Its outer resource root contains the
|
|
590
|
+
narrowly patched renderer ASAR used at runtime. The wrapper sets `CODEX_HOME`,
|
|
591
|
+
passes a separate Chromium `--user-data-dir`, and writes its provider config
|
|
592
|
+
and model catalog below `~/.config/impel/apps/chatgpt`. It also supplies a
|
|
593
|
+
process-scoped, Codex-compatible wrapper around the Impel PAT so native
|
|
594
|
+
`/status` requests can read the organization's aggregated subscription pool
|
|
595
|
+
from the gateway. Its generated Codex config uses the same snapshot and
|
|
581
596
|
subprocess-environment hardening as `impel codex`; install, update, and every
|
|
582
597
|
direct app launch also remove older snapshots for the selected profile, while
|
|
583
598
|
app install/update/open sweeps inactive tenant and legacy Impel app profiles.
|
|
@@ -585,22 +600,25 @@ configuration:
|
|
|
585
600
|
normal vendor app's browser/profile data.
|
|
586
601
|
|
|
587
602
|
The official Claude and ChatGPT apps remain separate installations and keep
|
|
588
|
-
their normal accounts, updater, profiles, and bundle identities.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
603
|
+
their normal accounts, updater, profiles, and bundle identities. Each CLI
|
|
604
|
+
release pins exact, tested vendor builds (v0.7.2 pins Claude `1.20186.9` and
|
|
605
|
+
ChatGPT `26.707.72221`) and their archive SHA-256 digests. The installer reuses
|
|
606
|
+
an existing official app only when its version exactly matches; otherwise it
|
|
607
|
+
downloads the pinned signed archive into Impel's managed vendor cache under
|
|
608
|
+
`~/.config/impel/vendor`, verifies its digest, bundle version, layout, and code
|
|
609
|
+
signature, and only then rebuilds the isolated app. It never clones a moving
|
|
610
|
+
Homebrew or vendor “latest” release, and it never downgrades or replaces the
|
|
611
|
+
user's normal vendor app. A future app version requires a new tested CLI release
|
|
612
|
+
and pin update. Config refreshes do not download or replace apps. If the model
|
|
613
|
+
endpoint is temporarily unavailable, the installer retains functionality using
|
|
614
|
+
the CLI's bundled catalog.
|
|
598
615
|
|
|
599
616
|
For ChatGPT/Codex, the gateway controls which models and capabilities are
|
|
600
617
|
enabled while the installer merges those entries with the vendor app's native
|
|
601
618
|
catalog. The Impel copy relaxes the vendor renderer's ChatGPT-account-only Fast
|
|
602
|
-
gate because gateway auth supplies the entitlement instead. The
|
|
603
|
-
ad-hoc signed locally
|
|
619
|
+
gate because gateway auth supplies the entitlement instead. The outer wrapper
|
|
620
|
+
is then ad-hoc signed locally without replacing the nested OpenAI signing
|
|
621
|
+
identity. This preserves model-specific instructions and exposes
|
|
604
622
|
the same reasoning-effort and service-tier controls as the normal app, including
|
|
605
623
|
Ultra where supported and Fast only on models that advertise the `priority`
|
|
606
624
|
tier. Inference uses an explicit non-WebSocket Impel model provider so Codex
|
|
@@ -609,8 +627,10 @@ Impel PAT. Config refreshes replace only the Impel-managed gateway block; plugin
|
|
|
609
627
|
desktop preferences, projects, and other settings written by the isolated app
|
|
610
628
|
are kept.
|
|
611
629
|
|
|
612
|
-
|
|
613
|
-
launch wrappers are installed. The
|
|
630
|
+
Impel Claude and the outer Impel ChatGPT wrapper are ad-hoc signed locally after
|
|
631
|
+
their bundle identities and launch wrappers are installed. The nested ChatGPT
|
|
632
|
+
bundle retains OpenAI's signature. The Claude 3P config necessarily contains
|
|
633
|
+
the PAT because that app accepts a
|
|
614
634
|
gateway API key rather than a token-helper command. It is stored in the same
|
|
615
635
|
owner-only Impel config tree as the primary credential. ChatGPT/Codex uses a
|
|
616
636
|
command-backed helper and does not copy the PAT into TOML. The installable app
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -21,18 +21,50 @@ const CLI_VERSION = JSON.parse(
|
|
|
21
21
|
const APP_DEFINITIONS = {
|
|
22
22
|
claude: {
|
|
23
23
|
label: "Impel Claude",
|
|
24
|
-
cask: "claude",
|
|
25
24
|
names: ["Claude.app"],
|
|
26
25
|
executableNames: ["Claude"],
|
|
27
26
|
},
|
|
28
27
|
chatgpt: {
|
|
29
28
|
label: "Impel ChatGPT",
|
|
30
|
-
cask: "codex-app",
|
|
31
29
|
names: ["ChatGPT.app", "Codex.app"],
|
|
32
30
|
executableNames: ["ChatGPT", "Codex"],
|
|
33
31
|
},
|
|
34
32
|
};
|
|
35
33
|
|
|
34
|
+
// Vendor updates can change minified renderer contracts without notice. Each
|
|
35
|
+
// CLI release therefore installs and accepts only the exact app builds tested
|
|
36
|
+
// with its compatibility patches. Updating a pin requires a new CLI release.
|
|
37
|
+
export const PINNED_VENDOR_APPS = Object.freeze({
|
|
38
|
+
claude: Object.freeze({
|
|
39
|
+
version: "1.20186.9",
|
|
40
|
+
bundleName: "Claude.app",
|
|
41
|
+
downloads: Object.freeze({
|
|
42
|
+
arm64: Object.freeze({
|
|
43
|
+
url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
|
|
44
|
+
sha256: "9364648c74bd525aa293a28103eb37262a5553de21c7ca907766d7fc40623015",
|
|
45
|
+
}),
|
|
46
|
+
x64: Object.freeze({
|
|
47
|
+
url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
|
|
48
|
+
sha256: "9364648c74bd525aa293a28103eb37262a5553de21c7ca907766d7fc40623015",
|
|
49
|
+
}),
|
|
50
|
+
}),
|
|
51
|
+
}),
|
|
52
|
+
chatgpt: Object.freeze({
|
|
53
|
+
version: "26.707.72221",
|
|
54
|
+
bundleName: "ChatGPT.app",
|
|
55
|
+
downloads: Object.freeze({
|
|
56
|
+
arm64: Object.freeze({
|
|
57
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.707.72221.zip",
|
|
58
|
+
sha256: "d47a2c0dfb07afec21c415fcec672035a2e90410ecb7c6dc60141518fba524e0",
|
|
59
|
+
}),
|
|
60
|
+
x64: Object.freeze({
|
|
61
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.707.72221.zip",
|
|
62
|
+
sha256: "cadfdc8e1efa809b021bcf4466ae8c115ffdd280f1a467b518eca8ce8f6717fe",
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
36
68
|
const STANDARD_REASONING_LEVELS = [
|
|
37
69
|
{ effort: "low", description: "Fast responses with lighter reasoning" },
|
|
38
70
|
{ effort: "medium", description: "Balances speed and reasoning depth for everyday tasks" },
|
|
@@ -42,6 +74,8 @@ const STANDARD_REASONING_LEVELS = [
|
|
|
42
74
|
const MAX_REASONING_LEVEL = { effort: "max", description: "Maximum reasoning depth for the hardest problems" };
|
|
43
75
|
const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning with automatic task delegation" };
|
|
44
76
|
const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
|
|
77
|
+
const FAST_MODE_AUTH_GATE = /([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)&&!([A-Za-z_$][\w$]*)&&([A-Za-z_$][\w$]*)!=null&&\4\?\.requirements\?\.featureRequirements\?\.fast_mode!==!1/gu;
|
|
78
|
+
const CHATGPT_RESOURCE_ROOT = "linked_vendor_resources_with_patched_asar";
|
|
45
79
|
const CLAUDE_PLAN_USAGE_GUARD = "if(!Ze().hasOrgPolicyBackend())return;if((r=fr())";
|
|
46
80
|
const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `${"if(!wze())return;".padEnd("if(!Ze().hasOrgPolicyBackend())return;".length, " ")}if((r=fr())`;
|
|
47
81
|
const CLAUDE_PLAN_USAGE_REQUEST = 'P.net.fetch(`${ht()}/api/organizations/${o}/usage`,{signal:AbortSignal.timeout(mcr)})';
|
|
@@ -110,16 +144,37 @@ export function bundleIsCurrent(status) {
|
|
|
110
144
|
compatibility
|
|
111
145
|
&& compatibility.cliVersion === CLI_VERSION
|
|
112
146
|
&& compatibility.vendorVersion === status.vendorVersion
|
|
147
|
+
&& managedBundleMatchesCompatibility(status, compatibility)
|
|
113
148
|
&& (status.target !== "chatgpt" || (
|
|
114
149
|
compatibility.vendorSignaturesPreserved === true
|
|
115
|
-
&& compatibility.resourceRoot ===
|
|
150
|
+
&& compatibility.resourceRoot === CHATGPT_RESOURCE_ROOT
|
|
151
|
+
&& compatibility.patches?.fastModeForGatewayAuth > 0
|
|
116
152
|
)),
|
|
117
153
|
);
|
|
118
154
|
}
|
|
119
155
|
|
|
156
|
+
function managedBundleMatchesCompatibility(status, compatibility) {
|
|
157
|
+
try {
|
|
158
|
+
const managedVendorBundle = status.target === "claude"
|
|
159
|
+
? status.launcher
|
|
160
|
+
: APP_DEFINITIONS.chatgpt.names
|
|
161
|
+
.map((name) => path.join(status.launcher, "Contents", "Resources", name))
|
|
162
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
163
|
+
const runtimeAsar = path.join(status.launcher, "Contents", "Resources", "app.asar");
|
|
164
|
+
return Boolean(
|
|
165
|
+
managedVendorBundle
|
|
166
|
+
&& bundleVersion(managedVendorBundle) === compatibility.vendorVersion
|
|
167
|
+
&& compatibility.asarHeaderSha256
|
|
168
|
+
&& asarHeaderHash(runtimeAsar) === compatibility.asarHeaderSha256
|
|
169
|
+
);
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
120
175
|
// Install/update must not run while the vendor or Impel-managed apps are open:
|
|
121
|
-
//
|
|
122
|
-
//
|
|
176
|
+
// Cloning/replacing a bundle under a running instance produces broken copies
|
|
177
|
+
// and codesign failures.
|
|
123
178
|
|
|
124
179
|
/** osascript application names whose bundles install/update will touch. */
|
|
125
180
|
export function blockingAppNames(targets) {
|
|
@@ -269,6 +324,9 @@ export function installManagedAppFiles({
|
|
|
269
324
|
// token helper, catalog, and manifest only. Bundle swaps require the app
|
|
270
325
|
// to be closed (see quitBlockingApps), so those paths never attempt one.
|
|
271
326
|
if (bundleTargets.has(target)) {
|
|
327
|
+
if (resolvedVendorPaths[target] && isVendableVendorApp(target, resolvedVendorPaths[target])) {
|
|
328
|
+
assertPinnedVendorAppVersion(target, resolvedVendorPaths[target]);
|
|
329
|
+
}
|
|
272
330
|
if (target === "chatgpt") writeVendoredChatGPTBundle(paths, resolvedVendorPaths.chatgpt, config.gatewayUrl);
|
|
273
331
|
else writeVendoredClaudeBundle(paths, resolvedVendorPaths.claude, config.gatewayUrl);
|
|
274
332
|
}
|
|
@@ -289,8 +347,11 @@ export function installManagedAppFiles({
|
|
|
289
347
|
return installed;
|
|
290
348
|
}
|
|
291
349
|
|
|
292
|
-
function vendorSearchRoots(homeDir) {
|
|
293
|
-
|
|
350
|
+
function vendorSearchRoots(homeDir, target) {
|
|
351
|
+
const roots = ["/Applications", path.join(homeDir, "Applications")];
|
|
352
|
+
const pin = PINNED_VENDOR_APPS[target];
|
|
353
|
+
if (pin) roots.push(path.join(homeDir, ".config", "impel", "vendor", target, pin.version));
|
|
354
|
+
return roots;
|
|
294
355
|
}
|
|
295
356
|
|
|
296
357
|
export function readBundleIdentifier(appPath) {
|
|
@@ -320,19 +381,26 @@ export function isVendableVendorApp(target, appPath) {
|
|
|
320
381
|
);
|
|
321
382
|
}
|
|
322
383
|
|
|
323
|
-
export function findVendorApp(target, homeDir = os.homedir(), {
|
|
384
|
+
export function findVendorApp(target, homeDir = os.homedir(), {
|
|
385
|
+
roots = vendorSearchRoots(homeDir, target),
|
|
386
|
+
version = PINNED_VENDOR_APPS[target]?.version,
|
|
387
|
+
} = {}) {
|
|
324
388
|
const definition = APP_DEFINITIONS[target];
|
|
325
389
|
for (const base of roots) {
|
|
326
390
|
for (const name of definition.names) {
|
|
327
391
|
const candidate = path.join(base, name);
|
|
328
|
-
if (
|
|
392
|
+
if (
|
|
393
|
+
fs.existsSync(candidate)
|
|
394
|
+
&& isVendableVendorApp(target, candidate)
|
|
395
|
+
&& (!version || bundleVersion(candidate) === version)
|
|
396
|
+
) return candidate;
|
|
329
397
|
}
|
|
330
398
|
}
|
|
331
399
|
return null;
|
|
332
400
|
}
|
|
333
401
|
|
|
334
402
|
/** Name-matched bundles that are NOT vendable — surfaced so "why was my app ignored?" is answerable. */
|
|
335
|
-
export function rejectedVendorCandidates(target, homeDir = os.homedir(), { roots = vendorSearchRoots(homeDir) } = {}) {
|
|
403
|
+
export function rejectedVendorCandidates(target, homeDir = os.homedir(), { roots = vendorSearchRoots(homeDir, target) } = {}) {
|
|
336
404
|
const definition = APP_DEFINITIONS[target];
|
|
337
405
|
const rejected = [];
|
|
338
406
|
for (const base of roots) {
|
|
@@ -346,33 +414,103 @@ export function rejectedVendorCandidates(target, homeDir = os.homedir(), { roots
|
|
|
346
414
|
return rejected;
|
|
347
415
|
}
|
|
348
416
|
|
|
349
|
-
|
|
417
|
+
function unpinnedVendorCandidates(target, homeDir = os.homedir(), { roots = vendorSearchRoots(homeDir, target) } = {}) {
|
|
350
418
|
const definition = APP_DEFINITIONS[target];
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
419
|
+
const expectedVersion = PINNED_VENDOR_APPS[target].version;
|
|
420
|
+
const candidates = [];
|
|
421
|
+
for (const base of roots) {
|
|
422
|
+
for (const name of definition.names) {
|
|
423
|
+
const candidate = path.join(base, name);
|
|
424
|
+
if (fs.existsSync(candidate) && isVendableVendorApp(target, candidate)) {
|
|
425
|
+
const version = bundleVersion(candidate);
|
|
426
|
+
if (version !== expectedVersion) candidates.push({ path: candidate, version });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return candidates;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function assertPinnedVendorAppVersion(target, appPath) {
|
|
434
|
+
const expected = PINNED_VENDOR_APPS[target]?.version;
|
|
435
|
+
const actual = bundleVersion(appPath);
|
|
436
|
+
if (!expected || actual !== expected) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`${APP_DEFINITIONS[target]?.label || target} vendor version ${actual || "unknown"} is unsupported; impel-cli v${CLI_VERSION} requires the verified ${expected || "pinned"} build`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return actual;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function installPinnedVendorApp(target, homeDir) {
|
|
445
|
+
const pin = PINNED_VENDOR_APPS[target];
|
|
446
|
+
const download = pin?.downloads?.[process.arch];
|
|
447
|
+
if (!pin || !download) throw new Error(`${target}: no verified vendor download is available for ${process.arch}`);
|
|
448
|
+
|
|
449
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `impel-${target}-vendor-`));
|
|
450
|
+
const archive = path.join(tempRoot, `${target}.zip`);
|
|
451
|
+
const extractedRoot = path.join(tempRoot, "extracted");
|
|
452
|
+
try {
|
|
453
|
+
console.log(`${target}: downloading verified vendor app ${pin.version}…`);
|
|
454
|
+
const fetched = spawnSync(
|
|
455
|
+
"/usr/bin/curl",
|
|
456
|
+
["--fail", "--location", "--progress-bar", "--retry", "2", "--output", archive, download.url],
|
|
457
|
+
{ stdio: ["ignore", "inherit", "inherit"] },
|
|
458
|
+
);
|
|
459
|
+
if (fetched.status !== 0 || fetched.error) {
|
|
460
|
+
throw new Error(`${target}: failed to download verified vendor app ${pin.version}`);
|
|
461
|
+
}
|
|
462
|
+
const actualHash = sha256(fs.readFileSync(archive));
|
|
463
|
+
if (actualHash !== download.sha256) {
|
|
464
|
+
throw new Error(`${target}: vendor archive integrity check failed for ${pin.version}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
fs.mkdirSync(extractedRoot, { recursive: true });
|
|
468
|
+
const extracted = spawnSync("/usr/bin/ditto", ["-x", "-k", archive, extractedRoot], { encoding: "utf8" });
|
|
469
|
+
if (extracted.status !== 0 || extracted.error) {
|
|
470
|
+
throw new Error(`${target}: failed to extract verified vendor app ${pin.version}`);
|
|
471
|
+
}
|
|
472
|
+
const extractedApp = path.join(extractedRoot, pin.bundleName);
|
|
473
|
+
if (!isVendableVendorApp(target, extractedApp)) {
|
|
474
|
+
throw new Error(`${target}: verified vendor archive ${pin.version} has an unsupported app layout`);
|
|
475
|
+
}
|
|
476
|
+
assertPinnedVendorAppVersion(target, extractedApp);
|
|
477
|
+
const verified = spawnSync("/usr/bin/codesign", ["--verify", "--deep", "--strict", extractedApp], { encoding: "utf8" });
|
|
478
|
+
if (verified.status !== 0 || verified.error) {
|
|
479
|
+
throw new Error(`${target}: vendor signature verification failed for ${pin.version}`);
|
|
360
480
|
}
|
|
361
|
-
|
|
362
|
-
|
|
481
|
+
|
|
482
|
+
const destination = path.join(
|
|
483
|
+
homeDir, ".config", "impel", "vendor", target, pin.version, pin.bundleName,
|
|
484
|
+
);
|
|
485
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
486
|
+
replaceDirectory(destination, extractedApp);
|
|
487
|
+
return destination;
|
|
488
|
+
} finally {
|
|
489
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
363
490
|
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function ensureVendorApp(target, { homeDir = os.homedir() } = {}) {
|
|
494
|
+
const definition = APP_DEFINITIONS[target];
|
|
495
|
+
const pin = PINNED_VENDOR_APPS[target];
|
|
496
|
+
const current = findVendorApp(target, homeDir);
|
|
497
|
+
if (current) return { path: current, action: "existing", note: `verified ${pin.version}` };
|
|
364
498
|
|
|
365
499
|
// Name-collision transparency: e.g. OpenAI's native ChatGPT chat app
|
|
366
|
-
// (com.openai.chat)
|
|
367
|
-
// app and cannot be vendored — say so instead of silently installing.
|
|
500
|
+
// (com.openai.chat) is NOT the Electron app and cannot be vendored.
|
|
368
501
|
for (const candidate of rejectedVendorCandidates(target, homeDir)) {
|
|
369
502
|
console.log(
|
|
370
|
-
`${target}: ignoring ${candidate.path} (${candidate.bundleId || "unknown bundle id"}) — not a vendorable ${definition.label.replace("Impel ", "")} desktop app
|
|
503
|
+
`${target}: ignoring ${candidate.path} (${candidate.bundleId || "unknown bundle id"}) — not a vendorable ${definition.label.replace("Impel ", "")} desktop app`,
|
|
371
504
|
);
|
|
372
505
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
506
|
+
for (const candidate of unpinnedVendorCandidates(target, homeDir)) {
|
|
507
|
+
console.log(
|
|
508
|
+
`${target}: ignoring ${candidate.path} (${candidate.version || "unknown version"}) — this CLI is pinned to verified version ${pin.version}`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const installed = installPinnedVendorApp(target, homeDir);
|
|
513
|
+
return { path: installed, action: "installed", note: `verified ${pin.version}` };
|
|
376
514
|
}
|
|
377
515
|
|
|
378
516
|
export function appStatus(targets, homeDir = os.homedir(), tenantId = null) {
|
|
@@ -749,12 +887,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl) {
|
|
|
749
887
|
IMPEL_CLAUDE_PLAN_USAGE_GUARD,
|
|
750
888
|
"Claude plan-usage eligibility guard",
|
|
751
889
|
);
|
|
752
|
-
planUsageRequestPatchCount =
|
|
753
|
-
asarPath,
|
|
754
|
-
CLAUDE_PLAN_USAGE_REQUEST,
|
|
755
|
-
IMPEL_CLAUDE_PLAN_USAGE_REQUEST,
|
|
756
|
-
"Claude plan-usage gateway request",
|
|
757
|
-
);
|
|
890
|
+
planUsageRequestPatchCount = patchClaudePlanUsageRequest(asarPath);
|
|
758
891
|
}
|
|
759
892
|
const newAsarHash = asarHeaderHash(asarPath);
|
|
760
893
|
|
|
@@ -825,22 +958,27 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
825
958
|
if (!fs.existsSync(asarPath) || !fs.existsSync(plistPath)) {
|
|
826
959
|
const bundleId = readBundleIdentifier(vendorPath);
|
|
827
960
|
const hint = bundleId === "com.openai.chat"
|
|
828
|
-
? " — this is OpenAI's ChatGPT
|
|
961
|
+
? " — this is OpenAI's non-Electron ChatGPT app, not the supported desktop build; re-run `impel app install chatgpt`"
|
|
829
962
|
: bundleId
|
|
830
963
|
? ` (bundle id ${bundleId})`
|
|
831
964
|
: "";
|
|
832
965
|
throw new Error(`unsupported ChatGPT/Codex bundle layout in ${vendorPath}${hint}`);
|
|
833
966
|
}
|
|
834
967
|
|
|
835
|
-
const asarHash = asarHeaderHash(asarPath);
|
|
836
|
-
|
|
837
968
|
const macos = path.join(staging, "Contents", "MacOS");
|
|
838
|
-
writeAtomic(path.join(staging, "Contents", "Info.plist"), vendoredChatGPTWrapperPlist(vendorPath), 0o644);
|
|
839
969
|
const vendorIcon = path.join(vendorBundle, "Contents", "Resources", "electron.icns");
|
|
840
970
|
if (fs.existsSync(vendorIcon)) {
|
|
841
971
|
fs.copyFileSync(vendorIcon, path.join(staging, "Contents", "Resources", "electron.icns"));
|
|
842
972
|
}
|
|
843
973
|
linkVendoredChatGPTResources(staging, vendorBundleName);
|
|
974
|
+
const runtimeAsarPath = path.join(staging, "Contents", "Resources", "app.asar");
|
|
975
|
+
const fastModePatchCount = patchFastModeAuthGate(runtimeAsarPath);
|
|
976
|
+
const asarHash = asarHeaderHash(runtimeAsarPath);
|
|
977
|
+
writeAtomic(
|
|
978
|
+
path.join(staging, "Contents", "Info.plist"),
|
|
979
|
+
vendoredChatGPTWrapperPlist(vendorPath, asarHash),
|
|
980
|
+
0o644,
|
|
981
|
+
);
|
|
844
982
|
writeAtomic(
|
|
845
983
|
path.join(macos, "launch"),
|
|
846
984
|
vendoredChatGPTLauncher(paths, vendorBundleName, executableName, gatewayUrl),
|
|
@@ -851,9 +989,9 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
851
989
|
cliVersion: CLI_VERSION,
|
|
852
990
|
vendorVersion: bundleVersion(vendorPath),
|
|
853
991
|
sourceBundle: vendorPath,
|
|
854
|
-
patches: { fastModeForGatewayAuth:
|
|
992
|
+
patches: { fastModeForGatewayAuth: fastModePatchCount, desktopAPIForGatewayAuth: 0 },
|
|
855
993
|
vendorSignaturesPreserved: true,
|
|
856
|
-
resourceRoot:
|
|
994
|
+
resourceRoot: CHATGPT_RESOURCE_ROOT,
|
|
857
995
|
asarHeaderSha256: asarHash,
|
|
858
996
|
}, null, 2) + "\n", 0o644);
|
|
859
997
|
|
|
@@ -870,11 +1008,17 @@ function linkVendoredChatGPTResources(bundle, vendorBundleName) {
|
|
|
870
1008
|
const vendorResources = path.join(outerResources, vendorBundleName, "Contents", "Resources");
|
|
871
1009
|
for (const entry of fs.readdirSync(vendorResources, { withFileTypes: true })) {
|
|
872
1010
|
// The wrapper owns its icon and compatibility record. Every other vendor
|
|
873
|
-
// resource is exposed at the outer Electron resource root
|
|
874
|
-
//
|
|
1011
|
+
// resource is exposed at the outer Electron resource root, leaving the
|
|
1012
|
+
// OpenAI-signed nested bundle untouched. app.asar is the sole copied file
|
|
1013
|
+
// because the runtime needs the narrowly scoped compatibility patch below.
|
|
875
1014
|
if (entry.name === "electron.icns") continue;
|
|
876
1015
|
const target = path.join(vendorBundleName, "Contents", "Resources", entry.name);
|
|
877
|
-
|
|
1016
|
+
const destination = path.join(outerResources, entry.name);
|
|
1017
|
+
// Electron resolves app.asar from the outer wrapper's resource root. Keep
|
|
1018
|
+
// the nested OpenAI bundle byte-for-byte signed, but use a patched copy at
|
|
1019
|
+
// runtime so gateway-authenticated profiles retain the native Fast control.
|
|
1020
|
+
if (entry.name === "app.asar") fs.copyFileSync(path.join(vendorResources, entry.name), destination);
|
|
1021
|
+
else fs.symlinkSync(target, destination);
|
|
878
1022
|
}
|
|
879
1023
|
}
|
|
880
1024
|
|
|
@@ -883,17 +1027,88 @@ function patchFixedWidthAsarString(asarPath, original, replacement, label) {
|
|
|
883
1027
|
throw new Error(`${label} replacement must preserve the vendor ASAR byte length`);
|
|
884
1028
|
}
|
|
885
1029
|
const archive = fs.readFileSync(asarPath);
|
|
886
|
-
const metadata = readAsarMetadata(archive);
|
|
887
1030
|
const source = archive.toString("latin1");
|
|
888
|
-
const
|
|
1031
|
+
const patches = [];
|
|
889
1032
|
for (let offset = source.indexOf(original); offset !== -1; offset = source.indexOf(original, offset + original.length)) {
|
|
890
|
-
|
|
1033
|
+
patches.push({ offset, original, replacement });
|
|
1034
|
+
}
|
|
1035
|
+
return applyFixedWidthAsarPatches(asarPath, archive, patches, label);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function patchClaudePlanUsageRequest(asarPath) {
|
|
1039
|
+
const archive = fs.readFileSync(asarPath);
|
|
1040
|
+
const source = archive.toString("latin1");
|
|
1041
|
+
const legacyPatches = [];
|
|
1042
|
+
for (
|
|
1043
|
+
let offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST);
|
|
1044
|
+
offset !== -1;
|
|
1045
|
+
offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST, offset + CLAUDE_PLAN_USAGE_REQUEST.length)
|
|
1046
|
+
) {
|
|
1047
|
+
legacyPatches.push({
|
|
1048
|
+
offset,
|
|
1049
|
+
original: CLAUDE_PLAN_USAGE_REQUEST,
|
|
1050
|
+
replacement: IMPEL_CLAUDE_PLAN_USAGE_REQUEST,
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
if (legacyPatches.length > 0) {
|
|
1054
|
+
return applyFixedWidthAsarPatches(
|
|
1055
|
+
asarPath, archive, legacyPatches, "Claude plan-usage gateway request",
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
const credentialAccessor = source.match(
|
|
1059
|
+
/function ([A-Za-z_$][\w$]*)\(e\)\{var t;return [\s\S]{0,500}?\[custom-3p\] Credentials loaded from managed config/u,
|
|
1060
|
+
)?.[1];
|
|
1061
|
+
if (!credentialAccessor) {
|
|
1062
|
+
return applyFixedWidthAsarPatches(asarPath, archive, [], "Claude plan-usage gateway request");
|
|
1063
|
+
}
|
|
1064
|
+
const configAccessorPattern = new RegExp(
|
|
1065
|
+
`${escapeRegex(credentialAccessor)}\\(([A-Za-z_$][\\w$]*)\\(\\)\\)`,
|
|
1066
|
+
"u",
|
|
1067
|
+
);
|
|
1068
|
+
const configAccessor = source.match(configAccessorPattern)?.[1];
|
|
1069
|
+
if (!configAccessor) {
|
|
1070
|
+
return applyFixedWidthAsarPatches(asarPath, archive, [], "Claude plan-usage gateway request");
|
|
891
1071
|
}
|
|
892
|
-
|
|
1072
|
+
|
|
1073
|
+
const requestPattern = /([A-Za-z_$][\w$]*)\.net\.fetch\(`\$\{[A-Za-z_$][\w$]*\(\)\}\/api\/organizations\/\$\{[A-Za-z_$][\w$]*\}\/usage`,\{signal:AbortSignal\.timeout\([A-Za-z_$][\w$]*\)\}\)/gu;
|
|
1074
|
+
const patches = [...source.matchAll(requestPattern)].map((match) => {
|
|
1075
|
+
const original = match[0];
|
|
1076
|
+
const replacement = `${match[1]}.net.fetch("https://gateway.useimpel.com/ui/a/u",{headers:{key:${credentialAccessor}(${configAccessor}()).apiKey}})`;
|
|
1077
|
+
if (replacement.length > original.length) {
|
|
1078
|
+
throw new Error("Claude plan-usage gateway request replacement no longer fits the vendor ASAR contract");
|
|
1079
|
+
}
|
|
1080
|
+
return { offset: match.index, original, replacement: replacement.padEnd(original.length, " ") };
|
|
1081
|
+
});
|
|
1082
|
+
return applyFixedWidthAsarPatches(asarPath, archive, patches, "Claude plan-usage gateway request");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function patchFastModeAuthGate(asarPath) {
|
|
1086
|
+
const archive = fs.readFileSync(asarPath);
|
|
1087
|
+
const source = archive.toString("latin1");
|
|
1088
|
+
const patches = [...source.matchAll(FAST_MODE_AUTH_GATE)].map((match) => {
|
|
1089
|
+
const replacement = `${match[1]}=!${match[3]}`;
|
|
1090
|
+
return {
|
|
1091
|
+
offset: match.index,
|
|
1092
|
+
original: match[0],
|
|
1093
|
+
replacement: replacement.padEnd(match[0].length, " "),
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
return applyFixedWidthAsarPatches(asarPath, archive, patches, "ChatGPT/Codex Fast-mode eligibility contract");
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function applyFixedWidthAsarPatches(asarPath, archive, patches, label) {
|
|
1100
|
+
if (patches.length === 0) {
|
|
893
1101
|
throw new Error(`this desktop app version has an unsupported ${label}; update impel-cli before installing it`);
|
|
894
1102
|
}
|
|
895
|
-
const
|
|
896
|
-
const
|
|
1103
|
+
const metadata = readAsarMetadata(archive);
|
|
1104
|
+
const patchedArchive = Buffer.from(archive);
|
|
1105
|
+
for (const { offset, original, replacement } of patches) {
|
|
1106
|
+
if (Buffer.byteLength(original, "latin1") !== Buffer.byteLength(replacement, "latin1")) {
|
|
1107
|
+
throw new Error(`${label} replacement must preserve the vendor ASAR byte length`);
|
|
1108
|
+
}
|
|
1109
|
+
Buffer.from(replacement, "latin1").copy(patchedArchive, offset);
|
|
1110
|
+
}
|
|
1111
|
+
const entries = new Set(patches.map(({ offset }) => findAsarEntry(metadata.header, metadata.dataOffset, offset)));
|
|
897
1112
|
for (const entry of entries) updateAsarEntryIntegrity(patchedArchive, metadata.dataOffset, entry);
|
|
898
1113
|
const header = Buffer.from(JSON.stringify(metadata.header));
|
|
899
1114
|
if (header.length !== metadata.headerLength) {
|
|
@@ -901,7 +1116,7 @@ function patchFixedWidthAsarString(asarPath, original, replacement, label) {
|
|
|
901
1116
|
}
|
|
902
1117
|
header.copy(patchedArchive, 16);
|
|
903
1118
|
fs.writeFileSync(asarPath, patchedArchive);
|
|
904
|
-
return
|
|
1119
|
+
return patches.length;
|
|
905
1120
|
}
|
|
906
1121
|
|
|
907
1122
|
function cloneAppBundle(source, destination) {
|
|
@@ -1039,7 +1254,7 @@ function rebrandElectronHelpers(bundle, { fromName, toName, fromIdentifier, toId
|
|
|
1039
1254
|
}
|
|
1040
1255
|
}
|
|
1041
1256
|
|
|
1042
|
-
function vendoredChatGPTWrapperPlist(vendorPath) {
|
|
1257
|
+
function vendoredChatGPTWrapperPlist(vendorPath, asarHash) {
|
|
1043
1258
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1044
1259
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1045
1260
|
<plist version="1.0"><dict>
|
|
@@ -1050,6 +1265,7 @@ function vendoredChatGPTWrapperPlist(vendorPath) {
|
|
|
1050
1265
|
<key>CFBundleName</key><string>Impel ChatGPT</string>
|
|
1051
1266
|
<key>CFBundlePackageType</key><string>APPL</string>
|
|
1052
1267
|
<key>CFBundleShortVersionString</key><string>${xmlEscape(bundleVersion(vendorPath) || "1")}</string>
|
|
1268
|
+
<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>
|
|
1053
1269
|
<key>LSMinimumSystemVersion</key><string>12.0</string>
|
|
1054
1270
|
<key>NSHighResolutionCapable</key><true/>
|
|
1055
1271
|
</dict></plist>
|
|
@@ -1191,9 +1407,26 @@ function isMachO(file) {
|
|
|
1191
1407
|
}
|
|
1192
1408
|
|
|
1193
1409
|
function asarHeaderHash(file) {
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1410
|
+
const handle = fs.openSync(file, "r");
|
|
1411
|
+
try {
|
|
1412
|
+
const prefix = Buffer.alloc(16);
|
|
1413
|
+
if (fs.readSync(handle, prefix, 0, prefix.length, 0) !== prefix.length) {
|
|
1414
|
+
throw new Error("desktop app ASAR header is invalid");
|
|
1415
|
+
}
|
|
1416
|
+
const headerLength = prefix.readUInt32LE(12);
|
|
1417
|
+
const dataOffset = 8 + prefix.readUInt32LE(4);
|
|
1418
|
+
const fileSize = fs.fstatSync(handle).size;
|
|
1419
|
+
if (headerLength <= 0 || 16 + headerLength > dataOffset || dataOffset > fileSize) {
|
|
1420
|
+
throw new Error("desktop app ASAR offsets are invalid");
|
|
1421
|
+
}
|
|
1422
|
+
const header = Buffer.alloc(headerLength);
|
|
1423
|
+
if (fs.readSync(handle, header, 0, header.length, 16) !== header.length) {
|
|
1424
|
+
throw new Error("desktop app ASAR header is truncated");
|
|
1425
|
+
}
|
|
1426
|
+
return sha256(header);
|
|
1427
|
+
} finally {
|
|
1428
|
+
fs.closeSync(handle);
|
|
1429
|
+
}
|
|
1197
1430
|
}
|
|
1198
1431
|
|
|
1199
1432
|
function sha256(contents) {
|
|
@@ -1241,11 +1474,6 @@ function bundleVersion(bundle) {
|
|
|
1241
1474
|
}
|
|
1242
1475
|
}
|
|
1243
1476
|
|
|
1244
|
-
function findCommand(command) {
|
|
1245
|
-
const result = spawnSync("/usr/bin/which", [command], { encoding: "utf8" });
|
|
1246
|
-
return result.status === 0 ? result.stdout.trim() : null;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
1477
|
function shellQuote(value) {
|
|
1250
1478
|
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
1251
1479
|
}
|
package/src/commands/apps.js
CHANGED
|
@@ -352,19 +352,18 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
352
352
|
|
|
353
353
|
const config = await selectedAppConfig(targets);
|
|
354
354
|
|
|
355
|
-
// A running vendor or Impel app makes the
|
|
355
|
+
// A running vendor or Impel app makes the pinned vendor install / bundle swap fail.
|
|
356
356
|
await quitBlockingApps(targets);
|
|
357
357
|
|
|
358
358
|
const vendorPaths = {};
|
|
359
359
|
if (!flags["skip-vendor"]) {
|
|
360
360
|
for (const target of targets) {
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
const result = ensureVendorApp(target, { update: true });
|
|
361
|
+
// Reuse or install only the exact vendor build verified by this CLI.
|
|
362
|
+
// Moving "latest" releases are never cloned into a managed bundle.
|
|
363
|
+
const result = ensureVendorApp(target);
|
|
365
364
|
vendorPaths[target] = result.path;
|
|
366
365
|
console.log(`${target}: vendor app ${result.action}${result.note ? ` (${result.note})` : ""}`);
|
|
367
|
-
if (!result.path) throw new Error(`${target} vendor app is unavailable;
|
|
366
|
+
if (!result.path) throw new Error(`${target} verified vendor app is unavailable; retry the pinned download`);
|
|
368
367
|
}
|
|
369
368
|
}
|
|
370
369
|
|
package/src/commands/setup.js
CHANGED
|
@@ -225,7 +225,7 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
225
225
|
|
|
226
226
|
// ── Step 3: apps ─────────────────────────────────────────────────────────
|
|
227
227
|
// Order inside `impel app install`: close running Claude/ChatGPT apps (a
|
|
228
|
-
// running app makes the install fail), bring the vendor apps to the
|
|
228
|
+
// running app makes the install fail), bring the vendor apps to the verified
|
|
229
229
|
// version, then build the vendored Impel bundles from that fresh copy.
|
|
230
230
|
// Native Claude Code and Codex profiles are never touched.
|
|
231
231
|
let platformSetupFailed = false;
|
|
@@ -301,7 +301,7 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
301
301
|
);
|
|
302
302
|
} else {
|
|
303
303
|
console.log(
|
|
304
|
-
"3/4 Installing the Impel desktop apps (running Claude/ChatGPT apps are closed, vendor
|
|
304
|
+
"3/4 Installing the Impel desktop apps (running Claude/ChatGPT apps are closed, verified vendor builds installed first)…"
|
|
305
305
|
);
|
|
306
306
|
try {
|
|
307
307
|
await io.installApps(["install", "all"]);
|
package/src/commands/update.js
CHANGED
|
@@ -196,7 +196,7 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
196
196
|
} else {
|
|
197
197
|
console.log(io.platform === "win32"
|
|
198
198
|
? "Apps: updating the signed Claude and ChatGPT vendor apps and isolated profiles…"
|
|
199
|
-
: "Apps: updating (running apps are closed, vendor
|
|
199
|
+
: "Apps: updating (running apps are closed, verified vendor builds installed, bundles rebuilt)…");
|
|
200
200
|
if (!io.runAppsUpdate()) {
|
|
201
201
|
console.error("impel update: the app update failed; re-run `impel app update` after fixing the issue.");
|
|
202
202
|
cascadeFailed = true;
|