@ystemsrx/cfshare 0.1.1 → 0.1.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/README.md +33 -6
- package/README.zh.md +33 -6
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +368 -0
- package/dist/src/manager.d.ts +142 -0
- package/dist/src/manager.d.ts.map +1 -0
- package/dist/src/manager.js +2251 -0
- package/dist/src/policy.d.ts +15 -0
- package/dist/src/policy.d.ts.map +1 -0
- package/dist/src/policy.js +130 -0
- package/dist/src/schemas.d.ts +97 -0
- package/dist/src/schemas.d.ts.map +1 -0
- package/dist/src/schemas.js +149 -0
- package/dist/src/templates/fileExplorerTemplate.d.ts +12 -0
- package/dist/src/templates/fileExplorerTemplate.d.ts.map +1 -0
- package/dist/src/templates/fileExplorerTemplate.js +1340 -0
- package/dist/src/templates/markdownPreviewTemplate.d.ts +5 -0
- package/dist/src/templates/markdownPreviewTemplate.d.ts.map +1 -0
- package/dist/src/templates/markdownPreviewTemplate.js +243 -0
- package/dist/src/tools.d.ts +3 -0
- package/dist/src/tools.d.ts.map +1 -0
- package/dist/src/tools.js +138 -0
- package/dist/src/types.d.ts +100 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/package.json +10 -1
- package/skills/cfshare/SKILL.md +1 -1
- package/src/cli.ts +484 -0
- package/src/manager.ts +45 -9
- package/src/shims.d.ts +55 -0
- package/src/tools.ts +11 -2
package/src/cli.ts
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { CfshareManager, type CfshareRuntimeApi } from "./manager.js";
|
|
8
|
+
import type { CfsharePluginConfig } from "./types.js";
|
|
9
|
+
|
|
10
|
+
type CliOptions = {
|
|
11
|
+
command?: string;
|
|
12
|
+
paramsJson?: string;
|
|
13
|
+
paramsFile?: string;
|
|
14
|
+
configJson?: string;
|
|
15
|
+
configFile?: string;
|
|
16
|
+
workspaceDir?: string;
|
|
17
|
+
keepAlive?: boolean;
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
help?: boolean;
|
|
20
|
+
version?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const TOOL_NAMES = new Set([
|
|
24
|
+
"env_check",
|
|
25
|
+
"expose_port",
|
|
26
|
+
"expose_files",
|
|
27
|
+
"exposure_list",
|
|
28
|
+
"exposure_get",
|
|
29
|
+
"exposure_stop",
|
|
30
|
+
"exposure_logs",
|
|
31
|
+
"maintenance",
|
|
32
|
+
"audit_query",
|
|
33
|
+
"audit_export",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function normalizeCommand(input: string): string {
|
|
37
|
+
return input.trim().toLowerCase().replace(/-/g, "_");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function expandHome(input: string): string {
|
|
41
|
+
if (input === "~") {
|
|
42
|
+
return os.homedir();
|
|
43
|
+
}
|
|
44
|
+
if (input.startsWith("~/")) {
|
|
45
|
+
return path.join(os.homedir(), input.slice(2));
|
|
46
|
+
}
|
|
47
|
+
return input;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolvePathFromCwd(input: string): string {
|
|
51
|
+
const expanded = expandHome(input);
|
|
52
|
+
return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(process.cwd(), expanded);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printHelp() {
|
|
56
|
+
const lines = [
|
|
57
|
+
"CFShare CLI",
|
|
58
|
+
"",
|
|
59
|
+
"Usage:",
|
|
60
|
+
" cfshare <tool> [params-json] [options]",
|
|
61
|
+
"",
|
|
62
|
+
"Tools:",
|
|
63
|
+
" env_check",
|
|
64
|
+
" expose_port",
|
|
65
|
+
" expose_files",
|
|
66
|
+
" exposure_list",
|
|
67
|
+
" exposure_get",
|
|
68
|
+
" exposure_stop",
|
|
69
|
+
" exposure_logs",
|
|
70
|
+
" maintenance",
|
|
71
|
+
" audit_query",
|
|
72
|
+
" audit_export",
|
|
73
|
+
"",
|
|
74
|
+
"Options:",
|
|
75
|
+
" --params <json> Tool parameters as JSON",
|
|
76
|
+
" --params-file <path> Read tool parameters from JSON file",
|
|
77
|
+
" --config <json> Runtime config JSON (same as plugin config)",
|
|
78
|
+
" --config-file <path> Read runtime config from JSON file",
|
|
79
|
+
" --workspace-dir <dir> Workspace dir for expose_files context",
|
|
80
|
+
" --keep-alive Keep process running after expose_*",
|
|
81
|
+
" --no-keep-alive Exit immediately after expose_* result",
|
|
82
|
+
" --compact Compact JSON output",
|
|
83
|
+
" -h, --help Show help",
|
|
84
|
+
" -v, --version Show version",
|
|
85
|
+
"",
|
|
86
|
+
"Examples:",
|
|
87
|
+
" cfshare env_check",
|
|
88
|
+
" cfshare expose_port '{\"port\":3000,\"opts\":{\"access\":\"token\"}}'",
|
|
89
|
+
" cfshare expose_files --params '{\"paths\":[\"./build\"],\"opts\":{\"access\":\"none\"}}'",
|
|
90
|
+
" cfshare exposure_stop --params '{\"id\":\"all\"}'",
|
|
91
|
+
];
|
|
92
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertValue(args: string[], index: number, flag: string): string {
|
|
96
|
+
const value = args[index];
|
|
97
|
+
if (!value || value.startsWith("-")) {
|
|
98
|
+
throw new Error(`missing value for ${flag}`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseArgs(argv: string[]): CliOptions {
|
|
104
|
+
const opts: CliOptions = {};
|
|
105
|
+
const positionals: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
108
|
+
const token = argv[i];
|
|
109
|
+
if (!token) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (token === "-h" || token === "--help") {
|
|
113
|
+
opts.help = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (token === "-v" || token === "--version") {
|
|
117
|
+
opts.version = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (token === "--params") {
|
|
121
|
+
opts.paramsJson = assertValue(argv, i + 1, token);
|
|
122
|
+
i += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (token === "--params-file") {
|
|
126
|
+
opts.paramsFile = assertValue(argv, i + 1, token);
|
|
127
|
+
i += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (token === "--config") {
|
|
131
|
+
opts.configJson = assertValue(argv, i + 1, token);
|
|
132
|
+
i += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (token === "--config-file") {
|
|
136
|
+
opts.configFile = assertValue(argv, i + 1, token);
|
|
137
|
+
i += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (token === "--workspace-dir") {
|
|
141
|
+
opts.workspaceDir = assertValue(argv, i + 1, token);
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (token === "--keep-alive") {
|
|
146
|
+
opts.keepAlive = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (token === "--no-keep-alive") {
|
|
150
|
+
opts.keepAlive = false;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (token === "--compact") {
|
|
154
|
+
opts.compact = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (token.startsWith("-")) {
|
|
158
|
+
throw new Error(`unknown option: ${token}`);
|
|
159
|
+
}
|
|
160
|
+
positionals.push(token);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (positionals.length > 0) {
|
|
164
|
+
opts.command = positionals[0];
|
|
165
|
+
}
|
|
166
|
+
if (positionals.length > 1) {
|
|
167
|
+
if (opts.paramsJson || opts.paramsFile) {
|
|
168
|
+
throw new Error("params-json conflicts with --params/--params-file");
|
|
169
|
+
}
|
|
170
|
+
opts.paramsJson = positionals[1];
|
|
171
|
+
}
|
|
172
|
+
if (positionals.length > 2) {
|
|
173
|
+
throw new Error("too many positional arguments");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return opts;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function parseJsonInput(source: string, label: string): Promise<unknown> {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(source);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new Error(`failed to parse ${label}: ${String(error)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function parseJsonFile(filePath: string, label: string): Promise<unknown> {
|
|
188
|
+
const resolved = resolvePathFromCwd(filePath);
|
|
189
|
+
const content = await fs.readFile(resolved, "utf8");
|
|
190
|
+
return await parseJsonInput(content, `${label} (${resolved})`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function asObject(input: unknown, label: string): Record<string, unknown> {
|
|
194
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
195
|
+
throw new Error(`${label} must be a JSON object`);
|
|
196
|
+
}
|
|
197
|
+
return input as Record<string, unknown>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createRuntimeApi(config: CfsharePluginConfig): CfshareRuntimeApi {
|
|
201
|
+
const stringifyArgs = (args: unknown[]) =>
|
|
202
|
+
args
|
|
203
|
+
.map((value) => {
|
|
204
|
+
if (typeof value === "string") {
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
return JSON.stringify(value);
|
|
209
|
+
} catch {
|
|
210
|
+
return String(value);
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
.join(" ");
|
|
214
|
+
|
|
215
|
+
const logger = {
|
|
216
|
+
info: (...args: unknown[]) => {
|
|
217
|
+
if (process.env.CFSHARE_LOG_LEVEL === "info" || process.env.CFSHARE_LOG_LEVEL === "debug") {
|
|
218
|
+
process.stderr.write(`[cfshare] ${stringifyArgs(args)}\n`);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
warn: (...args: unknown[]) => {
|
|
222
|
+
process.stderr.write(`[cfshare][warn] ${stringifyArgs(args)}\n`);
|
|
223
|
+
},
|
|
224
|
+
error: (...args: unknown[]) => {
|
|
225
|
+
process.stderr.write(`[cfshare][error] ${stringifyArgs(args)}\n`);
|
|
226
|
+
},
|
|
227
|
+
debug: (...args: unknown[]) => {
|
|
228
|
+
if (process.env.CFSHARE_LOG_LEVEL === "debug") {
|
|
229
|
+
process.stderr.write(`[cfshare][debug] ${stringifyArgs(args)}\n`);
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
} as unknown as CfshareRuntimeApi["logger"];
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
logger,
|
|
236
|
+
resolvePath: resolvePathFromCwd,
|
|
237
|
+
pluginConfig: config,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function shouldKeepAlive(command: string, keepAliveFlag: boolean | undefined): boolean {
|
|
242
|
+
if (typeof keepAliveFlag === "boolean") {
|
|
243
|
+
return keepAliveFlag;
|
|
244
|
+
}
|
|
245
|
+
return command === "expose_port" || command === "expose_files";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function waitUntilExposureStops(manager: CfshareManager, id: string): Promise<void> {
|
|
249
|
+
await new Promise<void>((resolve, reject) => {
|
|
250
|
+
let stopping = false;
|
|
251
|
+
let interval: NodeJS.Timeout | undefined;
|
|
252
|
+
|
|
253
|
+
const shutdown = async (reason: string) => {
|
|
254
|
+
if (stopping) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
stopping = true;
|
|
258
|
+
if (interval) {
|
|
259
|
+
clearInterval(interval);
|
|
260
|
+
}
|
|
261
|
+
process.removeListener("SIGINT", onSigint);
|
|
262
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
263
|
+
try {
|
|
264
|
+
await manager.stopExposure(id, { reason });
|
|
265
|
+
} catch {
|
|
266
|
+
// best effort cleanup on signal
|
|
267
|
+
} finally {
|
|
268
|
+
resolve();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const onSigint = () => {
|
|
273
|
+
void shutdown("cli interrupted");
|
|
274
|
+
};
|
|
275
|
+
const onSigterm = () => {
|
|
276
|
+
void shutdown("cli terminated");
|
|
277
|
+
};
|
|
278
|
+
process.once("SIGINT", onSigint);
|
|
279
|
+
process.once("SIGTERM", onSigterm);
|
|
280
|
+
|
|
281
|
+
interval = setInterval(async () => {
|
|
282
|
+
try {
|
|
283
|
+
const detail = (await manager.exposureGet({ id })) as { status?: unknown };
|
|
284
|
+
const statusValue = detail.status;
|
|
285
|
+
const state =
|
|
286
|
+
typeof statusValue === "string"
|
|
287
|
+
? statusValue
|
|
288
|
+
: typeof statusValue === "object" && statusValue && "state" in statusValue
|
|
289
|
+
? String((statusValue as { state?: unknown }).state ?? "")
|
|
290
|
+
: "";
|
|
291
|
+
if (state === "stopped" || state === "expired" || state === "error" || state === "not_found") {
|
|
292
|
+
clearInterval(interval);
|
|
293
|
+
process.removeListener("SIGINT", onSigint);
|
|
294
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
295
|
+
resolve();
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
clearInterval(interval);
|
|
299
|
+
process.removeListener("SIGINT", onSigint);
|
|
300
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
301
|
+
reject(error);
|
|
302
|
+
}
|
|
303
|
+
}, 1000);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function runTool(
|
|
308
|
+
manager: CfshareManager,
|
|
309
|
+
command: string,
|
|
310
|
+
params: Record<string, unknown>,
|
|
311
|
+
opts: CliOptions,
|
|
312
|
+
): Promise<unknown> {
|
|
313
|
+
if (command === "env_check") {
|
|
314
|
+
return await manager.envCheck();
|
|
315
|
+
}
|
|
316
|
+
if (command === "expose_port") {
|
|
317
|
+
return await manager.exposePort(params as { port: number; opts?: Record<string, unknown> });
|
|
318
|
+
}
|
|
319
|
+
if (command === "expose_files") {
|
|
320
|
+
const ctx = opts.workspaceDir ? { workspaceDir: opts.workspaceDir } : undefined;
|
|
321
|
+
return await manager.exposeFiles(
|
|
322
|
+
params as {
|
|
323
|
+
paths: string[];
|
|
324
|
+
opts?: {
|
|
325
|
+
mode?: "normal" | "zip";
|
|
326
|
+
presentation?: "download" | "preview" | "raw";
|
|
327
|
+
ttl_seconds?: number;
|
|
328
|
+
access?: "token" | "basic" | "none";
|
|
329
|
+
max_downloads?: number;
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
ctx,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (command === "exposure_list") {
|
|
336
|
+
return manager.exposureList();
|
|
337
|
+
}
|
|
338
|
+
if (command === "exposure_get") {
|
|
339
|
+
return await manager.exposureGet(
|
|
340
|
+
params as {
|
|
341
|
+
id?: string;
|
|
342
|
+
ids?: string[];
|
|
343
|
+
filter?: {
|
|
344
|
+
status?: "starting" | "running" | "stopped" | "error" | "expired";
|
|
345
|
+
type?: "port" | "files";
|
|
346
|
+
};
|
|
347
|
+
fields?: Array<
|
|
348
|
+
| "id"
|
|
349
|
+
| "type"
|
|
350
|
+
| "status"
|
|
351
|
+
| "port"
|
|
352
|
+
| "public_url"
|
|
353
|
+
| "expires_at"
|
|
354
|
+
| "local_url"
|
|
355
|
+
| "stats"
|
|
356
|
+
| "file_sharing"
|
|
357
|
+
| "last_error"
|
|
358
|
+
| "manifest"
|
|
359
|
+
| "created_at"
|
|
360
|
+
>;
|
|
361
|
+
opts?: {
|
|
362
|
+
probe_public?: boolean;
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (command === "exposure_stop") {
|
|
368
|
+
const stopParams = params as { id?: string; ids?: string[]; opts?: { reason?: string } };
|
|
369
|
+
const target = stopParams.ids ?? stopParams.id;
|
|
370
|
+
if (!target) {
|
|
371
|
+
throw new Error("exposure_stop requires id or ids");
|
|
372
|
+
}
|
|
373
|
+
return await manager.stopExposure(target, stopParams.opts);
|
|
374
|
+
}
|
|
375
|
+
if (command === "exposure_logs") {
|
|
376
|
+
const logParams = params as {
|
|
377
|
+
id?: string;
|
|
378
|
+
ids?: string[];
|
|
379
|
+
opts?: { lines?: number; since_seconds?: number; component?: "tunnel" | "origin" | "all" };
|
|
380
|
+
};
|
|
381
|
+
const target = logParams.ids ?? logParams.id;
|
|
382
|
+
if (!target) {
|
|
383
|
+
throw new Error("exposure_logs requires id or ids");
|
|
384
|
+
}
|
|
385
|
+
return manager.exposureLogs(target, logParams.opts);
|
|
386
|
+
}
|
|
387
|
+
if (command === "maintenance") {
|
|
388
|
+
const maintenanceParams = params as {
|
|
389
|
+
action: "start_guard" | "run_gc" | "set_policy";
|
|
390
|
+
opts?: { policy?: unknown; ignore_patterns?: string[] };
|
|
391
|
+
};
|
|
392
|
+
return await manager.maintenance(maintenanceParams.action, maintenanceParams.opts);
|
|
393
|
+
}
|
|
394
|
+
if (command === "audit_query") {
|
|
395
|
+
const queryParams = params as {
|
|
396
|
+
filters?: {
|
|
397
|
+
id?: string;
|
|
398
|
+
event?: string;
|
|
399
|
+
type?: "port" | "files";
|
|
400
|
+
from_ts?: string;
|
|
401
|
+
to_ts?: string;
|
|
402
|
+
limit?: number;
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
return await manager.auditQuery(queryParams.filters);
|
|
406
|
+
}
|
|
407
|
+
if (command === "audit_export") {
|
|
408
|
+
const exportParams = params as {
|
|
409
|
+
range?: {
|
|
410
|
+
from_ts?: string;
|
|
411
|
+
to_ts?: string;
|
|
412
|
+
id?: string;
|
|
413
|
+
event?: string;
|
|
414
|
+
type?: "port" | "files";
|
|
415
|
+
output_path?: string;
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
return await manager.auditExport(exportParams.range);
|
|
419
|
+
}
|
|
420
|
+
throw new Error(`unsupported command: ${command}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function readVersion(): Promise<string> {
|
|
424
|
+
const packagePath = new URL("../../package.json", import.meta.url);
|
|
425
|
+
const content = await fs.readFile(packagePath, "utf8");
|
|
426
|
+
const parsed = JSON.parse(content) as { version?: string };
|
|
427
|
+
return parsed.version ?? "unknown";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function main() {
|
|
431
|
+
const options = parseArgs(process.argv.slice(2));
|
|
432
|
+
|
|
433
|
+
if (options.version) {
|
|
434
|
+
process.stdout.write(`${await readVersion()}\n`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (options.help || !options.command) {
|
|
439
|
+
printHelp();
|
|
440
|
+
process.exit(options.help ? 0 : 1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const command = normalizeCommand(options.command);
|
|
444
|
+
if (!TOOL_NAMES.has(command)) {
|
|
445
|
+
throw new Error(`unknown tool: ${options.command}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const paramsInput =
|
|
449
|
+
options.paramsJson !== undefined
|
|
450
|
+
? await parseJsonInput(options.paramsJson, "--params")
|
|
451
|
+
: options.paramsFile
|
|
452
|
+
? await parseJsonFile(options.paramsFile, "--params-file")
|
|
453
|
+
: {};
|
|
454
|
+
const configInput =
|
|
455
|
+
options.configJson !== undefined
|
|
456
|
+
? await parseJsonInput(options.configJson, "--config")
|
|
457
|
+
: options.configFile
|
|
458
|
+
? await parseJsonFile(options.configFile, "--config-file")
|
|
459
|
+
: {};
|
|
460
|
+
|
|
461
|
+
const params = asObject(paramsInput, "params");
|
|
462
|
+
const config = asObject(configInput, "config") as CfsharePluginConfig;
|
|
463
|
+
const manager = new CfshareManager(createRuntimeApi(config));
|
|
464
|
+
|
|
465
|
+
const result = await runTool(manager, command, params, options);
|
|
466
|
+
process.stdout.write(`${JSON.stringify(result, null, options.compact ? undefined : 2)}\n`);
|
|
467
|
+
|
|
468
|
+
if (shouldKeepAlive(command, options.keepAlive)) {
|
|
469
|
+
const exposureId = typeof result === "object" && result ? (result as { id?: unknown }).id : undefined;
|
|
470
|
+
if (typeof exposureId !== "string" || !exposureId) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
process.stderr.write(
|
|
474
|
+
`cfshare: exposure ${exposureId} is running. Press Ctrl+C to stop or use --no-keep-alive.\n`,
|
|
475
|
+
);
|
|
476
|
+
await waitUntilExposureStops(manager, exposureId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
void main().catch((error) => {
|
|
481
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
482
|
+
process.stderr.write(`cfshare error: ${message}\n`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
});
|
package/src/manager.ts
CHANGED
|
@@ -30,6 +30,8 @@ import type {
|
|
|
30
30
|
RateLimitPolicy,
|
|
31
31
|
} from "./types.js";
|
|
32
32
|
|
|
33
|
+
export type CfshareRuntimeApi = Pick<OpenClawPluginApi, "logger" | "resolvePath" | "pluginConfig">;
|
|
34
|
+
|
|
33
35
|
const MAX_LOG_LINES = 4000;
|
|
34
36
|
const MAX_RESPONSE_MANIFEST_ITEMS = 200;
|
|
35
37
|
const MAX_RESPONSE_MANIFEST_ITEMS_MULTI_GET = 20;
|
|
@@ -282,7 +284,22 @@ function normalizeWorkspaceRelativePath(input: string | undefined): string | und
|
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
function sanitizeFilename(input: string): string {
|
|
285
|
-
|
|
287
|
+
// Keep Unicode to preserve original filenames, but replace characters that are
|
|
288
|
+
// invalid or problematic across common filesystems (Windows in particular).
|
|
289
|
+
// We only sanitize a single path segment (basename), not a full path.
|
|
290
|
+
//
|
|
291
|
+
// Reference set:
|
|
292
|
+
// - ASCII control chars: 0x00-0x1F and 0x7F
|
|
293
|
+
// - Windows reserved: <>:"/\\|?*
|
|
294
|
+
// - Trailing dots/spaces are invalid on Windows
|
|
295
|
+
const cleaned = input
|
|
296
|
+
.replace(/[\u0000-\u001F\u007F]/g, "_")
|
|
297
|
+
.replace(/[<>:"/\\|?*]/g, "_")
|
|
298
|
+
.replace(/_+/g, "_")
|
|
299
|
+
.replace(/[. ]+$/g, "_");
|
|
300
|
+
|
|
301
|
+
const trimmed = cleaned.trim();
|
|
302
|
+
return trimmed || "item";
|
|
286
303
|
}
|
|
287
304
|
|
|
288
305
|
function ensureString(input: unknown): string | undefined {
|
|
@@ -700,7 +717,7 @@ function matchAuditFilters(
|
|
|
700
717
|
}
|
|
701
718
|
|
|
702
719
|
export class CfshareManager {
|
|
703
|
-
private readonly logger:
|
|
720
|
+
private readonly logger: CfshareRuntimeApi["logger"];
|
|
704
721
|
private readonly resolvePath: (input: string) => string;
|
|
705
722
|
private readonly pluginConfig: CfsharePluginConfig;
|
|
706
723
|
private readonly cloudflaredPathInput: string;
|
|
@@ -721,7 +738,7 @@ export class CfshareManager {
|
|
|
721
738
|
private guardTimer?: NodeJS.Timeout;
|
|
722
739
|
private readonly sessions = new Map<string, ExposureSession>();
|
|
723
740
|
|
|
724
|
-
constructor(api:
|
|
741
|
+
constructor(api: CfshareRuntimeApi) {
|
|
725
742
|
this.logger = api.logger;
|
|
726
743
|
this.resolvePath = api.resolvePath;
|
|
727
744
|
this.pluginConfig = (api.pluginConfig ?? {}) as CfsharePluginConfig;
|
|
@@ -878,7 +895,7 @@ export class CfshareManager {
|
|
|
878
895
|
return queryToken === access.token || headerToken === access.token || bearer === access.token;
|
|
879
896
|
}
|
|
880
897
|
const basic = parseBasicAuth(req.headers.authorization);
|
|
881
|
-
return basic?.username === access.username && basic
|
|
898
|
+
return basic?.username === access.username && basic?.password === access.password;
|
|
882
899
|
}
|
|
883
900
|
|
|
884
901
|
private async startReverseProxy(params: {
|
|
@@ -1097,10 +1114,12 @@ export class CfshareManager {
|
|
|
1097
1114
|
zip.outputStream.pipe(out);
|
|
1098
1115
|
|
|
1099
1116
|
for (const relPath of files) {
|
|
1100
|
-
|
|
1117
|
+
// Zip entries should use "/" separators regardless of OS.
|
|
1118
|
+
const zipEntry = relPath.split(path.sep).join("/");
|
|
1119
|
+
if (zipEntry === path.basename(zipPath)) {
|
|
1101
1120
|
continue;
|
|
1102
1121
|
}
|
|
1103
|
-
zip.addFile(path.join(workspaceDir, relPath),
|
|
1122
|
+
zip.addFile(path.join(workspaceDir, relPath), zipEntry);
|
|
1104
1123
|
}
|
|
1105
1124
|
zip.end();
|
|
1106
1125
|
});
|
|
@@ -1316,6 +1335,7 @@ export class CfshareManager {
|
|
|
1316
1335
|
res,
|
|
1317
1336
|
session: params.session,
|
|
1318
1337
|
filePath: zipBundle.zipPath,
|
|
1338
|
+
downloadName: "download.zip",
|
|
1319
1339
|
presentation: "download",
|
|
1320
1340
|
countAsDownload: true,
|
|
1321
1341
|
});
|
|
@@ -1421,10 +1441,23 @@ export class CfshareManager {
|
|
|
1421
1441
|
|
|
1422
1442
|
const sourceStat = await fs.stat(real);
|
|
1423
1443
|
const baseName = sanitizeFilename(path.basename(real) || "item");
|
|
1424
|
-
|
|
1444
|
+
const makeCandidate = (n: number) => {
|
|
1445
|
+
if (n === 0) {
|
|
1446
|
+
return baseName;
|
|
1447
|
+
}
|
|
1448
|
+
// For directories, treat dots as part of the name (do not split extension).
|
|
1449
|
+
if (sourceStat.isDirectory()) {
|
|
1450
|
+
return `${baseName}_${n}`;
|
|
1451
|
+
}
|
|
1452
|
+
// For files, keep extension stable: "a.txt" -> "a_1.txt".
|
|
1453
|
+
const parsed = path.parse(baseName);
|
|
1454
|
+
return `${parsed.name || "item"}_${n}${parsed.ext || ""}`;
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
let target = path.join(workspaceDir, makeCandidate(0));
|
|
1425
1458
|
let seq = 1;
|
|
1426
1459
|
while (await fileExists(target)) {
|
|
1427
|
-
target = path.join(workspaceDir,
|
|
1460
|
+
target = path.join(workspaceDir, makeCandidate(seq));
|
|
1428
1461
|
seq += 1;
|
|
1429
1462
|
}
|
|
1430
1463
|
|
|
@@ -1578,7 +1611,7 @@ export class CfshareManager {
|
|
|
1578
1611
|
this.appendLog(session, "tunnel", `spawn: ${cloudflaredBin} ${args.join(" ")}`);
|
|
1579
1612
|
|
|
1580
1613
|
const proc = spawn(cloudflaredBin, args, {
|
|
1581
|
-
stdio: ["
|
|
1614
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1582
1615
|
});
|
|
1583
1616
|
|
|
1584
1617
|
let settled = false;
|
|
@@ -1694,6 +1727,9 @@ export class CfshareManager {
|
|
|
1694
1727
|
return;
|
|
1695
1728
|
}
|
|
1696
1729
|
const pid = proc.pid;
|
|
1730
|
+
if (!pid) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1697
1733
|
try {
|
|
1698
1734
|
process.kill(pid, 0);
|
|
1699
1735
|
} catch {
|
package/src/shims.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
declare module "openclaw/plugin-sdk" {
|
|
2
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
3
|
+
|
|
4
|
+
export type OpenClawToolContext = {
|
|
5
|
+
workspaceDir?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type OpenClawToolDefinition = {
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters?: TSchema | Record<string, unknown>;
|
|
13
|
+
execute: (...args: any[]) => unknown | Promise<unknown>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type OpenClawPluginApi = {
|
|
17
|
+
logger: {
|
|
18
|
+
info: (...args: unknown[]) => void;
|
|
19
|
+
warn: (...args: unknown[]) => void;
|
|
20
|
+
error: (...args: unknown[]) => void;
|
|
21
|
+
debug: (...args: unknown[]) => void;
|
|
22
|
+
};
|
|
23
|
+
resolvePath: (input: string) => string;
|
|
24
|
+
pluginConfig?: Record<string, unknown>;
|
|
25
|
+
registerTool: (
|
|
26
|
+
factory:
|
|
27
|
+
| ((ctx: OpenClawToolContext) => OpenClawToolDefinition[])
|
|
28
|
+
| ((ctx: OpenClawToolContext) => Promise<OpenClawToolDefinition[]>),
|
|
29
|
+
options?: {
|
|
30
|
+
names?: string[];
|
|
31
|
+
},
|
|
32
|
+
) => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function emptyPluginConfigSchema(): Record<string, unknown>;
|
|
36
|
+
export function jsonResult<T>(value: T): T;
|
|
37
|
+
export function stringEnum<const T extends readonly string[]>(
|
|
38
|
+
values: T,
|
|
39
|
+
options?: Record<string, unknown>,
|
|
40
|
+
): TSchema;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare module "yazl" {
|
|
44
|
+
class ZipFile {
|
|
45
|
+
outputStream: NodeJS.ReadableStream;
|
|
46
|
+
addFile(realPath: string, metadataPath: string, options?: Record<string, unknown>): void;
|
|
47
|
+
end(options?: Record<string, unknown>, callback?: () => void): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const yazl: {
|
|
51
|
+
ZipFile: typeof ZipFile;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default yazl;
|
|
55
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
1
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
3
|
import { jsonResult } from "openclaw/plugin-sdk";
|
|
3
4
|
import { CfshareManager } from "./manager.js";
|
|
@@ -27,7 +28,15 @@ type ToolContext = {
|
|
|
27
28
|
workspaceDir?: string;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
type RegisteredTool = {
|
|
32
|
+
name: string;
|
|
33
|
+
label: string;
|
|
34
|
+
description: string;
|
|
35
|
+
parameters: TSchema | Record<string, unknown>;
|
|
36
|
+
execute: (...args: any[]) => Promise<unknown>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function registerToolsForContext(api: OpenClawPluginApi, ctx: ToolContext): RegisteredTool[] {
|
|
31
40
|
const manager = getManager(api);
|
|
32
41
|
|
|
33
42
|
return [
|
|
@@ -250,7 +259,7 @@ export function registerCfshareTools(api: OpenClawPluginApi) {
|
|
|
250
259
|
"audit_export",
|
|
251
260
|
];
|
|
252
261
|
|
|
253
|
-
api.registerTool((ctx) => registerToolsForContext(api, ctx), {
|
|
262
|
+
api.registerTool((ctx: ToolContext) => registerToolsForContext(api, ctx), {
|
|
254
263
|
names,
|
|
255
264
|
});
|
|
256
265
|
}
|