@4pm/cli 1.8.4-b → 1.9.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 +92 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -33965,6 +33965,14 @@ var issueProvisioningTokenRequestSchema = external_exports.object({
33965
33965
  /** TTL seconds (0/absent ⇒ a default short TTL applied server-side). */
33966
33966
  ttlSec: external_exports.number().int().min(0).max(2592e3).optional()
33967
33967
  });
33968
+ var provisionRequestSchema = external_exports.object({
33969
+ mode: external_exports.enum(["sync", "force"]).optional().default("sync")
33970
+ });
33971
+ var provisionForUserRequestSchema = external_exports.object({
33972
+ userId: external_exports.string().uuid(),
33973
+ projectId: external_exports.string().uuid(),
33974
+ mode: external_exports.enum(["sync", "force"]).optional().default("sync")
33975
+ });
33968
33976
  var listWorkersQuerySchema = baseRequestSchema.extend({
33969
33977
  status: external_exports.enum(["online", "offline"]).optional(),
33970
33978
  /** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
@@ -34240,7 +34248,13 @@ var repoSpecSchema = external_exports.object({
34240
34248
  /** Clone url — `https` or `ssh` (required; ADR-0172). */
34241
34249
  url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
34242
34250
  /** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
34243
- subdir: external_exports.string().max(120).optional().default("")
34251
+ subdir: external_exports.string().max(120).optional().default(""),
34252
+ /**
34253
+ * Primary branch to clone / check out (ADR-0292). Empty ⇒ the repo's default branch. The
34254
+ * worker clones with `-b <branch>` and the on-demand "update repos" action checks it out +
34255
+ * fast-forward pulls it.
34256
+ */
34257
+ branch: external_exports.string().max(200).optional().default("")
34244
34258
  });
34245
34259
  function repoName(url) {
34246
34260
  const last = (url || "").trim().replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
@@ -34368,7 +34382,7 @@ var activityHeartbeatSchema = external_exports.object({
34368
34382
  });
34369
34383
 
34370
34384
  // src/core/scaffold.ts
34371
- import { cp, mkdir, writeFile } from "fs/promises";
34385
+ import { cp, mkdir, readdir, rename, rm, writeFile } from "fs/promises";
34372
34386
  import { existsSync as existsSync4 } from "fs";
34373
34387
  import { execFile as execFile2 } from "child_process";
34374
34388
  import { dirname as dirname3, join as join6, resolve } from "path";
@@ -34457,16 +34471,47 @@ function subDirName(repo) {
34457
34471
  const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
34458
34472
  return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
34459
34473
  }
34474
+ function cloneArgs(url, dest, branch) {
34475
+ const b = (branch ?? "").trim();
34476
+ return b ? ["clone", "-b", b, url, dest] : ["clone", url, dest];
34477
+ }
34478
+ async function updateRepo(dir2, label, branch, emit) {
34479
+ const b = (branch ?? "").trim();
34480
+ emit("git", `Updating ${label} (fetch + ${b ? `checkout ${b} + ` : ""}fast-forward pull)\u2026`);
34481
+ await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
34482
+ if (b) await run2("git", ["checkout", b], { cwd: dir2, timeout: 6e4 });
34483
+ await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
34484
+ }
34460
34485
  async function provisionRepos(target, repos, emit, opts = {}) {
34486
+ const mode = opts.mode ?? "clone";
34487
+ const subDirsToKeep = new Set(repos.filter((r) => !r.primary && r.url).map((r) => subDirName(r)));
34461
34488
  for (const repo of repos) {
34462
34489
  if (repo.primary) {
34463
34490
  if (opts.clonePrimary && repo.url) {
34464
34491
  if (existsSync4(join6(target, ".git"))) {
34465
- emit("git", `Primary repo already present \u2014 skipping clone.`);
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
+ }
34466
34511
  continue;
34467
34512
  }
34468
34513
  emit("git", `Cloning ${repo.url}\u2026`);
34469
- await run2("git", ["clone", repo.url, target], { timeout: 12e4 });
34514
+ await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
34470
34515
  continue;
34471
34516
  }
34472
34517
  emit("git", "Initializing the primary repo\u2026");
@@ -34481,11 +34526,19 @@ async function provisionRepos(target, repos, emit, opts = {}) {
34481
34526
  const dir2 = join6(target, subDirName(repo));
34482
34527
  if (repo.url) {
34483
34528
  if (existsSync4(join6(dir2, ".git"))) {
34484
- emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
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
+ }
34485
34538
  continue;
34486
34539
  }
34487
34540
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
34488
- await run2("git", ["clone", repo.url, dir2], { timeout: 12e4 });
34541
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34489
34542
  } else {
34490
34543
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
34491
34544
  await mkdir(dir2, { recursive: true });
@@ -34590,11 +34643,14 @@ async function addProject(payload, profileDir2, onProgress) {
34590
34643
  const target = join6(root, payload.projectName);
34591
34644
  await mkdir(target, { recursive: true });
34592
34645
  const repos = payload.repos ?? [];
34593
- if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
34646
+ const mode = payload.mode ?? "clone";
34647
+ if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true, mode });
34594
34648
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
34595
34649
  return { ok: true, path: target };
34596
34650
  } catch (err) {
34597
- return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
34651
+ const message = err instanceof Error ? err.message : String(err);
34652
+ onProgress?.({ projectId: payload.projectId, step: "error", message: `Failed: ${message}`, done: true });
34653
+ return { ok: false, error: message, step: lastStep };
34598
34654
  }
34599
34655
  }
34600
34656
 
@@ -35082,7 +35138,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
35082
35138
  import { spawn as spawn2 } from "child_process";
35083
35139
 
35084
35140
  // src/common/logger/logger.ts
35085
- import { appendFile, mkdir as mkdir2, readdir, unlink } from "fs/promises";
35141
+ import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
35086
35142
  import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
35087
35143
  import { join as join7 } from "path";
35088
35144
  var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
@@ -35142,7 +35198,7 @@ var CliLogger = class {
35142
35198
  async cleanupOldLogs() {
35143
35199
  const dir2 = this.dir;
35144
35200
  if (!dir2) return;
35145
- const files = await readdir(dir2).catch(() => []);
35201
+ const files = await readdir2(dir2).catch(() => []);
35146
35202
  const cutoff = Date.now() - this.retentionDays * DAY_MS;
35147
35203
  for (const file of files) {
35148
35204
  const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
@@ -38123,7 +38179,7 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
38123
38179
  }
38124
38180
 
38125
38181
  // src/core/fs-browse.ts
38126
- import { readdir as readdir2 } from "fs/promises";
38182
+ import { readdir as readdir3 } from "fs/promises";
38127
38183
  import { resolve as resolve2, sep as sep2 } from "path";
38128
38184
  async function listDir(path, root) {
38129
38185
  if (!root) return { path: "", entries: [] };
@@ -38131,7 +38187,7 @@ async function listDir(path, root) {
38131
38187
  const requested = resolve2(base, path && path.trim() ? path : ".");
38132
38188
  const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
38133
38189
  try {
38134
- const dirents = await readdir2(target, { withFileTypes: true });
38190
+ const dirents = await readdir3(target, { withFileTypes: true });
38135
38191
  const entries2 = dirents.map((d) => ({
38136
38192
  name: d.name,
38137
38193
  type: d.isDirectory() ? "dir" : "file"
@@ -38199,7 +38255,7 @@ async function writeWorkerFile(root, relPath, content) {
38199
38255
  }
38200
38256
 
38201
38257
  // src/core/fs-mutate.ts
38202
- import { mkdir as mkdir4, writeFile as writeFile4, rename, rm, stat } from "fs/promises";
38258
+ import { mkdir as mkdir4, writeFile as writeFile4, rename as rename2, rm as rm2, stat } from "fs/promises";
38203
38259
  import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
38204
38260
  var MAX_BYTES3 = 2 * 1024 * 1024;
38205
38261
  function resolveInRoot3(root, rel) {
@@ -38254,14 +38310,14 @@ async function mutateFs(root, req) {
38254
38310
  if (to !== resolve5(root) && !to.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
38255
38311
  if (await exists(to)) return { ok: false, path: to, error: "destination already exists" };
38256
38312
  await mkdir4(dirname5(to), { recursive: true });
38257
- await rename(from, to);
38313
+ await rename2(from, to);
38258
38314
  return { ok: true, path: to };
38259
38315
  }
38260
38316
  case "delete": {
38261
38317
  const target = resolveInRoot3(root, req.path ?? "");
38262
38318
  if (!target) return escaped(req.path ?? "");
38263
38319
  if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
38264
- await rm(target, { recursive: true, force: true });
38320
+ await rm2(target, { recursive: true, force: true });
38265
38321
  return { ok: true, path: target };
38266
38322
  }
38267
38323
  default:
@@ -38486,7 +38542,7 @@ ${stderr}`;
38486
38542
 
38487
38543
  // src/core/git-ssh-key.ts
38488
38544
  import { execFile as execFile8 } from "child_process";
38489
- import { readFile as readFile6, rm as rm2, mkdir as mkdir6 } from "fs/promises";
38545
+ import { readFile as readFile6, rm as rm3, mkdir as mkdir6 } from "fs/promises";
38490
38546
  import { join as join18 } from "path";
38491
38547
  import { promisify as promisify8 } from "util";
38492
38548
  var run8 = promisify8(execFile8);
@@ -38520,8 +38576,8 @@ async function configureGit(key) {
38520
38576
  async function generate(profileDir2) {
38521
38577
  const { dir: dir2, key, pub } = keyPaths(profileDir2);
38522
38578
  await mkdir6(dir2, { recursive: true });
38523
- await rm2(key, { force: true });
38524
- await rm2(pub, { force: true });
38579
+ await rm3(key, { force: true });
38580
+ await rm3(pub, { force: true });
38525
38581
  await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
38526
38582
  timeout: 15e3
38527
38583
  });
@@ -38530,8 +38586,8 @@ async function generate(profileDir2) {
38530
38586
  }
38531
38587
  async function remove(profileDir2) {
38532
38588
  const { key, pub } = keyPaths(profileDir2);
38533
- await rm2(key, { force: true });
38534
- await rm2(pub, { force: true });
38589
+ await rm3(key, { force: true });
38590
+ await rm3(pub, { force: true });
38535
38591
  await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
38536
38592
  () => void 0
38537
38593
  );
@@ -38553,7 +38609,7 @@ async function manageSshKey(op, profileDir2) {
38553
38609
  }
38554
38610
 
38555
38611
  // src/core/git-history.ts
38556
- import { readdir as readdir3 } from "fs/promises";
38612
+ import { readdir as readdir4 } from "fs/promises";
38557
38613
  import { execFile as execFile9 } from "child_process";
38558
38614
  import { resolve as resolve8, sep as sep8 } from "path";
38559
38615
  import { promisify as promisify9 } from "util";
@@ -38594,13 +38650,13 @@ async function gitRepos(root) {
38594
38650
  };
38595
38651
  await add(base);
38596
38652
  try {
38597
- const lvl1 = await readdir3(base, { withFileTypes: true });
38653
+ const lvl1 = await readdir4(base, { withFileTypes: true });
38598
38654
  for (const d of lvl1) {
38599
38655
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
38600
38656
  const p1 = resolve8(base, d.name);
38601
38657
  await add(p1);
38602
38658
  try {
38603
- const lvl2 = await readdir3(p1, { withFileTypes: true });
38659
+ const lvl2 = await readdir4(p1, { withFileTypes: true });
38604
38660
  for (const d2 of lvl2) {
38605
38661
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
38606
38662
  await add(resolve8(p1, d2.name));
@@ -39130,7 +39186,7 @@ function handleToolsChannels(ctx, message, payload) {
39130
39186
  }
39131
39187
 
39132
39188
  // src/core/agents.ts
39133
- import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir4, rm as rm3 } from "fs/promises";
39189
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir5, rm as rm4 } from "fs/promises";
39134
39190
  import { join as join21 } from "path";
39135
39191
  var AGENTS_REL = ".claude/agents";
39136
39192
  var SKILLS_REL = ".claude/skills";
@@ -39155,7 +39211,7 @@ async function listAgents(root) {
39155
39211
  const subagents = [];
39156
39212
  const skills = [];
39157
39213
  try {
39158
- for (const e of await readdir4(join21(root, AGENTS_REL), { withFileTypes: true })) {
39214
+ for (const e of await readdir5(join21(root, AGENTS_REL), { withFileTypes: true })) {
39159
39215
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
39160
39216
  const fm = frontmatter(await readText2(join21(root, AGENTS_REL, e.name)));
39161
39217
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
@@ -39163,7 +39219,7 @@ async function listAgents(root) {
39163
39219
  } catch {
39164
39220
  }
39165
39221
  try {
39166
- for (const e of await readdir4(join21(root, SKILLS_REL), { withFileTypes: true })) {
39222
+ for (const e of await readdir5(join21(root, SKILLS_REL), { withFileTypes: true })) {
39167
39223
  if (!e.isDirectory()) continue;
39168
39224
  const fm = frontmatter(await readText2(join21(root, SKILLS_REL, e.name, "SKILL.md")));
39169
39225
  skills.push({ name: e.name, description: fm.description });
@@ -39193,8 +39249,8 @@ async function writeAgent(root, req) {
39193
39249
  if (!n) return { ok: false, error: "invalid name" };
39194
39250
  try {
39195
39251
  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 });
39252
+ if (req.kind === "subagent") await rm4(itemPath(root, "subagent", n), { force: true });
39253
+ else await rm4(join21(root, SKILLS_REL, n), { recursive: true, force: true });
39198
39254
  return { ok: true };
39199
39255
  }
39200
39256
  if (req.content === void 0) return { ok: false, error: "content is required" };
@@ -39315,7 +39371,7 @@ async function writeAgentTools(root, scope, permissions) {
39315
39371
 
39316
39372
  // src/core/packages.ts
39317
39373
  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";
39374
+ import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir6, rm as rm5, stat as stat3 } from "fs/promises";
39319
39375
  import { dirname as dirname9, join as join24, relative } from "path";
39320
39376
  var AGENTS_REL2 = ".claude/agents";
39321
39377
  var SKILLS_REL2 = ".claude/skills";
@@ -39335,7 +39391,7 @@ async function listFilesRel(dir2) {
39335
39391
  async function walk2(d) {
39336
39392
  let entries2;
39337
39393
  try {
39338
- entries2 = await readdir5(d, { withFileTypes: true });
39394
+ entries2 = await readdir6(d, { withFileTypes: true });
39339
39395
  } catch {
39340
39396
  return;
39341
39397
  }
@@ -39488,11 +39544,11 @@ async function removePackage(root, slug) {
39488
39544
  const entry = manifest[n];
39489
39545
  if (!entry) return { ok: false, error: "not installed" };
39490
39546
  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 });
39547
+ await rm5(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
39548
+ await rm5(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
39549
+ await rm5(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
39494
39550
  } else {
39495
- await rm4(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
39551
+ await rm5(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
39496
39552
  }
39497
39553
  delete manifest[n];
39498
39554
  await writeManifest(root, manifest);
@@ -39503,7 +39559,7 @@ async function removePackage(root, slug) {
39503
39559
  }
39504
39560
 
39505
39561
  // src/core/graph.ts
39506
- import { readFile as readFile11, readdir as readdir6 } from "fs/promises";
39562
+ import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
39507
39563
  import { join as join25, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
39508
39564
  var MAX_FILES = 2e3;
39509
39565
  var MAX_NODES = 3e3;
@@ -39516,7 +39572,7 @@ async function walk(root, accept) {
39516
39572
  const dir2 = stack.pop();
39517
39573
  let entries2;
39518
39574
  try {
39519
- entries2 = await readdir6(dir2, { withFileTypes: true });
39575
+ entries2 = await readdir7(dir2, { withFileTypes: true });
39520
39576
  } catch {
39521
39577
  continue;
39522
39578
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.8.4b",
3
+ "version": "1.9.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",