@buildinternet/uploads 0.8.0 → 0.10.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 +25 -9
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.js +5 -0
- package/dist/cli-brand.d.ts +89 -0
- package/dist/cli-brand.js +166 -0
- package/dist/cli-catalog.d.ts +32 -0
- package/dist/cli-catalog.js +187 -0
- package/dist/cli-help.d.ts +26 -0
- package/dist/cli-help.js +176 -0
- package/dist/cli-style.d.ts +42 -0
- package/dist/cli-style.js +108 -0
- package/dist/cli.js +67 -73
- package/dist/client.d.ts +19 -0
- package/dist/client.js +31 -0
- package/dist/commands/admin-enrollment.js +2 -1
- package/dist/commands/completion.d.ts +3 -0
- package/dist/commands/completion.js +284 -0
- package/dist/commands/config.js +9 -7
- package/dist/commands/install.js +2 -1
- package/dist/commands/invite.js +15 -2
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +55 -10
- package/dist/commands/mcp.js +2 -1
- package/dist/commands/session.d.ts +31 -0
- package/dist/commands/session.js +140 -0
- package/dist/commands/setup.js +2 -1
- package/dist/commands.js +125 -24
- package/dist/config-file.d.ts +12 -1
- package/dist/config-file.js +49 -9
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/format-bytes.d.ts +5 -0
- package/dist/format-bytes.js +11 -0
- package/dist/github-gh.d.ts +7 -0
- package/dist/github-gh.js +24 -0
- package/dist/update-check.d.ts +10 -0
- package/dist/update-check.js +28 -12
- package/package.json +1 -1
package/dist/cli-help.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { DEFAULT_TAGLINE, formatAuthBanner, formatBrandHeader, formatUpdateBanner, } from "./cli-brand.js";
|
|
2
|
+
import { ROOT_COMMANDS } from "./cli-catalog.js";
|
|
3
|
+
import { colorEnabled, createStyle, padCmd } from "./cli-style.js";
|
|
4
|
+
import { packageVersion } from "./package-version.js";
|
|
5
|
+
const CMD_WIDTH = 22;
|
|
6
|
+
function toRow(c) {
|
|
7
|
+
return [c.usage ?? c.name, c.summary];
|
|
8
|
+
}
|
|
9
|
+
/** Preferred order for the short essentials help (subset of ROOT_COMMANDS). */
|
|
10
|
+
const ESSENTIAL_ORDER = [
|
|
11
|
+
"put",
|
|
12
|
+
"attach",
|
|
13
|
+
"login",
|
|
14
|
+
"whoami",
|
|
15
|
+
"list",
|
|
16
|
+
"delete",
|
|
17
|
+
"doctor",
|
|
18
|
+
"install",
|
|
19
|
+
];
|
|
20
|
+
/** Day-to-day commands shown on bare `uploads` / `uploads help`. */
|
|
21
|
+
const ESSENTIALS = ESSENTIAL_ORDER.map((name) => {
|
|
22
|
+
const cmd = ROOT_COMMANDS.find((c) => c.name === name);
|
|
23
|
+
if (!cmd)
|
|
24
|
+
throw new Error(`cli-catalog missing essential command: ${name}`);
|
|
25
|
+
return toRow(cmd);
|
|
26
|
+
});
|
|
27
|
+
/** Full catalog (same surface as before, still discoverable via help --all). */
|
|
28
|
+
const ALL_COMMANDS = ROOT_COMMANDS.map(toRow);
|
|
29
|
+
function rows(style, items) {
|
|
30
|
+
return items
|
|
31
|
+
.map(([name, desc]) => ` ${padCmd(name, CMD_WIDTH, style)}${style.body(desc)}`)
|
|
32
|
+
.join("\n");
|
|
33
|
+
}
|
|
34
|
+
function section(style, title) {
|
|
35
|
+
return style.heading(title);
|
|
36
|
+
}
|
|
37
|
+
function header(style, opts) {
|
|
38
|
+
const version = opts.version ?? packageVersion();
|
|
39
|
+
const brandMark = opts.brandMark !== false;
|
|
40
|
+
const parts = [];
|
|
41
|
+
// Auth first — loud and above everything when there's no token yet.
|
|
42
|
+
if (opts.needsAuth) {
|
|
43
|
+
parts.push(formatAuthBanner({ color: style.enabled }));
|
|
44
|
+
}
|
|
45
|
+
// Half-block mark when color is on. Plain three-line title otherwise so
|
|
46
|
+
// piped/agent output stays compact and greppable.
|
|
47
|
+
if (!brandMark || !style.enabled) {
|
|
48
|
+
parts.push(`${style.title("uploads.sh")}\n` +
|
|
49
|
+
`${style.muted(DEFAULT_TAGLINE)}\n` +
|
|
50
|
+
`${style.muted(`v${version}`)}\n`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
parts.push(formatBrandHeader({
|
|
54
|
+
color: true,
|
|
55
|
+
label: "uploads.sh",
|
|
56
|
+
tagline: DEFAULT_TAGLINE,
|
|
57
|
+
version,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
if (opts.latestVersion && opts.latestVersion !== version) {
|
|
61
|
+
parts.push(formatUpdateBanner({
|
|
62
|
+
current: version,
|
|
63
|
+
latest: opts.latestVersion,
|
|
64
|
+
color: style.enabled,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
return parts.join("");
|
|
68
|
+
}
|
|
69
|
+
function essentialsBody(style, opts) {
|
|
70
|
+
return `${header(style, opts)}
|
|
71
|
+
${section(style, "Usage:")}
|
|
72
|
+
uploads [globals] <command> [args]
|
|
73
|
+
|
|
74
|
+
${section(style, "Essentials:")}
|
|
75
|
+
${rows(style, ESSENTIALS)}
|
|
76
|
+
|
|
77
|
+
${section(style, "More help:")}
|
|
78
|
+
${padCmd("uploads help --all", CMD_WIDTH, style)}${style.body("Full command list, globals, and config")}
|
|
79
|
+
${padCmd("uploads <cmd> --help", CMD_WIDTH, style)}${style.body("Per-command options and examples")}
|
|
80
|
+
|
|
81
|
+
${section(style, "Globals (before command):")}
|
|
82
|
+
${style.muted("--api-url, --token, --workspace/-w, --env-file, --json, --quiet, --version/-V")}
|
|
83
|
+
|
|
84
|
+
${section(style, "Examples:")}
|
|
85
|
+
${style.command("uploads login")}
|
|
86
|
+
${style.command("uploads whoami")}
|
|
87
|
+
${style.command("uploads put")} ./shot.png --pr 123 --name hero.png
|
|
88
|
+
${style.command("uploads put")} ./after.png --pr 123 --comment
|
|
89
|
+
${style.command("uploads put")} ./bug.png --issue 45
|
|
90
|
+
${style.command("uploads put")} ./shot.png --meta app=myapp --meta page=settings
|
|
91
|
+
${style.command("uploads attach")} ./before.png ./after.png
|
|
92
|
+
${style.command("uploads attach")} ./shot.png --pr 123 --repo myorg/myapp
|
|
93
|
+
${style.command("uploads attach")} ./shot.png --meta app=myapp --meta page=settings
|
|
94
|
+
${style.command("uploads doctor")}
|
|
95
|
+
${style.command("uploads install")}
|
|
96
|
+
${style.command("uploads logout")}
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
function fullBody(style, opts) {
|
|
100
|
+
return `${header(style, opts)}
|
|
101
|
+
${section(style, "Usage:")}
|
|
102
|
+
uploads [globals] <command> [args]
|
|
103
|
+
|
|
104
|
+
${section(style, "Config")} ${style.muted("(first match wins, per key):")}
|
|
105
|
+
CLI flags --api-url, --token, --workspace
|
|
106
|
+
environment UPLOADS_API_URL, UPLOADS_TOKEN, UPLOADS_WORKSPACE
|
|
107
|
+
--env-file <path>
|
|
108
|
+
$BUILDINTERNET_CONFIG
|
|
109
|
+
~/.config/buildinternet/config
|
|
110
|
+
|
|
111
|
+
${section(style, "Workspace")} ${style.muted("(within config layers):")}
|
|
112
|
+
--workspace, -w override — global (before command) or per-command (after)
|
|
113
|
+
UPLOADS_WORKSPACE env / config file
|
|
114
|
+
(else inferred from token up_<name>_…, else "default")
|
|
115
|
+
|
|
116
|
+
${section(style, "Other globals")} ${style.muted("(before command):")}
|
|
117
|
+
--api-url <url> default: https://api.uploads.sh
|
|
118
|
+
--token <token> or UPLOADS_TOKEN
|
|
119
|
+
--env-file <path>
|
|
120
|
+
--json JSON on stdout
|
|
121
|
+
--quiet Suppress stderr progress and update hints
|
|
122
|
+
--version, -V Print package version and exit
|
|
123
|
+
|
|
124
|
+
${section(style, "Commands:")}
|
|
125
|
+
${rows(style, ALL_COMMANDS)}
|
|
126
|
+
|
|
127
|
+
${section(style, "Put/list defaults")} ${style.muted("(config file or env):")}
|
|
128
|
+
UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
|
|
129
|
+
UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
|
|
130
|
+
|
|
131
|
+
${section(style, "Update hints")} ${style.muted("(stderr, once/day):")} silence with --quiet / UPLOADS_NO_UPDATE=1 / NO_UPDATE_NOTIFIER=1
|
|
132
|
+
|
|
133
|
+
${section(style, "Examples:")}
|
|
134
|
+
${style.command("uploads login")}
|
|
135
|
+
${style.command("uploads whoami")}
|
|
136
|
+
${style.command("uploads put")} ./shot.png --pr 123 --name hero.png
|
|
137
|
+
${style.command("uploads put")} ./after.png --pr 123 --comment
|
|
138
|
+
${style.command("uploads put")} ./bug.png --issue 45 --repo myorg/myapp
|
|
139
|
+
${style.command("uploads put")} ./shot.png --dry-run --format url
|
|
140
|
+
${style.command("uploads put")} ./shot.png --meta app=myapp --meta page=settings
|
|
141
|
+
${style.command("uploads attach")} ./before.png ./after.png
|
|
142
|
+
${style.command("uploads attach")} ./shot.png --pr 123 --repo myorg/myapp
|
|
143
|
+
${style.command("uploads attach")} ./artifact.zip --issue 45 --no-comment
|
|
144
|
+
${style.command("uploads attach")} ./shot.png --meta app=myapp --meta page=settings
|
|
145
|
+
${style.command("uploads gallery")} create --title "Release screenshots"
|
|
146
|
+
${style.command("uploads doctor")}
|
|
147
|
+
${style.command("uploads logout")}
|
|
148
|
+
${style.command("uploads --version")}
|
|
149
|
+
|
|
150
|
+
${section(style, "Agent/MCP:")} ${style.body("`uploads install` sets up the agent skill and the hosted MCP server")}
|
|
151
|
+
${style.body("(https://agents.uploads.sh/mcp, workspace inferred from the token). Run")}
|
|
152
|
+
${style.body("`uploads mcp` for local stdio, or use createUploadsWorkerFileTools()")}
|
|
153
|
+
${style.body("from @buildinternet/uploads/agent on the Worker.")}
|
|
154
|
+
|
|
155
|
+
${style.muted("Tip: uploads help essentials only")}
|
|
156
|
+
${style.muted(" uploads help --all this full listing")}
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Root help text. Default is a short essentials view; pass `full: true` for
|
|
161
|
+
* the complete command + config dump (`uploads help --all`).
|
|
162
|
+
*/
|
|
163
|
+
export function formatRootHelp(options = {}) {
|
|
164
|
+
const style = options.style ??
|
|
165
|
+
createStyle(options.color !== undefined ? options.color : colorEnabled(process.stderr));
|
|
166
|
+
const body = options.full ? fullBody(style, options) : essentialsBody(style, options);
|
|
167
|
+
return body.endsWith("\n") ? body : `${body}\n`;
|
|
168
|
+
}
|
|
169
|
+
/** True when argv for the `help` command requests the full listing. */
|
|
170
|
+
export function wantsFullHelp(args) {
|
|
171
|
+
for (const a of args) {
|
|
172
|
+
if (a === "--all" || a === "-a" || a === "all")
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ANSI styling for CLI help hierarchy.
|
|
3
|
+
* Accent palette matches packages/ui tokens (truecolor when enabled).
|
|
4
|
+
* Honors NO_COLOR / FORCE_COLOR and the target stream's TTY status.
|
|
5
|
+
* @see https://no-color.org/
|
|
6
|
+
* @see packages/ui/src/tokens.css
|
|
7
|
+
*/
|
|
8
|
+
export type StyleFn = (text: string) => string;
|
|
9
|
+
export interface CliStyle {
|
|
10
|
+
bold: StyleFn;
|
|
11
|
+
dim: StyleFn;
|
|
12
|
+
/** Section headings — brand accent violet */
|
|
13
|
+
heading: StyleFn;
|
|
14
|
+
/** Command / flag names — brand green */
|
|
15
|
+
command: StyleFn;
|
|
16
|
+
/** Muted secondary text — token muted gray */
|
|
17
|
+
muted: StyleFn;
|
|
18
|
+
/** Body / description text */
|
|
19
|
+
body: StyleFn;
|
|
20
|
+
/** High-emphasis title / wordmark */
|
|
21
|
+
title: StyleFn;
|
|
22
|
+
/** Errors / unknown-command banner — brand red */
|
|
23
|
+
error: StyleFn;
|
|
24
|
+
/** Brand accent (links, tips) */
|
|
25
|
+
accent: StyleFn;
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Whether color should be enabled for a given stream. */
|
|
29
|
+
export declare function colorEnabled(stream?: {
|
|
30
|
+
isTTY?: boolean;
|
|
31
|
+
}, env?: NodeJS.ProcessEnv): boolean;
|
|
32
|
+
export declare function createStyle(enabled: boolean): CliStyle;
|
|
33
|
+
/** Pad a left column so descriptions line up (ANSI-aware length). */
|
|
34
|
+
export declare function padCmd(name: string, width: number, style: CliStyle): string;
|
|
35
|
+
/**
|
|
36
|
+
* Apply root-help visual hierarchy to a plain multi-line command help string:
|
|
37
|
+
* accent section headers, green flags/commands, muted body.
|
|
38
|
+
* No-op when color is disabled (returns text unchanged aside from trailing newline).
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatCommandHelp(text: string, style?: CliStyle): string;
|
|
41
|
+
/** Write styled command help to stderr (or a custom writer). */
|
|
42
|
+
export declare function writeCommandHelp(text: string, write?: (chunk: string) => void): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ANSI styling for CLI help hierarchy.
|
|
3
|
+
* Accent palette matches packages/ui tokens (truecolor when enabled).
|
|
4
|
+
* Honors NO_COLOR / FORCE_COLOR and the target stream's TTY status.
|
|
5
|
+
* @see https://no-color.org/
|
|
6
|
+
* @see packages/ui/src/tokens.css
|
|
7
|
+
*/
|
|
8
|
+
import { BRAND } from "./cli-brand.js";
|
|
9
|
+
const identity = (t) => t;
|
|
10
|
+
const RESET = "\u001b[0m";
|
|
11
|
+
const BOLD = "\u001b[1m";
|
|
12
|
+
function fgTrue(c) {
|
|
13
|
+
return `\u001b[38;2;${c.r};${c.g};${c.b}m`;
|
|
14
|
+
}
|
|
15
|
+
function wrapRgb(c, bold = false) {
|
|
16
|
+
const open = (bold ? BOLD : "") + fgTrue(c);
|
|
17
|
+
return (text) => `${open}${text}${RESET}`;
|
|
18
|
+
}
|
|
19
|
+
/** Whether color should be enabled for a given stream. */
|
|
20
|
+
export function colorEnabled(stream = process.stderr, env = process.env) {
|
|
21
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "")
|
|
22
|
+
return false;
|
|
23
|
+
if (env.FORCE_COLOR === "0")
|
|
24
|
+
return false;
|
|
25
|
+
if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "")
|
|
26
|
+
return true;
|
|
27
|
+
return stream.isTTY === true;
|
|
28
|
+
}
|
|
29
|
+
export function createStyle(enabled) {
|
|
30
|
+
if (!enabled) {
|
|
31
|
+
return {
|
|
32
|
+
bold: identity,
|
|
33
|
+
dim: identity,
|
|
34
|
+
heading: identity,
|
|
35
|
+
command: identity,
|
|
36
|
+
muted: identity,
|
|
37
|
+
body: identity,
|
|
38
|
+
title: identity,
|
|
39
|
+
error: identity,
|
|
40
|
+
accent: identity,
|
|
41
|
+
enabled: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
bold: (text) => `${BOLD}${fgTrue(BRAND.fg)}${text}${RESET}`,
|
|
46
|
+
dim: (text) => `\u001b[2m${text}${RESET}`,
|
|
47
|
+
heading: wrapRgb(BRAND.accent, true),
|
|
48
|
+
command: wrapRgb(BRAND.green, true),
|
|
49
|
+
muted: wrapRgb(BRAND.muted),
|
|
50
|
+
body: wrapRgb(BRAND.body),
|
|
51
|
+
title: wrapRgb(BRAND.fg, true),
|
|
52
|
+
error: wrapRgb(BRAND.red, true),
|
|
53
|
+
accent: wrapRgb(BRAND.accent),
|
|
54
|
+
enabled: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Pad a left column so descriptions line up (ANSI-aware length). */
|
|
58
|
+
export function padCmd(name, width, style) {
|
|
59
|
+
const pad = Math.max(0, width - name.length);
|
|
60
|
+
return style.command(name) + " ".repeat(pad);
|
|
61
|
+
}
|
|
62
|
+
/** Section labels used across command --help blocks. */
|
|
63
|
+
const SECTION_RE = /^(Options|Examples|Commands|Subcommands|Keys|Shells|Usage|What it does|What runs under the hood|Exit codes|Config|Workspace):\s*$/;
|
|
64
|
+
/** Flag column: ` --pr <num> …` or ` --workspace, -w <name> …`. */
|
|
65
|
+
const FLAG_RE = /^(\s+)(-[\w-]+(?:,\s*-[\w-]+)?(?:\s+<[^>]+>)?)(\s{2,})(.*)$/;
|
|
66
|
+
/** Example / invocation line starting with the CLI name. */
|
|
67
|
+
const EXAMPLE_RE = /^(\s*)(uploads(?:\s+\S+)*)(.*)$/;
|
|
68
|
+
/**
|
|
69
|
+
* Apply root-help visual hierarchy to a plain multi-line command help string:
|
|
70
|
+
* accent section headers, green flags/commands, muted body.
|
|
71
|
+
* No-op when color is disabled (returns text unchanged aside from trailing newline).
|
|
72
|
+
*/
|
|
73
|
+
export function formatCommandHelp(text, style = createStyle(colorEnabled(process.stderr))) {
|
|
74
|
+
const normalized = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
75
|
+
if (!style.enabled)
|
|
76
|
+
return `${normalized}\n`;
|
|
77
|
+
const lines = normalized.split("\n");
|
|
78
|
+
let firstContent = true;
|
|
79
|
+
const out = lines.map((line) => {
|
|
80
|
+
if (line.trim() === "")
|
|
81
|
+
return line;
|
|
82
|
+
// Synopsis line (first non-empty): high emphasis
|
|
83
|
+
if (firstContent) {
|
|
84
|
+
firstContent = false;
|
|
85
|
+
return style.title(line);
|
|
86
|
+
}
|
|
87
|
+
if (SECTION_RE.test(line))
|
|
88
|
+
return style.heading(line);
|
|
89
|
+
const flag = FLAG_RE.exec(line);
|
|
90
|
+
if (flag) {
|
|
91
|
+
const [, indent, name, gap, desc] = flag;
|
|
92
|
+
return `${indent}${style.command(name)}${gap}${style.body(desc)}`;
|
|
93
|
+
}
|
|
94
|
+
const example = EXAMPLE_RE.exec(line);
|
|
95
|
+
if (example && line.trimStart().startsWith("uploads")) {
|
|
96
|
+
const [, indent, cmd, rest] = example;
|
|
97
|
+
return `${indent}${style.command(cmd)}${style.body(rest)}`;
|
|
98
|
+
}
|
|
99
|
+
return style.body(line);
|
|
100
|
+
});
|
|
101
|
+
return `${out.join("\n")}\n`;
|
|
102
|
+
}
|
|
103
|
+
/** Write styled command help to stderr (or a custom writer). */
|
|
104
|
+
export function writeCommandHelp(text, write = (c) => {
|
|
105
|
+
process.stderr.write(c);
|
|
106
|
+
}) {
|
|
107
|
+
write(formatCommandHelp(text));
|
|
108
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createUploadsClient } from "./client.js";
|
|
|
2
2
|
import { 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";
|
|
6
|
+
import { colorEnabled, createStyle } from "./cli-style.js";
|
|
5
7
|
import { runPut, runAttach, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
6
8
|
import { runConfig } from "./commands/config.js";
|
|
7
9
|
import { runSetup } from "./commands/setup.js";
|
|
@@ -10,75 +12,34 @@ import { runInvite } from "./commands/invite.js";
|
|
|
10
12
|
import { runAdmin } from "./commands/admin-enrollment.js";
|
|
11
13
|
import { runMcp } from "./commands/mcp.js";
|
|
12
14
|
import { runInstall } from "./commands/install.js";
|
|
15
|
+
import { runCompletion } from "./commands/completion.js";
|
|
16
|
+
import { runLogout, runWhoami } from "./commands/session.js";
|
|
13
17
|
import { packageVersion } from "./package-version.js";
|
|
14
|
-
import { maybeHintUpdate } from "./update-check.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Commands:
|
|
41
|
-
attach <file...> Attach media to the current PR (stable URLs + managed comment)
|
|
42
|
-
put <file> Upload (+ URL + markdown for GitHub)
|
|
43
|
-
gallery Create and organize public media galleries
|
|
44
|
-
comment Create/update a PR/issue attachments comment (via gh)
|
|
45
|
-
list List objects (--meta k=v filters by queryable metadata)
|
|
46
|
-
find k=v... List objects matching metadata (alias for list --meta)
|
|
47
|
-
meta Get/set an object's queryable metadata
|
|
48
|
-
delete <key> Delete object
|
|
49
|
-
usage Workspace storage / upload counters
|
|
50
|
-
reconcile Rebuild usage ledger from storage
|
|
51
|
-
purge-expired Delete objects past retentionDays
|
|
52
|
-
setup Inspect/configure advanced CLI settings
|
|
53
|
-
install Install the agent skill + register the remote MCP server
|
|
54
|
-
login Sign in via browser (or an enrollment code) and save credentials
|
|
55
|
-
invite Invite a teammate to a workspace (workspace admin; device login)
|
|
56
|
-
admin Site-operator invitation management (ADMIN_TOKEN)
|
|
57
|
-
config Show path, init, or set shared config
|
|
58
|
-
doctor Health + auth + workspace checks
|
|
59
|
-
health API liveness (no auth)
|
|
60
|
-
mcp Serve MCP over stdio (tools mirror the CLI)
|
|
61
|
-
|
|
62
|
-
Put/list defaults (config file or env):
|
|
63
|
-
UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
|
|
64
|
-
UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
|
|
65
|
-
|
|
66
|
-
Update hints (stderr, once/day): silence with --quiet / UPLOADS_NO_UPDATE=1 / NO_UPDATE_NOTIFIER=1
|
|
67
|
-
|
|
68
|
-
Examples:
|
|
69
|
-
uploads setup
|
|
70
|
-
uploads setup --token up_default_… --repo myorg/myapp
|
|
71
|
-
uploads attach ./before.png ./after.png
|
|
72
|
-
uploads put ./shot.png --ref 42
|
|
73
|
-
uploads gallery create --title "Release screenshots"
|
|
74
|
-
uploads doctor
|
|
75
|
-
uploads --version
|
|
76
|
-
|
|
77
|
-
Agent/MCP: \`uploads install\` sets up the agent skill and the hosted MCP server
|
|
78
|
-
(https://agents.uploads.sh/mcp, workspace inferred from the token). Run
|
|
79
|
-
\`uploads mcp\` for local stdio, or use createUploadsWorkerFileTools()
|
|
80
|
-
from @buildinternet/uploads/agent on the Worker.
|
|
81
|
-
`;
|
|
18
|
+
import { checkForUpdate, maybeHintUpdate } from "./update-check.js";
|
|
19
|
+
async function writeRootHelp(options = {}) {
|
|
20
|
+
// Best-effort version check so the help header can show a banner when outdated.
|
|
21
|
+
// Short timeout; cache still applies (once/day). Never blocks help on network.
|
|
22
|
+
const update = await checkForUpdate({ timeoutMs: 800 });
|
|
23
|
+
// Empty token (and no env/config) → first-run auth banner.
|
|
24
|
+
let needsAuth = true;
|
|
25
|
+
try {
|
|
26
|
+
const cfg = resolveConfig({
|
|
27
|
+
requireToken: false,
|
|
28
|
+
token: options.token,
|
|
29
|
+
envFile: options.envFile,
|
|
30
|
+
});
|
|
31
|
+
needsAuth = !cfg.token;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
needsAuth = true;
|
|
35
|
+
}
|
|
36
|
+
process.stderr.write(formatRootHelp({
|
|
37
|
+
full: options.full,
|
|
38
|
+
version: update.current,
|
|
39
|
+
latestVersion: update.updateAvailable ? update.latest : undefined,
|
|
40
|
+
needsAuth,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
82
43
|
function createContext(globals, requireToken, commandArgs) {
|
|
83
44
|
const cmdWorkspace = commandWorkspace(parseCommandArgs(commandArgs).flags);
|
|
84
45
|
const config = resolveConfig({
|
|
@@ -191,9 +152,18 @@ export async function runCli(argv) {
|
|
|
191
152
|
process.stdout.write(`${packageVersion()}\n`);
|
|
192
153
|
return 0;
|
|
193
154
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
155
|
+
// Root help: bare `uploads`, `--help`/`-h`, or `help` / `help --all`.
|
|
156
|
+
const isHelpCommand = parsed.command === "help";
|
|
157
|
+
if (!parsed.command || isHelpCommand) {
|
|
158
|
+
const helpArgs = isHelpCommand ? parsed.rest.slice(1) : parsed.rest;
|
|
159
|
+
const full = Boolean(parsed.globals.all) || wantsFullHelp(helpArgs);
|
|
160
|
+
await writeRootHelp({
|
|
161
|
+
full,
|
|
162
|
+
token: parsed.globals.token,
|
|
163
|
+
envFile: parsed.globals.envFile,
|
|
164
|
+
});
|
|
165
|
+
// Explicit help exits 0; bare `uploads` is usage → 2.
|
|
166
|
+
return parsed.help || isHelpCommand ? 0 : 2;
|
|
197
167
|
}
|
|
198
168
|
const cmdArgs = parsed.rest.slice(1);
|
|
199
169
|
const showHelp = parsed.help || cmdArgs.some(isHelpFlag);
|
|
@@ -211,6 +181,19 @@ export async function runCli(argv) {
|
|
|
211
181
|
case "login":
|
|
212
182
|
code = await runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
213
183
|
break;
|
|
184
|
+
case "whoami":
|
|
185
|
+
case "status":
|
|
186
|
+
code = await runWhoami(cmdArgs, {
|
|
187
|
+
json,
|
|
188
|
+
envFile: parsed.globals.envFile,
|
|
189
|
+
token: parsed.globals.token,
|
|
190
|
+
workspace: parsed.globals.workspace,
|
|
191
|
+
apiUrl: parsed.globals.apiUrl,
|
|
192
|
+
}, showHelp);
|
|
193
|
+
break;
|
|
194
|
+
case "logout":
|
|
195
|
+
code = await runLogout(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
196
|
+
break;
|
|
214
197
|
case "invite":
|
|
215
198
|
code = await runInvite(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
216
199
|
break;
|
|
@@ -223,6 +206,10 @@ export async function runCli(argv) {
|
|
|
223
206
|
case "install":
|
|
224
207
|
code = await runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
|
|
225
208
|
break;
|
|
209
|
+
case "completion":
|
|
210
|
+
case "completions":
|
|
211
|
+
code = await runCompletion(cmdArgs, showHelp);
|
|
212
|
+
break;
|
|
226
213
|
case "attach":
|
|
227
214
|
case "put":
|
|
228
215
|
case "gallery":
|
|
@@ -276,9 +263,16 @@ export async function runCli(argv) {
|
|
|
276
263
|
}
|
|
277
264
|
break;
|
|
278
265
|
}
|
|
279
|
-
default:
|
|
280
|
-
process.stderr
|
|
266
|
+
default: {
|
|
267
|
+
const style = createStyle(colorEnabled(process.stderr));
|
|
268
|
+
process.stderr.write(`${style.error(`unknown command: ${parsed.command}`)}\n\n`);
|
|
269
|
+
await writeRootHelp({
|
|
270
|
+
full: false,
|
|
271
|
+
token: parsed.globals.token,
|
|
272
|
+
envFile: parsed.globals.envFile,
|
|
273
|
+
});
|
|
281
274
|
return 2;
|
|
275
|
+
}
|
|
282
276
|
}
|
|
283
277
|
// Best-effort; skipped for mcp, --quiet/--json, and opt-out env vars.
|
|
284
278
|
if (code === 0 && !showHelp) {
|
package/dist/client.d.ts
CHANGED
|
@@ -60,6 +60,11 @@ export interface PutResult {
|
|
|
60
60
|
embedUrl: string | null;
|
|
61
61
|
size: number;
|
|
62
62
|
contentType: string;
|
|
63
|
+
/**
|
|
64
|
+
* True when the put overwrote an existing key, or (with dryRun) when a put
|
|
65
|
+
* at this key would overwrite. Always set by the API for put/dry-run.
|
|
66
|
+
*/
|
|
67
|
+
replaced?: boolean;
|
|
63
68
|
metadata?: Record<string, string>;
|
|
64
69
|
}
|
|
65
70
|
export interface ListItem {
|
|
@@ -282,6 +287,18 @@ export interface MintWorkspaceSummary {
|
|
|
282
287
|
export declare function listMintWorkspaces(apiUrl: string, accessToken: string): Promise<{
|
|
283
288
|
workspaces: MintWorkspaceSummary[];
|
|
284
289
|
}>;
|
|
290
|
+
export interface CreateWorkspaceResult {
|
|
291
|
+
name: string;
|
|
292
|
+
publicBaseUrl: string;
|
|
293
|
+
selfServe: boolean;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* POST /v1/workspaces — self-serve workspace creation from a device-flow
|
|
297
|
+
* session (presented as a bearer). Throws `UsageError` with a message tuned
|
|
298
|
+
* for CLI display: a linked-GitHub requirement gets an actionable pointer,
|
|
299
|
+
* everything else surfaces the server's message.
|
|
300
|
+
*/
|
|
301
|
+
export declare function createWorkspaceRequest(apiUrl: string, accessToken: string, name: string): Promise<CreateWorkspaceResult>;
|
|
285
302
|
export interface MintTokenResult {
|
|
286
303
|
token: string;
|
|
287
304
|
workspace: string;
|
|
@@ -306,6 +323,8 @@ export declare function createWorkspaceInvite(apiUrl: string, accessToken: strin
|
|
|
306
323
|
status: string;
|
|
307
324
|
};
|
|
308
325
|
acceptUrl?: string;
|
|
326
|
+
/** Whether the install can send invite emails; absent on older auth workers. */
|
|
327
|
+
emailConfigured?: boolean;
|
|
309
328
|
}>;
|
|
310
329
|
/**
|
|
311
330
|
* POST /v1/tokens — mint a `up_<workspace>_…` workspace token from a device-flow
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { inferContentType } from "./embed.js";
|
|
2
|
+
import { UsageError } from "./cli-args.js";
|
|
2
3
|
import { UploadsError } from "./errors.js";
|
|
3
4
|
import { buildScreenshotKey } from "./keys.js";
|
|
4
5
|
import { packageVersion } from "./package-version.js";
|
|
@@ -112,6 +113,31 @@ export function listMintWorkspaces(apiUrl, accessToken) {
|
|
|
112
113
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
113
114
|
});
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* POST /v1/workspaces — self-serve workspace creation from a device-flow
|
|
118
|
+
* session (presented as a bearer). Throws `UsageError` with a message tuned
|
|
119
|
+
* for CLI display: a linked-GitHub requirement gets an actionable pointer,
|
|
120
|
+
* everything else surfaces the server's message.
|
|
121
|
+
*/
|
|
122
|
+
export async function createWorkspaceRequest(apiUrl, accessToken, name) {
|
|
123
|
+
try {
|
|
124
|
+
const { workspace } = await jsonRequest(`${apiUrl.replace(/\/$/, "")}/v1/workspaces`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${accessToken}`,
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({ name }),
|
|
131
|
+
});
|
|
132
|
+
return workspace;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err instanceof UploadsError && err.code === "GITHUB_REQUIRED") {
|
|
136
|
+
throw new UsageError("creating a workspace requires a linked GitHub account — connect one at https://uploads.sh/account/profile and re-run `uploads login`");
|
|
137
|
+
}
|
|
138
|
+
throw new UsageError(err instanceof Error ? err.message : "workspace creation failed");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
115
141
|
/**
|
|
116
142
|
* POST /me/workspaces/:name/invites — org invitation for a workspace.
|
|
117
143
|
* Requires a Better Auth session bearer (device flow), not a workspace token.
|
|
@@ -176,6 +202,9 @@ function mapApiError(status, error, code) {
|
|
|
176
202
|
if (code === "upload_budget_exceeded") {
|
|
177
203
|
return new UploadsError(error, "UPLOAD_BUDGET", status);
|
|
178
204
|
}
|
|
205
|
+
if (code === "github_required") {
|
|
206
|
+
return new UploadsError(error, "GITHUB_REQUIRED", status);
|
|
207
|
+
}
|
|
179
208
|
return new UploadsError(error, "API_ERROR", status);
|
|
180
209
|
}
|
|
181
210
|
/**
|
|
@@ -277,6 +306,7 @@ export function createUploadsClient(config) {
|
|
|
277
306
|
embedUrl: resolveEmbedUrl(preview.url, preview.embedUrl),
|
|
278
307
|
size: body.byteLength,
|
|
279
308
|
contentType,
|
|
309
|
+
replaced: preview.replaced === true,
|
|
280
310
|
};
|
|
281
311
|
}
|
|
282
312
|
const headers = { "Content-Type": contentType };
|
|
@@ -305,6 +335,7 @@ export function createUploadsClient(config) {
|
|
|
305
335
|
...result,
|
|
306
336
|
url: result.url,
|
|
307
337
|
embedUrl: resolveEmbedUrl(result.url, result.embedUrl),
|
|
338
|
+
replaced: result.replaced === true,
|
|
308
339
|
};
|
|
309
340
|
},
|
|
310
341
|
list,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createEnrollment } from "../client.js";
|
|
2
2
|
import { flagBool, flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
3
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
3
4
|
const HELP = `uploads admin invite create [options]
|
|
4
5
|
|
|
5
6
|
Admin-only: create a short-lived invitation for an existing workspace.
|
|
@@ -65,7 +66,7 @@ export function parseScopes(raw) {
|
|
|
65
66
|
export async function runAdmin(args, opts, help = false) {
|
|
66
67
|
const parsed = parseCommandArgs(args);
|
|
67
68
|
if (help || parsed.help) {
|
|
68
|
-
|
|
69
|
+
writeCommandHelp(HELP);
|
|
69
70
|
return 0;
|
|
70
71
|
}
|
|
71
72
|
if (!["invite", "enrollment"].includes(parsed.positionals[0] ?? "") ||
|