@4pm/cli 0.2.0 → 0.2.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 (3) hide show
  1. package/README.md +11 -8
  2. package/dist/index.js +219 -65
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # @4pm/cli
2
2
 
3
- CLI chạy trên máy worker: nhận lệnh từ server và điều phối các CLI AI
4
- (claude / codex …). Node ESM + TypeScript.
3
+ **Agent chạy trên máy worker** — nhận lệnh từ `@4pm/server` qua WebSocket và điều phối
4
+ các CLI AI (`claude` / `codex`) cùng công cụ khác (`git`, `gh`, `glab`, `shell`).
5
+ Node ESM + TypeScript.
5
6
 
6
- Logic điều phối ở `src/core/`, subcommand ở `src/commands/`, giao tiếp server qua
7
- `src/services/` (dùng `@4pm/sdk`). Phần dùng chung tách vào `src/common/` (errors,
8
- logger, io); hàm thuần để ở `src/utils/`.
7
+ Vòng đời: **pairing** với server bằng hashcode (file `.cre`) → xin `ws_token` → mở
8
+ WebSocket TLS → nhận lệnh → **spawn tiến trình con**, gom output (backpressure) trả về
9
+ server. Nhiều instance trên một máy dùng profile riêng, server gom về một **worker**.
9
10
 
10
- > Cây thư mục: xem `README.md` (gốc) § Cấu trúc thư mục.
11
+ ```bash
12
+ pnpm --filter @4pm/cli dev
13
+ ```
11
14
 
12
- Còn thiếu (thêm khi bắt đầu phát triển): `tsconfig.json`, cấu hình ESLint,
13
- dependencies (vd `tsx`, thư viện parse CLI).
15
+ > Tổ chức mã, quy ước & phần còn thiếu: [`CLAUDE.md`](./CLAUDE.md).
16
+ > Đặc tả hành vi cli: [`11-docs/61-cli/`](../../11-docs/61-cli/README.md).
package/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __createRequire } from 'node:module';
3
+ import { fileURLToPath as __fileURLToPath } from 'node:url';
4
+ import { dirname as __pathDirname } from 'node:path';
5
+ const require = __createRequire(import.meta.url);
6
+ const __filename = __fileURLToPath(import.meta.url);
7
+ const __dirname = __pathDirname(__filename);
2
8
  var __create = Object.create;
3
9
  var __defProp = Object.defineProperty;
4
10
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -3735,7 +3741,7 @@ function resolveProfile(args) {
3735
3741
  if (index >= 0) {
3736
3742
  const name = args[index + 1];
3737
3743
  if (!name || name.startsWith("--")) {
3738
- throw new Error("--profile c\u1EA7n k\xE8m t\xEAn profile.");
3744
+ throw new Error("--profile requires a profile name.");
3739
3745
  }
3740
3746
  const rest = [...args.slice(0, index), ...args.slice(index + 2)];
3741
3747
  return [name, rest];
@@ -3881,17 +3887,17 @@ async function fetchCliVersion(serverUrl) {
3881
3887
  async function runLink(serverUrl, profileDir2, profileName) {
3882
3888
  if (readCredential(profileDir2, profileName)) {
3883
3889
  console.log(
3884
- `Profile "${profileName}" \u0111\xE3 c\xF3 li\xEAn k\u1EBFt (.cre). Ch\u1EA1y \`4pm unlink --profile ${profileName}\` tr\u01B0\u1EDBc n\u1EBFu mu\u1ED1n pair l\u1EA1i.`
3890
+ `Profile "${profileName}" is already linked (.cre). Run \`4pm unlink --profile ${profileName}\` first if you want to pair again.`
3885
3891
  );
3886
3892
  return;
3887
3893
  }
3888
3894
  const hashcode1 = randomBytes(32).toString("hex");
3889
3895
  console.log(`\u2500\u2500 4PM cli pairing (profile: ${profileName}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
3890
- console.log("1. M\u1EDF web 4PM \u2192 /machines, nh\u1EADp hashcode (1):");
3896
+ console.log("1. Open the 4PM web \u2192 /machines, enter hashcode (1):");
3891
3897
  console.log(`
3892
3898
  ${hashcode1}
3893
3899
  `);
3894
- console.log("2. Web s\u1EBD hi\u1EC3n th\u1ECB hashcode (2) \u2014 d\xE1n v\xE0o \u0111\xE2y.");
3900
+ console.log("2. The web will show hashcode (2) \u2014 paste it here.");
3895
3901
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3896
3902
  const hashcode2 = (await rl.question("hashcode (2): ")).trim();
3897
3903
  rl.close();
@@ -3900,10 +3906,10 @@ async function runLink(serverUrl, profileDir2, profileName) {
3900
3906
  serverUrl,
3901
3907
  hashcode3,
3902
3908
  scope: "unknown",
3903
- // scope do server quyết theo user pair (ADR-0010)
3909
+ // scope is decided by the server based on the pairing user (ADR-0010)
3904
3910
  pairedAt: (/* @__PURE__ */ new Date()).toISOString()
3905
3911
  });
3906
- console.log("\u2714 Li\xEAn k\u1EBFt th\xE0nh c\xF4ng \u2014 \u0111\xE3 l\u01B0u .cre. Ch\u1EA1y `4pm start` \u0111\u1EC3 k\u1EBFt n\u1ED1i.");
3912
+ console.log("\u2714 Linked successfully \u2014 saved .cre. Run `4pm start` to connect.");
3907
3913
  }
3908
3914
 
3909
3915
  // src/commands/start.ts
@@ -3924,7 +3930,7 @@ MCowBQYDK2VwAyEARlfzjaFhe5bhRrMp/ZuH6sVqg/S7f8B1dliuF21iDTw=
3924
3930
  `;
3925
3931
 
3926
3932
  // src/version.ts
3927
- var CLI_VERSION = "0.2.0";
3933
+ var CLI_VERSION = "0.2.2";
3928
3934
 
3929
3935
  // src/core/update.ts
3930
3936
  function compareSemver(a, b) {
@@ -3944,15 +3950,15 @@ function updateViaNpm(version) {
3944
3950
  }
3945
3951
  async function updateViaDownload(tarballUrl, checksum, signature) {
3946
3952
  const res = await fetch(tarballUrl);
3947
- if (!res.ok) throw new Error(`T\u1EA3i tarball l\u1ED7i: HTTP ${res.status}`);
3953
+ if (!res.ok) throw new Error(`Failed to download tarball: HTTP ${res.status}`);
3948
3954
  const buf = Buffer.from(await res.arrayBuffer());
3949
3955
  const actual = createHash2("sha256").update(buf).digest("hex");
3950
3956
  if (actual !== checksum) {
3951
- throw new Error("Checksum tarball kh\xF4ng kh\u1EDBp \u2014 h\u1EE7y update (ADR-0015).");
3957
+ throw new Error("Tarball checksum mismatch \u2014 aborting update (ADR-0015).");
3952
3958
  }
3953
3959
  if (CLI_SIGNING_PUBLIC_KEY) {
3954
3960
  if (!signature) {
3955
- throw new Error("Thi\u1EBFu ch\u1EEF k\xFD tarball \u2014 h\u1EE7y update (ADR-0015).");
3961
+ throw new Error("Missing tarball signature \u2014 aborting update (ADR-0015).");
3956
3962
  }
3957
3963
  const trimmed = CLI_SIGNING_PUBLIC_KEY.trim();
3958
3964
  const publicKey = trimmed.includes("BEGIN") ? createPublicKey(trimmed) : createPublicKey({
@@ -3962,7 +3968,7 @@ async function updateViaDownload(tarballUrl, checksum, signature) {
3962
3968
  });
3963
3969
  const ok = verify(null, buf, publicKey, Buffer.from(signature, "base64"));
3964
3970
  if (!ok) {
3965
- throw new Error("Ch\u1EEF k\xFD tarball kh\xF4ng h\u1EE3p l\u1EC7 \u2014 h\u1EE7y update (ADR-0015).");
3971
+ throw new Error("Invalid tarball signature \u2014 aborting update (ADR-0015).");
3966
3972
  }
3967
3973
  }
3968
3974
  const tmp = join3(mkdtempSync(join3(tmpdir(), "4pm-update-")), "cli.tgz");
@@ -3972,12 +3978,33 @@ async function updateViaDownload(tarballUrl, checksum, signature) {
3972
3978
  stdio: "inherit"
3973
3979
  });
3974
3980
  }
3981
+ async function updateToLatest(serverUrl) {
3982
+ const meta = await fetchCliVersion(serverUrl);
3983
+ if (compareSemver(CLI_VERSION, meta.latest) >= 0) {
3984
+ return { action: "already-latest", version: CLI_VERSION };
3985
+ }
3986
+ console.log(`Updating ${CLI_VERSION} \u2192 ${meta.latest}\u2026`);
3987
+ try {
3988
+ if (installedViaNpm()) {
3989
+ updateViaNpm(meta.latest);
3990
+ } else {
3991
+ await updateViaDownload(
3992
+ meta.source.tarballUrl,
3993
+ meta.source.checksum,
3994
+ meta.source.signature
3995
+ );
3996
+ }
3997
+ return { action: "updated", version: meta.latest };
3998
+ } catch (err) {
3999
+ return { action: "failed", error: String(err) };
4000
+ }
4001
+ }
3975
4002
  async function checkAndUpdate(serverUrl, autoUpdate) {
3976
4003
  let meta;
3977
4004
  try {
3978
4005
  meta = await fetchCliVersion(serverUrl);
3979
4006
  } catch (err) {
3980
- console.warn(`Kh\xF4ng check \u0111\u01B0\u1EE3c version cli: ${err}`);
4007
+ console.warn(`Could not check cli version: ${err}`);
3981
4008
  return { action: "none" };
3982
4009
  }
3983
4010
  const mandatory = compareSemver(CLI_VERSION, meta.minSupported) < 0;
@@ -3985,12 +4012,12 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
3985
4012
  if (!outdated) return { action: "none" };
3986
4013
  if (!mandatory && !autoUpdate) {
3987
4014
  console.warn(
3988
- `C\xF3 version m\u1EDBi ${meta.latest} (\u0111ang ch\u1EA1y ${CLI_VERSION}) \u2014 autoUpdate \u0111ang t\u1EAFt.`
4015
+ `A new version ${meta.latest} is available (running ${CLI_VERSION}) \u2014 autoUpdate is off.`
3989
4016
  );
3990
4017
  return { action: "none" };
3991
4018
  }
3992
4019
  console.log(
3993
- `${mandatory ? "B\u1EAFt bu\u1ED9c" : "T\u1EF1 \u0111\u1ED9ng"} update cli ${CLI_VERSION} \u2192 ${meta.latest}\u2026`
4020
+ `${mandatory ? "Mandatory" : "Automatic"} cli update ${CLI_VERSION} \u2192 ${meta.latest}\u2026`
3994
4021
  );
3995
4022
  try {
3996
4023
  if (installedViaNpm()) {
@@ -4005,10 +4032,10 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
4005
4032
  return { action: "updated", version: meta.latest };
4006
4033
  } catch (err) {
4007
4034
  if (mandatory) {
4008
- console.error(`Update th\u1EA5t b\u1EA1i: ${err}`);
4035
+ console.error(`Update failed: ${err}`);
4009
4036
  return { action: "blocked", minSupported: meta.minSupported };
4010
4037
  }
4011
- console.warn(`Update th\u1EA5t b\u1EA1i (b\u1ECF qua \u2014 kh\xF4ng b\u1EAFt bu\u1ED9c): ${err}`);
4038
+ console.warn(`Update failed (skipping \u2014 not mandatory): ${err}`);
4012
4039
  return { action: "none" };
4013
4040
  }
4014
4041
  }
@@ -4056,14 +4083,18 @@ var PermissionCode = {
4056
4083
  // org
4057
4084
  ORG_READ: "org.read",
4058
4085
  ORG_UPDATE: "org.update",
4086
+ // soft-delete the whole org + cancel its subscription (ADMIN only)
4087
+ ORG_DELETE: "org.delete",
4059
4088
  // user
4060
4089
  USER_READ_SELF: "user.read_self",
4061
4090
  USER_LIST: "user.list",
4062
4091
  USER_CREATE: "user.create",
4063
4092
  USER_READ: "user.read",
4064
4093
  USER_UPDATE: "user.update",
4094
+ // ADMIN resets a sub-account's password directly (user-0010)
4095
+ USER_SET_PASSWORD: "user.set_password",
4065
4096
  USER_DELETE: "user.delete",
4066
- // mời user qua email (ADR-0021)
4097
+ // invite users by email (ADR-0021)
4067
4098
  USER_INVITE: "user.invite",
4068
4099
  // team
4069
4100
  TEAM_LIST: "team.list",
@@ -4085,6 +4116,8 @@ var PermissionCode = {
4085
4116
  PROJECT_DETACH_TEAM: "project.detach_team",
4086
4117
  PROJECT_ATTACH_USER: "project.attach_user",
4087
4118
  PROJECT_DETACH_USER: "project.detach_user",
4119
+ PROJECT_ATTACH_MANAGER: "project.attach_manager",
4120
+ PROJECT_DETACH_MANAGER: "project.detach_manager",
4088
4121
  PROJECT_AI_ASSIST: "project.ai_assist",
4089
4122
  PROJECT_MEMO_READ: "project.memo_read",
4090
4123
  PROJECT_MEMO_WRITE: "project.memo_write",
@@ -4097,7 +4130,7 @@ var PermissionCode = {
4097
4130
  MACHINE_LIST: "machine.list",
4098
4131
  MACHINE_UPDATE: "machine.update",
4099
4132
  MACHINE_UNLINK: "machine.unlink",
4100
- // gán/gỡ physic project cho cli
4133
+ // assign/detach a cli's physic project
4101
4134
  MACHINE_PHYSIC_MANAGE: "machine.physic_manage",
4102
4135
  // command
4103
4136
  COMMAND_EXECUTE: "command.execute",
@@ -4112,16 +4145,21 @@ var PermissionCode = {
4112
4145
  BILLING_MANAGE: "billing.manage"
4113
4146
  };
4114
4147
  var ALL_PERMISSION_CODES = Object.values(PermissionCode);
4148
+ var GrantSubjectType = {
4149
+ USER: "user",
4150
+ TEAM: "team",
4151
+ PROJECT: "project"
4152
+ };
4115
4153
  var P = PermissionCode;
4116
4154
  var COMMON = [
4117
4155
  P.AUTH_LOGOUT,
4118
4156
  P.AUTH_CHANGE_PASSWORD,
4119
4157
  P.USER_READ_SELF,
4120
- // notification của chính mình
4158
+ // one's own notifications
4121
4159
  P.NOTIFICATION_READ
4122
4160
  ];
4123
4161
  var ROLE_DEFAULT_PERMISSIONS = {
4124
- // ADMIN: full quyền
4162
+ // ADMIN: full permissions
4125
4163
  [Role.ADMIN]: ALL_PERMISSION_CODES,
4126
4164
  [Role.PM]: [
4127
4165
  ...COMMON,
@@ -4150,6 +4188,8 @@ var ROLE_DEFAULT_PERMISSIONS = {
4150
4188
  P.PROJECT_DETACH_TEAM,
4151
4189
  P.PROJECT_ATTACH_USER,
4152
4190
  P.PROJECT_DETACH_USER,
4191
+ P.PROJECT_ATTACH_MANAGER,
4192
+ P.PROJECT_DETACH_MANAGER,
4153
4193
  P.PROJECT_AI_ASSIST,
4154
4194
  P.PROJECT_MEMO_READ,
4155
4195
  P.PROJECT_MEMO_WRITE,
@@ -4195,7 +4235,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
4195
4235
  P.PROJECT_READ,
4196
4236
  P.PROJECT_MEMO_READ,
4197
4237
  P.PROJECT_MEMO_WRITE,
4198
- // xem worker/cli của mình + setting physic project
4238
+ // view one's own worker/cli + configure the physic project
4199
4239
  P.MACHINE_LIST,
4200
4240
  P.MACHINE_PHYSIC_MANAGE,
4201
4241
  P.COMMAND_EXECUTE,
@@ -4210,8 +4250,70 @@ var PROJECT_SCOPE_CHECKED_CODES = [
4210
4250
  P.PROJECT_DETACH_TEAM,
4211
4251
  P.PROJECT_ATTACH_USER,
4212
4252
  P.PROJECT_DETACH_USER,
4253
+ P.PROJECT_ATTACH_MANAGER,
4254
+ P.PROJECT_DETACH_MANAGER,
4213
4255
  P.PROJECT_AI_ASSIST
4214
4256
  ];
4257
+ var SELF_SERVICE_CODES = [
4258
+ P.AUTH_LOGOUT,
4259
+ P.AUTH_CHANGE_PASSWORD,
4260
+ P.USER_READ_SELF,
4261
+ P.NOTIFICATION_READ
4262
+ ];
4263
+ var TEAM_GRANTABLE_CODES = [
4264
+ P.TEAM_READ,
4265
+ P.TEAM_UPDATE,
4266
+ P.TEAM_DELETE,
4267
+ P.TEAM_ATTACH_USER,
4268
+ P.TEAM_DETACH_USER,
4269
+ P.TEAM_ATTACH_PROJECT,
4270
+ P.TEAM_DETACH_PROJECT,
4271
+ P.PROJECT_READ,
4272
+ P.PROJECT_UPDATE,
4273
+ P.PROJECT_ATTACH_TEAM,
4274
+ P.PROJECT_DETACH_TEAM,
4275
+ P.PROJECT_ATTACH_USER,
4276
+ P.PROJECT_DETACH_USER,
4277
+ P.PROJECT_ATTACH_MANAGER,
4278
+ P.PROJECT_DETACH_MANAGER,
4279
+ P.PROJECT_AI_ASSIST,
4280
+ P.PROJECT_MEMO_READ,
4281
+ P.PROJECT_MEMO_WRITE,
4282
+ P.COMMAND_EXECUTE,
4283
+ P.COMMAND_READ,
4284
+ P.MACHINE_LIST,
4285
+ P.MACHINE_PHYSIC_MANAGE
4286
+ ];
4287
+ var PROJECT_GRANTABLE_CODES = [
4288
+ P.PROJECT_READ,
4289
+ P.PROJECT_UPDATE,
4290
+ P.PROJECT_DELETE,
4291
+ P.PROJECT_ATTACH_TEAM,
4292
+ P.PROJECT_DETACH_TEAM,
4293
+ P.PROJECT_ATTACH_USER,
4294
+ P.PROJECT_DETACH_USER,
4295
+ P.PROJECT_ATTACH_MANAGER,
4296
+ P.PROJECT_DETACH_MANAGER,
4297
+ P.PROJECT_AI_ASSIST,
4298
+ P.PROJECT_MEMO_READ,
4299
+ P.PROJECT_MEMO_WRITE,
4300
+ P.COMMAND_EXECUTE,
4301
+ P.COMMAND_READ,
4302
+ P.MACHINE_PHYSIC_MANAGE
4303
+ ];
4304
+ var SELF_SERVICE_SET = new Set(SELF_SERVICE_CODES);
4305
+ var TEAM_GRANTABLE_SET = new Set(TEAM_GRANTABLE_CODES);
4306
+ var PROJECT_GRANTABLE_SET = new Set(PROJECT_GRANTABLE_CODES);
4307
+ function grantSubjectsFor(code) {
4308
+ if (SELF_SERVICE_SET.has(code)) return [];
4309
+ const subjects = [GrantSubjectType.USER];
4310
+ if (TEAM_GRANTABLE_SET.has(code)) subjects.push(GrantSubjectType.TEAM);
4311
+ if (PROJECT_GRANTABLE_SET.has(code)) subjects.push(GrantSubjectType.PROJECT);
4312
+ return subjects;
4313
+ }
4314
+ var PERMISSION_GRANT_SUBJECTS = Object.fromEntries(
4315
+ ALL_PERMISSION_CODES.map((code) => [code, grantSubjectsFor(code)])
4316
+ );
4215
4317
 
4216
4318
  // ../../22-packages/11-shared/11-constants/src/auth.ts
4217
4319
  var ACCESS_TOKEN_TTL_SEC = 60 * 60;
@@ -4310,8 +4412,8 @@ var OutputBatcher = class {
4310
4412
  };
4311
4413
  }
4312
4414
  /**
4313
- * Nạp thêm output; flush khi đạt ngưỡng; áp backpressure/truncate khi
4314
- * vượt trần.
4415
+ * Push more output; flush at the threshold; apply backpressure/truncate when
4416
+ * over the cap.
4315
4417
  */
4316
4418
  push(chunk) {
4317
4419
  if (this.buffer.length >= this.opts.maxBufferKb * 1024) {
@@ -4335,7 +4437,7 @@ var OutputBatcher = class {
4335
4437
  }
4336
4438
  }
4337
4439
  /**
4338
- * Đẩy toàn bộ buffer hiện tại đi ngay (1 batch).
4440
+ * Flush the entire current buffer immediately (one batch).
4339
4441
  */
4340
4442
  flush() {
4341
4443
  if (this.timer) {
@@ -4399,7 +4501,7 @@ async function runCommand(dispatch, emit) {
4399
4501
  emit({
4400
4502
  commandId: dispatch.commandId,
4401
4503
  seq: seq++,
4402
- chunk: `spawn l\u1ED7i: ${err.message}
4504
+ chunk: `spawn error: ${err.message}
4403
4505
  `,
4404
4506
  done: true,
4405
4507
  exitCode: -1
@@ -4748,13 +4850,13 @@ var WsClient = class {
4748
4850
  heartbeat = null;
4749
4851
  stopped = false;
4750
4852
  credential;
4751
- /** Output đệm trong lúc mất kết nối (flush khi nối lại). */
4853
+ /** Output buffered while disconnected (flushed on reconnect). */
4752
4854
  offlineBuffer = [];
4753
4855
  offlineBufferBytes = 0;
4754
- /** Request cli → server đang chờ reply (quota.check — ADR-0020). */
4856
+ /** cli → server requests awaiting a reply (quota.check — ADR-0020). */
4755
4857
  pending = /* @__PURE__ */ new Map();
4756
4858
  /**
4757
- * Vòng đời chính: connect + reconnect backoff cho tới khi bị revoke.
4859
+ * Main lifecycle: connect + reconnect backoff until revoked.
4758
4860
  */
4759
4861
  async run() {
4760
4862
  for (let attempt = 0; !this.stopped; attempt++) {
@@ -4766,16 +4868,16 @@ var WsClient = class {
4766
4868
  this.logout(err.errorCode);
4767
4869
  return;
4768
4870
  }
4769
- console.error(`K\u1EBFt n\u1ED1i l\u1ED7i: ${err}`);
4871
+ console.error(`Connection error: ${err}`);
4770
4872
  }
4771
4873
  if (this.stopped) return;
4772
4874
  const wait = backoffJitterMs(Math.min(attempt, 6), 1e3, 6e4);
4773
- console.log(`K\u1EBFt n\u1ED1i l\u1EA1i sau ${Math.round(wait / 1e3)}s\u2026`);
4875
+ console.log(`Reconnecting in ${Math.round(wait / 1e3)}s\u2026`);
4774
4876
  await sleep(wait);
4775
4877
  }
4776
4878
  }
4777
4879
  /**
4778
- * 1 phiên WS: token → connect → handshake → xử lý message tới khi đóng.
4880
+ * One WS session: token → connect → handshake → handle messages until close.
4779
4881
  */
4780
4882
  connectOnce() {
4781
4883
  return new Promise((resolve5, reject) => {
@@ -4802,14 +4904,14 @@ var WsClient = class {
4802
4904
  try {
4803
4905
  this.handleMessage(JSON.parse(String(raw)));
4804
4906
  } catch (err) {
4805
- console.error(`Message l\u1ED7i: ${err}`);
4907
+ console.error(`Message error: ${err}`);
4806
4908
  }
4807
4909
  });
4808
4910
  socket.on("close", (code) => {
4809
4911
  this.clearHeartbeat();
4810
4912
  this.sessionKey = null;
4811
4913
  if (code === CLOSE_DRAINING) {
4812
- console.log("Server draining \u2014 s\u1EBD k\u1EBFt n\u1ED1i l\u1EA1i (backoff + jitter).");
4914
+ console.log("Server draining \u2014 will reconnect (backoff + jitter).");
4813
4915
  }
4814
4916
  resolve5();
4815
4917
  });
@@ -4821,13 +4923,13 @@ var WsClient = class {
4821
4923
  });
4822
4924
  }
4823
4925
  /**
4824
- * Xử lý control message (hello_ack/error) và envelope kênh.
4926
+ * Handle control messages (hello_ack/error) and channel envelopes.
4825
4927
  */
4826
4928
  handleMessage(message) {
4827
4929
  if ("type" in message) {
4828
4930
  if (message.type === "hello_ack" && this.ecdh) {
4829
4931
  this.sessionKey = deriveSessionKey(this.ecdh, message.pubkey);
4830
- console.log("\u2714 \u0110\xE3 k\u1EBFt n\u1ED1i server (m\xE3 h\xF3a phi\xEAn s\u1EB5n s\xE0ng).");
4932
+ console.log("\u2714 Connected to the server (session encryption ready).");
4831
4933
  this.sendMachineStatus();
4832
4934
  this.flushOfflineBuffer();
4833
4935
  this.startHeartbeat();
@@ -4840,11 +4942,11 @@ var WsClient = class {
4840
4942
  }
4841
4943
  if (message.errorCode === "CLI_VERSION_UNSUPPORTED") {
4842
4944
  this.stopped = true;
4843
- console.error(`Server t\u1EEB ch\u1ED1i version cli (${message.message}) \u2014 h\xE3y update.`);
4945
+ console.error(`Server rejected the cli version (${message.message}) \u2014 please update.`);
4844
4946
  this.socket?.close();
4845
4947
  return;
4846
4948
  }
4847
- console.warn(`Server b\xE1o l\u1ED7i: ${message.errorCode}`);
4949
+ console.warn(`Server reported an error: ${message.errorCode}`);
4848
4950
  this.socket?.close();
4849
4951
  return;
4850
4952
  }
@@ -4927,9 +5029,9 @@ var WsClient = class {
4927
5029
  }
4928
5030
  }
4929
5031
  /**
4930
- * Gửi 1 envelope (payload mã hóa + wsToken từng message — arch 0004).
4931
- * Mất kết nối: output lệnh đang chạy giữ trong buffer (trần 512KB,
4932
- * bỏ cũ nhất) rồi flush khi nối lại.
5032
+ * Send one envelope (encrypted payload + per-message wsToken — arch 0004).
5033
+ * When disconnected: the running command's output is held in a buffer (512KB cap,
5034
+ * dropping the oldest) then flushed on reconnect.
4933
5035
  */
4934
5036
  send(channel, data, replyTo = null) {
4935
5037
  if (!this.socket || !this.sessionKey) {
@@ -4948,13 +5050,13 @@ var WsClient = class {
4948
5050
  this.socket.send(JSON.stringify(envelope));
4949
5051
  }
4950
5052
  /**
4951
- * Request cli → server và chờ reply theo id envelope (timeout 15s).
4952
- * Dùng cho quota.check trước khi spawn AI cli (ADR-0020).
5053
+ * Send a cli → server request and await the reply by envelope id (15s timeout).
5054
+ * Used for quota.check before spawning an AI cli (ADR-0020).
4953
5055
  */
4954
5056
  request(channel, data) {
4955
5057
  return new Promise((resolve5, reject) => {
4956
5058
  if (!this.socket || !this.sessionKey) {
4957
- reject(new Error("Ch\u01B0a k\u1EBFt n\u1ED1i server."));
5059
+ reject(new Error("Not connected to the server."));
4958
5060
  return;
4959
5061
  }
4960
5062
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
@@ -4968,7 +5070,7 @@ var WsClient = class {
4968
5070
  };
4969
5071
  const timer = setTimeout(() => {
4970
5072
  this.pending.delete(envelope.id);
4971
- reject(new Error("Server kh\xF4ng tr\u1EA3 l\u1EDDi (timeout)."));
5073
+ reject(new Error("Server did not respond (timeout)."));
4972
5074
  }, 15e3);
4973
5075
  timer.unref?.();
4974
5076
  this.pending.set(envelope.id, {
@@ -4978,16 +5080,16 @@ var WsClient = class {
4978
5080
  this.socket.send(JSON.stringify(envelope));
4979
5081
  });
4980
5082
  }
4981
- /** quota.check trước khi spawn AI cli (ADR-0020). */
5083
+ /** quota.check before spawning an AI cli (ADR-0020). */
4982
5084
  checkQuota(metric, amount = 1) {
4983
5085
  return this.request(WsChannels.QUOTA_CHECK, { metric, amount });
4984
5086
  }
4985
- /** usage.report batch về server (ADR-0020). */
5087
+ /** usage.report batched to the server (ADR-0020). */
4986
5088
  reportUsage(events) {
4987
5089
  this.send(WsChannels.USAGE_REPORT, { events });
4988
5090
  }
4989
5091
  /**
4990
- * Đệm output khi offline (trần bytes — bỏ message cũ nhất khi vượt).
5092
+ * Buffer output while offline (byte cap — drop the oldest message when exceeded).
4991
5093
  */
4992
5094
  bufferOffline(channel, data) {
4993
5095
  const bytes = JSON.stringify(data).length;
@@ -4999,7 +5101,7 @@ var WsClient = class {
4999
5101
  }
5000
5102
  }
5001
5103
  /**
5002
- * Flush buffer output sau khi handshake lại thành công.
5104
+ * Flush the output buffer after a successful re-handshake.
5003
5105
  */
5004
5106
  flushOfflineBuffer() {
5005
5107
  const queued = this.offlineBuffer.splice(0);
@@ -5007,8 +5109,8 @@ var WsClient = class {
5007
5109
  for (const item of queued) this.send(item.channel, item.data);
5008
5110
  }
5009
5111
  /**
5010
- * Báo machine.status: version + trạng thái physic project đang phục vụ
5011
- * (path, folder tồn tại?, autonomous).
5112
+ * Report machine.status: version + the state of the physic project being served
5113
+ * (path, does the folder exist?, autonomous).
5012
5114
  */
5013
5115
  sendMachineStatus() {
5014
5116
  const config = readProfileConfig(this.context.profileDir);
@@ -5022,7 +5124,7 @@ var WsClient = class {
5022
5124
  autonomousRunning: false
5023
5125
  });
5024
5126
  }
5025
- /** Heartbeat 30s: ping + báo lại machine.status định kỳ. */
5127
+ /** Heartbeat every 30s: ping + periodically re-report machine.status. */
5026
5128
  startHeartbeat() {
5027
5129
  this.clearHeartbeat();
5028
5130
  this.heartbeat = setInterval(() => {
@@ -5036,14 +5138,14 @@ var WsClient = class {
5036
5138
  this.heartbeat = null;
5037
5139
  }
5038
5140
  /**
5039
- * Logout: đóng WS, xóa `.cre`, hướng dẫn pair lại (state Unlinked).
5141
+ * Logout: close WS, delete `.cre`, and prompt to pair again (Unlinked state).
5040
5142
  */
5041
5143
  logout(reason) {
5042
5144
  this.stopped = true;
5043
5145
  this.clearHeartbeat();
5044
5146
  this.socket?.close();
5045
5147
  deleteCredential(this.context.profileDir);
5046
- console.error(`Li\xEAn k\u1EBFt kh\xF4ng c\xF2n hi\u1EC7u l\u1EF1c (${reason}) \u2014 \u0111\xE3 x\xF3a .cre. Ch\u1EA1y \`4pm link\` \u0111\u1EC3 pair l\u1EA1i.`);
5148
+ console.error(`Link is no longer valid (${reason}) \u2014 deleted .cre. Run \`4pm link\` to pair again.`);
5047
5149
  }
5048
5150
  };
5049
5151
 
@@ -5052,7 +5154,7 @@ async function runStart(profileDir2, profileName) {
5052
5154
  const credential = readCredential(profileDir2, profileName);
5053
5155
  if (!credential) {
5054
5156
  console.error(
5055
- `Profile "${profileName}" ch\u01B0a li\xEAn k\u1EBFt \u2014 ch\u1EA1y \`4pm link --server <URL>\` tr\u01B0\u1EDBc.`
5157
+ `Profile "${profileName}" is not linked \u2014 run \`4pm link --server <URL>\` first.`
5056
5158
  );
5057
5159
  process.exitCode = 1;
5058
5160
  return;
@@ -5062,13 +5164,13 @@ async function runStart(profileDir2, profileName) {
5062
5164
  const result = await checkAndUpdate(credential.serverUrl, autoUpdate);
5063
5165
  if (result.action === "blocked") {
5064
5166
  console.error(
5065
- `CLI c\u0169 h\u01A1n minSupported (${result.minSupported}) v\xE0 update th\u1EA5t b\u1EA1i \u2014 kh\xF4ng th\u1EC3 connect.`
5167
+ `CLI is older than minSupported (${result.minSupported}) and the update failed \u2014 cannot connect.`
5066
5168
  );
5067
5169
  process.exitCode = 1;
5068
5170
  return;
5069
5171
  }
5070
5172
  if (result.action === "updated") {
5071
- console.log("Kh\u1EDFi \u0111\u1ED9ng l\u1EA1i v\u1EDBi version m\u1EDBi\u2026");
5173
+ console.log("Restarting with the new version\u2026");
5072
5174
  const child = spawn2(process.execPath, process.argv.slice(1), {
5073
5175
  stdio: "inherit",
5074
5176
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
@@ -5077,7 +5179,7 @@ async function runStart(profileDir2, profileName) {
5077
5179
  return;
5078
5180
  }
5079
5181
  initCommandHistory(profileDir2, profileName, config.commandHistoryUploadMinutes ?? 10);
5080
- console.log(`K\u1EBFt n\u1ED1i t\u1EDBi ${credential.serverUrl} (profile: ${profileName})\u2026`);
5182
+ console.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
5081
5183
  await new WsClient({
5082
5184
  credential,
5083
5185
  machine: collectFingerprint(),
@@ -5088,11 +5190,52 @@ async function runStart(profileDir2, profileName) {
5088
5190
  // src/commands/unlink.ts
5089
5191
  function runUnlink(profileDir2, profileName) {
5090
5192
  if (!readCredential(profileDir2, profileName)) {
5091
- console.log(`Profile "${profileName}" ch\u01B0a c\xF3 li\xEAn k\u1EBFt n\xE0o.`);
5193
+ console.log(`Profile "${profileName}" has no link yet.`);
5092
5194
  return;
5093
5195
  }
5094
5196
  deleteCredential(profileDir2);
5095
- console.log("\u2714 \u0110\xE3 x\xF3a .cre \u2014 h\u1EE7y li\xEAn k\u1EBFt ph\xEDa server \u1EDF web /machines.");
5197
+ console.log("\u2714 Deleted .cre \u2014 revoke the server-side link at web /machines.");
5198
+ }
5199
+
5200
+ // src/commands/update.ts
5201
+ async function runUpdate(profileDir2, profileName, serverUrlFlag) {
5202
+ const serverUrl = (serverUrlFlag ?? readCredential(profileDir2, profileName)?.serverUrl ?? process.env.FOURPM_SERVER ?? "http://localhost:3001").replace(/\/$/, "");
5203
+ let result;
5204
+ try {
5205
+ result = await updateToLatest(serverUrl);
5206
+ } catch (err) {
5207
+ console.error(`Could not check for updates: ${err}`);
5208
+ process.exitCode = 1;
5209
+ return;
5210
+ }
5211
+ switch (result.action) {
5212
+ case "already-latest":
5213
+ console.log(`Already up to date (${result.version}).`);
5214
+ break;
5215
+ case "updated":
5216
+ console.log(`\u2714 Updated to ${result.version}. Run \`4pm start\` to reconnect.`);
5217
+ break;
5218
+ case "failed":
5219
+ console.error(`Update failed: ${result.error}`);
5220
+ process.exitCode = 1;
5221
+ break;
5222
+ }
5223
+ }
5224
+
5225
+ // src/commands/version.ts
5226
+ async function runVersion(profileDir2, profileName, serverUrlFlag) {
5227
+ console.log(`4pm cli ${CLI_VERSION}`);
5228
+ const serverUrl = serverUrlFlag ?? readCredential(profileDir2, profileName)?.serverUrl ?? process.env.FOURPM_SERVER;
5229
+ if (!serverUrl) return;
5230
+ try {
5231
+ const meta = await fetchCliVersion(serverUrl.replace(/\/$/, ""));
5232
+ const outdated = compareSemver(CLI_VERSION, meta.latest) < 0;
5233
+ const hint = outdated ? " (update available \u2014 run `4pm update`)" : " (up to date)";
5234
+ console.log(`latest: ${meta.latest}${hint}`);
5235
+ console.log(`minSupported: ${meta.minSupported}`);
5236
+ } catch (err) {
5237
+ console.warn(`Could not reach the server for the latest version: ${err}`);
5238
+ }
5096
5239
  }
5097
5240
 
5098
5241
  // src/index.ts
@@ -5116,17 +5259,28 @@ async function main() {
5116
5259
  case "unlink":
5117
5260
  runUnlink(dir, profileName);
5118
5261
  break;
5262
+ case "version":
5263
+ case "--version":
5264
+ case "-v":
5265
+ await runVersion(dir, profileName, flagValue(rest, "server"));
5266
+ break;
5267
+ case "update":
5268
+ case "upgrade":
5269
+ await runUpdate(dir, profileName, flagValue(rest, "server"));
5270
+ break;
5119
5271
  default:
5120
5272
  console.log(
5121
5273
  [
5122
- "4pm \u2014 CLI m\xE1y worker c\u1EE7a 4PM",
5274
+ "4pm \u2014 the 4PM worker-machine CLI",
5123
5275
  "",
5124
- "C\xE1ch d\xF9ng:",
5125
- " 4pm [--profile <t\xEAn>] link [--server URL] Pairing v\u1EDBi server (hashcode 1\u21922\u21923)",
5126
- " 4pm [--profile <t\xEAn>] start K\u1EBFt n\u1ED1i WS, nh\u1EADn l\u1EC7nh t\u1EEB server",
5127
- " 4pm [--profile <t\xEAn>] unlink X\xF3a credential c\u1EE5c b\u1ED9 (.cre)",
5276
+ "Usage:",
5277
+ " 4pm [--profile <name>] link [--server URL] Pair with the server (hashcode 1\u21922\u21923)",
5278
+ " 4pm [--profile <name>] start Connect WS, receive commands from the server",
5279
+ " 4pm [--profile <name>] unlink Delete the local credential (.cre)",
5280
+ " 4pm version [--server URL] Show the installed version (+ latest from the server)",
5281
+ " 4pm update [--server URL] Update to the latest version now",
5128
5282
  "",
5129
- "Kh\xF4ng c\xF3 --profile \u21D2 profile `default` (ADR-0014)."
5283
+ "Without --profile \u21D2 the `default` profile (ADR-0014)."
5130
5284
  ].join("\n")
5131
5285
  );
5132
5286
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "type": "module",