@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.
- package/dist/index.js +99 -72
- 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
|
|
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
|
|
34509
|
-
|
|
34510
|
-
|
|
34511
|
-
|
|
34512
|
-
|
|
34513
|
-
|
|
34514
|
-
|
|
34515
|
-
|
|
34516
|
-
|
|
34517
|
-
|
|
34518
|
-
|
|
34519
|
-
|
|
34520
|
-
|
|
34521
|
-
|
|
34522
|
-
|
|
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
|
-
|
|
34548
|
-
|
|
34549
|
-
|
|
34550
|
-
|
|
34551
|
-
|
|
34552
|
-
|
|
34553
|
-
|
|
34554
|
-
|
|
34555
|
-
|
|
34556
|
-
|
|
34557
|
-
|
|
34558
|
-
}
|
|
34559
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
34605
|
-
|
|
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, {
|
|
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
|
|
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 =
|
|
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" };
|