harnery 0.46.0 → 0.47.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/README.md +24 -0
- package/bin/README.md +10 -0
- package/bin/agent-coord.mjs +3 -0
- package/bin/agent-hook.mjs +3 -0
- package/bin/harn.mjs +3 -0
- package/bin/run.mjs +43 -0
- package/dist/commander.d.ts +18 -6
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +74 -2
- package/dist/commands/agents.d.ts +25 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -27
- package/dist/commands/autoupdate.d.ts +12 -0
- package/dist/commands/autoupdate.d.ts.map +1 -0
- package/dist/commands/autoupdate.js +190 -0
- package/dist/commands/computer.d.ts +45 -0
- package/dist/commands/computer.d.ts.map +1 -0
- package/dist/commands/computer.js +509 -0
- package/dist/commands/docs.js +3 -1
- package/dist/commands/fetch.js +11 -1
- package/dist/commands/grep.d.ts +72 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +200 -47
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -9
- package/dist/commands/rm.js +2 -2
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +2 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -3
- package/dist/core/agents/coord-bin.d.ts +3 -0
- package/dist/core/agents/coord-bin.d.ts.map +1 -1
- package/dist/core/agents/coord-bin.js +6 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +6 -2
- package/dist/core/agents/render/prompt-context.d.ts +55 -7
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +122 -48
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- package/dist/core/agents/state/proc-start.d.ts +3 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +26 -4
- package/dist/core/autoupdate/engine.d.ts +97 -0
- package/dist/core/autoupdate/engine.d.ts.map +1 -0
- package/dist/core/autoupdate/engine.js +548 -0
- package/dist/core/autoupdate/fingerprint.d.ts +16 -0
- package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
- package/dist/core/autoupdate/fingerprint.js +73 -0
- package/dist/core/autoupdate/git.d.ts +30 -0
- package/dist/core/autoupdate/git.d.ts.map +1 -0
- package/dist/core/autoupdate/git.js +82 -0
- package/dist/core/autoupdate/index.d.ts +6 -0
- package/dist/core/autoupdate/index.d.ts.map +1 -0
- package/dist/core/autoupdate/index.js +4 -0
- package/dist/core/autoupdate/state.d.ts +59 -0
- package/dist/core/autoupdate/state.d.ts.map +1 -0
- package/dist/core/autoupdate/state.js +214 -0
- package/dist/core/autoupdate/types.d.ts +113 -0
- package/dist/core/autoupdate/types.d.ts.map +1 -0
- package/dist/core/autoupdate/types.js +35 -0
- package/dist/core/config.d.ts +53 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +114 -1
- package/dist/core/events/v3/bootstrap.js +1 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +1 -0
- package/dist/core/hooks/adapter/wiring.d.ts +12 -2
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +38 -8
- package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
- package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
- package/dist/core/hooks/autoupdate-cue.js +53 -0
- package/dist/core/hooks/cli.js +52 -4
- package/dist/core/hooks/effects/index.js +4 -4
- package/dist/core/hooks/resolve/coord-root.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- package/dist/core/semantic/contract.d.ts +3 -3
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +4 -2
- package/dist/core/storage/modes.d.ts +1 -1
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +5 -1
- package/dist/core/workflow/durable-record.d.ts +2 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +13 -1
- package/dist/core/workflow/engine.js +4 -0
- package/dist/core/workflow/spawn-claude.d.ts +7 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +16 -4
- package/dist/core/workflow/specialists.d.ts.map +1 -1
- package/dist/core/workflow/specialists.js +1 -0
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -9
- package/dist/lib/computer/cua.d.ts +50 -0
- package/dist/lib/computer/cua.d.ts.map +1 -0
- package/dist/lib/computer/cua.js +474 -0
- package/dist/lib/computer/host.d.ts +19 -0
- package/dist/lib/computer/host.d.ts.map +1 -0
- package/dist/lib/computer/host.js +72 -0
- package/dist/lib/computer/lease.d.ts +13 -0
- package/dist/lib/computer/lease.d.ts.map +1 -0
- package/dist/lib/computer/lease.js +269 -0
- package/dist/lib/computer/types.d.ts +163 -0
- package/dist/lib/computer/types.d.ts.map +1 -0
- package/dist/lib/computer/types.js +10 -0
- package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
- package/dist/lib/docs-metadata-v2.js +6 -5
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +1 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +5 -4
- package/dist/lib/headless/index.d.ts +33 -0
- package/dist/lib/headless/index.d.ts.map +1 -1
- package/dist/lib/headless/index.js +38 -2
- package/dist/lib/http/client.d.ts +2 -9
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +30 -9
- package/dist/lib/instructions/apply.d.ts +9 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +92 -14
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
- package/dist/lib/instructions/git-hooks.js +8 -4
- package/dist/lib/instructions/templates.d.ts +11 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +81 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +89 -1
- package/src/commander.ts +98 -8
- package/src/commands/agents.ts +100 -32
- package/src/commands/autoupdate.ts +250 -0
- package/src/commands/computer.ts +682 -0
- package/src/commands/docs.ts +3 -1
- package/src/commands/fetch.ts +11 -1
- package/src/commands/grep.ts +274 -59
- package/src/commands/init.ts +17 -7
- package/src/commands/rm.ts +2 -2
- package/src/commands/web.ts +2 -0
- package/src/core/agents/canonical-emit.ts +2 -3
- package/src/core/agents/coord-bin.ts +30 -0
- package/src/core/agents/coord-client.ts +6 -2
- package/src/core/agents/render/prompt-context.ts +152 -53
- package/src/core/agents/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- package/src/core/agents/state/proc-start.ts +29 -5
- package/src/core/autoupdate/engine.ts +691 -0
- package/src/core/autoupdate/fingerprint.ts +89 -0
- package/src/core/autoupdate/git.ts +120 -0
- package/src/core/autoupdate/index.ts +29 -0
- package/src/core/autoupdate/state.ts +269 -0
- package/src/core/autoupdate/types.ts +135 -0
- package/src/core/config.ts +142 -1
- package/src/core/events/v3/bootstrap.ts +1 -0
- package/src/core/events/v3/runtime-build.ts +1 -0
- package/src/core/hooks/adapter/wiring.ts +51 -6
- package/src/core/hooks/autoupdate-cue.ts +72 -0
- package/src/core/hooks/cli.ts +54 -1
- package/src/core/hooks/effects/index.ts +4 -4
- package/src/core/hooks/resolve/coord-root.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +61 -0
- package/src/core/semantic/service.ts +6 -2
- package/src/core/storage/modes.ts +7 -1
- package/src/core/workflow/durable-record.ts +15 -1
- package/src/core/workflow/engine.ts +3 -0
- package/src/core/workflow/spawn-claude.ts +16 -4
- package/src/core/workflow/specialists.ts +1 -0
- package/src/core/workflow/types.ts +10 -0
- package/src/core/workflow/workspaces/leases.ts +1 -9
- package/src/lib/computer/cua.ts +572 -0
- package/src/lib/computer/host.ts +87 -0
- package/src/lib/computer/lease.ts +299 -0
- package/src/lib/computer/types.ts +177 -0
- package/src/lib/docs-metadata-v2.ts +24 -5
- package/src/lib/exec.ts +1 -0
- package/src/lib/guarded-remove.ts +5 -3
- package/src/lib/headless/index.ts +66 -2
- package/src/lib/http/client.ts +33 -19
- package/src/lib/instructions/apply.ts +100 -12
- package/src/lib/instructions/git-hooks.ts +8 -4
- package/src/lib/instructions/templates.ts +95 -1
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project self-update engine (ADR 0229).
|
|
3
|
+
*
|
|
4
|
+
* `autoUpdateTick` is the hot path every prompt, session start, and host CLI
|
|
5
|
+
* command calls: one stat of the throttle stamp when nothing is due. When the
|
|
6
|
+
* stamp is stale it claims the stamp and launches a detached
|
|
7
|
+
* `<harn> autoupdate run --background`, which decides with `checkForUpdate`
|
|
8
|
+
* and, only when needed, runs the host's configured steps under a lock.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync, utimesSync, writeFileSync, } from "node:fs";
|
|
12
|
+
import { dirname, extname, resolve, win32 } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { readAutoUpdateConfig, resolveBinName } from "../config.js";
|
|
15
|
+
import { fingerprint } from "./fingerprint.js";
|
|
16
|
+
import { changedFiles, contains, defaultGitRunner, head, lsRemote, upstream, } from "./git.js";
|
|
17
|
+
import { acquireRunLock, addNotice, autoUpdateDir, autoUpdateLogPath, autoUpdateStampPath, inspectLock, mutateAutoUpdateState, readAutoUpdateState, } from "./state.js";
|
|
18
|
+
import { AUTOUPDATE_DEFAULTS, AUTOUPDATE_FILES, EXIT_NEEDS_PERSON, NOTICE_PREFIX, } from "./types.js";
|
|
19
|
+
/** Set on the detached runner and every step so nested CLI calls never tick. */
|
|
20
|
+
export const AUTOUPDATE_CHILD_ENV = "HARNERY_AUTOUPDATE_CHILD";
|
|
21
|
+
const MINUTE_MS = 60_000;
|
|
22
|
+
const DAY_MS = AUTOUPDATE_DEFAULTS.dailyHours * 60 * MINUTE_MS;
|
|
23
|
+
const LOG_ROTATE_BYTES = 256 * 1024;
|
|
24
|
+
const LOG_KEEP_BYTES = 64 * 1024;
|
|
25
|
+
const CLEANUP_TIMEOUT_MS = 5 * MINUTE_MS;
|
|
26
|
+
const KILL_GRACE_MS = 5_000;
|
|
27
|
+
function resolveDeps(deps) {
|
|
28
|
+
return {
|
|
29
|
+
now: deps.now ?? Date.now,
|
|
30
|
+
env: deps.env ?? process.env,
|
|
31
|
+
readConfig: deps.readConfig ?? readAutoUpdateConfig,
|
|
32
|
+
git: deps.git ?? defaultGitRunner,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// ------------------------------------------------------------------ helpers
|
|
36
|
+
/** The CLI entry the detached runner executes with `process.execPath`. */
|
|
37
|
+
export function harnCliEntry() {
|
|
38
|
+
const ext = extname(fileURLToPath(import.meta.url)) || ".js";
|
|
39
|
+
const entry = fileURLToPath(new URL(`../../cli${ext}`, import.meta.url));
|
|
40
|
+
if (existsSync(entry))
|
|
41
|
+
return entry;
|
|
42
|
+
return fileURLToPath(new URL("../../../bin/harn.mjs", import.meta.url));
|
|
43
|
+
}
|
|
44
|
+
/** Set the throttle stamp's mtime (creating it), which is all the tick reads. */
|
|
45
|
+
export function setStamp(coordRoot, atMs) {
|
|
46
|
+
const path = autoUpdateStampPath(coordRoot);
|
|
47
|
+
if (!existsSync(path)) {
|
|
48
|
+
mkdirSync(autoUpdateDir(coordRoot), { recursive: true });
|
|
49
|
+
writeFileSync(path, "");
|
|
50
|
+
}
|
|
51
|
+
const at = atMs / 1000;
|
|
52
|
+
utimesSync(path, at, at);
|
|
53
|
+
}
|
|
54
|
+
function stampMtime(coordRoot) {
|
|
55
|
+
try {
|
|
56
|
+
return statSync(autoUpdateStampPath(coordRoot)).mtimeMs;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Keep the log bounded: past 256 KB, keep only its last 64 KB. */
|
|
63
|
+
export function rotateLog(path) {
|
|
64
|
+
let size;
|
|
65
|
+
try {
|
|
66
|
+
size = statSync(path).size;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (size <= LOG_ROTATE_BYTES)
|
|
72
|
+
return;
|
|
73
|
+
const fd = openSync(path, "r");
|
|
74
|
+
const buffer = Buffer.alloc(LOG_KEEP_BYTES);
|
|
75
|
+
try {
|
|
76
|
+
readSync(fd, buffer, 0, LOG_KEEP_BYTES, size - LOG_KEEP_BYTES);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
closeSync(fd);
|
|
80
|
+
}
|
|
81
|
+
const tmp = `${path}.rotating`;
|
|
82
|
+
writeFileSync(tmp, buffer);
|
|
83
|
+
renameSync(tmp, path);
|
|
84
|
+
}
|
|
85
|
+
function appendLog(coordRoot, text) {
|
|
86
|
+
try {
|
|
87
|
+
const path = autoUpdateLogPath(coordRoot);
|
|
88
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
89
|
+
appendFileSync(path, text);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Logging is best effort.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function logLine(coordRoot, now, line) {
|
|
96
|
+
appendLog(coordRoot, `${new Date(now).toISOString()} ${line}\n`);
|
|
97
|
+
}
|
|
98
|
+
function isChild(env) {
|
|
99
|
+
return env[AUTOUPDATE_CHILD_ENV] === "1";
|
|
100
|
+
}
|
|
101
|
+
function dailyDue(cfg, state, now) {
|
|
102
|
+
if (cfg.dailySteps.length === 0)
|
|
103
|
+
return false;
|
|
104
|
+
if (!state.dailyAt)
|
|
105
|
+
return true;
|
|
106
|
+
const at = Date.parse(state.dailyAt);
|
|
107
|
+
return !Number.isFinite(at) || now - at >= DAY_MS;
|
|
108
|
+
}
|
|
109
|
+
function short(sha) {
|
|
110
|
+
return sha ? sha.slice(0, 7) : "unknown";
|
|
111
|
+
}
|
|
112
|
+
const INSTRUCTION_PREFIXES = [".agents/", ".claude/skills/", ".claude/rules/", ".cursor/rules/"];
|
|
113
|
+
const HOOK_PATHS = new Set([
|
|
114
|
+
".claude/settings.json",
|
|
115
|
+
".codex/hooks.json",
|
|
116
|
+
".codex/config.toml",
|
|
117
|
+
".cursor/hooks.json",
|
|
118
|
+
]);
|
|
119
|
+
export function isInstructionPath(path) {
|
|
120
|
+
const normalized = path.replace(/\\/g, "/");
|
|
121
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
122
|
+
if (base === "AGENTS.md" || base === "CLAUDE.md" || base === "SKILL.md")
|
|
123
|
+
return true;
|
|
124
|
+
if (base.endsWith(".mdc"))
|
|
125
|
+
return true;
|
|
126
|
+
return INSTRUCTION_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
127
|
+
}
|
|
128
|
+
export function isHookConfigPath(path) {
|
|
129
|
+
return HOOK_PATHS.has(path.replace(/\\/g, "/"));
|
|
130
|
+
}
|
|
131
|
+
/** `harnery-notice:` lines in step output. */
|
|
132
|
+
export function parseNoticeLines(output) {
|
|
133
|
+
const texts = [];
|
|
134
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
135
|
+
const line = raw.trimStart();
|
|
136
|
+
if (!line.startsWith(NOTICE_PREFIX))
|
|
137
|
+
continue;
|
|
138
|
+
const text = line.slice(NOTICE_PREFIX.length).trim();
|
|
139
|
+
if (text)
|
|
140
|
+
texts.push(text);
|
|
141
|
+
}
|
|
142
|
+
return texts;
|
|
143
|
+
}
|
|
144
|
+
// ------------------------------------------------------------------ tick
|
|
145
|
+
function defaultSpawnRunner(coordRoot) {
|
|
146
|
+
const root = resolve(coordRoot);
|
|
147
|
+
const logPath = autoUpdateLogPath(root);
|
|
148
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
149
|
+
rotateLog(logPath);
|
|
150
|
+
const fd = openSync(logPath, "a");
|
|
151
|
+
try {
|
|
152
|
+
const child = spawn(process.execPath, [harnCliEntry(), "autoupdate", "run", "--background"], {
|
|
153
|
+
cwd: root,
|
|
154
|
+
detached: true,
|
|
155
|
+
windowsHide: true,
|
|
156
|
+
stdio: ["ignore", fd, fd],
|
|
157
|
+
env: {
|
|
158
|
+
...process.env,
|
|
159
|
+
[AUTOUPDATE_CHILD_ENV]: "1",
|
|
160
|
+
HARNERY_COORD_ROOT_OVERRIDE: root,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
child.once("error", () => {
|
|
164
|
+
logLine(root, Date.now(), "runner could not start");
|
|
165
|
+
});
|
|
166
|
+
child.unref();
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
closeSync(fd);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Activity hook. Never throws. When throttled it reads the config and stats
|
|
174
|
+
* one file; it runs no Git and parses no state.
|
|
175
|
+
*/
|
|
176
|
+
export function autoUpdateTick(coordRoot, deps = {}) {
|
|
177
|
+
try {
|
|
178
|
+
const d = resolveDeps(deps);
|
|
179
|
+
if (isChild(d.env))
|
|
180
|
+
return { action: "disabled", detail: "inside an auto-update run" };
|
|
181
|
+
const cfg = d.readConfig(coordRoot);
|
|
182
|
+
if (!cfg?.enabled)
|
|
183
|
+
return { action: "disabled" };
|
|
184
|
+
const now = d.now();
|
|
185
|
+
const mtime = stampMtime(coordRoot);
|
|
186
|
+
if (mtime !== null && now - mtime < cfg.checkMinutes * MINUTE_MS) {
|
|
187
|
+
return { action: "throttled" };
|
|
188
|
+
}
|
|
189
|
+
const lock = inspectLock(coordRoot, cfg, now);
|
|
190
|
+
if (lock.held && !lock.stale) {
|
|
191
|
+
return { action: "locked", detail: lock.owner ? `pid ${lock.owner.pid}` : undefined };
|
|
192
|
+
}
|
|
193
|
+
// Claim before launching so concurrent ticks see a fresh stamp.
|
|
194
|
+
setStamp(coordRoot, now);
|
|
195
|
+
(deps.spawnRunner ?? defaultSpawnRunner)(coordRoot);
|
|
196
|
+
return { action: "launched" };
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return { action: "error", detail: err?.message ?? String(err) };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ------------------------------------------------------------------ check
|
|
203
|
+
export function checkForUpdate(coordRoot, cfg, state, deps = {}) {
|
|
204
|
+
const d = resolveDeps(deps);
|
|
205
|
+
const now = d.now();
|
|
206
|
+
const root = resolve(coordRoot);
|
|
207
|
+
const fp = fingerprint(cfg.watch, root, d.env);
|
|
208
|
+
const currentHead = head(root, d.git);
|
|
209
|
+
const base = {
|
|
210
|
+
head: currentHead,
|
|
211
|
+
upstream: null,
|
|
212
|
+
remoteSha: null,
|
|
213
|
+
fingerprint: fp,
|
|
214
|
+
dailyOnly: false,
|
|
215
|
+
};
|
|
216
|
+
if (deps.force)
|
|
217
|
+
return { ...base, needed: true, reason: "forced" };
|
|
218
|
+
const drift = !state.converged || state.converged.fingerprint !== fp;
|
|
219
|
+
const up = upstream(root, d.git);
|
|
220
|
+
if (!up) {
|
|
221
|
+
return drift
|
|
222
|
+
? { ...base, needed: true, reason: "drift", detail: "watched paths changed" }
|
|
223
|
+
: { ...base, needed: false, reason: "no-upstream" };
|
|
224
|
+
}
|
|
225
|
+
const withUp = { ...base, upstream: up.display };
|
|
226
|
+
const remoteSha = lsRemote(root, up.remote, up.branch, d.git);
|
|
227
|
+
if (!remoteSha) {
|
|
228
|
+
return { ...withUp, needed: false, reason: "offline", detail: `could not reach ${up.remote}` };
|
|
229
|
+
}
|
|
230
|
+
const withRemote = { ...withUp, remoteSha };
|
|
231
|
+
if (!contains(root, remoteSha, d.git)) {
|
|
232
|
+
return {
|
|
233
|
+
...withRemote,
|
|
234
|
+
needed: true,
|
|
235
|
+
reason: "behind",
|
|
236
|
+
detail: `${up.display} at ${short(remoteSha)}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (drift)
|
|
240
|
+
return { ...withRemote, needed: true, reason: "drift", detail: "watched paths changed" };
|
|
241
|
+
const last = state.lastRun;
|
|
242
|
+
if (last?.outcome === "failed") {
|
|
243
|
+
const finished = Date.parse(last.finishedAt);
|
|
244
|
+
if (!Number.isFinite(finished) ||
|
|
245
|
+
now - finished >= AUTOUPDATE_DEFAULTS.failureBackoffMinutes * MINUTE_MS) {
|
|
246
|
+
return { ...withRemote, needed: true, reason: "retry" };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (dailyDue(cfg, state, now)) {
|
|
250
|
+
return { ...withRemote, needed: true, reason: "daily", dailyOnly: true };
|
|
251
|
+
}
|
|
252
|
+
return { ...withRemote, needed: false, reason: "current" };
|
|
253
|
+
}
|
|
254
|
+
// ------------------------------------------------------------------ run
|
|
255
|
+
/**
|
|
256
|
+
* Resolve a step's argv for the current platform. On Windows a bare command
|
|
257
|
+
* name such as `bun` or `git` is looked up on PATH with PATHEXT, because
|
|
258
|
+
* spawning without a shell does not try `.cmd` shims; a `.cmd` or `.bat`
|
|
259
|
+
* match runs through `cmd.exe`. Elsewhere the argv is used as is.
|
|
260
|
+
*/
|
|
261
|
+
export function resolveStepCommand(cmd, args, env, platform = process.platform, exists = existsSync) {
|
|
262
|
+
if (platform !== "win32" || /[\\/]/.test(cmd) || extname(cmd) !== "") {
|
|
263
|
+
return { cmd, args, verbatim: false };
|
|
264
|
+
}
|
|
265
|
+
const pathVar = env.Path ?? env.PATH ?? "";
|
|
266
|
+
const exts = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
267
|
+
for (const dir of pathVar.split(";").filter(Boolean)) {
|
|
268
|
+
for (const ext of exts) {
|
|
269
|
+
const candidate = win32.join(dir, cmd + ext.toLowerCase());
|
|
270
|
+
if (!exists(candidate))
|
|
271
|
+
continue;
|
|
272
|
+
if (/^\.(cmd|bat)$/i.test(ext)) {
|
|
273
|
+
const quote = (s) => (/[\s"&|<>^]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s);
|
|
274
|
+
const line = [candidate, ...args].map(quote).join(" ");
|
|
275
|
+
return {
|
|
276
|
+
cmd: env.ComSpec ?? "cmd.exe",
|
|
277
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
278
|
+
verbatim: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { cmd: candidate, args, verbatim: false };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { cmd, args, verbatim: false };
|
|
285
|
+
}
|
|
286
|
+
export const defaultStepRunner = (argv, opts) => new Promise((resolveRun) => {
|
|
287
|
+
const started = Date.now();
|
|
288
|
+
let output = "";
|
|
289
|
+
let timedOut = false;
|
|
290
|
+
let settled = false;
|
|
291
|
+
const finish = (exitCode) => {
|
|
292
|
+
if (settled)
|
|
293
|
+
return;
|
|
294
|
+
settled = true;
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
resolveRun({ exitCode, timedOut, output, ms: Date.now() - started });
|
|
297
|
+
};
|
|
298
|
+
const [rawCmd, ...rawArgs] = argv;
|
|
299
|
+
if (!rawCmd) {
|
|
300
|
+
finish(null);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const { cmd, args, verbatim } = resolveStepCommand(rawCmd, rawArgs, opts.env ?? process.env);
|
|
304
|
+
const child = spawn(cmd, args, {
|
|
305
|
+
cwd: opts.cwd,
|
|
306
|
+
env: opts.env,
|
|
307
|
+
windowsHide: true,
|
|
308
|
+
windowsVerbatimArguments: verbatim,
|
|
309
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
310
|
+
});
|
|
311
|
+
const onData = (stream) => (data) => {
|
|
312
|
+
const chunk = data.toString("utf8");
|
|
313
|
+
output += chunk;
|
|
314
|
+
opts.onChunk?.(chunk, stream);
|
|
315
|
+
};
|
|
316
|
+
child.stdout?.on("data", onData("stdout"));
|
|
317
|
+
child.stderr?.on("data", onData("stderr"));
|
|
318
|
+
const timer = setTimeout(() => {
|
|
319
|
+
timedOut = true;
|
|
320
|
+
child.kill("SIGTERM");
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
if (!settled)
|
|
323
|
+
child.kill("SIGKILL");
|
|
324
|
+
}, KILL_GRACE_MS).unref();
|
|
325
|
+
}, opts.timeoutMs);
|
|
326
|
+
child.once("error", (err) => {
|
|
327
|
+
output += `${err.message}\n`;
|
|
328
|
+
finish(null);
|
|
329
|
+
});
|
|
330
|
+
child.once("exit", (code) => {
|
|
331
|
+
// A grandchild that keeps the pipes open must not hold the run.
|
|
332
|
+
setTimeout(() => finish(timedOut ? null : code), 2_000).unref();
|
|
333
|
+
});
|
|
334
|
+
child.once("close", (code) => finish(timedOut ? null : code));
|
|
335
|
+
});
|
|
336
|
+
function defaultCleanup(coordRoot) {
|
|
337
|
+
spawnSync(process.execPath, [harnCliEntry(), "artifacts", "clean", "--yes"], {
|
|
338
|
+
cwd: coordRoot,
|
|
339
|
+
env: { ...process.env, [AUTOUPDATE_CHILD_ENV]: "1", HARNERY_COORD_ROOT_OVERRIDE: coordRoot },
|
|
340
|
+
stdio: "ignore",
|
|
341
|
+
timeout: CLEANUP_TIMEOUT_MS,
|
|
342
|
+
windowsHide: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
export async function runAutoUpdate(coordRootRaw, opts = {}) {
|
|
346
|
+
const deps = opts.deps ?? {};
|
|
347
|
+
const d = resolveDeps(deps);
|
|
348
|
+
const root = resolve(coordRootRaw);
|
|
349
|
+
const cfg = d.readConfig(root);
|
|
350
|
+
if (!cfg || (!cfg.enabled && !opts.force)) {
|
|
351
|
+
return { outcome: "current", decision: null, record: null, notices: [] };
|
|
352
|
+
}
|
|
353
|
+
const startedMs = d.now();
|
|
354
|
+
const lock = acquireRunLock(root, cfg, startedMs);
|
|
355
|
+
if (!lock.acquired) {
|
|
356
|
+
return {
|
|
357
|
+
outcome: "skipped-locked",
|
|
358
|
+
decision: null,
|
|
359
|
+
record: null,
|
|
360
|
+
notices: [],
|
|
361
|
+
lockOwner: lock.owner,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const binName = deps.binName ?? resolveBinName(root);
|
|
365
|
+
const runStep = deps.runStep ?? defaultStepRunner;
|
|
366
|
+
const stepEnv = { ...d.env, [AUTOUPDATE_CHILD_ENV]: "1" };
|
|
367
|
+
const pending = [];
|
|
368
|
+
const steps = [];
|
|
369
|
+
const startedAt = new Date(startedMs).toISOString();
|
|
370
|
+
let decision = null;
|
|
371
|
+
let outcome = "current";
|
|
372
|
+
let fromHead = null;
|
|
373
|
+
let toHead = null;
|
|
374
|
+
let error;
|
|
375
|
+
let convergedUpdate;
|
|
376
|
+
let dailyAt;
|
|
377
|
+
let backoff = false;
|
|
378
|
+
const say = (line) => {
|
|
379
|
+
logLine(root, d.now(), line);
|
|
380
|
+
opts.onOutput?.(`${line}\n`, "stderr");
|
|
381
|
+
};
|
|
382
|
+
// Returns "ok" | "blocked" | "failed".
|
|
383
|
+
const runPhase = async (list, label) => {
|
|
384
|
+
for (const argv of list) {
|
|
385
|
+
const shown = argv.join(" ");
|
|
386
|
+
say(`${label} start: ${shown}`);
|
|
387
|
+
const result = await runStep(argv, {
|
|
388
|
+
cwd: root,
|
|
389
|
+
timeoutMs: cfg.timeoutMinutes * MINUTE_MS,
|
|
390
|
+
env: stepEnv,
|
|
391
|
+
onChunk: (chunk, stream) => {
|
|
392
|
+
appendLog(root, chunk);
|
|
393
|
+
opts.onOutput?.(chunk, stream);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
steps.push({
|
|
397
|
+
argv,
|
|
398
|
+
exitCode: result.exitCode,
|
|
399
|
+
ms: result.ms,
|
|
400
|
+
...(result.timedOut ? { timedOut: true } : {}),
|
|
401
|
+
});
|
|
402
|
+
const needsPerson = result.exitCode === EXIT_NEEDS_PERSON;
|
|
403
|
+
for (const text of parseNoticeLines(result.output)) {
|
|
404
|
+
pending.push({ kind: needsPerson ? "person" : "info", text });
|
|
405
|
+
}
|
|
406
|
+
say(`${label} exit ${result.exitCode ?? "none"}${result.timedOut ? " (timed out)" : ""} in ${result.ms}ms: ${shown}`);
|
|
407
|
+
if (needsPerson)
|
|
408
|
+
return "blocked";
|
|
409
|
+
if (result.exitCode !== 0) {
|
|
410
|
+
const how = result.timedOut
|
|
411
|
+
? `timed out after ${cfg.timeoutMinutes} min`
|
|
412
|
+
: `exit ${result.exitCode ?? "none"}`;
|
|
413
|
+
pending.push({
|
|
414
|
+
kind: "failure",
|
|
415
|
+
text: `Auto-update step \`${shown}\` failed (${how}); see .harnery/logs/${AUTOUPDATE_FILES.log} or run \`${binName} autoupdate run\`.`,
|
|
416
|
+
});
|
|
417
|
+
return "failed";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return "ok";
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
const state = readAutoUpdateState(root);
|
|
424
|
+
decision = checkForUpdate(root, cfg, state, { ...deps, force: opts.force });
|
|
425
|
+
fromHead = decision.head;
|
|
426
|
+
toHead = decision.head;
|
|
427
|
+
say(`check: ${decision.reason}${decision.detail ? ` (${decision.detail})` : ""}`);
|
|
428
|
+
if (!decision.needed) {
|
|
429
|
+
outcome = decision.reason === "offline" ? "offline" : "current";
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
let phase = "ok";
|
|
433
|
+
if (!decision.dailyOnly)
|
|
434
|
+
phase = await runPhase(cfg.steps, "step");
|
|
435
|
+
if (phase === "ok") {
|
|
436
|
+
toHead = head(root, d.git);
|
|
437
|
+
if (fromHead && toHead && fromHead !== toHead) {
|
|
438
|
+
const changed = changedFiles(root, fromHead, toHead, d.git);
|
|
439
|
+
const moved = `This checkout updated ${short(fromHead)}→${short(toHead)}.`;
|
|
440
|
+
if (changed.some(isInstructionPath)) {
|
|
441
|
+
pending.push({
|
|
442
|
+
kind: "instructions",
|
|
443
|
+
text: `${moved} Re-read AGENTS.md and any skill you are using before continuing.`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (changed.some(isHookConfigPath)) {
|
|
447
|
+
pending.push({
|
|
448
|
+
kind: "hooks",
|
|
449
|
+
text: `${moved} Hook settings changed; start a new session so they load.`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
convergedUpdate = {
|
|
454
|
+
head: toHead ?? "",
|
|
455
|
+
remoteSha: decision.remoteSha,
|
|
456
|
+
fingerprint: fingerprint(cfg.watch, root, d.env),
|
|
457
|
+
at: new Date(d.now()).toISOString(),
|
|
458
|
+
};
|
|
459
|
+
if (dailyDue(cfg, state, d.now())) {
|
|
460
|
+
const daily = await runPhase(cfg.dailySteps, "daily");
|
|
461
|
+
if (daily !== "failed")
|
|
462
|
+
dailyAt = new Date(d.now()).toISOString();
|
|
463
|
+
phase = daily;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
outcome =
|
|
467
|
+
phase === "blocked"
|
|
468
|
+
? "blocked"
|
|
469
|
+
: phase === "failed"
|
|
470
|
+
? "failed"
|
|
471
|
+
: decision.dailyOnly
|
|
472
|
+
? "current"
|
|
473
|
+
: "updated";
|
|
474
|
+
backoff = phase === "failed";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
outcome = "failed";
|
|
479
|
+
backoff = true;
|
|
480
|
+
error = err?.message ?? String(err);
|
|
481
|
+
say(`error: ${error}`);
|
|
482
|
+
}
|
|
483
|
+
const notices = [];
|
|
484
|
+
const finishedMs = d.now();
|
|
485
|
+
const record = {
|
|
486
|
+
startedAt,
|
|
487
|
+
finishedAt: new Date(finishedMs).toISOString(),
|
|
488
|
+
outcome,
|
|
489
|
+
reason: decision?.reason ?? "forced",
|
|
490
|
+
fromHead,
|
|
491
|
+
toHead,
|
|
492
|
+
steps,
|
|
493
|
+
...(error ? { error } : {}),
|
|
494
|
+
};
|
|
495
|
+
let cleanupAt;
|
|
496
|
+
try {
|
|
497
|
+
const priorState = readAutoUpdateState(root);
|
|
498
|
+
const lastCleanup = priorState.cleanupAt ? Date.parse(priorState.cleanupAt) : Number.NaN;
|
|
499
|
+
const cleanupDue = cfg.cleanup &&
|
|
500
|
+
(outcome === "updated" || outcome === "current") &&
|
|
501
|
+
(!Number.isFinite(lastCleanup) || finishedMs - lastCleanup >= DAY_MS);
|
|
502
|
+
if (cleanupDue) {
|
|
503
|
+
try {
|
|
504
|
+
(deps.runCleanup ?? defaultCleanup)(root);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Cleanup is best effort.
|
|
508
|
+
}
|
|
509
|
+
cleanupAt = new Date(d.now()).toISOString();
|
|
510
|
+
}
|
|
511
|
+
mutateAutoUpdateState(root, (s) => {
|
|
512
|
+
s.lastRun = record;
|
|
513
|
+
if (convergedUpdate)
|
|
514
|
+
s.converged = convergedUpdate;
|
|
515
|
+
if (dailyAt)
|
|
516
|
+
s.dailyAt = dailyAt;
|
|
517
|
+
if (cleanupAt)
|
|
518
|
+
s.cleanupAt = cleanupAt;
|
|
519
|
+
// A run that succeeded resolves earlier failures and person requests,
|
|
520
|
+
// so sessions that start later are not told about a fixed problem.
|
|
521
|
+
if (outcome === "updated" || outcome === "current") {
|
|
522
|
+
s.notices = s.notices.filter((n) => n.kind !== "failure" && n.kind !== "person");
|
|
523
|
+
}
|
|
524
|
+
for (const n of pending)
|
|
525
|
+
notices.push(addNotice(s, n.kind, n.text, finishedMs));
|
|
526
|
+
});
|
|
527
|
+
// A failed run pushes the stamp forward so the tick stays one stat while backing off.
|
|
528
|
+
const backoffMs = AUTOUPDATE_DEFAULTS.failureBackoffMinutes * MINUTE_MS;
|
|
529
|
+
setStamp(root, backoff ? finishedMs + backoffMs - cfg.checkMinutes * MINUTE_MS : finishedMs);
|
|
530
|
+
say(`run ${outcome} (${record.reason})`);
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
lock.release();
|
|
534
|
+
}
|
|
535
|
+
return { outcome, decision, record, notices };
|
|
536
|
+
}
|
|
537
|
+
export function autoUpdateStatus(coordRootRaw, deps = {}) {
|
|
538
|
+
const d = resolveDeps(deps);
|
|
539
|
+
const root = resolve(coordRootRaw);
|
|
540
|
+
const config = d.readConfig(root);
|
|
541
|
+
const now = d.now();
|
|
542
|
+
const mtime = stampMtime(root);
|
|
543
|
+
const stampAge = mtime === null ? null : now - mtime;
|
|
544
|
+
const checkMs = (config?.checkMinutes ?? AUTOUPDATE_DEFAULTS.checkMinutes) * MINUTE_MS;
|
|
545
|
+
const nextCheckMs = stampAge === null ? 0 : Math.max(0, checkMs - stampAge);
|
|
546
|
+
const locked = inspectLock(root, config ?? { timeoutMinutes: AUTOUPDATE_DEFAULTS.timeoutMinutes, steps: [], dailySteps: [] }, now);
|
|
547
|
+
return { config, state: readAutoUpdateState(root), stampAge, nextCheckMs, locked };
|
|
548
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fingerprint of the watched paths outside Git. A change in any watched
|
|
3
|
+
* file's mtime or size, or in a watched directory's direct children, changes
|
|
4
|
+
* the fingerprint and forces an update run (`drift`).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Expand `~/` (or a bare `~`) and `$VAR` / `${VAR}`. Returns null when a
|
|
8
|
+
* referenced variable is unset, so the entry fingerprints as missing instead
|
|
9
|
+
* of resolving somewhere unintended.
|
|
10
|
+
*/
|
|
11
|
+
export declare function expandWatchPath(entry: string, projectRoot: string, env?: NodeJS.ProcessEnv): string | null;
|
|
12
|
+
/** The raw lines behind the fingerprint (sorted), for diagnostics and tests. */
|
|
13
|
+
export declare function fingerprintLines(watch: readonly string[], projectRoot: string, env?: NodeJS.ProcessEnv): string[];
|
|
14
|
+
/** sha1 hex of the sorted fingerprint lines. */
|
|
15
|
+
export declare function fingerprint(watch: readonly string[], projectRoot: string, env?: NodeJS.ProcessEnv): string;
|
|
16
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../../src/core/autoupdate/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,IAAI,CAmBf;AAWD,gFAAgF;AAChF,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,EAAE,CAsBV;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAIR"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fingerprint of the watched paths outside Git. A change in any watched
|
|
3
|
+
* file's mtime or size, or in a watched directory's direct children, changes
|
|
4
|
+
* the fingerprint and forces an update run (`drift`).
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
/**
|
|
11
|
+
* Expand `~/` (or a bare `~`) and `$VAR` / `${VAR}`. Returns null when a
|
|
12
|
+
* referenced variable is unset, so the entry fingerprints as missing instead
|
|
13
|
+
* of resolving somewhere unintended.
|
|
14
|
+
*/
|
|
15
|
+
export function expandWatchPath(entry, projectRoot, env = process.env) {
|
|
16
|
+
let unset = false;
|
|
17
|
+
let out = entry.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_m, a, b) => {
|
|
18
|
+
const value = env[(a ?? b)];
|
|
19
|
+
if (value === undefined || value === "") {
|
|
20
|
+
unset = true;
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
});
|
|
25
|
+
if (unset)
|
|
26
|
+
return null;
|
|
27
|
+
if (out === "~" || out.startsWith("~/") || out.startsWith("~\\")) {
|
|
28
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
29
|
+
out = join(home, out.slice(1));
|
|
30
|
+
}
|
|
31
|
+
return isAbsolute(out) ? resolve(out) : resolve(projectRoot, out);
|
|
32
|
+
}
|
|
33
|
+
function statLine(path) {
|
|
34
|
+
try {
|
|
35
|
+
const st = statSync(path);
|
|
36
|
+
return `${st.isDirectory() ? "dir" : "file"}\t${Math.trunc(st.mtimeMs)}\t${st.isDirectory() ? 0 : st.size}`;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The raw lines behind the fingerprint (sorted), for diagnostics and tests. */
|
|
43
|
+
export function fingerprintLines(watch, projectRoot, env = process.env) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
for (const entry of watch) {
|
|
46
|
+
const path = expandWatchPath(entry, projectRoot, env);
|
|
47
|
+
const self = path ? statLine(path) : null;
|
|
48
|
+
if (!path || !self) {
|
|
49
|
+
lines.push(`${entry}\tmissing`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
lines.push(`${entry}\t${self}`);
|
|
53
|
+
if (!self.startsWith("dir\t"))
|
|
54
|
+
continue;
|
|
55
|
+
let children = [];
|
|
56
|
+
try {
|
|
57
|
+
children = readdirSync(path);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
children = [];
|
|
61
|
+
}
|
|
62
|
+
for (const child of children) {
|
|
63
|
+
lines.push(`${entry}/${child}\t${statLine(join(path, child)) ?? "missing"}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return lines.sort();
|
|
67
|
+
}
|
|
68
|
+
/** sha1 hex of the sorted fingerprint lines. */
|
|
69
|
+
export function fingerprint(watch, projectRoot, env = process.env) {
|
|
70
|
+
return createHash("sha1")
|
|
71
|
+
.update(fingerprintLines(watch, projectRoot, env).join("\n"))
|
|
72
|
+
.digest("hex");
|
|
73
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The few Git reads self-update needs. Every call is synchronous, bounded by a
|
|
3
|
+
* timeout, and goes through an injectable runner so tests never touch a
|
|
4
|
+
* network remote.
|
|
5
|
+
*/
|
|
6
|
+
export interface GitResult {
|
|
7
|
+
status: number | null;
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
}
|
|
11
|
+
export type GitRunner = (args: string[], opts: {
|
|
12
|
+
cwd: string;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
}) => GitResult;
|
|
15
|
+
export declare const defaultGitRunner: GitRunner;
|
|
16
|
+
export declare function head(root: string, run?: GitRunner): string | null;
|
|
17
|
+
export interface Upstream {
|
|
18
|
+
remote: string;
|
|
19
|
+
branch: string;
|
|
20
|
+
/** e.g. `origin/main`. */
|
|
21
|
+
display: string;
|
|
22
|
+
}
|
|
23
|
+
/** The current branch's upstream, or null when detached or untracked. */
|
|
24
|
+
export declare function upstream(root: string, run?: GitRunner): Upstream | null;
|
|
25
|
+
/** The remote branch's sha, or null when the remote cannot be reached or lacks the branch. */
|
|
26
|
+
export declare function lsRemote(root: string, remote: string, branch: string, run?: GitRunner): string | null;
|
|
27
|
+
/** True when `sha` exists locally and is an ancestor of HEAD. */
|
|
28
|
+
export declare function contains(root: string, sha: string, run?: GitRunner): boolean;
|
|
29
|
+
export declare function changedFiles(root: string, from: string, to: string, run?: GitRunner): string[];
|
|
30
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../src/core/autoupdate/git.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KAAK,SAAS,CAAC;AAKhG,eAAO,MAAM,gBAAgB,EAAE,SAc9B,CAAC;AAcF,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,SAA4B,GAAG,MAAM,GAAG,IAAI,CAEnF;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,SAA4B,GAAG,QAAQ,GAAG,IAAI,CAczF;AAED,8FAA8F;AAC9F,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,SAA4B,GAChC,MAAM,GAAG,IAAI,CAUf;AAED,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,SAA4B,GAAG,OAAO,CAW9F;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,GAAG,GAAE,SAA4B,GAChC,MAAM,EAAE,CAOV"}
|