@buildinternet/uploads 0.1.0 → 0.1.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 +64 -64
- package/bin/uploads.js +0 -0
- package/dist/cli.js +55 -47
- package/dist/client.d.ts +20 -0
- package/dist/client.js +26 -0
- package/dist/commands/admin-enrollment.d.ts +7 -0
- package/dist/commands/admin-enrollment.js +59 -0
- package/dist/commands/login.d.ts +11 -0
- package/dist/commands/login.js +160 -0
- package/dist/commands/setup.js +6 -16
- package/dist/commands.js +76 -76
- package/dist/config-file.js +14 -2
- package/dist/config.js +1 -0
- package/package.json +60 -60
package/README.md
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
# @buildinternet/uploads
|
|
2
|
-
|
|
3
|
-
CLI and client for **uploads.sh** — upload files, get public URLs, and produce GitHub-ready markdown. Successor to the R2 scripts in `buildinternet-skills/github-screenshots`.
|
|
4
|
-
|
|
5
|
-
## CLI
|
|
6
|
-
|
|
7
|
-
Binary: `uploads` (also `pnpm uploads` from repo root after `pnpm install`).
|
|
8
|
-
|
|
9
|
-
Install globally or run a pinned version without installing:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install --global @buildinternet/uploads
|
|
13
|
-
npx @buildinternet/uploads@0.1.0 --help
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
pnpm uploads setup --env-file .env
|
|
18
|
-
pnpm uploads attach ./before.png ./after.png --env-file .env
|
|
19
|
-
pnpm uploads put ./shot.png --env-file .env
|
|
20
|
-
pnpm uploads put ./after.png --pr 123 --comment --env-file .env
|
|
21
|
-
pnpm uploads doctor --env-file .env
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Commands: `attach`, `put`, `comment`, `list`, `delete`, `setup`, `config`, `doctor`, `health`.
|
|
25
|
-
|
|
26
|
-
`attach` is the agent-friendly default for GitHub media. It accepts one or more files,
|
|
27
|
-
infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
|
|
28
|
-
or updates one managed attachments comment. Use `--pr`, `--issue`, and `--repo` to select
|
|
29
|
-
the target explicitly, or `--no-comment` to upload without changing GitHub comments.
|
|
30
|
-
|
|
31
|
-
Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
|
|
32
|
-
|
|
33
|
-
## Programmatic use
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
import { createUploadsClient } from "@buildinternet/uploads";
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers).
|
|
40
|
-
|
|
41
|
-
## Layout
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
src/
|
|
45
|
-
cli.ts Entry + help
|
|
46
|
-
commands.ts put, list, delete, comment, …
|
|
47
|
-
client.ts HTTP client for the API
|
|
48
|
-
github.ts PR/issue key paths + attachment comments
|
|
49
|
-
embed.ts Markdown image output
|
|
50
|
-
bin/uploads.js Bin shim
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Commands
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
pnpm build # tsc → dist/
|
|
57
|
-
pnpm typecheck
|
|
58
|
-
pnpm test
|
|
59
|
-
pnpm pack:check # verify the npm tarball contents
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Maintainer release instructions: [`docs/releasing.md`](../../docs/releasing.md).
|
|
63
|
-
|
|
64
|
-
Agent-oriented usage: [`skills/uploads-cli/SKILL.md`](../../skills/uploads-cli/SKILL.md). REST details: [`docs/api.md`](../../docs/api.md).
|
|
1
|
+
# @buildinternet/uploads
|
|
2
|
+
|
|
3
|
+
CLI and client for **uploads.sh** — upload files, get public URLs, and produce GitHub-ready markdown. Successor to the R2 scripts in `buildinternet-skills/github-screenshots`.
|
|
4
|
+
|
|
5
|
+
## CLI
|
|
6
|
+
|
|
7
|
+
Binary: `uploads` (also `pnpm uploads` from repo root after `pnpm install`).
|
|
8
|
+
|
|
9
|
+
Install globally or run a pinned version without installing:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install --global @buildinternet/uploads
|
|
13
|
+
npx @buildinternet/uploads@0.1.0 --help
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm uploads setup --env-file .env
|
|
18
|
+
pnpm uploads attach ./before.png ./after.png --env-file .env
|
|
19
|
+
pnpm uploads put ./shot.png --env-file .env
|
|
20
|
+
pnpm uploads put ./after.png --pr 123 --comment --env-file .env
|
|
21
|
+
pnpm uploads doctor --env-file .env
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Commands: `attach`, `put`, `comment`, `list`, `delete`, `setup`, `config`, `doctor`, `health`.
|
|
25
|
+
|
|
26
|
+
`attach` is the agent-friendly default for GitHub media. It accepts one or more files,
|
|
27
|
+
infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
|
|
28
|
+
or updates one managed attachments comment. Use `--pr`, `--issue`, and `--repo` to select
|
|
29
|
+
the target explicitly, or `--no-comment` to upload without changing GitHub comments.
|
|
30
|
+
|
|
31
|
+
Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
|
|
32
|
+
|
|
33
|
+
## Programmatic use
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { createUploadsClient } from "@buildinternet/uploads";
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers).
|
|
40
|
+
|
|
41
|
+
## Layout
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
src/
|
|
45
|
+
cli.ts Entry + help
|
|
46
|
+
commands.ts put, list, delete, comment, …
|
|
47
|
+
client.ts HTTP client for the API
|
|
48
|
+
github.ts PR/issue key paths + attachment comments
|
|
49
|
+
embed.ts Markdown image output
|
|
50
|
+
bin/uploads.js Bin shim
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm build # tsc → dist/
|
|
57
|
+
pnpm typecheck
|
|
58
|
+
pnpm test
|
|
59
|
+
pnpm pack:check # verify the npm tarball contents
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Maintainer release instructions: [`docs/releasing.md`](../../docs/releasing.md).
|
|
63
|
+
|
|
64
|
+
Agent-oriented usage: [`skills/uploads-cli/SKILL.md`](../../skills/uploads-cli/SKILL.md). REST details: [`docs/api.md`](../../docs/api.md).
|
package/bin/uploads.js
CHANGED
|
File without changes
|
package/dist/cli.js
CHANGED
|
@@ -5,53 +5,57 @@ import { commandWorkspace, isHelpFlag, parseArgv, parseCommandArgs, UsageError,
|
|
|
5
5
|
import { runPut, runAttach, runList, runDelete, runHealth, runDoctor, runComment, } from "./commands.js";
|
|
6
6
|
import { runConfig } from "./commands/config.js";
|
|
7
7
|
import { runSetup } from "./commands/setup.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
uploads
|
|
53
|
-
|
|
54
|
-
|
|
8
|
+
import { runLogin } from "./commands/login.js";
|
|
9
|
+
import { runAdmin } from "./commands/admin-enrollment.js";
|
|
10
|
+
const ROOT_HELP = `uploads — CLI for uploads.sh (GitHub image embeds)
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
uploads [globals] <command> [args]
|
|
14
|
+
|
|
15
|
+
Config (first match wins, per key):
|
|
16
|
+
CLI flags --api-url, --token, --workspace
|
|
17
|
+
environment UPLOADS_API_URL, UPLOADS_TOKEN, UPLOADS_WORKSPACE
|
|
18
|
+
--env-file <path>
|
|
19
|
+
$BUILDINTERNET_CONFIG
|
|
20
|
+
~/.config/buildinternet/config (shared with github-screenshots)
|
|
21
|
+
|
|
22
|
+
Workspace (within config layers):
|
|
23
|
+
--workspace, -w override — global (before command) or per-command (after)
|
|
24
|
+
UPLOADS_WORKSPACE env / config file
|
|
25
|
+
(else inferred from token up_<name>_…, else "default")
|
|
26
|
+
|
|
27
|
+
Other globals (before command):
|
|
28
|
+
--api-url <url> default: https://api.uploads.sh
|
|
29
|
+
--token <token> or UPLOADS_TOKEN
|
|
30
|
+
--env-file <path>
|
|
31
|
+
--json JSON on stdout
|
|
32
|
+
--quiet
|
|
33
|
+
|
|
34
|
+
Commands:
|
|
35
|
+
attach <file...> Attach media to the current PR (stable URLs + managed comment)
|
|
36
|
+
put <file> Upload (+ URL + markdown for GitHub)
|
|
37
|
+
comment Create/update a PR/issue attachments comment (via gh)
|
|
38
|
+
list List objects
|
|
39
|
+
delete <key> Delete object
|
|
40
|
+
setup Inspect/configure advanced CLI settings
|
|
41
|
+
login Exchange an enrollment code and configure credentials
|
|
42
|
+
admin Admin enrollment management
|
|
43
|
+
config Show path, init, or set shared config
|
|
44
|
+
doctor Health + auth + workspace checks
|
|
45
|
+
health API liveness (no auth)
|
|
46
|
+
|
|
47
|
+
Put/list defaults (config file or env):
|
|
48
|
+
UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
|
|
49
|
+
UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
uploads setup
|
|
53
|
+
uploads setup --token up_default_… --repo myorg/myapp
|
|
54
|
+
uploads attach ./before.png ./after.png
|
|
55
|
+
uploads put ./shot.png --ref 42
|
|
56
|
+
uploads doctor
|
|
57
|
+
|
|
58
|
+
Agent/MCP: use createUploadsWorkerFileTools() from @buildinternet/uploads/agent on the Worker.
|
|
55
59
|
`;
|
|
56
60
|
function createContext(globals, requireToken, commandArgs) {
|
|
57
61
|
const cmdWorkspace = commandWorkspace(parseCommandArgs(commandArgs).flags);
|
|
@@ -122,6 +126,10 @@ export async function runCli(argv) {
|
|
|
122
126
|
return runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
123
127
|
case "setup":
|
|
124
128
|
return runSetup(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
129
|
+
case "login":
|
|
130
|
+
return runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
131
|
+
case "admin":
|
|
132
|
+
return runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
125
133
|
case "attach":
|
|
126
134
|
case "put":
|
|
127
135
|
case "list":
|
package/dist/client.d.ts
CHANGED
|
@@ -43,6 +43,26 @@ export interface DeleteResult {
|
|
|
43
43
|
export interface HealthResult {
|
|
44
44
|
ok: boolean;
|
|
45
45
|
}
|
|
46
|
+
export interface EnrollmentExchangeResult {
|
|
47
|
+
apiUrl?: string;
|
|
48
|
+
workspace: string;
|
|
49
|
+
token: string;
|
|
50
|
+
scopes?: Array<"files:read" | "files:write" | "files:delete">;
|
|
51
|
+
expiresAt?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface EnrollmentCreateResult {
|
|
54
|
+
code: string;
|
|
55
|
+
expiresAt: string;
|
|
56
|
+
tokenExpiresAt: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function exchangeEnrollment(apiUrl: string, code: string): Promise<EnrollmentExchangeResult>;
|
|
59
|
+
export declare function createEnrollment(apiUrl: string, adminToken: string, input: {
|
|
60
|
+
workspace?: string;
|
|
61
|
+
label?: string;
|
|
62
|
+
enrollmentSeconds?: number;
|
|
63
|
+
tokenExpiresInSeconds?: number;
|
|
64
|
+
scopes?: Array<"files:read" | "files:write" | "files:delete">;
|
|
65
|
+
}): Promise<EnrollmentCreateResult>;
|
|
46
66
|
export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
47
67
|
put(body: Uint8Array, opts: PutOptions & {
|
|
48
68
|
filename: string;
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { inferContentType } from "./embed.js";
|
|
2
2
|
import { UploadsError } from "./errors.js";
|
|
3
3
|
import { buildScreenshotKey } from "./keys.js";
|
|
4
|
+
async function jsonRequest(url, init) {
|
|
5
|
+
let res;
|
|
6
|
+
try {
|
|
7
|
+
res = await fetch(url, init);
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
throw new UploadsError(err instanceof Error ? err.message : "network request failed", "NETWORK");
|
|
11
|
+
}
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw await parseErrorResponse(res);
|
|
14
|
+
return (await res.json());
|
|
15
|
+
}
|
|
16
|
+
export function exchangeEnrollment(apiUrl, code) {
|
|
17
|
+
return jsonRequest(`${apiUrl.replace(/\/$/, "")}/auth/enrollments/exchange`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
body: JSON.stringify({ code }),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function createEnrollment(apiUrl, adminToken, input) {
|
|
24
|
+
return jsonRequest(`${apiUrl.replace(/\/$/, "")}/admin/enrollments`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify(input),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
4
30
|
function encodeKeyPath(key) {
|
|
5
31
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
6
32
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type FileScope = "files:read" | "files:write" | "files:delete";
|
|
2
|
+
export declare function parseScopes(raw: string | undefined): FileScope[] | undefined;
|
|
3
|
+
export declare function runAdmin(args: string[], opts: {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
}, help?: boolean): Promise<number>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createEnrollment } from "../client.js";
|
|
2
|
+
import { flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
3
|
+
const HELP = `uploads admin enrollment create [options]
|
|
4
|
+
|
|
5
|
+
Admin-only: create a short-lived, one-time enrollment code.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
--admin-token <token> Or ADMIN_TOKEN (UPLOADS_ADMIN_TOKEN is a legacy alias)
|
|
9
|
+
--workspace <name> Default: default
|
|
10
|
+
--label <label>
|
|
11
|
+
--expires-in <seconds> Default: server policy
|
|
12
|
+
--token-expires-in <seconds> Upload token lifetime (default: server policy)
|
|
13
|
+
--scopes <list> Comma-separated files:read,files:write,files:delete
|
|
14
|
+
--api-url <url> Default: https://api.uploads.sh
|
|
15
|
+
`;
|
|
16
|
+
const FILE_SCOPES = new Set(["files:read", "files:write", "files:delete"]);
|
|
17
|
+
export function parseScopes(raw) {
|
|
18
|
+
if (raw === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const scopes = raw
|
|
21
|
+
.split(",")
|
|
22
|
+
.map((scope) => scope.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
if (scopes.length === 0)
|
|
25
|
+
throw new UsageError("--scopes must contain at least one scope");
|
|
26
|
+
const invalid = scopes.find((scope) => !FILE_SCOPES.has(scope));
|
|
27
|
+
if (invalid)
|
|
28
|
+
throw new UsageError(`invalid scope: ${invalid} (expected files:read, files:write, or files:delete)`);
|
|
29
|
+
return [...new Set(scopes)];
|
|
30
|
+
}
|
|
31
|
+
export async function runAdmin(args, opts, help = false) {
|
|
32
|
+
const parsed = parseCommandArgs(args);
|
|
33
|
+
if (help || parsed.help) {
|
|
34
|
+
process.stderr.write(HELP);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
if (parsed.positionals[0] !== "enrollment" || parsed.positionals[1] !== "create")
|
|
38
|
+
throw new UsageError("expected: uploads admin enrollment create");
|
|
39
|
+
const adminToken = flagString(parsed.flags, "--admin-token") ??
|
|
40
|
+
process.env.ADMIN_TOKEN ??
|
|
41
|
+
process.env.UPLOADS_ADMIN_TOKEN;
|
|
42
|
+
if (!adminToken)
|
|
43
|
+
throw new UsageError("ADMIN_TOKEN is required for admin enrollment creation");
|
|
44
|
+
const apiUrl = flagString(parsed.flags, "--api-url") ?? opts.apiUrl ?? "https://api.uploads.sh";
|
|
45
|
+
const workspace = flagString(parsed.flags, "--workspace") ?? "default";
|
|
46
|
+
const label = flagString(parsed.flags, "--label");
|
|
47
|
+
const result = await createEnrollment(apiUrl, adminToken, {
|
|
48
|
+
workspace,
|
|
49
|
+
label,
|
|
50
|
+
enrollmentSeconds: flagInt(parsed.flags, "--expires-in", "--expires-in"),
|
|
51
|
+
tokenExpiresInSeconds: flagInt(parsed.flags, "--token-expires-in", "--token-expires-in"),
|
|
52
|
+
scopes: parseScopes(flagString(parsed.flags, "--scopes")),
|
|
53
|
+
});
|
|
54
|
+
if (opts.json)
|
|
55
|
+
process.stdout.write(JSON.stringify({ workspace, label: label ?? null, ...result }, null, 2) + "\n");
|
|
56
|
+
else
|
|
57
|
+
process.stdout.write(`Enrollment code (share once): ${result.code}\nworkspace: ${workspace}\nexpires: ${result.expiresAt}\n`);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseCommandArgs } from "../cli-args.js";
|
|
2
|
+
export declare function validateEnrollmentCode(raw: string): string;
|
|
3
|
+
export declare function resolveEnrollmentCode(parsed: ReturnType<typeof parseCommandArgs>, io?: {
|
|
4
|
+
isTTY: boolean;
|
|
5
|
+
readLine: () => Promise<string>;
|
|
6
|
+
hiddenPrompt: () => Promise<string>;
|
|
7
|
+
}): Promise<string>;
|
|
8
|
+
export declare function runLogin(args: string[], opts: {
|
|
9
|
+
json?: boolean;
|
|
10
|
+
apiUrl?: string;
|
|
11
|
+
}, help?: boolean): Promise<number>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { stdin, stdout } from "node:process";
|
|
2
|
+
import { loadConfigFile, redactToken, resolveConfigPath, writeConfigKeys, workspaceFromToken, } from "../config.js";
|
|
3
|
+
import { exchangeEnrollment, createUploadsClient } from "../client.js";
|
|
4
|
+
import { flagBool, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
5
|
+
const HELP = `uploads login [options]
|
|
6
|
+
|
|
7
|
+
Exchange a one-time enrollment code for workspace credentials, save them, and
|
|
8
|
+
verify access. Ask your uploads.sh administrator for an enrollment code.
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--code <code> Code in argv (may be visible in shell history/process lists)
|
|
12
|
+
--code-stdin Read one line from stdin
|
|
13
|
+
--non-interactive Never prompt
|
|
14
|
+
--api-url <url> API base (default: https://api.uploads.sh)
|
|
15
|
+
--path <file> Config destination
|
|
16
|
+
--force Replace existing saved credentials
|
|
17
|
+
--no-check Skip doctor verification
|
|
18
|
+
`;
|
|
19
|
+
export function validateEnrollmentCode(raw) {
|
|
20
|
+
const code = raw.trim();
|
|
21
|
+
if (!/^upe_[A-Za-z0-9_-]{20,}$/.test(code))
|
|
22
|
+
throw new UsageError("invalid enrollment code");
|
|
23
|
+
return code;
|
|
24
|
+
}
|
|
25
|
+
async function readLine() {
|
|
26
|
+
let out = "";
|
|
27
|
+
for await (const chunk of stdin) {
|
|
28
|
+
out += String(chunk);
|
|
29
|
+
if (out.includes("\n"))
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
return out.split(/\r?\n/, 1)[0] ?? "";
|
|
33
|
+
}
|
|
34
|
+
async function hiddenPrompt() {
|
|
35
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function")
|
|
36
|
+
return readLine();
|
|
37
|
+
stdout.write("Enrollment code: ");
|
|
38
|
+
stdin.setRawMode(true);
|
|
39
|
+
stdin.resume();
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let value = "";
|
|
42
|
+
let settled = false;
|
|
43
|
+
const done = (err) => {
|
|
44
|
+
if (settled)
|
|
45
|
+
return;
|
|
46
|
+
settled = true;
|
|
47
|
+
stdin.off("data", onData);
|
|
48
|
+
stdin.off("error", onError);
|
|
49
|
+
try {
|
|
50
|
+
stdin.setRawMode(false);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
stdin.pause();
|
|
54
|
+
stdout.write("\n");
|
|
55
|
+
}
|
|
56
|
+
if (err)
|
|
57
|
+
reject(err);
|
|
58
|
+
else
|
|
59
|
+
resolve(value);
|
|
60
|
+
};
|
|
61
|
+
const onData = (chunk) => {
|
|
62
|
+
for (const char of chunk.toString("utf8")) {
|
|
63
|
+
if (char === "\r" || char === "\n")
|
|
64
|
+
return done();
|
|
65
|
+
if (char === "\u0003")
|
|
66
|
+
return done(new UsageError("login cancelled"));
|
|
67
|
+
if (char === "\u007f")
|
|
68
|
+
value = value.slice(0, -1);
|
|
69
|
+
else
|
|
70
|
+
value += char;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const onError = (err) => done(err);
|
|
74
|
+
stdin.on("data", onData);
|
|
75
|
+
stdin.on("error", onError);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function resolveEnrollmentCode(parsed, io = {
|
|
79
|
+
isTTY: Boolean(stdin.isTTY),
|
|
80
|
+
readLine,
|
|
81
|
+
hiddenPrompt,
|
|
82
|
+
}) {
|
|
83
|
+
const direct = flagString(parsed.flags, "--code");
|
|
84
|
+
const env = process.env.UPLOADS_ENROLLMENT_CODE;
|
|
85
|
+
const fromStdin = flagBool(parsed.flags, "--code-stdin");
|
|
86
|
+
const sources = [Boolean(direct), Boolean(env), fromStdin].filter(Boolean).length;
|
|
87
|
+
if (sources > 1)
|
|
88
|
+
throw new UsageError("provide enrollment code through only one source");
|
|
89
|
+
if (direct)
|
|
90
|
+
return validateEnrollmentCode(direct);
|
|
91
|
+
if (env)
|
|
92
|
+
return validateEnrollmentCode(env);
|
|
93
|
+
if (fromStdin)
|
|
94
|
+
return validateEnrollmentCode(await io.readLine());
|
|
95
|
+
if (flagBool(parsed.flags, "--non-interactive"))
|
|
96
|
+
throw new UsageError("enrollment code required in non-interactive mode");
|
|
97
|
+
if (!io.isTTY)
|
|
98
|
+
return validateEnrollmentCode(await io.readLine());
|
|
99
|
+
return validateEnrollmentCode(await io.hiddenPrompt());
|
|
100
|
+
}
|
|
101
|
+
export async function runLogin(args, opts, help = false) {
|
|
102
|
+
const parsed = parseCommandArgs(args);
|
|
103
|
+
if (help || parsed.help) {
|
|
104
|
+
process.stderr.write(HELP);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
const apiUrl = flagString(parsed.flags, "--api-url") ?? opts.apiUrl ?? "https://api.uploads.sh";
|
|
108
|
+
const path = flagString(parsed.flags, "--path") ?? resolveConfigPath();
|
|
109
|
+
const force = flagBool(parsed.flags, "--force");
|
|
110
|
+
const existing = loadConfigFile(path);
|
|
111
|
+
if (existing.UPLOADS_TOKEN && !force)
|
|
112
|
+
throw new UsageError(`credentials already exist in ${path}; use --force to replace them`);
|
|
113
|
+
if (process.env.UPLOADS_TOKEN && !force)
|
|
114
|
+
throw new UsageError("UPLOADS_TOKEN is already set in the environment; unset it or use --force");
|
|
115
|
+
const code = await resolveEnrollmentCode(parsed);
|
|
116
|
+
const result = await exchangeEnrollment(apiUrl, code);
|
|
117
|
+
const encoded = workspaceFromToken(result.token);
|
|
118
|
+
if (!encoded || encoded !== result.workspace || /[\r\n]/.test(result.token))
|
|
119
|
+
throw new UsageError("enrollment returned invalid credentials");
|
|
120
|
+
const savedApiUrl = result.apiUrl ?? apiUrl;
|
|
121
|
+
const write = writeConfigKeys(path, {
|
|
122
|
+
UPLOADS_API_URL: savedApiUrl,
|
|
123
|
+
UPLOADS_WORKSPACE: result.workspace,
|
|
124
|
+
UPLOADS_TOKEN: result.token,
|
|
125
|
+
}, { force });
|
|
126
|
+
if (!["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN"].every((key) => write.updated.includes(key)))
|
|
127
|
+
throw new UsageError("credentials were not fully written; retry with --force");
|
|
128
|
+
const checked = !flagBool(parsed.flags, "--no-check");
|
|
129
|
+
let doctor = { ok: true, error: undefined };
|
|
130
|
+
if (checked) {
|
|
131
|
+
try {
|
|
132
|
+
const client = createUploadsClient({
|
|
133
|
+
apiUrl: savedApiUrl,
|
|
134
|
+
workspace: result.workspace,
|
|
135
|
+
token: result.token,
|
|
136
|
+
});
|
|
137
|
+
const health = await client.health();
|
|
138
|
+
if (!health.ok)
|
|
139
|
+
throw new Error("API unhealthy");
|
|
140
|
+
await client.list({ limit: 1 });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
doctor = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const payload = {
|
|
147
|
+
ok: doctor.ok,
|
|
148
|
+
configPath: path,
|
|
149
|
+
workspace: result.workspace,
|
|
150
|
+
token: redactToken(result.token),
|
|
151
|
+
doctor: checked ? doctor : { skipped: true },
|
|
152
|
+
};
|
|
153
|
+
if (opts.json)
|
|
154
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
155
|
+
else {
|
|
156
|
+
process.stdout.write(`saved credentials to ${path}\nworkspace: ${result.workspace}\ntoken: ${redactToken(result.token)}\n`);
|
|
157
|
+
process[doctor.ok ? "stdout" : "stderr"].write(`doctor: ${checked ? (doctor.ok ? "ok" : `failed — ${doctor.error}`) : "skipped"}\n`);
|
|
158
|
+
}
|
|
159
|
+
return doctor.ok ? 0 : 1;
|
|
160
|
+
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -13,7 +13,7 @@ With flags: saves provided values, then optionally verifies with doctor.
|
|
|
13
13
|
Options:
|
|
14
14
|
--api-url <url> API base (default: ${DEFAULT_API_URL})
|
|
15
15
|
--workspace, -w <name>
|
|
16
|
-
--token <token>
|
|
16
|
+
--token <token> Existing bearer token (or use uploads login)
|
|
17
17
|
--prefix <path> Default key prefix for put/list (default: screenshots)
|
|
18
18
|
--repo <owner/repo> Default repo segment for put
|
|
19
19
|
--ref <id> Default ref segment for put (PR/issue/branch/date)
|
|
@@ -41,15 +41,6 @@ function buildStatus(envFile) {
|
|
|
41
41
|
sources: describeConfigSources({ envFile }),
|
|
42
42
|
};
|
|
43
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
44
|
function formatWizard(status) {
|
|
54
45
|
const lines = ["uploads setup", ""];
|
|
55
46
|
lines.push(`Config file: ${status.configPath}`);
|
|
@@ -72,11 +63,10 @@ function formatWizard(status) {
|
|
|
72
63
|
}
|
|
73
64
|
lines.push("");
|
|
74
65
|
if (!status.token) {
|
|
75
|
-
lines.push("Step 1 —
|
|
76
|
-
lines.push("
|
|
77
|
-
lines.push("
|
|
78
|
-
lines.push(
|
|
79
|
-
lines.push(" Save:");
|
|
66
|
+
lines.push("Step 1 — Sign in");
|
|
67
|
+
lines.push(" Ask your uploads.sh administrator for a one-time enrollment code, then run:");
|
|
68
|
+
lines.push(" uploads login");
|
|
69
|
+
lines.push(" If you already have a bearer token:");
|
|
80
70
|
lines.push(" uploads setup --token up_<workspace>_…");
|
|
81
71
|
lines.push("");
|
|
82
72
|
}
|
|
@@ -217,7 +207,7 @@ export async function runSetup(args, opts, help = false) {
|
|
|
217
207
|
process.stderr.write("hint: run uploads doctor to verify\n");
|
|
218
208
|
}
|
|
219
209
|
else {
|
|
220
|
-
process.stderr.write("hint: run uploads
|
|
210
|
+
process.stderr.write("hint: run uploads login with an admin-provided enrollment code\n");
|
|
221
211
|
}
|
|
222
212
|
return doctorOk === false ? 1 : 0;
|
|
223
213
|
}
|
package/dist/commands.js
CHANGED
|
@@ -16,29 +16,29 @@ async function writeJson(value) {
|
|
|
16
16
|
await writeStdout(JSON.stringify(value, null, 2) + "\n");
|
|
17
17
|
}
|
|
18
18
|
// --- put ---
|
|
19
|
-
const PUT_HELP = `uploads put <file> [options]
|
|
20
|
-
|
|
21
|
-
Upload an image for GitHub embeds. Use "-" for stdin.
|
|
22
|
-
|
|
23
|
-
Options:
|
|
24
|
-
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
25
|
-
--prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
|
|
26
|
-
--repo <owner/repo> Repo segment (default: git remote, or UPLOADS_DEFAULT_REPO)
|
|
27
|
-
--ref <id> PR/issue/branch segment (default: today, or UPLOADS_DEFAULT_REF)
|
|
28
|
-
--alt <text> Alt text (default: filename)
|
|
29
|
-
--width <px> <img width=…> markdown (or UPLOADS_DEFAULT_WIDTH)
|
|
30
|
-
--content-type <mime> Override Content-Type
|
|
31
|
-
--no-git Don't derive --repo from git (or UPLOADS_NO_GIT=1)
|
|
32
|
-
--workspace, -w <name> Override workspace (wins over UPLOADS_WORKSPACE and token inference)
|
|
33
|
-
--format human|url|markdown|json
|
|
34
|
-
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
35
|
-
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
36
|
-
--comment With --pr/--issue: create/update the attachments comment via your local gh auth
|
|
37
|
-
|
|
38
|
-
Examples:
|
|
39
|
-
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
40
|
-
uploads --env-file .env put ./shot.png
|
|
41
|
-
uploads --env-file .env put ./after.png --pr 123 --comment
|
|
19
|
+
const PUT_HELP = `uploads put <file> [options]
|
|
20
|
+
|
|
21
|
+
Upload an image for GitHub embeds. Use "-" for stdin.
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
25
|
+
--prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
|
|
26
|
+
--repo <owner/repo> Repo segment (default: git remote, or UPLOADS_DEFAULT_REPO)
|
|
27
|
+
--ref <id> PR/issue/branch segment (default: today, or UPLOADS_DEFAULT_REF)
|
|
28
|
+
--alt <text> Alt text (default: filename)
|
|
29
|
+
--width <px> <img width=…> markdown (or UPLOADS_DEFAULT_WIDTH)
|
|
30
|
+
--content-type <mime> Override Content-Type
|
|
31
|
+
--no-git Don't derive --repo from git (or UPLOADS_NO_GIT=1)
|
|
32
|
+
--workspace, -w <name> Override workspace (wins over UPLOADS_WORKSPACE and token inference)
|
|
33
|
+
--format human|url|markdown|json
|
|
34
|
+
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
35
|
+
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
36
|
+
--comment With --pr/--issue: create/update the attachments comment via your local gh auth
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
40
|
+
uploads --env-file .env put ./shot.png
|
|
41
|
+
uploads --env-file .env put ./after.png --pr 123 --comment
|
|
42
42
|
`;
|
|
43
43
|
/** Reads --pr/--issue (+ --repo) into a GhTarget; undefined when neither flag is present. */
|
|
44
44
|
function ghTargetFromFlags(flags, run) {
|
|
@@ -72,23 +72,23 @@ async function syncAttachmentsComment(ctx, target, run) {
|
|
|
72
72
|
return { action: created ? "created" : "updated", count: items.length };
|
|
73
73
|
}
|
|
74
74
|
// --- attach ---
|
|
75
|
-
const ATTACH_HELP = `uploads attach <file...> [options]
|
|
76
|
-
|
|
77
|
-
Upload one or more stable PR/issue attachments and maintain a single GitHub
|
|
78
|
-
comment. With no target, uses the pull request for the current branch.
|
|
79
|
-
|
|
80
|
-
Options:
|
|
81
|
-
--pr <num> Attach to this pull request
|
|
82
|
-
--issue <num> Attach to this issue
|
|
83
|
-
--repo <owner/repo> Repository (default: gh/git inference)
|
|
84
|
-
--no-comment Upload only; don't create/update the managed comment
|
|
85
|
-
--content-type <mime> Override Content-Type (applied to every file)
|
|
86
|
-
--workspace, -w <name> Override workspace
|
|
87
|
-
|
|
88
|
-
Examples:
|
|
89
|
-
uploads attach ./before.png ./after.png
|
|
90
|
-
uploads attach ./shot.png --pr 123 --repo myorg/myapp
|
|
91
|
-
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
75
|
+
const ATTACH_HELP = `uploads attach <file...> [options]
|
|
76
|
+
|
|
77
|
+
Upload one or more stable PR/issue attachments and maintain a single GitHub
|
|
78
|
+
comment. With no target, uses the pull request for the current branch.
|
|
79
|
+
|
|
80
|
+
Options:
|
|
81
|
+
--pr <num> Attach to this pull request
|
|
82
|
+
--issue <num> Attach to this issue
|
|
83
|
+
--repo <owner/repo> Repository (default: gh/git inference)
|
|
84
|
+
--no-comment Upload only; don't create/update the managed comment
|
|
85
|
+
--content-type <mime> Override Content-Type (applied to every file)
|
|
86
|
+
--workspace, -w <name> Override workspace
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
uploads attach ./before.png ./after.png
|
|
90
|
+
uploads attach ./shot.png --pr 123 --repo myorg/myapp
|
|
91
|
+
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
92
92
|
`;
|
|
93
93
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
94
94
|
const parsed = parseCommandArgs(args);
|
|
@@ -243,14 +243,14 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
243
243
|
return 0;
|
|
244
244
|
}
|
|
245
245
|
// --- list ---
|
|
246
|
-
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
247
|
-
|
|
248
|
-
Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
249
|
-
|
|
250
|
-
Examples:
|
|
251
|
-
uploads list --prefix screenshots/
|
|
252
|
-
uploads list --pr 123
|
|
253
|
-
uploads list --all --json
|
|
246
|
+
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
247
|
+
|
|
248
|
+
Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
249
|
+
|
|
250
|
+
Examples:
|
|
251
|
+
uploads list --prefix screenshots/
|
|
252
|
+
uploads list --pr 123
|
|
253
|
+
uploads list --all --json
|
|
254
254
|
`;
|
|
255
255
|
export async function runList(ctx, args, help = false, run = execRunner) {
|
|
256
256
|
const parsed = parseCommandArgs(args);
|
|
@@ -296,10 +296,10 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
296
296
|
return 0;
|
|
297
297
|
}
|
|
298
298
|
// --- delete ---
|
|
299
|
-
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
300
|
-
|
|
301
|
-
Examples:
|
|
302
|
-
uploads delete screenshots/myapp/42/shot-a1b2c3.png
|
|
299
|
+
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
300
|
+
|
|
301
|
+
Examples:
|
|
302
|
+
uploads delete screenshots/myapp/42/shot-a1b2c3.png
|
|
303
303
|
`;
|
|
304
304
|
export async function runDelete(ctx, args, help = false) {
|
|
305
305
|
const parsed = parseCommandArgs(args);
|
|
@@ -327,16 +327,16 @@ export async function runDelete(ctx, args, help = false) {
|
|
|
327
327
|
return 0;
|
|
328
328
|
}
|
|
329
329
|
// --- comment ---
|
|
330
|
-
const COMMENT_HELP = `uploads comment (--pr <num> | --issue <num>) [--repo <owner/name>] [--workspace <name>]
|
|
331
|
-
|
|
332
|
-
Create or update the managed attachments comment on a GitHub PR or issue,
|
|
333
|
-
listing everything uploaded for it. Uses your local gh auth. Finds its own
|
|
334
|
-
prior comment via a hidden marker and edits it in place; never touches other
|
|
335
|
-
comments or the description.
|
|
336
|
-
|
|
337
|
-
Examples:
|
|
338
|
-
uploads --env-file .env comment --pr 123
|
|
339
|
-
uploads comment --issue 45 --repo buildinternet/uploads
|
|
330
|
+
const COMMENT_HELP = `uploads comment (--pr <num> | --issue <num>) [--repo <owner/name>] [--workspace <name>]
|
|
331
|
+
|
|
332
|
+
Create or update the managed attachments comment on a GitHub PR or issue,
|
|
333
|
+
listing everything uploaded for it. Uses your local gh auth. Finds its own
|
|
334
|
+
prior comment via a hidden marker and edits it in place; never touches other
|
|
335
|
+
comments or the description.
|
|
336
|
+
|
|
337
|
+
Examples:
|
|
338
|
+
uploads --env-file .env comment --pr 123
|
|
339
|
+
uploads comment --issue 45 --repo buildinternet/uploads
|
|
340
340
|
`;
|
|
341
341
|
export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
342
342
|
const parsed = parseCommandArgs(args);
|
|
@@ -359,13 +359,13 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
|
359
359
|
return 0;
|
|
360
360
|
}
|
|
361
361
|
// --- health & doctor ---
|
|
362
|
-
const HEALTH_HELP = `uploads health
|
|
363
|
-
|
|
364
|
-
API liveness (no auth).
|
|
365
|
-
|
|
366
|
-
Examples:
|
|
367
|
-
uploads health
|
|
368
|
-
uploads --api-url http://localhost:8787 health
|
|
362
|
+
const HEALTH_HELP = `uploads health
|
|
363
|
+
|
|
364
|
+
API liveness (no auth).
|
|
365
|
+
|
|
366
|
+
Examples:
|
|
367
|
+
uploads health
|
|
368
|
+
uploads --api-url http://localhost:8787 health
|
|
369
369
|
`;
|
|
370
370
|
export async function runHealth(ctx, args, help = false) {
|
|
371
371
|
if (help || parseCommandArgs(args).help) {
|
|
@@ -383,13 +383,13 @@ export async function runHealth(ctx, args, help = false) {
|
|
|
383
383
|
await writeStdout(result.ok ? `ok (${ctx.apiUrl})\n` : `unhealthy (${ctx.apiUrl})\n`);
|
|
384
384
|
return result.ok ? 0 : 1;
|
|
385
385
|
}
|
|
386
|
-
const DOCTOR_HELP = `uploads doctor [--workspace <name>]
|
|
387
|
-
|
|
388
|
-
Checks API health, token auth, and workspace/token alignment.
|
|
389
|
-
|
|
390
|
-
Examples:
|
|
391
|
-
uploads --env-file .env doctor
|
|
392
|
-
uploads --workspace acme --env-file .env doctor
|
|
386
|
+
const DOCTOR_HELP = `uploads doctor [--workspace <name>]
|
|
387
|
+
|
|
388
|
+
Checks API health, token auth, and workspace/token alignment.
|
|
389
|
+
|
|
390
|
+
Examples:
|
|
391
|
+
uploads --env-file .env doctor
|
|
392
|
+
uploads --workspace acme --env-file .env doctor
|
|
393
393
|
`;
|
|
394
394
|
export async function runDoctor(ctx, args, help = false) {
|
|
395
395
|
if (help || parseCommandArgs(args).help) {
|
package/dist/config-file.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
export const UPLOADS_CONFIG_KEYS = [
|
|
@@ -162,6 +162,10 @@ const INIT_HEADER = `# uploads.sh CLI — shared buildinternet config
|
|
|
162
162
|
/** Create or update UPLOADS_* keys in the shared config file. Preserves other keys. */
|
|
163
163
|
export function writeConfigKeys(path, keys, opts) {
|
|
164
164
|
const entries = Object.entries(keys).filter(([, v]) => v !== undefined && v !== "");
|
|
165
|
+
for (const [key, value] of entries) {
|
|
166
|
+
if (/[\r\n]/.test(value))
|
|
167
|
+
throw new Error(`invalid newline in ${key}`);
|
|
168
|
+
}
|
|
165
169
|
if (entries.length === 0) {
|
|
166
170
|
throw new Error("no config values to write");
|
|
167
171
|
}
|
|
@@ -187,7 +191,15 @@ export function writeConfigKeys(path, keys, opts) {
|
|
|
187
191
|
updated.push(key);
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
|
-
|
|
194
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
195
|
+
writeFileSync(tmp, lines.join("\n").replace(/\n*$/, "\n"), { encoding: "utf8", mode: 0o600 });
|
|
196
|
+
renameSync(tmp, path);
|
|
197
|
+
try {
|
|
198
|
+
chmodSync(path, 0o600);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* Windows/filesystems may not support modes. */
|
|
202
|
+
}
|
|
191
203
|
return { path, created: !existed, updated };
|
|
192
204
|
}
|
|
193
205
|
export function configValuesFromClient(config, defaults) {
|
package/dist/config.js
CHANGED
|
@@ -141,6 +141,7 @@ export function resolveConfig(flags) {
|
|
|
141
141
|
function missingTokenMessage(configPath) {
|
|
142
142
|
return [
|
|
143
143
|
"UPLOADS_TOKEN is required.",
|
|
144
|
+
" uploads login # exchange an admin-provided enrollment code",
|
|
144
145
|
` uploads setup --token <token> # guided setup → ${configPath}`,
|
|
145
146
|
` uploads config init --token <token> # writes ${configPath}`,
|
|
146
147
|
" or set UPLOADS_TOKEN in env, pass --token, or use --env-file",
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@buildinternet/uploads",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/buildinternet/uploads.git",
|
|
10
|
-
"directory": "packages/uploads"
|
|
11
|
-
},
|
|
12
|
-
"types": "./dist/index.d.ts",
|
|
13
|
-
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
"types": "./dist/index.d.ts",
|
|
16
|
-
"import": "./dist/index.js"
|
|
17
|
-
},
|
|
18
|
-
"./agent": {
|
|
19
|
-
"types": "./dist/agent.d.ts",
|
|
20
|
-
"import": "./dist/agent.js"
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
"bin": {
|
|
24
|
-
"uploads": "
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"bin",
|
|
28
|
-
"dist",
|
|
29
|
-
"README.md"
|
|
30
|
-
],
|
|
31
|
-
"scripts": {
|
|
32
|
-
"test": "vitest run",
|
|
33
|
-
"typecheck": "tsc --noEmit",
|
|
34
|
-
"build": "tsc",
|
|
35
|
-
"pack:check": "node ./scripts/check-pack.mjs",
|
|
36
|
-
"prepublishOnly": "npm run build"
|
|
37
|
-
},
|
|
38
|
-
"engines": {
|
|
39
|
-
"node": ">=22"
|
|
40
|
-
},
|
|
41
|
-
"peerDependencies": {
|
|
42
|
-
"files-sdk": "^2.1.0"
|
|
43
|
-
},
|
|
44
|
-
"peerDependenciesMeta": {
|
|
45
|
-
"files-sdk": {
|
|
46
|
-
"optional": true
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@types/node": "^26.1.0",
|
|
51
|
-
"ai": "^6.0.0",
|
|
52
|
-
"files-sdk": "^2.1.0",
|
|
53
|
-
"typescript": "^6.0.3",
|
|
54
|
-
"vitest": "^4.1.10"
|
|
55
|
-
},
|
|
56
|
-
"publishConfig": {
|
|
57
|
-
"access": "public",
|
|
58
|
-
"provenance": true
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@buildinternet/uploads",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/buildinternet/uploads.git",
|
|
10
|
+
"directory": "packages/uploads"
|
|
11
|
+
},
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./agent": {
|
|
19
|
+
"types": "./dist/agent.d.ts",
|
|
20
|
+
"import": "./dist/agent.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"uploads": "bin/uploads.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"pack:check": "node ./scripts/check-pack.mjs",
|
|
36
|
+
"prepublishOnly": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"files-sdk": "^2.1.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"files-sdk": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^26.1.0",
|
|
51
|
+
"ai": "^6.0.0",
|
|
52
|
+
"files-sdk": "^2.1.0",
|
|
53
|
+
"typescript": "^6.0.3",
|
|
54
|
+
"vitest": "^4.1.10"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public",
|
|
58
|
+
"provenance": true
|
|
59
|
+
}
|
|
60
|
+
}
|