misterpropre 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/chunk-FDTKUXXE.js +418 -0
- package/dist/chunk-HMGF6JWH.js +884 -0
- package/dist/chunk-JCB4UDCP.js +1348 -0
- package/dist/cli.js +256 -0
- package/dist/dashboard-W6QCV3NV.js +335 -0
- package/dist/setup-R2IL4RHH.js +8 -0
- package/package.json +64 -0
- package/skills/dependabot-purge/SKILL.md.tmpl +137 -0
- package/skills/dependabot-purge/references/fix-matrix.md.tmpl +106 -0
- package/skills/dependabot-purge/scripts/ensure_docker.sh +33 -0
- package/skills/dependabot-purge/scripts/wait_checks.sh +66 -0
- package/skills/security-dependabot-fix/SKILL.md.tmpl +171 -0
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Banner,
|
|
3
|
+
FIX_INSTALL_NAME,
|
|
4
|
+
GhCli,
|
|
5
|
+
PURGE_INSTALL_NAME,
|
|
6
|
+
Panel,
|
|
7
|
+
appT,
|
|
8
|
+
appendRun,
|
|
9
|
+
configDir,
|
|
10
|
+
phaseLabel,
|
|
11
|
+
readConfig,
|
|
12
|
+
renderBundledSkill,
|
|
13
|
+
summarizeOutcome,
|
|
14
|
+
theme,
|
|
15
|
+
writeConfig
|
|
16
|
+
} from "./chunk-HMGF6JWH.js";
|
|
17
|
+
|
|
18
|
+
// src/core/spawn.ts
|
|
19
|
+
import { spawn } from "child_process";
|
|
20
|
+
var activeAgents = /* @__PURE__ */ new Set();
|
|
21
|
+
function killGroup(child, signal) {
|
|
22
|
+
if (child.pid == null) return;
|
|
23
|
+
try {
|
|
24
|
+
process.kill(-child.pid, signal);
|
|
25
|
+
} catch {
|
|
26
|
+
try {
|
|
27
|
+
child.kill(signal);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function killActiveAgents(signal) {
|
|
33
|
+
for (const child of activeAgents) killGroup(child, signal);
|
|
34
|
+
}
|
|
35
|
+
function runAgent(opts) {
|
|
36
|
+
const { command, args, env, cwd, timeoutMs, killGraceMs = 6e4, onStdout, onStderr } = opts;
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
let stdout = "";
|
|
40
|
+
let stderr = "";
|
|
41
|
+
let timedOut = false;
|
|
42
|
+
let settled = false;
|
|
43
|
+
let killTimer;
|
|
44
|
+
let graceTimer;
|
|
45
|
+
const child = spawn(command, args, {
|
|
46
|
+
env: env ?? process.env,
|
|
47
|
+
cwd,
|
|
48
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
49
|
+
detached: true
|
|
50
|
+
// own process group → the shutdown handler/timeout can kill the agent + its children
|
|
51
|
+
});
|
|
52
|
+
activeAgents.add(child);
|
|
53
|
+
child.stdout?.setEncoding("utf8");
|
|
54
|
+
child.stderr?.setEncoding("utf8");
|
|
55
|
+
child.stdout?.on("data", (c) => {
|
|
56
|
+
stdout += c;
|
|
57
|
+
onStdout?.(c);
|
|
58
|
+
});
|
|
59
|
+
child.stderr?.on("data", (c) => {
|
|
60
|
+
stderr += c;
|
|
61
|
+
onStderr?.(c);
|
|
62
|
+
});
|
|
63
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
64
|
+
killTimer = setTimeout(() => {
|
|
65
|
+
timedOut = true;
|
|
66
|
+
killGroup(child, "SIGTERM");
|
|
67
|
+
graceTimer = setTimeout(() => killGroup(child, "SIGKILL"), killGraceMs);
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
}
|
|
70
|
+
const finish = (code, signal) => {
|
|
71
|
+
if (settled) return;
|
|
72
|
+
settled = true;
|
|
73
|
+
activeAgents.delete(child);
|
|
74
|
+
if (killTimer) clearTimeout(killTimer);
|
|
75
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
76
|
+
resolve({ stdout, stderr, code, signal, timedOut, durationMs: Date.now() - start });
|
|
77
|
+
};
|
|
78
|
+
child.on("error", (err) => {
|
|
79
|
+
stderr += String(err);
|
|
80
|
+
finish(null, null);
|
|
81
|
+
});
|
|
82
|
+
child.on("close", (code, signal) => finish(code, signal));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/core/lock.ts
|
|
87
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
88
|
+
import { homedir } from "os";
|
|
89
|
+
import { join } from "path";
|
|
90
|
+
|
|
91
|
+
// src/core/proc.ts
|
|
92
|
+
function isProcessAlive(pid) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, 0);
|
|
95
|
+
return true;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return e?.code === "EPERM";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/core/lock.ts
|
|
102
|
+
var safe = (s) => s.replace(/[/\\]/g, "_");
|
|
103
|
+
function locksRoot() {
|
|
104
|
+
const base = process.env.XDG_CACHE_HOME?.trim() || join(homedir(), ".cache");
|
|
105
|
+
return join(base, "misterpropre", "locks");
|
|
106
|
+
}
|
|
107
|
+
function lockDirFor(repo) {
|
|
108
|
+
return join(locksRoot(), `${safe(repo)}.lock`);
|
|
109
|
+
}
|
|
110
|
+
function readMeta(lockDir) {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(readFileSync(join(lockDir, "meta.json"), "utf8"));
|
|
113
|
+
} catch {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function acquireRepoLock(repo) {
|
|
118
|
+
const lockDir = lockDirFor(repo);
|
|
119
|
+
mkdirSync(locksRoot(), { recursive: true });
|
|
120
|
+
const tryCreate = () => {
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(lockDir);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
if (!tryCreate()) {
|
|
129
|
+
const meta = readMeta(lockDir);
|
|
130
|
+
if (meta.pid && isProcessAlive(meta.pid)) {
|
|
131
|
+
return { acquired: false, heldByPid: meta.pid, since: meta.since };
|
|
132
|
+
}
|
|
133
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
134
|
+
if (!tryCreate()) {
|
|
135
|
+
const m2 = readMeta(lockDir);
|
|
136
|
+
return { acquired: false, heldByPid: m2.pid, since: m2.since };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
writeFileSync(
|
|
140
|
+
join(lockDir, "meta.json"),
|
|
141
|
+
JSON.stringify({ pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
142
|
+
"utf8"
|
|
143
|
+
);
|
|
144
|
+
return {
|
|
145
|
+
acquired: true,
|
|
146
|
+
handle: { repo, path: lockDir, release: () => rmSync(lockDir, { recursive: true, force: true }) }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function releaseOwnLocks() {
|
|
150
|
+
let names;
|
|
151
|
+
try {
|
|
152
|
+
names = readdirSync(locksRoot());
|
|
153
|
+
} catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
const lockDir = join(locksRoot(), name);
|
|
158
|
+
if (readMeta(lockDir).pid === process.pid) {
|
|
159
|
+
try {
|
|
160
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/core/worktree.ts
|
|
168
|
+
import { existsSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, realpathSync, rmSync as rmSync2, statSync } from "fs";
|
|
169
|
+
import { homedir as homedir2 } from "os";
|
|
170
|
+
import { dirname, isAbsolute, join as join2, relative } from "path";
|
|
171
|
+
|
|
172
|
+
// src/core/git.ts
|
|
173
|
+
import { execFile } from "child_process";
|
|
174
|
+
import { promisify } from "util";
|
|
175
|
+
var pexec = promisify(execFile);
|
|
176
|
+
async function git(args, opts = {}) {
|
|
177
|
+
const { stdout, stderr } = await pexec("git", args, {
|
|
178
|
+
cwd: opts.cwd,
|
|
179
|
+
encoding: "utf8",
|
|
180
|
+
maxBuffer: 32 * 1024 * 1024
|
|
181
|
+
});
|
|
182
|
+
return { stdout, stderr };
|
|
183
|
+
}
|
|
184
|
+
async function gitSafe(args, opts = {}) {
|
|
185
|
+
try {
|
|
186
|
+
const { stdout, stderr } = await git(args, opts);
|
|
187
|
+
return { ok: true, stdout, stderr };
|
|
188
|
+
} catch (e) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
stdout: typeof e?.stdout === "string" ? e.stdout : "",
|
|
192
|
+
stderr: typeof e?.stderr === "string" ? e.stderr : String(e)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/core/worktree.ts
|
|
198
|
+
function under(child, parent) {
|
|
199
|
+
const real = (p) => {
|
|
200
|
+
try {
|
|
201
|
+
return realpathSync(p);
|
|
202
|
+
} catch {
|
|
203
|
+
return p;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const rel = relative(real(parent), real(child));
|
|
207
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
208
|
+
}
|
|
209
|
+
var safe2 = (s) => s.replace(/[/\\]/g, "_");
|
|
210
|
+
function worktreeRoot() {
|
|
211
|
+
const base = process.env.XDG_CACHE_HOME?.trim() || join2(homedir2(), ".cache");
|
|
212
|
+
return join2(base, "misterpropre", "wt");
|
|
213
|
+
}
|
|
214
|
+
function worktreePath(repo, phase, runId) {
|
|
215
|
+
return join2(worktreeRoot(), safe2(repo), `${safe2(phase)}-${safe2(runId)}`);
|
|
216
|
+
}
|
|
217
|
+
async function worktreeHoldingBranch(clonePath, branch) {
|
|
218
|
+
const { stdout } = await git(["-C", clonePath, "worktree", "list", "--porcelain"]);
|
|
219
|
+
let current = null;
|
|
220
|
+
for (const line of stdout.split("\n")) {
|
|
221
|
+
if (line.startsWith("worktree ")) current = line.slice("worktree ".length);
|
|
222
|
+
else if (line === `branch refs/heads/${branch}` && current) return current;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
async function branchExists(clonePath, branch) {
|
|
227
|
+
return (await gitSafe(["-C", clonePath, "rev-parse", "--verify", "--quiet", `refs/heads/${branch}`])).ok;
|
|
228
|
+
}
|
|
229
|
+
var activeWorktrees = /* @__PURE__ */ new Set();
|
|
230
|
+
function track(wt) {
|
|
231
|
+
if (!wt.external && under(wt.path, worktreeRoot())) activeWorktrees.add(wt.path);
|
|
232
|
+
return wt;
|
|
233
|
+
}
|
|
234
|
+
function removeActiveWorktreesSync() {
|
|
235
|
+
for (const p of activeWorktrees) {
|
|
236
|
+
if (under(p, worktreeRoot())) {
|
|
237
|
+
try {
|
|
238
|
+
rmSync2(p, { recursive: true, force: true });
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
activeWorktrees.clear();
|
|
244
|
+
}
|
|
245
|
+
async function addWorktree(opts) {
|
|
246
|
+
const { clonePath, path, newBranch, startPoint, ref, force } = opts;
|
|
247
|
+
const targetBranch = newBranch ?? ref;
|
|
248
|
+
if (targetBranch) {
|
|
249
|
+
const holder = await worktreeHoldingBranch(clonePath, targetBranch);
|
|
250
|
+
if (holder) {
|
|
251
|
+
return track({ path: holder, branch: targetBranch, reused: true, external: !under(holder, worktreeRoot()) });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
255
|
+
const args = ["-C", clonePath, "worktree", "add"];
|
|
256
|
+
if (force) args.push("--force");
|
|
257
|
+
if (newBranch) {
|
|
258
|
+
if (!startPoint) throw new Error("addWorktree: startPoint is required with newBranch");
|
|
259
|
+
if (await branchExists(clonePath, newBranch)) {
|
|
260
|
+
args.push(path, newBranch);
|
|
261
|
+
await git(args);
|
|
262
|
+
return track({ path, branch: newBranch, reused: true, external: false });
|
|
263
|
+
}
|
|
264
|
+
args.push("-b", newBranch, path, startPoint);
|
|
265
|
+
} else if (ref) {
|
|
266
|
+
args.push(path, ref);
|
|
267
|
+
} else {
|
|
268
|
+
args.push(path);
|
|
269
|
+
}
|
|
270
|
+
await git(args);
|
|
271
|
+
return track({ path, branch: newBranch, external: false });
|
|
272
|
+
}
|
|
273
|
+
async function removeWorktree(clonePath, path) {
|
|
274
|
+
activeWorktrees.delete(path);
|
|
275
|
+
await gitSafe(["-C", clonePath, "worktree", "remove", "--force", path]);
|
|
276
|
+
if (under(path, worktreeRoot())) rmSync2(path, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
async function gcWorktrees(opts = {}) {
|
|
279
|
+
const root = worktreeRoot();
|
|
280
|
+
const removed = [];
|
|
281
|
+
if (!existsSync(root)) return { removed };
|
|
282
|
+
const now = opts.now ?? Date.now();
|
|
283
|
+
const maxAge = opts.maxAgeMs ?? 3 * 24 * 60 * 60 * 1e3;
|
|
284
|
+
for (const repo of readdirSync2(root)) {
|
|
285
|
+
const repoDir = join2(root, repo);
|
|
286
|
+
let entries;
|
|
287
|
+
try {
|
|
288
|
+
entries = readdirSync2(repoDir);
|
|
289
|
+
} catch {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
for (const name of entries) {
|
|
293
|
+
const p = join2(repoDir, name);
|
|
294
|
+
let mtimeMs;
|
|
295
|
+
try {
|
|
296
|
+
mtimeMs = statSync(p).mtimeMs;
|
|
297
|
+
} catch {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (now - mtimeMs > maxAge) {
|
|
301
|
+
rmSync2(p, { recursive: true, force: true });
|
|
302
|
+
removed.push(p);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (opts.clonePath) await gitSafe(["-C", opts.clonePath, "worktree", "prune"]);
|
|
307
|
+
return { removed };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/core/shutdown.ts
|
|
311
|
+
var shuttingDown = false;
|
|
312
|
+
function requestShutdown(code) {
|
|
313
|
+
if (shuttingDown) return;
|
|
314
|
+
shuttingDown = true;
|
|
315
|
+
try {
|
|
316
|
+
process.stderr.write("\n\u23F9 Stopping \u2014 killing the agent and cleaning up\u2026\n");
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
killActiveAgents("SIGTERM");
|
|
320
|
+
try {
|
|
321
|
+
releaseOwnLocks();
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
removeActiveWorktreesSync();
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
setTimeout(() => {
|
|
329
|
+
killActiveAgents("SIGKILL");
|
|
330
|
+
process.exit(code);
|
|
331
|
+
}, 1500);
|
|
332
|
+
}
|
|
333
|
+
function installSignalHandlers() {
|
|
334
|
+
process.on("SIGINT", () => requestShutdown(130));
|
|
335
|
+
process.on("SIGTERM", () => requestShutdown(143));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/commands/run.ts
|
|
339
|
+
import { existsSync as existsSync4 } from "fs";
|
|
340
|
+
import { homedir as homedir4 } from "os";
|
|
341
|
+
import { join as join6 } from "path";
|
|
342
|
+
import { execFile as execFile2 } from "child_process";
|
|
343
|
+
import { promisify as promisify2 } from "util";
|
|
344
|
+
|
|
345
|
+
// src/core/providers/model.ts
|
|
346
|
+
var LATEST_OPUS = {
|
|
347
|
+
claude: "opus",
|
|
348
|
+
kiro: "claude-opus-4.8",
|
|
349
|
+
q: "opus"
|
|
350
|
+
// unused — the Q adapter inlines the skill and uses Q's default model
|
|
351
|
+
};
|
|
352
|
+
function resolveModel(model, provider) {
|
|
353
|
+
if (!model) return void 0;
|
|
354
|
+
return model === "opus" ? LATEST_OPUS[provider] : model;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/core/providers/claude.ts
|
|
358
|
+
var CLAUDE_BIN = process.env.MRP_CLAUDE_BIN || "claude";
|
|
359
|
+
function buildClaudeInvocation(skill, repo, opts) {
|
|
360
|
+
const args = ["-p", `/${skill} ${repo}`, "--dangerously-skip-permissions"];
|
|
361
|
+
if (opts.maxTurns) args.push("--max-turns", String(opts.maxTurns));
|
|
362
|
+
const model = resolveModel(opts.model, "claude");
|
|
363
|
+
if (model) args.push("--model", model);
|
|
364
|
+
if (opts.effort) args.push("--effort", opts.effort);
|
|
365
|
+
return { command: CLAUDE_BIN, args };
|
|
366
|
+
}
|
|
367
|
+
var ClaudeAdapter = class {
|
|
368
|
+
id = "claude";
|
|
369
|
+
buildInvocation(skill, repo, opts) {
|
|
370
|
+
return buildClaudeInvocation(skill, repo, opts);
|
|
371
|
+
}
|
|
372
|
+
invoke(skill, repo, opts) {
|
|
373
|
+
const { command, args } = buildClaudeInvocation(skill, repo, opts);
|
|
374
|
+
const env = { ...opts.env ?? process.env };
|
|
375
|
+
delete env.ANTHROPIC_API_KEY;
|
|
376
|
+
return runAgent({
|
|
377
|
+
command,
|
|
378
|
+
args,
|
|
379
|
+
env,
|
|
380
|
+
cwd: opts.cwd,
|
|
381
|
+
timeoutMs: opts.timeoutMs,
|
|
382
|
+
onStdout: opts.onStdout
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/core/providers/kiro.ts
|
|
388
|
+
import { execFileSync } from "child_process";
|
|
389
|
+
var KIRO_BIN = process.env.MRP_KIRO_BIN || "kiro-cli";
|
|
390
|
+
function promptFor(skill, repo) {
|
|
391
|
+
if (skill === PURGE_INSTALL_NAME) {
|
|
392
|
+
return `You ARE the misterpropre orchestrator's prepared invocation, operating in the "${repo}" repository's clone. The setup is done \u2014 do NOT ask for confirmation, do NOT clone, and do NOT propose alternatives. Run the "${skill}" skill's workflow NOW: process the open Renovate/Dependabot dependency-update PRs (merge what is safe, hand off the rest), then print the machine-readable MISTERPROPRE_RESULT_V1 and MISTERPROPRE_NOTIFY_V1 lines.`;
|
|
393
|
+
}
|
|
394
|
+
return `You ARE the misterpropre orchestrator's prepared invocation. The environment is ALREADY set up: your current working directory IS the isolated git worktree for the "${repo}" repository, already checked out on the security-fix branch (off dev). The setup is done \u2014 do NOT clone, do NOT create or switch worktrees/branches, do NOT ask for confirmation, and do NOT propose alternatives. Run the "${skill}" skill's workflow NOW: fix all open Dependabot security alerts, open or update the PR, then print the machine-readable MISTERPROPRE_RESULT_V1 line.`;
|
|
395
|
+
}
|
|
396
|
+
function buildKiroInvocation(skill, repo, _opts) {
|
|
397
|
+
return {
|
|
398
|
+
command: KIRO_BIN,
|
|
399
|
+
args: ["chat", "--no-interactive", "--trust-all-tools", promptFor(skill, repo)]
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function isKiroAuthed(env = process.env) {
|
|
403
|
+
if (env.KIRO_API_KEY) return true;
|
|
404
|
+
try {
|
|
405
|
+
execFileSync(KIRO_BIN, ["whoami"], { env, stdio: "ignore" });
|
|
406
|
+
return true;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
var KiroAdapter = class {
|
|
412
|
+
id = "kiro";
|
|
413
|
+
buildInvocation(skill, repo, opts) {
|
|
414
|
+
return buildKiroInvocation(skill, repo, opts);
|
|
415
|
+
}
|
|
416
|
+
invoke(skill, repo, opts) {
|
|
417
|
+
try {
|
|
418
|
+
execFileSync(KIRO_BIN, ["settings", "chat.defaultModel", "auto"], { env: opts.env, stdio: "ignore" });
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
const { command, args } = buildKiroInvocation(skill, repo, opts);
|
|
422
|
+
return runAgent({
|
|
423
|
+
command,
|
|
424
|
+
args,
|
|
425
|
+
env: opts.env,
|
|
426
|
+
// SSO session (or KIRO_API_KEY, if set) authorizes the headless run
|
|
427
|
+
cwd: opts.cwd,
|
|
428
|
+
timeoutMs: opts.timeoutMs,
|
|
429
|
+
onStdout: opts.onStdout
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// src/core/providers/q.ts
|
|
435
|
+
var Q_BIN = process.env.MRP_Q_BIN || "q";
|
|
436
|
+
var QAdapter = class {
|
|
437
|
+
constructor(vars) {
|
|
438
|
+
this.vars = vars;
|
|
439
|
+
}
|
|
440
|
+
vars;
|
|
441
|
+
id = "q";
|
|
442
|
+
buildInvocation(skill, repo, _opts) {
|
|
443
|
+
const body = renderBundledSkill(skill, this.vars) ?? `Run the "${skill}" dependency-automation skill.`;
|
|
444
|
+
const prompt = `${body}
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
You ARE the misterpropre orchestrator's prepared invocation for the "${repo}" repository \u2014 the environment is ALREADY set up (your current working directory is the prepared checkout). The setup is done: do NOT clone, do NOT create/switch worktrees or branches, do NOT ask for confirmation, and do NOT propose alternatives. Run the skill's workflow above NOW and print the framed MISTERPROPRE_RESULT_V1 (and MISTERPROPRE_NOTIFY_V1, if the skill defines it) line(s).`;
|
|
448
|
+
return { command: Q_BIN, args: ["chat", "--no-interactive", "--trust-all-tools", prompt] };
|
|
449
|
+
}
|
|
450
|
+
invoke(skill, repo, opts) {
|
|
451
|
+
const { command, args } = this.buildInvocation(skill, repo, opts);
|
|
452
|
+
return runAgent({
|
|
453
|
+
command,
|
|
454
|
+
args,
|
|
455
|
+
env: opts.env,
|
|
456
|
+
// auth via the `q login` session (Builder ID / IAM Identity Center) — no API key
|
|
457
|
+
cwd: opts.cwd,
|
|
458
|
+
timeoutMs: opts.timeoutMs,
|
|
459
|
+
onStdout: opts.onStdout
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/core/registry.ts
|
|
465
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
466
|
+
import { homedir as homedir3 } from "os";
|
|
467
|
+
import { join as join3 } from "path";
|
|
468
|
+
function registryRoot() {
|
|
469
|
+
const base = process.env.XDG_CACHE_HOME?.trim() || join3(homedir3(), ".cache");
|
|
470
|
+
return join3(base, "misterpropre", "runs", "active");
|
|
471
|
+
}
|
|
472
|
+
function runFile(id) {
|
|
473
|
+
return join3(registryRoot(), `${id.replace(/[/\\]/g, "_")}.json`);
|
|
474
|
+
}
|
|
475
|
+
function registerRun(run) {
|
|
476
|
+
mkdirSync3(registryRoot(), { recursive: true });
|
|
477
|
+
writeFileSync2(runFile(run.id), JSON.stringify(run), "utf8");
|
|
478
|
+
}
|
|
479
|
+
function unregisterRun(id) {
|
|
480
|
+
rmSync3(runFile(id), { force: true });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/core/concurrency.ts
|
|
484
|
+
async function mapLimit(items, limit, fn) {
|
|
485
|
+
const results = new Array(items.length);
|
|
486
|
+
let next = 0;
|
|
487
|
+
const workers = Math.max(1, Math.min(limit, items.length));
|
|
488
|
+
const worker = async () => {
|
|
489
|
+
for (; ; ) {
|
|
490
|
+
const i = next++;
|
|
491
|
+
if (i >= items.length) return;
|
|
492
|
+
results[i] = await fn(items[i], i);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
await Promise.all(Array.from({ length: workers }, worker));
|
|
496
|
+
return results;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/core/reconcile.ts
|
|
500
|
+
async function snapshotFix(client, repo) {
|
|
501
|
+
const [alerts, prs] = await Promise.all([
|
|
502
|
+
client.listOpenAlerts(repo),
|
|
503
|
+
client.listAutomationPRs(repo)
|
|
504
|
+
]);
|
|
505
|
+
const alertSeverities = {};
|
|
506
|
+
for (const a of alerts) alertSeverities[a.severity] = (alertSeverities[a.severity] ?? 0) + 1;
|
|
507
|
+
const newest = prs.slice().sort((a, b) => b.number - a.number)[0];
|
|
508
|
+
return {
|
|
509
|
+
openAlertCount: alerts.length,
|
|
510
|
+
alertSeverities,
|
|
511
|
+
automationPr: newest ? {
|
|
512
|
+
number: newest.number,
|
|
513
|
+
headSha: newest.headSha,
|
|
514
|
+
url: newest.url,
|
|
515
|
+
headRefName: newest.headRefName
|
|
516
|
+
} : null
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
async function localFixState(worktreePath2, baseRef) {
|
|
520
|
+
const status = await gitSafe(["-C", worktreePath2, "status", "--porcelain"]);
|
|
521
|
+
const dirty = status.ok && status.stdout.trim().length > 0;
|
|
522
|
+
const rev = await gitSafe(["-C", worktreePath2, "rev-list", "--count", `${baseRef}..HEAD`]);
|
|
523
|
+
const commitsAhead = rev.ok ? Number.parseInt(rev.stdout.trim(), 10) || 0 : 0;
|
|
524
|
+
return { commitsAhead, dirty };
|
|
525
|
+
}
|
|
526
|
+
function reconcileFix(input) {
|
|
527
|
+
const { process: proc, before, after, local, hint } = input;
|
|
528
|
+
const decide = () => {
|
|
529
|
+
if (proc.timedOut) return { kind: "FAIL", detail: "timed out" };
|
|
530
|
+
if (after.automationPr) {
|
|
531
|
+
const url = after.automationPr.url;
|
|
532
|
+
if (!before.automationPr) return { kind: "NEW_PR", prUrl: url };
|
|
533
|
+
if (before.automationPr.headSha !== after.automationPr.headSha)
|
|
534
|
+
return { kind: "UPDATED_PR", prUrl: url };
|
|
535
|
+
return { kind: "PR_UP_TO_DATE", prUrl: url };
|
|
536
|
+
}
|
|
537
|
+
if (local.commitsAhead > 0 || local.dirty)
|
|
538
|
+
return { kind: "LEFT_LOCAL", detail: `${local.commitsAhead} local commit(s)` };
|
|
539
|
+
if (proc.code === null) return { kind: "FAIL", detail: "the agent could not start" };
|
|
540
|
+
if (proc.code !== 0) return { kind: "FAIL", detail: "the agent exited with an error" };
|
|
541
|
+
if (before.openAlertCount === 0) return { kind: "NO_ALERTS" };
|
|
542
|
+
return { kind: "NO_FIXABLE" };
|
|
543
|
+
};
|
|
544
|
+
const d = decide();
|
|
545
|
+
const hintAgreed = hint?.kind ? hint.kind === d.kind : true;
|
|
546
|
+
return { ...d, hintAgreed };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/core/result.ts
|
|
550
|
+
import { z } from "zod";
|
|
551
|
+
var RESULT_PREFIX = "MISTERPROPRE_RESULT_V1";
|
|
552
|
+
var CountsSchema = z.object({
|
|
553
|
+
// fix-phase counts
|
|
554
|
+
fixed: z.number().int().nonnegative(),
|
|
555
|
+
critical: z.number().int().nonnegative(),
|
|
556
|
+
high: z.number().int().nonnegative(),
|
|
557
|
+
medium: z.number().int().nonnegative(),
|
|
558
|
+
low: z.number().int().nonnegative(),
|
|
559
|
+
// purge-phase counts
|
|
560
|
+
merged: z.number().int().nonnegative(),
|
|
561
|
+
handedOff: z.number().int().nonnegative(),
|
|
562
|
+
stillOpen: z.number().int().nonnegative()
|
|
563
|
+
}).partial();
|
|
564
|
+
var ResultSchema = z.object({
|
|
565
|
+
v: z.literal(1),
|
|
566
|
+
phase: z.enum(["fix", "purge"]),
|
|
567
|
+
kind: z.string().min(1),
|
|
568
|
+
prUrl: z.string().url().optional(),
|
|
569
|
+
counts: CountsSchema.optional(),
|
|
570
|
+
message: z.string().optional()
|
|
571
|
+
});
|
|
572
|
+
function parseResults(stdout) {
|
|
573
|
+
let result = null;
|
|
574
|
+
let validCount = 0;
|
|
575
|
+
let malformedCount = 0;
|
|
576
|
+
for (const rawLine of stdout.split("\n")) {
|
|
577
|
+
const line = rawLine.trim();
|
|
578
|
+
if (!line.startsWith(RESULT_PREFIX)) continue;
|
|
579
|
+
const json = line.slice(RESULT_PREFIX.length).trim();
|
|
580
|
+
let parsed;
|
|
581
|
+
try {
|
|
582
|
+
parsed = JSON.parse(json);
|
|
583
|
+
} catch {
|
|
584
|
+
malformedCount++;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const validated = ResultSchema.safeParse(parsed);
|
|
588
|
+
if (validated.success) {
|
|
589
|
+
result = validated.data;
|
|
590
|
+
validCount++;
|
|
591
|
+
} else {
|
|
592
|
+
malformedCount++;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return { result, validCount, malformedCount };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/core/logs.ts
|
|
599
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
600
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
601
|
+
function runLogsDir() {
|
|
602
|
+
return join4(configDir(), "logs");
|
|
603
|
+
}
|
|
604
|
+
var safe3 = (s) => s.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
605
|
+
function writeRunLog(runId, repo, phase, content) {
|
|
606
|
+
const path = join4(runLogsDir(), `${safe3(runId)}-${safe3(repo)}-${safe3(phase)}.log`);
|
|
607
|
+
try {
|
|
608
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
609
|
+
writeFileSync3(path, content, "utf8");
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
return path;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/core/orchestrator.ts
|
|
616
|
+
async function runFix(opts) {
|
|
617
|
+
const start = Date.now();
|
|
618
|
+
const skill = opts.skill ?? "security-dependabot-fix";
|
|
619
|
+
await gcWorktrees({ clonePath: opts.clonePath }).catch(() => {
|
|
620
|
+
});
|
|
621
|
+
await gitSafe(["-C", opts.clonePath, "fetch", "origin", "--prune"]);
|
|
622
|
+
const before = await snapshotFix(opts.github, opts.repo);
|
|
623
|
+
const wtPlanned = worktreePath(opts.repo, "fix", opts.runId);
|
|
624
|
+
let wt;
|
|
625
|
+
try {
|
|
626
|
+
if (before.automationPr?.headRefName) {
|
|
627
|
+
wt = await addWorktree({
|
|
628
|
+
clonePath: opts.clonePath,
|
|
629
|
+
path: wtPlanned,
|
|
630
|
+
ref: before.automationPr.headRefName,
|
|
631
|
+
force: true
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
634
|
+
wt = await addWorktree({
|
|
635
|
+
clonePath: opts.clonePath,
|
|
636
|
+
path: wtPlanned,
|
|
637
|
+
newBranch: `fix/dependabot-security-${opts.branchDate}`,
|
|
638
|
+
startPoint: opts.baseRef,
|
|
639
|
+
force: true
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const run = await opts.provider.invoke(skill, opts.repo, {
|
|
643
|
+
cwd: wt.path,
|
|
644
|
+
model: opts.model,
|
|
645
|
+
maxTurns: opts.maxTurns,
|
|
646
|
+
timeoutMs: opts.timeoutMs,
|
|
647
|
+
env: opts.env,
|
|
648
|
+
onStdout: opts.onStdout
|
|
649
|
+
});
|
|
650
|
+
writeRunLog(opts.runId, opts.repo, "fix", run.stdout + (run.stderr ? `
|
|
651
|
+
--- stderr ---
|
|
652
|
+
${run.stderr}` : ""));
|
|
653
|
+
const hint = parseResults(run.stdout);
|
|
654
|
+
const after = await snapshotFix(opts.github, opts.repo);
|
|
655
|
+
const local = await localFixState(wt.path, opts.baseRef);
|
|
656
|
+
const outcome = reconcileFix({
|
|
657
|
+
process: { timedOut: run.timedOut, code: run.code },
|
|
658
|
+
before,
|
|
659
|
+
after,
|
|
660
|
+
local,
|
|
661
|
+
hint: hint.result ? { kind: hint.result.kind } : null
|
|
662
|
+
});
|
|
663
|
+
return {
|
|
664
|
+
repo: opts.repo,
|
|
665
|
+
outcome: { ...outcome, fixed: hint.result?.counts?.fixed },
|
|
666
|
+
durationMs: Date.now() - start,
|
|
667
|
+
resultHintMalformed: hint.malformedCount,
|
|
668
|
+
worktreePath: wt.path
|
|
669
|
+
};
|
|
670
|
+
} finally {
|
|
671
|
+
if (wt && !wt.external) await removeWorktree(opts.clonePath, wt.path).catch(() => {
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/core/notify-events.ts
|
|
677
|
+
import { z as z2 } from "zod";
|
|
678
|
+
var NOTIFY_PREFIX = "MISTERPROPRE_NOTIFY_V1";
|
|
679
|
+
var NotifyEventSchema = z2.object({
|
|
680
|
+
v: z2.literal(1),
|
|
681
|
+
repo: z2.string().optional(),
|
|
682
|
+
pr: z2.number().optional(),
|
|
683
|
+
url: z2.string().url().optional(),
|
|
684
|
+
outcome: z2.string().min(1),
|
|
685
|
+
package: z2.string().optional(),
|
|
686
|
+
bump: z2.string().optional(),
|
|
687
|
+
message: z2.string().min(1)
|
|
688
|
+
});
|
|
689
|
+
function parseNotifyEvents(stdout) {
|
|
690
|
+
const events = [];
|
|
691
|
+
let malformedCount = 0;
|
|
692
|
+
for (const raw of stdout.split("\n")) {
|
|
693
|
+
const line = raw.trim();
|
|
694
|
+
if (!line.startsWith(NOTIFY_PREFIX)) continue;
|
|
695
|
+
const json = line.slice(NOTIFY_PREFIX.length).trim();
|
|
696
|
+
let parsed;
|
|
697
|
+
try {
|
|
698
|
+
parsed = JSON.parse(json);
|
|
699
|
+
} catch {
|
|
700
|
+
malformedCount++;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const v = NotifyEventSchema.safeParse(parsed);
|
|
704
|
+
if (v.success) events.push(v.data);
|
|
705
|
+
else malformedCount++;
|
|
706
|
+
}
|
|
707
|
+
return { events, malformedCount };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/core/reconcile-purge.ts
|
|
711
|
+
var NEEDS_HUMAN_LABEL = "purge:needs-human";
|
|
712
|
+
async function snapshotPurge(client, repo) {
|
|
713
|
+
const prs = await client.listBotPRs(repo);
|
|
714
|
+
return { open: prs.map((p) => ({ number: p.number, headRefName: p.headRefName, labels: p.labels })) };
|
|
715
|
+
}
|
|
716
|
+
async function resolveMergedNumbers(client, repo, beforeOpenNumbers, afterOpenNumbers) {
|
|
717
|
+
const disappeared = beforeOpenNumbers.filter((n) => !afterOpenNumbers.includes(n));
|
|
718
|
+
const states = await Promise.all(
|
|
719
|
+
disappeared.map(async (n) => ({ n, state: await client.pullState(repo, n) }))
|
|
720
|
+
);
|
|
721
|
+
return states.filter((s) => s.state === "MERGED").map((s) => s.n);
|
|
722
|
+
}
|
|
723
|
+
function reconcilePurge(input) {
|
|
724
|
+
const { process: proc, afterOpen, mergedNumbers, hint } = input;
|
|
725
|
+
const merged = mergedNumbers.length;
|
|
726
|
+
const handedOff = afterOpen.filter((p) => p.labels.includes(NEEDS_HUMAN_LABEL)).length;
|
|
727
|
+
const stillOpen = afterOpen.length - handedOff;
|
|
728
|
+
let kind = "DONE";
|
|
729
|
+
let detail;
|
|
730
|
+
if (proc.timedOut) {
|
|
731
|
+
kind = "FAIL";
|
|
732
|
+
detail = "timeout";
|
|
733
|
+
} else if (proc.code === null) {
|
|
734
|
+
kind = "FAIL";
|
|
735
|
+
detail = "spawn failed";
|
|
736
|
+
} else if (proc.code !== 0) {
|
|
737
|
+
kind = "FAIL";
|
|
738
|
+
detail = `rc=${proc.code}`;
|
|
739
|
+
}
|
|
740
|
+
const hintAgreed = hint ? (hint.merged ?? merged) === merged && (hint.handedOff ?? handedOff) === handedOff : true;
|
|
741
|
+
return { kind, merged, handedOff, stillOpen, detail, hintAgreed };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/core/orchestrator-purge.ts
|
|
745
|
+
async function runPurge(opts) {
|
|
746
|
+
const start = Date.now();
|
|
747
|
+
const skill = opts.skill ?? "mrp-dependabot-purge";
|
|
748
|
+
await gcWorktrees({ clonePath: opts.clonePath }).catch(() => {
|
|
749
|
+
});
|
|
750
|
+
await gitSafe(["-C", opts.clonePath, "fetch", "origin", "--prune"]);
|
|
751
|
+
const before = await snapshotPurge(opts.github, opts.repo);
|
|
752
|
+
const run = await opts.provider.invoke(skill, opts.repo, {
|
|
753
|
+
cwd: opts.clonePath,
|
|
754
|
+
model: opts.model,
|
|
755
|
+
maxTurns: opts.maxTurns,
|
|
756
|
+
timeoutMs: opts.timeoutMs,
|
|
757
|
+
env: opts.env,
|
|
758
|
+
onStdout: opts.onStdout
|
|
759
|
+
});
|
|
760
|
+
writeRunLog(opts.runId, opts.repo, "purge", run.stdout + (run.stderr ? `
|
|
761
|
+
--- stderr ---
|
|
762
|
+
${run.stderr}` : ""));
|
|
763
|
+
const hint = parseResults(run.stdout);
|
|
764
|
+
const notify = parseNotifyEvents(run.stdout);
|
|
765
|
+
const after = await snapshotPurge(opts.github, opts.repo);
|
|
766
|
+
const mergedNumbers = await resolveMergedNumbers(
|
|
767
|
+
opts.github,
|
|
768
|
+
opts.repo,
|
|
769
|
+
before.open.map((p) => p.number),
|
|
770
|
+
after.open.map((p) => p.number)
|
|
771
|
+
);
|
|
772
|
+
const outcome = reconcilePurge({
|
|
773
|
+
process: { timedOut: run.timedOut, code: run.code },
|
|
774
|
+
afterOpen: after.open,
|
|
775
|
+
mergedNumbers,
|
|
776
|
+
hint: hint.result?.counts ? { merged: hint.result.counts.merged, handedOff: hint.result.counts.handedOff } : null
|
|
777
|
+
});
|
|
778
|
+
return {
|
|
779
|
+
repo: opts.repo,
|
|
780
|
+
outcome,
|
|
781
|
+
notifyEvents: notify.events,
|
|
782
|
+
durationMs: Date.now() - start,
|
|
783
|
+
resultHintMalformed: hint.malformedCount
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/core/orchestrator-multi.ts
|
|
788
|
+
async function runRepos(opts) {
|
|
789
|
+
registerRun({
|
|
790
|
+
id: opts.runId,
|
|
791
|
+
pid: process.pid,
|
|
792
|
+
repos: opts.repos,
|
|
793
|
+
phase: opts.phases.join("+"),
|
|
794
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
795
|
+
});
|
|
796
|
+
try {
|
|
797
|
+
return await mapLimit(opts.repos, opts.maxParallel, (repo) => processRepo(repo, opts));
|
|
798
|
+
} finally {
|
|
799
|
+
unregisterRun(opts.runId);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function processRepo(repo, opts) {
|
|
803
|
+
const lock = acquireRepoLock(repo);
|
|
804
|
+
if (!lock.acquired) {
|
|
805
|
+
opts.onLog?.(`skip ${repo}: a run is already operating on it (pid ${lock.heldByPid})`);
|
|
806
|
+
opts.onProgress?.({ repo, state: "skip", detail: `held by pid ${lock.heldByPid}` });
|
|
807
|
+
return { repo, status: "skipped-busy", outcomes: {}, notifyEvents: [], heldByPid: lock.heldByPid };
|
|
808
|
+
}
|
|
809
|
+
const outcomes = {};
|
|
810
|
+
const notifyEvents = [];
|
|
811
|
+
let failedPhase;
|
|
812
|
+
try {
|
|
813
|
+
const clonePath = await opts.resolveClone(repo);
|
|
814
|
+
for (const phase of opts.phases) {
|
|
815
|
+
failedPhase = phase;
|
|
816
|
+
opts.onProgress?.({ repo, phase, state: "start" });
|
|
817
|
+
if (phase === "purge") {
|
|
818
|
+
opts.onLog?.(`purge ${repo} \u2026`);
|
|
819
|
+
const r = await runPurge({
|
|
820
|
+
repo,
|
|
821
|
+
clonePath,
|
|
822
|
+
runId: opts.runId,
|
|
823
|
+
provider: opts.provider,
|
|
824
|
+
github: opts.github,
|
|
825
|
+
skill: PURGE_INSTALL_NAME,
|
|
826
|
+
model: opts.model,
|
|
827
|
+
maxTurns: opts.maxTurns?.purge,
|
|
828
|
+
timeoutMs: opts.timeoutMs?.purge,
|
|
829
|
+
env: opts.env
|
|
830
|
+
});
|
|
831
|
+
outcomes.purge = r.outcome;
|
|
832
|
+
notifyEvents.push(...r.notifyEvents);
|
|
833
|
+
opts.onLog?.(`purge ${repo}: ${r.outcome.kind} (merged ${r.outcome.merged}, handed off ${r.outcome.handedOff})`);
|
|
834
|
+
opts.onProgress?.({ repo, phase, state: "phase-done", detail: r.outcome.kind });
|
|
835
|
+
} else {
|
|
836
|
+
opts.onLog?.(`fix ${repo} \u2026`);
|
|
837
|
+
const r = await runFix({
|
|
838
|
+
repo,
|
|
839
|
+
skill: FIX_INSTALL_NAME,
|
|
840
|
+
clonePath,
|
|
841
|
+
baseRef: opts.baseRef,
|
|
842
|
+
runId: opts.runId,
|
|
843
|
+
branchDate: opts.branchDate,
|
|
844
|
+
provider: opts.provider,
|
|
845
|
+
github: opts.github,
|
|
846
|
+
model: opts.model,
|
|
847
|
+
maxTurns: opts.maxTurns?.fix,
|
|
848
|
+
timeoutMs: opts.timeoutMs?.fix,
|
|
849
|
+
env: opts.env
|
|
850
|
+
});
|
|
851
|
+
outcomes.fix = r.outcome;
|
|
852
|
+
opts.onLog?.(`fix ${repo}: ${r.outcome.kind}`);
|
|
853
|
+
opts.onProgress?.({ repo, phase, state: "phase-done", detail: r.outcome.kind });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return { repo, status: "done", outcomes, notifyEvents };
|
|
857
|
+
} catch (e) {
|
|
858
|
+
const error = e?.message || String(e);
|
|
859
|
+
opts.onProgress?.({ repo, state: "error", detail: error });
|
|
860
|
+
return { repo, status: "error", outcomes, notifyEvents, error, errorPhase: failedPhase };
|
|
861
|
+
} finally {
|
|
862
|
+
lock.handle.release();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/ui/run-progress.tsx
|
|
867
|
+
import { createElement } from "react";
|
|
868
|
+
import { Box, Text, render, useInput } from "ink";
|
|
869
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
870
|
+
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
871
|
+
function glyph(state, spin, L) {
|
|
872
|
+
switch (state) {
|
|
873
|
+
case "purging":
|
|
874
|
+
return { icon: spin, color: theme.primary, label: L.purging };
|
|
875
|
+
case "fixing":
|
|
876
|
+
return { icon: spin, color: theme.accent, label: L.fixing };
|
|
877
|
+
case "done":
|
|
878
|
+
return { icon: "\u2713", color: theme.success, label: L.done };
|
|
879
|
+
case "failed":
|
|
880
|
+
return { icon: "\u2717", color: theme.error, label: L.failed };
|
|
881
|
+
case "skipped":
|
|
882
|
+
return { icon: "\u25D1", color: theme.warn, label: L.skipped };
|
|
883
|
+
default:
|
|
884
|
+
return { icon: "\u25CC", color: theme.dim, label: L.queued };
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function RunProgress({
|
|
888
|
+
rows,
|
|
889
|
+
spinnerTick = 0,
|
|
890
|
+
title = "Running",
|
|
891
|
+
lang = "en"
|
|
892
|
+
}) {
|
|
893
|
+
const labels = appT(lang).grid;
|
|
894
|
+
useInput((input, key) => {
|
|
895
|
+
if (key.ctrl && input === "c") requestShutdown(130);
|
|
896
|
+
});
|
|
897
|
+
const spin = SPINNER[spinnerTick % SPINNER.length];
|
|
898
|
+
const settled = rows.filter((r) => r.state === "done" || r.state === "failed" || r.state === "skipped").length;
|
|
899
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
900
|
+
/* @__PURE__ */ jsx(Banner, { lang }),
|
|
901
|
+
/* @__PURE__ */ jsx(Panel, { title: `\u25B6 ${title} \xB7 ${settled}/${rows.length}`, color: theme.primary, children: rows.map((r) => {
|
|
902
|
+
const g = glyph(r.state, spin, labels);
|
|
903
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
904
|
+
/* @__PURE__ */ jsx(Text, { color: g.color, children: g.icon }),
|
|
905
|
+
" ",
|
|
906
|
+
r.repo.padEnd(38),
|
|
907
|
+
" ",
|
|
908
|
+
/* @__PURE__ */ jsx(Text, { color: theme.dim, children: r.detail ? `${g.label} \xB7 ${r.detail}` : g.label })
|
|
909
|
+
] }, r.repo);
|
|
910
|
+
}) })
|
|
911
|
+
] });
|
|
912
|
+
}
|
|
913
|
+
async function runWithProgress(opts, lang = "en") {
|
|
914
|
+
const live = new Map(opts.repos.map((r) => [r, { repo: r, state: "queued" }]));
|
|
915
|
+
const lastPhase = opts.phases[opts.phases.length - 1];
|
|
916
|
+
const title = opts.phases.map((p) => phaseLabel(p, lang)).join(" \u2192 ");
|
|
917
|
+
let tick = 0;
|
|
918
|
+
const view = () => createElement(RunProgress, { rows: [...live.values()], spinnerTick: tick, title, lang });
|
|
919
|
+
const inst = render(view(), { exitOnCtrlC: false });
|
|
920
|
+
const draw = () => inst.rerender(view());
|
|
921
|
+
const spinner = setInterval(() => {
|
|
922
|
+
tick++;
|
|
923
|
+
draw();
|
|
924
|
+
}, 120);
|
|
925
|
+
const onProgress = (e) => {
|
|
926
|
+
const cur = live.get(e.repo) ?? { repo: e.repo, state: "queued" };
|
|
927
|
+
const next = { ...cur };
|
|
928
|
+
if (e.state === "skip") next.state = "skipped";
|
|
929
|
+
else if (e.state === "error") {
|
|
930
|
+
next.state = "failed";
|
|
931
|
+
next.detail = e.detail;
|
|
932
|
+
} else if (e.state === "start") {
|
|
933
|
+
next.state = e.phase === "purge" ? "purging" : "fixing";
|
|
934
|
+
} else if (e.state === "phase-done") {
|
|
935
|
+
next.detail = e.detail;
|
|
936
|
+
if (e.phase === lastPhase) next.state = "done";
|
|
937
|
+
}
|
|
938
|
+
live.set(e.repo, next);
|
|
939
|
+
draw();
|
|
940
|
+
};
|
|
941
|
+
try {
|
|
942
|
+
return await runRepos({ ...opts, onProgress });
|
|
943
|
+
} finally {
|
|
944
|
+
clearInterval(spinner);
|
|
945
|
+
draw();
|
|
946
|
+
inst.unmount();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/core/secrets.ts
|
|
951
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
952
|
+
import { join as join5 } from "path";
|
|
953
|
+
function secretsPath() {
|
|
954
|
+
return join5(configDir(), "secrets.json");
|
|
955
|
+
}
|
|
956
|
+
var KNOWN_SECRET_KEYS = [
|
|
957
|
+
"TELEGRAM_BOT_TOKEN",
|
|
958
|
+
"TELEGRAM_CHAT_ID",
|
|
959
|
+
"SLACK_WEBHOOK_URL",
|
|
960
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
961
|
+
"KIRO_API_KEY",
|
|
962
|
+
"GH_TOKEN"
|
|
963
|
+
];
|
|
964
|
+
function read() {
|
|
965
|
+
const p = secretsPath();
|
|
966
|
+
if (!existsSync3(p)) return {};
|
|
967
|
+
try {
|
|
968
|
+
const v = JSON.parse(readFileSync3(p, "utf8"));
|
|
969
|
+
return v && typeof v === "object" ? v : {};
|
|
970
|
+
} catch {
|
|
971
|
+
return {};
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function write(map) {
|
|
975
|
+
mkdirSync5(configDir(), { recursive: true });
|
|
976
|
+
const p = secretsPath();
|
|
977
|
+
writeFileSync4(p, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
978
|
+
try {
|
|
979
|
+
chmodSync(p, 384);
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function getSecret(key) {
|
|
984
|
+
return read()[key];
|
|
985
|
+
}
|
|
986
|
+
function setSecret(key, value) {
|
|
987
|
+
const m = read();
|
|
988
|
+
m[key] = value;
|
|
989
|
+
write(m);
|
|
990
|
+
}
|
|
991
|
+
function deleteSecret(key) {
|
|
992
|
+
const m = read();
|
|
993
|
+
delete m[key];
|
|
994
|
+
write(m);
|
|
995
|
+
}
|
|
996
|
+
function listSecretKeys() {
|
|
997
|
+
return Object.keys(read());
|
|
998
|
+
}
|
|
999
|
+
function secretsEnv() {
|
|
1000
|
+
const m = read();
|
|
1001
|
+
const env = {};
|
|
1002
|
+
for (const key of KNOWN_SECRET_KEYS) {
|
|
1003
|
+
if (m[key]) env[key] = m[key];
|
|
1004
|
+
}
|
|
1005
|
+
return env;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/commands/run.ts
|
|
1009
|
+
var pexec2 = promisify2(execFile2);
|
|
1010
|
+
function expandTilde(p) {
|
|
1011
|
+
return p === "~" || p.startsWith("~/") ? join6(homedir4(), p.slice(1)) : p;
|
|
1012
|
+
}
|
|
1013
|
+
function parsePhases(phase) {
|
|
1014
|
+
if (phase === "fix") return ["fix"];
|
|
1015
|
+
if (phase === "purge") return ["purge"];
|
|
1016
|
+
if (phase === "both") return ["purge", "fix"];
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
async function runCommand(repos, options) {
|
|
1020
|
+
const cfg = readConfig();
|
|
1021
|
+
if (!cfg) {
|
|
1022
|
+
console.error("Not configured \u2014 run `misterpropre setup` first.");
|
|
1023
|
+
return 1;
|
|
1024
|
+
}
|
|
1025
|
+
const phases = parsePhases(options.phase);
|
|
1026
|
+
if (!phases) {
|
|
1027
|
+
console.error(`Invalid --phase "${options.phase}" (expected purge | fix | both).`);
|
|
1028
|
+
return 1;
|
|
1029
|
+
}
|
|
1030
|
+
const targetRepos = options.all ? cfg.repos : repos;
|
|
1031
|
+
if (targetRepos.length === 0) {
|
|
1032
|
+
console.error(
|
|
1033
|
+
options.all ? "No repos configured (config.repos is empty)." : "Specify one or more repos, or use --all."
|
|
1034
|
+
);
|
|
1035
|
+
return 1;
|
|
1036
|
+
}
|
|
1037
|
+
const providerId = options.provider ?? cfg.provider.default;
|
|
1038
|
+
const baseDir = expandTilde(cfg.repoBaseDir);
|
|
1039
|
+
const skillVars = { ORG: cfg.org, REPO_BASE: baseDir, PKG_SCOPE: cfg.packageScope, BASE_BRANCH: cfg.baseBranch };
|
|
1040
|
+
const env = { ...process.env, ...secretsEnv() };
|
|
1041
|
+
if (providerId === "kiro" && !isKiroAuthed(env)) {
|
|
1042
|
+
console.error(
|
|
1043
|
+
"Kiro is not authenticated. Run `kiro-cli login` once (AWS Builder ID / IAM Identity Center), or set KIRO_API_KEY."
|
|
1044
|
+
);
|
|
1045
|
+
return 1;
|
|
1046
|
+
}
|
|
1047
|
+
const provider = providerId === "kiro" ? new KiroAdapter() : providerId === "q" ? new QAdapter(skillVars) : new ClaudeAdapter();
|
|
1048
|
+
const resolveClone = async (repo) => {
|
|
1049
|
+
const clonePath = join6(baseDir, repo);
|
|
1050
|
+
if (!existsSync4(join6(clonePath, ".git"))) {
|
|
1051
|
+
console.log(`Cloning ${cfg.org}/${repo} \u2192 ${clonePath} \u2026`);
|
|
1052
|
+
await pexec2("gh", ["repo", "clone", `${cfg.org}/${repo}`, clonePath]);
|
|
1053
|
+
}
|
|
1054
|
+
return clonePath;
|
|
1055
|
+
};
|
|
1056
|
+
const trigger = options.scheduled ? "scheduled" : "manual";
|
|
1057
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1058
|
+
const runId = `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
1059
|
+
if (!process.stdout.isTTY) {
|
|
1060
|
+
console.log(
|
|
1061
|
+
`\u25B6 ${phases.map((p) => phaseLabel(p, cfg.lang)).join(" \u2192 ")} \xB7 ${cfg.org} \xB7 ${targetRepos.length} repo(s) \xB7 provider=${providerId}
|
|
1062
|
+
`
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
const baseOpts = {
|
|
1066
|
+
repos: targetRepos,
|
|
1067
|
+
phases,
|
|
1068
|
+
provider,
|
|
1069
|
+
github: new GhCli(cfg.org),
|
|
1070
|
+
resolveClone,
|
|
1071
|
+
baseRef: `origin/${cfg.baseBranch}`,
|
|
1072
|
+
branchDate: startedAt.slice(0, 10),
|
|
1073
|
+
runId,
|
|
1074
|
+
trigger,
|
|
1075
|
+
maxParallel: cfg.execution.maxParallel,
|
|
1076
|
+
model: cfg.execution.model || void 0,
|
|
1077
|
+
maxTurns: cfg.execution.maxTurns,
|
|
1078
|
+
timeoutMs: {
|
|
1079
|
+
fix: cfg.execution.timeouts.fix * 1e3,
|
|
1080
|
+
purge: cfg.execution.timeouts.purge * 1e3
|
|
1081
|
+
},
|
|
1082
|
+
env
|
|
1083
|
+
};
|
|
1084
|
+
const results = process.stdout.isTTY ? await runWithProgress(baseOpts, cfg.lang) : await runRepos({ ...baseOpts, onLog: (msg) => console.log(` ${msg}`) });
|
|
1085
|
+
const repoOutcomes = [];
|
|
1086
|
+
for (const r of results) {
|
|
1087
|
+
if (r.status === "skipped-busy") {
|
|
1088
|
+
repoOutcomes.push({ repo: r.repo, phase: phases[0], kind: "SKIPPED_BUSY", detail: `held by pid ${r.heldByPid}` });
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (r.status === "error") {
|
|
1092
|
+
repoOutcomes.push({ repo: r.repo, phase: r.errorPhase ?? phases[0], kind: "ERROR", detail: r.error });
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
if (r.outcomes.purge) {
|
|
1096
|
+
const p = r.outcomes.purge;
|
|
1097
|
+
repoOutcomes.push({
|
|
1098
|
+
repo: r.repo,
|
|
1099
|
+
phase: "purge",
|
|
1100
|
+
kind: p.kind,
|
|
1101
|
+
detail: `merged ${p.merged}, handed off ${p.handedOff}, open ${p.stillOpen}`,
|
|
1102
|
+
merged: p.merged,
|
|
1103
|
+
handedOff: p.handedOff,
|
|
1104
|
+
stillOpen: p.stillOpen
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
if (r.outcomes.fix) {
|
|
1108
|
+
const f = r.outcomes.fix;
|
|
1109
|
+
repoOutcomes.push({ repo: r.repo, phase: "fix", kind: f.kind, prUrl: f.prUrl, detail: f.detail, hintAgreed: f.hintAgreed, fixed: f.fixed });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
const record = {
|
|
1113
|
+
id: runId,
|
|
1114
|
+
startedAt,
|
|
1115
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1116
|
+
trigger,
|
|
1117
|
+
provider: providerId,
|
|
1118
|
+
phases,
|
|
1119
|
+
repos: repoOutcomes
|
|
1120
|
+
};
|
|
1121
|
+
appendRun(record);
|
|
1122
|
+
const t = appT(cfg.lang);
|
|
1123
|
+
console.log("\n\u2500\u2500 Summary \u2500\u2500");
|
|
1124
|
+
for (const o of repoOutcomes) {
|
|
1125
|
+
const line = summarizeOutcome(o, t.status);
|
|
1126
|
+
const pr = o.prUrl ? ` \u2192 ${o.prUrl}` : "";
|
|
1127
|
+
console.log(` ${line.ok ? "\u2713" : "\u2717"} ${o.repo} \xB7 ${phaseLabel(o.phase, cfg.lang)}: ${line.status}${pr}`);
|
|
1128
|
+
}
|
|
1129
|
+
const notifyEvents = results.flatMap((r) => r.notifyEvents);
|
|
1130
|
+
if (notifyEvents.length > 0) {
|
|
1131
|
+
console.log(`
|
|
1132
|
+
${t.handoffsHeader(notifyEvents.length)}`);
|
|
1133
|
+
for (const e of notifyEvents) console.log(` \u2022 #${e.pr ?? "?"} ${e.outcome} \u2014 ${e.message}`);
|
|
1134
|
+
}
|
|
1135
|
+
const failed = results.some(
|
|
1136
|
+
(r) => r.status === "error" || r.outcomes.fix?.kind === "FAIL" || r.outcomes.purge?.kind === "FAIL"
|
|
1137
|
+
);
|
|
1138
|
+
if (failed) {
|
|
1139
|
+
console.log(`
|
|
1140
|
+
\u2139 Agent logs: ${runLogsDir()} (the .log files there show exactly what the agent did)`);
|
|
1141
|
+
}
|
|
1142
|
+
return failed ? 1 : 0;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/commands/schedule.ts
|
|
1146
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
1147
|
+
import { homedir as homedir6 } from "os";
|
|
1148
|
+
import { join as join8 } from "path";
|
|
1149
|
+
|
|
1150
|
+
// src/core/scheduler.ts
|
|
1151
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1152
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1153
|
+
import { homedir as homedir5, platform } from "os";
|
|
1154
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
1155
|
+
var SCHEDULE_LABEL = "com.misterpropre.daily";
|
|
1156
|
+
function escapeXml(s) {
|
|
1157
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1158
|
+
}
|
|
1159
|
+
function buildLaunchdPlist(s) {
|
|
1160
|
+
const argv = [s.nodePath, s.cliPath, ...s.args].map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
1161
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1162
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1163
|
+
<plist version="1.0">
|
|
1164
|
+
<dict>
|
|
1165
|
+
<key>Label</key>
|
|
1166
|
+
<string>${SCHEDULE_LABEL}</string>
|
|
1167
|
+
<key>ProgramArguments</key>
|
|
1168
|
+
<array>
|
|
1169
|
+
${argv}
|
|
1170
|
+
</array>
|
|
1171
|
+
<key>EnvironmentVariables</key>
|
|
1172
|
+
<dict>
|
|
1173
|
+
<key>PATH</key>
|
|
1174
|
+
<string>${escapeXml(s.path)}</string>
|
|
1175
|
+
</dict>
|
|
1176
|
+
<key>StartCalendarInterval</key>
|
|
1177
|
+
<dict>
|
|
1178
|
+
<key>Hour</key>
|
|
1179
|
+
<integer>${s.hour}</integer>
|
|
1180
|
+
<key>Minute</key>
|
|
1181
|
+
<integer>${s.minute}</integer>
|
|
1182
|
+
</dict>
|
|
1183
|
+
<key>WorkingDirectory</key>
|
|
1184
|
+
<string>${escapeXml(s.workingDir)}</string>
|
|
1185
|
+
<key>StandardOutPath</key>
|
|
1186
|
+
<string>${escapeXml(s.logPath)}</string>
|
|
1187
|
+
<key>StandardErrorPath</key>
|
|
1188
|
+
<string>${escapeXml(s.logPath)}</string>
|
|
1189
|
+
<key>RunAtLoad</key>
|
|
1190
|
+
<false/>
|
|
1191
|
+
<key>ProcessType</key>
|
|
1192
|
+
<string>Background</string>
|
|
1193
|
+
</dict>
|
|
1194
|
+
</plist>
|
|
1195
|
+
`;
|
|
1196
|
+
}
|
|
1197
|
+
function isMac() {
|
|
1198
|
+
return platform() === "darwin";
|
|
1199
|
+
}
|
|
1200
|
+
function launchAgentsDir() {
|
|
1201
|
+
return join7(homedir5(), "Library", "LaunchAgents");
|
|
1202
|
+
}
|
|
1203
|
+
function plistPath() {
|
|
1204
|
+
return join7(launchAgentsDir(), `${SCHEDULE_LABEL}.plist`);
|
|
1205
|
+
}
|
|
1206
|
+
function schedulePath(nodePath) {
|
|
1207
|
+
const dirs = [
|
|
1208
|
+
dirname3(nodePath),
|
|
1209
|
+
join7(homedir5(), ".local", "bin"),
|
|
1210
|
+
"/opt/homebrew/bin",
|
|
1211
|
+
"/usr/local/bin",
|
|
1212
|
+
"/usr/bin",
|
|
1213
|
+
"/bin"
|
|
1214
|
+
];
|
|
1215
|
+
return [...new Set(dirs)].join(":");
|
|
1216
|
+
}
|
|
1217
|
+
function installSchedule(spec) {
|
|
1218
|
+
if (!isMac()) {
|
|
1219
|
+
return { ok: false, message: `native scheduling is only wired for macOS so far (got ${platform()})` };
|
|
1220
|
+
}
|
|
1221
|
+
mkdirSync6(launchAgentsDir(), { recursive: true });
|
|
1222
|
+
const p = plistPath();
|
|
1223
|
+
try {
|
|
1224
|
+
execFileSync2("launchctl", ["unload", p], { stdio: "ignore" });
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
writeFileSync5(p, buildLaunchdPlist(spec), "utf8");
|
|
1228
|
+
try {
|
|
1229
|
+
execFileSync2("launchctl", ["load", p], { stdio: "ignore" });
|
|
1230
|
+
return { ok: true, message: `scheduled daily at ${pad(spec.hour)}:${pad(spec.minute)} (launchd)`, path: p };
|
|
1231
|
+
} catch (e) {
|
|
1232
|
+
return { ok: false, message: `wrote ${p} but \`launchctl load\` failed: ${e.message}`, path: p };
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function uninstallSchedule() {
|
|
1236
|
+
if (!isMac()) return { ok: false, message: `native scheduling is only wired for macOS so far (got ${platform()})` };
|
|
1237
|
+
const p = plistPath();
|
|
1238
|
+
if (!existsSync5(p)) return { ok: true, message: "no schedule was installed" };
|
|
1239
|
+
try {
|
|
1240
|
+
execFileSync2("launchctl", ["unload", p], { stdio: "ignore" });
|
|
1241
|
+
} catch {
|
|
1242
|
+
}
|
|
1243
|
+
rmSync4(p, { force: true });
|
|
1244
|
+
return { ok: true, message: "schedule removed" };
|
|
1245
|
+
}
|
|
1246
|
+
function scheduleInstalled() {
|
|
1247
|
+
return existsSync5(plistPath());
|
|
1248
|
+
}
|
|
1249
|
+
function pad(n) {
|
|
1250
|
+
return String(n).padStart(2, "0");
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/commands/schedule.ts
|
|
1254
|
+
function resolveCliPath() {
|
|
1255
|
+
const entry = process.argv[1] ?? "";
|
|
1256
|
+
try {
|
|
1257
|
+
return realpathSync2(entry);
|
|
1258
|
+
} catch {
|
|
1259
|
+
return entry;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function applySchedule(cfg, input) {
|
|
1263
|
+
const time = input.time;
|
|
1264
|
+
const m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(time);
|
|
1265
|
+
if (!m) return { ok: false, message: `Invalid time "${time}" (expected HH:MM, 24h).` };
|
|
1266
|
+
const phase = input.phase ?? "both";
|
|
1267
|
+
if (!["fix", "purge", "both"].includes(phase)) {
|
|
1268
|
+
return { ok: false, message: `Invalid phase "${phase}" (expected fix | purge | both).` };
|
|
1269
|
+
}
|
|
1270
|
+
if (cfg.repos.length === 0) {
|
|
1271
|
+
return { ok: false, message: "No repos configured (config.repos is empty) \u2014 nothing to schedule." };
|
|
1272
|
+
}
|
|
1273
|
+
const chosen = (input.repos ?? []).filter((r) => cfg.repos.includes(r));
|
|
1274
|
+
const runAll = chosen.length === 0 || chosen.length === cfg.repos.length;
|
|
1275
|
+
const runArgs = runAll ? ["run", "--all"] : ["run", ...chosen];
|
|
1276
|
+
const nodePath = process.execPath;
|
|
1277
|
+
const spec = {
|
|
1278
|
+
hour: Number(m[1]),
|
|
1279
|
+
minute: Number(m[2]),
|
|
1280
|
+
nodePath,
|
|
1281
|
+
cliPath: resolveCliPath(),
|
|
1282
|
+
args: [...runArgs, "--provider", cfg.provider.scheduled, "--phase", phase, "--scheduled"],
|
|
1283
|
+
path: schedulePath(nodePath),
|
|
1284
|
+
logPath: join8(configDir(), "schedule.log"),
|
|
1285
|
+
workingDir: homedir6()
|
|
1286
|
+
};
|
|
1287
|
+
const res = installSchedule(spec);
|
|
1288
|
+
if (!res.ok) return { ok: false, message: res.message };
|
|
1289
|
+
const repos = runAll ? [] : chosen;
|
|
1290
|
+
writeConfig({ ...cfg, schedule: { enabled: true, time, phase, repos } });
|
|
1291
|
+
const count = runAll ? cfg.repos.length : chosen.length;
|
|
1292
|
+
return { ok: true, message: res.message, detail: `${cfg.provider.scheduled} \xB7 phase=${phase} \xB7 ${count} repo(s)` };
|
|
1293
|
+
}
|
|
1294
|
+
function applyUnschedule() {
|
|
1295
|
+
const res = uninstallSchedule();
|
|
1296
|
+
const cfg = readConfig();
|
|
1297
|
+
if (cfg) writeConfig({ ...cfg, schedule: { ...cfg.schedule, enabled: false } });
|
|
1298
|
+
return { ok: res.ok, message: res.message };
|
|
1299
|
+
}
|
|
1300
|
+
function runScheduleSet(opts) {
|
|
1301
|
+
const cfg = readConfig();
|
|
1302
|
+
if (!cfg) {
|
|
1303
|
+
console.error("Not configured \u2014 run `misterpropre setup` first.");
|
|
1304
|
+
return 1;
|
|
1305
|
+
}
|
|
1306
|
+
const res = applySchedule(cfg, { time: opts.time ?? cfg.schedule.time, phase: opts.phase });
|
|
1307
|
+
if (!res.ok) {
|
|
1308
|
+
console.error(res.message);
|
|
1309
|
+
return 1;
|
|
1310
|
+
}
|
|
1311
|
+
console.log(`\u2713 ${res.message}`);
|
|
1312
|
+
if (res.detail) console.log(` ${res.detail}`);
|
|
1313
|
+
console.log(` plist: ${plistPath()}`);
|
|
1314
|
+
console.log(` logs: ${join8(configDir(), "schedule.log")}`);
|
|
1315
|
+
return 0;
|
|
1316
|
+
}
|
|
1317
|
+
function runScheduleStatus() {
|
|
1318
|
+
const cfg = readConfig();
|
|
1319
|
+
const installed = scheduleInstalled();
|
|
1320
|
+
console.log(`Schedule: ${cfg?.schedule.enabled ? `enabled at ${cfg.schedule.time}` : "disabled"}`);
|
|
1321
|
+
console.log(`launchd job: ${installed ? `installed (${plistPath()})` : "not installed"}`);
|
|
1322
|
+
if (cfg) {
|
|
1323
|
+
const count = cfg.schedule.repos.length || cfg.repos.length;
|
|
1324
|
+
console.log(`Scheduled run: ${cfg.provider.scheduled} \xB7 phase=${cfg.schedule.phase} \xB7 ${count} repo(s)`);
|
|
1325
|
+
}
|
|
1326
|
+
return 0;
|
|
1327
|
+
}
|
|
1328
|
+
function runScheduleClear() {
|
|
1329
|
+
const res = applyUnschedule();
|
|
1330
|
+
console.log(res.ok ? `\u2713 ${res.message}` : res.message);
|
|
1331
|
+
return res.ok ? 0 : 1;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
export {
|
|
1335
|
+
KNOWN_SECRET_KEYS,
|
|
1336
|
+
getSecret,
|
|
1337
|
+
setSecret,
|
|
1338
|
+
deleteSecret,
|
|
1339
|
+
listSecretKeys,
|
|
1340
|
+
installSignalHandlers,
|
|
1341
|
+
runCommand,
|
|
1342
|
+
applySchedule,
|
|
1343
|
+
applyUnschedule,
|
|
1344
|
+
runScheduleSet,
|
|
1345
|
+
runScheduleStatus,
|
|
1346
|
+
runScheduleClear
|
|
1347
|
+
};
|
|
1348
|
+
//# sourceMappingURL=chunk-JCB4UDCP.js.map
|