impel-cli 0.13.0 → 0.13.1

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
@@ -803,7 +803,9 @@ retains OpenAI's signature.
803
803
 
804
804
  By default the CLI signs each rebuild with a **stable per-machine identity** — a
805
805
  self-signed certificate created once in a dedicated keychain under
806
- `~/.config/impel/codesign` (no Apple Developer account is involved). macOS ties
806
+ `~/.config/impel/codesign` (no Apple Developer account is involved). The
807
+ certificate is trusted locally for the code-signing policy only; it is not a
808
+ TLS or document-signing authority. macOS ties
807
809
  every privacy (TCC) grant — microphone, screen recording, folder access,
808
810
  Automation, accessibility — to the app's code-signing designated requirement. A
809
811
  stable identity keeps that requirement constant across rebuilds, so the grants
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Configure Claude Code and Codex CLI to talk to Impel's gateway, authenticated by an Impel Personal Access Token",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -152,7 +152,7 @@ export const CURRENT_CONFIG_VERSION = 10;
152
152
  // — which is what made every `impel update` re-trigger macOS permission
153
153
  // prompts. Bump this ONLY when a code change alters the bytes of a built
154
154
  // bundle; leave it alone for changes that don't touch bundle contents.
155
- export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-16.1";
155
+ export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-16.2";
156
156
 
157
157
  /** Parse the tenant's install manifest, or null when absent/corrupt. */
158
158
  export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
package/src/codesign.js CHANGED
@@ -143,17 +143,28 @@ function ensureLocalSigningIdentity({ run, configDir, fsImpl, randomBytes }) {
143
143
  if (fsImpl.existsSync(keychain) && fsImpl.existsSync(passwordFile)) {
144
144
  const password = fsImpl.readFileSync(passwordFile, "utf8").trim();
145
145
  unlockKeychain(run, keychain, password);
146
+ ensureKeychainInSearchList(run, keychain);
146
147
  const existing = findLocalIdentitySha(run, keychain);
147
148
  if (existing) return localIdentity(existing, keychain);
149
+
150
+ // v0.13.0 imported a self-signed certificate but never established code-
151
+ // signing trust for it. `security find-identity -v` therefore reported
152
+ // zero valid identities and every rebuild fell back to ad-hoc signing.
153
+ // Repair that exact state in place so the already-generated identity stays
154
+ // stable instead of accumulating a second certificate with the same name.
155
+ trustExistingLocalCertificate({ run, keychain, fsImpl });
156
+ const repaired = findLocalIdentitySha(run, keychain);
157
+ if (repaired) return localIdentity(repaired, keychain);
158
+ throw new Error("existing signing identity could not be trusted");
148
159
  }
149
160
 
150
161
  const password = randomBytes(24).toString("hex");
151
162
  createLocalSigningIdentity({ run, dir, keychain, password, fsImpl });
152
163
  fsImpl.writeFileSync(passwordFile, `${password}\n`, { mode: 0o600 });
153
164
 
165
+ ensureKeychainInSearchList(run, keychain);
154
166
  const sha = findLocalIdentitySha(run, keychain);
155
167
  if (!sha) throw new Error("created signing identity was not found in its keychain");
156
- ensureKeychainInSearchList(run, keychain);
157
168
  return localIdentity(sha, keychain);
158
169
  }
159
170
 
@@ -214,13 +225,44 @@ function createLocalSigningIdentity({ run, dir, keychain, password, fsImpl }) {
214
225
  runOrThrow(run, "/usr/bin/security", [
215
226
  "set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", password, keychain,
216
227
  ], "authorize codesign for the signing key");
228
+ trustLocalCertificate(run, keychain, certPath);
217
229
  } finally {
218
230
  fsImpl.rmSync(work, { recursive: true, force: true });
219
231
  }
220
232
  }
221
233
 
222
234
  function unlockKeychain(run, keychain, password) {
223
- run("/usr/bin/security", ["unlock-keychain", "-p", password, keychain]);
235
+ runOrThrow(
236
+ run,
237
+ "/usr/bin/security",
238
+ ["unlock-keychain", "-p", password, keychain],
239
+ "unlock signing keychain",
240
+ );
241
+ }
242
+
243
+ function trustExistingLocalCertificate({ run, keychain, fsImpl }) {
244
+ const exported = run("/usr/bin/security", [
245
+ "find-certificate", "-p", "-c", SIGNING_IDENTITY_NAME, keychain,
246
+ ]);
247
+ if (exported.status !== 0 || exported.error || !String(exported.stdout || "").includes("BEGIN CERTIFICATE")) {
248
+ throw new Error("existing signing certificate was not found in its keychain");
249
+ }
250
+ const certPath = path.join(os.tmpdir(), `impel-codesign-cert-${process.pid}.pem`);
251
+ fsImpl.writeFileSync(certPath, exported.stdout, { mode: 0o600 });
252
+ try {
253
+ trustLocalCertificate(run, keychain, certPath);
254
+ } finally {
255
+ fsImpl.rmSync(certPath, { force: true });
256
+ }
257
+ }
258
+
259
+ function trustLocalCertificate(run, keychain, certPath) {
260
+ // Limit trust to the code-signing policy. This is a local build identity,
261
+ // not a TLS/document-signing CA, and it remains isolated in Impel's own
262
+ // keychain rather than the user's login keychain.
263
+ runOrThrow(run, "/usr/bin/security", [
264
+ "add-trusted-cert", "-r", "trustRoot", "-p", "codeSign", "-k", keychain, certPath,
265
+ ], "trust local code-signing certificate");
224
266
  }
225
267
 
226
268
  // codesign resolves an identity by searching the user's keychain search list,
@@ -228,12 +270,21 @@ function unlockKeychain(run, keychain, password) {
228
270
  // user's existing keychains (a bare `-s <one>` would replace the whole list).
229
271
  function ensureKeychainInSearchList(run, keychain) {
230
272
  const listed = run("/usr/bin/security", ["list-keychains", "-d", "user"]);
273
+ if (listed.status !== 0 || listed.error) {
274
+ const detail = listed.error?.message || String(listed.stderr || "").trim() || `exit ${listed.status}`;
275
+ throw new Error(`read user keychain search list failed: ${detail}`);
276
+ }
231
277
  const current = String(listed.stdout || "")
232
278
  .split("\n")
233
279
  .map((line) => line.trim().replace(/^"|"$/gu, ""))
234
280
  .filter(Boolean);
235
281
  if (current.includes(keychain)) return;
236
- run("/usr/bin/security", ["list-keychains", "-d", "user", "-s", ...current, keychain]);
282
+ runOrThrow(
283
+ run,
284
+ "/usr/bin/security",
285
+ ["list-keychains", "-d", "user", "-s", ...current, keychain],
286
+ "add Impel signing keychain to the user search list",
287
+ );
237
288
  }
238
289
 
239
290
  function runOrThrow(run, command, args, description) {
@@ -165,6 +165,7 @@ export async function cmdUpdate(argv, overrides = {}) {
165
165
  runAgentsSync: defaultRunAgentsSync,
166
166
  appsInstalled: anyAppInstalled,
167
167
  platform: process.platform,
168
+ progress: withProgress,
168
169
  ...overrides,
169
170
  };
170
171
  const { flags } = parseFlags(argv, {
@@ -185,7 +186,7 @@ export async function cmdUpdate(argv, overrides = {}) {
185
186
  }
186
187
 
187
188
  const current = io.installedVersion();
188
- const remote = await withProgress("Checking npm for impel-cli updates", () => io.fetchRemoteVersion());
189
+ const remote = await io.progress("Checking npm for impel-cli updates", () => io.fetchRemoteVersion());
189
190
  if (remote) io.writeCache({ remoteVersion: remote, checkedAt: Date.now() });
190
191
 
191
192
  console.log(`impel-cli v${current ?? "?"}`);
@@ -204,7 +205,7 @@ export async function cmdUpdate(argv, overrides = {}) {
204
205
  console.log("CLI: already up to date.");
205
206
  } else {
206
207
  console.log("CLI: installing the latest build…");
207
- if (!await withProgress("Installing the latest impel-cli build", () => io.selfUpdate(updateInstallSpec()))) {
208
+ if (!await io.progress("Installing the latest impel-cli build", () => io.selfUpdate(updateInstallSpec()))) {
208
209
  console.error("impel update: `npm install -g` failed; the CLI was not updated.");
209
210
  if (io.platform === "win32") {
210
211
  console.error(" Verify `npm --version` in PowerShell, then retry `impel update`.");
@@ -228,20 +229,24 @@ export async function cmdUpdate(argv, overrides = {}) {
228
229
  console.log(io.platform === "win32"
229
230
  ? "Apps: updating every installed tenant's signed Claude and ChatGPT profiles…"
230
231
  : "Apps: refreshing every installed tenant and rebuilding only stale app bundles…");
231
- if (!await withProgress("Updating managed desktop apps", () => io.runAppsUpdate())) {
232
+ // These child commands inherit the terminal and render their own progress
233
+ // and log lines. Wrapping them in another spinner makes both processes
234
+ // write the same terminal row, producing glued output such as
235
+ // "Updating managed desktop apps (...)Updating all managed...".
236
+ if (!await io.runAppsUpdate()) {
232
237
  console.error("impel update: the app update failed; re-run `impel app update` after fixing the issue.");
233
238
  cascadeFailed = true;
234
239
  }
235
240
  }
236
241
 
237
242
  console.log("Skills: syncing native and isolated CLI profiles…");
238
- if (!await withProgress("Syncing skills across managed profiles", () => io.runSkillsSync())) {
243
+ if (!await io.runSkillsSync()) {
239
244
  console.error("impel update: skill sync failed; re-run `impel skills sync` after fixing the issue.");
240
245
  cascadeFailed = true;
241
246
  }
242
247
 
243
248
  console.log("Agents: syncing the selected tenant into native and isolated CLI profiles…");
244
- if (!await withProgress("Syncing agents across managed profiles", () => io.runAgentsSync())) {
249
+ if (!await io.runAgentsSync()) {
245
250
  console.error("impel update: agent sync failed; re-run `impel agents sync` after fixing the issue.");
246
251
  cascadeFailed = true;
247
252
  }