@paneui/cli 0.0.9 → 0.0.10

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.
@@ -3,13 +3,13 @@
3
3
  // This is the one command that needs no API key: it is the call that obtains
4
4
  // one. If the relay runs REGISTRATION_MODE=secret, pass the shared
5
5
  // registration secret via --secret or PANE_REGISTER_SECRET. On success the key
6
- // (and relay URL) are persisted to the CLI config file, so every later command
7
- // works with only PANE_URL (or nothing) set.
6
+ // (and relay URL) are persisted under a named profile in the CLI config file,
7
+ // so every later command works with only PANE_URL (or nothing) set.
8
8
  import { registerAgent, PaneApiError } from "@paneui/core";
9
9
  import { assertKnownFlags } from "../argv.js";
10
10
  import { DEFAULT_RELAY_URL } from "../config.js";
11
11
  import { printJson, fail, failUpgradeRequired } from "../output.js";
12
- import { readStore, writeStore } from "../store.js";
12
+ import { isValidProfileName, DEFAULT_PROFILE_NAME, readStore, resolveProfile, upsertProfile, } from "../store.js";
13
13
  import { VERSION } from "../version.js";
14
14
  const KNOWN_FLAGS = ["name", "secret"];
15
15
  const KNOWN_BOOLS = ["print-key"];
@@ -18,14 +18,24 @@ export const registerHelp = `pane agent register — register this agent with th
18
18
  Usage:
19
19
  pane agent register [options]
20
20
 
21
- Calls POST /v1/register, then saves the returned API key (and relay URL) to the
22
- CLI config file — so afterwards every other command works with only PANE_URL
23
- set (no PANE_API_KEY needed).
21
+ Calls POST /v1/register, then saves the returned API key (and relay URL) under
22
+ a named profile in the CLI config file — so afterwards every other command
23
+ works with only PANE_URL set (no PANE_API_KEY needed).
24
+
25
+ If --profile is omitted, the registered key goes under the currently-active
26
+ profile (or 'default' for a fresh install). Pass --profile <name> to keep
27
+ multiple environments (dev/staging/prod) side by side; switch between them
28
+ with 'pane config use <name>' or '--profile <name>' / PANE_PROFILE.
24
29
 
25
30
  Options:
26
- --name <n> Agent display name. The relay defaults it if omitted.
27
- --url <url> Relay base URL. Falls back to PANE_URL, then the config
28
- file, then the hosted Pane relay. Self-hosters set this.
31
+ --name <n> Agent display name on the relay. The relay defaults it
32
+ if omitted.
33
+ --profile <name> Local profile name to save under. Defaults to the active
34
+ profile, or 'default' on a fresh install. Letters,
35
+ digits, _ and -, up to 32 chars.
36
+ --url <url> Relay base URL. Falls back to PANE_URL, then the active
37
+ profile, then the hosted Pane relay. Self-hosters set
38
+ this.
29
39
  --secret <s> Registration secret, sent as a Bearer token. Only needed
30
40
  when the relay uses REGISTRATION_MODE=secret. Falls back
31
41
  to the PANE_REGISTER_SECRET env var.
@@ -34,16 +44,42 @@ Options:
34
44
  -h, --help Show this help.
35
45
 
36
46
  Output (stdout, JSON):
37
- { agent_id, key_prefix, saved_to } (+ api_key when --print-key is given)
47
+ { agent_id, key_prefix, profile, saved_to } (+ api_key when --print-key)
38
48
 
39
49
  The API key is saved to the CLI config file (mode 0600); it is not printed
40
50
  unless --print-key is passed.`;
41
51
  export async function runRegister(args) {
42
52
  assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane agent register");
53
+ // Profile selection for the WRITE side: --profile flag → PANE_PROFILE env
54
+ // → the store's current profile → DEFAULT_PROFILE_NAME ('default') for
55
+ // a fresh install. We deliberately don't fall through to "no profile, use
56
+ // a fresh name" — the agent needs to end up somewhere callable, and
57
+ // 'default' is a stable, predictable home.
58
+ const profileFlag = args.flags.get("profile") ?? process.env.PANE_PROFILE;
43
59
  const store = readStore();
60
+ const profileName = profileFlag !== undefined && profileFlag !== ""
61
+ ? profileFlag
62
+ : (store.currentProfile ?? DEFAULT_PROFILE_NAME);
63
+ if (!isValidProfileName(profileName)) {
64
+ fail(`invalid profile name '${profileName}' — letters, digits, _ and -, up to 32 chars`, "invalid_args");
65
+ }
66
+ // URL precedence for the relay we're registering against:
67
+ // --url flag > PANE_URL env > target-profile's existing url > default.
68
+ // The "target profile's url" path means re-running `pane agent register
69
+ // --profile dev` against a profile that already exists keeps hitting the
70
+ // same dev relay without retyping --url.
71
+ let activeUrl;
72
+ try {
73
+ const active = resolveProfile(store, profileFlag);
74
+ activeUrl = active?.profile.url;
75
+ }
76
+ catch {
77
+ // Selector didn't resolve — fine on register: we're about to create it.
78
+ activeUrl = undefined;
79
+ }
44
80
  const url = args.flags.get("url") ??
45
81
  process.env.PANE_URL ??
46
- store.url ??
82
+ activeUrl ??
47
83
  DEFAULT_RELAY_URL;
48
84
  const name = args.flags.get("name");
49
85
  const secret = args.flags.get("secret") ?? process.env.PANE_REGISTER_SECRET ?? undefined;
@@ -75,13 +111,15 @@ export async function runRegister(args) {
75
111
  }
76
112
  fail(e instanceof Error ? e.message : String(e), "internal");
77
113
  }
78
- const savedTo = writeStore({
79
- url: url.replace(/\/$/, ""),
80
- apiKey: result.api_key,
81
- });
114
+ // Save under the chosen profile. We pass setCurrent=true: the user just
115
+ // registered against this relay, so the only sensible follow-up is to
116
+ // start using it. The previous behaviour (one global URL+key) is exactly
117
+ // the single-profile case of this.
118
+ const savedTo = upsertProfile(profileName, { url: url.replace(/\/$/, ""), apiKey: result.api_key }, true);
82
119
  const out = {
83
120
  agent_id: result.agent_id,
84
121
  key_prefix: result.key_prefix,
122
+ profile: profileName,
85
123
  saved_to: savedTo,
86
124
  };
87
125
  if (args.bools.has("print-key")) {
@@ -1,4 +1,4 @@
1
- // `pane surface send <id>` — append an agent event to a surface.
1
+ // `pane send <id>` — append an agent event to a pane.
2
2
  import { readFileSync } from "node:fs";
3
3
  import { basename } from "node:path";
4
4
  import { assertKnownFlags } from "../argv.js";
@@ -13,23 +13,23 @@ const KNOWN_FLAGS = [
13
13
  "idempotency-key",
14
14
  ];
15
15
  const KNOWN_BOOLS = [];
16
- export const sendHelp = `pane surface send — emit an agent event into a surface
16
+ export const sendHelp = `pane send — emit an agent event into a pane
17
17
 
18
18
  Usage:
19
- pane surface send <surface-id> --type <event-type> --data <path|json> [options]
20
- pane surface send <surface-id> --type <event-type> --attachment <file-path> [options]
19
+ pane send <pane-id> --type <event-type> --data <path|json> [options]
20
+ pane send <pane-id> --type <event-type> --attachment <file-path> [options]
21
21
 
22
- POSTs an event to /v1/surfaces/:id/events. The event is stamped as authored by
22
+ POSTs an event to /v1/panes/:id/events. The event is stamped as authored by
23
23
  the agent (the relay derives identity from the API key — it cannot be spoofed).
24
24
 
25
25
  Required:
26
- --type <t> Event type. Must exist in the surface's event schema
26
+ --type <t> Event type. Must exist in the pane's event schema
27
27
  with the agent in its emittedBy list.
28
28
  --data <v> Event payload: a file path to a .json file, or inline
29
29
  JSON. Use --data 'null' or --data '{}' for no payload.
30
30
 
31
31
  ALTERNATIVE to --data:
32
- --attachment <path> One-shot: upload <path> as a surface-scope attachment, then
32
+ --attachment <path> One-shot: upload <path> as a pane-scope attachment, then
33
33
  send an event whose payload is the AttachmentRef. The event
34
34
  data is { attachment: <AttachmentRef> }; declare it in your event
35
35
  schema with \`format: pane-attachment-id\` on \`attachment.attachment_id\`.
@@ -44,10 +44,10 @@ Options:
44
44
  Output (stdout, JSON):
45
45
  { event, deduped }`;
46
46
  export async function runSend(args) {
47
- assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane surface send");
48
- const surfaceId = args.positionals[0];
49
- if (!surfaceId)
50
- fail("missing <surface-id>", "invalid_args");
47
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane send");
48
+ const paneId = args.positionals[0];
49
+ if (!paneId)
50
+ fail("missing <pane-id>", "invalid_args");
51
51
  const type = args.flags.get("type");
52
52
  if (!type)
53
53
  fail("missing --type", "invalid_args");
@@ -60,8 +60,8 @@ export async function runSend(args) {
60
60
  fail("missing --data or --attachment", "invalid_args");
61
61
  }
62
62
  const client = makeClient(args);
63
- // --attachment path: upload the file as a surface-scope attachment, then send an
64
- // event whose data is { attachment: <AttachmentRef> }. The surface's event schema
63
+ // --attachment path: upload the file as a pane-scope attachment, then send an
64
+ // event whose data is { attachment: <AttachmentRef> }. The pane's event schema
65
65
  // is expected to declare a attachment field with format: pane-attachment-id.
66
66
  if (blobPath !== undefined) {
67
67
  let bytes;
@@ -73,11 +73,11 @@ export async function runSend(args) {
73
73
  }
74
74
  try {
75
75
  const ref = await client.uploadBlob(bytes, {
76
- scope: "surface",
77
- surfaceId: surfaceId,
76
+ scope: "pane",
77
+ paneId: paneId,
78
78
  filename: basename(blobPath),
79
79
  });
80
- const res = await client.sendEvent(surfaceId, {
80
+ const res = await client.sendEvent(paneId, {
81
81
  type: type,
82
82
  data: { attachment: ref },
83
83
  causationId: args.flags.get("causation-id"),
@@ -98,7 +98,7 @@ export async function runSend(args) {
98
98
  fail(e instanceof Error ? e.message : String(e), "invalid_args");
99
99
  }
100
100
  try {
101
- const res = await client.sendEvent(surfaceId, {
101
+ const res = await client.sendEvent(paneId, {
102
102
  type: type,
103
103
  data,
104
104
  causationId: args.flags.get("causation-id"),
@@ -0,0 +1,92 @@
1
+ // `pane agent set-key <key>` — write a fresh API key into the CLI config
2
+ // file. The companion to the human-side rotation flow on /my-agents: after
3
+ // the human regenerates a key in the browser, this command lands it on
4
+ // the agent's machine without making them hand-edit ~/.config/pane/config.json.
5
+ //
6
+ // No relay round-trip: we trust the human-supplied key. The relay will
7
+ // reject it on the next call if it's wrong (401 invalid_api_key) — better
8
+ // than guessing here and adding a network hop for what's a local config
9
+ // write.
10
+ import { assertKnownFlags } from "../argv.js";
11
+ import { isValidProfileName, DEFAULT_PROFILE_NAME, readStore, resolveProfile, upsertProfile, } from "../store.js";
12
+ import { printJson, fail } from "../output.js";
13
+ const KNOWN_FLAGS = ["url"];
14
+ const KNOWN_BOOLS = [];
15
+ export const setKeyHelp = `pane agent set-key <api-key> — save a new API key to the local config
16
+
17
+ Usage:
18
+ pane agent set-key <api-key> [--url <url>] [--profile <name>]
19
+
20
+ After regenerating an agent's API key in the relay's My-agents UI, run
21
+ this on the agent's machine to land the new key in the CLI config file
22
+ (\${XDG_CONFIG_HOME:-~/.config}/pane/config.json, mode 0600). Every later
23
+ command then works with no PANE_API_KEY env var.
24
+
25
+ The key is saved under the ACTIVE profile (unless --profile picks a different
26
+ one). To add a brand-new profile by hand (e.g. for an out-of-band key from a
27
+ closed-registration relay), use 'pane config add'.
28
+
29
+ If you'd rather not touch the config file at all, set the new key as the
30
+ PANE_API_KEY env var on the agent process — both work.
31
+
32
+ Options:
33
+ --url <url> Also update the saved relay URL on the target profile.
34
+ Useful when pointing the agent at a different relay
35
+ alongside the key swap.
36
+ --profile <name> Target this profile instead of the active one. Created
37
+ if it doesn't exist.
38
+ -h, --help Show this help.
39
+
40
+ Output (stdout, JSON):
41
+ { saved_to, profile, key_prefix }
42
+
43
+ The key is never echoed back. To verify, run \`pane key list\` afterwards.`;
44
+ function keyPrefixOf(key) {
45
+ // Match the relay's keyPrefix() display width for "pane_" + 6 hex chars
46
+ // (11 total). Falls back to the first 8 chars for any unrecognised shape.
47
+ if (key.startsWith("pane_") && key.length >= 11)
48
+ return key.slice(0, 11);
49
+ return key.slice(0, 8);
50
+ }
51
+ export async function runSetKey(args) {
52
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane agent set-key");
53
+ const apiKey = args.positionals[0];
54
+ if (!apiKey) {
55
+ fail("missing api-key — usage: pane agent set-key <api-key>", "invalid_args");
56
+ }
57
+ if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
58
+ fail("api-key must be a non-empty string", "invalid_args");
59
+ }
60
+ // Best-effort shape check. The relay generates `pane_<32 hex>`; we don't
61
+ // reject other shapes outright (a future format change shouldn't strand
62
+ // older CLIs), but we warn on something obviously wrong like leading
63
+ // whitespace.
64
+ const trimmed = apiKey.trim();
65
+ if (trimmed !== apiKey) {
66
+ fail("api-key has surrounding whitespace — copy it without leading/trailing spaces", "invalid_args");
67
+ }
68
+ // Profile selection mirrors `pane agent register`: --profile flag →
69
+ // PANE_PROFILE env → store's current_profile → 'default'.
70
+ const profileFlag = args.flags.get("profile") ?? process.env.PANE_PROFILE;
71
+ const store = readStore();
72
+ const profileName = profileFlag !== undefined && profileFlag !== ""
73
+ ? profileFlag
74
+ : (store.currentProfile ?? DEFAULT_PROFILE_NAME);
75
+ if (!isValidProfileName(profileName)) {
76
+ fail(`invalid profile name '${profileName}' — letters, digits, _ and -, up to 32 chars`, "invalid_args");
77
+ }
78
+ const urlFlag = args.flags.get("url");
79
+ const patch = { apiKey };
80
+ if (urlFlag !== undefined)
81
+ patch.url = urlFlag;
82
+ const saved = upsertProfile(profileName, patch);
83
+ // Re-resolve so we report the prefix from the persisted value, not the
84
+ // argument — defensive against future write-side normalisation.
85
+ const after = readStore();
86
+ const reread = resolveProfile(after, profileName);
87
+ printJson({
88
+ saved_to: saved,
89
+ profile: profileName,
90
+ key_prefix: keyPrefixOf(reread?.profile.apiKey ?? apiKey),
91
+ });
92
+ }
@@ -78,7 +78,7 @@ async function failOnNon2xx(res, target) {
78
78
  if (res.ok)
79
79
  return;
80
80
  // 404 if the operator stripped the route, 5xx on a static-read failure.
81
- // Surface the body inline — it may carry a useful message.
81
+ // Pane the body inline — it may carry a useful message.
82
82
  const body = await res.text().catch(() => "");
83
83
  fail(`relay returned ${res.status} for ${target}${body ? ": " + body.slice(0, 200) : ""}`, "relay_error");
84
84
  }
@@ -1,22 +1,22 @@
1
- // `pane surface show <id>` — snapshot of a surface, optionally long-polled.
1
+ // `pane show <id>` — snapshot of a pane, optionally long-polled.
2
2
  import { assertKnownFlags } from "../argv.js";
3
3
  import { makeClient } from "../config.js";
4
4
  import { printJson, fail, failFromError } from "../output.js";
5
5
  const KNOWN_FLAGS = ["since", "wait"];
6
6
  const KNOWN_BOOLS = [];
7
- export const stateHelp = `pane surface show — show a surface's metadata and event log
7
+ export const stateHelp = `pane show — show a pane's metadata and event log
8
8
 
9
9
  Usage:
10
- pane surface show <surface-id> [options]
10
+ pane show <pane-id> [options]
11
11
 
12
- By default non-blocking: fetches surface metadata (GET /v1/surfaces/:id) plus
13
- the event log (GET /v1/surfaces/:id/events) and prints them together.
12
+ By default non-blocking: fetches pane metadata (GET /v1/panes/:id) plus
13
+ the event log (GET /v1/panes/:id/events) and prints them together.
14
14
 
15
15
  With --wait, blocks at the relay for up to <secs> if no new events are
16
16
  available since the cursor — returns as soon as something lands. Use this
17
17
  for headless polling agents that can't keep a WebSocket open (cron,
18
18
  FaaS, slow links): poll, then re-poll using next_cursor as --since on the
19
- next call. Compared to 'pane surface watch', it's higher latency per
19
+ next call. Compared to 'pane watch', it's higher latency per
20
20
  round-trip but no long-lived connection.
21
21
 
22
22
  Options:
@@ -34,10 +34,10 @@ Options:
34
34
  Output (stdout, JSON):
35
35
  { meta, events, next_cursor }`;
36
36
  export async function runState(args) {
37
- assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane surface show");
38
- const surfaceId = args.positionals[0];
39
- if (!surfaceId)
40
- fail("missing <surface-id>", "invalid_args");
37
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane show");
38
+ const paneId = args.positionals[0];
39
+ if (!paneId)
40
+ fail("missing <pane-id>", "invalid_args");
41
41
  const since = args.flags.get("since") ?? null;
42
42
  // --wait <secs>: hand the server the long-poll window. The relay caps
43
43
  // this at 30s; we pass the raw value and let the relay clamp (sending
@@ -54,8 +54,8 @@ export async function runState(args) {
54
54
  }
55
55
  const client = makeClient(args);
56
56
  try {
57
- const meta = await client.getSession(surfaceId);
58
- const page = await client.getEvents(surfaceId, {
57
+ const meta = await client.getPane(paneId);
58
+ const page = await client.getEvents(paneId, {
59
59
  since,
60
60
  ...(waitSeconds !== undefined ? { waitSeconds } : {}),
61
61
  });
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Taste notes are presentation preferences the agent has learned from human
5
5
  // feedback ("denser layout", "no rounded corners", "use a dark header") — the
6
- // kind of guidance that should outlive a single surface. The intended loop:
6
+ // kind of guidance that should outlive a single pane. The intended loop:
7
7
  //
8
8
  // 1. Before generating a pane template, run `pane taste get` and feed the
9
9
  // `taste` field into the prompt so prior preferences shape the output.
@@ -14,7 +14,7 @@
14
14
  // unbounded into noise).
15
15
  //
16
16
  // Keep taste notes about *presentation/UI taste only* — colours, density,
17
- // component preferences. Project context, todos, and per-surface state belong
17
+ // component preferences. Project context, todos, and per-pane state belong
18
18
  // somewhere else. Today the attachment is keyed by the agent's API key (per-agent);
19
19
  // when pane gains first-class humans, this may move to per-human.
20
20
  import { readFileSync } from "node:fs";
@@ -32,7 +32,7 @@ agent has picked up from human feedback ("denser table", "no rounded corners",
32
32
  "use a dark header"). Read them before generating a pane template so prior
33
33
  feedback shapes the output; rewrite them whenever the human gives new
34
34
  presentation feedback. Keep entries about UI/presentation taste only — not
35
- project context, todos, or surface state.
35
+ project context, todos, or pane state.
36
36
 
37
37
  Usage:
38
38
  pane taste <subcommand> [options]
@@ -0,0 +1,195 @@
1
+ // `pane template-records` — CRUD for template-level record collections.
2
+ //
3
+ // Owner-curated content scoped to a Template head, visible to every pane
4
+ // derived from any version of the template. Mirrors `pane records` (per-pane
5
+ // records) verb-for-verb; the only difference is the resource path goes
6
+ // `/templates/:id/...` instead of `/panes/:id/...`.
7
+ import { assertKnownFlags } from "../argv.js";
8
+ import { makeClient } from "../config.js";
9
+ import { fail, failFromError, printJson } from "../output.js";
10
+ import { resolveJson } from "../input.js";
11
+ export const templateRecordsHelp = `pane template-records — CRUD for template-level record collections
12
+
13
+ Template records are owner-curated shared content anchored to a Template head.
14
+ Every pane derived from any version of the template sees the same rows.
15
+ Declared via the template version's \`template_record_schema\`; writes are
16
+ owner-only (the template's agent + same-human-claimed agents). Page-side
17
+ reads use the in-iframe \`pane.template.records.*\` bridge (no HTTP).
18
+
19
+ Usage:
20
+ pane template-records <verb> [options]
21
+
22
+ Verbs:
23
+ list <template-id|slug> <collection>
24
+ [--since <seq>] [--limit <n>] [--include-tombstones]
25
+ get <template-id|slug> <collection> <record-key>
26
+ upsert <template-id|slug> <collection>
27
+ --data <path|json> [--key <record-key>]
28
+ update <template-id|slug> <collection> <record-key>
29
+ --data <path|json> [--if-match <version>]
30
+ delete <template-id|slug> <collection> <record-key>
31
+ [--if-match <version>] [--yes]
32
+
33
+ Output (stdout): single JSON object per command.
34
+ Errors on stderr: {"error":{"code","message"}} with non-zero exit.`;
35
+ export async function runTemplateRecords(args) {
36
+ const verb = args.positionals[0];
37
+ if ((verb === undefined || verb === "help") && args.bools.has("help")) {
38
+ process.stdout.write(templateRecordsHelp + "\n");
39
+ return;
40
+ }
41
+ if (verb === undefined) {
42
+ fail("missing verb — pane template-records <list|get|upsert|update|delete>", "invalid_args");
43
+ }
44
+ const sub = {
45
+ positionals: args.positionals.slice(1),
46
+ flags: args.flags,
47
+ bools: args.bools,
48
+ ...(args.danglingValueFlags !== undefined
49
+ ? { danglingValueFlags: args.danglingValueFlags }
50
+ : {}),
51
+ };
52
+ switch (verb) {
53
+ case "list":
54
+ return runList(sub);
55
+ case "get":
56
+ return runGet(sub);
57
+ case "upsert":
58
+ return runUpsert(sub);
59
+ case "update":
60
+ return runUpdate(sub);
61
+ case "delete":
62
+ return runDelete(sub);
63
+ default:
64
+ fail(`unknown verb '${verb}' — pane template-records <list|get|upsert|update|delete>`, "invalid_args");
65
+ }
66
+ }
67
+ async function runList(args) {
68
+ assertKnownFlags(args, ["since", "limit", "url", "api-key"], ["include-tombstones", "help"], "pane template-records list");
69
+ const templateId = args.positionals[0];
70
+ const collection = args.positionals[1];
71
+ if (!templateId || !collection) {
72
+ fail("usage: pane template-records list <template-id|slug> <collection>", "invalid_args");
73
+ }
74
+ const since = parseIntFlag(args, "since", 0);
75
+ const limit = parseIntFlag(args, "limit", undefined, { min: 1, max: 200 });
76
+ const includeTombstones = args.bools.has("include-tombstones");
77
+ const client = makeClient(args);
78
+ try {
79
+ const page = await client.listTemplateRecords(templateId, collection, {
80
+ since,
81
+ ...(limit !== undefined ? { limit } : {}),
82
+ });
83
+ const records = includeTombstones
84
+ ? page.records
85
+ : page.records.filter((r) => r.deleted_at === null);
86
+ printJson({
87
+ records,
88
+ next_since: page.next_since,
89
+ has_more: page.has_more,
90
+ });
91
+ }
92
+ catch (e) {
93
+ failFromError(e);
94
+ }
95
+ }
96
+ async function runGet(args) {
97
+ assertKnownFlags(args, ["url", "api-key"], ["help"], "pane template-records get");
98
+ const [templateId, collection, recordKey] = args.positionals;
99
+ if (!templateId || !collection || !recordKey) {
100
+ fail("usage: pane template-records get <template-id|slug> <collection> <record-key>", "invalid_args");
101
+ }
102
+ const client = makeClient(args);
103
+ try {
104
+ const row = await client.getTemplateRecord(templateId, collection, recordKey);
105
+ if (!row) {
106
+ fail(`no template record at key '${recordKey}' in collection '${collection}'`, "template_record_not_found");
107
+ }
108
+ printJson({ record: row });
109
+ }
110
+ catch (e) {
111
+ failFromError(e);
112
+ }
113
+ }
114
+ async function runUpsert(args) {
115
+ assertKnownFlags(args, ["data", "key", "url", "api-key"], ["help"], "pane template-records upsert");
116
+ const [templateId, collection] = args.positionals;
117
+ if (!templateId || !collection) {
118
+ fail("usage: pane template-records upsert <template-id|slug> <collection> --data <path|json>", "invalid_args");
119
+ }
120
+ const dataRaw = args.flags.get("data");
121
+ if (dataRaw === undefined) {
122
+ fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
123
+ }
124
+ const data = resolveJson(dataRaw, "--data");
125
+ const key = args.flags.get("key");
126
+ const client = makeClient(args);
127
+ try {
128
+ const body = { data };
129
+ if (key !== undefined)
130
+ body.record_key = key;
131
+ const out = await client.upsertTemplateRecord(templateId, collection, body);
132
+ printJson(out);
133
+ }
134
+ catch (e) {
135
+ failFromError(e);
136
+ }
137
+ }
138
+ async function runUpdate(args) {
139
+ assertKnownFlags(args, ["data", "if-match", "url", "api-key"], ["help"], "pane template-records update");
140
+ const [templateId, collection, recordKey] = args.positionals;
141
+ if (!templateId || !collection || !recordKey) {
142
+ fail("usage: pane template-records update <template-id|slug> <collection> <record-key> --data <path|json>", "invalid_args");
143
+ }
144
+ const dataRaw = args.flags.get("data");
145
+ if (dataRaw === undefined) {
146
+ fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
147
+ }
148
+ const data = resolveJson(dataRaw, "--data");
149
+ const ifMatch = parseIntFlag(args, "if-match", undefined, { min: 0 });
150
+ const client = makeClient(args);
151
+ try {
152
+ const body = { data };
153
+ if (ifMatch !== undefined)
154
+ body.if_match = ifMatch;
155
+ const out = await client.updateTemplateRecord(templateId, collection, recordKey, body);
156
+ printJson(out);
157
+ }
158
+ catch (e) {
159
+ failFromError(e);
160
+ }
161
+ }
162
+ async function runDelete(args) {
163
+ assertKnownFlags(args, ["if-match", "url", "api-key"], ["yes", "help"], "pane template-records delete");
164
+ const [templateId, collection, recordKey] = args.positionals;
165
+ if (!templateId || !collection || !recordKey) {
166
+ fail("usage: pane template-records delete <template-id|slug> <collection> <record-key>", "invalid_args");
167
+ }
168
+ const ifMatch = parseIntFlag(args, "if-match", undefined, { min: 0 });
169
+ const client = makeClient(args);
170
+ try {
171
+ await client.deleteTemplateRecord(templateId, collection, recordKey, {
172
+ ...(ifMatch !== undefined ? { ifMatch } : {}),
173
+ });
174
+ printJson({ deleted: true, key: recordKey });
175
+ }
176
+ catch (e) {
177
+ failFromError(e);
178
+ }
179
+ }
180
+ function parseIntFlag(args, name, defaultValue, bounds = {}) {
181
+ const raw = args.flags.get(name);
182
+ if (raw === undefined)
183
+ return defaultValue;
184
+ const n = Number(raw);
185
+ if (!Number.isInteger(n)) {
186
+ fail(`--${name} must be an integer`, "invalid_args");
187
+ }
188
+ if (bounds.min !== undefined && n < bounds.min) {
189
+ fail(`--${name} must be >= ${bounds.min}`, "invalid_args");
190
+ }
191
+ if (bounds.max !== undefined && n > bounds.max) {
192
+ fail(`--${name} must be <= ${bounds.max}`, "invalid_args");
193
+ }
194
+ return n;
195
+ }