@ystemsrx/cfshare 0.1.4 → 0.1.6
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 +336 -336
- package/README.zh.md +336 -336
- package/dist/src/cli.js +136 -6
- package/dist/src/manager.d.ts +15 -1
- package/dist/src/manager.d.ts.map +1 -1
- package/dist/src/manager.js +479 -110
- package/dist/src/tools.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +153 -6
- package/src/manager.ts +569 -115
- package/src/tools.ts +1 -1
package/dist/src/tools.js
CHANGED
|
@@ -44,7 +44,7 @@ function registerToolsForContext(api, ctx) {
|
|
|
44
44
|
description: "List all active and tracked exposure sessions",
|
|
45
45
|
parameters: ExposureListSchema,
|
|
46
46
|
async execute() {
|
|
47
|
-
return jsonResult(manager.exposureList());
|
|
47
|
+
return jsonResult(await manager.exposureList());
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
{
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { writeFileSync } from "node:fs";
|
|
3
5
|
import fs from "node:fs/promises";
|
|
4
6
|
import os from "node:os";
|
|
5
7
|
import path from "node:path";
|
|
@@ -15,11 +17,15 @@ type CliOptions = {
|
|
|
15
17
|
configFile?: string;
|
|
16
18
|
workspaceDir?: string;
|
|
17
19
|
keepAlive?: boolean;
|
|
20
|
+
detachedWorker?: boolean;
|
|
21
|
+
handoffFile?: string;
|
|
18
22
|
compact?: boolean;
|
|
19
23
|
help?: boolean;
|
|
20
24
|
version?: boolean;
|
|
21
25
|
};
|
|
22
26
|
|
|
27
|
+
const DETACHED_HANDOFF_TIMEOUT_MS = 45_000;
|
|
28
|
+
|
|
23
29
|
const TOOL_NAMES = new Set([
|
|
24
30
|
"env_check",
|
|
25
31
|
"expose_port",
|
|
@@ -79,8 +85,8 @@ function printHelp() {
|
|
|
79
85
|
" --config <json> Runtime config JSON (same as plugin config)",
|
|
80
86
|
" --config-file <path> Read runtime config from JSON file",
|
|
81
87
|
" --workspace-dir <dir> Workspace dir for expose_files context",
|
|
82
|
-
" --keep-alive Keep process running after expose_*",
|
|
83
|
-
" --no-keep-alive Exit
|
|
88
|
+
" --keep-alive Keep process running after expose_* (foreground)",
|
|
89
|
+
" --no-keep-alive Exit after printing expose_* result (default)",
|
|
84
90
|
" --compact Compact JSON output",
|
|
85
91
|
" -h, --help Show help",
|
|
86
92
|
" -v, --version Show version",
|
|
@@ -144,6 +150,15 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
144
150
|
i += 1;
|
|
145
151
|
continue;
|
|
146
152
|
}
|
|
153
|
+
if (token === "--detached-worker") {
|
|
154
|
+
opts.detachedWorker = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (token === "--handoff-file") {
|
|
158
|
+
opts.handoffFile = assertValue(argv, i + 1, token);
|
|
159
|
+
i += 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
147
162
|
if (token === "--keep-alive") {
|
|
148
163
|
opts.keepAlive = true;
|
|
149
164
|
continue;
|
|
@@ -245,13 +260,127 @@ function createRuntimeApi(config: CfsharePluginConfig): CfshareRuntimeApi {
|
|
|
245
260
|
};
|
|
246
261
|
}
|
|
247
262
|
|
|
248
|
-
function shouldKeepAlive(
|
|
263
|
+
function shouldKeepAlive(keepAliveFlag: boolean | undefined): boolean {
|
|
249
264
|
if (typeof keepAliveFlag === "boolean") {
|
|
250
265
|
return keepAliveFlag;
|
|
251
266
|
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function isExposeCommand(command: string): boolean {
|
|
252
271
|
return command === "expose_port" || command === "expose_files";
|
|
253
272
|
}
|
|
254
273
|
|
|
274
|
+
function extractHandoffFileFromArgv(argv: string[]): string | undefined {
|
|
275
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
276
|
+
if (argv[i] !== "--handoff-file") {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const value = argv[i + 1];
|
|
280
|
+
if (value && !value.startsWith("-")) {
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function stripDetachedControlArgs(argv: string[]): string[] {
|
|
289
|
+
const out: string[] = [];
|
|
290
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
291
|
+
const token = argv[i];
|
|
292
|
+
if (token === "--keep-alive" || token === "--no-keep-alive" || token === "--detached-worker") {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (token === "--handoff-file") {
|
|
296
|
+
i += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
out.push(token);
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function makeHandoffPath(): string {
|
|
305
|
+
return path.join(
|
|
306
|
+
os.tmpdir(),
|
|
307
|
+
`cfshare-handoff-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function writeHandoffFile(filePath: string | undefined, payload: unknown): Promise<void> {
|
|
312
|
+
if (!filePath) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
316
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
317
|
+
await fs.writeFile(tempPath, JSON.stringify(payload), "utf8");
|
|
318
|
+
await fs.rename(tempPath, filePath);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function waitForDetachedHandoff(filePath: string, workerPid?: number): Promise<unknown> {
|
|
322
|
+
const startedAt = Date.now();
|
|
323
|
+
while (Date.now() - startedAt < DETACHED_HANDOFF_TIMEOUT_MS) {
|
|
324
|
+
try {
|
|
325
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
326
|
+
await fs.rm(filePath, { force: true });
|
|
327
|
+
const parsed = JSON.parse(raw) as { ok?: unknown; result?: unknown; error?: unknown };
|
|
328
|
+
if (parsed.ok === true) {
|
|
329
|
+
return parsed.result;
|
|
330
|
+
}
|
|
331
|
+
if (typeof parsed.error === "string" && parsed.error.trim()) {
|
|
332
|
+
throw new Error(parsed.error);
|
|
333
|
+
}
|
|
334
|
+
throw new Error("failed to start detached exposure");
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const errno = error as NodeJS.ErrnoException;
|
|
337
|
+
if (errno?.code !== "ENOENT" && !(error instanceof SyntaxError)) {
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (workerPid) {
|
|
345
|
+
try {
|
|
346
|
+
process.kill(workerPid, "SIGTERM");
|
|
347
|
+
} catch {
|
|
348
|
+
// ignore best-effort cleanup
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
throw new Error("timed out waiting for detached exposure startup");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function runDetachedExposureWorker(): Promise<unknown> {
|
|
355
|
+
const scriptArg = process.argv[1];
|
|
356
|
+
if (!scriptArg) {
|
|
357
|
+
throw new Error("unable to resolve cli entry");
|
|
358
|
+
}
|
|
359
|
+
const scriptPath = path.resolve(scriptArg);
|
|
360
|
+
const handoffFile = makeHandoffPath();
|
|
361
|
+
const childArgs = [
|
|
362
|
+
scriptPath,
|
|
363
|
+
...stripDetachedControlArgs(process.argv.slice(2)),
|
|
364
|
+
"--keep-alive",
|
|
365
|
+
"--detached-worker",
|
|
366
|
+
"--handoff-file",
|
|
367
|
+
handoffFile,
|
|
368
|
+
"--compact",
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
const child = spawn(process.execPath, childArgs, {
|
|
372
|
+
detached: true,
|
|
373
|
+
stdio: "ignore",
|
|
374
|
+
env: {
|
|
375
|
+
...process.env,
|
|
376
|
+
CFSHARE_DETACHED_WORKER: "1",
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
child.unref();
|
|
380
|
+
|
|
381
|
+
return await waitForDetachedHandoff(handoffFile, child.pid ?? undefined);
|
|
382
|
+
}
|
|
383
|
+
|
|
255
384
|
async function waitUntilExposureStops(manager: CfshareManager, id: string): Promise<void> {
|
|
256
385
|
await new Promise<void>((resolve, reject) => {
|
|
257
386
|
let stopping = false;
|
|
@@ -340,7 +469,7 @@ async function runTool(
|
|
|
340
469
|
);
|
|
341
470
|
}
|
|
342
471
|
if (command === "exposure_list") {
|
|
343
|
-
return manager.exposureList();
|
|
472
|
+
return await manager.exposureList();
|
|
344
473
|
}
|
|
345
474
|
if (command === "exposure_get") {
|
|
346
475
|
return await manager.exposureGet(
|
|
@@ -467,18 +596,28 @@ async function main() {
|
|
|
467
596
|
|
|
468
597
|
const params = asObject(paramsInput, "params");
|
|
469
598
|
const config = asObject(configInput, "config") as CfsharePluginConfig;
|
|
599
|
+
|
|
600
|
+
if (isExposeCommand(command) && !shouldKeepAlive(options.keepAlive) && !options.detachedWorker) {
|
|
601
|
+
const detachedResult = await runDetachedExposureWorker();
|
|
602
|
+
process.stdout.write(
|
|
603
|
+
`${JSON.stringify(detachedResult, null, options.compact ? undefined : 2)}\n`,
|
|
604
|
+
);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
470
608
|
const manager = new CfshareManager(createRuntimeApi(config));
|
|
471
609
|
|
|
472
610
|
const result = await runTool(manager, command, params, options);
|
|
611
|
+
await writeHandoffFile(options.handoffFile, { ok: true, result });
|
|
473
612
|
process.stdout.write(`${JSON.stringify(result, null, options.compact ? undefined : 2)}\n`);
|
|
474
613
|
|
|
475
|
-
if (shouldKeepAlive(
|
|
614
|
+
if (shouldKeepAlive(options.keepAlive)) {
|
|
476
615
|
const exposureId = typeof result === "object" && result ? (result as { id?: unknown }).id : undefined;
|
|
477
616
|
if (typeof exposureId !== "string" || !exposureId) {
|
|
478
617
|
return;
|
|
479
618
|
}
|
|
480
619
|
process.stderr.write(
|
|
481
|
-
`cfshare: exposure ${exposureId} is running. Press Ctrl+C to stop
|
|
620
|
+
`cfshare: exposure ${exposureId} is running. Press Ctrl+C to stop.\n`,
|
|
482
621
|
);
|
|
483
622
|
await waitUntilExposureStops(manager, exposureId);
|
|
484
623
|
}
|
|
@@ -486,6 +625,14 @@ async function main() {
|
|
|
486
625
|
|
|
487
626
|
void main().catch((error) => {
|
|
488
627
|
const message = error instanceof Error ? error.message : String(error);
|
|
628
|
+
const handoffFile = extractHandoffFileFromArgv(process.argv.slice(2));
|
|
629
|
+
if (handoffFile) {
|
|
630
|
+
try {
|
|
631
|
+
writeFileSync(handoffFile, JSON.stringify({ ok: false, error: message }), "utf8");
|
|
632
|
+
} catch {
|
|
633
|
+
// ignore handoff write failure
|
|
634
|
+
}
|
|
635
|
+
}
|
|
489
636
|
process.stderr.write(`cfshare error: ${message}\n`);
|
|
490
637
|
process.exit(1);
|
|
491
638
|
});
|