@4pm/cli 1.9.0 → 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 +122 -74
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -29506,17 +29506,38 @@ MCowBQYDK2VwAyEARlfzjaFhe5bhRrMp/ZuH6sVqg/S7f8B1dliuF21iDTw=
29506
29506
  `;
29507
29507
 
29508
29508
  // src/version.ts
29509
+ import { execFileSync } from "child_process";
29509
29510
  import { readFileSync as readFileSync4 } from "fs";
29510
29511
  import { dirname, join as join4 } from "path";
29511
29512
  import { fileURLToPath } from "url";
29513
+ function gitDescribeVersion(cwd2) {
29514
+ try {
29515
+ const out = execFileSync("git", ["describe", "--tags", "--match", "cli-v*"], {
29516
+ cwd: cwd2,
29517
+ timeout: 3e3,
29518
+ stdio: ["ignore", "pipe", "ignore"]
29519
+ }).toString().trim();
29520
+ const stripped = out.replace(/^cli-v/, "");
29521
+ return stripped || null;
29522
+ } catch {
29523
+ return null;
29524
+ }
29525
+ }
29512
29526
  function readCliVersion() {
29527
+ const envVersion = process.env.FOURPM_CLI_VERSION?.trim();
29528
+ if (envVersion) return envVersion.replace(/^cli-v/, "");
29529
+ let version = "0.0.0";
29513
29530
  try {
29514
29531
  const here = dirname(fileURLToPath(import.meta.url));
29515
29532
  const pkg = JSON.parse(readFileSync4(join4(here, "..", "package.json"), "utf8"));
29516
- return pkg.version ?? "0.0.0";
29533
+ version = pkg.version ?? "0.0.0";
29534
+ if (version === "0.0.0") {
29535
+ const dev = gitDescribeVersion(here);
29536
+ if (dev) version = dev;
29537
+ }
29517
29538
  } catch {
29518
- return "0.0.0";
29519
29539
  }
29540
+ return version;
29520
29541
  }
29521
29542
  var CLI_VERSION = readCliVersion();
29522
29543
 
@@ -34318,6 +34339,16 @@ var addRepoRequestSchema = external_exports.object({
34318
34339
  });
34319
34340
  }
34320
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
+ });
34321
34352
 
34322
34353
  // vendor/@4pm/dto/user.ts
34323
34354
  var AVATAR_MAX_BYTES = 2 * 1024 * 1024;
@@ -34467,10 +34498,21 @@ function reposOf(spec) {
34467
34498
  const repos = spec?.repos;
34468
34499
  return Array.isArray(repos) ? repos : [];
34469
34500
  }
34470
- function subDirName(repo) {
34501
+ function folderName(repo) {
34471
34502
  const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
34472
34503
  return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
34473
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
+ }
34474
34516
  function cloneArgs(url, dest, branch) {
34475
34517
  const b = (branch ?? "").trim();
34476
34518
  return b ? ["clone", "-b", b, url, dest] : ["clone", url, dest];
@@ -34482,73 +34524,63 @@ async function updateRepo(dir2, label, branch, emit) {
34482
34524
  if (b) await run2("git", ["checkout", b], { cwd: dir2, timeout: 6e4 });
34483
34525
  await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
34484
34526
  }
34527
+ function repoDir(target, repo, folder, legacyRoot) {
34528
+ if (repo.primary && legacyRoot) return target;
34529
+ return join6(target, folder);
34530
+ }
34485
34531
  async function provisionRepos(target, repos, emit, opts = {}) {
34486
34532
  const mode = opts.mode ?? "clone";
34487
- const subDirsToKeep = new Set(repos.filter((r) => !r.primary && r.url).map((r) => subDirName(r)));
34488
- for (const repo of repos) {
34489
- if (repo.primary) {
34490
- if (opts.clonePrimary && repo.url) {
34491
- if (existsSync4(join6(target, ".git"))) {
34492
- if (mode === "sync") {
34493
- await updateRepo(target, "primary repo", repo.branch, emit);
34494
- } else if (mode === "force") {
34495
- emit("git", `Re-cloning ${repo.url} (force)\u2026`);
34496
- const tmp = `${target}.4pm-reclone`;
34497
- await rm(tmp, { recursive: true, force: true });
34498
- await run2("git", cloneArgs(repo.url, tmp, repo.branch), { timeout: 12e4 });
34499
- for (const entry of await readdir(target)) {
34500
- if (subDirsToKeep.has(entry)) continue;
34501
- await rm(join6(target, entry), { recursive: true, force: true });
34502
- }
34503
- for (const entry of await readdir(tmp)) {
34504
- if (subDirsToKeep.has(entry)) continue;
34505
- await rename(join6(tmp, entry), join6(target, entry));
34506
- }
34507
- await rm(tmp, { recursive: true, force: true });
34508
- } else {
34509
- emit("git", `Primary repo already present \u2014 skipping clone.`);
34510
- }
34511
- continue;
34512
- }
34513
- emit("git", `Cloning ${repo.url}\u2026`);
34514
- await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
34515
- continue;
34516
- }
34517
- emit("git", "Initializing the primary repo\u2026");
34518
- await run2("git", ["init"], { cwd: target, timeout: 2e4 });
34519
- if (repo.url) {
34520
- await run2("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
34521
- () => void 0
34522
- );
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 });
34523
34548
  }
34524
34549
  continue;
34525
34550
  }
34526
- const dir2 = join6(target, subDirName(repo));
34527
- if (repo.url) {
34528
- if (existsSync4(join6(dir2, ".git"))) {
34529
- if (mode === "sync") {
34530
- await updateRepo(dir2, `sub-repo ${subDirName(repo)}`, repo.branch, emit);
34531
- } else if (mode === "force") {
34532
- emit("git", `Re-cloning sub-repo ${repo.url} (force)\u2026`);
34533
- await rm(dir2, { recursive: true, force: true });
34534
- await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34535
- } else {
34536
- emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
34537
- }
34538
- 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.`);
34539
34574
  }
34540
- emit("git", `Cloning sub-repo ${repo.url}\u2026`);
34541
- await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34542
- } else {
34543
- emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
34544
- await mkdir(dir2, { recursive: true });
34545
- await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
34575
+ continue;
34546
34576
  }
34577
+ emit("git", `Cloning ${repo.url}\u2026`);
34578
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34547
34579
  }
34548
34580
  }
34549
34581
  async function ensureReposCloned(physicRoot, repos, emit) {
34550
34582
  if (repos.length === 0) return;
34551
- await provisionRepos(physicRoot, repos, emit ?? (() => void 0), { clonePrimary: true });
34583
+ await provisionRepos(physicRoot, repos, emit ?? (() => void 0));
34552
34584
  }
34553
34585
  function sampleDir() {
34554
34586
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
@@ -34569,19 +34601,17 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
34569
34601
  const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
34570
34602
  const target = join6(root, payload.projectName);
34571
34603
  await mkdir(target, { recursive: true });
34572
- emit("copy", "Copying the sample template\u2026");
34573
- await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
34574
- if (payload.spec) {
34575
- emit("spec", "Writing project.spec.json\u2026");
34576
- await writeFile(
34577
- join6(target, "project.spec.json"),
34578
- JSON.stringify(payload.spec, null, 2),
34579
- "utf8"
34580
- );
34581
- }
34582
34604
  const repos = reposOf(payload.spec);
34583
- if (repos.length > 0) await provisionRepos(target, repos, emit);
34584
- 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
+ }
34585
34615
  emit("version", "Writing .4pm/.4pm.json\u2026");
34586
34616
  await writeTemplateMarker(target);
34587
34617
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
@@ -34590,6 +34620,16 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
34590
34620
  return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
34591
34621
  }
34592
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
+ }
34593
34633
  async function writeTemplateMarker(target) {
34594
34634
  const dir2 = join6(target, ".4pm");
34595
34635
  await mkdir(dir2, { recursive: true });
@@ -34622,6 +34662,11 @@ ${specJson}`
34622
34662
  await generateFile(
34623
34663
  join6(target, "CLAUDE.md"),
34624
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:
34625
34670
  ${specJson}`
34626
34671
  );
34627
34672
  }
@@ -34644,7 +34689,10 @@ async function addProject(payload, profileDir2, onProgress) {
34644
34689
  await mkdir(target, { recursive: true });
34645
34690
  const repos = payload.repos ?? [];
34646
34691
  const mode = payload.mode ?? "clone";
34647
- 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
+ }
34648
34696
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
34649
34697
  return { ok: true, path: target };
34650
34698
  } catch (err) {
@@ -36449,7 +36497,7 @@ var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
36449
36497
  function kbRoot(profileDir2) {
36450
36498
  return join14(profileDir2, "support-kb");
36451
36499
  }
36452
- function repoDir(profileDir2, url, branch) {
36500
+ function repoDir2(profileDir2, url, branch) {
36453
36501
  const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
36454
36502
  return join14(kbRoot(profileDir2), key);
36455
36503
  }
@@ -36459,7 +36507,7 @@ function authedUrl(url, token) {
36459
36507
  return url;
36460
36508
  }
36461
36509
  async function ensureRepo(profileDir2, repo) {
36462
- const dir2 = repoDir(profileDir2, repo.url, repo.branch);
36510
+ const dir2 = repoDir2(profileDir2, repo.url, repo.branch);
36463
36511
  if (existsSync10(join14(dir2, ".git"))) return dir2;
36464
36512
  const url = authedUrl(repo.url, repo.token);
36465
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.0",
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",