ccem 2.28.0 → 2.30.0
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 +304 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2702,16 +2702,70 @@ var config = new Conf({
|
|
|
2702
2702
|
cwd: getCcemConfigDir()
|
|
2703
2703
|
// 使用统一的配置目录
|
|
2704
2704
|
});
|
|
2705
|
-
var
|
|
2706
|
-
|
|
2705
|
+
var REMOTE_CRYPTO_ALGORITHM_V2 = "aes-256-gcm";
|
|
2706
|
+
var REMOTE_CRYPTO_ALGORITHM_V1 = "aes-256-cbc";
|
|
2707
|
+
var isEnvelopeV2 = (value) => {
|
|
2708
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2709
|
+
const v = value;
|
|
2710
|
+
return v.v === 2 && typeof v.nonce === "string" && typeof v.ciphertext === "string" && typeof v.tag === "string";
|
|
2711
|
+
};
|
|
2712
|
+
var deriveRemoteKey = (secret) => crypto3.scryptSync(secret, "ccem-salt", 32);
|
|
2713
|
+
var decryptV2Envelope = (envelope, key) => {
|
|
2714
|
+
const nonce = Buffer.from(envelope.nonce, "base64");
|
|
2715
|
+
const ciphertext = Buffer.from(envelope.ciphertext, "base64");
|
|
2716
|
+
const tag = Buffer.from(envelope.tag, "base64");
|
|
2717
|
+
if (nonce.length === 0 || ciphertext.length === 0 || tag.length === 0) {
|
|
2718
|
+
throw new Error("v2 envelope has empty nonce/ciphertext/tag");
|
|
2719
|
+
}
|
|
2720
|
+
const decipher = crypto3.createDecipheriv(
|
|
2721
|
+
REMOTE_CRYPTO_ALGORITHM_V2,
|
|
2722
|
+
key,
|
|
2723
|
+
nonce
|
|
2724
|
+
);
|
|
2725
|
+
decipher.setAuthTag(tag);
|
|
2726
|
+
let decrypted = decipher.update(ciphertext);
|
|
2727
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
2728
|
+
return decrypted.toString("utf8");
|
|
2729
|
+
};
|
|
2730
|
+
var decryptV1Legacy = (encryptedBase64, key) => {
|
|
2707
2731
|
const combined = Buffer.from(encryptedBase64, "base64");
|
|
2732
|
+
if (combined.length < 16) {
|
|
2733
|
+
throw new Error("v1 payload too short");
|
|
2734
|
+
}
|
|
2708
2735
|
const iv = combined.subarray(0, 16);
|
|
2709
2736
|
const encryptedHex = combined.subarray(16).toString("hex");
|
|
2710
|
-
const decipher = crypto3.createDecipheriv(
|
|
2737
|
+
const decipher = crypto3.createDecipheriv(REMOTE_CRYPTO_ALGORITHM_V1, key, iv);
|
|
2711
2738
|
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
2712
2739
|
decrypted += decipher.final("utf8");
|
|
2713
2740
|
return decrypted;
|
|
2714
2741
|
};
|
|
2742
|
+
var decryptWithSecret = (encryptedBase64, secret) => {
|
|
2743
|
+
const key = deriveRemoteKey(secret);
|
|
2744
|
+
let parsedObj = null;
|
|
2745
|
+
try {
|
|
2746
|
+
const jsonStr = Buffer.from(encryptedBase64, "base64").toString("utf8");
|
|
2747
|
+
parsedObj = JSON.parse(jsonStr);
|
|
2748
|
+
} catch {
|
|
2749
|
+
}
|
|
2750
|
+
let envelopeV2 = null;
|
|
2751
|
+
if (parsedObj !== null) {
|
|
2752
|
+
if (isEnvelopeV2(parsedObj)) {
|
|
2753
|
+
envelopeV2 = parsedObj;
|
|
2754
|
+
} else if (parsedObj && typeof parsedObj === "object" && "v" in parsedObj) {
|
|
2755
|
+
const version = parsedObj.v;
|
|
2756
|
+
if (version === 2) {
|
|
2757
|
+
throw new Error(
|
|
2758
|
+
"Malformed v2 envelope: missing required fields (nonce, ciphertext, tag)"
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
throw new Error(`Unsupported remote envelope version: ${version}`);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
if (envelopeV2) {
|
|
2765
|
+
return decryptV2Envelope(envelopeV2, key);
|
|
2766
|
+
}
|
|
2767
|
+
return decryptV1Legacy(encryptedBase64, key);
|
|
2768
|
+
};
|
|
2715
2769
|
var getUniqueName = (baseName, existingNames) => {
|
|
2716
2770
|
if (!existingNames.has(baseName)) {
|
|
2717
2771
|
return baseName;
|
|
@@ -3168,8 +3222,132 @@ function formatCronTaskTableRows(tasks) {
|
|
|
3168
3222
|
// src/desktopControl.ts
|
|
3169
3223
|
import fs10 from "fs";
|
|
3170
3224
|
import path8 from "path";
|
|
3225
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 5e3;
|
|
3226
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
3227
|
+
var ENDPOINT_UNREACHABLE_CODES = /* @__PURE__ */ new Set([
|
|
3228
|
+
"ECONNREFUSED",
|
|
3229
|
+
"ECONNRESET",
|
|
3230
|
+
"ENOTCONN",
|
|
3231
|
+
"EHOSTUNREACH",
|
|
3232
|
+
"ECONNABORTED"
|
|
3233
|
+
]);
|
|
3234
|
+
var StaleDesktopControlDescriptorError = class extends Error {
|
|
3235
|
+
name = "StaleDesktopControlDescriptorError";
|
|
3236
|
+
reason;
|
|
3237
|
+
descriptorPath;
|
|
3238
|
+
pid;
|
|
3239
|
+
cleanedUp;
|
|
3240
|
+
cause;
|
|
3241
|
+
constructor(details) {
|
|
3242
|
+
const subject = details.pid !== null ? `process ${details.pid}` : "the publishing process";
|
|
3243
|
+
const symptom = details.reason === "dead-pid" ? `${subject} is no longer running` : details.reason === "endpoint-unreachable" ? "the control endpoint refused the connection" : `the control request timed out after ${details.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS}ms`;
|
|
3244
|
+
const remedy = details.cleanedUp ? "The stale descriptor was removed automatically; start CCEM Desktop and rerun the command." : details.pid !== null ? "Restart CCEM Desktop so it republishes a fresh control endpoint." : "Restart CCEM Desktop to refresh the descriptor, or remove the stale file manually if it is no longer managed.";
|
|
3245
|
+
super(
|
|
3246
|
+
`CCEM Desktop control descriptor at ${details.descriptorPath} is stale: ${symptom}. ${remedy}`
|
|
3247
|
+
);
|
|
3248
|
+
this.reason = details.reason;
|
|
3249
|
+
this.descriptorPath = details.descriptorPath;
|
|
3250
|
+
this.pid = details.pid;
|
|
3251
|
+
this.cleanedUp = details.cleanedUp;
|
|
3252
|
+
if (details.cause !== void 0) {
|
|
3253
|
+
this.cause = details.cause;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
};
|
|
3171
3257
|
function getDesktopControlDescriptorPath() {
|
|
3172
|
-
return process.env.CCEM_CONTROL_FILE?.trim() ||
|
|
3258
|
+
return process.env.CCEM_CONTROL_FILE?.trim() || getDefaultControlDescriptorPath();
|
|
3259
|
+
}
|
|
3260
|
+
function getDefaultControlDescriptorPath() {
|
|
3261
|
+
return path8.join(getCcemConfigDir(), "control.json");
|
|
3262
|
+
}
|
|
3263
|
+
function isDefaultDescriptorPath(descriptorPath) {
|
|
3264
|
+
try {
|
|
3265
|
+
return path8.resolve(descriptorPath) === path8.resolve(getDefaultControlDescriptorPath());
|
|
3266
|
+
} catch {
|
|
3267
|
+
return false;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
function safeRemoveStaleDescriptor(descriptorPath) {
|
|
3271
|
+
if (!isDefaultDescriptorPath(descriptorPath)) {
|
|
3272
|
+
return false;
|
|
3273
|
+
}
|
|
3274
|
+
try {
|
|
3275
|
+
fs10.rmSync(descriptorPath);
|
|
3276
|
+
return true;
|
|
3277
|
+
} catch {
|
|
3278
|
+
return false;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
function readErrnoCode(error) {
|
|
3282
|
+
if (!error || typeof error !== "object") return void 0;
|
|
3283
|
+
const candidate = error;
|
|
3284
|
+
return candidate.code ?? candidate.cause?.code;
|
|
3285
|
+
}
|
|
3286
|
+
function readPid(value) {
|
|
3287
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
3288
|
+
}
|
|
3289
|
+
function isLoopbackHost(host) {
|
|
3290
|
+
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
3291
|
+
if (LOOPBACK_HOSTS.has(normalized)) {
|
|
3292
|
+
return true;
|
|
3293
|
+
}
|
|
3294
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized)) {
|
|
3295
|
+
return true;
|
|
3296
|
+
}
|
|
3297
|
+
if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
|
|
3298
|
+
return true;
|
|
3299
|
+
}
|
|
3300
|
+
return false;
|
|
3301
|
+
}
|
|
3302
|
+
function extractHost(endpoint) {
|
|
3303
|
+
const withoutScheme = endpoint.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
|
|
3304
|
+
const hostPort = withoutScheme.split(/[\/?#]/)[0] || "";
|
|
3305
|
+
if (hostPort.startsWith("[")) {
|
|
3306
|
+
const end = hostPort.indexOf("]");
|
|
3307
|
+
if (end === -1) return null;
|
|
3308
|
+
return hostPort.slice(1, end);
|
|
3309
|
+
}
|
|
3310
|
+
const colonIndex = hostPort.lastIndexOf(":");
|
|
3311
|
+
if (colonIndex === -1) return hostPort;
|
|
3312
|
+
return hostPort.slice(0, colonIndex);
|
|
3313
|
+
}
|
|
3314
|
+
function validateLoopbackEndpoint(endpoint) {
|
|
3315
|
+
const host = extractHost(endpoint);
|
|
3316
|
+
if (!host) {
|
|
3317
|
+
throw new Error(
|
|
3318
|
+
`CCEM Desktop control endpoint '${redactEndpoint(endpoint)}' is missing a host. Refusing to continue.`
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
if (!isLoopbackHost(host)) {
|
|
3322
|
+
throw new Error(
|
|
3323
|
+
`CCEM Desktop control endpoint '${redactEndpoint(endpoint)}' is not bound to loopback. Only 127.0.0.1, localhost, or ::1 are allowed.`
|
|
3324
|
+
);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
function isPidAlive(pid) {
|
|
3328
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
3329
|
+
try {
|
|
3330
|
+
process.kill(pid, 0);
|
|
3331
|
+
return true;
|
|
3332
|
+
} catch (error) {
|
|
3333
|
+
const code = error.code;
|
|
3334
|
+
if (code === "ESRCH") return false;
|
|
3335
|
+
if (code === "EPERM") return true;
|
|
3336
|
+
return false;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
function redactEndpoint(endpoint) {
|
|
3340
|
+
try {
|
|
3341
|
+
const url = new URL(endpoint);
|
|
3342
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
3343
|
+
} catch {
|
|
3344
|
+
return "<invalid endpoint>";
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
function isDescriptor(value) {
|
|
3348
|
+
return Boolean(
|
|
3349
|
+
value && typeof value === "object" && typeof value.endpoint === "string" && typeof value.token === "string"
|
|
3350
|
+
);
|
|
3173
3351
|
}
|
|
3174
3352
|
function resolveDesktopControlDescriptor(descriptorPath = getDesktopControlDescriptorPath()) {
|
|
3175
3353
|
if (!fs10.existsSync(descriptorPath)) {
|
|
@@ -3181,27 +3359,93 @@ function resolveDesktopControlDescriptor(descriptorPath = getDesktopControlDescr
|
|
|
3181
3359
|
if (!endpoint || !token) {
|
|
3182
3360
|
throw new Error(`Invalid CCEM Desktop control descriptor at ${descriptorPath}`);
|
|
3183
3361
|
}
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3362
|
+
validateLoopbackEndpoint(endpoint);
|
|
3363
|
+
const pid = readPid(parsed.pid);
|
|
3364
|
+
if (pid !== null && !isPidAlive(pid)) {
|
|
3365
|
+
const cleanedUp = safeRemoveStaleDescriptor(descriptorPath);
|
|
3366
|
+
throw new StaleDesktopControlDescriptorError({
|
|
3367
|
+
reason: "dead-pid",
|
|
3368
|
+
descriptorPath,
|
|
3369
|
+
pid,
|
|
3370
|
+
cleanedUp
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
return { endpoint, token, pid };
|
|
3189
3374
|
}
|
|
3190
|
-
async function requestDesktopControl(method, params,
|
|
3375
|
+
async function requestDesktopControl(method, params, descriptorOrOptions, maybeOptions = {}) {
|
|
3376
|
+
const descriptorPath = getDesktopControlDescriptorPath();
|
|
3377
|
+
const hasInjectedDescriptor = isDescriptor(descriptorOrOptions);
|
|
3378
|
+
const descriptor = hasInjectedDescriptor ? descriptorOrOptions : resolveDesktopControlDescriptor(descriptorPath);
|
|
3379
|
+
const options = hasInjectedDescriptor ? maybeOptions : descriptorOrOptions ?? maybeOptions ?? {};
|
|
3380
|
+
const timeoutMs = options.timeoutMs ?? options.fetchTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
3381
|
+
const pid = readPid(descriptor.pid);
|
|
3382
|
+
validateLoopbackEndpoint(descriptor.endpoint);
|
|
3383
|
+
if (pid !== null && !isPidAlive(pid)) {
|
|
3384
|
+
const cleanedUp = hasInjectedDescriptor ? false : safeRemoveStaleDescriptor(descriptorPath);
|
|
3385
|
+
throw new StaleDesktopControlDescriptorError({
|
|
3386
|
+
reason: "dead-pid",
|
|
3387
|
+
descriptorPath,
|
|
3388
|
+
pid,
|
|
3389
|
+
cleanedUp
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3191
3392
|
const id = `ccem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3393
|
+
const controller = new AbortController();
|
|
3394
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3395
|
+
const externalSignal = options.signal;
|
|
3396
|
+
const onExternalAbort = () => controller.abort();
|
|
3397
|
+
if (externalSignal) {
|
|
3398
|
+
if (externalSignal.aborted) {
|
|
3399
|
+
clearTimeout(timer);
|
|
3400
|
+
controller.abort();
|
|
3401
|
+
} else {
|
|
3402
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
let response;
|
|
3406
|
+
try {
|
|
3407
|
+
response = await fetch(descriptor.endpoint, {
|
|
3408
|
+
method: "POST",
|
|
3409
|
+
headers: {
|
|
3410
|
+
authorization: `Bearer ${descriptor.token}`,
|
|
3411
|
+
"content-type": "application/json"
|
|
3412
|
+
},
|
|
3413
|
+
body: JSON.stringify({
|
|
3414
|
+
jsonrpc: "2.0",
|
|
3415
|
+
id,
|
|
3416
|
+
method,
|
|
3417
|
+
params: params ?? {}
|
|
3418
|
+
}),
|
|
3419
|
+
signal: controller.signal
|
|
3420
|
+
});
|
|
3421
|
+
} catch (error) {
|
|
3422
|
+
if (controller.signal.aborted) {
|
|
3423
|
+
throw new StaleDesktopControlDescriptorError({
|
|
3424
|
+
reason: "request-timeout",
|
|
3425
|
+
descriptorPath,
|
|
3426
|
+
pid,
|
|
3427
|
+
cleanedUp: false,
|
|
3428
|
+
timeoutMs,
|
|
3429
|
+
cause: error
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
const code = readErrnoCode(error);
|
|
3433
|
+
if (code && ENDPOINT_UNREACHABLE_CODES.has(code)) {
|
|
3434
|
+
throw new StaleDesktopControlDescriptorError({
|
|
3435
|
+
reason: "endpoint-unreachable",
|
|
3436
|
+
descriptorPath,
|
|
3437
|
+
pid,
|
|
3438
|
+
cleanedUp: false,
|
|
3439
|
+
cause: error
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
throw error;
|
|
3443
|
+
} finally {
|
|
3444
|
+
clearTimeout(timer);
|
|
3445
|
+
if (externalSignal) {
|
|
3446
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3205
3449
|
if (!response.ok) {
|
|
3206
3450
|
throw new Error(`CCEM Desktop control request failed: HTTP ${response.status}`);
|
|
3207
3451
|
}
|
|
@@ -3211,6 +3455,26 @@ async function requestDesktopControl(method, params, descriptor = resolveDesktop
|
|
|
3211
3455
|
}
|
|
3212
3456
|
return payload.result;
|
|
3213
3457
|
}
|
|
3458
|
+
function parseSinceOption(raw) {
|
|
3459
|
+
if (raw === void 0 || raw === null || raw === "") return null;
|
|
3460
|
+
const value = Number(raw);
|
|
3461
|
+
if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {
|
|
3462
|
+
throw new Error(
|
|
3463
|
+
`Invalid --since value '${raw}'. Expected a non-negative integer sequence number (e.g. 0, 42).`
|
|
3464
|
+
);
|
|
3465
|
+
}
|
|
3466
|
+
return value;
|
|
3467
|
+
}
|
|
3468
|
+
function parseLimitOption(raw) {
|
|
3469
|
+
if (raw === void 0 || raw === null || raw === "") return null;
|
|
3470
|
+
const value = Number(raw);
|
|
3471
|
+
if (!Number.isFinite(value) || value <= 0 || !Number.isInteger(value)) {
|
|
3472
|
+
throw new Error(
|
|
3473
|
+
`Invalid --limit value '${raw}'. Expected a positive integer (e.g. 1, 100).`
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3476
|
+
return value;
|
|
3477
|
+
}
|
|
3214
3478
|
function printJson(value) {
|
|
3215
3479
|
console.log(JSON.stringify(value, null, 2));
|
|
3216
3480
|
}
|
|
@@ -4192,10 +4456,12 @@ desktopCmd.command("status <runtimeId>").description("Get one CCEM Desktop works
|
|
|
4192
4456
|
});
|
|
4193
4457
|
desktopCmd.command("events <runtimeId>").description("Read CCEM Desktop workspace session events").option("--since <seq>", "First event sequence after this value").option("--limit <count>", "Maximum events to return").option("--json", "Output JSON").action(async function(runtimeId) {
|
|
4194
4458
|
const opts = this.opts();
|
|
4459
|
+
const sinceSeq = parseSinceOption(opts.since);
|
|
4460
|
+
const limit = parseLimitOption(opts.limit);
|
|
4195
4461
|
const result = await requestDesktopControl("ccem.workspace.getEvents", {
|
|
4196
4462
|
runtimeId,
|
|
4197
|
-
sinceSeq
|
|
4198
|
-
limit
|
|
4463
|
+
sinceSeq,
|
|
4464
|
+
limit
|
|
4199
4465
|
});
|
|
4200
4466
|
outputDesktopResult(result, opts);
|
|
4201
4467
|
});
|
|
@@ -4420,4 +4686,16 @@ Editing environment '${result.name}'`));
|
|
|
4420
4686
|
}
|
|
4421
4687
|
}
|
|
4422
4688
|
});
|
|
4423
|
-
|
|
4689
|
+
function handleCliError(error) {
|
|
4690
|
+
if (error instanceof StaleDesktopControlDescriptorError) {
|
|
4691
|
+
console.error(chalk7.red(error.message));
|
|
4692
|
+
process.exit(1);
|
|
4693
|
+
}
|
|
4694
|
+
if (error instanceof Error) {
|
|
4695
|
+
console.error(chalk7.red(error.message));
|
|
4696
|
+
process.exit(1);
|
|
4697
|
+
}
|
|
4698
|
+
console.error(chalk7.red(String(error)));
|
|
4699
|
+
process.exit(1);
|
|
4700
|
+
}
|
|
4701
|
+
await program.parseAsync(process.argv).catch(handleCliError);
|