@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.
- package/dist/index.js +122 -74
- 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
|
-
|
|
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
|
|
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
|
|
34488
|
-
|
|
34489
|
-
|
|
34490
|
-
|
|
34491
|
-
|
|
34492
|
-
|
|
34493
|
-
|
|
34494
|
-
|
|
34495
|
-
|
|
34496
|
-
|
|
34497
|
-
|
|
34498
|
-
|
|
34499
|
-
|
|
34500
|
-
|
|
34501
|
-
|
|
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
|
-
|
|
34527
|
-
|
|
34528
|
-
|
|
34529
|
-
|
|
34530
|
-
|
|
34531
|
-
|
|
34532
|
-
|
|
34533
|
-
|
|
34534
|
-
|
|
34535
|
-
|
|
34536
|
-
|
|
34537
|
-
}
|
|
34538
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
34584
|
-
|
|
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, {
|
|
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
|
|
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 =
|
|
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" };
|