@aiviatic/kindling 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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/bin/kindling.js +14 -0
- package/bootstrap/kindling.cmd +13 -0
- package/bootstrap/setup.ps1 +98 -0
- package/bootstrap/setup.sh +59 -0
- package/dist/chunk-IS6LC3HK.js +210 -0
- package/dist/chunk-IS6LC3HK.js.map +1 -0
- package/dist/chunk-MW7UAGER.js +890 -0
- package/dist/chunk-MW7UAGER.js.map +1 -0
- package/dist/chunk-OU3WSB6B.js +77 -0
- package/dist/chunk-OU3WSB6B.js.map +1 -0
- package/dist/cli/main.d.ts +21 -0
- package/dist/cli/main.js +258 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/emitter-oidLJDmn.d.ts +135 -0
- package/dist/engine/index.d.ts +546 -0
- package/dist/engine/index.js +234 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/exec-JnCZZPZU.d.ts +8 -0
- package/dist/server/index.d.ts +39 -0
- package/dist/server/index.js +10 -0
- package/dist/server/index.js.map +1 -0
- package/dist/ui/assets/index-Bw_xLj6a.css +1 -0
- package/dist/ui/assets/index-CoPlNDA-.js +40 -0
- package/dist/ui/index.html +13 -0
- package/dist/ui/platform-codes.yaml +54 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aiviatic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Kindling
|
|
2
|
+
|
|
3
|
+
**Blank computer to building in about five minutes — no terminal knowledge required.**
|
|
4
|
+
|
|
5
|
+
Kindling is a cross-platform installer that gets a non-technical person from a
|
|
6
|
+
fresh computer (Mac, Windows, or Linux) to a working, [BMad](https://github.com/bmad-code-org/BMAD-METHOD)-scaffolded
|
|
7
|
+
project set up for AI-driven app development. You run one step, make a few choices
|
|
8
|
+
in your browser, and Kindling installs and wires everything up for you.
|
|
9
|
+
|
|
10
|
+
It's open source on purpose: the very first thing Kindling does is run a script on
|
|
11
|
+
your machine, so you should be able to read exactly what that script does. See
|
|
12
|
+
[SECURITY.md](SECURITY.md) for the security model.
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
Three layers, so the scary parts happen where they can be explained:
|
|
17
|
+
|
|
18
|
+
1. **A per-OS bootstrap script** (`bootstrap/`) handles the steps that need Node
|
|
19
|
+
present: it provisions a pinned Node.js (via `nvm` on macOS/Linux; on Windows it
|
|
20
|
+
downloads a pinned, SHA-256-verified portable Node, or reuses an existing Node 20+),
|
|
21
|
+
then launches Kindling in the same shell.
|
|
22
|
+
- macOS/Linux: `curl -fsSL https://kindling.aiviatic.com/go | bash`
|
|
23
|
+
- Windows: download `kindling.cmd` and double-click it (it fetches and runs
|
|
24
|
+
`setup.ps1` over HTTPS).
|
|
25
|
+
2. **A temporary localhost server** (`server/`) stands up on `127.0.0.1`, serves a
|
|
26
|
+
friendly browser UI, provisions Git, scaffolds the project, runs
|
|
27
|
+
`npx bmad-method install`, and installs any agent CLIs you opt into. It exits
|
|
28
|
+
when the install completes.
|
|
29
|
+
3. **A browser UI** (`ui/`) walks you through the few choices (project name,
|
|
30
|
+
tools) and shows honest progress — including the ~5-minute macOS developer-tools
|
|
31
|
+
dialog, so it never looks frozen.
|
|
32
|
+
|
|
33
|
+
The installer is published to npm as **`@aiviatic/kindling`**; the bootstrap's
|
|
34
|
+
final step is `npx @aiviatic/kindling@<pinned-version>`.
|
|
35
|
+
|
|
36
|
+
> This repo is the **installer** (what runs on your machine). The marketing/landing
|
|
37
|
+
> site and its serverless bits are maintained separately.
|
|
38
|
+
|
|
39
|
+
## Try it
|
|
40
|
+
|
|
41
|
+
If you already have Node 20+, you can run the installer directly:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @aiviatic/kindling
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Otherwise, get the one-step command for your OS at
|
|
48
|
+
**<https://kindling.aiviatic.com/install>**.
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/Aiviatic/kindling.git
|
|
54
|
+
cd kindling
|
|
55
|
+
nvm use && npm install
|
|
56
|
+
npm test # unit suite (Vitest)
|
|
57
|
+
npm run build # tsup (Node payload) + Vite (browser UI)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- **TypeScript + ESM** throughout; dev/build on the Node in `.nvmrc`, runtime floor **Node 20+**.
|
|
61
|
+
- **React + Vite** for the browser UI; **tsup** bundles the Node side (`engine/`, `server/`, `cli/`).
|
|
62
|
+
- **Vitest** for tests (co-located `*.test.ts(x)`); CI runs typecheck + lint + test + build on
|
|
63
|
+
macOS/Windows/Ubuntu × Node 20/24.
|
|
64
|
+
|
|
65
|
+
Layout: `engine/` (headless install logic) · `server/` (localhost server + Welcome
|
|
66
|
+
page) · `ui/` (React install UI) · `cli/` + `bin/` (the `npx` entry) · `bootstrap/`
|
|
67
|
+
(per-OS entry scripts) · `scripts/` (dev helpers).
|
|
68
|
+
|
|
69
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) to get started — Windows fixes especially welcome.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
[MIT](LICENSE) © Aiviatic
|
package/bin/kindling.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npx entry — dispatches to the built CLI (server | --json | --verbose).
|
|
3
|
+
// Resolves the built output relative to this file (robust under global/symlinked installs).
|
|
4
|
+
const target = new URL('../dist/cli/main.js', import.meta.url);
|
|
5
|
+
import(target.href)
|
|
6
|
+
.then((cli) => cli.run(process.argv.slice(2)))
|
|
7
|
+
.catch((err) => {
|
|
8
|
+
if (err && err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
9
|
+
console.error('Kindling must be built first: run `npm run build`.');
|
|
10
|
+
} else {
|
|
11
|
+
console.error('Kindling failed to start:', err);
|
|
12
|
+
}
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
@@ -0,0 +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 .../go | 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
|
|
@@ -0,0 +1,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.0' # == 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
|
+
if (Test-NodeOk $NodeFloorMajor) {
|
|
43
|
+
Say "Node is already installed - reusing it."
|
|
44
|
+
} else {
|
|
45
|
+
Say "Setting up Node - the engine your project runs on. This downloads about 30 MB, one time."
|
|
46
|
+
# Portable Node: download the pinned Windows zip from nodejs.org, VERIFY its SHA-256 against Node's
|
|
47
|
+
# published SHASUMS256.txt before touching it, extract, and point the launch at the absolute
|
|
48
|
+
# node.exe (clean-runtime rule AR6 — never rely on a mutated PATH). Mirrors node-windows.ts.
|
|
49
|
+
# NOTE: newly implemented; validate on real Windows (proxy/TLS-interception, extract, non-admin exec).
|
|
50
|
+
$ProgressPreference = 'SilentlyContinue' # a visible progress bar makes Invoke-WebRequest ~10x slower
|
|
51
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
52
|
+
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
|
|
53
|
+
$distName = "node-v$KindlingNodeVersion-win-$arch"
|
|
54
|
+
$nodeRoot = Join-Path $env:LOCALAPPDATA 'kindling\node'
|
|
55
|
+
$zipPath = Join-Path $nodeRoot "$distName.zip"
|
|
56
|
+
$baseUrl = "https://nodejs.org/dist/v$KindlingNodeVersion"
|
|
57
|
+
New-Item -ItemType Directory -Force -Path $nodeRoot | Out-Null
|
|
58
|
+
try {
|
|
59
|
+
Invoke-WebRequest -Uri "$baseUrl/$distName.zip" -OutFile $zipPath -UseBasicParsing
|
|
60
|
+
$shaLine = ((Invoke-WebRequest -Uri "$baseUrl/SHASUMS256.txt" -UseBasicParsing).Content -split "`n" |
|
|
61
|
+
Where-Object { $_ -match [regex]::Escape("$distName.zip") } | Select-Object -First 1)
|
|
62
|
+
$expected = ($shaLine -split '\s+')[0].ToLower()
|
|
63
|
+
$actual = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower()
|
|
64
|
+
if (-not $expected -or $actual -ne $expected) {
|
|
65
|
+
throw "downloaded Node failed its integrity check (expected '$expected', got '$actual')"
|
|
66
|
+
}
|
|
67
|
+
Expand-Archive -Path $zipPath -DestinationPath $nodeRoot -Force
|
|
68
|
+
} catch {
|
|
69
|
+
throw "Couldn't set up Node ($($_.Exception.Message)). Check your internet connection, then run this again - it's safe to re-run."
|
|
70
|
+
}
|
|
71
|
+
$NodeExe = Join-Path $nodeRoot "$distName\node.exe"
|
|
72
|
+
if (-not (Test-Path $NodeExe)) { throw "Node was downloaded but node.exe wasn't found at $NodeExe." }
|
|
73
|
+
Say "Node is ready."
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# --- Git (portable) -----------------------------------------------------------
|
|
77
|
+
if (Test-Cmd 'git') {
|
|
78
|
+
Say "Git is already installed - reusing it."
|
|
79
|
+
} else {
|
|
80
|
+
Say "Setting up Git - it keeps the history of your project."
|
|
81
|
+
# PortableGit download/extract — deferred (needs pinned version + release URL); resolved at rehearsal.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# --- Launch Kindling (clean-runtime: absolute node when portable, else npx on PATH) -----------
|
|
85
|
+
Say "Starting Kindling..."
|
|
86
|
+
if ($null -eq $NodeExe) {
|
|
87
|
+
& npx -y "@aiviatic/kindling@$KindlingVersion"
|
|
88
|
+
} else {
|
|
89
|
+
# Invoke npx's CLI through the exact provisioned node binary — no reliance on PATH.
|
|
90
|
+
$NpxCli = Join-Path (Split-Path $NodeExe) 'node_modules\npm\bin\npx-cli.js'
|
|
91
|
+
& $NodeExe $NpxCli -y "@aiviatic/kindling@$KindlingVersion"
|
|
92
|
+
}
|
|
93
|
+
# A native exe (npx) does NOT raise a terminating error on failure even with
|
|
94
|
+
# ErrorActionPreference='Stop' — it only sets $LASTEXITCODE. So check that explicitly.
|
|
95
|
+
if ($LASTEXITCODE -ne 0) {
|
|
96
|
+
Say "Kindling couldn't start. Check that you're connected to the internet, then run it again. It's safe to re-run."
|
|
97
|
+
exit 1
|
|
98
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Kindling bootstrap — macOS/Linux. Served by the Landing Page as:
|
|
3
|
+
# curl -fsSL https://kindling.aiviatic.com/go | bash
|
|
4
|
+
# Ensures the pinned Node + Git, then launches Kindling in the SAME shell (no new terminal).
|
|
5
|
+
#
|
|
6
|
+
# Pinned versions below MUST match engine/pins.ts + node-unix.ts (a unit test asserts this).
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
KINDLING_NODE_VERSION="24.16.0" # == pins.node
|
|
10
|
+
KINDLING_VERSION="0.1.0" # == pins.kindling
|
|
11
|
+
NVM_VERSION="v0.40.3" # == NVM_VERSION (engine/provision/node-unix.ts)
|
|
12
|
+
NODE_FLOOR_MAJOR=20
|
|
13
|
+
|
|
14
|
+
# Self-contained helpers — this file is delivered via `curl … | bash`, where there is NO file on
|
|
15
|
+
# disk (BASH_SOURCE is empty), so we cannot source a shared helper file. Keep these inline.
|
|
16
|
+
say() { printf '\n\033[1m%s\033[0m\n' "$*"; }
|
|
17
|
+
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
|
18
|
+
node_ok() {
|
|
19
|
+
have_cmd node || return 1
|
|
20
|
+
local major
|
|
21
|
+
major="$(node -v 2>/dev/null | sed 's/^v//; s/\..*//')"
|
|
22
|
+
[ -n "$major" ] && [ "$major" -ge "$1" ]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
say "🔥 Getting your computer ready. This usually takes about 5 minutes."
|
|
26
|
+
|
|
27
|
+
# --- Node (pinned, via nvm) ---------------------------------------------------
|
|
28
|
+
if node_ok "$NODE_FLOOR_MAJOR"; then
|
|
29
|
+
say "Node is already installed, reusing it."
|
|
30
|
+
else
|
|
31
|
+
say "Setting up Node, the engine your project runs on."
|
|
32
|
+
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
|
33
|
+
if [ ! -s "$NVM_DIR/nvm.sh" ]; then
|
|
34
|
+
if ! curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/$NVM_VERSION/install.sh" | bash; then
|
|
35
|
+
say "Couldn't install nvm. Check your internet connection, then run the line again. It's safe to re-run."
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
set +u # nvm.sh references unset vars; `set -u` would abort while sourcing it
|
|
40
|
+
# shellcheck source=/dev/null
|
|
41
|
+
. "$NVM_DIR/nvm.sh"
|
|
42
|
+
set -u
|
|
43
|
+
if ! nvm install "$KINDLING_NODE_VERSION"; then
|
|
44
|
+
say "Couldn't set up Node. Check your internet connection, then run the line again."
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# --- Git is provisioned by the ENGINE (in the browser), not here -------------
|
|
50
|
+
# On macOS/Linux, Git/Xcode-CLT provisioning runs inside kindling so the browser shows the
|
|
51
|
+
# never-frozen progress (the ~5-min macOS dialog with reassurance). The bootstrap only needs
|
|
52
|
+
# Node to launch kindling — Git isn't required to start. (Provisioning-reconciliation Option C.)
|
|
53
|
+
|
|
54
|
+
# --- Launch Kindling (same shell — node is on PATH via nvm or already present) -
|
|
55
|
+
say "Starting Kindling…"
|
|
56
|
+
if ! npx -y "@aiviatic/kindling@$KINDLING_VERSION"; then
|
|
57
|
+
say "Kindling couldn't start. Check that you're connected to the internet, then run the line again. It's safe to re-run."
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StepId,
|
|
3
|
+
exec,
|
|
4
|
+
expandTilde
|
|
5
|
+
} from "./chunk-OU3WSB6B.js";
|
|
6
|
+
|
|
7
|
+
// server/server.ts
|
|
8
|
+
import { createServer as createHttpServer } from "http";
|
|
9
|
+
import { once } from "events";
|
|
10
|
+
import { readFile } from "fs/promises";
|
|
11
|
+
import { join, normalize, extname } from "path";
|
|
12
|
+
var MIME = {
|
|
13
|
+
".html": "text/html; charset=utf-8",
|
|
14
|
+
".js": "text/javascript; charset=utf-8",
|
|
15
|
+
".css": "text/css; charset=utf-8",
|
|
16
|
+
".json": "application/json; charset=utf-8",
|
|
17
|
+
".svg": "image/svg+xml",
|
|
18
|
+
".ico": "image/x-icon"
|
|
19
|
+
};
|
|
20
|
+
var MAX_BODY_BYTES = 64 * 1024;
|
|
21
|
+
var KNOWN_STEPS = new Set(Object.values(StepId));
|
|
22
|
+
async function readBody(req) {
|
|
23
|
+
let body = "";
|
|
24
|
+
for await (const chunk of req) {
|
|
25
|
+
body += String(chunk);
|
|
26
|
+
if (body.length > MAX_BODY_BYTES) throw new Error("body too large");
|
|
27
|
+
}
|
|
28
|
+
return body;
|
|
29
|
+
}
|
|
30
|
+
async function serveStatic(uiDir, pathname, res) {
|
|
31
|
+
const rel = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, "");
|
|
32
|
+
const filePath = normalize(join(uiDir, rel));
|
|
33
|
+
if (filePath !== normalize(uiDir) && !filePath.startsWith(normalize(uiDir) + "/")) {
|
|
34
|
+
res.writeHead(403).end("forbidden");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const data = await readFile(filePath);
|
|
39
|
+
res.writeHead(200, { "Content-Type": MIME[extname(filePath)] ?? "application/octet-stream" });
|
|
40
|
+
res.end(data);
|
|
41
|
+
} catch {
|
|
42
|
+
res.writeHead(404).end("not found");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function startServer(opts) {
|
|
46
|
+
const { emitter, commands, uiDir, onWelcomeAck } = opts;
|
|
47
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
48
|
+
const handler = async (req, res) => {
|
|
49
|
+
const { pathname } = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
50
|
+
const hostname = (req.headers.host ?? "").split(":")[0].toLowerCase();
|
|
51
|
+
if (hostname !== "127.0.0.1" && hostname !== "localhost") {
|
|
52
|
+
res.writeHead(403).end("forbidden");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (req.method === "GET" && pathname === "/events") {
|
|
56
|
+
res.writeHead(200, {
|
|
57
|
+
"Content-Type": "text/event-stream",
|
|
58
|
+
"Cache-Control": "no-cache",
|
|
59
|
+
Connection: "keep-alive"
|
|
60
|
+
});
|
|
61
|
+
const send = (event) => {
|
|
62
|
+
if (!res.writableEnded) res.write(`data: ${JSON.stringify(event)}
|
|
63
|
+
|
|
64
|
+
`);
|
|
65
|
+
};
|
|
66
|
+
for (const event of emitter.events()) send(event);
|
|
67
|
+
const off = emitter.on(send);
|
|
68
|
+
sseClients.add(res);
|
|
69
|
+
req.on("close", () => {
|
|
70
|
+
off();
|
|
71
|
+
sseClients.delete(res);
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (req.method === "POST") {
|
|
76
|
+
if (req.headers["x-kindling"] !== "1") {
|
|
77
|
+
res.writeHead(403).end("forbidden");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (pathname === "/start") {
|
|
81
|
+
let body;
|
|
82
|
+
try {
|
|
83
|
+
body = await readBody(req);
|
|
84
|
+
} catch {
|
|
85
|
+
res.writeHead(413).end("body too large");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let config;
|
|
89
|
+
try {
|
|
90
|
+
const p = JSON.parse(body || "null");
|
|
91
|
+
if (p && typeof p === "object" && typeof p.projectDir === "string" && p.projectDir.length > 0 && typeof p.projectName === "string" && p.projectName.length > 0 && Array.isArray(p.ides) && p.ides.length > 0 && Array.isArray(p.modules) && p.pins !== null && typeof p.pins === "object") {
|
|
92
|
+
config = p;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
config = void 0;
|
|
96
|
+
}
|
|
97
|
+
if (!config) {
|
|
98
|
+
res.writeHead(400).end("invalid config");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
res.writeHead(202).end();
|
|
102
|
+
void Promise.resolve(commands.start(config)).catch(() => {
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (pathname === "/inspect") {
|
|
107
|
+
let body;
|
|
108
|
+
try {
|
|
109
|
+
body = await readBody(req);
|
|
110
|
+
} catch {
|
|
111
|
+
res.writeHead(413).end("body too large");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let projectDir;
|
|
115
|
+
try {
|
|
116
|
+
const p = JSON.parse(body || "null");
|
|
117
|
+
if (p && typeof p === "object" && typeof p.projectDir === "string" && p.projectDir.length > 0) {
|
|
118
|
+
projectDir = p.projectDir;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
projectDir = void 0;
|
|
122
|
+
}
|
|
123
|
+
if (!projectDir) {
|
|
124
|
+
res.writeHead(400).end("invalid dir");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const result = commands.inspect ? await commands.inspect(expandTilde(projectDir)) : { isKindlingProject: false, installedBmadVersion: null };
|
|
128
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }).end(JSON.stringify(result));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (pathname === "/cancel") {
|
|
132
|
+
commands.cancel();
|
|
133
|
+
res.writeHead(202).end();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (pathname === "/ack") {
|
|
137
|
+
res.writeHead(202).end();
|
|
138
|
+
onWelcomeAck?.();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (pathname === "/retry") {
|
|
142
|
+
let body;
|
|
143
|
+
try {
|
|
144
|
+
body = await readBody(req);
|
|
145
|
+
} catch {
|
|
146
|
+
res.writeHead(413).end("body too large");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let step;
|
|
150
|
+
try {
|
|
151
|
+
step = JSON.parse(body || "{}").step;
|
|
152
|
+
} catch {
|
|
153
|
+
step = void 0;
|
|
154
|
+
}
|
|
155
|
+
if (!step || !KNOWN_STEPS.has(step)) {
|
|
156
|
+
res.writeHead(400).end("invalid step");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
res.writeHead(202).end();
|
|
160
|
+
void Promise.resolve(commands.retry(step)).catch(() => {
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (req.method === "GET" && uiDir) {
|
|
166
|
+
await serveStatic(uiDir, pathname, res);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
res.writeHead(404).end("not found");
|
|
170
|
+
};
|
|
171
|
+
const server = createHttpServer((req, res) => {
|
|
172
|
+
handler(req, res).catch(() => {
|
|
173
|
+
if (!res.headersSent) res.writeHead(500).end("server error");
|
|
174
|
+
else res.destroy();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
server.listen(0, "127.0.0.1");
|
|
178
|
+
await once(server, "listening");
|
|
179
|
+
const { port } = server.address();
|
|
180
|
+
return {
|
|
181
|
+
server,
|
|
182
|
+
port,
|
|
183
|
+
url: `http://127.0.0.1:${port}/`,
|
|
184
|
+
close: () => new Promise((resolve, reject) => {
|
|
185
|
+
for (const res of sseClients) res.end();
|
|
186
|
+
sseClients.clear();
|
|
187
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
188
|
+
server.closeAllConnections();
|
|
189
|
+
})
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// server/open-browser.ts
|
|
194
|
+
async function openBrowser(url, opts = {}) {
|
|
195
|
+
const exec2 = opts.exec ?? exec;
|
|
196
|
+
const platform = opts.platform ?? process.platform;
|
|
197
|
+
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
198
|
+
try {
|
|
199
|
+
const r = await exec2(cmd, args);
|
|
200
|
+
return r.code === 0;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export {
|
|
207
|
+
startServer,
|
|
208
|
+
openBrowser
|
|
209
|
+
};
|
|
210
|
+
//# sourceMappingURL=chunk-IS6LC3HK.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 } 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 if (filePath !== normalize(uiDir) && !filePath.startsWith(normalize(uiDir) + '/')) {\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"]}
|