@4pm/cli 1.9.0-c → 1.10.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.
Files changed (2) hide show
  1. package/dist/index.js +99 -72
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -34339,6 +34339,16 @@ var addRepoRequestSchema = external_exports.object({
34339
34339
  });
34340
34340
  }
34341
34341
  });
34342
+ var addProjectRepoRequestSchema = external_exports.object({
34343
+ /** Clone url — `https` or `ssh` (required; ADR-0172). */
34344
+ url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
34345
+ /** Primary branch to clone / check out (ADR-0292); empty ⇒ the repo's default branch. */
34346
+ branch: external_exports.string().max(200).optional().default(""),
34347
+ /** Role label / folder hint (e.g. "docs" / "web"); empty ⇒ derived from the url basename. */
34348
+ role: external_exports.string().max(60).optional().default(""),
34349
+ /** true = Confirm (clone + scaffold); false = Skip (clone as-is, no template/AI-init). */
34350
+ scaffold: external_exports.boolean()
34351
+ });
34342
34352
 
34343
34353
  // vendor/@4pm/dto/user.ts
34344
34354
  var AVATAR_MAX_BYTES = 2 * 1024 * 1024;
@@ -34488,10 +34498,21 @@ function reposOf(spec) {
34488
34498
  const repos = spec?.repos;
34489
34499
  return Array.isArray(repos) ? repos : [];
34490
34500
  }
34491
- function subDirName(repo) {
34501
+ function folderName(repo) {
34492
34502
  const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
34493
34503
  return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
34494
34504
  }
34505
+ function repoFolders(repos) {
34506
+ const used = /* @__PURE__ */ new Set();
34507
+ return repos.map((r) => {
34508
+ const base = folderName(r);
34509
+ let name = base;
34510
+ let n = 2;
34511
+ while (used.has(name)) name = `${base}-${n++}`;
34512
+ used.add(name);
34513
+ return name;
34514
+ });
34515
+ }
34495
34516
  function cloneArgs(url, dest, branch) {
34496
34517
  const b = (branch ?? "").trim();
34497
34518
  return b ? ["clone", "-b", b, url, dest] : ["clone", url, dest];
@@ -34503,73 +34524,63 @@ async function updateRepo(dir2, label, branch, emit) {
34503
34524
  if (b) await run2("git", ["checkout", b], { cwd: dir2, timeout: 6e4 });
34504
34525
  await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
34505
34526
  }
34527
+ function repoDir(target, repo, folder, legacyRoot) {
34528
+ if (repo.primary && legacyRoot) return target;
34529
+ return join6(target, folder);
34530
+ }
34506
34531
  async function provisionRepos(target, repos, emit, opts = {}) {
34507
34532
  const mode = opts.mode ?? "clone";
34508
- const subDirsToKeep = new Set(repos.filter((r) => !r.primary && r.url).map((r) => subDirName(r)));
34509
- for (const repo of repos) {
34510
- if (repo.primary) {
34511
- if (opts.clonePrimary && repo.url) {
34512
- if (existsSync4(join6(target, ".git"))) {
34513
- if (mode === "sync") {
34514
- await updateRepo(target, "primary repo", repo.branch, emit);
34515
- } else if (mode === "force") {
34516
- emit("git", `Re-cloning ${repo.url} (force)\u2026`);
34517
- const tmp = `${target}.4pm-reclone`;
34518
- await rm(tmp, { recursive: true, force: true });
34519
- await run2("git", cloneArgs(repo.url, tmp, repo.branch), { timeout: 12e4 });
34520
- for (const entry of await readdir(target)) {
34521
- if (subDirsToKeep.has(entry)) continue;
34522
- await rm(join6(target, entry), { recursive: true, force: true });
34523
- }
34524
- for (const entry of await readdir(tmp)) {
34525
- if (subDirsToKeep.has(entry)) continue;
34526
- await rename(join6(tmp, entry), join6(target, entry));
34527
- }
34528
- await rm(tmp, { recursive: true, force: true });
34529
- } else {
34530
- emit("git", `Primary repo already present \u2014 skipping clone.`);
34531
- }
34532
- continue;
34533
- }
34534
- emit("git", `Cloning ${repo.url}\u2026`);
34535
- await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
34536
- continue;
34537
- }
34538
- emit("git", "Initializing the primary repo\u2026");
34539
- await run2("git", ["init"], { cwd: target, timeout: 2e4 });
34540
- if (repo.url) {
34541
- await run2("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
34542
- () => void 0
34543
- );
34533
+ const legacyRoot = existsSync4(join6(target, ".git"));
34534
+ const folders = repoFolders(repos);
34535
+ const foldersToKeep = new Set(
34536
+ repos.map((r, i) => r.primary && legacyRoot ? null : folders[i]).filter((f) => !!f)
34537
+ );
34538
+ for (const [i, repo] of repos.entries()) {
34539
+ const folder = folders[i];
34540
+ const atRoot = !!repo.primary && legacyRoot;
34541
+ const dir2 = repoDir(target, repo, folder, legacyRoot);
34542
+ const label = repo.primary ? "primary repo" : `repo ${folder}`;
34543
+ if (!repo.url) {
34544
+ if (!existsSync4(join6(dir2, ".git"))) {
34545
+ emit("git", `Initializing ${label}\u2026`);
34546
+ await mkdir(dir2, { recursive: true });
34547
+ await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
34544
34548
  }
34545
34549
  continue;
34546
34550
  }
34547
- const dir2 = join6(target, subDirName(repo));
34548
- if (repo.url) {
34549
- if (existsSync4(join6(dir2, ".git"))) {
34550
- if (mode === "sync") {
34551
- await updateRepo(dir2, `sub-repo ${subDirName(repo)}`, repo.branch, emit);
34552
- } else if (mode === "force") {
34553
- emit("git", `Re-cloning sub-repo ${repo.url} (force)\u2026`);
34554
- await rm(dir2, { recursive: true, force: true });
34555
- await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34556
- } else {
34557
- emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
34558
- }
34559
- continue;
34551
+ if (existsSync4(join6(dir2, ".git"))) {
34552
+ if (mode === "sync") {
34553
+ await updateRepo(dir2, label, repo.branch, emit);
34554
+ } else if (mode === "force" && atRoot) {
34555
+ emit("git", `Re-cloning ${repo.url} (force)\u2026`);
34556
+ const tmp = `${target}.4pm-reclone`;
34557
+ await rm(tmp, { recursive: true, force: true });
34558
+ await run2("git", cloneArgs(repo.url, tmp, repo.branch), { timeout: 12e4 });
34559
+ for (const entry of await readdir(target)) {
34560
+ if (foldersToKeep.has(entry)) continue;
34561
+ await rm(join6(target, entry), { recursive: true, force: true });
34562
+ }
34563
+ for (const entry of await readdir(tmp)) {
34564
+ if (foldersToKeep.has(entry)) continue;
34565
+ await rename(join6(tmp, entry), join6(target, entry));
34566
+ }
34567
+ await rm(tmp, { recursive: true, force: true });
34568
+ } else if (mode === "force") {
34569
+ emit("git", `Re-cloning ${repo.url} (force)\u2026`);
34570
+ await rm(dir2, { recursive: true, force: true });
34571
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34572
+ } else {
34573
+ emit("git", `${label} already present \u2014 skipping clone.`);
34560
34574
  }
34561
- emit("git", `Cloning sub-repo ${repo.url}\u2026`);
34562
- await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34563
- } else {
34564
- emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
34565
- await mkdir(dir2, { recursive: true });
34566
- await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
34575
+ continue;
34567
34576
  }
34577
+ emit("git", `Cloning ${repo.url}\u2026`);
34578
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34568
34579
  }
34569
34580
  }
34570
34581
  async function ensureReposCloned(physicRoot, repos, emit) {
34571
34582
  if (repos.length === 0) return;
34572
- await provisionRepos(physicRoot, repos, emit ?? (() => void 0), { clonePrimary: true });
34583
+ await provisionRepos(physicRoot, repos, emit ?? (() => void 0));
34573
34584
  }
34574
34585
  function sampleDir() {
34575
34586
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
@@ -34590,19 +34601,17 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
34590
34601
  const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
34591
34602
  const target = join6(root, payload.projectName);
34592
34603
  await mkdir(target, { recursive: true });
34593
- emit("copy", "Copying the sample template\u2026");
34594
- await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
34595
- if (payload.spec) {
34596
- emit("spec", "Writing project.spec.json\u2026");
34597
- await writeFile(
34598
- join6(target, "project.spec.json"),
34599
- JSON.stringify(payload.spec, null, 2),
34600
- "utf8"
34601
- );
34602
- }
34603
34604
  const repos = reposOf(payload.spec);
34604
- if (repos.length > 0) await provisionRepos(target, repos, emit);
34605
- if (payload.spec) await aiInit(target, payload.spec, emit);
34605
+ if (repos.length > 0) {
34606
+ emit("git", "Cloning repositories\u2026");
34607
+ await provisionRepos(target, repos, emit);
34608
+ const folders = repoFolders(repos);
34609
+ for (let i = 0; i < repos.length; i++) {
34610
+ await scaffoldRepo(join6(target, folders[i]), folders[i], payload.spec, emit);
34611
+ }
34612
+ } else {
34613
+ await scaffoldRepo(target, payload.projectName, payload.spec, emit);
34614
+ }
34606
34615
  emit("version", "Writing .4pm/.4pm.json\u2026");
34607
34616
  await writeTemplateMarker(target);
34608
34617
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
@@ -34611,6 +34620,16 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
34611
34620
  return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
34612
34621
  }
34613
34622
  }
34623
+ async function scaffoldRepo(dir2, label, spec, emit) {
34624
+ await mkdir(dir2, { recursive: true });
34625
+ emit("copy", `Copying the sample template into ${label}\u2026`);
34626
+ await cp(sampleDir(), dir2, { recursive: true, force: false, errorOnExist: false });
34627
+ if (spec) {
34628
+ emit("spec", `Writing project.spec.json into ${label}\u2026`);
34629
+ await writeFile(join6(dir2, "project.spec.json"), JSON.stringify(spec, null, 2), "utf8");
34630
+ await aiInit(dir2, spec, emit);
34631
+ }
34632
+ }
34614
34633
  async function writeTemplateMarker(target) {
34615
34634
  const dir2 = join6(target, ".4pm");
34616
34635
  await mkdir(dir2, { recursive: true });
@@ -34643,6 +34662,11 @@ ${specJson}`
34643
34662
  await generateFile(
34644
34663
  join6(target, "CLAUDE.md"),
34645
34664
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
34665
+ ${specJson}`
34666
+ );
34667
+ await generateFile(
34668
+ join6(target, "AGENT.md"),
34669
+ `Write an AGENT.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
34646
34670
  ${specJson}`
34647
34671
  );
34648
34672
  }
@@ -34665,7 +34689,10 @@ async function addProject(payload, profileDir2, onProgress) {
34665
34689
  await mkdir(target, { recursive: true });
34666
34690
  const repos = payload.repos ?? [];
34667
34691
  const mode = payload.mode ?? "clone";
34668
- if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true, mode });
34692
+ if (repos.length > 0) await provisionRepos(target, repos, emit, { mode });
34693
+ for (const sub of payload.scaffoldRepos ?? []) {
34694
+ await scaffoldRepo(join6(target, sub), sub, payload.spec, emit);
34695
+ }
34669
34696
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
34670
34697
  return { ok: true, path: target };
34671
34698
  } catch (err) {
@@ -36470,7 +36497,7 @@ var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
36470
36497
  function kbRoot(profileDir2) {
36471
36498
  return join14(profileDir2, "support-kb");
36472
36499
  }
36473
- function repoDir(profileDir2, url, branch) {
36500
+ function repoDir2(profileDir2, url, branch) {
36474
36501
  const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
36475
36502
  return join14(kbRoot(profileDir2), key);
36476
36503
  }
@@ -36480,7 +36507,7 @@ function authedUrl(url, token) {
36480
36507
  return url;
36481
36508
  }
36482
36509
  async function ensureRepo(profileDir2, repo) {
36483
- const dir2 = repoDir(profileDir2, repo.url, repo.branch);
36510
+ const dir2 = repoDir2(profileDir2, repo.url, repo.branch);
36484
36511
  if (existsSync10(join14(dir2, ".git"))) return dir2;
36485
36512
  const url = authedUrl(repo.url, repo.token);
36486
36513
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.9.0c",
3
+ "version": "1.10.0",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "license": "LicenseRef-4PM-Source-Available",