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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/dist/cli/index.js +187 -0
  4. package/dist/cli/index.js.map +1 -0
  5. package/dist/core/bundled.js +29 -0
  6. package/dist/core/bundled.js.map +1 -0
  7. package/dist/core/cache.js +41 -0
  8. package/dist/core/cache.js.map +1 -0
  9. package/dist/core/capture.js +161 -0
  10. package/dist/core/capture.js.map +1 -0
  11. package/dist/core/compose.js +66 -0
  12. package/dist/core/compose.js.map +1 -0
  13. package/dist/core/errors.js +40 -0
  14. package/dist/core/errors.js.map +1 -0
  15. package/dist/core/extract.js +78 -0
  16. package/dist/core/extract.js.map +1 -0
  17. package/dist/core/golden.js +44 -0
  18. package/dist/core/golden.js.map +1 -0
  19. package/dist/core/onboarding.js +19 -0
  20. package/dist/core/onboarding.js.map +1 -0
  21. package/dist/core/override.js +26 -0
  22. package/dist/core/override.js.map +1 -0
  23. package/dist/core/resolve.js +22 -0
  24. package/dist/core/resolve.js.map +1 -0
  25. package/dist/core/schema-file.js +34 -0
  26. package/dist/core/schema-file.js.map +1 -0
  27. package/dist/core/schema.js +58 -0
  28. package/dist/core/schema.js.map +1 -0
  29. package/dist/server/browser.js +38 -0
  30. package/dist/server/browser.js.map +1 -0
  31. package/dist/server/client.js +146 -0
  32. package/dist/server/client.js.map +1 -0
  33. package/dist/server/page.js +172 -0
  34. package/dist/server/page.js.map +1 -0
  35. package/dist/server/run.js +71 -0
  36. package/dist/server/run.js.map +1 -0
  37. package/dist/server/server.js +205 -0
  38. package/dist/server/server.js.map +1 -0
  39. package/dist/shared/claude-code.js +89 -0
  40. package/dist/shared/claude-code.js.map +1 -0
  41. package/dist/shared/claude.js +33 -0
  42. package/dist/shared/claude.js.map +1 -0
  43. package/dist/shared/config.js +17 -0
  44. package/dist/shared/config.js.map +1 -0
  45. package/dist/shared/engine.js +17 -0
  46. package/dist/shared/engine.js.map +1 -0
  47. package/package.json +61 -0
  48. package/schemas/README.md +32 -0
  49. package/schemas/ffmpeg.json +564 -0
  50. package/schemas/pandoc.json +277 -0
  51. package/schemas/yt-dlp.json +446 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omar
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,144 @@
1
+ # instagui
2
+
3
+ **Any CLI. Instant GUI. One command.**
4
+
5
+ Turn any command-line tool into a clean local web form — no config, no code changes to the tool.
6
+
7
+ <!-- TODO(pre-launch): record and drop the money-demo GIF here.
8
+ ~10s: `npx instagui ffmpeg` → browser opens → fill a couple fields → Run → output streams.
9
+ Save as docs/demo.gif and it renders below. -->
10
+ ![instagui demo — npx instagui ffmpeg opens a web form, click Run, output streams](docs/demo.gif)
11
+
12
+ ```sh
13
+ npx instagui ffmpeg
14
+ ```
15
+
16
+ That's it. instagui reads the tool's `--help`, turns it into a web form, opens your browser, and
17
+ (when you click **Run**) executes the command locally and streams the output back — while always
18
+ showing you the exact command it will run, so it teaches you the CLI instead of hiding it.
19
+
20
+ ---
21
+
22
+ ## Why
23
+
24
+ Thousands of powerful CLIs (ffmpeg, pandoc, yt-dlp, curl, imagemagick…) are unfriendly to anyone
25
+ who doesn't live in a terminal — and even experts re-read man pages to recall flag syntax. Tools
26
+ like Gooey require the tool's *author* to change their code. instagui needs nothing from the tool: it
27
+ parses the tool's own `--help` text with AI into a structured schema and renders that as a form.
28
+
29
+ ## Quick start
30
+
31
+ The three demo tools ship with **bundled schemas**, so they work instantly with **no API key**:
32
+
33
+ ```sh
34
+ npx instagui ffmpeg # video/audio transcoding
35
+ npx instagui yt-dlp # download media
36
+ npx instagui pandoc # convert documents
37
+ ```
38
+
39
+ For any *other* tool, instagui extracts the schema on first run using the Claude API (see
40
+ [How it stays free](#how-it-stays-free)):
41
+
42
+ ```sh
43
+ export ANTHROPIC_API_KEY=sk-ant-... # POSIX
44
+ $env:ANTHROPIC_API_KEY="sk-ant-..." # Windows / PowerShell
45
+ npx instagui curl
46
+ ```
47
+
48
+ Get a key at <https://console.anthropic.com>. The first extraction is cached, so every launch
49
+ after that is instant and free.
50
+
51
+ ## How it works
52
+
53
+ 1. **Capture** — run `<tool> --help` (falling back to `-h`, `help`, then the man page), reading
54
+ both stdout and stderr, under a timeout and size cap so a misbehaving tool can't hang the launch.
55
+ 2. **Extract** — send the help text to the Claude API (`claude-haiku-4-5`) and get back a validated
56
+ JSON schema of the tool's options (name, flag, type, description, enum values, required, grouping)
57
+ plus positional arguments. Invalid output is retried once, then fails clearly.
58
+ 3. **Serve** — render the schema as a single-page form on `http://127.0.0.1`, grouped, with the
59
+ right control per type (checkbox / dropdown / number / text).
60
+ 4. **Preview** — show the exact command as you edit the form, one-click copyable.
61
+ 5. **Run** — execute the command with `spawn` (arguments array, never a shell string) and stream
62
+ stdout/stderr live into the page until it exits.
63
+
64
+ ## How it stays free
65
+
66
+ instagui resolves a schema in this order, and only the last step costs an API call:
67
+
68
+ | Precedence | Source | Needs a key? |
69
+ | --- | --- | --- |
70
+ | 1 | `--schema <file>` override you supply | no |
71
+ | 2 | Your cache in `~/.instagui/` (written on first extraction) | no |
72
+ | 3 | Bundled schemas shipped with the package (ffmpeg, yt-dlp, pandoc) | no |
73
+ | 4 | Fresh extraction via the Claude API | **yes** |
74
+
75
+ So the demo tools are free forever, any tool you've used once is free forever after, and you only
76
+ need a key the first time you point instagui at a brand-new tool. A friendly message tells you exactly
77
+ what to do if a key is needed and missing — you're never dropped into a stack trace.
78
+
79
+ - `--refresh` re-extracts and overwrites your cache entry.
80
+ - `--schema ./mytool.json` uses a hand-tuned schema and skips capture **and** the AI entirely.
81
+
82
+ ## Usage
83
+
84
+ ```
85
+ instagui <tool> resolve <tool>'s Schema and serve the Form (auto-opens the browser)
86
+ instagui <tool> --print resolve and print the Schema JSON instead of serving
87
+ instagui <tool> --schema <path> use a hand-supplied Schema file (no capture, no AI)
88
+ instagui <tool> --refresh ignore cache + bundled and re-extract fresh
89
+ instagui <tool> --help-file <p> extract from a captured help-text file
90
+ <tool> --help | instagui <tool> or pipe help text on stdin
91
+
92
+ --port <n> preferred port for the Form server (default 5177; falls back if busy)
93
+ --no-open do not auto-open the browser (still prints the URL)
94
+ --model <id> extraction model (default: claude-haiku-4-5)
95
+ -v, --version print the instagui version
96
+ -h, --help show help
97
+ ```
98
+
99
+ ## Security / threat model
100
+
101
+ instagui is a **local, single-user tool**. Be clear-eyed about what it does:
102
+
103
+ - **It runs commands you compose.** The whole point is to execute a real CLI with the arguments you
104
+ set in the form. Treat the form like your own terminal — don't run something you wouldn't type.
105
+ - **The exact command is always shown before you Run it.** No hidden arguments; preview is generated
106
+ from the *same* argument array that Run executes, so what you see is what runs.
107
+ - **Arguments are passed as an array to `spawn`, never concatenated into a shell.** A value
108
+ containing spaces, quotes, `;`, or `&&` is passed verbatim as a single argument — there is no
109
+ shell to interpret it, so form input can't inject extra commands.
110
+ - **The server binds `127.0.0.1` only.** It is not reachable from your network.
111
+ - **State-changing requests fail closed.** `POST /run` and `POST /stop` require a matching `Origin`
112
+ header; a missing or foreign origin is rejected (CSRF protection). Exactly one run at a time, and
113
+ closing the tab (dropping the stream) kills the child process — no orphans.
114
+ - **Your API key is never logged, echoed, or embedded in any served page.** The only data that
115
+ leaves your machine is the tool's help text, sent to the Claude API for extraction. No telemetry.
116
+
117
+ ## Contribute a schema
118
+
119
+ Want a tool to work keyless for everyone, like the demo tools do? Bundled schemas live in
120
+ [`schemas/`](schemas/) and are generated from captured `--help` fixtures:
121
+
122
+ 1. Capture the tool's help into `test/fixtures/<tool>-help.txt`.
123
+ 2. Add the tool to `scripts/gen-bundled-schemas.ts`.
124
+ 3. Regenerate with your key: `ANTHROPIC_API_KEY=sk-ant-... npx tsx scripts/gen-bundled-schemas.ts`
125
+ (a hallucination guard + golden check run before anything is written).
126
+ 4. Open a PR with the fixture and the generated `schemas/<tool>.json`.
127
+
128
+ Each generated schema is validated so every flag appears verbatim in the source help text — no
129
+ hallucinated options. See [`schemas/README.md`](schemas/README.md) for provenance details.
130
+
131
+ ## Requirements
132
+
133
+ - **Node.js ≥ 22**
134
+ - An `ANTHROPIC_API_KEY` only for extracting a tool that isn't bundled or cached.
135
+
136
+ ## Non-goals (v0.1)
137
+
138
+ Deliberately out of scope to keep it small and sharp: interactive/TUI programs (vim, top, REPLs);
139
+ subcommand trees (flat tools only — `git commit` vs `git push` is v0.2); native file-picker dialogs;
140
+ a hosted version, auth, telemetry, or a plugin system; a local-LLM option (contributions welcome).
141
+
142
+ ## License
143
+
144
+ MIT © Omar
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // cli/index.ts — `instagui <tool>` resolves a validated Schema and serves it as a local web Form
3
+ // (Epic 3). `--print` keeps the Epic 1/2 behaviour of emitting the Schema JSON and exiting.
4
+ //
5
+ // Resolution precedence (Epic 2): --schema override > user cache (~/.instagui) > bundled
6
+ // schemas > fresh extraction. Only the extraction tier captures help or calls the AI, and
7
+ // only it needs an API key — so the demo tools, a cache hit, or --schema never prompt for
8
+ // one. A fresh extraction is written back to the user cache so the next run is free.
9
+ //
10
+ // Within extraction, help source precedence is --help-file > piped stdin > live capture.
11
+ // Maps core errors to the exit-code contract (0 ok · 2 known precondition · 1 unexpected).
12
+ import { readFileSync } from 'node:fs';
13
+ import { parseArgs } from 'node:util';
14
+ import { extractSchema } from '../core/extract.js';
15
+ import { captureHelp } from '../core/capture.js';
16
+ import { resolveSchema } from '../core/resolve.js';
17
+ import { readCache, writeCache } from '../core/cache.js';
18
+ import { readBundled } from '../core/bundled.js';
19
+ import { loadOverrideSchema } from '../core/override.js';
20
+ import { apiKeyOnboardingError } from '../core/onboarding.js';
21
+ import { PreconditionError } from '../core/errors.js';
22
+ import { hasApiKey } from '../shared/config.js';
23
+ import { activeEngineName, ENGINE_ENV } from '../shared/engine.js';
24
+ import { startServer } from '../server/server.js';
25
+ import { openBrowser } from '../server/browser.js';
26
+ const usingClaudeCode = process.env[ENGINE_ENV] === 'claude-code';
27
+ /** Our own version, read from the shipped package.json (works from both dist/ and src/ — the
28
+ * relative path to the package root is the same). We parse everyone else's help text; our own
29
+ * --version and --help should be exemplary. */
30
+ function readVersion() {
31
+ try {
32
+ const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
33
+ return pkg.version ?? '0.0.0';
34
+ }
35
+ catch {
36
+ return '0.0.0';
37
+ }
38
+ }
39
+ const USAGE = `instagui <tool> — turn a CLI tool into a local web Form. Resolves a validated option
40
+ Schema, serves it as a single-page Form, and (on Run) executes the command and streams output.
41
+
42
+ Usage:
43
+ instagui <tool> resolve <tool>'s Schema and serve the Form (auto-opens browser)
44
+ instagui <tool> --print resolve and print the Schema JSON instead of serving
45
+ instagui <tool> --schema <path> use a hand-supplied Schema file (no capture, no AI)
46
+ instagui <tool> --refresh ignore cache + bundled and re-extract fresh
47
+ instagui <tool> --help-file <path> extract from a captured help-text file
48
+ <tool> --help | instagui <tool> or pipe help text on stdin
49
+
50
+ Resolution precedence: --schema > user cache (~/.instagui) > bundled schemas > fresh extraction.
51
+ The demo tools (ffmpeg, yt-dlp, pandoc) ship bundled Schemas, so they work with no API key.
52
+
53
+ Options:
54
+ --print print the resolved Schema JSON to stdout and exit (no server)
55
+ --port <n> preferred port for the Form server (default 5177; falls back if busy)
56
+ --no-open do not auto-open the browser (still prints the URL)
57
+ --schema <path> use this Schema file directly (top precedence)
58
+ --refresh bypass cache + bundled and re-extract, overwriting the cache
59
+ --help-file <path> read help text from a file instead of capturing (extraction only)
60
+ --capture force live capture (ignore piped stdin)
61
+ --model <id> extraction model (default: claude-haiku-4-5)
62
+ -v, --version print the instagui version and exit
63
+ -h, --help show this message
64
+
65
+ Examples:
66
+ instagui ffmpeg open a Form for ffmpeg (bundled — no key needed)
67
+ instagui yt-dlp --no-open serve without launching a browser; print the URL
68
+ instagui mytool --print just resolve and print the Schema as JSON
69
+ instagui mytool --schema ./mytool.json use a hand-tuned Schema, skip capture + AI
70
+
71
+ The server binds 127.0.0.1 only. Extraction needs ANTHROPIC_API_KEY (https://console.anthropic.com).
72
+ Exit codes: 0 ok · 2 known failure · 1 unexpected.`;
73
+ async function readStdin() {
74
+ if (process.stdin.isTTY)
75
+ return '';
76
+ const chunks = [];
77
+ for await (const chunk of process.stdin)
78
+ chunks.push(chunk);
79
+ return Buffer.concat(chunks).toString('utf8');
80
+ }
81
+ /** Resolve help text by precedence: --help-file > piped stdin > live capture.
82
+ * `--capture` forces live capture past the stdin heuristic. */
83
+ async function resolveHelpText(tool, helpFile, forceCapture) {
84
+ if (helpFile) {
85
+ return { helpText: readFileSync(helpFile, 'utf8'), source: `file:${helpFile}` };
86
+ }
87
+ if (!forceCapture && !process.stdin.isTTY) {
88
+ return { helpText: await readStdin(), source: 'stdin' };
89
+ }
90
+ const { helpText, method } = await captureHelp(tool);
91
+ return { helpText, source: `capture:${method}` };
92
+ }
93
+ async function main() {
94
+ const { values, positionals } = parseArgs({
95
+ allowPositionals: true,
96
+ options: {
97
+ schema: { type: 'string' },
98
+ refresh: { type: 'boolean' },
99
+ 'help-file': { type: 'string' },
100
+ capture: { type: 'boolean' },
101
+ model: { type: 'string' },
102
+ print: { type: 'boolean' },
103
+ port: { type: 'string' },
104
+ 'no-open': { type: 'boolean' },
105
+ version: { type: 'boolean', short: 'v' },
106
+ help: { type: 'boolean', short: 'h' },
107
+ },
108
+ });
109
+ if (values.version) {
110
+ console.log(readVersion());
111
+ return 0;
112
+ }
113
+ if (values.help || positionals.length === 0) {
114
+ console.log(USAGE);
115
+ return values.help ? 0 : 2;
116
+ }
117
+ const tool = positionals[0];
118
+ // The extraction tier: capture help, require a key (unless the dev engine is active), and
119
+ // run the AI. Only reached when override/cache/bundled all miss (or --refresh is set), so
120
+ // the key requirement surfaces exactly when it is genuinely needed.
121
+ const extract = async () => {
122
+ const { helpText, source } = await resolveHelpText(tool, values['help-file'], values.capture ?? false);
123
+ console.error(`instagui: help from ${source}`);
124
+ // captureHelp throws on empty/no-help; only file/stdin can reach here empty.
125
+ if (helpText.trim().length === 0) {
126
+ throw new PreconditionError(`No help text provided for "${tool}" (${source}). Pass --help-file <path> or pipe the tool's help output on stdin.`);
127
+ }
128
+ if (!usingClaudeCode && !hasApiKey())
129
+ throw apiKeyOnboardingError();
130
+ if (usingClaudeCode)
131
+ console.error(`instagui: using ${activeEngineName()} extraction engine`);
132
+ const { schema } = await extractSchema(helpText, tool, { model: values.model });
133
+ return schema;
134
+ };
135
+ const result = await resolveSchema({ tool, schemaFile: values.schema, refresh: values.refresh ?? false }, {
136
+ loadOverride: loadOverrideSchema,
137
+ readCache: (t) => readCache(t),
138
+ readBundled: (t) => readBundled(t),
139
+ extract,
140
+ writeCache: (t, s) => writeCache(t, s),
141
+ });
142
+ console.error(`instagui: schema from ${result.source}`);
143
+ if (result.cachedTo)
144
+ console.error(`instagui: cached to ${result.cachedTo}`);
145
+ // --print keeps the Epic 1/2 behaviour: emit the Schema JSON and exit (no server).
146
+ if (values.print) {
147
+ console.log(JSON.stringify(result.schema, null, 2));
148
+ return 0;
149
+ }
150
+ // Default (Epic 3): serve the Schema as a Form and hold the process open until Ctrl-C.
151
+ let port;
152
+ if (values.port !== undefined) {
153
+ port = Number(values.port);
154
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
155
+ throw new PreconditionError(`Invalid --port "${values.port}": expected an integer 0–65535.`);
156
+ }
157
+ }
158
+ const server = await startServer({ schema: result.schema, port });
159
+ console.error(`instagui: serving ${tool} form at ${server.url}`);
160
+ console.log(server.url); // the URL on stdout so it's scriptable / copyable
161
+ if (!values['no-open'])
162
+ openBrowser(server.url);
163
+ console.error('instagui: press Ctrl-C to stop.');
164
+ // Hold the process open; shut the server down cleanly on Ctrl-C.
165
+ await new Promise((resolve) => {
166
+ const shutdown = () => {
167
+ console.error('\ninstagui: shutting down.');
168
+ server.close().finally(resolve);
169
+ };
170
+ process.on('SIGINT', shutdown);
171
+ process.on('SIGTERM', shutdown);
172
+ });
173
+ return 0;
174
+ }
175
+ main()
176
+ .then((code) => process.exit(code))
177
+ .catch((err) => {
178
+ if (err instanceof PreconditionError) {
179
+ console.error(`instagui: ${err.message}`);
180
+ process.exit(err.exitCode);
181
+ }
182
+ // Unexpected: stack to stderr only, exit 1.
183
+ console.error('instagui: unexpected error');
184
+ console.error(err instanceof Error ? err.stack : String(err));
185
+ process.exit(1);
186
+ });
187
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ // Story 2.3 — bundled demo schemas. The published package ships a read-only `schemas/`
2
+ // dir so the demo tools (ffmpeg, yt-dlp, pandoc, …) work with NO API key and NO capture:
3
+ // the `npx instagui ffmpeg` magic isn't gated behind getting a key.
4
+ //
5
+ // This tier is READ-ONLY. It sits below the user cache in precedence (cache wins), and a
6
+ // fresh extraction is never written here — it goes to ~/.instagui (see core/cache.ts). So a
7
+ // user who re-extracts a bundled tool gets their own entry going forward while the shipped
8
+ // fallback stays pristine.
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { readSchemaFile } from './schema-file.js';
12
+ import { toolKey } from './cache.js';
13
+ /** The packaged schemas/ directory, resolved relative to this module so it works both from
14
+ * source (src/core/ → ../../schemas) and from the published tarball (dist/core/ →
15
+ * ../../schemas). Injectable in tests. */
16
+ export function bundledDir() {
17
+ const here = path.dirname(fileURLToPath(import.meta.url));
18
+ return path.join(here, '..', '..', 'schemas');
19
+ }
20
+ /**
21
+ * Load the bundled Schema for `tool`, or null if the tool isn't bundled or the shipped file
22
+ * is somehow unreadable/invalid (fall through to extraction — never crash a launch). Uses
23
+ * the same tool-name keying as the user cache so a bundled file matches a cache lookup.
24
+ */
25
+ export function readBundled(tool, dir = bundledDir()) {
26
+ const result = readSchemaFile(path.join(dir, `${toolKey(tool)}.json`));
27
+ return result.ok ? result.schema : null;
28
+ }
29
+ //# sourceMappingURL=bundled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled.js","sourceRoot":"","sources":["../../src/core/bundled.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,yFAAyF;AACzF,oEAAoE;AACpE,EAAE;AACF,yFAAyF;AACzF,4FAA4F;AAC5F,2FAA2F;AAC3F,2BAA2B;AAC3B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAGrC;;2CAE2C;AAC3C,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,MAAc,UAAU,EAAE;IAClE,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACvE,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,41 @@
1
+ // Story 2.1 — the user schema cache in ~/.instagui. A successful extraction is written here,
2
+ // keyed by tool name, so repeat launches load instantly with zero capture and zero API
3
+ // call. This is the WRITE target of the resolution chain; the packaged schemas/ dir
4
+ // (Story 2.3) is a read-only fallback and is never written here.
5
+ //
6
+ // A corrupt or stale cache file must never crash a launch: readCache returns null on any
7
+ // unreadable/invalid file so the resolver falls through to bundled/extraction.
8
+ import { mkdirSync, writeFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+ import { instaguiDir } from '../shared/config.js';
11
+ import { readSchemaFile } from './schema-file.js';
12
+ /** Reduce a tool name to a filesystem-safe cache key. Keeps word chars, dot and dash
13
+ * (covers "ffmpeg", "yt-dlp", "pandoc"); collapses anything else (path separators, etc.)
14
+ * to "_" so a tool name can never escape the cache dir. */
15
+ export function toolKey(tool) {
16
+ return tool.replace(/[^\w.-]/g, '_');
17
+ }
18
+ /** Absolute path of the cache entry for `tool`. `dir` is injectable for tests. */
19
+ export function cachePath(tool, dir = instaguiDir()) {
20
+ return path.join(dir, `${toolKey(tool)}.json`);
21
+ }
22
+ /**
23
+ * Load a cached Schema for `tool`, or null if there is no usable cache entry (absent,
24
+ * unreadable, non-JSON, or failing Schema.parse). Null means "cache miss" — the caller
25
+ * falls through to the next resolution tier rather than crashing.
26
+ */
27
+ export function readCache(tool, dir = instaguiDir()) {
28
+ const result = readSchemaFile(cachePath(tool, dir));
29
+ return result.ok ? result.schema : null;
30
+ }
31
+ /**
32
+ * Persist a validated Schema to the user cache, creating ~/.instagui if needed. Returns the
33
+ * path written. Overwrites any existing entry (this is how --refresh re-caches).
34
+ */
35
+ export function writeCache(tool, schema, dir = instaguiDir()) {
36
+ mkdirSync(dir, { recursive: true });
37
+ const file = cachePath(tool, dir);
38
+ writeFileSync(file, `${JSON.stringify(schema, null, 2)}\n`, 'utf8');
39
+ return file;
40
+ }
41
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/core/cache.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,uFAAuF;AACvF,oFAAoF;AACpF,iEAAiE;AACjE,EAAE;AACF,yFAAyF;AACzF,+EAA+E;AAC/E,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGlD;;4DAE4D;AAC5D,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;AACvC,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,MAAc,WAAW,EAAE;IACjE,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,MAAc,WAAW,EAAE;IACjE,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,MAAc,EAAE,MAAc,WAAW,EAAE;IAClF,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAClC,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,161 @@
1
+ // core/capture.ts — Story 1.2. Capture a tool's help text via the fallback chain
2
+ // (--help → -h → help → man), reading BOTH stdout and stderr, under a timeout and an
3
+ // output-size cap so a misbehaving tool can't hang the launch. Output feeds the existing
4
+ // extractor unchanged.
5
+ import { spawn } from 'node:child_process';
6
+ import path from 'node:path';
7
+ import { ToolNotFoundError, NoHelpError } from './errors.js';
8
+ /** Pinned capture limits (PRD §9 / spine). */
9
+ export const DEFAULT_TIMEOUT_MS = 10_000;
10
+ export const DEFAULT_MAX_BYTES = 1_000_000; // 1MB
11
+ const DEFAULT_ARG_SETS = [['--help'], ['-h'], ['help']];
12
+ /** Candidate executable names to try. On Windows, resolve extensionless names against
13
+ * common executable extensions (spawn without a shell does not append PATHEXT). */
14
+ function candidates(tool) {
15
+ if (process.platform !== 'win32')
16
+ return [tool];
17
+ if (path.extname(tool))
18
+ return [tool];
19
+ return [`${tool}.exe`, `${tool}.com`, tool];
20
+ }
21
+ /** Merge a run's streams into one help-text blob (help may land on either stream). */
22
+ function combine(o) {
23
+ const parts = [o.stdout, o.stderr].filter((s) => s.trim().length > 0);
24
+ return parts.join('\n');
25
+ }
26
+ /** Decide whether captured text is real help vs an error about the flag we passed. */
27
+ export function isUsableHelp(text) {
28
+ const t = text.trim();
29
+ if (t.length < 20)
30
+ return false;
31
+ const firstLine = t.split('\n', 1)[0] ?? '';
32
+ if (/\b(unknown|unrecognized|invalid|illegal|no such)\b.*\b(option|flag|argument|command)\b/i.test(firstLine)) {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ /** Remove man's backspace overstrike sequences (bold/underline) so extraction sees
38
+ * plain text. */
39
+ export function stripManFormatting(s) {
40
+ return s.replace(/.\x08/g, '');
41
+ }
42
+ /**
43
+ * Capture help text for `tool`. Throws ToolNotFoundError if the tool isn't on PATH, or
44
+ * NoHelpError if every fallback failed to yield usable help. Both surface as clear
45
+ * messages (exit 2 via PreconditionError) — never a hang.
46
+ */
47
+ export async function captureHelp(tool, opts = {}) {
48
+ const run = opts.runner ?? defaultRun;
49
+ const limits = {
50
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
51
+ maxBytes: opts.maxBytes ?? DEFAULT_MAX_BYTES,
52
+ };
53
+ const argSets = opts.argSets ?? DEFAULT_ARG_SETS;
54
+ const cmds = candidates(tool);
55
+ let resolvedCmd = null;
56
+ let lastReason = '';
57
+ for (let i = 0; i < argSets.length; i++) {
58
+ const args = argSets[i];
59
+ // First probe also resolves which executable name actually runs. If none of the
60
+ // candidates can be spawned at all, the tool is not on PATH.
61
+ let outcome;
62
+ if (resolvedCmd === null) {
63
+ let resolvedOutcome = null;
64
+ for (const c of cmds) {
65
+ const o = await run(c, args, limits);
66
+ if (o.spawnError?.code === 'ENOENT')
67
+ continue;
68
+ resolvedCmd = c;
69
+ resolvedOutcome = o;
70
+ break;
71
+ }
72
+ if (resolvedCmd === null || resolvedOutcome === null) {
73
+ throw new ToolNotFoundError(tool);
74
+ }
75
+ outcome = resolvedOutcome;
76
+ }
77
+ else {
78
+ outcome = await run(resolvedCmd, args, limits);
79
+ }
80
+ if (outcome.spawnError) {
81
+ lastReason = `spawn error (${outcome.spawnError.code ?? 'unknown'})`;
82
+ continue;
83
+ }
84
+ if (outcome.timedOut) {
85
+ lastReason = `timed out after ${limits.timeoutMs}ms`;
86
+ continue;
87
+ }
88
+ const text = combine(outcome);
89
+ if (isUsableHelp(text)) {
90
+ return { helpText: text, method: args.join(' ') };
91
+ }
92
+ lastReason = outcome.capped
93
+ ? 'output cap reached before usable help appeared'
94
+ : `no usable help (exit ${outcome.code})`;
95
+ }
96
+ // Last resort: man page.
97
+ if (opts.tryMan !== false) {
98
+ const manText = await tryMan(tool, run, limits);
99
+ if (manText)
100
+ return { helpText: manText, method: 'man' };
101
+ }
102
+ throw new NoHelpError(tool, lastReason || 'all fallbacks failed');
103
+ }
104
+ async function tryMan(tool, run, limits) {
105
+ const o = await run('man', [tool], {
106
+ ...limits,
107
+ env: { ...process.env, MANPAGER: 'cat', PAGER: 'cat', MANWIDTH: '80' },
108
+ });
109
+ if (o.spawnError || o.timedOut)
110
+ return null;
111
+ const text = stripManFormatting(combine(o)).trim();
112
+ return isUsableHelp(text) ? text : null;
113
+ }
114
+ /** Real runner: spawn (no shell → args-array only), read both streams into a capped
115
+ * buffer, kill on timeout or byte-cap. */
116
+ function defaultRun(cmd, args, limits) {
117
+ return new Promise((resolve) => {
118
+ const child = spawn(cmd, args, { env: limits.env ?? process.env, windowsHide: true });
119
+ const out = [];
120
+ const err = [];
121
+ let total = 0;
122
+ let timedOut = false;
123
+ let capped = false;
124
+ let settled = false;
125
+ const timer = setTimeout(() => {
126
+ timedOut = true;
127
+ child.kill('SIGKILL');
128
+ }, limits.timeoutMs);
129
+ const settle = (code, spawnError) => {
130
+ if (settled)
131
+ return;
132
+ settled = true;
133
+ clearTimeout(timer);
134
+ resolve({
135
+ stdout: Buffer.concat(out).toString('utf8'),
136
+ stderr: Buffer.concat(err).toString('utf8'),
137
+ code,
138
+ timedOut,
139
+ capped,
140
+ spawnError,
141
+ });
142
+ };
143
+ const append = (sink, chunk) => {
144
+ const remaining = limits.maxBytes - total;
145
+ if (remaining <= 0)
146
+ return;
147
+ const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
148
+ sink.push(slice);
149
+ total += slice.length;
150
+ if (total >= limits.maxBytes) {
151
+ capped = true;
152
+ child.kill('SIGKILL');
153
+ }
154
+ };
155
+ child.stdout?.on('data', (c) => append(out, c));
156
+ child.stderr?.on('data', (c) => append(err, c));
157
+ child.on('error', (e) => settle(null, e));
158
+ child.on('close', (code) => settle(code));
159
+ });
160
+ }
161
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/core/capture.ts"],"names":[],"mappings":"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"}