impel-cli 0.18.0 → 0.18.2
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 +24 -15
- package/package.json +1 -1
- package/src/apps.js +16 -5
- package/src/cli.js +2 -0
- package/src/macDeveloperTools.js +65 -0
- package/src/sessionHooks.js +8 -7
package/README.md
CHANGED
|
@@ -22,6 +22,11 @@ npm install --global impel-cli
|
|
|
22
22
|
|
|
23
23
|
Node.js 18 or newer is required.
|
|
24
24
|
|
|
25
|
+
On macOS, setup and update verify Apple's Xcode Command Line Tools before any
|
|
26
|
+
profile, app, or skills work begins. If they are missing, Impel opens the
|
|
27
|
+
system installer with `xcode-select --install` and stops safely; finish that
|
|
28
|
+
installation, then rerun the same Impel command.
|
|
29
|
+
|
|
25
30
|
Then run setup and paste an Impel Personal Access Token when prompted:
|
|
26
31
|
|
|
27
32
|
```sh
|
|
@@ -35,17 +40,19 @@ surfaces for each tenant. You do not need to repeat setup tenant by tenant.
|
|
|
35
40
|
|
|
36
41
|
`impel setup`:
|
|
37
42
|
|
|
38
|
-
1.
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
1. On macOS, verifies the Xcode Command Line Tools and opens Apple's installer
|
|
44
|
+
before doing any other setup work when they are missing.
|
|
45
|
+
2. Stores the PAT in the private Impel config file.
|
|
46
|
+
3. Fetches the complete live tenant list.
|
|
47
|
+
4. Preserves the current CLI tenant when it is still accessible, or selects the
|
|
41
48
|
control-plane default.
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
5. Creates isolated Claude and Codex CLI profiles for every supported tenant.
|
|
50
|
+
6. On Windows machines without git, installs a pinned, checksum-verified
|
|
44
51
|
MinGit into `~/.config/impel/tools/git` (no admin rights, no system PATH
|
|
45
52
|
change) so Claude and Codex can install the Impel skills marketplace.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
7. Installs or repairs supported tenant desktop apps on macOS and Windows.
|
|
54
|
+
8. Registers tenant apps with Finder/Spotlight or Windows Start/Search.
|
|
55
|
+
9. Verifies every supported tenant surface and prints a tenant-sorted summary.
|
|
49
56
|
|
|
50
57
|
Use `--tenant` only to choose the default for CLI launches. It does not limit
|
|
51
58
|
which tenants setup prepares:
|
|
@@ -117,13 +124,15 @@ impel update
|
|
|
117
124
|
|
|
118
125
|
Update:
|
|
119
126
|
|
|
120
|
-
1.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
1. On macOS, verifies the Xcode Command Line Tools and opens Apple's installer
|
|
128
|
+
before doing any other update work when they are missing.
|
|
129
|
+
2. Checks and installs the latest `impel-cli` package from npm.
|
|
130
|
+
3. Continues under the freshly installed build.
|
|
131
|
+
4. Fetches the current live tenant list.
|
|
132
|
+
5. Fully prepares tenants added since the previous run.
|
|
133
|
+
6. Repairs and upgrades existing tenants in place.
|
|
134
|
+
7. Re-registers operating-system launch entries.
|
|
135
|
+
8. Reports local tenants that are no longer accessible without deleting them.
|
|
127
136
|
|
|
128
137
|
An update preserves histories, browser state, preferences, permissions, stable
|
|
129
138
|
app identity, and user-authored settings outside Impel-managed blocks. Desktop
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -211,7 +211,9 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
211
211
|
// hook/auth/MCP artifacts instead of the npm-prefix bin path.
|
|
212
212
|
// 20: managed Claude sandbox disabled by default; the rewrite strips the old
|
|
213
213
|
// strict sandbox keys from already-provisioned profiles.
|
|
214
|
-
|
|
214
|
+
// 21: Windows Codex hooks use the pinned client's PowerShell execution
|
|
215
|
+
// contract instead of cmd.exe quoting that failed every managed hook.
|
|
216
|
+
export const CURRENT_CONFIG_VERSION = 21;
|
|
215
217
|
|
|
216
218
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
217
219
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -724,7 +726,12 @@ export function installManagedAppFiles({
|
|
|
724
726
|
if (resolvedVendorPaths[target] && isVendableVendorApp(target, resolvedVendorPaths[target])) {
|
|
725
727
|
assertPinnedVendorAppVersion(target, resolvedVendorPaths[target]);
|
|
726
728
|
}
|
|
727
|
-
if (target === "chatgpt") writeVendoredChatGPTBundle(
|
|
729
|
+
if (target === "chatgpt") writeVendoredChatGPTBundle(
|
|
730
|
+
paths,
|
|
731
|
+
resolvedVendorPaths.chatgpt,
|
|
732
|
+
config.gatewayUrl,
|
|
733
|
+
homeDir,
|
|
734
|
+
);
|
|
728
735
|
else writeVendoredClaudeBundle(
|
|
729
736
|
paths,
|
|
730
737
|
resolvedVendorPaths.claude,
|
|
@@ -1559,7 +1566,7 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
|
|
|
1559
1566
|
}
|
|
1560
1567
|
}
|
|
1561
1568
|
|
|
1562
|
-
function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
1569
|
+
function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1563
1570
|
if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
|
|
1564
1571
|
const definition = APP_DEFINITIONS.chatgpt;
|
|
1565
1572
|
const executableName = definition.executableNames.find((name) => (
|
|
@@ -1598,7 +1605,7 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
1598
1605
|
const asarHash = asarHeaderHash(runtimeAsarPath);
|
|
1599
1606
|
const vendorExecutable = path.join(vendorBundle, "Contents", "MacOS", executableName);
|
|
1600
1607
|
const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
|
|
1601
|
-
const launchHost = ensureChatGPTLaunchHost();
|
|
1608
|
+
const launchHost = ensureChatGPTLaunchHost({ cacheDir: chatGPTLaunchHostCacheDir(homeDir) });
|
|
1602
1609
|
writeAtomic(
|
|
1603
1610
|
path.join(staging, "Contents", "Info.plist"),
|
|
1604
1611
|
vendoredChatGPTWrapperPlist(vendorPath, asarHash, paths.chatgpt, launchHost ? "launch-host" : "launch"),
|
|
@@ -2081,7 +2088,7 @@ export function ensureChatGPTLaunchHost({
|
|
|
2081
2088
|
platform = process.platform,
|
|
2082
2089
|
osRelease = os.release(),
|
|
2083
2090
|
compiler = "/usr/bin/cc",
|
|
2084
|
-
cacheDir =
|
|
2091
|
+
cacheDir = chatGPTLaunchHostCacheDir(),
|
|
2085
2092
|
warn = console.warn,
|
|
2086
2093
|
} = {}) {
|
|
2087
2094
|
if (platform !== "darwin") return null;
|
|
@@ -2125,6 +2132,10 @@ export function ensureChatGPTLaunchHost({
|
|
|
2125
2132
|
}
|
|
2126
2133
|
}
|
|
2127
2134
|
|
|
2135
|
+
export function chatGPTLaunchHostCacheDir(homeDir = os.homedir()) {
|
|
2136
|
+
return path.join(homeDir, ".config", "impel", "cache", "chatgpt-launch-host");
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2128
2139
|
function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity, executableName = "launch") {
|
|
2129
2140
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2130
2141
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
package/src/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { cmdUpdate } from "./commands/update.js";
|
|
|
20
20
|
import { cmdNuke } from "./commands/nuke.js";
|
|
21
21
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
22
22
|
import { cmdConverge } from "./commands/converge.js";
|
|
23
|
+
import { ensureMacDeveloperTools } from "./macDeveloperTools.js";
|
|
23
24
|
import { refuseElevatedMacExecution } from "./privileges.js";
|
|
24
25
|
|
|
25
26
|
const HELP = `impel — isolated Impel workspaces for every tenant
|
|
@@ -82,6 +83,7 @@ function normalizeTarget(token) {
|
|
|
82
83
|
|
|
83
84
|
export async function main(argv) {
|
|
84
85
|
if (refuseElevatedMacExecution(argv)) return;
|
|
86
|
+
if (!ensureMacDeveloperTools(argv)) return;
|
|
85
87
|
const [cmd, ...rest] = argv;
|
|
86
88
|
|
|
87
89
|
switch (cmd) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const XCODE_SELECT = "/usr/bin/xcode-select";
|
|
4
|
+
|
|
5
|
+
function hasHelpFlag(argv) {
|
|
6
|
+
return argv[0] === "help" || argv.slice(1).some((arg) => ["--help", "-h"].includes(arg));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Commands that may clone a skills marketplace or build a managed macOS app.
|
|
11
|
+
* Read-only commands deliberately stay available while Apple's installer is
|
|
12
|
+
* pending, and `update --check` keeps its documented no-mutation contract.
|
|
13
|
+
*/
|
|
14
|
+
export function commandRequiresMacDeveloperTools(argv = []) {
|
|
15
|
+
const [command, subcommand] = argv;
|
|
16
|
+
if (!command || hasHelpFlag(argv)) return false;
|
|
17
|
+
|
|
18
|
+
if (["setup", "_converge"].includes(command)) return true;
|
|
19
|
+
if (["update", "upgrade"].includes(command)) {
|
|
20
|
+
return !argv.includes("--check") && !argv.includes("--refresh-cache");
|
|
21
|
+
}
|
|
22
|
+
if (["skill", "skills"].includes(command)) return !subcommand || subcommand === "sync";
|
|
23
|
+
if (["app", "apps"].includes(command)) {
|
|
24
|
+
return ["install", "update", "open", "refresh"].includes(subcommand);
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ensure Apple's Command Line Tools are available before Impel mutates any
|
|
31
|
+
* lifecycle state that depends on git or the native compiler. The Apple
|
|
32
|
+
* installer runs asynchronously, so a newly requested install must stop this
|
|
33
|
+
* invocation and let the operator rerun it after installation finishes.
|
|
34
|
+
*/
|
|
35
|
+
export function ensureMacDeveloperTools(argv = [], dependencies = {}) {
|
|
36
|
+
const io = {
|
|
37
|
+
platform: process.platform,
|
|
38
|
+
spawn: spawnSync,
|
|
39
|
+
log: (message) => console.log(message),
|
|
40
|
+
error: (message) => console.error(message),
|
|
41
|
+
setExitCode: (code) => {
|
|
42
|
+
process.exitCode = code;
|
|
43
|
+
},
|
|
44
|
+
...dependencies,
|
|
45
|
+
};
|
|
46
|
+
if (io.platform !== "darwin" || !commandRequiresMacDeveloperTools(argv)) return true;
|
|
47
|
+
|
|
48
|
+
const selected = io.spawn(XCODE_SELECT, ["-p"], { stdio: "ignore" });
|
|
49
|
+
if (!selected.error && selected.status === 0) return true;
|
|
50
|
+
|
|
51
|
+
io.log("Xcode Command Line Tools are required; opening Apple's installer…");
|
|
52
|
+
const requested = io.spawn(XCODE_SELECT, ["--install"], { stdio: "inherit" });
|
|
53
|
+
if (requested.error) {
|
|
54
|
+
io.error(`impel: could not start the Xcode Command Line Tools installer (${requested.error.message || requested.error}).`);
|
|
55
|
+
io.error(" Run `xcode-select --install`, finish the system installation, then rerun this Impel command.");
|
|
56
|
+
} else if (requested.status !== 0) {
|
|
57
|
+
io.error("impel: the Xcode Command Line Tools installer did not start successfully.");
|
|
58
|
+
io.error(" Run `xcode-select --install`, finish the system installation, then rerun this Impel command.");
|
|
59
|
+
} else {
|
|
60
|
+
io.error("impel: finish installing the Xcode Command Line Tools in the system dialog, then rerun this command.");
|
|
61
|
+
io.error(" No Impel profiles, apps, or skills were changed.");
|
|
62
|
+
}
|
|
63
|
+
io.setExitCode(1);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
package/src/sessionHooks.js
CHANGED
|
@@ -141,10 +141,10 @@ function shellQuote(value) {
|
|
|
141
141
|
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function
|
|
144
|
+
function powershellQuote(value) {
|
|
145
145
|
const text = String(value);
|
|
146
|
-
if (/[\r\n\0
|
|
147
|
-
return `
|
|
146
|
+
if (/[\r\n\0]/u.test(text)) throw new Error("cannot safely write this Windows hook command");
|
|
147
|
+
return `'${text.replaceAll("'", "''")}'`;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function canonicalize(value) {
|
|
@@ -209,10 +209,11 @@ export function ensureCodexSessionHooks(codexHome, tenantId, surface = "codex_cl
|
|
|
209
209
|
const document = readJsonObject(hooksPath);
|
|
210
210
|
const invocation = managedInvocation("codex", surface, tenantId);
|
|
211
211
|
const unixCommand = [invocation.command, ...invocation.args].map(shellQuote).join(" ");
|
|
212
|
-
const windowsTokens = [invocation.command, ...invocation.args].map(
|
|
213
|
-
// Codex
|
|
214
|
-
//
|
|
215
|
-
|
|
212
|
+
const windowsTokens = [invocation.command, ...invocation.args].map(powershellQuote).join(" ");
|
|
213
|
+
// Codex runs commandWindows through the session's normal Windows shell,
|
|
214
|
+
// which is PowerShell. A quoted executable is only invoked when prefixed by
|
|
215
|
+
// the call operator; cmd.exe's doubled outer-quote form is invalid here.
|
|
216
|
+
const windowsCommand = `& ${windowsTokens}`;
|
|
216
217
|
const handler = {
|
|
217
218
|
type: "command",
|
|
218
219
|
command: unixCommand,
|