@homespunapps/cli 1.0.1 → 1.4.3
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/commands/claim.js
CHANGED
|
@@ -14,39 +14,11 @@
|
|
|
14
14
|
// keeps working.
|
|
15
15
|
import { HomespunClient, HomespunApiError } from "@homespunapps/core";
|
|
16
16
|
import { assertKnownFlags } from "../argv.js";
|
|
17
|
+
import { specFor } from "../help-catalog.js";
|
|
17
18
|
import { resolveConfig } from "../config.js";
|
|
18
19
|
import { printJson, fail } from "../output.js";
|
|
19
|
-
const KNOWN_FLAGS = ["url", "api-key"];
|
|
20
|
-
const KNOWN_BOOLS = [];
|
|
21
|
-
export const claimHelp = `homespun agent claim — claim this agent for a human
|
|
22
|
-
|
|
23
|
-
Usage:
|
|
24
|
-
homespun agent claim <code>
|
|
25
|
-
|
|
26
|
-
Binds the calling agent to the human whose one-shot claim code is provided.
|
|
27
|
-
The human generates the code in their settings UI (or via the relay's
|
|
28
|
-
POST /v1/self/claim-codes endpoint) and hands it to the agent out-of-band.
|
|
29
|
-
|
|
30
|
-
Arguments:
|
|
31
|
-
<code> The one-shot claim code (begins with cc_). Required.
|
|
32
|
-
|
|
33
|
-
Options:
|
|
34
|
-
--url <url> Relay base URL. Falls back to HOMESPUN_URL / config file.
|
|
35
|
-
--api-key <key> Agent API key. Falls back to HOMESPUN_API_KEY / config file.
|
|
36
|
-
-h, --help Show this help.
|
|
37
|
-
|
|
38
|
-
Output (stdout, JSON):
|
|
39
|
-
{ ok: true, owner_human_id, claimed_at }
|
|
40
|
-
|
|
41
|
-
Errors:
|
|
42
|
-
invalid_code code is unknown, expired, or already consumed
|
|
43
|
-
agent_already_claimed this agent already has an owning human
|
|
44
|
-
|
|
45
|
-
Notes:
|
|
46
|
-
This is a one-way operation. To rotate the owner, revoke this agent
|
|
47
|
-
(\`homespun key revoke\`) and register a new one.`;
|
|
48
20
|
export async function runClaim(args) {
|
|
49
|
-
assertKnownFlags(args,
|
|
21
|
+
assertKnownFlags(args, ...specFor("agent", "claim"));
|
|
50
22
|
const code = args.positionals[0];
|
|
51
23
|
if (!code) {
|
|
52
24
|
fail("missing required argument: <code> — run 'homespun agent claim --help'", "invalid_args");
|
package/dist/commands/config.js
CHANGED
|
@@ -10,39 +10,10 @@
|
|
|
10
10
|
// $XDG_CONFIG_HOME/homespun/config.json. See store.ts for the layout. They make
|
|
11
11
|
// NO network calls — purely local config management.
|
|
12
12
|
import { assertKnownFlags } from "../argv.js";
|
|
13
|
+
import { specFor } from "../help-catalog.js";
|
|
13
14
|
import { describeConfig } from "../config.js";
|
|
14
15
|
import { isValidProfileName, readStore, removeProfile, setCurrentProfile, storePath, upsertProfile, } from "../store.js";
|
|
15
16
|
import { printJson, fail } from "../output.js";
|
|
16
|
-
const SHOW_FLAGS = [];
|
|
17
|
-
const SHOW_BOOLS = [];
|
|
18
|
-
const LIST_FLAGS = [];
|
|
19
|
-
const LIST_BOOLS = [];
|
|
20
|
-
const USE_FLAGS = [];
|
|
21
|
-
const USE_BOOLS = [];
|
|
22
|
-
const ADD_FLAGS = ["api-key"];
|
|
23
|
-
const ADD_BOOLS = [];
|
|
24
|
-
const RM_FLAGS = [];
|
|
25
|
-
const RM_BOOLS = [];
|
|
26
|
-
export const configHelp = `homespun config — show and manage the CLI config (multi-profile)
|
|
27
|
-
|
|
28
|
-
Usage:
|
|
29
|
-
homespun config show Show the resolved relay config
|
|
30
|
-
homespun config list List saved profiles
|
|
31
|
-
homespun config use <profile> Switch the active profile
|
|
32
|
-
homespun config add <profile> --url <u> --api-key <k>
|
|
33
|
-
Add a profile manually
|
|
34
|
-
homespun config rm <profile> Delete a profile
|
|
35
|
-
|
|
36
|
-
A profile is one (url, api_key) pair under a short name (dev, staging, prod).
|
|
37
|
-
Switch via 'homespun config use', '--profile <name>', or the HOMESPUN_PROFILE env var.
|
|
38
|
-
The active profile's (url, api_key) is what every other command sees unless
|
|
39
|
-
overridden by --url / --api-key or HOMESPUN_URL / HOMESPUN_API_KEY.
|
|
40
|
-
|
|
41
|
-
Run \`homespun config <verb> --help\` for verb-specific help. The full API key is
|
|
42
|
-
never printed; only a short masked prefix.
|
|
43
|
-
|
|
44
|
-
The config file lives at \${XDG_CONFIG_HOME:-~/.config}/homespun/config.json
|
|
45
|
-
(mode 0600).`;
|
|
46
17
|
const showHelp = `homespun config show — show the resolved relay config
|
|
47
18
|
|
|
48
19
|
Usage:
|
|
@@ -135,7 +106,7 @@ Options:
|
|
|
135
106
|
Output (stdout, JSON):
|
|
136
107
|
{ profile, was_current, path }`;
|
|
137
108
|
async function runConfigShow(args) {
|
|
138
|
-
assertKnownFlags(args,
|
|
109
|
+
assertKnownFlags(args, ...specFor("config", "show"));
|
|
139
110
|
printJson(describeConfig(args));
|
|
140
111
|
}
|
|
141
112
|
function maskKey(key) {
|
|
@@ -146,7 +117,7 @@ function maskKey(key) {
|
|
|
146
117
|
return key.slice(0, 8) + "…";
|
|
147
118
|
}
|
|
148
119
|
async function runConfigList(args) {
|
|
149
|
-
assertKnownFlags(args,
|
|
120
|
+
assertKnownFlags(args, ...specFor("config", "list"));
|
|
150
121
|
const store = readStore();
|
|
151
122
|
const profiles = Object.entries(store.profiles)
|
|
152
123
|
.map(([name, p]) => ({
|
|
@@ -163,7 +134,7 @@ async function runConfigList(args) {
|
|
|
163
134
|
});
|
|
164
135
|
}
|
|
165
136
|
async function runConfigUse(args) {
|
|
166
|
-
assertKnownFlags(args,
|
|
137
|
+
assertKnownFlags(args, ...specFor("config", "use"));
|
|
167
138
|
const name = args.positionals[1];
|
|
168
139
|
if (!name) {
|
|
169
140
|
fail("missing profile name — usage: homespun config use <profile>", "invalid_args");
|
|
@@ -178,7 +149,7 @@ async function runConfigUse(args) {
|
|
|
178
149
|
printJson({ profile: name, saved_to: savedTo });
|
|
179
150
|
}
|
|
180
151
|
async function runConfigAdd(args) {
|
|
181
|
-
assertKnownFlags(args,
|
|
152
|
+
assertKnownFlags(args, ...specFor("config", "add"));
|
|
182
153
|
const name = args.positionals[1];
|
|
183
154
|
if (!name) {
|
|
184
155
|
fail("missing profile name — usage: homespun config add <profile> --url <url> --api-key <key>", "invalid_args");
|
|
@@ -202,7 +173,7 @@ async function runConfigAdd(args) {
|
|
|
202
173
|
printJson({ profile: name, saved_to: savedTo });
|
|
203
174
|
}
|
|
204
175
|
async function runConfigRm(args) {
|
|
205
|
-
assertKnownFlags(args,
|
|
176
|
+
assertKnownFlags(args, ...specFor("config", "rm"));
|
|
206
177
|
const name = args.positionals[1];
|
|
207
178
|
if (!name) {
|
|
208
179
|
fail("missing profile name — usage: homespun config rm <profile>", "invalid_args");
|
package/dist/commands/data.js
CHANGED
|
@@ -2,37 +2,23 @@
|
|
|
2
2
|
// `homespun records` (v1 app collections) but against `/v1/apps/:id/collections`
|
|
3
3
|
// and with a dedicated per-row GET (no client-side scan — spec-cli §8 ruling
|
|
4
4
|
// 3 confirms the relay route is real).
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
5
6
|
import { assertKnownFlags } from "../argv.js";
|
|
7
|
+
import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
|
|
6
8
|
import { makeClient } from "../config.js";
|
|
7
9
|
import { fail, failFromError, printJson } from "../output.js";
|
|
8
10
|
import { resolveJson } from "../input.js";
|
|
9
11
|
import { resolveAppId } from "../resolve-app.js";
|
|
10
|
-
export const dataHelp = `homespun data — collection row CRUD for a v2 app
|
|
11
|
-
|
|
12
|
-
Usage:
|
|
13
|
-
homespun data <app> <collection> list [--since <cursor>] [--limit <n>]
|
|
14
|
-
homespun data <app> <collection> get <key>
|
|
15
|
-
homespun data <app> <collection> upsert --data <path|json> [--key <key>]
|
|
16
|
-
homespun data <app> <collection> update <key> --data <path|json> [--if-match <version>]
|
|
17
|
-
homespun data <app> <collection> delete <key> [--if-match <version>] [--yes]
|
|
18
|
-
|
|
19
|
-
<app> accepts either the app_id or its slug. upsert is the ONLY create-shaped
|
|
20
|
-
verb (spec-cli §8 ruling 4): omit --key to add a new row (server-generates
|
|
21
|
-
the key); pass --key to ensure a row exists at that key (returns the
|
|
22
|
-
existing row with deduped:true on a collision, never errors).
|
|
23
|
-
|
|
24
|
-
Output (stdout, single JSON). Errors on stderr:
|
|
25
|
-
{"error":{"code","message"}} with non-zero exit.`;
|
|
26
12
|
export async function runData(args) {
|
|
27
13
|
const appArg = args.positionals[0];
|
|
28
14
|
const collection = args.positionals[1];
|
|
29
15
|
const verb = args.positionals[2];
|
|
30
16
|
if ((appArg === undefined || verb === undefined) && args.bools.has("help")) {
|
|
31
|
-
process.stdout.write(
|
|
17
|
+
process.stdout.write(renderNounHelp(nounSpec("data")) + "\n");
|
|
32
18
|
return;
|
|
33
19
|
}
|
|
34
20
|
if (!appArg || !collection || !verb) {
|
|
35
|
-
fail("usage: homespun data <app> <collection> <list|get|upsert|update|delete>", "invalid_args");
|
|
21
|
+
fail("usage: homespun data <app> <collection> <list|get|upsert|update|delete|purge|import>", "invalid_args");
|
|
36
22
|
}
|
|
37
23
|
const sub = {
|
|
38
24
|
positionals: args.positionals.slice(3),
|
|
@@ -53,8 +39,12 @@ export async function runData(args) {
|
|
|
53
39
|
return runUpdate(appArg, collection, sub);
|
|
54
40
|
case "delete":
|
|
55
41
|
return runDelete(appArg, collection, sub);
|
|
42
|
+
case "purge":
|
|
43
|
+
return runPurge(appArg, collection, sub);
|
|
44
|
+
case "import":
|
|
45
|
+
return runImport(appArg, collection, sub);
|
|
56
46
|
default:
|
|
57
|
-
fail(`unknown verb '${verb}'
|
|
47
|
+
fail(`unknown verb '${verb}': homespun data <app> <collection> <list|get|upsert|update|delete|purge|import>`, "invalid_args");
|
|
58
48
|
}
|
|
59
49
|
}
|
|
60
50
|
function parseIntFlag(args, name, defaultValue, bounds = {}) {
|
|
@@ -73,23 +63,50 @@ function parseIntFlag(args, name, defaultValue, bounds = {}) {
|
|
|
73
63
|
return n;
|
|
74
64
|
}
|
|
75
65
|
async function runList(appArg, collection, args) {
|
|
76
|
-
assertKnownFlags(args,
|
|
66
|
+
assertKnownFlags(args, ...specFor("data", "list"));
|
|
77
67
|
const since = args.flags.get("since");
|
|
78
68
|
const limit = parseIntFlag(args, "limit", undefined, { min: 1, max: 1000 });
|
|
69
|
+
const where = parseJsonArrayFlag(args, "where");
|
|
70
|
+
const sort = parseJsonArrayFlag(args, "sort");
|
|
71
|
+
if (since !== undefined && sort !== undefined) {
|
|
72
|
+
fail("--since (cursor pagination) cannot be combined with --sort", "invalid_args");
|
|
73
|
+
}
|
|
79
74
|
const client = makeClient(args);
|
|
80
75
|
const appId = await resolveAppId(client, appArg);
|
|
81
76
|
try {
|
|
82
77
|
printJson(await client.listAppRows(appId, collection, {
|
|
83
78
|
since,
|
|
84
79
|
...(limit !== undefined ? { limit } : {}),
|
|
80
|
+
...(where !== undefined ? { where } : {}),
|
|
81
|
+
...(sort !== undefined ? { sort } : {}),
|
|
85
82
|
}));
|
|
86
83
|
}
|
|
87
84
|
catch (e) {
|
|
88
85
|
failFromError(e);
|
|
89
86
|
}
|
|
90
87
|
}
|
|
88
|
+
// Parse a `--<name> <json>` flag whose value must be a JSON array. Returns
|
|
89
|
+
// undefined when the flag is absent; fails cleanly (invalid_args) when present
|
|
90
|
+
// but not a JSON array. Element-level validation is left to the relay, which
|
|
91
|
+
// returns a precise 400 the CLI surfaces verbatim.
|
|
92
|
+
function parseJsonArrayFlag(args, name) {
|
|
93
|
+
const raw = args.flags.get(name);
|
|
94
|
+
if (raw === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(raw);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
fail(`--${name} must be valid JSON (${e instanceof Error ? e.message : String(e)})`, "invalid_args");
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(parsed)) {
|
|
104
|
+
fail(`--${name} must be a JSON array`, "invalid_args");
|
|
105
|
+
}
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
91
108
|
async function runGet(appArg, collection, args) {
|
|
92
|
-
assertKnownFlags(args,
|
|
109
|
+
assertKnownFlags(args, ...specFor("data", "get"));
|
|
93
110
|
const key = args.positionals[0];
|
|
94
111
|
if (!key) {
|
|
95
112
|
fail("usage: homespun data <app> <collection> get <key>", "invalid_args");
|
|
@@ -104,19 +121,25 @@ async function runGet(appArg, collection, args) {
|
|
|
104
121
|
}
|
|
105
122
|
}
|
|
106
123
|
async function runUpsert(appArg, collection, args) {
|
|
107
|
-
assertKnownFlags(args,
|
|
124
|
+
assertKnownFlags(args, ...specFor("data", "upsert"));
|
|
108
125
|
const dataRaw = args.flags.get("data");
|
|
109
126
|
if (dataRaw === undefined) {
|
|
110
127
|
fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
|
|
111
128
|
}
|
|
112
129
|
const data = resolveJson(dataRaw, "--data");
|
|
113
130
|
const key = args.flags.get("key");
|
|
131
|
+
const on = args.flags.get("on");
|
|
132
|
+
if (key !== undefined && on !== undefined) {
|
|
133
|
+
fail("--key and --on are mutually exclusive (upsert by key, or by a unique field, not both)", "invalid_args");
|
|
134
|
+
}
|
|
114
135
|
const client = makeClient(args);
|
|
115
136
|
const appId = await resolveAppId(client, appArg);
|
|
116
137
|
try {
|
|
117
138
|
const body = { data };
|
|
118
139
|
if (key !== undefined)
|
|
119
140
|
body.key = key;
|
|
141
|
+
if (on !== undefined)
|
|
142
|
+
body.on = on;
|
|
120
143
|
printJson(await client.upsertAppRow(appId, collection, body));
|
|
121
144
|
}
|
|
122
145
|
catch (e) {
|
|
@@ -124,7 +147,7 @@ async function runUpsert(appArg, collection, args) {
|
|
|
124
147
|
}
|
|
125
148
|
}
|
|
126
149
|
async function runUpdate(appArg, collection, args) {
|
|
127
|
-
assertKnownFlags(args,
|
|
150
|
+
assertKnownFlags(args, ...specFor("data", "update"));
|
|
128
151
|
const key = args.positionals[0];
|
|
129
152
|
if (!key) {
|
|
130
153
|
fail("usage: homespun data <app> <collection> update <key> --data <path|json>", "invalid_args");
|
|
@@ -148,7 +171,7 @@ async function runUpdate(appArg, collection, args) {
|
|
|
148
171
|
}
|
|
149
172
|
}
|
|
150
173
|
async function runDelete(appArg, collection, args) {
|
|
151
|
-
assertKnownFlags(args,
|
|
174
|
+
assertKnownFlags(args, ...specFor("data", "delete"));
|
|
152
175
|
const key = args.positionals[0];
|
|
153
176
|
if (!key) {
|
|
154
177
|
fail("usage: homespun data <app> <collection> delete <key>", "invalid_args");
|
|
@@ -166,3 +189,167 @@ async function runDelete(appArg, collection, args) {
|
|
|
166
189
|
failFromError(e);
|
|
167
190
|
}
|
|
168
191
|
}
|
|
192
|
+
// `homespun data <app> <coll> purge --key <key>`, owner/agent-only removal that
|
|
193
|
+
// bypasses an append-only collection (Wave C1). The row key comes from --key
|
|
194
|
+
// (not a positional) so a purge reads deliberately, harder to fire by accident.
|
|
195
|
+
async function runPurge(appArg, collection, args) {
|
|
196
|
+
assertKnownFlags(args, ...specFor("data", "purge"));
|
|
197
|
+
const key = args.flags.get("key");
|
|
198
|
+
if (!key) {
|
|
199
|
+
fail("usage: homespun data <app> <collection> purge --key <key>", "invalid_args");
|
|
200
|
+
}
|
|
201
|
+
const client = makeClient(args);
|
|
202
|
+
const appId = await resolveAppId(client, appArg);
|
|
203
|
+
try {
|
|
204
|
+
await client.purgeAppRow(appId, collection, key);
|
|
205
|
+
printJson({ purged: true, key });
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
failFromError(e);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Default per-batch chunk size (matches the relay's BATCH_MAX_ROWS default). A
|
|
212
|
+
// larger --chunk is capped server-side (a batch over the cap 400s), so the CLI
|
|
213
|
+
// keeps a conservative default and lets an operator raise it if their tier does.
|
|
214
|
+
const DEFAULT_IMPORT_CHUNK = 100;
|
|
215
|
+
/**
|
|
216
|
+
* Parse an import file into an ordered list of raw row objects. Accepts either a
|
|
217
|
+
* single JSON array (the whole file parses as an array) or NDJSON (one JSON
|
|
218
|
+
* object per non-blank line). A malformed line reports its 1-based line number.
|
|
219
|
+
*/
|
|
220
|
+
function parseImportRows(raw) {
|
|
221
|
+
const trimmed = raw.trim();
|
|
222
|
+
if (trimmed.length === 0)
|
|
223
|
+
return [];
|
|
224
|
+
// JSON array form: the entire file is one array literal.
|
|
225
|
+
if (trimmed.startsWith("[")) {
|
|
226
|
+
let parsed;
|
|
227
|
+
try {
|
|
228
|
+
parsed = JSON.parse(trimmed);
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
fail(`--file is not valid JSON (${e instanceof Error ? e.message : String(e)})`, "invalid_args");
|
|
232
|
+
}
|
|
233
|
+
if (!Array.isArray(parsed)) {
|
|
234
|
+
fail("--file top-level JSON must be an array of objects", "invalid_args");
|
|
235
|
+
}
|
|
236
|
+
return parsed.map((el, i) => asRowObject(el, i + 1));
|
|
237
|
+
}
|
|
238
|
+
// NDJSON form: one JSON object per non-blank line.
|
|
239
|
+
const out = [];
|
|
240
|
+
const lines = raw.split("\n");
|
|
241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
242
|
+
const line = lines[i].trim();
|
|
243
|
+
if (line.length === 0)
|
|
244
|
+
continue;
|
|
245
|
+
let parsed;
|
|
246
|
+
try {
|
|
247
|
+
parsed = JSON.parse(line);
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
fail(`--file line ${i + 1} is not valid JSON (${e instanceof Error ? e.message : String(e)})`, "invalid_args");
|
|
251
|
+
}
|
|
252
|
+
out.push(asRowObject(parsed, i + 1));
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function asRowObject(value, where) {
|
|
257
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
258
|
+
fail(`--file entry ${where} must be a JSON object (got ${Array.isArray(value) ? "array" : typeof value})`, "invalid_args");
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
async function runImport(appArg, collection, args) {
|
|
263
|
+
assertKnownFlags(args, ...specFor("data", "import"));
|
|
264
|
+
const file = args.flags.get("file");
|
|
265
|
+
if (file === undefined) {
|
|
266
|
+
fail("--file is required (path to an NDJSON or JSON-array file)", "invalid_args");
|
|
267
|
+
}
|
|
268
|
+
let raw;
|
|
269
|
+
try {
|
|
270
|
+
raw = readFileSync(file, "utf8");
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
fail(`cannot read --file '${file}': ${e instanceof Error ? e.message : String(e)}`, "invalid_args");
|
|
274
|
+
}
|
|
275
|
+
const objects = parseImportRows(raw);
|
|
276
|
+
if (objects.length === 0) {
|
|
277
|
+
fail("--file contained no rows to import", "invalid_args");
|
|
278
|
+
}
|
|
279
|
+
const chunkSize = parseIntFlag(args, "chunk", DEFAULT_IMPORT_CHUNK, {
|
|
280
|
+
min: 1,
|
|
281
|
+
max: 1000,
|
|
282
|
+
});
|
|
283
|
+
const keyField = args.flags.get("key-field");
|
|
284
|
+
const on = args.flags.get("on");
|
|
285
|
+
const emitEffects = args.bools.has("emit-effects");
|
|
286
|
+
if (keyField !== undefined && on !== undefined) {
|
|
287
|
+
fail("--key-field and --on are mutually exclusive (derive the row key from a field, or upsert on a unique field, not both)", "invalid_args");
|
|
288
|
+
}
|
|
289
|
+
// Build the batch inputs once: each object is a row's `data`; --key-field pulls
|
|
290
|
+
// the row key from a field (create-or-skip-by-id on re-import), while --on
|
|
291
|
+
// upserts on a declared-unique field (update-in-place on re-import).
|
|
292
|
+
const rows = objects.map((obj) => {
|
|
293
|
+
if (keyField === undefined)
|
|
294
|
+
return { data: obj };
|
|
295
|
+
const kv = obj[keyField];
|
|
296
|
+
if (kv === undefined ||
|
|
297
|
+
kv === null ||
|
|
298
|
+
(typeof kv !== "string" && typeof kv !== "number")) {
|
|
299
|
+
fail(`--key-field '${keyField}' missing or not a string/number in an input row`, "invalid_args");
|
|
300
|
+
}
|
|
301
|
+
const key = String(kv);
|
|
302
|
+
if (key.length === 0) {
|
|
303
|
+
fail(`--key-field '${keyField}' is empty in an input row`, "invalid_args");
|
|
304
|
+
}
|
|
305
|
+
return { key, data: obj };
|
|
306
|
+
});
|
|
307
|
+
const client = makeClient(args);
|
|
308
|
+
// resolveAppId ONCE for the whole import (not per chunk / per row): the app id
|
|
309
|
+
// is stable for the process, so one lookup drives every batch call.
|
|
310
|
+
const appId = await resolveAppId(client, appArg);
|
|
311
|
+
const total = rows.length;
|
|
312
|
+
const chunkCount = Math.ceil(total / chunkSize);
|
|
313
|
+
let imported = 0;
|
|
314
|
+
let failed = 0;
|
|
315
|
+
const failures = [];
|
|
316
|
+
try {
|
|
317
|
+
for (let start = 0, chunkNo = 1; start < total; start += chunkSize, chunkNo++) {
|
|
318
|
+
const chunk = rows.slice(start, start + chunkSize);
|
|
319
|
+
const res = await client.batchRows(appId, collection, chunk, {
|
|
320
|
+
...(emitEffects ? { emitEffects: true } : {}),
|
|
321
|
+
...(on !== undefined ? { on } : {}),
|
|
322
|
+
});
|
|
323
|
+
for (const r of res.results) {
|
|
324
|
+
// Translate the per-chunk index back to the GLOBAL row index.
|
|
325
|
+
const globalIndex = start + r.index;
|
|
326
|
+
if (r.ok) {
|
|
327
|
+
imported += 1;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
failed += 1;
|
|
331
|
+
failures.push({
|
|
332
|
+
index: globalIndex,
|
|
333
|
+
...(r.key !== undefined ? { key: r.key } : {}),
|
|
334
|
+
error: r.error,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Human-readable progress on stderr so stdout stays a single JSON summary.
|
|
339
|
+
process.stderr.write(`imported ${imported}/${total} (chunk ${chunkNo}/${chunkCount}, ${failed} failed)\n`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
failFromError(e);
|
|
344
|
+
}
|
|
345
|
+
printJson({
|
|
346
|
+
app: appId,
|
|
347
|
+
collection,
|
|
348
|
+
total,
|
|
349
|
+
imported,
|
|
350
|
+
failed,
|
|
351
|
+
chunks: chunkCount,
|
|
352
|
+
silent: !emitEffects,
|
|
353
|
+
failures,
|
|
354
|
+
});
|
|
355
|
+
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -5,36 +5,10 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { makeClient } from "../config.js";
|
|
7
7
|
import { assertKnownFlags } from "../argv.js";
|
|
8
|
+
import { specFor } from "../help-catalog.js";
|
|
8
9
|
import { fail, failFromError, printJson } from "../output.js";
|
|
9
10
|
import { resolveJson } from "../input.js";
|
|
10
11
|
import { resolveAppId } from "../resolve-app.js";
|
|
11
|
-
const KNOWN_FLAGS = ["app", "manifest", "slug", "visibility"];
|
|
12
|
-
const KNOWN_BOOLS = ["force"];
|
|
13
|
-
export const deployHelp = `homespun deploy — create or redeploy an app
|
|
14
|
-
|
|
15
|
-
Usage:
|
|
16
|
-
homespun deploy <dir|file> [--app <id>] [--manifest <path|json>]
|
|
17
|
-
[--slug <slug>] [--visibility private|link|public] [--force]
|
|
18
|
-
|
|
19
|
-
Packaging (one canonical shape, one escape hatch):
|
|
20
|
-
Directory (canonical): homespun deploy ./my-app
|
|
21
|
-
Reads ./my-app/index.html and ./my-app/manifest.json — fixed filenames,
|
|
22
|
-
no discovery heuristics. Both files are required.
|
|
23
|
-
Single file (escape hatch): homespun deploy ./index.html --manifest ./manifest.json
|
|
24
|
-
--manifest accepts a file path OR inline JSON.
|
|
25
|
-
|
|
26
|
-
Create vs. redeploy — decided by --app's presence, not two verbs:
|
|
27
|
-
(no --app) Create (POST /v1/apps). New apps default to private
|
|
28
|
-
(owner plus invited members, sign-in gated). --slug is
|
|
29
|
-
accepted with private or public visibility, including the
|
|
30
|
-
default; an explicit --visibility link always gets a
|
|
31
|
-
server-generated slug and rejects --slug.
|
|
32
|
-
--app <id> Redeploy (POST /v1/apps/:id/versions). --slug/--visibility
|
|
33
|
-
are rejected here (slug is immutable; change visibility via
|
|
34
|
-
'homespun apps update'). --force overrides the compat gate.
|
|
35
|
-
|
|
36
|
-
Output: { app_id, slug, url, version, visibility, created, compat?, breaks? }
|
|
37
|
-
Errors on stderr: {"error":{"code","message"}} with non-zero exit.`;
|
|
38
12
|
function readBundle(source, manifestFlag) {
|
|
39
13
|
if (!existsSync(source)) {
|
|
40
14
|
fail(`no such file or directory: ${source}`, "invalid_args");
|
|
@@ -69,7 +43,7 @@ function readBundle(source, manifestFlag) {
|
|
|
69
43
|
};
|
|
70
44
|
}
|
|
71
45
|
export async function runDeploy(args) {
|
|
72
|
-
assertKnownFlags(args,
|
|
46
|
+
assertKnownFlags(args, ...specFor("deploy"));
|
|
73
47
|
const source = args.positionals[0];
|
|
74
48
|
if (!source) {
|
|
75
49
|
fail("usage: homespun deploy <dir|file> [--app <id>] ...", "invalid_args");
|
|
@@ -82,8 +56,28 @@ export async function runDeploy(args) {
|
|
|
82
56
|
fail("--visibility must be private|link|public", "invalid_args");
|
|
83
57
|
}
|
|
84
58
|
const force = args.bools.has("force");
|
|
59
|
+
const check = args.bools.has("check");
|
|
85
60
|
const bundle = readBundle(source, args.flags.get("manifest"));
|
|
86
61
|
const client = makeClient(args);
|
|
62
|
+
// Dry run (--check): validate + report what a real deploy would do, persist
|
|
63
|
+
// NOTHING. Runs for both create (no --app) and redeploy (--app), the latter
|
|
64
|
+
// reporting the compat gate. slug/visibility are not part of a dry run.
|
|
65
|
+
if (check) {
|
|
66
|
+
try {
|
|
67
|
+
const id = appId !== undefined ? await resolveAppId(client, appId) : undefined;
|
|
68
|
+
const result = await client.checkDeploy({
|
|
69
|
+
...(id !== undefined ? { app_id: id } : {}),
|
|
70
|
+
html: bundle.html,
|
|
71
|
+
manifest: bundle.manifest,
|
|
72
|
+
...(force ? { force } : {}),
|
|
73
|
+
});
|
|
74
|
+
printJson(result);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
failFromError(e);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
87
81
|
if (appId === undefined) {
|
|
88
82
|
// Create. Client-side mirror of the relay's slug_not_allowed_for_link —
|
|
89
83
|
// fail fast rather than round-trip a request that will 400 (spec-cli §3.1).
|
|
@@ -128,6 +122,7 @@ export async function runDeploy(args) {
|
|
|
128
122
|
created: false,
|
|
129
123
|
compat: redeployed.compat,
|
|
130
124
|
...(redeployed.breaks ? { breaks: redeployed.breaks } : {}),
|
|
125
|
+
...(redeployed.warnings ? { warnings: redeployed.warnings } : {}),
|
|
131
126
|
});
|
|
132
127
|
}
|
|
133
128
|
catch (e) {
|
|
@@ -1,48 +1,7 @@
|
|
|
1
1
|
import { assertKnownFlags } from "../argv.js";
|
|
2
|
+
import { specFor } from "../help-catalog.js";
|
|
2
3
|
import { makeClient } from "../config.js";
|
|
3
4
|
import { printJson, fail, failFromError } from "../output.js";
|
|
4
|
-
const CREATE_FLAGS = ["type", "message", "app-id"];
|
|
5
|
-
const LIST_FLAGS = ["limit", "before"];
|
|
6
|
-
const NO_BOOLS = [];
|
|
7
|
-
export const feedbackHelp = `homespun feedback — submit / list feedback to the relay operator
|
|
8
|
-
|
|
9
|
-
Feedback is a one-shot bug report, feature request, or note from YOUR agent
|
|
10
|
-
to whoever runs the relay. Submissions are stored in the relay DB; the
|
|
11
|
-
operator triages out of band.
|
|
12
|
-
|
|
13
|
-
Usage:
|
|
14
|
-
homespun feedback <subcommand> [options]
|
|
15
|
-
|
|
16
|
-
Subcommands:
|
|
17
|
-
create Submit one feedback row. Requires --type and --message.
|
|
18
|
-
Prints { id, type, created_at } — the message is not echoed back.
|
|
19
|
-
|
|
20
|
-
list List YOUR agent's own submissions, newest first. Prints
|
|
21
|
-
{ items: [...], next_before?: <cursor> }. Pass --before <cursor>
|
|
22
|
-
from a previous page to fetch the next page.
|
|
23
|
-
|
|
24
|
-
Options for 'create':
|
|
25
|
-
--type <bug|feature|note> Feedback category. Required.
|
|
26
|
-
--message <text|-> Message body. Pass '-' to read from stdin.
|
|
27
|
-
1..4000 chars after trim.
|
|
28
|
-
--app-id <id> Optional App this feedback relates to;
|
|
29
|
-
must be owned by YOUR agent's human.
|
|
30
|
-
|
|
31
|
-
Options for 'list':
|
|
32
|
-
--limit <N> Page size (default 50, max 100).
|
|
33
|
-
--before <cursor> Opaque cursor from a previous page's next_before.
|
|
34
|
-
|
|
35
|
-
Global:
|
|
36
|
-
--url <url> Relay base URL (overrides HOMESPUN_URL).
|
|
37
|
-
--api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
|
|
38
|
-
-h, --help Show this help.
|
|
39
|
-
|
|
40
|
-
Examples:
|
|
41
|
-
homespun feedback create --type bug --message "watch hangs on empty app"
|
|
42
|
-
echo "long-form note..." | homespun feedback create --type note --message -
|
|
43
|
-
homespun feedback list --limit 20
|
|
44
|
-
|
|
45
|
-
Output: stdout is machine-readable JSON.`;
|
|
46
5
|
const FEEDBACK_TYPES = ["bug", "feature", "note"];
|
|
47
6
|
async function readStdin() {
|
|
48
7
|
const chunks = [];
|
|
@@ -52,7 +11,7 @@ async function readStdin() {
|
|
|
52
11
|
return Buffer.concat(chunks).toString("utf8");
|
|
53
12
|
}
|
|
54
13
|
async function runFeedbackCreate(args) {
|
|
55
|
-
assertKnownFlags(args,
|
|
14
|
+
assertKnownFlags(args, ...specFor("feedback", "create"));
|
|
56
15
|
const type = args.flags.get("type");
|
|
57
16
|
const rawMessage = args.flags.get("message");
|
|
58
17
|
const appId = args.flags.get("app-id");
|
|
@@ -92,7 +51,7 @@ async function runFeedbackCreate(args) {
|
|
|
92
51
|
}
|
|
93
52
|
}
|
|
94
53
|
async function runFeedbackList(args) {
|
|
95
|
-
assertKnownFlags(args,
|
|
54
|
+
assertKnownFlags(args, ...specFor("feedback", "list"));
|
|
96
55
|
const limitRaw = args.flags.get("limit");
|
|
97
56
|
const before = args.flags.get("before");
|
|
98
57
|
let limit;
|