@4pm/cli 1.14.0 → 1.17.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/dist/index.js
CHANGED
|
@@ -37098,9 +37098,24 @@ var taskApproveRequestSchema = external_exports.object({
|
|
|
37098
37098
|
taskId: external_exports.string().min(1).max(64),
|
|
37099
37099
|
approved: external_exports.boolean()
|
|
37100
37100
|
});
|
|
37101
|
+
var taskApproveEntrySchema = external_exports.object({
|
|
37102
|
+
taskId: external_exports.string().min(1).max(64),
|
|
37103
|
+
tags: external_exports.array(external_exports.string().min(1).max(64)).max(16).default([])
|
|
37104
|
+
});
|
|
37105
|
+
var taskApproveBatchRequestSchema = external_exports.object({
|
|
37106
|
+
approvals: external_exports.array(taskApproveEntrySchema).max(500).default([]),
|
|
37107
|
+
unapprovals: external_exports.array(external_exports.string().min(1).max(64)).max(500).default([])
|
|
37108
|
+
});
|
|
37101
37109
|
var autonomousWriteRequestSchema = external_exports.discriminatedUnion("kind", [
|
|
37102
37110
|
external_exports.object({ kind: external_exports.literal("settings"), settings: external_exports.string().max(64 * 1024) }),
|
|
37103
37111
|
external_exports.object({ kind: external_exports.literal("approvals"), taskId: external_exports.string().min(1).max(64), approved: external_exports.boolean() }),
|
|
37112
|
+
// Batched approvals (ADR-0311): commit many approve/unapprove ids in ONE approvals-file write, so a
|
|
37113
|
+
// Save's coupled batch lands atomically. Constructed by the server after its tag actions succeed.
|
|
37114
|
+
external_exports.object({
|
|
37115
|
+
kind: external_exports.literal("approvalsBatch"),
|
|
37116
|
+
approve: external_exports.array(external_exports.string().min(1).max(64)).max(500),
|
|
37117
|
+
unapprove: external_exports.array(external_exports.string().min(1).max(64)).max(500)
|
|
37118
|
+
}),
|
|
37104
37119
|
external_exports.object({ kind: external_exports.literal("userTodo"), content: external_exports.string().min(1).max(16 * 1024) }),
|
|
37105
37120
|
external_exports.object({ kind: external_exports.literal("cron"), action: external_exports.enum(["install", "uninstall"]) })
|
|
37106
37121
|
]);
|
|
@@ -37288,11 +37303,14 @@ var repoSpecSchema = external_exports.object({
|
|
|
37288
37303
|
role: external_exports.string().max(60).optional().default(""),
|
|
37289
37304
|
/** Free-form description of what this repo holds (optional). */
|
|
37290
37305
|
desc: external_exports.string().max(500).optional().default(""),
|
|
37291
|
-
/** Exactly one repo of the project is the primary. */
|
|
37306
|
+
/** Exactly one repo of the project is the primary (the root); the rest are submodules. */
|
|
37292
37307
|
primary: external_exports.boolean().optional().default(false),
|
|
37293
37308
|
/** Clone url — `https` or `ssh` (required; ADR-0172). */
|
|
37294
37309
|
url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
|
|
37295
|
-
/**
|
|
37310
|
+
/**
|
|
37311
|
+
* Folder under the root: **empty for the primary** (the root itself), **non-empty and unique for a
|
|
37312
|
+
* submodule** — the path `git submodule add` attaches it at (ADR-0316).
|
|
37313
|
+
*/
|
|
37296
37314
|
subdir: external_exports.string().max(120).optional().default(""),
|
|
37297
37315
|
/**
|
|
37298
37316
|
* Primary branch to clone / check out (ADR-0292). Empty ⇒ the repo's default branch. The
|
|
@@ -37301,9 +37319,38 @@ var repoSpecSchema = external_exports.object({
|
|
|
37301
37319
|
*/
|
|
37302
37320
|
branch: external_exports.string().max(200).optional().default("")
|
|
37303
37321
|
});
|
|
37304
|
-
function
|
|
37305
|
-
const
|
|
37306
|
-
|
|
37322
|
+
function validateProjectRepos(repos, ctx, path = ["repos"]) {
|
|
37323
|
+
const list = repos ?? [];
|
|
37324
|
+
const issue = (message) => ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path, message });
|
|
37325
|
+
if (list.length < 1) {
|
|
37326
|
+
issue("a project must declare at least one repo");
|
|
37327
|
+
return;
|
|
37328
|
+
}
|
|
37329
|
+
const primaries = list.filter((r) => r.primary);
|
|
37330
|
+
if (primaries.length !== 1) {
|
|
37331
|
+
issue("exactly one repo must be primary");
|
|
37332
|
+
}
|
|
37333
|
+
for (const r of list) {
|
|
37334
|
+
if (r.primary) {
|
|
37335
|
+
if ((r.subdir ?? "").trim() !== "") issue("the primary repo must be at the root (no subfolder)");
|
|
37336
|
+
} else if ((r.subdir ?? "").trim() === "") {
|
|
37337
|
+
issue("each submodule needs a folder path");
|
|
37338
|
+
}
|
|
37339
|
+
}
|
|
37340
|
+
const seenDir = /* @__PURE__ */ new Set();
|
|
37341
|
+
for (const r of list) {
|
|
37342
|
+
const dir2 = (r.subdir ?? "").trim().toLowerCase();
|
|
37343
|
+
if (!dir2) continue;
|
|
37344
|
+
if (seenDir.has(dir2)) issue("submodule folder paths must be unique");
|
|
37345
|
+
seenDir.add(dir2);
|
|
37346
|
+
}
|
|
37347
|
+
const seenUrl = /* @__PURE__ */ new Set();
|
|
37348
|
+
for (const r of list) {
|
|
37349
|
+
const url = (r.url ?? "").trim().toLowerCase();
|
|
37350
|
+
if (!url) continue;
|
|
37351
|
+
if (seenUrl.has(url)) issue("repo URLs must be unique");
|
|
37352
|
+
seenUrl.add(url);
|
|
37353
|
+
}
|
|
37307
37354
|
}
|
|
37308
37355
|
var specFieldEnvelopeSchema = external_exports.object({
|
|
37309
37356
|
id: external_exports.string().min(1),
|
|
@@ -37335,16 +37382,7 @@ var projectSpecSchema = external_exports.object({
|
|
|
37335
37382
|
});
|
|
37336
37383
|
}
|
|
37337
37384
|
}
|
|
37338
|
-
|
|
37339
|
-
const primaries = v2.repos.filter((r) => r.primary).length;
|
|
37340
|
-
if (primaries !== 1) {
|
|
37341
|
-
ctx.addIssue({
|
|
37342
|
-
code: external_exports.ZodIssueCode.custom,
|
|
37343
|
-
path: ["repos"],
|
|
37344
|
-
message: "exactly one primary repo is required"
|
|
37345
|
-
});
|
|
37346
|
-
}
|
|
37347
|
-
}
|
|
37385
|
+
validateProjectRepos(v2.repos, ctx);
|
|
37348
37386
|
});
|
|
37349
37387
|
var createFromSpecRequestSchema = external_exports.object({
|
|
37350
37388
|
projectId: external_exports.string().uuid(),
|
|
@@ -37354,14 +37392,7 @@ var addRepoRequestSchema = external_exports.object({
|
|
|
37354
37392
|
projectId: external_exports.string().uuid(),
|
|
37355
37393
|
repos: external_exports.array(repoSpecSchema).min(1)
|
|
37356
37394
|
}).superRefine((v2, ctx) => {
|
|
37357
|
-
|
|
37358
|
-
if (primaries !== 1) {
|
|
37359
|
-
ctx.addIssue({
|
|
37360
|
-
code: external_exports.ZodIssueCode.custom,
|
|
37361
|
-
path: ["repos"],
|
|
37362
|
-
message: "Exactly one repo must be primary"
|
|
37363
|
-
});
|
|
37364
|
-
}
|
|
37395
|
+
validateProjectRepos(v2.repos, ctx);
|
|
37365
37396
|
});
|
|
37366
37397
|
var addProjectRepoRequestSchema = external_exports.object({
|
|
37367
37398
|
/** Clone url — `https` or `ssh` (required; ADR-0172). */
|
|
@@ -37439,7 +37470,7 @@ var activityHeartbeatSchema = external_exports.object({
|
|
|
37439
37470
|
});
|
|
37440
37471
|
|
|
37441
37472
|
// src/core/scaffold.ts
|
|
37442
|
-
import { cp, mkdir,
|
|
37473
|
+
import { cp, mkdir, rm, writeFile } from "fs/promises";
|
|
37443
37474
|
import { existsSync as existsSync4 } from "fs";
|
|
37444
37475
|
import { execFile as execFile2 } from "child_process";
|
|
37445
37476
|
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
@@ -37524,89 +37555,111 @@ function reposOf(spec) {
|
|
|
37524
37555
|
const repos = spec?.repos;
|
|
37525
37556
|
return Array.isArray(repos) ? repos : [];
|
|
37526
37557
|
}
|
|
37527
|
-
function
|
|
37528
|
-
|
|
37529
|
-
|
|
37530
|
-
|
|
37531
|
-
|
|
37532
|
-
|
|
37533
|
-
|
|
37534
|
-
|
|
37535
|
-
|
|
37536
|
-
|
|
37537
|
-
|
|
37538
|
-
|
|
37539
|
-
|
|
37540
|
-
|
|
37558
|
+
function singleRepo(repos) {
|
|
37559
|
+
return repos.find((r) => r.primary) ?? repos[0] ?? null;
|
|
37560
|
+
}
|
|
37561
|
+
function submodulesOf(repos) {
|
|
37562
|
+
const primary = singleRepo(repos);
|
|
37563
|
+
return repos.filter((r) => r !== primary && !!r.url && !!(r.subdir ?? "").trim());
|
|
37564
|
+
}
|
|
37565
|
+
async function attachSubmodules(root, submodules, emit) {
|
|
37566
|
+
if (submodules.length === 0) return;
|
|
37567
|
+
if (!existsSync4(join6(root, ".git"))) return;
|
|
37568
|
+
let added = 0;
|
|
37569
|
+
for (const sub of submodules) {
|
|
37570
|
+
const dir2 = (sub.subdir ?? "").trim();
|
|
37571
|
+
if (!dir2 || !sub.url) continue;
|
|
37572
|
+
if (existsSync4(join6(root, dir2, ".git")) || existsSync4(join6(root, ".git", "modules", dir2))) {
|
|
37573
|
+
emit("submodule", `Submodule ${dir2} already present \u2014 updating\u2026`);
|
|
37574
|
+
try {
|
|
37575
|
+
await run2("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37576
|
+
} catch {
|
|
37577
|
+
}
|
|
37578
|
+
continue;
|
|
37579
|
+
}
|
|
37580
|
+
emit("submodule", `Adding submodule ${sub.url} \u2192 ${dir2}\u2026`);
|
|
37581
|
+
const b2 = (sub.branch ?? "").trim();
|
|
37582
|
+
try {
|
|
37583
|
+
await run2("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
|
|
37584
|
+
added++;
|
|
37585
|
+
} catch (err) {
|
|
37586
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37587
|
+
if (/already exists|already registered|in the index/i.test(msg)) {
|
|
37588
|
+
try {
|
|
37589
|
+
await run2("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37590
|
+
added++;
|
|
37591
|
+
} catch {
|
|
37592
|
+
}
|
|
37593
|
+
} else {
|
|
37594
|
+
throw new Error(`git submodule add ${dir2} failed: ${msg}`);
|
|
37595
|
+
}
|
|
37596
|
+
}
|
|
37597
|
+
}
|
|
37598
|
+
if (added > 0) await commitAndPushSubmodules(root, submodules, emit);
|
|
37599
|
+
}
|
|
37600
|
+
async function commitAndPushSubmodules(root, submodules, emit) {
|
|
37601
|
+
const dirs = submodules.map((s3) => (s3.subdir ?? "").trim()).filter(Boolean);
|
|
37602
|
+
emit("submodule", "Committing .gitmodules\u2026");
|
|
37603
|
+
await run2("git", ["add", ".gitmodules", ...dirs], { cwd: root, timeout: 6e4 });
|
|
37604
|
+
const staged = (await run2("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
|
|
37605
|
+
if (!staged) return;
|
|
37606
|
+
const commitArgs = ["commit", "-m", "chore: add git submodules (4PM)"];
|
|
37607
|
+
try {
|
|
37608
|
+
await run2("git", commitArgs, { cwd: root, timeout: 6e4 });
|
|
37609
|
+
} catch {
|
|
37610
|
+
await run2("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
|
|
37611
|
+
}
|
|
37612
|
+
emit("submodule", "Pushing .gitmodules to the primary remote\u2026");
|
|
37613
|
+
try {
|
|
37614
|
+
await run2("git", ["push"], { cwd: root, timeout: 12e4 });
|
|
37615
|
+
} catch (err) {
|
|
37616
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37617
|
+
emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
|
|
37618
|
+
}
|
|
37541
37619
|
}
|
|
37542
37620
|
function cloneArgs(url, dest, branch) {
|
|
37543
37621
|
const b2 = (branch ?? "").trim();
|
|
37544
37622
|
return b2 ? ["clone", "-b", b2, url, dest] : ["clone", url, dest];
|
|
37545
37623
|
}
|
|
37546
|
-
async function updateRepo(dir2,
|
|
37624
|
+
async function updateRepo(dir2, branch, emit) {
|
|
37547
37625
|
const b2 = (branch ?? "").trim();
|
|
37548
|
-
emit("git", `Updating
|
|
37626
|
+
emit("git", `Updating repository (fetch + ${b2 ? `checkout ${b2} + ` : ""}fast-forward pull)\u2026`);
|
|
37549
37627
|
await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
|
|
37550
37628
|
if (b2) await run2("git", ["checkout", b2], { cwd: dir2, timeout: 6e4 });
|
|
37551
37629
|
await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
|
|
37552
37630
|
}
|
|
37553
|
-
function
|
|
37554
|
-
if (repo.primary && legacyRoot) return target;
|
|
37555
|
-
return join6(target, folder);
|
|
37556
|
-
}
|
|
37557
|
-
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
37631
|
+
async function provisionRepo(target, repo, emit, opts = {}) {
|
|
37558
37632
|
const mode = opts.mode ?? "clone";
|
|
37559
|
-
|
|
37560
|
-
|
|
37561
|
-
|
|
37562
|
-
|
|
37563
|
-
|
|
37564
|
-
for (const [i, repo] of repos.entries()) {
|
|
37565
|
-
const folder = folders[i];
|
|
37566
|
-
const atRoot = !!repo.primary && legacyRoot;
|
|
37567
|
-
const dir2 = repoDir(target, repo, folder, legacyRoot);
|
|
37568
|
-
const label = repo.primary ? "primary repo" : `repo ${folder}`;
|
|
37569
|
-
if (!repo.url) {
|
|
37570
|
-
if (!existsSync4(join6(dir2, ".git"))) {
|
|
37571
|
-
emit("git", `Initializing ${label}\u2026`);
|
|
37572
|
-
await mkdir(dir2, { recursive: true });
|
|
37573
|
-
await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
37574
|
-
}
|
|
37575
|
-
continue;
|
|
37633
|
+
if (!repo.url) {
|
|
37634
|
+
if (!existsSync4(join6(target, ".git"))) {
|
|
37635
|
+
emit("git", "Initializing repository\u2026");
|
|
37636
|
+
await mkdir(target, { recursive: true });
|
|
37637
|
+
await run2("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
37576
37638
|
}
|
|
37577
|
-
|
|
37578
|
-
|
|
37579
|
-
|
|
37580
|
-
|
|
37581
|
-
|
|
37582
|
-
|
|
37583
|
-
|
|
37584
|
-
|
|
37585
|
-
|
|
37586
|
-
|
|
37587
|
-
|
|
37588
|
-
}
|
|
37589
|
-
for (const entry of await readdir(tmp)) {
|
|
37590
|
-
if (foldersToKeep.has(entry)) continue;
|
|
37591
|
-
await rename(join6(tmp, entry), join6(target, entry));
|
|
37592
|
-
}
|
|
37593
|
-
await rm(tmp, { recursive: true, force: true });
|
|
37594
|
-
} else if (mode === "force") {
|
|
37595
|
-
emit("git", `Re-cloning ${repo.url} (force)\u2026`);
|
|
37596
|
-
await rm(dir2, { recursive: true, force: true });
|
|
37597
|
-
await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
|
|
37598
|
-
} else {
|
|
37599
|
-
emit("git", `${label} already present \u2014 skipping clone.`);
|
|
37600
|
-
}
|
|
37601
|
-
continue;
|
|
37639
|
+
return;
|
|
37640
|
+
}
|
|
37641
|
+
if (existsSync4(join6(target, ".git"))) {
|
|
37642
|
+
if (mode === "sync") {
|
|
37643
|
+
await updateRepo(target, repo.branch, emit);
|
|
37644
|
+
} else if (mode === "force") {
|
|
37645
|
+
emit("git", `Re-cloning ${repo.url} (force)\u2026`);
|
|
37646
|
+
await rm(target, { recursive: true, force: true });
|
|
37647
|
+
await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
37648
|
+
} else {
|
|
37649
|
+
emit("git", "Repository already present \u2014 skipping clone.");
|
|
37602
37650
|
}
|
|
37603
|
-
|
|
37604
|
-
await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
|
|
37651
|
+
return;
|
|
37605
37652
|
}
|
|
37653
|
+
emit("git", `Cloning ${repo.url}\u2026`);
|
|
37654
|
+
await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
37606
37655
|
}
|
|
37607
37656
|
async function ensureReposCloned(physicRoot, repos, emit) {
|
|
37608
|
-
|
|
37609
|
-
|
|
37657
|
+
const list = repos;
|
|
37658
|
+
const repo = singleRepo(list);
|
|
37659
|
+
if (!repo) return;
|
|
37660
|
+
const step = emit ?? (() => void 0);
|
|
37661
|
+
await provisionRepo(physicRoot, repo, step);
|
|
37662
|
+
await attachSubmodules(physicRoot, submodulesOf(list), step);
|
|
37610
37663
|
}
|
|
37611
37664
|
function sampleDir() {
|
|
37612
37665
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
@@ -37627,17 +37680,12 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
|
37627
37680
|
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
37628
37681
|
const target = join6(root, payload.projectName);
|
|
37629
37682
|
await mkdir(target, { recursive: true });
|
|
37630
|
-
const
|
|
37631
|
-
if (
|
|
37632
|
-
|
|
37633
|
-
|
|
37634
|
-
|
|
37635
|
-
|
|
37636
|
-
await scaffoldRepo(join6(target, folders[i]), folders[i], payload.spec, emit);
|
|
37637
|
-
}
|
|
37638
|
-
} else {
|
|
37639
|
-
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
37640
|
-
}
|
|
37683
|
+
const repo = singleRepo(reposOf(payload.spec));
|
|
37684
|
+
if (!repo) throw new Error("A project must declare one repo (ADR-0314).");
|
|
37685
|
+
emit("git", "Cloning repository\u2026");
|
|
37686
|
+
await provisionRepo(target, repo, emit);
|
|
37687
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
37688
|
+
await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit);
|
|
37641
37689
|
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
37642
37690
|
await writeTemplateMarker(target);
|
|
37643
37691
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
@@ -37723,12 +37771,13 @@ async function addProject(payload, profileDir2, onProgress) {
|
|
|
37723
37771
|
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
37724
37772
|
const target = join6(root, payload.projectName);
|
|
37725
37773
|
await mkdir(target, { recursive: true });
|
|
37726
|
-
const
|
|
37774
|
+
const repo = singleRepo(payload.repos ?? []);
|
|
37727
37775
|
const mode = payload.mode ?? "clone";
|
|
37728
|
-
if (
|
|
37729
|
-
|
|
37730
|
-
await scaffoldRepo(
|
|
37776
|
+
if (repo) await provisionRepo(target, repo, emit, { mode });
|
|
37777
|
+
if (payload.scaffoldRepos && payload.scaffoldRepos.length > 0) {
|
|
37778
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
37731
37779
|
}
|
|
37780
|
+
await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit);
|
|
37732
37781
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
37733
37782
|
return { ok: true, path: target };
|
|
37734
37783
|
} catch (err) {
|
|
@@ -38227,7 +38276,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
38227
38276
|
import { spawn as spawn2 } from "child_process";
|
|
38228
38277
|
|
|
38229
38278
|
// src/common/logger/logger.ts
|
|
38230
|
-
import { appendFile, mkdir as mkdir2, readdir
|
|
38279
|
+
import { appendFile, mkdir as mkdir2, readdir, unlink } from "fs/promises";
|
|
38231
38280
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
38232
38281
|
import { join as join7 } from "path";
|
|
38233
38282
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
@@ -38287,7 +38336,7 @@ var CliLogger = class {
|
|
|
38287
38336
|
async cleanupOldLogs() {
|
|
38288
38337
|
const dir2 = this.dir;
|
|
38289
38338
|
if (!dir2) return;
|
|
38290
|
-
const files = await
|
|
38339
|
+
const files = await readdir(dir2).catch(() => []);
|
|
38291
38340
|
const cutoff = Date.now() - this.retentionDays * DAY_MS;
|
|
38292
38341
|
for (const file of files) {
|
|
38293
38342
|
const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
|
|
@@ -38823,7 +38872,6 @@ var ONE_SHOT_DISALLOWED_CLAUDE_TOOLS = [
|
|
|
38823
38872
|
"WebSearch",
|
|
38824
38873
|
"WebFetch",
|
|
38825
38874
|
"Skill",
|
|
38826
|
-
"SlashCommand",
|
|
38827
38875
|
"ToolSearch",
|
|
38828
38876
|
"TaskCreate",
|
|
38829
38877
|
"TaskGet",
|
|
@@ -39564,7 +39612,7 @@ var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
|
|
|
39564
39612
|
function kbRoot(profileDir2) {
|
|
39565
39613
|
return join14(profileDir2, "support-kb");
|
|
39566
39614
|
}
|
|
39567
|
-
function
|
|
39615
|
+
function repoDir(profileDir2, url, branch) {
|
|
39568
39616
|
const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
|
|
39569
39617
|
return join14(kbRoot(profileDir2), key);
|
|
39570
39618
|
}
|
|
@@ -39574,7 +39622,7 @@ function authedUrl(url, token) {
|
|
|
39574
39622
|
return url;
|
|
39575
39623
|
}
|
|
39576
39624
|
async function ensureRepo(profileDir2, repo) {
|
|
39577
|
-
const dir2 =
|
|
39625
|
+
const dir2 = repoDir(profileDir2, repo.url, repo.branch);
|
|
39578
39626
|
if (existsSync10(join14(dir2, ".git"))) return dir2;
|
|
39579
39627
|
const url = authedUrl(repo.url, repo.token);
|
|
39580
39628
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
@@ -39954,6 +40002,14 @@ async function writeAutonomous(root, req, by) {
|
|
|
39954
40002
|
await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
39955
40003
|
break;
|
|
39956
40004
|
}
|
|
40005
|
+
case "approvalsBatch": {
|
|
40006
|
+
const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
|
|
40007
|
+
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
40008
|
+
for (const taskId of req.approve) map[taskId] = { approved: true, by, at: at2 };
|
|
40009
|
+
for (const taskId of req.unapprove) delete map[taskId];
|
|
40010
|
+
await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
40011
|
+
break;
|
|
40012
|
+
}
|
|
39957
40013
|
case "userTodo": {
|
|
39958
40014
|
const cur = await readText(join15(root, BOOK_FILES.userTodo));
|
|
39959
40015
|
const trace = `posted by ${tableCell(by)} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
@@ -40828,8 +40884,10 @@ function handleCommandChannels(ctx, message, payload) {
|
|
|
40828
40884
|
text: `$ ${dispatch.cmd} ${dispatch.args.join(" ")}`.trimEnd()
|
|
40829
40885
|
});
|
|
40830
40886
|
ctx.bus.startBusy(dispatch.cmd);
|
|
40887
|
+
const cwd2 = dispatch.path ?? ctx.physicRoot ?? readProfileConfig(ctx.profileDir).physicPath ?? process.cwd();
|
|
40888
|
+
const dispatchInRoot = { ...dispatch, path: cwd2 };
|
|
40831
40889
|
const cmdStartMs = Date.now();
|
|
40832
|
-
void runCommand(
|
|
40890
|
+
void runCommand(dispatchInRoot, (out) => {
|
|
40833
40891
|
if (out.chunk) {
|
|
40834
40892
|
ctx.bus.push({ source: "server", kind: "out", text: out.chunk });
|
|
40835
40893
|
appendCommandOutput(out.commandId, out.chunk);
|
|
@@ -41294,7 +41352,7 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
41294
41352
|
}
|
|
41295
41353
|
|
|
41296
41354
|
// src/core/fs-browse.ts
|
|
41297
|
-
import { readdir as
|
|
41355
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
41298
41356
|
import { resolve as resolve2, sep as sep2 } from "path";
|
|
41299
41357
|
async function listDir(path, root) {
|
|
41300
41358
|
if (!root) return { path: "", entries: [] };
|
|
@@ -41302,7 +41360,7 @@ async function listDir(path, root) {
|
|
|
41302
41360
|
const requested = resolve2(base, path && path.trim() ? path : ".");
|
|
41303
41361
|
const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
|
|
41304
41362
|
try {
|
|
41305
|
-
const dirents = await
|
|
41363
|
+
const dirents = await readdir2(target, { withFileTypes: true });
|
|
41306
41364
|
const entries2 = dirents.map((d) => ({
|
|
41307
41365
|
name: d.name,
|
|
41308
41366
|
type: d.isDirectory() ? "dir" : "file"
|
|
@@ -41370,7 +41428,7 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
41370
41428
|
}
|
|
41371
41429
|
|
|
41372
41430
|
// src/core/fs-mutate.ts
|
|
41373
|
-
import { mkdir as mkdir4, writeFile as writeFile4, rename
|
|
41431
|
+
import { mkdir as mkdir4, writeFile as writeFile4, rename, rm as rm2, stat } from "fs/promises";
|
|
41374
41432
|
import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
|
|
41375
41433
|
var MAX_BYTES3 = 2 * 1024 * 1024;
|
|
41376
41434
|
function resolveInRoot3(root, rel) {
|
|
@@ -41425,7 +41483,7 @@ async function mutateFs(root, req) {
|
|
|
41425
41483
|
if (to2 !== resolve5(root) && !to2.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
|
|
41426
41484
|
if (await exists(to2)) return { ok: false, path: to2, error: "destination already exists" };
|
|
41427
41485
|
await mkdir4(dirname5(to2), { recursive: true });
|
|
41428
|
-
await
|
|
41486
|
+
await rename(from, to2);
|
|
41429
41487
|
return { ok: true, path: to2 };
|
|
41430
41488
|
}
|
|
41431
41489
|
case "delete": {
|
|
@@ -41724,7 +41782,6 @@ async function manageSshKey(op, profileDir2) {
|
|
|
41724
41782
|
}
|
|
41725
41783
|
|
|
41726
41784
|
// src/core/git-history.ts
|
|
41727
|
-
import { readdir as readdir4 } from "fs/promises";
|
|
41728
41785
|
import { execFile as execFile9 } from "child_process";
|
|
41729
41786
|
import { resolve as resolve8, sep as sep8 } from "path";
|
|
41730
41787
|
import { promisify as promisify9 } from "util";
|
|
@@ -41752,36 +41809,21 @@ async function git(cwd2, args) {
|
|
|
41752
41809
|
async function gitRepos(root) {
|
|
41753
41810
|
if (!root) return { repos: [] };
|
|
41754
41811
|
const base = resolve8(root);
|
|
41755
|
-
const
|
|
41812
|
+
const top = (await git(base, ["rev-parse", "--show-toplevel"])).trim();
|
|
41813
|
+
if (!top || !(top === base || top.startsWith(base + sep8))) return { repos: [] };
|
|
41756
41814
|
const repos = [];
|
|
41757
|
-
const
|
|
41758
|
-
|
|
41759
|
-
|
|
41760
|
-
|
|
41761
|
-
|
|
41762
|
-
const
|
|
41763
|
-
|
|
41764
|
-
|
|
41765
|
-
|
|
41766
|
-
|
|
41767
|
-
|
|
41768
|
-
const lvl1 = await readdir4(base, { withFileTypes: true });
|
|
41769
|
-
for (const d of lvl1) {
|
|
41770
|
-
if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
|
|
41771
|
-
const p1 = resolve8(base, d.name);
|
|
41772
|
-
await add(p1);
|
|
41773
|
-
try {
|
|
41774
|
-
const lvl2 = await readdir4(p1, { withFileTypes: true });
|
|
41775
|
-
for (const d2 of lvl2) {
|
|
41776
|
-
if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
|
|
41777
|
-
await add(resolve8(p1, d2.name));
|
|
41778
|
-
}
|
|
41779
|
-
} catch {
|
|
41780
|
-
}
|
|
41781
|
-
}
|
|
41782
|
-
} catch {
|
|
41815
|
+
const rootRemote = (await git(base, ["remote", "get-url", "origin"])).trim();
|
|
41816
|
+
repos.push({ subdir: "", name: "", remote: rootRemote || null });
|
|
41817
|
+
const subOut = await git(base, ["submodule", "status"]);
|
|
41818
|
+
for (const line of subOut.split("\n")) {
|
|
41819
|
+
const m2 = line.trim().match(/^[-+U ]?[0-9a-f]{7,40}\s+(\S+)/);
|
|
41820
|
+
const subPath = m2?.[1];
|
|
41821
|
+
if (!subPath) continue;
|
|
41822
|
+
const abs = resolve8(base, subPath);
|
|
41823
|
+
if (!(abs === base || abs.startsWith(base + sep8))) continue;
|
|
41824
|
+
const remote = (await git(abs, ["remote", "get-url", "origin"])).trim();
|
|
41825
|
+
repos.push({ subdir: subPath, name: subPath, remote: remote || null });
|
|
41783
41826
|
}
|
|
41784
|
-
repos.sort((a, b2) => a.subdir.localeCompare(b2.subdir));
|
|
41785
41827
|
return { repos };
|
|
41786
41828
|
}
|
|
41787
41829
|
async function gitLog(root, repo, skip, limit) {
|
|
@@ -42304,7 +42346,7 @@ function handleToolsChannels(ctx, message, payload) {
|
|
|
42304
42346
|
}
|
|
42305
42347
|
|
|
42306
42348
|
// src/core/agents.ts
|
|
42307
|
-
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as
|
|
42349
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir3, rm as rm4 } from "fs/promises";
|
|
42308
42350
|
import { join as join21 } from "path";
|
|
42309
42351
|
var AGENTS_REL = ".claude/agents";
|
|
42310
42352
|
var SKILLS_REL = ".claude/skills";
|
|
@@ -42329,7 +42371,7 @@ async function listAgents(root) {
|
|
|
42329
42371
|
const subagents = [];
|
|
42330
42372
|
const skills = [];
|
|
42331
42373
|
try {
|
|
42332
|
-
for (const e of await
|
|
42374
|
+
for (const e of await readdir3(join21(root, AGENTS_REL), { withFileTypes: true })) {
|
|
42333
42375
|
if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
|
|
42334
42376
|
const fm = frontmatter(await readText2(join21(root, AGENTS_REL, e.name)));
|
|
42335
42377
|
subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
|
|
@@ -42337,7 +42379,7 @@ async function listAgents(root) {
|
|
|
42337
42379
|
} catch {
|
|
42338
42380
|
}
|
|
42339
42381
|
try {
|
|
42340
|
-
for (const e of await
|
|
42382
|
+
for (const e of await readdir3(join21(root, SKILLS_REL), { withFileTypes: true })) {
|
|
42341
42383
|
if (!e.isDirectory()) continue;
|
|
42342
42384
|
const fm = frontmatter(await readText2(join21(root, SKILLS_REL, e.name, "SKILL.md")));
|
|
42343
42385
|
skills.push({ name: e.name, description: fm.description });
|
|
@@ -42489,7 +42531,7 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
42489
42531
|
|
|
42490
42532
|
// src/core/packages.ts
|
|
42491
42533
|
import { createHash as createHash4 } from "crypto";
|
|
42492
|
-
import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as
|
|
42534
|
+
import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir4, rm as rm5, stat as stat3 } from "fs/promises";
|
|
42493
42535
|
import { dirname as dirname9, join as join24, relative } from "path";
|
|
42494
42536
|
var AGENTS_REL2 = ".claude/agents";
|
|
42495
42537
|
var SKILLS_REL2 = ".claude/skills";
|
|
@@ -42509,7 +42551,7 @@ async function listFilesRel(dir2) {
|
|
|
42509
42551
|
async function walk2(d) {
|
|
42510
42552
|
let entries2;
|
|
42511
42553
|
try {
|
|
42512
|
-
entries2 = await
|
|
42554
|
+
entries2 = await readdir4(d, { withFileTypes: true });
|
|
42513
42555
|
} catch {
|
|
42514
42556
|
return;
|
|
42515
42557
|
}
|
|
@@ -42677,7 +42719,7 @@ async function removePackage(root, slug) {
|
|
|
42677
42719
|
}
|
|
42678
42720
|
|
|
42679
42721
|
// src/core/graph.ts
|
|
42680
|
-
import { readFile as readFile11, readdir as
|
|
42722
|
+
import { readFile as readFile11, readdir as readdir5 } from "fs/promises";
|
|
42681
42723
|
import { join as join25, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
|
|
42682
42724
|
var MAX_FILES = 2e3;
|
|
42683
42725
|
var MAX_NODES = 3e3;
|
|
@@ -42690,7 +42732,7 @@ async function walk(root, accept) {
|
|
|
42690
42732
|
const dir2 = stack.pop();
|
|
42691
42733
|
let entries2;
|
|
42692
42734
|
try {
|
|
42693
|
-
entries2 = await
|
|
42735
|
+
entries2 = await readdir5(dir2, { withFileTypes: true });
|
|
42694
42736
|
} catch {
|
|
42695
42737
|
continue;
|
|
42696
42738
|
}
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
> Tasks are generated from `USER_TODO.md`. ID format `TSK-{groupid:0000}-{taskid:0000}`
|
|
4
4
|
> (group = one request/batch, task = a small sub-task doable in ~1 cycle).
|
|
5
5
|
> **Priority** = High / Medium / Low.
|
|
6
|
-
> **
|
|
7
|
-
>
|
|
6
|
+
> **Tag** = optional catalog tag(s) (e.g. `UpdateSpecFromDB`) whose action runs from the server down to
|
|
7
|
+
> the project when the task is approved (approval is committed on Save — ADR-0311).
|
|
8
|
+
> **Approval** is NOT a table column — it lives in `.claude/.autonomous.approvals.json` (ADR-0152), set
|
|
9
|
+
> from the web AI Todo grid; `/auto-cycle` reads that file, never the table.
|
|
8
10
|
> **Depends** = the `TSK-…` ids that must be DONE (present in `AI_DONE.md`) first.
|
|
9
11
|
> `/auto-cycle` only takes tasks that are approved AND have their dependencies met → moves them to
|
|
10
12
|
> `AI_PROGRESS.md`; runs group by group, within a group High → Medium → Low.
|
|
11
13
|
|
|
12
|
-
| ID | Priority |
|
|
13
|
-
|
|
14
|
+
| ID | Priority | Tag | Depends | Group | Task description | Notes |
|
|
15
|
+
|----|----------|-----|---------|-------|------------------|-------|
|
|
14
16
|
| | | | | | | |
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# AI_TODO — Task queue (generated by the AI)
|
|
2
2
|
|
|
3
3
|
> Tasks are generated from `USER_TODO.md`. ID format `TSK-{groupid:0000}-{taskid:0000}`.
|
|
4
|
-
> **Priority** = High / Medium / Low. **
|
|
5
|
-
>
|
|
4
|
+
> **Priority** = High / Medium / Low. **Tag** = optional catalog tag(s) (e.g. `UpdateSpecFromDB`) whose
|
|
5
|
+
> action runs from the server down to the project on approve (committed on Save — ADR-0311).
|
|
6
|
+
> **Approval** lives in `.claude/.autonomous.approvals.json` (ADR-0152), not the table. **Depends** =
|
|
7
|
+
> `TSK-…` ids that must be in `AI_DONE.md` first.
|
|
6
8
|
|
|
7
|
-
| ID | Priority |
|
|
8
|
-
|
|
9
|
-
| TSK-0001-0001 | High |
|
|
10
|
-
| TSK-0001-0002 | Medium |
|
|
9
|
+
| ID | Priority | Tag | Depends | Group | Task description | Notes |
|
|
10
|
+
|----|----------|-----|---------|-------|------------------|-------|
|
|
11
|
+
| TSK-0001-0001 | High | | | Auth | Add login-form validation per docs/auth.md | |
|
|
12
|
+
| TSK-0001-0002 | Medium | | TSK-0001-0001 | Auth | Wire the login API call + error handling | After 0001 |
|
|
13
|
+
| TSK-0001-0003 | Low | UpdateSpecFromDB | | Spec | Sync the project spec into project.spec.json on approve | Server action |
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
> Tasks are generated from `USER_TODO.md`. ID format `TSK-{groupid:0000}-{taskid:0000}`
|
|
4
4
|
> (group = one request/batch, task = a small sub-task doable in ~1 cycle).
|
|
5
5
|
> **Priority** = High / Medium / Low.
|
|
6
|
-
> **
|
|
7
|
-
>
|
|
6
|
+
> **Tag** = optional catalog tag(s) (e.g. `UpdateSpecFromDB`) whose action runs from the server down to
|
|
7
|
+
> the project when the task is approved (approval is committed on Save — ADR-0311).
|
|
8
|
+
> **Approval** is NOT a table column — it lives in `.claude/.autonomous.approvals.json` (ADR-0152), set
|
|
9
|
+
> from the web AI Todo grid; `/auto-cycle` reads that file, never the table.
|
|
8
10
|
> **Depends** = the `TSK-…` ids that must be DONE (present in `AI_DONE.md`) first.
|
|
9
11
|
> `/auto-cycle` only takes tasks that are approved AND have their dependencies met → moves them to
|
|
10
12
|
> `AI_PROGRESS.md`; runs group by group, within a group High → Medium → Low.
|
|
11
13
|
|
|
12
|
-
| ID | Priority |
|
|
13
|
-
|
|
14
|
+
| ID | Priority | Tag | Depends | Group | Task description | Notes |
|
|
15
|
+
|----|----------|-----|---------|-------|------------------|-------|
|
|
14
16
|
| | | | | | | |
|