@easonwumac/computer-linker 0.1.2

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/SECURITY.md +48 -0
  5. package/dist/api.d.ts +2 -0
  6. package/dist/api.js +360 -0
  7. package/dist/audit.d.ts +70 -0
  8. package/dist/audit.js +102 -0
  9. package/dist/capabilities.d.ts +98 -0
  10. package/dist/capabilities.js +718 -0
  11. package/dist/capability-policy.d.ts +22 -0
  12. package/dist/capability-policy.js +103 -0
  13. package/dist/chatgpt.d.ts +167 -0
  14. package/dist/chatgpt.js +561 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +4621 -0
  17. package/dist/client-smoke.d.ts +44 -0
  18. package/dist/client-smoke.js +639 -0
  19. package/dist/client.d.ts +217 -0
  20. package/dist/client.js +357 -0
  21. package/dist/codex-runs.d.ts +35 -0
  22. package/dist/codex-runs.js +66 -0
  23. package/dist/computer-contract.d.ts +33 -0
  24. package/dist/computer-contract.js +384 -0
  25. package/dist/computer-operation-registry.d.ts +45 -0
  26. package/dist/computer-operation-registry.js +179 -0
  27. package/dist/config-diagnostics.d.ts +11 -0
  28. package/dist/config-diagnostics.js +185 -0
  29. package/dist/config.d.ts +10 -0
  30. package/dist/config.js +69 -0
  31. package/dist/history-insights.d.ts +132 -0
  32. package/dist/history-insights.js +457 -0
  33. package/dist/http-auth.d.ts +3 -0
  34. package/dist/http-auth.js +15 -0
  35. package/dist/mcp-surface.d.ts +5 -0
  36. package/dist/mcp-surface.js +25 -0
  37. package/dist/oauth-provider.d.ts +52 -0
  38. package/dist/oauth-provider.js +325 -0
  39. package/dist/package-metadata.d.ts +7 -0
  40. package/dist/package-metadata.js +24 -0
  41. package/dist/permissions.d.ts +43 -0
  42. package/dist/permissions.js +150 -0
  43. package/dist/platform-shell.d.ts +28 -0
  44. package/dist/platform-shell.js +124 -0
  45. package/dist/processes.d.ts +50 -0
  46. package/dist/processes.js +178 -0
  47. package/dist/profile.d.ts +159 -0
  48. package/dist/profile.js +416 -0
  49. package/dist/screenshot.d.ts +47 -0
  50. package/dist/screenshot.js +302 -0
  51. package/dist/search.d.ts +34 -0
  52. package/dist/search.js +340 -0
  53. package/dist/security.d.ts +10 -0
  54. package/dist/security.js +108 -0
  55. package/dist/sensitive-files.d.ts +4 -0
  56. package/dist/sensitive-files.js +96 -0
  57. package/dist/server.d.ts +9 -0
  58. package/dist/server.js +713 -0
  59. package/dist/service.d.ts +125 -0
  60. package/dist/service.js +486 -0
  61. package/dist/sessions.d.ts +26 -0
  62. package/dist/sessions.js +34 -0
  63. package/dist/tunnels.d.ts +161 -0
  64. package/dist/tunnels.js +1243 -0
  65. package/dist/workspace-operations.d.ts +170 -0
  66. package/dist/workspace-operations.js +3219 -0
  67. package/dist/workspaces.d.ts +61 -0
  68. package/dist/workspaces.js +353 -0
  69. package/docs/agent-instructions.md +65 -0
  70. package/docs/alpha-evidence.example.json +54 -0
  71. package/docs/api-compatibility.md +56 -0
  72. package/docs/architecture.md +561 -0
  73. package/docs/chatgpt-setup.md +397 -0
  74. package/docs/client-recipes.md +98 -0
  75. package/docs/client-sdk.md +163 -0
  76. package/docs/computer-operation-v1.schema.json +143 -0
  77. package/docs/manual-test-plan.md +322 -0
  78. package/docs/product-spec.md +911 -0
  79. package/docs/release-checklist.md +285 -0
  80. package/docs/service-mode.md +99 -0
  81. package/examples/minimal-mcp-client.mjs +114 -0
  82. package/package.json +87 -0
@@ -0,0 +1,125 @@
1
+ import { type LocalPortConfig } from "./permissions.js";
2
+ export type ServicePlatform = "linux" | "macos" | "windows";
3
+ export type ServiceFormat = "profile" | "manifest";
4
+ export interface ServiceProfileOptions {
5
+ platform?: ServicePlatform;
6
+ nodePath?: string;
7
+ cliPath?: string;
8
+ serviceName?: string;
9
+ configDirectory?: string;
10
+ outputDir?: string;
11
+ }
12
+ export interface ServiceFileSet {
13
+ profile: string;
14
+ manifest: string;
15
+ install: string;
16
+ uninstall: string;
17
+ }
18
+ export interface ServiceProfile {
19
+ kind: "computer-linker-service-profile";
20
+ schemaVersion: 1;
21
+ platform: ServicePlatform;
22
+ serviceName: string;
23
+ label: string;
24
+ command: string[];
25
+ commandDisplay: string;
26
+ configDir: string;
27
+ configPath: string;
28
+ manifestPath: string;
29
+ manifest: string;
30
+ installCommands: string[];
31
+ uninstallCommands: string[];
32
+ statusCommands: string[];
33
+ startCommands: string[];
34
+ stopCommands: string[];
35
+ logCommands: string[];
36
+ logFiles: {
37
+ stdout: string;
38
+ stderr: string;
39
+ };
40
+ notes: string[];
41
+ }
42
+ export interface WrittenServiceProfile {
43
+ kind: "computer-linker-service-files";
44
+ outputDir: string;
45
+ platform: ServicePlatform;
46
+ files: ServiceFileSet;
47
+ }
48
+ export interface ServiceStatus {
49
+ kind: "computer-linker-service-status";
50
+ schemaVersion: 1;
51
+ platform: ServicePlatform;
52
+ serviceName: string;
53
+ label: string;
54
+ configDir: string;
55
+ configPath: string;
56
+ manifestPath: string;
57
+ manifestExists: boolean | null;
58
+ commandDisplay: string;
59
+ statusCommands: string[];
60
+ installCommands: string[];
61
+ uninstallCommands: string[];
62
+ startCommands: string[];
63
+ stopCommands: string[];
64
+ logCommands: string[];
65
+ logFiles: {
66
+ stdout: string;
67
+ stderr: string;
68
+ };
69
+ notes: string[];
70
+ }
71
+ export type ServicePlanAction = "install" | "uninstall" | "start" | "stop";
72
+ export interface ServicePlan {
73
+ kind: "computer-linker-service-plan";
74
+ schemaVersion: 1;
75
+ action: ServicePlanAction;
76
+ dryRun: boolean;
77
+ platform: ServicePlatform;
78
+ serviceName: string;
79
+ label: string;
80
+ requiresElevation: boolean;
81
+ commands: string[];
82
+ recommendedProfileCommand: string;
83
+ notes: string[];
84
+ }
85
+ export interface ServiceLogReport {
86
+ kind: "computer-linker-service-logs";
87
+ schemaVersion: 1;
88
+ platform: ServicePlatform;
89
+ serviceName: string;
90
+ label: string;
91
+ logFiles: {
92
+ stdout: string;
93
+ stderr: string;
94
+ };
95
+ stdout: {
96
+ exists: boolean;
97
+ path: string;
98
+ tail: string;
99
+ };
100
+ stderr: {
101
+ exists: boolean;
102
+ path: string;
103
+ tail: string;
104
+ };
105
+ commands: string[];
106
+ notes: string[];
107
+ }
108
+ export declare function serviceProfile(config: LocalPortConfig, options?: ServiceProfileOptions): ServiceProfile;
109
+ export declare function serviceProfileOutput(config: LocalPortConfig, options?: ServiceProfileOptions & {
110
+ format?: ServiceFormat;
111
+ }): string;
112
+ export declare function writeServiceProfileFiles(config: LocalPortConfig, options?: ServiceProfileOptions): WrittenServiceProfile;
113
+ export declare function serviceStatus(config: LocalPortConfig, options?: ServiceProfileOptions): ServiceStatus;
114
+ export declare function servicePlan(config: LocalPortConfig, action: ServicePlanAction, options?: ServiceProfileOptions & {
115
+ dryRun?: boolean;
116
+ }): ServicePlan;
117
+ export declare function defaultServiceOutputDir(options?: ServiceProfileOptions): string;
118
+ export declare function serviceLogs(config: LocalPortConfig, options?: ServiceProfileOptions & {
119
+ lines?: number;
120
+ }): ServiceLogReport;
121
+ export declare function formatServiceStatus(status: ServiceStatus): string;
122
+ export declare function formatServicePlan(plan: ServicePlan): string;
123
+ export declare function formatServiceLogs(report: ServiceLogReport): string;
124
+ export declare function parseServicePlatform(value: string | undefined): ServicePlatform;
125
+ export declare function parseServiceFormat(value: string | undefined): ServiceFormat;
@@ -0,0 +1,486 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { configDir } from "./config.js";
5
+ import { expandHomePath } from "./permissions.js";
6
+ export function serviceProfile(config, options = {}) {
7
+ const platform = options.platform ?? currentServicePlatform();
8
+ const serviceName = sanitizeServiceName(options.serviceName ?? "computer-linker");
9
+ const label = platform === "macos" ? `com.computer-linker.${serviceName}` : serviceName;
10
+ const nodePath = resolve(options.nodePath ?? process.execPath);
11
+ const cliPath = resolve(options.cliPath ?? process.argv[1] ?? "dist/cli.js");
12
+ const serviceConfigDir = resolve(expandHomePath(options.configDirectory ?? configDir()));
13
+ const command = [nodePath, cliPath, "serve", "--transport", "http"];
14
+ const manifestPath = defaultManifestPath(platform, label, serviceName);
15
+ const logFiles = serviceLogFiles(serviceConfigDir);
16
+ const manifest = serviceManifest(platform, {
17
+ label,
18
+ serviceName,
19
+ command,
20
+ configDirectory: serviceConfigDir,
21
+ });
22
+ return {
23
+ kind: "computer-linker-service-profile",
24
+ schemaVersion: 1,
25
+ platform,
26
+ serviceName,
27
+ label,
28
+ command,
29
+ commandDisplay: command.map(shellQuote).join(" "),
30
+ configDir: serviceConfigDir,
31
+ configPath: join(serviceConfigDir, "config.json"),
32
+ manifestPath,
33
+ manifest,
34
+ installCommands: installCommands(platform, manifestPath, label, serviceName),
35
+ uninstallCommands: uninstallCommands(platform, manifestPath, label, serviceName),
36
+ statusCommands: statusCommands(platform, label, serviceName),
37
+ startCommands: startCommands(platform, label, serviceName),
38
+ stopCommands: stopCommands(platform, label, serviceName),
39
+ logCommands: logCommands(platform, label, serviceName, logFiles),
40
+ logFiles,
41
+ notes: serviceNotes(platform),
42
+ };
43
+ }
44
+ export function serviceProfileOutput(config, options = {}) {
45
+ const profile = serviceProfile(config, options);
46
+ return options.format === "manifest"
47
+ ? profile.manifest
48
+ : `${JSON.stringify(profile, null, 2)}\n`;
49
+ }
50
+ export function writeServiceProfileFiles(config, options = {}) {
51
+ if (!options.outputDir || options.outputDir.startsWith("--")) {
52
+ throw new Error("service profile --output-dir requires a directory path");
53
+ }
54
+ const outputDir = resolve(expandHomePath(options.outputDir));
55
+ const profile = serviceProfile(config, options);
56
+ const extension = profile.platform === "macos" ? "plist" : profile.platform === "linux" ? "service" : "ps1";
57
+ const files = {
58
+ profile: join(outputDir, "service-profile.json"),
59
+ manifest: join(outputDir, `${profile.serviceName}.${extension}`),
60
+ install: join(outputDir, installScriptName(profile.platform)),
61
+ uninstall: join(outputDir, uninstallScriptName(profile.platform)),
62
+ };
63
+ mkdirSync(outputDir, { recursive: true });
64
+ writeFileSync(files.profile, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
65
+ writeFileSync(files.manifest, profile.manifest, { mode: 0o600 });
66
+ writeFileSync(files.install, installScriptBody(profile, files), { mode: 0o700 });
67
+ writeFileSync(files.uninstall, uninstallScriptBody(profile), { mode: 0o700 });
68
+ return {
69
+ kind: "computer-linker-service-files",
70
+ outputDir,
71
+ platform: profile.platform,
72
+ files,
73
+ };
74
+ }
75
+ export function serviceStatus(config, options = {}) {
76
+ const profile = serviceProfile(config, options);
77
+ return {
78
+ kind: "computer-linker-service-status",
79
+ schemaVersion: 1,
80
+ platform: profile.platform,
81
+ serviceName: profile.serviceName,
82
+ label: profile.label,
83
+ configDir: profile.configDir,
84
+ configPath: profile.configPath,
85
+ manifestPath: profile.manifestPath,
86
+ manifestExists: profile.platform === "windows" ? null : existsSync(profile.manifestPath),
87
+ commandDisplay: profile.commandDisplay,
88
+ statusCommands: profile.statusCommands,
89
+ installCommands: profile.installCommands,
90
+ uninstallCommands: profile.uninstallCommands,
91
+ startCommands: profile.startCommands,
92
+ stopCommands: profile.stopCommands,
93
+ logCommands: profile.logCommands,
94
+ logFiles: profile.logFiles,
95
+ notes: profile.notes,
96
+ };
97
+ }
98
+ export function servicePlan(config, action, options = {}) {
99
+ const profile = serviceProfile(config, options);
100
+ const commands = action === "install"
101
+ ? profile.installCommands
102
+ : action === "uninstall"
103
+ ? profile.uninstallCommands
104
+ : action === "start"
105
+ ? profile.startCommands
106
+ : profile.stopCommands;
107
+ return {
108
+ kind: "computer-linker-service-plan",
109
+ schemaVersion: 1,
110
+ action,
111
+ dryRun: options.dryRun ?? true,
112
+ platform: profile.platform,
113
+ serviceName: profile.serviceName,
114
+ label: profile.label,
115
+ requiresElevation: serviceActionRequiresElevation(profile.platform, action),
116
+ commands,
117
+ recommendedProfileCommand: `computer-linker service profile --platform ${profile.platform} --output-dir ${shellQuote(defaultServiceOutputDir(options))}`,
118
+ notes: [
119
+ options.dryRun === false
120
+ ? "This action may change the OS service manager."
121
+ : "Dry run only. No service files were written and no OS service was changed.",
122
+ ...profile.notes,
123
+ ],
124
+ };
125
+ }
126
+ export function defaultServiceOutputDir(options = {}) {
127
+ const serviceConfigDir = resolve(expandHomePath(options.configDirectory ?? configDir()));
128
+ return resolve(expandHomePath(options.outputDir ?? join(serviceConfigDir, "service-profile")));
129
+ }
130
+ export function serviceLogs(config, options = {}) {
131
+ const profile = serviceProfile(config, options);
132
+ const lines = normalizeLogLines(options.lines);
133
+ return {
134
+ kind: "computer-linker-service-logs",
135
+ schemaVersion: 1,
136
+ platform: profile.platform,
137
+ serviceName: profile.serviceName,
138
+ label: profile.label,
139
+ logFiles: profile.logFiles,
140
+ stdout: readLogTail(profile.logFiles.stdout, lines),
141
+ stderr: readLogTail(profile.logFiles.stderr, lines),
142
+ commands: profile.logCommands,
143
+ notes: serviceLogNotes(profile.platform),
144
+ };
145
+ }
146
+ export function formatServiceStatus(status) {
147
+ const manifest = status.manifestExists === null
148
+ ? "service-manager"
149
+ : status.manifestExists ? "present" : "missing";
150
+ return [
151
+ `Computer Linker service status (${status.platform})`,
152
+ `serviceName: ${status.serviceName}`,
153
+ `label: ${status.label}`,
154
+ `configPath: ${status.configPath}`,
155
+ `manifestPath: ${status.manifestPath}`,
156
+ `manifest: ${manifest}`,
157
+ `command: ${status.commandDisplay}`,
158
+ "status commands:",
159
+ ...status.statusCommands.map((command) => ` ${command}`),
160
+ "daily commands:",
161
+ ...status.startCommands.map((command) => ` start: ${command}`),
162
+ ...status.stopCommands.map((command) => ` stop: ${command}`),
163
+ ...status.logCommands.map((command) => ` logs: ${command}`),
164
+ "notes:",
165
+ ...status.notes.map((note) => ` - ${note}`),
166
+ ].join("\n") + "\n";
167
+ }
168
+ export function formatServicePlan(plan) {
169
+ return [
170
+ `Computer Linker service ${plan.action}${plan.dryRun ? " dry run" : ""} (${plan.platform})`,
171
+ `serviceName: ${plan.serviceName}`,
172
+ `requiresElevation: ${plan.requiresElevation ? "yes" : "no"}`,
173
+ `profileCommand: ${plan.recommendedProfileCommand}`,
174
+ "commands:",
175
+ ...plan.commands.map((command) => ` ${command}`),
176
+ "notes:",
177
+ ...plan.notes.map((note) => ` - ${note}`),
178
+ ].join("\n") + "\n";
179
+ }
180
+ export function formatServiceLogs(report) {
181
+ const lines = [
182
+ `Computer Linker service logs (${report.platform})`,
183
+ `serviceName: ${report.serviceName}`,
184
+ `stdout: ${report.stdout.path} (${report.stdout.exists ? "present" : "missing"})`,
185
+ report.stdout.tail ? report.stdout.tail : " (no stdout log content)",
186
+ `stderr: ${report.stderr.path} (${report.stderr.exists ? "present" : "missing"})`,
187
+ report.stderr.tail ? report.stderr.tail : " (no stderr log content)",
188
+ "commands:",
189
+ ...report.commands.map((command) => ` ${command}`),
190
+ "notes:",
191
+ ...report.notes.map((note) => ` - ${note}`),
192
+ ];
193
+ return `${lines.join("\n")}\n`;
194
+ }
195
+ export function parseServicePlatform(value) {
196
+ if (!value)
197
+ return currentServicePlatform();
198
+ if (value === "linux" || value === "macos" || value === "windows")
199
+ return value;
200
+ throw new Error("service --platform must be one of: linux, macos, windows");
201
+ }
202
+ export function parseServiceFormat(value) {
203
+ if (!value)
204
+ return "profile";
205
+ if (value === "profile" || value === "manifest")
206
+ return value;
207
+ throw new Error("service --format must be one of: profile, manifest");
208
+ }
209
+ function currentServicePlatform() {
210
+ if (process.platform === "darwin")
211
+ return "macos";
212
+ if (process.platform === "win32")
213
+ return "windows";
214
+ return "linux";
215
+ }
216
+ function defaultManifestPath(platform, label, serviceName) {
217
+ if (platform === "macos")
218
+ return join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
219
+ if (platform === "windows")
220
+ return `${serviceName} Windows Service`;
221
+ return `/etc/systemd/system/${serviceName}.service`;
222
+ }
223
+ function serviceManifest(platform, input) {
224
+ if (platform === "macos")
225
+ return launchdPlist(input);
226
+ if (platform === "windows")
227
+ return windowsServiceScript(input);
228
+ return systemdUnit(input);
229
+ }
230
+ function systemdUnit(input) {
231
+ return `[Unit]
232
+ Description=Computer Linker HTTP MCP server
233
+ After=network-online.target
234
+ Wants=network-online.target
235
+
236
+ [Service]
237
+ Type=simple
238
+ ExecStart=${input.command.map(systemdEscapeArg).join(" ")}
239
+ Restart=on-failure
240
+ RestartSec=3
241
+ Environment=COMPUTER_LINKER_CONFIG_DIR=${systemdEscapeArg(input.configDirectory)}
242
+
243
+ [Install]
244
+ WantedBy=multi-user.target
245
+ `;
246
+ }
247
+ function launchdPlist(input) {
248
+ const args = input.command.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
249
+ return `<?xml version="1.0" encoding="UTF-8"?>
250
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
251
+ <plist version="1.0">
252
+ <dict>
253
+ <key>Label</key>
254
+ <string>${xmlEscape(input.label)}</string>
255
+ <key>ProgramArguments</key>
256
+ <array>
257
+ ${args}
258
+ </array>
259
+ <key>EnvironmentVariables</key>
260
+ <dict>
261
+ <key>COMPUTER_LINKER_CONFIG_DIR</key>
262
+ <string>${xmlEscape(input.configDirectory)}</string>
263
+ </dict>
264
+ <key>RunAtLoad</key>
265
+ <true/>
266
+ <key>KeepAlive</key>
267
+ <true/>
268
+ <key>StandardOutPath</key>
269
+ <string>${xmlEscape(join(input.configDirectory, "service.out.log"))}</string>
270
+ <key>StandardErrorPath</key>
271
+ <string>${xmlEscape(join(input.configDirectory, "service.err.log"))}</string>
272
+ </dict>
273
+ </plist>
274
+ `;
275
+ }
276
+ function windowsServiceScript(input) {
277
+ const logFiles = serviceLogFiles(input.configDirectory);
278
+ const command = [
279
+ `if not exist ${windowsQuote(input.configDirectory)} mkdir ${windowsQuote(input.configDirectory)}`,
280
+ `set "COMPUTER_LINKER_CONFIG_DIR=${input.configDirectory}"`,
281
+ `${input.command.map(windowsQuote).join(" ")} >> ${windowsQuote(logFiles.stdout)} 2>> ${windowsQuote(logFiles.stderr)}`,
282
+ ].join(" && ");
283
+ const binPath = `${windowsQuote(process.env.ComSpec ?? "C:\\Windows\\System32\\cmd.exe")} /d /s /c ${windowsQuote(command)}`;
284
+ return `$ErrorActionPreference = "Stop"
285
+ sc.exe create ${powershellQuote(input.serviceName)} binPath= ${powershellQuote(binPath)} start= auto DisplayName= "Computer Linker"
286
+ sc.exe description ${powershellQuote(input.serviceName)} "Computer Linker HTTP MCP server"
287
+ sc.exe start ${powershellQuote(input.serviceName)}
288
+ `;
289
+ }
290
+ function installCommands(platform, manifestPath, label, serviceName) {
291
+ if (platform === "macos") {
292
+ return [
293
+ `mkdir -p ${shellQuote(dirnamePath(manifestPath))}`,
294
+ `cp ./service-profile/${basename(manifestPath)} ${shellQuote(manifestPath)}`,
295
+ `launchctl bootstrap gui/$(id -u) ${shellQuote(manifestPath)}`,
296
+ `launchctl enable gui/$(id -u)/${shellQuote(label)}`,
297
+ ];
298
+ }
299
+ if (platform === "windows") {
300
+ return [
301
+ "Run install-service.ps1 from an elevated PowerShell prompt.",
302
+ `Get-Service ${serviceName}`,
303
+ ];
304
+ }
305
+ return [
306
+ `sudo cp ./service-profile/${basename(manifestPath)} ${shellQuote(manifestPath)}`,
307
+ "sudo systemctl daemon-reload",
308
+ `sudo systemctl enable --now ${shellQuote(serviceName)}`,
309
+ ];
310
+ }
311
+ function uninstallCommands(platform, manifestPath, label, serviceName) {
312
+ if (platform === "macos") {
313
+ return [
314
+ `launchctl bootout gui/$(id -u)/${shellQuote(label)} || true`,
315
+ `rm -f ${shellQuote(manifestPath)}`,
316
+ ];
317
+ }
318
+ if (platform === "windows") {
319
+ return [
320
+ `sc.exe stop ${powershellQuote(serviceName)}`,
321
+ `sc.exe delete ${powershellQuote(serviceName)}`,
322
+ ];
323
+ }
324
+ return [
325
+ `sudo systemctl disable --now ${shellQuote(serviceName)}`,
326
+ `sudo rm -f ${shellQuote(manifestPath)}`,
327
+ "sudo systemctl daemon-reload",
328
+ ];
329
+ }
330
+ function statusCommands(platform, label, serviceName) {
331
+ if (platform === "macos")
332
+ return [`launchctl print gui/$(id -u)/${label}`];
333
+ if (platform === "windows")
334
+ return [`Get-Service ${serviceName}`, `sc.exe query ${serviceName}`];
335
+ return [`systemctl status ${serviceName}`, `journalctl -u ${serviceName} -n 100 --no-pager`];
336
+ }
337
+ function startCommands(platform, label, serviceName) {
338
+ if (platform === "macos")
339
+ return [`launchctl kickstart -k gui/$(id -u)/${label}`];
340
+ if (platform === "windows")
341
+ return [`sc.exe start ${powershellQuote(serviceName)}`];
342
+ return [`sudo systemctl start ${shellQuote(serviceName)}`];
343
+ }
344
+ function stopCommands(platform, label, serviceName) {
345
+ if (platform === "macos")
346
+ return [`launchctl bootout gui/$(id -u)/${shellQuote(label)}`];
347
+ if (platform === "windows")
348
+ return [`sc.exe stop ${powershellQuote(serviceName)}`];
349
+ return [`sudo systemctl stop ${shellQuote(serviceName)}`];
350
+ }
351
+ function logCommands(platform, label, serviceName, logFiles) {
352
+ if (platform === "linux")
353
+ return [`journalctl -u ${serviceName} -n 100 --no-pager`];
354
+ if (platform === "windows") {
355
+ return [
356
+ `Get-Content -Tail 100 ${powershellQuote(logFiles.stdout)}`,
357
+ `Get-Content -Tail 100 ${powershellQuote(logFiles.stderr)}`,
358
+ ];
359
+ }
360
+ return [
361
+ `tail -n 100 ${shellQuote(logFiles.stdout)}`,
362
+ `tail -n 100 ${shellQuote(logFiles.stderr)}`,
363
+ `launchctl print gui/$(id -u)/${label}`,
364
+ ];
365
+ }
366
+ function serviceNotes(platform) {
367
+ const common = [
368
+ "Run `computer-linker init` before installing so ownerToken is configured.",
369
+ "The service starts HTTP mode on the configured host and port.",
370
+ "Use Tailscale Serve, Cloudflare Access, or equivalent controls before exposing it beyond loopback.",
371
+ ];
372
+ if (platform === "windows") {
373
+ return [...common, "Windows Service creation requires an elevated PowerShell prompt."];
374
+ }
375
+ if (platform === "linux") {
376
+ return [...common, "systemd install commands require sudo."];
377
+ }
378
+ return [...common, "launchd runs this as the current user through ~/Library/LaunchAgents."];
379
+ }
380
+ function serviceLogNotes(platform) {
381
+ if (platform === "linux") {
382
+ return ["Linux systemd services usually log to journald; use the printed journalctl command when local log files are empty."];
383
+ }
384
+ return ["Logs are written by the generated service profile after the service starts."];
385
+ }
386
+ function serviceLogFiles(serviceConfigDir) {
387
+ return {
388
+ stdout: join(serviceConfigDir, "service.out.log"),
389
+ stderr: join(serviceConfigDir, "service.err.log"),
390
+ };
391
+ }
392
+ function readLogTail(path, lines) {
393
+ if (!existsSync(path))
394
+ return { exists: false, path, tail: "" };
395
+ const content = readFileSync(path, "utf8");
396
+ return {
397
+ exists: true,
398
+ path,
399
+ tail: tailLines(content, lines),
400
+ };
401
+ }
402
+ function tailLines(content, lines) {
403
+ const items = content.split(/\r?\n/);
404
+ const hasTrailingNewline = items.length > 0 && items.at(-1) === "";
405
+ const trimmed = hasTrailingNewline ? items.slice(0, -1) : items;
406
+ return trimmed.slice(-lines).join("\n");
407
+ }
408
+ function normalizeLogLines(value) {
409
+ if (!Number.isFinite(value ?? 100))
410
+ return 100;
411
+ return Math.max(1, Math.min(1000, Math.floor(value ?? 100)));
412
+ }
413
+ function serviceActionRequiresElevation(platform, action) {
414
+ if (platform === "macos")
415
+ return false;
416
+ if (platform === "windows")
417
+ return action === "install" || action === "uninstall";
418
+ return true;
419
+ }
420
+ function sanitizeServiceName(value) {
421
+ const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
422
+ return sanitized || "computer-linker";
423
+ }
424
+ function shellQuote(value) {
425
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
426
+ return value;
427
+ return `'${value.replace(/'/g, "'\\''")}'`;
428
+ }
429
+ function systemdEscapeArg(value) {
430
+ return value.replace(/([\\s\\\\\"'`$])/g, "\\$1");
431
+ }
432
+ function windowsQuote(value) {
433
+ if (!/[\s"]/g.test(value))
434
+ return value;
435
+ return `"${value.replace(/"/g, '\\"')}"`;
436
+ }
437
+ function powershellQuote(value) {
438
+ return `'${value.replace(/'/g, "''")}'`;
439
+ }
440
+ function xmlEscape(value) {
441
+ return value
442
+ .replace(/&/g, "&amp;")
443
+ .replace(/</g, "&lt;")
444
+ .replace(/>/g, "&gt;")
445
+ .replace(/"/g, "&quot;")
446
+ .replace(/'/g, "&apos;");
447
+ }
448
+ function scriptBody(platform, commands) {
449
+ if (platform === "windows") {
450
+ return `$ErrorActionPreference = "Stop"\n${commands.join("\n")}\n`;
451
+ }
452
+ return `#!/usr/bin/env sh\nset -eu\n${commands.join("\n")}\n`;
453
+ }
454
+ function installScriptBody(profile, files) {
455
+ if (profile.platform === "windows")
456
+ return profile.manifest;
457
+ const scriptDir = "SCRIPT_DIR=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)";
458
+ if (profile.platform === "macos") {
459
+ return scriptBody("macos", [
460
+ scriptDir,
461
+ `mkdir -p ${shellQuote(dirnamePath(profile.manifestPath))}`,
462
+ `cp "$SCRIPT_DIR/${basename(files.manifest)}" ${shellQuote(profile.manifestPath)}`,
463
+ `launchctl bootstrap gui/$(id -u) ${shellQuote(profile.manifestPath)}`,
464
+ `launchctl enable gui/$(id -u)/${shellQuote(profile.label)}`,
465
+ ]);
466
+ }
467
+ return scriptBody("linux", [
468
+ scriptDir,
469
+ `sudo cp "$SCRIPT_DIR/${basename(files.manifest)}" ${shellQuote(profile.manifestPath)}`,
470
+ "sudo systemctl daemon-reload",
471
+ `sudo systemctl enable --now ${shellQuote(profile.serviceName)}`,
472
+ ]);
473
+ }
474
+ function uninstallScriptBody(profile) {
475
+ return scriptBody(profile.platform, profile.uninstallCommands);
476
+ }
477
+ function installScriptName(platform) {
478
+ return platform === "windows" ? "install-service.ps1" : "install-service.sh";
479
+ }
480
+ function uninstallScriptName(platform) {
481
+ return platform === "windows" ? "uninstall-service.ps1" : "uninstall-service.sh";
482
+ }
483
+ function dirnamePath(path) {
484
+ const index = path.lastIndexOf("/");
485
+ return index === -1 ? "." : path.slice(0, index);
486
+ }
@@ -0,0 +1,26 @@
1
+ export type SessionAuthType = "owner-token" | "oauth" | "loopback";
2
+ export interface ActiveSession {
3
+ id: string;
4
+ idPrefix: string;
5
+ createdAt: string;
6
+ lastSeenAt: string;
7
+ authType: SessionAuthType;
8
+ requestCount: number;
9
+ clientId?: string;
10
+ clientName?: string;
11
+ userAgent?: string;
12
+ remoteAddress?: string;
13
+ }
14
+ export interface RegisterSessionInput {
15
+ id: string;
16
+ authType: SessionAuthType;
17
+ clientId?: string;
18
+ clientName?: string;
19
+ userAgent?: string;
20
+ remoteAddress?: string;
21
+ }
22
+ export declare function registerActiveSession(input: RegisterSessionInput): ActiveSession;
23
+ export declare function touchActiveSession(id: string): void;
24
+ export declare function closeActiveSession(id: string): void;
25
+ export declare function listActiveSessions(): ActiveSession[];
26
+ export declare function clearActiveSessionsForTest(): void;
@@ -0,0 +1,34 @@
1
+ const activeSessions = new Map();
2
+ export function registerActiveSession(input) {
3
+ const now = new Date().toISOString();
4
+ const session = {
5
+ id: input.id,
6
+ idPrefix: input.id.slice(0, 8),
7
+ createdAt: now,
8
+ lastSeenAt: now,
9
+ authType: input.authType,
10
+ requestCount: 1,
11
+ clientId: input.clientId,
12
+ clientName: input.clientName,
13
+ userAgent: input.userAgent,
14
+ remoteAddress: input.remoteAddress,
15
+ };
16
+ activeSessions.set(input.id, session);
17
+ return session;
18
+ }
19
+ export function touchActiveSession(id) {
20
+ const session = activeSessions.get(id);
21
+ if (!session)
22
+ return;
23
+ session.lastSeenAt = new Date().toISOString();
24
+ session.requestCount += 1;
25
+ }
26
+ export function closeActiveSession(id) {
27
+ activeSessions.delete(id);
28
+ }
29
+ export function listActiveSessions() {
30
+ return Array.from(activeSessions.values()).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
31
+ }
32
+ export function clearActiveSessionsForTest() {
33
+ activeSessions.clear();
34
+ }