@codori/server 0.0.2 → 0.0.4

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 (49) hide show
  1. package/README.md +34 -0
  2. package/client-dist/200.html +1 -1
  3. package/client-dist/404.html +1 -1
  4. package/client-dist/_nuxt/5cGRqVd7.js +1 -0
  5. package/client-dist/_nuxt/B1wExIrb.js +3 -0
  6. package/client-dist/_nuxt/{bTgPCKPF.js → B3F7AEX8.js} +1 -1
  7. package/client-dist/_nuxt/BLQDSwSv.js +1 -0
  8. package/client-dist/_nuxt/CNJnWNJ6.js +1 -0
  9. package/client-dist/_nuxt/CTkc1dr0.js +30 -0
  10. package/client-dist/_nuxt/Ce2C-7Tv.js +1 -0
  11. package/client-dist/_nuxt/CnVIfmli.js +1 -0
  12. package/client-dist/_nuxt/DQ92n70Y.js +202 -0
  13. package/client-dist/_nuxt/{BqBt7DE7.js → DfKxoeGc.js} +1 -1
  14. package/client-dist/_nuxt/VKebgJ9X.js +1 -0
  15. package/client-dist/_nuxt/_6KBkEBa.js +1 -0
  16. package/client-dist/_nuxt/_threadId_.BfTZeVnD.css +1 -0
  17. package/client-dist/_nuxt/builds/latest.json +1 -1
  18. package/client-dist/_nuxt/builds/meta/9f29d3f8-cf52-453d-9a00-836b5db2a30e.json +1 -0
  19. package/client-dist/_nuxt/entry.BFUss7SH.css +1 -0
  20. package/client-dist/index.html +1 -1
  21. package/dist/attachment-store.d.ts +32 -0
  22. package/dist/attachment-store.js +84 -0
  23. package/dist/cli.d.ts +5 -1
  24. package/dist/cli.js +171 -19
  25. package/dist/config.d.ts +2 -0
  26. package/dist/config.js +2 -2
  27. package/dist/http-server.d.ts +3 -0
  28. package/dist/http-server.js +181 -2
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +3 -0
  31. package/dist/service-adapters.d.ts +39 -0
  32. package/dist/service-adapters.js +185 -0
  33. package/dist/service-update.d.ts +26 -0
  34. package/dist/service-update.js +196 -0
  35. package/dist/service.d.ts +86 -0
  36. package/dist/service.js +616 -0
  37. package/package.json +6 -1
  38. package/client-dist/_nuxt/7-GD2bnK.js +0 -1
  39. package/client-dist/_nuxt/B3mzt2bn.js +0 -1
  40. package/client-dist/_nuxt/BC-nNqet.js +0 -1
  41. package/client-dist/_nuxt/BZ0PKZpG.js +0 -1
  42. package/client-dist/_nuxt/BtYrONTB.js +0 -30
  43. package/client-dist/_nuxt/CG5UFFba.js +0 -203
  44. package/client-dist/_nuxt/CcHTZT9i.js +0 -1
  45. package/client-dist/_nuxt/D39j61CJ.js +0 -1
  46. package/client-dist/_nuxt/Dgfnd7_d.js +0 -1
  47. package/client-dist/_nuxt/_threadId_.DWwkJvLa.css +0 -1
  48. package/client-dist/_nuxt/builds/meta/3230b6da-fb0f-48a4-a654-1a7f8145eecd.json +0 -1
  49. package/client-dist/_nuxt/entry.V8kD4EEO.css +0 -1
@@ -0,0 +1,616 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import os from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { createDarwinServiceDefinition, createLinuxServiceDefinition, getDarwinInstallCommands, getDarwinRestartCommands, getDarwinUninstallCommands, getLinuxInstallCommands, getLinuxRestartCommands, getLinuxUninstallCommands, resolveServicePlatform } from './service-adapters.js';
8
+ import { DEFAULT_SERVER_PORT, resolveCodoriHome } from './config.js';
9
+ import { CodoriError } from './errors.js';
10
+ import { scanProjects } from './project-scanner.js';
11
+ const WORKSPACE_MARKER_NAMES = [
12
+ 'package.json',
13
+ 'pnpm-workspace.yaml',
14
+ 'turbo.json'
15
+ ];
16
+ const SUPPORTED_SERVICE_SCOPES = new Set(['user', 'system']);
17
+ const WILDCARD_HOST_WARNING = [
18
+ 'Binding Codori to 0.0.0.0 can expose it without authentication.',
19
+ 'Set up a firewall or use a private network such as Tailscale before continuing.'
20
+ ].join(' ');
21
+ export const CODORI_SERVICE_MANAGED_ENV = 'CODORI_SERVICE_MANAGED';
22
+ export const CODORI_SERVICE_INSTALL_ID_ENV = 'CODORI_SERVICE_INSTALL_ID';
23
+ export const CODORI_SERVICE_SCOPE_ENV = 'CODORI_SERVICE_SCOPE';
24
+ const defaultCommandRunner = (command, args) => new Promise((resolvePromise, reject) => {
25
+ const child = spawn(command, args, {
26
+ stdio: ['ignore', 'pipe', 'pipe']
27
+ });
28
+ let stdout = '';
29
+ let stderr = '';
30
+ child.stdout.on('data', (chunk) => {
31
+ stdout += chunk.toString();
32
+ });
33
+ child.stderr.on('data', (chunk) => {
34
+ stderr += chunk.toString();
35
+ });
36
+ child.once('error', reject);
37
+ child.once('close', (exitCode) => {
38
+ resolvePromise({
39
+ exitCode,
40
+ stdout,
41
+ stderr
42
+ });
43
+ });
44
+ });
45
+ const shellEscape = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
46
+ const findFirstIpv4 = (values) => {
47
+ if (!Array.isArray(values)) {
48
+ return null;
49
+ }
50
+ for (const value of values) {
51
+ if (typeof value === 'string' && /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value)) {
52
+ return value;
53
+ }
54
+ }
55
+ return null;
56
+ };
57
+ const writeLine = (stream, message) => {
58
+ stream.write(`${message}\n`);
59
+ };
60
+ const getCurrentUserId = () => (typeof process.getuid === 'function' ? process.getuid() : 0);
61
+ const createDefaultPrompt = (input = process.stdin, output = process.stdout) => {
62
+ const rl = createInterface({
63
+ input,
64
+ output
65
+ });
66
+ return {
67
+ input: async (message, defaultValue) => {
68
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
69
+ const answer = (await rl.question(`${message}${suffix}: `)).trim();
70
+ return answer || defaultValue || '';
71
+ },
72
+ confirm: async (message, defaultValue) => {
73
+ const suffix = defaultValue ? ' [Y/n]' : ' [y/N]';
74
+ const answer = (await rl.question(`${message}${suffix}: `)).trim().toLowerCase();
75
+ if (!answer) {
76
+ return defaultValue;
77
+ }
78
+ if (answer === 'y' || answer === 'yes') {
79
+ return true;
80
+ }
81
+ if (answer === 'n' || answer === 'no') {
82
+ return false;
83
+ }
84
+ return defaultValue;
85
+ },
86
+ close: async () => {
87
+ rl.close();
88
+ }
89
+ };
90
+ };
91
+ const buildCanonicalInvocation = (command, options) => {
92
+ const parts = ['npx', '@codori/server', command];
93
+ if (options.root) {
94
+ parts.push('--root', options.root);
95
+ }
96
+ if (options.host) {
97
+ parts.push('--host', options.host);
98
+ }
99
+ if (typeof options.port === 'number') {
100
+ parts.push('--port', String(options.port));
101
+ }
102
+ if (options.scope) {
103
+ parts.push('--scope', options.scope);
104
+ }
105
+ if (options.yes) {
106
+ parts.push('--yes');
107
+ }
108
+ return parts.map(shellEscape).join(' ');
109
+ };
110
+ const ensureDirectory = (path) => {
111
+ mkdirSync(path, { recursive: true });
112
+ };
113
+ const ensureExistingDirectory = (path) => {
114
+ if (!existsSync(path) || !statSync(path).isDirectory()) {
115
+ throw new CodoriError('INVALID_ROOT', `Project root "${path}" does not exist or is not a directory.`);
116
+ }
117
+ };
118
+ const normalizeServiceMetadata = (value) => {
119
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
120
+ return null;
121
+ }
122
+ const record = value;
123
+ if (typeof record.installId !== 'string'
124
+ || typeof record.root !== 'string'
125
+ || typeof record.host !== 'string'
126
+ || typeof record.port !== 'number'
127
+ || (record.scope !== 'user' && record.scope !== 'system')
128
+ || (record.platform !== 'darwin' && record.platform !== 'linux')
129
+ || typeof record.serviceName !== 'string'
130
+ || typeof record.serviceFilePath !== 'string'
131
+ || typeof record.launcherPath !== 'string'
132
+ || typeof record.installedAt !== 'string') {
133
+ return null;
134
+ }
135
+ return record;
136
+ };
137
+ const loadServiceMetadata = (root, homeDir = os.homedir()) => {
138
+ const installId = toServiceInstallId(root);
139
+ const metadataPath = getServiceMetadataPath(installId, homeDir);
140
+ if (!existsSync(metadataPath)) {
141
+ throw new CodoriError('SERVICE_NOT_INSTALLED', `No service metadata was found for ${resolve(root)}. Install it first with npx @codori/server install-service.`);
142
+ }
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(readFileSync(metadataPath, 'utf8'));
146
+ }
147
+ catch (error) {
148
+ throw new CodoriError('INVALID_SERVICE_METADATA', `Failed to parse ${metadataPath}.`, error);
149
+ }
150
+ const metadata = normalizeServiceMetadata(parsed);
151
+ if (!metadata) {
152
+ throw new CodoriError('INVALID_SERVICE_METADATA', `Service metadata at ${metadataPath} is malformed.`);
153
+ }
154
+ return metadata;
155
+ };
156
+ const resolveServiceDefinition = (metadata, homeDir) => {
157
+ const metadataDirectory = getServiceMetadataDirectory(metadata.installId, homeDir);
158
+ const launcherPath = getServiceLauncherPath(metadata.installId, homeDir);
159
+ if (metadata.platform === 'darwin') {
160
+ return createDarwinServiceDefinition({
161
+ installId: metadata.installId,
162
+ scope: metadata.scope,
163
+ launcherPath,
164
+ root: metadata.root,
165
+ metadataDirectory,
166
+ homeDir
167
+ });
168
+ }
169
+ return createLinuxServiceDefinition({
170
+ installId: metadata.installId,
171
+ scope: metadata.scope,
172
+ launcherPath,
173
+ root: metadata.root,
174
+ metadataDirectory,
175
+ homeDir
176
+ });
177
+ };
178
+ const resolveServiceCommands = (action, metadata, definition) => {
179
+ if (metadata.platform === 'darwin') {
180
+ if (action === 'install') {
181
+ return getDarwinInstallCommands(definition, metadata.scope);
182
+ }
183
+ if (action === 'restart') {
184
+ return getDarwinRestartCommands(definition, metadata.scope);
185
+ }
186
+ return getDarwinUninstallCommands(definition, metadata.scope);
187
+ }
188
+ if (action === 'install') {
189
+ return getLinuxInstallCommands(definition, metadata.scope);
190
+ }
191
+ if (action === 'restart') {
192
+ return getLinuxRestartCommands(definition, metadata.scope);
193
+ }
194
+ return getLinuxUninstallCommands(definition, metadata.scope);
195
+ };
196
+ const runCommandSequence = async (commands, runCommand, allowFailure = () => false) => {
197
+ for (const command of commands) {
198
+ let result;
199
+ try {
200
+ result = await runCommand(command.command, command.args);
201
+ }
202
+ catch (error) {
203
+ throw new CodoriError('SERVICE_COMMAND_FAILED', `Failed to execute ${command.command} ${command.args.join(' ')}.`, error);
204
+ }
205
+ if (result.exitCode === 0 || allowFailure(command, result)) {
206
+ continue;
207
+ }
208
+ throw new CodoriError('SERVICE_COMMAND_FAILED', `Command failed: ${command.command} ${command.args.join(' ')}`, result.stderr || result.stdout || null);
209
+ }
210
+ };
211
+ const ensureLinuxServiceManager = async (scope, runCommand) => {
212
+ let version;
213
+ try {
214
+ version = await runCommand('systemctl', ['--version']);
215
+ }
216
+ catch (error) {
217
+ throw new CodoriError('UNSUPPORTED_SERVICE_MANAGER', 'systemctl is required on Linux.', error);
218
+ }
219
+ if (version.exitCode !== 0) {
220
+ throw new CodoriError('UNSUPPORTED_SERVICE_MANAGER', 'systemctl is required on Linux.');
221
+ }
222
+ if (scope === 'system') {
223
+ return;
224
+ }
225
+ const environment = await runCommand('systemctl', ['--user', 'show-environment']);
226
+ if (environment.exitCode !== 0) {
227
+ throw new CodoriError('UNSUPPORTED_SERVICE_MANAGER', 'systemd user services are unavailable for this session.', environment.stderr || environment.stdout || null);
228
+ }
229
+ };
230
+ const ensureSystemScopePrivileges = (scope, command, options) => {
231
+ if (scope !== 'system' || getCurrentUserId() === 0) {
232
+ return;
233
+ }
234
+ const rerun = `sudo ${buildCanonicalInvocation(command, options)}`;
235
+ throw new CodoriError('SERVICE_REQUIRES_SUDO', `System service registration requires elevated privileges. Re-run with: ${rerun}`);
236
+ };
237
+ const shouldIgnoreCommandFailure = (action, metadata, command) => metadata.platform === 'darwin' && action !== 'restart' && command.args[0] === 'bootout';
238
+ const resolveRootWithPrompt = async (root, yes, cwd, prompt) => {
239
+ if (root) {
240
+ const resolvedRoot = resolve(root);
241
+ ensureExistingDirectory(resolvedRoot);
242
+ return resolvedRoot;
243
+ }
244
+ const defaultRoot = detectRootPromptDefault(cwd);
245
+ if (yes) {
246
+ if (!defaultRoot.value) {
247
+ throw new CodoriError('MISSING_ROOT', 'Project root is required. Pass --root or run interactively from a likely project root.');
248
+ }
249
+ ensureExistingDirectory(defaultRoot.value);
250
+ return defaultRoot.value;
251
+ }
252
+ if (defaultRoot.value) {
253
+ const useDefault = await prompt.confirm(`Use ${defaultRoot.value} as the project root`, true);
254
+ if (useDefault) {
255
+ ensureExistingDirectory(defaultRoot.value);
256
+ return defaultRoot.value;
257
+ }
258
+ }
259
+ const answer = await prompt.input('Project root directory', defaultRoot.value ?? undefined);
260
+ if (!answer) {
261
+ throw new CodoriError('MISSING_ROOT', 'Project root is required.');
262
+ }
263
+ const resolvedRoot = resolve(answer);
264
+ ensureExistingDirectory(resolvedRoot);
265
+ return resolvedRoot;
266
+ };
267
+ const resolvePromptedScope = async (scope, yes, prompt) => {
268
+ if (scope || yes) {
269
+ return resolveServiceScope(scope);
270
+ }
271
+ return resolveServiceScope(await prompt.input('Service scope', 'user'));
272
+ };
273
+ const resolvePromptedPort = async (port, yes, prompt) => {
274
+ const explicitPort = parseServicePort(port);
275
+ if (explicitPort !== undefined || yes) {
276
+ return resolveDefaultServicePort(explicitPort);
277
+ }
278
+ const answer = await prompt.input('Port for the Codori server', String(DEFAULT_SERVER_PORT));
279
+ return resolveDefaultServicePort(parseServicePort(answer));
280
+ };
281
+ const resolvePromptedHost = async (host, yes, prompt, runCommand, stdout) => {
282
+ const hostDefault = await resolveHostPromptDefault(host, runCommand);
283
+ if (hostDefault.warning) {
284
+ writeLine(stdout, `Warning: ${hostDefault.warning}`);
285
+ }
286
+ if (host || yes) {
287
+ return hostDefault.value;
288
+ }
289
+ const answer = await prompt.input('Host to bind Codori', hostDefault.value);
290
+ return answer || hostDefault.value;
291
+ };
292
+ const writeLauncherAndServiceFiles = (metadata, definition, homeDir, nodePath, npxPath) => {
293
+ const metadataDirectory = getServiceMetadataDirectory(metadata.installId, homeDir);
294
+ const launcherPath = getServiceLauncherPath(metadata.installId, homeDir);
295
+ ensureDirectory(metadataDirectory);
296
+ ensureDirectory(dirname(definition.serviceFilePath));
297
+ const launcherScript = buildLauncherScript({
298
+ installId: metadata.installId,
299
+ root: metadata.root,
300
+ host: metadata.host,
301
+ port: metadata.port,
302
+ scope: metadata.scope,
303
+ nodePath,
304
+ npxPath
305
+ });
306
+ writeFileSync(launcherPath, `${launcherScript}\n`, 'utf8');
307
+ chmodSync(launcherPath, 0o755);
308
+ writeFileSync(definition.serviceFilePath, `${definition.serviceFileContents}\n`, 'utf8');
309
+ };
310
+ const writeServiceMetadata = (metadata, homeDir) => {
311
+ const metadataPath = getServiceMetadataPath(metadata.installId, homeDir);
312
+ ensureDirectory(dirname(metadataPath));
313
+ writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
314
+ };
315
+ const printInstallSummary = (stdout, summary) => {
316
+ writeLine(stdout, 'Service installation summary:');
317
+ writeLine(stdout, ` root: ${summary.root}`);
318
+ writeLine(stdout, ` host: ${summary.host}`);
319
+ writeLine(stdout, ` port: ${summary.port}`);
320
+ writeLine(stdout, ` scope: ${summary.scope}`);
321
+ writeLine(stdout, ` launcher: ${summary.launcherPath}`);
322
+ writeLine(stdout, ` service file: ${summary.serviceFilePath}`);
323
+ };
324
+ const createOperationMetadata = (installId, action, values, now, previousMetadata) => ({
325
+ installId,
326
+ root: values.root,
327
+ host: values.host,
328
+ port: values.port,
329
+ scope: values.scope,
330
+ platform: values.platform,
331
+ serviceName: values.definition.serviceName,
332
+ serviceFilePath: values.definition.serviceFilePath,
333
+ launcherPath: getServiceLauncherPath(installId, values.homeDir),
334
+ installedAt: action === 'install'
335
+ ? now().toISOString()
336
+ : previousMetadata?.installedAt ?? now().toISOString()
337
+ });
338
+ export const toServiceInstallId = (root) => createHash('sha256').update(resolve(root)).digest('hex').slice(0, 12);
339
+ export const getServiceMetadataDirectory = (installId, homeDir = os.homedir()) => join(resolveCodoriHome(homeDir), 'services', installId);
340
+ export const getServiceMetadataPath = (installId, homeDir = os.homedir()) => join(getServiceMetadataDirectory(installId, homeDir), 'service.json');
341
+ export const getServiceLauncherPath = (installId, homeDir = os.homedir()) => join(getServiceMetadataDirectory(installId, homeDir), 'run-service.sh');
342
+ export const detectRootPromptDefault = (cwd) => {
343
+ const resolvedCwd = resolve(cwd);
344
+ if (existsSync(join(resolvedCwd, '.git'))) {
345
+ return {
346
+ value: resolvedCwd,
347
+ reason: 'git',
348
+ shouldConfirm: true
349
+ };
350
+ }
351
+ if (WORKSPACE_MARKER_NAMES.some(marker => existsSync(join(resolvedCwd, marker)))) {
352
+ return {
353
+ value: resolvedCwd,
354
+ reason: 'workspace-marker',
355
+ shouldConfirm: true
356
+ };
357
+ }
358
+ if (scanProjects(resolvedCwd).length > 0) {
359
+ return {
360
+ value: resolvedCwd,
361
+ reason: 'nested-git-projects',
362
+ shouldConfirm: true
363
+ };
364
+ }
365
+ return {
366
+ value: null,
367
+ reason: 'none',
368
+ shouldConfirm: false
369
+ };
370
+ };
371
+ export const resolveServiceScope = (value) => {
372
+ if (!value) {
373
+ return 'user';
374
+ }
375
+ if (SUPPORTED_SERVICE_SCOPES.has(value)) {
376
+ return value;
377
+ }
378
+ throw new Error(`Unsupported service scope "${value}". Expected "user" or "system".`);
379
+ };
380
+ export const parseServicePort = (value) => {
381
+ if (value === undefined) {
382
+ return undefined;
383
+ }
384
+ const parsed = typeof value === 'number'
385
+ ? value
386
+ : Number.parseInt(value, 10);
387
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
388
+ throw new Error(`Invalid service port "${value}". Expected an integer between 1 and 65535.`);
389
+ }
390
+ return parsed;
391
+ };
392
+ export const resolveDefaultServicePort = (value) => value ?? DEFAULT_SERVER_PORT;
393
+ export const getWildcardHostWarning = () => WILDCARD_HOST_WARNING;
394
+ export const detectTailscaleIpv4 = async (runCommand = defaultCommandRunner) => {
395
+ try {
396
+ const status = await runCommand('tailscale', ['status', '--json']);
397
+ if (status.exitCode === 0) {
398
+ const parsed = JSON.parse(status.stdout);
399
+ if (typeof parsed === 'object' && parsed !== null) {
400
+ const backendState = 'BackendState' in parsed ? parsed.BackendState : undefined;
401
+ const self = 'Self' in parsed ? parsed.Self : undefined;
402
+ const selfIps = typeof self === 'object' && self !== null && 'TailscaleIPs' in self
403
+ ? self.TailscaleIPs
404
+ : undefined;
405
+ const statusIps = 'TailscaleIPs' in parsed ? parsed.TailscaleIPs : undefined;
406
+ if (backendState === 'Running') {
407
+ return findFirstIpv4(selfIps) ?? findFirstIpv4(statusIps);
408
+ }
409
+ }
410
+ }
411
+ }
412
+ catch {
413
+ // Fall back to the direct IP command.
414
+ }
415
+ try {
416
+ const ipResult = await runCommand('tailscale', ['ip', '-4']);
417
+ if (ipResult.exitCode !== 0) {
418
+ return null;
419
+ }
420
+ const line = ipResult.stdout
421
+ .split(/\r?\n/u)
422
+ .map(value => value.trim())
423
+ .find(Boolean);
424
+ return line && /^\d{1,3}(?:\.\d{1,3}){3}$/.test(line) ? line : null;
425
+ }
426
+ catch {
427
+ return null;
428
+ }
429
+ };
430
+ export const resolveHostPromptDefault = async (explicitHost, runCommand = defaultCommandRunner) => {
431
+ if (explicitHost) {
432
+ return {
433
+ value: explicitHost,
434
+ source: 'explicit',
435
+ warning: explicitHost === '0.0.0.0' ? WILDCARD_HOST_WARNING : null
436
+ };
437
+ }
438
+ const tailscaleIpv4 = await detectTailscaleIpv4(runCommand);
439
+ if (tailscaleIpv4) {
440
+ return {
441
+ value: tailscaleIpv4,
442
+ source: 'tailscale',
443
+ warning: null
444
+ };
445
+ }
446
+ return {
447
+ value: '0.0.0.0',
448
+ source: 'wildcard',
449
+ warning: WILDCARD_HOST_WARNING
450
+ };
451
+ };
452
+ export const buildLauncherScript = ({ installId, root, host, port, scope, nodePath, npxPath }) => {
453
+ const pathEntries = Array.from(new Set([dirname(nodePath), dirname(npxPath)]));
454
+ const exportPath = `${pathEntries.map(shellEscape).join(':')}:$PATH`;
455
+ return [
456
+ '#!/bin/sh',
457
+ 'set -eu',
458
+ `export PATH=${exportPath}`,
459
+ `export ${CODORI_SERVICE_MANAGED_ENV}=1`,
460
+ `export ${CODORI_SERVICE_INSTALL_ID_ENV}=${shellEscape(installId)}`,
461
+ `export ${CODORI_SERVICE_SCOPE_ENV}=${shellEscape(scope)}`,
462
+ `exec ${shellEscape(npxPath)} --yes @codori/server serve --root ${shellEscape(resolve(root))} --host ${shellEscape(host)} --port ${port}`
463
+ ].join('\n');
464
+ };
465
+ export const installService = async (options = {}, dependencies = {}) => {
466
+ const runCommand = dependencies.runCommand ?? defaultCommandRunner;
467
+ const stdout = dependencies.stdout ?? process.stdout;
468
+ const cwd = dependencies.cwd ?? process.cwd();
469
+ const homeDir = dependencies.homeDir ?? os.homedir();
470
+ const nodePath = dependencies.nodePath ?? process.execPath;
471
+ const npxPath = dependencies.npxPath ?? join(dirname(nodePath), 'npx');
472
+ const platform = resolveServicePlatform(dependencies.platform);
473
+ const now = dependencies.now ?? (() => new Date());
474
+ const prompt = dependencies.prompt ?? createDefaultPrompt((dependencies.stdin ?? process.stdin), stdout);
475
+ const yes = options.yes ?? false;
476
+ try {
477
+ const root = await resolveRootWithPrompt(options.root, yes, cwd, prompt);
478
+ const host = await resolvePromptedHost(options.host, yes, prompt, runCommand, stdout);
479
+ const port = await resolvePromptedPort(options.port, yes, prompt);
480
+ const scope = await resolvePromptedScope(options.scope, yes, prompt);
481
+ ensureSystemScopePrivileges(scope, 'install-service', {
482
+ root,
483
+ host,
484
+ port,
485
+ scope,
486
+ yes
487
+ });
488
+ if (platform === 'linux') {
489
+ await ensureLinuxServiceManager(scope, runCommand);
490
+ }
491
+ const installId = toServiceInstallId(root);
492
+ const definition = resolveServiceDefinition({
493
+ installId,
494
+ scope,
495
+ root,
496
+ platform
497
+ }, homeDir);
498
+ printInstallSummary(stdout, {
499
+ root,
500
+ host,
501
+ port,
502
+ scope,
503
+ launcherPath: getServiceLauncherPath(installId, homeDir),
504
+ serviceFilePath: definition.serviceFilePath
505
+ });
506
+ if (!yes) {
507
+ const confirmed = await prompt.confirm('Install this service now', true);
508
+ if (!confirmed) {
509
+ throw new CodoriError('SERVICE_ABORTED', 'Service installation was cancelled.');
510
+ }
511
+ }
512
+ const metadata = createOperationMetadata(installId, 'install', {
513
+ root,
514
+ host,
515
+ port,
516
+ scope,
517
+ platform,
518
+ definition,
519
+ homeDir
520
+ }, now);
521
+ writeLauncherAndServiceFiles(metadata, definition, homeDir, nodePath, npxPath);
522
+ await runCommandSequence(resolveServiceCommands('install', metadata, definition), runCommand, (command, result) => shouldIgnoreCommandFailure('install', metadata, command) && result.exitCode !== 0);
523
+ writeServiceMetadata(metadata, homeDir);
524
+ return {
525
+ action: 'install',
526
+ metadata
527
+ };
528
+ }
529
+ finally {
530
+ if (!dependencies.prompt) {
531
+ await prompt.close();
532
+ }
533
+ }
534
+ };
535
+ export const restartService = async (options = {}, dependencies = {}) => {
536
+ const runCommand = dependencies.runCommand ?? defaultCommandRunner;
537
+ const cwd = dependencies.cwd ?? process.cwd();
538
+ const homeDir = dependencies.homeDir ?? os.homedir();
539
+ const nodePath = dependencies.nodePath ?? process.execPath;
540
+ const npxPath = dependencies.npxPath ?? join(dirname(nodePath), 'npx');
541
+ const prompt = dependencies.prompt ?? createDefaultPrompt((dependencies.stdin ?? process.stdin), (dependencies.stdout ?? process.stdout));
542
+ const yes = options.yes ?? false;
543
+ try {
544
+ const root = await resolveRootWithPrompt(options.root, yes, cwd, prompt);
545
+ const metadata = loadServiceMetadata(root, homeDir);
546
+ if (options.scope) {
547
+ const requestedScope = resolveServiceScope(options.scope);
548
+ if (requestedScope !== metadata.scope) {
549
+ throw new CodoriError('SERVICE_SCOPE_MISMATCH', `Installed scope is ${metadata.scope}, not ${requestedScope}.`);
550
+ }
551
+ }
552
+ ensureSystemScopePrivileges(metadata.scope, 'restart-service', {
553
+ root: metadata.root,
554
+ scope: metadata.scope,
555
+ yes
556
+ });
557
+ if (metadata.platform === 'linux') {
558
+ await ensureLinuxServiceManager(metadata.scope, runCommand);
559
+ }
560
+ const definition = resolveServiceDefinition(metadata, homeDir);
561
+ writeLauncherAndServiceFiles(metadata, definition, homeDir, nodePath, npxPath);
562
+ await runCommandSequence(resolveServiceCommands('restart', metadata, definition), runCommand);
563
+ return {
564
+ action: 'restart',
565
+ metadata
566
+ };
567
+ }
568
+ finally {
569
+ if (!dependencies.prompt) {
570
+ await prompt.close();
571
+ }
572
+ }
573
+ };
574
+ export const uninstallService = async (options = {}, dependencies = {}) => {
575
+ const runCommand = dependencies.runCommand ?? defaultCommandRunner;
576
+ const cwd = dependencies.cwd ?? process.cwd();
577
+ const homeDir = dependencies.homeDir ?? os.homedir();
578
+ const prompt = dependencies.prompt ?? createDefaultPrompt((dependencies.stdin ?? process.stdin), (dependencies.stdout ?? process.stdout));
579
+ const yes = options.yes ?? false;
580
+ try {
581
+ const root = await resolveRootWithPrompt(options.root, yes, cwd, prompt);
582
+ const metadata = loadServiceMetadata(root, homeDir);
583
+ ensureSystemScopePrivileges(metadata.scope, 'uninstall-service', {
584
+ root: metadata.root,
585
+ scope: metadata.scope,
586
+ yes
587
+ });
588
+ if (!yes) {
589
+ const confirmed = await prompt.confirm(`Remove the service for ${metadata.root}`, true);
590
+ if (!confirmed) {
591
+ throw new CodoriError('SERVICE_ABORTED', 'Service removal was cancelled.');
592
+ }
593
+ }
594
+ if (metadata.platform === 'linux') {
595
+ await ensureLinuxServiceManager(metadata.scope, runCommand);
596
+ }
597
+ const definition = resolveServiceDefinition(metadata, homeDir);
598
+ const commands = resolveServiceCommands('uninstall', metadata, definition);
599
+ if (commands.length > 0) {
600
+ const [first, ...rest] = commands;
601
+ await runCommandSequence([first], runCommand, (command, result) => shouldIgnoreCommandFailure('uninstall', metadata, command) && result.exitCode !== 0);
602
+ rmSync(definition.serviceFilePath, { force: true });
603
+ await runCommandSequence(rest, runCommand);
604
+ rmSync(getServiceMetadataDirectory(metadata.installId, homeDir), { recursive: true, force: true });
605
+ }
606
+ return {
607
+ action: 'uninstall',
608
+ metadata
609
+ };
610
+ }
611
+ finally {
612
+ if (!dependencies.prompt) {
613
+ await prompt.close();
614
+ }
615
+ }
616
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codori/server",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
5
  "description": "Codori server for Git project discovery, Codex runtime management, and bundled dashboard serving.",
6
6
  "type": "module",
@@ -41,9 +41,14 @@
41
41
  "test": "vitest run"
42
42
  },
43
43
  "dependencies": {
44
+ "@fastify/multipart": "^10.0.0",
44
45
  "@fastify/static": "^9.1.0",
45
46
  "@fastify/websocket": "^11.2.0",
46
47
  "fastify": "^5.6.1",
48
+ "mime-types": "^3.0.2",
47
49
  "ws": "^8.18.3"
50
+ },
51
+ "devDependencies": {
52
+ "@types/mime-types": "^3.0.1"
48
53
  }
49
54
  }
@@ -1 +0,0 @@
1
- import{n as ce,p as de,q as me,o as c,c as w,w as u,r as W,s as I,l as z,v as N,d as T,F as D,x as O,y as ve,P as fe,m as s,z as H,A as pe,B as ge,g as K,C as V,D as _e,E as he,a as v,t as A,b as i,e as J,G as xe,_ as be,H as ye,h as ke,i as Se,j as we,k as Pe,I as E}from"./BtYrONTB.js";import{b as Q,u as Ce,_ as ze,a as Te,n as je,t as Ie}from"./D39j61CJ.js";import{a as Ae,b as $e}from"./BZ0PKZpG.js";import{b as Re,c as Ue,i as Be,u as Ee,_ as Ne,a as Ve,d as Y}from"./CG5UFFba.js";import"./Dgfnd7_d.js";const qe={slots:{root:"inline-flex flex-row-reverse justify-end",base:"relative rounded-full ring-bg first:me-0"},variants:{size:{"3xs":{base:"ring -me-0.5"},"2xs":{base:"ring -me-0.5"},xs:{base:"ring -me-0.5"},sm:{base:"ring-2 -me-1.5"},md:{base:"ring-2 -me-1.5"},lg:{base:"ring-2 -me-1.5"},xl:{base:"ring-3 -me-2"},"2xl":{base:"ring-3 -me-2"},"3xl":{base:"ring-3 -me-2"}}},defaultVariants:{size:"md"}},Le={__name:"UAvatarGroup",props:{as:{type:null,required:!1},size:{type:null,required:!1},max:{type:[Number,String],required:!1},class:{type:null,required:!1},ui:{type:Object,required:!1}},setup(b){const l=b,y=ce(),p=de(),m=me("avatarGroup",l),k=s(()=>H({extend:H(qe),...p.ui?.avatarGroup||{}})({size:l.size})),P=s(()=>typeof l.max=="string"?Number.parseInt(l.max,10):l.max),_=s(()=>{let g=y.default?.();return g?.length&&(g=g.flatMap(f=>typeof f.type=="symbol"?typeof f.children=="string"?void 0:f.children:f).filter(Boolean)),g||[]}),a=s(()=>_.value.length?!P.value||P.value<=0?[..._.value].reverse():[..._.value].slice(0,P.value).reverse():[]),h=s(()=>_.value.length?_.value.length-a.value.length:0);return pe(ge,s(()=>({size:l.size}))),(g,f)=>(c(),w(z(fe),{as:b.as,"data-slot":"root",class:I(k.value.root({class:[z(m)?.root,l.class]}))},{default:u(()=>[h.value>0?(c(),w(W,{key:0,text:`+${h.value}`,"data-slot":"base",class:I(k.value.base({class:z(m)?.base}))},null,8,["text","class"])):N("",!0),(c(!0),T(D,null,O(a.value,(S,C)=>(c(),w(ve(S),{key:C,"data-slot":"base",class:I(k.value.base({class:z(m)?.base}))},null,8,["class"]))),128))]),_:1},8,["as","class"]))}},Me={class:"flex h-full min-h-0 flex-col divide-y divide-default"},Ge={class:"flex items-center justify-between gap-2 border-b border-default px-3 py-2"},De={class:"min-w-0"},Oe={key:0,class:"rounded-lg border border-dashed border-default px-3 py-4 text-sm text-muted"},Fe=48,He=4,Ye=K({__name:"VisualSubagentStack",props:{agents:{}},setup(b){const l=b,y=["text-emerald-700 dark:text-emerald-300","text-sky-700 dark:text-sky-300","text-amber-800 dark:text-amber-300","text-rose-700 dark:text-rose-300","text-violet-700 dark:text-violet-300","text-cyan-700 dark:text-cyan-300","text-lime-800 dark:text-lime-300","text-orange-700 dark:text-orange-300"],p=new Map,m=new Map,k=new Map,P=s(()=>l.agents.length?100/l.agents.length:100),_=e=>y[e%y.length],a=e=>{switch(e){case"running":return"info";case"pendingInit":return"primary";case"completed":return"success";case"interrupted":return"warning";case"errored":return"error";case"shutdown":case"notFound":return"neutral";default:return"neutral"}},h=e=>{switch(e){case"pendingInit":return"pending";case"running":return"running";case"completed":return"completed";case"interrupted":return"interrupted";case"errored":return"errored";case"shutdown":return"shutdown";case"notFound":return"not found";default:return"active"}},g=e=>e==="pendingInit"||e==="running",f=e=>{const t=m.get(e);t!==void 0&&(window.clearTimeout(t),m.delete(e))},S=e=>{const t=p.get(e);t&&(t.scrollTop=t.scrollHeight)},C=(e,t=0)=>{t===0&&f(e),xe(()=>{if(S(e),t>=He)return;const n=window.setTimeout(()=>{C(e,t+1)},Fe);m.set(e,n)})},q=(e,t)=>{if(!(t instanceof HTMLElement)){p.delete(e),f(e);return}p.set(e,t),C(e)},L=e=>{const t=k.get(e);if(t)return t;const n=d=>{const x=d instanceof Element?d:d&&"$el"in d&&d.$el instanceof Element?d.$el:null;q(e,x)};return k.set(e,n),n},$=s(()=>l.agents.map(e=>({threadId:e.threadId,signature:[e.messages.length,e.messages.at(-1)?.id??"",e.status??""].join(":")})));return V($,e=>{const t=new Set(e.map(n=>n.threadId));for(const{threadId:n}of e)C(n);for(const n of m.keys())t.has(n)||f(n)},{immediate:!0}),_e(()=>{for(const e of m.keys())f(e)}),(e,t)=>{const n=Q,d=Ue,x=Re;return c(),T("div",Me,[(c(!0),T(D,null,O(b.agents,(r,M)=>(c(),T("section",{key:r.threadId,class:"flex min-h-0 flex-1 flex-col bg-elevated/25",style:he({flexBasis:`${P.value}%`})},[v("header",Ge,[v("div",De,[v("p",{class:I(["truncate text-sm font-semibold",_(M)])},A(r.name),3)]),i(n,{color:a(r.status),variant:"soft",size:"sm"},{default:u(()=>[J(A(h(r.status)),1)]),_:2},1032,["color"])]),v("div",{ref_for:!0,ref:L(r.threadId),class:"min-h-0 flex-1 overflow-y-auto px-3 py-2"},[r.messages.length===0?(c(),T("div",Oe," Waiting for subagent output... ")):(c(),w(x,{key:1,messages:r.messages,status:g(r.status)?"streaming":"ready",user:{ui:{root:"scroll-mt-4",container:"gap-3 pb-8",content:"px-4 py-3 rounded-lg min-h-12"}},ui:{root:"subagent-chat-messages min-h-full min-w-0 [&>article]:min-w-0 [&_[data-slot=content]]:min-w-0"},compact:"","should-auto-scroll":""},{content:u(({message:G})=>[i(d,{message:G},null,8,["message"])]),_:1},8,["messages","status"]))],512)],4))),128))])}}}),We=Object.assign(be(Ye,[["__scopeId","data-v-fe550d8c"]]),{__name:"VisualSubagentStack"}),Ke=b=>{const l=s(()=>[...ye(b)??[]].sort((p,m)=>p.firstSeenAt-m.firstSeenAt)),y=s(()=>l.value.filter(p=>Be(p.status)));return{availablePanels:l,activePanels:y}},Je={class:"flex h-screen min-h-0 flex-1 min-w-0"},Qe={class:"min-w-0 overflow-hidden pe-2 space-y-0.5"},Xe={class:"min-w-0"},Ze={class:"truncate text-xs font-medium uppercase tracking-[0.22em] text-muted"},et={class:"min-w-0"},tt={class:"truncate text-sm font-medium text-highlighted"},st={class:"flex items-center gap-1.5 lg:gap-2"},at={class:"flex items-center justify-between gap-2 px-4 py-3"},nt={class:"flex items-center gap-2"},ct=K({__name:"[threadId]",setup(b){const l=ke(),y=Se(),{openPanel:p}=Ee(),{loaded:m,refreshProjects:k,getProject:P,pendingProjectId:_}=Ce(),a=s(()=>je(l.params.projectId)),h=s(()=>{const o=l.params.threadId;return typeof o=="string"?o:null}),g=s(()=>P(a.value)),f=s(()=>a.value?Y(a.value).subagentPanels.value:[]),{availablePanels:S,activePanels:C}=Ke(()=>f.value),q=s(()=>g.value?.projectId??a.value??"Project"),L=s(()=>a.value?Y(a.value).threadTitle.value??h.value??"Thread":h.value??"Thread"),$=s(()=>{if(!a.value)return"Offline";if(_.value===a.value)return"Starting";switch(g.value?.status){case"running":return"Running";case"stopped":return"Stopped";case"error":return"Error";default:return"Checking"}}),e=E(!1),t=E(!1),n=E(!1),d=E(0),x=s(()=>S.value.length>0),r=s(()=>e.value&&x.value),M=s(()=>r.value?"i-lucide-panel-right-close":"i-lucide-panel-right-open"),G=o=>{const j=o.replace(/\s+/g,"").trim();return Array.from(j||"AG").slice(0,2).join("")},X=s(()=>S.value.map((o,j)=>({threadId:o.threadId,name:o.name,text:G(o.name),class:["bg-emerald-500/15 text-emerald-700 ring-1 ring-inset ring-emerald-500/30 dark:text-emerald-300","bg-sky-500/15 text-sky-700 ring-1 ring-inset ring-sky-500/30 dark:text-sky-300","bg-amber-500/15 text-amber-800 ring-1 ring-inset ring-amber-500/35 dark:text-amber-300","bg-rose-500/15 text-rose-700 ring-1 ring-inset ring-rose-500/30 dark:text-rose-300","bg-violet-500/15 text-violet-700 ring-1 ring-inset ring-violet-500/30 dark:text-violet-300"][j%5]}))),Z=()=>{x.value&&(t.value=!0,e.value=!e.value)},ee=()=>{t.value=!0,e.value=!1},te=async()=>{a.value&&await y.push(Ie(a.value))};return we(()=>{m.value||k()}),V(h,()=>{t.value=!1,n.value=!1,d.value=0,e.value=!1},{immediate:!0}),V(x,o=>{o||(t.value=!1,n.value=!1,d.value=0,e.value=!1)},{immediate:!0}),V(()=>C.value.length,o=>{if(x.value){if(!n.value){n.value=!0,d.value=o,e.value=o>0;return}!t.value&&d.value===0&&o>0&&(e.value=!0),d.value=o}},{immediate:!0}),(o,j)=>{const se=W,ae=Le,R=Pe,U=ze,ne=Te,re=Ae,le=Ne,F=$e,oe=Q,ie=We,ue=Ve;return c(),T("div",Je,[i(F,{id:"thread-shell",class:"min-h-0 min-w-0 flex-1","default-size":r.value?70:void 0,"min-size":r.value?50:void 0,"max-size":r.value?75:void 0,resizable:r.value,ui:{root:"!p-0",body:"!p-0 sm:!p-0 !gap-0 sm:!gap-0"}},{header:u(()=>[i(re,{icon:"i-lucide-message-square-text",ui:{title:"min-w-0 flex-1",left:"min-w-0 flex-1",right:"shrink-0"}},{title:u(()=>[v("div",Qe,[v("div",Xe,[v("div",Ze,A(q.value),1)]),v("div",et,[v("div",tt,A(L.value),1)])])]),right:u(()=>[v("div",st,[x.value?(c(),w(U,{key:0,text:"Subagents"},{default:u(()=>[i(R,{color:r.value?"primary":"neutral",variant:r.value?"soft":"ghost",icon:M.value,size:"sm",class:"px-2 xl:ps-2 xl:pe-2.5","aria-label":r.value?"Hide subagents":"Show subagents",onClick:Z},{default:u(()=>[i(ae,{class:"hidden xl:flex",size:"xs",max:4,ui:{base:"ring-2 -me-2 first:me-0"}},{default:u(()=>[(c(!0),T(D,null,O(X.value,B=>(c(),w(se,{key:B.threadId,text:B.text,alt:B.name,class:I(B.class)},null,8,["text","alt","class"]))),128))]),_:1})]),_:1},8,["color","variant","icon","aria-label"])]),_:1})):N("",!0),i(U,{text:`RPC ${$.value}`},{default:u(()=>[i(ne,{status:$.value,pulse:"",padded:""},null,8,["status"])]),_:1},8,["text"]),i(U,{text:"New thread"},{default:u(()=>[i(R,{icon:"i-lucide-plus",color:"primary",variant:"soft",square:"",onClick:te})]),_:1}),i(U,{text:"Previous threads"},{default:u(()=>[i(R,{icon:"i-lucide-history",color:"neutral",variant:"outline",square:"",onClick:z(p)},null,8,["onClick"])]),_:1})])]),_:1})]),body:u(()=>[a.value?(c(),w(le,{key:0,"project-id":a.value,"thread-id":h.value,class:"min-h-0 flex-1"},null,8,["project-id","thread-id"])):N("",!0)]),_:1},8,["default-size","min-size","max-size","resizable"]),r.value?(c(),w(F,{key:0,id:"thread-subagents-panel",class:"h-full min-h-0","default-size":30,"min-size":20,"max-size":40,resizable:"",ui:{body:"!p-0"}},{header:u(()=>[v("div",at,[v("div",nt,[j[0]||(j[0]=v("span",{class:"text-sm font-semibold text-highlighted"},"Subagents",-1)),i(oe,{color:"primary",variant:"soft",size:"sm"},{default:u(()=>[J(A(z(S).length),1)]),_:1})]),i(R,{icon:"i-lucide-x",color:"neutral",variant:"ghost",size:"xs","aria-label":"Close subagents panel",onClick:ee})])]),body:u(()=>[i(ie,{agents:z(S),class:"h-full min-h-0"},null,8,["agents"])]),_:1})):N("",!0),i(ue,{"project-id":a.value},null,8,["project-id"])])}}});export{ct as default};
@@ -1 +0,0 @@
1
- import{u as g,_ as k,a as w,n as C}from"./D39j61CJ.js";import{g as b,h as $,i as y,j as N,o as B,d as I,b as e,w as o,a as R,k as S,l as T,m as n}from"./BtYrONTB.js";import{a as U,b as D}from"./BZ0PKZpG.js";import{u as q,_ as E,a as V}from"./CG5UFFba.js";import"./Dgfnd7_d.js";const z={class:"flex h-screen min-h-0 flex-1 min-w-0"},M={class:"flex items-center gap-2"},L=b({__name:"index",setup(O){const c=$(),u=y(),{openPanel:i}=q(),{loaded:l,refreshProjects:_,getProject:p,pendingProjectId:d}=g(),t=n(()=>C(c.params.projectId)),m=n(()=>p(t.value)),s=n(()=>{if(!t.value)return"Offline";if(d.value===t.value)return"Starting";switch(m.value?.status){case"running":return"Running";case"stopped":return"Stopped";case"error":return"Error";default:return"Checking"}}),f=async()=>{t.value&&await u.push(`/projects/${t.value}`)};return N(()=>{l.value||_()}),(W,A)=>{const h=w,a=k,r=S,v=U,j=E,x=D,P=V;return B(),I("div",z,[e(x,{id:"project-shell",class:"min-h-0 min-w-0 flex-1",ui:{root:"!p-0",body:"!p-0 sm:!p-0 !gap-0 sm:!gap-0"}},{header:o(()=>[e(v,{title:t.value??"Project",icon:"i-lucide-folder-git-2"},{right:o(()=>[R("div",M,[e(a,{text:`RPC ${s.value}`},{default:o(()=>[e(h,{status:s.value,pulse:"",padded:""},null,8,["status"])]),_:1},8,["text"]),e(a,{text:"New thread"},{default:o(()=>[e(r,{icon:"i-lucide-plus",color:"primary",variant:"soft",square:"",onClick:f})]),_:1}),e(a,{text:"Previous threads"},{default:o(()=>[e(r,{icon:"i-lucide-history",color:"neutral",variant:"outline",square:"",onClick:T(i)},null,8,["onClick"])]),_:1})])]),_:1},8,["title"])]),body:o(()=>[e(j,{"project-id":t.value??"",class:"min-h-0 flex-1"},null,8,["project-id"])]),_:1}),e(P,{"project-id":t.value},null,8,["project-id"])])}}});export{L as default};