@jamiexiongr/panda-hub 0.1.18 → 0.1.20

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 CHANGED
@@ -2,19 +2,30 @@
2
2
 
3
3
  Published hub runtime for Panda, including the built web UI.
4
4
 
5
- ## Usage
6
-
7
- ```bash
8
- panda-hub
9
- panda-hub tailscareserv
10
- ```
11
-
12
- `tailscareserv` and `--tailscale-serve` both enable automatic `tailscale serve` publishing.
13
-
14
- When enabled, hub startup will:
15
-
16
- - detect whether `tailscale` is available and online
17
- - run `tailscale serve --bg`
5
+ ## Usage
6
+
7
+ ```bash
8
+ panda-hub
9
+ panda-hub tailscareserv
10
+ ```
11
+
12
+ `tailscareserv` and `--tailscale-serve` both enable automatic `tailscale serve` publishing.
13
+
14
+ Windows service management:
15
+
16
+ ```powershell
17
+ panda-hub service install --name=PandaHub tailscareserv
18
+ panda-hub service status
19
+ panda-hub service restart
20
+ panda-hub service uninstall
21
+ ```
22
+
23
+ `service install` stores the startup args and current `PANDA_*` environment values in the Windows service definition. If you change them later, run `service install` again to update the service.
24
+
25
+ When enabled, hub startup will:
26
+
27
+ - detect whether `tailscale` is available and online
28
+ - run `tailscale serve --bg`
18
29
  - print the generated Tailscale HTTPS URL in the startup log
19
30
 
20
31
  ## Environment
@@ -0,0 +1,455 @@
1
+ import {
2
+ configureTailscaleServe,
3
+ ensurePandaHubApiKey,
4
+ printTerminalQr,
5
+ resolveTailscalePublicationMode,
6
+ resolveTailscaleServePort,
7
+ startPandaSessionService
8
+ } from "./chunk-I2346PMO.mjs";
9
+
10
+ // release/panda-hub/src/index.ts
11
+ import fs from "fs";
12
+ import path2 from "path";
13
+ import { fileURLToPath as fileURLToPath2 } from "url";
14
+
15
+ // release/panda-hub/package.json
16
+ var package_default = {
17
+ name: "@jamiexiongr/panda-hub",
18
+ version: "0.1.20",
19
+ type: "module",
20
+ private: false,
21
+ description: "Panda hub runtime",
22
+ dependencies: {
23
+ "@fastify/compress": "^8.3.1",
24
+ "@fastify/cors": "^10.0.2",
25
+ "@fastify/websocket": "^11.0.2",
26
+ fastify: "^5.2.1",
27
+ "node-windows": "^1.0.0-beta.8",
28
+ "web-push": "^3.6.7"
29
+ },
30
+ bin: {
31
+ "panda-hub": "./bin/panda-hub.cjs"
32
+ },
33
+ exports: {
34
+ ".": "./dist/index.mjs"
35
+ },
36
+ files: [
37
+ "bin",
38
+ "dist"
39
+ ],
40
+ publishConfig: {
41
+ access: "public",
42
+ registry: "https://registry.npmjs.org/"
43
+ },
44
+ engines: {
45
+ node: ">=20.19.0"
46
+ }
47
+ };
48
+
49
+ // release/panda-hub/src/service.ts
50
+ import path from "path";
51
+ import { fileURLToPath } from "url";
52
+
53
+ // release/shared/src/windows-service.ts
54
+ import { spawnSync } from "child_process";
55
+ import { createRequire } from "module";
56
+ var require2 = createRequire(import.meta.url);
57
+ var cachedServiceConstructor = null;
58
+ var WINDOWS_SERVICE_TIMEOUT_MS = 2e4;
59
+ var ensureWindows = () => {
60
+ if (process.platform !== "win32") {
61
+ throw new Error("Windows \u670D\u52A1\u7BA1\u7406\u5F53\u524D\u53EA\u652F\u6301 Windows\u3002");
62
+ }
63
+ };
64
+ var trimToNull = (value) => {
65
+ const normalized = value?.trim() ?? "";
66
+ return normalized || null;
67
+ };
68
+ var escapePowerShellString = (value) => value.replace(/'/g, "''");
69
+ var quoteForSc = (value) => value.trim();
70
+ var describeCommandFailure = (output, fallback) => {
71
+ const normalized = trimToNull(output);
72
+ return normalized ?? fallback;
73
+ };
74
+ var runPowerShell = (script) => spawnSync("powershell.exe", ["-NoProfile", "-Command", script], {
75
+ encoding: "utf8",
76
+ windowsHide: true
77
+ });
78
+ var runSc = (args) => {
79
+ ensureWindows();
80
+ const command = ["sc.exe", ...args.map((value) => /[\s"]/u.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value)].join(" ");
81
+ return spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", command], {
82
+ encoding: "utf8",
83
+ windowsHide: true
84
+ });
85
+ };
86
+ var parseWindowsServiceState = (output) => {
87
+ const normalized = output.trim().toLowerCase();
88
+ if (normalized === "__missing__") {
89
+ return "missing";
90
+ }
91
+ if (normalized === "running") {
92
+ return "running";
93
+ }
94
+ if (normalized === "stopped") {
95
+ return "stopped";
96
+ }
97
+ return "unknown";
98
+ };
99
+ var queryWindowsServiceStatus = (name) => {
100
+ ensureWindows();
101
+ const result = runPowerShell(
102
+ [
103
+ `$service = Get-Service -Name '${escapePowerShellString(name)}' -ErrorAction SilentlyContinue`,
104
+ `if ($null -eq $service) { '__MISSING__' } else { $service.Status.ToString() }`
105
+ ].join("; ")
106
+ );
107
+ const rawOutput = `${result.stdout ?? ""}
108
+ ${result.stderr ?? ""}`.trim();
109
+ const state = parseWindowsServiceState(rawOutput);
110
+ return {
111
+ name,
112
+ exists: state !== "missing",
113
+ state,
114
+ rawOutput
115
+ };
116
+ };
117
+ var wait = (ms) => new Promise((resolve) => {
118
+ setTimeout(resolve, ms);
119
+ });
120
+ var waitForWindowsServiceState = async (name, desiredState, timeoutMs = WINDOWS_SERVICE_TIMEOUT_MS) => {
121
+ const startedAt = Date.now();
122
+ let latest = queryWindowsServiceStatus(name);
123
+ while (latest.state !== desiredState && Date.now() - startedAt < timeoutMs) {
124
+ await wait(750);
125
+ latest = queryWindowsServiceStatus(name);
126
+ }
127
+ return latest;
128
+ };
129
+ var startWindowsService = async (name) => {
130
+ ensureWindows();
131
+ const current = queryWindowsServiceStatus(name);
132
+ if (!current.exists) {
133
+ throw new Error(`Windows \u670D\u52A1 ${name} \u4E0D\u5B58\u5728\u3002`);
134
+ }
135
+ if (current.state === "running") {
136
+ return current;
137
+ }
138
+ const result = runPowerShell(`Start-Service -Name '${escapePowerShellString(name)}' -ErrorAction Stop`);
139
+ const output = `${result.stdout ?? ""}
140
+ ${result.stderr ?? ""}`.trim();
141
+ const latest = await waitForWindowsServiceState(name, "running");
142
+ if (latest.state !== "running") {
143
+ throw new Error(describeCommandFailure(output || latest.rawOutput, `\u542F\u52A8\u670D\u52A1 ${name} \u5931\u8D25\u3002`));
144
+ }
145
+ return latest;
146
+ };
147
+ var stopWindowsService = async (name) => {
148
+ ensureWindows();
149
+ const current = queryWindowsServiceStatus(name);
150
+ if (!current.exists || current.state === "stopped") {
151
+ return current;
152
+ }
153
+ const result = runPowerShell(`Stop-Service -Name '${escapePowerShellString(name)}' -Force -ErrorAction Stop`);
154
+ const output = `${result.stdout ?? ""}
155
+ ${result.stderr ?? ""}`.trim();
156
+ const latest = await waitForWindowsServiceState(name, "stopped");
157
+ if (latest.state !== "stopped") {
158
+ throw new Error(describeCommandFailure(output || latest.rawOutput, `\u505C\u6B62\u670D\u52A1 ${name} \u5931\u8D25\u3002`));
159
+ }
160
+ return latest;
161
+ };
162
+ var restartWindowsService = async (name) => {
163
+ await stopWindowsService(name);
164
+ return await startWindowsService(name);
165
+ };
166
+ var collectServiceEnvironment = (env) => Object.entries(env ?? process.env).filter(([key, value]) => key.startsWith("PANDA_") && typeof value === "string" && value.trim()).map(([name, value]) => ({
167
+ name,
168
+ value: value.trim()
169
+ }));
170
+ var getNodeWindowsServiceConstructor = () => {
171
+ if (cachedServiceConstructor) {
172
+ return cachedServiceConstructor;
173
+ }
174
+ const mod = require2("node-windows");
175
+ if (!mod.Service) {
176
+ throw new Error("\u7F3A\u5C11 node-windows \u4F9D\u8D56\uFF0C\u65E0\u6CD5\u6CE8\u518C Windows \u670D\u52A1\u3002");
177
+ }
178
+ cachedServiceConstructor = mod.Service;
179
+ return cachedServiceConstructor;
180
+ };
181
+ var createNodeWindowsService = (definition) => {
182
+ ensureWindows();
183
+ const Service = getNodeWindowsServiceConstructor();
184
+ return new Service({
185
+ name: definition.name,
186
+ description: definition.description,
187
+ script: definition.scriptPath,
188
+ scriptOptions: trimToNull(definition.scriptOptions) ?? void 0,
189
+ workingDirectory: trimToNull(definition.workingDirectory) ?? void 0,
190
+ execPath: trimToNull(definition.execPath) ?? process.execPath,
191
+ env: collectServiceEnvironment(definition.env)
192
+ });
193
+ };
194
+ var waitForNodeWindowsAction = async (service, action) => await new Promise((resolve, reject) => {
195
+ let settled = false;
196
+ const finish = (error) => {
197
+ if (settled) {
198
+ return;
199
+ }
200
+ settled = true;
201
+ if (error) {
202
+ reject(error);
203
+ return;
204
+ }
205
+ resolve();
206
+ };
207
+ const fail = (error) => {
208
+ finish(error instanceof Error ? error : new Error(String(error)));
209
+ };
210
+ service.once(action, () => finish());
211
+ service.once(action === "install" ? "alreadyinstalled" : "alreadyuninstalled", () => finish());
212
+ service.once(
213
+ "invalidinstallation",
214
+ () => fail(new Error(`Windows \u670D\u52A1 ${service.exists ? "\u5B89\u88C5" : "\u5378\u8F7D"}\u72B6\u6001\u65E0\u6548\u3002`))
215
+ );
216
+ service.once("error", fail);
217
+ try {
218
+ service[action]();
219
+ } catch (error) {
220
+ fail(error);
221
+ }
222
+ });
223
+ var uninstallWindowsService = async (definition) => {
224
+ ensureWindows();
225
+ const current = queryWindowsServiceStatus(definition.name);
226
+ if (!current.exists) {
227
+ return current;
228
+ }
229
+ if (current.state === "running") {
230
+ await stopWindowsService(definition.name);
231
+ }
232
+ const service = createNodeWindowsService(definition);
233
+ await waitForNodeWindowsAction(service, "uninstall");
234
+ return queryWindowsServiceStatus(definition.name);
235
+ };
236
+ var installOrUpdateWindowsService = async (definition, options) => {
237
+ ensureWindows();
238
+ const existing = queryWindowsServiceStatus(definition.name);
239
+ if (existing.exists) {
240
+ await uninstallWindowsService(definition);
241
+ }
242
+ const service = createNodeWindowsService(definition);
243
+ await waitForNodeWindowsAction(service, "install");
244
+ runSc(["config", quoteForSc(definition.name), "start=", "auto"]);
245
+ let started = false;
246
+ let startError = null;
247
+ if (options?.start !== false) {
248
+ try {
249
+ const startedStatus = await startWindowsService(definition.name);
250
+ started = startedStatus.state === "running";
251
+ } catch (error) {
252
+ startError = error instanceof Error ? error.message : String(error);
253
+ }
254
+ }
255
+ return {
256
+ status: queryWindowsServiceStatus(definition.name),
257
+ started,
258
+ startError
259
+ };
260
+ };
261
+
262
+ // release/panda-hub/src/service.ts
263
+ var currentDirectory = path.dirname(fileURLToPath(import.meta.url));
264
+ var packageRoot = path.resolve(currentDirectory, "..");
265
+ var defaultServiceName = "PandaHub";
266
+ var trimToNull2 = (value) => {
267
+ const normalized = value?.trim() ?? "";
268
+ return normalized || null;
269
+ };
270
+ var normalizeServiceName = (value) => trimToNull2(value) ?? defaultServiceName;
271
+ var joinScriptOptions = (argv) => argv.map((value) => value.trim()).filter(Boolean).join(" ");
272
+ var buildHubServiceDefinition = (options) => ({
273
+ name: normalizeServiceName(options?.name),
274
+ description: "Panda Hub Windows service",
275
+ scriptPath: path.join(packageRoot, "bin", "panda-hub.cjs"),
276
+ scriptOptions: trimToNull2(options?.scriptOptions),
277
+ workingDirectory: packageRoot,
278
+ env: options?.env ?? process.env
279
+ });
280
+ var parseHubServiceCommand = (argv) => {
281
+ let action = null;
282
+ let name = null;
283
+ let shouldStart = true;
284
+ const runtimeArgs = [];
285
+ for (let index = 0; index < argv.length; index += 1) {
286
+ const candidate = argv[index]?.trim() ?? "";
287
+ if (!candidate) {
288
+ continue;
289
+ }
290
+ if (!action) {
291
+ action = candidate.toLowerCase();
292
+ continue;
293
+ }
294
+ const normalized = candidate.toLowerCase();
295
+ if (normalized === "--name" || normalized === "name" || normalized === "--service-name" || normalized === "service-name") {
296
+ name = trimToNull2(argv[index + 1]);
297
+ index += 1;
298
+ continue;
299
+ }
300
+ if (normalized.startsWith("--name=") || normalized.startsWith("name=") || normalized.startsWith("--service-name=") || normalized.startsWith("service-name=")) {
301
+ name = trimToNull2(candidate.slice(candidate.indexOf("=") + 1));
302
+ continue;
303
+ }
304
+ if (normalized === "--no-start") {
305
+ shouldStart = false;
306
+ continue;
307
+ }
308
+ runtimeArgs.push(candidate);
309
+ }
310
+ return {
311
+ action: action ?? "status",
312
+ name,
313
+ shouldStart,
314
+ runtimeArgs
315
+ };
316
+ };
317
+ var manageJamiexiongrHubService = async (options) => {
318
+ const logger = options?.logger ?? console;
319
+ const parsed = parseHubServiceCommand(options?.argv ?? []);
320
+ const action = parsed.action;
321
+ const definition = buildHubServiceDefinition({
322
+ name: parsed.name,
323
+ scriptOptions: joinScriptOptions(parsed.runtimeArgs),
324
+ env: options?.env
325
+ });
326
+ if (action === "install" || action === "update" || action === "upsert" || action === "sync") {
327
+ const result = await installOrUpdateWindowsService(definition, {
328
+ start: parsed.shouldStart
329
+ });
330
+ if (result.startError) {
331
+ logger.warn(
332
+ `Windows \u670D\u52A1 ${definition.name} \u5DF2\u6CE8\u518C\uFF0C\u4F46\u542F\u52A8\u5931\u8D25\uFF1A${result.startError}`
333
+ );
334
+ return result;
335
+ }
336
+ logger.info(
337
+ result.started ? `Windows \u670D\u52A1 ${definition.name} \u5DF2\u6CE8\u518C\u5E76\u542F\u52A8\u3002` : `Windows \u670D\u52A1 ${definition.name} \u5DF2\u6CE8\u518C\u3002`
338
+ );
339
+ return result;
340
+ }
341
+ if (action === "uninstall" || action === "remove" || action === "delete") {
342
+ const status = await uninstallWindowsService(definition);
343
+ logger.info(
344
+ status.exists ? `Windows \u670D\u52A1 ${definition.name} \u5378\u8F7D\u540E\u4ECD\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u7CFB\u7EDF\u670D\u52A1\u7BA1\u7406\u5668\u3002` : `Windows \u670D\u52A1 ${definition.name} \u5DF2\u5378\u8F7D\u3002`
345
+ );
346
+ return {
347
+ status,
348
+ started: false,
349
+ startError: null
350
+ };
351
+ }
352
+ if (action === "start") {
353
+ const status = await startWindowsService(definition.name);
354
+ logger.info(`Windows \u670D\u52A1 ${definition.name} \u5F53\u524D\u72B6\u6001\uFF1A${status.state}`);
355
+ return {
356
+ status,
357
+ started: status.state === "running",
358
+ startError: null
359
+ };
360
+ }
361
+ if (action === "stop") {
362
+ const status = await stopWindowsService(definition.name);
363
+ logger.info(`Windows \u670D\u52A1 ${definition.name} \u5F53\u524D\u72B6\u6001\uFF1A${status.state}`);
364
+ return {
365
+ status,
366
+ started: false,
367
+ startError: null
368
+ };
369
+ }
370
+ if (action === "restart") {
371
+ const status = await restartWindowsService(definition.name);
372
+ logger.info(`Windows \u670D\u52A1 ${definition.name} \u5F53\u524D\u72B6\u6001\uFF1A${status.state}`);
373
+ return {
374
+ status,
375
+ started: status.state === "running",
376
+ startError: null
377
+ };
378
+ }
379
+ if (action === "status") {
380
+ const status = queryWindowsServiceStatus(definition.name);
381
+ logger.info(
382
+ status.exists ? `Windows \u670D\u52A1 ${definition.name} \u5F53\u524D\u72B6\u6001\uFF1A${status.state}` : `Windows \u670D\u52A1 ${definition.name} \u5C1A\u672A\u6CE8\u518C\u3002`
383
+ );
384
+ return {
385
+ status,
386
+ started: status.state === "running",
387
+ startError: null
388
+ };
389
+ }
390
+ throw new Error(`Unknown hub service action: ${action}`);
391
+ };
392
+
393
+ // release/panda-hub/src/index.ts
394
+ var currentDirectory2 = path2.dirname(fileURLToPath2(import.meta.url));
395
+ var resolveBundledWebUiDir = () => {
396
+ const candidates = [
397
+ path2.resolve(currentDirectory2, "web"),
398
+ path2.resolve(currentDirectory2, "../dist/web"),
399
+ path2.resolve(currentDirectory2, "../web")
400
+ ];
401
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
402
+ };
403
+ var startJamiexiongrHub = async (options) => {
404
+ const publishMode = options?.tailscalePublicationMode ?? resolveTailscalePublicationMode({
405
+ envPrefix: "PANDA_HUB"
406
+ });
407
+ const port = Number(process.env.PANDA_HUB_PORT ?? 4343);
408
+ const tailscaleServe = configureTailscaleServe({
409
+ enabled: publishMode !== "disabled",
410
+ mode: publishMode === "disabled" ? void 0 : publishMode,
411
+ serviceName: "panda-hub",
412
+ localPort: port,
413
+ servePort: resolveTailscaleServePort({
414
+ envPrefix: "PANDA_HUB",
415
+ defaultPort: 443
416
+ }),
417
+ logger: console
418
+ });
419
+ const hubApiKey = await ensurePandaHubApiKey({
420
+ configuredApiKey: process.env.PANDA_HUB_API_KEY ?? null,
421
+ codexHome: process.env.PANDA_CODEX_HOME ?? null,
422
+ logger: console
423
+ });
424
+ if (hubApiKey.apiKey) {
425
+ process.env.PANDA_HUB_API_KEY = hubApiKey.apiKey;
426
+ }
427
+ const webUiDir = resolveBundledWebUiDir();
428
+ const app = await startPandaSessionService({
429
+ serviceName: "panda-hub",
430
+ mode: "hub",
431
+ port,
432
+ transport: "hub-routed",
433
+ version: package_default.version,
434
+ webUiDir
435
+ });
436
+ if (tailscaleServe.active && tailscaleServe.baseUrl) {
437
+ if (tailscaleServe.mode === "funnel") {
438
+ console.info(`Panda hub Public HTTPS URL: ${tailscaleServe.baseUrl}`);
439
+ console.info(`Public PWA install URL: ${tailscaleServe.baseUrl}`);
440
+ } else {
441
+ console.info(`Panda hub Tailscale HTTPS URL: ${tailscaleServe.baseUrl}`);
442
+ console.info(`Agent hub URL env: PANDA_HUB_URL=${tailscaleServe.baseUrl}`);
443
+ }
444
+ printTerminalQr(tailscaleServe.baseUrl, {
445
+ logger: console,
446
+ label: tailscaleServe.mode === "funnel" ? "Scan this QR code to open the public Panda hub on your phone:" : "Scan this QR code to open Panda hub on your phone:"
447
+ });
448
+ }
449
+ return app;
450
+ };
451
+
452
+ export {
453
+ manageJamiexiongrHubService,
454
+ startJamiexiongrHub
455
+ };