@linzumi/cli 0.0.20-beta → 0.0.22-beta
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.
- package/README.md +65 -62
- package/bin/linzumi.js +10 -18
- package/dist/assets/linzumi-logo.svg +1 -0
- package/dist/index.js +9135 -0
- package/package.json +9 -4
- package/src/agentBootstrap.ts +0 -872
- package/src/authCache.ts +0 -157
- package/src/authResolution.ts +0 -77
- package/src/boundedCache.ts +0 -57
- package/src/channelSession.ts +0 -4301
- package/src/channelSessionSupport.ts +0 -308
- package/src/codexAppServer.ts +0 -380
- package/src/codexOutput.ts +0 -846
- package/src/codexRuntimeOptions.ts +0 -80
- package/src/dependencyStatus.ts +0 -198
- package/src/forwardTunnel.ts +0 -859
- package/src/forwardTunnelProtocol.ts +0 -324
- package/src/index.ts +0 -1080
- package/src/json.ts +0 -49
- package/src/kandanQueue.ts +0 -113
- package/src/kandanTls.ts +0 -86
- package/src/localCapabilities.ts +0 -143
- package/src/localCodexMessageState.ts +0 -135
- package/src/localCodexTurnState.ts +0 -108
- package/src/localConfig.ts +0 -99
- package/src/localEditor.ts +0 -1061
- package/src/localEditorRuntime.ts +0 -717
- package/src/localForwarding.ts +0 -523
- package/src/oauth.ts +0 -425
- package/src/pendingKandanMessageQueue.ts +0 -109
- package/src/phoenix.ts +0 -359
- package/src/portForwardApproval.ts +0 -181
- package/src/portForwardWatcher.ts +0 -404
- package/src/protocol.ts +0 -321
- package/src/runner.ts +0 -943
- package/src/runnerConsoleReporter.ts +0 -142
- package/src/runnerLogger.ts +0 -50
- package/src/streamDeltaCoalescing.ts +0 -129
- package/src/streamDeltaQueue.ts +0 -102
|
@@ -1,717 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
- Date: 2026-04-29
|
|
3
|
-
Spec: kandan/server_v2/plans/2026-04-29-local-runner-editor-runtime-distribution-note.md
|
|
4
|
-
Relationship: Keeps local editor launches on a Kandan-approved,
|
|
5
|
-
checksummed runtime distribution instead of relying on host package-manager
|
|
6
|
-
code-server layouts.
|
|
7
|
-
*/
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
-
import {
|
|
11
|
-
createReadStream,
|
|
12
|
-
createWriteStream,
|
|
13
|
-
existsSync,
|
|
14
|
-
mkdirSync,
|
|
15
|
-
mkdtempSync,
|
|
16
|
-
readFileSync,
|
|
17
|
-
renameSync,
|
|
18
|
-
rmSync,
|
|
19
|
-
writeFileSync,
|
|
20
|
-
} from "node:fs";
|
|
21
|
-
import { homedir } from "node:os";
|
|
22
|
-
import { dirname, join, resolve } from "node:path";
|
|
23
|
-
import { Readable } from "node:stream";
|
|
24
|
-
import { pipeline } from "node:stream/promises";
|
|
25
|
-
import { kandanHttpBaseUrl } from "./oauth";
|
|
26
|
-
import { isJsonObject, type JsonObject } from "./protocol";
|
|
27
|
-
|
|
28
|
-
export type EditorRuntimeManifest = {
|
|
29
|
-
readonly version: string;
|
|
30
|
-
readonly platform: string;
|
|
31
|
-
readonly archiveUrl: string;
|
|
32
|
-
readonly archiveSha256: string;
|
|
33
|
-
readonly codeServerVersion: string;
|
|
34
|
-
readonly codeServerBinPath: string;
|
|
35
|
-
readonly manifestPath: string;
|
|
36
|
-
readonly assets: readonly EditorRuntimeAsset[];
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type EditorRuntimeAsset = {
|
|
40
|
-
readonly path: string;
|
|
41
|
-
readonly sha256: string;
|
|
42
|
-
readonly url?: string | undefined;
|
|
43
|
-
readonly contentBase64?: string | undefined;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export type EditorRuntimeStatus =
|
|
47
|
-
| {
|
|
48
|
-
readonly status: "ready";
|
|
49
|
-
readonly mode: "server_managed";
|
|
50
|
-
readonly version: string;
|
|
51
|
-
readonly platform: string;
|
|
52
|
-
readonly archiveSha256: string;
|
|
53
|
-
readonly codeServerVersion: string;
|
|
54
|
-
readonly codeServerBin: string;
|
|
55
|
-
readonly updated: boolean;
|
|
56
|
-
}
|
|
57
|
-
| {
|
|
58
|
-
readonly status: "custom";
|
|
59
|
-
readonly mode: "custom";
|
|
60
|
-
readonly codeServerBin: string;
|
|
61
|
-
}
|
|
62
|
-
| {
|
|
63
|
-
readonly status: "unavailable";
|
|
64
|
-
readonly mode: "server_managed";
|
|
65
|
-
readonly reason:
|
|
66
|
-
| "unsupported_platform"
|
|
67
|
-
| "manifest_unavailable"
|
|
68
|
-
| "download_failed"
|
|
69
|
-
| "checksum_mismatch"
|
|
70
|
-
| "archive_extract_failed"
|
|
71
|
-
| "invalid_archive"
|
|
72
|
-
| "install_failed";
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export type InstalledEditorRuntime = {
|
|
76
|
-
readonly mode: "server_managed";
|
|
77
|
-
readonly root: string;
|
|
78
|
-
readonly codeServerBin: string;
|
|
79
|
-
readonly assets: {
|
|
80
|
-
readonly collaborationExtensionTarball: string;
|
|
81
|
-
readonly collaborationServerTarball: string;
|
|
82
|
-
readonly documentStateExtensionDir: string;
|
|
83
|
-
};
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export type ResolveEditorRuntimeOptions = {
|
|
87
|
-
readonly kandanUrl: string;
|
|
88
|
-
readonly token: string;
|
|
89
|
-
readonly customCodeServerBin?: string | undefined;
|
|
90
|
-
readonly cacheRoot?: string | undefined;
|
|
91
|
-
readonly platform?: NodeJS.Platform | undefined;
|
|
92
|
-
readonly arch?: NodeJS.Architecture | undefined;
|
|
93
|
-
readonly fetchImpl?: typeof fetch | undefined;
|
|
94
|
-
readonly extractArchive?: ((archivePath: string, destination: string) => Promise<boolean>) | undefined;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export type ResolveEditorRuntimeResult = {
|
|
98
|
-
readonly status: EditorRuntimeStatus;
|
|
99
|
-
readonly codeServerBin?: string | undefined;
|
|
100
|
-
readonly runtime?: InstalledEditorRuntime | undefined;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export async function resolveEditorRuntime(
|
|
104
|
-
options: ResolveEditorRuntimeOptions,
|
|
105
|
-
): Promise<ResolveEditorRuntimeResult> {
|
|
106
|
-
if (options.customCodeServerBin !== undefined) {
|
|
107
|
-
return {
|
|
108
|
-
codeServerBin: options.customCodeServerBin,
|
|
109
|
-
status: {
|
|
110
|
-
status: "custom",
|
|
111
|
-
mode: "custom",
|
|
112
|
-
codeServerBin: options.customCodeServerBin,
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const platform = editorRuntimePlatform(
|
|
118
|
-
options.platform ?? process.platform,
|
|
119
|
-
options.arch ?? process.arch,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if (platform === undefined) {
|
|
123
|
-
return unavailable("unsupported_platform");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const manifest = await fetchApprovedManifest({
|
|
127
|
-
kandanUrl: options.kandanUrl,
|
|
128
|
-
token: options.token,
|
|
129
|
-
platform,
|
|
130
|
-
fetchImpl: options.fetchImpl ?? fetch,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
if (!manifest.ok) {
|
|
134
|
-
return unavailable("manifest_unavailable");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const cacheRoot = options.cacheRoot ?? defaultEditorRuntimeCacheRoot();
|
|
138
|
-
const installed = installedRuntime(cacheRoot, manifest.manifest);
|
|
139
|
-
|
|
140
|
-
if (installed.ok) {
|
|
141
|
-
return ready(manifest.manifest, installed.runtime, false);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const installResult = await installRuntime({
|
|
145
|
-
kandanUrl: options.kandanUrl,
|
|
146
|
-
token: options.token,
|
|
147
|
-
manifest: manifest.manifest,
|
|
148
|
-
cacheRoot,
|
|
149
|
-
fetchImpl: options.fetchImpl ?? fetch,
|
|
150
|
-
extractArchive: options.extractArchive ?? extractTarGz,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
if (!installResult.ok) {
|
|
154
|
-
return unavailable(installResult.reason);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return ready(manifest.manifest, installResult.runtime, true);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function editorRuntimeStatusPayload(status: EditorRuntimeStatus): JsonObject {
|
|
161
|
-
switch (status.status) {
|
|
162
|
-
case "ready":
|
|
163
|
-
return {
|
|
164
|
-
status: "ready",
|
|
165
|
-
mode: status.mode,
|
|
166
|
-
version: status.version,
|
|
167
|
-
platform: status.platform,
|
|
168
|
-
archiveSha256: status.archiveSha256,
|
|
169
|
-
codeServerVersion: status.codeServerVersion,
|
|
170
|
-
updated: status.updated,
|
|
171
|
-
};
|
|
172
|
-
case "custom":
|
|
173
|
-
return {
|
|
174
|
-
status: "custom",
|
|
175
|
-
mode: status.mode,
|
|
176
|
-
codeServerBin: status.codeServerBin,
|
|
177
|
-
};
|
|
178
|
-
case "unavailable":
|
|
179
|
-
return {
|
|
180
|
-
status: "unavailable",
|
|
181
|
-
mode: status.mode,
|
|
182
|
-
reason: status.reason,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function editorRuntimePlatform(
|
|
188
|
-
platform: NodeJS.Platform,
|
|
189
|
-
arch: NodeJS.Architecture,
|
|
190
|
-
): string | undefined {
|
|
191
|
-
switch (`${platform}-${arch}`) {
|
|
192
|
-
case "darwin-arm64":
|
|
193
|
-
return "darwin-arm64";
|
|
194
|
-
case "darwin-x64":
|
|
195
|
-
return "darwin-x64";
|
|
196
|
-
case "linux-x64":
|
|
197
|
-
return "linux-x64";
|
|
198
|
-
default:
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function unavailable(reason: Extract<EditorRuntimeStatus, { status: "unavailable" }>["reason"]): ResolveEditorRuntimeResult {
|
|
204
|
-
return {
|
|
205
|
-
status: {
|
|
206
|
-
status: "unavailable",
|
|
207
|
-
mode: "server_managed",
|
|
208
|
-
reason,
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function ready(
|
|
214
|
-
manifest: EditorRuntimeManifest,
|
|
215
|
-
runtime: InstalledEditorRuntime,
|
|
216
|
-
updated: boolean,
|
|
217
|
-
): ResolveEditorRuntimeResult {
|
|
218
|
-
return {
|
|
219
|
-
codeServerBin: runtime.codeServerBin,
|
|
220
|
-
runtime,
|
|
221
|
-
status: {
|
|
222
|
-
status: "ready",
|
|
223
|
-
mode: "server_managed",
|
|
224
|
-
version: manifest.version,
|
|
225
|
-
platform: manifest.platform,
|
|
226
|
-
archiveSha256: manifest.archiveSha256,
|
|
227
|
-
codeServerVersion: manifest.codeServerVersion,
|
|
228
|
-
codeServerBin: runtime.codeServerBin,
|
|
229
|
-
updated,
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function fetchApprovedManifest(args: {
|
|
235
|
-
readonly kandanUrl: string;
|
|
236
|
-
readonly token: string;
|
|
237
|
-
readonly platform: string;
|
|
238
|
-
readonly fetchImpl: typeof fetch;
|
|
239
|
-
}): Promise<
|
|
240
|
-
| { readonly ok: true; readonly manifest: EditorRuntimeManifest }
|
|
241
|
-
| { readonly ok: false }
|
|
242
|
-
> {
|
|
243
|
-
const url = new URL(
|
|
244
|
-
`/api/v2/local-codex-runner/editor-runtime/${encodeURIComponent(args.platform)}/manifest`,
|
|
245
|
-
kandanHttpBaseUrl(args.kandanUrl),
|
|
246
|
-
);
|
|
247
|
-
const response = await args.fetchImpl(url, {
|
|
248
|
-
headers: { authorization: `Bearer ${args.token}` },
|
|
249
|
-
}).catch(() => undefined);
|
|
250
|
-
|
|
251
|
-
if (response === undefined) {
|
|
252
|
-
return { ok: false };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (response.status !== 200) {
|
|
256
|
-
return { ok: false };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const body: unknown = await response.json();
|
|
260
|
-
|
|
261
|
-
if (!isJsonObject(body) || body.ok !== true || !isJsonObject(body.runtime)) {
|
|
262
|
-
return { ok: false };
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return normalizeManifest(body.runtime);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function normalizeManifest(value: JsonObject):
|
|
269
|
-
| { readonly ok: true; readonly manifest: EditorRuntimeManifest }
|
|
270
|
-
| { readonly ok: false } {
|
|
271
|
-
const version = nonEmptyString(value.version);
|
|
272
|
-
const platform = nonEmptyString(value.platform);
|
|
273
|
-
const archiveUrl = nonEmptyString(value.archiveUrl);
|
|
274
|
-
const archiveSha256 = sha256String(value.archiveSha256);
|
|
275
|
-
const codeServerVersion = nonEmptyString(value.codeServerVersion);
|
|
276
|
-
const codeServerBinPath = nonEmptyString(value.codeServerBinPath) ?? "bin/code-server";
|
|
277
|
-
const manifestPath = nonEmptyString(value.manifestPath) ?? "linzumi-editor-runtime.json";
|
|
278
|
-
const assets = normalizeRuntimeAssets(value.assets);
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
version === undefined ||
|
|
282
|
-
platform === undefined ||
|
|
283
|
-
archiveUrl === undefined ||
|
|
284
|
-
archiveSha256 === undefined ||
|
|
285
|
-
codeServerVersion === undefined ||
|
|
286
|
-
assets === undefined
|
|
287
|
-
) {
|
|
288
|
-
return { ok: false };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
ok: true,
|
|
293
|
-
manifest: {
|
|
294
|
-
version,
|
|
295
|
-
platform,
|
|
296
|
-
archiveUrl,
|
|
297
|
-
archiveSha256,
|
|
298
|
-
codeServerVersion,
|
|
299
|
-
codeServerBinPath,
|
|
300
|
-
manifestPath,
|
|
301
|
-
assets,
|
|
302
|
-
},
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function normalizeRuntimeAssets(value: unknown): readonly EditorRuntimeAsset[] | undefined {
|
|
307
|
-
if (!Array.isArray(value)) {
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const assets: EditorRuntimeAsset[] = [];
|
|
312
|
-
|
|
313
|
-
for (const asset of value) {
|
|
314
|
-
if (!isJsonObject(asset)) {
|
|
315
|
-
return undefined;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const path = nonEmptyString(asset.path);
|
|
319
|
-
const sha256 = sha256String(asset.sha256);
|
|
320
|
-
const url = nonEmptyString(asset.url);
|
|
321
|
-
const contentBase64 = nonEmptyString(asset.contentBase64);
|
|
322
|
-
|
|
323
|
-
if (path === undefined || sha256 === undefined) {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
assets.push({
|
|
328
|
-
path,
|
|
329
|
-
sha256,
|
|
330
|
-
...(url === undefined ? {} : { url }),
|
|
331
|
-
...(contentBase64 === undefined ? {} : { contentBase64 }),
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return assets;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function installedRuntime(
|
|
339
|
-
cacheRoot: string,
|
|
340
|
-
manifest: EditorRuntimeManifest,
|
|
341
|
-
): { readonly ok: true; readonly runtime: InstalledEditorRuntime } | { readonly ok: false } {
|
|
342
|
-
const runtimeRoot = runtimeInstallRoot(cacheRoot, manifest);
|
|
343
|
-
const manifestPath = join(runtimeRoot, manifest.manifestPath);
|
|
344
|
-
const codeServerBin = join(runtimeRoot, manifest.codeServerBinPath);
|
|
345
|
-
const assets = verifiedRuntimeAssetPaths(runtimeRoot, manifest);
|
|
346
|
-
|
|
347
|
-
if (!existsSync(manifestPath) || !existsSync(codeServerBin) || assets === undefined) {
|
|
348
|
-
return { ok: false };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
const installed: unknown = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
isJsonObject(installed) &&
|
|
356
|
-
installed.version === manifest.version &&
|
|
357
|
-
installed.platform === manifest.platform &&
|
|
358
|
-
(installed.archiveSha256 === undefined ||
|
|
359
|
-
installed.archiveSha256 === manifest.archiveSha256)
|
|
360
|
-
) {
|
|
361
|
-
return {
|
|
362
|
-
ok: true,
|
|
363
|
-
runtime: {
|
|
364
|
-
mode: "server_managed",
|
|
365
|
-
root: runtimeRoot,
|
|
366
|
-
codeServerBin,
|
|
367
|
-
assets,
|
|
368
|
-
},
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
} catch (_error) {
|
|
372
|
-
return { ok: false };
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return { ok: false };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async function installRuntime(args: {
|
|
379
|
-
readonly kandanUrl: string;
|
|
380
|
-
readonly token: string;
|
|
381
|
-
readonly manifest: EditorRuntimeManifest;
|
|
382
|
-
readonly cacheRoot: string;
|
|
383
|
-
readonly fetchImpl: typeof fetch;
|
|
384
|
-
readonly extractArchive: (archivePath: string, destination: string) => Promise<boolean>;
|
|
385
|
-
}): Promise<
|
|
386
|
-
| { readonly ok: true; readonly runtime: InstalledEditorRuntime }
|
|
387
|
-
| { readonly ok: false; readonly reason: Extract<EditorRuntimeStatus, { status: "unavailable" }>["reason"] }
|
|
388
|
-
> {
|
|
389
|
-
mkdirSync(args.cacheRoot, { recursive: true });
|
|
390
|
-
|
|
391
|
-
const tempRoot = mkdtempSync(join(args.cacheRoot, ".install-"));
|
|
392
|
-
const archivePath = join(tempRoot, "runtime.tar.gz");
|
|
393
|
-
const extractRoot = join(tempRoot, "runtime");
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
const downloaded = await downloadArchive({
|
|
397
|
-
kandanUrl: args.kandanUrl,
|
|
398
|
-
token: args.token,
|
|
399
|
-
manifest: args.manifest,
|
|
400
|
-
archivePath,
|
|
401
|
-
fetchImpl: args.fetchImpl,
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
if (!downloaded.ok) {
|
|
405
|
-
return downloaded;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
mkdirSync(extractRoot, { recursive: true });
|
|
409
|
-
|
|
410
|
-
if (!(await args.extractArchive(archivePath, extractRoot))) {
|
|
411
|
-
return { ok: false, reason: "archive_extract_failed" };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const assetsInstalled = await materializeRuntimeAssets({
|
|
415
|
-
kandanUrl: args.kandanUrl,
|
|
416
|
-
manifest: args.manifest,
|
|
417
|
-
runtimeRoot: extractRoot,
|
|
418
|
-
fetchImpl: args.fetchImpl,
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
if (!assetsInstalled) {
|
|
422
|
-
return { ok: false, reason: "install_failed" };
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const manifestPath = join(extractRoot, args.manifest.manifestPath);
|
|
426
|
-
const codeServerBin = join(extractRoot, args.manifest.codeServerBinPath);
|
|
427
|
-
const assets = verifiedRuntimeAssetPaths(extractRoot, args.manifest);
|
|
428
|
-
|
|
429
|
-
if (!existsSync(codeServerBin) || assets === undefined) {
|
|
430
|
-
return { ok: false, reason: "invalid_archive" };
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
434
|
-
writeFileSync(
|
|
435
|
-
manifestPath,
|
|
436
|
-
JSON.stringify(
|
|
437
|
-
{
|
|
438
|
-
version: args.manifest.version,
|
|
439
|
-
platform: args.manifest.platform,
|
|
440
|
-
archiveSha256: args.manifest.archiveSha256,
|
|
441
|
-
codeServerVersion: args.manifest.codeServerVersion,
|
|
442
|
-
codeServerBinPath: args.manifest.codeServerBinPath,
|
|
443
|
-
manifestPath: args.manifest.manifestPath,
|
|
444
|
-
assets: args.manifest.assets,
|
|
445
|
-
},
|
|
446
|
-
null,
|
|
447
|
-
2,
|
|
448
|
-
),
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
const targetRoot = runtimeInstallRoot(args.cacheRoot, args.manifest);
|
|
452
|
-
rmSync(targetRoot, { recursive: true, force: true });
|
|
453
|
-
mkdirSync(dirname(targetRoot), { recursive: true });
|
|
454
|
-
renameSync(extractRoot, targetRoot);
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
ok: true,
|
|
458
|
-
runtime: {
|
|
459
|
-
mode: "server_managed",
|
|
460
|
-
root: targetRoot,
|
|
461
|
-
codeServerBin: join(targetRoot, args.manifest.codeServerBinPath),
|
|
462
|
-
assets: {
|
|
463
|
-
collaborationExtensionTarball: join(
|
|
464
|
-
targetRoot,
|
|
465
|
-
"kandan",
|
|
466
|
-
"editor_extensions",
|
|
467
|
-
"typefox.open-collaboration-tools.tar.gz",
|
|
468
|
-
),
|
|
469
|
-
collaborationServerTarball: join(
|
|
470
|
-
targetRoot,
|
|
471
|
-
"kandan",
|
|
472
|
-
"editor_servers",
|
|
473
|
-
"open-collaboration-server.tar.gz",
|
|
474
|
-
),
|
|
475
|
-
documentStateExtensionDir: join(
|
|
476
|
-
targetRoot,
|
|
477
|
-
"kandan",
|
|
478
|
-
"editor_extensions",
|
|
479
|
-
"kandan.document-state-telemetry",
|
|
480
|
-
),
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
};
|
|
484
|
-
} catch (_error) {
|
|
485
|
-
return { ok: false, reason: "install_failed" };
|
|
486
|
-
} finally {
|
|
487
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async function materializeRuntimeAssets(args: {
|
|
492
|
-
readonly kandanUrl: string;
|
|
493
|
-
readonly manifest: EditorRuntimeManifest;
|
|
494
|
-
readonly runtimeRoot: string;
|
|
495
|
-
readonly fetchImpl: typeof fetch;
|
|
496
|
-
}): Promise<boolean> {
|
|
497
|
-
for (const asset of args.manifest.assets) {
|
|
498
|
-
const targetPath = join(args.runtimeRoot, asset.path);
|
|
499
|
-
|
|
500
|
-
try {
|
|
501
|
-
const bytes = await runtimeAssetBytes({
|
|
502
|
-
kandanUrl: args.kandanUrl,
|
|
503
|
-
asset,
|
|
504
|
-
fetchImpl: args.fetchImpl,
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
if (bytes === undefined) {
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
512
|
-
writeFileSync(targetPath, bytes);
|
|
513
|
-
} catch (_error) {
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return true;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
async function runtimeAssetBytes(args: {
|
|
522
|
-
readonly kandanUrl: string;
|
|
523
|
-
readonly asset: EditorRuntimeAsset;
|
|
524
|
-
readonly fetchImpl: typeof fetch;
|
|
525
|
-
}): Promise<Buffer | undefined> {
|
|
526
|
-
if (args.asset.contentBase64 !== undefined) {
|
|
527
|
-
return Buffer.from(args.asset.contentBase64, "base64");
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (args.asset.url !== undefined) {
|
|
531
|
-
const url = new URL(args.asset.url, kandanHttpBaseUrl(args.kandanUrl));
|
|
532
|
-
const response = await args.fetchImpl(url);
|
|
533
|
-
|
|
534
|
-
if (response.status !== 200) {
|
|
535
|
-
return undefined;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
return Buffer.from(await response.arrayBuffer());
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return undefined;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
async function downloadArchive(args: {
|
|
545
|
-
readonly kandanUrl: string;
|
|
546
|
-
readonly token: string;
|
|
547
|
-
readonly manifest: EditorRuntimeManifest;
|
|
548
|
-
readonly archivePath: string;
|
|
549
|
-
readonly fetchImpl: typeof fetch;
|
|
550
|
-
}): Promise<
|
|
551
|
-
| { readonly ok: true }
|
|
552
|
-
| { readonly ok: false; readonly reason: "download_failed" | "checksum_mismatch" }
|
|
553
|
-
> {
|
|
554
|
-
const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
|
|
555
|
-
const url = new URL(args.manifest.archiveUrl, kandanBaseUrl);
|
|
556
|
-
const response = await args.fetchImpl(url, {
|
|
557
|
-
...(sameOrigin(url, new URL(kandanBaseUrl))
|
|
558
|
-
? { headers: { authorization: `Bearer ${args.token}` } }
|
|
559
|
-
: {}),
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
if (response.status !== 200 || response.body === null) {
|
|
563
|
-
return { ok: false, reason: "download_failed" };
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
await pipeline(
|
|
567
|
-
Readable.fromWeb(response.body),
|
|
568
|
-
createWriteStream(args.archivePath),
|
|
569
|
-
);
|
|
570
|
-
|
|
571
|
-
const sha256 = await fileSha256(args.archivePath);
|
|
572
|
-
|
|
573
|
-
if (sha256 !== args.manifest.archiveSha256) {
|
|
574
|
-
return { ok: false, reason: "checksum_mismatch" };
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return { ok: true };
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function sameOrigin(left: URL, right: URL): boolean {
|
|
581
|
-
return left.protocol === right.protocol && left.host === right.host;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function extractTarGz(archivePath: string, destination: string): Promise<boolean> {
|
|
585
|
-
return new Promise((resolveExtract) => {
|
|
586
|
-
const child = spawn("tar", ["-xzf", archivePath, "-C", destination], {
|
|
587
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
child.on("error", () => resolveExtract(false));
|
|
591
|
-
child.on("exit", (code) => resolveExtract(code === 0));
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function fileSha256(path: string): Promise<string> {
|
|
596
|
-
return new Promise((resolveHash, rejectHash) => {
|
|
597
|
-
const hash = createHash("sha256");
|
|
598
|
-
const stream = createReadStream(path);
|
|
599
|
-
|
|
600
|
-
stream.on("error", rejectHash);
|
|
601
|
-
stream.on("data", (chunk) => hash.update(chunk));
|
|
602
|
-
stream.on("end", () => resolveHash(hash.digest("hex")));
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function runtimeInstallRoot(
|
|
607
|
-
cacheRoot: string,
|
|
608
|
-
manifest: EditorRuntimeManifest,
|
|
609
|
-
): string {
|
|
610
|
-
return resolve(cacheRoot, manifest.platform, manifest.archiveSha256);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function verifiedRuntimeAssetPaths(
|
|
614
|
-
runtimeRoot: string,
|
|
615
|
-
manifest: EditorRuntimeManifest,
|
|
616
|
-
): InstalledEditorRuntime["assets"] | undefined {
|
|
617
|
-
const collaborationExtensionTarball = join(
|
|
618
|
-
runtimeRoot,
|
|
619
|
-
"kandan",
|
|
620
|
-
"editor_extensions",
|
|
621
|
-
"typefox.open-collaboration-tools.tar.gz",
|
|
622
|
-
);
|
|
623
|
-
const collaborationServerTarball = join(
|
|
624
|
-
runtimeRoot,
|
|
625
|
-
"kandan",
|
|
626
|
-
"editor_servers",
|
|
627
|
-
"open-collaboration-server.tar.gz",
|
|
628
|
-
);
|
|
629
|
-
const documentStateExtensionDir = join(
|
|
630
|
-
runtimeRoot,
|
|
631
|
-
"kandan",
|
|
632
|
-
"editor_extensions",
|
|
633
|
-
"kandan.document-state-telemetry",
|
|
634
|
-
);
|
|
635
|
-
|
|
636
|
-
const codeServerRoot = codeServerRuntimeRoot(manifest.codeServerBinPath);
|
|
637
|
-
const requiredPaths = [
|
|
638
|
-
manifest.codeServerBinPath,
|
|
639
|
-
join(codeServerRoot, "lib", "vscode", "node_modules", "vsda", "rust", "web", "vsda.js"),
|
|
640
|
-
join(codeServerRoot, "lib", "vscode", "node_modules", "vsda", "rust", "web", "vsda_bg.wasm"),
|
|
641
|
-
"kandan/editor_extensions/typefox.open-collaboration-tools.tar.gz",
|
|
642
|
-
"kandan/editor_servers/open-collaboration-server.tar.gz",
|
|
643
|
-
"kandan/editor_extensions/kandan.document-state-telemetry/package.json",
|
|
644
|
-
"kandan/editor_extensions/kandan.document-state-telemetry/out/extension.js",
|
|
645
|
-
];
|
|
646
|
-
const assetChecksums = manifestAssetChecksums(manifest.assets);
|
|
647
|
-
|
|
648
|
-
for (const relativePath of requiredPaths) {
|
|
649
|
-
const expectedSha256 = assetChecksums.get(relativePath);
|
|
650
|
-
|
|
651
|
-
if (expectedSha256 === undefined && relativePath !== manifest.codeServerBinPath) {
|
|
652
|
-
return undefined;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const absolutePath = join(runtimeRoot, relativePath);
|
|
656
|
-
|
|
657
|
-
if (!existsSync(absolutePath)) {
|
|
658
|
-
return undefined;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (expectedSha256 !== undefined && fileSha256Sync(absolutePath) !== expectedSha256) {
|
|
662
|
-
return undefined;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (!existsSync(collaborationExtensionTarball) || !existsSync(collaborationServerTarball)) {
|
|
667
|
-
return undefined;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
return {
|
|
671
|
-
collaborationExtensionTarball,
|
|
672
|
-
collaborationServerTarball,
|
|
673
|
-
documentStateExtensionDir,
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function codeServerRuntimeRoot(codeServerBinPath: string): string {
|
|
678
|
-
const normalized = codeServerBinPath.replaceAll("\\", "/");
|
|
679
|
-
|
|
680
|
-
return normalized === "bin/code-server"
|
|
681
|
-
? "."
|
|
682
|
-
: normalized.endsWith("/bin/code-server")
|
|
683
|
-
? normalized.slice(0, -"/bin/code-server".length) || "."
|
|
684
|
-
: dirname(normalized);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function manifestAssetChecksums(assets: readonly EditorRuntimeAsset[]): Map<string, string> {
|
|
688
|
-
const checksums = new Map<string, string>();
|
|
689
|
-
|
|
690
|
-
for (const asset of assets) {
|
|
691
|
-
checksums.set(asset.path, asset.sha256);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
return checksums;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function fileSha256Sync(path: string): string {
|
|
698
|
-
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function defaultEditorRuntimeCacheRoot(): string {
|
|
702
|
-
return join(homedir(), ".linzumi", "editor-runtimes");
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function nonEmptyString(value: unknown): string | undefined {
|
|
706
|
-
return typeof value === "string" && value.trim() !== ""
|
|
707
|
-
? value.trim()
|
|
708
|
-
: undefined;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function sha256String(value: unknown): string | undefined {
|
|
712
|
-
const normalized = nonEmptyString(value);
|
|
713
|
-
|
|
714
|
-
return normalized !== undefined && /^[a-f0-9]{64}$/i.test(normalized)
|
|
715
|
-
? normalized.toLowerCase()
|
|
716
|
-
: undefined;
|
|
717
|
-
}
|