impel-cli 0.16.4 → 0.17.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 +224 -945
- package/package.json +2 -2
- package/src/apps.js +8 -2
- package/src/cli.js +38 -41
- package/src/cliProfiles.js +5 -1
- package/src/commands/apps.js +215 -0
- package/src/commands/converge.js +174 -0
- package/src/commands/sessions.js +114 -0
- package/src/commands/setup.js +312 -428
- package/src/commands/status.js +133 -83
- package/src/commands/tasks.js +1 -1
- package/src/commands/tenant.js +3 -5
- package/src/commands/update.js +39 -85
- package/src/commands/use.js +67 -238
- package/src/config.js +8 -3
- package/src/installRecovery/engine.js +9 -27
- package/src/installRecovery/redact.js +4 -0
- package/src/installRecovery/tools.js +1 -1
- package/src/provisioning.js +354 -0
- package/src/sessionCollector.js +996 -0
- package/src/sessionHooks.js +242 -0
- package/src/shellEntries.js +186 -0
- package/src/skills.js +1 -1
- package/src/tenants.js +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { impelCliInvocation } from "./selfInvocation.js";
|
|
6
|
+
|
|
7
|
+
const MANAGED_MARKER = "--impel-managed-session-hook-v1";
|
|
8
|
+
const CODEX_TRUST_START = "# >>> impel managed session hook trust >>>";
|
|
9
|
+
const CODEX_TRUST_END = "# <<< impel managed session hook trust <<<";
|
|
10
|
+
|
|
11
|
+
export const CLAUDE_SESSION_EVENTS = Object.freeze([
|
|
12
|
+
"SessionStart",
|
|
13
|
+
"UserPromptSubmit",
|
|
14
|
+
"PostToolUse",
|
|
15
|
+
"PostToolUseFailure",
|
|
16
|
+
"SubagentStart",
|
|
17
|
+
"SubagentStop",
|
|
18
|
+
"PreCompact",
|
|
19
|
+
"PostCompact",
|
|
20
|
+
"Stop",
|
|
21
|
+
"StopFailure",
|
|
22
|
+
"SessionEnd",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const CODEX_SESSION_EVENTS = Object.freeze([
|
|
26
|
+
"SessionStart",
|
|
27
|
+
"UserPromptSubmit",
|
|
28
|
+
"PostToolUse",
|
|
29
|
+
"SubagentStart",
|
|
30
|
+
"SubagentStop",
|
|
31
|
+
"PreCompact",
|
|
32
|
+
"PostCompact",
|
|
33
|
+
"Stop",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function ensurePrivateDirectory(directory) {
|
|
37
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
38
|
+
try {
|
|
39
|
+
fs.chmodSync(directory, 0o700);
|
|
40
|
+
} catch {
|
|
41
|
+
// Best effort on platforms where chmod is unavailable.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readJsonObject(filePath) {
|
|
46
|
+
if (!fs.existsSync(filePath)) return {};
|
|
47
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
48
|
+
if (!raw) return {};
|
|
49
|
+
try {
|
|
50
|
+
const value = JSON.parse(raw);
|
|
51
|
+
if (!value || Array.isArray(value) || typeof value !== "object") throw new Error();
|
|
52
|
+
return value;
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error(`${filePath} exists but isn't a valid JSON object. Fix or remove it, then re-run.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writePrivateJson(filePath, value) {
|
|
59
|
+
ensurePrivateDirectory(path.dirname(filePath));
|
|
60
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
63
|
+
fs.renameSync(temporaryPath, filePath);
|
|
64
|
+
try {
|
|
65
|
+
fs.chmodSync(filePath, 0o600);
|
|
66
|
+
} catch {
|
|
67
|
+
// Best effort on platforms where chmod is unavailable.
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
try {
|
|
71
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
72
|
+
} catch {
|
|
73
|
+
// The successful rename already removed the temporary file.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function managedInvocation(provider, surface, tenantId) {
|
|
79
|
+
return impelCliInvocation([
|
|
80
|
+
"sessions",
|
|
81
|
+
"hook",
|
|
82
|
+
"--provider",
|
|
83
|
+
provider,
|
|
84
|
+
"--surface",
|
|
85
|
+
surface,
|
|
86
|
+
"--tenant",
|
|
87
|
+
tenantId,
|
|
88
|
+
MANAGED_MARKER,
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validTenantId(tenantId) {
|
|
93
|
+
return typeof tenantId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(tenantId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handlerIsManaged(handler) {
|
|
97
|
+
if (!handler || typeof handler !== "object") return false;
|
|
98
|
+
if (Array.isArray(handler.args)) return handler.args.includes(MANAGED_MARKER);
|
|
99
|
+
return typeof handler.command === "string" && handler.command.includes(MANAGED_MARKER);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mergeEventHook(existing, event, handler) {
|
|
103
|
+
const groups = Array.isArray(existing?.[event]) ? existing[event] : [];
|
|
104
|
+
const preserved = [];
|
|
105
|
+
for (const group of groups) {
|
|
106
|
+
if (!group || typeof group !== "object") {
|
|
107
|
+
preserved.push(group);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const hooks = Array.isArray(group.hooks)
|
|
111
|
+
? group.hooks.filter((candidate) => !handlerIsManaged(candidate))
|
|
112
|
+
: [];
|
|
113
|
+
if (hooks.length > 0 || !Array.isArray(group.hooks)) preserved.push({ ...group, hooks });
|
|
114
|
+
}
|
|
115
|
+
preserved.push({ hooks: [handler] });
|
|
116
|
+
return preserved;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function ensureClaudeSessionHooks(configDir, tenantId, surface = "claude_cli") {
|
|
120
|
+
const settingsPath = path.join(configDir, "settings.json");
|
|
121
|
+
if (!validTenantId(tenantId)) return { settingsPath, events: [], skipped: true };
|
|
122
|
+
const settings = readJsonObject(settingsPath);
|
|
123
|
+
const invocation = managedInvocation("claude_code", surface, tenantId);
|
|
124
|
+
const handler = {
|
|
125
|
+
type: "command",
|
|
126
|
+
command: invocation.command,
|
|
127
|
+
args: invocation.args,
|
|
128
|
+
timeout: 15,
|
|
129
|
+
async: true,
|
|
130
|
+
};
|
|
131
|
+
const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks)
|
|
132
|
+
? { ...settings.hooks }
|
|
133
|
+
: {};
|
|
134
|
+
for (const event of CLAUDE_SESSION_EVENTS) hooks[event] = mergeEventHook(hooks, event, handler);
|
|
135
|
+
settings.hooks = hooks;
|
|
136
|
+
writePrivateJson(settingsPath, settings);
|
|
137
|
+
return { settingsPath, events: [...CLAUDE_SESSION_EVENTS] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function shellQuote(value) {
|
|
141
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function windowsCommandQuote(value) {
|
|
145
|
+
const text = String(value);
|
|
146
|
+
if (/[\r\n\0%]/u.test(text)) throw new Error("cannot safely write this Windows hook command");
|
|
147
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function canonicalize(value) {
|
|
151
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
152
|
+
if (!value || typeof value !== "object") return value;
|
|
153
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function codexHookHash(event, handler) {
|
|
157
|
+
const command = process.platform === "win32" && handler.commandWindows
|
|
158
|
+
? handler.commandWindows
|
|
159
|
+
: handler.command;
|
|
160
|
+
const identity = canonicalize({
|
|
161
|
+
event_name: event.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toLowerCase(),
|
|
162
|
+
hooks: [{
|
|
163
|
+
type: "command",
|
|
164
|
+
command,
|
|
165
|
+
timeout: handler.timeout,
|
|
166
|
+
async: false,
|
|
167
|
+
}],
|
|
168
|
+
});
|
|
169
|
+
return `sha256:${crypto.createHash("sha256").update(JSON.stringify(identity)).digest("hex")}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function tomlString(value) {
|
|
173
|
+
return JSON.stringify(String(value));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function replaceManagedTrustBlock(configPath, entries) {
|
|
177
|
+
const current = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
178
|
+
const start = current.indexOf(CODEX_TRUST_START);
|
|
179
|
+
let without = current;
|
|
180
|
+
if (start !== -1) {
|
|
181
|
+
const end = current.indexOf(CODEX_TRUST_END, start);
|
|
182
|
+
if (end === -1) throw new Error(`${configPath} has an incomplete Impel session-hook trust block`);
|
|
183
|
+
without = current.slice(0, start) + current.slice(end + CODEX_TRUST_END.length);
|
|
184
|
+
}
|
|
185
|
+
const block = [CODEX_TRUST_START];
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
block.push(
|
|
188
|
+
`[hooks.state.${tomlString(entry.key)}]`,
|
|
189
|
+
`trusted_hash = ${tomlString(entry.hash)}`,
|
|
190
|
+
"",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
block.push(CODEX_TRUST_END);
|
|
194
|
+
const next = `${without.trimEnd()}${without.trim() ? "\n\n" : ""}${block.join("\n")}\n`;
|
|
195
|
+
ensurePrivateDirectory(path.dirname(configPath));
|
|
196
|
+
const temporaryPath = `${configPath}.tmp-${process.pid}`;
|
|
197
|
+
try {
|
|
198
|
+
fs.writeFileSync(temporaryPath, next, { mode: 0o600 });
|
|
199
|
+
fs.renameSync(temporaryPath, configPath);
|
|
200
|
+
try { fs.chmodSync(configPath, 0o600); } catch { /* Best effort on Windows. */ }
|
|
201
|
+
} finally {
|
|
202
|
+
try { fs.rmSync(temporaryPath, { force: true }); } catch { /* Rename removed it. */ }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function ensureCodexSessionHooks(codexHome, tenantId, surface = "codex_cli") {
|
|
207
|
+
const hooksPath = path.join(codexHome, "hooks.json");
|
|
208
|
+
if (!validTenantId(tenantId)) return { hooksPath, events: [], skipped: true };
|
|
209
|
+
const document = readJsonObject(hooksPath);
|
|
210
|
+
const invocation = managedInvocation("codex", surface, tenantId);
|
|
211
|
+
const unixCommand = [invocation.command, ...invocation.args].map(shellQuote).join(" ");
|
|
212
|
+
const windowsTokens = [invocation.command, ...invocation.args].map(windowsCommandQuote).join(" ");
|
|
213
|
+
// Codex executes commandWindows with `cmd.exe /C`. The outer quote preserves
|
|
214
|
+
// a quoted executable path when it contains spaces.
|
|
215
|
+
const windowsCommand = `"${windowsTokens}"`;
|
|
216
|
+
const handler = {
|
|
217
|
+
type: "command",
|
|
218
|
+
command: unixCommand,
|
|
219
|
+
commandWindows: windowsCommand,
|
|
220
|
+
timeout: 15,
|
|
221
|
+
};
|
|
222
|
+
const hooks = document.hooks && typeof document.hooks === "object" && !Array.isArray(document.hooks)
|
|
223
|
+
? { ...document.hooks }
|
|
224
|
+
: {};
|
|
225
|
+
const trustEntries = [];
|
|
226
|
+
for (const event of CODEX_SESSION_EVENTS) {
|
|
227
|
+
hooks[event] = mergeEventHook(hooks, event, handler);
|
|
228
|
+
const groupIndex = hooks[event].length - 1;
|
|
229
|
+
trustEntries.push({ event, groupIndex, hash: codexHookHash(event, handler) });
|
|
230
|
+
}
|
|
231
|
+
document.hooks = hooks;
|
|
232
|
+
writePrivateJson(hooksPath, document);
|
|
233
|
+
const canonicalHooksPath = fs.realpathSync(hooksPath);
|
|
234
|
+
for (const entry of trustEntries) {
|
|
235
|
+
entry.key = `${canonicalHooksPath}:${entry.event.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toLowerCase()}:${entry.groupIndex}:0`;
|
|
236
|
+
delete entry.event;
|
|
237
|
+
delete entry.groupIndex;
|
|
238
|
+
}
|
|
239
|
+
const configPath = path.join(codexHome, "config.toml");
|
|
240
|
+
replaceManagedTrustBlock(configPath, trustEntries);
|
|
241
|
+
return { hooksPath, configPath, events: [...CODEX_SESSION_EVENTS], trustEntries };
|
|
242
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
import { appPaths, CLAUDE_CONFIG_ID } from "./apps.js";
|
|
7
|
+
import { redactSecretText } from "./config.js";
|
|
8
|
+
import { environmentValue, nativeCommandInvocation, resolveNativeBinary } from "./nativeProcess.js";
|
|
9
|
+
import { IMPEL_CLI_ENTRYPOINT } from "./selfInvocation.js";
|
|
10
|
+
import { normalizeTenantId } from "./tenants.js";
|
|
11
|
+
import { windowsClaudeUserData } from "./windowsApps.js";
|
|
12
|
+
|
|
13
|
+
const LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
14
|
+
|
|
15
|
+
function safeShortcutSegment(value, fallback) {
|
|
16
|
+
const cleaned = String(value || "")
|
|
17
|
+
.replace(/[<>:"/\\|?*\u0000-\u001F]/gu, " ")
|
|
18
|
+
.replace(/[. ]+$/gu, "")
|
|
19
|
+
.replace(/\s+/gu, " ")
|
|
20
|
+
.trim()
|
|
21
|
+
.slice(0, 80);
|
|
22
|
+
const reserved = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
|
|
23
|
+
return cleaned && !reserved.test(cleaned) ? cleaned : fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function powershellLiteral(value) {
|
|
27
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Serialize one argv token using the CommandLineToArgvW rules used by native
|
|
31
|
+
// Windows executables. The resulting string is stored in a .lnk Arguments
|
|
32
|
+
// field; no tenant-controlled value is evaluated as PowerShell source.
|
|
33
|
+
export function windowsShortcutArgument(value) {
|
|
34
|
+
const text = String(value);
|
|
35
|
+
if (/[\u0000\r\n]/u.test(text)) throw new Error("cannot put a NUL or line break in a Windows shortcut argument");
|
|
36
|
+
if (text && !/[\s"]/u.test(text)) return text;
|
|
37
|
+
return `"${text
|
|
38
|
+
.replace(/(\\*)"/gu, "$1$1\\\"")
|
|
39
|
+
.replace(/(\\*)$/u, "$1$1")}"`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function windowsTenantShortcutName(product, tenantId, tenantName) {
|
|
43
|
+
const id = normalizeTenantId(tenantId);
|
|
44
|
+
const name = safeShortcutSegment(tenantName, id);
|
|
45
|
+
const label = product === "claude" ? "Impel Claude" : "Impel ChatGPT";
|
|
46
|
+
return `${label} (${name} · ${id}).lnk`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerWindowsTenantShortcut({
|
|
50
|
+
product,
|
|
51
|
+
tenantId,
|
|
52
|
+
tenantName,
|
|
53
|
+
environment = process.env,
|
|
54
|
+
run = spawnSync,
|
|
55
|
+
execPath = process.execPath,
|
|
56
|
+
cliEntrypoint = IMPEL_CLI_ENTRYPOINT,
|
|
57
|
+
iconPath = null,
|
|
58
|
+
mkdirSync = fs.mkdirSync,
|
|
59
|
+
}) {
|
|
60
|
+
const appData = environmentValue(environment, "APPDATA");
|
|
61
|
+
if (!appData) throw new Error("Windows APPDATA is unavailable; Start menu entry was not created");
|
|
62
|
+
const shortcutDirectory = path.win32.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Impel");
|
|
63
|
+
mkdirSync(shortcutDirectory, { recursive: true });
|
|
64
|
+
const shortcutPath = path.win32.join(
|
|
65
|
+
shortcutDirectory,
|
|
66
|
+
windowsTenantShortcutName(product, tenantId, tenantName),
|
|
67
|
+
);
|
|
68
|
+
const command = [
|
|
69
|
+
"&",
|
|
70
|
+
powershellLiteral(execPath),
|
|
71
|
+
powershellLiteral(cliEntrypoint),
|
|
72
|
+
powershellLiteral("_app-launch"),
|
|
73
|
+
powershellLiteral(product),
|
|
74
|
+
powershellLiteral("--tenant"),
|
|
75
|
+
powershellLiteral(normalizeTenantId(tenantId)),
|
|
76
|
+
].join(" ");
|
|
77
|
+
const launcherTarget = resolveNativeBinary("powershell", environment, "win32");
|
|
78
|
+
const launcherArgs = [
|
|
79
|
+
"-NoLogo",
|
|
80
|
+
"-NoProfile",
|
|
81
|
+
"-NonInteractive",
|
|
82
|
+
"-ExecutionPolicy",
|
|
83
|
+
"Bypass",
|
|
84
|
+
"-Command",
|
|
85
|
+
command,
|
|
86
|
+
];
|
|
87
|
+
const createScript = [
|
|
88
|
+
"$w=New-Object -ComObject WScript.Shell",
|
|
89
|
+
"$s=$w.CreateShortcut($env:IMPEL_SHORTCUT_PATH)",
|
|
90
|
+
"$s.TargetPath=$env:IMPEL_SHORTCUT_TARGET",
|
|
91
|
+
"$s.Arguments=$env:IMPEL_SHORTCUT_ARGS",
|
|
92
|
+
"$s.WorkingDirectory=$env:IMPEL_SHORTCUT_WORKDIR",
|
|
93
|
+
"if($env:IMPEL_SHORTCUT_ICON){$s.IconLocation=$env:IMPEL_SHORTCUT_ICON}",
|
|
94
|
+
"$s.WindowStyle=7",
|
|
95
|
+
"$s.Save()",
|
|
96
|
+
].join("; ");
|
|
97
|
+
const powershell = nativeCommandInvocation("powershell", [
|
|
98
|
+
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", createScript,
|
|
99
|
+
], environment, "win32");
|
|
100
|
+
const result = run(powershell.command, powershell.args, {
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
env: {
|
|
103
|
+
...environment,
|
|
104
|
+
IMPEL_SHORTCUT_PATH: shortcutPath,
|
|
105
|
+
IMPEL_SHORTCUT_TARGET: launcherTarget,
|
|
106
|
+
IMPEL_SHORTCUT_ARGS: launcherArgs.map(windowsShortcutArgument).join(" "),
|
|
107
|
+
IMPEL_SHORTCUT_WORKDIR: path.win32.dirname(execPath),
|
|
108
|
+
IMPEL_SHORTCUT_ICON: iconPath || "",
|
|
109
|
+
},
|
|
110
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
windowsVerbatimArguments: powershell.windowsVerbatimArguments,
|
|
113
|
+
});
|
|
114
|
+
if (result?.error || result?.status !== 0) {
|
|
115
|
+
throw new Error(`could not create ${path.win32.basename(shortcutPath)} (${redactSecretText(result?.error?.message || result?.stderr || `exit ${result?.status}`)})`);
|
|
116
|
+
}
|
|
117
|
+
return shortcutPath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeShellEntryManifest(manifestPath, entries, {
|
|
121
|
+
mkdirSync = fs.mkdirSync,
|
|
122
|
+
writeFileSync = fs.writeFileSync,
|
|
123
|
+
renameSync = fs.renameSync,
|
|
124
|
+
rmSync = fs.rmSync,
|
|
125
|
+
} = {}) {
|
|
126
|
+
mkdirSync(path.dirname(manifestPath), { recursive: true, mode: 0o700 });
|
|
127
|
+
const temporary = `${manifestPath}.tmp-${process.pid}`;
|
|
128
|
+
try {
|
|
129
|
+
writeFileSync(temporary, `${JSON.stringify({
|
|
130
|
+
schemaVersion: 1,
|
|
131
|
+
entries: entries.map(({ product, path: entryPath }) => ({ product, path: entryPath })),
|
|
132
|
+
updatedAt: new Date().toISOString(),
|
|
133
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
134
|
+
renameSync(temporary, manifestPath);
|
|
135
|
+
} finally {
|
|
136
|
+
rmSync(temporary, { force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function registerTenantShellEntries({
|
|
141
|
+
tenantId,
|
|
142
|
+
tenantName,
|
|
143
|
+
targets = ["claude", "chatgpt"],
|
|
144
|
+
platform = process.platform,
|
|
145
|
+
homeDir = os.homedir(),
|
|
146
|
+
environment = process.env,
|
|
147
|
+
existsSync = fs.existsSync,
|
|
148
|
+
run = spawnSync,
|
|
149
|
+
writeManifest = writeShellEntryManifest,
|
|
150
|
+
shortcut = registerWindowsTenantShortcut,
|
|
151
|
+
} = {}) {
|
|
152
|
+
const id = normalizeTenantId(tenantId);
|
|
153
|
+
const paths = appPaths(homeDir, id, {
|
|
154
|
+
tenantName,
|
|
155
|
+
claudeUserData: platform === "win32" ? windowsClaudeUserData(environment, id) : null,
|
|
156
|
+
});
|
|
157
|
+
const registered = [];
|
|
158
|
+
if (platform === "darwin") {
|
|
159
|
+
for (const product of targets) {
|
|
160
|
+
const launcher = paths[product].launcher;
|
|
161
|
+
if (!existsSync(launcher)) throw new Error(`managed launcher is missing: ${launcher}`);
|
|
162
|
+
const result = run(LSREGISTER, ["-f", launcher], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
163
|
+
if (result?.error || result?.status !== 0) {
|
|
164
|
+
throw new Error(`LaunchServices registration failed for ${launcher} (${redactSecretText(result?.error?.message || result?.stderr || `exit ${result?.status}`)})`);
|
|
165
|
+
}
|
|
166
|
+
registered.push({ product, path: launcher, status: "ready" });
|
|
167
|
+
}
|
|
168
|
+
} else if (platform === "win32") {
|
|
169
|
+
const available = {
|
|
170
|
+
claude: existsSync(path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`)),
|
|
171
|
+
chatgpt: existsSync(path.join(paths.chatgpt.codexHome, "config.toml")),
|
|
172
|
+
};
|
|
173
|
+
for (const product of targets) {
|
|
174
|
+
if (!available[product]) throw new Error(`managed ${product} profile is missing for tenant ${id}`);
|
|
175
|
+
registered.push({
|
|
176
|
+
product,
|
|
177
|
+
path: shortcut({ product, tenantId: id, tenantName, environment, run }),
|
|
178
|
+
status: "ready",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (registered.length) {
|
|
183
|
+
writeManifest(path.join(paths.tenantRoot, "shell-entries.json"), registered);
|
|
184
|
+
}
|
|
185
|
+
return registered;
|
|
186
|
+
}
|
package/src/skills.js
CHANGED
|
@@ -409,7 +409,7 @@ export async function syncSkills({
|
|
|
409
409
|
const first = failures[0];
|
|
410
410
|
logger.warn(
|
|
411
411
|
`impel: skill sync for ${displayLabel} finished with warnings (${first.phase}: ${first.reason}). ` +
|
|
412
|
-
`Skills may be stale; re-run \`impel
|
|
412
|
+
`Skills may be stale; re-run \`impel update\`.`
|
|
413
413
|
);
|
|
414
414
|
return { client, label: displayLabel, synced: false, marketplaceName, failures };
|
|
415
415
|
}
|
package/src/tenants.js
CHANGED
|
@@ -58,7 +58,7 @@ export function assertProviderScopes(scopes, providers, { requireLive = false }
|
|
|
58
58
|
if (!missing.length) return;
|
|
59
59
|
const labels = missing.map((scope) => `"${scope}"`).join(" and ");
|
|
60
60
|
throw new Error(
|
|
61
|
-
`this PAT is missing the ${labels} scope${missing.length === 1 ? "" : "s"}; create a fresh PAT in Impel Gateway setup, then run \`impel
|
|
61
|
+
`this PAT is missing the ${labels} scope${missing.length === 1 ? "" : "s"}; create a fresh PAT in Impel Gateway setup, then run \`impel setup\` and provide it`,
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|