wicked-interactive 0.4.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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/ensure-siblings.mjs +94 -0
- package/bin/wi-watch.mjs +111 -0
- package/bin/wicked-interactive.js +96 -0
- package/frontend/dist/assets/index-Df5rc-Mm.js +41 -0
- package/frontend/dist/assets/index-Dq_AQHYX.css +1 -0
- package/frontend/dist/index.html +13 -0
- package/package.json +40 -0
- package/src/core/feedback-schema.js +124 -0
- package/src/core/instrument.js +116 -0
- package/src/core/regenerate.js +140 -0
- package/src/core/theme.js +79 -0
- package/src/core/versions.js +109 -0
- package/src/index.js +7 -0
- package/src/service/bus.js +30 -0
- package/src/service/demo.js +411 -0
- package/src/service/export.js +124 -0
- package/src/service/fsstore.js +26 -0
- package/src/service/generation.js +105 -0
- package/src/service/preflight.js +84 -0
- package/src/service/server.js +580 -0
- package/src/service/structural.js +103 -0
- package/src/service/theme-source.js +63 -0
- package/src/service/workspace.js +141 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wicked-interactive",
|
|
3
|
+
"description": "Interactive HTML & presentation builder with an in-browser feedback loop for non-technical business users. Highlight blocks, attach plain-language feedback, regenerate live with the supervising Claude session in the loop. Versioned, forkable, exportable to self-contained HTML/PDF.",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "Mike Parcewski",
|
|
6
|
+
"url": "https://github.com/mikeparcewski"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "wicked-interactive",
|
|
11
|
+
"description": "Interactive HTML & presentation builder with an in-browser feedback loop. One command starts the service, opens the browser, and puts the running Claude session into the supervising-agent loop. Deterministic edits apply instantly; structural edits are delegated to the agent through a data-wid-preserving request/response protocol. Requires sibling plugins wicked-prezzie, wicked-garden, and wicked-brain.",
|
|
12
|
+
"version": "0.4.0",
|
|
13
|
+
"source": "./",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Mike Parcewski",
|
|
16
|
+
"url": "https://github.com/mikeparcewski"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wicked-interactive",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Interactive HTML & presentation builder with an in-browser feedback loop for non-technical business users. Build a draft, review it in the browser, highlight any block, attach plain-language feedback, and watch it regenerate live — with the supervising Claude session as the intelligence in the loop. Versioned, forkable, and exportable to self-contained HTML or PDF.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Mike Parcewski",
|
|
7
|
+
"url": "https://github.com/mikeparcewski"
|
|
8
|
+
},
|
|
9
|
+
"repository": "https://github.com/mikeparcewski/wicked-interactive",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"interactive-html",
|
|
13
|
+
"presentation-builder",
|
|
14
|
+
"in-browser-feedback",
|
|
15
|
+
"live-reload",
|
|
16
|
+
"data-wid-anchoring",
|
|
17
|
+
"versioned-documents",
|
|
18
|
+
"structural-editing",
|
|
19
|
+
"agent-in-the-loop",
|
|
20
|
+
"self-contained-export",
|
|
21
|
+
"html-to-pdf",
|
|
22
|
+
"business-users",
|
|
23
|
+
"wicked-prezzie",
|
|
24
|
+
"wicked-bus"
|
|
25
|
+
]
|
|
26
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mike Parcewski
|
|
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,76 @@
|
|
|
1
|
+
```
|
|
2
|
+
_ _ _ _ _ _ _
|
|
3
|
+
__ _(_) ___| | _____ __| | (_)_ __ | |_ ___ _ __ __ _ ___| |_(_)_ _____
|
|
4
|
+
\ \ /\ / / |/ __| |/ / _ \/ _` |_____| | '_ \| __/ _ \ '__/ _` |/ __| __| \ \ / / _ \
|
|
5
|
+
\ V V /| | (__| < __/ (_| |_____| | | | | || __/ | | (_| | (__| |_| |\ V / __/
|
|
6
|
+
\_/\_/ |_|\___|_|\_\___|\__,_| |_|_| |_|\__\___|_| \__,_|\___|\__|_| \_/ \___|
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## It's 11pm. The deck's due tomorrow. You haven't opened PowerPoint.
|
|
10
|
+
|
|
11
|
+
Good news: you don't have to. Just tell it what you need — out loud, like you'd tell a coworker — and watch it build the thing in your browser. The board deck. The launch one-pager. The sales page. A narrated demo video of your product. Then point at anything you don't like and say what to fix.
|
|
12
|
+
|
|
13
|
+
No code. No design tickets. No "let me loop in the team." Just you, describing what's in your head, watching it appear.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Describe it → Watch it build → Point at what to change → Ship it (HTML · PDF · video)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If you can say it, you can make it.
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/mikeparcewski/wicked-interactive/main/assets/wicked-interactive-demo.gif" alt="wicked-interactive in action: describe a launch page in chat, watch it build live, point at text to change it, remove a block, ask to make it premium, and rewind any version — all in the browser" width="100%">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Stuff you'd normally dread making
|
|
28
|
+
|
|
29
|
+
- 📊 **Decks** — board updates, pitch decks, QBRs. On-brand and export-ready, not a sad bulleted list.
|
|
30
|
+
- 📄 **Docs & one-pagers** — reports, briefs, proposals, the FAQ nobody wants to write.
|
|
31
|
+
- 📣 **Marketing** — landing pages, launch posts, sales pages, anything that lives on the web.
|
|
32
|
+
- 🎬 **Demo videos** — point it at your live app, say what to show, get a narrated walkthrough with clickable chapter thumbnails.
|
|
33
|
+
|
|
34
|
+
Under the hood it's all real, interactive HTML — built, polished, and exported without you ever leaving the browser.
|
|
35
|
+
|
|
36
|
+
## Try it in 30 seconds
|
|
37
|
+
|
|
38
|
+
Talk to it like this:
|
|
39
|
+
|
|
40
|
+
> *"Build me a deck about our Q3 results from scratch."*
|
|
41
|
+
> *"Make a landing page for the new pricing tier."*
|
|
42
|
+
> *"Record a walkthrough of my app showing sign-up and the dashboard."*
|
|
43
|
+
> *"Make this headline punchier — and that number's $4.2M, not $4M."*
|
|
44
|
+
> *"Honestly? Make the whole thing feel more expensive."*
|
|
45
|
+
|
|
46
|
+
It hands you a first draft. You click a thing, say what's wrong, and watch it fix itself — live, while you're looking at it. Every version quietly saves, so you can rewind to that one you liked three changes ago — or **fork** it and chase two ideas at once without losing either. When it looks right, export clean HTML or PDF, or grab the video. Done. Go to bed.
|
|
47
|
+
|
|
48
|
+
## Why people get hooked
|
|
49
|
+
|
|
50
|
+
- 🪄 **Just start talking.** Give it a topic, get a real first draft back. No blank page, ever.
|
|
51
|
+
- 🖱️ **Fix it by pointing.** See something off? Highlight it, say what you want in plain English, done.
|
|
52
|
+
- 📎 **It uses your actual numbers.** Drop in your files and folders; it reads them so the real figures show up — no copy-paste.
|
|
53
|
+
- 🎬 **Your app becomes a video.** Narrated walkthrough with YouTube-style chapters. Want a different take? Just ask again.
|
|
54
|
+
- ⏪ **You literally cannot lose work.** Every change is a saved version. Rewind to any of them, anytime.
|
|
55
|
+
- 🍴 **Can't decide? Don't.** Fork any version and keep both, side by side.
|
|
56
|
+
- 📤 **Send it to anyone.** One self-contained file — HTML, PDF, or video. Nothing for them to download or figure out.
|
|
57
|
+
- 🙅 **No scary black terminal.** Once it's going, it all happens in your browser.
|
|
58
|
+
|
|
59
|
+
## Get it running
|
|
60
|
+
|
|
61
|
+
First, a one-time bit of setup. You'll need [Claude Code](https://claude.com/claude-code) — install it, then paste these two lines where it asks:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
/plugin marketplace add mikeparcewski/wicked-interactive
|
|
65
|
+
/plugin install wicked-interactive
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
And from then on, the only thing you ever type is:
|
|
69
|
+
|
|
70
|
+
> **"start wicked-interactive"**
|
|
71
|
+
|
|
72
|
+
That's genuinely it. The first time, it sets up a few helper tools behind the scenes — it'll show you exactly what it's installing, nothing sneaky — then your browser pops open and you're off. (Rather install the helpers yourself? Set `WI_NO_AUTOINSTALL=1` and it'll just tell you what to run.)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
MIT licensed — see [LICENSE](LICENSE). Built on [wicked-prezzie](https://github.com/mikeparcewski/wicked-prezzie), [wicked-garden](https://github.com/mikeparcewski/wicked-garden), and [wicked-brain](https://github.com/mikeparcewski/wicked-brain).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ensure-siblings.mjs — set up the helper tools wicked-interactive needs (ADR-0016).
|
|
3
|
+
//
|
|
4
|
+
// Run once at startup by the `serve` skill. For each MISSING sibling it runs the real
|
|
5
|
+
// install command — transparently: every command is printed before it runs, so the user
|
|
6
|
+
// always sees exactly what's happening on their machine. Nothing is installed silently.
|
|
7
|
+
//
|
|
8
|
+
// node bin/ensure-siblings.mjs # auto-install anything missing
|
|
9
|
+
// node bin/ensure-siblings.mjs --check # report only, install nothing
|
|
10
|
+
// WI_NO_AUTOINSTALL=1 node … # same as --check (opt out of auto-install)
|
|
11
|
+
//
|
|
12
|
+
// Cross-platform: spawnSync(..., { shell:true }) resolves `claude`/`npx` via PATH on
|
|
13
|
+
// macOS, Linux, and Windows.
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { preflight, playwrightInstalled } from "../src/service/preflight.js";
|
|
17
|
+
|
|
18
|
+
// Playwright (ADR-0018) powers demo recording. It's an npm dependency, not a Claude plugin,
|
|
19
|
+
// so it installs differently: pull the package + browser binaries, then the skills (the
|
|
20
|
+
// supervising agent uses these to learn the target app). `playwright-cli install --skills`
|
|
21
|
+
// relies on the softlinked skill dirs in the standard locations.
|
|
22
|
+
const PLAYWRIGHT_STEPS = [
|
|
23
|
+
"npm install playwright",
|
|
24
|
+
"npx playwright install",
|
|
25
|
+
"playwright-cli install --skills",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Each sibling installs differently — an ordered list of shell commands per plugin.
|
|
29
|
+
// prezzie / garden → Claude Code plugins (marketplace add, then install)
|
|
30
|
+
// brain → npm package, run via npx
|
|
31
|
+
const INSTALL_STEPS = {
|
|
32
|
+
"wicked-prezzie": [
|
|
33
|
+
"claude plugin marketplace add mikeparcewski/wicked-prezzie",
|
|
34
|
+
"claude plugin install wicked-prezzie",
|
|
35
|
+
],
|
|
36
|
+
"wicked-garden": [
|
|
37
|
+
"claude plugin marketplace add mikeparcewski/wicked-garden",
|
|
38
|
+
"claude plugin install wicked-garden",
|
|
39
|
+
],
|
|
40
|
+
"wicked-brain": [
|
|
41
|
+
"npx -y wicked-brain",
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function run(cmd) {
|
|
46
|
+
console.log(` $ ${cmd}`);
|
|
47
|
+
return spawnSync(cmd, { stdio: "inherit", shell: true }).status === 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const checkOnly = process.argv.includes("--check") || process.env.WI_NO_AUTOINSTALL === "1";
|
|
51
|
+
|
|
52
|
+
let pf = preflight();
|
|
53
|
+
const pwMissing = !playwrightInstalled();
|
|
54
|
+
if (pf.ok && !pwMissing) {
|
|
55
|
+
console.log("Helper tools: all present. Nothing to install.");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const wanted = [...pf.missing, ...(pwMissing ? ["playwright (demo recorder)"] : [])];
|
|
60
|
+
console.log(`wicked-interactive needs ${wanted.length} helper tool(s): ${wanted.join(", ")}`);
|
|
61
|
+
|
|
62
|
+
if (checkOnly) {
|
|
63
|
+
console.log("\nAuto-install is off. Install these yourself, then restart:");
|
|
64
|
+
if (pf.install_hint) console.log(pf.install_hint);
|
|
65
|
+
if (pwMissing) console.log(pf.playwright.install_hint);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log("\nSetting them up for you now (one time). Each command is shown before it runs:");
|
|
70
|
+
for (const name of pf.missing) {
|
|
71
|
+
console.log(`\n• ${name}`);
|
|
72
|
+
for (const cmd of INSTALL_STEPS[name]) {
|
|
73
|
+
if (!run(cmd)) console.error(` ! '${cmd}' did not succeed — continuing.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (pwMissing) {
|
|
77
|
+
console.log(`\n• playwright (demo recorder)`);
|
|
78
|
+
for (const cmd of PLAYWRIGHT_STEPS) {
|
|
79
|
+
if (!run(cmd)) console.error(` ! '${cmd}' did not succeed — continuing.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pf = preflight();
|
|
84
|
+
if (pf.ok && playwrightInstalled()) {
|
|
85
|
+
console.log("\nAll set — every helper tool is installed.");
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const stillMissing = [...pf.missing, ...(playwrightInstalled() ? [] : ["playwright"])];
|
|
90
|
+
console.error(`\nStill missing: ${stillMissing.join(", ")}. Finish these by hand, then restart:`);
|
|
91
|
+
if (pf.install_hint) console.error(pf.install_hint);
|
|
92
|
+
if (!playwrightInstalled()) console.error(pf.playwright.install_hint);
|
|
93
|
+
console.error("\n(If 'claude' isn't found, run this from inside Claude Code — that's where the plugin CLI lives.)");
|
|
94
|
+
process.exit(1);
|
package/bin/wi-watch.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// wi-watch.mjs — operator-facing event tail for the multi-doc service.
|
|
3
|
+
//
|
|
4
|
+
// Connects to GET /api/events/all (added in ADR-0015 follow-up) and prints one event
|
|
5
|
+
// per stdout line: `HH:MM:SS doc event {json}`. Each line is one event so it composes
|
|
6
|
+
// with the Monitor tool's line-as-event model.
|
|
7
|
+
//
|
|
8
|
+
// Resilience (after the 2026-05-28 silent-watcher incident):
|
|
9
|
+
// - Reconnects on every terminal signal the socket can emit: end, error, close,
|
|
10
|
+
// AND socket-level error/close. The earlier version only handled end+error and
|
|
11
|
+
// went silent when an abrupt service kill emitted `close` first.
|
|
12
|
+
// - Stream watchdog: if no bytes (not even a heartbeat) arrive for STALL_MS, treat
|
|
13
|
+
// the connection as dead and reconnect. Catches the case where the socket is
|
|
14
|
+
// half-open (kernel never delivers an EOF).
|
|
15
|
+
// - 30s server-side heartbeat ping keeps proxies + the watchdog happy.
|
|
16
|
+
// - Reconnect is idempotent — only one reconnect timer fires per disconnect.
|
|
17
|
+
// - Backs off briefly between retries (250ms / 1s) so a crash-looping service doesn't
|
|
18
|
+
// melt the watcher.
|
|
19
|
+
// - Long-lived: never exits on its own. Stop it with SIGINT/SIGTERM or Monitor TaskStop.
|
|
20
|
+
|
|
21
|
+
import http from "node:http";
|
|
22
|
+
import { parseArgs } from "node:util";
|
|
23
|
+
|
|
24
|
+
const { values: argv } = parseArgs({
|
|
25
|
+
options: {
|
|
26
|
+
base: { type: "string", default: process.env.WI_BASE || "http://localhost:4400" },
|
|
27
|
+
quiet: { type: "boolean", default: false }, // suppress connect/reconnect lines
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const url = new URL("/api/events/all", argv.base);
|
|
32
|
+
// 60s was too tight in practice — long-lived loopback streams occasionally go quiet for
|
|
33
|
+
// ~1 min even with 15s heartbeats. 180s catches genuine dead sockets without false-tripping
|
|
34
|
+
// every ~17 min. Real drops fire socket-close events immediately anyway.
|
|
35
|
+
const STALL_MS = 180_000;
|
|
36
|
+
|
|
37
|
+
function ts() { return new Date().toISOString().slice(11, 19); }
|
|
38
|
+
function note(msg) { if (!argv.quiet) console.log(`${ts()} watcher ${msg}`); }
|
|
39
|
+
|
|
40
|
+
let activeReq = null; // the in-flight request; destroyed before each reconnect to
|
|
41
|
+
// prevent parallel SSE streams (event-spam after a stall reconnect)
|
|
42
|
+
|
|
43
|
+
function connect() {
|
|
44
|
+
let scheduled = false;
|
|
45
|
+
let stallTimer = null;
|
|
46
|
+
function scheduleReconnect(reason, delayMs) {
|
|
47
|
+
if (scheduled) return; // first signal wins; ignore the others
|
|
48
|
+
scheduled = true;
|
|
49
|
+
if (stallTimer) { clearTimeout(stallTimer); stallTimer = null; }
|
|
50
|
+
if (activeReq) { try { activeReq.destroy(); } catch {} activeReq = null; }
|
|
51
|
+
note(`${reason} — reconnecting in ${delayMs}ms`);
|
|
52
|
+
setTimeout(connect, delayMs);
|
|
53
|
+
}
|
|
54
|
+
function bumpWatchdog() {
|
|
55
|
+
if (stallTimer) clearTimeout(stallTimer);
|
|
56
|
+
stallTimer = setTimeout(() => scheduleReconnect(`no traffic for ${STALL_MS}ms`, 250), STALL_MS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const req = http.get(url, (res) => {
|
|
60
|
+
if (res.statusCode !== 200) {
|
|
61
|
+
note(`http ${res.statusCode}`);
|
|
62
|
+
res.resume();
|
|
63
|
+
scheduleReconnect("non-200", 1000);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
note(`connected ${url.href}`);
|
|
67
|
+
bumpWatchdog();
|
|
68
|
+
let buf = "";
|
|
69
|
+
res.setEncoding("utf8");
|
|
70
|
+
res.on("data", (chunk) => {
|
|
71
|
+
bumpWatchdog();
|
|
72
|
+
buf += chunk;
|
|
73
|
+
let i;
|
|
74
|
+
while ((i = buf.indexOf("\n\n")) !== -1) {
|
|
75
|
+
const frame = buf.slice(0, i); buf = buf.slice(i + 2);
|
|
76
|
+
let ev = "?", data = "";
|
|
77
|
+
for (const line of frame.split("\n")) {
|
|
78
|
+
if (line.startsWith("event: ")) ev = line.slice(7);
|
|
79
|
+
else if (line.startsWith("data: ")) data += line.slice(6);
|
|
80
|
+
}
|
|
81
|
+
if (ev === "ready") continue;
|
|
82
|
+
// SSE comment frames (server-side heartbeats start with `:`) have no event:/data:
|
|
83
|
+
// lines, so ev/data stay empty. Skip them — they keep the connection warm but
|
|
84
|
+
// shouldn't surface as conversation notifications.
|
|
85
|
+
if (ev === "?" && !data) continue;
|
|
86
|
+
let parsed = data;
|
|
87
|
+
try { parsed = JSON.stringify(JSON.parse(data)); } catch { /* keep raw */ }
|
|
88
|
+
let doc = "?";
|
|
89
|
+
try { const o = JSON.parse(data); if (o.doc) doc = o.doc; } catch {}
|
|
90
|
+
console.log(`${ts()} ${doc} ${ev} ${parsed.slice(0, 280)}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
res.on("end", () => scheduleReconnect("stream end", 250));
|
|
94
|
+
res.on("close", () => scheduleReconnect("stream close", 250));
|
|
95
|
+
res.on("error", (e) => scheduleReconnect(`stream error: ${e.message}`, 1000));
|
|
96
|
+
// Socket-level events catch abrupt TCP resets that don't surface as response events.
|
|
97
|
+
if (res.socket) {
|
|
98
|
+
res.socket.on("error", (e) => scheduleReconnect(`socket error: ${e.message}`, 1000));
|
|
99
|
+
res.socket.on("close", () => scheduleReconnect("socket close", 250));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
req.setTimeout(0);
|
|
103
|
+
req.on("error", (e) => scheduleReconnect(`http error: ${e.message}`, 1000));
|
|
104
|
+
activeReq = req;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
process.on("SIGINT", () => { note("SIGINT — bye"); process.exit(0); });
|
|
108
|
+
process.on("SIGTERM", () => { note("SIGTERM — bye"); process.exit(0); });
|
|
109
|
+
|
|
110
|
+
note(`starting; tailing ${url.href}`);
|
|
111
|
+
connect();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// wicked-interactive CLI — the one command a business user runs (INV-6).
|
|
3
|
+
//
|
|
4
|
+
// wicked-interactive serve --root <docs-dir> [--port N] [--watch]
|
|
5
|
+
// Multi-document mode (ADR-0015). Hosts every workspace under <docs-dir>;
|
|
6
|
+
// new docs are created from the UI ("New document" modal). Preferred.
|
|
7
|
+
// --watch also tails /api/events/all in the same terminal — operator visibility
|
|
8
|
+
// into every per-doc broadcast.
|
|
9
|
+
//
|
|
10
|
+
// wicked-interactive serve --dir <workspace> [--html <file>] [--port N]
|
|
11
|
+
// Legacy single-document mode. Workspace must exist; --html seeds _v0.
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { createServer, createMultiServer } from "../src/service/server.js";
|
|
18
|
+
import { initWorkspace } from "../src/service/workspace.js";
|
|
19
|
+
|
|
20
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const args = { _: [] };
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (!a.startsWith("--")) { args._.push(a); continue; }
|
|
27
|
+
const key = a.slice(2);
|
|
28
|
+
const next = argv[i + 1];
|
|
29
|
+
// Boolean flag if the next arg is missing or itself another --flag; otherwise consume it.
|
|
30
|
+
if (next === undefined || next.startsWith("--")) args[key] = true;
|
|
31
|
+
else { args[key] = next; i++; }
|
|
32
|
+
}
|
|
33
|
+
return args;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Spawn wi-watch.mjs (sibling in bin/) as a child and pipe its lines into the parent's stdout. */
|
|
37
|
+
function spawnWatcher(base) {
|
|
38
|
+
const script = resolve(HERE, "wi-watch.mjs");
|
|
39
|
+
const child = spawn(process.execPath, [script, "--base", base], { stdio: ["ignore", "inherit", "inherit"] });
|
|
40
|
+
child.on("exit", (code, sig) => console.log(`[watch] exited (code=${code} sig=${sig})`));
|
|
41
|
+
return child;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const args = parseArgs(process.argv.slice(2));
|
|
46
|
+
const cmd = args._[0];
|
|
47
|
+
if (cmd !== "serve") {
|
|
48
|
+
console.error("usage: wicked-interactive serve { --root <docs-dir> | --dir <workspace> [--html <file>] } [--port N]");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const port = args.port ? Number(args.port) : 4400;
|
|
53
|
+
|
|
54
|
+
if (args.root) {
|
|
55
|
+
const svc = createMultiServer({ root: args.root });
|
|
56
|
+
const actualPort = await svc.start(port);
|
|
57
|
+
const base = `http://localhost:${actualPort}`;
|
|
58
|
+
console.log(`wicked-interactive (multi-doc) serving ${args.root} on ${base}`);
|
|
59
|
+
console.log(` docs: ${base}/api/docs`);
|
|
60
|
+
console.log(` open: ${base}/?doc=<name>`);
|
|
61
|
+
console.log(` tail: ${base}/api/events/all`);
|
|
62
|
+
let watcher = null;
|
|
63
|
+
if (args.watch) {
|
|
64
|
+
console.log(" --watch: tailing events in this terminal");
|
|
65
|
+
watcher = spawnWatcher(base);
|
|
66
|
+
}
|
|
67
|
+
const shutdown = async () => { if (watcher) watcher.kill(); await svc.stop(); process.exit(0); };
|
|
68
|
+
process.on("SIGINT", shutdown);
|
|
69
|
+
process.on("SIGTERM", shutdown);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!args.dir) {
|
|
74
|
+
console.error("error: pass --root <docs-dir> (multi-doc) or --dir <workspace> (legacy)");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const dir = args.dir;
|
|
78
|
+
if (!existsSync(join(dir, "versions.json"))) {
|
|
79
|
+
if (!args.html) {
|
|
80
|
+
console.error(`error: workspace ${dir} is not initialised; pass --html <file> to seed it`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
initWorkspace(dir, readFileSync(args.html, "utf-8"));
|
|
84
|
+
console.log(`initialised workspace at ${dir} from ${args.html}`);
|
|
85
|
+
}
|
|
86
|
+
const svc = createServer({ dir, documentId: dir });
|
|
87
|
+
const actualPort = await svc.start(port);
|
|
88
|
+
console.log(`wicked-interactive (single-doc) serving ${dir} on http://localhost:${actualPort}`);
|
|
89
|
+
console.log(` head: http://localhost:${actualPort}/doc`);
|
|
90
|
+
console.log(` events: http://localhost:${actualPort}/events`);
|
|
91
|
+
const shutdown = async () => { await svc.stop(); process.exit(0); };
|
|
92
|
+
process.on("SIGINT", shutdown);
|
|
93
|
+
process.on("SIGTERM", shutdown);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|