@buildinternet/uploads 0.34.0 → 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 +73 -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");
|
|
@@ -1882,6 +1884,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1882
1884
|
optimize: result.optimize,
|
|
1883
1885
|
frame: result.frame,
|
|
1884
1886
|
gallery,
|
|
1887
|
+
comment,
|
|
1888
|
+
commentError,
|
|
1885
1889
|
...(dryRun ? { dryRun: true } : {}),
|
|
1886
1890
|
...(jsonHint ? { hint: jsonHint } : {}),
|
|
1887
1891
|
});
|
|
@@ -1948,15 +1952,21 @@ function githubCoordinateFromFlags(flags) {
|
|
|
1948
1952
|
export async function runGallery(ctx, args, help = false) {
|
|
1949
1953
|
const parsed = parseCommandArgs(args);
|
|
1950
1954
|
const action = parsed.positionals[0];
|
|
1951
|
-
if (help || parsed.help
|
|
1955
|
+
if (help || parsed.help) {
|
|
1952
1956
|
writeCommandHelp(GALLERY_HELP);
|
|
1953
|
-
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"' });
|
|
1954
1961
|
}
|
|
1955
1962
|
switch (action) {
|
|
1956
1963
|
case "create": {
|
|
1957
1964
|
const title = flagString(parsed.flags, "--title");
|
|
1958
|
-
if (!title)
|
|
1959
|
-
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
|
+
}
|
|
1960
1970
|
const gallery = await ctx.client.createGallery({
|
|
1961
1971
|
title,
|
|
1962
1972
|
description: flagString(parsed.flags, "--description"),
|
|
@@ -2120,7 +2130,7 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
2120
2130
|
return failures.length === 0 ? 0 : 1;
|
|
2121
2131
|
}
|
|
2122
2132
|
default:
|
|
2123
|
-
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"' });
|
|
2124
2134
|
}
|
|
2125
2135
|
}
|
|
2126
2136
|
// --- list ---
|
|
@@ -2212,11 +2222,12 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
2212
2222
|
const FIND_HELP = `uploads find k=v [k=v...] [--prefix <p>] [--limit <n>] [--workspace <name>]
|
|
2213
2223
|
|
|
2214
2224
|
Human-friendly alias for \`uploads list --meta k=v...\` — same metadata filter
|
|
2215
|
-
(ANDed equality), same output; pairs are positional
|
|
2225
|
+
(ANDed equality), same output; pairs are positional, or spelled --meta k=v.
|
|
2216
2226
|
|
|
2217
2227
|
Examples:
|
|
2218
2228
|
uploads find gh.repo=buildinternet/uploads gh.number=123
|
|
2219
2229
|
uploads find path=/settings state=after --prefix screenshots/
|
|
2230
|
+
uploads find --meta path=/settings
|
|
2220
2231
|
`;
|
|
2221
2232
|
export async function runFind(ctx, args, help = false) {
|
|
2222
2233
|
const parsed = parseCommandArgs(args);
|
|
@@ -2224,11 +2235,15 @@ export async function runFind(ctx, args, help = false) {
|
|
|
2224
2235
|
writeCommandHelp(FIND_HELP);
|
|
2225
2236
|
return 0;
|
|
2226
2237
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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
|
+
});
|
|
2230
2245
|
}
|
|
2231
|
-
const filters = parseMetaFlags(
|
|
2246
|
+
const filters = parseMetaFlags(pairs);
|
|
2232
2247
|
return runFindFiles(ctx, filters, parsed.flags);
|
|
2233
2248
|
}
|
|
2234
2249
|
// --- meta ---
|
|
@@ -2241,23 +2256,35 @@ Commands:
|
|
|
2241
2256
|
get <key> Show metadata for an object
|
|
2242
2257
|
set <key> k=v [k=v...] [--delete k]... Merge-set and/or delete pairs
|
|
2243
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
|
+
|
|
2244
2262
|
Examples:
|
|
2245
2263
|
uploads meta get screenshots/myapp/42/shot.png
|
|
2246
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
|
|
2247
2266
|
uploads meta set screenshots/myapp/42/shot.png --delete path --delete state
|
|
2248
2267
|
`;
|
|
2249
2268
|
export async function runMeta(ctx, args, help = false) {
|
|
2250
2269
|
const parsed = parseCommandArgs(args);
|
|
2251
2270
|
const action = parsed.positionals[0];
|
|
2252
|
-
if (help || parsed.help
|
|
2271
|
+
if (help || parsed.help) {
|
|
2253
2272
|
writeCommandHelp(META_HELP);
|
|
2254
|
-
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
|
+
});
|
|
2255
2279
|
}
|
|
2256
2280
|
switch (action) {
|
|
2257
2281
|
case "get": {
|
|
2258
2282
|
const key = parsed.positionals[1];
|
|
2259
|
-
if (!key)
|
|
2260
|
-
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
|
+
}
|
|
2261
2288
|
const result = await ctx.client.getMetadata(key);
|
|
2262
2289
|
if (ctx.json)
|
|
2263
2290
|
await writeJson(result);
|
|
@@ -2273,12 +2300,22 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
2273
2300
|
}
|
|
2274
2301
|
case "set": {
|
|
2275
2302
|
const key = parsed.positionals[1];
|
|
2276
|
-
if (!key)
|
|
2277
|
-
throw new UsageError("meta set requires an object key"
|
|
2278
|
-
|
|
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")];
|
|
2279
2314
|
const del = flagValues(parsed.flags, "--delete");
|
|
2280
2315
|
if (pairs.length === 0 && del.length === 0) {
|
|
2281
|
-
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
|
+
});
|
|
2282
2319
|
}
|
|
2283
2320
|
const set = pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
2284
2321
|
const result = await ctx.client.patchMetadata(key, {
|
|
@@ -2294,7 +2331,9 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
2294
2331
|
return 0;
|
|
2295
2332
|
}
|
|
2296
2333
|
default:
|
|
2297
|
-
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
|
+
});
|
|
2298
2337
|
}
|
|
2299
2338
|
}
|
|
2300
2339
|
/** The metadata keys the managed comment renders (path/state, PR #370). */
|
|
@@ -2354,8 +2393,9 @@ export async function runDelete(ctx, args, help = false) {
|
|
|
2354
2393
|
}
|
|
2355
2394
|
const key = parsed.positionals[0];
|
|
2356
2395
|
if (!key) {
|
|
2357
|
-
|
|
2358
|
-
|
|
2396
|
+
throw new UsageError("delete requires an object key", {
|
|
2397
|
+
example: "uploads delete screenshots/myapp/42/shot.png --dry-run",
|
|
2398
|
+
});
|
|
2359
2399
|
}
|
|
2360
2400
|
if (flagBool(parsed.flags, "--dry-run")) {
|
|
2361
2401
|
if (ctx.json)
|
|
@@ -2395,8 +2435,11 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
|
2395
2435
|
return 0;
|
|
2396
2436
|
}
|
|
2397
2437
|
const target = ghTargetFromFlags(parsed.flags, run);
|
|
2398
|
-
if (!target)
|
|
2399
|
-
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
|
+
}
|
|
2400
2443
|
const result = await syncAttachmentsComment(ctx.client, target, run, ctx.config.workspace, {
|
|
2401
2444
|
resync: true,
|
|
2402
2445
|
});
|
|
@@ -2566,7 +2609,9 @@ export async function runGithub(ctx, args, help = false, run = execRunner) {
|
|
|
2566
2609
|
return help || parsed.help ? 0 : 2;
|
|
2567
2610
|
}
|
|
2568
2611
|
if (action !== "link" && action !== "unlink" && action !== "doctor") {
|
|
2569
|
-
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
|
+
});
|
|
2570
2615
|
}
|
|
2571
2616
|
if (action === "doctor")
|
|
2572
2617
|
return runGithubDoctor(ctx);
|