@homespunapps/cli 1.0.1 → 1.4.2
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/dist/argv.js +69 -4
- package/dist/commands/agent.js +0 -20
- package/dist/commands/apps.js +54 -37
- package/dist/commands/attachment-delete.js +2 -19
- package/dist/commands/attachment-download.js +2 -22
- package/dist/commands/attachment-list.js +2 -22
- package/dist/commands/attachment-show.js +2 -19
- package/dist/commands/attachment-token.js +4 -44
- package/dist/commands/attachment-upload.js +2 -25
- package/dist/commands/attachment.js +9 -52
- package/dist/commands/claim.js +2 -30
- package/dist/commands/config.js +6 -35
- package/dist/commands/data.js +211 -24
- package/dist/commands/deploy.js +23 -28
- package/dist/commands/feedback.js +3 -44
- package/dist/commands/grant.js +158 -0
- package/dist/commands/ingest.js +85 -0
- package/dist/commands/key.js +20 -31
- package/dist/commands/logout.js +2 -28
- package/dist/commands/members.js +64 -32
- package/dist/commands/register.js +2 -49
- package/dist/commands/set-key.js +2 -32
- package/dist/commands/skill.js +3 -39
- package/dist/commands/taste.js +4 -49
- package/dist/help-catalog.js +1087 -0
- package/dist/index.js +28 -103
- package/package.json +11 -6
package/dist/argv.js
CHANGED
|
@@ -18,6 +18,43 @@ export class ArgvError extends Error {
|
|
|
18
18
|
this.hint = hint;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
// Flags that never take a value. `json` is kept here purely for forward-compat
|
|
22
|
+
// (JSON is currently the only output mode): accepting `--json` as a no-op bool
|
|
23
|
+
// means a future `--text`/`--json` toggle won't break existing invocations. It
|
|
24
|
+
// is intentionally undocumented in --help.
|
|
25
|
+
//
|
|
26
|
+
// `version` is deliberately NOT here: the top-level `-v` / `--version` is
|
|
27
|
+
// handled from rawArgv[0] before parseArgs runs, so it never needs to be a
|
|
28
|
+
// boolean flag, and keeping it out leaves room for a future noun-level
|
|
29
|
+
// `--version <n>` value flag without a collision.
|
|
30
|
+
//
|
|
31
|
+
// Exported (and colocated with the parser rather than the bin entry) so tests
|
|
32
|
+
// exercise the REAL parse-time set. A per-test copy of this list is how
|
|
33
|
+
// `deploy --check` shipped un-parsed and ran a real deploy (#827): the flag
|
|
34
|
+
// was in the command's KNOWN_BOOLS and the test's copy, but not here, so
|
|
35
|
+
// parseArgs treated it as a value flag and `bools.has("check")` stayed false.
|
|
36
|
+
export const BOOLEAN_FLAGS = new Set([
|
|
37
|
+
"json",
|
|
38
|
+
"once",
|
|
39
|
+
"help",
|
|
40
|
+
"print-key",
|
|
41
|
+
"yes",
|
|
42
|
+
"plain",
|
|
43
|
+
// `homespun deploy --force` / `homespun apps ... --force`: override a compat gate.
|
|
44
|
+
"force",
|
|
45
|
+
// `homespun deploy --check`: validate-only dry run, persists nothing (#827).
|
|
46
|
+
"check",
|
|
47
|
+
// `homespun agent register --no-device`: skip the browser device-authorization
|
|
48
|
+
// flow and register directly (unowned agent), the pre-device-flow behavior.
|
|
49
|
+
"no-device",
|
|
50
|
+
// `homespun data ... import --emit-effects`: opt a silent bulk import back into
|
|
51
|
+
// firing notify/webhooks (import defaults to silent).
|
|
52
|
+
"emit-effects",
|
|
53
|
+
// `homespun agent logout --all`: wipe every saved profile, not just the active one.
|
|
54
|
+
"all",
|
|
55
|
+
// `homespun members set-role --clear-role`: drop a custom role back to plain member.
|
|
56
|
+
"clear-role",
|
|
57
|
+
]);
|
|
21
58
|
/**
|
|
22
59
|
* Parse argv tokens. `booleanFlags` lists flags that never consume a value
|
|
23
60
|
* (e.g. --json, --once, --help); everything else with a `--name` form
|
|
@@ -97,10 +134,19 @@ const GLOBAL_BOOLS = ["help", "json"];
|
|
|
97
134
|
* `args`. The thrown ArgvError carries a hint pointing at the verb's own
|
|
98
135
|
* --help, so a user fixing a typo lands on the canonical list of flags.
|
|
99
136
|
*
|
|
100
|
-
* Why per-command and not at parse time: the parser
|
|
101
|
-
* generic
|
|
102
|
-
*
|
|
103
|
-
*
|
|
137
|
+
* Why per-command and not at parse time: the parser stays single-pass and
|
|
138
|
+
* generic, and each runner asserts only its own surface.
|
|
139
|
+
*
|
|
140
|
+
* This comment used to argue the allow-list must be co-located with the runner
|
|
141
|
+
* so a new flag would not require a shared registry. That reasoning assumed
|
|
142
|
+
* two consumers (the parser and the runner). There are now four: the runner,
|
|
143
|
+
* `--help`, the published CLI reference on docs.homespun.dev, and this check.
|
|
144
|
+
* Co-location bought nothing against the other three, and in practice the
|
|
145
|
+
* inline lists, the help text, and the docs page had already drifted apart.
|
|
146
|
+
* So the allow-list moved into help-catalog.ts, which every consumer reads via
|
|
147
|
+
* specFor(). A new flag is still a one-line edit, it just lands in the table
|
|
148
|
+
* instead of the runner, and the drift it used to cause is now impossible
|
|
149
|
+
* rather than merely discouraged.
|
|
104
150
|
*
|
|
105
151
|
* Also resolves the parser's `danglingValueFlags`: an unknown name there
|
|
106
152
|
* is reported alongside other unknowns ("unknown flag(s): --bogus"); a
|
|
@@ -128,6 +174,25 @@ export function assertKnownFlags(args, knownFlags, knownBools, helpCommand) {
|
|
|
128
174
|
if (unknown.length > 0) {
|
|
129
175
|
throw new ArgvError(`unknown flag(s): ${unknown.join(", ")}`, `run \`${helpCommand} --help\` for the supported flags`);
|
|
130
176
|
}
|
|
177
|
+
// Defense against parse-time drift (#827): a flag the command declares
|
|
178
|
+
// boolean must never arrive as a value flag. That happens only when the
|
|
179
|
+
// name is missing from the parse-time BOOLEAN_FLAGS set, so parseArgs
|
|
180
|
+
// consumed a value for it (`--check ./dir`, `--check=x`) or recorded it
|
|
181
|
+
// as dangling (trailing `--check`). Before this check, that mismatch
|
|
182
|
+
// passed silently, `bools.has()` stayed false, and `deploy --check` ran
|
|
183
|
+
// a REAL deploy. Fail loudly instead: in tests and dev this surfaces the
|
|
184
|
+
// missing BOOLEAN_FLAGS entry immediately, and no invocation can fall
|
|
185
|
+
// through to the non-boolean code path.
|
|
186
|
+
for (const k of args.flags.keys()) {
|
|
187
|
+
if (boolSet.has(k) && !flagSet.has(k)) {
|
|
188
|
+
throw new ArgvError(`--${k} takes no value`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const k of dangling) {
|
|
192
|
+
if (boolSet.has(k) && !flagSet.has(k)) {
|
|
193
|
+
throw new ArgvError(`--${k} takes no value`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
131
196
|
// No unknowns — but a known value-flag may still have been left without
|
|
132
197
|
// a value. Handle the first such case with the pre-existing message
|
|
133
198
|
// shape ("--name requires a value"). Reporting only the first keeps the
|
package/dist/commands/agent.js
CHANGED
|
@@ -12,26 +12,6 @@ import { runLogout } from "./logout.js";
|
|
|
12
12
|
import { runClaim } from "./claim.js";
|
|
13
13
|
import { runSetKey } from "./set-key.js";
|
|
14
14
|
import { fail } from "../output.js";
|
|
15
|
-
export const agentHelp = `homespun agent — manage this agent's identity on the relay
|
|
16
|
-
|
|
17
|
-
Usage:
|
|
18
|
-
homespun agent <verb> [options]
|
|
19
|
-
|
|
20
|
-
Verbs:
|
|
21
|
-
register Provision an agent API key (POST /v1/register) and save it
|
|
22
|
-
to the CLI config file. Run this once before other commands.
|
|
23
|
-
claim <code> Bind this agent to a human via a one-shot claim code the
|
|
24
|
-
human generated in their Settings UI (POST /v1/agents/claim).
|
|
25
|
-
One-way; no unclaim in v1.
|
|
26
|
-
set-key <key> Save a new API key into the CLI config file. Used after
|
|
27
|
-
regenerating the agent's key in the relay's My-agents UI:
|
|
28
|
-
the human pastes the new key here so subsequent commands
|
|
29
|
-
authenticate as the same agent.
|
|
30
|
-
logout Clear the locally-saved relay URL + API key. Does NOT
|
|
31
|
-
revoke the key on the relay — use 'homespun key revoke' for
|
|
32
|
-
that.
|
|
33
|
-
|
|
34
|
-
Run \`homespun agent <verb> --help\` for verb-specific options.`;
|
|
35
15
|
export async function runAgent(args) {
|
|
36
16
|
// Strip the first positional (the verb) so each verb runner sees its
|
|
37
17
|
// own arguments at positionals[0..n].
|
package/dist/commands/apps.js
CHANGED
|
@@ -14,40 +14,18 @@
|
|
|
14
14
|
// to reclaim verbatim.
|
|
15
15
|
import { HomespunApiError, appWsUrlFromAppUrl, openAppStream, } from "@homespunapps/core";
|
|
16
16
|
import { assertKnownFlags } from "../argv.js";
|
|
17
|
+
import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
|
|
17
18
|
import { makeClient, resolveConfig } from "../config.js";
|
|
18
19
|
import { fail, failFromError, printJson, printJsonLine } from "../output.js";
|
|
19
20
|
import { resolveAppId } from "../resolve-app.js";
|
|
20
|
-
export const appsHelp = `homespun apps — app lifecycle management
|
|
21
|
-
|
|
22
|
-
Usage:
|
|
23
|
-
homespun apps list [--status active|dormant|archived|all] [--limit] [--cursor] [--slug <slug>]
|
|
24
|
-
homespun apps show <app>
|
|
25
|
-
homespun apps update <app> --visibility <private|link|public>
|
|
26
|
-
homespun apps delete <app> [--yes]
|
|
27
|
-
homespun apps wake <app>
|
|
28
|
-
homespun apps watch <app> [--since <cursor>] [--collection <name[,name2,...]>]
|
|
29
|
-
[--once] [--timeout <secs>]
|
|
30
|
-
|
|
31
|
-
<app> accepts either the app_id or its slug (resolved via GET /v1/apps?slug=
|
|
32
|
-
when it doesn't look like a cuid).
|
|
33
|
-
|
|
34
|
-
watch streams the app's change feed as JSON-lines on stdout — one compact
|
|
35
|
-
SerializedFeedEntry object per line, identical whether served over the live
|
|
36
|
-
WebSocket (primary) or the long-poll fallback (used automatically when the
|
|
37
|
-
WS upgrade fails, e.g. self-host mode has no WS support yet, or a locked-down
|
|
38
|
-
network blocks outbound WS). A dormancy transition mid-watch emits a single
|
|
39
|
-
{"type":"_dormant"} line and exits 0.
|
|
40
|
-
|
|
41
|
-
Output (JSON; JSON-lines for watch). Errors on stderr:
|
|
42
|
-
{"error":{"code","message"}} with non-zero exit.`;
|
|
43
21
|
export async function runApps(args) {
|
|
44
22
|
const verb = args.positionals[0];
|
|
45
23
|
if ((verb === undefined || verb === "help") && args.bools.has("help")) {
|
|
46
|
-
process.stdout.write(
|
|
24
|
+
process.stdout.write(renderNounHelp(nounSpec("apps")) + "\n");
|
|
47
25
|
return;
|
|
48
26
|
}
|
|
49
27
|
if (verb === undefined) {
|
|
50
|
-
fail("missing verb
|
|
28
|
+
fail("missing verb: homespun apps <list|show|update|share-link|delete|wake|watch>", "invalid_args");
|
|
51
29
|
}
|
|
52
30
|
const sub = {
|
|
53
31
|
positionals: args.positionals.slice(1),
|
|
@@ -64,6 +42,8 @@ export async function runApps(args) {
|
|
|
64
42
|
return runShow(sub);
|
|
65
43
|
case "update":
|
|
66
44
|
return runUpdate(sub);
|
|
45
|
+
case "share-link":
|
|
46
|
+
return runShareLink(sub);
|
|
67
47
|
case "delete":
|
|
68
48
|
return runDelete(sub);
|
|
69
49
|
case "wake":
|
|
@@ -71,14 +51,14 @@ export async function runApps(args) {
|
|
|
71
51
|
case "watch":
|
|
72
52
|
return runWatch(sub);
|
|
73
53
|
default:
|
|
74
|
-
fail(`unknown verb '${verb}'
|
|
54
|
+
fail(`unknown verb '${verb}': homespun apps <list|show|update|share-link|delete|wake|watch>`, "invalid_args");
|
|
75
55
|
}
|
|
76
56
|
}
|
|
77
57
|
// ---------------------------------------------------------------------------
|
|
78
58
|
// list
|
|
79
59
|
// ---------------------------------------------------------------------------
|
|
80
60
|
async function runList(args) {
|
|
81
|
-
assertKnownFlags(args,
|
|
61
|
+
assertKnownFlags(args, ...specFor("apps", "list"));
|
|
82
62
|
const status = args.flags.get("status");
|
|
83
63
|
if (status !== undefined &&
|
|
84
64
|
!["active", "dormant", "archived", "all"].includes(status)) {
|
|
@@ -107,7 +87,7 @@ async function runList(args) {
|
|
|
107
87
|
// show
|
|
108
88
|
// ---------------------------------------------------------------------------
|
|
109
89
|
async function runShow(args) {
|
|
110
|
-
assertKnownFlags(args,
|
|
90
|
+
assertKnownFlags(args, ...specFor("apps", "show"));
|
|
111
91
|
const appArg = args.positionals[0];
|
|
112
92
|
if (!appArg)
|
|
113
93
|
fail("usage: homespun apps show <app>", "invalid_args");
|
|
@@ -124,22 +104,59 @@ async function runShow(args) {
|
|
|
124
104
|
// update
|
|
125
105
|
// ---------------------------------------------------------------------------
|
|
126
106
|
async function runUpdate(args) {
|
|
127
|
-
assertKnownFlags(args,
|
|
107
|
+
assertKnownFlags(args, ...specFor("apps", "update"));
|
|
128
108
|
const appArg = args.positionals[0];
|
|
129
109
|
if (!appArg) {
|
|
130
|
-
fail("usage: homespun apps update <app> --visibility <private|link|public>", "invalid_args");
|
|
110
|
+
fail("usage: homespun apps update <app> [--visibility <private|link|public>] [--timezone <IANA zone>]", "invalid_args");
|
|
131
111
|
}
|
|
132
112
|
const visibility = args.flags.get("visibility");
|
|
133
|
-
|
|
134
|
-
|
|
113
|
+
const timezone = args.flags.get("timezone");
|
|
114
|
+
if (visibility === undefined && timezone === undefined) {
|
|
115
|
+
fail("nothing to update; pass --visibility and/or --timezone", "invalid_args");
|
|
135
116
|
}
|
|
136
|
-
if (
|
|
117
|
+
if (visibility !== undefined &&
|
|
118
|
+
!["private", "link", "public"].includes(visibility)) {
|
|
137
119
|
fail("--visibility must be private|link|public", "invalid_args");
|
|
138
120
|
}
|
|
121
|
+
if (timezone !== undefined && timezone.trim() === "") {
|
|
122
|
+
fail("--timezone must be an IANA zone name, e.g. Europe/Berlin", "invalid_args");
|
|
123
|
+
}
|
|
124
|
+
const client = makeClient(args);
|
|
125
|
+
const id = await resolveAppId(client, appArg);
|
|
126
|
+
try {
|
|
127
|
+
printJson(await client.updateApp(id, {
|
|
128
|
+
...(visibility !== undefined
|
|
129
|
+
? { visibility: visibility }
|
|
130
|
+
: {}),
|
|
131
|
+
...(timezone !== undefined ? { timezone } : {}),
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
failFromError(e);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// share-link (rotate)
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// `homespun apps share-link rotate <app>` rotates a link app's share token.
|
|
142
|
+
// Prints the new { share_url } (its #k= fragment carries the token, shown once).
|
|
143
|
+
// Rotating instantly revokes the previous share URL. Also generates a link for a
|
|
144
|
+
// link app that has none yet. Only `rotate` is supported (rotate-only, no
|
|
145
|
+
// delete): revoking a link IS rotating it.
|
|
146
|
+
async function runShareLink(args) {
|
|
147
|
+
const action = args.positionals[0];
|
|
148
|
+
if (action !== "rotate") {
|
|
149
|
+
fail("usage: homespun apps share-link rotate <app>", "invalid_args");
|
|
150
|
+
}
|
|
151
|
+
assertKnownFlags(args, ...specFor("apps", "share-link"));
|
|
152
|
+
const appArg = args.positionals[1];
|
|
153
|
+
if (!appArg) {
|
|
154
|
+
fail("usage: homespun apps share-link rotate <app>", "invalid_args");
|
|
155
|
+
}
|
|
139
156
|
const client = makeClient(args);
|
|
140
157
|
const id = await resolveAppId(client, appArg);
|
|
141
158
|
try {
|
|
142
|
-
printJson(await client.
|
|
159
|
+
printJson(await client.rotateShareLink(id));
|
|
143
160
|
}
|
|
144
161
|
catch (e) {
|
|
145
162
|
failFromError(e);
|
|
@@ -149,7 +166,7 @@ async function runUpdate(args) {
|
|
|
149
166
|
// delete
|
|
150
167
|
// ---------------------------------------------------------------------------
|
|
151
168
|
async function runDelete(args) {
|
|
152
|
-
assertKnownFlags(args,
|
|
169
|
+
assertKnownFlags(args, ...specFor("apps", "delete"));
|
|
153
170
|
const appArg = args.positionals[0];
|
|
154
171
|
if (!appArg)
|
|
155
172
|
fail("usage: homespun apps delete <app> [--yes]", "invalid_args");
|
|
@@ -170,7 +187,7 @@ async function runDelete(args) {
|
|
|
170
187
|
// wake
|
|
171
188
|
// ---------------------------------------------------------------------------
|
|
172
189
|
async function runWake(args) {
|
|
173
|
-
assertKnownFlags(args,
|
|
190
|
+
assertKnownFlags(args, ...specFor("apps", "wake"));
|
|
174
191
|
const appArg = args.positionals[0];
|
|
175
192
|
if (!appArg)
|
|
176
193
|
fail("usage: homespun apps wake <app>", "invalid_args");
|
|
@@ -221,7 +238,7 @@ export function isDormantConflict(err) {
|
|
|
221
238
|
err.message === "app is dormant");
|
|
222
239
|
}
|
|
223
240
|
async function runWatch(args) {
|
|
224
|
-
assertKnownFlags(args,
|
|
241
|
+
assertKnownFlags(args, ...specFor("apps", "watch"));
|
|
225
242
|
const appArg = args.positionals[0];
|
|
226
243
|
if (!appArg)
|
|
227
244
|
fail("usage: homespun apps watch <app>", "invalid_args");
|
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
// `homespun attachment delete <attachment-id>` — soft-delete a attachment.
|
|
2
2
|
import { assertKnownFlags } from "../argv.js";
|
|
3
|
+
import { specFor } from "../help-catalog.js";
|
|
3
4
|
import { makeClient } from "../config.js";
|
|
4
5
|
import { fail, failFromError, printJson } from "../output.js";
|
|
5
|
-
const KNOWN_FLAGS = [];
|
|
6
|
-
const KNOWN_BOOLS = [];
|
|
7
|
-
export const blobDeleteHelp = `homespun attachment delete — soft-delete a attachment
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
homespun attachment delete <attachment-id> [options]
|
|
11
|
-
|
|
12
|
-
Marks the attachment as deleted (DELETE /v1/attachments/:id). Idempotent: deleting an
|
|
13
|
-
already-deleted attachment still returns success. Tokens minted against this attachment
|
|
14
|
-
become unusable.
|
|
15
|
-
|
|
16
|
-
Options:
|
|
17
|
-
--url <url> Relay base URL (overrides HOMESPUN_URL).
|
|
18
|
-
--api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
|
|
19
|
-
-h, --help Show this help.
|
|
20
|
-
|
|
21
|
-
Output (stdout, JSON):
|
|
22
|
-
{ attachment_id, deleted: true }`;
|
|
23
6
|
export async function runBlobDelete(args) {
|
|
24
|
-
assertKnownFlags(args,
|
|
7
|
+
assertKnownFlags(args, ...specFor("attachment", "delete"));
|
|
25
8
|
const attachmentId = args.positionals[0];
|
|
26
9
|
if (!attachmentId) {
|
|
27
10
|
fail("missing <attachment-id> — 'homespun attachment delete <attachment-id>'", "invalid_args");
|
|
@@ -1,31 +1,11 @@
|
|
|
1
1
|
// `homespun attachment download <attachment-id>` — fetch attachment bytes by id.
|
|
2
2
|
import { writeFileSync } from "node:fs";
|
|
3
3
|
import { assertKnownFlags } from "../argv.js";
|
|
4
|
+
import { specFor } from "../help-catalog.js";
|
|
4
5
|
import { makeClient } from "../config.js";
|
|
5
6
|
import { fail, failFromError, printJson } from "../output.js";
|
|
6
|
-
const KNOWN_FLAGS = ["out"];
|
|
7
|
-
const KNOWN_BOOLS = [];
|
|
8
|
-
export const blobDownloadHelp = `homespun attachment download — fetch a attachment's bytes
|
|
9
|
-
|
|
10
|
-
Usage:
|
|
11
|
-
homespun attachment download <attachment-id> [--out <path>] [options]
|
|
12
|
-
|
|
13
|
-
GETs the attachment bytes. With --out <path> the bytes are written to that file and
|
|
14
|
-
a JSON summary is printed on stdout; without --out the bytes are written to
|
|
15
|
-
stdout verbatim (useful for piping into another tool — but binary on a TTY
|
|
16
|
-
is rarely useful).
|
|
17
|
-
|
|
18
|
-
Options:
|
|
19
|
-
--out <path> Write bytes to <path> instead of stdout.
|
|
20
|
-
--url <url> Relay base URL (overrides HOMESPUN_URL).
|
|
21
|
-
--api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
|
|
22
|
-
-h, --help Show this help.
|
|
23
|
-
|
|
24
|
-
Output:
|
|
25
|
-
Without --out: raw bytes to stdout.
|
|
26
|
-
With --out: { attachment_id, written: <path>, bytes: <n> } to stdout.`;
|
|
27
7
|
export async function runBlobDownload(args) {
|
|
28
|
-
assertKnownFlags(args,
|
|
8
|
+
assertKnownFlags(args, ...specFor("attachment", "download"));
|
|
29
9
|
const attachmentId = args.positionals[0];
|
|
30
10
|
if (!attachmentId) {
|
|
31
11
|
fail("missing <attachment-id> — 'homespun attachment download <attachment-id>'", "invalid_args");
|
|
@@ -4,31 +4,11 @@
|
|
|
4
4
|
// are excluded; tokens are not enumerated here (use 'homespun attachment token list
|
|
5
5
|
// <attachment-id>' for that).
|
|
6
6
|
import { assertKnownFlags } from "../argv.js";
|
|
7
|
+
import { specFor } from "../help-catalog.js";
|
|
7
8
|
import { makeClient } from "../config.js";
|
|
8
9
|
import { fail, printJson, failFromError } from "../output.js";
|
|
9
|
-
const KNOWN_FLAGS = ["cursor", "limit"];
|
|
10
|
-
const KNOWN_BOOLS = [];
|
|
11
|
-
export const blobListHelp = `homespun attachment list — enumerate YOUR agent's attachments
|
|
12
|
-
|
|
13
|
-
Usage:
|
|
14
|
-
homespun attachment list [--cursor <token>] [--limit <n>] [options]
|
|
15
|
-
|
|
16
|
-
Returns the agent's non-deleted attachments (newest first). Paginated via opaque
|
|
17
|
-
cursor: when next_cursor is non-null in the response, pass it back as
|
|
18
|
-
--cursor to get the next page.
|
|
19
|
-
|
|
20
|
-
Options:
|
|
21
|
-
--cursor <token> Opaque pagination cursor from a prior response.
|
|
22
|
-
--limit <n> Page size (1..100). Defaults to the relay default
|
|
23
|
-
(50).
|
|
24
|
-
--url <url> Relay base URL (overrides HOMESPUN_URL).
|
|
25
|
-
--api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
|
|
26
|
-
-h, --help Show this help.
|
|
27
|
-
|
|
28
|
-
Output (stdout, JSON):
|
|
29
|
-
{ items: AttachmentRef[], next_cursor: string | null }`;
|
|
30
10
|
export async function runBlobList(args) {
|
|
31
|
-
assertKnownFlags(args,
|
|
11
|
+
assertKnownFlags(args, ...specFor("attachment", "list"));
|
|
32
12
|
const cursor = args.flags.get("cursor");
|
|
33
13
|
const limitRaw = args.flags.get("limit");
|
|
34
14
|
let limit;
|
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
// `homespun attachment show <attachment-id>` — print a attachment's metadata.
|
|
2
2
|
import { assertKnownFlags } from "../argv.js";
|
|
3
|
+
import { specFor } from "../help-catalog.js";
|
|
3
4
|
import { makeClient } from "../config.js";
|
|
4
5
|
import { fail, failFromError, printJson } from "../output.js";
|
|
5
|
-
const KNOWN_FLAGS = [];
|
|
6
|
-
const KNOWN_BOOLS = [];
|
|
7
|
-
export const blobShowHelp = `homespun attachment show — print a attachment's metadata (no bytes)
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
homespun attachment show <attachment-id> [options]
|
|
11
|
-
|
|
12
|
-
Looks up the attachment by id and prints its AttachmentRef metadata — owner, scope,
|
|
13
|
-
mime, size, sha256, etc. Does NOT download the bytes; use 'homespun attachment
|
|
14
|
-
download' for that.
|
|
15
|
-
|
|
16
|
-
Options:
|
|
17
|
-
--url <url> Relay base URL (overrides HOMESPUN_URL).
|
|
18
|
-
--api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
|
|
19
|
-
-h, --help Show this help.
|
|
20
|
-
|
|
21
|
-
Output (stdout, JSON):
|
|
22
|
-
AttachmentRef`;
|
|
23
6
|
export async function runBlobShow(args) {
|
|
24
|
-
assertKnownFlags(args,
|
|
7
|
+
assertKnownFlags(args, ...specFor("attachment", "show"));
|
|
25
8
|
const attachmentId = args.positionals[0];
|
|
26
9
|
if (!attachmentId) {
|
|
27
10
|
fail("missing <attachment-id> — 'homespun attachment show <attachment-id>'", "invalid_args");
|
|
@@ -12,51 +12,11 @@
|
|
|
12
12
|
// positionals[2..]. Mirrors how participant.ts dispatches under `app
|
|
13
13
|
// app participant`.
|
|
14
14
|
import { assertKnownFlags } from "../argv.js";
|
|
15
|
+
import { specFor } from "../help-catalog.js";
|
|
15
16
|
import { makeClient } from "../config.js";
|
|
16
17
|
import { fail, failFromError, printJson } from "../output.js";
|
|
17
|
-
const MINT_FLAGS = ["ttl"];
|
|
18
|
-
const MINT_BOOLS = ["once"];
|
|
19
|
-
const NO_FLAGS = [];
|
|
20
|
-
const NO_BOOLS = [];
|
|
21
|
-
export const blobTokenHelp = `homespun attachment token — manage a attachment's capability URLs
|
|
22
|
-
|
|
23
|
-
Capability URLs let a participant (or any browser holding the URL) fetch a
|
|
24
|
-
attachment without the agent's API key. Tokens are stored HASHED on the relay; the
|
|
25
|
-
plaintext token is returned only ONCE from 'mint' — save the response before
|
|
26
|
-
delivering the URL.
|
|
27
|
-
|
|
28
|
-
Usage:
|
|
29
|
-
homespun attachment token <verb> <args>
|
|
30
|
-
|
|
31
|
-
Verbs:
|
|
32
|
-
mint <attachment-id> Mint a /b/<token> capability URL for one attachment.
|
|
33
|
-
Optional: --ttl <seconds> (defaults by scope:
|
|
34
|
-
30d app / 24h agent; the caller can only
|
|
35
|
-
shorten), --once (token self-deletes on
|
|
36
|
-
first successful GET). Returns { token, url,
|
|
37
|
-
expires_at, ... } — ONCE.
|
|
38
|
-
|
|
39
|
-
revoke <attachment-id> <token-id>
|
|
40
|
-
Invalidate one previously-minted token by id.
|
|
41
|
-
Idempotent: revoking twice still returns success.
|
|
42
|
-
|
|
43
|
-
list <attachment-id> Enumerate the tokens minted against one attachment,
|
|
44
|
-
including revoked rows (for audit). Returns
|
|
45
|
-
{ attachment_id, items: [...] } where each item carries
|
|
46
|
-
{ token_id, token_prefix, expires_at, once,
|
|
47
|
-
created_at, last_used_at, use_count, revoked_at }.
|
|
48
|
-
The token plaintext is NEVER returned.
|
|
49
|
-
|
|
50
|
-
Options:
|
|
51
|
-
--ttl <seconds> (mint) per-token TTL; clamped by scope default.
|
|
52
|
-
--once (mint) token self-deletes on first GET.
|
|
53
|
-
--url <url> Relay base URL (overrides HOMESPUN_URL).
|
|
54
|
-
--api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
|
|
55
|
-
-h, --help Show this help.
|
|
56
|
-
|
|
57
|
-
Output: stdout is machine-readable JSON.`;
|
|
58
18
|
async function runBlobTokenMint(args) {
|
|
59
|
-
assertKnownFlags(args,
|
|
19
|
+
assertKnownFlags(args, ...specFor("attachment", "token mint"));
|
|
60
20
|
const attachmentId = args.positionals[1];
|
|
61
21
|
if (!attachmentId) {
|
|
62
22
|
fail("missing <attachment-id> — 'homespun attachment token mint <attachment-id>'", "invalid_args");
|
|
@@ -79,7 +39,7 @@ async function runBlobTokenMint(args) {
|
|
|
79
39
|
}
|
|
80
40
|
}
|
|
81
41
|
async function runBlobTokenRevoke(args) {
|
|
82
|
-
assertKnownFlags(args,
|
|
42
|
+
assertKnownFlags(args, ...specFor("attachment", "token revoke"));
|
|
83
43
|
const attachmentId = args.positionals[1];
|
|
84
44
|
const tokenId = args.positionals[2];
|
|
85
45
|
if (!attachmentId || !tokenId) {
|
|
@@ -95,7 +55,7 @@ async function runBlobTokenRevoke(args) {
|
|
|
95
55
|
}
|
|
96
56
|
}
|
|
97
57
|
async function runBlobTokenList(args) {
|
|
98
|
-
assertKnownFlags(args,
|
|
58
|
+
assertKnownFlags(args, ...specFor("attachment", "token list"));
|
|
99
59
|
const attachmentId = args.positionals[1];
|
|
100
60
|
if (!attachmentId) {
|
|
101
61
|
fail("missing <attachment-id> — 'homespun attachment token list <attachment-id>'", "invalid_args");
|
|
@@ -2,34 +2,11 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { assertKnownFlags } from "../argv.js";
|
|
5
|
+
import { specFor } from "../help-catalog.js";
|
|
5
6
|
import { makeClient } from "../config.js";
|
|
6
7
|
import { fail, failFromError, printJson } from "../output.js";
|
|
7
|
-
const KNOWN_FLAGS = ["file", "scope", "app-id", "filename", "mime"];
|
|
8
|
-
const KNOWN_BOOLS = [];
|
|
9
|
-
export const blobUploadHelp = `homespun attachment upload — upload a local file as a attachment
|
|
10
|
-
|
|
11
|
-
Usage:
|
|
12
|
-
homespun attachment upload --file <path> [options]
|
|
13
|
-
|
|
14
|
-
Required:
|
|
15
|
-
--file <path> Local file to upload.
|
|
16
|
-
|
|
17
|
-
Scope (default: agent):
|
|
18
|
-
--scope <s> "agent" | "app".
|
|
19
|
-
--app-id <id> Required when --scope=app.
|
|
20
|
-
|
|
21
|
-
Optional:
|
|
22
|
-
--filename <name> Display filename (otherwise basename of --file).
|
|
23
|
-
--mime <type> Declared Content-Type. The relay sniffs the bytes
|
|
24
|
-
regardless — this is advisory.
|
|
25
|
-
--url <url> Relay base URL (overrides HOMESPUN_URL).
|
|
26
|
-
--api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
|
|
27
|
-
-h, --help Show this help.
|
|
28
|
-
|
|
29
|
-
Output (stdout, JSON):
|
|
30
|
-
AttachmentRef — { attachment_id, scope, mime, size, sha256, ... }`;
|
|
31
8
|
export async function runBlobUpload(args) {
|
|
32
|
-
assertKnownFlags(args,
|
|
9
|
+
assertKnownFlags(args, ...specFor("attachment", "upload"));
|
|
33
10
|
const filePath = args.flags.get("file");
|
|
34
11
|
if (!filePath) {
|
|
35
12
|
fail("missing --file <path> — 'homespun attachment upload' requires a local file to upload", "invalid_args");
|
|
@@ -12,54 +12,14 @@
|
|
|
12
12
|
// Most attachment verbs read their primary positional (the attachment_id) at
|
|
13
13
|
// positionals[0]; we slice off our own verb before delegating so each verb
|
|
14
14
|
// runner doesn't need to know it was reached through `homespun attachment`.
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
15
|
+
import { nounSpec, renderNounHelp } from "../help-catalog.js";
|
|
16
|
+
import { runBlobUpload } from "./attachment-upload.js";
|
|
17
|
+
import { runBlobDownload } from "./attachment-download.js";
|
|
18
|
+
import { runBlobShow } from "./attachment-show.js";
|
|
19
|
+
import { runBlobList } from "./attachment-list.js";
|
|
20
|
+
import { runBlobDelete } from "./attachment-delete.js";
|
|
21
|
+
import { runBlobToken } from "./attachment-token.js";
|
|
21
22
|
import { fail } from "../output.js";
|
|
22
|
-
export const blobHelp = `homespun attachment — manage attachments (binary attachments) on the relay
|
|
23
|
-
|
|
24
|
-
A attachment is a typed binary file (image, PDF, audio, video, ...) the agent has
|
|
25
|
-
uploaded to the relay. Blobs are scoped:
|
|
26
|
-
|
|
27
|
-
agent reusable across the agent's apps (default)
|
|
28
|
-
app bound to one App; deleted with it
|
|
29
|
-
|
|
30
|
-
Pages reference attachments by id (the relay's schema validates the id with
|
|
31
|
-
\`format: homespun-attachment-id\`). For a participant-facing URL that bypasses the
|
|
32
|
-
agent's API key, mint a token with 'homespun attachment token mint'.
|
|
33
|
-
|
|
34
|
-
Usage:
|
|
35
|
-
homespun attachment <verb> [options]
|
|
36
|
-
|
|
37
|
-
Verbs:
|
|
38
|
-
upload Upload a local file. Required: --file. Optional:
|
|
39
|
-
--scope, --app-id, --filename, --mime. Prints
|
|
40
|
-
{ attachment_id, scope, mime, size, sha256, ... }.
|
|
41
|
-
|
|
42
|
-
download <attachment-id> Download a attachment by id. Use --out <path> to write a
|
|
43
|
-
file (default: writes to stdout — useful for piping).
|
|
44
|
-
|
|
45
|
-
show <attachment-id> Print a attachment's metadata (HEAD-based — doesn't
|
|
46
|
-
download the bytes).
|
|
47
|
-
|
|
48
|
-
list Enumerate YOUR agent's non-deleted attachments (newest
|
|
49
|
-
first). Supports --cursor + --limit for pagination.
|
|
50
|
-
|
|
51
|
-
delete <attachment-id> Soft-delete a attachment. Idempotent.
|
|
52
|
-
|
|
53
|
-
token <verb> Capability URLs for a attachment (mint | revoke | list).
|
|
54
|
-
'mint' returns a /b/<token> URL anyone can GET, with
|
|
55
|
-
optional --ttl and --once. 'revoke' invalidates one
|
|
56
|
-
token. 'list' enumerates a attachment's tokens (without
|
|
57
|
-
the token plaintext, which is unrecoverable).
|
|
58
|
-
|
|
59
|
-
Run \`homespun attachment <verb> --help\` for verb-specific options.
|
|
60
|
-
|
|
61
|
-
Output: stdout is machine-readable JSON. Errors go to stderr as
|
|
62
|
-
{"error":{"code","message"}} with a non-zero exit.`;
|
|
63
23
|
/**
|
|
64
24
|
* Build a new ParsedArgs with the leading positional (the verb) stripped.
|
|
65
25
|
* The downstream verb runners read their primary positional (the attachment_id)
|
|
@@ -89,7 +49,7 @@ export async function runBlob(args) {
|
|
|
89
49
|
if (verb === "token" &&
|
|
90
50
|
args.bools.has("help") &&
|
|
91
51
|
args.positionals.length === 1) {
|
|
92
|
-
process.stdout.write(
|
|
52
|
+
process.stdout.write(renderNounHelp(nounSpec("attachment")) + "\n");
|
|
93
53
|
return;
|
|
94
54
|
}
|
|
95
55
|
// `homespun attachment list --help` — same pattern (list takes no required positional
|
|
@@ -98,7 +58,7 @@ export async function runBlob(args) {
|
|
|
98
58
|
if (verb === "list" &&
|
|
99
59
|
args.bools.has("help") &&
|
|
100
60
|
args.positionals.length === 1) {
|
|
101
|
-
process.stdout.write(
|
|
61
|
+
process.stdout.write(renderNounHelp(nounSpec("attachment")) + "\n");
|
|
102
62
|
return;
|
|
103
63
|
}
|
|
104
64
|
const inner = shiftPositionals(args);
|
|
@@ -128,6 +88,3 @@ export async function runBlob(args) {
|
|
|
128
88
|
fail(`unknown attachment verb '${verb}' — expected upload|download|show|list|delete|token (run 'homespun attachment --help')`, "invalid_args");
|
|
129
89
|
}
|
|
130
90
|
}
|
|
131
|
-
// Re-export per-verb helps so tests / docs can import them by canonical name
|
|
132
|
-
// without knowing which file owns each verb.
|
|
133
|
-
export { blobUploadHelp, blobDownloadHelp, blobShowHelp, blobListHelp, blobDeleteHelp, blobTokenHelp, };
|