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