impel-cli 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +695 -0
- package/bin/impel.js +7 -0
- package/package.json +29 -0
- package/src/apps.js +1263 -0
- package/src/args.js +36 -0
- package/src/claudeSetup.js +207 -0
- package/src/cli.js +184 -0
- package/src/cliProfiles.js +216 -0
- package/src/codexSecurity.js +184 -0
- package/src/codexSetup.js +224 -0
- package/src/commands/apps.js +538 -0
- package/src/commands/auth.js +89 -0
- package/src/commands/doctor.js +215 -0
- package/src/commands/experimental.js +60 -0
- package/src/commands/launch.js +161 -0
- package/src/commands/mcp.js +94 -0
- package/src/commands/setup.js +350 -0
- package/src/commands/skills.js +108 -0
- package/src/commands/status.js +95 -0
- package/src/commands/tasks.js +359 -0
- package/src/commands/tenant.js +77 -0
- package/src/commands/token.js +25 -0
- package/src/commands/update.js +217 -0
- package/src/commands/use.js +208 -0
- package/src/config.js +98 -0
- package/src/doctor.js +546 -0
- package/src/nativeProcess.js +192 -0
- package/src/prompt.js +51 -0
- package/src/selfInvocation.js +21 -0
- package/src/skills.js +314 -0
- package/src/tenants.js +194 -0
- package/src/updates.js +181 -0
- package/src/windowsApps.js +439 -0
- package/src/windowsSetup.js +122 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { environmentValue, nativeCommandInvocation } from "./nativeProcess.js";
|
|
7
|
+
import { normalizeTenantId } from "./tenants.js";
|
|
8
|
+
|
|
9
|
+
export const WINDOWS_CLAUDE_PACKAGE = "Anthropic.Claude";
|
|
10
|
+
export const WINDOWS_CHATGPT_PACKAGE = "9PLM9XGG6VKS";
|
|
11
|
+
export const WINDOWS_CHATGPT_PACKAGE_NAME = "OpenAI.Codex";
|
|
12
|
+
export const WINDOWS_CHATGPT_PUBLISHER_ID = "2p2nqsd0c76g0";
|
|
13
|
+
|
|
14
|
+
export function windowsClaudeUserData(environment = process.env, tenantId = null) {
|
|
15
|
+
const localAppData = environmentValue(environment, "LOCALAPPDATA")
|
|
16
|
+
|| path.win32.join(environmentValue(environment, "USERPROFILE") || os.homedir(), "AppData", "Local");
|
|
17
|
+
return path.win32.join(
|
|
18
|
+
localAppData,
|
|
19
|
+
"Claude-3p",
|
|
20
|
+
"Impel",
|
|
21
|
+
normalizeTenantId(tenantId || "default"),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isFile(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.statSync(filePath).isFile();
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function versionedSquirrelCandidates(root, readDirectory = fs.readdirSync) {
|
|
34
|
+
try {
|
|
35
|
+
return readDirectory(root, { withFileTypes: true })
|
|
36
|
+
.filter((entry) => entry.isDirectory() && entry.name.toLowerCase().startsWith("app-"))
|
|
37
|
+
.sort((left, right) => right.name.localeCompare(left.name, undefined, { numeric: true }))
|
|
38
|
+
.flatMap((entry) => [
|
|
39
|
+
path.win32.join(root, entry.name, "Claude.exe"),
|
|
40
|
+
path.win32.join(root, entry.name, "claude.exe"),
|
|
41
|
+
]);
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function installedMsixClaude(environment, run = spawnSync) {
|
|
48
|
+
const script = [
|
|
49
|
+
"$package = Get-AppxPackage -Name Claude | Sort-Object Version -Descending | Select-Object -First 1",
|
|
50
|
+
"if ($package) { Join-Path $package.InstallLocation 'app\\Claude.exe' }",
|
|
51
|
+
].join("; ");
|
|
52
|
+
try {
|
|
53
|
+
const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
env: environment,
|
|
56
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
});
|
|
59
|
+
return result?.status === 0 ? String(result.stdout || "").trim() || null : null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function installedMsixChatGPT(environment, run = spawnSync) {
|
|
66
|
+
const script = [
|
|
67
|
+
`$package = Get-AppxPackage -Name '${WINDOWS_CHATGPT_PACKAGE_NAME}' -ErrorAction SilentlyContinue`,
|
|
68
|
+
`$package = $package | Where-Object PublisherId -eq '${WINDOWS_CHATGPT_PUBLISHER_ID}' | Sort-Object Version -Descending | Select-Object -First 1`,
|
|
69
|
+
"if ($package) {",
|
|
70
|
+
" $manifest = Get-AppxPackageManifest -Package $package",
|
|
71
|
+
" $relative = $manifest.Package.Applications.Application | Where-Object Id -eq 'App' | Select-Object -First 1 -ExpandProperty Executable",
|
|
72
|
+
" if ($relative) { Join-Path $package.InstallLocation $relative }",
|
|
73
|
+
"}",
|
|
74
|
+
].join("; ");
|
|
75
|
+
try {
|
|
76
|
+
const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
env: environment,
|
|
79
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
});
|
|
82
|
+
return result?.status === 0 ? String(result.stdout || "").trim() || null : null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Candidate paths for Anthropic's signed per-user Windows desktop install. */
|
|
89
|
+
export function windowsClaudeAppCandidates(environment = process.env, dependencies = {}) {
|
|
90
|
+
const io = { readDirectory: fs.readdirSync, ...dependencies };
|
|
91
|
+
const overridden = environmentValue(environment, "IMPEL_CLAUDE_APP_BIN");
|
|
92
|
+
const localAppData = environmentValue(environment, "LOCALAPPDATA")
|
|
93
|
+
|| path.win32.join(environmentValue(environment, "USERPROFILE") || os.homedir(), "AppData", "Local");
|
|
94
|
+
const programFiles = environmentValue(environment, "ProgramFiles");
|
|
95
|
+
const squirrelRoot = path.win32.join(localAppData, "AnthropicClaude");
|
|
96
|
+
const candidates = [
|
|
97
|
+
overridden,
|
|
98
|
+
path.win32.join(squirrelRoot, "Claude.exe"),
|
|
99
|
+
path.win32.join(squirrelRoot, "claude.exe"),
|
|
100
|
+
...versionedSquirrelCandidates(squirrelRoot, io.readDirectory),
|
|
101
|
+
path.win32.join(localAppData, "Programs", "Claude", "Claude.exe"),
|
|
102
|
+
path.win32.join(localAppData, "Programs", "AnthropicClaude", "Claude.exe"),
|
|
103
|
+
path.win32.join(localAppData, "Claude", "Claude.exe"),
|
|
104
|
+
programFiles ? path.win32.join(programFiles, "Claude", "Claude.exe") : null,
|
|
105
|
+
].filter(Boolean);
|
|
106
|
+
return [...new Set(candidates.map((candidate) => path.win32.normalize(candidate)))];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function findWindowsClaudeApp(environment = process.env, dependencies = {}) {
|
|
110
|
+
const exists = dependencies.isFile || isFile;
|
|
111
|
+
const unpackaged = windowsClaudeAppCandidates(environment, dependencies).find((candidate) => exists(candidate));
|
|
112
|
+
if (unpackaged) return unpackaged;
|
|
113
|
+
const msix = (dependencies.queryMsix || installedMsixClaude)(environment, dependencies.run);
|
|
114
|
+
return msix && exists(msix) ? path.win32.normalize(msix) : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Candidate paths for an unpackaged OpenAI ChatGPT/Codex desktop install. */
|
|
118
|
+
export function windowsChatGPTAppCandidates(environment = process.env) {
|
|
119
|
+
const overridden = environmentValue(environment, "IMPEL_CHATGPT_APP_BIN")
|
|
120
|
+
|| environmentValue(environment, "IMPEL_CODEX_APP_BIN");
|
|
121
|
+
const localAppData = environmentValue(environment, "LOCALAPPDATA")
|
|
122
|
+
|| path.win32.join(environmentValue(environment, "USERPROFILE") || os.homedir(), "AppData", "Local");
|
|
123
|
+
const programFiles = environmentValue(environment, "ProgramFiles");
|
|
124
|
+
const candidates = [
|
|
125
|
+
overridden,
|
|
126
|
+
path.win32.join(localAppData, "Programs", "OpenAI", "ChatGPT", "ChatGPT.exe"),
|
|
127
|
+
path.win32.join(localAppData, "Programs", "OpenAI", "Codex", "ChatGPT.exe"),
|
|
128
|
+
path.win32.join(localAppData, "Programs", "OpenAI", "Codex", "Codex.exe"),
|
|
129
|
+
path.win32.join(localAppData, "Programs", "ChatGPT", "ChatGPT.exe"),
|
|
130
|
+
path.win32.join(localAppData, "Programs", "Codex", "Codex.exe"),
|
|
131
|
+
programFiles ? path.win32.join(programFiles, "OpenAI", "ChatGPT", "ChatGPT.exe") : null,
|
|
132
|
+
programFiles ? path.win32.join(programFiles, "OpenAI", "Codex", "Codex.exe") : null,
|
|
133
|
+
].filter(Boolean);
|
|
134
|
+
return [...new Set(candidates.map((candidate) => path.win32.normalize(candidate)))];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function findWindowsChatGPTApp(environment = process.env, dependencies = {}) {
|
|
138
|
+
const exists = dependencies.isFile || isFile;
|
|
139
|
+
const unpackaged = windowsChatGPTAppCandidates(environment).find((candidate) => exists(candidate));
|
|
140
|
+
if (unpackaged) return unpackaged;
|
|
141
|
+
const msix = (dependencies.queryMsix || installedMsixChatGPT)(environment, dependencies.run);
|
|
142
|
+
return msix && exists(msix) ? path.win32.normalize(msix) : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function wingetInvocation(action, environment, { packageId, source = "winget", scope = "user" }) {
|
|
146
|
+
const verb = action === "update" ? "upgrade" : "install";
|
|
147
|
+
const scopeArgs = scope ? ["--scope", scope] : [];
|
|
148
|
+
return nativeCommandInvocation(
|
|
149
|
+
"winget",
|
|
150
|
+
[
|
|
151
|
+
verb,
|
|
152
|
+
"--id",
|
|
153
|
+
packageId,
|
|
154
|
+
"--exact",
|
|
155
|
+
"--source",
|
|
156
|
+
source,
|
|
157
|
+
...scopeArgs,
|
|
158
|
+
"--silent",
|
|
159
|
+
"--disable-interactivity",
|
|
160
|
+
"--accept-package-agreements",
|
|
161
|
+
"--accept-source-agreements",
|
|
162
|
+
],
|
|
163
|
+
environment,
|
|
164
|
+
"win32",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Install/update Anthropic's signed per-user app without modifying its files. */
|
|
169
|
+
export function ensureWindowsClaudeApp({ update = false } = {}, dependencies = {}) {
|
|
170
|
+
const io = {
|
|
171
|
+
environment: process.env,
|
|
172
|
+
find: findWindowsClaudeApp,
|
|
173
|
+
run: spawnSync,
|
|
174
|
+
...dependencies,
|
|
175
|
+
};
|
|
176
|
+
const before = io.find(io.environment);
|
|
177
|
+
if (before && !update) return { binary: before, action: "existing", result: null };
|
|
178
|
+
|
|
179
|
+
const invocation = wingetInvocation(update ? "update" : "install", io.environment, {
|
|
180
|
+
packageId: WINDOWS_CLAUDE_PACKAGE,
|
|
181
|
+
});
|
|
182
|
+
let result;
|
|
183
|
+
try {
|
|
184
|
+
result = io.run(invocation.command, invocation.args, {
|
|
185
|
+
env: io.environment,
|
|
186
|
+
stdio: "inherit",
|
|
187
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
result = { status: null, error };
|
|
191
|
+
}
|
|
192
|
+
const binary = io.find(io.environment) || before;
|
|
193
|
+
const succeeded = result?.status === 0 && !result?.error;
|
|
194
|
+
return {
|
|
195
|
+
binary,
|
|
196
|
+
action: succeeded ? (update ? "updated" : "installed") : (before ? "existing" : "install-failed"),
|
|
197
|
+
result,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Install/update OpenAI's official ChatGPT/Codex Store package. */
|
|
202
|
+
export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies = {}) {
|
|
203
|
+
const io = {
|
|
204
|
+
environment: process.env,
|
|
205
|
+
find: findWindowsChatGPTApp,
|
|
206
|
+
run: spawnSync,
|
|
207
|
+
...dependencies,
|
|
208
|
+
};
|
|
209
|
+
const before = io.find(io.environment);
|
|
210
|
+
if (before && !update) return { binary: before, action: "existing", result: null };
|
|
211
|
+
|
|
212
|
+
const invocation = wingetInvocation(update ? "update" : "install", io.environment, {
|
|
213
|
+
packageId: WINDOWS_CHATGPT_PACKAGE,
|
|
214
|
+
source: "msstore",
|
|
215
|
+
scope: null,
|
|
216
|
+
});
|
|
217
|
+
let result;
|
|
218
|
+
try {
|
|
219
|
+
result = io.run(invocation.command, invocation.args, {
|
|
220
|
+
env: io.environment,
|
|
221
|
+
stdio: "inherit",
|
|
222
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
result = { status: null, error };
|
|
226
|
+
}
|
|
227
|
+
const binary = io.find(io.environment) || before;
|
|
228
|
+
const succeeded = result?.status === 0 && !result?.error;
|
|
229
|
+
return {
|
|
230
|
+
binary,
|
|
231
|
+
action: succeeded ? (update ? "updated" : "installed") : (before ? "existing" : "install-failed"),
|
|
232
|
+
result,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function windowsChatGPTCacheRoot(appsRoot) {
|
|
237
|
+
return path.join(appsRoot, "windows-apps", "chatgpt");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function windowsManagedChatGPTRoot(appsRoot) {
|
|
241
|
+
return path.join(windowsChatGPTCacheRoot(appsRoot), "current");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function findManagedWindowsChatGPTApp(appsRoot, dependencies = {}) {
|
|
245
|
+
const exists = dependencies.isFile || isFile;
|
|
246
|
+
const root = windowsManagedChatGPTRoot(appsRoot);
|
|
247
|
+
for (const name of ["ChatGPT.exe", "Codex.exe"]) {
|
|
248
|
+
const candidate = path.join(root, name);
|
|
249
|
+
if (exists(candidate)) return candidate;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function windowsChatGPTStageIsCurrent(binary, appsRoot) {
|
|
255
|
+
if (!binary || !findManagedWindowsChatGPTApp(appsRoot)) return false;
|
|
256
|
+
try {
|
|
257
|
+
const record = JSON.parse(
|
|
258
|
+
fs.readFileSync(path.join(windowsChatGPTCacheRoot(appsRoot), "source.json"), "utf8"),
|
|
259
|
+
);
|
|
260
|
+
return path.win32.normalize(record.sourceBinary || "").toLowerCase()
|
|
261
|
+
=== path.win32.normalize(binary).toLowerCase();
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Copy the signed Electron payload out of WindowsApps into an Impel-owned
|
|
269
|
+
* location. Windows blocks direct execution from another package's protected
|
|
270
|
+
* install directory; the copied files remain byte-for-byte vendor files and
|
|
271
|
+
* can inherit the tenant-scoped environment supplied by the Impel launcher.
|
|
272
|
+
*/
|
|
273
|
+
export function stageWindowsChatGPTApp(binary, appsRoot, dependencies = {}) {
|
|
274
|
+
const io = {
|
|
275
|
+
copyDirectory: (source, destination) => fs.cpSync(source, destination, {
|
|
276
|
+
recursive: true,
|
|
277
|
+
preserveTimestamps: true,
|
|
278
|
+
}),
|
|
279
|
+
remove: (target) => fs.rmSync(target, { recursive: true, force: true }),
|
|
280
|
+
rename: fs.renameSync,
|
|
281
|
+
isFile,
|
|
282
|
+
...dependencies,
|
|
283
|
+
};
|
|
284
|
+
const sourceRoot = path.dirname(binary);
|
|
285
|
+
const executableName = path.basename(binary);
|
|
286
|
+
const sourceAsar = path.join(sourceRoot, "resources", "app.asar");
|
|
287
|
+
const sourceCodex = path.join(sourceRoot, "resources", "codex.exe");
|
|
288
|
+
if (!io.isFile(binary) || !io.isFile(sourceAsar) || !io.isFile(sourceCodex)) {
|
|
289
|
+
throw new Error(`unsupported ChatGPT/Codex Windows app layout at ${sourceRoot}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const cacheRoot = windowsChatGPTCacheRoot(appsRoot);
|
|
293
|
+
const destination = windowsManagedChatGPTRoot(appsRoot);
|
|
294
|
+
const staging = `${destination}.tmp-${process.pid}`;
|
|
295
|
+
const backup = `${destination}.previous-${process.pid}`;
|
|
296
|
+
io.remove(staging);
|
|
297
|
+
io.remove(backup);
|
|
298
|
+
io.copyDirectory(sourceRoot, staging);
|
|
299
|
+
const stagedBinary = path.join(staging, executableName);
|
|
300
|
+
if (!io.isFile(stagedBinary)
|
|
301
|
+
|| !io.isFile(path.join(staging, "resources", "app.asar"))
|
|
302
|
+
|| !io.isFile(path.join(staging, "resources", "codex.exe"))) {
|
|
303
|
+
io.remove(staging);
|
|
304
|
+
throw new Error("the staged ChatGPT/Codex Windows app is incomplete");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
308
|
+
if (fs.existsSync(destination)) io.rename(destination, backup);
|
|
309
|
+
try {
|
|
310
|
+
io.rename(staging, destination);
|
|
311
|
+
fs.writeFileSync(path.join(cacheRoot, "source.json"), `${JSON.stringify({
|
|
312
|
+
schemaVersion: 1,
|
|
313
|
+
sourceBinary: path.win32.normalize(binary),
|
|
314
|
+
stagedAt: new Date().toISOString(),
|
|
315
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
316
|
+
io.remove(backup);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (fs.existsSync(backup)) {
|
|
319
|
+
io.remove(destination);
|
|
320
|
+
io.rename(backup, destination);
|
|
321
|
+
}
|
|
322
|
+
io.remove(staging);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
return path.join(destination, executableName);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function removeManagedWindowsChatGPTApp(appsRoot) {
|
|
329
|
+
fs.rmSync(windowsChatGPTCacheRoot(appsRoot), { recursive: true, force: true });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Launch the signed vendor app with only its profile root redirected to Impel. */
|
|
333
|
+
export function launchWindowsClaudeApp(binary, userData, dependencies = {}) {
|
|
334
|
+
const io = {
|
|
335
|
+
environment: process.env,
|
|
336
|
+
makeDirectory: fs.mkdirSync,
|
|
337
|
+
spawnProcess: spawn,
|
|
338
|
+
...dependencies,
|
|
339
|
+
};
|
|
340
|
+
io.makeDirectory(userData, { recursive: true, mode: 0o700 });
|
|
341
|
+
const environment = { ...io.environment };
|
|
342
|
+
// These developer-only overrides require a signed CDP token and must never
|
|
343
|
+
// bleed from an unrelated native Claude session into the managed app.
|
|
344
|
+
const managedKeys = new Set(["CLAUDE_USER_DATA_DIR", "CLAUDE_CDP_AUTH", "CLAUDE_AI_URL"]);
|
|
345
|
+
for (const key of Object.keys(environment)) {
|
|
346
|
+
if (managedKeys.has(key.toUpperCase())) delete environment[key];
|
|
347
|
+
}
|
|
348
|
+
environment.CLAUDE_USER_DATA_DIR = userData;
|
|
349
|
+
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
const child = io.spawnProcess(binary, [], {
|
|
352
|
+
detached: true,
|
|
353
|
+
env: environment,
|
|
354
|
+
stdio: "ignore",
|
|
355
|
+
windowsHide: false,
|
|
356
|
+
});
|
|
357
|
+
child.once("error", reject);
|
|
358
|
+
child.once("spawn", () => {
|
|
359
|
+
child.unref();
|
|
360
|
+
resolve();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Launch the staged OpenAI app with isolated Codex and Electron state. */
|
|
366
|
+
export function launchWindowsChatGPTApp(binary, {
|
|
367
|
+
browserData,
|
|
368
|
+
codexHome,
|
|
369
|
+
credential,
|
|
370
|
+
gatewayUrl,
|
|
371
|
+
}, dependencies = {}) {
|
|
372
|
+
const io = {
|
|
373
|
+
environment: process.env,
|
|
374
|
+
makeDirectory: fs.mkdirSync,
|
|
375
|
+
run: spawnSync,
|
|
376
|
+
spawnProcess: spawn,
|
|
377
|
+
...dependencies,
|
|
378
|
+
};
|
|
379
|
+
io.makeDirectory(codexHome, { recursive: true, mode: 0o700 });
|
|
380
|
+
io.makeDirectory(browserData, { recursive: true, mode: 0o700 });
|
|
381
|
+
const environment = { ...io.environment };
|
|
382
|
+
const managedKeys = new Set([
|
|
383
|
+
"CODEX_ACCESS_TOKEN",
|
|
384
|
+
"CODEX_API_KEY",
|
|
385
|
+
"CODEX_AUTHAPI_BASE_URL",
|
|
386
|
+
"CODEX_ELECTRON_USER_DATA_PATH",
|
|
387
|
+
"CODEX_HOME",
|
|
388
|
+
"OPENAI_API_KEY",
|
|
389
|
+
"OPENAI_BASE_URL",
|
|
390
|
+
]);
|
|
391
|
+
for (const key of Object.keys(environment)) {
|
|
392
|
+
if (managedKeys.has(key.toUpperCase())) delete environment[key];
|
|
393
|
+
}
|
|
394
|
+
environment.CODEX_HOME = codexHome;
|
|
395
|
+
environment.CODEX_AUTHAPI_BASE_URL = `${gatewayUrl}/chatgpt_passthrough/backend-api`;
|
|
396
|
+
// Codex Desktop resolves this before acquiring Electron's single-instance
|
|
397
|
+
// lock. Keep --user-data-dir below as a compatibility fallback for builds
|
|
398
|
+
// that predate the dedicated desktop override.
|
|
399
|
+
environment.CODEX_ELECTRON_USER_DATA_PATH = browserData;
|
|
400
|
+
|
|
401
|
+
const appRoot = path.win32.dirname(binary);
|
|
402
|
+
const codex = path.win32.join(appRoot, "resources", "codex.exe");
|
|
403
|
+
io.run(codex, ["features", "disable", "shell_snapshot"], {
|
|
404
|
+
env: environment,
|
|
405
|
+
stdio: "ignore",
|
|
406
|
+
windowsHide: true,
|
|
407
|
+
});
|
|
408
|
+
const login = io.run(codex, ["login", "--with-access-token"], {
|
|
409
|
+
encoding: "utf8",
|
|
410
|
+
env: environment,
|
|
411
|
+
input: `at-${credential}\n`,
|
|
412
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
413
|
+
windowsHide: true,
|
|
414
|
+
});
|
|
415
|
+
if (login?.error || login?.status !== 0) {
|
|
416
|
+
const failure = login?.error?.message || (Number.isInteger(login?.status)
|
|
417
|
+
? `exit code ${login.status}`
|
|
418
|
+
: "no exit status");
|
|
419
|
+
throw new Error(`ChatGPT/Codex login helper failed (${failure})`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return new Promise((resolve, reject) => {
|
|
423
|
+
const child = io.spawnProcess(binary, [
|
|
424
|
+
`--user-data-dir=${browserData}`,
|
|
425
|
+
path.win32.join("resources", "app.asar"),
|
|
426
|
+
], {
|
|
427
|
+
cwd: appRoot,
|
|
428
|
+
detached: true,
|
|
429
|
+
env: environment,
|
|
430
|
+
stdio: "ignore",
|
|
431
|
+
windowsHide: false,
|
|
432
|
+
});
|
|
433
|
+
child.once("error", reject);
|
|
434
|
+
child.once("spawn", () => {
|
|
435
|
+
child.unref();
|
|
436
|
+
resolve();
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { ensureImpelClaudeProfile, ensureImpelCodexProfile } from "./cliProfiles.js";
|
|
4
|
+
import { findNativeBinary, nativeCommandInvocation } from "./nativeProcess.js";
|
|
5
|
+
import { syncSkillsSafe } from "./skills.js";
|
|
6
|
+
|
|
7
|
+
export const WINDOWS_CLI_PACKAGES = {
|
|
8
|
+
claude: "@anthropic-ai/claude-code@latest",
|
|
9
|
+
codex: "@openai/codex@latest",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function windowsCliInstallCommand(tools = Object.keys(WINDOWS_CLI_PACKAGES)) {
|
|
13
|
+
return `npm install --global ${tools.map((tool) => WINDOWS_CLI_PACKAGES[tool]).join(" ")}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function detectWindowsClis(find, environment) {
|
|
17
|
+
return {
|
|
18
|
+
claude: find("claude", environment, "win32"),
|
|
19
|
+
codex: find("codex", environment, "win32"),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function installMissingClis(missing, { environment, run }) {
|
|
24
|
+
const invocation = nativeCommandInvocation(
|
|
25
|
+
"npm",
|
|
26
|
+
["install", "--global", ...missing.map((tool) => WINDOWS_CLI_PACKAGES[tool])],
|
|
27
|
+
environment,
|
|
28
|
+
"win32",
|
|
29
|
+
);
|
|
30
|
+
return run(invocation.command, invocation.args, {
|
|
31
|
+
env: environment,
|
|
32
|
+
stdio: "inherit",
|
|
33
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function installationFailure(result, thrown = null) {
|
|
38
|
+
const error = thrown || result?.error;
|
|
39
|
+
if (error) {
|
|
40
|
+
return {
|
|
41
|
+
code: error.code || null,
|
|
42
|
+
message: error.message || String(error),
|
|
43
|
+
status: null,
|
|
44
|
+
signal: null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
code: null,
|
|
49
|
+
message: null,
|
|
50
|
+
status: Number.isInteger(result?.status) ? result.status : null,
|
|
51
|
+
signal: result?.signal || null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Make `impel setup` self-contained on Windows: install only missing official
|
|
57
|
+
* vendor CLIs, create the same tenant-isolated profiles used at launch time,
|
|
58
|
+
* and sync the shared Impel skill bundle into each available client.
|
|
59
|
+
*/
|
|
60
|
+
export async function prepareWindowsClis({ gatewayUrl, tenantId, skipInstall = false } = {}, dependencies = {}) {
|
|
61
|
+
const io = {
|
|
62
|
+
environment: process.env,
|
|
63
|
+
find: findNativeBinary,
|
|
64
|
+
run: spawnSync,
|
|
65
|
+
ensureClaudeProfile: ensureImpelClaudeProfile,
|
|
66
|
+
ensureCodexProfile: ensureImpelCodexProfile,
|
|
67
|
+
syncSkills: syncSkillsSafe,
|
|
68
|
+
...dependencies,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const before = detectWindowsClis(io.find, io.environment);
|
|
72
|
+
const missingBefore = Object.entries(before)
|
|
73
|
+
.filter(([, binary]) => !binary)
|
|
74
|
+
.map(([tool]) => tool);
|
|
75
|
+
let installAttempted = false;
|
|
76
|
+
let installSucceeded = null;
|
|
77
|
+
let installFailure = null;
|
|
78
|
+
|
|
79
|
+
if (missingBefore.length > 0 && !skipInstall) {
|
|
80
|
+
installAttempted = true;
|
|
81
|
+
try {
|
|
82
|
+
const result = installMissingClis(missingBefore, io);
|
|
83
|
+
installSucceeded = result?.status === 0 && !result?.error;
|
|
84
|
+
if (!installSucceeded) installFailure = installationFailure(result);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
installSucceeded = false;
|
|
87
|
+
installFailure = installationFailure(null, error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const binaries = installAttempted ? detectWindowsClis(io.find, io.environment) : before;
|
|
92
|
+
const claudeProfile = io.ensureClaudeProfile(gatewayUrl, tenantId);
|
|
93
|
+
const codexProfile = io.ensureCodexProfile(gatewayUrl, tenantId);
|
|
94
|
+
|
|
95
|
+
if (binaries.claude) {
|
|
96
|
+
await io.syncSkills({
|
|
97
|
+
client: "claude",
|
|
98
|
+
gatewayUrl,
|
|
99
|
+
env: { CLAUDE_CONFIG_DIR: claudeProfile.configDir },
|
|
100
|
+
label: "Impel isolated Claude (impel claude)",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (binaries.codex) {
|
|
104
|
+
await io.syncSkills({
|
|
105
|
+
client: "codex",
|
|
106
|
+
gatewayUrl,
|
|
107
|
+
env: { CODEX_HOME: codexProfile.codexHome },
|
|
108
|
+
label: "Impel isolated Codex (impel codex)",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
binaries,
|
|
114
|
+
missingBefore,
|
|
115
|
+
missingAfter: Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool),
|
|
116
|
+
installAttempted,
|
|
117
|
+
installSucceeded,
|
|
118
|
+
installFailure,
|
|
119
|
+
installCommand: windowsCliInstallCommand(missingBefore),
|
|
120
|
+
profiles: { claude: claudeProfile.configDir, codex: codexProfile.codexHome },
|
|
121
|
+
};
|
|
122
|
+
}
|