@spunto/build 0.25.0 → 0.26.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.
- package/package.json +1 -1
- package/src/script/setup-script.ts +163 -106
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spunto/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Spunto's shared Build engine — the devcontainer image protocol and VS Code extension registry clients, with no database, no HTTP framework and no UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -447,6 +447,15 @@ function buildFeatureInstallScript(ociRef: string, options?: Record<string, stri
|
|
|
447
447
|
function buildLocalFeatureInstallScript(featureId: string, scriptFile: string): string | null {
|
|
448
448
|
const scriptContent = localFeatureScript(scriptFile)
|
|
449
449
|
if (!scriptContent) return null
|
|
450
|
+
return buildInlineFeatureInstallScript(featureId, scriptContent)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* A feature whose install script the caller hands over as text — embedded in the recipe like a
|
|
455
|
+
* local one. What an app that serves its own feature gives (Spunto IDE's `ide.feature`, RFC 0059):
|
|
456
|
+
* no registry in between, and the recipe's text, so its key, moves with the script.
|
|
457
|
+
*/
|
|
458
|
+
function buildInlineFeatureInstallScript(featureId: string, scriptContent: string): string {
|
|
450
459
|
const encoded = b64(scriptContent)
|
|
451
460
|
return [
|
|
452
461
|
`echo "[feature] ${featureId}: running local install script..."`,
|
|
@@ -468,11 +477,15 @@ function buildLocalFeatureInstallScript(featureId: string, scriptFile: string):
|
|
|
468
477
|
export type ResolvedFeature = { id: string; script: string; ociRef?: string }
|
|
469
478
|
|
|
470
479
|
function resolveFeatures(
|
|
471
|
-
features: { id: string; ociRef?: string; options?: Record<string, string
|
|
480
|
+
features: { id: string; ociRef?: string; options?: Record<string, string>; script?: string }[],
|
|
472
481
|
warnId?: string,
|
|
473
482
|
): ResolvedFeature[] {
|
|
474
483
|
const resolved: ResolvedFeature[] = []
|
|
475
484
|
for (const f of features) {
|
|
485
|
+
if (f.script) {
|
|
486
|
+
resolved.push({ id: f.id, script: buildInlineFeatureInstallScript(f.id, f.script) })
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
476
489
|
const known = AVAILABLE_FEATURES.find((af) => af.id === f.id)
|
|
477
490
|
if (known?.localScript) {
|
|
478
491
|
const script = buildLocalFeatureInstallScript(f.id, known.localScript)
|
|
@@ -555,8 +568,14 @@ function commonUtilsOptions(): Record<string, string> {
|
|
|
555
568
|
* Not strict: a base image where tmux won't install must still produce a usable worker, exactly as
|
|
556
569
|
* the best-effort blocks it replaces did.
|
|
557
570
|
*/
|
|
558
|
-
function spuntoPackOptions(params: { sshGatewayPublicKey?: string }): Record<string, string> {
|
|
559
|
-
return {
|
|
571
|
+
function spuntoPackOptions(params: { sshGatewayPublicKey?: string; codeServerInPack?: boolean }): Record<string, string> {
|
|
572
|
+
return {
|
|
573
|
+
installSshd: params.sshGatewayPublicKey ? "true" : "false",
|
|
574
|
+
// A platform that installs the IDE apart (`spunto-ide`, Spunto's IDE app — RFC 0059) keeps the
|
|
575
|
+
// pack to its plumbing: tmux, dtach, sshd. Left out otherwise, so the recipe of a caller that
|
|
576
|
+
// never asks (spunto-lite) stays the same text.
|
|
577
|
+
...(params.codeServerInPack === false ? { installCodeServer: "false" } : {}),
|
|
578
|
+
}
|
|
560
579
|
}
|
|
561
580
|
|
|
562
581
|
// The system-wide tmux configuration (mouse, OSC 52 clipboard, 50k scrollback, warm status bar)
|
|
@@ -689,11 +708,14 @@ export function defaultVscodeUserSettings(opts?: {
|
|
|
689
708
|
* 1 → the hand-written blocks (vscode user, code-server, tmux, dtach, sshd).
|
|
690
709
|
* 2 → common-utils + spunto-pack.
|
|
691
710
|
* 3 → spunto-pack 1.1: the IDE wears Spunto's name, icons and themes (coderhammer/features).
|
|
711
|
+
* 4 → the IDE out of the pack (`codeServerInPack: false`): code-server comes from `spunto-ide`,
|
|
712
|
+
* the feature of an IDE app, and only in the image of a project that has one (RFC 0059).
|
|
692
713
|
*/
|
|
693
|
-
export const IMAGE_RECIPE_VERSION =
|
|
714
|
+
export const IMAGE_RECIPE_VERSION = 4
|
|
694
715
|
|
|
695
716
|
export function buildImageScript(params: {
|
|
696
|
-
|
|
717
|
+
/** `script`: the feature's install script itself, embedded as is (an app that serves its own). */
|
|
718
|
+
features: { id: string; ociRef?: string; options?: Record<string, string>; script?: string }[]
|
|
697
719
|
vscodeExtensions?: string[]
|
|
698
720
|
sshGatewayPublicKey?: string
|
|
699
721
|
/** Project-level DinD opt-in (base image bakes Docker itself, no feature). See projects.dind. */
|
|
@@ -703,6 +725,13 @@ export function buildImageScript(params: {
|
|
|
703
725
|
* `--install-extension` calls below resolve ids against. Absent/unusable = Open VSX.
|
|
704
726
|
*/
|
|
705
727
|
extensionsGallery?: string | null
|
|
728
|
+
/**
|
|
729
|
+
* Whether spunto-pack installs code-server, as it does by default. `false`: the pack is plumbing
|
|
730
|
+
* only (tmux, dtach, sshd), and code-server comes from a feature of the caller's — Spunto's IDE
|
|
731
|
+
* app puts `spunto-ide` among `features` for a project that has an IDE (RFC 0059). Extensions are
|
|
732
|
+
* then installed only if one of the features brought a code-server.
|
|
733
|
+
*/
|
|
734
|
+
codeServerInPack?: boolean
|
|
706
735
|
}): { script: string; hasDinD: boolean; resolvedFeatures: ResolvedFeature[]; steps: BuildStep[] } {
|
|
707
736
|
const lines: string[] = ["set -e"]
|
|
708
737
|
|
|
@@ -818,6 +847,18 @@ export function buildImageScript(params: {
|
|
|
818
847
|
)
|
|
819
848
|
}
|
|
820
849
|
extLines.push('echo "[build] Extensions installed"')
|
|
850
|
+
// code-server from a feature of the caller's (`codeServerInPack: false`): said plainly when none
|
|
851
|
+
// brought it, rather than one "command not found" per extension.
|
|
852
|
+
if (params.codeServerInPack === false) {
|
|
853
|
+
extLines.splice(
|
|
854
|
+
0,
|
|
855
|
+
0,
|
|
856
|
+
"if ! command -v code-server >/dev/null 2>&1; then",
|
|
857
|
+
' echo "[build] No code-server in this image (no IDE feature) — VS Code extensions skipped"',
|
|
858
|
+
"else",
|
|
859
|
+
)
|
|
860
|
+
extLines.push("fi")
|
|
861
|
+
}
|
|
821
862
|
const n = params.vscodeExtensions.length
|
|
822
863
|
block("extensions", "VS Code extensions", "extensions", `${n} extension${n > 1 ? "s" : ""} · ${registry}`, extLines)
|
|
823
864
|
}
|
|
@@ -1229,6 +1270,14 @@ export type StartScriptParams = {
|
|
|
1229
1270
|
* assume). For a box where 8080 is taken by the host itself — a Blaxel sandbox's own API.
|
|
1230
1271
|
*/
|
|
1231
1272
|
idePort?: number
|
|
1273
|
+
/**
|
|
1274
|
+
* Whether the box has an IDE to start — code-server, as it always had. `false`: a project without
|
|
1275
|
+
* an IDE app (RFC 0059) — no extensions copied, no editor settings, no code-server loop; the
|
|
1276
|
+
* terminal (tmux/dtach, the agent's) is not the IDE's and stays.
|
|
1277
|
+
*/
|
|
1278
|
+
ide?: boolean
|
|
1279
|
+
/** Settings the IDE app adds to the seeded defaults (`ide.config` → `userSettings`), over them. */
|
|
1280
|
+
ideUserSettings?: Record<string, unknown>
|
|
1232
1281
|
}
|
|
1233
1282
|
|
|
1234
1283
|
/** code-server's port when the caller doesn't say otherwise. */
|
|
@@ -1243,6 +1292,7 @@ function idePortOf(port: number | undefined): number {
|
|
|
1243
1292
|
export function buildStartScript(params: StartScriptParams): { script: string; hasDinD: boolean } {
|
|
1244
1293
|
const { project, sshGatewayPublicKey, colorTabsByProject, extensionsGallery } = params
|
|
1245
1294
|
const idePort = idePortOf(params.idePort)
|
|
1295
|
+
const ide = params.ide !== false
|
|
1246
1296
|
const homeDir = "/home/vscode"
|
|
1247
1297
|
const username = "vscode"
|
|
1248
1298
|
|
|
@@ -1333,112 +1383,119 @@ export function buildStartScript(params: StartScriptParams): { script: string; h
|
|
|
1333
1383
|
"done",
|
|
1334
1384
|
)
|
|
1335
1385
|
|
|
1336
|
-
// ── 4.
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1386
|
+
// ── 4–5. The IDE: code-server, its extensions and settings (none without an IDE app) ───
|
|
1387
|
+
if (ide) {
|
|
1388
|
+
// ── 4. Copy prebuilt extensions ───────────────────────────────────────────
|
|
1389
|
+
push(
|
|
1390
|
+
"",
|
|
1391
|
+
'if [ -d ' + EXTENSIONS_DIR + ' ] && [ "$(ls -A ' + EXTENSIONS_DIR + ' 2>/dev/null)" ]; then',
|
|
1392
|
+
` mkdir -p ${homeDir}/.local/share/code-server/extensions`,
|
|
1393
|
+
` cp -rn ${EXTENSIONS_DIR}/. ${homeDir}/.local/share/code-server/extensions/ 2>/dev/null || true`,
|
|
1394
|
+
` chown -R ${username}:${username} ${homeDir}/.local`,
|
|
1395
|
+
"fi",
|
|
1396
|
+
)
|
|
1345
1397
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1398
|
+
// ── 4b. Seed code-server user settings (no-clobber, stays user-overridable) ─
|
|
1399
|
+
// Reordered window.title + per-project window color make parallel worker tabs
|
|
1400
|
+
// tellable apart at a glance (see defaultVscodeUserSettings). Written no-clobber
|
|
1401
|
+
// (guarded on the file, not `cp -n`) so a user's own settings.json survives restarts.
|
|
1402
|
+
const vscodeSettings = JSON.stringify(
|
|
1403
|
+
{ ...defaultVscodeUserSettings({ projectName: project.name, colorByProject: colorTabsByProject }), ...params.ideUserSettings },
|
|
1404
|
+
null,
|
|
1405
|
+
2,
|
|
1406
|
+
)
|
|
1407
|
+
push(
|
|
1408
|
+
"",
|
|
1409
|
+
`mkdir -p ${homeDir}/.local/share/code-server/User`,
|
|
1410
|
+
`if [ ! -f ${homeDir}/.local/share/code-server/User/settings.json ]; then`,
|
|
1411
|
+
` echo ${JSON.stringify(b64(vscodeSettings))} | base64 -d > ${homeDir}/.local/share/code-server/User/settings.json`,
|
|
1412
|
+
` chown -R ${username}:${username} ${homeDir}/.local`,
|
|
1413
|
+
"fi",
|
|
1414
|
+
)
|
|
1363
1415
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1416
|
+
// ── 4c. Restore the user's PORT secret for code-server's own terminal panel ─
|
|
1417
|
+
// code-server reads $PORT to override --bind-addr (see § 5 below), so its
|
|
1418
|
+
// whole process tree — including the integrated terminal it spawns — inherits
|
|
1419
|
+
// PORT=<idePort> unless corrected. `terminal.integrated.env.linux` lets VS Code
|
|
1420
|
+
// inject env overrides into terminals it spawns without touching the server
|
|
1421
|
+
// process itself, so a `PORT` project secret still reaches the user's shell.
|
|
1422
|
+
// Uses code-server's own bundled node (guaranteed present regardless of the
|
|
1423
|
+
// devcontainer base image) since jq/node aren't guaranteed there.
|
|
1424
|
+
const seedTerminalPortEnv = [
|
|
1425
|
+
"const fs = require('fs')",
|
|
1426
|
+
"const path = process.env.MP_SETTINGS_PATH",
|
|
1427
|
+
"let settings = {}",
|
|
1428
|
+
"try { settings = JSON.parse(fs.readFileSync(path, 'utf8')) } catch {}",
|
|
1429
|
+
"settings['terminal.integrated.env.linux'] = { ...settings['terminal.integrated.env.linux'], PORT: process.env.MP_ORIGINAL_PORT }",
|
|
1430
|
+
"fs.writeFileSync(path, JSON.stringify(settings, null, 2))",
|
|
1431
|
+
].join("\n")
|
|
1432
|
+
const seedTerminalPortEnvEncoded = b64(seedTerminalPortEnv)
|
|
1381
1433
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1434
|
+
push(
|
|
1435
|
+
"",
|
|
1436
|
+
'if [ -n "${PORT:-}" ]; then',
|
|
1437
|
+
" _mp_cs_bin=$(command -v code-server 2>/dev/null || true)",
|
|
1438
|
+
' if [ -n "$_mp_cs_bin" ]; then',
|
|
1439
|
+
' _mp_cs_bin=$(readlink -f "$_mp_cs_bin" 2>/dev/null || echo "$_mp_cs_bin")',
|
|
1440
|
+
' _mp_cs_node="$(dirname "$(dirname "$_mp_cs_bin")")/lib/node"',
|
|
1441
|
+
' if [ -x "$_mp_cs_node" ]; then',
|
|
1442
|
+
` mkdir -p ${homeDir}/.local/share/code-server/User`,
|
|
1443
|
+
` echo ${JSON.stringify(seedTerminalPortEnvEncoded)} | base64 -d > ${homeDir}/.mp_seed_terminal_port.js`,
|
|
1444
|
+
` MP_ORIGINAL_PORT="$PORT" MP_SETTINGS_PATH="${homeDir}/.local/share/code-server/User/settings.json" "$_mp_cs_node" ${homeDir}/.mp_seed_terminal_port.js`,
|
|
1445
|
+
` rm -f ${homeDir}/.mp_seed_terminal_port.js`,
|
|
1446
|
+
` chown -R ${username}:${username} ${homeDir}/.local`,
|
|
1447
|
+
" fi",
|
|
1448
|
+
" fi",
|
|
1449
|
+
"fi",
|
|
1450
|
+
)
|
|
1399
1451
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1452
|
+
// ── 5. Start code-server as vscode ────────────────────────────────────────
|
|
1453
|
+
// Write the restart-loop to a temp script so quoting stays clean.
|
|
1454
|
+
// Single-quoted heredoc: $VARS are NOT expanded here, they expand when the script runs
|
|
1455
|
+
// (env is inherited from the start script via `su vscode` without -l).
|
|
1456
|
+
// Write the restart-loop using base64 to avoid heredoc issues in DinD contexts
|
|
1457
|
+
// where dockerd startup can affect /tmp mount visibility for child processes.
|
|
1458
|
+
// Script lives in home dir (persistent volume) rather than /tmp.
|
|
1459
|
+
const codeServerLoop = [
|
|
1460
|
+
"#!/bin/bash",
|
|
1461
|
+
"export SHELL=$(command -v zsh 2>/dev/null || echo /bin/bash)",
|
|
1462
|
+
// Makes the editor's own Extensions view search the same registry the picker and the image
|
|
1463
|
+
// build used (org setting — see lib/extension-registry.ts). Nothing when the org is on Open VSX.
|
|
1464
|
+
...galleryExport(extensionsGallery),
|
|
1465
|
+
// The IDE is Spunto's, not Coder's (spunto-pack rebrands the files): the name in the title,
|
|
1466
|
+
// no "Deploy code-server for your team" on the welcome page, no "code-server vX is out"
|
|
1467
|
+
// notification. Each flag only if this code-server knows it — an unknown one stops it dead.
|
|
1468
|
+
'_mp_cs_flags=""',
|
|
1469
|
+
"for _f in --app-name --disable-getting-started-override --disable-update-check; do",
|
|
1470
|
+
' code-server --help 2>/dev/null | grep -q -- "$_f" || continue',
|
|
1471
|
+
' case "$_f" in --app-name) _mp_cs_flags="$_mp_cs_flags --app-name Spunto" ;; *) _mp_cs_flags="$_mp_cs_flags $_f" ;; esac',
|
|
1472
|
+
"done",
|
|
1473
|
+
"while true; do",
|
|
1474
|
+
' VSCODE_PROXY_URI="${PUBLIC_PROTOCOL}://${WORKER_SLUG}-{{port}}.${BASE_DOMAIN}" \\',
|
|
1475
|
+
// PORT is overridden here (not unset globally) so a project secret named
|
|
1476
|
+
// PORT can't make code-server bind elsewhere. This also leaks PORT=<idePort>
|
|
1477
|
+
// into code-server's own process tree (its integrated terminal included) —
|
|
1478
|
+
// § 4c above restores the original value for that terminal specifically.
|
|
1479
|
+
// apps/agent/src/terminal.ts (dashboard's separate terminal feature) is a
|
|
1480
|
+
// different process tree entirely and was never affected.
|
|
1481
|
+
` PORT=${idePort} code-server --bind-addr 0.0.0.0:${idePort} --auth none $_mp_cs_flags /workspace 2>&1`,
|
|
1482
|
+
' echo "[start] code-server exited ($?), restarting in 3s..."',
|
|
1483
|
+
" sleep 3",
|
|
1484
|
+
"done",
|
|
1485
|
+
].join("\n")
|
|
1486
|
+
const codeServerEncoded = b64(codeServerLoop)
|
|
1435
1487
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1488
|
+
push(
|
|
1489
|
+
...banner("START: CODE-SERVER"),
|
|
1490
|
+
`echo ${JSON.stringify(codeServerEncoded)} | base64 -d > ${homeDir}/.mp_codeserver.sh`,
|
|
1491
|
+
`chmod +x ${homeDir}/.mp_codeserver.sh`,
|
|
1492
|
+
`su ${username} -s /bin/bash -c "${homeDir}/.mp_codeserver.sh" &`,
|
|
1493
|
+
)
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// No IDE: nothing else would keep the script — and so the project's process, which the agent
|
|
1497
|
+
// reports as `running` only while it lives — waiting at the end. A sleeper does, in its place.
|
|
1498
|
+
if (!ide) push("", "while true; do sleep 3600; done &")
|
|
1442
1499
|
|
|
1443
1500
|
// ── 6. postStartCommand ───────────────────────────────────────────────────
|
|
1444
1501
|
// Wait for Docker if feature entrypoints were started — regardless of whether
|