impel-cli 0.12.0 → 0.12.1
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 +17 -8
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/commands/agents.js +6 -1
- package/src/commands/apps.js +111 -7
- package/src/commands/skills.js +21 -7
- package/src/commands/update.js +27 -13
- package/src/progress.js +21 -0
- package/src/skills.js +80 -29
package/README.md
CHANGED
|
@@ -153,7 +153,7 @@ impel agents sync [claude|codex|all] Sync explicit tenant agents into na
|
|
|
153
153
|
impel status Launcher readiness + native mode + gateway reachability
|
|
154
154
|
impel app install [target] [--tenant <org>] Install isolated apps for one tenant
|
|
155
155
|
impel app update [target] [--tenant <org>] [--force]
|
|
156
|
-
|
|
156
|
+
`all` without --tenant updates every installed tenant;
|
|
157
157
|
--force performs a clean full rebuild
|
|
158
158
|
impel app refresh [target] [--tenant <org>] Configs/catalog/skills/agents only; safe while apps run
|
|
159
159
|
(--stale-only: no-op unless 6h+ since last sync)
|
|
@@ -167,7 +167,9 @@ impel help Show help
|
|
|
167
167
|
impel --version Show version
|
|
168
168
|
|
|
169
169
|
For app commands, `target` is `claude`, `chatgpt`/`codex`, or `all` (the
|
|
170
|
-
default).
|
|
170
|
+
default). `impel app update all` without `--tenant` updates every locally
|
|
171
|
+
installed tenant. Other app commands, a specific update target, or an explicit
|
|
172
|
+
`--tenant` operate on one organization and default to the current selection.
|
|
171
173
|
|
|
172
174
|
# `impel setup` no longer takes a per-tool target and never flips native
|
|
173
175
|
# configs; flip a native tool explicitly with `impel use gateway [claude|codex]`.
|
|
@@ -239,10 +241,10 @@ before running another protected command.
|
|
|
239
241
|
One command brings everything current, in dependency order: the CLI itself
|
|
240
242
|
(`npm install -g impel-cli@latest` from npm, with no GitHub credentials),
|
|
241
243
|
then — re-executing the freshly installed build — `impel app update all`
|
|
242
|
-
(
|
|
244
|
+
(update every locally installed tenant and rebuild only stale vendored Impel apps)
|
|
243
245
|
followed by `impel skills sync all` and `impel agents sync all` across every
|
|
244
|
-
|
|
245
|
-
profiles
|
|
246
|
+
native and isolated CLI profile. Each tenant's app step syncs that tenant's app
|
|
247
|
+
profiles; the dedicated steps exclude apps, so every profile is synced once.
|
|
246
248
|
|
|
247
249
|
Update discovery compares the installed package version with npm's public
|
|
248
250
|
`latest` metadata, cached for 6 hours in
|
|
@@ -737,9 +739,12 @@ impel app open all --tenant acme
|
|
|
737
739
|
impel app open all --tenant globex
|
|
738
740
|
```
|
|
739
741
|
|
|
740
|
-
Updates and uninstalls touch only the named tenant variant.
|
|
741
|
-
`
|
|
742
|
-
|
|
742
|
+
Updates with `--tenant` and all uninstalls touch only the named tenant variant.
|
|
743
|
+
`impel app update all` without `--tenant` discovers every locally installed
|
|
744
|
+
tenant and updates each sequentially; it does not install variants for tenants
|
|
745
|
+
that have never been installed. The older global `Impel Claude.app` /
|
|
746
|
+
`Impel ChatGPT.app` launcher is removed after the first successful
|
|
747
|
+
tenant-specific rebuild.
|
|
743
748
|
|
|
744
749
|
`impel app update` compares the installed bundle's CLI version, pinned vendor
|
|
745
750
|
version, identity, profile paths, and ASAR compatibility hash before touching
|
|
@@ -749,6 +754,10 @@ in a bad state, `impel app update --force` removes the selected launcher and
|
|
|
749
754
|
performs a clean rebuild while sweeping abandoned `.tmp-*` and `.previous-*`
|
|
750
755
|
staging directories.
|
|
751
756
|
|
|
757
|
+
Skills plugin subprocesses run asynchronously, so the TTY progress indicator
|
|
758
|
+
continues animating while Claude or Codex refreshes a marketplace. Skill status
|
|
759
|
+
lines temporarily clear and redraw the spinner instead of overwriting it.
|
|
760
|
+
|
|
752
761
|
The official Claude and ChatGPT apps remain separate installations and keep
|
|
753
762
|
their normal accounts, updater, profiles, and bundle identities. Each CLI
|
|
754
763
|
release pins exact, tested vendor builds (v0.10.0 pins Claude `1.20186.9` and
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -49,8 +49,8 @@ Manage:
|
|
|
49
49
|
Desktop apps (macOS and Windows):
|
|
50
50
|
target is claude, chatgpt/codex, or all (default: all)
|
|
51
51
|
impel app install [target] [--tenant <org>] Install that tenant's isolated app/profile
|
|
52
|
-
impel app update [target] [--tenant <org>] Update profiles;
|
|
53
|
-
(--force
|
|
52
|
+
impel app update [target] [--tenant <org>] Update profiles; all without --tenant updates
|
|
53
|
+
every installed tenant (--force: clean rebuild)
|
|
54
54
|
impel app refresh [target] [--tenant <org>] Configs/catalog/skills/agents only; safe while apps run
|
|
55
55
|
impel app status [target] [--tenant <org>] Show isolated app and vendor versions
|
|
56
56
|
impel app uninstall [target] [--tenant <org>] [--keep-data]
|
package/src/commands/agents.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
|
|
6
6
|
import { agentProfileRoot, syncAgentProfiles } from "../agents.js";
|
|
7
|
+
import { parseFlags } from "../args.js";
|
|
7
8
|
import { loadConfig, normalizeGatewayUrl, redactSecretText, resolveDefaultGateway } from "../config.js";
|
|
8
9
|
import { ensureTenantSelection, tenantCredential } from "../tenants.js";
|
|
9
10
|
import { managedSkillProfiles } from "./skills.js";
|
|
@@ -41,7 +42,8 @@ export function managedAgentProfiles(client, options = {}) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export async function cmdAgents(argv, overrides = {}) {
|
|
44
|
-
const [action = "sync",
|
|
45
|
+
const [action = "sync", rawTarget, ...rest] = argv;
|
|
46
|
+
const targetToken = rawTarget?.startsWith("--") ? undefined : rawTarget;
|
|
45
47
|
if (["help", "--help", "-h"].includes(action)) {
|
|
46
48
|
console.log(HELP);
|
|
47
49
|
return;
|
|
@@ -57,6 +59,8 @@ export async function cmdAgents(argv, overrides = {}) {
|
|
|
57
59
|
process.exitCode = 1;
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
62
|
+
const flagArgs = rawTarget?.startsWith("--") ? [rawTarget, ...rest] : rest;
|
|
63
|
+
const { flags } = parseFlags(flagArgs, { "skip-apps": { type: "boolean" } });
|
|
60
64
|
|
|
61
65
|
const io = {
|
|
62
66
|
loadConfig,
|
|
@@ -83,6 +87,7 @@ export async function cmdAgents(argv, overrides = {}) {
|
|
|
83
87
|
tenantId,
|
|
84
88
|
environment: io.environment,
|
|
85
89
|
homeDir: io.homeDir,
|
|
90
|
+
includeApps: !flags["skip-apps"],
|
|
86
91
|
}));
|
|
87
92
|
await io.syncAgentProfiles({
|
|
88
93
|
profiles,
|
package/src/commands/apps.js
CHANGED
|
@@ -40,7 +40,7 @@ import { resolveSkillsGateway, syncSkillsSafe } from "../skills.js";
|
|
|
40
40
|
import { syncAgentProfilesSafe } from "../agents.js";
|
|
41
41
|
import { secureManagedCodexHome } from "../codexSecurity.js";
|
|
42
42
|
import { impelCliInvocation } from "../selfInvocation.js";
|
|
43
|
-
import { withProgress } from "../progress.js";
|
|
43
|
+
import { createProgressLogger, withProgress } from "../progress.js";
|
|
44
44
|
import {
|
|
45
45
|
ensureWindowsClaudeApp,
|
|
46
46
|
ensureWindowsChatGPTApp,
|
|
@@ -79,6 +79,102 @@ function windowsAppTargets(targetToken) {
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function macTargetOrArtifactExists(paths, target, existsSync = fs.existsSync) {
|
|
83
|
+
const launcher = paths[target].launcher;
|
|
84
|
+
if (existsSync(launcher)) return true;
|
|
85
|
+
try {
|
|
86
|
+
const baseName = path.basename(launcher);
|
|
87
|
+
return fs.readdirSync(path.dirname(launcher)).some((entry) => (
|
|
88
|
+
entry.startsWith(`${baseName}.tmp-`) || entry.startsWith(`${baseName}.previous-`)
|
|
89
|
+
));
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Locally installed tenant variants eligible for an all-target app update. */
|
|
96
|
+
export function installedAppTenantIds(targets, {
|
|
97
|
+
homeDir = os.homedir(),
|
|
98
|
+
platform = process.platform,
|
|
99
|
+
environment = process.env,
|
|
100
|
+
existsSync = fs.existsSync,
|
|
101
|
+
readdirSync = fs.readdirSync,
|
|
102
|
+
} = {}) {
|
|
103
|
+
const tenantsRoot = path.join(appPaths(homeDir).root, "tenants");
|
|
104
|
+
let entries;
|
|
105
|
+
try {
|
|
106
|
+
entries = readdirSync(tenantsRoot, { withFileTypes: true });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error?.code === "ENOENT") return [];
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const installed = [];
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
if (!entry.isDirectory()) continue;
|
|
115
|
+
let tenantId;
|
|
116
|
+
try {
|
|
117
|
+
tenantId = normalizeTenantId(entry.name);
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const manifest = readTenantManifest(homeDir, tenantId);
|
|
122
|
+
const claudeUserData = platform === "win32"
|
|
123
|
+
? windowsClaudeUserData(environment, tenantId)
|
|
124
|
+
: null;
|
|
125
|
+
const paths = appPaths(homeDir, tenantId, {
|
|
126
|
+
claudeUserData,
|
|
127
|
+
tenantName: manifest?.tenantName,
|
|
128
|
+
});
|
|
129
|
+
const hasTarget = targets.some((target) => {
|
|
130
|
+
if (platform !== "win32") return macTargetOrArtifactExists(paths, target, existsSync);
|
|
131
|
+
return target === "claude"
|
|
132
|
+
? existsSync(path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`))
|
|
133
|
+
: existsSync(path.join(paths.chatgpt.codexHome, "config.toml"));
|
|
134
|
+
});
|
|
135
|
+
if (hasTarget) installed.push(tenantId);
|
|
136
|
+
}
|
|
137
|
+
return installed.sort((left, right) => left.localeCompare(right));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function maybeUpdateAllInstalledTenants(argv, overrides, platform) {
|
|
141
|
+
if (overrides.singleTenantUpdate) return null;
|
|
142
|
+
const [action = "status", rawTarget, ...rest] = argv;
|
|
143
|
+
if (action !== "update") return null;
|
|
144
|
+
const targetToken = rawTarget?.startsWith("--") ? undefined : rawTarget;
|
|
145
|
+
const targets = normalizeAppTarget(targetToken);
|
|
146
|
+
if (!targets || targets.length !== 2) return null;
|
|
147
|
+
const flagArgs = rawTarget?.startsWith("--") ? [rawTarget, ...rest] : rest;
|
|
148
|
+
const { flags } = parseFlags(flagArgs, {
|
|
149
|
+
"skip-vendor": { type: "boolean" },
|
|
150
|
+
"skip-skills": { type: "boolean" },
|
|
151
|
+
"skip-agents": { type: "boolean" },
|
|
152
|
+
force: { type: "boolean" },
|
|
153
|
+
tenant: { type: "string" },
|
|
154
|
+
});
|
|
155
|
+
if (flags.tenant) return null;
|
|
156
|
+
|
|
157
|
+
const discover = overrides.installedTenantIds || installedAppTenantIds;
|
|
158
|
+
const tenantIds = discover(targets, {
|
|
159
|
+
homeDir: overrides.homeDir || os.homedir(),
|
|
160
|
+
platform,
|
|
161
|
+
environment: overrides.environment || process.env,
|
|
162
|
+
});
|
|
163
|
+
if (tenantIds.length === 0) return null;
|
|
164
|
+
|
|
165
|
+
const log = overrides.log || console.log;
|
|
166
|
+
log(`Updating all managed app targets across ${tenantIds.length} installed tenant${tenantIds.length === 1 ? "" : "s"}: ${tenantIds.join(", ")}`);
|
|
167
|
+
const runTenantUpdate = overrides.runTenantUpdate || cmdApps;
|
|
168
|
+
for (const tenantId of tenantIds) {
|
|
169
|
+
const result = await runTenantUpdate(
|
|
170
|
+
["update", "all", ...flagArgs, "--tenant", tenantId],
|
|
171
|
+
{ ...overrides, singleTenantUpdate: true },
|
|
172
|
+
);
|
|
173
|
+
if (result === false) return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
82
178
|
function windowsProcessFailure(result) {
|
|
83
179
|
if (result?.error?.message) return redactSecretText(result.error.message);
|
|
84
180
|
if (Number.isInteger(result?.status)) return `exit code ${result.status}`;
|
|
@@ -304,8 +400,14 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
304
400
|
console.log(`${verb} Impel ${target === "claude" ? "Claude" : "ChatGPT"} profile at ${profile}`);
|
|
305
401
|
const { client, env, label } = appSkillTarget(target, tenantPaths);
|
|
306
402
|
if (!flags["skip-skills"]) {
|
|
307
|
-
await withProgress(`Syncing skills for ${label}`, () => (
|
|
308
|
-
io.syncSkills({
|
|
403
|
+
await withProgress(`Syncing skills for ${label}`, (spinner) => (
|
|
404
|
+
io.syncSkills({
|
|
405
|
+
client,
|
|
406
|
+
gatewayUrl: resolveSkillsGateway(config.gatewayUrl),
|
|
407
|
+
env,
|
|
408
|
+
label,
|
|
409
|
+
logger: createProgressLogger(spinner),
|
|
410
|
+
})
|
|
309
411
|
));
|
|
310
412
|
}
|
|
311
413
|
if (target === "chatgpt") secureManagedCodexHome(tenantPaths.chatgpt.codexHome);
|
|
@@ -347,6 +449,8 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
347
449
|
|
|
348
450
|
export async function cmdApps(argv, overrides = {}) {
|
|
349
451
|
const platform = overrides.platform || process.platform;
|
|
452
|
+
const allTenantUpdate = await maybeUpdateAllInstalledTenants(argv, overrides, platform);
|
|
453
|
+
if (allTenantUpdate !== null) return allTenantUpdate;
|
|
350
454
|
if (platform === "win32") return cmdWindowsApps(argv, overrides);
|
|
351
455
|
if (platform !== "darwin") {
|
|
352
456
|
console.error("impel app: isolated Impel desktop apps are unavailable on this platform.");
|
|
@@ -520,8 +624,8 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
520
624
|
for (const item of installed) {
|
|
521
625
|
const { client, env, label } = appSkillTarget(item.target, paths);
|
|
522
626
|
if (!flags["skip-skills"]) {
|
|
523
|
-
await withProgress(`Syncing skills for ${label}`, () => (
|
|
524
|
-
io.syncSkills({ client, gatewayUrl, env, label })
|
|
627
|
+
await withProgress(`Syncing skills for ${label}`, (spinner) => (
|
|
628
|
+
io.syncSkills({ client, gatewayUrl, env, label, logger: createProgressLogger(spinner) })
|
|
525
629
|
));
|
|
526
630
|
}
|
|
527
631
|
if (item.target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
|
|
@@ -628,8 +732,8 @@ export async function provisionAndOpenManagedApps({
|
|
|
628
732
|
const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
|
|
629
733
|
for (const item of installed) {
|
|
630
734
|
const { client, env, label } = appSkillTarget(item.target, paths);
|
|
631
|
-
await withProgress(`Syncing skills for ${label}`, () => (
|
|
632
|
-
io.syncSkills({ client, gatewayUrl, env, label })
|
|
735
|
+
await withProgress(`Syncing skills for ${label}`, (spinner) => (
|
|
736
|
+
io.syncSkills({ client, gatewayUrl, env, label, logger: createProgressLogger(spinner) })
|
|
633
737
|
));
|
|
634
738
|
if (item.target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
|
|
635
739
|
}
|
package/src/commands/skills.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
|
|
11
|
+
import { parseFlags } from "../args.js";
|
|
11
12
|
import { loadConfig } from "../config.js";
|
|
12
13
|
import { CODEX_HOME } from "../codexSetup.js";
|
|
13
14
|
import { tenantCliProfilePaths } from "../cliProfiles.js";
|
|
@@ -15,7 +16,7 @@ import { CLAUDE_CONFIG_ID, appPaths } from "../apps.js";
|
|
|
15
16
|
import { resolveSkillsGateway, syncSkillsSafe } from "../skills.js";
|
|
16
17
|
import { ensureTenantSelection } from "../tenants.js";
|
|
17
18
|
import { windowsClaudeUserData } from "../windowsApps.js";
|
|
18
|
-
import { mapWithConcurrency, withProgress } from "../progress.js";
|
|
19
|
+
import { createProgressLogger, mapWithConcurrency, withProgress } from "../progress.js";
|
|
19
20
|
|
|
20
21
|
const VALID_TARGETS = ["claude", "codex", "all"];
|
|
21
22
|
|
|
@@ -40,6 +41,7 @@ export function managedSkillProfiles(
|
|
|
40
41
|
tenantId = null,
|
|
41
42
|
platform = process.platform,
|
|
42
43
|
environment = process.env,
|
|
44
|
+
includeApps = true,
|
|
43
45
|
} = {},
|
|
44
46
|
) {
|
|
45
47
|
const profiles = [];
|
|
@@ -59,7 +61,7 @@ export function managedSkillProfiles(
|
|
|
59
61
|
const appInstalled = platform === "win32"
|
|
60
62
|
? existsSync(path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`))
|
|
61
63
|
: existsSync(paths.claude.launcher);
|
|
62
|
-
if (appInstalled) {
|
|
64
|
+
if (includeApps && appInstalled) {
|
|
63
65
|
profiles.push({ label: "Impel Claude app", env: { CLAUDE_CONFIG_DIR: paths.claude.userData } });
|
|
64
66
|
}
|
|
65
67
|
} else {
|
|
@@ -71,7 +73,7 @@ export function managedSkillProfiles(
|
|
|
71
73
|
const appInstalled = platform === "win32"
|
|
72
74
|
? existsSync(path.join(paths.chatgpt.codexHome, "config.toml"))
|
|
73
75
|
: existsSync(paths.chatgpt.launcher);
|
|
74
|
-
if (appInstalled) {
|
|
76
|
+
if (includeApps && appInstalled) {
|
|
75
77
|
profiles.push({ label: "Impel ChatGPT app", env: { CODEX_HOME: paths.chatgpt.codexHome } });
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -80,7 +82,8 @@ export function managedSkillProfiles(
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export async function cmdSkills(argv, overrides = {}) {
|
|
83
|
-
const [action = "sync",
|
|
85
|
+
const [action = "sync", rawTarget, ...rest] = argv;
|
|
86
|
+
const targetToken = rawTarget?.startsWith("--") ? undefined : rawTarget;
|
|
84
87
|
if (action !== "sync") {
|
|
85
88
|
console.error(`impel skills: unknown action "${action}". Try \`impel skills sync [claude|codex|all]\`.`);
|
|
86
89
|
process.exitCode = 1;
|
|
@@ -93,6 +96,8 @@ export async function cmdSkills(argv, overrides = {}) {
|
|
|
93
96
|
process.exitCode = 1;
|
|
94
97
|
return;
|
|
95
98
|
}
|
|
99
|
+
const flagArgs = rawTarget?.startsWith("--") ? [rawTarget, ...rest] : rest;
|
|
100
|
+
const { flags } = parseFlags(flagArgs, { "skip-apps": { type: "boolean" } });
|
|
96
101
|
|
|
97
102
|
const io = {
|
|
98
103
|
loadConfig,
|
|
@@ -108,12 +113,21 @@ export async function cmdSkills(argv, overrides = {}) {
|
|
|
108
113
|
: null;
|
|
109
114
|
const clients = target === "all" ? ["claude", "codex"] : [target];
|
|
110
115
|
const profiles = clients.flatMap((client) => (
|
|
111
|
-
io.managedProfiles(client, {
|
|
116
|
+
io.managedProfiles(client, {
|
|
117
|
+
tenantId,
|
|
118
|
+
includeApps: !flags["skip-apps"],
|
|
119
|
+
}).map((profile) => ({ client, profile }))
|
|
112
120
|
));
|
|
113
121
|
|
|
114
|
-
await withProgress(`Syncing skills into ${profiles.length} managed profile${profiles.length === 1 ? "" : "s"}`, () => (
|
|
122
|
+
await withProgress(`Syncing skills into ${profiles.length} managed profile${profiles.length === 1 ? "" : "s"}`, (spinner) => (
|
|
115
123
|
mapWithConcurrency(profiles, 4, ({ client, profile }) => (
|
|
116
|
-
io.syncSkills({
|
|
124
|
+
io.syncSkills({
|
|
125
|
+
client,
|
|
126
|
+
gatewayUrl,
|
|
127
|
+
env: profile.env,
|
|
128
|
+
label: profile.label,
|
|
129
|
+
logger: createProgressLogger(spinner),
|
|
130
|
+
})
|
|
117
131
|
))
|
|
118
132
|
));
|
|
119
133
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -15,6 +15,7 @@ import { loadConfig, redactSecretText } from "../config.js";
|
|
|
15
15
|
import { nativeCommandInvocation } from "../nativeProcess.js";
|
|
16
16
|
import { windowsClaudeUserData } from "../windowsApps.js";
|
|
17
17
|
import { withProgress } from "../progress.js";
|
|
18
|
+
import { installedAppTenantIds } from "./apps.js";
|
|
18
19
|
import {
|
|
19
20
|
fetchRemoteVersion,
|
|
20
21
|
installedVersion,
|
|
@@ -29,9 +30,9 @@ const CLI_BIN = fileURLToPath(new URL("../../bin/impel.js", import.meta.url));
|
|
|
29
30
|
const HELP = `impel update - update everything Impel in one command
|
|
30
31
|
|
|
31
32
|
Reinstalls impel-cli from npm, then cascades with the NEW build:
|
|
32
|
-
\`impel app update all\` (update
|
|
33
|
-
|
|
34
|
-
\`impel agents sync all\` across
|
|
33
|
+
\`impel app update all\` (update every locally installed tenant's vendor apps
|
|
34
|
+
and managed app profiles), then \`impel skills sync all\` and
|
|
35
|
+
\`impel agents sync all\` across native and isolated CLI profiles.
|
|
35
36
|
|
|
36
37
|
Usage:
|
|
37
38
|
impel update Update the CLI, then cascade to apps, skills, and agents
|
|
@@ -94,27 +95,40 @@ export function defaultRunAppsUpdate({
|
|
|
94
95
|
execPath = process.execPath,
|
|
95
96
|
cliBin = CLI_BIN,
|
|
96
97
|
} = {}) {
|
|
97
|
-
const result = spawn(execPath, [cliBin, "app", "update", "all"
|
|
98
|
+
const result = spawn(execPath, [cliBin, "app", "update", "all"], {
|
|
98
99
|
stdio: "inherit",
|
|
99
100
|
});
|
|
100
101
|
return result.status === 0;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
function defaultRunSkillsSync(
|
|
104
|
-
|
|
104
|
+
export function defaultRunSkillsSync({
|
|
105
|
+
spawn = spawnSync,
|
|
106
|
+
execPath = process.execPath,
|
|
107
|
+
cliBin = CLI_BIN,
|
|
108
|
+
} = {}) {
|
|
109
|
+
const result = spawn(execPath, [cliBin, "skills", "sync", "all", "--skip-apps"], {
|
|
105
110
|
stdio: "inherit",
|
|
106
111
|
});
|
|
107
112
|
return result.status === 0;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
function defaultRunAgentsSync(
|
|
111
|
-
|
|
115
|
+
export function defaultRunAgentsSync({
|
|
116
|
+
spawn = spawnSync,
|
|
117
|
+
execPath = process.execPath,
|
|
118
|
+
cliBin = CLI_BIN,
|
|
119
|
+
} = {}) {
|
|
120
|
+
const result = spawn(execPath, [cliBin, "agents", "sync", "all", "--skip-apps"], {
|
|
112
121
|
stdio: "inherit",
|
|
113
122
|
});
|
|
114
123
|
return result.status === 0;
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
function anyAppInstalled(homeDir = os.homedir()) {
|
|
127
|
+
if (installedAppTenantIds(["claude", "chatgpt"], {
|
|
128
|
+
homeDir,
|
|
129
|
+
platform: process.platform,
|
|
130
|
+
environment: process.env,
|
|
131
|
+
}).length > 0) return true;
|
|
118
132
|
const config = loadConfig();
|
|
119
133
|
const claudeUserData = process.platform === "win32"
|
|
120
134
|
? windowsClaudeUserData(process.env, config?.tenantId || null)
|
|
@@ -202,7 +216,7 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
202
216
|
console.log(`CLI: updated${remote ? ` to v${remote}` : ""}.`);
|
|
203
217
|
}
|
|
204
218
|
|
|
205
|
-
// ── Cascade:
|
|
219
|
+
// ── Cascade: every tenant app, then non-app managed profiles ───────────
|
|
206
220
|
let cascadeFailed = false;
|
|
207
221
|
if (flags["skip-apps"]) {
|
|
208
222
|
console.log("Apps: skipped (--skip-apps).");
|
|
@@ -212,21 +226,21 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
212
226
|
console.log("Apps: none installed; skipping (run `impel app install` or `impel setup`).");
|
|
213
227
|
} else {
|
|
214
228
|
console.log(io.platform === "win32"
|
|
215
|
-
? "Apps: updating
|
|
216
|
-
: "Apps: refreshing
|
|
229
|
+
? "Apps: updating every installed tenant's signed Claude and ChatGPT profiles…"
|
|
230
|
+
: "Apps: refreshing every installed tenant and rebuilding only stale app bundles…");
|
|
217
231
|
if (!await withProgress("Updating managed desktop apps", () => io.runAppsUpdate())) {
|
|
218
232
|
console.error("impel update: the app update failed; re-run `impel app update` after fixing the issue.");
|
|
219
233
|
cascadeFailed = true;
|
|
220
234
|
}
|
|
221
235
|
}
|
|
222
236
|
|
|
223
|
-
console.log("Skills: syncing
|
|
237
|
+
console.log("Skills: syncing native and isolated CLI profiles…");
|
|
224
238
|
if (!await withProgress("Syncing skills across managed profiles", () => io.runSkillsSync())) {
|
|
225
239
|
console.error("impel update: skill sync failed; re-run `impel skills sync` after fixing the issue.");
|
|
226
240
|
cascadeFailed = true;
|
|
227
241
|
}
|
|
228
242
|
|
|
229
|
-
console.log("Agents: syncing the selected tenant into
|
|
243
|
+
console.log("Agents: syncing the selected tenant into native and isolated CLI profiles…");
|
|
230
244
|
if (!await withProgress("Syncing agents across managed profiles", () => io.runAgentsSync())) {
|
|
231
245
|
console.error("impel update: agent sync failed; re-run `impel agents sync` after fixing the issue.");
|
|
232
246
|
cascadeFailed = true;
|
package/src/progress.js
CHANGED
|
@@ -2,7 +2,9 @@ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "
|
|
|
2
2
|
|
|
3
3
|
function noopSpinner() {
|
|
4
4
|
return {
|
|
5
|
+
active: false,
|
|
5
6
|
update() {},
|
|
7
|
+
writeLine() { return false; },
|
|
6
8
|
succeed() {},
|
|
7
9
|
fail() {},
|
|
8
10
|
};
|
|
@@ -37,9 +39,16 @@ export function createSpinner(label, { stream = process.stderr } = {}) {
|
|
|
37
39
|
const timer = setInterval(render, 80);
|
|
38
40
|
timer.unref?.();
|
|
39
41
|
return {
|
|
42
|
+
active: true,
|
|
40
43
|
update(nextLabel) {
|
|
41
44
|
if (!finished && nextLabel) currentLabel = String(nextLabel);
|
|
42
45
|
},
|
|
46
|
+
writeLine(message) {
|
|
47
|
+
if (finished) return false;
|
|
48
|
+
stream.write(`\r\x1b[K${String(message)}\n`);
|
|
49
|
+
render();
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
43
52
|
succeed(nextLabel) {
|
|
44
53
|
finish("✓", nextLabel);
|
|
45
54
|
},
|
|
@@ -49,6 +58,18 @@ export function createSpinner(label, { stream = process.stderr } = {}) {
|
|
|
49
58
|
};
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
/** Keep ordinary log lines from overwriting an active progress display. */
|
|
62
|
+
export function createProgressLogger(spinner, fallback = console) {
|
|
63
|
+
const write = (method, args) => {
|
|
64
|
+
const message = args.map((value) => String(value)).join(" ");
|
|
65
|
+
if (!spinner?.writeLine?.(message)) fallback[method](...args);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
log: (...args) => write("log", args),
|
|
69
|
+
warn: (...args) => write("warn", args),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
export async function withProgress(label, fn, options) {
|
|
53
74
|
const spinner = createSpinner(label, options);
|
|
54
75
|
try {
|
package/src/skills.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// - Codex: `plugin marketplace add|upgrade`, `plugin add` (install + refresh)
|
|
20
20
|
// so we keep a per-client command table rather than assuming one shape.
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
23
|
|
|
24
24
|
import { resolveDefaultGateway, normalizeGatewayUrl } from "./config.js";
|
|
25
25
|
import { nativeCommandInvocation } from "./nativeProcess.js";
|
|
@@ -159,44 +159,95 @@ export function buildSkillCommands({ client, marketplaceSourceUrl: sourceUrl, ma
|
|
|
159
159
|
return commands;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
const SKILL_COMMAND_TIMEOUT_MS = 120_000;
|
|
163
|
+
const SKILL_COMMAND_OUTPUT_LIMIT = 10 * 1024 * 1024;
|
|
164
|
+
|
|
165
|
+
/** Default async command runner: keeps progress timers responsive while plugins sync. */
|
|
166
|
+
export function runSkillCommand(bin, args, env, {
|
|
167
|
+
spawnImpl = spawn,
|
|
168
|
+
timeoutMs = SKILL_COMMAND_TIMEOUT_MS,
|
|
169
|
+
} = {}) {
|
|
164
170
|
const environment = { ...process.env, ...env };
|
|
165
171
|
let invocation;
|
|
166
172
|
try {
|
|
167
173
|
invocation = nativeCommandInvocation(bin, args, environment);
|
|
168
174
|
} catch (error) {
|
|
169
|
-
return {
|
|
175
|
+
return Promise.resolve({
|
|
170
176
|
ok: false,
|
|
171
177
|
missing: false,
|
|
172
178
|
status: null,
|
|
173
179
|
stdout: "",
|
|
174
180
|
stderr: error?.message || String(error),
|
|
175
|
-
};
|
|
181
|
+
});
|
|
176
182
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
let child;
|
|
186
|
+
try {
|
|
187
|
+
child = spawnImpl(invocation.command, invocation.args, {
|
|
188
|
+
env: environment,
|
|
189
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
190
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
resolve({
|
|
194
|
+
ok: false,
|
|
195
|
+
missing: error?.code === "ENOENT",
|
|
196
|
+
status: null,
|
|
197
|
+
stdout: "",
|
|
198
|
+
stderr: error?.message || String(error),
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let stdout = "";
|
|
204
|
+
let stderr = "";
|
|
205
|
+
let timedOut = false;
|
|
206
|
+
let settled = false;
|
|
207
|
+
const append = (current, chunk) => (
|
|
208
|
+
`${current}${String(chunk)}`.slice(-SKILL_COMMAND_OUTPUT_LIMIT)
|
|
209
|
+
);
|
|
210
|
+
child.stdout?.setEncoding?.("utf8");
|
|
211
|
+
child.stderr?.setEncoding?.("utf8");
|
|
212
|
+
child.stdout?.on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
213
|
+
child.stderr?.on("data", (chunk) => { stderr = append(stderr, chunk); });
|
|
214
|
+
|
|
215
|
+
const finish = (result) => {
|
|
216
|
+
if (settled) return;
|
|
217
|
+
settled = true;
|
|
218
|
+
clearTimeout(timeout);
|
|
219
|
+
resolve(result);
|
|
220
|
+
};
|
|
221
|
+
const timeout = setTimeout(() => {
|
|
222
|
+
timedOut = true;
|
|
223
|
+
try {
|
|
224
|
+
child.kill();
|
|
225
|
+
} catch {
|
|
226
|
+
// The process may have exited between the timer firing and kill().
|
|
227
|
+
}
|
|
228
|
+
finish({
|
|
229
|
+
ok: false,
|
|
230
|
+
missing: false,
|
|
231
|
+
status: null,
|
|
232
|
+
stdout,
|
|
233
|
+
stderr: `skill command timed out after ${timeoutMs}ms`,
|
|
234
|
+
});
|
|
235
|
+
}, timeoutMs);
|
|
236
|
+
child.on("error", (error) => finish({
|
|
186
237
|
ok: false,
|
|
187
|
-
missing:
|
|
238
|
+
missing: error?.code === "ENOENT",
|
|
188
239
|
status: null,
|
|
189
|
-
stdout
|
|
190
|
-
stderr:
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
};
|
|
240
|
+
stdout,
|
|
241
|
+
stderr: error?.message || String(error),
|
|
242
|
+
}));
|
|
243
|
+
child.on("close", (status) => finish({
|
|
244
|
+
ok: !timedOut && status === 0,
|
|
245
|
+
missing: false,
|
|
246
|
+
status,
|
|
247
|
+
stdout,
|
|
248
|
+
stderr: timedOut ? `skill command timed out after ${timeoutMs}ms` : stderr,
|
|
249
|
+
}));
|
|
250
|
+
});
|
|
200
251
|
}
|
|
201
252
|
|
|
202
253
|
function firstLine(text) {
|
|
@@ -237,7 +288,7 @@ export async function syncSkills({
|
|
|
237
288
|
env = {},
|
|
238
289
|
label,
|
|
239
290
|
fetchImpl = fetch,
|
|
240
|
-
run =
|
|
291
|
+
run = runSkillCommand,
|
|
241
292
|
logger = console,
|
|
242
293
|
} = {}) {
|
|
243
294
|
const spec = CLIENT_SPECS[client];
|
|
@@ -254,7 +305,7 @@ export async function syncSkills({
|
|
|
254
305
|
|
|
255
306
|
try {
|
|
256
307
|
// Confirm the binary and its `plugin` subcommand exist before doing anything.
|
|
257
|
-
const help = run(spec.bin, ["plugin", "--help"], env);
|
|
308
|
+
const help = await run(spec.bin, ["plugin", "--help"], env);
|
|
258
309
|
if (help.missing) {
|
|
259
310
|
logger.warn(`impel: skipping skill sync for ${displayLabel} — \`${spec.bin}\` CLI not found on PATH.`);
|
|
260
311
|
return { client, label: displayLabel, skipped: true, reason: "binary-missing" };
|
|
@@ -272,7 +323,7 @@ export async function syncSkills({
|
|
|
272
323
|
logger.log(`Skills: syncing ${SKILL_PLUGIN_NAME} for ${displayLabel}…`);
|
|
273
324
|
const failures = [];
|
|
274
325
|
for (const command of commands) {
|
|
275
|
-
const result = run(spec.bin, command.args, env);
|
|
326
|
+
const result = await run(spec.bin, command.args, env);
|
|
276
327
|
if (result.missing) {
|
|
277
328
|
failures.push({ phase: command.phase, reason: `\`${spec.bin}\` disappeared mid-sync` });
|
|
278
329
|
break;
|