@buildinternet/uploads 0.34.1 → 0.35.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/README.md +7 -0
- package/dist/cli-args.d.ts +10 -1
- package/dist/cli-args.js +9 -1
- package/dist/cli-help.d.ts +18 -0
- package/dist/cli-help.js +18 -0
- package/dist/cli-suggest.d.ts +8 -0
- package/dist/cli-suggest.js +151 -0
- package/dist/cli.js +28 -9
- package/dist/commands/annotate.js +3 -2
- package/dist/commands/config.js +6 -5
- package/dist/commands/screenshot.js +3 -2
- package/dist/commands.js +71 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,13 @@ Commands: `attach`, `put`, `screenshot`, `annotate`, `gallery`, `comment`, `list
|
|
|
50
50
|
`uploads help --all` (or `--help --all`) for the full command list. Per-command:
|
|
51
51
|
`uploads <cmd> --help`.
|
|
52
52
|
|
|
53
|
+
**Errors:** every failure prints a short `error:` line on stderr, followed by a
|
|
54
|
+
runnable example when an argument is missing, then a hint — never a help dump,
|
|
55
|
+
so trimmed output (`| tail`) still carries the reason.
|
|
56
|
+
A mistyped command also suggests the closest real one: `uploads set-metadata`
|
|
57
|
+
answers `did you mean: uploads meta set`. With `--json`, an unknown command
|
|
58
|
+
returns `{ "error", "code": "USAGE", "didYouMean" }` on stdout.
|
|
59
|
+
|
|
53
60
|
**Shell completion:** `uploads completion bash|zsh|fish` prints a script to
|
|
54
61
|
stdout. Example (zsh): `uploads completion zsh > ~/.zsh/completions/_uploads`.
|
|
55
62
|
|
package/dist/cli-args.d.ts
CHANGED
|
@@ -24,7 +24,16 @@ export declare function isHelpFlag(arg: string): boolean;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function parseArgv(argv: string[]): ParsedArgv;
|
|
26
26
|
export declare class UsageError extends Error {
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* A correct invocation to show under the error. Agents pattern-match on a
|
|
29
|
+
* runnable line far better than on prose, so every "you left out a required
|
|
30
|
+
* argument" error should carry one — the CLI prints it, and `--json`
|
|
31
|
+
* callers get it as `example`.
|
|
32
|
+
*/
|
|
33
|
+
readonly example?: string;
|
|
34
|
+
constructor(message: string, options?: {
|
|
35
|
+
example?: string;
|
|
36
|
+
});
|
|
28
37
|
}
|
|
29
38
|
export interface CommandFlags {
|
|
30
39
|
positionals: string[];
|
package/dist/cli-args.js
CHANGED
|
@@ -69,9 +69,17 @@ export function parseArgv(argv) {
|
|
|
69
69
|
return { globals, help, command: rest[0], rest };
|
|
70
70
|
}
|
|
71
71
|
export class UsageError extends Error {
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* A correct invocation to show under the error. Agents pattern-match on a
|
|
74
|
+
* runnable line far better than on prose, so every "you left out a required
|
|
75
|
+
* argument" error should carry one — the CLI prints it, and `--json`
|
|
76
|
+
* callers get it as `example`.
|
|
77
|
+
*/
|
|
78
|
+
example;
|
|
79
|
+
constructor(message, options = {}) {
|
|
73
80
|
super(message);
|
|
74
81
|
this.name = "UsageError";
|
|
82
|
+
this.example = options.example;
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
/** Records a flag occurrence, turning a repeated string flag into an array. */
|
package/dist/cli-help.d.ts
CHANGED
|
@@ -17,6 +17,24 @@ export interface RootHelpOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
needsAuth?: boolean;
|
|
19
19
|
}
|
|
20
|
+
export interface UnknownCommandOptions {
|
|
21
|
+
/** The command the caller typed. */
|
|
22
|
+
command: string;
|
|
23
|
+
/** Suggested command phrase, from `suggestCommand`. */
|
|
24
|
+
suggestion?: string;
|
|
25
|
+
/** Catalog summary for the suggestion, shown beside it. */
|
|
26
|
+
summary?: string;
|
|
27
|
+
color?: boolean;
|
|
28
|
+
style?: CliStyle;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Unknown-command output: deliberately a handful of lines, not the root help
|
|
32
|
+
* dump (issue #545). Agents trim command output (`| tail -20`), and a 27-line
|
|
33
|
+
* banner pushed the one line that mattered — the error — out of the window.
|
|
34
|
+
* Short output survives truncation, and the pointers below still lead to the
|
|
35
|
+
* full list.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatUnknownCommand(options: UnknownCommandOptions): string;
|
|
20
38
|
/**
|
|
21
39
|
* Root help text. Default is a short essentials view; pass `full: true` for
|
|
22
40
|
* the complete command + config dump (`uploads help --all`).
|
package/dist/cli-help.js
CHANGED
|
@@ -175,6 +175,24 @@ ${style.muted("Tip: uploads help essentials only")}
|
|
|
175
175
|
${style.muted(" uploads help --all this full listing")}
|
|
176
176
|
`;
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Unknown-command output: deliberately a handful of lines, not the root help
|
|
180
|
+
* dump (issue #545). Agents trim command output (`| tail -20`), and a 27-line
|
|
181
|
+
* banner pushed the one line that mattered — the error — out of the window.
|
|
182
|
+
* Short output survives truncation, and the pointers below still lead to the
|
|
183
|
+
* full list.
|
|
184
|
+
*/
|
|
185
|
+
export function formatUnknownCommand(options) {
|
|
186
|
+
const style = options.style ??
|
|
187
|
+
createStyle(options.color !== undefined ? options.color : colorEnabled(process.stderr));
|
|
188
|
+
const lines = [style.error(`unknown command: ${options.command}`)];
|
|
189
|
+
if (options.suggestion) {
|
|
190
|
+
const summary = options.summary ? ` ${style.body(options.summary)}` : "";
|
|
191
|
+
lines.push("", `did you mean: ${style.command(`uploads ${options.suggestion}`)}${summary}`);
|
|
192
|
+
}
|
|
193
|
+
lines.push("", ` ${padCmd("uploads help --all", CMD_WIDTH, style)}${style.body("Full command list")}`, ` ${padCmd("uploads <cmd> --help", CMD_WIDTH, style)}${style.body("Per-command options and examples")}`);
|
|
194
|
+
return `${lines.join("\n")}\n`;
|
|
195
|
+
}
|
|
178
196
|
/**
|
|
179
197
|
* Root help text. Default is a short essentials view; pass `full: true` for
|
|
180
198
|
* the complete command + config dump (`uploads help --all`).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command phrase a mistyped command most likely meant, or undefined when
|
|
3
|
+
* nothing is close enough to be worth printing. Aliases win over distance so
|
|
4
|
+
* `metadata` resolves to `meta set` rather than the bare `meta` group.
|
|
5
|
+
*/
|
|
6
|
+
export declare function suggestCommand(input: string): string | undefined;
|
|
7
|
+
/** Catalog summary for a suggested phrase (`meta set` → the subcommand's). */
|
|
8
|
+
export declare function commandSummary(phrase: string): string | undefined;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Did you mean" for a mistyped root command (issue #545).
|
|
3
|
+
*
|
|
4
|
+
* The unknown-command path used to answer with the essentials list, which
|
|
5
|
+
* can't correct a wrong guess for anything outside it — `uploads set-metadata`
|
|
6
|
+
* printed a help dump that never mentions `meta`. Two sources feed a
|
|
7
|
+
* suggestion: a table of spellings agents actually reach for, and an edit
|
|
8
|
+
* distance over the catalog. Same rule as the metadata vocabulary
|
|
9
|
+
* (metadata-vocab.ts): suggest, never silently rewrite.
|
|
10
|
+
*/
|
|
11
|
+
import { ROOT_COMMANDS } from "./cli-catalog.js";
|
|
12
|
+
/**
|
|
13
|
+
* Wrong-but-natural spellings, mapped to the command phrase that does the
|
|
14
|
+
* job. Values are full phrases (`meta set`), not just root names, because the
|
|
15
|
+
* work an agent wants often lives on a subcommand.
|
|
16
|
+
*/
|
|
17
|
+
const COMMAND_ALIASES = {
|
|
18
|
+
// metadata — the case in issue #545
|
|
19
|
+
setmetadata: "meta set",
|
|
20
|
+
setmeta: "meta set",
|
|
21
|
+
metadata: "meta set",
|
|
22
|
+
metaset: "meta set",
|
|
23
|
+
tag: "meta set",
|
|
24
|
+
tags: "meta set",
|
|
25
|
+
label: "meta set",
|
|
26
|
+
getmetadata: "meta get",
|
|
27
|
+
getmeta: "meta get",
|
|
28
|
+
showmetadata: "meta get",
|
|
29
|
+
metaget: "meta get",
|
|
30
|
+
// upload
|
|
31
|
+
upload: "put",
|
|
32
|
+
uploadfile: "put",
|
|
33
|
+
cp: "put",
|
|
34
|
+
copy: "put",
|
|
35
|
+
send: "put",
|
|
36
|
+
// listing and search
|
|
37
|
+
ls: "list",
|
|
38
|
+
files: "list",
|
|
39
|
+
objects: "list",
|
|
40
|
+
search: "find",
|
|
41
|
+
query: "find",
|
|
42
|
+
filter: "find",
|
|
43
|
+
// removal
|
|
44
|
+
rm: "delete",
|
|
45
|
+
remove: "delete",
|
|
46
|
+
del: "delete",
|
|
47
|
+
destroy: "delete",
|
|
48
|
+
// capture
|
|
49
|
+
capture: "screenshot",
|
|
50
|
+
shot: "screenshot",
|
|
51
|
+
snap: "screenshot",
|
|
52
|
+
screengrab: "screenshot",
|
|
53
|
+
screencapture: "screenshot",
|
|
54
|
+
// session
|
|
55
|
+
signin: "login",
|
|
56
|
+
auth: "login",
|
|
57
|
+
authenticate: "login",
|
|
58
|
+
signout: "logout",
|
|
59
|
+
// github
|
|
60
|
+
pr: "attach",
|
|
61
|
+
issue: "attach",
|
|
62
|
+
upsertcomment: "comment",
|
|
63
|
+
};
|
|
64
|
+
/** Compare on letters and digits only, so `set-metadata` ≡ `set_metadata`. */
|
|
65
|
+
function normalize(value) {
|
|
66
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
67
|
+
}
|
|
68
|
+
/** Plain Levenshtein distance (two-row); inputs here are a few characters. */
|
|
69
|
+
function editDistance(a, b) {
|
|
70
|
+
if (a === b)
|
|
71
|
+
return 0;
|
|
72
|
+
if (a.length === 0)
|
|
73
|
+
return b.length;
|
|
74
|
+
if (b.length === 0)
|
|
75
|
+
return a.length;
|
|
76
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
77
|
+
for (let i = 1; i <= a.length; i++) {
|
|
78
|
+
const row = [i];
|
|
79
|
+
for (let j = 1; j <= b.length; j++) {
|
|
80
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
81
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + cost);
|
|
82
|
+
}
|
|
83
|
+
prev = row;
|
|
84
|
+
}
|
|
85
|
+
return prev[b.length];
|
|
86
|
+
}
|
|
87
|
+
/** Crude de-pluralizer, so `galleries` still reaches `gallery`. */
|
|
88
|
+
function singular(value) {
|
|
89
|
+
if (value.endsWith("ies") && value.length > 4)
|
|
90
|
+
return `${value.slice(0, -3)}y`;
|
|
91
|
+
if (value.endsWith("es") && value.length > 3)
|
|
92
|
+
return value.slice(0, -2);
|
|
93
|
+
if (value.endsWith("s") && value.length > 3)
|
|
94
|
+
return value.slice(0, -1);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
/** Short words tolerate one typo, longer ones two. */
|
|
98
|
+
function threshold(length) {
|
|
99
|
+
if (length <= 3)
|
|
100
|
+
return 0;
|
|
101
|
+
if (length <= 5)
|
|
102
|
+
return 1;
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The command phrase a mistyped command most likely meant, or undefined when
|
|
107
|
+
* nothing is close enough to be worth printing. Aliases win over distance so
|
|
108
|
+
* `metadata` resolves to `meta set` rather than the bare `meta` group.
|
|
109
|
+
*/
|
|
110
|
+
export function suggestCommand(input) {
|
|
111
|
+
const norm = normalize(input);
|
|
112
|
+
if (!norm)
|
|
113
|
+
return undefined;
|
|
114
|
+
const alias = COMMAND_ALIASES[norm];
|
|
115
|
+
if (alias)
|
|
116
|
+
return alias;
|
|
117
|
+
let best;
|
|
118
|
+
const consider = (candidate, phrase, allowContainment) => {
|
|
119
|
+
// A plural or a trailing qualifier (`screenshots`, `put-file`) is the same
|
|
120
|
+
// intent, not a typo — treat containment as a very strong match.
|
|
121
|
+
const contained = allowContainment &&
|
|
122
|
+
candidate.length >= 3 &&
|
|
123
|
+
(norm.startsWith(candidate) || candidate.startsWith(norm));
|
|
124
|
+
const distance = contained
|
|
125
|
+
? 0
|
|
126
|
+
: Math.min(editDistance(norm, candidate), editDistance(singular(norm), candidate));
|
|
127
|
+
if (!contained && distance > threshold(Math.max(norm.length, candidate.length)))
|
|
128
|
+
return;
|
|
129
|
+
if (!best || distance < best.distance)
|
|
130
|
+
best = { phrase, distance };
|
|
131
|
+
};
|
|
132
|
+
for (const cmd of ROOT_COMMANDS)
|
|
133
|
+
consider(normalize(cmd.name), cmd.name, true);
|
|
134
|
+
// Aliases join the fuzzy pass too, so a typo *of* a synonym (`uplaod`) still
|
|
135
|
+
// lands. No containment for these — an alias is a whole word, and `tag` as a
|
|
136
|
+
// prefix would swallow unrelated input.
|
|
137
|
+
for (const [spelling, phrase] of Object.entries(COMMAND_ALIASES)) {
|
|
138
|
+
consider(spelling, phrase, false);
|
|
139
|
+
}
|
|
140
|
+
return best?.phrase;
|
|
141
|
+
}
|
|
142
|
+
/** Catalog summary for a suggested phrase (`meta set` → the subcommand's). */
|
|
143
|
+
export function commandSummary(phrase) {
|
|
144
|
+
const [name, sub] = phrase.split(" ");
|
|
145
|
+
const cmd = ROOT_COMMANDS.find((c) => c.name === name);
|
|
146
|
+
if (!cmd)
|
|
147
|
+
return undefined;
|
|
148
|
+
if (!sub)
|
|
149
|
+
return cmd.summary;
|
|
150
|
+
return cmd.subcommands?.find((s) => s.name === sub)?.summary;
|
|
151
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,9 @@ import { createUploadsClient } from "./client.js";
|
|
|
2
2
|
import { DEFAULT_API_URL, resolveApiUrl, resolveConfig } from "./config.js";
|
|
3
3
|
import { UploadsError } from "./errors.js";
|
|
4
4
|
import { commandWorkspace, flagString, isHelpFlag, parseArgv, parseCommandArgs, UsageError, } from "./cli-args.js";
|
|
5
|
-
import { formatRootHelp, wantsFullHelp } from "./cli-help.js";
|
|
5
|
+
import { formatRootHelp, formatUnknownCommand, wantsFullHelp } from "./cli-help.js";
|
|
6
|
+
import { commandSummary, suggestCommand } from "./cli-suggest.js";
|
|
7
|
+
import { writeJson } from "./io.js";
|
|
6
8
|
import { colorEnabled, createStyle } from "./cli-style.js";
|
|
7
9
|
import { runPut, runAttach, runStaged, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
8
10
|
import { runConfig } from "./commands/config.js";
|
|
@@ -117,7 +119,7 @@ function errorOut(err, format) {
|
|
|
117
119
|
const payload = err instanceof UploadsError
|
|
118
120
|
? { error: err.message, code: err.code, status: err.status }
|
|
119
121
|
: err instanceof UsageError
|
|
120
|
-
? { error: err.message, code: "USAGE" }
|
|
122
|
+
? { error: err.message, code: "USAGE", ...(err.example ? { example: err.example } : {}) }
|
|
121
123
|
: { error: err instanceof Error ? err.message : String(err) };
|
|
122
124
|
if (format === "json") {
|
|
123
125
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
@@ -136,6 +138,11 @@ function errorOut(err, format) {
|
|
|
136
138
|
process.stderr.write(`${msg}\n`);
|
|
137
139
|
else
|
|
138
140
|
process.stderr.write(`error: ${msg}\n`);
|
|
141
|
+
// A runnable line beats prose for both agents and humans; the `--help`
|
|
142
|
+
// pointer that follows stays for the full option list.
|
|
143
|
+
if (err instanceof UsageError && err.example) {
|
|
144
|
+
process.stderr.write(` ${err.example}\n`);
|
|
145
|
+
}
|
|
139
146
|
if (err instanceof UploadsError) {
|
|
140
147
|
const hint = ERROR_HINTS[err.code];
|
|
141
148
|
if (hint)
|
|
@@ -353,13 +360,25 @@ export async function runCli(argv) {
|
|
|
353
360
|
break;
|
|
354
361
|
}
|
|
355
362
|
default: {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
+
// Short, greppable, and suggestion-first (issue #545) — never the root
|
|
364
|
+
// help dump, which buried the error when agents piped through `tail`.
|
|
365
|
+
const suggestion = suggestCommand(parsed.command);
|
|
366
|
+
const message = `unknown command: ${parsed.command}`;
|
|
367
|
+
if (json) {
|
|
368
|
+
await writeJson({
|
|
369
|
+
error: message,
|
|
370
|
+
code: "USAGE",
|
|
371
|
+
...(suggestion ? { didYouMean: suggestion } : {}),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
process.stderr.write(formatUnknownCommand({
|
|
376
|
+
command: parsed.command,
|
|
377
|
+
suggestion,
|
|
378
|
+
summary: suggestion ? commandSummary(suggestion) : undefined,
|
|
379
|
+
style: createStyle(colorEnabled(process.stderr)),
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
363
382
|
flushTelemetry(2);
|
|
364
383
|
return 2;
|
|
365
384
|
}
|
|
@@ -48,8 +48,9 @@ readStdinImpl = readStdin) {
|
|
|
48
48
|
}
|
|
49
49
|
const imagePath = parsed.positionals[0];
|
|
50
50
|
if (!imagePath) {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
throw new UsageError("annotate requires an image path", {
|
|
52
|
+
example: "uploads annotate ./shot.png --spec ./callouts.json",
|
|
53
|
+
});
|
|
53
54
|
}
|
|
54
55
|
if (parsed.positionals.length > 1) {
|
|
55
56
|
throw new UsageError("annotate takes exactly one image argument");
|
package/dist/commands/config.js
CHANGED
|
@@ -59,9 +59,9 @@ export async function runConfig(args, opts, help = false) {
|
|
|
59
59
|
case "set":
|
|
60
60
|
return runConfigSet(rest, subArgs, opts, help);
|
|
61
61
|
default:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Short, not a help dump: agents trim output, and a dump pushes the
|
|
63
|
+
// reason out of the window (issue #545).
|
|
64
|
+
throw new UsageError(`unknown config subcommand: ${sub} (expected path, show, init, or set)`, { example: "uploads config show" });
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
async function runConfigPath(args, opts, help) {
|
|
@@ -191,8 +191,9 @@ Examples:
|
|
|
191
191
|
const key = positionals[0];
|
|
192
192
|
const value = positionals[1];
|
|
193
193
|
if (!key || !value) {
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
throw new UsageError("config set requires a key and a value", {
|
|
195
|
+
example: "uploads config set UPLOADS_WORKSPACE myteam",
|
|
196
|
+
});
|
|
196
197
|
}
|
|
197
198
|
if (!VALID_KEYS.has(key)) {
|
|
198
199
|
throw new UsageError(`unknown key: ${key} (expected ${[...VALID_KEYS].join(", ")})`);
|
|
@@ -156,8 +156,9 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
156
156
|
}
|
|
157
157
|
const target = parsed.positionals[0];
|
|
158
158
|
if (!target) {
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
throw new UsageError("screenshot requires a target URL or .html file", {
|
|
160
|
+
example: "uploads screenshot http://localhost:4321/settings --out settings.png",
|
|
161
|
+
});
|
|
161
162
|
}
|
|
162
163
|
if (parsed.positionals.length > 1) {
|
|
163
164
|
throw new UsageError("screenshot takes exactly one target");
|
package/dist/commands.js
CHANGED
|
@@ -941,8 +941,9 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
941
941
|
// file args) would silently print help instead of a clear UsageError.
|
|
942
942
|
const branchArg = branchFromFlags(parsed.flags, run);
|
|
943
943
|
if (parsed.positionals.length === 0) {
|
|
944
|
-
|
|
945
|
-
|
|
944
|
+
throw new UsageError("attach requires at least one file", {
|
|
945
|
+
example: "uploads attach ./shot.png --pr 123",
|
|
946
|
+
});
|
|
946
947
|
}
|
|
947
948
|
if (branchArg !== undefined) {
|
|
948
949
|
if (parsed.flags.has("--pr"))
|
|
@@ -1523,8 +1524,9 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1523
1524
|
}
|
|
1524
1525
|
const files = parsed.positionals;
|
|
1525
1526
|
if (files.length === 0) {
|
|
1526
|
-
|
|
1527
|
-
|
|
1527
|
+
throw new UsageError("put requires at least one file", {
|
|
1528
|
+
example: "uploads put ./shot.png --pr 123",
|
|
1529
|
+
});
|
|
1528
1530
|
}
|
|
1529
1531
|
const multi = files.length > 1;
|
|
1530
1532
|
const keyHint = flagString(parsed.flags, "--key");
|
|
@@ -1950,15 +1952,21 @@ function githubCoordinateFromFlags(flags) {
|
|
|
1950
1952
|
export async function runGallery(ctx, args, help = false) {
|
|
1951
1953
|
const parsed = parseCommandArgs(args);
|
|
1952
1954
|
const action = parsed.positionals[0];
|
|
1953
|
-
if (help || parsed.help
|
|
1955
|
+
if (help || parsed.help) {
|
|
1954
1956
|
writeCommandHelp(GALLERY_HELP);
|
|
1955
|
-
return
|
|
1957
|
+
return 0;
|
|
1958
|
+
}
|
|
1959
|
+
if (!action) {
|
|
1960
|
+
throw new UsageError("gallery requires a subcommand: create, show, list, delete, add, link, or unlink", { example: 'uploads gallery create --title "Release screenshots"' });
|
|
1956
1961
|
}
|
|
1957
1962
|
switch (action) {
|
|
1958
1963
|
case "create": {
|
|
1959
1964
|
const title = flagString(parsed.flags, "--title");
|
|
1960
|
-
if (!title)
|
|
1961
|
-
throw new UsageError("gallery create requires --title"
|
|
1965
|
+
if (!title) {
|
|
1966
|
+
throw new UsageError("gallery create requires --title", {
|
|
1967
|
+
example: 'uploads gallery create --title "Release screenshots"',
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1962
1970
|
const gallery = await ctx.client.createGallery({
|
|
1963
1971
|
title,
|
|
1964
1972
|
description: flagString(parsed.flags, "--description"),
|
|
@@ -2122,7 +2130,7 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
2122
2130
|
return failures.length === 0 ? 0 : 1;
|
|
2123
2131
|
}
|
|
2124
2132
|
default:
|
|
2125
|
-
throw new UsageError(`unknown gallery command: ${action}
|
|
2133
|
+
throw new UsageError(`unknown gallery command: ${action} (expected create, show, list, delete, add, link, or unlink)`, { example: 'uploads gallery create --title "Release screenshots"' });
|
|
2126
2134
|
}
|
|
2127
2135
|
}
|
|
2128
2136
|
// --- list ---
|
|
@@ -2214,11 +2222,12 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
2214
2222
|
const FIND_HELP = `uploads find k=v [k=v...] [--prefix <p>] [--limit <n>] [--workspace <name>]
|
|
2215
2223
|
|
|
2216
2224
|
Human-friendly alias for \`uploads list --meta k=v...\` — same metadata filter
|
|
2217
|
-
(ANDed equality), same output; pairs are positional
|
|
2225
|
+
(ANDed equality), same output; pairs are positional, or spelled --meta k=v.
|
|
2218
2226
|
|
|
2219
2227
|
Examples:
|
|
2220
2228
|
uploads find gh.repo=buildinternet/uploads gh.number=123
|
|
2221
2229
|
uploads find path=/settings state=after --prefix screenshots/
|
|
2230
|
+
uploads find --meta path=/settings
|
|
2222
2231
|
`;
|
|
2223
2232
|
export async function runFind(ctx, args, help = false) {
|
|
2224
2233
|
const parsed = parseCommandArgs(args);
|
|
@@ -2226,11 +2235,15 @@ export async function runFind(ctx, args, help = false) {
|
|
|
2226
2235
|
writeCommandHelp(FIND_HELP);
|
|
2227
2236
|
return 0;
|
|
2228
2237
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2238
|
+
// Same flag/positional symmetry as `meta set` (issue #545): `find` is the
|
|
2239
|
+
// alias for `list --meta`, so `find --meta k=v` must not dead-end.
|
|
2240
|
+
const pairs = [...parsed.positionals, ...flagValues(parsed.flags, "--meta")];
|
|
2241
|
+
if (pairs.length === 0) {
|
|
2242
|
+
throw new UsageError("find requires at least one k=v pair (or --meta k=v)", {
|
|
2243
|
+
example: "uploads find path=/settings state=after",
|
|
2244
|
+
});
|
|
2232
2245
|
}
|
|
2233
|
-
const filters = parseMetaFlags(
|
|
2246
|
+
const filters = parseMetaFlags(pairs);
|
|
2234
2247
|
return runFindFiles(ctx, filters, parsed.flags);
|
|
2235
2248
|
}
|
|
2236
2249
|
// --- meta ---
|
|
@@ -2243,23 +2256,35 @@ Commands:
|
|
|
2243
2256
|
get <key> Show metadata for an object
|
|
2244
2257
|
set <key> k=v [k=v...] [--delete k]... Merge-set and/or delete pairs
|
|
2245
2258
|
|
|
2259
|
+
Pairs take either form: positional k=v, or --meta k=v (same spelling as
|
|
2260
|
+
put/screenshot/list). Both can appear in one call.
|
|
2261
|
+
|
|
2246
2262
|
Examples:
|
|
2247
2263
|
uploads meta get screenshots/myapp/42/shot.png
|
|
2248
2264
|
uploads meta set screenshots/myapp/42/shot.png path=/settings state=after
|
|
2265
|
+
uploads meta set screenshots/myapp/42/shot.png --meta path=/settings
|
|
2249
2266
|
uploads meta set screenshots/myapp/42/shot.png --delete path --delete state
|
|
2250
2267
|
`;
|
|
2251
2268
|
export async function runMeta(ctx, args, help = false) {
|
|
2252
2269
|
const parsed = parseCommandArgs(args);
|
|
2253
2270
|
const action = parsed.positionals[0];
|
|
2254
|
-
if (help || parsed.help
|
|
2271
|
+
if (help || parsed.help) {
|
|
2255
2272
|
writeCommandHelp(META_HELP);
|
|
2256
|
-
return
|
|
2273
|
+
return 0;
|
|
2274
|
+
}
|
|
2275
|
+
if (!action) {
|
|
2276
|
+
throw new UsageError("meta requires a subcommand: get or set", {
|
|
2277
|
+
example: "uploads meta set screenshots/myapp/42/shot.png --meta path=/settings",
|
|
2278
|
+
});
|
|
2257
2279
|
}
|
|
2258
2280
|
switch (action) {
|
|
2259
2281
|
case "get": {
|
|
2260
2282
|
const key = parsed.positionals[1];
|
|
2261
|
-
if (!key)
|
|
2262
|
-
throw new UsageError("meta get requires an object key"
|
|
2283
|
+
if (!key) {
|
|
2284
|
+
throw new UsageError("meta get requires an object key", {
|
|
2285
|
+
example: "uploads meta get screenshots/myapp/42/shot.png",
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2263
2288
|
const result = await ctx.client.getMetadata(key);
|
|
2264
2289
|
if (ctx.json)
|
|
2265
2290
|
await writeJson(result);
|
|
@@ -2275,12 +2300,22 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
2275
2300
|
}
|
|
2276
2301
|
case "set": {
|
|
2277
2302
|
const key = parsed.positionals[1];
|
|
2278
|
-
if (!key)
|
|
2279
|
-
throw new UsageError("meta set requires an object key"
|
|
2280
|
-
|
|
2303
|
+
if (!key) {
|
|
2304
|
+
throw new UsageError("meta set requires an object key", {
|
|
2305
|
+
example: "uploads meta set screenshots/myapp/42/shot.png --meta path=/settings",
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
// `--meta k=v` is accepted alongside the positional form: `put`, `list`,
|
|
2309
|
+
// and `screenshot` all spell metadata that way, and `put`'s own success
|
|
2310
|
+
// tip teaches the flag, so carrying it here is the natural guess
|
|
2311
|
+
// (issue #545). Positionals come first so argument order still reads
|
|
2312
|
+
// left to right when both are used.
|
|
2313
|
+
const pairs = [...parsed.positionals.slice(2), ...flagValues(parsed.flags, "--meta")];
|
|
2281
2314
|
const del = flagValues(parsed.flags, "--delete");
|
|
2282
2315
|
if (pairs.length === 0 && del.length === 0) {
|
|
2283
|
-
throw new UsageError("meta set requires k=v pairs and/or --delete <key>"
|
|
2316
|
+
throw new UsageError("meta set requires k=v pairs and/or --delete <key>", {
|
|
2317
|
+
example: `uploads meta set ${key} --meta path=/settings`,
|
|
2318
|
+
});
|
|
2284
2319
|
}
|
|
2285
2320
|
const set = pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
2286
2321
|
const result = await ctx.client.patchMetadata(key, {
|
|
@@ -2296,7 +2331,9 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
2296
2331
|
return 0;
|
|
2297
2332
|
}
|
|
2298
2333
|
default:
|
|
2299
|
-
throw new UsageError(`unknown meta command: ${action}
|
|
2334
|
+
throw new UsageError(`unknown meta command: ${action} (expected get or set)`, {
|
|
2335
|
+
example: "uploads meta get screenshots/myapp/42/shot.png",
|
|
2336
|
+
});
|
|
2300
2337
|
}
|
|
2301
2338
|
}
|
|
2302
2339
|
/** The metadata keys the managed comment renders (path/state, PR #370). */
|
|
@@ -2356,8 +2393,9 @@ export async function runDelete(ctx, args, help = false) {
|
|
|
2356
2393
|
}
|
|
2357
2394
|
const key = parsed.positionals[0];
|
|
2358
2395
|
if (!key) {
|
|
2359
|
-
|
|
2360
|
-
|
|
2396
|
+
throw new UsageError("delete requires an object key", {
|
|
2397
|
+
example: "uploads delete screenshots/myapp/42/shot.png --dry-run",
|
|
2398
|
+
});
|
|
2361
2399
|
}
|
|
2362
2400
|
if (flagBool(parsed.flags, "--dry-run")) {
|
|
2363
2401
|
if (ctx.json)
|
|
@@ -2397,8 +2435,11 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
|
2397
2435
|
return 0;
|
|
2398
2436
|
}
|
|
2399
2437
|
const target = ghTargetFromFlags(parsed.flags, run);
|
|
2400
|
-
if (!target)
|
|
2401
|
-
throw new UsageError("comment requires --pr or --issue"
|
|
2438
|
+
if (!target) {
|
|
2439
|
+
throw new UsageError("comment requires --pr or --issue", {
|
|
2440
|
+
example: "uploads comment --pr 123",
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2402
2443
|
const result = await syncAttachmentsComment(ctx.client, target, run, ctx.config.workspace, {
|
|
2403
2444
|
resync: true,
|
|
2404
2445
|
});
|
|
@@ -2568,7 +2609,9 @@ export async function runGithub(ctx, args, help = false, run = execRunner) {
|
|
|
2568
2609
|
return help || parsed.help ? 0 : 2;
|
|
2569
2610
|
}
|
|
2570
2611
|
if (action !== "link" && action !== "unlink" && action !== "doctor") {
|
|
2571
|
-
throw new UsageError(`unknown github subcommand: ${action}
|
|
2612
|
+
throw new UsageError(`unknown github subcommand: ${action} (expected link or doctor)`, {
|
|
2613
|
+
example: "uploads github link",
|
|
2614
|
+
});
|
|
2572
2615
|
}
|
|
2573
2616
|
if (action === "doctor")
|
|
2574
2617
|
return runGithubDoctor(ctx);
|