@buildinternet/uploads 0.9.0 → 0.10.1
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 +23 -7
- 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 +41 -20
- package/dist/config-file.d.ts +9 -0
- package/dist/config-file.js +39 -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/update-check.d.ts +10 -0
- package/dist/update-check.js +28 -12
- package/package.json +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { parseCommandArgs, UsageError } from "../cli-args.js";
|
|
2
|
+
import { COMPLETION_SHELLS, GLOBAL_FLAGS, LIST_LIKE_FLAGS, PUT_LIKE_FLAGS, ROOT_COMMANDS, isCompletionShell, } from "../cli-catalog.js";
|
|
3
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
4
|
+
const HELP = `uploads completion <shell>
|
|
5
|
+
|
|
6
|
+
Print a shell completion script to stdout. Source it or install it so Tab
|
|
7
|
+
completes commands, subcommands, and common flags.
|
|
8
|
+
|
|
9
|
+
Shells:
|
|
10
|
+
bash Bash (complete -F)
|
|
11
|
+
zsh Zsh (#compdef)
|
|
12
|
+
fish Fish (complete -c)
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
# zsh (Oh My Zsh / fpath)
|
|
16
|
+
uploads completion zsh > ~/.zsh/completions/_uploads
|
|
17
|
+
|
|
18
|
+
# bash
|
|
19
|
+
uploads completion bash > ~/.local/share/bash-completion/completions/uploads
|
|
20
|
+
# or for the current session:
|
|
21
|
+
eval "$(uploads completion bash)"
|
|
22
|
+
|
|
23
|
+
# fish
|
|
24
|
+
uploads completion fish > ~/.config/fish/completions/uploads.fish
|
|
25
|
+
`;
|
|
26
|
+
function bashScript() {
|
|
27
|
+
const cmds = ROOT_COMMANDS.map((c) => c.name).join(" ");
|
|
28
|
+
const globals = GLOBAL_FLAGS.map((g) => g.flag).join(" ");
|
|
29
|
+
const putFlags = PUT_LIKE_FLAGS.join(" ");
|
|
30
|
+
const listFlags = LIST_LIKE_FLAGS.join(" ");
|
|
31
|
+
const subMaps = ROOT_COMMANDS.filter((c) => c.subcommands?.length).map((c) => {
|
|
32
|
+
const names = c.subcommands.map((s) => s.name).join(" ");
|
|
33
|
+
return ` ${c.name}) subs="${names}" ;;`;
|
|
34
|
+
});
|
|
35
|
+
// Shell `$var` / `$(…)` must not be JS-escaped (`\$`); only shell `${…}` needs `\${…}`
|
|
36
|
+
// so the template literal doesn't treat it as interpolation.
|
|
37
|
+
return `# uploads bash completion — generated by: uploads completion bash
|
|
38
|
+
# shellcheck shell=bash disable=SC2207
|
|
39
|
+
|
|
40
|
+
_uploads() {
|
|
41
|
+
local cur prev words cword
|
|
42
|
+
if declare -F _init_completion >/dev/null 2>&1; then
|
|
43
|
+
_init_completion -n : || return
|
|
44
|
+
else
|
|
45
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
46
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
47
|
+
words=("\${COMP_WORDS[@]}")
|
|
48
|
+
cword=\${COMP_CWORD}
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
local -a root_cmds=(${cmds})
|
|
52
|
+
local -a globals=(${globals})
|
|
53
|
+
local -a put_flags=(${putFlags})
|
|
54
|
+
local -a list_flags=(${listFlags})
|
|
55
|
+
|
|
56
|
+
# Find the first non-global positional (the subcommand).
|
|
57
|
+
local cmd="" i=1
|
|
58
|
+
while [[ $i -lt $cword ]]; do
|
|
59
|
+
local w="\${words[i]}"
|
|
60
|
+
case "$w" in
|
|
61
|
+
--api-url|--token|--workspace|-w|--env-file)
|
|
62
|
+
i=$((i + 2))
|
|
63
|
+
continue
|
|
64
|
+
;;
|
|
65
|
+
--json|--quiet|--version|-V|--help|-h|--all)
|
|
66
|
+
i=$((i + 1))
|
|
67
|
+
continue
|
|
68
|
+
;;
|
|
69
|
+
-*)
|
|
70
|
+
i=$((i + 1))
|
|
71
|
+
continue
|
|
72
|
+
;;
|
|
73
|
+
*)
|
|
74
|
+
cmd="$w"
|
|
75
|
+
break
|
|
76
|
+
;;
|
|
77
|
+
esac
|
|
78
|
+
done
|
|
79
|
+
|
|
80
|
+
if [[ -z "$cmd" ]]; then
|
|
81
|
+
if [[ "$cur" == -* ]]; then
|
|
82
|
+
COMPREPLY=( $(compgen -W "\${globals[*]}" -- "$cur") )
|
|
83
|
+
else
|
|
84
|
+
COMPREPLY=( $(compgen -W "\${root_cmds[*]}" -- "$cur") )
|
|
85
|
+
fi
|
|
86
|
+
return
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
local subs=""
|
|
90
|
+
case "$cmd" in
|
|
91
|
+
${subMaps.join("\n")}
|
|
92
|
+
esac
|
|
93
|
+
|
|
94
|
+
# Completing the subcommand word (or first arg after command).
|
|
95
|
+
if [[ "$prev" == "$cmd" && -n "$subs" ]]; then
|
|
96
|
+
COMPREPLY=( $(compgen -W "$subs" -- "$cur") )
|
|
97
|
+
return
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if [[ "$cur" == -* ]]; then
|
|
101
|
+
case "$cmd" in
|
|
102
|
+
put|attach)
|
|
103
|
+
COMPREPLY=( $(compgen -W "\${put_flags[*]}" -- "$cur") )
|
|
104
|
+
;;
|
|
105
|
+
list|find)
|
|
106
|
+
COMPREPLY=( $(compgen -W "\${list_flags[*]}" -- "$cur") )
|
|
107
|
+
;;
|
|
108
|
+
*)
|
|
109
|
+
COMPREPLY=( $(compgen -W "--help -h --workspace -w --json --quiet" -- "$cur") )
|
|
110
|
+
;;
|
|
111
|
+
esac
|
|
112
|
+
return
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# File paths for upload-style commands.
|
|
116
|
+
case "$cmd" in
|
|
117
|
+
put|attach)
|
|
118
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
119
|
+
;;
|
|
120
|
+
esac
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
complete -o default -F _uploads uploads
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
function zshScript() {
|
|
127
|
+
const cmdDescribe = ROOT_COMMANDS.map((c) => {
|
|
128
|
+
const desc = c.summary.replace(/'/g, "'\\''");
|
|
129
|
+
return ` '${c.name}:${desc}'`;
|
|
130
|
+
}).join("\n");
|
|
131
|
+
const globalArgs = GLOBAL_FLAGS.map((g) => {
|
|
132
|
+
const desc = g.summary.replace(/'/g, "'\\''");
|
|
133
|
+
// Short and long flags as separate specs when needed.
|
|
134
|
+
if (g.flag === "-w")
|
|
135
|
+
return ` '(-w --workspace)'{-w,--workspace}'[${desc}]:workspace:'`;
|
|
136
|
+
if (g.flag === "--workspace")
|
|
137
|
+
return null; // paired with -w
|
|
138
|
+
if (g.flag === "-V")
|
|
139
|
+
return ` '(-V --version)'{-V,--version}'[${desc}]'`;
|
|
140
|
+
if (g.flag === "--version")
|
|
141
|
+
return null;
|
|
142
|
+
if (g.flag === "-h")
|
|
143
|
+
return ` '(-h --help)'{-h,--help}'[${desc}]'`;
|
|
144
|
+
if (g.flag === "--help")
|
|
145
|
+
return null;
|
|
146
|
+
if (g.flag === "--api-url")
|
|
147
|
+
return ` '--api-url[${desc}]:url:'`;
|
|
148
|
+
if (g.flag === "--token")
|
|
149
|
+
return ` '--token[${desc}]:token:'`;
|
|
150
|
+
if (g.flag === "--env-file")
|
|
151
|
+
return ` '--env-file[${desc}]:file:_files'`;
|
|
152
|
+
return ` '${g.flag}[${desc}]'`;
|
|
153
|
+
})
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.join("\n");
|
|
156
|
+
const subCases = ROOT_COMMANDS.filter((c) => c.subcommands?.length)
|
|
157
|
+
.map((c) => {
|
|
158
|
+
const lines = c
|
|
159
|
+
.subcommands.map((s) => {
|
|
160
|
+
const desc = s.summary.replace(/'/g, "'\\''");
|
|
161
|
+
return ` '${s.name}:${desc}'`;
|
|
162
|
+
})
|
|
163
|
+
.join("\n");
|
|
164
|
+
return ` ${c.name})
|
|
165
|
+
local -a subs
|
|
166
|
+
subs=(
|
|
167
|
+
${lines}
|
|
168
|
+
)
|
|
169
|
+
_describe 'subcommand' subs
|
|
170
|
+
;;`;
|
|
171
|
+
})
|
|
172
|
+
.join("\n");
|
|
173
|
+
return `#compdef uploads
|
|
174
|
+
# uploads zsh completion — generated by: uploads completion zsh
|
|
175
|
+
|
|
176
|
+
_uploads() {
|
|
177
|
+
local -a commands
|
|
178
|
+
commands=(
|
|
179
|
+
${cmdDescribe}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
local context state state_descr line
|
|
183
|
+
typeset -A opt_args
|
|
184
|
+
|
|
185
|
+
_arguments -C -s -S \\
|
|
186
|
+
${globalArgs} \\
|
|
187
|
+
'1:command:->cmds' \\
|
|
188
|
+
'*::arg:->args'
|
|
189
|
+
|
|
190
|
+
case $state in
|
|
191
|
+
cmds)
|
|
192
|
+
_describe -t commands 'uploads command' commands
|
|
193
|
+
;;
|
|
194
|
+
args)
|
|
195
|
+
case $line[1] in
|
|
196
|
+
${subCases}
|
|
197
|
+
put|attach)
|
|
198
|
+
_files
|
|
199
|
+
;;
|
|
200
|
+
esac
|
|
201
|
+
;;
|
|
202
|
+
esac
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_uploads "$@"
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
function fishScript() {
|
|
209
|
+
const lines = [
|
|
210
|
+
`# uploads fish completion — generated by: uploads completion fish`,
|
|
211
|
+
`complete -c uploads -e`,
|
|
212
|
+
`complete -c uploads -f`,
|
|
213
|
+
];
|
|
214
|
+
for (const g of GLOBAL_FLAGS) {
|
|
215
|
+
const short = g.flag.length === 2 && g.flag.startsWith("-") && !g.flag.startsWith("--");
|
|
216
|
+
if (short) {
|
|
217
|
+
// Pair handled with long form when possible
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const esc = g.summary.replace(/'/g, "\\'");
|
|
221
|
+
if (g.flag === "--workspace") {
|
|
222
|
+
lines.push(`complete -c uploads -s w -l workspace -d '${esc}' -r`);
|
|
223
|
+
}
|
|
224
|
+
else if (g.flag === "--version") {
|
|
225
|
+
lines.push(`complete -c uploads -s V -l version -d '${esc}'`);
|
|
226
|
+
}
|
|
227
|
+
else if (g.flag === "--help") {
|
|
228
|
+
lines.push(`complete -c uploads -s h -l help -d '${esc}'`);
|
|
229
|
+
}
|
|
230
|
+
else if (g.flag === "--api-url" || g.flag === "--token" || g.flag === "--env-file") {
|
|
231
|
+
lines.push(`complete -c uploads -l ${g.flag.slice(2)} -d '${esc}' -r`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
lines.push(`complete -c uploads -l ${g.flag.slice(2)} -d '${esc}'`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const c of ROOT_COMMANDS) {
|
|
238
|
+
const esc = c.summary.replace(/'/g, "\\'");
|
|
239
|
+
lines.push(`complete -c uploads -n '__fish_use_subcommand' -a '${c.name}' -d '${esc}'`);
|
|
240
|
+
if (c.subcommands?.length) {
|
|
241
|
+
for (const s of c.subcommands) {
|
|
242
|
+
const sEsc = s.summary.replace(/'/g, "\\'");
|
|
243
|
+
// --all under help is a flag-like token; fish still accepts it as -a
|
|
244
|
+
lines.push(`complete -c uploads -n '__fish_seen_subcommand_from ${c.name}' -a '${s.name}' -d '${sEsc}'`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const flag of PUT_LIKE_FLAGS) {
|
|
249
|
+
if (!flag.startsWith("--"))
|
|
250
|
+
continue;
|
|
251
|
+
lines.push(`complete -c uploads -n '__fish_seen_subcommand_from put attach' -l ${flag.slice(2)}`);
|
|
252
|
+
}
|
|
253
|
+
for (const flag of LIST_LIKE_FLAGS) {
|
|
254
|
+
if (!flag.startsWith("--"))
|
|
255
|
+
continue;
|
|
256
|
+
lines.push(`complete -c uploads -n '__fish_seen_subcommand_from list find' -l ${flag.slice(2)}`);
|
|
257
|
+
}
|
|
258
|
+
// File completion for put/attach
|
|
259
|
+
lines.push(`complete -c uploads -n '__fish_seen_subcommand_from put attach' -F`);
|
|
260
|
+
return lines.join("\n") + "\n";
|
|
261
|
+
}
|
|
262
|
+
export function generateCompletionScript(shell) {
|
|
263
|
+
switch (shell) {
|
|
264
|
+
case "bash":
|
|
265
|
+
return bashScript();
|
|
266
|
+
case "zsh":
|
|
267
|
+
return zshScript();
|
|
268
|
+
case "fish":
|
|
269
|
+
return fishScript();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
export async function runCompletion(args, help = false) {
|
|
273
|
+
const parsed = parseCommandArgs(args);
|
|
274
|
+
if (help || parsed.help || parsed.positionals.length === 0) {
|
|
275
|
+
writeCommandHelp(HELP);
|
|
276
|
+
return help || parsed.help ? 0 : 2;
|
|
277
|
+
}
|
|
278
|
+
const shell = parsed.positionals[0]?.toLowerCase() ?? "";
|
|
279
|
+
if (!isCompletionShell(shell)) {
|
|
280
|
+
throw new UsageError(`unknown shell: ${parsed.positionals[0]} (expected ${COMPLETION_SHELLS.join(", ")})`);
|
|
281
|
+
}
|
|
282
|
+
process.stdout.write(generateCompletionScript(shell));
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
package/dist/commands/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, describeConfigSources, redactToken, resolveConfig, resolveConfigPath, resolvePutDefaults, writeConfigKeys, } from "../config.js";
|
|
3
3
|
import { flagBool, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
4
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
4
5
|
const CONFIG_HELP = `uploads config — manage shared buildinternet config
|
|
5
6
|
|
|
6
7
|
Shared file (with github-screenshots and other skills):
|
|
@@ -41,7 +42,7 @@ function writeJson(payload) {
|
|
|
41
42
|
export async function runConfig(args, opts, help = false) {
|
|
42
43
|
const parsed = parseCommandArgs(args);
|
|
43
44
|
if (help || parsed.help || parsed.positionals.length === 0) {
|
|
44
|
-
|
|
45
|
+
writeCommandHelp(CONFIG_HELP);
|
|
45
46
|
return 0;
|
|
46
47
|
}
|
|
47
48
|
const sub = parsed.positionals[0];
|
|
@@ -57,13 +58,14 @@ export async function runConfig(args, opts, help = false) {
|
|
|
57
58
|
case "set":
|
|
58
59
|
return runConfigSet(rest, subArgs, opts, help);
|
|
59
60
|
default:
|
|
60
|
-
process.stderr.write(`unknown config subcommand: ${sub}\n\n
|
|
61
|
+
process.stderr.write(`unknown config subcommand: ${sub}\n\n`);
|
|
62
|
+
writeCommandHelp(CONFIG_HELP);
|
|
61
63
|
return 2;
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
async function runConfigPath(args, opts, help) {
|
|
65
67
|
if (help || parseCommandArgs(args).help) {
|
|
66
|
-
|
|
68
|
+
writeCommandHelp(`uploads config path\n\nPrint the resolved config file path.\n`);
|
|
67
69
|
return 0;
|
|
68
70
|
}
|
|
69
71
|
const path = resolveConfigPath({ envFile: opts.envFile });
|
|
@@ -76,7 +78,7 @@ async function runConfigPath(args, opts, help) {
|
|
|
76
78
|
}
|
|
77
79
|
async function runConfigShow(args, opts, help) {
|
|
78
80
|
if (help || parseCommandArgs(args).help) {
|
|
79
|
-
|
|
81
|
+
writeCommandHelp(`uploads config show\n\nShow effective settings (token redacted).\n`);
|
|
80
82
|
return 0;
|
|
81
83
|
}
|
|
82
84
|
const config = resolveConfig({ envFile: opts.envFile, requireToken: false });
|
|
@@ -117,7 +119,7 @@ async function runConfigShow(args, opts, help) {
|
|
|
117
119
|
async function runConfigInit(args, opts, help) {
|
|
118
120
|
const parsed = parseCommandArgs(args);
|
|
119
121
|
if (help || parsed.help) {
|
|
120
|
-
|
|
122
|
+
writeCommandHelp(`uploads config init [options]
|
|
121
123
|
|
|
122
124
|
Create or update UPLOADS_* keys in the shared config file.
|
|
123
125
|
|
|
@@ -173,7 +175,7 @@ Examples:
|
|
|
173
175
|
async function runConfigSet(positionals, args, opts, help) {
|
|
174
176
|
const parsed = parseCommandArgs(args);
|
|
175
177
|
if (help || parsed.help) {
|
|
176
|
-
|
|
178
|
+
writeCommandHelp(`uploads config set <key> <value> [--path <file>] [--force]
|
|
177
179
|
|
|
178
180
|
Examples:
|
|
179
181
|
uploads config set UPLOADS_TOKEN up_default_…
|
|
@@ -184,7 +186,7 @@ Examples:
|
|
|
184
186
|
const key = positionals[0];
|
|
185
187
|
const value = positionals[1];
|
|
186
188
|
if (!key || !value) {
|
|
187
|
-
|
|
189
|
+
writeCommandHelp(`uploads config set <key> <value>\n`);
|
|
188
190
|
return 2;
|
|
189
191
|
}
|
|
190
192
|
if (!VALID_KEYS.has(key)) {
|
package/dist/commands/install.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { flagBool, flagString, parseCommandArgs, UsageError, } from "../cli-args.js";
|
|
2
2
|
import { resolveConfig } from "../config.js";
|
|
3
3
|
import { execRunner } from "../github-gh.js";
|
|
4
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
4
5
|
export const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
|
|
5
6
|
const SKILL_SOURCE = "buildinternet/uploads";
|
|
6
7
|
const SKILL_NAME = "uploads-cli";
|
|
@@ -123,7 +124,7 @@ function printSuccessFooter(steps, signedIn) {
|
|
|
123
124
|
export async function runInstall(args, opts, help = false) {
|
|
124
125
|
const parsed = parseCommandArgs(args);
|
|
125
126
|
if (help || parsed.help) {
|
|
126
|
-
|
|
127
|
+
writeCommandHelp(INSTALL_HELP);
|
|
127
128
|
return 0;
|
|
128
129
|
}
|
|
129
130
|
const target = parsed.positionals[0] ?? "all";
|
package/dist/commands/invite.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createWorkspaceInvite, listMintWorkspaces } from "../client.js";
|
|
6
6
|
import { flagBool, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
7
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
7
8
|
import { defaultDeviceIo, obtainDeviceAccessToken, resolveAuthUrl, } from "./login.js";
|
|
8
9
|
const HELP = `uploads invite create [options]
|
|
9
10
|
|
|
@@ -53,7 +54,7 @@ export function resolveInviteWorkspace(workspaces, requested) {
|
|
|
53
54
|
export async function runInvite(args, opts, help = false, io = defaultDeviceIo) {
|
|
54
55
|
const parsed = parseCommandArgs(args);
|
|
55
56
|
if (help || parsed.help) {
|
|
56
|
-
|
|
57
|
+
writeCommandHelp(HELP);
|
|
57
58
|
return 0;
|
|
58
59
|
}
|
|
59
60
|
if (parsed.positionals[0] !== "create") {
|
|
@@ -88,13 +89,25 @@ export async function runInvite(args, opts, help = false, io = defaultDeviceIo)
|
|
|
88
89
|
invitationId: result.invitation.id,
|
|
89
90
|
status: result.invitation.status,
|
|
90
91
|
acceptUrl: result.acceptUrl ?? null,
|
|
92
|
+
emailConfigured: result.emailConfigured ?? null,
|
|
91
93
|
};
|
|
92
94
|
if (opts.json)
|
|
93
95
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
94
96
|
else {
|
|
95
97
|
process.stdout.write(`Invited ${email} to ${workspace} as ${role} (${result.invitation.status}).\n`);
|
|
98
|
+
if (result.emailConfigured === true) {
|
|
99
|
+
process.stdout.write(`Invitation emailed to ${email}.\n`);
|
|
100
|
+
}
|
|
101
|
+
else if (result.emailConfigured === false) {
|
|
102
|
+
process.stdout.write("Email isn't configured on this install — share the accept link yourself.\n");
|
|
103
|
+
}
|
|
96
104
|
if (result.acceptUrl) {
|
|
97
|
-
|
|
105
|
+
const label = result.emailConfigured === true
|
|
106
|
+
? "Accept link (backup)"
|
|
107
|
+
: result.emailConfigured === false
|
|
108
|
+
? "Accept link"
|
|
109
|
+
: "Accept link (share if email isn't configured)";
|
|
110
|
+
process.stdout.write(`${label}:\n ${result.acceptUrl}\n`);
|
|
98
111
|
}
|
|
99
112
|
process.stdout.write("They accept, then run: uploads login\n");
|
|
100
113
|
}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface DeviceLoginIo {
|
|
|
17
17
|
now: () => number;
|
|
18
18
|
openUrl: (url: string) => void;
|
|
19
19
|
write: (text: string) => void;
|
|
20
|
+
/** Whether the CLI can prompt the user (a real TTY, not a script/CI pipe). */
|
|
21
|
+
isTTY: boolean;
|
|
22
|
+
/** Prompt for a new workspace name when the account has zero. */
|
|
23
|
+
promptWorkspaceName: () => Promise<string>;
|
|
20
24
|
}
|
|
21
25
|
export declare const defaultDeviceIo: DeviceLoginIo;
|
|
22
26
|
/**
|
package/dist/commands/login.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { hostname } from "node:os";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
import { stdin, stdout } from "node:process";
|
|
4
5
|
import { loadConfigFile, redactToken, resolveConfigPath, writeConfigKeys, workspaceFromToken, } from "../config.js";
|
|
5
|
-
import { createUploadsClient, exchangeEnrollment, listMintWorkspaces, mintWorkspaceToken, requestDeviceCode, requestDeviceToken, } from "../client.js";
|
|
6
|
+
import { createUploadsClient, createWorkspaceRequest, exchangeEnrollment, listMintWorkspaces, mintWorkspaceToken, requestDeviceCode, requestDeviceToken, } from "../client.js";
|
|
6
7
|
import { flagBool, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
7
8
|
import { parseScopes } from "./admin-enrollment.js";
|
|
9
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
8
10
|
const HELP = `uploads login [options]
|
|
9
11
|
|
|
10
12
|
Sign in and save workspace credentials. With no flags, opens a browser to
|
|
@@ -14,6 +16,9 @@ code only if you were given one from before device login (fallback path).
|
|
|
14
16
|
Options:
|
|
15
17
|
--workspace <name> Workspace to mint a token for (device flow; required if
|
|
16
18
|
your account can access more than one)
|
|
19
|
+
--create With --workspace: create the workspace first if your
|
|
20
|
+
account doesn't have it yet (device flow only) — lets
|
|
21
|
+
scripted/agent logins provision without a prompt
|
|
17
22
|
--scopes <list> Comma-separated scopes (default: files:read,files:write)
|
|
18
23
|
--label <text> Token label (default: this machine's hostname)
|
|
19
24
|
--auth-url <url> Auth base (default: https://auth.uploads.sh)
|
|
@@ -30,6 +35,7 @@ Options:
|
|
|
30
35
|
Examples:
|
|
31
36
|
uploads login
|
|
32
37
|
uploads login --workspace acme
|
|
38
|
+
uploads login --workspace acme --create # provision if it doesn't exist
|
|
33
39
|
uploads login --code upe_… --force # fallback: pre-existing invite
|
|
34
40
|
printf '%s' upe_… | uploads login --code-stdin --non-interactive
|
|
35
41
|
`;
|
|
@@ -157,6 +163,15 @@ function openUrl(url) {
|
|
|
157
163
|
// ignore — the URL is printed for manual navigation.
|
|
158
164
|
}
|
|
159
165
|
}
|
|
166
|
+
async function promptWorkspaceName() {
|
|
167
|
+
const rl = createInterface({ input: stdin, output: process.stderr });
|
|
168
|
+
try {
|
|
169
|
+
return (await rl.question("no workspaces yet — enter a name to create one (lowercase, hyphens): ")).trim();
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
rl.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
160
175
|
export const defaultDeviceIo = {
|
|
161
176
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
162
177
|
now: () => Date.now(),
|
|
@@ -164,6 +179,8 @@ export const defaultDeviceIo = {
|
|
|
164
179
|
write: (text) => {
|
|
165
180
|
process.stderr.write(text);
|
|
166
181
|
},
|
|
182
|
+
isTTY: Boolean(stdin.isTTY),
|
|
183
|
+
promptWorkspaceName,
|
|
167
184
|
};
|
|
168
185
|
/**
|
|
169
186
|
* Browser device-authorization session only (no workspace token mint).
|
|
@@ -187,8 +204,11 @@ async function runDeviceLogin(parsed, opts, io) {
|
|
|
187
204
|
const scopes = parseScopes(flagString(parsed.flags, "--scopes"));
|
|
188
205
|
const label = flagString(parsed.flags, "--label") ?? safeHostname();
|
|
189
206
|
const requestedWorkspace = flagString(parsed.flags, "--workspace");
|
|
207
|
+
// Make the target explicit: a bare `uploads login` on a self-hosted install
|
|
208
|
+
// would otherwise silently sign in to the cloud service.
|
|
209
|
+
io.write(`signing in to ${opts.authUrl} (self-hosted? pass --api-url or set UPLOADS_API_URL)\n\n`);
|
|
190
210
|
const accessToken = await obtainDeviceAccessToken(opts.authUrl, { noOpen: opts.noOpen }, io);
|
|
191
|
-
const workspace = await resolveMintWorkspace(opts.apiUrl, accessToken, requestedWorkspace);
|
|
211
|
+
const workspace = await resolveMintWorkspace(opts.apiUrl, accessToken, requestedWorkspace, io, flagBool(parsed.flags, "--create"));
|
|
192
212
|
const minted = await mintWorkspaceToken(opts.apiUrl, accessToken, { workspace, scopes, label });
|
|
193
213
|
return { workspace: minted.workspace, token: minted.token, apiUrl: opts.apiUrl };
|
|
194
214
|
}
|
|
@@ -235,18 +255,36 @@ export async function pollForDeviceToken(authUrl, code, io) {
|
|
|
235
255
|
throw new UsageError("timed out waiting for device authorization");
|
|
236
256
|
}
|
|
237
257
|
/**
|
|
238
|
-
* Pick the workspace to mint for. An explicit --workspace wins
|
|
239
|
-
*
|
|
258
|
+
* Pick the workspace to mint for. An explicit --workspace wins (with --create,
|
|
259
|
+
* it's provisioned first when the account doesn't have it); otherwise, if the
|
|
260
|
+
* account can access exactly one workspace, use it — and if it can access
|
|
240
261
|
* several, require the flag rather than guessing.
|
|
241
262
|
*/
|
|
242
|
-
async function resolveMintWorkspace(apiUrl, accessToken, requested) {
|
|
243
|
-
if (requested)
|
|
244
|
-
|
|
263
|
+
async function resolveMintWorkspace(apiUrl, accessToken, requested, io, create = false) {
|
|
264
|
+
if (requested) {
|
|
265
|
+
if (!create)
|
|
266
|
+
return requested;
|
|
267
|
+
// Idempotent from the caller's view: an existing membership just mints.
|
|
268
|
+
const { workspaces } = await listMintWorkspaces(apiUrl, accessToken);
|
|
269
|
+
if (workspaces.some((w) => w.workspace === requested))
|
|
270
|
+
return requested;
|
|
271
|
+
const created = await createWorkspaceRequest(apiUrl, accessToken, requested);
|
|
272
|
+
io.write(`created workspace "${created.name}" — files will get public URLs under ${created.publicBaseUrl}/\n`);
|
|
273
|
+
return created.name;
|
|
274
|
+
}
|
|
245
275
|
const { workspaces } = await listMintWorkspaces(apiUrl, accessToken);
|
|
246
276
|
if (workspaces.length === 1)
|
|
247
277
|
return workspaces[0].workspace;
|
|
248
278
|
if (workspaces.length === 0) {
|
|
249
|
-
|
|
279
|
+
if (!io.isTTY) {
|
|
280
|
+
throw new UsageError("your account has no workspace access yet — pass `--workspace <name> --create` to provision one, run `uploads login` interactively, or ask an administrator for an invitation");
|
|
281
|
+
}
|
|
282
|
+
const name = (await io.promptWorkspaceName()).trim();
|
|
283
|
+
if (!name)
|
|
284
|
+
throw new UsageError("workspace creation cancelled");
|
|
285
|
+
const created = await createWorkspaceRequest(apiUrl, accessToken, name);
|
|
286
|
+
io.write(`created workspace "${created.name}" — files will get public URLs under ${created.publicBaseUrl}/\n`);
|
|
287
|
+
return created.name;
|
|
250
288
|
}
|
|
251
289
|
const names = workspaces.map((w) => w.workspace).join(", ");
|
|
252
290
|
throw new UsageError(`multiple workspaces available (${names}); pass --workspace <name>`);
|
|
@@ -254,7 +292,7 @@ async function resolveMintWorkspace(apiUrl, accessToken, requested) {
|
|
|
254
292
|
export async function runLogin(args, opts, help = false, deviceIo = defaultDeviceIo) {
|
|
255
293
|
const parsed = parseCommandArgs(args);
|
|
256
294
|
if (help || parsed.help) {
|
|
257
|
-
|
|
295
|
+
writeCommandHelp(HELP);
|
|
258
296
|
return 0;
|
|
259
297
|
}
|
|
260
298
|
const apiUrl = flagString(parsed.flags, "--api-url") ?? opts.apiUrl ?? "https://api.uploads.sh";
|
|
@@ -265,8 +303,12 @@ export async function runLogin(args, opts, help = false, deviceIo = defaultDevic
|
|
|
265
303
|
throw new UsageError(`credentials already exist in ${path}; use --force to replace them`);
|
|
266
304
|
if (process.env.UPLOADS_TOKEN && !force)
|
|
267
305
|
throw new UsageError("UPLOADS_TOKEN is already set in the environment; unset it or use --force");
|
|
306
|
+
if (flagBool(parsed.flags, "--create") && !flagString(parsed.flags, "--workspace"))
|
|
307
|
+
throw new UsageError("--create requires --workspace <name>");
|
|
268
308
|
let result;
|
|
269
309
|
if (hasEnrollmentSource(parsed)) {
|
|
310
|
+
if (flagBool(parsed.flags, "--create"))
|
|
311
|
+
throw new UsageError("--create is device-flow only; enrollment codes are workspace-bound");
|
|
270
312
|
const code = await resolveEnrollmentCode(parsed);
|
|
271
313
|
result = await exchangeEnrollment(apiUrl, code);
|
|
272
314
|
}
|
|
@@ -312,6 +354,7 @@ export async function runLogin(args, opts, help = false, deviceIo = defaultDevic
|
|
|
312
354
|
const payload = {
|
|
313
355
|
ok: doctor.ok,
|
|
314
356
|
configPath: path,
|
|
357
|
+
apiUrl: savedApiUrl,
|
|
315
358
|
workspace: result.workspace,
|
|
316
359
|
token: redactToken(result.token),
|
|
317
360
|
doctor: checked ? doctor : { skipped: true },
|
|
@@ -319,8 +362,10 @@ export async function runLogin(args, opts, help = false, deviceIo = defaultDevic
|
|
|
319
362
|
if (opts.json)
|
|
320
363
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
321
364
|
else {
|
|
322
|
-
process.stdout.write(`saved credentials to ${path}\nworkspace: ${result.workspace}\ntoken: ${redactToken(result.token)}\n`);
|
|
365
|
+
process.stdout.write(`saved credentials to ${path}\napi: ${savedApiUrl}\nworkspace: ${result.workspace}\ntoken: ${redactToken(result.token)}\n`);
|
|
323
366
|
process[doctor.ok ? "stdout" : "stderr"].write(`doctor: ${checked ? (doctor.ok ? "ok" : `failed — ${doctor.error}`) : "skipped"}\n`);
|
|
367
|
+
if (doctor.ok)
|
|
368
|
+
process.stdout.write("\nusing a coding agent? run `uploads install` to add the uploads skill + MCP server to Claude Code\n");
|
|
324
369
|
}
|
|
325
370
|
return doctor.ok ? 0 : 1;
|
|
326
371
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createMcpServer } from "../mcp/server.js";
|
|
|
3
3
|
import { serveStdio } from "../mcp/stdio.js";
|
|
4
4
|
import { createUploadsMcpTools } from "../mcp/tools.js";
|
|
5
5
|
import { packageVersion } from "../package-version.js";
|
|
6
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
6
7
|
const MCP_HELP = `uploads [globals] mcp
|
|
7
8
|
|
|
8
9
|
Serve the Model Context Protocol (MCP) over stdio for agent clients. Tools
|
|
@@ -24,7 +25,7 @@ Examples:
|
|
|
24
25
|
`;
|
|
25
26
|
export async function runMcp(args, opts, help = false) {
|
|
26
27
|
if (help || parseCommandArgs(args).help) {
|
|
27
|
-
|
|
28
|
+
writeCommandHelp(MCP_HELP);
|
|
28
29
|
return 0;
|
|
29
30
|
}
|
|
30
31
|
const server = createMcpServer({
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface WhoamiReport {
|
|
2
|
+
signedIn: boolean;
|
|
3
|
+
workspace: string;
|
|
4
|
+
workspaceSource: string;
|
|
5
|
+
workspaceFromToken: string | undefined;
|
|
6
|
+
token: string;
|
|
7
|
+
tokenSource: string;
|
|
8
|
+
/** True when the config file holds UPLOADS_TOKEN. */
|
|
9
|
+
tokenInConfig: boolean;
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
apiUrlSource: string;
|
|
12
|
+
configPath: string;
|
|
13
|
+
configExists: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildWhoamiReport(opts: {
|
|
16
|
+
envFile?: string;
|
|
17
|
+
token?: string;
|
|
18
|
+
workspace?: string;
|
|
19
|
+
apiUrl?: string;
|
|
20
|
+
}): WhoamiReport;
|
|
21
|
+
export declare function runWhoami(args: string[], opts: {
|
|
22
|
+
json?: boolean;
|
|
23
|
+
envFile?: string;
|
|
24
|
+
token?: string;
|
|
25
|
+
workspace?: string;
|
|
26
|
+
apiUrl?: string;
|
|
27
|
+
}, help?: boolean): Promise<number>;
|
|
28
|
+
export declare function runLogout(args: string[], opts: {
|
|
29
|
+
json?: boolean;
|
|
30
|
+
envFile?: string;
|
|
31
|
+
}, help?: boolean): Promise<number>;
|