@tscomfy/cli 0.0.0-stage → 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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -2
  3. package/dist/args.d.ts +24 -0
  4. package/dist/args.d.ts.map +1 -0
  5. package/dist/args.js +66 -0
  6. package/dist/args.js.map +1 -0
  7. package/dist/check.d.ts +6 -0
  8. package/dist/check.d.ts.map +1 -0
  9. package/dist/check.js +185 -0
  10. package/dist/check.js.map +1 -0
  11. package/dist/generate.d.ts +17 -0
  12. package/dist/generate.d.ts.map +1 -0
  13. package/dist/generate.js +166 -0
  14. package/dist/generate.js.map +1 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +12 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/io.d.ts +26 -0
  20. package/dist/io.d.ts.map +1 -0
  21. package/dist/io.js +80 -0
  22. package/dist/io.js.map +1 -0
  23. package/dist/main.d.ts +3 -0
  24. package/dist/main.d.ts.map +1 -0
  25. package/dist/main.js +6 -0
  26. package/dist/main.js.map +1 -0
  27. package/dist/manifest.d.ts +47 -0
  28. package/dist/manifest.d.ts.map +1 -0
  29. package/dist/manifest.js +66 -0
  30. package/dist/manifest.js.map +1 -0
  31. package/dist/report.d.ts +13 -0
  32. package/dist/report.d.ts.map +1 -0
  33. package/dist/report.js +69 -0
  34. package/dist/report.js.map +1 -0
  35. package/dist/run.d.ts +9 -0
  36. package/dist/run.d.ts.map +1 -0
  37. package/dist/run.js +89 -0
  38. package/dist/run.js.map +1 -0
  39. package/dist/source.d.ts +28 -0
  40. package/dist/source.d.ts.map +1 -0
  41. package/dist/source.js +92 -0
  42. package/dist/source.js.map +1 -0
  43. package/dist/transpile.d.ts +6 -0
  44. package/dist/transpile.d.ts.map +1 -0
  45. package/dist/transpile.js +178 -0
  46. package/dist/transpile.js.map +1 -0
  47. package/dist/util.d.ts +33 -0
  48. package/dist/util.d.ts.map +1 -0
  49. package/dist/util.js +81 -0
  50. package/dist/util.js.map +1 -0
  51. package/package.json +56 -4
  52. package/src/args.ts +78 -0
  53. package/src/check.ts +224 -0
  54. package/src/generate.ts +198 -0
  55. package/src/index.ts +12 -0
  56. package/src/io.ts +99 -0
  57. package/src/main.ts +7 -0
  58. package/src/manifest.ts +105 -0
  59. package/src/report.ts +73 -0
  60. package/src/run.ts +90 -0
  61. package/src/source.ts +114 -0
  62. package/src/transpile.ts +201 -0
  63. package/src/util.ts +87 -0
@@ -0,0 +1,178 @@
1
+ import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
2
+ import { CODEGEN_VERSION, exportNameProblem, transpile, transpileNameProblem, } from "@tscomfy/codegen";
3
+ import { hashSchema, importApi, importUi, parse, typedSchema } from "@tscomfy/core";
4
+ import { HELP, parseFlags, required, value } from "./args.js";
5
+ import { CliError } from "./io.js";
6
+ import { MANIFEST, readGenerated, SNAPSHOT } from "./manifest.js";
7
+ import { count, formatDiagnostic } from "./report.js";
8
+ import { camelName, flagWords, isRecord } from "./util.js";
9
+ /** `tscomfy transpile --help`. */
10
+ export const TRANSPILE_USAGE = `Usage: tscomfy transpile (--prompt <prompt.json> | --workflow <workflow.json>) --schema <dir> [options]
11
+
12
+ Turns an API-format prompt (the frontend's "Export (API)"), or a UI workflow (its "Save" or
13
+ "Export"), into TypeScript that builds it with the types \`tscomfy generate\` wrote into <dir>.
14
+ Running the code and exporting gives the prompt back: for a UI workflow, the prompt the frontend
15
+ would queue for it, with Reroutes, PrimitiveNodes and subgraphs resolved. Nodes the types can't
16
+ express are written as raw(…), with a warning.
17
+
18
+ Options:
19
+ --prompt <file> The prompt to transpile.
20
+ --workflow <file> The UI workflow to transpile, instead of a prompt.
21
+ --schema <dir> The directory \`tscomfy generate\` wrote (it reads ${SNAPSHOT} and ${MANIFEST}).
22
+ --out <file> Where to write the module. Default: standard output, importing the
23
+ schema relative to the current directory.
24
+ --name <identifier> The exported constant. Default: from the file name, camelCased
25
+ (My Txt2Img.json → myTxt2Img).
26
+ --no-inline Give every node a variable, rather than inlining those used once.
27
+ -h, --help Show this help.
28
+
29
+ The header records the command that transpiles the file again, for a POSIX shell in the
30
+ directory transpile ran in. A value that starts with - is written --flag=<value>.
31
+ `;
32
+ const FLAGS = [
33
+ { name: "prompt", takesValue: true },
34
+ { name: "workflow", takesValue: true },
35
+ { name: "schema", takesValue: true },
36
+ { name: "out", takesValue: true },
37
+ { name: "name", takesValue: true },
38
+ { name: "no-inline", takesValue: false },
39
+ HELP,
40
+ ];
41
+ /** `tscomfy transpile`. */
42
+ export async function transpileCommand(io, args) {
43
+ const flags = parseFlags(args, FLAGS);
44
+ if (flags.has("help")) {
45
+ io.out(TRANSPILE_USAGE);
46
+ return 0;
47
+ }
48
+ if (flags.has("prompt") && flags.has("workflow")) {
49
+ throw new CliError("Pass --prompt or --workflow, not both.", 2);
50
+ }
51
+ if (!flags.has("prompt") && !flags.has("workflow")) {
52
+ throw new CliError("--prompt or --workflow is required.", 2);
53
+ }
54
+ const kind = flags.has("workflow") ? "workflow" : "prompt";
55
+ const promptPath = required(flags, kind);
56
+ const schemaDir = required(flags, "schema");
57
+ const out = value(flags, "out");
58
+ const { manifest, schema } = await readGenerated(io, schemaDir);
59
+ const name = readName(flags, promptPath, manifest.exportName);
60
+ const text = await io.readText(promptPath);
61
+ if (text === undefined)
62
+ throw new CliError(`There is no file at ${promptPath}.`);
63
+ // The types in <dir> must be the ones this version would write, or the code won't fit them.
64
+ if (manifest.codegen !== CODEGEN_VERSION || manifest.schema !== hashSchema(schema)) {
65
+ throw new CliError(`${schemaDir} was generated by another version of tscomfy; run tscomfy generate again first.`);
66
+ }
67
+ const what = kind === "prompt" ? "a prompt" : "a UI workflow";
68
+ const mixUp = kind === "prompt" ? notAPrompt(text) : notAWorkflow(text);
69
+ if (mixUp !== undefined)
70
+ throw new CliError(`${promptPath} isn't ${what}: ${mixUp}`);
71
+ const typed = typedSchema(schema);
72
+ const imported = kind === "prompt" ? importApi(text, typed) : importUi(text, typed);
73
+ const fatal = imported.diagnostics.find((diagnostic) => diagnostic.path.length === 0 &&
74
+ (diagnostic.code === "E_INVALID_JSON" ||
75
+ diagnostic.code === "E_INVALID_PROMPT" ||
76
+ diagnostic.code === "E_INVALID_WORKFLOW"));
77
+ if (fatal !== undefined) {
78
+ throw new CliError(`${promptPath} isn't ${what}: ${fatal.message}`);
79
+ }
80
+ if (imported.workflow.graph.nodes.size === 0) {
81
+ throw new CliError(`${promptPath} isn't ${what}: it has no node that can be read.`);
82
+ }
83
+ const command = [
84
+ "tscomfy transpile",
85
+ flagWords(kind, promptPath),
86
+ flagWords("schema", schemaDir),
87
+ ...(out === undefined ? [] : [flagWords("out", out)]),
88
+ ...(flags.has("name") ? [flagWords("name", name)] : []),
89
+ ...(flags.has("no-inline") ? ["--no-inline"] : []),
90
+ ].join(" ");
91
+ const result = transpile(imported.workflow, {
92
+ name,
93
+ schemaModule: moduleSpecifier(out, schemaDir, manifest.importExtension),
94
+ schemaName: manifest.exportName,
95
+ inline: !flags.has("no-inline"),
96
+ header: [`Regenerate with: ${command}`],
97
+ });
98
+ for (const diagnostic of [...imported.diagnostics, ...result.diagnostics]) {
99
+ io.err(`${formatDiagnostic(diagnostic)}\n`);
100
+ }
101
+ if (out === undefined) {
102
+ io.out(result.code);
103
+ return 0;
104
+ }
105
+ await io.writeText(out, result.code);
106
+ io.out(`Transpiled ${count(imported.workflow.graph.nodes.size, "node")} from ${promptPath} into ${out}.\n`);
107
+ return 0;
108
+ }
109
+ /**
110
+ * What a file that isn't a prompt looks like instead, when it is a common mix-up: the frontend's
111
+ * UI workflow (`nodes` and `links`), or a `POST /prompt` body (`{ "prompt": … }`).
112
+ */
113
+ function notAPrompt(text) {
114
+ const parsed = parse(text).value;
115
+ if (parsed === undefined || !isRecord(parsed))
116
+ return undefined;
117
+ if (isUiWorkflow(parsed))
118
+ return "it is a UI workflow; pass it with --workflow instead.";
119
+ // A node with the id "prompt" has a class_type; a POST /prompt body's prompt doesn't.
120
+ const prompt = parsed["prompt"];
121
+ if (prompt !== undefined && isRecord(prompt) && !Object.hasOwn(prompt, "class_type")) {
122
+ return "it is a POST /prompt body; save its prompt on its own.";
123
+ }
124
+ return undefined;
125
+ }
126
+ /** What a file passed as a UI workflow looks like instead: a prompt, or a `POST /prompt` body. */
127
+ function notAWorkflow(text) {
128
+ const parsed = parse(text).value;
129
+ if (parsed === undefined || !isRecord(parsed) || isUiWorkflow(parsed))
130
+ return undefined;
131
+ const nodes = Object.values(parsed);
132
+ if (nodes.length > 0 && nodes.every((node) => isRecord(node) && "class_type" in node)) {
133
+ return "it is an API prompt; pass it with --prompt instead.";
134
+ }
135
+ if (Object.hasOwn(parsed, "prompt")) {
136
+ return "it is a POST /prompt body; save its prompt on its own and pass it with --prompt.";
137
+ }
138
+ return undefined;
139
+ }
140
+ function isUiWorkflow(parsed) {
141
+ return Array.isArray(parsed["nodes"]) && Array.isArray(parsed["links"]);
142
+ }
143
+ /**
144
+ * How the module imports the schema: relative to where it is written (the current directory
145
+ * without --out), with POSIX separators.
146
+ */
147
+ function moduleSpecifier(out, schemaDir, extension) {
148
+ const from = out === undefined ? "." : dirname(out);
149
+ const target = relative(from, join(schemaDir, `index${extension}`));
150
+ // On another drive (Windows) there is no relative path.
151
+ if (isAbsolute(target)) {
152
+ throw new CliError("--schema and --out are on different drives; no relative import joins them.");
153
+ }
154
+ const posix = target.split(sep).join("/");
155
+ return posix.startsWith("./") || posix.startsWith("../") ? posix : `./${posix}`;
156
+ }
157
+ /**
158
+ * The export name: `--name`, or from the file name (`Anima-API.json` → `animaApi`, see
159
+ * `camelName`). The schema's name and this one must both be usable by `transpile`, by the rule
160
+ * `generate` checked the schema's with.
161
+ */
162
+ function readName(flags, promptPath, schemaName) {
163
+ const schemaProblem = exportNameProblem(schemaName);
164
+ if (schemaProblem !== undefined) {
165
+ // Generated before generate checked this.
166
+ throw new CliError(`The schema's export name, ${schemaName}, won't do: ${schemaProblem}. Run tscomfy generate again with another --name.`);
167
+ }
168
+ const given = value(flags, "name");
169
+ const name = given ?? camelName(basename(promptPath).replace(/\.[^.]*$/, ""));
170
+ const problem = exportNameProblem(name) ?? transpileNameProblem({ name, schemaName });
171
+ if (problem !== undefined) {
172
+ throw new CliError(given === undefined
173
+ ? `Can't name the export ${JSON.stringify(name)}, after ${JSON.stringify(promptPath)}: ${problem}; pass --name.`
174
+ : `--name ${JSON.stringify(given)} isn't usable: ${problem}.`, 2);
175
+ }
176
+ return name;
177
+ }
178
+ //# sourceMappingURL=transpile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transpile.js","sourceRoot":"","sources":["../src/transpile.ts"],"names":[],"mappings":"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"}
package/dist/util.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { type JsonValue } from "@tscomfy/core";
2
+ /** Whether a JSON value is an object (not null, not an array). */
3
+ export declare function isRecord(value: JsonValue): value is {
4
+ readonly [key: string]: JsonValue;
5
+ };
6
+ /** A word for a POSIX shell, quoted only when it has to be: `out dir` → `'out dir'`. */
7
+ export declare function shellQuote(word: string): string;
8
+ /**
9
+ * `--name value` for a POSIX shell, as the CLI reads it back: `--name=value` when the value
10
+ * starts with `-`, which the separate form would take for a flag (`--out=--weird`).
11
+ */
12
+ export declare function flagWords(name: string, value: string): string;
13
+ /**
14
+ * An export name from a file or directory name, the same rule for `generate --out` and
15
+ * `transpile --prompt`: camelCase from the runs of ASCII letters and digits, a word in capitals
16
+ * lowercased, the first word starting lowercase and the others uppercase
17
+ * (`src/comfy/local` → `local`, `my-server` → `myServer`, `Anima-API.json` → `animaApi`).
18
+ * May be empty or start with a digit: callers check the result.
19
+ */
20
+ export declare function camelName(text: string): string;
21
+ /**
22
+ * The message of an error body, when `text` is one rather than an answer: an object with a lone
23
+ * `detail`, `error` or `message` (`{"detail": "Not authenticated"}`, as auth proxies in front of
24
+ * ComfyUI send), whose value isn't a node definition. Shown as compact JSON, at most 200
25
+ * characters, with every control character escaped, so none reaches the terminal.
26
+ */
27
+ export declare function errorBodyMessage(text: string): string | undefined;
28
+ /**
29
+ * A URL fit for messages and generated files: without credentials, query or fragment, which is
30
+ * where servers expect passwords and tokens. `http://user:pw@host/api?token=t` → `http://host/api`.
31
+ */
32
+ export declare function redactUrl(url: URL): string;
33
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAoB,MAAM,eAAe,CAAC;AAEjE,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,IAAI;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAEzF;AAED,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAU9C;AAKD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAuBjE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAO1C"}
package/dist/util.js ADDED
@@ -0,0 +1,81 @@
1
+ import { parse, stringify } from "@tscomfy/core";
2
+ /** Whether a JSON value is an object (not null, not an array). */
3
+ export function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ /** A word for a POSIX shell, quoted only when it has to be: `out dir` → `'out dir'`. */
7
+ export function shellQuote(word) {
8
+ return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
9
+ }
10
+ /**
11
+ * `--name value` for a POSIX shell, as the CLI reads it back: `--name=value` when the value
12
+ * starts with `-`, which the separate form would take for a flag (`--out=--weird`).
13
+ */
14
+ export function flagWords(name, value) {
15
+ return value.startsWith("-")
16
+ ? `--${name}=${shellQuote(value)}`
17
+ : `--${name} ${shellQuote(value)}`;
18
+ }
19
+ /**
20
+ * An export name from a file or directory name, the same rule for `generate --out` and
21
+ * `transpile --prompt`: camelCase from the runs of ASCII letters and digits, a word in capitals
22
+ * lowercased, the first word starting lowercase and the others uppercase
23
+ * (`src/comfy/local` → `local`, `my-server` → `myServer`, `Anima-API.json` → `animaApi`).
24
+ * May be empty or start with a digit: callers check the result.
25
+ */
26
+ export function camelName(text) {
27
+ const words = text.match(/[A-Za-z0-9]+/g) ?? [];
28
+ return words
29
+ .map((word, index) => {
30
+ const lower = word === word.toUpperCase() ? word.toLowerCase() : word;
31
+ return index === 0
32
+ ? lower.charAt(0).toLowerCase() + lower.slice(1)
33
+ : lower.charAt(0).toUpperCase() + lower.slice(1);
34
+ })
35
+ .join("");
36
+ }
37
+ /** How much of an error body's message a message shows. */
38
+ const SHOWN_DETAIL = 200;
39
+ /**
40
+ * The message of an error body, when `text` is one rather than an answer: an object with a lone
41
+ * `detail`, `error` or `message` (`{"detail": "Not authenticated"}`, as auth proxies in front of
42
+ * ComfyUI send), whose value isn't a node definition. Shown as compact JSON, at most 200
43
+ * characters, with every control character escaped, so none reaches the terminal.
44
+ */
45
+ export function errorBodyMessage(text) {
46
+ const value = parse(text).value;
47
+ if (value === undefined || !isRecord(value))
48
+ return undefined;
49
+ const keys = Object.keys(value);
50
+ const [key] = keys;
51
+ if (keys.length !== 1 || (key !== "detail" && key !== "error" && key !== "message")) {
52
+ return undefined;
53
+ }
54
+ const message = value[key];
55
+ if (message === undefined || message === null)
56
+ return undefined;
57
+ // A node class named `error` is an object with its inputs and outputs.
58
+ if (isRecord(message) && (Object.hasOwn(message, "input") || Object.hasOwn(message, "output"))) {
59
+ return undefined;
60
+ }
61
+ // `stringify` escapes C0 controls; DEL and C1 controls (a CSI is U+009B) are escaped here.
62
+ const shown = stringify(message).replaceAll(/[\u007f-\u009f]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
63
+ if (shown.length <= SHOWN_DETAIL)
64
+ return shown;
65
+ const cut = shown.slice(0, SHOWN_DETAIL);
66
+ // Not half a surrogate pair.
67
+ return `${/[\ud800-\udbff]$/.test(cut) ? cut.slice(0, -1) : cut}…`;
68
+ }
69
+ /**
70
+ * A URL fit for messages and generated files: without credentials, query or fragment, which is
71
+ * where servers expect passwords and tokens. `http://user:pw@host/api?token=t` → `http://host/api`.
72
+ */
73
+ export function redactUrl(url) {
74
+ const shown = new URL(url.href);
75
+ shown.username = "";
76
+ shown.password = "";
77
+ shown.search = "";
78
+ shown.hash = "";
79
+ return shown.href;
80
+ }
81
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@tscomfy/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "The tscomfy command: generate types from a ComfyUI server, check them for drift, and transpile API prompts and UI workflows to code.",
5
+ "keywords": [
6
+ "cli",
7
+ "codegen",
8
+ "comfyui",
9
+ "typescript",
10
+ "workflow"
11
+ ],
12
+ "homepage": "https://github.com/TheeKia/tscomfy/tree/main/packages/cli#readme",
13
+ "bugs": "https://github.com/TheeKia/tscomfy/issues",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/TheeKia/tscomfy.git",
18
+ "directory": "packages/cli"
19
+ },
20
+ "bin": {
21
+ "tscomfy": "dist/main.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "!dist/**/*.tsbuildinfo",
26
+ "src"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": [
30
+ "./dist/main.js",
31
+ "./src/main.ts"
32
+ ],
33
+ "exports": {
34
+ ".": {
35
+ "@tscomfy/source": "./src/index.ts",
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ }
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@tscomfy/codegen": "0.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@tscomfy/core": "0.1.0",
48
+ "@tscomfy/fixtures": "0.0.0",
49
+ "@types/node": "24.19.2"
50
+ },
51
+ "peerDependencies": {
52
+ "@tscomfy/core": "^0.1.0"
53
+ },
54
+ "engines": {
55
+ "bun": ">=1.4",
56
+ "node": ">=24"
57
+ }
58
+ }
package/src/args.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { CliError } from "./io.ts";
2
+
3
+ /** One flag a command accepts: `--name value`, `--name=value`, or a boolean `--name`. */
4
+ export interface FlagSpec {
5
+ readonly name: string;
6
+ readonly takesValue: boolean;
7
+ /** A one-letter alias of a boolean flag: `h` for `-h`. */
8
+ readonly short?: string;
9
+ }
10
+
11
+ /** Parsed flags: the value of each given flag, `true` for a boolean one. */
12
+ export type Flags = ReadonlyMap<string, string | true>;
13
+
14
+ /** `-h, --help`, which every command takes. */
15
+ export const HELP: FlagSpec = { name: "help", takesValue: false, short: "h" };
16
+
17
+ /**
18
+ * Parses the arguments after the command name. Throws a usage {@link CliError} (exit 2).
19
+ *
20
+ * A value given apart (`--out dir`) can't start with `-`, so a forgotten value isn't read from
21
+ * the next flag (`--out --from x`); `--out=-dir` gives such a value. Every value is recorded in
22
+ * a regenerate command, which can't keep control characters, so none may have one.
23
+ */
24
+ export function parseFlags(args: readonly string[], specs: readonly FlagSpec[]): Flags {
25
+ const flags = new Map<string, string | true>();
26
+ for (let index = 0; index < args.length; index++) {
27
+ const arg = args[index] ?? "";
28
+ const short = /^-([A-Za-z])$/.exec(arg)?.[1];
29
+ if (short !== undefined) {
30
+ const spec = specs.find((each) => each.short === short);
31
+ if (spec === undefined) throw new CliError(`Unknown option -${short}.`, 2);
32
+ if (flags.has(spec.name)) throw new CliError(`--${spec.name} is given twice.`, 2);
33
+ flags.set(spec.name, true);
34
+ continue;
35
+ }
36
+ if (!arg.startsWith("--")) throw new CliError(`Unexpected argument ${JSON.stringify(arg)}.`, 2);
37
+ const equals = arg.indexOf("=");
38
+ const name = arg.slice(2, equals === -1 ? undefined : equals);
39
+ const spec = specs.find((each) => each.name === name);
40
+ if (spec === undefined) throw new CliError(`Unknown option --${name}.`, 2);
41
+ if (flags.has(name)) throw new CliError(`--${name} is given twice.`, 2);
42
+ if (!spec.takesValue) {
43
+ if (equals !== -1) throw new CliError(`--${name} takes no value.`, 2);
44
+ flags.set(name, true);
45
+ continue;
46
+ }
47
+ const given = equals === -1 ? args[++index] : arg.slice(equals + 1);
48
+ if (given === undefined || given === "") throw new CliError(`--${name} needs a value.`, 2);
49
+ if (equals === -1 && given.startsWith("-")) {
50
+ throw new CliError(
51
+ `--${name} needs a value; for one that starts with -, write --${name}=<value>.`,
52
+ 2,
53
+ );
54
+ }
55
+ // oxlint-disable-next-line no-control-regex -- control characters are what this looks for
56
+ if (/[\u0000-\u001f\u007f]/.test(given)) {
57
+ throw new CliError(
58
+ `--${name} has a control character, which a regenerate command can't keep.`,
59
+ 2,
60
+ );
61
+ }
62
+ flags.set(name, given);
63
+ }
64
+ return flags;
65
+ }
66
+
67
+ /** A flag's value, if given. */
68
+ export function value(flags: Flags, name: string): string | undefined {
69
+ const given = flags.get(name);
70
+ return typeof given === "string" ? given : undefined;
71
+ }
72
+
73
+ /** A flag's value, which must be given. */
74
+ export function required(flags: Flags, name: string): string {
75
+ const given = value(flags, name);
76
+ if (given === undefined) throw new CliError(`--${name} is required.`, 2);
77
+ return given;
78
+ }