@buildinternet/uploads 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ import { existsSync } from "node:fs";
2
+ import { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, describeConfigSources, redactToken, resolveConfig, resolveConfigPath, resolvePutDefaults, writeConfigKeys, } from "../config.js";
3
+ import { flagBool, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
4
+ const CONFIG_HELP = `uploads config — manage shared buildinternet config
5
+
6
+ Shared file (with github-screenshots and other skills):
7
+ ~/.config/buildinternet/config
8
+ or $XDG_CONFIG_HOME/buildinternet/config
9
+ or override with $BUILDINTERNET_CONFIG
10
+
11
+ Subcommands:
12
+ path Print the resolved config file path
13
+ show Show effective settings (token redacted)
14
+ init Create or update UPLOADS_* keys in the config file
15
+ set <key> <value> Set one UPLOADS_* key
16
+
17
+ Keys:
18
+ UPLOADS_API_URL API base URL (default: ${DEFAULT_API_URL})
19
+ UPLOADS_WORKSPACE Workspace / bucket tenant (default: ${DEFAULT_WORKSPACE})
20
+ UPLOADS_TOKEN Bearer token for the workspace
21
+ UPLOADS_DEFAULT_PREFIX Default key prefix for put/list
22
+ UPLOADS_DEFAULT_REPO Default repo segment for put
23
+ UPLOADS_DEFAULT_REF Default ref segment for put
24
+ UPLOADS_DEFAULT_WIDTH Default markdown image width
25
+ UPLOADS_NO_GIT Set to 1 to skip git remote for --repo
26
+
27
+ Examples:
28
+ uploads config path
29
+ uploads config show
30
+ uploads config init --token up_default_… --workspace default
31
+ uploads config set UPLOADS_TOKEN up_default_…
32
+ uploads config init --api-url http://localhost:8787 --force
33
+ `;
34
+ const VALID_KEYS = new Set(UPLOADS_CONFIG_KEYS);
35
+ function writeStdout(text) {
36
+ process.stdout.write(text);
37
+ }
38
+ function writeJson(payload) {
39
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
40
+ }
41
+ export async function runConfig(args, opts, help = false) {
42
+ const parsed = parseCommandArgs(args);
43
+ if (help || parsed.help || parsed.positionals.length === 0) {
44
+ process.stderr.write(CONFIG_HELP);
45
+ return 0;
46
+ }
47
+ const sub = parsed.positionals[0];
48
+ const rest = parsed.positionals.slice(1);
49
+ const subArgs = args.slice(args.indexOf(sub) + 1);
50
+ switch (sub) {
51
+ case "path":
52
+ return runConfigPath(subArgs, opts, help);
53
+ case "show":
54
+ return runConfigShow(subArgs, opts, help);
55
+ case "init":
56
+ return runConfigInit(subArgs, opts, help);
57
+ case "set":
58
+ return runConfigSet(rest, subArgs, opts, help);
59
+ default:
60
+ process.stderr.write(`unknown config subcommand: ${sub}\n\n${CONFIG_HELP}`);
61
+ return 2;
62
+ }
63
+ }
64
+ async function runConfigPath(args, opts, help) {
65
+ if (help || parseCommandArgs(args).help) {
66
+ process.stderr.write(`uploads config path\n\nPrint the resolved config file path.\n`);
67
+ return 0;
68
+ }
69
+ const path = resolveConfigPath({ envFile: opts.envFile });
70
+ const payload = { path, exists: existsSync(path) };
71
+ if (opts.json)
72
+ writeJson(payload);
73
+ else
74
+ writeStdout(`${path}\n`);
75
+ return 0;
76
+ }
77
+ async function runConfigShow(args, opts, help) {
78
+ if (help || parseCommandArgs(args).help) {
79
+ process.stderr.write(`uploads config show\n\nShow effective settings (token redacted).\n`);
80
+ return 0;
81
+ }
82
+ const config = resolveConfig({ envFile: opts.envFile, requireToken: false });
83
+ const sources = describeConfigSources({ envFile: opts.envFile });
84
+ const defaults = resolvePutDefaults({ envFile: opts.envFile });
85
+ const payload = {
86
+ configPath: config.configPath,
87
+ configExists: config.configExists,
88
+ apiUrl: config.apiUrl,
89
+ workspace: config.workspace,
90
+ token: redactToken(config.token || undefined),
91
+ sources,
92
+ defaults,
93
+ };
94
+ if (opts.json) {
95
+ writeJson(payload);
96
+ return 0;
97
+ }
98
+ const lines = [
99
+ `config: ${config.configPath}${config.configExists ? "" : " (missing)"}`,
100
+ `api: ${config.apiUrl} (${sources.apiUrl})`,
101
+ `workspace: ${config.workspace} (${sources.workspace})`,
102
+ `token: ${redactToken(config.token || undefined)} (${sources.token})`,
103
+ ];
104
+ if (defaults.prefix)
105
+ lines.push(`prefix: ${defaults.prefix}`);
106
+ if (defaults.repo)
107
+ lines.push(`repo: ${defaults.repo}`);
108
+ if (defaults.ref)
109
+ lines.push(`ref: ${defaults.ref}`);
110
+ if (defaults.width != null)
111
+ lines.push(`width: ${defaults.width}`);
112
+ if (defaults.noGit)
113
+ lines.push("no-git: true");
114
+ writeStdout(lines.join("\n") + "\n");
115
+ return 0;
116
+ }
117
+ async function runConfigInit(args, opts, help) {
118
+ const parsed = parseCommandArgs(args);
119
+ if (help || parsed.help) {
120
+ process.stderr.write(`uploads config init [options]
121
+
122
+ Create or update UPLOADS_* keys in the shared config file.
123
+
124
+ Options:
125
+ --api-url <url>
126
+ --workspace, -w <name>
127
+ --token <token>
128
+ --path <file> Write to this file instead of the default
129
+ --force Overwrite existing non-empty values
130
+
131
+ Examples:
132
+ uploads config init --token up_default_…
133
+ uploads config init --api-url http://localhost:8787 --workspace default --token up_default_…
134
+ `);
135
+ return 0;
136
+ }
137
+ const apiUrl = flagString(parsed.flags, "--api-url");
138
+ const workspace = flagString(parsed.flags, "--workspace") ?? flagString(parsed.flags, "-w");
139
+ const token = flagString(parsed.flags, "--token");
140
+ const path = flagString(parsed.flags, "--path") ?? resolveConfigPath({ envFile: opts.envFile });
141
+ const force = flagBool(parsed.flags, "--force");
142
+ const keys = {};
143
+ if (apiUrl)
144
+ keys.UPLOADS_API_URL = apiUrl;
145
+ if (workspace)
146
+ keys.UPLOADS_WORKSPACE = workspace;
147
+ if (token)
148
+ keys.UPLOADS_TOKEN = token;
149
+ if (Object.keys(keys).length === 0) {
150
+ keys.UPLOADS_API_URL = DEFAULT_API_URL;
151
+ keys.UPLOADS_WORKSPACE = DEFAULT_WORKSPACE;
152
+ }
153
+ try {
154
+ const result = writeConfigKeys(path, keys, { force });
155
+ const payload = { ...result, keys: Object.keys(keys) };
156
+ if (opts.json)
157
+ writeJson(payload);
158
+ else {
159
+ const verb = result.created ? "created" : "updated";
160
+ process.stdout.write(`${verb} ${result.path}\n`);
161
+ if (result.updated.length)
162
+ process.stdout.write(`keys: ${result.updated.join(", ")}\n`);
163
+ if (!token) {
164
+ process.stderr.write("hint: add a token with uploads config set UPLOADS_TOKEN <token>\n");
165
+ }
166
+ }
167
+ return 0;
168
+ }
169
+ catch (err) {
170
+ throw new UsageError(err instanceof Error ? err.message : String(err));
171
+ }
172
+ }
173
+ async function runConfigSet(positionals, args, opts, help) {
174
+ const parsed = parseCommandArgs(args);
175
+ if (help || parsed.help) {
176
+ process.stderr.write(`uploads config set <key> <value> [--path <file>] [--force]
177
+
178
+ Examples:
179
+ uploads config set UPLOADS_TOKEN up_default_…
180
+ uploads config set UPLOADS_WORKSPACE acme
181
+ `);
182
+ return 0;
183
+ }
184
+ const key = positionals[0];
185
+ const value = positionals[1];
186
+ if (!key || !value) {
187
+ process.stderr.write(`uploads config set <key> <value>\n`);
188
+ return 2;
189
+ }
190
+ if (!VALID_KEYS.has(key)) {
191
+ throw new UsageError(`unknown key: ${key} (expected ${[...VALID_KEYS].join(", ")})`);
192
+ }
193
+ const path = flagString(parsed.flags, "--path") ?? resolveConfigPath({ envFile: opts.envFile });
194
+ const force = flagBool(parsed.flags, "--force");
195
+ const result = writeConfigKeys(path, { [key]: value }, { force });
196
+ const payload = { ...result, key, value: key === "UPLOADS_TOKEN" ? redactToken(value) : value };
197
+ if (opts.json)
198
+ writeJson(payload);
199
+ else
200
+ process.stdout.write(`set ${key} in ${result.path}\n`);
201
+ return 0;
202
+ }
@@ -0,0 +1,4 @@
1
+ export declare function runSetup(args: string[], opts: {
2
+ json?: boolean;
3
+ envFile?: string;
4
+ }, help?: boolean): Promise<number>;
@@ -0,0 +1,223 @@
1
+ import { createUploadsClient } from "../client.js";
2
+ import { DEFAULT_API_URL, DEFAULT_WORKSPACE, describeConfigSources, putDefaultsToConfigValues, redactToken, resolveApiUrl, resolveConfig, resolveConfigPath, resolvePutDefaults, writeConfigKeys, workspaceFromToken, } from "../config.js";
3
+ import { flagBool, flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
4
+ import { UploadsError } from "../errors.js";
5
+ const SETUP_HELP = `uploads setup — guided CLI configuration
6
+
7
+ Writes UPLOADS_* keys to the shared buildinternet config file and prints
8
+ step-by-step instructions for anything still missing.
9
+
10
+ Without flags: shows current status and the next steps to finish setup.
11
+ With flags: saves provided values, then optionally verifies with doctor.
12
+
13
+ Options:
14
+ --api-url <url> API base (default: ${DEFAULT_API_URL})
15
+ --workspace, -w <name>
16
+ --token <token> Bearer token (mint via admin endpoint — see below)
17
+ --prefix <path> Default key prefix for put/list (default: screenshots)
18
+ --repo <owner/repo> Default repo segment for put
19
+ --ref <id> Default ref segment for put (PR/issue/branch/date)
20
+ --width <px> Default markdown image width
21
+ --no-git Don't derive --repo from git remote
22
+ --path <file> Config file (default: ~/.config/buildinternet/config)
23
+ --force Overwrite existing non-empty values
24
+ --check Run doctor after saving (default when --token is set)
25
+ --no-check Skip doctor after saving
26
+
27
+ Examples:
28
+ uploads setup
29
+ uploads setup --token up_default_…
30
+ uploads setup --api-url http://localhost:8787 --workspace default --token up_default_…
31
+ uploads setup --prefix screenshots --repo myorg/myapp --check
32
+ `;
33
+ function buildStatus(envFile) {
34
+ const config = resolveConfig({ envFile, requireToken: false });
35
+ return {
36
+ configPath: config.configPath,
37
+ apiUrl: config.apiUrl,
38
+ workspace: config.workspace,
39
+ token: config.token || undefined,
40
+ defaults: resolvePutDefaults({ envFile }),
41
+ sources: describeConfigSources({ envFile }),
42
+ };
43
+ }
44
+ function mintTokenCommand(apiUrl, workspace) {
45
+ const body = workspace && workspace !== DEFAULT_WORKSPACE
46
+ ? ` \\\n -H "Content-Type: application/json" \\\n -d '{"workspace":"${workspace}","label":"cli"}'`
47
+ : "";
48
+ return [
49
+ `curl -XPOST ${apiUrl}/admin/tokens \\`,
50
+ ` -H "Authorization: Bearer $ADMIN_TOKEN"${body}`,
51
+ ].join("\n");
52
+ }
53
+ function formatWizard(status) {
54
+ const lines = ["uploads setup", ""];
55
+ lines.push(`Config file: ${status.configPath}`);
56
+ lines.push(`API: ${status.apiUrl} (${status.sources.apiUrl})`);
57
+ lines.push(`Workspace: ${status.workspace} (${status.sources.workspace})`);
58
+ lines.push(`Token: ${redactToken(status.token)} (${status.sources.token})`);
59
+ const d = status.defaults;
60
+ if (d.prefix || d.repo || d.ref || d.width != null || d.noGit) {
61
+ lines.push("Defaults:");
62
+ if (d.prefix)
63
+ lines.push(` prefix: ${d.prefix}`);
64
+ if (d.repo)
65
+ lines.push(` repo: ${d.repo}`);
66
+ if (d.ref)
67
+ lines.push(` ref: ${d.ref}`);
68
+ if (d.width != null)
69
+ lines.push(` width: ${d.width}`);
70
+ if (d.noGit)
71
+ lines.push(" no-git: true");
72
+ }
73
+ lines.push("");
74
+ if (!status.token) {
75
+ lines.push("Step 1 — Mint a token");
76
+ lines.push(" You need ADMIN_TOKEN for the API (ask your uploads.sh admin, or set it locally in apps/api/.dev.vars).");
77
+ lines.push(" Mint:");
78
+ lines.push(` ${mintTokenCommand(status.apiUrl, status.workspace)}`);
79
+ lines.push(" Save:");
80
+ lines.push(" uploads setup --token up_<workspace>_…");
81
+ lines.push("");
82
+ }
83
+ else {
84
+ lines.push("Step 1 — Token: ok");
85
+ lines.push("");
86
+ }
87
+ if (status.apiUrl.includes("localhost") || status.apiUrl.includes("127.0.0.1")) {
88
+ lines.push("Note — local API");
89
+ lines.push(" Tokens minted with workspace:add --local only work against localhost.");
90
+ lines.push(" Prod tokens need UPLOADS_API_URL=https://api.uploads.sh");
91
+ lines.push("");
92
+ }
93
+ lines.push("Step 2 — Optional put defaults");
94
+ lines.push(" uploads setup --prefix screenshots --repo myorg/myapp");
95
+ lines.push(" uploads config set UPLOADS_DEFAULT_REPO myorg/myapp");
96
+ lines.push("");
97
+ lines.push("Step 3 — Verify");
98
+ lines.push(" uploads doctor");
99
+ lines.push(" uploads put ./shot.png");
100
+ lines.push("");
101
+ if (status.token) {
102
+ lines.push("Quick save (all-in-one):");
103
+ lines.push(` uploads setup --api-url ${status.apiUrl} --workspace ${status.workspace} --token <token> --check`);
104
+ }
105
+ return lines.join("\n");
106
+ }
107
+ export async function runSetup(args, opts, help = false) {
108
+ const parsed = parseCommandArgs(args);
109
+ if (help || parsed.help) {
110
+ process.stderr.write(SETUP_HELP);
111
+ return 0;
112
+ }
113
+ const apiUrl = flagString(parsed.flags, "--api-url");
114
+ const workspace = flagString(parsed.flags, "--workspace") ?? flagString(parsed.flags, "-w");
115
+ const token = flagString(parsed.flags, "--token");
116
+ const prefix = flagString(parsed.flags, "--prefix");
117
+ const repo = flagString(parsed.flags, "--repo");
118
+ const ref = flagString(parsed.flags, "--ref");
119
+ const width = flagInt(parsed.flags, "--width", "--width");
120
+ const noGit = flagBool(parsed.flags, "--no-git");
121
+ const path = flagString(parsed.flags, "--path") ?? resolveConfigPath({ envFile: opts.envFile });
122
+ const force = flagBool(parsed.flags, "--force");
123
+ const checkExplicit = flagBool(parsed.flags, "--check");
124
+ const noCheck = flagBool(parsed.flags, "--no-check");
125
+ const hasWrites = apiUrl != null ||
126
+ workspace != null ||
127
+ token != null ||
128
+ prefix != null ||
129
+ repo != null ||
130
+ ref != null ||
131
+ width != null ||
132
+ noGit;
133
+ if (!hasWrites) {
134
+ const status = buildStatus(opts.envFile);
135
+ if (opts.json) {
136
+ process.stdout.write(JSON.stringify({
137
+ ...status,
138
+ token: redactToken(status.token),
139
+ complete: Boolean(status.token),
140
+ next: status.token ? "doctor" : "mint-token",
141
+ }, null, 2) + "\n");
142
+ }
143
+ else {
144
+ process.stdout.write(formatWizard(status));
145
+ }
146
+ return status.token ? 0 : 1;
147
+ }
148
+ const keys = {};
149
+ if (apiUrl)
150
+ keys.UPLOADS_API_URL = apiUrl;
151
+ if (workspace)
152
+ keys.UPLOADS_WORKSPACE = workspace;
153
+ if (token)
154
+ keys.UPLOADS_TOKEN = token;
155
+ Object.assign(keys, putDefaultsToConfigValues({
156
+ prefix: prefix ?? undefined,
157
+ repo: repo ?? undefined,
158
+ ref: ref ?? undefined,
159
+ width: width ?? undefined,
160
+ noGit: noGit || undefined,
161
+ }));
162
+ if (Object.keys(keys).length === 0) {
163
+ throw new UsageError("no setup values provided");
164
+ }
165
+ const result = writeConfigKeys(path, keys, { force });
166
+ const savedApiUrl = apiUrl ?? resolveApiUrl({ envFile: opts.envFile });
167
+ const savedWorkspace = workspace ?? (token ? workspaceFromToken(token) : undefined) ?? DEFAULT_WORKSPACE;
168
+ const shouldCheck = !noCheck && (checkExplicit || Boolean(token));
169
+ let doctorOk;
170
+ let doctorError;
171
+ if (shouldCheck) {
172
+ const cfg = resolveConfig({ envFile: opts.envFile, requireToken: false });
173
+ if (!cfg.token) {
174
+ doctorError = "no token configured — run uploads setup --token <token>";
175
+ }
176
+ else {
177
+ try {
178
+ const client = createUploadsClient({
179
+ apiUrl: cfg.apiUrl,
180
+ workspace: cfg.workspace,
181
+ token: cfg.token,
182
+ });
183
+ const health = await client.health();
184
+ if (!health.ok)
185
+ throw new UploadsError("API unhealthy", "API_ERROR");
186
+ await client.list({ limit: 1 });
187
+ doctorOk = true;
188
+ }
189
+ catch (err) {
190
+ doctorOk = false;
191
+ doctorError = err instanceof UploadsError ? err.message : String(err);
192
+ }
193
+ }
194
+ }
195
+ const payload = {
196
+ ...result,
197
+ keys: Object.keys(keys),
198
+ apiUrl: savedApiUrl,
199
+ workspace: savedWorkspace,
200
+ doctor: shouldCheck ? { ok: doctorOk, error: doctorError } : undefined,
201
+ };
202
+ if (opts.json) {
203
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
204
+ return doctorOk === false ? 1 : 0;
205
+ }
206
+ const verb = result.created ? "created" : "updated";
207
+ process.stdout.write(`${verb} ${result.path}\n`);
208
+ if (result.updated.length)
209
+ process.stdout.write(`keys: ${result.updated.join(", ")}\n`);
210
+ if (shouldCheck) {
211
+ if (doctorOk)
212
+ process.stdout.write("doctor: ok\n");
213
+ else
214
+ process.stderr.write(`doctor: failed — ${doctorError}\n`);
215
+ }
216
+ else if (token || checkExplicit) {
217
+ process.stderr.write("hint: run uploads doctor to verify\n");
218
+ }
219
+ else {
220
+ process.stderr.write("hint: run uploads setup to see minting instructions\n");
221
+ }
222
+ return doctorOk === false ? 1 : 0;
223
+ }
@@ -0,0 +1,19 @@
1
+ import { type UploadsClient } from "./client.js";
2
+ import { type ResolvedConfig } from "./config.js";
3
+ import { type CommandRunner } from "./github-gh.js";
4
+ export interface CliContext {
5
+ config: ResolvedConfig;
6
+ client: UploadsClient;
7
+ json: boolean;
8
+ quiet: boolean;
9
+ envFile?: string;
10
+ }
11
+ export declare function runAttach(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
12
+ export declare function runPut(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
13
+ export declare function runList(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
14
+ export declare function runDelete(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
15
+ export declare function runComment(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
16
+ export declare function runHealth(ctx: Pick<CliContext, "json"> & {
17
+ apiUrl: string;
18
+ }, args: string[], help?: boolean): Promise<number>;
19
+ export declare function runDoctor(ctx: CliContext, args: string[], help?: boolean): Promise<number>;