@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.
- package/README.md +38 -0
- package/dist/attachments/codex-stage.d.ts +11 -0
- package/dist/attachments/codex-stage.d.ts.map +1 -0
- package/dist/attachments/codex-stage.js +82 -0
- package/dist/attachments/codex-stage.js.map +1 -0
- package/dist/attachments/fetch-attachment.d.ts +17 -0
- package/dist/attachments/fetch-attachment.d.ts.map +1 -1
- package/dist/attachments/fetch-attachment.js +134 -6
- package/dist/attachments/fetch-attachment.js.map +1 -1
- package/dist/attachments/index.d.ts +2 -1
- package/dist/attachments/index.d.ts.map +1 -1
- package/dist/attachments/index.js +2 -1
- package/dist/attachments/index.js.map +1 -1
- package/dist/build-info.generated.d.ts +1 -1
- package/dist/build-info.generated.js +2 -2
- package/dist/commit-sha.d.ts +31 -0
- package/dist/commit-sha.d.ts.map +1 -0
- package/dist/commit-sha.js +31 -0
- package/dist/commit-sha.js.map +1 -0
- package/dist/companion-run.d.ts.map +1 -1
- package/dist/companion-run.js +14 -1
- package/dist/companion-run.js.map +1 -1
- package/dist/doctor.d.ts +16 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +107 -0
- package/dist/doctor.js.map +1 -1
- package/dist/engines/codex-chat.d.ts +24 -4
- package/dist/engines/codex-chat.d.ts.map +1 -1
- package/dist/engines/codex-chat.js +102 -20
- package/dist/engines/codex-chat.js.map +1 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +7 -1
- package/dist/login.js.map +1 -1
- package/dist/machine-console/index.d.ts +37 -0
- package/dist/machine-console/index.d.ts.map +1 -0
- package/dist/machine-console/index.js +263 -0
- package/dist/machine-console/index.js.map +1 -0
- package/dist/machine-console/policy.d.ts +16 -0
- package/dist/machine-console/policy.d.ts.map +1 -0
- package/dist/machine-console/policy.js +83 -0
- package/dist/machine-console/policy.js.map +1 -0
- package/dist/machine-console/redact.d.ts +12 -0
- package/dist/machine-console/redact.d.ts.map +1 -0
- package/dist/machine-console/redact.js +48 -0
- package/dist/machine-console/redact.js.map +1 -0
- package/dist/machine-console/transport.d.ts +57 -0
- package/dist/machine-console/transport.d.ts.map +1 -0
- package/dist/machine-console/transport.js +170 -0
- package/dist/machine-console/transport.js.map +1 -0
- package/dist/mcp-spawn.d.ts +2 -1
- package/dist/mcp-spawn.d.ts.map +1 -1
- package/dist/mcp-spawn.js +2 -2
- package/dist/mcp-spawn.js.map +1 -1
- package/dist/preview-environments/control-plane.d.ts +11 -0
- package/dist/preview-environments/control-plane.d.ts.map +1 -0
- package/dist/preview-environments/control-plane.js +49 -0
- package/dist/preview-environments/control-plane.js.map +1 -0
- package/dist/preview-environments/http-provider.d.ts +13 -0
- package/dist/preview-environments/http-provider.d.ts.map +1 -0
- package/dist/preview-environments/http-provider.js +64 -0
- package/dist/preview-environments/http-provider.js.map +1 -0
- package/dist/preview-environments/profiles.d.ts +6 -0
- package/dist/preview-environments/profiles.d.ts.map +1 -0
- package/dist/preview-environments/profiles.js +34 -0
- package/dist/preview-environments/profiles.js.map +1 -0
- package/dist/preview-environments/safety.d.ts +7 -0
- package/dist/preview-environments/safety.d.ts.map +1 -0
- package/dist/preview-environments/safety.js +47 -0
- package/dist/preview-environments/safety.js.map +1 -0
- package/dist/preview-environments/service.d.ts +36 -0
- package/dist/preview-environments/service.d.ts.map +1 -0
- package/dist/preview-environments/service.js +149 -0
- package/dist/preview-environments/service.js.map +1 -0
- package/dist/preview-environments/types.d.ts +60 -0
- package/dist/preview-environments/types.d.ts.map +1 -0
- package/dist/preview-environments/types.js +2 -0
- package/dist/preview-environments/types.js.map +1 -0
- package/dist/production-migrations/command.d.ts +3 -0
- package/dist/production-migrations/command.d.ts.map +1 -0
- package/dist/production-migrations/command.js +21 -0
- package/dist/production-migrations/command.js.map +1 -0
- package/dist/production-migrations/executor.d.ts +59 -0
- package/dist/production-migrations/executor.d.ts.map +1 -0
- package/dist/production-migrations/executor.js +161 -0
- package/dist/production-migrations/executor.js.map +1 -0
- package/dist/production-migrations/pg-target.d.ts +22 -0
- package/dist/production-migrations/pg-target.d.ts.map +1 -0
- package/dist/production-migrations/pg-target.js +87 -0
- package/dist/production-migrations/pg-target.js.map +1 -0
- package/dist/production-migrations/profiles.d.ts +5 -0
- package/dist/production-migrations/profiles.d.ts.map +1 -0
- package/dist/production-migrations/profiles.js +39 -0
- package/dist/production-migrations/profiles.js.map +1 -0
- package/dist/production-migrations/service.d.ts +39 -0
- package/dist/production-migrations/service.d.ts.map +1 -0
- package/dist/production-migrations/service.js +166 -0
- package/dist/production-migrations/service.js.map +1 -0
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +17 -0
- package/dist/program.js.map +1 -1
- package/dist/runtime/worktree.d.ts.map +1 -1
- package/dist/runtime/worktree.js +23 -1
- package/dist/runtime/worktree.js.map +1 -1
- package/dist/secure-execution/auto-lease.d.ts +57 -0
- package/dist/secure-execution/auto-lease.d.ts.map +1 -0
- package/dist/secure-execution/auto-lease.js +116 -0
- package/dist/secure-execution/auto-lease.js.map +1 -0
- package/dist/secure-execution/command-policy.d.ts +5 -0
- package/dist/secure-execution/command-policy.d.ts.map +1 -0
- package/dist/secure-execution/command-policy.js +95 -0
- package/dist/secure-execution/command-policy.js.map +1 -0
- package/dist/secure-execution/confinement.d.ts +3 -0
- package/dist/secure-execution/confinement.d.ts.map +1 -0
- package/dist/secure-execution/confinement.js +18 -0
- package/dist/secure-execution/confinement.js.map +1 -0
- package/dist/secure-execution/engine.d.ts +24 -0
- package/dist/secure-execution/engine.d.ts.map +1 -0
- package/dist/secure-execution/engine.js +272 -0
- package/dist/secure-execution/engine.js.map +1 -0
- package/dist/secure-execution/index.d.ts +7 -0
- package/dist/secure-execution/index.d.ts.map +1 -0
- package/dist/secure-execution/index.js +6 -0
- package/dist/secure-execution/index.js.map +1 -0
- package/dist/secure-execution/node-pty.d.ts +11 -0
- package/dist/secure-execution/node-pty.d.ts.map +1 -0
- package/dist/secure-execution/node-pty.js +27 -0
- package/dist/secure-execution/node-pty.js.map +1 -0
- package/dist/secure-execution/types.d.ts +176 -0
- package/dist/secure-execution/types.d.ts.map +1 -0
- package/dist/secure-execution/types.js +2 -0
- package/dist/secure-execution/types.js.map +1 -0
- package/dist/staging-migrations/command.d.ts +2 -0
- package/dist/staging-migrations/command.d.ts.map +1 -0
- package/dist/staging-migrations/command.js +58 -0
- package/dist/staging-migrations/command.js.map +1 -0
- package/dist/staging-migrations/executor.d.ts +49 -0
- package/dist/staging-migrations/executor.d.ts.map +1 -0
- package/dist/staging-migrations/executor.js +125 -0
- package/dist/staging-migrations/executor.js.map +1 -0
- package/dist/staging-migrations/pg-target.d.ts +20 -0
- package/dist/staging-migrations/pg-target.d.ts.map +1 -0
- package/dist/staging-migrations/pg-target.js +63 -0
- package/dist/staging-migrations/pg-target.js.map +1 -0
- package/dist/staging-migrations/profiles.d.ts +5 -0
- package/dist/staging-migrations/profiles.d.ts.map +1 -0
- package/dist/staging-migrations/profiles.js +31 -0
- package/dist/staging-migrations/profiles.js.map +1 -0
- package/dist/staging-migrations/service.d.ts +40 -0
- package/dist/staging-migrations/service.d.ts.map +1 -0
- package/dist/staging-migrations/service.js +157 -0
- package/dist/staging-migrations/service.js.map +1 -0
- package/dist/supervisor.d.ts +15 -0
- package/dist/supervisor.d.ts.map +1 -0
- package/dist/supervisor.js +54 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/terminal/http-carrier.d.ts +17 -0
- package/dist/terminal/http-carrier.d.ts.map +1 -0
- package/dist/terminal/http-carrier.js +39 -0
- package/dist/terminal/http-carrier.js.map +1 -0
- package/dist/terminal/pty-adapter.d.ts +88 -0
- package/dist/terminal/pty-adapter.d.ts.map +1 -0
- package/dist/terminal/pty-adapter.js +252 -0
- package/dist/terminal/pty-adapter.js.map +1 -0
- package/dist/terminal/transport-contract.d.ts +115 -0
- package/dist/terminal/transport-contract.d.ts.map +1 -0
- package/dist/terminal/transport-contract.js +2 -0
- package/dist/terminal/transport-contract.js.map +1 -0
- package/dist/terminal/transport.d.ts +32 -0
- package/dist/terminal/transport.d.ts.map +1 -0
- package/dist/terminal/transport.js +208 -0
- package/dist/terminal/transport.js.map +1 -0
- package/dist/watch-chat/wire.d.ts.map +1 -1
- package/dist/watch-chat/wire.js +14 -1
- package/dist/watch-chat/wire.js.map +1 -1
- package/dist/watch.d.ts +3 -0
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +57 -0
- package/dist/watch.js.map +1 -1
- package/package.json +6 -1
- package/scripts/com.tokenfactory.acc-runner-dedicated.plist +64 -0
- package/scripts/com.tokenfactory.acc-runner.plist +90 -0
- package/scripts/gen-build-info.d.mts +3 -0
- package/scripts/gen-build-info.mjs +72 -0
- package/scripts/install-launchd.sh +235 -0
- package/scripts/setup-dedicated-runner-darwin.sh +75 -0
- package/scripts/setup-dedicated-runner-linux.sh +173 -0
- 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,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/&/\&/g' -e 's/</\</g' -e 's/>/\>/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"
|