impel-cli 0.17.12 → 0.17.13
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/commands/apps.js +32 -7
- package/src/commands/skills.js +12 -2
- package/src/nativeProcess.js +9 -0
- package/src/skills.js +64 -3
package/package.json
CHANGED
package/src/commands/apps.js
CHANGED
|
@@ -43,7 +43,7 @@ import { syncAgentProfilesSafe } from "../agents.js";
|
|
|
43
43
|
import { secureManagedCodexHome } from "../codexSecurity.js";
|
|
44
44
|
import { impelCliInvocation } from "../selfInvocation.js";
|
|
45
45
|
import { createProgressLogger, withProgress } from "../progress.js";
|
|
46
|
-
import { findNativeBinary } from "../nativeProcess.js";
|
|
46
|
+
import { environmentValue, findNativeBinary } from "../nativeProcess.js";
|
|
47
47
|
import {
|
|
48
48
|
ensureWindowsClaudeApp,
|
|
49
49
|
ensureWindowsChatGPTApp,
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
stageWindowsChatGPTApp,
|
|
57
57
|
windowsChatGPTStageIsCurrent,
|
|
58
58
|
windowsClaudeUserData,
|
|
59
|
+
windowsManagedChatGPTRoot,
|
|
59
60
|
} from "../windowsApps.js";
|
|
60
61
|
|
|
61
62
|
const CLAUDE_KEYCHAIN_NOTICE = "Claude Keychain: enter your Mac login password and choose Always Allow on the first prompt for this tenant; Allow is temporary.";
|
|
@@ -112,11 +113,26 @@ function appTargetLabel(displayName) {
|
|
|
112
113
|
// Each isolated desktop app maps to a client CLI + the env override that points
|
|
113
114
|
// that CLI at the app's private profile, so skill syncing lands in the app's
|
|
114
115
|
// installation rather than a global one.
|
|
115
|
-
function appSkillTarget(target, paths
|
|
116
|
+
function appSkillTarget(target, paths, {
|
|
117
|
+
platform = process.platform,
|
|
118
|
+
existsSync = fs.existsSync,
|
|
119
|
+
environment = process.env,
|
|
120
|
+
} = {}) {
|
|
116
121
|
if (target === "claude") {
|
|
117
122
|
return { client: "claude", env: { CLAUDE_CONFIG_DIR: paths.claude.userData }, label: appTargetLabel(paths.claude.displayName) };
|
|
118
123
|
}
|
|
119
|
-
|
|
124
|
+
const env = { CODEX_HOME: paths.chatgpt.codexHome };
|
|
125
|
+
// The staged Windows app ships the exact codex.exe the app itself runs, so
|
|
126
|
+
// plugin sync uses it instead of requiring a separately installed Codex CLI
|
|
127
|
+
// (a box without one would otherwise get no app skills at all) and the
|
|
128
|
+
// plugin state is always written by the same codex version that reads it.
|
|
129
|
+
// An operator's explicit IMPEL_CODEX_BIN stays authoritative — it is an
|
|
130
|
+
// override everywhere else the CLI resolves codex.
|
|
131
|
+
if (platform === "win32" && !environmentValue(environment, "IMPEL_CODEX_BIN")) {
|
|
132
|
+
const embedded = path.join(windowsManagedChatGPTRoot(paths.root), "resources", "codex.exe");
|
|
133
|
+
if (existsSync(embedded)) env.IMPEL_CODEX_BIN = embedded;
|
|
134
|
+
}
|
|
135
|
+
return { client: "codex", env, label: appTargetLabel(paths.chatgpt.displayName) };
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
function appAgentProfile(target, paths) {
|
|
@@ -335,6 +351,7 @@ export async function reconcileWindowsTenantApps({
|
|
|
335
351
|
installFiles: installManagedAppFiles,
|
|
336
352
|
syncSkills: syncSkillsSafe,
|
|
337
353
|
syncAgents: syncAgentProfilesSafe,
|
|
354
|
+
secureCodexHome: secureManagedCodexHome,
|
|
338
355
|
existsSync: fs.existsSync,
|
|
339
356
|
findBinary: findNativeBinary,
|
|
340
357
|
log: (message) => console.log(message),
|
|
@@ -389,12 +406,20 @@ export async function reconcileWindowsTenantApps({
|
|
|
389
406
|
});
|
|
390
407
|
const agentTargets = [];
|
|
391
408
|
for (const target of actionTargets) {
|
|
392
|
-
const { client, env, label } = appSkillTarget(target, paths
|
|
393
|
-
|
|
409
|
+
const { client, env, label } = appSkillTarget(target, paths, {
|
|
410
|
+
platform: "win32",
|
|
411
|
+
existsSync: io.existsSync,
|
|
412
|
+
environment,
|
|
413
|
+
});
|
|
414
|
+
// Merge the sync env so an app-embedded binary (IMPEL_CODEX_BIN) satisfies
|
|
415
|
+
// the availability check even when no standalone CLI is installed.
|
|
416
|
+
if (io.findBinary(client, { ...environment, ...env }, "win32")) {
|
|
394
417
|
await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label });
|
|
395
418
|
agentTargets.push(target);
|
|
419
|
+
} else {
|
|
420
|
+
io.log(`Skipping skill/agent sync for ${label} — no ${client === "claude" ? "Claude Code" : "Codex"} binary is available to run plugin commands.`);
|
|
396
421
|
}
|
|
397
|
-
if (target === "chatgpt")
|
|
422
|
+
if (target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
|
|
398
423
|
}
|
|
399
424
|
if (agentTargets.length) {
|
|
400
425
|
await io.syncAgents({
|
|
@@ -719,7 +744,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
719
744
|
for (const target of configuredTargets) {
|
|
720
745
|
const profile = target === "claude" ? tenantPaths.claude.userData : tenantPaths.chatgpt.root;
|
|
721
746
|
console.log(`${verb} Impel ${target === "claude" ? "Claude" : "ChatGPT"} profile at ${profile}`);
|
|
722
|
-
const { client, env, label } = appSkillTarget(target, tenantPaths);
|
|
747
|
+
const { client, env, label } = appSkillTarget(target, tenantPaths, { platform: "win32" });
|
|
723
748
|
if (!flags["skip-skills"]) {
|
|
724
749
|
await withProgress(`Syncing skills for ${label}`, (spinner) => (
|
|
725
750
|
io.syncSkills({
|
package/src/commands/skills.js
CHANGED
|
@@ -13,9 +13,10 @@ import { loadConfig } from "../config.js";
|
|
|
13
13
|
import { CODEX_HOME } from "../codexSetup.js";
|
|
14
14
|
import { tenantCliProfilePaths } from "../cliProfiles.js";
|
|
15
15
|
import { CLAUDE_CONFIG_ID, appPaths } from "../apps.js";
|
|
16
|
+
import { environmentValue } from "../nativeProcess.js";
|
|
16
17
|
import { resolveSkillsGateway, syncSkillsSafe } from "../skills.js";
|
|
17
18
|
import { ensureTenantSelection } from "../tenants.js";
|
|
18
|
-
import { windowsClaudeUserData } from "../windowsApps.js";
|
|
19
|
+
import { windowsClaudeUserData, windowsManagedChatGPTRoot } from "../windowsApps.js";
|
|
19
20
|
import { createProgressLogger, mapWithConcurrency, withProgress } from "../progress.js";
|
|
20
21
|
|
|
21
22
|
const VALID_TARGETS = ["claude", "codex", "all"];
|
|
@@ -74,7 +75,16 @@ export function managedSkillProfiles(
|
|
|
74
75
|
? existsSync(path.join(paths.chatgpt.codexHome, "config.toml"))
|
|
75
76
|
: existsSync(paths.chatgpt.launcher);
|
|
76
77
|
if (includeApps && appInstalled) {
|
|
77
|
-
|
|
78
|
+
const env = { CODEX_HOME: paths.chatgpt.codexHome };
|
|
79
|
+
// Mirror appSkillTarget (commands/apps.js): the staged Windows app
|
|
80
|
+
// bundles the codex.exe that reads this profile, so `impel skills sync`
|
|
81
|
+
// works on boxes without a standalone Codex CLI too. An operator's
|
|
82
|
+
// explicit IMPEL_CODEX_BIN stays authoritative.
|
|
83
|
+
if (platform === "win32" && !environmentValue(environment, "IMPEL_CODEX_BIN")) {
|
|
84
|
+
const embedded = path.join(windowsManagedChatGPTRoot(paths.root), "resources", "codex.exe");
|
|
85
|
+
if (existsSync(embedded)) env.IMPEL_CODEX_BIN = embedded;
|
|
86
|
+
}
|
|
87
|
+
profiles.push({ label: "Impel ChatGPT app", env });
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
|
package/src/nativeProcess.js
CHANGED
|
@@ -118,6 +118,15 @@ function commonCandidates(tool, environment, platform) {
|
|
|
118
118
|
if (tool === "powershell" && systemRoot) {
|
|
119
119
|
locations.push([paths.join(systemRoot, "System32", "WindowsPowerShell", "v1.0"), "powershell"]);
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
// Git for Windows standard install roots; `cmd` holds the PATH-safe
|
|
123
|
+
// git.exe. Codex shells out to git for marketplace clones, so skill
|
|
124
|
+
// syncing must find git even when the parent shell's PATH predates the
|
|
125
|
+
// Git installation.
|
|
126
|
+
if (tool === "git") {
|
|
127
|
+
if (programFiles) locations.push([paths.join(programFiles, "Git", "cmd"), "git"]);
|
|
128
|
+
if (localAppData) locations.push([paths.join(localAppData, "Programs", "Git", "cmd"), "git"]);
|
|
129
|
+
}
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
return locations.flatMap(([directory, name]) => binaryCandidates(directory, name, environment, platform));
|
package/src/skills.js
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
// so we keep a per-client command table rather than assuming one shape.
|
|
21
21
|
|
|
22
22
|
import { spawn } from "node:child_process";
|
|
23
|
+
import path from "node:path";
|
|
23
24
|
|
|
24
25
|
import { resolveDefaultGateway, normalizeGatewayUrl } from "./config.js";
|
|
25
|
-
import { nativeCommandInvocation } from "./nativeProcess.js";
|
|
26
|
+
import { findNativeBinary, nativeCommandInvocation } from "./nativeProcess.js";
|
|
26
27
|
|
|
27
28
|
/** The bundled plugin the Bifrost registry publishes; contains every served skill. */
|
|
28
29
|
export const SKILL_PLUGIN_NAME = "bifrost-all-skills";
|
|
@@ -183,18 +184,56 @@ export function buildSkillCommands({ client, marketplaceSourceUrl: sourceUrl, ma
|
|
|
183
184
|
return commands;
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Codex registers marketplaces by shelling out to `git clone` (Claude fetches
|
|
189
|
+
* its manifest over plain HTTP), so a Windows machine without git breaks
|
|
190
|
+
* exactly the Codex half of skill syncing. Resolve git up front: when a
|
|
191
|
+
* standard Git for Windows install exists but the inherited PATH cannot see
|
|
192
|
+
* it, prepend its directory for the child commands; when git is genuinely
|
|
193
|
+
* absent, report that so the caller can skip with an actionable message
|
|
194
|
+
* instead of surfacing codex's raw "failed to run git clone" error.
|
|
195
|
+
*/
|
|
196
|
+
export function withCodexGitEnvironment(env = {}, {
|
|
197
|
+
platform = process.platform,
|
|
198
|
+
baseEnvironment = process.env,
|
|
199
|
+
find = findNativeBinary,
|
|
200
|
+
} = {}) {
|
|
201
|
+
if (platform !== "win32") return { env, gitAvailable: true };
|
|
202
|
+
const merged = { ...baseEnvironment, ...env };
|
|
203
|
+
const git = find("git", merged, platform);
|
|
204
|
+
if (!git) return { env, gitAvailable: false };
|
|
205
|
+
const gitDirectory = path.win32.dirname(git);
|
|
206
|
+
const normalize = (entry) => entry.trim().replace(/[\\/]+$/u, "").toLowerCase();
|
|
207
|
+
const pathKey = Object.keys(merged).find((key) => key.toLowerCase() === "path") || "Path";
|
|
208
|
+
const currentPath = String(merged[pathKey] || "");
|
|
209
|
+
if (currentPath.split(";").some((entry) => normalize(entry) === normalize(gitDirectory))) {
|
|
210
|
+
return { env, gitAvailable: true };
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
env: { ...env, [pathKey]: currentPath ? `${gitDirectory};${currentPath}` : gitDirectory },
|
|
214
|
+
gitAvailable: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
186
218
|
const SKILL_COMMAND_TIMEOUT_MS = 120_000;
|
|
187
219
|
const SKILL_COMMAND_OUTPUT_LIMIT = 10 * 1024 * 1024;
|
|
188
220
|
|
|
221
|
+
// cmd.exe reports a nonexistent command with exit code 9009 (and this stderr
|
|
222
|
+
// line on English installs); the spawn itself succeeds, so without this check
|
|
223
|
+
// a missing CLI is misreported as "has no plugin command".
|
|
224
|
+
const WINDOWS_COMMAND_NOT_FOUND_STATUS = 9009;
|
|
225
|
+
const WINDOWS_COMMAND_NOT_FOUND_RE = /is not recognized as an internal or external command/iu;
|
|
226
|
+
|
|
189
227
|
/** Default async command runner: keeps progress timers responsive while plugins sync. */
|
|
190
228
|
export function runSkillCommand(bin, args, env, {
|
|
191
229
|
spawnImpl = spawn,
|
|
192
230
|
timeoutMs = SKILL_COMMAND_TIMEOUT_MS,
|
|
231
|
+
platform = process.platform,
|
|
193
232
|
} = {}) {
|
|
194
233
|
const environment = { ...process.env, ...env };
|
|
195
234
|
let invocation;
|
|
196
235
|
try {
|
|
197
|
-
invocation = nativeCommandInvocation(bin, args, environment);
|
|
236
|
+
invocation = nativeCommandInvocation(bin, args, environment, platform);
|
|
198
237
|
} catch (error) {
|
|
199
238
|
return Promise.resolve({
|
|
200
239
|
ok: false,
|
|
@@ -269,7 +308,14 @@ export function runSkillCommand(bin, args, env, {
|
|
|
269
308
|
}));
|
|
270
309
|
child.on("close", (status) => finish({
|
|
271
310
|
ok: !timedOut && status === 0,
|
|
272
|
-
|
|
311
|
+
// Only a FAILED cmd.exe-wrapped launch can mean "the CLI itself was not
|
|
312
|
+
// found"; a successful run owns its own stderr, and a directly spawned
|
|
313
|
+
// binary owns its exit codes, where the same phrasing could describe
|
|
314
|
+
// some inner command instead.
|
|
315
|
+
missing: status !== 0
|
|
316
|
+
&& invocation.windowsVerbatimArguments
|
|
317
|
+
&& (status === WINDOWS_COMMAND_NOT_FOUND_STATUS
|
|
318
|
+
|| WINDOWS_COMMAND_NOT_FOUND_RE.test(stderr)),
|
|
273
319
|
status,
|
|
274
320
|
stdout,
|
|
275
321
|
stderr: timedOut ? `skill command timed out after ${timeoutMs}ms` : stderr,
|
|
@@ -357,6 +403,8 @@ export async function syncSkills({
|
|
|
357
403
|
fetchImpl = fetch,
|
|
358
404
|
run = runSkillCommand,
|
|
359
405
|
logger = console,
|
|
406
|
+
platform = process.platform,
|
|
407
|
+
findGit = findNativeBinary,
|
|
360
408
|
} = {}) {
|
|
361
409
|
const spec = CLIENT_SPECS[client];
|
|
362
410
|
if (!spec) {
|
|
@@ -382,6 +430,19 @@ export async function syncSkills({
|
|
|
382
430
|
return { client, label: displayLabel, skipped: true, reason: "no-plugin-subcommand" };
|
|
383
431
|
}
|
|
384
432
|
|
|
433
|
+
if (client === "codex") {
|
|
434
|
+
const gitReady = withCodexGitEnvironment(env, { platform, find: findGit });
|
|
435
|
+
if (!gitReady.gitAvailable) {
|
|
436
|
+
logger.warn(
|
|
437
|
+
`impel: skipping skill sync for ${displayLabel} — Codex fetches its skills marketplace with \`git\`, `
|
|
438
|
+
+ "which is not installed. Install Git for Windows (https://git-scm.com/download/win), "
|
|
439
|
+
+ "then re-run `impel update`."
|
|
440
|
+
);
|
|
441
|
+
return { client, label: displayLabel, skipped: true, reason: "git-missing" };
|
|
442
|
+
}
|
|
443
|
+
env = gitReady.env;
|
|
444
|
+
}
|
|
445
|
+
|
|
385
446
|
const manifestUrl = marketplaceUrl(gatewayUrl, client);
|
|
386
447
|
const sourceUrl = marketplaceSourceUrl(gatewayUrl, client);
|
|
387
448
|
let marketplaceName = await fetchMarketplaceName(manifestUrl, fetchImpl);
|