@aiviatic/kindling 0.1.0 → 0.1.2
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/bootstrap/kindling.cmd +13 -13
- package/bootstrap/setup.ps1 +150 -98
- package/bootstrap/setup.sh +2 -2
- package/dist/{chunk-IS6LC3HK.js → chunk-DTSAPFB2.js} +3 -3
- package/dist/chunk-DTSAPFB2.js.map +1 -0
- package/dist/{chunk-MW7UAGER.js → chunk-DZ2RR3SP.js} +136 -78
- package/dist/chunk-DZ2RR3SP.js.map +1 -0
- package/dist/cli/main.js +4 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/engine/index.d.ts +15 -0
- package/dist/engine/index.js +6 -17
- package/dist/engine/index.js.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/ui/assets/index-QhMuWZCb.js +40 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/chunk-IS6LC3HK.js.map +0 -1
- package/dist/chunk-MW7UAGER.js.map +0 -1
- package/dist/ui/assets/index-CoPlNDA-.js +0 -40
package/bootstrap/kindling.cmd
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
REM Kindling bootstrap entry — Windows. The user downloads THIS ONE FILE and double-clicks it.
|
|
3
|
-
REM It FETCHES + runs the setup script from the host, so no sibling files are needed (the earlier
|
|
4
|
-
REM `-File %~dp0setup.ps1` required setup.ps1 + lib\common.ps1 next to the .cmd, which a lone
|
|
5
|
-
REM download doesn't have). Mirrors the macOS/Linux one-liner `curl -fsSL .../
|
|
6
|
-
REM -ExecutionPolicy Bypass : lets the unsigned setup script run FOR THIS PROCESS ONLY
|
|
7
|
-
REM (it does not change any system-wide policy — safe + reversible).
|
|
8
|
-
REM -NoProfile : skips the user's PowerShell profile so nothing interferes (required).
|
|
9
|
-
REM irm ... | iex : Invoke-RestMethod fetches setup.ps1 over HTTPS, Invoke-Expression
|
|
10
|
-
REM runs it (the PowerShell equivalent of curl|bash). setup.ps1 is
|
|
11
|
-
REM self-contained (helpers inlined), so no on-disk siblings are needed.
|
|
12
|
-
powershell -ExecutionPolicy Bypass -NoProfile -Command "irm https://kindling.aiviatic.com/setup.ps1 | iex"
|
|
13
|
-
pause
|
|
1
|
+
@echo off
|
|
2
|
+
REM Kindling bootstrap entry — Windows. The user downloads THIS ONE FILE and double-clicks it.
|
|
3
|
+
REM It FETCHES + runs the setup script from the host, so no sibling files are needed (the earlier
|
|
4
|
+
REM `-File %~dp0setup.ps1` required setup.ps1 + lib\common.ps1 next to the .cmd, which a lone
|
|
5
|
+
REM download doesn't have). Mirrors the macOS/Linux one-liner `curl -fsSL .../install | bash`.
|
|
6
|
+
REM -ExecutionPolicy Bypass : lets the unsigned setup script run FOR THIS PROCESS ONLY
|
|
7
|
+
REM (it does not change any system-wide policy — safe + reversible).
|
|
8
|
+
REM -NoProfile : skips the user's PowerShell profile so nothing interferes (required).
|
|
9
|
+
REM irm ... | iex : Invoke-RestMethod fetches setup.ps1 over HTTPS, Invoke-Expression
|
|
10
|
+
REM runs it (the PowerShell equivalent of curl|bash). setup.ps1 is
|
|
11
|
+
REM self-contained (helpers inlined), so no on-disk siblings are needed.
|
|
12
|
+
powershell -ExecutionPolicy Bypass -NoProfile -Command "irm https://kindling.aiviatic.com/setup.ps1 | iex"
|
|
13
|
+
pause
|
package/bootstrap/setup.ps1
CHANGED
|
@@ -1,98 +1,150 @@
|
|
|
1
|
-
# Kindling bootstrap — Windows. FETCHED + run by kindling.cmd (self-fetching one-file entry):
|
|
2
|
-
# powershell -ExecutionPolicy Bypass -NoProfile -Command "irm https://kindling.aiviatic.com/setup.ps1 | iex"
|
|
3
|
-
# Ensures the pinned Node + Git (portable), then launches Kindling.
|
|
4
|
-
#
|
|
5
|
-
# SELF-CONTAINED for the `irm | iex` delivery path: there is NO file on disk (no $PSScriptRoot), so
|
|
6
|
-
# the helpers are INLINED below rather than dot-sourced — mirroring setup.sh's inline helpers for
|
|
7
|
-
# `curl | bash`. Pinned versions below MUST match engine/pins.ts (a unit test asserts this).
|
|
8
|
-
# NOTE: portable Node/Git provisioning is the Windows SPIKE — validated on real Windows at the
|
|
9
|
-
# dress rehearsal (mirrors engine/provision/node-windows.ts).
|
|
10
|
-
$ErrorActionPreference = 'Stop'
|
|
11
|
-
|
|
12
|
-
$KindlingNodeVersion = '24.16.0' # == pins.node
|
|
13
|
-
$KindlingVersion = '0.1.
|
|
14
|
-
$NodeFloorMajor = 20
|
|
15
|
-
|
|
16
|
-
# --- Inline helpers (were bootstrap/lib/common.ps1; inlined for the file-less delivery) ----------
|
|
17
|
-
function Say([string]$msg) { Write-Host "`n$msg" -ForegroundColor White }
|
|
18
|
-
function Test-Cmd([string]$name) {
|
|
19
|
-
return [bool](Get-Command $name -ErrorAction SilentlyContinue)
|
|
20
|
-
}
|
|
21
|
-
# True if a Node on PATH meets the major-version floor.
|
|
22
|
-
function Test-NodeOk([int]$floorMajor) {
|
|
23
|
-
if (-not (Test-Cmd 'node')) { return $false }
|
|
24
|
-
try {
|
|
25
|
-
$v = (& node -v) -replace '^v', ''
|
|
26
|
-
$major = [int]($v -split '\.')[0]
|
|
27
|
-
return $major -ge $floorMajor
|
|
28
|
-
} catch { return $false }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
Say "Getting your computer ready. This usually takes about 5 minutes."
|
|
32
|
-
|
|
33
|
-
# Plain-language guidance for the Windows security prompts (FR-7) - expected / safe / reversible.
|
|
34
|
-
Say "Heads up: Windows may show a couple of security prompts. They're expected and safe:"
|
|
35
|
-
Say " - SmartScreen ('Windows protected your PC'): click 'More info', then 'Run anyway'. Nothing is changed on your PC."
|
|
36
|
-
Say " - This window already runs with -ExecutionPolicy Bypass for THIS process only; it does not change any system setting and is fully reversible."
|
|
37
|
-
|
|
38
|
-
# --- Node (pinned, portable) --------------------------------------------------
|
|
39
|
-
# $NodeExe stays $null when a system Node is reused (launch via PATH); the portable-install path
|
|
40
|
-
# sets it to the absolute node.exe so the launch never depends on PATH (clean-runtime rule, AR6).
|
|
41
|
-
$NodeExe = $null
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
$
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (Test-
|
|
78
|
-
Say "
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1
|
+
# Kindling bootstrap — Windows. FETCHED + run by kindling.cmd (self-fetching one-file entry):
|
|
2
|
+
# powershell -ExecutionPolicy Bypass -NoProfile -Command "irm https://kindling.aiviatic.com/setup.ps1 | iex"
|
|
3
|
+
# Ensures the pinned Node + Git (portable), then launches Kindling.
|
|
4
|
+
#
|
|
5
|
+
# SELF-CONTAINED for the `irm | iex` delivery path: there is NO file on disk (no $PSScriptRoot), so
|
|
6
|
+
# the helpers are INLINED below rather than dot-sourced — mirroring setup.sh's inline helpers for
|
|
7
|
+
# `curl | bash`. Pinned versions below MUST match engine/pins.ts (a unit test asserts this).
|
|
8
|
+
# NOTE: portable Node/Git provisioning is the Windows SPIKE — validated on real Windows at the
|
|
9
|
+
# dress rehearsal (mirrors engine/provision/node-windows.ts).
|
|
10
|
+
$ErrorActionPreference = 'Stop'
|
|
11
|
+
|
|
12
|
+
$KindlingNodeVersion = '24.16.0' # == pins.node
|
|
13
|
+
$KindlingVersion = '0.1.2' # == pins.kindling
|
|
14
|
+
$NodeFloorMajor = 20
|
|
15
|
+
|
|
16
|
+
# --- Inline helpers (were bootstrap/lib/common.ps1; inlined for the file-less delivery) ----------
|
|
17
|
+
function Say([string]$msg) { Write-Host "`n$msg" -ForegroundColor White }
|
|
18
|
+
function Test-Cmd([string]$name) {
|
|
19
|
+
return [bool](Get-Command $name -ErrorAction SilentlyContinue)
|
|
20
|
+
}
|
|
21
|
+
# True if a Node on PATH meets the major-version floor.
|
|
22
|
+
function Test-NodeOk([int]$floorMajor) {
|
|
23
|
+
if (-not (Test-Cmd 'node')) { return $false }
|
|
24
|
+
try {
|
|
25
|
+
$v = (& node -v) -replace '^v', ''
|
|
26
|
+
$major = [int]($v -split '\.')[0]
|
|
27
|
+
return $major -ge $floorMajor
|
|
28
|
+
} catch { return $false }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Say "Getting your computer ready. This usually takes about 5 minutes."
|
|
32
|
+
|
|
33
|
+
# Plain-language guidance for the Windows security prompts (FR-7) - expected / safe / reversible.
|
|
34
|
+
Say "Heads up: Windows may show a couple of security prompts. They're expected and safe:"
|
|
35
|
+
Say " - SmartScreen ('Windows protected your PC'): click 'More info', then 'Run anyway'. Nothing is changed on your PC."
|
|
36
|
+
Say " - This window already runs with -ExecutionPolicy Bypass for THIS process only; it does not change any system setting and is fully reversible."
|
|
37
|
+
|
|
38
|
+
# --- Node (pinned, portable) --------------------------------------------------
|
|
39
|
+
# $NodeExe stays $null when a system Node is reused (launch via PATH); the portable-install path
|
|
40
|
+
# sets it to the absolute node.exe so the launch never depends on PATH (clean-runtime rule, AR6).
|
|
41
|
+
$NodeExe = $null
|
|
42
|
+
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
|
|
43
|
+
$distName = "node-v$KindlingNodeVersion-win-$arch"
|
|
44
|
+
$nodeRoot = Join-Path $env:LOCALAPPDATA 'kindling\node'
|
|
45
|
+
$portableNode = Join-Path $nodeRoot "$distName\node.exe"
|
|
46
|
+
if (Test-NodeOk $NodeFloorMajor) {
|
|
47
|
+
Say "Node is already installed - reusing it."
|
|
48
|
+
} elseif (Test-Path $portableNode) {
|
|
49
|
+
# Portable Node from a previous run is already extracted here - reuse it, don't re-download the
|
|
50
|
+
# 30 MB. (The portable dir is never persisted to PATH, so Test-NodeOk alone can't see it.)
|
|
51
|
+
Say "Node is already installed - reusing it."
|
|
52
|
+
$NodeExe = $portableNode
|
|
53
|
+
} else {
|
|
54
|
+
Say "Setting up Node - the engine your project runs on. This downloads about 30 MB, one time."
|
|
55
|
+
# Portable Node: download the pinned Windows zip from nodejs.org, VERIFY its SHA-256 against Node's
|
|
56
|
+
# published SHASUMS256.txt before touching it, extract, and point the launch at the absolute
|
|
57
|
+
# node.exe (clean-runtime rule AR6 — never rely on a mutated PATH). Mirrors node-windows.ts.
|
|
58
|
+
$ProgressPreference = 'SilentlyContinue' # a visible progress bar makes Invoke-WebRequest ~10x slower
|
|
59
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
60
|
+
$zipPath = Join-Path $nodeRoot "$distName.zip"
|
|
61
|
+
$baseUrl = "https://nodejs.org/dist/v$KindlingNodeVersion"
|
|
62
|
+
New-Item -ItemType Directory -Force -Path $nodeRoot | Out-Null
|
|
63
|
+
try {
|
|
64
|
+
Invoke-WebRequest -Uri "$baseUrl/$distName.zip" -OutFile $zipPath -UseBasicParsing
|
|
65
|
+
$shaLine = ((Invoke-WebRequest -Uri "$baseUrl/SHASUMS256.txt" -UseBasicParsing).Content -split "`n" |
|
|
66
|
+
Where-Object { $_ -match [regex]::Escape("$distName.zip") } | Select-Object -First 1)
|
|
67
|
+
$expected = ($shaLine -split '\s+')[0].ToLower()
|
|
68
|
+
$actual = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower()
|
|
69
|
+
if (-not $expected -or $actual -ne $expected) {
|
|
70
|
+
throw "downloaded Node failed its integrity check (expected '$expected', got '$actual')"
|
|
71
|
+
}
|
|
72
|
+
Expand-Archive -Path $zipPath -DestinationPath $nodeRoot -Force
|
|
73
|
+
} catch {
|
|
74
|
+
throw "Couldn't set up Node ($($_.Exception.Message)). Check your internet connection, then run this again - it's safe to re-run."
|
|
75
|
+
}
|
|
76
|
+
$NodeExe = Join-Path $nodeRoot "$distName\node.exe"
|
|
77
|
+
if (-not (Test-Path $NodeExe)) { throw "Node was downloaded but node.exe wasn't found at $NodeExe." }
|
|
78
|
+
Say "Node is ready."
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# --- Git (pinned, portable MinGit) -------------------------------------------
|
|
82
|
+
# $GitCmdDir stays $null when a system Git is reused (already on PATH); the portable path sets it to
|
|
83
|
+
# MinGit's cmd\ dir, prepended to PATH at launch so the engine can `git init` the new project.
|
|
84
|
+
$GitCmdDir = $null
|
|
85
|
+
$gitRoot = Join-Path $env:LOCALAPPDATA 'kindling\git'
|
|
86
|
+
$portableGitCmd = Join-Path $gitRoot 'cmd'
|
|
87
|
+
if (Test-Cmd 'git') {
|
|
88
|
+
Say "Git is already installed - reusing it."
|
|
89
|
+
} elseif (Test-Path (Join-Path $portableGitCmd 'git.exe')) {
|
|
90
|
+
# Portable MinGit from a previous run is already extracted here - reuse it, don't re-download.
|
|
91
|
+
Say "Git is already installed - reusing it."
|
|
92
|
+
$GitCmdDir = $portableGitCmd
|
|
93
|
+
} else {
|
|
94
|
+
Say "Setting up Git - it keeps the history of your project. This downloads about 35 MB, one time."
|
|
95
|
+
# Portable MinGit (the ZIP build made for bundling): download the pinned release, VERIFY its SHA-256
|
|
96
|
+
# before touching it, extract, and expose cmd\ on PATH. Mirrors the Node block. Pinned version +
|
|
97
|
+
# hash below MUST be bumped together (git-for-windows publishes the digest on each release asset).
|
|
98
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
99
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
100
|
+
$gitVersion = '2.55.0.2'
|
|
101
|
+
$gitZip = Join-Path $env:LOCALAPPDATA 'kindling\mingit.zip'
|
|
102
|
+
$gitUrl = "https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.2/MinGit-$gitVersion-64-bit.zip"
|
|
103
|
+
$gitSha = 'e3ea2944cea4b3fabcd69c7c1669ef69b1b66c05ac7806d81224d0abad2dec31'
|
|
104
|
+
New-Item -ItemType Directory -Force -Path $gitRoot | Out-Null
|
|
105
|
+
try {
|
|
106
|
+
Invoke-WebRequest -Uri $gitUrl -OutFile $gitZip -UseBasicParsing
|
|
107
|
+
$actual = (Get-FileHash -Path $gitZip -Algorithm SHA256).Hash.ToLower()
|
|
108
|
+
if ($actual -ne $gitSha) {
|
|
109
|
+
throw "downloaded Git failed its integrity check (expected '$gitSha', got '$actual')"
|
|
110
|
+
}
|
|
111
|
+
Expand-Archive -Path $gitZip -DestinationPath $gitRoot -Force
|
|
112
|
+
} catch {
|
|
113
|
+
throw "Couldn't set up Git ($($_.Exception.Message)). Check your internet connection, then run this again - it's safe to re-run."
|
|
114
|
+
}
|
|
115
|
+
$GitCmdDir = $portableGitCmd
|
|
116
|
+
if (-not (Test-Path (Join-Path $GitCmdDir 'git.exe'))) { throw "Git was downloaded but git.exe wasn't found at $GitCmdDir." }
|
|
117
|
+
Say "Git is ready."
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# --- Launch Kindling (clean-runtime: absolute node when portable, else npx on PATH) -----------
|
|
121
|
+
Say "Starting Kindling..."
|
|
122
|
+
# Launch from a NEUTRAL directory, never the folder the download was run from. npx runs the
|
|
123
|
+
# package's `kindling` bin BY NAME, and Windows searches the CURRENT directory first — so if the
|
|
124
|
+
# downloaded `kindling.cmd` sits in the cwd, `kindling` resolves to IT instead of npx's shim and
|
|
125
|
+
# re-runs the whole bootstrap forever (dress-rehearsal Windows loop, 2026-07-03). LOCALAPPDATA\kindling
|
|
126
|
+
# holds only the portable Node, never a `kindling.cmd`.
|
|
127
|
+
$launchDir = Join-Path $env:LOCALAPPDATA 'kindling'
|
|
128
|
+
New-Item -ItemType Directory -Force -Path $launchDir | Out-Null
|
|
129
|
+
Set-Location -LiteralPath $launchDir
|
|
130
|
+
# Portable Git on PATH (the engine spawns `git` by name to scaffold the project's history). Applies
|
|
131
|
+
# to both launch branches; the provisioned Node dir is added inside the portable-Node branch below.
|
|
132
|
+
if ($GitCmdDir) { $env:Path = "$GitCmdDir;$env:Path" }
|
|
133
|
+
if ($null -eq $NodeExe) {
|
|
134
|
+
& npx -y "@aiviatic/kindling@$KindlingVersion"
|
|
135
|
+
} else {
|
|
136
|
+
# Put the provisioned Node dir on PATH so bare `node`/`npm`/`npx` resolve by NAME. Launching via the
|
|
137
|
+
# absolute node binary isn't enough: npx and the launched package (plus the child processes the
|
|
138
|
+
# engine spawns for provisioning) call `node` by name and inherit this PATH — without it they fail
|
|
139
|
+
# with "'node' is not recognized" and Kindling never starts. (Dress-rehearsal finding, 2026-07-03.)
|
|
140
|
+
$env:Path = "$(Split-Path $NodeExe);$env:Path"
|
|
141
|
+
# Invoke npx's CLI through the exact provisioned node binary — no reliance on PATH for the launch itself.
|
|
142
|
+
$NpxCli = Join-Path (Split-Path $NodeExe) 'node_modules\npm\bin\npx-cli.js'
|
|
143
|
+
& $NodeExe $NpxCli -y "@aiviatic/kindling@$KindlingVersion"
|
|
144
|
+
}
|
|
145
|
+
# A native exe (npx) does NOT raise a terminating error on failure even with
|
|
146
|
+
# ErrorActionPreference='Stop' — it only sets $LASTEXITCODE. So check that explicitly.
|
|
147
|
+
if ($LASTEXITCODE -ne 0) {
|
|
148
|
+
Say "Kindling couldn't start. Check that you're connected to the internet, then run it again. It's safe to re-run."
|
|
149
|
+
exit 1
|
|
150
|
+
}
|
package/bootstrap/setup.sh
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Kindling bootstrap — macOS/Linux. Served by the Landing Page as:
|
|
3
|
-
# curl -fsSL https://kindling.aiviatic.com/
|
|
3
|
+
# curl -fsSL https://kindling.aiviatic.com/install | bash
|
|
4
4
|
# Ensures the pinned Node + Git, then launches Kindling in the SAME shell (no new terminal).
|
|
5
5
|
#
|
|
6
6
|
# Pinned versions below MUST match engine/pins.ts + node-unix.ts (a unit test asserts this).
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
9
9
|
KINDLING_NODE_VERSION="24.16.0" # == pins.node
|
|
10
|
-
KINDLING_VERSION="0.1.
|
|
10
|
+
KINDLING_VERSION="0.1.2" # == pins.kindling
|
|
11
11
|
NVM_VERSION="v0.40.3" # == NVM_VERSION (engine/provision/node-unix.ts)
|
|
12
12
|
NODE_FLOOR_MAJOR=20
|
|
13
13
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { createServer as createHttpServer } from "http";
|
|
9
9
|
import { once } from "events";
|
|
10
10
|
import { readFile } from "fs/promises";
|
|
11
|
-
import { join, normalize, extname } from "path";
|
|
11
|
+
import { join, normalize, extname, sep } from "path";
|
|
12
12
|
var MIME = {
|
|
13
13
|
".html": "text/html; charset=utf-8",
|
|
14
14
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -30,7 +30,7 @@ async function readBody(req) {
|
|
|
30
30
|
async function serveStatic(uiDir, pathname, res) {
|
|
31
31
|
const rel = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, "");
|
|
32
32
|
const filePath = normalize(join(uiDir, rel));
|
|
33
|
-
if (filePath !== normalize(uiDir) && !filePath.startsWith(normalize(uiDir) +
|
|
33
|
+
if (filePath !== normalize(uiDir) && !filePath.startsWith(normalize(uiDir) + sep)) {
|
|
34
34
|
res.writeHead(403).end("forbidden");
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
@@ -207,4 +207,4 @@ export {
|
|
|
207
207
|
startServer,
|
|
208
208
|
openBrowser
|
|
209
209
|
};
|
|
210
|
-
//# sourceMappingURL=chunk-
|
|
210
|
+
//# sourceMappingURL=chunk-DTSAPFB2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../server/server.ts","../server/open-browser.ts"],"sourcesContent":["import { createServer as createHttpServer, type IncomingMessage, type ServerResponse, type Server } from 'node:http';\nimport { once } from 'node:events';\nimport { readFile } from 'node:fs/promises';\nimport { join, normalize, extname, sep } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport type { EngineEmitter } from '../engine/emitter';\nimport { StepId, type Config, type KindlingEvent, type InspectResult } from '../engine/contract';\nimport { expandTilde } from '../engine/expand-tilde';\n\n// Minimal command surface the server drives (the Engine satisfies this).\nexport interface ServerCommands {\n start(config: Config): unknown | Promise<unknown>;\n cancel(): void;\n retry(step: StepId): unknown | Promise<unknown>;\n /**\n * Read-only filesystem probe for the Configure-time \"existing project?\" detection (Story 7.2 /\n * `POST /inspect`). OPTIONAL so existing `ServerCommands` fakes (e.g. server.test.ts) don't break;\n * when absent the handler returns the neutral `{ isKindlingProject: false, installedBmadVersion: null }`.\n */\n inspect?(projectDir: string): Promise<InspectResult>;\n}\n\nexport interface StartServerOptions {\n emitter: EngineEmitter;\n commands: ServerCommands;\n /** Built UI directory (dist/ui) to serve statically; omit to skip static serving. */\n uiDir?: string;\n /** Called when the Welcome screen render-acks (POST /ack). The host uses this to write the\n * static welcome.html and exit the ephemeral server — only fired on success (3.7 lifecycle). */\n onWelcomeAck?: () => void;\n}\n\nexport interface RunningServer {\n server: Server;\n url: string;\n port: number;\n close(): Promise<void>;\n}\n\nconst MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n};\n\nconst MAX_BODY_BYTES = 64 * 1024;\nconst KNOWN_STEPS = new Set<string>(Object.values(StepId));\n\n// Reads the request body with a hard size cap (throws past the cap → caller responds 413).\nasync function readBody(req: IncomingMessage): Promise<string> {\n let body = '';\n for await (const chunk of req) {\n body += String(chunk);\n if (body.length > MAX_BODY_BYTES) throw new Error('body too large');\n }\n return body;\n}\n\nasync function serveStatic(uiDir: string, pathname: string, res: ServerResponse): Promise<void> {\n const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\\/+/, '');\n const filePath = normalize(join(uiDir, rel));\n // Path-traversal guard: the resolved path must stay within uiDir. (We serve only our own\n // built dist/ui — not attacker-writable — so symlink resolution is out of scope.)\n // Use the platform path separator, NOT a literal '/': on Windows `normalize` yields backslash\n // paths, so `+ '/'` never matched and EVERY file was forbidden (dress-rehearsal Windows bug).\n if (filePath !== normalize(uiDir) && !filePath.startsWith(normalize(uiDir) + sep)) {\n res.writeHead(403).end('forbidden');\n return;\n }\n try {\n const data = await readFile(filePath);\n res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream' });\n res.end(data);\n } catch {\n res.writeHead(404).end('not found');\n }\n}\n\n// Stands up the localhost server: SSE event stream, command POSTs, and (optional) static UI.\n// Binds 127.0.0.1 on an ephemeral port (NFR6); resolves once listening.\nexport async function startServer(opts: StartServerOptions): Promise<RunningServer> {\n const { emitter, commands, uiDir, onWelcomeAck } = opts;\n const sseClients = new Set<ServerResponse>();\n\n const handler = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const { pathname } = new URL(req.url ?? '/', 'http://127.0.0.1');\n\n // DNS-rebinding guard: only serve requests whose Host header is loopback. A page that rebinds\n // its own domain's DNS to 127.0.0.1 still sends that domain as Host, not 127.0.0.1 / localhost.\n const hostname = (req.headers.host ?? '').split(':')[0].toLowerCase();\n if (hostname !== '127.0.0.1' && hostname !== 'localhost') {\n res.writeHead(403).end('forbidden');\n return;\n }\n\n if (req.method === 'GET' && pathname === '/events') {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n });\n // Data-only SSE framing (one stream per connection); the UI client uses `onmessage`.\n const send = (event: KindlingEvent): void => {\n if (!res.writableEnded) res.write(`data: ${JSON.stringify(event)}\\n\\n`);\n };\n for (const event of emitter.events()) send(event); // replay backlog so late clients catch up\n const off = emitter.on(send);\n sseClients.add(res);\n req.on('close', () => {\n off();\n sseClients.delete(res);\n });\n return;\n }\n\n if (req.method === 'POST') {\n // CSRF guard: a cross-origin browser page can reach 127.0.0.1, but cannot set a custom\n // header without a CORS preflight (which this server never approves). The UI sends it.\n if (req.headers['x-kindling'] !== '1') {\n res.writeHead(403).end('forbidden');\n return;\n }\n if (pathname === '/start') {\n let body: string;\n try {\n body = await readBody(req);\n } catch {\n res.writeHead(413).end('body too large');\n return;\n }\n let config: Config | undefined;\n try {\n const p = JSON.parse(body || 'null') as Partial<Config> | null;\n // Shape-guard every field the engine dereferences (flags.ts reads projectDir; the\n // self-check/validation reads pins) so a malformed body is rejected as 400 rather\n // than reaching the engine and crashing after we've already sent 202.\n if (\n p &&\n typeof p === 'object' &&\n typeof p.projectDir === 'string' &&\n p.projectDir.length > 0 &&\n typeof p.projectName === 'string' &&\n p.projectName.length > 0 &&\n Array.isArray(p.ides) &&\n p.ides.length > 0 &&\n Array.isArray(p.modules) &&\n p.pins !== null &&\n typeof p.pins === 'object'\n ) {\n config = p as Config;\n }\n } catch {\n config = undefined;\n }\n if (!config) {\n res.writeHead(400).end('invalid config');\n return;\n }\n res.writeHead(202).end(); // progress flows over SSE; don't block on the run\n void Promise.resolve(commands.start(config)).catch(() => {});\n return;\n }\n if (pathname === '/inspect') {\n // Read-only, fixed-suffix probe (Story 7.2). Same CSRF guard + readBody cap as /start;\n // shape-guard the body → 400; else 200 + the InspectResult JSON. No engine, no writes,\n // no user-controlled traversal (the inspector only stats <dir>/_bmad + reads the fixed\n // <dir>/_bmad/_config/manifest.yaml). Both underlying reads never throw.\n let body: string;\n try {\n body = await readBody(req);\n } catch {\n res.writeHead(413).end('body too large');\n return;\n }\n let projectDir: string | undefined;\n try {\n const p = JSON.parse(body || 'null') as { projectDir?: unknown } | null;\n if (p && typeof p === 'object' && typeof p.projectDir === 'string' && p.projectDir.length > 0) {\n projectDir = p.projectDir;\n }\n } catch {\n projectDir = undefined;\n }\n if (!projectDir) {\n res.writeHead(400).end('invalid dir');\n return;\n }\n const result: InspectResult = commands.inspect\n ? await commands.inspect(expandTilde(projectDir))\n : { isKindlingProject: false, installedBmadVersion: null };\n res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }).end(JSON.stringify(result));\n return;\n }\n if (pathname === '/cancel') {\n commands.cancel();\n res.writeHead(202).end();\n return;\n }\n if (pathname === '/ack') {\n // The Welcome screen rendered — let the host exit the ephemeral server. Respond first so\n // the ack completes even if onWelcomeAck closes the server synchronously.\n res.writeHead(202).end();\n onWelcomeAck?.();\n return;\n }\n if (pathname === '/retry') {\n let body: string;\n try {\n body = await readBody(req);\n } catch {\n res.writeHead(413).end('body too large');\n return;\n }\n let step: string | undefined;\n try {\n step = (JSON.parse(body || '{}') as { step?: string }).step;\n } catch {\n step = undefined;\n }\n if (!step || !KNOWN_STEPS.has(step)) {\n res.writeHead(400).end('invalid step');\n return;\n }\n res.writeHead(202).end();\n void Promise.resolve(commands.retry(step as StepId)).catch(() => {});\n return;\n }\n }\n\n if (req.method === 'GET' && uiDir) {\n await serveStatic(uiDir, pathname, res);\n return;\n }\n\n res.writeHead(404).end('not found');\n };\n\n const server = createHttpServer((req, res) => {\n handler(req, res).catch(() => {\n if (!res.headersSent) res.writeHead(500).end('server error');\n else res.destroy();\n });\n });\n server.listen(0, '127.0.0.1');\n await once(server, 'listening');\n const { port } = server.address() as AddressInfo;\n\n return {\n server,\n port,\n url: `http://127.0.0.1:${port}/`,\n close: () =>\n new Promise<void>((resolve, reject) => {\n for (const res of sseClients) res.end(); // SSE keeps sockets alive — close them first\n sseClients.clear();\n server.close((err) => (err ? reject(err) : resolve()));\n server.closeAllConnections(); // terminate any remaining keep-alive sockets (Node 18.2+)\n }),\n };\n}\n","import { exec as defaultExec, type ExecResult } from '../engine/exec';\n\nexport interface OpenBrowserOptions {\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n platform?: NodeJS.Platform;\n}\n\n// Best-effort: opens `url` in the default browser. NEVER throws; returns whether the open\n// command launched. The printed/returned URL is the real contract (FR5 — auto-open is decoration).\nexport async function openBrowser(url: string, opts: OpenBrowserOptions = {}): Promise<boolean> {\n const exec = opts.exec ?? defaultExec;\n const platform = opts.platform ?? process.platform;\n\n // `cmd.exe` is a real executable (spawns fine with shell:false); `start` is its builtin, and\n // its first quoted arg is the window title (empty here).\n const [cmd, args]: [string, string[]] =\n platform === 'darwin'\n ? ['open', [url]]\n : platform === 'win32'\n ? ['cmd', ['/c', 'start', '', url]]\n : ['xdg-open', [url]];\n\n try {\n const r = await exec(cmd, args);\n return r.code === 0;\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["exec"]}
|