@ystemsrx/cfshare 0.1.5 → 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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ystemsrx/cfshare",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin to safely expose local ports/files via Cloudflare Quick Tunnel",
6
6
  "license": "MIT",
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",
@@ -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;
@@ -252,6 +267,120 @@ function shouldKeepAlive(keepAliveFlag: boolean | undefined): boolean {
252
267
  return false;
253
268
  }
254
269
 
270
+ function isExposeCommand(command: string): boolean {
271
+ return command === "expose_port" || command === "expose_files";
272
+ }
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,9 +596,19 @@ 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
614
  if (shouldKeepAlive(options.keepAlive)) {
@@ -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
  });