@4pm/cli 1.8.4-b → 1.9.0-b

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 +115 -38
  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
 
@@ -33965,6 +33986,14 @@ var issueProvisioningTokenRequestSchema = external_exports.object({
33965
33986
  /** TTL seconds (0/absent ⇒ a default short TTL applied server-side). */
33966
33987
  ttlSec: external_exports.number().int().min(0).max(2592e3).optional()
33967
33988
  });
33989
+ var provisionRequestSchema = external_exports.object({
33990
+ mode: external_exports.enum(["sync", "force"]).optional().default("sync")
33991
+ });
33992
+ var provisionForUserRequestSchema = external_exports.object({
33993
+ userId: external_exports.string().uuid(),
33994
+ projectId: external_exports.string().uuid(),
33995
+ mode: external_exports.enum(["sync", "force"]).optional().default("sync")
33996
+ });
33968
33997
  var listWorkersQuerySchema = baseRequestSchema.extend({
33969
33998
  status: external_exports.enum(["online", "offline"]).optional(),
33970
33999
  /** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
@@ -34240,7 +34269,13 @@ var repoSpecSchema = external_exports.object({
34240
34269
  /** Clone url — `https` or `ssh` (required; ADR-0172). */
34241
34270
  url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
34242
34271
  /** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
34243
- subdir: external_exports.string().max(120).optional().default("")
34272
+ subdir: external_exports.string().max(120).optional().default(""),
34273
+ /**
34274
+ * Primary branch to clone / check out (ADR-0292). Empty ⇒ the repo's default branch. The
34275
+ * worker clones with `-b <branch>` and the on-demand "update repos" action checks it out +
34276
+ * fast-forward pulls it.
34277
+ */
34278
+ branch: external_exports.string().max(200).optional().default("")
34244
34279
  });
34245
34280
  function repoName(url) {
34246
34281
  const last = (url || "").trim().replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
@@ -34368,7 +34403,7 @@ var activityHeartbeatSchema = external_exports.object({
34368
34403
  });
34369
34404
 
34370
34405
  // src/core/scaffold.ts
34371
- import { cp, mkdir, writeFile } from "fs/promises";
34406
+ import { cp, mkdir, readdir, rename, rm, writeFile } from "fs/promises";
34372
34407
  import { existsSync as existsSync4 } from "fs";
34373
34408
  import { execFile as execFile2 } from "child_process";
34374
34409
  import { dirname as dirname3, join as join6, resolve } from "path";
@@ -34457,16 +34492,47 @@ function subDirName(repo) {
34457
34492
  const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
34458
34493
  return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
34459
34494
  }
34495
+ function cloneArgs(url, dest, branch) {
34496
+ const b = (branch ?? "").trim();
34497
+ return b ? ["clone", "-b", b, url, dest] : ["clone", url, dest];
34498
+ }
34499
+ async function updateRepo(dir2, label, branch, emit) {
34500
+ const b = (branch ?? "").trim();
34501
+ emit("git", `Updating ${label} (fetch + ${b ? `checkout ${b} + ` : ""}fast-forward pull)\u2026`);
34502
+ await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
34503
+ if (b) await run2("git", ["checkout", b], { cwd: dir2, timeout: 6e4 });
34504
+ await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
34505
+ }
34460
34506
  async function provisionRepos(target, repos, emit, opts = {}) {
34507
+ const mode = opts.mode ?? "clone";
34508
+ const subDirsToKeep = new Set(repos.filter((r) => !r.primary && r.url).map((r) => subDirName(r)));
34461
34509
  for (const repo of repos) {
34462
34510
  if (repo.primary) {
34463
34511
  if (opts.clonePrimary && repo.url) {
34464
34512
  if (existsSync4(join6(target, ".git"))) {
34465
- emit("git", `Primary repo already present \u2014 skipping clone.`);
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
+ }
34466
34532
  continue;
34467
34533
  }
34468
34534
  emit("git", `Cloning ${repo.url}\u2026`);
34469
- await run2("git", ["clone", repo.url, target], { timeout: 12e4 });
34535
+ await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
34470
34536
  continue;
34471
34537
  }
34472
34538
  emit("git", "Initializing the primary repo\u2026");
@@ -34481,11 +34547,19 @@ async function provisionRepos(target, repos, emit, opts = {}) {
34481
34547
  const dir2 = join6(target, subDirName(repo));
34482
34548
  if (repo.url) {
34483
34549
  if (existsSync4(join6(dir2, ".git"))) {
34484
- emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
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
+ }
34485
34559
  continue;
34486
34560
  }
34487
34561
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
34488
- await run2("git", ["clone", repo.url, dir2], { timeout: 12e4 });
34562
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34489
34563
  } else {
34490
34564
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
34491
34565
  await mkdir(dir2, { recursive: true });
@@ -34590,11 +34664,14 @@ async function addProject(payload, profileDir2, onProgress) {
34590
34664
  const target = join6(root, payload.projectName);
34591
34665
  await mkdir(target, { recursive: true });
34592
34666
  const repos = payload.repos ?? [];
34593
- if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
34667
+ const mode = payload.mode ?? "clone";
34668
+ if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true, mode });
34594
34669
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
34595
34670
  return { ok: true, path: target };
34596
34671
  } catch (err) {
34597
- return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
34672
+ const message = err instanceof Error ? err.message : String(err);
34673
+ onProgress?.({ projectId: payload.projectId, step: "error", message: `Failed: ${message}`, done: true });
34674
+ return { ok: false, error: message, step: lastStep };
34598
34675
  }
34599
34676
  }
34600
34677
 
@@ -35082,7 +35159,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
35082
35159
  import { spawn as spawn2 } from "child_process";
35083
35160
 
35084
35161
  // src/common/logger/logger.ts
35085
- import { appendFile, mkdir as mkdir2, readdir, unlink } from "fs/promises";
35162
+ import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
35086
35163
  import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
35087
35164
  import { join as join7 } from "path";
35088
35165
  var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
@@ -35142,7 +35219,7 @@ var CliLogger = class {
35142
35219
  async cleanupOldLogs() {
35143
35220
  const dir2 = this.dir;
35144
35221
  if (!dir2) return;
35145
- const files = await readdir(dir2).catch(() => []);
35222
+ const files = await readdir2(dir2).catch(() => []);
35146
35223
  const cutoff = Date.now() - this.retentionDays * DAY_MS;
35147
35224
  for (const file of files) {
35148
35225
  const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
@@ -38123,7 +38200,7 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
38123
38200
  }
38124
38201
 
38125
38202
  // src/core/fs-browse.ts
38126
- import { readdir as readdir2 } from "fs/promises";
38203
+ import { readdir as readdir3 } from "fs/promises";
38127
38204
  import { resolve as resolve2, sep as sep2 } from "path";
38128
38205
  async function listDir(path, root) {
38129
38206
  if (!root) return { path: "", entries: [] };
@@ -38131,7 +38208,7 @@ async function listDir(path, root) {
38131
38208
  const requested = resolve2(base, path && path.trim() ? path : ".");
38132
38209
  const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
38133
38210
  try {
38134
- const dirents = await readdir2(target, { withFileTypes: true });
38211
+ const dirents = await readdir3(target, { withFileTypes: true });
38135
38212
  const entries2 = dirents.map((d) => ({
38136
38213
  name: d.name,
38137
38214
  type: d.isDirectory() ? "dir" : "file"
@@ -38199,7 +38276,7 @@ async function writeWorkerFile(root, relPath, content) {
38199
38276
  }
38200
38277
 
38201
38278
  // src/core/fs-mutate.ts
38202
- import { mkdir as mkdir4, writeFile as writeFile4, rename, rm, stat } from "fs/promises";
38279
+ import { mkdir as mkdir4, writeFile as writeFile4, rename as rename2, rm as rm2, stat } from "fs/promises";
38203
38280
  import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
38204
38281
  var MAX_BYTES3 = 2 * 1024 * 1024;
38205
38282
  function resolveInRoot3(root, rel) {
@@ -38254,14 +38331,14 @@ async function mutateFs(root, req) {
38254
38331
  if (to !== resolve5(root) && !to.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
38255
38332
  if (await exists(to)) return { ok: false, path: to, error: "destination already exists" };
38256
38333
  await mkdir4(dirname5(to), { recursive: true });
38257
- await rename(from, to);
38334
+ await rename2(from, to);
38258
38335
  return { ok: true, path: to };
38259
38336
  }
38260
38337
  case "delete": {
38261
38338
  const target = resolveInRoot3(root, req.path ?? "");
38262
38339
  if (!target) return escaped(req.path ?? "");
38263
38340
  if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
38264
- await rm(target, { recursive: true, force: true });
38341
+ await rm2(target, { recursive: true, force: true });
38265
38342
  return { ok: true, path: target };
38266
38343
  }
38267
38344
  default:
@@ -38486,7 +38563,7 @@ ${stderr}`;
38486
38563
 
38487
38564
  // src/core/git-ssh-key.ts
38488
38565
  import { execFile as execFile8 } from "child_process";
38489
- import { readFile as readFile6, rm as rm2, mkdir as mkdir6 } from "fs/promises";
38566
+ import { readFile as readFile6, rm as rm3, mkdir as mkdir6 } from "fs/promises";
38490
38567
  import { join as join18 } from "path";
38491
38568
  import { promisify as promisify8 } from "util";
38492
38569
  var run8 = promisify8(execFile8);
@@ -38520,8 +38597,8 @@ async function configureGit(key) {
38520
38597
  async function generate(profileDir2) {
38521
38598
  const { dir: dir2, key, pub } = keyPaths(profileDir2);
38522
38599
  await mkdir6(dir2, { recursive: true });
38523
- await rm2(key, { force: true });
38524
- await rm2(pub, { force: true });
38600
+ await rm3(key, { force: true });
38601
+ await rm3(pub, { force: true });
38525
38602
  await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
38526
38603
  timeout: 15e3
38527
38604
  });
@@ -38530,8 +38607,8 @@ async function generate(profileDir2) {
38530
38607
  }
38531
38608
  async function remove(profileDir2) {
38532
38609
  const { key, pub } = keyPaths(profileDir2);
38533
- await rm2(key, { force: true });
38534
- await rm2(pub, { force: true });
38610
+ await rm3(key, { force: true });
38611
+ await rm3(pub, { force: true });
38535
38612
  await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
38536
38613
  () => void 0
38537
38614
  );
@@ -38553,7 +38630,7 @@ async function manageSshKey(op, profileDir2) {
38553
38630
  }
38554
38631
 
38555
38632
  // src/core/git-history.ts
38556
- import { readdir as readdir3 } from "fs/promises";
38633
+ import { readdir as readdir4 } from "fs/promises";
38557
38634
  import { execFile as execFile9 } from "child_process";
38558
38635
  import { resolve as resolve8, sep as sep8 } from "path";
38559
38636
  import { promisify as promisify9 } from "util";
@@ -38594,13 +38671,13 @@ async function gitRepos(root) {
38594
38671
  };
38595
38672
  await add(base);
38596
38673
  try {
38597
- const lvl1 = await readdir3(base, { withFileTypes: true });
38674
+ const lvl1 = await readdir4(base, { withFileTypes: true });
38598
38675
  for (const d of lvl1) {
38599
38676
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
38600
38677
  const p1 = resolve8(base, d.name);
38601
38678
  await add(p1);
38602
38679
  try {
38603
- const lvl2 = await readdir3(p1, { withFileTypes: true });
38680
+ const lvl2 = await readdir4(p1, { withFileTypes: true });
38604
38681
  for (const d2 of lvl2) {
38605
38682
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
38606
38683
  await add(resolve8(p1, d2.name));
@@ -39130,7 +39207,7 @@ function handleToolsChannels(ctx, message, payload) {
39130
39207
  }
39131
39208
 
39132
39209
  // src/core/agents.ts
39133
- import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir4, rm as rm3 } from "fs/promises";
39210
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir5, rm as rm4 } from "fs/promises";
39134
39211
  import { join as join21 } from "path";
39135
39212
  var AGENTS_REL = ".claude/agents";
39136
39213
  var SKILLS_REL = ".claude/skills";
@@ -39155,7 +39232,7 @@ async function listAgents(root) {
39155
39232
  const subagents = [];
39156
39233
  const skills = [];
39157
39234
  try {
39158
- for (const e of await readdir4(join21(root, AGENTS_REL), { withFileTypes: true })) {
39235
+ for (const e of await readdir5(join21(root, AGENTS_REL), { withFileTypes: true })) {
39159
39236
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
39160
39237
  const fm = frontmatter(await readText2(join21(root, AGENTS_REL, e.name)));
39161
39238
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
@@ -39163,7 +39240,7 @@ async function listAgents(root) {
39163
39240
  } catch {
39164
39241
  }
39165
39242
  try {
39166
- for (const e of await readdir4(join21(root, SKILLS_REL), { withFileTypes: true })) {
39243
+ for (const e of await readdir5(join21(root, SKILLS_REL), { withFileTypes: true })) {
39167
39244
  if (!e.isDirectory()) continue;
39168
39245
  const fm = frontmatter(await readText2(join21(root, SKILLS_REL, e.name, "SKILL.md")));
39169
39246
  skills.push({ name: e.name, description: fm.description });
@@ -39193,8 +39270,8 @@ async function writeAgent(root, req) {
39193
39270
  if (!n) return { ok: false, error: "invalid name" };
39194
39271
  try {
39195
39272
  if (req.action === "delete") {
39196
- if (req.kind === "subagent") await rm3(itemPath(root, "subagent", n), { force: true });
39197
- else await rm3(join21(root, SKILLS_REL, n), { recursive: true, force: true });
39273
+ if (req.kind === "subagent") await rm4(itemPath(root, "subagent", n), { force: true });
39274
+ else await rm4(join21(root, SKILLS_REL, n), { recursive: true, force: true });
39198
39275
  return { ok: true };
39199
39276
  }
39200
39277
  if (req.content === void 0) return { ok: false, error: "content is required" };
@@ -39315,7 +39392,7 @@ async function writeAgentTools(root, scope, permissions) {
39315
39392
 
39316
39393
  // src/core/packages.ts
39317
39394
  import { createHash as createHash4 } from "crypto";
39318
- import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir5, rm as rm4, stat as stat3 } from "fs/promises";
39395
+ import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir6, rm as rm5, stat as stat3 } from "fs/promises";
39319
39396
  import { dirname as dirname9, join as join24, relative } from "path";
39320
39397
  var AGENTS_REL2 = ".claude/agents";
39321
39398
  var SKILLS_REL2 = ".claude/skills";
@@ -39335,7 +39412,7 @@ async function listFilesRel(dir2) {
39335
39412
  async function walk2(d) {
39336
39413
  let entries2;
39337
39414
  try {
39338
- entries2 = await readdir5(d, { withFileTypes: true });
39415
+ entries2 = await readdir6(d, { withFileTypes: true });
39339
39416
  } catch {
39340
39417
  return;
39341
39418
  }
@@ -39488,11 +39565,11 @@ async function removePackage(root, slug) {
39488
39565
  const entry = manifest[n];
39489
39566
  if (!entry) return { ok: false, error: "not installed" };
39490
39567
  if (entry.kind === "subagent") {
39491
- await rm4(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
39492
- await rm4(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
39493
- await rm4(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
39568
+ await rm5(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
39569
+ await rm5(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
39570
+ await rm5(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
39494
39571
  } else {
39495
- await rm4(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
39572
+ await rm5(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
39496
39573
  }
39497
39574
  delete manifest[n];
39498
39575
  await writeManifest(root, manifest);
@@ -39503,7 +39580,7 @@ async function removePackage(root, slug) {
39503
39580
  }
39504
39581
 
39505
39582
  // src/core/graph.ts
39506
- import { readFile as readFile11, readdir as readdir6 } from "fs/promises";
39583
+ import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
39507
39584
  import { join as join25, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
39508
39585
  var MAX_FILES = 2e3;
39509
39586
  var MAX_NODES = 3e3;
@@ -39516,7 +39593,7 @@ async function walk(root, accept) {
39516
39593
  const dir2 = stack.pop();
39517
39594
  let entries2;
39518
39595
  try {
39519
- entries2 = await readdir6(dir2, { withFileTypes: true });
39596
+ entries2 = await readdir7(dir2, { withFileTypes: true });
39520
39597
  } catch {
39521
39598
  continue;
39522
39599
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.8.4b",
3
+ "version": "1.9.0b",
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",