@tokenfactory/acc-runner 0.44.7 → 0.44.9

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.
Files changed (187) hide show
  1. package/README.md +38 -0
  2. package/dist/attachments/codex-stage.d.ts +11 -0
  3. package/dist/attachments/codex-stage.d.ts.map +1 -0
  4. package/dist/attachments/codex-stage.js +82 -0
  5. package/dist/attachments/codex-stage.js.map +1 -0
  6. package/dist/attachments/fetch-attachment.d.ts +17 -0
  7. package/dist/attachments/fetch-attachment.d.ts.map +1 -1
  8. package/dist/attachments/fetch-attachment.js +134 -6
  9. package/dist/attachments/fetch-attachment.js.map +1 -1
  10. package/dist/attachments/index.d.ts +2 -1
  11. package/dist/attachments/index.d.ts.map +1 -1
  12. package/dist/attachments/index.js +2 -1
  13. package/dist/attachments/index.js.map +1 -1
  14. package/dist/build-info.generated.d.ts +1 -1
  15. package/dist/build-info.generated.js +2 -2
  16. package/dist/commit-sha.d.ts +31 -0
  17. package/dist/commit-sha.d.ts.map +1 -0
  18. package/dist/commit-sha.js +31 -0
  19. package/dist/commit-sha.js.map +1 -0
  20. package/dist/companion-run.d.ts.map +1 -1
  21. package/dist/companion-run.js +14 -1
  22. package/dist/companion-run.js.map +1 -1
  23. package/dist/doctor.d.ts +16 -0
  24. package/dist/doctor.d.ts.map +1 -1
  25. package/dist/doctor.js +107 -0
  26. package/dist/doctor.js.map +1 -1
  27. package/dist/engines/codex-chat.d.ts +24 -4
  28. package/dist/engines/codex-chat.d.ts.map +1 -1
  29. package/dist/engines/codex-chat.js +102 -20
  30. package/dist/engines/codex-chat.js.map +1 -1
  31. package/dist/login.d.ts.map +1 -1
  32. package/dist/login.js +7 -1
  33. package/dist/login.js.map +1 -1
  34. package/dist/machine-console/index.d.ts +37 -0
  35. package/dist/machine-console/index.d.ts.map +1 -0
  36. package/dist/machine-console/index.js +263 -0
  37. package/dist/machine-console/index.js.map +1 -0
  38. package/dist/machine-console/policy.d.ts +16 -0
  39. package/dist/machine-console/policy.d.ts.map +1 -0
  40. package/dist/machine-console/policy.js +83 -0
  41. package/dist/machine-console/policy.js.map +1 -0
  42. package/dist/machine-console/redact.d.ts +12 -0
  43. package/dist/machine-console/redact.d.ts.map +1 -0
  44. package/dist/machine-console/redact.js +48 -0
  45. package/dist/machine-console/redact.js.map +1 -0
  46. package/dist/machine-console/transport.d.ts +57 -0
  47. package/dist/machine-console/transport.d.ts.map +1 -0
  48. package/dist/machine-console/transport.js +170 -0
  49. package/dist/machine-console/transport.js.map +1 -0
  50. package/dist/mcp-spawn.d.ts +2 -1
  51. package/dist/mcp-spawn.d.ts.map +1 -1
  52. package/dist/mcp-spawn.js +2 -2
  53. package/dist/mcp-spawn.js.map +1 -1
  54. package/dist/preview-environments/control-plane.d.ts +11 -0
  55. package/dist/preview-environments/control-plane.d.ts.map +1 -0
  56. package/dist/preview-environments/control-plane.js +49 -0
  57. package/dist/preview-environments/control-plane.js.map +1 -0
  58. package/dist/preview-environments/http-provider.d.ts +13 -0
  59. package/dist/preview-environments/http-provider.d.ts.map +1 -0
  60. package/dist/preview-environments/http-provider.js +64 -0
  61. package/dist/preview-environments/http-provider.js.map +1 -0
  62. package/dist/preview-environments/profiles.d.ts +6 -0
  63. package/dist/preview-environments/profiles.d.ts.map +1 -0
  64. package/dist/preview-environments/profiles.js +34 -0
  65. package/dist/preview-environments/profiles.js.map +1 -0
  66. package/dist/preview-environments/safety.d.ts +7 -0
  67. package/dist/preview-environments/safety.d.ts.map +1 -0
  68. package/dist/preview-environments/safety.js +47 -0
  69. package/dist/preview-environments/safety.js.map +1 -0
  70. package/dist/preview-environments/service.d.ts +36 -0
  71. package/dist/preview-environments/service.d.ts.map +1 -0
  72. package/dist/preview-environments/service.js +149 -0
  73. package/dist/preview-environments/service.js.map +1 -0
  74. package/dist/preview-environments/types.d.ts +60 -0
  75. package/dist/preview-environments/types.d.ts.map +1 -0
  76. package/dist/preview-environments/types.js +2 -0
  77. package/dist/preview-environments/types.js.map +1 -0
  78. package/dist/production-migrations/command.d.ts +3 -0
  79. package/dist/production-migrations/command.d.ts.map +1 -0
  80. package/dist/production-migrations/command.js +21 -0
  81. package/dist/production-migrations/command.js.map +1 -0
  82. package/dist/production-migrations/executor.d.ts +59 -0
  83. package/dist/production-migrations/executor.d.ts.map +1 -0
  84. package/dist/production-migrations/executor.js +161 -0
  85. package/dist/production-migrations/executor.js.map +1 -0
  86. package/dist/production-migrations/pg-target.d.ts +22 -0
  87. package/dist/production-migrations/pg-target.d.ts.map +1 -0
  88. package/dist/production-migrations/pg-target.js +87 -0
  89. package/dist/production-migrations/pg-target.js.map +1 -0
  90. package/dist/production-migrations/profiles.d.ts +5 -0
  91. package/dist/production-migrations/profiles.d.ts.map +1 -0
  92. package/dist/production-migrations/profiles.js +39 -0
  93. package/dist/production-migrations/profiles.js.map +1 -0
  94. package/dist/production-migrations/service.d.ts +39 -0
  95. package/dist/production-migrations/service.d.ts.map +1 -0
  96. package/dist/production-migrations/service.js +166 -0
  97. package/dist/production-migrations/service.js.map +1 -0
  98. package/dist/program.d.ts.map +1 -1
  99. package/dist/program.js +17 -0
  100. package/dist/program.js.map +1 -1
  101. package/dist/runtime/worktree.d.ts.map +1 -1
  102. package/dist/runtime/worktree.js +23 -1
  103. package/dist/runtime/worktree.js.map +1 -1
  104. package/dist/secure-execution/auto-lease.d.ts +57 -0
  105. package/dist/secure-execution/auto-lease.d.ts.map +1 -0
  106. package/dist/secure-execution/auto-lease.js +116 -0
  107. package/dist/secure-execution/auto-lease.js.map +1 -0
  108. package/dist/secure-execution/command-policy.d.ts +5 -0
  109. package/dist/secure-execution/command-policy.d.ts.map +1 -0
  110. package/dist/secure-execution/command-policy.js +95 -0
  111. package/dist/secure-execution/command-policy.js.map +1 -0
  112. package/dist/secure-execution/confinement.d.ts +3 -0
  113. package/dist/secure-execution/confinement.d.ts.map +1 -0
  114. package/dist/secure-execution/confinement.js +18 -0
  115. package/dist/secure-execution/confinement.js.map +1 -0
  116. package/dist/secure-execution/engine.d.ts +24 -0
  117. package/dist/secure-execution/engine.d.ts.map +1 -0
  118. package/dist/secure-execution/engine.js +272 -0
  119. package/dist/secure-execution/engine.js.map +1 -0
  120. package/dist/secure-execution/index.d.ts +7 -0
  121. package/dist/secure-execution/index.d.ts.map +1 -0
  122. package/dist/secure-execution/index.js +6 -0
  123. package/dist/secure-execution/index.js.map +1 -0
  124. package/dist/secure-execution/node-pty.d.ts +11 -0
  125. package/dist/secure-execution/node-pty.d.ts.map +1 -0
  126. package/dist/secure-execution/node-pty.js +27 -0
  127. package/dist/secure-execution/node-pty.js.map +1 -0
  128. package/dist/secure-execution/types.d.ts +176 -0
  129. package/dist/secure-execution/types.d.ts.map +1 -0
  130. package/dist/secure-execution/types.js +2 -0
  131. package/dist/secure-execution/types.js.map +1 -0
  132. package/dist/staging-migrations/command.d.ts +2 -0
  133. package/dist/staging-migrations/command.d.ts.map +1 -0
  134. package/dist/staging-migrations/command.js +58 -0
  135. package/dist/staging-migrations/command.js.map +1 -0
  136. package/dist/staging-migrations/executor.d.ts +49 -0
  137. package/dist/staging-migrations/executor.d.ts.map +1 -0
  138. package/dist/staging-migrations/executor.js +125 -0
  139. package/dist/staging-migrations/executor.js.map +1 -0
  140. package/dist/staging-migrations/pg-target.d.ts +20 -0
  141. package/dist/staging-migrations/pg-target.d.ts.map +1 -0
  142. package/dist/staging-migrations/pg-target.js +63 -0
  143. package/dist/staging-migrations/pg-target.js.map +1 -0
  144. package/dist/staging-migrations/profiles.d.ts +5 -0
  145. package/dist/staging-migrations/profiles.d.ts.map +1 -0
  146. package/dist/staging-migrations/profiles.js +31 -0
  147. package/dist/staging-migrations/profiles.js.map +1 -0
  148. package/dist/staging-migrations/service.d.ts +40 -0
  149. package/dist/staging-migrations/service.d.ts.map +1 -0
  150. package/dist/staging-migrations/service.js +157 -0
  151. package/dist/staging-migrations/service.js.map +1 -0
  152. package/dist/supervisor.d.ts +15 -0
  153. package/dist/supervisor.d.ts.map +1 -0
  154. package/dist/supervisor.js +54 -0
  155. package/dist/supervisor.js.map +1 -0
  156. package/dist/terminal/http-carrier.d.ts +17 -0
  157. package/dist/terminal/http-carrier.d.ts.map +1 -0
  158. package/dist/terminal/http-carrier.js +39 -0
  159. package/dist/terminal/http-carrier.js.map +1 -0
  160. package/dist/terminal/pty-adapter.d.ts +88 -0
  161. package/dist/terminal/pty-adapter.d.ts.map +1 -0
  162. package/dist/terminal/pty-adapter.js +252 -0
  163. package/dist/terminal/pty-adapter.js.map +1 -0
  164. package/dist/terminal/transport-contract.d.ts +115 -0
  165. package/dist/terminal/transport-contract.d.ts.map +1 -0
  166. package/dist/terminal/transport-contract.js +2 -0
  167. package/dist/terminal/transport-contract.js.map +1 -0
  168. package/dist/terminal/transport.d.ts +32 -0
  169. package/dist/terminal/transport.d.ts.map +1 -0
  170. package/dist/terminal/transport.js +208 -0
  171. package/dist/terminal/transport.js.map +1 -0
  172. package/dist/watch-chat/wire.d.ts.map +1 -1
  173. package/dist/watch-chat/wire.js +14 -1
  174. package/dist/watch-chat/wire.js.map +1 -1
  175. package/dist/watch.d.ts +3 -0
  176. package/dist/watch.d.ts.map +1 -1
  177. package/dist/watch.js +57 -0
  178. package/dist/watch.js.map +1 -1
  179. package/package.json +6 -1
  180. package/scripts/com.tokenfactory.acc-runner-dedicated.plist +64 -0
  181. package/scripts/com.tokenfactory.acc-runner.plist +90 -0
  182. package/scripts/gen-build-info.d.mts +3 -0
  183. package/scripts/gen-build-info.mjs +72 -0
  184. package/scripts/install-launchd.sh +235 -0
  185. package/scripts/setup-dedicated-runner-darwin.sh +75 -0
  186. package/scripts/setup-dedicated-runner-linux.sh +173 -0
  187. package/scripts/uninstall-launchd.sh +67 -0
@@ -0,0 +1,90 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ macOS LaunchAgent template for `acc-runner watch`.
4
+
5
+ Tokens replaced by install-launchd.sh:
6
+ __CAFFEINATE_PATH__ absolute path to /usr/bin/caffeinate
7
+ __NODE_INVOKER__ absolute path to the node@22 binary (or
8
+ /usr/bin/env when the CLI is a shell shim that
9
+ must resolve node itself through PATH below)
10
+ __ACC_RUNNER_PATH__ absolute path to the acc-runner CLI entrypoint
11
+ __NODE_BIN_DIR__ bin dir of node@22, e.g. /opt/homebrew/opt/node@22/bin
12
+ __ACC_ENV_ENTRIES__ marker line; replaced by the resolved ACC_* env
13
+ entries (ACC_TARGET_REPO, ACC_REPO_PATH,
14
+ ACC_RUNNER_REPOS, ACC_RUNNER_CONCURRENCY), or by
15
+ nothing when none are set
16
+ __HOME__ operator's $HOME
17
+
18
+ Why PATH and the ACC_* vars are baked in rather than inherited:
19
+ launchd does NOT source ~/.zshrc or ~/.zprofile. A job started at login
20
+ gets a bare PATH (/usr/bin:/bin:/usr/sbin:/sbin) and none of the shell's
21
+ exports. On the fleet host ~/.zprofile is what puts node@20 on PATH, so
22
+ an inherited-PATH agent would run the runner under the wrong node (or no
23
+ node at all) and the repo's node-22 pin (docs/acc/NODE_VERSION_PIN.md)
24
+ would be violated on every pre-PR gate. node@22's bin dir is therefore
25
+ FIRST on the PATH string below, ahead of /opt/homebrew/bin where the
26
+ default-linked (node@20) symlink lives.
27
+
28
+ Secrets are deliberately NOT baked in. The plist is a plain 0644 file in
29
+ ~/Library/LaunchAgents; the runner's OAuth tokens stay in the OS keychain
30
+ via keytar, and ANTHROPIC_API_KEY belongs in the keychain-backed config,
31
+ never here.
32
+
33
+ Behavior:
34
+ - caffeinate -dis keeps display, idle, system from sleeping while
35
+ acc-runner is running. Released the moment caffeinate exits, so
36
+ the lid-close behavior of the operator's Mac is unchanged when
37
+ the agent is unloaded.
38
+ - RunAtLoad=true starts the runner at login (and therefore after a
39
+ reboot, once the operator's session opens).
40
+ - KeepAlive=true auto-restarts on crash. launchd applies its
41
+ exponential throttle (default 10s, raised here to 60s) so a
42
+ crash-loop only restarts once a minute.
43
+ - StandardOut/ErrorPath capture the watch loop's stdio so the
44
+ doctor command and ops runbook have a single log location.
45
+ -->
46
+ <plist version="1.0">
47
+ <dict>
48
+ <key>Label</key>
49
+ <string>com.tokenfactory.acc-runner</string>
50
+
51
+ <key>ProgramArguments</key>
52
+ <array>
53
+ <string>__CAFFEINATE_PATH__</string>
54
+ <string>-dis</string>
55
+ <string>__NODE_INVOKER__</string>
56
+ <string>__ACC_RUNNER_PATH__</string>
57
+ <string>watch</string>
58
+ </array>
59
+
60
+ <key>RunAtLoad</key>
61
+ <true/>
62
+
63
+ <key>KeepAlive</key>
64
+ <true/>
65
+
66
+ <key>ThrottleInterval</key>
67
+ <integer>60</integer>
68
+
69
+ <key>ProcessType</key>
70
+ <string>Interactive</string>
71
+
72
+ <key>StandardOutPath</key>
73
+ <string>__HOME__/Library/Logs/acc-runner.out.log</string>
74
+
75
+ <key>StandardErrorPath</key>
76
+ <string>__HOME__/Library/Logs/acc-runner.err.log</string>
77
+
78
+ <key>EnvironmentVariables</key>
79
+ <dict>
80
+ <key>HOME</key>
81
+ <string>__HOME__</string>
82
+ <key>PATH</key>
83
+ <string>__NODE_BIN_DIR__:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
84
+ __ACC_ENV_ENTRIES__
85
+ </dict>
86
+
87
+ <key>WorkingDirectory</key>
88
+ <string>__HOME__</string>
89
+ </dict>
90
+ </plist>
@@ -0,0 +1,3 @@
1
+ // Type surface for the provenance helper in gen-build-info.mjs, so the unit
2
+ // test can import it under NodeNext + strict without allowJs.
3
+ export function commitField(sha: string, statusPorcelain: string): string;
@@ -0,0 +1,72 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const generatedTs = path.join(root, "src", "build-info.generated.ts");
9
+ const generatedJs = path.join(root, "dist", "build-info.generated.js");
10
+
11
+ function git(args) {
12
+ const r = spawnSync("git", args, { cwd: root, encoding: "utf8" });
13
+ return r.status === 0 ? r.stdout.trim() : "unknown";
14
+ }
15
+
16
+ // AC1: HEAD alone can't prove the built tree was clean (a hand-built tree with
17
+ // uncommitted edits still points at a clean commit), so suffix `-dirty` for an
18
+ // unclean tree. Exclude the generated file itself: the `pre` step rewrites it,
19
+ // so by `post` it is ALWAYS modified and counting it would pin every build to
20
+ // `-dirty` and destroy the signal. The dist hash already captures built output.
21
+ export function commitField(sha, statusPorcelain) {
22
+ if (sha === "unknown") return sha;
23
+ const dirty =
24
+ statusPorcelain !== "" &&
25
+ statusPorcelain !== "unknown" &&
26
+ statusPorcelain
27
+ .split("\n")
28
+ .some((line) => line.trim() !== "" && !line.includes("build-info.generated.ts"));
29
+ return dirty ? `${sha}-dirty` : sha;
30
+ }
31
+
32
+ function gitCommit() {
33
+ return commitField(git(["rev-parse", "HEAD"]), git(["status", "--porcelain"]));
34
+ }
35
+
36
+ async function distHash() {
37
+ const dist = path.join(root, "dist");
38
+ const files = [];
39
+ async function walk(dir) {
40
+ for (const ent of await readdir(dir, { withFileTypes: true })) {
41
+ const p = path.join(dir, ent.name);
42
+ if (ent.isDirectory()) await walk(p);
43
+ else if (ent.isFile() && !p.endsWith("build-info.generated.js")) files.push(p);
44
+ }
45
+ }
46
+ await walk(dist);
47
+ const h = createHash("sha256");
48
+ for (const file of files.sort()) {
49
+ h.update(path.relative(dist, file));
50
+ h.update("\0");
51
+ h.update(await readFile(file));
52
+ h.update("\0");
53
+ }
54
+ return h.digest("hex");
55
+ }
56
+
57
+ function ts(commitSha, distSha256) {
58
+ return `// Generated by scripts/gen-build-info.mjs. Do not edit by hand.\n` +
59
+ `export const BUILD_COMMIT_SHA = ${JSON.stringify(commitSha)};\n` +
60
+ `export const BUILD_DIST_SHA256 = ${JSON.stringify(distSha256)};\n`;
61
+ }
62
+
63
+ // File-writing steps run only when invoked directly (`node …/gen-build-info.mjs
64
+ // pre|post`); importing the module (e.g. from tests) is side-effect free.
65
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
66
+ if (process.argv[2] === "post") {
67
+ await writeFile(generatedJs, ts(gitCommit(), await distHash()));
68
+ await writeFile(generatedTs, ts("unknown", "unknown"));
69
+ } else {
70
+ await writeFile(generatedTs, ts(gitCommit(), "pending-build"));
71
+ }
72
+ }
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Install the acc-runner macOS LaunchAgent so `acc-runner watch` survives
4
+ # reboot, logout and crashes.
5
+ #
6
+ # Renders ./com.tokenfactory.acc-runner.plist into
7
+ # ~/Library/LaunchAgents and loads it via launchctl. Idempotent:
8
+ # re-running unloads any prior copy first.
9
+ #
10
+ # What it bakes into the plist (launchd sources NEITHER ~/.zshrc NOR
11
+ # ~/.zprofile, so nothing can be inherited):
12
+ # - PATH with node@22's bin dir FIRST, ahead of /opt/homebrew/bin where
13
+ # the .zprofile-pinned node@20 symlink lives. The repo pin is node 22
14
+ # (docs/acc/NODE_VERSION_PIN.md) and the pre-PR gate runs under it.
15
+ # - the ACC_* config vars: ACC_TARGET_REPO, ACC_REPO_PATH,
16
+ # ACC_RUNNER_REPOS, ACC_RUNNER_CONCURRENCY.
17
+ #
18
+ # Value resolution, per variable (first hit wins; unresolved = omitted,
19
+ # so the runner's own config.json/server defaults still apply):
20
+ # 1. the environment this script is invoked with
21
+ # 2. $ACC_RUNNER_LAUNCHD_ENV (default ~/.config/acc-runner/launchd.env),
22
+ # a KEY=VALUE file
23
+ # 3. ~/.config/acc-runner/config.json ("repo-path", "repos")
24
+ # ACC_RUNNER_CONCURRENCY is never defaulted: an env value outranks the
25
+ # operator's server-set concurrency, so it is baked only when explicit.
26
+ #
27
+ # Secrets are never written to the plist — tokens live in the OS keychain.
28
+ #
29
+ # Usage:
30
+ # packages/acc-runner/scripts/install-launchd.sh
31
+ # packages/acc-runner/scripts/install-launchd.sh --no-load # render only
32
+ # packages/acc-runner/scripts/install-launchd.sh --help
33
+ #
34
+ # Rollback: packages/acc-runner/scripts/uninstall-launchd.sh
35
+ # Docs: docs/acc/RUNNER_LAUNCHD.md
36
+
37
+ set -euo pipefail
38
+
39
+ NODE_MAJOR=22
40
+ LABEL="com.tokenfactory.acc-runner"
41
+ DO_LOAD=1
42
+
43
+ for arg in "$@"; do
44
+ case "$arg" in
45
+ --no-load) DO_LOAD=0 ;;
46
+ -h|--help) awk 'NR>=2 { if ($0 !~ /^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
47
+ *) echo "install-launchd.sh: unknown option '$arg' (see --help)" >&2; exit 2 ;;
48
+ esac
49
+ done
50
+
51
+ if [[ "$(uname)" != "Darwin" ]]; then
52
+ echo "install-launchd.sh: macOS-only (uname=$(uname))" >&2
53
+ exit 1
54
+ fi
55
+
56
+ CAFFEINATE_PATH="$(command -v caffeinate || true)"
57
+ if [[ -z "$CAFFEINATE_PATH" ]]; then
58
+ echo "install-launchd.sh: caffeinate not found in PATH" >&2
59
+ exit 1
60
+ fi
61
+
62
+ ACC_RUNNER_PATH="$(command -v acc-runner || true)"
63
+ if [[ -z "$ACC_RUNNER_PATH" ]]; then
64
+ echo "install-launchd.sh: acc-runner not found in PATH" >&2
65
+ echo " install globally first: pnpm add -g @tokenfactory/acc-runner" >&2
66
+ exit 1
67
+ fi
68
+
69
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
70
+ TEMPLATE="$SCRIPT_DIR/com.tokenfactory.acc-runner.plist"
71
+ if [[ ! -f "$TEMPLATE" ]]; then
72
+ echo "install-launchd.sh: template missing at $TEMPLATE" >&2
73
+ exit 1
74
+ fi
75
+
76
+ # ---------------------------------------------------------------- node@22
77
+
78
+ node_major_of() { "$1" -v 2>/dev/null | sed -n 's/^v\([0-9][0-9]*\).*/\1/p'; }
79
+
80
+ resolve_node_bin_dir() {
81
+ local candidates=() d cur
82
+ [[ -n "${ACC_RUNNER_NODE_BIN_DIR:-}" ]] && candidates+=("$ACC_RUNNER_NODE_BIN_DIR")
83
+ candidates+=("/opt/homebrew/opt/node@${NODE_MAJOR}/bin" "/usr/local/opt/node@${NODE_MAJOR}/bin")
84
+ # Version managers found on runner hosts. Unmatched globs stay literal and
85
+ # are dropped by the -d test below.
86
+ for d in "${NVM_DIR:-$HOME/.nvm}/versions/node/v${NODE_MAJOR}."*/bin \
87
+ "${FNM_DIR:-$HOME/.local/share/fnm}/node-versions/v${NODE_MAJOR}."*/installation/bin \
88
+ "$HOME/.volta/tools/image/node/${NODE_MAJOR}."*/bin; do
89
+ [[ -d "$d" ]] && candidates+=("$d")
90
+ done
91
+ cur="$(command -v node || true)"
92
+ [[ -n "$cur" ]] && candidates+=("$(dirname "$cur")")
93
+
94
+ for d in "${candidates[@]}"; do
95
+ [[ -x "$d/node" ]] || continue
96
+ [[ "$(node_major_of "$d/node")" == "$NODE_MAJOR" ]] || continue
97
+ printf '%s' "$d"
98
+ return 0
99
+ done
100
+ return 1
101
+ }
102
+
103
+ NODE_BIN_DIR="$(resolve_node_bin_dir || true)"
104
+ if [[ -z "$NODE_BIN_DIR" ]]; then
105
+ echo "install-launchd.sh: no node ${NODE_MAJOR} found on this host." >&2
106
+ echo " The repo pins node ${NODE_MAJOR} (docs/acc/NODE_VERSION_PIN.md); the agent must not" >&2
107
+ echo " run the .zprofile-pinned node@20. Install one, e.g.:" >&2
108
+ echo " brew install node@${NODE_MAJOR} # or: nvm install ${NODE_MAJOR}" >&2
109
+ echo " or point ACC_RUNNER_NODE_BIN_DIR at an existing install and re-run." >&2
110
+ exit 1
111
+ fi
112
+
113
+ # Resolve the CLI to a real file so we can decide how to invoke it: a JS
114
+ # entrypoint is run by node@22 directly; a shell shim (pnpm) must run itself
115
+ # and pick node up from the baked PATH.
116
+ real_path_of() {
117
+ local p="$1" target
118
+ while [[ -L "$p" ]]; do
119
+ target="$(readlink "$p")"
120
+ case "$target" in
121
+ /*) p="$target" ;;
122
+ *) p="$(cd "$(dirname "$p")" && cd "$(dirname "$target")" && pwd)/$(basename "$target")" ;;
123
+ esac
124
+ done
125
+ printf '%s' "$p"
126
+ }
127
+
128
+ ACC_RUNNER_ENTRY="$(real_path_of "$ACC_RUNNER_PATH")"
129
+ case "$ACC_RUNNER_ENTRY" in
130
+ *.js|*.mjs|*.cjs) NODE_INVOKER="$NODE_BIN_DIR/node" ;;
131
+ *) NODE_INVOKER="/usr/bin/env"; ACC_RUNNER_ENTRY="$ACC_RUNNER_PATH" ;;
132
+ esac
133
+
134
+ # ------------------------------------------------------------- ACC_* vars
135
+
136
+ ENV_FILE="${ACC_RUNNER_LAUNCHD_ENV:-$HOME/.config/acc-runner/launchd.env}"
137
+
138
+ from_env_file() { # $1 = key
139
+ [[ -f "$ENV_FILE" ]] || return 0
140
+ sed -n "s/^[[:space:]]*$1=//p" "$ENV_FILE" | tail -n1 | sed 's/^"\(.*\)"$/\1/'
141
+ }
142
+
143
+ from_config_json() { # $1 = config.json field; arrays are joined with commas
144
+ "$NODE_BIN_DIR/node" -e '
145
+ const fs = require("node:fs"), os = require("node:os"), p = require("node:path");
146
+ const base = process.env.XDG_CONFIG_HOME || p.join(os.homedir(), ".config");
147
+ try {
148
+ const j = JSON.parse(fs.readFileSync(p.join(base, "acc-runner", "config.json"), "utf8"));
149
+ const v = j[process.argv[1]];
150
+ if (Array.isArray(v)) process.stdout.write(v.filter(Boolean).join(","));
151
+ else if (typeof v === "string") process.stdout.write(v);
152
+ else if (typeof v === "number") process.stdout.write(String(v));
153
+ } catch { /* absent or unreadable config.json → no value */ }
154
+ ' "$1" 2>/dev/null || true
155
+ }
156
+
157
+ # Prints the first non-empty argument. Always succeeds (an all-empty list is
158
+ # a normal "not configured" outcome, not an error under `set -e`).
159
+ first_nonempty() { local v; for v in "$@"; do [[ -n "$v" ]] && { printf '%s' "$v"; break; }; done; return 0; }
160
+
161
+ VAL_TARGET_REPO="$(first_nonempty "${ACC_TARGET_REPO:-}" "$(from_env_file ACC_TARGET_REPO)")"
162
+ VAL_REPO_PATH="$(first_nonempty "${ACC_REPO_PATH:-}" "$(from_env_file ACC_REPO_PATH)" "$(from_config_json repo-path)")"
163
+ VAL_REPOS="$(first_nonempty "${ACC_RUNNER_REPOS:-}" "$(from_env_file ACC_RUNNER_REPOS)" "$(from_config_json repos)")"
164
+ VAL_CONCURRENCY="$(first_nonempty "${ACC_RUNNER_CONCURRENCY:-}" "$(from_env_file ACC_RUNNER_CONCURRENCY)")"
165
+
166
+ ENTRIES_FILE="$(mktemp -t acc-runner-launchd-env)"
167
+ trap 'rm -f "$ENTRIES_FILE" "${ENTRIES_FILE}.plist"' EXIT
168
+
169
+ xml_escape() { printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'; }
170
+
171
+ add_entry() { # $1 = key, $2 = value (skipped when empty)
172
+ [[ -n "$2" ]] || { echo " - $1: not set (omitted; runner falls back to config.json / server default)"; return 0; }
173
+ printf ' <key>%s</key>\n <string>%s</string>\n' "$1" "$(xml_escape "$2")" >> "$ENTRIES_FILE"
174
+ echo " - $1=$2"
175
+ }
176
+
177
+ echo "Baking environment into $LABEL:"
178
+ echo " - PATH: $NODE_BIN_DIR (node $("$NODE_BIN_DIR/node" -v)) first, then Homebrew + system dirs"
179
+ add_entry ACC_TARGET_REPO "$VAL_TARGET_REPO"
180
+ add_entry ACC_REPO_PATH "$VAL_REPO_PATH"
181
+ add_entry ACC_RUNNER_REPOS "$VAL_REPOS"
182
+ add_entry ACC_RUNNER_CONCURRENCY "$VAL_CONCURRENCY"
183
+
184
+ # ----------------------------------------------------------------- render
185
+
186
+ TARGET_DIR="$HOME/Library/LaunchAgents"
187
+ TARGET="$TARGET_DIR/$LABEL.plist"
188
+ LOG_DIR="$HOME/Library/Logs"
189
+ mkdir -p "$TARGET_DIR" "$LOG_DIR"
190
+
191
+ # Escape sed replacement slashes so paths containing `/` substitute cleanly.
192
+ esc() { printf '%s' "$1" | sed 's#[\\/&]#\\&#g'; }
193
+
194
+ sed \
195
+ -e "s/__CAFFEINATE_PATH__/$(esc "$CAFFEINATE_PATH")/g" \
196
+ -e "s/__NODE_INVOKER__/$(esc "$NODE_INVOKER")/g" \
197
+ -e "s/__ACC_RUNNER_PATH__/$(esc "$ACC_RUNNER_ENTRY")/g" \
198
+ -e "s/__NODE_BIN_DIR__/$(esc "$NODE_BIN_DIR")/g" \
199
+ -e "s/__HOME__/$(esc "$HOME")/g" \
200
+ "$TEMPLATE" > "${ENTRIES_FILE}.plist"
201
+
202
+ # Replace the marker LINE with the generated entries (or with nothing). The
203
+ # pattern is anchored so the token's mention in the template's comment block
204
+ # is left alone.
205
+ awk -v entries="$ENTRIES_FILE" '
206
+ /^[[:space:]]*__ACC_ENV_ENTRIES__[[:space:]]*$/ {
207
+ while ((getline line < entries) > 0) print line
208
+ close(entries)
209
+ next
210
+ }
211
+ { print }
212
+ ' "${ENTRIES_FILE}.plist" > "$TARGET"
213
+
214
+ plutil -lint "$TARGET" >/dev/null || { echo "install-launchd.sh: rendered plist is invalid: $TARGET" >&2; exit 1; }
215
+
216
+ echo "Installed $TARGET"
217
+
218
+ # ------------------------------------------------------------------- load
219
+
220
+ if [[ "$DO_LOAD" -eq 0 ]]; then
221
+ echo "Skipped launchctl load (--no-load). Load it with:"
222
+ echo " launchctl unload \"$TARGET\" 2>/dev/null; launchctl load \"$TARGET\""
223
+ exit 0
224
+ fi
225
+
226
+ if launchctl list | awk '{print $3}' | grep -qx "$LABEL"; then
227
+ launchctl unload "$TARGET" 2>/dev/null || true
228
+ fi
229
+ launchctl load "$TARGET"
230
+
231
+ echo "Loaded $LABEL via launchctl"
232
+ echo "Logs: $LOG_DIR/acc-runner.{out,err}.log"
233
+ echo "Verify: launchctl list | grep $LABEL # PID in col 1, exit code 0 in col 2"
234
+ echo "Verify node: grep -A2 '<key>PATH</key>' \"$TARGET\""
235
+ echo "Rollback: $SCRIPT_DIR/uninstall-launchd.sh"
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # v0.13-DEDICATED-RUNNER — set up a headless Mac mini as the
4
+ # acc-runner host.
5
+ #
6
+ # Idempotent: every step short-circuits if already applied.
7
+ # * pmset values are read first; only written if they differ.
8
+ # * launchd plist is unloaded before reload.
9
+ # * keychain unlock is best-effort (logs a hint on failure).
10
+ #
11
+ # Pairs with com.tokenfactory.acc-runner-dedicated.plist. See
12
+ # docs/acc/DEDICATED_RUNNER_RUNBOOK.md.
13
+
14
+ set -euo pipefail
15
+
16
+ if [[ "$(uname)" != "Darwin" ]]; then
17
+ echo "setup-dedicated-runner-darwin.sh: macOS-only (uname=$(uname))" >&2
18
+ exit 1
19
+ fi
20
+
21
+ CAFFEINATE_PATH="$(command -v caffeinate || true)"
22
+ ACC_RUNNER_PATH="$(command -v acc-runner || true)"
23
+ if [[ -z "$CAFFEINATE_PATH" || -z "$ACC_RUNNER_PATH" ]]; then
24
+ echo "Missing prerequisites: caffeinate=$CAFFEINATE_PATH acc-runner=$ACC_RUNNER_PATH" >&2
25
+ echo "Install acc-runner first: pnpm add -g @tokenfactory/acc-runner" >&2
26
+ exit 1
27
+ fi
28
+
29
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
30
+ TEMPLATE="$SCRIPT_DIR/com.tokenfactory.acc-runner-dedicated.plist"
31
+ if [[ ! -f "$TEMPLATE" ]]; then
32
+ echo "Template missing: $TEMPLATE" >&2
33
+ exit 1
34
+ fi
35
+
36
+ LABEL="com.tokenfactory.acc-runner-dedicated"
37
+ TARGET_DIR="$HOME/Library/LaunchAgents"
38
+ TARGET="$TARGET_DIR/$LABEL.plist"
39
+ LOG_DIR="$HOME/Library/Logs"
40
+ mkdir -p "$TARGET_DIR" "$LOG_DIR"
41
+
42
+ # 1. Power management — disable sleep on AC, disable Power Nap, restart on power loss.
43
+ pmset_set() {
44
+ local scope="$1" key="$2" want="$3"
45
+ local cur
46
+ cur="$(pmset -g custom 2>/dev/null | awk -v k="$key" '$1==k {print $2; exit}')"
47
+ if [[ "$cur" != "$want" ]]; then
48
+ echo "pmset $scope $key $want (was: ${cur:-unset})"
49
+ sudo pmset "$scope" "$key" "$want"
50
+ fi
51
+ }
52
+ pmset_set -c sleep 0
53
+ pmset_set -c disksleep 0
54
+ pmset_set -a powernap 0
55
+ pmset_set -a autorestart 1
56
+
57
+ # 2. Render the dedicated plist with absolute paths.
58
+ esc() { printf '%s' "$1" | sed 's#[\\/&]#\\&#g'; }
59
+ sed \
60
+ -e "s/__CAFFEINATE_PATH__/$(esc "$CAFFEINATE_PATH")/g" \
61
+ -e "s/__ACC_RUNNER_PATH__/$(esc "$ACC_RUNNER_PATH")/g" \
62
+ -e "s/__HOME__/$(esc "$HOME")/g" \
63
+ "$TEMPLATE" > "$TARGET"
64
+
65
+ # 3. Reload launchd (unload first; ignore missing).
66
+ if launchctl list 2>/dev/null | awk '{print $3}' | grep -qx "$LABEL"; then
67
+ launchctl unload "$TARGET" 2>/dev/null || true
68
+ fi
69
+ launchctl load "$TARGET"
70
+
71
+ echo "Installed $TARGET"
72
+ echo "Loaded $LABEL via launchctl"
73
+ echo "Logs: $LOG_DIR/acc-runner-dedicated.{out,err}.log"
74
+ echo "Verify: acc-runner doctor"
75
+ echo "Uninstall: launchctl unload \"$TARGET\" && rm \"$TARGET\""
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # v0.13-DEDICATED-RUNNER — set up a Linux VPS (or mini-PC) as the
4
+ # acc-runner host via a systemd user unit.
5
+ #
6
+ # Idempotent: re-running rewrites the unit file and restarts the
7
+ # service. enable-linger is checked before re-enabling so SSH-less
8
+ # operators don't see a spurious prompt.
9
+ #
10
+ # RUNNER-LINUX-SUPERVISOR-1: `--instance <name>` supervises a NAMED
11
+ # runner instance. The name forks the unit (acc-runner-<name>.service),
12
+ # the ExecStart (`watch --instance <name>`) and ACC_RUNNER_INSTANCE, so
13
+ # several named instances coexist on one host instead of overwriting the
14
+ # single acc-runner.service they used to share. Without the flag every
15
+ # rendered byte is what it was before — existing hosts are unaffected.
16
+ #
17
+ # Usage:
18
+ # packages/acc-runner/scripts/setup-dedicated-runner-linux.sh
19
+ # packages/acc-runner/scripts/setup-dedicated-runner-linux.sh --instance staging-smoke
20
+ # packages/acc-runner/scripts/setup-dedicated-runner-linux.sh --help
21
+ #
22
+ # Secrets are never written to the unit — tokens live in the OS keychain
23
+ # (per instance: `acc-runner login --instance <name>`).
24
+ #
25
+ # Verify: acc-runner doctor --instance <name> (checks this unit)
26
+ # See docs/acc/DEDICATED_RUNNER_RUNBOOK.md.
27
+
28
+ set -euo pipefail
29
+
30
+ INSTANCE=""
31
+
32
+ while [[ $# -gt 0 ]]; do
33
+ case "$1" in
34
+ --instance)
35
+ INSTANCE="${2:-}"
36
+ [[ -n "$INSTANCE" ]] || { echo "setup-dedicated-runner-linux.sh: --instance needs a name" >&2; exit 2; }
37
+ shift 2
38
+ ;;
39
+ --instance=*) INSTANCE="${1#*=}"; shift ;;
40
+ -h|--help)
41
+ awk 'NR>=2 { if ($0 !~ /^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"
42
+ exit 0
43
+ ;;
44
+ *) echo "setup-dedicated-runner-linux.sh: unknown option '$1' (see --help)" >&2; exit 2 ;;
45
+ esac
46
+ done
47
+
48
+ # Mirrors INSTANCE_NAME_RE in packages/acc-runner/src/instance.ts. The name
49
+ # lands in a unit name and a keychain account, so nothing escapable is allowed.
50
+ if [[ -n "$INSTANCE" && ! "$INSTANCE" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]]; then
51
+ echo "setup-dedicated-runner-linux.sh: invalid --instance '$INSTANCE'" >&2
52
+ echo " must match ^[a-z0-9][a-z0-9-]{0,31}$ (lowercase, digits, dashes)" >&2
53
+ exit 2
54
+ fi
55
+
56
+ if [[ "$(uname)" != "Linux" ]]; then
57
+ echo "setup-dedicated-runner-linux.sh: Linux-only (uname=$(uname))" >&2
58
+ exit 1
59
+ fi
60
+
61
+ ACC_RUNNER_PATH="$(command -v acc-runner || true)"
62
+ if [[ -z "$ACC_RUNNER_PATH" ]]; then
63
+ echo "acc-runner not found in PATH. Install: sudo npm install -g @tokenfactory/acc-runner" >&2
64
+ exit 1
65
+ fi
66
+
67
+ if ! command -v systemctl >/dev/null 2>&1; then
68
+ echo "systemctl not found; this script requires systemd." >&2
69
+ exit 1
70
+ fi
71
+
72
+ UNIT_DIR="$HOME/.config/systemd/user"
73
+ SERVICE="acc-runner${INSTANCE:+-$INSTANCE}"
74
+ UNIT="$UNIT_DIR/$SERVICE.service"
75
+ LOG_HINT="journalctl --user -u $SERVICE -f"
76
+ mkdir -p "$UNIT_DIR"
77
+
78
+ # 1. Render the unit file. systemd-inhibit blocks logind's automatic idle
79
+ # action while the runner is active (a no-op on always-on VPS instances,
80
+ # real protection on a desktop/mini-PC).
81
+ #
82
+ # `--what=idle`, NOT `idle:sleep`. The two halves are separate polkit
83
+ # actions and they do not have the same default:
84
+ #
85
+ # org.freedesktop.login1.inhibit-block-idle allow_any=yes
86
+ # org.freedesktop.login1.inhibit-block-sleep allow_any=no
87
+ #
88
+ # This unit runs as a child of a lingering user@<uid>.service, which has no
89
+ # logind session, so polkit evaluates the `allow_any` branch. Requesting
90
+ # `idle:sleep` therefore took the sleep action's `no`, systemd-inhibit
91
+ # exited 1 BEFORE exec'ing the runner, and Restart=always turned that into a
92
+ # permanent 60s restart loop in which acc-runner never ran at all (observed
93
+ # on a live VPS: `Failed to inhibit: Access denied`, CPU 3ms per attempt).
94
+ # `idle` alone is allow_any=yes, so it succeeds unprivileged and
95
+ # session-less on the stock policy.
96
+ #
97
+ # Dropping `sleep` costs nothing a runner host wants: it only blocked an
98
+ # explicit `systemctl suspend`, while `idle` is what blocks the automatic
99
+ # IdleAction suspend the runbook actually describes.
100
+ #
101
+ # Deliberately NOT gated on an install-time capability probe. The installer
102
+ # runs in the operator's logind session, which is `active`, so a probe would
103
+ # take polkit's allow_active branch and pass -- then the unit runs
104
+ # session-less at boot and fails anyway. A probe would ship this outage
105
+ # under the appearance of a fix.
106
+ WATCH_ARGS="watch${INSTANCE:+ --instance $INSTANCE}"
107
+ INHIBIT_PATH="$(command -v systemd-inhibit || true)"
108
+ if [[ -n "$INHIBIT_PATH" ]]; then
109
+ EXEC_LINE="ExecStart=$INHIBIT_PATH --what=idle --who=$SERVICE --why=ACC-runner-watch $ACC_RUNNER_PATH $WATCH_ARGS"
110
+ else
111
+ EXEC_LINE="ExecStart=$ACC_RUNNER_PATH $WATCH_ARGS"
112
+ fi
113
+
114
+ # ACC_RUNNER_INSTANCE is exported into the unit so every resolver the watch
115
+ # process spawns (keychain account, cache root, machine string) agrees with the
116
+ # ExecStart flag. Appended last, so the unnamed render is unchanged.
117
+ # packages/acc-runner/src/cli.ts is `#!/usr/bin/env node`, so an absolute
118
+ # ACC_RUNNER_PATH in ExecStart still resolves `node` against the unit's OWN
119
+ # PATH. A node from nvm/fnm/volta lives under $HOME and is not on the system
120
+ # path below, which exits 127 -- another silent restart loop, and the next
121
+ # failure after the inhibit one on any host that did not install node via apt.
122
+ # install-launchd.sh already solves this on macOS (resolve_node_bin_dir); this
123
+ # is the Linux counterpart, kept minimal: the installer runs in the operator's
124
+ # shell, where the intended node is already resolvable.
125
+ UNIT_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
126
+ NODE_BIN_DIR="$(cd "$(dirname "$(command -v node)")" 2>/dev/null && pwd || true)"
127
+ case ":$UNIT_PATH:" in
128
+ *":$NODE_BIN_DIR:"*) ;;
129
+ *) [[ -n "$NODE_BIN_DIR" ]] && UNIT_PATH="$NODE_BIN_DIR:$UNIT_PATH" ;;
130
+ esac
131
+
132
+ ENV_LINES="Environment=ACC_RUNNER_DEDICATED=1
133
+ Environment=PATH=$UNIT_PATH"
134
+ if [[ -n "$INSTANCE" ]]; then
135
+ ENV_LINES="$ENV_LINES
136
+ Environment=ACC_RUNNER_INSTANCE=$INSTANCE"
137
+ fi
138
+
139
+ cat > "$UNIT" <<EOF
140
+ [Unit]
141
+ Description=ACC runner (watch loop)${INSTANCE:+ — instance $INSTANCE}
142
+ Documentation=https://github.com/tokenfactory-pvt-ltd/agent-control-center
143
+ After=network-online.target
144
+ Wants=network-online.target
145
+
146
+ [Service]
147
+ Type=simple
148
+ $EXEC_LINE
149
+ Restart=always
150
+ RestartSec=60s
151
+ StandardOutput=journal
152
+ StandardError=journal
153
+ $ENV_LINES
154
+
155
+ [Install]
156
+ WantedBy=default.target
157
+ EOF
158
+
159
+ # 2. enable-linger so the unit survives the operator's SSH disconnect.
160
+ if ! loginctl show-user "$USER" 2>/dev/null | grep -q "Linger=yes"; then
161
+ echo "Enabling linger for $USER (requires sudo)..."
162
+ sudo loginctl enable-linger "$USER"
163
+ fi
164
+
165
+ # 3. Reload + start.
166
+ systemctl --user daemon-reload
167
+ systemctl --user enable --now "$SERVICE.service"
168
+
169
+ echo "Installed $UNIT"
170
+ echo "Service active: $(systemctl --user is-active "$SERVICE.service")"
171
+ echo "Logs: $LOG_HINT"
172
+ echo "Verify: acc-runner doctor${INSTANCE:+ --instance $INSTANCE}"
173
+ echo "Uninstall: systemctl --user disable --now $SERVICE && rm \"$UNIT\" && sudo loginctl disable-linger $USER"