gui4cli 0.0.0 → 0.0.1

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 (46) hide show
  1. package/README.md +74 -2
  2. package/bin/gui4cli.js +16 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +86 -0
  5. package/dist/config/load.d.ts +13 -0
  6. package/dist/config/load.js +66 -0
  7. package/dist/detect/ast.d.ts +6 -0
  8. package/dist/detect/ast.js +62 -0
  9. package/dist/detect/commander.d.ts +2 -0
  10. package/dist/detect/commander.js +45 -0
  11. package/dist/detect/flags.d.ts +25 -0
  12. package/dist/detect/flags.js +77 -0
  13. package/dist/detect/help.d.ts +3 -0
  14. package/dist/detect/help.js +53 -0
  15. package/dist/detect/index.d.ts +2 -0
  16. package/dist/detect/index.js +61 -0
  17. package/dist/detect/jsdoc.d.ts +2 -0
  18. package/dist/detect/jsdoc.js +22 -0
  19. package/dist/detect/yargs.d.ts +2 -0
  20. package/dist/detect/yargs.js +75 -0
  21. package/dist/errors.d.ts +5 -0
  22. package/dist/errors.js +17 -0
  23. package/dist/generate/project.d.ts +10 -0
  24. package/dist/generate/project.js +77 -0
  25. package/dist/generate/window-app.d.ts +2 -0
  26. package/dist/generate/window-app.js +282 -0
  27. package/dist/generate/window.d.ts +9 -0
  28. package/dist/generate/window.js +56 -0
  29. package/dist/last-run/store.d.ts +4 -0
  30. package/dist/last-run/store.js +23 -0
  31. package/dist/resolve/target.d.ts +4 -0
  32. package/dist/resolve/target.js +54 -0
  33. package/dist/run/argv.d.ts +5 -0
  34. package/dist/run/argv.js +59 -0
  35. package/dist/run/kill.d.ts +1 -0
  36. package/dist/run/kill.js +18 -0
  37. package/dist/schema/form.d.ts +139 -0
  38. package/dist/schema/form.js +51 -0
  39. package/dist/window/env.d.ts +1 -0
  40. package/dist/window/env.js +14 -0
  41. package/dist/window/open.d.ts +2 -0
  42. package/dist/window/open.js +44 -0
  43. package/dist/window/resolve.d.ts +6 -0
  44. package/dist/window/resolve.js +49 -0
  45. package/package.json +26 -4
  46. package/bin/argui.js +0 -4
package/README.md CHANGED
@@ -1,5 +1,77 @@
1
- # ArgUI
1
+ # GUI4CLI
2
2
 
3
3
  Turn a Node.js CLI script into a simple desktop form — inputs, a Run button, and live output — without rewriting the script.
4
4
 
5
- **This `0.0.0` package reserves the npm name `gui4cli`.** The CLI is not implemented yet. `npx gui4cli` will only print this notice.
5
+ ## Setup
6
+
7
+ ### Prerequisites
8
+
9
+ - Node.js 20+
10
+ - pnpm 9+
11
+
12
+ ### Installation (this repo)
13
+
14
+ ```bash
15
+ pnpm install
16
+ pnpm test
17
+ pnpm exec tsx src/cli.ts fixtures/resize.js --json
18
+ pnpm dev
19
+ ```
20
+
21
+ `pnpm dev` opens the Commander fixture in a desktop window. The first windowd launch may download the NW.js runtime (~200 MB).
22
+
23
+ If the window flashes and closes, run `pnpm dev` from a normal terminal (not Cursor's JavaScript Debug Terminal). Inspector flags inherited by NW.js will shut the window down.
24
+
25
+ `--json` prints the detected form and exits (no window). Use that in CI or when you cannot open a GUI.
26
+
27
+ `--build` writes a reusable project folder (GUI shell + `package.json`). It does not create an `.exe`, and it does not rewrite the original script.
28
+
29
+ ```bash
30
+ pnpm exec tsx src/cli.ts fixtures/resize.js --build
31
+ pnpm exec tsx src/cli.ts fixtures/resize.js --build --out ./my-resize-gui
32
+ ```
33
+
34
+ Then `npx --yes windowd` inside that folder.
35
+
36
+ ### Try a script
37
+
38
+ ```bash
39
+ pnpm exec tsx src/cli.ts path/to/your-script.js
40
+ pnpm exec tsx src/cli.ts . --entry bin/cli.js
41
+ ```
42
+
43
+ Optional overrides: `gui4cli.config.js` or `gui4cli.json` next to the script.
44
+
45
+ ## Project Structure
46
+
47
+ ```
48
+ src/ TypeScript CLI (detect, schema, window, run)
49
+ fixtures/ Commander + yargs sample scripts
50
+ docs/ GENESIS + Phase 1 brief
51
+ CONTEXT_PROMPT.md
52
+ TODO.md
53
+ ```
54
+
55
+ ## Features
56
+
57
+ | Feature | Description |
58
+ | ------- | ----------- |
59
+ | Detect | Static Commander / yargs, then JSDoc, config, `--help` |
60
+ | Instant GUI | One windowd window, no generated repo required |
61
+ | `--build` | Reusable project folder; run later with `npx windowd` |
62
+ | Run | Spawn the original script; stream stdout/stderr; cancel |
63
+ | Last run | Values stored under `~/.gui4cli/lastrun/` |
64
+
65
+ ## Tech Stack
66
+
67
+ - TypeScript, ESM, pnpm
68
+ - Zod
69
+ - windowd (Node + webview)
70
+ - No database, auth, or LLM
71
+
72
+ ## Documentation
73
+
74
+ - [docs/PHASE_1_BRIEF.md](docs/PHASE_1_BRIEF.md) — locked architecture
75
+ - [docs/GENESIS.md](docs/GENESIS.md) — product spec
76
+ - [CONTEXT_PROMPT.md](CONTEXT_PROMPT.md) — session handoff
77
+ - [TODO.md](TODO.md) — backlog
package/bin/gui4cli.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ const dist = resolve(root, "dist/cli.js");
9
+
10
+ if (existsSync(dist)) {
11
+ await import(pathToFileURL(dist).href);
12
+ } else {
13
+ const { register } = await import("tsx/esm/api");
14
+ register();
15
+ await import(pathToFileURL(resolve(root, "src/cli.ts")).href);
16
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,86 @@
1
+ import { parseArgs } from "node:util";
2
+ import { detectForm } from "./detect/index.js";
3
+ import { formatUserError, Gui4CliError } from "./errors.js";
4
+ import { defaultBuildDir, writeBuildProject } from "./generate/project.js";
5
+ import { writeWindowApp } from "./generate/window.js";
6
+ import { lastRunPath, loadLastRun } from "./last-run/store.js";
7
+ import { resolveTarget } from "./resolve/target.js";
8
+ import { valuesFromDefaults } from "./run/argv.js";
9
+ import { openWindow } from "./window/open.js";
10
+ const HELP = `GUI4CLI — turn a Node CLI script into a desktop form.
11
+
12
+ Usage:
13
+ gui4cli <script-or-folder> [--entry <path>] [--json]
14
+ gui4cli <script-or-folder> --build [--out <dir>] [--force]
15
+
16
+ Options:
17
+ --entry <path> Use this file as the script (overrides folder bin/main)
18
+ --json Print the detected form and exit (no window)
19
+ --build Write a reusable project folder and exit (no .exe)
20
+ --out <dir> Folder for --build (default: <title>-gui in the current directory)
21
+ --force Replace an existing --out folder
22
+ --help Show this help
23
+
24
+ Examples:
25
+ gui4cli fixtures/resize.js
26
+ gui4cli fixtures/resize.js --build
27
+ gui4cli . --entry bin/cli.js
28
+ `;
29
+ export async function main(argv = process.argv.slice(2)) {
30
+ const { values, positionals } = parseArgs({
31
+ args: argv,
32
+ allowPositionals: true,
33
+ options: {
34
+ entry: { type: "string" },
35
+ json: { type: "boolean", default: false },
36
+ build: { type: "boolean", default: false },
37
+ out: { type: "string" },
38
+ force: { type: "boolean", default: false },
39
+ help: { type: "boolean", default: false },
40
+ },
41
+ });
42
+ if (values.help || argv.includes("-h")) {
43
+ process.stdout.write(HELP);
44
+ return 0;
45
+ }
46
+ if (values.json && values.build) {
47
+ throw new Gui4CliError("Use either --json or --build, not both.");
48
+ }
49
+ const { target, cwd } = await resolveTarget(positionals[0], values.entry, process.cwd());
50
+ const spec = await detectForm(target, cwd);
51
+ const last = await loadLastRun(target);
52
+ const formValues = { ...valuesFromDefaults(spec.fields), ...last };
53
+ if (values.json) {
54
+ process.stdout.write(`${JSON.stringify({ spec, values: formValues }, null, 2)}\n`);
55
+ return 0;
56
+ }
57
+ if (values.build) {
58
+ const outDir = values.out ?? defaultBuildDir(spec.title, process.cwd());
59
+ const dir = await writeBuildProject({
60
+ spec,
61
+ values: formValues,
62
+ outDir,
63
+ lastRunPath: lastRunPath(target),
64
+ force: values.force,
65
+ });
66
+ process.stdout.write(`Wrote ${dir}\nRun it with: npx --yes windowd\n`);
67
+ process.stdout.write(`The original script was not changed: ${spec.target}\n`);
68
+ return 0;
69
+ }
70
+ const dir = await writeWindowApp({
71
+ spec,
72
+ values: formValues,
73
+ nodePath: process.execPath,
74
+ lastRunPath: lastRunPath(target),
75
+ platform: process.platform,
76
+ });
77
+ process.stderr.write(`Opening ${spec.title} (${spec.fields.length} fields, detected via ${spec.source})…\n`);
78
+ return openWindow(dir, spec);
79
+ }
80
+ const entry = process.argv[1] ?? "";
81
+ if (entry.endsWith("cli.ts") || entry.endsWith("cli.js") || entry.endsWith("gui4cli.js")) {
82
+ main().then((code) => process.exit(code), (error) => {
83
+ process.stderr.write(`${formatUserError(error)}\n`);
84
+ process.exit(error instanceof Gui4CliError ? 2 : 1);
85
+ });
86
+ }
@@ -0,0 +1,13 @@
1
+ import { type Field } from "../schema/form.js";
2
+ export type Gui4CliConfig = {
3
+ title?: string;
4
+ description?: string;
5
+ entry?: string;
6
+ cwd?: string;
7
+ window?: {
8
+ width?: number;
9
+ height?: number;
10
+ };
11
+ fields?: Field[];
12
+ };
13
+ export declare function loadConfig(target: string, cwd: string): Promise<Gui4CliConfig | null>;
@@ -0,0 +1,66 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { z } from "zod";
6
+ import { Gui4CliError } from "../errors.js";
7
+ import { fieldSchema } from "../schema/form.js";
8
+ const configFieldSchema = fieldSchema.partial().extend({
9
+ name: z.string().min(1),
10
+ longFlag: z.string().min(1).optional(),
11
+ label: z.string().min(1).optional(),
12
+ type: fieldSchema.shape.type.optional(),
13
+ required: z.boolean().optional(),
14
+ });
15
+ const configSchema = z.object({
16
+ title: z.string().optional(),
17
+ description: z.string().optional(),
18
+ entry: z.string().optional(),
19
+ cwd: z.string().optional(),
20
+ window: z
21
+ .object({
22
+ width: z.number().int().positive().optional(),
23
+ height: z.number().int().positive().optional(),
24
+ })
25
+ .optional(),
26
+ fields: z.array(configFieldSchema).optional(),
27
+ });
28
+ export async function loadConfig(target, cwd) {
29
+ const searchDirs = [dirname(target), cwd];
30
+ for (const dir of searchDirs) {
31
+ for (const name of ["gui4cli.config.js", "gui4cli.config.mjs", "gui4cli.json"]) {
32
+ const path = join(dir, name);
33
+ if (!existsSync(path))
34
+ continue;
35
+ return readConfigFile(path);
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ async function readConfigFile(path) {
41
+ try {
42
+ const raw = path.endsWith(".json")
43
+ ? JSON.parse(await readFile(path, "utf8"))
44
+ : (await import(pathToFileURL(path).href)).default;
45
+ const parsed = configSchema.parse(raw);
46
+ return {
47
+ ...parsed,
48
+ fields: parsed.fields?.map((field) => fieldSchema.parse({
49
+ name: field.name,
50
+ longFlag: field.longFlag ?? `--${field.name}`,
51
+ shortFlag: field.shortFlag,
52
+ type: field.type ?? "string",
53
+ label: field.label ?? field.name,
54
+ help: field.help,
55
+ required: field.required ?? false,
56
+ default: field.default,
57
+ choices: field.choices,
58
+ positional: field.positional,
59
+ })),
60
+ };
61
+ }
62
+ catch (error) {
63
+ const detail = error instanceof Error ? error.message : String(error);
64
+ throw new Gui4CliError(`The config file at ${path} is not valid.`, detail);
65
+ }
66
+ }
@@ -0,0 +1,6 @@
1
+ import ts from "typescript";
2
+ export declare function parseSource(fileName: string, source: string): ts.SourceFile;
3
+ export declare function stringLiteral(node: ts.Expression | undefined): string | undefined;
4
+ export declare function primitiveLiteral(node: ts.Expression | undefined): string | number | boolean | undefined;
5
+ export declare function objectProps(node: ts.Expression | undefined): Map<string, ts.Expression>;
6
+ export declare function walkCalls(sourceFile: ts.SourceFile, visit: (call: ts.CallExpression, method: string) => void): void;
@@ -0,0 +1,62 @@
1
+ import ts from "typescript";
2
+ export function parseSource(fileName, source) {
3
+ return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
4
+ }
5
+ export function stringLiteral(node) {
6
+ if (!node)
7
+ return undefined;
8
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
9
+ return node.text;
10
+ }
11
+ return undefined;
12
+ }
13
+ export function primitiveLiteral(node) {
14
+ if (!node)
15
+ return undefined;
16
+ const text = stringLiteral(node);
17
+ if (text !== undefined)
18
+ return text;
19
+ if (ts.isNumericLiteral(node))
20
+ return Number(node.text);
21
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
22
+ return true;
23
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
24
+ return false;
25
+ return undefined;
26
+ }
27
+ export function objectProps(node) {
28
+ const props = new Map();
29
+ if (!node || !ts.isObjectLiteralExpression(node))
30
+ return props;
31
+ for (const prop of node.properties) {
32
+ if (!ts.isPropertyAssignment(prop))
33
+ continue;
34
+ const name = ts.isIdentifier(prop.name)
35
+ ? prop.name.text
36
+ : ts.isStringLiteral(prop.name)
37
+ ? prop.name.text
38
+ : undefined;
39
+ if (name)
40
+ props.set(name, prop.initializer);
41
+ }
42
+ return props;
43
+ }
44
+ export function walkCalls(sourceFile, visit) {
45
+ const calls = [];
46
+ const walk = (node) => {
47
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
48
+ calls.push({ call: node, method: node.expression.name.text });
49
+ }
50
+ ts.forEachChild(node, walk);
51
+ };
52
+ walk(sourceFile);
53
+ calls.sort((a, b) => methodStart(a.call, sourceFile) - methodStart(b.call, sourceFile));
54
+ for (const item of calls)
55
+ visit(item.call, item.method);
56
+ }
57
+ function methodStart(call, sourceFile) {
58
+ if (ts.isPropertyAccessExpression(call.expression)) {
59
+ return call.expression.name.getStart(sourceFile);
60
+ }
61
+ return call.getStart(sourceFile);
62
+ }
@@ -0,0 +1,2 @@
1
+ import type { Field } from "../schema/form.js";
2
+ export declare function detectCommander(fileName: string, source: string): Field[];
@@ -0,0 +1,45 @@
1
+ import { objectProps, parseSource, primitiveLiteral, stringLiteral, walkCalls } from "./ast.js";
2
+ import { fieldFromFlag, parseFlagSpec } from "./flags.js";
3
+ export function detectCommander(fileName, source) {
4
+ const sf = parseSource(fileName, source);
5
+ const fields = [];
6
+ walkCalls(sf, (call, method) => {
7
+ if (method !== "option" && method !== "requiredOption")
8
+ return;
9
+ const spec = stringLiteral(call.arguments[0]);
10
+ if (!spec)
11
+ return;
12
+ const parsed = parseFlagSpec(spec);
13
+ if (!parsed)
14
+ return;
15
+ const help = stringLiteral(call.arguments[1]);
16
+ let defaultValue = primitiveLiteral(call.arguments[2]);
17
+ let choices;
18
+ const obj = objectProps(call.arguments[1]);
19
+ if (obj.size > 0) {
20
+ const described = stringLiteral(obj.get("description") ?? obj.get("desc"));
21
+ defaultValue = primitiveLiteral(obj.get("default")) ?? defaultValue;
22
+ }
23
+ fields.push(fieldFromFlag({
24
+ name: parsed.name,
25
+ longFlag: parsed.longFlag,
26
+ shortFlag: parsed.shortFlag,
27
+ typeHint: parsed.typeHint,
28
+ help,
29
+ required: method === "requiredOption",
30
+ default: defaultValue,
31
+ choices,
32
+ boolean: parsed.boolean,
33
+ }));
34
+ });
35
+ return uniqueFields(fields);
36
+ }
37
+ function uniqueFields(fields) {
38
+ const seen = new Set();
39
+ return fields.filter((field) => {
40
+ if (seen.has(field.name))
41
+ return false;
42
+ seen.add(field.name);
43
+ return true;
44
+ });
45
+ }
@@ -0,0 +1,25 @@
1
+ import { type Field, type FieldType } from "../schema/form.js";
2
+ export declare function shouldSkipFlag(name: string): boolean;
3
+ export declare function nameFromLongFlag(longFlag: string): string;
4
+ export declare function inferType(hint: string | undefined, help: string | undefined, name: string): FieldType;
5
+ export declare function parseFlagSpec(spec: string): {
6
+ shortFlag?: string;
7
+ longFlag: string;
8
+ name: string;
9
+ boolean: boolean;
10
+ typeHint?: string;
11
+ } | null;
12
+ export declare function fieldFromFlag(input: {
13
+ name: string;
14
+ longFlag: string;
15
+ shortFlag?: string;
16
+ type?: FieldType;
17
+ typeHint?: string;
18
+ help?: string;
19
+ required?: boolean;
20
+ default?: string | number | boolean;
21
+ choices?: string[];
22
+ boolean?: boolean;
23
+ positional?: boolean;
24
+ }): Field;
25
+ export declare function mergeFields(base: Field[], overlay: Field[]): Field[];
@@ -0,0 +1,77 @@
1
+ import { labelFromName } from "../schema/form.js";
2
+ const SKIP_FLAGS = new Set(["help", "version", "h", "V", "v"]);
3
+ export function shouldSkipFlag(name) {
4
+ return SKIP_FLAGS.has(name.replace(/^--?/, ""));
5
+ }
6
+ export function nameFromLongFlag(longFlag) {
7
+ return longFlag.replace(/^--/, "");
8
+ }
9
+ export function inferType(hint, help, name) {
10
+ const blob = `${hint ?? ""} ${help ?? ""} ${name}`.toLowerCase();
11
+ if (/\b(dir|directory|folder)\b/.test(blob))
12
+ return "directory";
13
+ if (/\b(file|path|filename)\b/.test(blob))
14
+ return "file";
15
+ if (/\b(number|int|integer|float|count|width|height|quality|port)\b/.test(blob)) {
16
+ return "number";
17
+ }
18
+ return "string";
19
+ }
20
+ export function parseFlagSpec(spec) {
21
+ const parts = spec.split(",").map((part) => part.trim()).filter(Boolean);
22
+ let shortFlag;
23
+ let longFlag = "";
24
+ let typeHint;
25
+ let boolean = true;
26
+ for (const part of parts) {
27
+ const short = part.match(/^(-[A-Za-z0-9])$/);
28
+ if (short) {
29
+ shortFlag = short[1];
30
+ continue;
31
+ }
32
+ const long = part.match(/^--([A-Za-z0-9][A-Za-z0-9-]*)(?:\s*[<[]([^>\]]+)[>\]])?/);
33
+ if (long) {
34
+ longFlag = `--${long[1]}`;
35
+ if (long[2]) {
36
+ boolean = false;
37
+ typeHint = long[2];
38
+ }
39
+ }
40
+ }
41
+ if (!longFlag && shortFlag) {
42
+ longFlag = `--${shortFlag.slice(1)}`;
43
+ }
44
+ if (!longFlag)
45
+ return null;
46
+ const name = nameFromLongFlag(longFlag);
47
+ if (shouldSkipFlag(name))
48
+ return null;
49
+ return { shortFlag, longFlag, name, boolean, typeHint };
50
+ }
51
+ export function fieldFromFlag(input) {
52
+ const type = input.boolean
53
+ ? "boolean"
54
+ : input.choices && input.choices.length > 0
55
+ ? "choice"
56
+ : (input.type ?? inferType(input.typeHint, input.help, input.name));
57
+ return {
58
+ name: input.name,
59
+ longFlag: input.longFlag,
60
+ shortFlag: input.shortFlag,
61
+ type,
62
+ label: labelFromName(input.name),
63
+ help: input.help,
64
+ required: Boolean(input.required),
65
+ default: input.default,
66
+ choices: input.choices,
67
+ positional: input.positional,
68
+ };
69
+ }
70
+ export function mergeFields(base, overlay) {
71
+ const byName = new Map(base.map((field) => [field.name, field]));
72
+ for (const field of overlay) {
73
+ const existing = byName.get(field.name);
74
+ byName.set(field.name, existing ? { ...existing, ...field } : field);
75
+ }
76
+ return [...byName.values()];
77
+ }
@@ -0,0 +1,3 @@
1
+ import type { Field } from "../schema/form.js";
2
+ export declare function parseHelpText(text: string): Field[];
3
+ export declare function collectHelpText(target: string, cwd: string, timeoutMs?: number): Promise<string>;
@@ -0,0 +1,53 @@
1
+ import { spawn } from "node:child_process";
2
+ import { fieldFromFlag, parseFlagSpec } from "./flags.js";
3
+ const HELP_LINE = /^\s*(-\S.*?)\s{2,}(\S.*)?$/;
4
+ export function parseHelpText(text) {
5
+ const fields = [];
6
+ for (const raw of text.split(/\r?\n/)) {
7
+ const line = raw.trimEnd();
8
+ const match = line.match(HELP_LINE);
9
+ if (!match)
10
+ continue;
11
+ const parsed = parseFlagSpec(match[1] ?? "");
12
+ if (!parsed)
13
+ continue;
14
+ const help = (match[2] ?? "").replace(/\(default:.*\)/i, "").trim() || undefined;
15
+ fields.push(fieldFromFlag({
16
+ name: parsed.name,
17
+ longFlag: parsed.longFlag,
18
+ shortFlag: parsed.shortFlag,
19
+ typeHint: parsed.typeHint,
20
+ help,
21
+ boolean: parsed.boolean,
22
+ }));
23
+ }
24
+ return fields;
25
+ }
26
+ export async function collectHelpText(target, cwd, timeoutMs = 4000) {
27
+ return new Promise((resolve, reject) => {
28
+ const child = spawn(process.execPath, [target, "--help"], {
29
+ cwd,
30
+ env: { ...process.env, GUI4CLI_DETECT: "1" },
31
+ stdio: ["ignore", "pipe", "pipe"],
32
+ });
33
+ let out = "";
34
+ const timer = setTimeout(() => {
35
+ child.kill();
36
+ reject(new Error("Timed out reading --help"));
37
+ }, timeoutMs);
38
+ child.stdout.on("data", (chunk) => {
39
+ out += chunk.toString("utf8");
40
+ });
41
+ child.stderr.on("data", (chunk) => {
42
+ out += chunk.toString("utf8");
43
+ });
44
+ child.on("error", (error) => {
45
+ clearTimeout(timer);
46
+ reject(error);
47
+ });
48
+ child.on("close", () => {
49
+ clearTimeout(timer);
50
+ resolve(out);
51
+ });
52
+ });
53
+ }
@@ -0,0 +1,2 @@
1
+ import { type FormSpec } from "../schema/form.js";
2
+ export declare function detectForm(target: string, cwd: string): Promise<FormSpec>;
@@ -0,0 +1,61 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
3
+ import { Gui4CliError } from "../errors.js";
4
+ import { formSpecSchema, labelFromName } from "../schema/form.js";
5
+ import { loadConfig } from "../config/load.js";
6
+ import { detectCommander } from "./commander.js";
7
+ import { collectHelpText, parseHelpText } from "./help.js";
8
+ import { detectJsdoc } from "./jsdoc.js";
9
+ import { detectYargs } from "./yargs.js";
10
+ import { mergeFields } from "./flags.js";
11
+ export async function detectForm(target, cwd) {
12
+ const source = await readFile(target, "utf8");
13
+ const config = await loadConfig(target, cwd);
14
+ const commander = detectCommander(target, source);
15
+ const yargs = detectYargs(target, source);
16
+ const jsdoc = detectJsdoc(source);
17
+ let fields = [];
18
+ let detectSource = "merged";
19
+ if (commander.length >= yargs.length && commander.length > 0) {
20
+ fields = commander;
21
+ detectSource = "commander";
22
+ }
23
+ else if (yargs.length > 0) {
24
+ fields = yargs;
25
+ detectSource = "yargs";
26
+ }
27
+ else if (jsdoc.length > 0) {
28
+ fields = jsdoc;
29
+ detectSource = "jsdoc";
30
+ }
31
+ if (fields.length === 0) {
32
+ try {
33
+ const help = parseHelpText(await collectHelpText(target, cwd));
34
+ if (help.length > 0) {
35
+ fields = help;
36
+ detectSource = "help";
37
+ }
38
+ }
39
+ catch {
40
+ // Help can have side effects or hang; config is the escape hatch.
41
+ }
42
+ }
43
+ if (config?.fields) {
44
+ fields = mergeFields(fields, config.fields);
45
+ detectSource = fields.length > 0 ? "merged" : "config";
46
+ }
47
+ if (fields.length === 0) {
48
+ throw new Gui4CliError("Couldn't detect arguments for this script.", "Add a gui4cli.config.js next to the script with a fields object, then try again.");
49
+ }
50
+ const title = config?.title ?? labelFromName(basename(target).replace(/\.[cm]?[jt]s$/, ""));
51
+ const spec = formSpecSchema.parse({
52
+ title,
53
+ description: config?.description,
54
+ target,
55
+ cwd: config?.cwd ?? cwd,
56
+ source: detectSource,
57
+ fields,
58
+ window: config?.window,
59
+ });
60
+ return spec;
61
+ }
@@ -0,0 +1,2 @@
1
+ import type { Field } from "../schema/form.js";
2
+ export declare function detectJsdoc(source: string): Field[];
@@ -0,0 +1,22 @@
1
+ import { fieldFromFlag, shouldSkipFlag } from "./flags.js";
2
+ const PARAM = /@param\s+(?:\{([^}]+)\}\s+)?(?:\[)?(-{0,2}[\w-]+)(?:\])?\s*-?\s*(.*)/g;
3
+ export function detectJsdoc(source) {
4
+ const fields = [];
5
+ for (const match of source.matchAll(PARAM)) {
6
+ const typeHint = match[1];
7
+ const rawName = match[2] ?? "";
8
+ const help = match[3]?.trim() || undefined;
9
+ const name = rawName.replace(/^--/, "");
10
+ if (!name || shouldSkipFlag(name))
11
+ continue;
12
+ fields.push(fieldFromFlag({
13
+ name,
14
+ longFlag: `--${name}`,
15
+ typeHint,
16
+ help,
17
+ required: !match[0].includes("["),
18
+ boolean: /boolean|flag/i.test(typeHint ?? ""),
19
+ }));
20
+ }
21
+ return fields;
22
+ }
@@ -0,0 +1,2 @@
1
+ import type { Field } from "../schema/form.js";
2
+ export declare function detectYargs(fileName: string, source: string): Field[];