@4pm/cli 0.3.1 → 0.3.3
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/dist/index.js +142 -21
- 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
|
|
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
|
-
|
|
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 =
|
|
4028
|
+
const tmp = join4(mkdtempSync(join4(tmpdir(), "4pm-update-")), "cli.tgz");
|
|
4008
4029
|
writeFileSync3(tmp, buf);
|
|
4009
|
-
const installRoot =
|
|
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
|
|
4632
|
-
import { join as
|
|
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 =
|
|
4726
|
+
filePath = join5(profileDir2, "command-history.json");
|
|
4695
4727
|
if (existsSync4(filePath)) {
|
|
4696
4728
|
try {
|
|
4697
|
-
entries = JSON.parse(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
5221
|
-
import { join as
|
|
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
|
|
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(
|
|
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(
|
|
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",
|