@paneui/cli 0.0.9 → 0.0.11
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 +8 -8
- package/dist/argv.js +3 -3
- package/dist/commands/agent.js +10 -2
- package/dist/commands/attachment-token.js +2 -2
- package/dist/commands/attachment-upload.js +8 -10
- package/dist/commands/attachment.js +7 -7
- package/dist/commands/claim.js +1 -1
- package/dist/commands/config.js +232 -20
- package/dist/commands/create.js +132 -21
- package/dist/commands/delete.js +12 -12
- package/dist/commands/feedback.js +5 -5
- package/dist/commands/list.js +17 -17
- package/dist/commands/logout.js +43 -13
- package/dist/commands/participant.js +38 -38
- package/dist/commands/query.js +204 -0
- package/dist/commands/records.js +285 -0
- package/dist/commands/register.js +53 -15
- package/dist/commands/send.js +17 -17
- package/dist/commands/set-key.js +92 -0
- package/dist/commands/skill.js +1 -1
- package/dist/commands/state.js +12 -12
- package/dist/commands/taste.js +3 -3
- package/dist/commands/template-records.js +195 -0
- package/dist/commands/template.js +243 -35
- package/dist/commands/trash.js +102 -0
- package/dist/commands/watch.js +22 -22
- package/dist/config.js +87 -20
- package/dist/format.js +133 -0
- package/dist/index.js +97 -20
- package/dist/output.js +1 -1
- package/dist/store.js +167 -26
- package/dist/upgrade.js +1 -1
- package/dist/version.js +2 -2
- package/package.json +5 -3
- package/dist/commands/surface.js +0 -118
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// `pane
|
|
2
|
-
// participant URL on an existing
|
|
3
|
-
// primitives that together replace the destructive `pane
|
|
4
|
-
// pane
|
|
1
|
+
// `pane participant <new|revoke>` — mint or invalidate one
|
|
2
|
+
// participant URL on an existing pane. Recovery + leak-containment
|
|
3
|
+
// primitives that together replace the destructive `pane delete +
|
|
4
|
+
// pane create` workaround for the lost-URL case.
|
|
5
5
|
//
|
|
6
|
-
// This file is a sub-noun dispatcher under `pane
|
|
6
|
+
// This file is a sub-noun dispatcher under `pane pane`. The pane
|
|
7
7
|
// dispatcher hands us a ParsedArgs whose positionals[0] is "participant"
|
|
8
8
|
// (our sub-noun marker), so we read the verb from positionals[1] and the
|
|
9
9
|
// args from positionals[2..]. This mirrors the way every other sub-verb
|
|
@@ -14,32 +14,32 @@ import { makeClient } from "../config.js";
|
|
|
14
14
|
import { printJson, fail, failFromError } from "../output.js";
|
|
15
15
|
const NO_FLAGS = [];
|
|
16
16
|
const NO_BOOLS = [];
|
|
17
|
-
export const participantHelp = `pane
|
|
17
|
+
export const participantHelp = `pane participant — manage one pane's participant URLs
|
|
18
18
|
|
|
19
19
|
Participant tokens are stored hashed on the relay and CANNOT be recovered.
|
|
20
20
|
If you lost the create-response (and the URL with it), use 'new' to mint a
|
|
21
|
-
fresh URL — the
|
|
22
|
-
Use 'revoke' to invalidate a single URL while keeping the
|
|
21
|
+
fresh URL — the pane keeps its event log, template pin, and created_at.
|
|
22
|
+
Use 'revoke' to invalidate a single URL while keeping the pane alive.
|
|
23
23
|
|
|
24
24
|
Usage:
|
|
25
|
-
pane
|
|
25
|
+
pane participant <verb> <args>
|
|
26
26
|
|
|
27
27
|
Verbs:
|
|
28
|
-
list <
|
|
28
|
+
list <pane-id> List the participants on one pane, including
|
|
29
29
|
revoked rows (for audit). Returns
|
|
30
|
-
{
|
|
30
|
+
{ pane_id, items: [...] } where each item
|
|
31
31
|
carries { participant_id, kind, token_prefix,
|
|
32
32
|
joined_at, revoked_at }. Use this to find the
|
|
33
33
|
participant_id you need to pass to 'revoke'.
|
|
34
34
|
|
|
35
|
-
new <
|
|
35
|
+
new <pane-id> Mint a fresh human URL on an existing pane.
|
|
36
36
|
Returns { participant_id, kind, token, url,
|
|
37
37
|
created_at } — ONCE. The plaintext token is
|
|
38
38
|
never recoverable; save the response (pipe to
|
|
39
39
|
a JSONL log) before delivering the URL.
|
|
40
40
|
|
|
41
|
-
revoke <
|
|
42
|
-
Invalidate one participant URL. The
|
|
41
|
+
revoke <pane-id> <participant-id>
|
|
42
|
+
Invalidate one participant URL. The pane's
|
|
43
43
|
other participants (and the agent's own
|
|
44
44
|
websocket) are untouched. Idempotent: running
|
|
45
45
|
revoke twice still returns success.
|
|
@@ -54,24 +54,24 @@ Options:
|
|
|
54
54
|
-h, --help Show this help.
|
|
55
55
|
|
|
56
56
|
Recovery recipe:
|
|
57
|
-
pane
|
|
58
|
-
pane
|
|
57
|
+
pane list # find pane_id
|
|
58
|
+
pane participant list <pane-id> # find participant
|
|
59
59
|
# ids on that
|
|
60
|
-
#
|
|
61
|
-
pane
|
|
62
|
-
pane
|
|
60
|
+
# pane
|
|
61
|
+
pane participant new <pane-id> # mint a new URL
|
|
62
|
+
pane participant revoke <pane-id> <p-id> # invalidate the
|
|
63
63
|
# old URL
|
|
64
64
|
|
|
65
65
|
Output: stdout is machine-readable JSON.`;
|
|
66
66
|
async function runParticipantList(args) {
|
|
67
|
-
assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
70
|
-
fail("missing <
|
|
67
|
+
assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane participant list");
|
|
68
|
+
const paneId = args.positionals[1];
|
|
69
|
+
if (!paneId) {
|
|
70
|
+
fail("missing <pane-id> — usage: pane participant list <pane-id>", "invalid_args");
|
|
71
71
|
}
|
|
72
72
|
const client = makeClient(args);
|
|
73
73
|
try {
|
|
74
|
-
const res = await client.listParticipants(
|
|
74
|
+
const res = await client.listParticipants(paneId);
|
|
75
75
|
printJson(res);
|
|
76
76
|
}
|
|
77
77
|
catch (e) {
|
|
@@ -79,14 +79,14 @@ async function runParticipantList(args) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
async function runParticipantNew(args) {
|
|
82
|
-
assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
85
|
-
fail("missing <
|
|
82
|
+
assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane participant new");
|
|
83
|
+
const paneId = args.positionals[1];
|
|
84
|
+
if (!paneId) {
|
|
85
|
+
fail("missing <pane-id> — usage: pane participant new <pane-id>", "invalid_args");
|
|
86
86
|
}
|
|
87
87
|
const client = makeClient(args);
|
|
88
88
|
try {
|
|
89
|
-
const res = await client.mintParticipant(
|
|
89
|
+
const res = await client.mintParticipant(paneId);
|
|
90
90
|
printJson(res);
|
|
91
91
|
}
|
|
92
92
|
catch (e) {
|
|
@@ -94,17 +94,17 @@ async function runParticipantNew(args) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
async function runParticipantRevoke(args) {
|
|
97
|
-
assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane
|
|
98
|
-
const
|
|
97
|
+
assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane participant revoke");
|
|
98
|
+
const paneId = args.positionals[1];
|
|
99
99
|
const participantId = args.positionals[2];
|
|
100
|
-
if (!
|
|
101
|
-
fail("missing arguments — usage: pane
|
|
100
|
+
if (!paneId || !participantId) {
|
|
101
|
+
fail("missing arguments — usage: pane participant revoke <pane-id> <participant-id>", "invalid_args");
|
|
102
102
|
}
|
|
103
103
|
const client = makeClient(args);
|
|
104
104
|
try {
|
|
105
|
-
await client.revokeParticipant(
|
|
105
|
+
await client.revokeParticipant(paneId, participantId);
|
|
106
106
|
printJson({
|
|
107
|
-
|
|
107
|
+
pane_id: paneId,
|
|
108
108
|
participant_id: participantId,
|
|
109
109
|
revoked: true,
|
|
110
110
|
});
|
|
@@ -115,7 +115,7 @@ async function runParticipantRevoke(args) {
|
|
|
115
115
|
}
|
|
116
116
|
export async function runParticipant(args) {
|
|
117
117
|
// positionals[0] is the verb (list | new | revoke), positionals[1..] are
|
|
118
|
-
// the verb's args. (The
|
|
118
|
+
// the verb's args. (The pane.ts dispatcher already shifted off the
|
|
119
119
|
// "participant" marker before calling us.)
|
|
120
120
|
const verb = args.positionals[0];
|
|
121
121
|
switch (verb) {
|
|
@@ -129,9 +129,9 @@ export async function runParticipant(args) {
|
|
|
129
129
|
await runParticipantRevoke(args);
|
|
130
130
|
break;
|
|
131
131
|
case undefined:
|
|
132
|
-
fail("missing verb — usage: pane
|
|
132
|
+
fail("missing verb — usage: pane participant <list|new|revoke> (run 'pane participant --help')", "invalid_args");
|
|
133
133
|
break;
|
|
134
134
|
default:
|
|
135
|
-
fail(`unknown participant verb '${verb}' — expected list|new|revoke (run 'pane
|
|
135
|
+
fail(`unknown participant verb '${verb}' — expected list|new|revoke (run 'pane participant --help')`, "invalid_args");
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// `pane query` — read-only SQL over the calling agent's scoped data (#355).
|
|
2
|
+
//
|
|
3
|
+
// The whole feature lives on the relay (POST /v1/query). The CLI is a thin
|
|
4
|
+
// shell over @paneui/core's `client.query(sql)`:
|
|
5
|
+
// 1. Read the SQL from args (positional), stdin, or --file.
|
|
6
|
+
// 2. Send it to the relay.
|
|
7
|
+
// 3. Format the result for human or pipe (json | csv | tsv | table),
|
|
8
|
+
// auto-detecting TTY when --format isn't passed.
|
|
9
|
+
import { assertKnownFlags } from "../argv.js";
|
|
10
|
+
import { makeClient } from "../config.js";
|
|
11
|
+
import { fail, failFromError, printJson } from "../output.js";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
const KNOWN_FLAGS = new Set(["file", "format", "pane", "url", "api-key"]);
|
|
14
|
+
const KNOWN_BOOLS = new Set(["help"]);
|
|
15
|
+
const VALID_FORMATS = new Set(["json", "csv", "tsv", "table"]);
|
|
16
|
+
export const queryHelp = `pane query — run read-only SQL over your scoped data (#355)
|
|
17
|
+
|
|
18
|
+
Available tables (all rows already scoped to panes you own):
|
|
19
|
+
|
|
20
|
+
panes id, title, template_id, template_version, status,
|
|
21
|
+
created_at, expires_at, deleted_at, metadata, input_data
|
|
22
|
+
records id, pane_id, collection, key, data, version, seq,
|
|
23
|
+
author_kind, author_id, created_at, updated_at, deleted_at
|
|
24
|
+
events id, pane_id, type, ts, author_kind, author_id, data,
|
|
25
|
+
template_version_id
|
|
26
|
+
|
|
27
|
+
\`data\` is a JSON column — project with Postgres-style operators:
|
|
28
|
+
data->>'title' text
|
|
29
|
+
(data->>'done')::boolean cast
|
|
30
|
+
data->'nested'->>'inner_field' deep
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
pane query "<SQL>" SQL as positional argument
|
|
34
|
+
pane query --file ./report.sql read from a file
|
|
35
|
+
echo "SELECT ..." | pane query read from stdin
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--file <path> read SQL from a file instead of an argument / stdin
|
|
39
|
+
--format <fmt> json | csv | tsv | table (default: table for TTYs,
|
|
40
|
+
json otherwise)
|
|
41
|
+
--pane <id> scope the query to a single pane (resolves Phase 2
|
|
42
|
+
view_conflict when two of your panes have the same
|
|
43
|
+
collection name with different schemas)
|
|
44
|
+
--url <url> relay base URL (overrides PANE_URL)
|
|
45
|
+
--api-key <key> agent API key (overrides PANE_API_KEY)
|
|
46
|
+
-h, --help show this help
|
|
47
|
+
|
|
48
|
+
Limits:
|
|
49
|
+
- Result is capped at 10,000 rows (response.truncated = true if hit).
|
|
50
|
+
- Statement timeout: 10 seconds.
|
|
51
|
+
- SQL: SELECT / WITH / SHOW / DESCRIBE / EXPLAIN / PRAGMA only.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
pane query "SELECT title FROM panes ORDER BY created_at DESC LIMIT 10"
|
|
55
|
+
pane query "SELECT type, COUNT(*) AS n FROM events GROUP BY 1 ORDER BY n DESC"
|
|
56
|
+
pane query "SELECT data->>'title' AS title, version
|
|
57
|
+
FROM records WHERE collection = 'todos' AND deleted_at IS NULL"
|
|
58
|
+
`;
|
|
59
|
+
export async function runQuery(args) {
|
|
60
|
+
if (args.bools.has("help")) {
|
|
61
|
+
process.stdout.write(queryHelp + "\n");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane query");
|
|
65
|
+
// Decide format. Default depends on TTY-ness of stdout so piping is
|
|
66
|
+
// never broken by accidentally getting a column-aligned table.
|
|
67
|
+
let format = args.flags.get("format");
|
|
68
|
+
if (format === undefined || format === "") {
|
|
69
|
+
format = process.stdout.isTTY ? "table" : "json";
|
|
70
|
+
}
|
|
71
|
+
if (!VALID_FORMATS.has(format)) {
|
|
72
|
+
fail(`--format must be one of ${[...VALID_FORMATS].join("|")} (got '${format}')`, "invalid_args");
|
|
73
|
+
}
|
|
74
|
+
// Source the SQL: --file > positional > stdin (in that order).
|
|
75
|
+
let sql = null;
|
|
76
|
+
const file = args.flags.get("file");
|
|
77
|
+
if (file !== undefined && file !== "") {
|
|
78
|
+
try {
|
|
79
|
+
sql = readFileSync(file, "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
fail(`--file '${file}' could not be read: ${e.message}`, "invalid_args");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (args.positionals.length > 0) {
|
|
86
|
+
sql = args.positionals.join(" ");
|
|
87
|
+
}
|
|
88
|
+
else if (!process.stdin.isTTY) {
|
|
89
|
+
sql = await readAllStdin();
|
|
90
|
+
}
|
|
91
|
+
if (sql == null || sql.trim().length === 0) {
|
|
92
|
+
fail("missing SQL — pass it as the positional argument, --file <path>, or pipe it to stdin", "invalid_args");
|
|
93
|
+
}
|
|
94
|
+
const paneId = args.flags.get("pane");
|
|
95
|
+
if (paneId !== undefined && paneId !== "" && !paneId.startsWith("pan_")) {
|
|
96
|
+
fail(`--pane must be a pane id (starts with 'pan_'); got '${paneId}'`, "invalid_args");
|
|
97
|
+
}
|
|
98
|
+
const client = makeClient(args);
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await client.query(sql, paneId !== undefined && paneId !== "" ? { paneId } : {});
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
failFromError(e);
|
|
105
|
+
return; // unreachable; failFromError exits
|
|
106
|
+
}
|
|
107
|
+
switch (format) {
|
|
108
|
+
case "json":
|
|
109
|
+
printJson(result);
|
|
110
|
+
break;
|
|
111
|
+
case "csv":
|
|
112
|
+
writeDelimited(result, ",");
|
|
113
|
+
break;
|
|
114
|
+
case "tsv":
|
|
115
|
+
writeDelimited(result, "\t");
|
|
116
|
+
break;
|
|
117
|
+
case "table":
|
|
118
|
+
writeTable(result);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function readAllStdin() {
|
|
123
|
+
const chunks = [];
|
|
124
|
+
for await (const chunk of process.stdin) {
|
|
125
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
126
|
+
}
|
|
127
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
128
|
+
}
|
|
129
|
+
// --------------------------------------------------------------------------
|
|
130
|
+
// Formatters
|
|
131
|
+
// --------------------------------------------------------------------------
|
|
132
|
+
function writeDelimited(result, sep) {
|
|
133
|
+
process.stdout.write(result.columns.map((c) => escapeDelimited(sep, c)).join(sep) + "\n");
|
|
134
|
+
for (const row of result.rows) {
|
|
135
|
+
process.stdout.write(row
|
|
136
|
+
.map((c) => escapeDelimited(sep, formatCellForText(c)))
|
|
137
|
+
.join(sep) + "\n");
|
|
138
|
+
}
|
|
139
|
+
if (result.truncated) {
|
|
140
|
+
process.stderr.write(`[truncated: result capped at ${result.rows.length} rows]\n`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function escapeDelimited(sep, value) {
|
|
144
|
+
// RFC-4180-ish: quote when the cell contains the delimiter, a quote, or a newline.
|
|
145
|
+
if (value.includes(sep) ||
|
|
146
|
+
value.includes('"') ||
|
|
147
|
+
value.includes("\n") ||
|
|
148
|
+
value.includes("\r")) {
|
|
149
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
function writeTable(result) {
|
|
154
|
+
if (result.columns.length === 0) {
|
|
155
|
+
process.stdout.write("(no columns)\n");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const cells = [];
|
|
159
|
+
cells.push(result.columns.slice());
|
|
160
|
+
for (const row of result.rows) {
|
|
161
|
+
cells.push(row.map((c) => formatCellForText(c)));
|
|
162
|
+
}
|
|
163
|
+
// Compute column widths (cap at 80 to keep the table sane for wide JSON).
|
|
164
|
+
const COL_MAX = 80;
|
|
165
|
+
const widths = result.columns.map((_, ci) => Math.min(COL_MAX, Math.max(...cells.map((r) => visualWidth(r[ci] ?? "")))));
|
|
166
|
+
const rule = "─".repeat(widths.reduce((a, b) => a + b, 0) + (widths.length - 1) * 3);
|
|
167
|
+
// Header
|
|
168
|
+
process.stdout.write(formatRow(cells[0], widths) + "\n");
|
|
169
|
+
process.stdout.write(rule + "\n");
|
|
170
|
+
for (let i = 1; i < cells.length; i++) {
|
|
171
|
+
process.stdout.write(formatRow(cells[i], widths) + "\n");
|
|
172
|
+
}
|
|
173
|
+
process.stderr.write(`\n${result.rows.length} row${result.rows.length === 1 ? "" : "s"}${result.truncated ? " (truncated; cap = 10000)" : ""} · scope: ${result.scope.kind} (${result.scope.pane_count} panes) · ${result.elapsed_ms}ms\n`);
|
|
174
|
+
}
|
|
175
|
+
function formatRow(cells, widths) {
|
|
176
|
+
return cells
|
|
177
|
+
.map((c, i) => truncate(c, widths[i] ?? 0).padEnd(widths[i] ?? 0))
|
|
178
|
+
.join(" │ ");
|
|
179
|
+
}
|
|
180
|
+
function truncate(s, w) {
|
|
181
|
+
if (visualWidth(s) <= w)
|
|
182
|
+
return s;
|
|
183
|
+
return s.slice(0, Math.max(0, w - 1)) + "…";
|
|
184
|
+
}
|
|
185
|
+
function visualWidth(s) {
|
|
186
|
+
// Strip ANSI / treat as raw text — agents won't be styling SQL output.
|
|
187
|
+
return s.length;
|
|
188
|
+
}
|
|
189
|
+
function formatCellForText(v) {
|
|
190
|
+
if (v === null || v === undefined)
|
|
191
|
+
return "";
|
|
192
|
+
if (typeof v === "string")
|
|
193
|
+
return v;
|
|
194
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
195
|
+
return String(v);
|
|
196
|
+
if (typeof v === "bigint")
|
|
197
|
+
return v.toString();
|
|
198
|
+
try {
|
|
199
|
+
return JSON.stringify(v);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return String(v);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// `pane records` — CRUD + watch for per-pane mutable record collections
|
|
2
|
+
// (#297). Thin wrapper over the @paneui/core PaneClient + openStream APIs.
|
|
3
|
+
import { assertKnownFlags } from "../argv.js";
|
|
4
|
+
import { makeClient, resolveConfig } from "../config.js";
|
|
5
|
+
import { fail, failFromError, printJson } from "../output.js";
|
|
6
|
+
import { resolveJson } from "../input.js";
|
|
7
|
+
import { openStream } from "@paneui/core";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Help text
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export const recordsHelp = `pane records — CRUD + watch for per-pane record collections
|
|
12
|
+
|
|
13
|
+
A record is a row in a mutable per-pane collection (posts, comments,
|
|
14
|
+
reactions, etc.) declared by the template's recordSchema. The deep design is
|
|
15
|
+
at https://github.com/aerolalit/paneui/issues/287.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
pane records <verb> [options]
|
|
19
|
+
|
|
20
|
+
Verbs:
|
|
21
|
+
list <pane-id> <collection>
|
|
22
|
+
[--since <seq>] [--limit <n>] [--include-tombstones]
|
|
23
|
+
get <pane-id> <collection> <record-key>
|
|
24
|
+
upsert <pane-id> <collection>
|
|
25
|
+
--data <path|json> [--key <record-key>]
|
|
26
|
+
update <pane-id> <collection> <record-key>
|
|
27
|
+
--data <path|json> [--if-match <version>]
|
|
28
|
+
delete <pane-id> <collection> <record-key>
|
|
29
|
+
[--if-match <version>] [--yes]
|
|
30
|
+
watch <pane-id>
|
|
31
|
+
[--collection <name>]... [--since-seq <name>=<n>]...
|
|
32
|
+
|
|
33
|
+
Output (stdout, JSON-per-line for watch, single JSON for others).
|
|
34
|
+
Errors on stderr: {"error":{"code","message"}} with non-zero exit.`;
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Dispatch
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
export async function runRecords(args) {
|
|
39
|
+
const verb = args.positionals[0];
|
|
40
|
+
// `pane records --help` (top-level, no verb)
|
|
41
|
+
if ((verb === undefined || verb === "help") && args.bools.has("help")) {
|
|
42
|
+
process.stdout.write(recordsHelp + "\n");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (verb === undefined) {
|
|
46
|
+
fail("missing verb — pane records <list|get|upsert|update|delete|watch>", "invalid_args");
|
|
47
|
+
}
|
|
48
|
+
const sub = {
|
|
49
|
+
positionals: args.positionals.slice(1),
|
|
50
|
+
flags: args.flags,
|
|
51
|
+
bools: args.bools,
|
|
52
|
+
...(args.danglingValueFlags !== undefined
|
|
53
|
+
? { danglingValueFlags: args.danglingValueFlags }
|
|
54
|
+
: {}),
|
|
55
|
+
};
|
|
56
|
+
switch (verb) {
|
|
57
|
+
case "list":
|
|
58
|
+
return runList(sub);
|
|
59
|
+
case "get":
|
|
60
|
+
return runGet(sub);
|
|
61
|
+
case "upsert":
|
|
62
|
+
return runUpsert(sub);
|
|
63
|
+
case "update":
|
|
64
|
+
return runUpdate(sub);
|
|
65
|
+
case "delete":
|
|
66
|
+
return runDelete(sub);
|
|
67
|
+
case "watch":
|
|
68
|
+
return runWatch(sub);
|
|
69
|
+
default:
|
|
70
|
+
fail(`unknown verb '${verb}' — pane records <list|get|upsert|update|delete|watch>`, "invalid_args");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// list
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
async function runList(args) {
|
|
77
|
+
assertKnownFlags(args, ["since", "limit", "url", "api-key"], ["include-tombstones", "help"], "pane records list");
|
|
78
|
+
const paneId = args.positionals[0];
|
|
79
|
+
const collection = args.positionals[1];
|
|
80
|
+
if (!paneId || !collection) {
|
|
81
|
+
fail("usage: pane records list <pane-id> <collection>", "invalid_args");
|
|
82
|
+
}
|
|
83
|
+
const since = parseIntFlag(args, "since", 0);
|
|
84
|
+
const limit = parseIntFlag(args, "limit", undefined, { min: 1, max: 200 });
|
|
85
|
+
const includeTombstones = args.bools.has("include-tombstones");
|
|
86
|
+
const client = makeClient(args);
|
|
87
|
+
try {
|
|
88
|
+
const page = await client.listRecords(paneId, collection, {
|
|
89
|
+
since,
|
|
90
|
+
...(limit !== undefined ? { limit } : {}),
|
|
91
|
+
});
|
|
92
|
+
const records = includeTombstones
|
|
93
|
+
? page.records
|
|
94
|
+
: page.records.filter((r) => r.deleted_at === null);
|
|
95
|
+
printJson({
|
|
96
|
+
records,
|
|
97
|
+
next_since: page.next_since,
|
|
98
|
+
has_more: page.has_more,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
failFromError(e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// get — client-side scan via listRecords (no dedicated route today)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
async function runGet(args) {
|
|
109
|
+
assertKnownFlags(args, ["url", "api-key"], ["help"], "pane records get");
|
|
110
|
+
const [paneId, collection, recordKey] = args.positionals;
|
|
111
|
+
if (!paneId || !collection || !recordKey) {
|
|
112
|
+
fail("usage: pane records get <pane-id> <collection> <record-key>", "invalid_args");
|
|
113
|
+
}
|
|
114
|
+
const client = makeClient(args);
|
|
115
|
+
try {
|
|
116
|
+
const row = await client.getRecord(paneId, collection, recordKey);
|
|
117
|
+
if (!row) {
|
|
118
|
+
fail(`no record at key '${recordKey}' in collection '${collection}'`, "record_not_found");
|
|
119
|
+
}
|
|
120
|
+
printJson({ record: row });
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
failFromError(e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// upsert — create-or-return-existing
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
async function runUpsert(args) {
|
|
130
|
+
assertKnownFlags(args, ["data", "key", "url", "api-key"], ["help"], "pane records upsert");
|
|
131
|
+
const [paneId, collection] = args.positionals;
|
|
132
|
+
if (!paneId || !collection) {
|
|
133
|
+
fail("usage: pane records upsert <pane-id> <collection> --data <path|json>", "invalid_args");
|
|
134
|
+
}
|
|
135
|
+
const dataRaw = args.flags.get("data");
|
|
136
|
+
if (dataRaw === undefined) {
|
|
137
|
+
fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
|
|
138
|
+
}
|
|
139
|
+
const data = resolveJson(dataRaw, "--data");
|
|
140
|
+
const key = args.flags.get("key");
|
|
141
|
+
const client = makeClient(args);
|
|
142
|
+
try {
|
|
143
|
+
const body = { data };
|
|
144
|
+
if (key !== undefined)
|
|
145
|
+
body.record_key = key;
|
|
146
|
+
const out = await client.upsertRecord(paneId, collection, body);
|
|
147
|
+
printJson(out);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
failFromError(e);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// update — optimistic-lock mutate
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
async function runUpdate(args) {
|
|
157
|
+
assertKnownFlags(args, ["data", "if-match", "url", "api-key"], ["help"], "pane records update");
|
|
158
|
+
const [paneId, collection, recordKey] = args.positionals;
|
|
159
|
+
if (!paneId || !collection || !recordKey) {
|
|
160
|
+
fail("usage: pane records update <pane-id> <collection> <record-key> --data <path|json>", "invalid_args");
|
|
161
|
+
}
|
|
162
|
+
const dataRaw = args.flags.get("data");
|
|
163
|
+
if (dataRaw === undefined) {
|
|
164
|
+
fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
|
|
165
|
+
}
|
|
166
|
+
const data = resolveJson(dataRaw, "--data");
|
|
167
|
+
const ifMatch = parseIntFlag(args, "if-match", undefined, { min: 0 });
|
|
168
|
+
const client = makeClient(args);
|
|
169
|
+
try {
|
|
170
|
+
const body = { data };
|
|
171
|
+
if (ifMatch !== undefined)
|
|
172
|
+
body.if_match = ifMatch;
|
|
173
|
+
const out = await client.updateRecord(paneId, collection, recordKey, body);
|
|
174
|
+
printJson(out);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
failFromError(e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// delete — soft-delete
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
async function runDelete(args) {
|
|
184
|
+
assertKnownFlags(args, ["if-match", "url", "api-key"], ["yes", "help"], "pane records delete");
|
|
185
|
+
const [paneId, collection, recordKey] = args.positionals;
|
|
186
|
+
if (!paneId || !collection || !recordKey) {
|
|
187
|
+
fail("usage: pane records delete <pane-id> <collection> <record-key>", "invalid_args");
|
|
188
|
+
}
|
|
189
|
+
const ifMatch = parseIntFlag(args, "if-match", undefined, { min: 0 });
|
|
190
|
+
const client = makeClient(args);
|
|
191
|
+
try {
|
|
192
|
+
await client.deleteRecord(paneId, collection, recordKey, {
|
|
193
|
+
...(ifMatch !== undefined ? { ifMatch } : {}),
|
|
194
|
+
});
|
|
195
|
+
printJson({ deleted: true, key: recordKey });
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
failFromError(e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// watch — stream record deltas as JSON-lines
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
async function runWatch(args) {
|
|
205
|
+
// --collection is repeated-value; collected via danglingValueFlags + flags
|
|
206
|
+
assertKnownFlags(args, ["collection", "since-seq", "url", "api-key"], ["help"], "pane records watch");
|
|
207
|
+
const [paneId] = args.positionals;
|
|
208
|
+
if (!paneId) {
|
|
209
|
+
fail("usage: pane records watch <pane-id>", "invalid_args");
|
|
210
|
+
}
|
|
211
|
+
// --collection a,b,c (single comma list) OR repeated --collection foo flags.
|
|
212
|
+
// The shared argv parser uses a Map<string,string> for flags so a repeated
|
|
213
|
+
// flag last-write-wins. To support repeats here would need a parser
|
|
214
|
+
// extension; for now we accept a single comma list — the common case.
|
|
215
|
+
const collectionsRaw = args.flags.get("collection");
|
|
216
|
+
const subscribeRecords = collectionsRaw && collectionsRaw.length > 0 ? collectionsRaw : "*";
|
|
217
|
+
// --since-seq is a single comma list "name=N,name=M" for the same reason.
|
|
218
|
+
const sinceRaw = args.flags.get("since-seq");
|
|
219
|
+
const sinceRecordSeq = {};
|
|
220
|
+
if (sinceRaw) {
|
|
221
|
+
for (const part of sinceRaw.split(",")) {
|
|
222
|
+
const [name, vRaw] = part.split("=");
|
|
223
|
+
if (!name || vRaw === undefined) {
|
|
224
|
+
fail("--since-seq must be a comma list of name=N pairs", "invalid_args");
|
|
225
|
+
}
|
|
226
|
+
const n = Number(vRaw);
|
|
227
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
228
|
+
fail(`--since-seq ${name}: value must be a non-negative integer`, "invalid_args");
|
|
229
|
+
}
|
|
230
|
+
sinceRecordSeq[name] = n;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const cfg = resolveConfig(args);
|
|
234
|
+
const handle = openStream({
|
|
235
|
+
wsBaseUrl: cfg.url.replace(/^http/, "ws"),
|
|
236
|
+
paneId: paneId,
|
|
237
|
+
token: cfg.apiKey,
|
|
238
|
+
subscribeRecords,
|
|
239
|
+
sinceRecordSeq,
|
|
240
|
+
}, {
|
|
241
|
+
onRecord: (msg) => {
|
|
242
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
243
|
+
},
|
|
244
|
+
onRelayError: (err) => {
|
|
245
|
+
process.stderr.write(JSON.stringify({ error: err }) + "\n");
|
|
246
|
+
},
|
|
247
|
+
onError: (err) => {
|
|
248
|
+
process.stderr.write(JSON.stringify({
|
|
249
|
+
error: { code: "ws_error", message: err.message },
|
|
250
|
+
}) + "\n");
|
|
251
|
+
},
|
|
252
|
+
onClose: () => {
|
|
253
|
+
// Clean exit on close (e.g. SIGINT closed the socket). Emit nothing —
|
|
254
|
+
// the JSON-line stream is the contract; a trailing summary would
|
|
255
|
+
// confuse pipe readers.
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
// Hold the process open until SIGINT closes the stream.
|
|
259
|
+
process.on("SIGINT", () => {
|
|
260
|
+
handle.close();
|
|
261
|
+
process.exit(0);
|
|
262
|
+
});
|
|
263
|
+
await new Promise(() => {
|
|
264
|
+
/* never resolves — SIGINT exits */
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Helpers
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
function parseIntFlag(args, name, defaultValue, bounds = {}) {
|
|
271
|
+
const raw = args.flags.get(name);
|
|
272
|
+
if (raw === undefined)
|
|
273
|
+
return defaultValue;
|
|
274
|
+
const n = Number(raw);
|
|
275
|
+
if (!Number.isInteger(n)) {
|
|
276
|
+
fail(`--${name} must be an integer`, "invalid_args");
|
|
277
|
+
}
|
|
278
|
+
if (bounds.min !== undefined && n < bounds.min) {
|
|
279
|
+
fail(`--${name} must be >= ${bounds.min}`, "invalid_args");
|
|
280
|
+
}
|
|
281
|
+
if (bounds.max !== undefined && n > bounds.max) {
|
|
282
|
+
fail(`--${name} must be <= ${bounds.max}`, "invalid_args");
|
|
283
|
+
}
|
|
284
|
+
return n;
|
|
285
|
+
}
|