@dreamlake/ml-dash 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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * A small argparse-shaped option parser.
3
+ *
4
+ * The flag surface is a contract: the Python CLI's `add_parser` functions gave
5
+ * several options more than two spellings (`--dash-url`/`--api-url`, and
6
+ * `-p`/`--pref`/`--prefix`/`--proj`/`--project`), and argparse also accepts
7
+ * unambiguous prefixes and `--flag=value`. `commander` keeps only a short and
8
+ * a long form per option, so porting through it would silently drop spellings
9
+ * that existing scripts pass. This reproduces the subset argparse actually
10
+ * exercises instead: aliases, `=`-joined values, `-p value`, choices, required
11
+ * options and mutually exclusive groups.
12
+ *
13
+ * Abbreviation matching is deliberately NOT implemented — argparse's prefix
14
+ * matching turns a future new option into a silent behaviour change for an
15
+ * existing abbreviation, and no documented invocation depends on it.
16
+ */
17
+ import { bold, red } from "../util/ansi.js";
18
+ export class ParseError extends Error {
19
+ }
20
+ export function parseArgs(spec, argv) {
21
+ const byFlag = new Map();
22
+ for (const opt of spec.options) {
23
+ for (const f of opt.flags)
24
+ byFlag.set(f, opt);
25
+ }
26
+ const out = {};
27
+ const seen = new Set();
28
+ const positionals = spec.positionals ?? [];
29
+ let nextPositional = 0;
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const token = argv[i];
32
+ if (token === "--help" || token === "-h") {
33
+ out.help = true;
34
+ return out;
35
+ }
36
+ if (!token.startsWith("-") || token === "-") {
37
+ if (nextPositional >= positionals.length) {
38
+ throw new ParseError(`unrecognized argument: ${token}`);
39
+ }
40
+ const positional = positionals[nextPositional++];
41
+ out[positional.dest] = token;
42
+ seen.add(positional.dest);
43
+ continue;
44
+ }
45
+ // `--flag=value` and `--flag value` are the same thing to argparse.
46
+ const eq = token.indexOf("=");
47
+ const flag = eq === -1 ? token : token.slice(0, eq);
48
+ const inlineValue = eq === -1 ? undefined : token.slice(eq + 1);
49
+ const opt = byFlag.get(flag);
50
+ if (!opt)
51
+ throw new ParseError(`unrecognized argument: ${flag}`);
52
+ if (opt.boolean) {
53
+ if (inlineValue !== undefined) {
54
+ throw new ParseError(`argument ${flag}: ignored explicit argument '${inlineValue}'`);
55
+ }
56
+ out[opt.dest] = true;
57
+ seen.add(opt.dest);
58
+ continue;
59
+ }
60
+ let value;
61
+ if (inlineValue !== undefined) {
62
+ value = inlineValue;
63
+ }
64
+ else {
65
+ // A value that itself looks like a known flag means the value is missing.
66
+ const next = argv[i + 1];
67
+ if (next === undefined || byFlag.has(next)) {
68
+ throw new ParseError(`argument ${flag}: expected one argument`);
69
+ }
70
+ value = next;
71
+ i++;
72
+ }
73
+ if (opt.choices && !opt.choices.includes(value)) {
74
+ throw new ParseError(`argument ${flag}: invalid choice: '${value}' (choose from ${opt.choices
75
+ .map((c) => `'${c}'`)
76
+ .join(", ")})`);
77
+ }
78
+ out[opt.dest] = value;
79
+ seen.add(opt.dest);
80
+ }
81
+ for (const group of spec.mutuallyExclusive ?? []) {
82
+ const present = group.dests.filter((d) => seen.has(d));
83
+ if (present.length > 1) {
84
+ const names = group.dests.map((d) => flagFor(spec, d)).join(" / ");
85
+ throw new ParseError(`arguments ${names}: not allowed with each other`);
86
+ }
87
+ if (group.required && present.length === 0) {
88
+ const names = group.dests.map((d) => flagFor(spec, d)).join(" / ");
89
+ throw new ParseError(`one of the arguments ${names} is required`);
90
+ }
91
+ }
92
+ for (const positional of positionals) {
93
+ if (!seen.has(positional.dest) && positional.default !== undefined) {
94
+ out[positional.dest] = positional.default;
95
+ }
96
+ }
97
+ for (const opt of spec.options) {
98
+ if (opt.required && !seen.has(opt.dest)) {
99
+ throw new ParseError(`the following arguments are required: ${opt.flags.join("/")}`);
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+ const flagFor = (spec, dest) => spec.options.find((o) => o.dest === dest)?.flags.join("/") ?? dest;
105
+ export function renderCommandHelp(spec) {
106
+ const lines = [];
107
+ const positionals = spec.positionals ?? [];
108
+ const usageTail = positionals.map((p) => ` [${p.metavar}]`).join("");
109
+ lines.push(bold(`usage: ml-dash ${spec.name} [options]${usageTail}`));
110
+ lines.push("");
111
+ if (spec.description) {
112
+ lines.push(spec.description.trimEnd());
113
+ lines.push("");
114
+ }
115
+ if (positionals.length > 0) {
116
+ lines.push(bold("positional arguments:"));
117
+ const width = Math.max(...positionals.map((p) => p.metavar.length));
118
+ for (const p of positionals)
119
+ lines.push(` ${p.metavar.padEnd(width)} ${p.help}`);
120
+ lines.push("");
121
+ }
122
+ lines.push(bold("options:"));
123
+ const rendered = spec.options.map((o) => {
124
+ const value = o.boolean ? "" : ` ${o.metavar ?? o.dest.toUpperCase()}`;
125
+ return [o.flags.join(", ") + value, o.help];
126
+ });
127
+ const width = Math.max(...rendered.map(([l]) => l.length), "-h, --help".length);
128
+ lines.push(` ${"-h, --help".padEnd(width)} show this help message and exit`);
129
+ for (const [left, help] of rendered)
130
+ lines.push(` ${left.padEnd(width)} ${help}`);
131
+ return lines.join("\n");
132
+ }
133
+ export function renderRootHelp(commands, available) {
134
+ const lines = [];
135
+ lines.push(bold("usage: ml-dash COMMAND [options]"));
136
+ lines.push("");
137
+ lines.push("ML-Dash: ML experiment tracking and data storage CLI");
138
+ lines.push("");
139
+ lines.push("View your experiments, statistics, and plots online at:");
140
+ lines.push(" https://dash.ml");
141
+ lines.push("");
142
+ lines.push(bold("commands:"));
143
+ const width = Math.max(...commands.map((c) => c.name.length));
144
+ for (const c of commands) {
145
+ if (!available.includes(c.name))
146
+ continue;
147
+ lines.push(` ${c.name.padEnd(width)} ${c.help}`);
148
+ }
149
+ lines.push("");
150
+ lines.push("Run 'ml-dash COMMAND --help' for command-specific options.");
151
+ return lines.join("\n");
152
+ }
153
+ export const usageError = (command, message) => `${red("error:")} ${message}\n\nRun 'ml-dash ${command} --help' for usage.`;