instagui 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 +144 -0
- package/dist/cli/index.js +187 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/bundled.js +29 -0
- package/dist/core/bundled.js.map +1 -0
- package/dist/core/cache.js +41 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/capture.js +161 -0
- package/dist/core/capture.js.map +1 -0
- package/dist/core/compose.js +66 -0
- package/dist/core/compose.js.map +1 -0
- package/dist/core/errors.js +40 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/extract.js +78 -0
- package/dist/core/extract.js.map +1 -0
- package/dist/core/golden.js +44 -0
- package/dist/core/golden.js.map +1 -0
- package/dist/core/onboarding.js +19 -0
- package/dist/core/onboarding.js.map +1 -0
- package/dist/core/override.js +26 -0
- package/dist/core/override.js.map +1 -0
- package/dist/core/resolve.js +22 -0
- package/dist/core/resolve.js.map +1 -0
- package/dist/core/schema-file.js +34 -0
- package/dist/core/schema-file.js.map +1 -0
- package/dist/core/schema.js +58 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/server/browser.js +38 -0
- package/dist/server/browser.js.map +1 -0
- package/dist/server/client.js +146 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/page.js +172 -0
- package/dist/server/page.js.map +1 -0
- package/dist/server/run.js +71 -0
- package/dist/server/run.js.map +1 -0
- package/dist/server/server.js +205 -0
- package/dist/server/server.js.map +1 -0
- package/dist/shared/claude-code.js +89 -0
- package/dist/shared/claude-code.js.map +1 -0
- package/dist/shared/claude.js +33 -0
- package/dist/shared/claude.js.map +1 -0
- package/dist/shared/config.js +17 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/engine.js +17 -0
- package/dist/shared/engine.js.map +1 -0
- package/package.json +61 -0
- package/schemas/README.md +32 -0
- package/schemas/ffmpeg.json +564 -0
- package/schemas/pandoc.json +277 -0
- package/schemas/yt-dlp.json +446 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** Escape a string for use as HTML text or inside a double-quoted attribute. */
|
|
2
|
+
function esc(s) {
|
|
3
|
+
return s
|
|
4
|
+
.replace(/&/g, '&')
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''');
|
|
9
|
+
}
|
|
10
|
+
/** Embed a value as JSON inside a <script> tag without letting "</script>" break out. */
|
|
11
|
+
function safeJson(value) {
|
|
12
|
+
return JSON.stringify(value).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
|
|
13
|
+
}
|
|
14
|
+
/** Render one option control. File paths are plain text fields — scope fence: no native
|
|
15
|
+
* picker (AC 3.1). Booleans → checkbox, enums → dropdown, number → number input. */
|
|
16
|
+
function optionControl(o) {
|
|
17
|
+
const id = `opt-${o.name}`;
|
|
18
|
+
const label = `<label for="${esc(id)}">` +
|
|
19
|
+
`<span class="name">${esc(o.name)}</span>` +
|
|
20
|
+
`<code class="flag">${esc(o.flag)}</code>` +
|
|
21
|
+
(o.required ? '<span class="req" title="required">*</span>' : '') +
|
|
22
|
+
`</label>`;
|
|
23
|
+
const desc = o.description ? `<p class="desc">${esc(o.description)}</p>` : '';
|
|
24
|
+
const common = `id="${esc(id)}" data-kind="option" data-name="${esc(o.name)}" data-type="${esc(o.type)}"` +
|
|
25
|
+
(o.required ? ' data-required="true"' : '');
|
|
26
|
+
let control;
|
|
27
|
+
if (o.type === 'boolean') {
|
|
28
|
+
control = `<input type="checkbox" ${common} />`;
|
|
29
|
+
}
|
|
30
|
+
else if (o.type === 'enum') {
|
|
31
|
+
const opts = ['<option value=""></option>', ...o.enumValues.map((v) => `<option value="${esc(v)}">${esc(v)}</option>`)];
|
|
32
|
+
control = `<select ${common}>${opts.join('')}</select>`;
|
|
33
|
+
}
|
|
34
|
+
else if (o.type === 'number') {
|
|
35
|
+
control = `<input type="number" ${common} placeholder="${esc(o.flag)}" />`;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// string | path — both plain text fields (no picker for path).
|
|
39
|
+
const hint = o.type === 'path' ? ' placeholder="path…"' : '';
|
|
40
|
+
control = `<input type="text" ${common}${hint} />`;
|
|
41
|
+
}
|
|
42
|
+
const cls = o.type === 'boolean' ? 'field field-bool' : 'field';
|
|
43
|
+
return `<div class="${cls}">${label}${control}${desc}</div>`;
|
|
44
|
+
}
|
|
45
|
+
/** Render one positional control. Always a text field (variadic is a hint only in v1 so each
|
|
46
|
+
* positional contributes exactly one verbatim argument). */
|
|
47
|
+
function positionalControl(p) {
|
|
48
|
+
const id = `pos-${p.name}`;
|
|
49
|
+
const label = `<label for="${esc(id)}">` +
|
|
50
|
+
`<span class="name">${esc(p.name)}</span>` +
|
|
51
|
+
(p.variadic ? '<span class="badge">multiple</span>' : '') +
|
|
52
|
+
(p.required ? '<span class="req" title="required">*</span>' : '') +
|
|
53
|
+
`</label>`;
|
|
54
|
+
const desc = p.description ? `<p class="desc">${esc(p.description)}</p>` : '';
|
|
55
|
+
const placeholder = p.type === 'path' ? 'path…' : p.name;
|
|
56
|
+
const control = `<input type="text" id="${esc(id)}" data-kind="positional" data-name="${esc(p.name)}" ` +
|
|
57
|
+
`data-type="${esc(p.type)}"${p.required ? ' data-required="true"' : ''} placeholder="${esc(placeholder)}" />`;
|
|
58
|
+
return `<div class="field">${label}${control}${desc}</div>`;
|
|
59
|
+
}
|
|
60
|
+
/** Group options by their `group` field, preserving first-seen order; "" → "Options". */
|
|
61
|
+
function groupOptions(options) {
|
|
62
|
+
const order = [];
|
|
63
|
+
const byGroup = new Map();
|
|
64
|
+
for (const o of options) {
|
|
65
|
+
const g = o.group || 'Options';
|
|
66
|
+
if (!byGroup.has(g)) {
|
|
67
|
+
byGroup.set(g, []);
|
|
68
|
+
order.push(g);
|
|
69
|
+
}
|
|
70
|
+
byGroup.get(g).push(o);
|
|
71
|
+
}
|
|
72
|
+
return order.map((group) => ({ group, options: byGroup.get(group) }));
|
|
73
|
+
}
|
|
74
|
+
const STYLE = `
|
|
75
|
+
:root { color-scheme: light dark; }
|
|
76
|
+
* { box-sizing: border-box; }
|
|
77
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
78
|
+
margin: 0; background: Canvas; color: CanvasText; line-height: 1.45; }
|
|
79
|
+
header { padding: 1rem 1.25rem; border-bottom: 1px solid rgba(128,128,128,.3); }
|
|
80
|
+
header h1 { margin: 0; font-size: 1.15rem; }
|
|
81
|
+
header h1 code { font-size: 1.15rem; }
|
|
82
|
+
header .summary { margin: .25rem 0 0; opacity: .8; font-size: .9rem; }
|
|
83
|
+
main { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,26rem); gap: 1.25rem;
|
|
84
|
+
padding: 1.25rem; align-items: start; }
|
|
85
|
+
@media (max-width: 800px) { main { grid-template-columns: 1fr; } }
|
|
86
|
+
fieldset { border: 1px solid rgba(128,128,128,.3); border-radius: 8px; margin: 0 0 1rem; padding: .75rem 1rem 1rem; }
|
|
87
|
+
legend { font-weight: 600; padding: 0 .4rem; }
|
|
88
|
+
.field { margin: .55rem 0; }
|
|
89
|
+
.field-bool { display: flex; align-items: center; gap: .5rem; }
|
|
90
|
+
.field-bool label { order: 2; margin: 0; }
|
|
91
|
+
label { display: flex; align-items: baseline; gap: .5rem; font-size: .9rem; margin-bottom: .2rem; }
|
|
92
|
+
label .name { font-weight: 600; }
|
|
93
|
+
code.flag { font-size: .8rem; opacity: .75; }
|
|
94
|
+
.req { color: #d33; font-weight: 700; }
|
|
95
|
+
.badge { font-size: .65rem; text-transform: uppercase; letter-spacing: .04em; opacity: .6;
|
|
96
|
+
border: 1px solid currentColor; border-radius: 4px; padding: 0 .25rem; }
|
|
97
|
+
.desc { margin: .15rem 0 0; font-size: .78rem; opacity: .7; }
|
|
98
|
+
input[type=text], input[type=number], select { width: 100%; padding: .4rem .5rem; font: inherit;
|
|
99
|
+
border: 1px solid rgba(128,128,128,.4); border-radius: 6px; background: Field; color: FieldText; }
|
|
100
|
+
.side { position: sticky; top: 1.25rem; }
|
|
101
|
+
.preview { border: 1px solid rgba(128,128,128,.3); border-radius: 8px; padding: .75rem; }
|
|
102
|
+
.preview h2 { margin: 0 0 .5rem; font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; opacity: .7; }
|
|
103
|
+
pre.cmd { margin: 0; padding: .6rem .7rem; background: rgba(128,128,128,.12); border-radius: 6px;
|
|
104
|
+
white-space: pre-wrap; word-break: break-all; font-size: .85rem; min-height: 1.2rem; }
|
|
105
|
+
.row { display: flex; gap: .5rem; margin-top: .6rem; flex-wrap: wrap; }
|
|
106
|
+
button { font: inherit; padding: .45rem .9rem; border-radius: 6px; border: 1px solid rgba(128,128,128,.4);
|
|
107
|
+
background: rgba(128,128,128,.12); color: inherit; cursor: pointer; }
|
|
108
|
+
button.primary { background: #2563eb; border-color: #2563eb; color: #fff; }
|
|
109
|
+
button:disabled { opacity: .5; cursor: not-allowed; }
|
|
110
|
+
.output { margin-top: 1rem; }
|
|
111
|
+
pre.stream { margin: .4rem 0 0; padding: .6rem .7rem; background: #0b0b0b; color: #e6e6e6;
|
|
112
|
+
border-radius: 6px; max-height: 24rem; overflow: auto; white-space: pre-wrap; word-break: break-all;
|
|
113
|
+
font-size: .82rem; min-height: 2rem; }
|
|
114
|
+
.exit { margin-top: .5rem; font-size: .85rem; }
|
|
115
|
+
.exit.ok { color: #16a34a; } .exit.bad { color: #dc2626; font-weight: 600; }
|
|
116
|
+
.status { font-size: .8rem; opacity: .7; margin-top: .3rem; }
|
|
117
|
+
`;
|
|
118
|
+
/**
|
|
119
|
+
* Render the full single-page HTML document for `schema`. Self-contained (inline CSS + JS,
|
|
120
|
+
* no external resources). The embedded client script lives in server/client.ts.
|
|
121
|
+
*/
|
|
122
|
+
export function renderPage(schema, clientScript) {
|
|
123
|
+
const groups = groupOptions(schema.options);
|
|
124
|
+
const optionsHtml = groups
|
|
125
|
+
.map((g) => `<fieldset><legend>${esc(g.group)}</legend>${g.options.map(optionControl).join('')}</fieldset>`)
|
|
126
|
+
.join('');
|
|
127
|
+
const positionalsHtml = schema.positionals.length
|
|
128
|
+
? `<fieldset><legend>Arguments</legend>${schema.positionals.map(positionalControl).join('')}</fieldset>`
|
|
129
|
+
: '';
|
|
130
|
+
const summary = schema.summary ? `<p class="summary">${esc(schema.summary)}</p>` : '';
|
|
131
|
+
return `<!doctype html>
|
|
132
|
+
<html lang="en">
|
|
133
|
+
<head>
|
|
134
|
+
<meta charset="utf-8" />
|
|
135
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
136
|
+
<title>instagui — ${esc(schema.tool)}</title>
|
|
137
|
+
<style>${STYLE}</style>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<header>
|
|
141
|
+
<h1>instagui <code>${esc(schema.tool)}</code></h1>
|
|
142
|
+
${summary}
|
|
143
|
+
</header>
|
|
144
|
+
<main>
|
|
145
|
+
<form id="form" autocomplete="off">
|
|
146
|
+
${positionalsHtml}
|
|
147
|
+
${optionsHtml}
|
|
148
|
+
</form>
|
|
149
|
+
<div class="side">
|
|
150
|
+
<div class="preview">
|
|
151
|
+
<h2>Command preview</h2>
|
|
152
|
+
<pre class="cmd" id="preview">${esc(schema.tool)}</pre>
|
|
153
|
+
<div class="row">
|
|
154
|
+
<button type="button" id="copy">Copy</button>
|
|
155
|
+
<button type="button" id="run" class="primary">Run</button>
|
|
156
|
+
<button type="button" id="stop" disabled>Stop</button>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="status" id="status"></div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="output" id="output" hidden>
|
|
161
|
+
<h2>Output</h2>
|
|
162
|
+
<pre class="stream" id="stream"></pre>
|
|
163
|
+
<div class="exit" id="exit"></div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</main>
|
|
167
|
+
<script type="application/json" id="schema">${safeJson(schema)}</script>
|
|
168
|
+
<script>${clientScript}</script>
|
|
169
|
+
</body>
|
|
170
|
+
</html>`;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/server/page.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// server/run.ts — Story 3.3. Owns the single-run lifecycle: spawn the composed command with
|
|
2
|
+
// an arguments ARRAY (never a shell), forward stdout+stderr to a sink, report the exit code,
|
|
3
|
+
// and kill on Stop. Exactly one run in flight (AD-5): a second start is refused while one is
|
|
4
|
+
// live. The server ties the sink's lifetime to the SSE connection, so a disconnect that
|
|
5
|
+
// stops the sink also drives Stop → no orphaned child.
|
|
6
|
+
//
|
|
7
|
+
// spawn is injected so the controller is unit-testable with a fake child (no real process).
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
/**
|
|
10
|
+
* Single-run controller. `start` refuses while a run is live; `stop` terminates it
|
|
11
|
+
* (SIGTERM, escalating to SIGKILL if ignored). Exactly one child at a time.
|
|
12
|
+
*/
|
|
13
|
+
export class RunController {
|
|
14
|
+
child = null;
|
|
15
|
+
spawnFn;
|
|
16
|
+
constructor(spawnFn = spawn) {
|
|
17
|
+
this.spawnFn = spawnFn;
|
|
18
|
+
}
|
|
19
|
+
get running() {
|
|
20
|
+
return this.child !== null;
|
|
21
|
+
}
|
|
22
|
+
/** Spawn `cmd` with `args` (args array — no shell). Refuses if a run is already in flight. */
|
|
23
|
+
start(cmd, args, sink) {
|
|
24
|
+
if (this.child)
|
|
25
|
+
return { ok: false, reason: 'a run is already in progress' };
|
|
26
|
+
let child;
|
|
27
|
+
try {
|
|
28
|
+
child = this.spawnFn(cmd, args, { shell: false, windowsHide: true });
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return { ok: false, reason: `failed to start: ${err.message}` };
|
|
32
|
+
}
|
|
33
|
+
this.child = child;
|
|
34
|
+
child.stdout?.on('data', (c) => sink.out(c.toString('utf8')));
|
|
35
|
+
child.stderr?.on('data', (c) => sink.out(c.toString('utf8')));
|
|
36
|
+
const finish = (code, signal) => {
|
|
37
|
+
if (this.child !== child)
|
|
38
|
+
return; // already finished (guards error+close double-fire)
|
|
39
|
+
this.child = null;
|
|
40
|
+
sink.end({ code, signal });
|
|
41
|
+
};
|
|
42
|
+
child.on('error', (err) => {
|
|
43
|
+
// Map the most common failure — ENOENT (binary missing) — to the same friendly wording
|
|
44
|
+
// the CLI's ToolNotFoundError uses, instead of leaking a raw "spawn <tool> ENOENT".
|
|
45
|
+
// Any other spawn error falls back to its message. Either way: surface, then end.
|
|
46
|
+
const code = err.code;
|
|
47
|
+
const msg = code === 'ENOENT'
|
|
48
|
+
? `instagui: "${cmd}" is not installed or not on your PATH.`
|
|
49
|
+
: `instagui: failed to run command: ${err.message}`;
|
|
50
|
+
sink.out(`${msg}\n`);
|
|
51
|
+
finish(null, null);
|
|
52
|
+
});
|
|
53
|
+
child.on('close', (code, signal) => finish(code, signal));
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
/** Kill the running child, if any. Returns whether there was one to kill. */
|
|
57
|
+
stop() {
|
|
58
|
+
const child = this.child;
|
|
59
|
+
if (!child)
|
|
60
|
+
return false;
|
|
61
|
+
child.kill('SIGTERM');
|
|
62
|
+
// Escalate if the child ignores SIGTERM. Timer is unref'd so it never holds the process.
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
if (this.child === child)
|
|
65
|
+
child.kill('SIGKILL');
|
|
66
|
+
}, 2000);
|
|
67
|
+
timer.unref?.();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/server/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// server/server.ts — Story 3.1 (serve the Form) + 3.2 (POST /preview) + 3.3 (run/stream).
|
|
2
|
+
// A single node:http server, bound to 127.0.0.1 only (NFR-2), that:
|
|
3
|
+
// GET / → the single-page Form (server/page.ts)
|
|
4
|
+
// POST /preview → { args, preview } from core/compose.ts (read-only; no CSRF needed)
|
|
5
|
+
// GET /events → SSE stream carrying run output + exit (AD-5)
|
|
6
|
+
// POST /run → compose + spawn (args array, never shell) and stream (AD-6)
|
|
7
|
+
// POST /stop → kill the running child
|
|
8
|
+
// State-changing endpoints (/run, /stop) fail closed on a missing/mismatched Origin (CSRF,
|
|
9
|
+
// AD-6). Exactly one run in flight; the SSE connection owns the run, so a disconnect kills
|
|
10
|
+
// the child. The API key never appears here — only the Schema and tool name are served.
|
|
11
|
+
import { createServer } from 'node:http';
|
|
12
|
+
import { compose } from '../core/compose.js';
|
|
13
|
+
import { renderPage } from './page.js';
|
|
14
|
+
import { CLIENT_SCRIPT } from './client.js';
|
|
15
|
+
import { RunController } from './run.js';
|
|
16
|
+
const DEFAULT_PORT = 5177;
|
|
17
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
18
|
+
const MAX_BODY_BYTES = 512 * 1024;
|
|
19
|
+
/** Read a request body up to a hard cap; returns '' if the cap is exceeded (caller 400s). */
|
|
20
|
+
function readBody(req) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let total = 0;
|
|
24
|
+
let aborted = false;
|
|
25
|
+
req.on('data', (c) => {
|
|
26
|
+
total += c.length;
|
|
27
|
+
if (total > MAX_BODY_BYTES) {
|
|
28
|
+
aborted = true;
|
|
29
|
+
resolve(null);
|
|
30
|
+
req.destroy();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
chunks.push(c);
|
|
34
|
+
});
|
|
35
|
+
req.on('end', () => {
|
|
36
|
+
if (!aborted)
|
|
37
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
38
|
+
});
|
|
39
|
+
req.on('error', () => {
|
|
40
|
+
if (!aborted)
|
|
41
|
+
resolve(null);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function sendJson(res, status, body) {
|
|
46
|
+
const text = JSON.stringify(body);
|
|
47
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
48
|
+
res.end(text);
|
|
49
|
+
}
|
|
50
|
+
function sendText(res, status, body) {
|
|
51
|
+
res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
52
|
+
res.end(body);
|
|
53
|
+
}
|
|
54
|
+
/** Listen with EADDRINUSE fallback: try `port`, and on a clash retry on port 0 (OS-assigned)
|
|
55
|
+
* so a busy default never crashes the launch (AC 3.1). Resolves the actually-bound port. */
|
|
56
|
+
function listenWithFallback(server, port, host) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const onError = (err) => {
|
|
59
|
+
server.removeListener('error', onError);
|
|
60
|
+
if (err.code === 'EADDRINUSE' && port !== 0) {
|
|
61
|
+
listenWithFallback(server, 0, host).then(resolve, reject);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
reject(err);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
server.once('error', onError);
|
|
68
|
+
server.listen(port, host, () => {
|
|
69
|
+
server.removeListener('error', onError);
|
|
70
|
+
const addr = server.address();
|
|
71
|
+
resolve(typeof addr === 'object' && addr ? addr.port : port);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Start the Form server for `schema`. Binds 127.0.0.1 only; falls back off an occupied port.
|
|
77
|
+
* Resolves once listening, with the bound URL and a `close()` that tears down cleanly
|
|
78
|
+
* (ends any SSE stream and kills a running child).
|
|
79
|
+
*/
|
|
80
|
+
export async function startServer(opts, deps = {}) {
|
|
81
|
+
const host = opts.host ?? DEFAULT_HOST;
|
|
82
|
+
const controller = deps.controller ?? new RunController(deps.spawnFn);
|
|
83
|
+
const page = renderPage(opts.schema, CLIENT_SCRIPT);
|
|
84
|
+
let boundPort = 0;
|
|
85
|
+
// The single active SSE response; the run's output sink and lifecycle are tied to it.
|
|
86
|
+
let sseRes = null;
|
|
87
|
+
/** CSRF fail-closed: the Origin must be present AND match our own bound host:port. */
|
|
88
|
+
function originAllowed(req) {
|
|
89
|
+
const origin = req.headers.origin;
|
|
90
|
+
if (typeof origin !== 'string' || origin.length === 0)
|
|
91
|
+
return false;
|
|
92
|
+
// Accept our own bound host and the 127.0.0.1/localhost alias for it.
|
|
93
|
+
const allowed = new Set([`http://${host}:${boundPort}`, `http://localhost:${boundPort}`, `http://127.0.0.1:${boundPort}`]);
|
|
94
|
+
return allowed.has(origin);
|
|
95
|
+
}
|
|
96
|
+
const server = createServer((req, res) => {
|
|
97
|
+
handle(req, res).catch(() => {
|
|
98
|
+
if (!res.headersSent)
|
|
99
|
+
sendText(res, 500, 'instagui: internal error');
|
|
100
|
+
else
|
|
101
|
+
res.end();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
async function handle(req, res) {
|
|
105
|
+
const method = req.method ?? 'GET';
|
|
106
|
+
const url = (req.url ?? '/').split('?')[0];
|
|
107
|
+
if (method === 'GET' && url === '/') {
|
|
108
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
109
|
+
res.end(page);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (method === 'POST' && url === '/preview') {
|
|
113
|
+
const raw = await readBody(req);
|
|
114
|
+
if (raw === null)
|
|
115
|
+
return sendText(res, 413, 'body too large');
|
|
116
|
+
let state;
|
|
117
|
+
try {
|
|
118
|
+
state = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return sendText(res, 400, 'invalid JSON');
|
|
122
|
+
}
|
|
123
|
+
const { args, preview } = compose(opts.schema, state ?? {});
|
|
124
|
+
return sendJson(res, 200, { args, preview });
|
|
125
|
+
}
|
|
126
|
+
if (method === 'GET' && url === '/events') {
|
|
127
|
+
res.writeHead(200, {
|
|
128
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
129
|
+
'cache-control': 'no-cache, no-transform',
|
|
130
|
+
connection: 'keep-alive',
|
|
131
|
+
});
|
|
132
|
+
res.write(': connected\n\n');
|
|
133
|
+
// Replace any prior stream; the newest tab owns the run.
|
|
134
|
+
if (sseRes && sseRes !== res)
|
|
135
|
+
sseRes.end();
|
|
136
|
+
sseRes = res;
|
|
137
|
+
req.on('close', () => {
|
|
138
|
+
if (sseRes === res) {
|
|
139
|
+
sseRes = null;
|
|
140
|
+
// Disconnect (tab close/reload) must not leave an orphan child (AD-5).
|
|
141
|
+
controller.stop();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (method === 'POST' && url === '/run') {
|
|
147
|
+
if (!originAllowed(req))
|
|
148
|
+
return sendText(res, 403, 'forbidden: bad Origin');
|
|
149
|
+
const sink = sseRes;
|
|
150
|
+
if (!sink)
|
|
151
|
+
return sendText(res, 428, 'open the event stream first');
|
|
152
|
+
const raw = await readBody(req);
|
|
153
|
+
if (raw === null)
|
|
154
|
+
return sendText(res, 413, 'body too large');
|
|
155
|
+
let state;
|
|
156
|
+
try {
|
|
157
|
+
state = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return sendText(res, 400, 'invalid JSON');
|
|
161
|
+
}
|
|
162
|
+
const { args } = compose(opts.schema, state ?? {});
|
|
163
|
+
const runSink = {
|
|
164
|
+
out: (chunk) => sseWrite(sink, 'out', chunk),
|
|
165
|
+
end: (result) => sseWrite(sink, 'exit', result),
|
|
166
|
+
};
|
|
167
|
+
const outcome = controller.start(opts.schema.tool, args, runSink);
|
|
168
|
+
if (!outcome.ok)
|
|
169
|
+
return sendText(res, 409, outcome.reason);
|
|
170
|
+
return sendText(res, 202, 'running');
|
|
171
|
+
}
|
|
172
|
+
if (method === 'POST' && url === '/stop') {
|
|
173
|
+
if (!originAllowed(req))
|
|
174
|
+
return sendText(res, 403, 'forbidden: bad Origin');
|
|
175
|
+
const killed = controller.stop();
|
|
176
|
+
return sendText(res, 200, killed ? 'stopping' : 'nothing to stop');
|
|
177
|
+
}
|
|
178
|
+
return sendText(res, 404, 'not found');
|
|
179
|
+
}
|
|
180
|
+
/** Serialize a value as one SSE event. JSON-encoded data stays single-line even with
|
|
181
|
+
* embedded newlines, so the framing is never broken by tool output. */
|
|
182
|
+
function sseWrite(res, event, data) {
|
|
183
|
+
if (res.writableEnded)
|
|
184
|
+
return;
|
|
185
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
186
|
+
}
|
|
187
|
+
boundPort = await listenWithFallback(server, opts.port ?? DEFAULT_PORT, host);
|
|
188
|
+
const url = `http://${host}:${boundPort}/`;
|
|
189
|
+
return {
|
|
190
|
+
url,
|
|
191
|
+
host,
|
|
192
|
+
port: boundPort,
|
|
193
|
+
close() {
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
controller.stop();
|
|
196
|
+
if (sseRes) {
|
|
197
|
+
sseRes.end();
|
|
198
|
+
sseRes = null;
|
|
199
|
+
}
|
|
200
|
+
server.close(() => resolve());
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// DEV/TEST-ONLY extraction engine. Shells out to headless Claude Code (`claude -p`,
|
|
2
|
+
// subscription-authenticated) instead of the Anthropic API, so extraction can be
|
|
3
|
+
// exercised without an ANTHROPIC_API_KEY / API credits.
|
|
4
|
+
//
|
|
5
|
+
// It implements the SAME CompleteFn seam as shared/claude.ts and returns raw JSON text —
|
|
6
|
+
// the caller (core/extract.ts) still runs Schema.parse() + one-retry + debug-file, so the
|
|
7
|
+
// validation pipeline is identical to the SDK path.
|
|
8
|
+
//
|
|
9
|
+
// NOT a published requirement and NOT for user docs: it is inert unless INSTAGUI_ENGINE is
|
|
10
|
+
// set to "claude-code", the SDK path remains primary/default, and it depends on a `claude`
|
|
11
|
+
// binary that end users are not expected to have.
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
|
|
14
|
+
/** Map an API model id to a Claude Code `--model` alias where possible; otherwise pass it
|
|
15
|
+
* through. Subscription auth may override the selection — reported as a divergence. */
|
|
16
|
+
function claudeCodeModel(model) {
|
|
17
|
+
if (model.includes('haiku'))
|
|
18
|
+
return 'haiku';
|
|
19
|
+
if (model.includes('sonnet'))
|
|
20
|
+
return 'sonnet';
|
|
21
|
+
if (model.includes('opus'))
|
|
22
|
+
return 'opus';
|
|
23
|
+
return model;
|
|
24
|
+
}
|
|
25
|
+
/** Compose the single prompt piped to `claude -p`. Claude Code has no server-side schema
|
|
26
|
+
* enforcement, so the exact JSON Schema (derived from the same zod object) is appended
|
|
27
|
+
* with a JSON-only instruction. */
|
|
28
|
+
function composePrompt(req) {
|
|
29
|
+
const fmt = zodOutputFormat(req.outputSchema);
|
|
30
|
+
const jsonSchema = JSON.stringify(fmt.schema, null, 2);
|
|
31
|
+
return (`${req.system}\n\n${req.user}\n\n` +
|
|
32
|
+
`Respond with ONLY a single JSON object that conforms to this JSON Schema. ` +
|
|
33
|
+
`No markdown, no code fences, no commentary before or after.\n\n` +
|
|
34
|
+
`JSON Schema:\n${jsonSchema}`);
|
|
35
|
+
}
|
|
36
|
+
/** Pull the JSON object out of Claude Code's stdout. Transport-layer normalization only
|
|
37
|
+
* (fences / surrounding prose); it does NOT validate — that stays in the shared pipeline
|
|
38
|
+
* so malformed output still flows through retry + debug-file. */
|
|
39
|
+
export function extractJsonText(stdout) {
|
|
40
|
+
let s = stdout.trim();
|
|
41
|
+
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
42
|
+
if (fence?.[1])
|
|
43
|
+
s = fence[1].trim();
|
|
44
|
+
const first = s.indexOf('{');
|
|
45
|
+
const last = s.lastIndexOf('}');
|
|
46
|
+
if (first !== -1 && last > first)
|
|
47
|
+
return s.slice(first, last + 1);
|
|
48
|
+
return s; // let JSON.parse fail downstream → retry → debug-file
|
|
49
|
+
}
|
|
50
|
+
function runClaude(args, stdin, timeoutMs) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
// shell:true so a Windows `claude.cmd` shim resolves via PATHEXT. Args are simple
|
|
53
|
+
// flags (no untrusted values); the prompt goes over stdin, never the command line.
|
|
54
|
+
const child = spawn('claude', args, { shell: true });
|
|
55
|
+
let stdout = '';
|
|
56
|
+
let stderr = '';
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
child.kill();
|
|
59
|
+
reject(new Error(`claude -p timed out after ${timeoutMs}ms`));
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
child.stdout.on('data', (d) => (stdout += d.toString('utf8')));
|
|
62
|
+
child.stderr.on('data', (d) => (stderr += d.toString('utf8')));
|
|
63
|
+
child.on('error', (err) => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
reject(err);
|
|
66
|
+
});
|
|
67
|
+
child.on('close', (code) => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve({ stdout, stderr, code });
|
|
70
|
+
});
|
|
71
|
+
child.stdin.write(stdin);
|
|
72
|
+
child.stdin.end();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** CompleteFn implemented over `claude -p`. Dev/test only. */
|
|
76
|
+
export const completeViaClaudeCode = async (req) => {
|
|
77
|
+
const prompt = composePrompt(req);
|
|
78
|
+
const args = ['-p', '--model', claudeCodeModel(req.model)];
|
|
79
|
+
const { stdout, stderr, code } = await runClaude(args, prompt, 180_000);
|
|
80
|
+
if (code !== 0) {
|
|
81
|
+
throw new Error(`claude -p exited with code ${code}: ${stderr.trim() || '(no stderr)'}`);
|
|
82
|
+
}
|
|
83
|
+
const json = extractJsonText(stdout);
|
|
84
|
+
if (json.trim().length === 0) {
|
|
85
|
+
throw new Error('claude -p returned empty output.');
|
|
86
|
+
}
|
|
87
|
+
return json;
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=claude-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/shared/claude-code.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// AD-3 — a instagui-agnostic Claude client: prompt + output shape in, raw JSON text out.
|
|
2
|
+
// It knows nothing about Tools, Schemas, or Forms — it takes any zod object as the
|
|
3
|
+
// output shape and returns the model's raw JSON string. Callers run their own
|
|
4
|
+
// validation (core does Schema.parse), which keeps the invalid raw text available for
|
|
5
|
+
// debugging when validation fails.
|
|
6
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
7
|
+
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
|
|
8
|
+
/**
|
|
9
|
+
* Send one completion and return the raw assistant text (expected to be JSON, but
|
|
10
|
+
* NOT validated here). Throws if the response carries no text block at all.
|
|
11
|
+
*
|
|
12
|
+
* @param client optional injected client; defaults to a real Anthropic() which reads
|
|
13
|
+
* ANTHROPIC_API_KEY from the environment.
|
|
14
|
+
*/
|
|
15
|
+
export async function complete(req, client) {
|
|
16
|
+
const anthropic = client ?? new Anthropic();
|
|
17
|
+
const message = await anthropic.messages.create({
|
|
18
|
+
model: req.model,
|
|
19
|
+
max_tokens: req.maxTokens ?? 16000,
|
|
20
|
+
system: req.system,
|
|
21
|
+
messages: [{ role: 'user', content: req.user }],
|
|
22
|
+
output_config: { format: zodOutputFormat(req.outputSchema) },
|
|
23
|
+
});
|
|
24
|
+
const text = message.content
|
|
25
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
26
|
+
.map((b) => b.text)
|
|
27
|
+
.join('');
|
|
28
|
+
if (text.trim().length === 0) {
|
|
29
|
+
throw new Error('Claude returned no text content.');
|
|
30
|
+
}
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=claude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/shared/claude.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,mFAAmF;AACnF,8EAA8E;AAC9E,sFAAsF;AACtF,mCAAmC;AACnC,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAuBhE;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAsB,EAAE,MAAqB;IAC1E,MAAM,SAAS,GAAiB,MAAM,IAAK,IAAI,SAAS,EAA8B,CAAC;IAEvF,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC9C,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,UAAU,EAAE,GAAG,CAAC,SAAS,IAAI,KAAK;QAClC,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;QAC/C,aAAa,EAAE,EAAE,MAAM,EAAE,eAAe,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE;KAC7D,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO;SACzB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC;SAC9D,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAc,CAAC;SAC5B,IAAI,CAAC,EAAE,CAAC,CAAC;IAEZ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// AD-3 — shared/ is instagui-agnostic. Config is limited to the two things the whole
|
|
2
|
+
// program shares: the API key and the on-disk data dir. No Tool/Schema/Form concepts.
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
/** The env var carrying the Anthropic key. Never logged or echoed. */
|
|
6
|
+
export const API_KEY_ENV = 'ANTHROPIC_API_KEY';
|
|
7
|
+
/** True when a key is present in the environment. Does not read or return its value. */
|
|
8
|
+
export function hasApiKey() {
|
|
9
|
+
const v = process.env[API_KEY_ENV];
|
|
10
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
11
|
+
}
|
|
12
|
+
/** The per-user data directory (`~/.instagui`). Used by the cache in Epic 2; defined here
|
|
13
|
+
* so the boundary lives in one place. */
|
|
14
|
+
export function instaguiDir() {
|
|
15
|
+
return path.join(os.homedir(), '.instagui');
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,sFAAsF;AACtF,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,sEAAsE;AACtE,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAE/C,wFAAwF;AACxF,MAAM,UAAU,SAAS;IACvB,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACnC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AACtD,CAAC;AAED;0CAC0C;AAC1C,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;AAC9C,CAAC"}
|