impel-cli 0.18.1 → 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 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. Stores the PAT in the private Impel config file.
39
- 2. Fetches the complete live tenant list.
40
- 3. Preserves the current CLI tenant when it is still accessible, or selects the
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
- 4. Creates isolated Claude and Codex CLI profiles for every supported tenant.
43
- 5. On Windows machines without git, installs a pinned, checksum-verified
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
- 6. Installs or repairs supported tenant desktop apps on macOS and Windows.
47
- 7. Registers tenant apps with Finder/Spotlight or Windows Start/Search.
48
- 8. Verifies every supported tenant surface and prints a tenant-sorted summary.
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. Checks and installs the latest `impel-cli` package from npm.
121
- 2. Continues under the freshly installed build.
122
- 3. Fetches the current live tenant list.
123
- 4. Fully prepares tenants added since the previous run.
124
- 5. Repairs and upgrades existing tenants in place.
125
- 6. Re-registers operating-system launch entries.
126
- 7. Reports local tenants that are no longer accessible without deleting them.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -726,7 +726,12 @@ export function installManagedAppFiles({
726
726
  if (resolvedVendorPaths[target] && isVendableVendorApp(target, resolvedVendorPaths[target])) {
727
727
  assertPinnedVendorAppVersion(target, resolvedVendorPaths[target]);
728
728
  }
729
- if (target === "chatgpt") writeVendoredChatGPTBundle(paths, resolvedVendorPaths.chatgpt, config.gatewayUrl);
729
+ if (target === "chatgpt") writeVendoredChatGPTBundle(
730
+ paths,
731
+ resolvedVendorPaths.chatgpt,
732
+ config.gatewayUrl,
733
+ homeDir,
734
+ );
730
735
  else writeVendoredClaudeBundle(
731
736
  paths,
732
737
  resolvedVendorPaths.claude,
@@ -1561,7 +1566,7 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
1561
1566
  }
1562
1567
  }
1563
1568
 
1564
- function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
1569
+ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
1565
1570
  if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
1566
1571
  const definition = APP_DEFINITIONS.chatgpt;
1567
1572
  const executableName = definition.executableNames.find((name) => (
@@ -1600,7 +1605,7 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
1600
1605
  const asarHash = asarHeaderHash(runtimeAsarPath);
1601
1606
  const vendorExecutable = path.join(vendorBundle, "Contents", "MacOS", executableName);
1602
1607
  const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
1603
- const launchHost = ensureChatGPTLaunchHost();
1608
+ const launchHost = ensureChatGPTLaunchHost({ cacheDir: chatGPTLaunchHostCacheDir(homeDir) });
1604
1609
  writeAtomic(
1605
1610
  path.join(staging, "Contents", "Info.plist"),
1606
1611
  vendoredChatGPTWrapperPlist(vendorPath, asarHash, paths.chatgpt, launchHost ? "launch-host" : "launch"),
@@ -2083,7 +2088,7 @@ export function ensureChatGPTLaunchHost({
2083
2088
  platform = process.platform,
2084
2089
  osRelease = os.release(),
2085
2090
  compiler = "/usr/bin/cc",
2086
- cacheDir = fileURLToPath(new URL("../cache", import.meta.url)),
2091
+ cacheDir = chatGPTLaunchHostCacheDir(),
2087
2092
  warn = console.warn,
2088
2093
  } = {}) {
2089
2094
  if (platform !== "darwin") return null;
@@ -2127,6 +2132,10 @@ export function ensureChatGPTLaunchHost({
2127
2132
  }
2128
2133
  }
2129
2134
 
2135
+ export function chatGPTLaunchHostCacheDir(homeDir = os.homedir()) {
2136
+ return path.join(homeDir, ".config", "impel", "cache", "chatgpt-launch-host");
2137
+ }
2138
+
2130
2139
  function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity, executableName = "launch") {
2131
2140
  return `<?xml version="1.0" encoding="UTF-8"?>
2132
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
+ }