autonomous-sdlc-harness 0.1.0
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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +24 -0
- package/dist/cli.js +194 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.js +561 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.js +791 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.js +336 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.js +2023 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/registry.js +42 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/config/check.js +505 -0
- package/dist/config/check.js.map +1 -0
- package/dist/config/io.js +177 -0
- package/dist/config/io.js.map +1 -0
- package/dist/config/model.js +406 -0
- package/dist/config/model.js.map +1 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +537 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/json.js +125 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/layerCoverage.js +141 -0
- package/dist/core/layerCoverage.js.map +1 -0
- package/dist/core/layerGapRemedy.js +62 -0
- package/dist/core/layerGapRemedy.js.map +1 -0
- package/dist/core/nameList.js +23 -0
- package/dist/core/nameList.js.map +1 -0
- package/dist/core/paths.js +153 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/prompt.js +206 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/repoPaths.js +55 -0
- package/dist/core/repoPaths.js.map +1 -0
- package/dist/core/report.js +150 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/templating.js +88 -0
- package/dist/core/templating.js.map +1 -0
- package/dist/core/writer.js +479 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/daemon/backend.js +180 -0
- package/dist/daemon/backend.js.map +1 -0
- package/dist/daemon/units.js +380 -0
- package/dist/daemon/units.js.map +1 -0
- package/dist/detect/nestedApplication.js +79 -0
- package/dist/detect/nestedApplication.js.map +1 -0
- package/dist/detect/presets.js +2033 -0
- package/dist/detect/presets.js.map +1 -0
- package/dist/detect/signals.js +1368 -0
- package/dist/detect/signals.js.map +1 -0
- package/dist/doctor/checks.js +3530 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/generators/claudeContext.js +588 -0
- package/dist/generators/claudeContext.js.map +1 -0
- package/dist/generators/githooks.js +446 -0
- package/dist/generators/githooks.js.map +1 -0
- package/dist/generators/harnessConfig.js +632 -0
- package/dist/generators/harnessConfig.js.map +1 -0
- package/dist/generators/notifications.js +191 -0
- package/dist/generators/notifications.js.map +1 -0
- package/dist/generators/outerLoopScripts.js +165 -0
- package/dist/generators/outerLoopScripts.js.map +1 -0
- package/dist/generators/permissionProfile.js +1172 -0
- package/dist/generators/permissionProfile.js.map +1 -0
- package/dist/generators/projectSettings.js +322 -0
- package/dist/generators/projectSettings.js.map +1 -0
- package/dist/generators/repoRoot.js +417 -0
- package/dist/generators/repoRoot.js.map +1 -0
- package/dist/generators/scripts.js +557 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/generators/stateDir.js +221 -0
- package/dist/generators/stateDir.js.map +1 -0
- package/dist/machine/paths.js +111 -0
- package/dist/machine/paths.js.map +1 -0
- package/dist/machine/plugins.js +224 -0
- package/dist/machine/plugins.js.map +1 -0
- package/dist/machine/registry.js +330 -0
- package/dist/machine/registry.js.map +1 -0
- package/package.json +23 -0
- package/scripts/README.md +13 -0
- package/scripts/daemon/launchd.plist.template +59 -0
- package/scripts/daemon/systemd.service.template +58 -0
- package/templates/README.md +15 -0
- package/templates/claude/CLAUDE.md +54 -0
- package/templates/claude/README.md +5 -0
- package/templates/claude/context/api.md +29 -0
- package/templates/claude/context/conventions.md +23 -0
- package/templates/claude/context/data-layer.md +28 -0
- package/templates/claude/context/data-storage.md +29 -0
- package/templates/claude/context/docs-catalog.md +29 -0
- package/templates/claude/context/domain.md +28 -0
- package/templates/claude/context/layer.md +20 -0
- package/templates/claude/context/module.md +30 -0
- package/templates/claude/context/package.md +29 -0
- package/templates/claude/context/presentation.md +32 -0
- package/templates/claude/context/state-slices.md +28 -0
- package/templates/claude/context/tests.md +28 -0
- package/templates/claude/harness-task-offer.md +58 -0
- package/templates/claude/push-notify.env.example +21 -0
- package/templates/claude/qa-accounts.env.example +38 -0
- package/templates/claude/qa_test_scenarios.md +110 -0
- package/templates/claude/settings.autonomous.json +93 -0
- package/templates/claude/settings.autonomous.qa.json +36 -0
- package/templates/githooks/README.md +3 -0
- package/templates/githooks/pre-push +72 -0
- package/templates/repo/README.md +3 -0
- package/templates/repo/gitattributes +16 -0
- package/templates/repo/gitignore +61 -0
- package/templates/repo/gitignore.qa +25 -0
- package/templates/repo/mcp.json +17 -0
- package/templates/scripts/README.md +5 -0
- package/templates/scripts/autonomous-format-stream.sh +95 -0
- package/templates/scripts/autonomous-notify.sh +337 -0
- package/templates/scripts/autonomous-watcher.sh +3087 -0
- package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
- package/templates/scripts/commit-on-branch.sh +288 -0
- package/templates/scripts/create-worktree.sh +360 -0
- package/templates/scripts/deploy.sh +47 -0
- package/templates/scripts/lib/harness-run-lib.sh +1481 -0
- package/templates/scripts/push-branch.sh +140 -0
- package/templates/scripts/refresh-branch.sh +244 -0
- package/templates/scripts/restart-watcher.sh +401 -0
- package/templates/scripts/scratch-run.sh +302 -0
- package/templates/scripts/setup-worktree.sh +262 -0
- package/templates/scripts/start-dev-server.sh +99 -0
- package/templates/scripts/test.sh +50 -0
- package/templates/scripts/typecheck.sh +50 -0
- package/templates/state-dir/README-root.md +13 -0
- package/templates/state-dir/README.md +9 -0
- package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
- package/templates/state-dir/architecture_reviews/README.md +9 -0
- package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
- package/templates/state-dir/autonomous_inbox/README.md +9 -0
- package/templates/state-dir/autonomous_logs/README.md +9 -0
- package/templates/state-dir/branch_statistics/README.md +9 -0
- package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
- package/templates/state-dir/clarification_digests/README.md +9 -0
- package/templates/state-dir/clarifications/README.md +9 -0
- package/templates/state-dir/code_reviews/README.md +9 -0
- package/templates/state-dir/dispatch_additions/README.md +19 -0
- package/templates/state-dir/docs_catalog/README.md +9 -0
- package/templates/state-dir/flow_progress/README.md +9 -0
- package/templates/state-dir/improvement_observations/README.md +19 -0
- package/templates/state-dir/improvement_suggestions.md +29 -0
- package/templates/state-dir/lessons.md +23 -0
- package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
- package/templates/state-dir/qa_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_reviews/README.md +9 -0
- package/templates/state-dir/scratch/README.md +11 -0
- package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_reviews/README.md +9 -0
- package/templates/state-dir/story_plans/README.md +9 -0
- package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/task_plan_reviews/README.md +9 -0
- package/templates/state-dir/task_plans/README.md +9 -0
- package/templates/state-dir/task_prompts/README.md +9 -0
- package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
- package/templates/state-dir/ui_test_plans/README.md +9 -0
- package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/user_reviews/README.md +9 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which service manager this host can install the run daemon into, and where the run watcher is.
|
|
3
|
+
*
|
|
4
|
+
* **The rule this module exists to enforce: there is one detection, and `doctor` and `daemon` both
|
|
5
|
+
* import it.** The two commands must agree — `doctor` telling an operator the daemon is installable
|
|
6
|
+
* while `daemon install` refuses, or the reverse, is worse than either answer alone, because the
|
|
7
|
+
* operator has no way to tell which one is wrong. A second copy of the probe in the command that
|
|
8
|
+
* wanted a slightly different message is how that disagreement starts, so the messages live here
|
|
9
|
+
* too, as the `reason` string each result carries.
|
|
10
|
+
*
|
|
11
|
+
* Two invariants hold in every probe below:
|
|
12
|
+
*
|
|
13
|
+
* 1. **`execFileSync` with an argv array, never a shell string** — no `shell: true`, no `which`
|
|
14
|
+
* pipeline, no interpolation. A pipeline would be a compound command, which an unattended run's
|
|
15
|
+
* permission profile cannot allow-list as one literal, and which stalls rather than failing.
|
|
16
|
+
* 2. **Every probe is bounded and its output discarded.** A service manager that hangs must cost a
|
|
17
|
+
* timeout rather than the run: `doctor` is the command an operator reaches for *because*
|
|
18
|
+
* something is wrong with the host, and it has to answer.
|
|
19
|
+
*
|
|
20
|
+
* ## The watcher is the repository's, not this package's
|
|
21
|
+
*
|
|
22
|
+
* The daemon runs one script — the run watcher — and `init` writes it into the adopter's configured
|
|
23
|
+
* `scriptsDir` with the rest of the outer-loop set. {@link resolveWatcherPath} therefore joins the
|
|
24
|
+
* repository root to that configured directory, and {@link WATCHER_ABSENCE_MESSAGE} is the one
|
|
25
|
+
* sentence both commands use when nothing is there: `doctor` reports it as a warning, so a
|
|
26
|
+
* repository whose watcher was deleted still exits 0, and `daemon install` refuses, because
|
|
27
|
+
* installing a unit whose `ExecStart` points at nothing gives a service that restarts forever and
|
|
28
|
+
* logs nothing anyone reads.
|
|
29
|
+
*
|
|
30
|
+
* The path is **repo-relative and not a configuration key of its own**: `scriptsDir` already is one,
|
|
31
|
+
* and the file name comes from `generators/outerLoopScripts.ts`'s table — the single declaration of
|
|
32
|
+
* which outer-loop files ship and where each lands — so the writer and the daemon cannot disagree
|
|
33
|
+
* about what the daemon runs. Resolving against the repository is also what keeps the rendered
|
|
34
|
+
* unit's `ExecStart` inside the same repository its `WorkingDirectory` already names: a CLI invoked
|
|
35
|
+
* through `npx` lives in a cache directory that is deleted out from under a long-running service,
|
|
36
|
+
* and a package-relative program would have made that path a daemon's `ExecStart`. `--watcher`
|
|
37
|
+
* overrides it for developing against a watcher that is not the written one.
|
|
38
|
+
*/
|
|
39
|
+
import { execFileSync } from 'node:child_process';
|
|
40
|
+
import { existsSync } from 'node:fs';
|
|
41
|
+
import { join, resolve } from 'node:path';
|
|
42
|
+
import { homeRoot } from '../core/paths.js';
|
|
43
|
+
import { outerLoopScriptPath, WATCHER_SCRIPT_NAME } from '../generators/outerLoopScripts.js';
|
|
44
|
+
/** `launchctl`'s `--version`-equivalent: it prints the running launchd's version and exits 0. */
|
|
45
|
+
const LAUNCHCTL_PROBE = Object.freeze(['version']);
|
|
46
|
+
/** `systemctl`'s version probe — resolution of the binary only, not of a user instance. */
|
|
47
|
+
const SYSTEMCTL_PROBE = Object.freeze(['--version']);
|
|
48
|
+
/**
|
|
49
|
+
* The per-user-instance probe.
|
|
50
|
+
*
|
|
51
|
+
* `--user show-environment` exits 0 exactly when the calling account's systemd manager is reachable
|
|
52
|
+
* over its bus, which is the condition a user unit needs and the binary's presence does not imply —
|
|
53
|
+
* a container image or an `ssh` session with no user bus has `systemctl` and no manager to talk to.
|
|
54
|
+
* `is-system-running` is the obvious alternative and is wrong here: it reports `degraded` with a
|
|
55
|
+
* non-zero status on a perfectly usable host, which would demote a working backend to `none`.
|
|
56
|
+
*/
|
|
57
|
+
const SYSTEMD_USER_PROBE = Object.freeze(['--user', 'show-environment']);
|
|
58
|
+
/** Longest a probe may take before it is treated as "this tool did not answer". */
|
|
59
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
60
|
+
/** The launchd user-agent directory, relative to the account home. */
|
|
61
|
+
const LAUNCHD_UNIT_DIR = Object.freeze(['Library', 'LaunchAgents']);
|
|
62
|
+
/** The systemd user-unit directory, relative to the account home. */
|
|
63
|
+
const SYSTEMD_UNIT_DIR = Object.freeze(['.config', 'systemd', 'user']);
|
|
64
|
+
/**
|
|
65
|
+
* The single sentence both commands use for the watcher's absence.
|
|
66
|
+
*
|
|
67
|
+
* Exported rather than written twice so a `doctor` warning and a `daemon install` refusal cannot
|
|
68
|
+
* describe the same condition differently — the second-worst outcome after describing it wrongly.
|
|
69
|
+
*
|
|
70
|
+
* It names the two ways a wired repository reaches this state, because they are the two the operator
|
|
71
|
+
* has to choose between: the file was removed, or `scriptsDir` was changed and `init` has not been
|
|
72
|
+
* run since. Both are repaired by the same command, which is why the remedy is one sentence.
|
|
73
|
+
*/
|
|
74
|
+
export const WATCHER_ABSENCE_MESSAGE = 'the run watcher is written into the configured scriptsDir by `init`, so nothing there means it was removed, or scriptsDir was changed without a re-run';
|
|
75
|
+
/**
|
|
76
|
+
* Run a tool with a fixed argument vector and report whether it answered.
|
|
77
|
+
*
|
|
78
|
+
* Both failure modes collapse to `false` on purpose: a tool that is not on PATH and a tool that
|
|
79
|
+
* exited non-zero are the same answer to the only question asked — can this host's service manager
|
|
80
|
+
* be driven from here. The distinction that *does* matter, between "no systemctl" and "systemctl
|
|
81
|
+
* with no user instance", is drawn by which probe failed, not by how it failed.
|
|
82
|
+
*/
|
|
83
|
+
function toolResponds(command, args) {
|
|
84
|
+
try {
|
|
85
|
+
execFileSync(command, [...args], {
|
|
86
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
87
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
88
|
+
windowsHide: true,
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** `~/Library/LaunchAgents` — where a launchd user agent is installed. */
|
|
97
|
+
export function launchdUnitDir() {
|
|
98
|
+
return join(homeRoot(), ...LAUNCHD_UNIT_DIR);
|
|
99
|
+
}
|
|
100
|
+
/** `~/.config/systemd/user` — where a systemd user unit is installed. */
|
|
101
|
+
export function systemdUnitDir() {
|
|
102
|
+
return join(homeRoot(), ...SYSTEMD_UNIT_DIR);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Detect the service manager the run daemon can be installed into.
|
|
106
|
+
*
|
|
107
|
+
* The order is fixed and first-match-wins:
|
|
108
|
+
*
|
|
109
|
+
* 1. **launchd**, when this is macOS *and* `launchctl` answers. The platform check comes first
|
|
110
|
+
* because launchd is not something a non-macOS host has a working `launchctl` for.
|
|
111
|
+
* 2. **systemd**, when `systemctl` answers *and* the calling account's user instance is reachable.
|
|
112
|
+
* Both halves are required: a `systemctl` with no user manager behind it produces a unit file
|
|
113
|
+
* that installs and never starts, which is the failure this second probe exists to turn into an
|
|
114
|
+
* up-front `none` with a fixable reason.
|
|
115
|
+
* 3. **none** otherwise, always with a reason naming what was missing. `none` is a statement about
|
|
116
|
+
* the host, not an error — the caller decides whether it is fatal (`daemon install`) or a
|
|
117
|
+
* reported condition (`doctor`).
|
|
118
|
+
*/
|
|
119
|
+
export function detectBackend() {
|
|
120
|
+
const platform = process.platform;
|
|
121
|
+
const isMac = platform === 'darwin';
|
|
122
|
+
if (isMac && toolResponds('launchctl', LAUNCHCTL_PROBE)) {
|
|
123
|
+
return {
|
|
124
|
+
kind: 'launchd',
|
|
125
|
+
reason: 'macOS with a responding launchctl: the run daemon installs as a launchd user agent',
|
|
126
|
+
unitPath: launchdUnitDir(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (toolResponds('systemctl', SYSTEMCTL_PROBE)) {
|
|
130
|
+
if (toolResponds('systemctl', SYSTEMD_USER_PROBE)) {
|
|
131
|
+
return {
|
|
132
|
+
kind: 'systemd',
|
|
133
|
+
reason: 'systemd with a reachable per-user instance: the run daemon installs as a user unit',
|
|
134
|
+
unitPath: systemdUnitDir(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
kind: 'none',
|
|
139
|
+
reason: 'systemctl responded but this account has no reachable systemd user instance (systemctl --user show-environment failed), so a user unit would install and never start: run the install from a logged-in session of the account that will own the run, or enable lingering for that account',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (isMac) {
|
|
143
|
+
return {
|
|
144
|
+
kind: 'none',
|
|
145
|
+
reason: 'this host reports itself as macOS but launchctl did not answer, and there is no systemctl either: the run daemon has no service manager to install into here',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
kind: 'none',
|
|
150
|
+
reason: `no supported service manager on ${platform}: the run daemon needs launchd (macOS) or a systemd user instance`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Where the run watcher is, and whether it is there.
|
|
155
|
+
*
|
|
156
|
+
* With no override the answer is `<repoRoot>/<scriptsDir>/<watcher>` — where `init` wrote it, taken
|
|
157
|
+
* from the writer's own {@link outerLoopScriptPath} so the daemon and the generator cannot resolve
|
|
158
|
+
* one configured `scriptsDir` differently. A `--watcher` value is resolved against the current
|
|
159
|
+
* directory instead, so a relative one behaves the way a path typed on a command line does.
|
|
160
|
+
*
|
|
161
|
+
* `present` is a plain existence check and is deliberately not upgraded to an executability or
|
|
162
|
+
* shebang check: the generated copy is the adopter's to edit, and a stricter probe here would start
|
|
163
|
+
* refusing to install a daemon over an edit that is none of this command's business.
|
|
164
|
+
*/
|
|
165
|
+
export function resolveWatcherPath({ repoRoot, config, override }) {
|
|
166
|
+
const trimmed = override?.trim();
|
|
167
|
+
const path = trimmed !== undefined && trimmed !== ''
|
|
168
|
+
? resolve(trimmed)
|
|
169
|
+
: outerLoopScriptPath(repoRoot, config, WATCHER_SCRIPT_NAME);
|
|
170
|
+
return { path, present: existsSync(path) };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The full sentence for a missing watcher, built from {@link WATCHER_ABSENCE_MESSAGE} so the
|
|
174
|
+
* warning `doctor` prints and the refusal `daemon install` throws say the same thing about the same
|
|
175
|
+
* path.
|
|
176
|
+
*/
|
|
177
|
+
export function watcherMissingMessage(path) {
|
|
178
|
+
return `no run watcher at ${path}: ${WATCHER_ABSENCE_MESSAGE}. Re-run \`init\` to write it back, or point --watcher at a watcher script of your own`;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/daemon/backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the run daemon's unit file for the detected backend, and say where it is installed.
|
|
3
|
+
*
|
|
4
|
+
* **The rule this module exists to enforce: the unit's identifier, its install path and its text are
|
|
5
|
+
* produced together, from one call.** A caller that composed a label itself and read the target path
|
|
6
|
+
* from somewhere else would eventually install a file at one name and address the service by
|
|
7
|
+
* another — a daemon that cannot be stopped by the command that installed it, which is the worst
|
|
8
|
+
* shape of this failure because it survives a reinstall.
|
|
9
|
+
*
|
|
10
|
+
* ## Five non-obvious choices, and where each comes from
|
|
11
|
+
*
|
|
12
|
+
* 1. **The identity is per repository, not per project name.** The launchd label is
|
|
13
|
+
* `com.autonomous-sdlc-harness.watcher.<slug>` and the systemd unit name
|
|
14
|
+
* `harness-watcher@<slug>.service`, where `<slug>` is {@link repoSlug} of the repository the
|
|
15
|
+
* daemon was installed from. Two repositories on one machine therefore install daemons that
|
|
16
|
+
* cannot collide — which two checkouts sharing a `projectName` would have, under a label built
|
|
17
|
+
* from that name. `docs/config.md` §4's `<launchd_label>` row is the register entry and
|
|
18
|
+
* `docs/cli.md` §9 states the per-repository consequence; this module is where the form is
|
|
19
|
+
* implemented, so nothing else in the CLI spells it out.
|
|
20
|
+
* 2. **The install directory comes from the detection result, not from a second table here.**
|
|
21
|
+
* `DaemonBackend.unitPath` is the backend's user-unit directory; this module contributes only
|
|
22
|
+
* the file *name*, which is the half that depends on the repository.
|
|
23
|
+
* 3. **Values are checked before they are substituted, and escaped for the format they land in.** A
|
|
24
|
+
* plist is XML, so a `&` or a `<` in a checkout path would produce a unit no `launchctl` will
|
|
25
|
+
* parse. A systemd unit is neither: `%` introduces a specifier in every directive rendered into
|
|
26
|
+
* here, and `ExecStart=` splits its command line at whitespace, so an unescaped checkout path
|
|
27
|
+
* holding a space runs `/bin/bash` on the first word of it — installed, started, failed and,
|
|
28
|
+
* under `Restart=always`, restarted forever. Both are escaped, per format and per directive; see
|
|
29
|
+
* {@link escapeSystemdValue} and {@link escapeSystemdArgument}. A newline is refused instead — it
|
|
30
|
+
* is not something to render carefully, it is something to report.
|
|
31
|
+
* 4. **On systemd a *concrete* `harness-watcher@<slug>.service` file is written, not a template
|
|
32
|
+
* unit plus an instance.** The `@` is what an operator reads as "one watcher per repository",
|
|
33
|
+
* and it costs nothing to spell it that way: systemd resolves an instance name to a literal
|
|
34
|
+
* file of that exact name *before* it falls back to the `harness-watcher@.service` template, so
|
|
35
|
+
* the file installed here is the one `systemctl --user` acts on. A real template unit would have
|
|
36
|
+
* to reach the repository path through `%i`, i.e. carry the checkout path in the instance name,
|
|
37
|
+
* escaped into it. This design renders that path into `ExecStart` and `WorkingDirectory`
|
|
38
|
+
* instead, quoted and specifier-escaped by choice 3, so a checkout holding a space or a `%` is
|
|
39
|
+
* one the operator never has to encode by hand.
|
|
40
|
+
* 5. **The unit renders a `PATH`, captured from the shell that ran `daemon install`.** A user agent
|
|
41
|
+
* or user unit inherits the *service manager's* environment, not a login shell's: on macOS that
|
|
42
|
+
* is `/usr/bin:/bin:/usr/sbin:/sbin`, where neither a Homebrew package manager nor an agent CLI
|
|
43
|
+
* under `~/.local/bin` resolves — the watcher launches both by name, so a unit without the key
|
|
44
|
+
* fails at the first configured command and, with the failure repaired one binary at a time,
|
|
45
|
+
* at the next. Three answers were weighed. Capturing the installing shell's `PATH` — this one —
|
|
46
|
+
* needs no dependency (this package declares none, and that is a property worth keeping), is
|
|
47
|
+
* deterministic Node with nothing to detect, and is exactly the hand edit that unblocked the
|
|
48
|
+
* first unattended run. Pinning the individual binaries cannot work: the toolchain is the
|
|
49
|
+
* adopter's `commands.*` configuration, which this CLI does not interpret. Sourcing the login
|
|
50
|
+
* shell's profile from the watcher would run arbitrary shell configuration inside the daemon's
|
|
51
|
+
* own process, on every dispatch. Its one weakness is that a captured value goes stale when the
|
|
52
|
+
* toolchain moves; that is a `doctor` question — grading the *installed unit's* `PATH` against
|
|
53
|
+
* the binaries the outer loop runs — rather than one the renderer can answer, and the remedy is
|
|
54
|
+
* `daemon install --force`. An absent or empty `PATH` renders **no** key: `PATH=""` overrides
|
|
55
|
+
* the service manager's own directories with nothing, which is strictly worse than the default
|
|
56
|
+
* the caller then warns about. The value it does render is escaped for a **quoted** destination
|
|
57
|
+
* on systemd — `Environment="PATH=…"` is quoted because an entry may hold a space, which makes a
|
|
58
|
+
* `"` or a `\` in one the hazard choice 3's `ExecStart` escaping exists for.
|
|
59
|
+
*
|
|
60
|
+
* ## Scope
|
|
61
|
+
*
|
|
62
|
+
* This module renders and computes; it writes nothing. Installing the rendered text, reloading the
|
|
63
|
+
* service manager and starting or stopping the daemon are the `daemon` subcommand's, and keeping
|
|
64
|
+
* them apart is what lets the rendering be tested without a service manager present.
|
|
65
|
+
*/
|
|
66
|
+
import { readFileSync } from 'node:fs';
|
|
67
|
+
import { join } from 'node:path';
|
|
68
|
+
import { DEFAULTS } from '../config/model.js';
|
|
69
|
+
import { EXIT, HarnessError } from '../core/errors.js';
|
|
70
|
+
import { defaultProjectName, packageScriptsDir } from '../core/paths.js';
|
|
71
|
+
import { normalizeRepoDir } from '../core/repoPaths.js';
|
|
72
|
+
import { renderTemplate } from '../core/templating.js';
|
|
73
|
+
/** The unit templates' subdirectory inside this package's `scripts/` directory. */
|
|
74
|
+
const TEMPLATE_DIR = 'daemon';
|
|
75
|
+
/** One template per installable backend. `none` has no row, because it has no unit. */
|
|
76
|
+
const UNIT_TEMPLATES = {
|
|
77
|
+
launchd: 'launchd.plist.template',
|
|
78
|
+
systemd: 'systemd.service.template',
|
|
79
|
+
};
|
|
80
|
+
/** Reverse-DNS prefix of the launchd label; {@link repoSlug} completes it. */
|
|
81
|
+
const LAUNCHD_LABEL_PREFIX = 'com.autonomous-sdlc-harness.watcher';
|
|
82
|
+
/** The systemd unit name, either side of {@link repoSlug}. */
|
|
83
|
+
const SYSTEMD_UNIT_PREFIX = 'harness-watcher@';
|
|
84
|
+
const SYSTEMD_UNIT_SUFFIX = '.service';
|
|
85
|
+
/** The longest a slug may be. Applied last, keeping the tail — see {@link repoSlug}. */
|
|
86
|
+
const SLUG_MAX_LENGTH = 64;
|
|
87
|
+
/**
|
|
88
|
+
* What a derived slug may be: non-empty, and lowercase letters, digits and `-` only.
|
|
89
|
+
*
|
|
90
|
+
* This is the check `projectName` used to carry, moved to the value that now names the service. It
|
|
91
|
+
* refuses a *derivation* rather than a configuration — {@link repoSlug} is total over any string,
|
|
92
|
+
* so nothing an adopter can write reaches it — which is why failing it exits {@link EXIT.INTERNAL}.
|
|
93
|
+
*/
|
|
94
|
+
const SLUG_PATTERN = /^[a-z0-9-]+$/;
|
|
95
|
+
/**
|
|
96
|
+
* The repository's machine-unique identifier: its own absolute path, lowercased, with every
|
|
97
|
+
* character outside `[a-z0-9]` replaced by `-`, runs collapsed, leading and trailing `-` stripped,
|
|
98
|
+
* and the result cut to {@link SLUG_MAX_LENGTH} characters **keeping the tail** — the tail is the
|
|
99
|
+
* distinguishing part, since two checkouts on one machine usually share a long prefix.
|
|
100
|
+
*
|
|
101
|
+
* **This function has a twin, and a test that keeps them equal.** The generated
|
|
102
|
+
* `<scriptsDir>/lib/harness-run-lib.sh` defines `hr_repo_slug` doing the same transformation for
|
|
103
|
+
* the watcher, the notifier and the machine-level usage lane, so the daemon a repository installs
|
|
104
|
+
* and the run artifacts that repository writes are keyed alike. `test/daemon.test.mjs` runs both
|
|
105
|
+
* halves over one path and compares — the only thing stopping them drifting. **Change one, change
|
|
106
|
+
* the other in the same commit**, and keep these three properties, each of which the shell half
|
|
107
|
+
* also has:
|
|
108
|
+
*
|
|
109
|
+
* - **Truncation is last**, so a truncated slug may *begin* with `-`. Reordering it to strip after
|
|
110
|
+
* cutting would be tidier and would silently disagree with the shell.
|
|
111
|
+
* - **Letters and digits are ASCII-only**, on both sides: the shell half runs under `LC_ALL=C` so
|
|
112
|
+
* that its `[!a-z0-9]` cannot admit `é` through a locale's collation, and the fold here is
|
|
113
|
+
* `[A-Z]` rather than `toLowerCase()` for the mirror-image reason — a locale-aware fold turns `İ`
|
|
114
|
+
* into an ASCII `i` where the shell yields a `-`. An accented letter is a separator, never a
|
|
115
|
+
* letter it resembles.
|
|
116
|
+
* - **No hash, no truncating digest.** Both halves have to be one readable transformation an
|
|
117
|
+
* operator can reproduce by eye from the path they are looking at.
|
|
118
|
+
*
|
|
119
|
+
* The argument is the repository the daemon is being installed for — `daemon install` resolves it
|
|
120
|
+
* from its own working directory. The shell half additionally resolves the *main* checkout first,
|
|
121
|
+
* so every worktree of one repository answers the same slug; nothing here needs that step, because
|
|
122
|
+
* the unit's `WorkingDirectory` is this same root and a unit installed from a checkout is a unit
|
|
123
|
+
* for that checkout.
|
|
124
|
+
*
|
|
125
|
+
* Not to be confused with `parseRepoSlug` in `core/paths.ts`, which is `<owner>/<repository>` off a
|
|
126
|
+
* git remote URL and has nothing to do with a service identifier.
|
|
127
|
+
*/
|
|
128
|
+
export function repoSlug(repoRoot) {
|
|
129
|
+
const slug = repoRoot
|
|
130
|
+
.replace(/[A-Z]/g, (letter) => letter.toLowerCase())
|
|
131
|
+
.replace(/[^a-z0-9]/g, '-')
|
|
132
|
+
.replace(/-+/g, '-')
|
|
133
|
+
.replace(/^-+|-+$/g, '')
|
|
134
|
+
.slice(-SLUG_MAX_LENGTH);
|
|
135
|
+
if (!SLUG_PATTERN.test(slug)) {
|
|
136
|
+
throw new HarnessError(`the daemon identifier derived from ${JSON.stringify(repoRoot)} is ${JSON.stringify(slug)}, which cannot name a service: the derivation admits only lowercase letters, digits and '-' and must produce at least one character, so this is a fault in this CLI rather than in the repository`, EXIT.INTERNAL);
|
|
137
|
+
}
|
|
138
|
+
return slug;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The project name the unit files *describe* themselves with: the configured `projectName`, or the
|
|
142
|
+
* repository directory name the schema says `init` defaults it to.
|
|
143
|
+
*
|
|
144
|
+
* Descriptive only, since decision 21 — it reaches a plist comment, through {@link commentSafe} for
|
|
145
|
+
* what that destination forbids, and a systemd `Description=`, and no longer the label or the file
|
|
146
|
+
* name. That is why it carries no pattern check any more: the check that used to live here is
|
|
147
|
+
* {@link SLUG_PATTERN}, on the value that names the service.
|
|
148
|
+
* `assertSingleLine` and the XML escaping still apply to it, as they do to every substituted value.
|
|
149
|
+
*/
|
|
150
|
+
function descriptiveProjectName(config, repoRoot) {
|
|
151
|
+
const configured = config.projectName?.trim();
|
|
152
|
+
return configured !== undefined && configured !== '' ? configured : defaultProjectName(repoRoot);
|
|
153
|
+
}
|
|
154
|
+
/** XML text-node escaping, for the values substituted into the plist's `<string>` elements. */
|
|
155
|
+
function escapeXml(value) {
|
|
156
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Separate adjacent hyphens in a value whose plist destination is a **comment**, where XML 1.0 §2.5
|
|
160
|
+
* forbids `--` and {@link escapeXml} — which touches only `&`, `<` and `>` — leaves one standing.
|
|
161
|
+
*
|
|
162
|
+
* The guarantee is the property, not one substitution: no two hyphens survive adjacent, for a run of
|
|
163
|
+
* **any** length, since every hyphen followed by another takes a trailing space (`notes--app` →
|
|
164
|
+
* `notes- -app`, `notes---app` → `notes- - -app`).
|
|
165
|
+
*
|
|
166
|
+
* `projectName` is the plist's only comment-destined substitution; `label` appears in that block too
|
|
167
|
+
* but cannot carry an adjacent pair, because {@link repoSlug} collapses runs of `-`. The rewrite
|
|
168
|
+
* reaches descriptive comment text and never a value the service manager reads, which is why
|
|
169
|
+
* mangling it is acceptable where refusing a legal `projectName` would not be. The systemd arm is
|
|
170
|
+
* untouched: `Description=` is a directive, not a comment.
|
|
171
|
+
*/
|
|
172
|
+
function commentSafe(value) {
|
|
173
|
+
return value.replace(/-(?=-)/g, '- ');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* systemd escaping for a value that is a whole directive argument — `WorkingDirectory=`,
|
|
177
|
+
* `Description=`, the `StandardOutput=append:` paths.
|
|
178
|
+
*
|
|
179
|
+
* Only `%` needs it: those directives take the rest of the line literally, but every one of them is
|
|
180
|
+
* specifier-expanded first, so a checkout under `/home/ada/100%-mine` would resolve `%-` and the
|
|
181
|
+
* service would run somewhere else. `%%` is systemd's own escape for a literal `%`.
|
|
182
|
+
*/
|
|
183
|
+
function escapeSystemdValue(value) {
|
|
184
|
+
return value.replace(/%/g, '%%');
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* systemd escaping for a value substituted **inside quotes** — `ExecStart=`'s program path, and the
|
|
188
|
+
* `PATH` inside `Environment="PATH=…"`, both of which the template quotes.
|
|
189
|
+
*
|
|
190
|
+
* `ExecStart=` splits at whitespace before anything runs and a `PATH` entry may hold a space, which
|
|
191
|
+
* is why each is quoted rather than trusted; a `"` or a `\` in one would end or eat that quoting, so
|
|
192
|
+
* both are backslash-escaped. Specifiers expand here too, so {@link escapeSystemdValue} still
|
|
193
|
+
* applies.
|
|
194
|
+
*/
|
|
195
|
+
function escapeSystemdArgument(value) {
|
|
196
|
+
return escapeSystemdValue(value.replace(/[\\"]/g, (character) => `\\${character}`));
|
|
197
|
+
}
|
|
198
|
+
/** The one token whose systemd destination is a command-line argument rather than a whole value. */
|
|
199
|
+
const SYSTEMD_ARGUMENT_TOKENS = new Set(['watcherPath']);
|
|
200
|
+
/**
|
|
201
|
+
* The `{{environment}}` stanza: the whole environment key for this backend, or nothing at all.
|
|
202
|
+
*
|
|
203
|
+
* The stanza is one token rather than a `{{envPath}}` inside a fixed key because the empty case has
|
|
204
|
+
* to remove the *key*, not leave it holding `""` (choice 5), and `renderTemplate` substitutes values
|
|
205
|
+
* and knows no conditional sections. `escaped` is the already-escaped value — {@link escapeXml} for
|
|
206
|
+
* the plist and {@link escapeSystemdArgument} for the quoted systemd directive below, applied by
|
|
207
|
+
* {@link renderUnit} — since this function's own output is markup that must not be escaped again.
|
|
208
|
+
*
|
|
209
|
+
* The trailing newline is what keeps both cases readable: each template puts `{{environment}}` on a
|
|
210
|
+
* line of its own, so a rendered stanza is followed by a blank line and an omitted one leaves the
|
|
211
|
+
* blank line that was already there.
|
|
212
|
+
*/
|
|
213
|
+
function environmentStanza(kind, escaped) {
|
|
214
|
+
if (escaped === undefined)
|
|
215
|
+
return '';
|
|
216
|
+
return kind === 'launchd'
|
|
217
|
+
? ` <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>${escaped}</string>\n </dict>\n`
|
|
218
|
+
: `Environment="PATH=${escaped}"\n`;
|
|
219
|
+
}
|
|
220
|
+
/** The plist's environment dictionary — {@link environmentStanza} renders it — and one named entry inside it. */
|
|
221
|
+
const PLIST_ENVIRONMENT = /<key>\s*EnvironmentVariables\s*<\/key>\s*<dict>([\s\S]*?)<\/dict>/;
|
|
222
|
+
const plistEntry = (key) => new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>([\\s\\S]*?)</string>`);
|
|
223
|
+
/**
|
|
224
|
+
* The systemd form, quoted as the renderer writes it and bare as a hand edit may leave it.
|
|
225
|
+
*
|
|
226
|
+
* The quoted alternative admits `\"` and `\\`, because that is what {@link escapeSystemdArgument}
|
|
227
|
+
* writes into it: a `[^"]*` value would stop at the first escaped quote and read back a truncated
|
|
228
|
+
* `PATH`.
|
|
229
|
+
*/
|
|
230
|
+
const systemdDirective = (key) => new RegExp(`^Environment=(?:"${key}=((?:[^"\\\\]|\\\\.)*)"|${key}=(.*))$`, 'gm');
|
|
231
|
+
/**
|
|
232
|
+
* One environment value an installed unit gives the daemon, or `undefined` when the unit does not set
|
|
233
|
+
* it — `PATH`, and the agent CLI variable beside it.
|
|
234
|
+
*
|
|
235
|
+
* The two shapes are {@link environmentStanza}'s, read back: a plist dictionary whose value is
|
|
236
|
+
* XML-escaped, and a systemd `Environment=` line whose value is specifier-escaped and, inside the
|
|
237
|
+
* quoting the renderer writes, backslash-escaped as well. Each is un-escaped by the inverse of the
|
|
238
|
+
* escape that wrote it — and in that order — so a toolchain directory holding an `&`, a `%`, a `"` or
|
|
239
|
+
* a `\` is compared as the path it is rather than as the markup it was stored as. A *bare* directive
|
|
240
|
+
* is left as written, because nothing here escaped it: it can only be a hand edit.
|
|
241
|
+
*
|
|
242
|
+
* The key is interpolated into both patterns and is always one of the caller's own literals, so it
|
|
243
|
+
* carries nothing a regular expression reads.
|
|
244
|
+
*
|
|
245
|
+
* The parse is deliberately tolerant of hand edits — the unit is the operator's file once it is
|
|
246
|
+
* installed, and a key the renderer never writes (the agent CLI variable `doctor` reads) can only
|
|
247
|
+
* arrive that way — which is why the systemd side takes the **last** matching directive: that is what
|
|
248
|
+
* systemd itself does with a second one.
|
|
249
|
+
*
|
|
250
|
+
* Exported because two consumers read an installed unit back: `doctor`'s `daemon-path` check, and
|
|
251
|
+
* `daemon install`'s comparison of the `PATH` it is about to write against the one on disk. It lives
|
|
252
|
+
* beside the escaping it inverts so that a change to {@link environmentStanza} and a change to this
|
|
253
|
+
* parse are one edit in one file — and so the round trip against {@link renderUnit}'s escaping, the
|
|
254
|
+
* property the un-escaping claims and cannot show on its own, has both sides in one module.
|
|
255
|
+
*/
|
|
256
|
+
export function unitEnvValue(kind, text, key) {
|
|
257
|
+
if (kind === 'launchd') {
|
|
258
|
+
const dict = PLIST_ENVIRONMENT.exec(text)?.[1];
|
|
259
|
+
if (dict === undefined)
|
|
260
|
+
return undefined;
|
|
261
|
+
const value = plistEntry(key).exec(dict)?.[1];
|
|
262
|
+
return value === undefined ? undefined : value.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
263
|
+
}
|
|
264
|
+
let last;
|
|
265
|
+
for (const match of text.matchAll(systemdDirective(key))) {
|
|
266
|
+
last = match[1] !== undefined ? { raw: match[1], quoted: true } : { raw: match[2] ?? '', quoted: false };
|
|
267
|
+
}
|
|
268
|
+
if (last === undefined)
|
|
269
|
+
return undefined;
|
|
270
|
+
// The escape's steps, undone in reverse: `%%` first, then the backslashes the quoting needed.
|
|
271
|
+
const withLiteralPercent = last.raw.replace(/%%/g, '%');
|
|
272
|
+
return last.quoted ? withLiteralPercent.replace(/\\(["\\])/g, '$1') : withLiteralPercent;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Refuse a value carrying a line break before it is substituted anywhere.
|
|
276
|
+
*
|
|
277
|
+
* A newline in a systemd unit's value ends the directive and starts another, and in a plist it
|
|
278
|
+
* survives into a string the service manager then acts on. Both are ways for a path to become a
|
|
279
|
+
* *setting*, so the check is applied to every value and to both backends rather than to the one
|
|
280
|
+
* format where the consequence is easiest to demonstrate.
|
|
281
|
+
*/
|
|
282
|
+
function assertSingleLine(token, value) {
|
|
283
|
+
if (/[\r\n]/.test(value)) {
|
|
284
|
+
throw new HarnessError(`the value for {{${token}}} contains a line break (${JSON.stringify(value)}), which would split the generated unit file into directives nobody wrote: move the checkout, or set the offending path in harness.config.json to one without a newline in it`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Read a unit template from this package's `scripts/daemon/` directory.
|
|
289
|
+
*
|
|
290
|
+
* It is read from `scripts/` rather than through `readTemplate` because these two files are not
|
|
291
|
+
* adopter-facing generator templates: they are shipped assets of the daemon, which is why they sit
|
|
292
|
+
* beside the watcher the daemon runs. A missing one is a packaging fault the adopter cannot act on,
|
|
293
|
+
* so it exits {@link EXIT.INTERNAL} with the path named.
|
|
294
|
+
*/
|
|
295
|
+
function readUnitTemplate(file) {
|
|
296
|
+
const absolute = join(packageScriptsDir(), TEMPLATE_DIR, file);
|
|
297
|
+
try {
|
|
298
|
+
return readFileSync(absolute, 'utf8');
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error?.code === 'ENOENT') {
|
|
302
|
+
throw new HarnessError(`daemon unit template missing from the installed package: ${absolute}`, EXIT.INTERNAL);
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Render a unit template through {@link renderTemplate}.
|
|
309
|
+
*
|
|
310
|
+
* **`assertNoneSurvive` is on**: the values are a service label, three absolute paths and an
|
|
311
|
+
* environment stanza built from a fourth, so one carrying `{{…}}` means a half-rendered unit
|
|
312
|
+
* installed into a service manager — the failure this module's header calls the worst shape,
|
|
313
|
+
* because it survives a reinstall.
|
|
314
|
+
*/
|
|
315
|
+
function render(file, values) {
|
|
316
|
+
return renderTemplate(readUnitTemplate(file), values, {
|
|
317
|
+
describe: `the daemon unit template ${file}`,
|
|
318
|
+
assertNoneSurvive: true,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Render the daemon's unit for the detected backend, and compute where it is installed and what it
|
|
323
|
+
* is then called.
|
|
324
|
+
*
|
|
325
|
+
* - **launchd** — `~/Library/LaunchAgents/com.autonomous-sdlc-harness.watcher.<slug>.plist`.
|
|
326
|
+
* - **systemd** — `~/.config/systemd/user/harness-watcher@<slug>.service`.
|
|
327
|
+
* - **`none`** — throws, naming the platform and carrying detection's own reason, because a caller
|
|
328
|
+
* that reached this function with no backend has a question with no answer rather than a value to
|
|
329
|
+
* fall back on.
|
|
330
|
+
*
|
|
331
|
+
* The install directory is the backend's `unitPath`; only the file name is computed here.
|
|
332
|
+
*/
|
|
333
|
+
export function renderUnit({ backend, config, repoRoot, watcherPath, envPath }) {
|
|
334
|
+
if (backend.kind === 'none') {
|
|
335
|
+
throw new HarnessError(`no service manager to install the run daemon into on ${process.platform}: ${backend.reason}`);
|
|
336
|
+
}
|
|
337
|
+
const unitDir = backend.unitPath;
|
|
338
|
+
if (unitDir === undefined) {
|
|
339
|
+
throw new HarnessError(`the ${backend.kind} backend was detected without a unit directory, so there is nowhere to install the daemon: detection and installation disagree, which is a fault in this CLI`, EXIT.INTERNAL);
|
|
340
|
+
}
|
|
341
|
+
const projectName = descriptiveProjectName(config, repoRoot);
|
|
342
|
+
const stateDirAbs = join(repoRoot, normalizeRepoDir(config.stateDir ?? DEFAULTS.stateDir));
|
|
343
|
+
const slug = repoSlug(repoRoot);
|
|
344
|
+
const label = backend.kind === 'launchd'
|
|
345
|
+
? `${LAUNCHD_LABEL_PREFIX}.${slug}`
|
|
346
|
+
: `${SYSTEMD_UNIT_PREFIX}${slug}${SYSTEMD_UNIT_SUFFIX}`;
|
|
347
|
+
const fileName = backend.kind === 'launchd' ? `${label}.plist` : label;
|
|
348
|
+
// Undefined and empty are one answer — no environment key — so the distinction is collapsed here
|
|
349
|
+
// rather than in each of the two places that would otherwise have to test for both.
|
|
350
|
+
const resolvedEnvPath = envPath !== undefined && envPath.trim() !== '' ? envPath : undefined;
|
|
351
|
+
const values = { label, projectName, watcherPath, repoRoot, stateDirAbs };
|
|
352
|
+
for (const [token, value] of Object.entries(values))
|
|
353
|
+
assertSingleLine(token, value);
|
|
354
|
+
if (resolvedEnvPath !== undefined)
|
|
355
|
+
assertSingleLine('envPath', resolvedEnvPath);
|
|
356
|
+
// Escaped per format: XML for the plist, and for systemd per directive — the command-line
|
|
357
|
+
// argument quoted-and-escaped, every other value specifier-escaped. Escaping a value for the
|
|
358
|
+
// wrong format puts the escape text into the path the service actually runs.
|
|
359
|
+
const escape = backend.kind === 'launchd'
|
|
360
|
+
? (token, value) => escapeXml(token === 'projectName' ? commentSafe(value) : value)
|
|
361
|
+
: (token, value) => SYSTEMD_ARGUMENT_TOKENS.has(token) ? escapeSystemdArgument(value) : escapeSystemdValue(value);
|
|
362
|
+
const substituted = Object.fromEntries(Object.entries(values).map(([token, value]) => [token, escape(token, value)]));
|
|
363
|
+
// Escaped for its own destination rather than through the map above: the systemd stanza renders a
|
|
364
|
+
// *quoted* `Environment="PATH=…"`, so a `"` or a `\` in a toolchain directory would end or eat that
|
|
365
|
+
// quoting — the same reason `ExecStart`'s path takes the argument escaper (choice 3), on the same
|
|
366
|
+
// kind of destination. The plist arm quotes nothing, so it stays on XML escaping.
|
|
367
|
+
const environment = environmentStanza(backend.kind, resolvedEnvPath === undefined
|
|
368
|
+
? undefined
|
|
369
|
+
: backend.kind === 'launchd'
|
|
370
|
+
? escapeXml(resolvedEnvPath)
|
|
371
|
+
: escapeSystemdArgument(resolvedEnvPath));
|
|
372
|
+
return {
|
|
373
|
+
text: render(UNIT_TEMPLATES[backend.kind], { ...substituted, environment }),
|
|
374
|
+
targetPath: join(unitDir, fileName),
|
|
375
|
+
label,
|
|
376
|
+
projectName,
|
|
377
|
+
envPath: resolvedEnvPath,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
//# sourceMappingURL=units.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"units.js","sourceRoot":"","sources":["../../src/daemon/units.ts"],"names":[],"mappings":"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"}
|