impel-cli 0.18.6 → 0.18.8
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/package.json +1 -1
- package/src/claudeSetup.js +8 -3
- package/src/codexSetup.js +12 -8
- package/src/commands/doctor.js +5 -1
- package/src/commands/nuke.js +9 -7
- package/src/commands/sessions.js +13 -6
- package/src/commands/status.js +8 -2
- package/src/commands/update.js +10 -8
- package/src/extension/index.js +31 -3
- package/src/runtimeBrand.js +27 -1
- package/src/selfInvocation.js +2 -2
- package/src/sessionCollector.js +25 -13
- package/src/sessionHooks.js +4 -3
- package/src/updates.js +63 -11
package/package.json
CHANGED
package/src/claudeSetup.js
CHANGED
|
@@ -10,6 +10,7 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
|
|
12
12
|
import { IMPEL_MANAGED_MCP_ENV, impelMcpInvocation } from "./selfInvocation.js";
|
|
13
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
13
14
|
import {
|
|
14
15
|
applyImpelClaudeSandbox,
|
|
15
16
|
captureClaudeSandbox,
|
|
@@ -20,9 +21,13 @@ export const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
|
20
21
|
export const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
21
22
|
export const CLAUDE_USER_CONFIG_PATH = path.join(os.homedir(), ".claude.json");
|
|
22
23
|
|
|
23
|
-
export const IMPEL_API_KEY_HELPER =
|
|
24
|
-
export const IMPEL_MCP_SERVER_NAME =
|
|
25
|
-
const LEGACY_IMPEL_MCP_SERVER = {
|
|
24
|
+
export const IMPEL_API_KEY_HELPER = `${RUNTIME_BRAND.cli.command} token`;
|
|
25
|
+
export const IMPEL_MCP_SERVER_NAME = RUNTIME_BRAND.cli.providerId;
|
|
26
|
+
const LEGACY_IMPEL_MCP_SERVER = {
|
|
27
|
+
type: "stdio",
|
|
28
|
+
command: RUNTIME_BRAND.cli.command,
|
|
29
|
+
args: ["mcp"],
|
|
30
|
+
};
|
|
26
31
|
|
|
27
32
|
function impelMcpServer() {
|
|
28
33
|
return impelMcpInvocation();
|
package/src/codexSetup.js
CHANGED
|
@@ -32,11 +32,12 @@ import os from "node:os";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
|
|
34
34
|
import { impelCliInvocation } from "./selfInvocation.js";
|
|
35
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
35
36
|
|
|
36
37
|
export const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
37
38
|
export const CODEX_CONFIG_PATH = path.join(CODEX_HOME, "config.toml");
|
|
38
39
|
|
|
39
|
-
export const PROVIDER_ID =
|
|
40
|
+
export const PROVIDER_ID = RUNTIME_BRAND.cli.providerId;
|
|
40
41
|
|
|
41
42
|
// Genuine Codex CLI traffic has its own byte-preserving compatibility route.
|
|
42
43
|
// With wire_api = "responses", Codex POSTs to `${base_url}/responses`, so this
|
|
@@ -45,8 +46,13 @@ export const PROVIDER_ID = "impel";
|
|
|
45
46
|
export const CODEX_CLI_BASE_PATH = "/chatgpt_passthrough/backend-api/codex";
|
|
46
47
|
export const impelCodexBaseUrl = (gatewayUrl) => `${gatewayUrl}${CODEX_CLI_BASE_PATH}`;
|
|
47
48
|
|
|
48
|
-
const START_MARK = `# >>>
|
|
49
|
-
const END_MARK = `# <<<
|
|
49
|
+
const START_MARK = `# >>> ${RUNTIME_BRAND.cli.managedMarker} managed block (model_providers.${PROVIDER_ID}) >>>`;
|
|
50
|
+
const END_MARK = `# <<< ${RUNTIME_BRAND.cli.managedMarker} managed block <<<`;
|
|
51
|
+
const escapedProviderId = PROVIDER_ID.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
52
|
+
const FOREIGN_PROVIDER_TABLE_RE = new RegExp(
|
|
53
|
+
`^(\\[model_providers\\.${escapedProviderId}(\\.|\\])|\\[mcp_servers\\.${escapedProviderId}\\])`,
|
|
54
|
+
"m",
|
|
55
|
+
);
|
|
50
56
|
|
|
51
57
|
const PROVIDER_LINE_RE = /^model_provider[ \t]*=[ \t]*"([^"]*)"[ \t]*$/m;
|
|
52
58
|
const NETWORK_TABLE = "sandbox_workspace_write";
|
|
@@ -65,12 +71,12 @@ function providerTablesBlock(
|
|
|
65
71
|
const mcp = impelCliInvocation(["mcp"]);
|
|
66
72
|
return [
|
|
67
73
|
START_MARK,
|
|
68
|
-
|
|
74
|
+
`# Generated by \`${RUNTIME_BRAND.cli.command} use gateway codex\`. Safe to re-run; do not hand-edit`,
|
|
69
75
|
"# the lines between the markers above/below, they'll be overwritten.",
|
|
70
76
|
...(includeSandboxMode ? ['sandbox_mode = "workspace-write"'] : []),
|
|
71
77
|
...(includeNetworkAccess ? [`${NETWORK_TABLE}.${NETWORK_KEY} = true`, ""] : []),
|
|
72
78
|
`[model_providers.${PROVIDER_ID}]`,
|
|
73
|
-
`name =
|
|
79
|
+
`name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
|
|
74
80
|
`base_url = "${baseUrl}"`,
|
|
75
81
|
`wire_api = "responses"`,
|
|
76
82
|
"",
|
|
@@ -234,9 +240,7 @@ function stripManagedBlock(text) {
|
|
|
234
240
|
|
|
235
241
|
/** True if a `[model_providers.impel` table exists outside of our own managed markers. */
|
|
236
242
|
function hasForeignImpelTable(textWithoutManagedBlock) {
|
|
237
|
-
return
|
|
238
|
-
textWithoutManagedBlock
|
|
239
|
-
);
|
|
243
|
+
return FOREIGN_PROVIDER_TABLE_RE.test(textWithoutManagedBlock);
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
/** Reads the current root-level `model_provider` value, or null if unset. */
|
package/src/commands/doctor.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
normalizeTenantId,
|
|
13
13
|
productAccessLabel,
|
|
14
14
|
} from "../tenants.js";
|
|
15
|
+
import { brandedEnvironmentName } from "../runtimeBrand.js";
|
|
15
16
|
|
|
16
17
|
const HELP = `impel doctor - run synthetic, billable end-to-end gateway checks
|
|
17
18
|
|
|
@@ -159,7 +160,10 @@ export async function cmdDoctor(argv) {
|
|
|
159
160
|
const gatewayUrl = doctorGatewayUrl(
|
|
160
161
|
flags.gateway !== undefined
|
|
161
162
|
? flags.gateway
|
|
162
|
-
: process.env
|
|
163
|
+
: process.env[brandedEnvironmentName("GATEWAY_URL")]
|
|
164
|
+
|| process.env.IMPEL_GATEWAY_URL
|
|
165
|
+
|| config.gatewayUrl
|
|
166
|
+
|| resolveDefaultGateway(),
|
|
163
167
|
);
|
|
164
168
|
const doctorConfig = { ...config, gatewayUrl };
|
|
165
169
|
const listing = await fetchTenants(config);
|
package/src/commands/nuke.js
CHANGED
|
@@ -18,16 +18,18 @@ import { promptText } from "../prompt.js";
|
|
|
18
18
|
import { appProcessPattern } from "../apps.js";
|
|
19
19
|
import { windowsStableEntrypointRoot } from "../selfInvocation.js";
|
|
20
20
|
import { windowsClaudeUserData } from "../windowsApps.js";
|
|
21
|
+
import { brandedEnvironmentName, RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
21
22
|
|
|
22
23
|
// Launcher artifacts the CLI may have written into ~/Applications: the
|
|
23
24
|
// tenant-scoped bundles, the pre-v0.8 global bundles, and any interrupted
|
|
24
25
|
// `.tmp-<pid>` staging or `.previous-<pid>` rotation directories they left
|
|
25
26
|
// behind. Anchored so unrelated apps that merely start with "Impel" survive.
|
|
26
|
-
const
|
|
27
|
+
const escapedDisplayPrefix = RUNTIME_BRAND.apps.displayPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
28
|
+
const MANAGED_LAUNCHER_RE = new RegExp(`^${escapedDisplayPrefix} (Claude|ChatGPT)(\\.app| \\()`, "u");
|
|
27
29
|
|
|
28
30
|
// Every Impel-managed bundle identifier starts with this prefix; the vendor
|
|
29
31
|
// apps use com.anthropic.* / com.openai.* and are never matched.
|
|
30
|
-
const MANAGED_BUNDLE_PREFIX =
|
|
32
|
+
const MANAGED_BUNDLE_PREFIX = `${RUNTIME_BRAND.apps.bundleIdentifierPrefix}.`;
|
|
31
33
|
|
|
32
34
|
// macOS per-app state locations that accumulate entries keyed by bundle id.
|
|
33
35
|
const MAC_LIBRARY_LOCATIONS = [
|
|
@@ -79,9 +81,9 @@ function keychainCandidates(appsRoot) {
|
|
|
79
81
|
// Cover both app-name eras: the current UA-safe "Impel [tenant] Claude"
|
|
80
82
|
// and the pre-0.17.9 "Impel Claude [tenant]" that may have left an orphaned
|
|
81
83
|
// Keychain item behind.
|
|
82
|
-
names.add(
|
|
83
|
-
names.add(
|
|
84
|
-
names.add(
|
|
84
|
+
names.add(`${RUNTIME_BRAND.apps.displayPrefix} [${tenantId}] Claude Safe Storage`);
|
|
85
|
+
names.add(`${RUNTIME_BRAND.apps.displayPrefix} Claude [${tenantId}] Safe Storage`);
|
|
86
|
+
names.add(`${RUNTIME_BRAND.apps.displayPrefix} ChatGPT [${tenantId}] Safe Storage`);
|
|
85
87
|
const metadataPath = path.join(tenantsRoot, tenantId, "claude", "safe-storage.json");
|
|
86
88
|
try {
|
|
87
89
|
const appName = JSON.parse(fs.readFileSync(metadataPath, "utf8"))?.appName;
|
|
@@ -166,7 +168,7 @@ export async function cmdNuke(argv = [], overrides = {}) {
|
|
|
166
168
|
...overrides,
|
|
167
169
|
};
|
|
168
170
|
|
|
169
|
-
const appsRoot = io.environment
|
|
171
|
+
const appsRoot = io.environment[brandedEnvironmentName("APP_HOME")] || path.join(io.configDir, "apps");
|
|
170
172
|
const darwin = io.platform === "darwin";
|
|
171
173
|
const launchers = darwin ? managedLauncherEntries(io.homeDir) : [];
|
|
172
174
|
const libraryRemnants = darwin ? macLibraryRemnants(io.homeDir) : [];
|
|
@@ -222,7 +224,7 @@ export async function cmdNuke(argv = [], overrides = {}) {
|
|
|
222
224
|
// Missing items exit non-zero; that just means there is nothing to remove.
|
|
223
225
|
io.run("/usr/bin/security", ["delete-generic-password", "-s", item]);
|
|
224
226
|
}
|
|
225
|
-
if (keychainItems.length) io.log(`Cleared ${keychainItems.length}
|
|
227
|
+
if (keychainItems.length) io.log(`Cleared ${keychainItems.length} ${RUNTIME_BRAND.product.displayName} Safe Storage Keychain item${keychainItems.length === 1 ? "" : "s"}.`);
|
|
226
228
|
|
|
227
229
|
for (const remnant of windowsRemnants) {
|
|
228
230
|
fs.rmSync(remnant, { recursive: true, force: true });
|
package/src/commands/sessions.js
CHANGED
|
@@ -16,13 +16,20 @@ import {
|
|
|
16
16
|
import { loadConfig, redactSecretText } from "../config.js";
|
|
17
17
|
import { impelCliInvocation } from "../selfInvocation.js";
|
|
18
18
|
import { spawnDetachedAppRefresh } from "../updates.js";
|
|
19
|
+
import { brandedEnvironmentName, RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
20
|
+
|
|
21
|
+
const MANAGED_SESSION_HOOK_FLAG = `${RUNTIME_BRAND.cli.command}-managed-session-hook-v1`;
|
|
22
|
+
|
|
23
|
+
function environmentValue(suffix) {
|
|
24
|
+
return process.env[brandedEnvironmentName(suffix)] ?? process.env[`IMPEL_${suffix}`];
|
|
25
|
+
}
|
|
19
26
|
|
|
20
27
|
const SPEC = {
|
|
21
28
|
provider: { type: "string" },
|
|
22
29
|
surface: { type: "string" },
|
|
23
30
|
tenant: { type: "string" },
|
|
24
31
|
session: { type: "string" },
|
|
25
|
-
|
|
32
|
+
[MANAGED_SESSION_HOOK_FLAG]: { type: "boolean" },
|
|
26
33
|
};
|
|
27
34
|
|
|
28
35
|
/**
|
|
@@ -60,12 +67,12 @@ function startDetachedFlush({ provider, tenant, session }) {
|
|
|
60
67
|
tenant,
|
|
61
68
|
"--session",
|
|
62
69
|
session,
|
|
63
|
-
|
|
70
|
+
`--${MANAGED_SESSION_HOOK_FLAG}`,
|
|
64
71
|
]);
|
|
65
72
|
const child = spawn(invocation.command, invocation.args, {
|
|
66
73
|
detached: true,
|
|
67
74
|
stdio: "ignore",
|
|
68
|
-
env: { ...process.env,
|
|
75
|
+
env: { ...process.env, [brandedEnvironmentName("SESSIONS_FLUSH_CHILD")]: "1" },
|
|
69
76
|
windowsHide: true,
|
|
70
77
|
});
|
|
71
78
|
child.once("error", () => {});
|
|
@@ -94,7 +101,7 @@ export async function cmdSessions(argv) {
|
|
|
94
101
|
flush: false,
|
|
95
102
|
});
|
|
96
103
|
if (flags.provider === "codex") maybeRepairManagedCodexApp(flags.tenant);
|
|
97
|
-
if (config && (config.tenantId === flags.tenant ||
|
|
104
|
+
if (config && (config.tenantId === flags.tenant || environmentValue("SESSIONS_DEV_ORG_ID"))) {
|
|
98
105
|
// Hooks fire on every session event; only spawn a flush child when no
|
|
99
106
|
// live one is already polling this session's outbox (heartbeat lock).
|
|
100
107
|
const lock = sessionFlushLockPath({
|
|
@@ -114,7 +121,7 @@ export async function cmdSessions(argv) {
|
|
|
114
121
|
} catch (error) {
|
|
115
122
|
// Session persistence is observational. A collector outage must never
|
|
116
123
|
// block a model turn or change a provider hook's decision semantics.
|
|
117
|
-
if (
|
|
124
|
+
if (environmentValue("SESSIONS_DEBUG") === "1") {
|
|
118
125
|
console.error(`impel sessions hook: ${redactSecretText(error?.message || error)}`);
|
|
119
126
|
}
|
|
120
127
|
}
|
|
@@ -130,7 +137,7 @@ export async function cmdSessions(argv) {
|
|
|
130
137
|
if (action === "flush") {
|
|
131
138
|
if (!flags.provider || !flags.session || !flags.tenant) return;
|
|
132
139
|
if (!["claude_code", "codex"].includes(flags.provider)) return;
|
|
133
|
-
if (!config || flags.tenant !== config.tenantId && !
|
|
140
|
+
if (!config || flags.tenant !== config.tenantId && !environmentValue("SESSIONS_DEV_ORG_ID")) return;
|
|
134
141
|
const lock = sessionFlushLockPath({
|
|
135
142
|
tenantId: flags.tenant,
|
|
136
143
|
provider: flags.provider,
|
package/src/commands/status.js
CHANGED
|
@@ -106,7 +106,9 @@ export async function cmdStatus(overrides = {}) {
|
|
|
106
106
|
} catch (error) {
|
|
107
107
|
console.log(`Authentication check: unavailable (${error?.message || error})`);
|
|
108
108
|
console.log(`Current CLI tenant: ${config.tenantId || "not selected"}`);
|
|
109
|
-
console.log(
|
|
109
|
+
console.log(RUNTIME_BRAND.tenant.defaultId
|
|
110
|
+
? `Run \`${RUNTIME_BRAND.cli.command} setup\` to refresh authentication and repair the local tenant.`
|
|
111
|
+
: "Run `impel setup` to refresh authentication, then `impel update` to repair local tenants.");
|
|
110
112
|
if (RUNTIME_BRAND.cli.packageName === "impel-cli") io.maybePrintUpdateNotice();
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
@@ -142,6 +144,10 @@ export async function cmdStatus(overrides = {}) {
|
|
|
142
144
|
+ `Claude app ${desktop.claude.app}, ChatGPT app ${desktop.codex.app}`,
|
|
143
145
|
);
|
|
144
146
|
}
|
|
145
|
-
if (incomplete)
|
|
147
|
+
if (incomplete) {
|
|
148
|
+
console.log(RUNTIME_BRAND.tenant.defaultId
|
|
149
|
+
? `Repair or finish missing tenant surfaces with: ${RUNTIME_BRAND.cli.command} setup`
|
|
150
|
+
: "Repair or finish missing tenant surfaces with: impel update");
|
|
151
|
+
}
|
|
146
152
|
if (RUNTIME_BRAND.cli.packageName === "impel-cli") io.maybePrintUpdateNotice();
|
|
147
153
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
// reconcile every tenant and managed surface from the live control-plane list.
|
|
3
3
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
5
6
|
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
import { parseFlags } from "../args.js";
|
|
9
9
|
import { loadConfig, redactSecretText } from "../config.js";
|
|
10
10
|
import { nativeCommandInvocation } from "../nativeProcess.js";
|
|
11
11
|
import { withProgress } from "../progress.js";
|
|
12
12
|
import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
13
|
+
import { brandedText, RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
14
|
+
import { IMPEL_CLI_ENTRYPOINT } from "../selfInvocation.js";
|
|
13
15
|
import {
|
|
14
16
|
fetchRemoteVersion,
|
|
15
17
|
installedVersion,
|
|
@@ -19,7 +21,7 @@ import {
|
|
|
19
21
|
writeUpdateCache,
|
|
20
22
|
} from "../updates.js";
|
|
21
23
|
|
|
22
|
-
const CLI_BIN =
|
|
24
|
+
const CLI_BIN = IMPEL_CLI_ENTRYPOINT;
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* The version now on disk at the package that owns CLI_BIN, read FRESH (never
|
|
@@ -28,7 +30,7 @@ const CLI_BIN = fileURLToPath(new URL("../../bin/impel.js", import.meta.url));
|
|
|
28
30
|
*/
|
|
29
31
|
export function postInstallCliVersion() {
|
|
30
32
|
try {
|
|
31
|
-
const packagePath =
|
|
33
|
+
const packagePath = path.resolve(path.dirname(CLI_BIN), "..", "package.json");
|
|
32
34
|
const version = JSON.parse(fs.readFileSync(packagePath, "utf8"))?.version;
|
|
33
35
|
return typeof version === "string" && version.trim() ? version.trim() : null;
|
|
34
36
|
} catch {
|
|
@@ -36,7 +38,7 @@ export function postInstallCliVersion() {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
const HELP = `impel update - update everything Impel in one command
|
|
41
|
+
const HELP = brandedText(`impel update - update everything Impel in one command
|
|
40
42
|
|
|
41
43
|
Reinstalls impel-cli from npm, then uses the new build to discover every
|
|
42
44
|
accessible tenant. Missing tenants are installed; existing tenant profiles and
|
|
@@ -48,7 +50,7 @@ Usage:
|
|
|
48
50
|
impel update --skip-apps Reconcile only isolated CLI profiles
|
|
49
51
|
impel update --skip-clis Do not install missing vendor CLIs
|
|
50
52
|
impel update --no-recovery Disable local and hosted recovery for this run
|
|
51
|
-
|
|
53
|
+
`);
|
|
52
54
|
|
|
53
55
|
function reportProcessFailure(stage, result) {
|
|
54
56
|
if (result?.error) {
|
|
@@ -237,14 +239,14 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
237
239
|
platform: io.platform,
|
|
238
240
|
architecture: process.arch,
|
|
239
241
|
step: "install.impel_cli",
|
|
240
|
-
command:
|
|
241
|
-
message:
|
|
242
|
+
command: `npm install --global ${RUNTIME_BRAND.cli.packageName}@latest`,
|
|
243
|
+
message: `The npm-verified global ${RUNTIME_BRAND.cli.packageName} update failed.`,
|
|
242
244
|
},
|
|
243
245
|
config,
|
|
244
246
|
goals: [
|
|
245
247
|
{
|
|
246
248
|
id: "cli-update",
|
|
247
|
-
description:
|
|
249
|
+
description: `The global ${RUNTIME_BRAND.cli.packageName} npm update completed successfully`,
|
|
248
250
|
run: () => updateState.ok === true,
|
|
249
251
|
},
|
|
250
252
|
],
|
package/src/extension/index.js
CHANGED
|
@@ -3,7 +3,25 @@ import path from "node:path";
|
|
|
3
3
|
import { main as upstreamMain } from "../cli.js";
|
|
4
4
|
import { brandedText, RUNTIME_BRAND, validateRuntimeBrand } from "../runtimeBrand.js";
|
|
5
5
|
|
|
6
|
-
const ALIASES = Object.freeze({
|
|
6
|
+
const ALIASES = Object.freeze({
|
|
7
|
+
apps: "app",
|
|
8
|
+
pats: "pat",
|
|
9
|
+
task: "tasks",
|
|
10
|
+
tickets: "tasks",
|
|
11
|
+
ticket: "tasks",
|
|
12
|
+
tenants: "tenant",
|
|
13
|
+
org: "tenant",
|
|
14
|
+
skill: "skills",
|
|
15
|
+
agent: "agents",
|
|
16
|
+
upgrade: "update",
|
|
17
|
+
on: "use",
|
|
18
|
+
off: "use",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const HIDDEN_COMMAND_CAPABILITIES = Object.freeze({
|
|
22
|
+
_converge: "update",
|
|
23
|
+
"_app-launch": "app",
|
|
24
|
+
});
|
|
7
25
|
|
|
8
26
|
function help(version) {
|
|
9
27
|
const command = RUNTIME_BRAND.cli.command;
|
|
@@ -13,10 +31,20 @@ function help(version) {
|
|
|
13
31
|
if (enabled.has("setup")) lines.push(` ${command} setup Configure and prepare ${product} CLI/app profiles`);
|
|
14
32
|
if (enabled.has("auth")) lines.push(` ${command} auth Store an existing ${product} PAT`);
|
|
15
33
|
if (enabled.has("pat")) lines.push(` ${command} pat create|revoke Mint or revoke a ${product} PAT`);
|
|
34
|
+
if (enabled.has("tasks")) lines.push(` ${command} tasks list|get|create|... Work with ${product} tickets`);
|
|
35
|
+
if (enabled.has("tenant")) lines.push(` ${command} tenant list|current|use Inspect or select an organization`);
|
|
16
36
|
if (enabled.has("app")) lines.push(` ${command} app install|update|open ... Manage isolated desktop apps`);
|
|
17
37
|
if (enabled.has("claude")) lines.push(` ${command} claude [args...] Launch isolated Claude Code`);
|
|
18
38
|
if (enabled.has("codex")) lines.push(` ${command} codex [args...] Launch isolated Codex`);
|
|
39
|
+
if (enabled.has("mcp")) lines.push(` ${command} mcp Run the authenticated MCP transport`);
|
|
40
|
+
if (enabled.has("sessions")) lines.push(` ${command} sessions ... Run managed session lifecycle hooks`);
|
|
41
|
+
if (enabled.has("skills")) lines.push(` ${command} skills sync [...] Sync gateway skills into managed clients`);
|
|
42
|
+
if (enabled.has("agents")) lines.push(` ${command} agents sync [...] Sync tenant agents into managed clients`);
|
|
19
43
|
if (enabled.has("status")) lines.push(` ${command} status Show local readiness`);
|
|
44
|
+
if (enabled.has("doctor")) lines.push(` ${command} doctor [...] Run gateway and provider diagnostics`);
|
|
45
|
+
if (enabled.has("update")) lines.push(` ${command} update [...] Update the CLI and reconcile tenants`);
|
|
46
|
+
if (enabled.has("nuke")) lines.push(` ${command} nuke [--yes] Erase all ${product}-managed local state`);
|
|
47
|
+
if (enabled.has("experimental")) lines.push(` ${command} experimental ... Manage gated experimental features`);
|
|
20
48
|
lines.push("", `Version: ${version}`);
|
|
21
49
|
return `${lines.join("\n")}\n`;
|
|
22
50
|
}
|
|
@@ -69,8 +97,8 @@ export function createImpelCliExtension({ brand, entrypoint, version }) {
|
|
|
69
97
|
return;
|
|
70
98
|
}
|
|
71
99
|
const command = ALIASES[rawCommand] || rawCommand;
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
100
|
+
const requiredCapability = HIDDEN_COMMAND_CAPABILITIES[rawCommand] || command;
|
|
101
|
+
if (!allowed.has(requiredCapability)) {
|
|
74
102
|
process.stderr.write(`${RUNTIME_BRAND.cli.command}: command ${JSON.stringify(rawCommand)} is not available\n`);
|
|
75
103
|
process.exitCode = 1;
|
|
76
104
|
return;
|
package/src/runtimeBrand.js
CHANGED
|
@@ -8,7 +8,11 @@ const SAFE_TENANT = /^[A-Za-z0-9_.-]{1,128}$/u;
|
|
|
8
8
|
const SAFE_PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]{0,62}\/)?[a-z0-9][a-z0-9._-]{0,126}$/u;
|
|
9
9
|
const SAFE_BUNDLE_PREFIX = /^[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)+$/u;
|
|
10
10
|
const CONTROL_RE = /[\u0000-\u001F\u007F-\u009F]/u;
|
|
11
|
-
const SUPPORTED_COMMANDS = new Set([
|
|
11
|
+
const SUPPORTED_COMMANDS = new Set([
|
|
12
|
+
"setup", "auth", "pat", "token", "mcp", "sessions", "claude", "codex",
|
|
13
|
+
"status", "doctor", "tasks", "tenant", "app", "nuke", "skills", "agents",
|
|
14
|
+
"update", "use", "experimental",
|
|
15
|
+
]);
|
|
12
16
|
|
|
13
17
|
const DEFAULT = Object.freeze({
|
|
14
18
|
schemaVersion: 1,
|
|
@@ -24,7 +28,9 @@ const DEFAULT = Object.freeze({
|
|
|
24
28
|
auth: Object.freeze({ patPrefix: "impel_pat_", tenantPrefix: "impel_tenant_" }),
|
|
25
29
|
tenant: Object.freeze({ defaultId: null, displayName: null }),
|
|
26
30
|
gateway: Object.freeze({ defaultOrigin: "https://gateway.useimpel.com" }),
|
|
31
|
+
sessions: Object.freeze({ defaultOrigin: "https://sessions.useimpel.com" }),
|
|
27
32
|
controlPlane: Object.freeze({ defaultOrigin: "https://www.useimpel.com" }),
|
|
33
|
+
updates: Object.freeze({ registry: null }),
|
|
28
34
|
apps: Object.freeze({
|
|
29
35
|
displayPrefix: "Impel",
|
|
30
36
|
bundleIdentifierPrefix: "com.useimpel",
|
|
@@ -51,6 +57,15 @@ function origin(name, value) {
|
|
|
51
57
|
return parsed.toString().replace(/\/$/u, "");
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
function registryUrl(name, value) {
|
|
61
|
+
const parsed = new URL(text(name, value));
|
|
62
|
+
const local = parsed.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
|
|
63
|
+
if ((parsed.protocol !== "https:" && !local) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
64
|
+
throw new Error(`impel-cli runtime brand ${name} must be an HTTPS registry URL`);
|
|
65
|
+
}
|
|
66
|
+
return parsed.toString().replace(/\/+$/u, "");
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
function pathSegment(name, value) {
|
|
55
70
|
const result = text(name, value);
|
|
56
71
|
if (result === "." || result === ".." || /[\\/:]/u.test(result)) {
|
|
@@ -98,7 +113,18 @@ export function validateRuntimeBrand(input) {
|
|
|
98
113
|
displayName: input.tenant?.displayName == null ? defaultTenant : text("tenant.displayName", input.tenant.displayName),
|
|
99
114
|
}),
|
|
100
115
|
gateway: Object.freeze({ defaultOrigin: origin("gateway.defaultOrigin", input.gateway?.defaultOrigin) }),
|
|
116
|
+
sessions: Object.freeze({
|
|
117
|
+
defaultOrigin: origin(
|
|
118
|
+
"sessions.defaultOrigin",
|
|
119
|
+
input.sessions?.defaultOrigin || input.gateway?.defaultOrigin,
|
|
120
|
+
),
|
|
121
|
+
}),
|
|
101
122
|
controlPlane: Object.freeze({ defaultOrigin: origin("controlPlane.defaultOrigin", input.controlPlane?.defaultOrigin) }),
|
|
123
|
+
updates: Object.freeze({
|
|
124
|
+
registry: input.updates?.registry == null
|
|
125
|
+
? null
|
|
126
|
+
: registryUrl("updates.registry", input.updates.registry),
|
|
127
|
+
}),
|
|
102
128
|
apps: Object.freeze({
|
|
103
129
|
displayPrefix: pathSegment("apps.displayPrefix", input.apps?.displayPrefix || input.product?.displayName),
|
|
104
130
|
bundleIdentifierPrefix: text("apps.bundleIdentifierPrefix", input.apps?.bundleIdentifierPrefix, SAFE_BUNDLE_PREFIX),
|
package/src/selfInvocation.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
4
|
import { environmentValue } from "./nativeProcess.js";
|
|
5
|
-
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
5
|
+
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
6
6
|
|
|
7
7
|
/** The running package's own bin script, used directly for live child spawns. */
|
|
8
8
|
export const IMPEL_CLI_ENTRYPOINT = process.env.IMPEL_CLI_EXTENSION_ENTRYPOINT
|
|
@@ -64,7 +64,7 @@ export function impelCliInvocation(args = [], options = {}) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export const IMPEL_MANAGED_MCP_ENV = "
|
|
67
|
+
export const IMPEL_MANAGED_MCP_ENV = brandedEnvironmentName("MANAGED_MCP");
|
|
68
68
|
|
|
69
69
|
export function impelMcpInvocation(args = [], options = {}) {
|
|
70
70
|
return {
|
package/src/sessionCollector.js
CHANGED
|
@@ -6,8 +6,9 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import * as zlib from "node:zlib";
|
|
7
7
|
|
|
8
8
|
import { CONFIG_DIR, loadConfig, redactCredentialText, redactSecretText } from "./config.js";
|
|
9
|
+
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
9
10
|
|
|
10
|
-
export const DEFAULT_SESSIONS_URL =
|
|
11
|
+
export const DEFAULT_SESSIONS_URL = RUNTIME_BRAND.sessions.defaultOrigin;
|
|
11
12
|
const MAX_HOOK_INPUT_BYTES = 2 * 1024 * 1024;
|
|
12
13
|
const MAX_LEDGER_PAYLOAD_BYTES = 256 * 1024;
|
|
13
14
|
const TRANSCRIPT_CHUNK_BYTES = 512 * 1024;
|
|
@@ -22,8 +23,14 @@ const DIRECT_UPLOAD_TIMEOUT_MS = 30_000;
|
|
|
22
23
|
const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
|
|
23
24
|
const TASK_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
24
25
|
|
|
26
|
+
function brandedEnvironmentValue(suffix) {
|
|
27
|
+
const branded = process.env[brandedEnvironmentName(suffix)];
|
|
28
|
+
if (branded !== undefined) return branded;
|
|
29
|
+
return process.env[`IMPEL_${suffix}`];
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
function stateRoot() {
|
|
26
|
-
return
|
|
33
|
+
return brandedEnvironmentValue("SESSIONS_STATE_DIR") || path.join(CONFIG_DIR, "sessions");
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
function sha256(value) {
|
|
@@ -195,7 +202,7 @@ function acquireLock(root, name, staleMs = 60_000) {
|
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
function providerRoots(provider) {
|
|
198
|
-
const roots = [
|
|
205
|
+
const roots = [brandedEnvironmentValue("SESSION_TRANSCRIPT_ROOT")];
|
|
199
206
|
if (provider === "claude_code") roots.push(process.env.CLAUDE_CONFIG_DIR, path.join(os.homedir(), ".claude"));
|
|
200
207
|
else roots.push(process.env.CODEX_HOME, path.join(os.homedir(), ".codex"));
|
|
201
208
|
return roots.filter(Boolean).map((candidate) => {
|
|
@@ -372,8 +379,8 @@ function enforceOutboxLimits(root) {
|
|
|
372
379
|
return { dropped: 0, pending: pendingDirectories(root).length, bytes: null, deferred: true };
|
|
373
380
|
}
|
|
374
381
|
try {
|
|
375
|
-
const maximumBytes = positiveIntegerEnvironment("
|
|
376
|
-
const maximumBatches = positiveIntegerEnvironment("
|
|
382
|
+
const maximumBytes = positiveIntegerEnvironment(brandedEnvironmentName("SESSIONS_MAX_OUTBOX_BYTES"), DEFAULT_MAX_OUTBOX_BYTES);
|
|
383
|
+
const maximumBatches = positiveIntegerEnvironment(brandedEnvironmentName("SESSIONS_MAX_OUTBOX_BATCHES"), DEFAULT_MAX_OUTBOX_BATCHES);
|
|
377
384
|
const directories = pendingDirectories(root);
|
|
378
385
|
let bytes = directories.reduce((total, directory) => total + batchDirectoryBytes(directory), 0);
|
|
379
386
|
let count = directories.length;
|
|
@@ -503,7 +510,11 @@ function repositoryMetadata(cwd) {
|
|
|
503
510
|
}
|
|
504
511
|
|
|
505
512
|
function sessionsUrl(config) {
|
|
506
|
-
return String(
|
|
513
|
+
return String(
|
|
514
|
+
brandedEnvironmentValue("SESSIONS_URL")
|
|
515
|
+
|| config?.sessionsUrl
|
|
516
|
+
|| DEFAULT_SESSIONS_URL,
|
|
517
|
+
).trim().replace(/\/+$/u, "");
|
|
507
518
|
}
|
|
508
519
|
|
|
509
520
|
function problemCode(body) {
|
|
@@ -516,16 +527,16 @@ function problemMessage(body) {
|
|
|
516
527
|
|
|
517
528
|
function requestHeaders(config, tenantId, extra = {}) {
|
|
518
529
|
const headers = { accept: "application/json", ...extra };
|
|
519
|
-
const devOrg =
|
|
530
|
+
const devOrg = brandedEnvironmentValue("SESSIONS_DEV_ORG_ID");
|
|
520
531
|
if (devOrg) {
|
|
521
532
|
headers["x-impel-dev-org-id"] = devOrg;
|
|
522
|
-
headers["x-impel-dev-user-id"] =
|
|
533
|
+
headers["x-impel-dev-user-id"] = brandedEnvironmentValue("SESSIONS_DEV_USER_ID") || `${RUNTIME_BRAND.cli.command}-cli-smoke`;
|
|
523
534
|
} else {
|
|
524
535
|
if (!config?.pat) throw new Error("Impel authentication is unavailable");
|
|
525
536
|
headers.authorization = `Bearer ${config.pat}`;
|
|
526
537
|
headers["x-impel-org-id"] = tenantId;
|
|
527
538
|
}
|
|
528
|
-
const bypass =
|
|
539
|
+
const bypass = brandedEnvironmentValue("SESSIONS_VERCEL_BYPASS_TOKEN");
|
|
529
540
|
if (bypass) headers["x-vercel-protection-bypass"] = bypass;
|
|
530
541
|
return headers;
|
|
531
542
|
}
|
|
@@ -558,7 +569,7 @@ async function apiRequest(config, tenantId, route, options = {}) {
|
|
|
558
569
|
}
|
|
559
570
|
|
|
560
571
|
function compressZstd(payload) {
|
|
561
|
-
if (
|
|
572
|
+
if (brandedEnvironmentValue("SESSIONS_DISABLE_DIRECT_UPLOAD") === "1") return null;
|
|
562
573
|
if (typeof zlib.zstdCompressSync !== "function") return null;
|
|
563
574
|
const level = zlib.constants?.ZSTD_c_compressionLevel;
|
|
564
575
|
const windowLog = zlib.constants?.ZSTD_c_windowLog;
|
|
@@ -938,8 +949,9 @@ export async function collectSessionHook({ provider, surface, tenantId, input, c
|
|
|
938
949
|
if (!SAFE_ID_RE.test(surface)) throw new Error("Invalid session surface");
|
|
939
950
|
const sessionKey = String(input?.session_id || "").trim();
|
|
940
951
|
if (!sessionKey || sessionKey.length > 512 || /[\r\n\0]/u.test(sessionKey)) throw new Error("Hook input has no valid session_id");
|
|
941
|
-
const
|
|
942
|
-
|
|
952
|
+
const taskIdEnvironment = brandedEnvironmentName("TASK_ID");
|
|
953
|
+
const taskId = String(brandedEnvironmentValue("TASK_ID") || "").trim();
|
|
954
|
+
if (taskId && !TASK_ID_RE.test(taskId)) throw new Error(`${taskIdEnvironment} is invalid`);
|
|
943
955
|
const sessionMeta = {
|
|
944
956
|
provider,
|
|
945
957
|
surface,
|
|
@@ -960,7 +972,7 @@ export async function collectSessionHook({ provider, surface, tenantId, input, c
|
|
|
960
972
|
}
|
|
961
973
|
}
|
|
962
974
|
const limits = enforceOutboxLimits(root);
|
|
963
|
-
const selectedTenant =
|
|
975
|
+
const selectedTenant = brandedEnvironmentValue("SESSIONS_DEV_ORG_ID") || config?.tenantId === tenantId;
|
|
964
976
|
if (flush && config && selectedTenant) return flushCollectedSession({ tenantId, provider, sessionKey, config });
|
|
965
977
|
return {
|
|
966
978
|
flushed: 0,
|
package/src/sessionHooks.js
CHANGED
|
@@ -3,10 +3,11 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { impelCliInvocation } from "./selfInvocation.js";
|
|
6
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
6
7
|
|
|
7
|
-
const MANAGED_MARKER =
|
|
8
|
-
const CODEX_TRUST_START =
|
|
9
|
-
const CODEX_TRUST_END =
|
|
8
|
+
const MANAGED_MARKER = `--${RUNTIME_BRAND.cli.command}-managed-session-hook-v1`;
|
|
9
|
+
const CODEX_TRUST_START = `# >>> ${RUNTIME_BRAND.product.id} managed session hook trust >>>`;
|
|
10
|
+
const CODEX_TRUST_END = `# <<< ${RUNTIME_BRAND.product.id} managed session hook trust <<<`;
|
|
10
11
|
|
|
11
12
|
export const CLAUDE_SESSION_EVENTS = Object.freeze([
|
|
12
13
|
"SessionStart",
|
package/src/updates.js
CHANGED
|
@@ -9,26 +9,31 @@
|
|
|
9
9
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import { spawn } from "node:child_process";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
14
13
|
|
|
15
14
|
import { CONFIG_DIR } from "./config.js";
|
|
15
|
+
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
16
|
+
import { IMPEL_CLI_ENTRYPOINT } from "./selfInvocation.js";
|
|
17
|
+
import { nativeCommandInvocation } from "./nativeProcess.js";
|
|
16
18
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
17
19
|
|
|
18
20
|
export const UPDATE_CACHE_PATH = path.join(CONFIG_DIR, "update-check.json");
|
|
19
21
|
export const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
20
22
|
|
|
21
|
-
const CLI_ROOT =
|
|
22
|
-
const DEFAULT_UPDATE_PACKAGE = "impel-cli";
|
|
23
|
+
const CLI_ROOT = path.resolve(path.dirname(IMPEL_CLI_ENTRYPOINT), "..");
|
|
23
24
|
const DEFAULT_UPDATE_REGISTRY = "https://registry.npmjs.org";
|
|
24
25
|
const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
25
26
|
|
|
26
27
|
export function updatePackage() {
|
|
27
|
-
return process.env
|
|
28
|
+
return process.env[brandedEnvironmentName("UPDATE_PACKAGE")] || RUNTIME_BRAND.cli.packageName;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function updateRegistry() {
|
|
31
|
-
return String(
|
|
32
|
+
return String(
|
|
33
|
+
process.env[brandedEnvironmentName("UPDATE_REGISTRY")]
|
|
34
|
+
|| RUNTIME_BRAND.updates.registry
|
|
35
|
+
|| DEFAULT_UPDATE_REGISTRY,
|
|
36
|
+
).replace(/\/+$/u, "");
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export function updateInstallSpec() {
|
|
@@ -80,18 +85,65 @@ export function isNewerVersion(candidate, current) {
|
|
|
80
85
|
return false;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
function fetchRemoteVersionWithNpm({
|
|
89
|
+
packageName,
|
|
90
|
+
registry,
|
|
91
|
+
timeoutMs,
|
|
92
|
+
spawnSyncImpl = spawnSync,
|
|
93
|
+
environment = process.env,
|
|
94
|
+
platform = process.platform,
|
|
95
|
+
}) {
|
|
96
|
+
try {
|
|
97
|
+
const invocation = nativeCommandInvocation(
|
|
98
|
+
"npm",
|
|
99
|
+
["view", packageName, "version", "--json", "--registry", registry],
|
|
100
|
+
environment,
|
|
101
|
+
platform,
|
|
102
|
+
);
|
|
103
|
+
const result = spawnSyncImpl(invocation.command, invocation.args, {
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
env: environment,
|
|
106
|
+
timeout: timeoutMs,
|
|
107
|
+
windowsHide: true,
|
|
108
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
109
|
+
});
|
|
110
|
+
if (result.status !== 0 || result.error) return null;
|
|
111
|
+
const raw = String(result.stdout || "").trim();
|
|
112
|
+
let version;
|
|
113
|
+
try { version = JSON.parse(raw); }
|
|
114
|
+
catch { version = raw; }
|
|
115
|
+
return validVersion(version) ? version : null;
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Read the latest published version from npm registry metadata. */
|
|
84
122
|
export async function fetchRemoteVersion({
|
|
85
123
|
packageName = updatePackage(),
|
|
86
124
|
registry = updateRegistry(),
|
|
87
125
|
timeoutMs = 10_000,
|
|
88
|
-
fetchImpl =
|
|
126
|
+
fetchImpl = null,
|
|
127
|
+
spawnSyncImpl = spawnSync,
|
|
128
|
+
environment = process.env,
|
|
129
|
+
platform = process.platform,
|
|
89
130
|
} = {}) {
|
|
131
|
+
const normalizedRegistry = String(registry).replace(/\/+$/u, "");
|
|
132
|
+
if (!fetchImpl && normalizedRegistry !== DEFAULT_UPDATE_REGISTRY) {
|
|
133
|
+
return fetchRemoteVersionWithNpm({
|
|
134
|
+
packageName,
|
|
135
|
+
registry: normalizedRegistry,
|
|
136
|
+
timeoutMs,
|
|
137
|
+
spawnSyncImpl,
|
|
138
|
+
environment,
|
|
139
|
+
platform,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
90
142
|
const controller = new AbortController();
|
|
91
143
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
92
144
|
try {
|
|
93
145
|
const encodedName = encodeURIComponent(packageName);
|
|
94
|
-
const response = await fetchImpl(`${
|
|
146
|
+
const response = await (fetchImpl || globalThis.fetch)(`${normalizedRegistry}/${encodedName}/latest`, {
|
|
95
147
|
headers: { accept: "application/json" },
|
|
96
148
|
signal: controller.signal,
|
|
97
149
|
});
|
|
@@ -162,7 +214,7 @@ export async function refreshUpdateCache(dependencies = {}) {
|
|
|
162
214
|
export function updateNoticeLine({ cache = readUpdateCache(), current = installedVersion() } = {}) {
|
|
163
215
|
const remote = cache?.remoteVersion;
|
|
164
216
|
if (!isNewerVersion(remote, current)) return null;
|
|
165
|
-
return
|
|
217
|
+
return `${RUNTIME_BRAND.cli.packageName} update available (v${current} → v${remote}): run \`${RUNTIME_BRAND.cli.command} update\``;
|
|
166
218
|
}
|
|
167
219
|
|
|
168
220
|
/**
|
|
@@ -198,7 +250,7 @@ export function spawnDetachedAppRefresh(tenantId = null) {
|
|
|
198
250
|
|
|
199
251
|
function spawnDetached(args) {
|
|
200
252
|
try {
|
|
201
|
-
const child = spawn(process.execPath, [
|
|
253
|
+
const child = spawn(process.execPath, [IMPEL_CLI_ENTRYPOINT, ...args], {
|
|
202
254
|
detached: true,
|
|
203
255
|
stdio: "ignore",
|
|
204
256
|
// Windows: a detached console-subsystem child gets its OWN console — a
|