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
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
|
+

|
|
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":"AAAA,iFAAiF;AACjF,qFAAqF;AACrF,yFAAyF;AACzF,uBAAuB;AACvB,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,8CAA8C;AAC9C,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC,CAAC,MAAM;AAElD,MAAM,gBAAgB,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;AAuCxD;oFACoF;AACpF,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,CAAC,GAAG,IAAI,MAAM,EAAE,GAAG,IAAI,MAAM,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,sFAAsF;AACtF,SAAS,OAAO,CAAC,CAAa;IAC5B,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACtE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACtB,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,KAAK,CAAC;IAChC,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,yFAAyF,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9G,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;kBACkB;AAClB,MAAM,UAAU,kBAAkB,CAAC,CAAS;IAC1C,OAAO,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,OAAuB,EAAE;IACvE,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC;IACtC,MAAM,MAAM,GAAc;QACxB,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,kBAAkB;QAC/C,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,iBAAiB;KAC7C,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC;IACjD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAE9B,IAAI,WAAW,GAAkB,IAAI,CAAC;IACtC,IAAI,UAAU,GAAG,EAAE,CAAC;IAEpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;QAEzB,gFAAgF;QAChF,6DAA6D;QAC7D,IAAI,OAAmB,CAAC;QACxB,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;YACzB,IAAI,eAAe,GAAsB,IAAI,CAAC;YAC9C,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACrB,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;gBACrC,IAAI,CAAC,CAAC,UAAU,EAAE,IAAI,KAAK,QAAQ;oBAAE,SAAS;gBAC9C,WAAW,GAAG,CAAC,CAAC;gBAChB,eAAe,GAAG,CAAC,CAAC;gBACpB,MAAM;YACR,CAAC;YACD,IAAI,WAAW,KAAK,IAAI,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;gBACrD,MAAM,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC;YACpC,CAAC;YACD,OAAO,GAAG,eAAe,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,OAAO,GAAG,MAAM,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACjD,CAAC;QAED,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,UAAU,GAAG,gBAAgB,OAAO,CAAC,UAAU,CAAC,IAAI,IAAI,SAAS,GAAG,CAAC;YACrE,SAAS;QACX,CAAC;QACD,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,UAAU,GAAG,mBAAmB,MAAM,CAAC,SAAS,IAAI,CAAC;YACrD,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACpD,CAAC;QACD,UAAU,GAAG,OAAO,CAAC,MAAM;YACzB,CAAC,CAAC,gDAAgD;YAClD,CAAC,CAAC,wBAAwB,OAAO,CAAC,IAAI,GAAG,CAAC;IAC9C,CAAC;IAED,yBAAyB;IACzB,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAChD,IAAI,OAAO;YAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC3D,CAAC;IAED,MAAM,IAAI,WAAW,CAAC,IAAI,EAAE,UAAU,IAAI,sBAAsB,CAAC,CAAC;AACpE,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,IAAY,EAAE,GAAU,EAAE,MAAiB;IAC/D,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE;QACjC,GAAG,MAAM;QACT,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE;KACvE,CAAC,CAAC;IACH,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACnD,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1C,CAAC;AAED;2CAC2C;AAC3C,SAAS,UAAU,CAAC,GAAW,EAAE,IAAc,EAAE,MAAiB;IAChE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAEtF,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,QAAQ,GAAG,IAAI,CAAC;YAChB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;QAErB,MAAM,MAAM,GAAG,CAAC,IAAmB,EAAE,UAAkC,EAAE,EAAE;YACzE,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,CAAC;gBACN,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC3C,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC3C,IAAI;gBACJ,QAAQ;gBACR,MAAM;gBACN,UAAU;aACX,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,MAAM,MAAM,GAAG,CAAC,IAAc,EAAE,KAAa,EAAE,EAAE;YAC/C,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC;YAC1C,IAAI,SAAS,IAAI,CAAC;gBAAE,OAAO;YAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAC9E,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjB,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC;YACtB,IAAI,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBAC7B,MAAM,GAAG,IAAI,CAAC;gBACd,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CAAC;QAEF,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACxD,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACxD,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAA0B,CAAC,CAAC,CAAC;QACnE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC"}
|