@4pm/cli 0.3.1 → 0.3.2

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 +142 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3947,13 +3947,14 @@ async function runLink(serverUrl, explicitProfile) {
3947
3947
 
3948
3948
  // src/commands/start.ts
3949
3949
  import { spawn as spawn2 } from "child_process";
3950
+ import { join as join9 } from "path";
3950
3951
 
3951
3952
  // src/core/update.ts
3952
3953
  import { execSync as execSync2 } from "child_process";
3953
3954
  import { createHash as createHash2, createPublicKey, verify } from "crypto";
3954
- import { mkdtempSync, writeFileSync as writeFileSync3 } from "fs";
3955
+ import { mkdtempSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
3955
3956
  import { tmpdir } from "os";
3956
- import { dirname, join as join3, sep } from "path";
3957
+ import { dirname as dirname2, join as join4, sep } from "path";
3957
3958
 
3958
3959
  // src/config/signing.ts
3959
3960
  var CLI_SIGNING_PUBLIC_KEY = `
@@ -3963,7 +3964,19 @@ MCowBQYDK2VwAyEARlfzjaFhe5bhRrMp/ZuH6sVqg/S7f8B1dliuF21iDTw=
3963
3964
  `;
3964
3965
 
3965
3966
  // src/version.ts
3966
- var CLI_VERSION = "0.3.1";
3967
+ import { readFileSync as readFileSync4 } from "fs";
3968
+ import { dirname, join as join3 } from "path";
3969
+ import { fileURLToPath } from "url";
3970
+ function readCliVersion() {
3971
+ try {
3972
+ const here = dirname(fileURLToPath(import.meta.url));
3973
+ const pkg = JSON.parse(readFileSync4(join3(here, "..", "package.json"), "utf8"));
3974
+ return pkg.version ?? "0.0.0";
3975
+ } catch {
3976
+ return "0.0.0";
3977
+ }
3978
+ }
3979
+ var CLI_VERSION = readCliVersion();
3967
3980
 
3968
3981
  // src/core/update.ts
3969
3982
  function compareSemver(a, b) {
@@ -3978,6 +3991,14 @@ function compareSemver(a, b) {
3978
3991
  function installedViaNpm() {
3979
3992
  return (process.argv[1] ?? "").split(sep).includes("node_modules");
3980
3993
  }
3994
+ function readInstalledVersion() {
3995
+ try {
3996
+ const pkgPath = join4(dirname2(dirname2(process.argv[1] ?? ".")), "package.json");
3997
+ return JSON.parse(readFileSync5(pkgPath, "utf8")).version ?? "0.0.0";
3998
+ } catch {
3999
+ return "0.0.0";
4000
+ }
4001
+ }
3981
4002
  function updateViaNpm(version) {
3982
4003
  execSync2(`npm i -g @4pm/cli@${version}`, { stdio: "inherit" });
3983
4004
  }
@@ -4004,9 +4025,9 @@ async function updateViaDownload(tarballUrl, checksum, signature) {
4004
4025
  throw new Error("Invalid tarball signature \u2014 aborting update (ADR-0015).");
4005
4026
  }
4006
4027
  }
4007
- const tmp = join3(mkdtempSync(join3(tmpdir(), "4pm-update-")), "cli.tgz");
4028
+ const tmp = join4(mkdtempSync(join4(tmpdir(), "4pm-update-")), "cli.tgz");
4008
4029
  writeFileSync3(tmp, buf);
4009
- const installRoot = dirname(dirname(process.argv[1] ?? "."));
4030
+ const installRoot = dirname2(dirname2(process.argv[1] ?? "."));
4010
4031
  execSync2(`tar -xzf "${tmp}" -C "${installRoot}" --strip-components=1`, {
4011
4032
  stdio: "inherit"
4012
4033
  });
@@ -4033,6 +4054,7 @@ async function updateToLatest(serverUrl) {
4033
4054
  }
4034
4055
  }
4035
4056
  async function checkAndUpdate(serverUrl, autoUpdate) {
4057
+ if (CLI_VERSION.startsWith("0.0.0")) return { action: "none" };
4036
4058
  let meta;
4037
4059
  try {
4038
4060
  meta = await fetchCliVersion(serverUrl);
@@ -4062,7 +4084,6 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
4062
4084
  meta.source.signature
4063
4085
  );
4064
4086
  }
4065
- return { action: "updated", version: meta.latest };
4066
4087
  } catch (err) {
4067
4088
  if (mandatory) {
4068
4089
  console.error(`Update failed: ${err}`);
@@ -4071,6 +4092,17 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
4071
4092
  console.warn(`Update failed (skipping \u2014 not mandatory): ${err}`);
4072
4093
  return { action: "none" };
4073
4094
  }
4095
+ const installed = readInstalledVersion();
4096
+ if (compareSemver(installed, meta.latest) < 0) {
4097
+ const msg = `Update reported success but the installed version is still ${installed} (expected ${meta.latest}) \u2014 it did not apply to the running install (check the global npm prefix vs the running binary).`;
4098
+ if (mandatory) {
4099
+ console.error(msg);
4100
+ return { action: "blocked", minSupported: meta.minSupported };
4101
+ }
4102
+ console.warn(`${msg} Skipping restart.`);
4103
+ return { action: "none" };
4104
+ }
4105
+ return { action: "updated", version: meta.latest };
4074
4106
  }
4075
4107
 
4076
4108
  // src/core/ws-client.ts
@@ -4628,8 +4660,8 @@ ${JSON.stringify(req.spec)}`;
4628
4660
  }
4629
4661
 
4630
4662
  // src/core/command-history.ts
4631
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4632
- import { join as join4 } from "path";
4663
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
4664
+ import { join as join5 } from "path";
4633
4665
  var MAX_ENTRIES = 1e3;
4634
4666
  var filePath = null;
4635
4667
  var entries = [];
@@ -4691,10 +4723,10 @@ async function uploadHistory(cfg) {
4691
4723
  }
4692
4724
  }
4693
4725
  function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
4694
- filePath = join4(profileDir2, "command-history.json");
4726
+ filePath = join5(profileDir2, "command-history.json");
4695
4727
  if (existsSync4(filePath)) {
4696
4728
  try {
4697
- entries = JSON.parse(readFileSync4(filePath, "utf8"));
4729
+ entries = JSON.parse(readFileSync6(filePath, "utf8"));
4698
4730
  } catch {
4699
4731
  entries = [];
4700
4732
  }
@@ -4767,12 +4799,12 @@ async function readWorkerFile(path) {
4767
4799
  // src/core/git-diff.ts
4768
4800
  import { readFile as readFile2 } from "fs/promises";
4769
4801
  import { execFile as execFile2 } from "child_process";
4770
- import { basename, dirname as dirname2, resolve as resolve3 } from "path";
4802
+ import { basename, dirname as dirname3, resolve as resolve3 } from "path";
4771
4803
  import { promisify as promisify2 } from "util";
4772
4804
  var run2 = promisify2(execFile2);
4773
4805
  async function gitDiff(path) {
4774
4806
  const target = resolve3(path);
4775
- const dir = dirname2(target);
4807
+ const dir = dirname3(target);
4776
4808
  const base = basename(target);
4777
4809
  let newContent = "";
4778
4810
  try {
@@ -4826,13 +4858,13 @@ ${stderr}`;
4826
4858
  // src/core/scaffold.ts
4827
4859
  import { cp, mkdir, writeFile } from "fs/promises";
4828
4860
  import { execFile as execFile4 } from "child_process";
4829
- import { dirname as dirname3, join as join5, resolve as resolve4 } from "path";
4830
- import { fileURLToPath } from "url";
4861
+ import { dirname as dirname4, join as join6, resolve as resolve4 } from "path";
4862
+ import { fileURLToPath as fileURLToPath2 } from "url";
4831
4863
  import { promisify as promisify4 } from "util";
4832
4864
  var run4 = promisify4(execFile4);
4833
4865
  function sampleDir() {
4834
4866
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
4835
- const here = dirname3(fileURLToPath(import.meta.url));
4867
+ const here = dirname4(fileURLToPath2(import.meta.url));
4836
4868
  return resolve4(here, "../../project-sample");
4837
4869
  }
4838
4870
  async function scaffoldProject(payload, onProgress) {
@@ -4845,7 +4877,7 @@ async function scaffoldProject(payload, onProgress) {
4845
4877
  if (payload.spec) {
4846
4878
  emit("spec", "Writing project.spec.json\u2026");
4847
4879
  await writeFile(
4848
- join5(target, "project.spec.json"),
4880
+ join6(target, "project.spec.json"),
4849
4881
  JSON.stringify(payload.spec, null, 2),
4850
4882
  "utf8"
4851
4883
  );
@@ -4875,6 +4907,72 @@ async function addProject(payload, onProgress) {
4875
4907
  }
4876
4908
  }
4877
4909
 
4910
+ // src/common/logger/logger.ts
4911
+ import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
4912
+ import { join as join7 } from "path";
4913
+ var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
4914
+ var DAY_MS = 24 * 60 * 60 * 1e3;
4915
+ var DEFAULT_RETENTION_DAYS = 14;
4916
+ var CliLogger = class {
4917
+ dir = null;
4918
+ minLevel = "info";
4919
+ base = null;
4920
+ retentionDays = DEFAULT_RETENTION_DAYS;
4921
+ queue = Promise.resolve();
4922
+ /** Wire the profile log dir + level + base context; runs a retention sweep. */
4923
+ configure(opts) {
4924
+ this.dir = opts.dir;
4925
+ this.minLevel = opts.level;
4926
+ this.base = opts.base;
4927
+ this.retentionDays = opts.retentionDays ?? DEFAULT_RETENTION_DAYS;
4928
+ void this.cleanupOldLogs();
4929
+ }
4930
+ /** Log an error-level event. */
4931
+ error(event, fields) {
4932
+ this.write("error", event, fields);
4933
+ }
4934
+ /** Log a warn-level event. */
4935
+ warn(event, fields) {
4936
+ this.write("warn", event, fields);
4937
+ }
4938
+ /** Log an info-level event. */
4939
+ info(event, fields) {
4940
+ this.write("info", event, fields);
4941
+ }
4942
+ /** Log a debug-level event. */
4943
+ debug(event, fields) {
4944
+ this.write("debug", event, fields);
4945
+ }
4946
+ /** Serialize one JSONL line into today's file (no-op until configured / below level). */
4947
+ write(level, event, fields) {
4948
+ const dir = this.dir;
4949
+ if (!dir || !this.base) return;
4950
+ if (LEVEL_ORDER[level] > LEVEL_ORDER[this.minLevel]) return;
4951
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4952
+ const entry = { timestamp, level, ...this.base, event, ...fields };
4953
+ const line = JSON.stringify(entry) + "\n";
4954
+ const path = join7(dir, `cli-${timestamp.slice(0, 10)}.jsonl`);
4955
+ this.queue = this.queue.then(() => mkdir2(dir, { recursive: true })).then(() => appendFile(path, line, "utf8")).catch((err) => {
4956
+ console.warn(`Failed to write cli log: ${err}`);
4957
+ });
4958
+ }
4959
+ /** Delete cli log files older than the retention window (best-effort). */
4960
+ async cleanupOldLogs() {
4961
+ const dir = this.dir;
4962
+ if (!dir) return;
4963
+ const files = await readdir2(dir).catch(() => []);
4964
+ const cutoff = Date.now() - this.retentionDays * DAY_MS;
4965
+ for (const file of files) {
4966
+ const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
4967
+ if (!match) continue;
4968
+ if ((/* @__PURE__ */ new Date(`${match[1]}T00:00:00Z`)).getTime() < cutoff) {
4969
+ await unlink(join7(dir, file)).catch(() => void 0);
4970
+ }
4971
+ }
4972
+ }
4973
+ };
4974
+ var logger = new CliLogger();
4975
+
4878
4976
  // src/core/ws-client.ts
4879
4977
  var HEARTBEAT_MS = 3e4;
4880
4978
  var MAX_HASHCODE_INVALID_RETRIES = 5;
@@ -4931,6 +5029,7 @@ var WsClient = class {
4931
5029
  }
4932
5030
  if (this.stopped) return;
4933
5031
  const wait = backoffJitterMs(Math.min(attempt, 6), 1e3, 6e4);
5032
+ logger.info("ws.reconnect", { attempt, waitMs: wait });
4934
5033
  console.log(`Reconnecting in ${Math.round(wait / 1e3)}s\u2026`);
4935
5034
  await sleep(wait);
4936
5035
  }
@@ -4969,6 +5068,7 @@ var WsClient = class {
4969
5068
  socket.on("close", (code) => {
4970
5069
  this.clearHeartbeat();
4971
5070
  this.sessionKey = null;
5071
+ logger.debug("ws.close", { code });
4972
5072
  if (code === CLOSE_DRAINING) {
4973
5073
  console.log("Server draining \u2014 will reconnect (backoff + jitter).");
4974
5074
  }
@@ -4988,6 +5088,7 @@ var WsClient = class {
4988
5088
  if ("type" in message) {
4989
5089
  if (message.type === "hello_ack" && this.ecdh) {
4990
5090
  this.sessionKey = deriveSessionKey(this.ecdh, message.pubkey);
5091
+ logger.info("ws.connected", { version: CLI_VERSION });
4991
5092
  console.log("\u2714 Connected to the server (session encryption ready).");
4992
5093
  this.sendMachineStatus();
4993
5094
  this.flushOfflineBuffer();
@@ -4995,6 +5096,7 @@ var WsClient = class {
4995
5096
  return;
4996
5097
  }
4997
5098
  if (message.type === "error") {
5099
+ logger.warn("ws.error", { errorCode: message.errorCode });
4998
5100
  if (["LINK_REVOKED", "HASHCODE_EXPIRED"].includes(message.errorCode)) {
4999
5101
  this.logout(message.errorCode);
5000
5102
  return;
@@ -5037,6 +5139,10 @@ var WsClient = class {
5037
5139
  switch (message.channel) {
5038
5140
  case WsChannels.COMMAND_DISPATCH: {
5039
5141
  const dispatch = payload;
5142
+ logger.info("command.dispatch", {
5143
+ commandId: dispatch.commandId,
5144
+ cmd: dispatch.cmd
5145
+ });
5040
5146
  recordCommand({
5041
5147
  commandId: dispatch.commandId,
5042
5148
  projectId: dispatch.projectId,
@@ -5211,16 +5317,17 @@ var WsClient = class {
5211
5317
  this.stopped = true;
5212
5318
  this.clearHeartbeat();
5213
5319
  this.socket?.close();
5320
+ logger.warn("ws.logout", { reason });
5214
5321
  deleteCredential(this.context.profileDir);
5215
5322
  console.error(`Link is no longer valid (${reason}) \u2014 deleted .cre. Run \`4pm link\` to pair again.`);
5216
5323
  }
5217
5324
  };
5218
5325
 
5219
5326
  // src/core/instance-lock.ts
5220
- import { readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
5221
- import { join as join6 } from "path";
5327
+ import { readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
5328
+ import { join as join8 } from "path";
5222
5329
  function lockPath(profileDir2) {
5223
- return join6(profileDir2, "start.lock");
5330
+ return join8(profileDir2, "start.lock");
5224
5331
  }
5225
5332
  function isAlive(pid) {
5226
5333
  try {
@@ -5232,7 +5339,7 @@ function isAlive(pid) {
5232
5339
  }
5233
5340
  function lockHolder(profileDir2) {
5234
5341
  try {
5235
- const pid = Number.parseInt(readFileSync5(lockPath(profileDir2), "utf8").trim(), 10);
5342
+ const pid = Number.parseInt(readFileSync7(lockPath(profileDir2), "utf8").trim(), 10);
5236
5343
  if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) return pid;
5237
5344
  } catch {
5238
5345
  }
@@ -5245,13 +5352,19 @@ function acquireInstanceLock(profileDir2) {
5245
5352
  }
5246
5353
  function releaseInstanceLock(profileDir2) {
5247
5354
  try {
5248
- const pid = Number.parseInt(readFileSync5(lockPath(profileDir2), "utf8").trim(), 10);
5355
+ const pid = Number.parseInt(readFileSync7(lockPath(profileDir2), "utf8").trim(), 10);
5249
5356
  if (pid === process.pid) rmSync3(lockPath(profileDir2), { force: true });
5250
5357
  } catch {
5251
5358
  }
5252
5359
  }
5253
5360
 
5254
5361
  // src/commands/start.ts
5362
+ function resolveLogLevel() {
5363
+ if (process.argv.includes("--verbose")) return "debug";
5364
+ const env = process.env.FOURPM_LOG_LEVEL;
5365
+ if (env === "error" || env === "warn" || env === "info" || env === "debug") return env;
5366
+ return "info";
5367
+ }
5255
5368
  async function runStart(profileDir2, profileName) {
5256
5369
  const credential = readCredential(profileDir2, profileName);
5257
5370
  if (!credential) {
@@ -5261,10 +5374,17 @@ async function runStart(profileDir2, profileName) {
5261
5374
  process.exitCode = 1;
5262
5375
  return;
5263
5376
  }
5377
+ logger.configure({
5378
+ dir: join9(profileDir2, "logs"),
5379
+ level: resolveLogLevel(),
5380
+ base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
5381
+ });
5382
+ logger.info("cli.start", { serverUrl: credential.serverUrl });
5264
5383
  const config = readProfileConfig(profileDir2);
5265
5384
  const autoUpdate = config.autoUpdate !== false && process.env.FOURPM_NO_UPDATE !== "1";
5266
5385
  const result = await checkAndUpdate(credential.serverUrl, autoUpdate);
5267
5386
  if (result.action === "blocked") {
5387
+ logger.error("update.blocked", { minSupported: result.minSupported });
5268
5388
  console.error(
5269
5389
  `CLI is older than minSupported (${result.minSupported}) and the update failed \u2014 cannot connect.`
5270
5390
  );
@@ -5272,6 +5392,7 @@ async function runStart(profileDir2, profileName) {
5272
5392
  return;
5273
5393
  }
5274
5394
  if (result.action === "updated") {
5395
+ logger.info("update.updated", { version: result.version });
5275
5396
  console.log("Restarting with the new version\u2026");
5276
5397
  const child = spawn2(process.execPath, process.argv.slice(1), {
5277
5398
  stdio: "inherit",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "type": "module",