@4pm/cli 0.3.0 → 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.
- package/dist/index.js +277 -48
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3725,18 +3725,41 @@ import {
|
|
|
3725
3725
|
chmodSync,
|
|
3726
3726
|
existsSync as existsSync2,
|
|
3727
3727
|
readFileSync as readFileSync2,
|
|
3728
|
-
rmSync,
|
|
3728
|
+
rmSync as rmSync2,
|
|
3729
3729
|
writeFileSync as writeFileSync2
|
|
3730
3730
|
} from "fs";
|
|
3731
3731
|
import { homedir as homedir2 } from "os";
|
|
3732
3732
|
import { join as join2 } from "path";
|
|
3733
3733
|
|
|
3734
3734
|
// src/config/profile.ts
|
|
3735
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3735
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
3736
3736
|
import { homedir } from "os";
|
|
3737
3737
|
import { join } from "path";
|
|
3738
3738
|
var DEFAULT_PROFILE = "default";
|
|
3739
|
-
function
|
|
3739
|
+
function defaultPointerPath() {
|
|
3740
|
+
return join(homedir(), ".4pm", "default");
|
|
3741
|
+
}
|
|
3742
|
+
function defaultProfileName() {
|
|
3743
|
+
try {
|
|
3744
|
+
const userId = readFileSync(defaultPointerPath(), "utf8").trim();
|
|
3745
|
+
if (userId) return userId;
|
|
3746
|
+
} catch {
|
|
3747
|
+
}
|
|
3748
|
+
return DEFAULT_PROFILE;
|
|
3749
|
+
}
|
|
3750
|
+
function writeDefaultProfile(userId) {
|
|
3751
|
+
mkdirSync(join(homedir(), ".4pm"), { recursive: true });
|
|
3752
|
+
writeFileSync(defaultPointerPath(), userId, "utf8");
|
|
3753
|
+
}
|
|
3754
|
+
function clearDefaultProfileIf(name) {
|
|
3755
|
+
try {
|
|
3756
|
+
if (readFileSync(defaultPointerPath(), "utf8").trim() === name) {
|
|
3757
|
+
rmSync(defaultPointerPath(), { force: true });
|
|
3758
|
+
}
|
|
3759
|
+
} catch {
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
function resolveProfileArg(args) {
|
|
3740
3763
|
const index = args.indexOf("--profile");
|
|
3741
3764
|
if (index >= 0) {
|
|
3742
3765
|
const name = args[index + 1];
|
|
@@ -3746,7 +3769,7 @@ function resolveProfile(args) {
|
|
|
3746
3769
|
const rest = [...args.slice(0, index), ...args.slice(index + 2)];
|
|
3747
3770
|
return [name, rest];
|
|
3748
3771
|
}
|
|
3749
|
-
return [process.env.FOURPM_PROFILE ??
|
|
3772
|
+
return [process.env.FOURPM_PROFILE ?? null, args];
|
|
3750
3773
|
}
|
|
3751
3774
|
function profileDir(name) {
|
|
3752
3775
|
const dir = join(homedir(), ".4pm", "profiles", name);
|
|
@@ -3777,7 +3800,7 @@ function readCredential(profileDir2, profileName) {
|
|
|
3777
3800
|
readFileSync2(LEGACY_CRE_PATH, "utf8")
|
|
3778
3801
|
);
|
|
3779
3802
|
writeCredential(profileDir2, legacy);
|
|
3780
|
-
|
|
3803
|
+
rmSync2(LEGACY_CRE_PATH, { force: true });
|
|
3781
3804
|
return legacy;
|
|
3782
3805
|
} catch {
|
|
3783
3806
|
return null;
|
|
@@ -3797,7 +3820,7 @@ function writeCredential(profileDir2, credential) {
|
|
|
3797
3820
|
chmodSync(path, 384);
|
|
3798
3821
|
}
|
|
3799
3822
|
function deleteCredential(profileDir2) {
|
|
3800
|
-
|
|
3823
|
+
rmSync2(credentialPath(profileDir2), { force: true });
|
|
3801
3824
|
}
|
|
3802
3825
|
|
|
3803
3826
|
// src/core/fingerprint.ts
|
|
@@ -3884,15 +3907,17 @@ async function fetchCliVersion(serverUrl) {
|
|
|
3884
3907
|
}
|
|
3885
3908
|
|
|
3886
3909
|
// src/commands/link.ts
|
|
3887
|
-
async function runLink(serverUrl,
|
|
3888
|
-
|
|
3910
|
+
async function runLink(serverUrl, explicitProfile) {
|
|
3911
|
+
const currentName = explicitProfile ?? defaultProfileName();
|
|
3912
|
+
if (readCredential(profileDir(currentName), currentName)) {
|
|
3913
|
+
const flag = explicitProfile ? ` --profile ${explicitProfile}` : "";
|
|
3889
3914
|
console.log(
|
|
3890
|
-
`Profile "${
|
|
3915
|
+
`Profile "${currentName}" is already linked (.cre). Run \`4pm unlink${flag}\` first if you want to pair again.`
|
|
3891
3916
|
);
|
|
3892
3917
|
return;
|
|
3893
3918
|
}
|
|
3894
3919
|
const hashcode1 = randomBytes(32).toString("hex");
|
|
3895
|
-
console.log(`\u2500\u2500 4PM cli pairing (profile: ${
|
|
3920
|
+
console.log(`\u2500\u2500 4PM cli pairing (profile: ${explicitProfile ?? "default"}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
3896
3921
|
console.log("1. Open the 4PM web \u2192 /machines, enter hashcode (1):");
|
|
3897
3922
|
console.log(`
|
|
3898
3923
|
${hashcode1}
|
|
@@ -3901,26 +3926,35 @@ async function runLink(serverUrl, profileDir2, profileName) {
|
|
|
3901
3926
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3902
3927
|
const hashcode2 = (await rl.question("hashcode (2): ")).trim();
|
|
3903
3928
|
rl.close();
|
|
3904
|
-
const { hashcode3 } = await confirmPairing(
|
|
3905
|
-
|
|
3929
|
+
const { hashcode3, userId, username } = await confirmPairing(
|
|
3930
|
+
serverUrl,
|
|
3931
|
+
hashcode2,
|
|
3932
|
+
collectFingerprint()
|
|
3933
|
+
);
|
|
3934
|
+
const profileName = explicitProfile ?? userId;
|
|
3935
|
+
writeCredential(profileDir(profileName), {
|
|
3906
3936
|
serverUrl,
|
|
3907
3937
|
hashcode3,
|
|
3908
3938
|
scope: "unknown",
|
|
3909
3939
|
// scope is decided by the server based on the pairing user (ADR-0010)
|
|
3910
3940
|
pairedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3911
3941
|
});
|
|
3912
|
-
|
|
3942
|
+
if (!explicitProfile) writeDefaultProfile(userId);
|
|
3943
|
+
console.log(
|
|
3944
|
+
`\u2714 Linked successfully as "${username}" \u2014 saved .cre. Run \`4pm start\` to connect.`
|
|
3945
|
+
);
|
|
3913
3946
|
}
|
|
3914
3947
|
|
|
3915
3948
|
// src/commands/start.ts
|
|
3916
3949
|
import { spawn as spawn2 } from "child_process";
|
|
3950
|
+
import { join as join9 } from "path";
|
|
3917
3951
|
|
|
3918
3952
|
// src/core/update.ts
|
|
3919
3953
|
import { execSync as execSync2 } from "child_process";
|
|
3920
3954
|
import { createHash as createHash2, createPublicKey, verify } from "crypto";
|
|
3921
|
-
import { mkdtempSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3955
|
+
import { mkdtempSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
3922
3956
|
import { tmpdir } from "os";
|
|
3923
|
-
import { dirname, join as
|
|
3957
|
+
import { dirname as dirname2, join as join4, sep } from "path";
|
|
3924
3958
|
|
|
3925
3959
|
// src/config/signing.ts
|
|
3926
3960
|
var CLI_SIGNING_PUBLIC_KEY = `
|
|
@@ -3930,7 +3964,19 @@ MCowBQYDK2VwAyEARlfzjaFhe5bhRrMp/ZuH6sVqg/S7f8B1dliuF21iDTw=
|
|
|
3930
3964
|
`;
|
|
3931
3965
|
|
|
3932
3966
|
// src/version.ts
|
|
3933
|
-
|
|
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();
|
|
3934
3980
|
|
|
3935
3981
|
// src/core/update.ts
|
|
3936
3982
|
function compareSemver(a, b) {
|
|
@@ -3945,6 +3991,14 @@ function compareSemver(a, b) {
|
|
|
3945
3991
|
function installedViaNpm() {
|
|
3946
3992
|
return (process.argv[1] ?? "").split(sep).includes("node_modules");
|
|
3947
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
|
+
}
|
|
3948
4002
|
function updateViaNpm(version) {
|
|
3949
4003
|
execSync2(`npm i -g @4pm/cli@${version}`, { stdio: "inherit" });
|
|
3950
4004
|
}
|
|
@@ -3971,9 +4025,9 @@ async function updateViaDownload(tarballUrl, checksum, signature) {
|
|
|
3971
4025
|
throw new Error("Invalid tarball signature \u2014 aborting update (ADR-0015).");
|
|
3972
4026
|
}
|
|
3973
4027
|
}
|
|
3974
|
-
const tmp =
|
|
4028
|
+
const tmp = join4(mkdtempSync(join4(tmpdir(), "4pm-update-")), "cli.tgz");
|
|
3975
4029
|
writeFileSync3(tmp, buf);
|
|
3976
|
-
const installRoot =
|
|
4030
|
+
const installRoot = dirname2(dirname2(process.argv[1] ?? "."));
|
|
3977
4031
|
execSync2(`tar -xzf "${tmp}" -C "${installRoot}" --strip-components=1`, {
|
|
3978
4032
|
stdio: "inherit"
|
|
3979
4033
|
});
|
|
@@ -4000,6 +4054,7 @@ async function updateToLatest(serverUrl) {
|
|
|
4000
4054
|
}
|
|
4001
4055
|
}
|
|
4002
4056
|
async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
4057
|
+
if (CLI_VERSION.startsWith("0.0.0")) return { action: "none" };
|
|
4003
4058
|
let meta;
|
|
4004
4059
|
try {
|
|
4005
4060
|
meta = await fetchCliVersion(serverUrl);
|
|
@@ -4029,7 +4084,6 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
4029
4084
|
meta.source.signature
|
|
4030
4085
|
);
|
|
4031
4086
|
}
|
|
4032
|
-
return { action: "updated", version: meta.latest };
|
|
4033
4087
|
} catch (err) {
|
|
4034
4088
|
if (mandatory) {
|
|
4035
4089
|
console.error(`Update failed: ${err}`);
|
|
@@ -4038,6 +4092,17 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
4038
4092
|
console.warn(`Update failed (skipping \u2014 not mandatory): ${err}`);
|
|
4039
4093
|
return { action: "none" };
|
|
4040
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 };
|
|
4041
4106
|
}
|
|
4042
4107
|
|
|
4043
4108
|
// src/core/ws-client.ts
|
|
@@ -4074,6 +4139,7 @@ var Role = {
|
|
|
4074
4139
|
};
|
|
4075
4140
|
var ALL_ROLES = Object.values(Role);
|
|
4076
4141
|
var EXCLUSIVE_ROLES = [Role.ADMIN, Role.MACHINE];
|
|
4142
|
+
var PM_GRANTABLE_ROLES = [Role.SA_SE, Role.QA, Role.BA];
|
|
4077
4143
|
|
|
4078
4144
|
// ../../22-packages/11-shared/11-constants/src/permissions.ts
|
|
4079
4145
|
var PermissionCode = {
|
|
@@ -4260,6 +4326,13 @@ var SELF_SERVICE_CODES = [
|
|
|
4260
4326
|
P.USER_READ_SELF,
|
|
4261
4327
|
P.NOTIFICATION_READ
|
|
4262
4328
|
];
|
|
4329
|
+
var ADMIN_ONLY_CODES = [
|
|
4330
|
+
P.ORG_READ,
|
|
4331
|
+
P.ORG_UPDATE,
|
|
4332
|
+
P.ORG_DELETE,
|
|
4333
|
+
P.BILLING_READ,
|
|
4334
|
+
P.BILLING_MANAGE
|
|
4335
|
+
];
|
|
4263
4336
|
var TEAM_GRANTABLE_CODES = [
|
|
4264
4337
|
P.TEAM_READ,
|
|
4265
4338
|
P.TEAM_UPDATE,
|
|
@@ -4302,10 +4375,11 @@ var PROJECT_GRANTABLE_CODES = [
|
|
|
4302
4375
|
P.MACHINE_PHYSIC_MANAGE
|
|
4303
4376
|
];
|
|
4304
4377
|
var SELF_SERVICE_SET = new Set(SELF_SERVICE_CODES);
|
|
4378
|
+
var ADMIN_ONLY_SET = new Set(ADMIN_ONLY_CODES);
|
|
4305
4379
|
var TEAM_GRANTABLE_SET = new Set(TEAM_GRANTABLE_CODES);
|
|
4306
4380
|
var PROJECT_GRANTABLE_SET = new Set(PROJECT_GRANTABLE_CODES);
|
|
4307
4381
|
function grantSubjectsFor(code) {
|
|
4308
|
-
if (SELF_SERVICE_SET.has(code)) return [];
|
|
4382
|
+
if (SELF_SERVICE_SET.has(code) || ADMIN_ONLY_SET.has(code)) return [];
|
|
4309
4383
|
const subjects = [GrantSubjectType.USER];
|
|
4310
4384
|
if (TEAM_GRANTABLE_SET.has(code)) subjects.push(GrantSubjectType.TEAM);
|
|
4311
4385
|
if (PROJECT_GRANTABLE_SET.has(code)) subjects.push(GrantSubjectType.PROJECT);
|
|
@@ -4586,8 +4660,8 @@ ${JSON.stringify(req.spec)}`;
|
|
|
4586
4660
|
}
|
|
4587
4661
|
|
|
4588
4662
|
// src/core/command-history.ts
|
|
4589
|
-
import { existsSync as existsSync4, readFileSync as
|
|
4590
|
-
import { join as
|
|
4663
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
4664
|
+
import { join as join5 } from "path";
|
|
4591
4665
|
var MAX_ENTRIES = 1e3;
|
|
4592
4666
|
var filePath = null;
|
|
4593
4667
|
var entries = [];
|
|
@@ -4649,10 +4723,10 @@ async function uploadHistory(cfg) {
|
|
|
4649
4723
|
}
|
|
4650
4724
|
}
|
|
4651
4725
|
function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
|
|
4652
|
-
filePath =
|
|
4726
|
+
filePath = join5(profileDir2, "command-history.json");
|
|
4653
4727
|
if (existsSync4(filePath)) {
|
|
4654
4728
|
try {
|
|
4655
|
-
entries = JSON.parse(
|
|
4729
|
+
entries = JSON.parse(readFileSync6(filePath, "utf8"));
|
|
4656
4730
|
} catch {
|
|
4657
4731
|
entries = [];
|
|
4658
4732
|
}
|
|
@@ -4725,12 +4799,12 @@ async function readWorkerFile(path) {
|
|
|
4725
4799
|
// src/core/git-diff.ts
|
|
4726
4800
|
import { readFile as readFile2 } from "fs/promises";
|
|
4727
4801
|
import { execFile as execFile2 } from "child_process";
|
|
4728
|
-
import { basename, dirname as
|
|
4802
|
+
import { basename, dirname as dirname3, resolve as resolve3 } from "path";
|
|
4729
4803
|
import { promisify as promisify2 } from "util";
|
|
4730
4804
|
var run2 = promisify2(execFile2);
|
|
4731
4805
|
async function gitDiff(path) {
|
|
4732
4806
|
const target = resolve3(path);
|
|
4733
|
-
const dir =
|
|
4807
|
+
const dir = dirname3(target);
|
|
4734
4808
|
const base = basename(target);
|
|
4735
4809
|
let newContent = "";
|
|
4736
4810
|
try {
|
|
@@ -4784,13 +4858,13 @@ ${stderr}`;
|
|
|
4784
4858
|
// src/core/scaffold.ts
|
|
4785
4859
|
import { cp, mkdir, writeFile } from "fs/promises";
|
|
4786
4860
|
import { execFile as execFile4 } from "child_process";
|
|
4787
|
-
import { dirname as
|
|
4788
|
-
import { fileURLToPath } from "url";
|
|
4861
|
+
import { dirname as dirname4, join as join6, resolve as resolve4 } from "path";
|
|
4862
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4789
4863
|
import { promisify as promisify4 } from "util";
|
|
4790
4864
|
var run4 = promisify4(execFile4);
|
|
4791
4865
|
function sampleDir() {
|
|
4792
4866
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
4793
|
-
const here =
|
|
4867
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
4794
4868
|
return resolve4(here, "../../project-sample");
|
|
4795
4869
|
}
|
|
4796
4870
|
async function scaffoldProject(payload, onProgress) {
|
|
@@ -4803,7 +4877,7 @@ async function scaffoldProject(payload, onProgress) {
|
|
|
4803
4877
|
if (payload.spec) {
|
|
4804
4878
|
emit("spec", "Writing project.spec.json\u2026");
|
|
4805
4879
|
await writeFile(
|
|
4806
|
-
|
|
4880
|
+
join6(target, "project.spec.json"),
|
|
4807
4881
|
JSON.stringify(payload.spec, null, 2),
|
|
4808
4882
|
"utf8"
|
|
4809
4883
|
);
|
|
@@ -4833,6 +4907,72 @@ async function addProject(payload, onProgress) {
|
|
|
4833
4907
|
}
|
|
4834
4908
|
}
|
|
4835
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
|
+
|
|
4836
4976
|
// src/core/ws-client.ts
|
|
4837
4977
|
var HEARTBEAT_MS = 3e4;
|
|
4838
4978
|
var MAX_HASHCODE_INVALID_RETRIES = 5;
|
|
@@ -4889,6 +5029,7 @@ var WsClient = class {
|
|
|
4889
5029
|
}
|
|
4890
5030
|
if (this.stopped) return;
|
|
4891
5031
|
const wait = backoffJitterMs(Math.min(attempt, 6), 1e3, 6e4);
|
|
5032
|
+
logger.info("ws.reconnect", { attempt, waitMs: wait });
|
|
4892
5033
|
console.log(`Reconnecting in ${Math.round(wait / 1e3)}s\u2026`);
|
|
4893
5034
|
await sleep(wait);
|
|
4894
5035
|
}
|
|
@@ -4927,6 +5068,7 @@ var WsClient = class {
|
|
|
4927
5068
|
socket.on("close", (code) => {
|
|
4928
5069
|
this.clearHeartbeat();
|
|
4929
5070
|
this.sessionKey = null;
|
|
5071
|
+
logger.debug("ws.close", { code });
|
|
4930
5072
|
if (code === CLOSE_DRAINING) {
|
|
4931
5073
|
console.log("Server draining \u2014 will reconnect (backoff + jitter).");
|
|
4932
5074
|
}
|
|
@@ -4946,6 +5088,7 @@ var WsClient = class {
|
|
|
4946
5088
|
if ("type" in message) {
|
|
4947
5089
|
if (message.type === "hello_ack" && this.ecdh) {
|
|
4948
5090
|
this.sessionKey = deriveSessionKey(this.ecdh, message.pubkey);
|
|
5091
|
+
logger.info("ws.connected", { version: CLI_VERSION });
|
|
4949
5092
|
console.log("\u2714 Connected to the server (session encryption ready).");
|
|
4950
5093
|
this.sendMachineStatus();
|
|
4951
5094
|
this.flushOfflineBuffer();
|
|
@@ -4953,6 +5096,7 @@ var WsClient = class {
|
|
|
4953
5096
|
return;
|
|
4954
5097
|
}
|
|
4955
5098
|
if (message.type === "error") {
|
|
5099
|
+
logger.warn("ws.error", { errorCode: message.errorCode });
|
|
4956
5100
|
if (["LINK_REVOKED", "HASHCODE_EXPIRED"].includes(message.errorCode)) {
|
|
4957
5101
|
this.logout(message.errorCode);
|
|
4958
5102
|
return;
|
|
@@ -4963,6 +5107,14 @@ var WsClient = class {
|
|
|
4963
5107
|
this.socket?.close();
|
|
4964
5108
|
return;
|
|
4965
5109
|
}
|
|
5110
|
+
if (message.errorCode === "SESSION_REPLACED") {
|
|
5111
|
+
this.stopped = true;
|
|
5112
|
+
console.error(
|
|
5113
|
+
"Another session for this profile took over \u2014 this instance is stopping. (Are you running `4pm start` twice for the same profile?)"
|
|
5114
|
+
);
|
|
5115
|
+
this.socket?.close();
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
4966
5118
|
console.warn(`Server reported an error: ${message.errorCode}`);
|
|
4967
5119
|
this.socket?.close();
|
|
4968
5120
|
return;
|
|
@@ -4987,6 +5139,10 @@ var WsClient = class {
|
|
|
4987
5139
|
switch (message.channel) {
|
|
4988
5140
|
case WsChannels.COMMAND_DISPATCH: {
|
|
4989
5141
|
const dispatch = payload;
|
|
5142
|
+
logger.info("command.dispatch", {
|
|
5143
|
+
commandId: dispatch.commandId,
|
|
5144
|
+
cmd: dispatch.cmd
|
|
5145
|
+
});
|
|
4990
5146
|
recordCommand({
|
|
4991
5147
|
commandId: dispatch.commandId,
|
|
4992
5148
|
projectId: dispatch.projectId,
|
|
@@ -5161,12 +5317,54 @@ var WsClient = class {
|
|
|
5161
5317
|
this.stopped = true;
|
|
5162
5318
|
this.clearHeartbeat();
|
|
5163
5319
|
this.socket?.close();
|
|
5320
|
+
logger.warn("ws.logout", { reason });
|
|
5164
5321
|
deleteCredential(this.context.profileDir);
|
|
5165
5322
|
console.error(`Link is no longer valid (${reason}) \u2014 deleted .cre. Run \`4pm link\` to pair again.`);
|
|
5166
5323
|
}
|
|
5167
5324
|
};
|
|
5168
5325
|
|
|
5326
|
+
// src/core/instance-lock.ts
|
|
5327
|
+
import { readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
5328
|
+
import { join as join8 } from "path";
|
|
5329
|
+
function lockPath(profileDir2) {
|
|
5330
|
+
return join8(profileDir2, "start.lock");
|
|
5331
|
+
}
|
|
5332
|
+
function isAlive(pid) {
|
|
5333
|
+
try {
|
|
5334
|
+
process.kill(pid, 0);
|
|
5335
|
+
return true;
|
|
5336
|
+
} catch (err) {
|
|
5337
|
+
return err.code === "EPERM";
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
function lockHolder(profileDir2) {
|
|
5341
|
+
try {
|
|
5342
|
+
const pid = Number.parseInt(readFileSync7(lockPath(profileDir2), "utf8").trim(), 10);
|
|
5343
|
+
if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) return pid;
|
|
5344
|
+
} catch {
|
|
5345
|
+
}
|
|
5346
|
+
return null;
|
|
5347
|
+
}
|
|
5348
|
+
function acquireInstanceLock(profileDir2) {
|
|
5349
|
+
if (lockHolder(profileDir2) !== null) return false;
|
|
5350
|
+
writeFileSync5(lockPath(profileDir2), String(process.pid), "utf8");
|
|
5351
|
+
return true;
|
|
5352
|
+
}
|
|
5353
|
+
function releaseInstanceLock(profileDir2) {
|
|
5354
|
+
try {
|
|
5355
|
+
const pid = Number.parseInt(readFileSync7(lockPath(profileDir2), "utf8").trim(), 10);
|
|
5356
|
+
if (pid === process.pid) rmSync3(lockPath(profileDir2), { force: true });
|
|
5357
|
+
} catch {
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
|
|
5169
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
|
+
}
|
|
5170
5368
|
async function runStart(profileDir2, profileName) {
|
|
5171
5369
|
const credential = readCredential(profileDir2, profileName);
|
|
5172
5370
|
if (!credential) {
|
|
@@ -5176,10 +5374,17 @@ async function runStart(profileDir2, profileName) {
|
|
|
5176
5374
|
process.exitCode = 1;
|
|
5177
5375
|
return;
|
|
5178
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 });
|
|
5179
5383
|
const config = readProfileConfig(profileDir2);
|
|
5180
5384
|
const autoUpdate = config.autoUpdate !== false && process.env.FOURPM_NO_UPDATE !== "1";
|
|
5181
5385
|
const result = await checkAndUpdate(credential.serverUrl, autoUpdate);
|
|
5182
5386
|
if (result.action === "blocked") {
|
|
5387
|
+
logger.error("update.blocked", { minSupported: result.minSupported });
|
|
5183
5388
|
console.error(
|
|
5184
5389
|
`CLI is older than minSupported (${result.minSupported}) and the update failed \u2014 cannot connect.`
|
|
5185
5390
|
);
|
|
@@ -5187,6 +5392,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
5187
5392
|
return;
|
|
5188
5393
|
}
|
|
5189
5394
|
if (result.action === "updated") {
|
|
5395
|
+
logger.info("update.updated", { version: result.version });
|
|
5190
5396
|
console.log("Restarting with the new version\u2026");
|
|
5191
5397
|
const child = spawn2(process.execPath, process.argv.slice(1), {
|
|
5192
5398
|
stdio: "inherit",
|
|
@@ -5195,13 +5401,28 @@ async function runStart(profileDir2, profileName) {
|
|
|
5195
5401
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
5196
5402
|
return;
|
|
5197
5403
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
}
|
|
5404
|
+
if (!acquireInstanceLock(profileDir2)) {
|
|
5405
|
+
console.error(
|
|
5406
|
+
`Profile "${profileName}" is already running (pid ${lockHolder(profileDir2)}). Only one \`4pm start\` per profile is allowed.`
|
|
5407
|
+
);
|
|
5408
|
+
process.exitCode = 1;
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5411
|
+
const release = () => releaseInstanceLock(profileDir2);
|
|
5412
|
+
process.once("exit", release);
|
|
5413
|
+
process.once("SIGINT", () => process.exit(0));
|
|
5414
|
+
process.once("SIGTERM", () => process.exit(0));
|
|
5415
|
+
try {
|
|
5416
|
+
initCommandHistory(profileDir2, profileName, config.commandHistoryUploadMinutes ?? 10);
|
|
5417
|
+
console.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
|
|
5418
|
+
await new WsClient({
|
|
5419
|
+
credential,
|
|
5420
|
+
machine: collectFingerprint(),
|
|
5421
|
+
profileDir: profileDir2
|
|
5422
|
+
}).run();
|
|
5423
|
+
} finally {
|
|
5424
|
+
release();
|
|
5425
|
+
}
|
|
5205
5426
|
}
|
|
5206
5427
|
|
|
5207
5428
|
// src/commands/unlink.ts
|
|
@@ -5211,6 +5432,7 @@ function runUnlink(profileDir2, profileName) {
|
|
|
5211
5432
|
return;
|
|
5212
5433
|
}
|
|
5213
5434
|
deleteCredential(profileDir2);
|
|
5435
|
+
clearDefaultProfileIf(profileName);
|
|
5214
5436
|
console.log("\u2714 Deleted .cre \u2014 revoke the server-side link at web /machines.");
|
|
5215
5437
|
}
|
|
5216
5438
|
|
|
@@ -5261,30 +5483,37 @@ function flagValue(args, name) {
|
|
|
5261
5483
|
return index >= 0 ? args[index + 1] : void 0;
|
|
5262
5484
|
}
|
|
5263
5485
|
async function main() {
|
|
5264
|
-
const [
|
|
5486
|
+
const [explicitProfile, args] = resolveProfileArg(process.argv.slice(2));
|
|
5265
5487
|
const [command, ...rest] = args;
|
|
5266
|
-
const dir = profileDir(profileName);
|
|
5267
5488
|
switch (command) {
|
|
5268
5489
|
case "link": {
|
|
5269
5490
|
const serverUrl = flagValue(rest, "server") ?? process.env.FOURPM_SERVER ?? "http://localhost:3001";
|
|
5270
|
-
await runLink(serverUrl.replace(/\/$/, ""),
|
|
5491
|
+
await runLink(serverUrl.replace(/\/$/, ""), explicitProfile);
|
|
5271
5492
|
break;
|
|
5272
5493
|
}
|
|
5273
|
-
case "start":
|
|
5274
|
-
|
|
5494
|
+
case "start": {
|
|
5495
|
+
const name = explicitProfile ?? defaultProfileName();
|
|
5496
|
+
await runStart(profileDir(name), name);
|
|
5275
5497
|
break;
|
|
5276
|
-
|
|
5277
|
-
|
|
5498
|
+
}
|
|
5499
|
+
case "unlink": {
|
|
5500
|
+
const name = explicitProfile ?? defaultProfileName();
|
|
5501
|
+
runUnlink(profileDir(name), name);
|
|
5278
5502
|
break;
|
|
5503
|
+
}
|
|
5279
5504
|
case "version":
|
|
5280
5505
|
case "--version":
|
|
5281
|
-
case "-v":
|
|
5282
|
-
|
|
5506
|
+
case "-v": {
|
|
5507
|
+
const name = explicitProfile ?? defaultProfileName();
|
|
5508
|
+
await runVersion(profileDir(name), name, flagValue(rest, "server"));
|
|
5283
5509
|
break;
|
|
5510
|
+
}
|
|
5284
5511
|
case "update":
|
|
5285
|
-
case "upgrade":
|
|
5286
|
-
|
|
5512
|
+
case "upgrade": {
|
|
5513
|
+
const name = explicitProfile ?? defaultProfileName();
|
|
5514
|
+
await runUpdate(profileDir(name), name, flagValue(rest, "server"));
|
|
5287
5515
|
break;
|
|
5516
|
+
}
|
|
5288
5517
|
default:
|
|
5289
5518
|
console.log(
|
|
5290
5519
|
[
|