@termfleet/core 0.1.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 (67) hide show
  1. package/dist/agent-launch.d.ts +78 -0
  2. package/dist/agent-launch.js +247 -0
  3. package/dist/agent-session-id.d.ts +10 -0
  4. package/dist/agent-session-id.js +36 -0
  5. package/dist/agent-session-index-client.d.ts +7 -0
  6. package/dist/agent-session-index-client.js +86 -0
  7. package/dist/agent-session-index-worker.d.ts +1 -0
  8. package/dist/agent-session-index-worker.js +20 -0
  9. package/dist/agent-session-index.d.ts +34 -0
  10. package/dist/agent-session-index.js +527 -0
  11. package/dist/agent-session-tail.d.ts +33 -0
  12. package/dist/agent-session-tail.js +184 -0
  13. package/dist/agent-session-watcher.d.ts +36 -0
  14. package/dist/agent-session-watcher.js +194 -0
  15. package/dist/agent-session.d.ts +380 -0
  16. package/dist/agent-session.js +1688 -0
  17. package/dist/background-runner.d.ts +3 -0
  18. package/dist/background-runner.js +55 -0
  19. package/dist/boot-queue.d.ts +35 -0
  20. package/dist/boot-queue.js +66 -0
  21. package/dist/build-info.d.ts +5 -0
  22. package/dist/build-info.js +38 -0
  23. package/dist/collab/canvas-doc.d.ts +47 -0
  24. package/dist/collab/canvas-doc.js +83 -0
  25. package/dist/contracts/auth.d.ts +77 -0
  26. package/dist/contracts/auth.js +1 -0
  27. package/dist/contracts/canvas.d.ts +34 -0
  28. package/dist/contracts/canvas.js +76 -0
  29. package/dist/contracts/console-layout.d.ts +39 -0
  30. package/dist/contracts/console-layout.js +135 -0
  31. package/dist/contracts/files.d.ts +38 -0
  32. package/dist/contracts/files.js +37 -0
  33. package/dist/contracts/provider-url.d.ts +3 -0
  34. package/dist/contracts/provider-url.js +49 -0
  35. package/dist/contracts/registry.d.ts +58 -0
  36. package/dist/contracts/registry.js +285 -0
  37. package/dist/launch-trace.d.ts +6 -0
  38. package/dist/launch-trace.js +33 -0
  39. package/dist/lib/errors.d.ts +1 -0
  40. package/dist/lib/errors.js +5 -0
  41. package/dist/lib/exec.d.ts +13 -0
  42. package/dist/lib/exec.js +134 -0
  43. package/dist/local-providers.d.ts +32 -0
  44. package/dist/local-providers.js +184 -0
  45. package/dist/local-tunnel.d.ts +6 -0
  46. package/dist/local-tunnel.js +258 -0
  47. package/dist/provider-access-token.d.ts +11 -0
  48. package/dist/provider-access-token.js +77 -0
  49. package/dist/provider-client.d.ts +152 -0
  50. package/dist/provider-client.js +666 -0
  51. package/dist/provider-url-resolver.d.ts +16 -0
  52. package/dist/provider-url-resolver.js +37 -0
  53. package/dist/registry-client.d.ts +93 -0
  54. package/dist/registry-client.js +170 -0
  55. package/dist/registry.d.ts +56 -0
  56. package/dist/registry.js +406 -0
  57. package/dist/session-attention.d.ts +24 -0
  58. package/dist/session-attention.js +54 -0
  59. package/dist/session-lifecycle.d.ts +83 -0
  60. package/dist/session-lifecycle.js +658 -0
  61. package/dist/session-window.d.ts +3 -0
  62. package/dist/session-window.js +20 -0
  63. package/dist/terminal-client.d.ts +49 -0
  64. package/dist/terminal-client.js +89 -0
  65. package/dist/types.d.ts +155 -0
  66. package/dist/types.js +21 -0
  67. package/package.json +26 -0
@@ -0,0 +1,135 @@
1
+ export function parseConsoleLayout(payload) {
2
+ if (!payload || typeof payload !== "object") {
3
+ return { providerGroups: {} };
4
+ }
5
+ const source = payload;
6
+ const viewport = parseViewport(source.viewport);
7
+ const providerGroupsPayload = source.providerGroups;
8
+ if (!providerGroupsPayload || typeof providerGroupsPayload !== "object" || Array.isArray(providerGroupsPayload)) {
9
+ return { providerGroups: {}, viewport };
10
+ }
11
+ const providerGroups = {};
12
+ for (const [providerGroupId, providerGroupPayload] of Object.entries(providerGroupsPayload)) {
13
+ if (!providerGroupPayload || typeof providerGroupPayload !== "object") {
14
+ continue;
15
+ }
16
+ const providerGroupSource = providerGroupPayload;
17
+ const appearance = parseProviderGroupAppearance(providerGroupSource.appearance);
18
+ const positionPayload = providerGroupPayload.position;
19
+ if (!positionPayload || typeof positionPayload !== "object") {
20
+ if (appearance) {
21
+ providerGroups[providerGroupId] = { appearance };
22
+ }
23
+ continue;
24
+ }
25
+ const positionSource = positionPayload;
26
+ const x = Number(positionSource.x);
27
+ const y = Number(positionSource.y);
28
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
29
+ if (appearance) {
30
+ providerGroups[providerGroupId] = { appearance };
31
+ }
32
+ continue;
33
+ }
34
+ providerGroups[providerGroupId] = { ...(appearance ? { appearance } : {}), position: { x, y } };
35
+ }
36
+ return { providerGroups, viewport };
37
+ }
38
+ export function parseConsoleLayoutPatch(payload) {
39
+ const layout = parseConsoleLayout(payload);
40
+ if (Object.keys(layout.providerGroups).length === 0 && !layout.viewport) {
41
+ throw new Error("Console layout patch must include at least one provider group position or viewport.");
42
+ }
43
+ return layout;
44
+ }
45
+ function parseViewport(payload) {
46
+ if (!payload || typeof payload !== "object") {
47
+ return undefined;
48
+ }
49
+ const source = payload;
50
+ const x = Number(source.x);
51
+ const y = Number(source.y);
52
+ const zoom = Number(source.zoom);
53
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(zoom) || zoom <= 0) {
54
+ return undefined;
55
+ }
56
+ return { x, y, zoom };
57
+ }
58
+ function parseProviderGroupAppearance(payload) {
59
+ if (!payload || typeof payload !== "object") {
60
+ return undefined;
61
+ }
62
+ const source = payload;
63
+ const appearance = {};
64
+ // `title` is intentionally NOT parsed here: a machine's name lives on its
65
+ // registry record, never in appearance. A legacy title in the file is dropped.
66
+ if (typeof source.backgroundColor === "string" && /^#[0-9a-f]{6}$/i.test(source.backgroundColor.trim())) {
67
+ appearance.backgroundColor = source.backgroundColor.trim();
68
+ }
69
+ if (typeof source.accentColor === "string" && /^#[0-9a-f]{6}$/i.test(source.accentColor.trim())) {
70
+ appearance.accentColor = source.accentColor.trim();
71
+ }
72
+ if (typeof source.wallpaper === "string" && /^[a-z0-9-]{1,32}$/.test(source.wallpaper.trim())) {
73
+ appearance.wallpaper = source.wallpaper.trim();
74
+ }
75
+ const backgroundOpacity = Number(source.backgroundOpacity);
76
+ if (Number.isFinite(backgroundOpacity)) {
77
+ appearance.backgroundOpacity = Math.min(1, Math.max(0, backgroundOpacity));
78
+ }
79
+ const backgroundBlur = Number(source.backgroundBlur);
80
+ if (Number.isFinite(backgroundBlur)) {
81
+ appearance.backgroundBlur = Math.min(40, Math.max(0, backgroundBlur));
82
+ }
83
+ if (typeof source.icon === "string" && /^[a-z0-9-]{1,24}$/.test(source.icon.trim())) {
84
+ appearance.icon = source.icon.trim();
85
+ }
86
+ if (source.windowTitlebar === "minimal" || source.windowTitlebar === "plain" || source.windowTitlebar === "traffic") {
87
+ appearance.windowTitlebar = source.windowTitlebar;
88
+ }
89
+ const windowCornerRadius = Number(source.windowCornerRadius);
90
+ if (Number.isFinite(windowCornerRadius)) {
91
+ appearance.windowCornerRadius = Math.min(24, Math.max(0, Math.round(windowCornerRadius)));
92
+ }
93
+ const windowOpacity = Number(source.windowOpacity);
94
+ if (Number.isFinite(windowOpacity)) {
95
+ appearance.windowOpacity = Math.min(1, Math.max(0.4, windowOpacity));
96
+ }
97
+ const windowFrost = Number(source.windowFrost);
98
+ if (Number.isFinite(windowFrost)) {
99
+ appearance.windowFrost = Math.min(24, Math.max(0, windowFrost));
100
+ }
101
+ if (source.mode === "dark" || source.mode === "light") {
102
+ appearance.mode = source.mode;
103
+ }
104
+ if (typeof source.desktopGrid === "boolean") {
105
+ appearance.desktopGrid = source.desktopGrid;
106
+ }
107
+ if (typeof source.terminalFontFamily === "string" && /^[a-z0-9-]{1,24}$/.test(source.terminalFontFamily.trim())) {
108
+ appearance.terminalFontFamily = source.terminalFontFamily.trim();
109
+ }
110
+ if (source.terminalCursorStyle === "bar" || source.terminalCursorStyle === "block" || source.terminalCursorStyle === "underline") {
111
+ appearance.terminalCursorStyle = source.terminalCursorStyle;
112
+ }
113
+ if (typeof source.terminalCursorBlink === "boolean") {
114
+ appearance.terminalCursorBlink = source.terminalCursorBlink;
115
+ }
116
+ if (source.wallpaperFit === "center" || source.wallpaperFit === "fill" || source.wallpaperFit === "fit" || source.wallpaperFit === "tile") {
117
+ appearance.wallpaperFit = source.wallpaperFit;
118
+ }
119
+ if (typeof source.wallpaperUrl === "string") {
120
+ const wallpaperUrl = source.wallpaperUrl.trim();
121
+ if (wallpaperUrl.length <= 600 && isHttpUrl(wallpaperUrl)) {
122
+ appearance.wallpaperUrl = wallpaperUrl;
123
+ }
124
+ }
125
+ return Object.keys(appearance).length > 0 ? appearance : undefined;
126
+ }
127
+ function isHttpUrl(value) {
128
+ try {
129
+ const url = new URL(value);
130
+ return url.protocol === "http:" || url.protocol === "https:";
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
@@ -0,0 +1,38 @@
1
+ export type ProviderFileStat = {
2
+ bytes: number;
3
+ mode?: string;
4
+ path: string;
5
+ terminalId: string;
6
+ };
7
+ export type ProviderFileKind = "directory" | "file" | "other";
8
+ export type ProviderDirectoryEntry = {
9
+ bytes: number;
10
+ kind: ProviderFileKind;
11
+ modifiedAt?: string;
12
+ mode?: string;
13
+ name: string;
14
+ path: string;
15
+ };
16
+ export type ProviderDirectoryList = {
17
+ entries: ProviderDirectoryEntry[];
18
+ filesystemId?: string;
19
+ path: string;
20
+ terminalId: string;
21
+ };
22
+ export type ProviderFileWriteOptions = {
23
+ data: Buffer;
24
+ filesystemId?: string;
25
+ mkdirs?: boolean;
26
+ mode?: string;
27
+ path: string;
28
+ terminalId: string;
29
+ };
30
+ export type ProviderFileWriteResult = {
31
+ bytes: number;
32
+ filesystemId?: string;
33
+ path: string;
34
+ terminalId: string;
35
+ };
36
+ export declare function parseFileMode(value: string | null | undefined): string | undefined;
37
+ export declare function parseProviderFilePath(value: string | null | undefined): string;
38
+ export declare function parseProviderFileTerminalId(value: string | null | undefined): string;
@@ -0,0 +1,37 @@
1
+ export function parseFileMode(value) {
2
+ if (value === null || value === undefined || value === "") {
3
+ return undefined;
4
+ }
5
+ if (!/^[0-7]{3,4}$/.test(value)) {
6
+ throw new Error("File mode must be an octal mode like 600 or 0644.");
7
+ }
8
+ return value;
9
+ }
10
+ export function parseProviderFilePath(value) {
11
+ if (!value) {
12
+ throw new Error("File path is required.");
13
+ }
14
+ if (value.includes("\0")) {
15
+ throw new Error("File path cannot contain null bytes.");
16
+ }
17
+ if (!value.startsWith("/")) {
18
+ throw new Error("File path must be absolute.");
19
+ }
20
+ // Reject "." / ".." segments so a path can never climb out of the directory it
21
+ // names. The file browser only ever builds canonical absolute paths from
22
+ // listDirectory results, so this never rejects legitimate use — it only blocks
23
+ // traversal tricks (e.g. /srv/data/../../etc/shadow).
24
+ if (value.split("/").some((segment) => segment === "." || segment === "..")) {
25
+ throw new Error("File path cannot contain '.' or '..' segments.");
26
+ }
27
+ return value;
28
+ }
29
+ export function parseProviderFileTerminalId(value) {
30
+ if (!value) {
31
+ throw new Error("File terminalId is required.");
32
+ }
33
+ if (value.includes("\0")) {
34
+ throw new Error("File terminalId cannot contain null bytes.");
35
+ }
36
+ return value;
37
+ }
@@ -0,0 +1,3 @@
1
+ export declare function normalizeProviderOrigin(raw: string): string;
2
+ export declare function isSsrfReservedProviderHost(raw: string): boolean;
3
+ export declare function isLocalProviderUrl(raw: string): boolean;
@@ -0,0 +1,49 @@
1
+ export function normalizeProviderOrigin(raw) {
2
+ try {
3
+ return new URL(raw).origin;
4
+ }
5
+ catch {
6
+ throw new Error(`Registry provided invalid provider URL: ${raw}`);
7
+ }
8
+ }
9
+ // Loopback only — used as a security boundary (CORS origin allow + the
10
+ // "local provider" SSRF/registration allowlist), so it must classify the host
11
+ // STRUCTURALLY, never by string prefix. "127.0.0.1.evil.com" and "127.evil.com"
12
+ // begin with "127." but resolve to attacker-controlled IPs; they are not local.
13
+ function isLoopbackIpv4(host) {
14
+ const octets = host.split(".");
15
+ if (octets.length !== 4)
16
+ return false;
17
+ if (!octets.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255))
18
+ return false;
19
+ return Number(octets[0]) === 127;
20
+ }
21
+ // Hosts that are never a legitimate provider and are the classic SSRF target:
22
+ // link-local (169.254.0.0/16 — including the cloud-metadata endpoint
23
+ // 169.254.169.254 — and IPv6 fe80::/10) plus the unspecified address. The
24
+ // console probes/proxies registered provider URLs, so it must refuse to fetch
25
+ // these. Loopback (console-managed providers) and LAN ranges (a supported remote
26
+ // provider location) are intentionally NOT rejected here.
27
+ export function isSsrfReservedProviderHost(raw) {
28
+ let host;
29
+ try {
30
+ host = new URL(raw).hostname.toLowerCase().replace(/^\[|\]$/g, "");
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ if (host === "0.0.0.0" || host === "::")
36
+ return true;
37
+ const octets = host.split(".");
38
+ if (octets.length === 4 && octets.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)) {
39
+ return Number(octets[0]) === 169 && Number(octets[1]) === 254;
40
+ }
41
+ // IPv6 link-local fe80::/10 spans fe80–febf.
42
+ return /^fe[89ab]/.test(host);
43
+ }
44
+ export function isLocalProviderUrl(raw) {
45
+ // URL.hostname keeps brackets for IPv6 (e.g. "[::1]"); strip them so the
46
+ // loopback literal compares cleanly.
47
+ const host = new URL(raw).hostname.toLowerCase().replace(/^\[|\]$/g, "");
48
+ return host === "localhost" || host === "::1" || isLoopbackIpv4(host);
49
+ }
@@ -0,0 +1,58 @@
1
+ export type RegistryProviderRecord = {
2
+ aliases?: string[];
3
+ baseUrl: string;
4
+ deploymentKind?: RegistryProviderDeploymentKind;
5
+ instanceId?: string;
6
+ label?: string;
7
+ lifecycle?: ManagedProviderSpec;
8
+ owner?: RegistryProviderOwner;
9
+ ownerOrganizationId?: string;
10
+ registrySource?: RegistryProviderSource;
11
+ requiresAuth?: boolean;
12
+ status?: RegistryProviderRuntimeStatus;
13
+ verifiedProvider?: RegistryVerifiedProvider;
14
+ };
15
+ export type RegistryProviderRuntimeStatus = {
16
+ lastProbedAt?: number;
17
+ message?: string;
18
+ phase: "down" | "failed" | "running" | "starting";
19
+ pid?: number;
20
+ };
21
+ export declare const managedProviderKinds: readonly ["docker-worker", "iterm", "virtual-tmux", "wezterm"];
22
+ export type ManagedProviderKind = (typeof managedProviderKinds)[number];
23
+ export type ManagedProviderSpec = {
24
+ kind: "iterm";
25
+ name: string;
26
+ } | {
27
+ count: number;
28
+ kind: "virtual-tmux";
29
+ prefix: string;
30
+ } | {
31
+ count: number;
32
+ kind: "wezterm";
33
+ prefix: string;
34
+ } | {
35
+ kind: "docker-worker";
36
+ themeProfile?: string;
37
+ };
38
+ export type RegistryProviderOwner = {
39
+ fullName: string;
40
+ imageUrl?: string;
41
+ source: RegistryProviderOwnerSource;
42
+ };
43
+ export type RegistryProviderOwnerSource = "authenticated" | "self-declared";
44
+ export type RegistryVerifiedProvider = {
45
+ buildSha?: string;
46
+ epoch?: string;
47
+ kind: string;
48
+ sourceBaseUrl?: string;
49
+ verifiedAt?: string;
50
+ version?: string;
51
+ };
52
+ export type RegistryProviderSource = "console-memory" | "local-registry" | "remote-registry";
53
+ export type RegistryProviderDeploymentKind = "docker-worker" | "console-managed";
54
+ export declare function normalizeRegistryPayload(payload: unknown): RegistryProviderRecord[];
55
+ export declare function normalizeRegistryRegistrationResponse(payload: unknown, fallback: RegistryProviderRecord): RegistryProviderRecord;
56
+ export declare function normalizeProviderAlias(value: string): string;
57
+ export declare function mergeRegistryProviders(providers: RegistryProviderRecord[]): RegistryProviderRecord[];
58
+ export declare function providerAliases(provider: RegistryProviderRecord): string[];
@@ -0,0 +1,285 @@
1
+ import { normalizeProviderOrigin } from "./provider-url.js";
2
+ // The kinds whose process lifecycle the console owns and can reconstruct. The
3
+ // array is the source of truth (the type derives from it) so a runtime guard test
4
+ // can assert every managed kind has a controller — catching a kind added here but
5
+ // not given an ensure/probe/stop controller.
6
+ export const managedProviderKinds = ["docker-worker", "iterm", "virtual-tmux", "wezterm"];
7
+ export function normalizeRegistryPayload(payload) {
8
+ const providers = Array.isArray(payload)
9
+ ? payload
10
+ : (() => {
11
+ const candidate = payload;
12
+ if (!candidate || typeof candidate !== "object" || !Array.isArray(candidate.providers)) {
13
+ throw new Error("Registry response must be an array or an object with a providers array.");
14
+ }
15
+ return candidate.providers;
16
+ })();
17
+ return providers.map((entry, index) => {
18
+ const provider = normalizeRegistryEntry(entry, index);
19
+ return {
20
+ ...provider,
21
+ baseUrl: normalizeProviderOrigin(provider.baseUrl)
22
+ };
23
+ });
24
+ }
25
+ export function normalizeRegistryRegistrationResponse(payload, fallback) {
26
+ const candidates = extractRegistrationCandidates(payload);
27
+ const normalizedFallback = {
28
+ ...fallback,
29
+ baseUrl: normalizeProviderOrigin(fallback.baseUrl)
30
+ };
31
+ const match = candidates.find((candidate) => candidate.baseUrl === normalizedFallback.baseUrl);
32
+ if (match) {
33
+ return match;
34
+ }
35
+ const [single] = candidates;
36
+ if (candidates.length === 1 && single) {
37
+ return single;
38
+ }
39
+ return normalizedFallback;
40
+ }
41
+ function extractRegistrationCandidates(payload) {
42
+ if (!payload || typeof payload !== "object") {
43
+ return [];
44
+ }
45
+ const source = payload;
46
+ if (source.provider && typeof source.provider === "object") {
47
+ return tryNormalizeRegistryPayload([source.provider]);
48
+ }
49
+ if (Array.isArray(payload) || Array.isArray(source.providers)) {
50
+ return tryNormalizeRegistryPayload(payload);
51
+ }
52
+ return tryNormalizeRegistryPayload([payload]);
53
+ }
54
+ function tryNormalizeRegistryPayload(payload) {
55
+ try {
56
+ return normalizeRegistryPayload(payload);
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ }
62
+ function normalizeRegistryEntry(entry, index) {
63
+ if (typeof entry === "string") {
64
+ return { baseUrl: entry, label: `Provider ${index + 1}` };
65
+ }
66
+ if (!entry || typeof entry !== "object") {
67
+ throw new Error(`Registry provider at index ${index} must be a string or object.`);
68
+ }
69
+ const source = entry;
70
+ const baseUrl = stringValue(source.baseUrl)
71
+ ?? stringValue(source.url)
72
+ ?? stringValue(source.endpoint)
73
+ ?? stringValue(source.tunnelUrl)
74
+ ?? stringValue(source.tunnel_url)
75
+ ?? stringValue(source.origin);
76
+ if (!baseUrl) {
77
+ throw new Error(`Registry provider at index ${index} is missing baseUrl.`);
78
+ }
79
+ return {
80
+ aliases: aliasesValue(source.aliases, source.alias),
81
+ baseUrl,
82
+ deploymentKind: deploymentKindValue(source.deploymentKind)
83
+ ?? deploymentKindValue(source.deployment_kind),
84
+ instanceId: stringValue(source.instanceId)
85
+ ?? stringValue(source.instance_id),
86
+ label: stringValue(source.label)
87
+ ?? stringValue(source.name)
88
+ ?? `Provider ${index + 1}`,
89
+ lifecycle: lifecycleValue(source.lifecycle),
90
+ owner: ownerValue(source.owner),
91
+ ownerOrganizationId: stringValue(source.ownerOrganizationId)
92
+ ?? stringValue(source.owner_organization_id)
93
+ ?? stringValue(source.organizationId)
94
+ ?? stringValue(source.organization_id),
95
+ registrySource: registrySourceValue(source.registrySource)
96
+ ?? registrySourceValue(source.registry_source),
97
+ requiresAuth: booleanValue(source.requiresAuth)
98
+ ?? booleanValue(source.requires_auth),
99
+ status: runtimeStatusValue(source.status),
100
+ verifiedProvider: verifiedProviderValue(source.verifiedProvider)
101
+ ?? verifiedProviderValue(source.verified_provider)
102
+ };
103
+ }
104
+ // The console's runtime liveness overlay, preserved through normalize so the UI
105
+ // can read it. Conservative: an unrecognized phase degrades to undefined.
106
+ function runtimeStatusValue(value) {
107
+ if (!value || typeof value !== "object") {
108
+ return undefined;
109
+ }
110
+ const source = value;
111
+ if (source.phase !== "down" && source.phase !== "failed" && source.phase !== "running" && source.phase !== "starting") {
112
+ return undefined;
113
+ }
114
+ return {
115
+ ...(typeof source.lastProbedAt === "number" ? { lastProbedAt: source.lastProbedAt } : {}),
116
+ ...(typeof source.message === "string" ? { message: source.message } : {}),
117
+ phase: source.phase,
118
+ ...(typeof source.pid === "number" ? { pid: source.pid } : {})
119
+ };
120
+ }
121
+ export function normalizeProviderAlias(value) {
122
+ const normalized = value.trim().toLowerCase();
123
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {
124
+ throw new Error(`Provider alias must use lowercase id-style characters: letters, numbers, dots, underscores, or hyphens. Got: ${JSON.stringify(value)}`);
125
+ }
126
+ return normalized;
127
+ }
128
+ function aliasesValue(aliases, alias) {
129
+ const values = [];
130
+ if (typeof alias === "string" && alias.trim()) {
131
+ values.push(alias);
132
+ }
133
+ if (typeof aliases === "string" && aliases.trim()) {
134
+ values.push(...aliases.split(","));
135
+ }
136
+ if (Array.isArray(aliases)) {
137
+ for (const entry of aliases) {
138
+ if (typeof entry === "string" && entry.trim()) {
139
+ values.push(entry);
140
+ }
141
+ }
142
+ }
143
+ const normalized = [...new Set(values.map(normalizeProviderAlias))];
144
+ return normalized.length > 0 ? normalized : undefined;
145
+ }
146
+ function stringValue(value) {
147
+ return typeof value === "string" && value.trim() ? value : undefined;
148
+ }
149
+ function booleanValue(value) {
150
+ return typeof value === "boolean" ? value : undefined;
151
+ }
152
+ function registrySourceValue(value) {
153
+ if (value === "console-memory" || value === "local-registry" || value === "remote-registry") {
154
+ return value;
155
+ }
156
+ return undefined;
157
+ }
158
+ function deploymentKindValue(value) {
159
+ if (value === "docker-worker" || value === "console-managed") {
160
+ return value;
161
+ }
162
+ return undefined;
163
+ }
164
+ // Conservative: an unrecognized or malformed lifecycle spec parses to undefined
165
+ // (the provider is treated as unmanaged) rather than throwing — a forward-compat
166
+ // kind from a newer console must not break an older one reading the registry.
167
+ function lifecycleValue(value) {
168
+ if (!value || typeof value !== "object") {
169
+ return undefined;
170
+ }
171
+ const source = value;
172
+ if (source.kind === "iterm") {
173
+ const name = stringValue(source.name);
174
+ return name ? { kind: "iterm", name } : undefined;
175
+ }
176
+ if (source.kind === "virtual-tmux" || source.kind === "wezterm") {
177
+ const prefix = stringValue(source.prefix);
178
+ const count = countValue(source.count);
179
+ return prefix !== undefined && count !== undefined ? { count, kind: source.kind, prefix } : undefined;
180
+ }
181
+ if (source.kind === "docker-worker") {
182
+ const themeProfile = stringValue(source.themeProfile);
183
+ return { kind: "docker-worker", ...(themeProfile ? { themeProfile } : {}) };
184
+ }
185
+ return undefined;
186
+ }
187
+ function countValue(value) {
188
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
189
+ }
190
+ function ownerValue(value) {
191
+ if (!value || typeof value !== "object") {
192
+ return undefined;
193
+ }
194
+ const source = value;
195
+ const fullName = stringValue(source.fullName) ?? stringValue(source.full_name);
196
+ if (!fullName) {
197
+ return undefined;
198
+ }
199
+ const imageUrl = stringValue(source.imageUrl) ?? stringValue(source.image_url);
200
+ return {
201
+ fullName,
202
+ ...(imageUrl ? { imageUrl } : {}),
203
+ // An owner without recorded provenance counts as self-declared; only the
204
+ // registry server stamps "authenticated" from a signed-in session.
205
+ source: ownerSourceValue(source.source) ?? "self-declared"
206
+ };
207
+ }
208
+ function ownerSourceValue(value) {
209
+ if (value === "authenticated" || value === "self-declared") {
210
+ return value;
211
+ }
212
+ return undefined;
213
+ }
214
+ function verifiedProviderValue(value) {
215
+ if (!value || typeof value !== "object") {
216
+ return undefined;
217
+ }
218
+ const source = value;
219
+ const kind = stringValue(source.kind)
220
+ ?? stringValue(source.provider)
221
+ ?? stringValue(source.providerKind)
222
+ ?? stringValue(source.provider_kind);
223
+ if (!kind) {
224
+ return undefined;
225
+ }
226
+ const version = stringValue(source.version);
227
+ const buildSha = stringValue(source.buildSha) ?? stringValue(source.build_sha);
228
+ const epoch = stringValue(source.epoch);
229
+ const sourceBaseUrl = stringValue(source.sourceBaseUrl) ?? stringValue(source.source_base_url);
230
+ const verifiedAt = stringValue(source.verifiedAt) ?? stringValue(source.verified_at);
231
+ return {
232
+ kind,
233
+ ...(version ? { version } : {}),
234
+ ...(buildSha ? { buildSha } : {}),
235
+ ...(epoch ? { epoch } : {}),
236
+ ...(sourceBaseUrl ? { sourceBaseUrl } : {}),
237
+ ...(verifiedAt ? { verifiedAt } : {})
238
+ };
239
+ }
240
+ // Merge registry records by baseUrl (last wins, but server-learned/stamped fields a
241
+ // bare re-registration omits — instanceId/owner/lifecycle/etc. — fall back to the
242
+ // existing value rather than being erased). The ONE merge: shared by the local
243
+ // ProviderRegistry, the standalone serve-registry, and the Cloud Worker, so they
244
+ // can't drift (the Worker's old private copy silently dropped aliases/lifecycle).
245
+ export function mergeRegistryProviders(providers) {
246
+ const merged = new Map();
247
+ for (const provider of providers) {
248
+ const existing = merged.get(provider.baseUrl);
249
+ merged.set(provider.baseUrl, {
250
+ ...existing,
251
+ ...provider,
252
+ aliases: mergeProviderAliases(existing, provider),
253
+ deploymentKind: provider.deploymentKind ?? existing?.deploymentKind,
254
+ instanceId: provider.instanceId ?? existing?.instanceId,
255
+ lifecycle: provider.lifecycle ?? existing?.lifecycle,
256
+ owner: provider.owner ?? existing?.owner,
257
+ ownerOrganizationId: provider.ownerOrganizationId ?? existing?.ownerOrganizationId,
258
+ registrySource: preferredRegistrySource(existing?.registrySource, provider.registrySource),
259
+ requiresAuth: provider.requiresAuth ?? existing?.requiresAuth,
260
+ verifiedProvider: provider.verifiedProvider ?? existing?.verifiedProvider
261
+ });
262
+ }
263
+ return [...merged.values()];
264
+ }
265
+ export function providerAliases(provider) {
266
+ return (provider.aliases ?? []).map(normalizeProviderAlias);
267
+ }
268
+ function mergeProviderAliases(left, right) {
269
+ const aliases = [...new Set([...providerAliases(left ?? { baseUrl: right.baseUrl }), ...providerAliases(right)])];
270
+ return aliases.length > 0 ? aliases : undefined;
271
+ }
272
+ function preferredRegistrySource(left, right) {
273
+ if (!left)
274
+ return right;
275
+ if (!right)
276
+ return left;
277
+ return registrySourceRank(right) > registrySourceRank(left) ? right : left;
278
+ }
279
+ function registrySourceRank(source) {
280
+ if (source === "local-registry")
281
+ return 3;
282
+ if (source === "console-memory")
283
+ return 2;
284
+ return 1;
285
+ }
@@ -0,0 +1,6 @@
1
+ export type LaunchTrace = {
2
+ readonly file: string;
3
+ event(type: string, data?: Record<string, unknown>): void;
4
+ };
5
+ export declare function launchTraceDir(): string;
6
+ export declare function createLaunchTrace(label: string): LaunchTrace;
@@ -0,0 +1,33 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function launchTraceDir() {
5
+ return process.env.TERMFLEET_LAUNCH_TRACE_DIR ?? join(homedir(), ".termfleet", "launch-traces");
6
+ }
7
+ let traceCounter = 0;
8
+ export function createLaunchTrace(label) {
9
+ const directory = launchTraceDir();
10
+ traceCounter += 1;
11
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
12
+ const slug = label.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "launch";
13
+ const file = join(directory, `${stamp}-${process.pid}-${traceCounter}-${slug}.jsonl`);
14
+ let disabled = false;
15
+ return {
16
+ event(type, data = {}) {
17
+ if (disabled) {
18
+ return;
19
+ }
20
+ try {
21
+ // 0700/0600: launch traces can capture terminal screen content on a
22
+ // failed launch — keep them owner-only on shared machines.
23
+ mkdirSync(directory, { mode: 0o700, recursive: true });
24
+ appendFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), type, ...data })}\n`, { mode: 0o600 });
25
+ }
26
+ catch (error) {
27
+ disabled = true;
28
+ console.error(`termfleet launch trace disabled, could not write ${file}: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ },
31
+ file
32
+ };
33
+ }
@@ -0,0 +1 @@
1
+ export declare function asError(value: unknown): Error;
@@ -0,0 +1,5 @@
1
+ // Coerce an unknown thrown value to an Error. Used everywhere a `catch (error)`
2
+ // (typed `unknown`) needs `.message` — one definition instead of ~15 copies.
3
+ export function asError(value) {
4
+ return value instanceof Error ? value : new Error(String(value));
5
+ }
@@ -0,0 +1,13 @@
1
+ export type RunOptions = {
2
+ cwd?: string;
3
+ env?: Record<string, string | undefined>;
4
+ timeoutMs?: number;
5
+ };
6
+ export declare function commandExists(command: string): boolean;
7
+ export declare function requireCommand(command: string, installHint?: string): void;
8
+ export declare function run(command: string, args: string[], options?: RunOptions): string;
9
+ export declare function runBuffer(command: string, args: string[], options?: RunOptions): Buffer;
10
+ export declare function runWithInput(command: string, args: string[], input: Buffer, options?: RunOptions): Buffer;
11
+ export declare function runAsync(command: string, args: string[], options?: RunOptions): Promise<string>;
12
+ export declare function runWithInputAsync(command: string, args: string[], input: Buffer | string, options?: RunOptions): Promise<string>;
13
+ export declare function spawnInherited(command: string, args: string[]): void;