@openape/apes 0.21.2 → 0.22.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/dist/cli.js CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  } from "./chunk-IDPV5SNB.js";
64
64
 
65
65
  // src/cli.ts
66
- import consola35 from "consola";
66
+ import consola36 from "consola";
67
67
 
68
68
  // src/ape-shell.ts
69
69
  import path from "path";
@@ -379,7 +379,7 @@ async function loginWithPKCE(idp) {
379
379
  consola2.success(`Logged in as ${payload.email || payload.sub}`);
380
380
  }
381
381
  async function loginWithKey(idp, keyPath, agentEmail) {
382
- const { readFileSync: readFileSync7 } = await import("fs");
382
+ const { readFileSync: readFileSync8 } = await import("fs");
383
383
  const { sign: sign3 } = await import("crypto");
384
384
  const { loadEd25519PrivateKey: loadEd25519PrivateKey2 } = await import("./ssh-key-YBNNG5K5.js");
385
385
  const challengeUrl = await getAgentChallengeEndpoint(idp);
@@ -392,7 +392,7 @@ async function loginWithKey(idp, keyPath, agentEmail) {
392
392
  throw new CliError(`Challenge failed: ${await challengeResp.text()}`);
393
393
  }
394
394
  const { challenge } = await challengeResp.json();
395
- const keyContent = readFileSync7(keyPath, "utf-8");
395
+ const keyContent = readFileSync8(keyPath, "utf-8");
396
396
  const privateKey = loadEd25519PrivateKey2(keyContent);
397
397
  const signature = sign3(null, Buffer2.from(challenge), privateKey).toString("base64");
398
398
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
@@ -1748,7 +1748,7 @@ import { defineCommand as defineCommand24 } from "citty";
1748
1748
  // src/commands/agents/destroy.ts
1749
1749
  import { execFileSync as execFileSync3 } from "child_process";
1750
1750
  import { mkdtempSync, rmSync, writeFileSync } from "fs";
1751
- import { tmpdir } from "os";
1751
+ import { tmpdir, userInfo } from "os";
1752
1752
  import { join as join2 } from "path";
1753
1753
  import { defineCommand as defineCommand20 } from "citty";
1754
1754
  import consola18 from "consola";
@@ -1889,7 +1889,7 @@ echo "OK $NAME uid=$NEXT_UID home=$HOME_DIR"
1889
1889
  `;
1890
1890
  }
1891
1891
  function buildDestroyTeardownScript(input) {
1892
- const { name, homeDir } = input;
1892
+ const { name, homeDir, adminUser } = input;
1893
1893
  return `#!/bin/bash
1894
1894
  # Best-effort teardown. set -u catches typos; we deliberately do NOT use -e
1895
1895
  # because pkill / launchctl are allowed to fail when the user has no live
@@ -1898,6 +1898,16 @@ set -u
1898
1898
 
1899
1899
  NAME=${shQuote(name)}
1900
1900
  HOME_DIR=${shQuote(homeDir)}
1901
+ ADMIN_USER=${shQuote(adminUser)}
1902
+
1903
+ # Read the admin password from stdin (line 1). The caller pipes it in.
1904
+ # We never accept it as an argv element so it can't show up in process
1905
+ # listings or escapes' audit log.
1906
+ read -r ADMIN_PASSWORD
1907
+ if [ -z "$ADMIN_PASSWORD" ]; then
1908
+ echo "ERROR: no admin password on stdin (expected one line)." >&2
1909
+ exit 2
1910
+ fi
1901
1911
 
1902
1912
  UID_OF=$(dscl . -read "/Users/$NAME" UniqueID 2>/dev/null | awk '/UniqueID:/ {print $2}')
1903
1913
 
@@ -1910,25 +1920,32 @@ if [ -d "$HOME_DIR" ] && [ "$HOME_DIR" != "/" ] && [ "$HOME_DIR" != "" ]; then
1910
1920
  rm -rf "$HOME_DIR"
1911
1921
  fi
1912
1922
 
1913
- # Delete the user record. \`sysadminctl -deleteUser\` is the canonical macOS
1914
- # API and removes Open Directory metadata that \`dscl . -delete\` leaves
1915
- # behind. Fall back to dscl if sysadminctl isn't available or rejects the
1916
- # user. Surface a clear error if both fail \u2014 silent failure here is what
1917
- # left orphaned dscl records on previous spawn/destroy round-trips.
1918
- if command -v sysadminctl >/dev/null 2>&1; then
1919
- if sysadminctl -deleteUser "$NAME" 2>/dev/null; then
1920
- :
1921
- elif dscl . -read "/Users/$NAME" >/dev/null 2>&1; then
1922
- dscl . -delete "/Users/$NAME" || {
1923
- echo "ERROR: failed to delete user record /Users/$NAME" >&2
1924
- exit 1
1925
- }
1926
- fi
1927
- elif dscl . -read "/Users/$NAME" >/dev/null 2>&1; then
1928
- dscl . -delete "/Users/$NAME" || {
1929
- echo "ERROR: failed to delete user record /Users/$NAME" >&2
1930
- exit 1
1931
- }
1923
+ # \`escapes\` is a plain setuid binary \u2014 opendirectoryd sees no audit/PAM
1924
+ # session attached (AUDIT_SESSION_ID=unset) and rejects DirectoryService
1925
+ # writes from this context: a bare \`sysadminctl -deleteUser\` or
1926
+ # \`dscl . -delete\` hangs ~5 minutes and exits with eUndefinedError -14987
1927
+ # at DSRecord.m:563. Passing explicit -adminUser/-adminPassword bypasses
1928
+ # opendirectoryd's implicit "is current session admin?" check and
1929
+ # authenticates against DirectoryService directly \u2014 the delete then
1930
+ # completes in ~1 second.
1931
+ if ! command -v sysadminctl >/dev/null 2>&1; then
1932
+ echo "ERROR: sysadminctl not available; cannot delete user record." >&2
1933
+ exit 1
1934
+ fi
1935
+
1936
+ sysadminctl \\
1937
+ -deleteUser "$NAME" \\
1938
+ -adminUser "$ADMIN_USER" \\
1939
+ -adminPassword "$ADMIN_PASSWORD"
1940
+ SYSAD_EC=$?
1941
+ unset ADMIN_PASSWORD
1942
+
1943
+ if [ $SYSAD_EC -ne 0 ]; then
1944
+ echo "ERROR: sysadminctl -deleteUser failed (exit=$SYSAD_EC)." >&2
1945
+ echo " Common causes: wrong admin password, admin user '$ADMIN_USER'" >&2
1946
+ echo " not in admin group, or target user '$NAME' is the last secure" >&2
1947
+ echo " token holder (run \\\`sysadminctl -secureTokenStatus $NAME\\\`)." >&2
1948
+ exit 1
1932
1949
  fi
1933
1950
 
1934
1951
  # Verify the record is actually gone.
@@ -2122,14 +2139,20 @@ ${consequences.join("\n")}`);
2122
2139
  if (!escapes) {
2123
2140
  throw new CliError("`escapes` not found on PATH; OS teardown requires escapes.");
2124
2141
  }
2142
+ const adminUser = userInfo().username;
2143
+ const adminPassword = await collectAdminPassword({ adminUser, force: !!args.force });
2125
2144
  const scratch = mkdtempSync(join2(tmpdir(), `apes-destroy-${name}-`));
2126
2145
  const scriptPath = join2(scratch, "teardown.sh");
2127
2146
  try {
2128
- const script = buildDestroyTeardownScript({ name, homeDir: `/Users/${name}` });
2147
+ const script = buildDestroyTeardownScript({ name, homeDir: `/Users/${name}`, adminUser });
2129
2148
  writeFileSync(scriptPath, script, { mode: 448 });
2130
2149
  consola18.start("Running teardown as root via `apes run --as root --wait`\u2026");
2131
2150
  consola18.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
2132
- execFileSync3(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], { stdio: "inherit" });
2151
+ execFileSync3(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], {
2152
+ input: `${adminPassword}
2153
+ `,
2154
+ stdio: ["pipe", "inherit", "inherit"]
2155
+ });
2133
2156
  } finally {
2134
2157
  rmSync(scratch, { recursive: true, force: true });
2135
2158
  }
@@ -2139,6 +2162,21 @@ ${consequences.join("\n")}`);
2139
2162
  consola18.success(`Destroyed ${name}.`);
2140
2163
  }
2141
2164
  });
2165
+ async function collectAdminPassword(opts) {
2166
+ const fromEnv = process.env.APES_ADMIN_PASSWORD;
2167
+ if (fromEnv && fromEnv.length > 0) return fromEnv;
2168
+ if (!process.stdin.isTTY) {
2169
+ throw new CliError(
2170
+ `Admin password required for sysadminctl -deleteUser. No TTY available for the silent prompt; set APES_ADMIN_PASSWORD in the environment (local admin password for ${opts.adminUser}). The teardown reads it from stdin and never stores it.`
2171
+ );
2172
+ }
2173
+ consola18.info(`Local admin password for ${opts.adminUser} (used for sysadminctl -deleteUser; not stored):`);
2174
+ const pw = await consola18.prompt("Admin password", { type: "text", mask: "*" });
2175
+ if (typeof pw === "symbol" || !pw || pw.length === 0) {
2176
+ throw new CliExit(0);
2177
+ }
2178
+ return pw;
2179
+ }
2142
2180
 
2143
2181
  // src/commands/agents/list.ts
2144
2182
  import { defineCommand as defineCommand21 } from "citty";
@@ -3770,7 +3808,7 @@ var mcpCommand = defineCommand32({
3770
3808
  if (transport !== "stdio" && transport !== "sse") {
3771
3809
  throw new Error('Transport must be "stdio" or "sse"');
3772
3810
  }
3773
- const { startMcpServer } = await import("./server-FO4XBTPU.js");
3811
+ const { startMcpServer } = await import("./server-WBR7VEDY.js");
3774
3812
  await startMcpServer(transport, port);
3775
3813
  }
3776
3814
  });
@@ -4408,7 +4446,7 @@ async function bestEffortGrantCount(idp) {
4408
4446
  }
4409
4447
  }
4410
4448
  async function runHealth(args) {
4411
- const version = true ? "0.21.2" : "0.0.0";
4449
+ const version = true ? "0.22.1" : "0.0.0";
4412
4450
  const auth = loadAuth();
4413
4451
  if (!auth) {
4414
4452
  throw new CliError("Not logged in. Run `apes login` first.", 1);
@@ -4600,6 +4638,77 @@ var workflowsCommand = defineCommand43({
4600
4638
  }
4601
4639
  });
4602
4640
 
4641
+ // src/version-check.ts
4642
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
4643
+ import { homedir as homedir5 } from "os";
4644
+ import { join as join6 } from "path";
4645
+ import consola35 from "consola";
4646
+ var PACKAGE_NAME = "@openape/apes";
4647
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4648
+ var CACHE_FILE = join6(homedir5(), ".config", "apes", ".version-check.json");
4649
+ function readCache() {
4650
+ if (!existsSync9(CACHE_FILE)) return null;
4651
+ try {
4652
+ return JSON.parse(readFileSync7(CACHE_FILE, "utf-8"));
4653
+ } catch {
4654
+ return null;
4655
+ }
4656
+ }
4657
+ function writeCache(entry) {
4658
+ try {
4659
+ const dir = join6(homedir5(), ".config", "apes");
4660
+ if (!existsSync9(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
4661
+ writeFileSync6(CACHE_FILE, JSON.stringify(entry), { mode: 384 });
4662
+ } catch {
4663
+ }
4664
+ }
4665
+ function compareSemver(a, b) {
4666
+ const pa = a.split(".").map(Number);
4667
+ const pb = b.split(".").map(Number);
4668
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
4669
+ const x = pa[i] ?? 0;
4670
+ const y = pb[i] ?? 0;
4671
+ if (x !== y) return x - y;
4672
+ }
4673
+ return 0;
4674
+ }
4675
+ async function fetchLatestVersion() {
4676
+ try {
4677
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`, {
4678
+ headers: { Accept: "application/json" },
4679
+ signal: AbortSignal.timeout(2e3)
4680
+ });
4681
+ if (!res.ok) return null;
4682
+ const body = await res.json();
4683
+ return typeof body.version === "string" ? body.version : null;
4684
+ } catch {
4685
+ return null;
4686
+ }
4687
+ }
4688
+ function warnIfBehind(currentVersion, latest) {
4689
+ if (compareSemver(currentVersion, latest) < 0) {
4690
+ consola35.warn(
4691
+ `apes ${currentVersion} is behind latest @openape/apes@${latest}. Run \`npm i -g @openape/apes@latest\` to update. (Suppress with APES_NO_UPDATE_CHECK=1.)`
4692
+ );
4693
+ }
4694
+ }
4695
+ async function maybeWarnStaleVersion(currentVersion) {
4696
+ if (process.env.APES_NO_UPDATE_CHECK) return;
4697
+ if (!currentVersion || currentVersion === "unknown") return;
4698
+ const cached = readCache();
4699
+ const now = Date.now();
4700
+ if (cached) {
4701
+ warnIfBehind(currentVersion, cached.latest);
4702
+ }
4703
+ if (!cached || now - cached.checkedAt >= CACHE_TTL_MS) {
4704
+ const latest = await fetchLatestVersion();
4705
+ if (latest) {
4706
+ writeCache({ latest, checkedAt: now });
4707
+ if (!cached) warnIfBehind(currentVersion, latest);
4708
+ }
4709
+ }
4710
+ }
4711
+
4603
4712
  // src/cli.ts
4604
4713
  process.stdout.on("error", (err) => {
4605
4714
  if (err.code === "EPIPE") process.exit(0);
@@ -4610,10 +4719,10 @@ if (shellRewrite) {
4610
4719
  if (shellRewrite.action === "rewrite") {
4611
4720
  process.argv = shellRewrite.argv;
4612
4721
  } else if (shellRewrite.action === "version") {
4613
- console.log(`ape-shell ${"0.21.2"} (OpenApe DDISA shell wrapper)`);
4722
+ console.log(`ape-shell ${"0.22.1"} (OpenApe DDISA shell wrapper)`);
4614
4723
  process.exit(0);
4615
4724
  } else if (shellRewrite.action === "help") {
4616
- console.log(`ape-shell ${"0.21.2"} \u2014 OpenApe DDISA shell wrapper`);
4725
+ console.log(`ape-shell ${"0.22.1"} \u2014 OpenApe DDISA shell wrapper`);
4617
4726
  console.log("");
4618
4727
  console.log("Usage:");
4619
4728
  console.log(" ape-shell Start interactive grant-mediated REPL");
@@ -4671,7 +4780,7 @@ var configCommand = defineCommand44({
4671
4780
  var main = defineCommand44({
4672
4781
  meta: {
4673
4782
  name: "apes",
4674
- version: "0.21.2",
4783
+ version: "0.22.1",
4675
4784
  description: "Unified CLI for OpenApe"
4676
4785
  },
4677
4786
  subCommands: {
@@ -4726,18 +4835,20 @@ async function maybeRefreshAuth() {
4726
4835
  }
4727
4836
  }
4728
4837
  await maybeRefreshAuth();
4838
+ await maybeWarnStaleVersion("0.22.1").catch(() => {
4839
+ });
4729
4840
  runMain(main).catch((err) => {
4730
4841
  if (err instanceof CliExit) {
4731
4842
  process.exit(err.exitCode);
4732
4843
  }
4733
4844
  if (err instanceof CliError) {
4734
- consola35.error(err.message);
4845
+ consola36.error(err.message);
4735
4846
  process.exit(err.exitCode);
4736
4847
  }
4737
4848
  if (debug) {
4738
- consola35.error(err);
4849
+ consola36.error(err);
4739
4850
  } else {
4740
- consola35.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
4851
+ consola36.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
4741
4852
  }
4742
4853
  process.exit(1);
4743
4854
  });