@homespunapps/cli 1.6.37 → 1.6.39

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 CHANGED
@@ -16,18 +16,22 @@ npx @homespunapps/cli <command>
16
16
 
17
17
  The binary is `homespun`.
18
18
 
19
- ## Try it
19
+ ## Quickstart
20
20
 
21
- Register once, then `homespun demo` spins up a short-lived sample app on the hosted
22
- relay, opens it in your browser, and prints the structured event back in your
23
- terminal the moment you interact (the demo app is cleaned up on exit):
21
+ Register once, then deploy:
24
22
 
25
23
  ```sh
26
24
  npx @homespunapps/cli agent register --name "my-agent" # one-time, hosted relay
27
- npx @homespunapps/cli demo # Node 20+ round-trip in ~60s
25
+ npx @homespunapps/cli deploy ./my-app # Node 20+, reads ./my-app/index.html + manifest.json
28
26
  ```
29
27
 
30
- Add `--no-open` on a headless / SSH box and it just prints the URL.
28
+ `agent register` uses browser approval by default: it prints a link and a short
29
+ code, you approve on any device, and the agent comes out already bound to your
30
+ account and ready to deploy. See [Setup](#setup) for the direct-registration
31
+ path and the one-time `agent claim` it needs.
32
+
33
+ New apps are private by default: only you and the people you invite can open
34
+ them. Pass `--visibility link` or `--visibility public` to share wider.
31
35
 
32
36
  ## Setup
33
37
 
@@ -53,41 +57,69 @@ Override per-invocation with `--url <url>` and `--api-key <key>`.
53
57
 
54
58
  ## Commands
55
59
 
56
- Uniform `homespun <noun> <verb> [options]`:
60
+ Uniform `homespun <noun> <verb> [options]` (`data` takes its collection
61
+ before the verb: `homespun data <app> <collection> <verb>`).
62
+
63
+ App commands, each operating on a deployed app:
64
+
65
+ ```
66
+ homespun deploy [dir|file] Create or redeploy an app (POST /v1/apps, or
67
+ /v1/apps/:id/versions with --app)
68
+ homespun apps <verb> App lifecycle: list, show, update, share-link,
69
+ delete, wake, domain, watch (the change feed as
70
+ JSON-lines)
71
+ homespun data <app> <collection> <verb>
72
+ Collection row CRUD: list, get, upsert, update,
73
+ delete, purge, import, retention
74
+ homespun members <verb> App membership: add, list, set-role, remove, roles
75
+ homespun grants <verb> Grant-link management: mint, list, revoke
76
+ homespun ingest <verb> Inbound catch-hook management: list, rotate,
77
+ signing-secret, backfill
78
+ ```
79
+
80
+ Other command groups:
57
81
 
58
82
  ```
59
- homespun demo Zero-setup guided tour see the round-trip live
60
- homespun agent register Provision an agent API key (browser approval
61
- by default; --no-device for direct) and save it
62
- homespun agent claim <code> Bind this agent to a human via a one-shot code
63
- homespun agent logout Clear the locally-saved URL + API key
64
- homespun create Create an app returns app_id, urls, tokens
65
- homespun show <id> Non-blocking snapshot: metadata + event log
66
- homespun send <id> Emit an agent event into an app
67
- homespun watch <id> Stream an app's events as JSON-lines on stdout
68
- homespun delete <id> Close / delete an app
69
- homespun template <verb> Manage reusable, versioned templates
70
- homespun key list | revoke Inspect or revoke your agent's API key
71
- homespun taste get | set | clear Read / write / clear UI-taste notes
72
- homespun feedback create | list Submit / list one-shot feedback to the operator
73
- homespun config show Show the resolved relay config (no network call)
74
- homespun skill show | version Fetch the relay's SKILL.md (or its version)
83
+ homespun publisher <verb> Your community publisher identity: claim, show,
84
+ update, set-trust
85
+ homespun template <verb> Community marketplace templates: publish,
86
+ unpublish, config-contract, install, list-pending,
87
+ show, approve, reject
88
+ homespun review <verb> Community template reviews: create, respond,
89
+ report, remove, unhold
90
+ homespun key <verb> Your agent's own API key: list, mint, revoke
91
+ homespun taste <verb> Your agent's UI taste notes: get, set, clear
92
+ homespun feedback <verb> One-shot feedback to the relay operator: create, list
93
+ homespun attachment <verb> Binary attachments: upload, download, show, list,
94
+ delete, token mint | revoke | list
95
+ homespun agent <verb> This agent's identity on the relay: register,
96
+ claim, set-key, logout
97
+ homespun config <verb> CLI config and profile management: show, list,
98
+ use, add, rm
99
+ homespun skill <verb> The relay's SKILL.md: show, version
75
100
  ```
76
101
 
77
102
  Run `homespun <noun> --help` for that noun's verbs, and
78
- `homespun <noun> <verb> --help` for verb-specific options.
103
+ `homespun <noun> <verb> --help` for verb-specific options. The same table
104
+ also drives the generated [CLI reference](https://docs.homespun.dev/agents/cli-reference/).
79
105
 
80
106
  ## Output
81
107
 
82
- stdout is machine-readable JSON. Errors go to stderr as
83
- `{"error":{"code","message"}}` with a non-zero exit.
108
+ stdout is JSON. Errors go to stderr as `{"error":{"code","message"}}` with a
109
+ non-zero exit. A deploy, then a read of the data it wrote back:
84
110
 
85
111
  ```sh
86
- SESSION=$(homespun create --template ./form.html --name "Quick poll" --event-schema ./q.json | jq -r .app_id)
87
- homespun watch "$SESSION" | jq 'select(.type == "human_response")'
112
+ homespun deploy ./grocery-list
113
+ # -> { app_id, slug, url, version, visibility: "private", created: true }
114
+
115
+ homespun data grocery-list items upsert --data '{"name":"Milk","checked":false}'
116
+ # -> { row: { key, data: { name: "Milk", checked: false }, version: 1, author, created_at, updated_at } }
117
+
118
+ homespun data grocery-list items list
119
+ # -> { rows: [...], next_cursor, has_more }
88
120
  ```
89
121
 
90
122
  ## Links
91
123
 
92
124
  - Docs: <https://docs.homespun.dev>
93
- - License: MIT
125
+ - License: [MIT](LICENSE)
@@ -18,7 +18,7 @@ export async function runData(args) {
18
18
  return;
19
19
  }
20
20
  if (!appArg || !collection || !verb) {
21
- fail("usage: homespun data <app> <collection> <list|get|upsert|update|delete|purge|import|retention>", "invalid_args");
21
+ fail("usage: homespun data <app> <collection> <list|get|upsert|update|delete|restore|deleted|purge|import|retention>", "invalid_args");
22
22
  }
23
23
  const sub = {
24
24
  positionals: args.positionals.slice(3),
@@ -39,6 +39,10 @@ export async function runData(args) {
39
39
  return runUpdate(appArg, collection, sub);
40
40
  case "delete":
41
41
  return runDelete(appArg, collection, sub);
42
+ case "restore":
43
+ return runRestore(appArg, collection, sub);
44
+ case "deleted":
45
+ return runListDeleted(appArg, collection, sub);
42
46
  case "purge":
43
47
  return runPurge(appArg, collection, sub);
44
48
  case "import":
@@ -46,7 +50,7 @@ export async function runData(args) {
46
50
  case "retention":
47
51
  return runRetention(appArg, collection, sub);
48
52
  default:
49
- fail(`unknown verb '${verb}': homespun data <app> <collection> <list|get|upsert|update|delete|purge|import|retention>`, "invalid_args");
53
+ fail(`unknown verb '${verb}': homespun data <app> <collection> <list|get|upsert|update|delete|restore|deleted|purge|import|retention>`, "invalid_args");
50
54
  }
51
55
  }
52
56
  function parseIntFlag(args, name, defaultValue, bounds = {}) {
@@ -191,6 +195,50 @@ async function runDelete(appArg, collection, args) {
191
195
  failFromError(e);
192
196
  }
193
197
  }
198
+ // `homespun data <app> <coll> restore --key <key>`: undo a delete. Owner/agent
199
+ // only, and --key rather than a positional for the same reason purge uses one:
200
+ // the verb reads deliberately at the call site.
201
+ async function runRestore(appArg, collection, args) {
202
+ assertKnownFlags(args, ...specFor("data", "restore"));
203
+ const key = args.flags.get("key");
204
+ if (!key) {
205
+ fail("usage: homespun data <app> <collection> restore --key <key>", "invalid_args");
206
+ }
207
+ const client = makeClient(args);
208
+ const appId = await resolveAppId(client, appArg);
209
+ try {
210
+ printJson(await client.restoreAppRow(appId, collection, key));
211
+ }
212
+ catch (e) {
213
+ failFromError(e);
214
+ }
215
+ }
216
+ // `homespun data <app> <coll> deleted`: the recovery bin, newest deletion
217
+ // first. Owner/agent only. Paged with --before, which takes the previous
218
+ // page's `next_before`.
219
+ async function runListDeleted(appArg, collection, args) {
220
+ assertKnownFlags(args, ...specFor("data", "deleted"));
221
+ const client = makeClient(args);
222
+ const appId = await resolveAppId(client, appArg);
223
+ const limitRaw = args.flags.get("limit");
224
+ const before = args.flags.get("before");
225
+ const opts = {};
226
+ if (limitRaw !== undefined) {
227
+ const n = Number(limitRaw);
228
+ if (!Number.isInteger(n) || n < 1) {
229
+ fail("--limit must be a positive integer", "invalid_args");
230
+ }
231
+ opts.limit = n;
232
+ }
233
+ if (before !== undefined)
234
+ opts.before = before;
235
+ try {
236
+ printJson(await client.listDeletedAppRows(appId, collection, opts));
237
+ }
238
+ catch (e) {
239
+ failFromError(e);
240
+ }
241
+ }
194
242
  // `homespun data <app> <coll> purge --key <key>`, owner/agent-only removal that
195
243
  // bypasses an append-only collection (Wave C1). The row key comes from --key
196
244
  // (not a positional) so a purge reads deliberately, harder to fire by accident.
@@ -1,10 +1,11 @@
1
1
  // `homespun template` (issue #890) community marketplace templates: publish an
2
- // owned app as a pending template, read a template's install-time config
3
- // contract, install a template for the caller, and (operator) list/show/approve/
4
- // reject pending submissions. Publish/config-contract/install act AS the calling
5
- // agent's owning human; list-pending/show/approve/reject are operator-gated
6
- // server-side. A template <ref> is a namespaced <handle>/<slug> or a snapshot id,
7
- // passed straight to the SDK.
2
+ // owned app as a pending template, unpublish your own live listing (#1299),
3
+ // read a template's install-time config contract, install a template for the
4
+ // caller, and (operator) list/show/approve/reject pending submissions.
5
+ // Publish/unpublish/config-contract/install act AS the calling agent's owning
6
+ // human; list-pending/show/approve/reject are operator-gated server-side. A
7
+ // template <ref> is a namespaced <handle>/<slug> or a snapshot id, passed
8
+ // straight to the SDK.
8
9
  import { assertKnownFlags } from "../argv.js";
9
10
  import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
10
11
  import { makeClient } from "../config.js";
@@ -18,7 +19,7 @@ export async function runTemplate(args) {
18
19
  return;
19
20
  }
20
21
  if (verb === undefined) {
21
- fail("missing verb: homespun template <publish|config-contract|install|list-pending|show|approve|reject>", "invalid_args");
22
+ fail("missing verb: homespun template <publish|unpublish|config-contract|install|list-pending|show|approve|reject>", "invalid_args");
22
23
  }
23
24
  const sub = {
24
25
  positionals: args.positionals.slice(1),
@@ -31,6 +32,8 @@ export async function runTemplate(args) {
31
32
  switch (verb) {
32
33
  case "publish":
33
34
  return runPublish(sub);
35
+ case "unpublish":
36
+ return runUnpublish(sub);
34
37
  case "config-contract":
35
38
  return runConfigContract(sub);
36
39
  case "install":
@@ -44,7 +47,7 @@ export async function runTemplate(args) {
44
47
  case "reject":
45
48
  return runReject(sub);
46
49
  default:
47
- fail(`unknown verb '${verb}' (homespun template <publish|config-contract|install|list-pending|show|approve|reject>)`, "invalid_args");
50
+ fail(`unknown verb '${verb}' (homespun template <publish|unpublish|config-contract|install|list-pending|show|approve|reject>)`, "invalid_args");
48
51
  }
49
52
  }
50
53
  function parseIntFlag(args, name, bounds = {}) {
@@ -109,6 +112,23 @@ async function runPublish(args) {
109
112
  }
110
113
  }
111
114
  // ---------------------------------------------------------------------------
115
+ // unpublish
116
+ // ---------------------------------------------------------------------------
117
+ async function runUnpublish(args) {
118
+ assertKnownFlags(args, ...specFor("template", "unpublish"));
119
+ const snapshotId = args.positionals[0];
120
+ if (!snapshotId) {
121
+ fail("usage: homespun template unpublish <snapshot-id>", "invalid_args");
122
+ }
123
+ const client = makeClient(args);
124
+ try {
125
+ printJson(await client.unpublishCommunityTemplate(snapshotId));
126
+ }
127
+ catch (e) {
128
+ failFromError(e);
129
+ }
130
+ }
131
+ // ---------------------------------------------------------------------------
112
132
  // config-contract
113
133
  // ---------------------------------------------------------------------------
114
134
  async function runConfigContract(args) {
@@ -133,7 +133,7 @@ const DATA = {
133
133
  noun: "data",
134
134
  tagline: "collection row CRUD for an app",
135
135
  group: "app",
136
- rootSummary: "Collection row CRUD for an app: list, get, upsert, update, delete, purge, import, plus retention (owner override).",
136
+ rootSummary: "Collection row CRUD for an app: list, get, upsert, update, delete, restore, deleted, purge, import, plus retention (owner override).",
137
137
  // The data parser is verb-LAST: `homespun data <app> <collection> <verb>`
138
138
  // (see commands/data.ts, which reads the verb from positionals[2]). Every
139
139
  // other noun is verb-first. Each verb below carries only the positionals that
@@ -219,6 +219,29 @@ const DATA = {
219
219
  ],
220
220
  bools: [{ name: "yes", description: "Skip the confirmation prompt" }],
221
221
  },
222
+ {
223
+ verb: "restore",
224
+ summary: "Restores a soft-deleted row. Owner and agent only.",
225
+ flags: [
226
+ {
227
+ name: "key",
228
+ value: "<key>",
229
+ description: "Key of the deleted row to restore (required)",
230
+ },
231
+ ],
232
+ },
233
+ {
234
+ verb: "deleted",
235
+ summary: "Lists recoverable deleted rows. Owner and agent only.",
236
+ flags: [
237
+ { name: "limit", value: "<n>", description: "Max rows (default 100)" },
238
+ {
239
+ name: "before",
240
+ value: "<iso>",
241
+ description: "Page cursor: the previous page's next_before",
242
+ },
243
+ ],
244
+ },
222
245
  {
223
246
  verb: "purge",
224
247
  summary: "Removes one row even from an append-only collection.",
@@ -297,7 +320,7 @@ const DATA = {
297
320
  notes: [
298
321
  "<app> accepts either the app_id or its slug. upsert is the ONLY create-shaped verb: omit --key to add a new row (the server generates the key); pass --key to ensure a row exists at that key (returns the existing row with deduped:true on a collision). A collision on a row the collection's read list does not reach for you is row_not_found (404) instead of the row, the same answer a get on that key gives, so upsert cannot read past read. Pass --on <field> to upsert on a manifest-declared UNIQUE field instead of the key: the row whose <field> value matches is updated in place (idempotent re-import), else created.",
299
322
  "list --where takes a JSON array of {field, op, value} conditions (ANDed), op one of eq, neq, in, notIn, gt, lt, gte, lte (in and notIn take an array value). --sort takes a JSON array of {field, dir} (dir asc or desc). Filtering is applied AFTER the read permission and author scoping, so a filtered list is always a subset of what you could already read. Comparisons are same-type only (no coercion); dates compare as ISO-8601 strings. A custom --sort cannot be combined with --since.",
300
- "purge removes ONE row by --key even in an append-only collection. Owner and agent only (never members or anyone); it bypasses append-only and the collection delete list on purpose, and writes an audited delete feed entry.",
323
+ "delete is RECOVERABLE: it tombstones the row, `deleted` lists what can still be brought back, and `restore` brings one back for 30 days. purge is the permanent one: it removes ONE row by --key even in an append-only collection, scrubs its contents immediately, and cannot be restored. Both are owner and agent only (never members or anyone); purge bypasses append-only and the collection delete list on purpose, and both write an audited delete feed entry.",
301
324
  "import reads NDJSON (one JSON object per line) OR a JSON array from --file and bulk-writes it in chunks via the batch API, in ONE process. Each object is a row's data. Pass --key-field to derive the row key from a field: an existing row at that key is LEFT UNCHANGED, so this is create-or-skip-by-id, not overwrite, and re-importing changed data for a known key does not update it. A skipped row is reported as a per-row row_not_found rather than an ok when the collection's read list does not reach that row for you (the row is still left unchanged); list 'agent' in read if you want the skip reported as a success. Import DEFAULTS TO SILENT (it suppresses notify and webhooks, since a bulk import is a migration); pass --emit-effects to fire them. A per-row failure is listed in the summary WITHOUT aborting the import.",
302
325
  "retention is an OWNER control: the author declares default retention in the manifest, and this tightens or loosens it per collection at runtime WITHOUT a redeploy. Effective retention is per-axis override-or-author-default: --max-rows/--max-age-days set an axis override, --clear-rows/--clear-age revert an axis to the author default, and with no flag (or --show) it just reads. The response reports the effective bounds, the author default, the override, and wouldPrune (how many live rows the effective bound would prune on the next sweep). The override survives redeploys and effective maxRows is capped at MAX_ROWS_PER_APP.",
303
326
  ],
@@ -1045,7 +1068,7 @@ const TEMPLATE = {
1045
1068
  noun: "template",
1046
1069
  tagline: "community marketplace templates",
1047
1070
  group: "other",
1048
- rootSummary: "Community marketplace templates: publish an owned app, read a template's config-contract, install one, and the operator review queue (list-pending, show, approve, reject).",
1071
+ rootSummary: "Community marketplace templates: publish an owned app, unpublish your own listing, read a template's config-contract, install one, and the operator review queue (list-pending, show, approve, reject).",
1049
1072
  verbs: [
1050
1073
  {
1051
1074
  verb: "publish",
@@ -1105,6 +1128,11 @@ const TEMPLATE = {
1105
1128
  },
1106
1129
  ],
1107
1130
  },
1131
+ {
1132
+ verb: "unpublish",
1133
+ positionals: "<snapshot-id>",
1134
+ summary: "Takes your own live listing out of the public gallery.",
1135
+ },
1108
1136
  {
1109
1137
  verb: "config-contract",
1110
1138
  positionals: "<ref>",
@@ -1156,6 +1184,7 @@ const TEMPLATE = {
1156
1184
  notes: [
1157
1185
  "A template <ref> is a namespaced <handle>/<slug> or a community snapshot id, passed straight to the relay. publish resolves <app> by id or slug, the same way apps and data do.",
1158
1186
  "publish, config-contract, and install act as the calling agent's owning human. Install always creates a new owned app and returns its id, slug, and url; use config-contract first to discover what config an install needs.",
1187
+ "unpublish takes one of YOUR OWN live listings down: it leaves the public gallery, search, and the direct snapshot install link. Existing installs keep working, because an install is a fresh private copy and not a live reference. It is idempotent, and it reports not_found for a snapshot that is not yours. Republish a new version to put a listing back.",
1159
1188
  "list-pending, show, approve, and reject drive the operator review queue and are operator-gated server-side. reject requires --note, which is delivered to the publisher's app feed.",
1160
1189
  ],
1161
1190
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homespunapps/cli",
3
- "version": "1.6.37",
3
+ "version": "1.6.39",
4
4
  "description": "Command-line client for the Homespun relay: deploy a real multi-user web app from your agent, then keep reading and writing its data.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,14 +36,14 @@
36
36
  "test:unit": "vitest run"
37
37
  },
38
38
  "dependencies": {
39
- "@homespunapps/core": "^1.6.37",
39
+ "@homespunapps/core": "^1.6.39",
40
40
  "qrcode-terminal": "^0.12.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/node": "^26.1.1",
43
+ "@types/node": "^26.1.2",
44
44
  "@types/qrcode-terminal": "^0.12.2",
45
45
  "typescript": "^7.0.2",
46
- "vitest": "^4.1.8"
46
+ "vitest": "^4.1.10"
47
47
  },
48
48
  "repository": {
49
49
  "type": "git",