@trebired/code-server-kit 0.2.0 → 1.0.0

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 (52) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +236 -218
  3. package/dist/diagnostics.d.ts +6 -0
  4. package/dist/diagnostics.d.ts.map +1 -0
  5. package/dist/diagnostics.js +150 -0
  6. package/dist/diagnostics.js.map +1 -0
  7. package/dist/errors.d.ts +23 -2
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/errors.js +43 -1
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +11 -6
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +10 -5
  14. package/dist/index.js.map +1 -1
  15. package/dist/logging.d.ts +5 -0
  16. package/dist/logging.d.ts.map +1 -0
  17. package/dist/logging.js +11 -0
  18. package/dist/logging.js.map +1 -0
  19. package/dist/plan.d.ts +3 -2
  20. package/dist/plan.d.ts.map +1 -1
  21. package/dist/plan.js +68 -68
  22. package/dist/plan.js.map +1 -1
  23. package/dist/preparation.d.ts +5 -0
  24. package/dist/preparation.d.ts.map +1 -0
  25. package/dist/preparation.js +194 -0
  26. package/dist/preparation.js.map +1 -0
  27. package/dist/profile.d.ts +5 -2
  28. package/dist/profile.d.ts.map +1 -1
  29. package/dist/profile.js +122 -1
  30. package/dist/profile.js.map +1 -1
  31. package/dist/proxy.d.ts +4 -2
  32. package/dist/proxy.d.ts.map +1 -1
  33. package/dist/proxy.js +62 -1
  34. package/dist/proxy.js.map +1 -1
  35. package/dist/resolve.d.ts.map +1 -1
  36. package/dist/resolve.js +43 -1
  37. package/dist/resolve.js.map +1 -1
  38. package/dist/session.d.ts +11 -0
  39. package/dist/session.d.ts.map +1 -0
  40. package/dist/session.js +770 -0
  41. package/dist/session.js.map +1 -0
  42. package/dist/spec.d.ts +3 -3
  43. package/dist/spec.d.ts.map +1 -1
  44. package/dist/spec.js +2 -33
  45. package/dist/spec.js.map +1 -1
  46. package/dist/systemd.d.ts +20 -0
  47. package/dist/systemd.d.ts.map +1 -0
  48. package/dist/systemd.js +305 -0
  49. package/dist/systemd.js.map +1 -0
  50. package/dist/types.d.ts +354 -7
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +3 -2
@@ -0,0 +1,770 @@
1
+ import fs from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import net from "node:net";
4
+ import path from "node:path";
5
+ import { CodeServerInvalidConfigurationError, CodeServerSessionLifecycleError, CodeServerSessionReuseConflictError, isCodeServerKitError, } from "./errors.js";
6
+ import { collectCodeServerStartupDiagnostics, normalizeCodeServerStartupFailure, sanitizeCodeServerDiagnostics, } from "./diagnostics.js";
7
+ import { launchCodeServerProcess } from "./launch.js";
8
+ import { logPackageInitialized, resolveLogger } from "./logging.js";
9
+ import { createCodeServerLaunchPlan } from "./plan.js";
10
+ import { ensureCodeServerPrepared } from "./preparation.js";
11
+ import { persistCodeServerProfileIfChanged, readCodeServerProfileSnapshot, syncCodeServerProfile, } from "./profile.js";
12
+ import { waitForCodeServerReady } from "./readiness.js";
13
+ import { launchCodeServerWithSystemd, readCodeServerSystemdStatus, restartCodeServerSystemdUnit, summarizeCodeServerSystemdJournal, } from "./systemd.js";
14
+ const DEFAULT_LAUNCH_STRATEGY = "direct";
15
+ const DEFAULT_READY_RETRY_INTERVAL_MS = 100;
16
+ const DEFAULT_READY_TIMEOUT_MS = 30000;
17
+ const handles = new Map();
18
+ const inflightStarts = new Map();
19
+ function createCodeServerSessionManager(options = {}) {
20
+ logPackageInitialized({
21
+ adapter: options.loggerAdapter,
22
+ logger: options.logger,
23
+ source: "@trebired/code-server-kit",
24
+ });
25
+ return {
26
+ async getStatus(input) {
27
+ return await getCodeServerSessionStatusInternal({
28
+ logger: input.logger ?? options.logger,
29
+ loggerAdapter: input.loggerAdapter ?? options.loggerAdapter,
30
+ sanitizer: input.sanitizer,
31
+ sessionKey: input.sessionKey,
32
+ stateRoot: input.stateRoot,
33
+ });
34
+ },
35
+ async readDiagnostics(input) {
36
+ return await readCodeServerSessionDiagnostics({
37
+ sessionKey: input.sessionKey,
38
+ stateRoot: input.stateRoot,
39
+ });
40
+ },
41
+ async restart(input) {
42
+ const stop = await this.stop({
43
+ logger: input.logger,
44
+ loggerAdapter: input.loggerAdapter,
45
+ profile: input.profile,
46
+ sanitizer: input.sanitizer,
47
+ sessionKey: input.sessionKey,
48
+ signal: "SIGTERM",
49
+ stateRoot: input.stateRoot,
50
+ }) ?? createEmptyStopResult(input.sessionKey);
51
+ const start = await this.start(input);
52
+ return {
53
+ start,
54
+ stop,
55
+ };
56
+ },
57
+ async start(input) {
58
+ return await startCodeServerSessionInternal({
59
+ ...input,
60
+ installation: input.installation ?? options.installation,
61
+ logger: input.logger ?? options.logger,
62
+ loggerAdapter: input.loggerAdapter ?? options.loggerAdapter,
63
+ resolveFrom: input.resolveFrom ?? options.resolveFrom,
64
+ });
65
+ },
66
+ async stop(input) {
67
+ return await stopCodeServerSessionInternal({
68
+ ...input,
69
+ logger: input.logger ?? options.logger,
70
+ loggerAdapter: input.loggerAdapter ?? options.loggerAdapter,
71
+ });
72
+ },
73
+ };
74
+ }
75
+ async function startCodeServerSession(options) {
76
+ const manager = createCodeServerSessionManager({
77
+ installation: options.installation,
78
+ logger: options.logger,
79
+ loggerAdapter: options.loggerAdapter,
80
+ resolveFrom: options.resolveFrom,
81
+ });
82
+ return await manager.start(options);
83
+ }
84
+ async function stopCodeServerSession(options) {
85
+ return await createCodeServerSessionManager({
86
+ logger: options.logger,
87
+ loggerAdapter: options.loggerAdapter,
88
+ }).stop(options);
89
+ }
90
+ async function restartCodeServerSession(options) {
91
+ return await createCodeServerSessionManager({
92
+ installation: options.installation,
93
+ logger: options.logger,
94
+ loggerAdapter: options.loggerAdapter,
95
+ resolveFrom: options.resolveFrom,
96
+ }).restart(options);
97
+ }
98
+ async function getCodeServerSessionStatus(options) {
99
+ return await createCodeServerSessionManager({
100
+ logger: options.logger,
101
+ loggerAdapter: options.loggerAdapter,
102
+ }).getStatus(options);
103
+ }
104
+ async function readCodeServerSessionDiagnostics(options) {
105
+ const paths = getSessionPaths(options.stateRoot, options.sessionKey);
106
+ return await readJsonFile(paths.diagnosticsPath);
107
+ }
108
+ async function startCodeServerSessionInternal(options) {
109
+ const sessionKey = normalizeSessionKey(options.sessionKey);
110
+ const stateRoot = path.resolve(options.stateRoot);
111
+ const requestedSpecHash = hashNormalizedSpec({
112
+ launchStrategy: options.launchStrategy ?? DEFAULT_LAUNCH_STRATEGY,
113
+ env: options.env ?? {},
114
+ host: options.host ?? null,
115
+ port: options.port ?? null,
116
+ trustedOrigins: options.trustedOrigins ?? [],
117
+ workspacePath: options.workspacePath ?? null,
118
+ profile: normalizeProfileConfig(options.profile),
119
+ systemd: options.systemd ?? null,
120
+ });
121
+ const inflightKey = `${stateRoot}:${sessionKey}`;
122
+ const running = inflightStarts.get(inflightKey);
123
+ if (running) {
124
+ if (running.specHash === requestedSpecHash) {
125
+ return await running.promise;
126
+ }
127
+ throw new CodeServerSessionReuseConflictError("A code-server session start is already in flight for this session key with a different effective spec.", {
128
+ sessionKey,
129
+ stateRoot,
130
+ });
131
+ }
132
+ const promise = (async () => {
133
+ const paths = getSessionPaths(stateRoot, sessionKey);
134
+ const existing = await readJsonFile(paths.recordPath);
135
+ const existingHost = existing ? extractHost(existing.bindAddr) : undefined;
136
+ const launchPlan = await createCodeServerLaunchPlan({
137
+ ...options,
138
+ host: options.bindAddr ? undefined : (options.host ?? existingHost),
139
+ port: options.bindAddr ? undefined : (options.port ?? existing?.port),
140
+ dataRoot: options.dataRoot ?? path.join(paths.sessionDir, "runtime"),
141
+ });
142
+ const specHash = hashNormalizedSpec({
143
+ launchStrategy: options.launchStrategy ?? DEFAULT_LAUNCH_STRATEGY,
144
+ plan: {
145
+ args: launchPlan.args,
146
+ bindAddr: launchPlan.bindAddr,
147
+ command: launchPlan.command,
148
+ trustedOrigins: launchPlan.trustedOrigins,
149
+ workspacePath: launchPlan.workspacePath,
150
+ },
151
+ profile: normalizeProfileConfig(options.profile),
152
+ systemd: options.systemd ?? null,
153
+ });
154
+ return await startCodeServerSessionInner({
155
+ existing,
156
+ launchPlan,
157
+ options,
158
+ paths,
159
+ sessionKey,
160
+ specHash,
161
+ stateRoot,
162
+ });
163
+ })();
164
+ inflightStarts.set(inflightKey, {
165
+ promise,
166
+ specHash: requestedSpecHash,
167
+ });
168
+ try {
169
+ return await promise;
170
+ }
171
+ finally {
172
+ inflightStarts.delete(inflightKey);
173
+ }
174
+ }
175
+ async function startCodeServerSessionInner(context) {
176
+ const { existing, launchPlan, options, paths, sessionKey, specHash, stateRoot } = context;
177
+ const log = resolveLogger(options.logger, options.loggerAdapter);
178
+ const launchStrategy = options.launchStrategy ?? DEFAULT_LAUNCH_STRATEGY;
179
+ const preparation = options.preparation?.mode === "skip"
180
+ ? launchPlan.preparationStatus
181
+ : (await ensureCodeServerPrepared({
182
+ logger: options.logger,
183
+ loggerAdapter: options.loggerAdapter,
184
+ resolveFrom: options.resolveFrom,
185
+ strictWatchdog: options.preparation?.strictWatchdog,
186
+ })).status;
187
+ const watchdogMode = preparation.watchdogMode;
188
+ await mkdirp(paths.sessionDir);
189
+ log.info("session", "starting code-server session", {
190
+ launchStrategy,
191
+ sessionKey,
192
+ stateRoot,
193
+ });
194
+ if (existing) {
195
+ const status = await probeSessionRecord(existing, options.sanitizer);
196
+ if (existing.specHash === specHash && status.ready) {
197
+ const reused = {
198
+ ...status,
199
+ state: "reusing_existing",
200
+ };
201
+ await writeSessionRecord({
202
+ ...existing,
203
+ health: "ready",
204
+ preparation,
205
+ state: "reusing_existing",
206
+ updatedAt: nowIso(),
207
+ }, paths.recordPath);
208
+ return {
209
+ created: false,
210
+ diagnostics: reused.diagnostics,
211
+ handle: handles.get(sessionKey) ?? null,
212
+ launchPlan,
213
+ launchStrategy,
214
+ reused: true,
215
+ status: reused,
216
+ };
217
+ }
218
+ if (isLiveOrStartingState(status.state)) {
219
+ await stopExistingRuntime(existing, options.profile, undefined, options.logger, options.loggerAdapter);
220
+ }
221
+ }
222
+ await maybeRestoreProfile(options.profile, launchPlan.userDataDir);
223
+ const baseRecord = createBaseRecord({
224
+ lastStartSummary: null,
225
+ launchPlan,
226
+ launchStrategy,
227
+ preparation,
228
+ sessionKey,
229
+ specHash,
230
+ watchdogMode,
231
+ });
232
+ await writeSessionRecord({
233
+ ...baseRecord,
234
+ state: "launching",
235
+ updatedAt: nowIso(),
236
+ }, paths.recordPath);
237
+ try {
238
+ let handle = null;
239
+ let journalTail = "";
240
+ if (launchStrategy === "direct") {
241
+ handle = await launchCodeServerProcess({
242
+ plan: launchPlan,
243
+ });
244
+ handles.set(sessionKey, handle);
245
+ }
246
+ else {
247
+ if (!options.systemd?.scope) {
248
+ throw new CodeServerInvalidConfigurationError("systemd session launches require an explicit scope of 'user' or 'system'.", {
249
+ sessionKey,
250
+ });
251
+ }
252
+ await launchCodeServerWithSystemd({
253
+ extraProperties: options.systemd.extraProperties,
254
+ logger: options.logger,
255
+ loggerAdapter: options.loggerAdapter,
256
+ plan: launchPlan,
257
+ scope: options.systemd.scope,
258
+ sessionKey,
259
+ unitName: options.systemd.unitName,
260
+ });
261
+ }
262
+ const ready = await waitForCodeServerReady({
263
+ failureProbe: options.failureProbe,
264
+ host: launchPlan.host,
265
+ port: launchPlan.port,
266
+ process: handle ?? undefined,
267
+ retryIntervalMs: options.readinessRetryIntervalMs ?? DEFAULT_READY_RETRY_INTERVAL_MS,
268
+ timeoutMs: options.readinessTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
269
+ });
270
+ if (launchStrategy === "systemd" && options.systemd?.scope) {
271
+ journalTail = await summarizeCodeServerSystemdJournal({
272
+ lines: 50,
273
+ scope: options.systemd.scope,
274
+ unitName: options.systemd.unitName ?? `trebired-code-server-kit-${sessionKey}.service`,
275
+ });
276
+ }
277
+ const normalizedFailure = collectCodeServerStartupDiagnostics({
278
+ journal: journalTail,
279
+ launchStrategy,
280
+ preparationStatus: preparation,
281
+ process: handle,
282
+ sanitizer: options.sanitizer,
283
+ watchdogMode,
284
+ });
285
+ const diagnostics = createDiagnosticsSnapshot({
286
+ handle,
287
+ journalTail,
288
+ normalizedFailure,
289
+ readyElapsedMs: ready.elapsedMs,
290
+ });
291
+ const record = {
292
+ ...baseRecord,
293
+ diagnostics,
294
+ health: "ready",
295
+ lastStartSummary: normalizedFailure.summary,
296
+ pid: handle?.pid ?? null,
297
+ preparation,
298
+ readyAt: nowIso(),
299
+ sanitizedDiagnostics: normalizedFailure.sanitized ?? null,
300
+ startedAt: nowIso(),
301
+ state: "ready",
302
+ systemdScope: options.systemd?.scope ?? null,
303
+ unitName: options.systemd?.unitName ?? null,
304
+ updatedAt: nowIso(),
305
+ };
306
+ await writeSessionRecord(record, paths.recordPath);
307
+ await writeDiagnosticsFile(record, paths);
308
+ return {
309
+ created: true,
310
+ diagnostics: await readCodeServerSessionDiagnostics({
311
+ sessionKey,
312
+ stateRoot,
313
+ }),
314
+ handle,
315
+ launchPlan,
316
+ launchStrategy,
317
+ reused: false,
318
+ status: await probeSessionRecord(record, options.sanitizer),
319
+ };
320
+ }
321
+ catch (error) {
322
+ const normalized = normalizeCodeServerStartupFailure(error, {
323
+ launchStrategy,
324
+ preparationStatus: preparation,
325
+ sanitizer: options.sanitizer,
326
+ watchdogMode,
327
+ });
328
+ const handle = handles.get(sessionKey) ?? null;
329
+ const failure = {
330
+ code: normalized.code,
331
+ details: normalized.details,
332
+ message: normalized.summary,
333
+ name: normalized.name,
334
+ };
335
+ if (handle) {
336
+ try {
337
+ handle.kill("SIGTERM");
338
+ }
339
+ catch {
340
+ }
341
+ handles.delete(sessionKey);
342
+ }
343
+ const record = {
344
+ ...baseRecord,
345
+ diagnostics: createDiagnosticsSnapshot({
346
+ handle,
347
+ journalTail: launchStrategy === "systemd" && options.systemd?.scope && options.systemd.unitName
348
+ ? await safeSystemdSummary(options.systemd.scope, options.systemd.unitName)
349
+ : "",
350
+ normalizedFailure: normalized,
351
+ readyElapsedMs: null,
352
+ }),
353
+ failure,
354
+ health: "failed",
355
+ lastStartSummary: normalized.summary,
356
+ pid: handle?.pid ?? null,
357
+ preparation,
358
+ sanitizedDiagnostics: normalized.sanitized ?? null,
359
+ startedAt: nowIso(),
360
+ state: "failed",
361
+ systemdScope: options.systemd?.scope ?? null,
362
+ unitName: options.systemd?.unitName ?? null,
363
+ updatedAt: nowIso(),
364
+ };
365
+ await writeSessionRecord(record, paths.recordPath);
366
+ await writeDiagnosticsFile(record, paths);
367
+ if (isCodeServerKitError(error)) {
368
+ throw error;
369
+ }
370
+ throw new CodeServerSessionLifecycleError("Could not start the code-server session.", {
371
+ cause: normalized.summary,
372
+ sessionKey,
373
+ stateRoot,
374
+ });
375
+ }
376
+ }
377
+ async function stopCodeServerSessionInternal(options) {
378
+ const log = resolveLogger(options.logger, options.loggerAdapter);
379
+ const paths = getSessionPaths(options.stateRoot, options.sessionKey);
380
+ const record = await readJsonFile(paths.recordPath);
381
+ if (!record)
382
+ return null;
383
+ log.info("session", "stopping code-server session", {
384
+ launchStrategy: record.launchStrategy,
385
+ sessionKey: options.sessionKey,
386
+ });
387
+ await stopExistingRuntime(record, options.profile, options.signal, options.logger, options.loggerAdapter);
388
+ const stoppedRecord = {
389
+ ...record,
390
+ health: "stopped",
391
+ pid: null,
392
+ state: "stopped",
393
+ stoppedAt: nowIso(),
394
+ updatedAt: nowIso(),
395
+ };
396
+ await writeSessionRecord(stoppedRecord, paths.recordPath);
397
+ await writeDiagnosticsFile(stoppedRecord, paths);
398
+ return {
399
+ diagnostics: await readCodeServerSessionDiagnostics({
400
+ sessionKey: options.sessionKey,
401
+ stateRoot: options.stateRoot,
402
+ }),
403
+ signal: options.signal,
404
+ status: await probeSessionRecord(stoppedRecord, options.sanitizer),
405
+ stopped: true,
406
+ };
407
+ }
408
+ async function getCodeServerSessionStatusInternal(options) {
409
+ const paths = getSessionPaths(options.stateRoot, options.sessionKey);
410
+ const record = await readJsonFile(paths.recordPath);
411
+ if (!record)
412
+ return null;
413
+ return await probeSessionRecord(record, options.sanitizer);
414
+ }
415
+ async function probeSessionRecord(record, sanitizer) {
416
+ const diagnostics = await readCodeServerSessionDiagnostics({
417
+ sessionKey: record.sessionKey,
418
+ stateRoot: path.dirname(path.dirname(path.dirname(record.userDataDir))),
419
+ });
420
+ const ready = record.launchStrategy === "systemd"
421
+ ? await probeSystemdReady(record)
422
+ : await probeDirectReady(record);
423
+ const sanitizedDiagnostics = sanitizer && diagnostics?.normalizedFailure
424
+ ? sanitizeCodeServerDiagnostics(diagnostics.normalizedFailure, sanitizer)
425
+ : record.sanitizedDiagnostics ?? null;
426
+ return {
427
+ bindAddr: record.bindAddr,
428
+ diagnostics,
429
+ extensionsDir: record.extensionsDir,
430
+ failure: record.failure ?? null,
431
+ health: ready ? "ready" : record.health,
432
+ lastStartSummary: record.lastStartSummary ?? null,
433
+ launchStrategy: record.launchStrategy,
434
+ pid: record.launchStrategy === "direct"
435
+ ? handles.get(record.sessionKey)?.pid ?? record.pid
436
+ : record.pid,
437
+ port: record.port,
438
+ preparation: record.preparation ?? null,
439
+ ready,
440
+ readyAt: ready ? record.readyAt : null,
441
+ sanitizedDiagnostics,
442
+ sessionKey: record.sessionKey,
443
+ specHash: record.specHash,
444
+ startedAt: record.startedAt,
445
+ state: ready ? record.state : deriveDeadState(record),
446
+ stoppedAt: record.stoppedAt,
447
+ systemdScope: record.systemdScope,
448
+ unitName: record.unitName,
449
+ updatedAt: record.updatedAt,
450
+ userDataDir: record.userDataDir,
451
+ watchdogMode: record.watchdogMode,
452
+ workspacePath: record.workspacePath,
453
+ };
454
+ }
455
+ async function probeSystemdReady(record) {
456
+ if (!record.systemdScope || !record.unitName)
457
+ return false;
458
+ try {
459
+ const status = await readCodeServerSystemdStatus({
460
+ scope: record.systemdScope,
461
+ unitName: record.unitName,
462
+ });
463
+ return status.reusable && await canConnect(record.bindAddr, record.port);
464
+ }
465
+ catch {
466
+ return false;
467
+ }
468
+ }
469
+ async function probeDirectReady(record) {
470
+ const pid = handles.get(record.sessionKey)?.pid ?? record.pid;
471
+ if (!pid || !isPidAlive(pid))
472
+ return false;
473
+ return await canConnect(record.bindAddr, record.port);
474
+ }
475
+ async function stopExistingRuntime(record, profile, signal, logger, loggerAdapter) {
476
+ if (record.launchStrategy === "systemd" && record.systemdScope && record.unitName) {
477
+ await restartCodeServerSystemdUnit({
478
+ logger,
479
+ loggerAdapter,
480
+ scope: record.systemdScope,
481
+ unitName: record.unitName,
482
+ });
483
+ }
484
+ else {
485
+ const handle = handles.get(record.sessionKey);
486
+ if (handle) {
487
+ handle.kill(signal ?? "SIGTERM");
488
+ handles.delete(record.sessionKey);
489
+ }
490
+ else if (record.pid && isPidAlive(record.pid)) {
491
+ process.kill(record.pid, signal ?? "SIGTERM");
492
+ }
493
+ }
494
+ await maybePersistProfile(profile, record.userDataDir);
495
+ }
496
+ async function maybeRestoreProfile(profile, userDataDir) {
497
+ if (!profile?.restoreFrom)
498
+ return;
499
+ const restorePolicy = profile.restorePolicy ?? "if-missing-or-empty";
500
+ if (restorePolicy === "if-missing-or-empty") {
501
+ const snapshot = await readCodeServerProfileSnapshot({
502
+ items: profile.items,
503
+ pathMap: profile.pathMap,
504
+ rootDir: userDataDir,
505
+ snapshotExtensions: profile.snapshotExtensions,
506
+ });
507
+ if (snapshot.entries.some((entry) => entry.present)) {
508
+ return;
509
+ }
510
+ }
511
+ await syncCodeServerProfile({
512
+ items: profile.items,
513
+ pathMap: profile.pathMap,
514
+ skipMissing: profile.skipMissing,
515
+ skipUnreadable: profile.skipUnreadable,
516
+ sourceDir: profile.restoreFrom,
517
+ targetDir: userDataDir,
518
+ });
519
+ }
520
+ async function maybePersistProfile(profile, userDataDir) {
521
+ if (!profile?.persistTo)
522
+ return;
523
+ const persistPolicy = profile.persistPolicy ?? "if-changed";
524
+ if (persistPolicy === "always") {
525
+ await syncCodeServerProfile({
526
+ items: profile.items,
527
+ pathMap: profile.pathMap,
528
+ skipMissing: profile.skipMissing,
529
+ skipUnreadable: profile.skipUnreadable,
530
+ sourceDir: userDataDir,
531
+ targetDir: profile.persistTo,
532
+ });
533
+ return;
534
+ }
535
+ await persistCodeServerProfileIfChanged({
536
+ items: profile.items,
537
+ pathMap: profile.pathMap,
538
+ signatureMode: profile.signatureMode,
539
+ skipMissing: profile.skipMissing,
540
+ skipUnreadable: profile.skipUnreadable,
541
+ snapshotExtensions: profile.snapshotExtensions,
542
+ sourceDir: userDataDir,
543
+ targetDir: profile.persistTo,
544
+ });
545
+ }
546
+ function createBaseRecord(options) {
547
+ return {
548
+ bindAddr: options.launchPlan.bindAddr,
549
+ diagnostics: null,
550
+ extensionsDir: options.launchPlan.extensionsDir,
551
+ health: "starting",
552
+ lastStartSummary: options.lastStartSummary,
553
+ launchStrategy: options.launchStrategy,
554
+ pid: null,
555
+ port: options.launchPlan.port,
556
+ preparation: options.preparation,
557
+ readyAt: null,
558
+ sanitizedDiagnostics: null,
559
+ sessionKey: options.sessionKey,
560
+ specHash: options.specHash,
561
+ startedAt: null,
562
+ state: "planned",
563
+ stoppedAt: null,
564
+ systemdScope: null,
565
+ trustedOrigins: [...options.launchPlan.trustedOrigins],
566
+ unitName: null,
567
+ updatedAt: nowIso(),
568
+ userDataDir: options.launchPlan.userDataDir,
569
+ watchdogMode: options.watchdogMode,
570
+ workspacePath: options.launchPlan.workspacePath,
571
+ };
572
+ }
573
+ function createDiagnosticsSnapshot(options) {
574
+ return {
575
+ journalTail: options.journalTail || undefined,
576
+ pid: options.handle?.pid ?? null,
577
+ readyElapsedMs: options.readyElapsedMs,
578
+ stderrTail: options.handle?.getStderr(),
579
+ stdoutTail: options.handle?.getStdout(),
580
+ summary: {
581
+ category: options.normalizedFailure.category,
582
+ details: options.normalizedFailure.details,
583
+ summary: options.normalizedFailure.summary,
584
+ watchdogMode: options.normalizedFailure.watchdogMode,
585
+ },
586
+ updatedAt: nowIso(),
587
+ };
588
+ }
589
+ async function writeDiagnosticsFile(record, paths) {
590
+ await mkdirp(path.dirname(paths.diagnosticsPath));
591
+ const snapshot = record.diagnostics;
592
+ const diagnostics = {
593
+ diagnosticsPath: paths.diagnosticsPath,
594
+ journalTail: snapshot?.journalTail,
595
+ normalizedFailure: snapshot?.summary
596
+ ? {
597
+ category: String(snapshot.summary.category ?? "unknown"),
598
+ }
599
+ : null,
600
+ readyElapsedMs: snapshot?.readyElapsedMs ?? null,
601
+ recordPath: paths.recordPath,
602
+ stderrTail: snapshot?.stderrTail,
603
+ stdoutTail: snapshot?.stdoutTail,
604
+ summary: snapshot?.summary ?? {},
605
+ updatedAt: snapshot?.updatedAt ?? nowIso(),
606
+ };
607
+ if (snapshot?.summary) {
608
+ const summary = snapshot.summary;
609
+ diagnostics.normalizedFailure = {
610
+ category: String(summary.category ?? "unknown"),
611
+ code: String(summary.category ?? "unknown"),
612
+ details: summary.details ?? {},
613
+ launchStrategy: record.launchStrategy,
614
+ summary: String(summary.summary ?? ""),
615
+ watchdogMode: record.watchdogMode,
616
+ };
617
+ }
618
+ await fs.promises.writeFile(paths.diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`, "utf8");
619
+ }
620
+ async function writeSessionRecord(record, recordPath) {
621
+ await mkdirp(path.dirname(recordPath));
622
+ await fs.promises.writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
623
+ }
624
+ function getSessionPaths(stateRoot, sessionKey) {
625
+ const normalizedStateRoot = path.resolve(stateRoot);
626
+ const safeKey = normalizeSessionKey(sessionKey);
627
+ const sessionDir = path.join(normalizedStateRoot, "sessions", safeKey);
628
+ return {
629
+ diagnosticsPath: path.join(sessionDir, "diagnostics.json"),
630
+ recordPath: path.join(sessionDir, "session.json"),
631
+ sessionDir,
632
+ stateRoot: normalizedStateRoot,
633
+ };
634
+ }
635
+ function normalizeSessionKey(value) {
636
+ const normalized = String(value ?? "").trim();
637
+ if (!normalized) {
638
+ throw new CodeServerInvalidConfigurationError("sessionKey is required for lifecycle-managed code-server APIs.");
639
+ }
640
+ return normalized.replace(/[^A-Za-z0-9._-]+/g, "-");
641
+ }
642
+ function normalizeProfileConfig(profile) {
643
+ if (!profile)
644
+ return null;
645
+ return {
646
+ items: [...(profile.items ?? [])].sort(),
647
+ pathMap: profile.pathMap ?? {},
648
+ persistPolicy: profile.persistPolicy ?? "if-changed",
649
+ persistTo: profile.persistTo ? path.resolve(profile.persistTo) : null,
650
+ restoreFrom: profile.restoreFrom ? path.resolve(profile.restoreFrom) : null,
651
+ restorePolicy: profile.restorePolicy ?? "if-missing-or-empty",
652
+ signatureMode: profile.signatureMode ?? "content-hash",
653
+ snapshotExtensions: profile.snapshotExtensions ?? false,
654
+ };
655
+ }
656
+ function hashNormalizedSpec(value) {
657
+ return createHash("sha256")
658
+ .update(JSON.stringify(value))
659
+ .digest("hex");
660
+ }
661
+ function deriveDeadState(record) {
662
+ if (record.state === "failed")
663
+ return "failed";
664
+ if (record.state === "stopped")
665
+ return "stopped";
666
+ return "stale";
667
+ }
668
+ function isLiveOrStartingState(state) {
669
+ return state === "launching" || state === "planned" || state === "ready" || state === "reusing_existing";
670
+ }
671
+ async function safeSystemdSummary(scope, unitName) {
672
+ try {
673
+ return await summarizeCodeServerSystemdJournal({
674
+ scope,
675
+ unitName,
676
+ });
677
+ }
678
+ catch {
679
+ return "";
680
+ }
681
+ }
682
+ function isPidAlive(pid) {
683
+ try {
684
+ process.kill(pid, 0);
685
+ return true;
686
+ }
687
+ catch {
688
+ return false;
689
+ }
690
+ }
691
+ async function canConnect(bindAddr, port) {
692
+ const host = extractHost(bindAddr);
693
+ return await new Promise((resolve) => {
694
+ const socket = net.connect({
695
+ host,
696
+ port,
697
+ });
698
+ let settled = false;
699
+ const finish = (value) => {
700
+ if (settled)
701
+ return;
702
+ settled = true;
703
+ socket.destroy();
704
+ resolve(value);
705
+ };
706
+ socket.setTimeout(250);
707
+ socket.once("connect", () => finish(true));
708
+ socket.once("error", () => finish(false));
709
+ socket.once("timeout", () => finish(false));
710
+ });
711
+ }
712
+ function extractHost(bindAddr) {
713
+ if (bindAddr.startsWith("[")) {
714
+ const end = bindAddr.indexOf("]");
715
+ return bindAddr.slice(1, end);
716
+ }
717
+ return bindAddr.slice(0, bindAddr.lastIndexOf(":"));
718
+ }
719
+ async function readJsonFile(filePath) {
720
+ try {
721
+ const contents = await fs.promises.readFile(filePath, "utf8");
722
+ return JSON.parse(contents);
723
+ }
724
+ catch (error) {
725
+ if (typeof error === "object" && error && "code" in error && String(error.code) === "ENOENT") {
726
+ return null;
727
+ }
728
+ throw error;
729
+ }
730
+ }
731
+ async function mkdirp(dirPath) {
732
+ await fs.promises.mkdir(dirPath, { recursive: true });
733
+ }
734
+ function nowIso() {
735
+ return new Date().toISOString();
736
+ }
737
+ function createEmptyStopResult(sessionKey) {
738
+ return {
739
+ diagnostics: null,
740
+ status: {
741
+ bindAddr: "",
742
+ diagnostics: null,
743
+ extensionsDir: "",
744
+ failure: null,
745
+ health: "stopped",
746
+ lastStartSummary: null,
747
+ launchStrategy: "direct",
748
+ pid: null,
749
+ port: 0,
750
+ preparation: null,
751
+ ready: false,
752
+ readyAt: null,
753
+ sanitizedDiagnostics: null,
754
+ sessionKey,
755
+ specHash: "",
756
+ startedAt: null,
757
+ state: "stopped",
758
+ stoppedAt: nowIso(),
759
+ systemdScope: null,
760
+ unitName: null,
761
+ updatedAt: nowIso(),
762
+ userDataDir: "",
763
+ watchdogMode: "disabled_fallback",
764
+ workspacePath: null,
765
+ },
766
+ stopped: false,
767
+ };
768
+ }
769
+ export { createCodeServerSessionManager, getCodeServerSessionStatus, readCodeServerSessionDiagnostics, restartCodeServerSession, startCodeServerSession, stopCodeServerSession, };
770
+ //# sourceMappingURL=session.js.map