@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.
- package/README.md +11 -8
- package/dist/index.js +219 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# @4pm/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
(claude / codex
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
+
```bash
|
|
12
|
+
pnpm --filter @4pm/cli dev
|
|
13
|
+
```
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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}"
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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(`
|
|
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("
|
|
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("
|
|
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("
|
|
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(`
|
|
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
|
-
`
|
|
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 ? "
|
|
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
|
|
4035
|
+
console.error(`Update failed: ${err}`);
|
|
4009
4036
|
return { action: "blocked", minSupported: meta.minSupported };
|
|
4010
4037
|
}
|
|
4011
|
-
console.warn(`Update
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
4158
|
+
// one's own notifications
|
|
4121
4159
|
P.NOTIFICATION_READ
|
|
4122
4160
|
];
|
|
4123
4161
|
var ROLE_DEFAULT_PERMISSIONS = {
|
|
4124
|
-
// ADMIN: full
|
|
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
|
-
//
|
|
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
|
-
*
|
|
4314
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
4853
|
+
/** Output buffered while disconnected (flushed on reconnect). */
|
|
4752
4854
|
offlineBuffer = [];
|
|
4753
4855
|
offlineBufferBytes = 0;
|
|
4754
|
-
/**
|
|
4856
|
+
/** cli → server requests awaiting a reply (quota.check — ADR-0020). */
|
|
4755
4857
|
pending = /* @__PURE__ */ new Map();
|
|
4756
4858
|
/**
|
|
4757
|
-
*
|
|
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(`
|
|
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(`
|
|
4875
|
+
console.log(`Reconnecting in ${Math.round(wait / 1e3)}s\u2026`);
|
|
4774
4876
|
await sleep(wait);
|
|
4775
4877
|
}
|
|
4776
4878
|
}
|
|
4777
4879
|
/**
|
|
4778
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
4931
|
-
*
|
|
4932
|
-
*
|
|
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
|
-
*
|
|
4952
|
-
*
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
5011
|
-
* (path, folder
|
|
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 +
|
|
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:
|
|
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(`
|
|
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}"
|
|
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
|
|
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("
|
|
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(`
|
|
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}"
|
|
5193
|
+
console.log(`Profile "${profileName}" has no link yet.`);
|
|
5092
5194
|
return;
|
|
5093
5195
|
}
|
|
5094
5196
|
deleteCredential(profileDir2);
|
|
5095
|
-
console.log("\u2714
|
|
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
|
|
5274
|
+
"4pm \u2014 the 4PM worker-machine CLI",
|
|
5123
5275
|
"",
|
|
5124
|
-
"
|
|
5125
|
-
" 4pm [--profile <
|
|
5126
|
-
" 4pm [--profile <
|
|
5127
|
-
" 4pm [--profile <
|
|
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
|
-
"
|
|
5283
|
+
"Without --profile \u21D2 the `default` profile (ADR-0014)."
|
|
5130
5284
|
].join("\n")
|
|
5131
5285
|
);
|
|
5132
5286
|
}
|