@vatio-ai/cli 0.53.0 → 0.54.0

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.
@@ -4,10 +4,13 @@
4
4
  // vatio.yml is a list of *names*, and a push neither fills nor empties one.
5
5
  // That is why this is a command and not part of the manifest.
6
6
  //
7
- // Every write goes to the base's draft, which preview answers from. Live
8
- // answers from the last published snapshot and changes only on `publish` --
9
- // or `rollback`, which points it at an earlier one. So: write, try it on
10
- // preview, publish.
7
+ // The entries are live. Reads and writes here are as one environment sees
8
+ // them: on a branch, the branch's own (see branch.mjs), which keeps the
9
+ // entries it changed on top of live and answers from them in its preview;
10
+ // otherwise `preview`; `--env live` writes live directly, the way a supervisor
11
+ // in the inbox does. `vatio publish`, or merging the branch's pull request,
12
+ // takes the changes live with the agent -- `vatio kb publish` takes them alone.
13
+ // So: write, try it on the branch's preview, publish.
11
14
  //
12
15
  // A base holds entries, and an entry is markdown. Either you wrote it, or a
13
16
  // site wrote it from a page it read — and a site is one URL or one pattern,
@@ -16,32 +19,48 @@
16
19
  import { readFileSync, statSync } from "node:fs";
17
20
 
18
21
  import { deployClient } from "./deploy.mjs";
19
- import { fail } from "../support.mjs";
22
+ import { fail, takeEnv, takeValue } from "../support.mjs";
23
+
24
+ // Only what reads or writes entries takes an environment; bases and sites
25
+ // belong to the workspace.
26
+ const ENVIRONMENT_SCOPED = new Set(["list", "show", "write", "cat", "rm-entry", "status", "publish"]);
20
27
 
21
28
  export async function kb(config, args) {
22
29
  const workspace = config.resolveWorkspaceRequired();
23
30
  const client = deployClient(config, workspace);
24
31
  const sub = args.shift() ?? "list";
32
+ const env = ENVIRONMENT_SCOPED.has(sub) ? takeEnv(args, { fallback: null, cwd: config.workspaceRoot }) : null;
33
+ const ctx = { client, workspace, env };
25
34
 
26
35
  switch (sub) {
27
- case "list": return await list(client, workspace);
28
- case "show": return await show(client, workspace, args.shift());
36
+ case "list": return await list(ctx);
37
+ case "show": return await show(ctx, args.shift());
29
38
  case "create": return await create(client, workspace, args.splice(0));
30
39
  case "rm": return await remove(client, workspace, args.shift());
31
- case "write": return await write(client, workspace, args);
32
- case "cat": return await cat(client, args.shift(), args.shift());
33
- case "rm-entry": return await removeEntry(client, workspace, args.shift(), args.shift());
40
+ case "write": return await write(ctx, args);
41
+ case "cat": return await cat(ctx, args);
42
+ case "rm-entry": return await removeEntry(ctx, args.shift(), args.shift());
34
43
  case "follow": return await follow(client, workspace, args.shift(), args.shift());
35
44
  case "unfollow": return await unfollow(client, workspace, args.shift(), args.shift());
36
45
  case "refresh": return await refresh(client, workspace, args.shift(), args.shift());
37
- case "status": return await status(client, workspace, args.shift());
38
- case "publish": return await publish(client, workspace, args);
46
+ case "status": return await status(ctx, args.shift());
47
+ case "publish": return await publish(ctx, args.shift());
39
48
  case "history": return await history(client, workspace, args.shift());
40
- case "rollback": return await rollback(client, workspace, args.shift(), args.shift());
49
+ case "rollback":
50
+ return fail(
51
+ "`vatio kb rollback` was removed. Restore one entry instead: `vatio kb history BASE` lists what " +
52
+ "reached live, `vatio kb cat BASE ENTRY --version N > old.md` prints one, and " +
53
+ "`vatio kb write BASE ENTRY old.md --env live` brings it back."
54
+ );
41
55
  default: fail(`Unknown kb subcommand: ${sub}\n\n${usage()}`);
42
56
  }
43
57
  }
44
58
 
59
+ // `preview` when the platform chose, for messages.
60
+ function envName(env) {
61
+ return env ?? "preview";
62
+ }
63
+
45
64
  function usage() {
46
65
  return `usage:
47
66
  vatio kb List knowledge bases
@@ -49,19 +68,21 @@ function usage() {
49
68
  vatio kb create NAME [NAME...] Create one base, or several
50
69
  vatio kb rm NAME Delete a base (must be unreferenced)
51
70
  vatio kb write BASE ENTRY [FILE] Write an entry, from FILE or stdin
52
- vatio kb cat BASE ENTRY Print an entry's markdown
71
+ vatio kb cat BASE ENTRY [--version N] Print an entry's markdown, or one of its versions
53
72
  vatio kb rm-entry BASE ENTRY Delete one entry
54
73
  vatio kb follow BASE URL Read a site into the base, nightly
55
74
  vatio kb unfollow BASE URL Stop reading it, and drop its entries
56
75
  vatio kb refresh BASE [URL] Read the sites again now
57
- vatio kb status BASE What the draft has that live does not
58
- vatio kb publish BASE [-m NOTE] Take the draft live
59
- vatio kb history BASE Every publication, newest first
60
- vatio kb rollback BASE NUMBER Point live back at publication NUMBER`;
76
+ vatio kb status BASE What this environment changed that live does not have
77
+ vatio kb publish BASE Take this environment's changes live, without the agent
78
+ vatio kb history BASE What reached live, newest first
79
+
80
+ Entry commands, status and publish take --env NAME; on a branch they default
81
+ to the branch's environment, otherwise to preview. --env live writes live.`;
61
82
  }
62
83
 
63
- async function list(client, workspace) {
64
- const bases = asArray((await client.knowledgeBases()).data);
84
+ async function list({ client, workspace, env }) {
85
+ const bases = asArray((await client.knowledgeBases({ environment: env })).data);
65
86
  if (bases.length === 0) {
66
87
  console.log(`No knowledge bases on workspace ${workspace}`);
67
88
  console.log("Create one with `vatio kb create docs`, then add `knowledge: [docs]` to vatio.yml");
@@ -70,9 +91,10 @@ async function list(client, workspace) {
70
91
 
71
92
  console.log(`Knowledge bases on workspace ${workspace} (${bases.length}):`);
72
93
  for (const row of bases) {
94
+ const pending = row.pending_changes ? ` ${row.pending_changes} unpublished in ${envName(env)}` : "";
73
95
  console.log(
74
96
  ` ${row.name} entries=${row.entries_count} ready=${row.answerable_count} ` +
75
- `sites=${row.sites_count}${referenceNote(row)}`
97
+ `sites=${row.sites_count}${pending}${referenceNote(row)}`
76
98
  );
77
99
  }
78
100
  }
@@ -84,15 +106,17 @@ function referenceNote(row) {
84
106
  return row.referenced ? "" : " (not referenced by any deployed agent)";
85
107
  }
86
108
 
87
- async function show(client, workspace, name) {
109
+ async function show({ client, workspace, env }, name) {
88
110
  if (!name) fail("usage: vatio kb show NAME");
89
111
 
90
- const base = await client.knowledgeBase(name);
112
+ const base = await client.knowledgeBase(name, { environment: env });
91
113
  console.log(
92
114
  `${base.name} on workspace ${workspace}: entries=${base.entries_count} ` +
93
115
  `ready=${base.answerable_count}${referenceNote(base)}`
94
116
  );
95
- console.log(` ${liveNote(base)}`);
117
+ if (base.pending_changes) {
118
+ console.log(` ${base.pending_changes} change(s) in ${envName(env)} that live does not have — \`vatio kb status ${base.name}\``);
119
+ }
96
120
 
97
121
  const sites = asArray(base.sites);
98
122
  if (sites.length > 0) {
@@ -116,23 +140,15 @@ async function show(client, workspace, name) {
116
140
  return;
117
141
  }
118
142
 
119
- console.log("\n entries:");
143
+ console.log(`\n entries, as ${envName(env)} sees them:`);
120
144
  for (const entry of entries) {
121
145
  const origin = entry.written ? "written here" : entry.url;
122
146
  const version = entry.version ? ` v${entry.version}` : "";
123
- console.log(` ${entry.name}${version} ${entry.state ?? "?"} ${origin}`);
147
+ const unpublished = entry.published === false ? " (not live yet)" : "";
148
+ console.log(` ${entry.name}${version} ${entry.state ?? "?"} ${origin}${unpublished}`);
124
149
  }
125
150
  }
126
151
 
127
- // Where live stands, and whether the draft has moved past it.
128
- function liveNote(base) {
129
- const live = base.live_snapshot ? `live=publication #${base.live_snapshot}` : "live=never published";
130
- const pending = base.pending_changes
131
- ? ` ${base.pending_changes} unpublished change${base.pending_changes === 1 ? "" : "s"} — \`vatio kb publish ${base.name}\``
132
- : "";
133
- return `${live}${pending}`;
134
- }
135
-
136
152
  // Several names, because the moment this command is most often run is a push
137
153
  // that just refused for naming bases that do not exist yet -- and that push
138
154
  // names all of them at once. One base per invocation turned a first deploy
@@ -177,7 +193,7 @@ async function remove(client, workspace, name) {
177
193
  // vatio kb cat docs horarios > horarios.md
178
194
  // $EDITOR horarios.md
179
195
  // vatio kb write docs horarios horarios.md
180
- async function write(client, workspace, args) {
196
+ async function write({ client, workspace, env }, args) {
181
197
  const [base, name, file] = args;
182
198
  if (!base || !name) fail("usage: vatio kb write BASE ENTRY [FILE] (or pipe the markdown in)");
183
199
  if (file && !isFile(file)) fail(`No such file: ${file}`);
@@ -187,27 +203,33 @@ async function write(client, workspace, args) {
187
203
  fail("Nothing to write: pass a FILE, or pipe the markdown in on stdin.");
188
204
  }
189
205
 
190
- const row = await client.writeKnowledgeEntry({ base, name, content });
206
+ const row = await client.writeKnowledgeEntry({ base, name, content, environment: env });
191
207
  const version = row.version ? ` (v${row.version})` : "";
192
- console.log(`Wrote ${row.name}${version} to the draft of ${base} on workspace ${workspace}`);
193
- console.log(`Preview answers with it now; \`vatio kb publish ${base}\` takes it live.`);
208
+ if (env === "live") {
209
+ console.log(`Wrote ${row.name}${version} to ${base} on workspace ${workspace}. Live answers with it now.`);
210
+ return;
211
+ }
212
+ console.log(`Wrote ${row.name}${version} to ${base} in ${envName(env)} on workspace ${workspace}`);
213
+ console.log(`${envName(env)} answers with it now; \`vatio publish\` takes it live with the agent.`);
194
214
  }
195
215
 
196
216
  // Prints exactly what the platform holds, so a diff against the file you
197
217
  // pushed is a real answer to "is what I sent what it has".
198
- async function cat(client, base, name) {
199
- if (!base || !name) fail("usage: vatio kb cat BASE ENTRY");
218
+ async function cat({ client, env }, args) {
219
+ const version = takeValue(args, "--version");
220
+ const [base, name] = args;
221
+ if (!base || !name) fail("usage: vatio kb cat BASE ENTRY [--version N]");
200
222
 
201
- const row = await client.knowledgeEntry({ base, name });
223
+ const row = await client.knowledgeEntry({ base, name, environment: env, version });
202
224
  const content = row.content ?? "";
203
225
  process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
204
226
  }
205
227
 
206
- async function removeEntry(client, workspace, base, name) {
228
+ async function removeEntry({ client, workspace, env }, base, name) {
207
229
  if (!base || !name) fail("usage: vatio kb rm-entry BASE ENTRY");
208
230
 
209
- await client.deleteKnowledgeEntry({ base, name });
210
- console.log(`Deleted entry ${name} from ${base} on workspace ${workspace}`);
231
+ await client.deleteKnowledgeEntry({ base, name, environment: env });
232
+ console.log(`Deleted entry ${name} from ${base} in ${envName(env)} on workspace ${workspace}`);
211
233
  }
212
234
 
213
235
  // One URL, and the shape of it is the whole configuration. A URL is that URL;
@@ -246,65 +268,61 @@ async function refresh(client, workspace, base, url) {
246
268
  console.log(`Runs in the background — check progress with \`vatio kb show ${base}\``);
247
269
  }
248
270
 
249
- // The draft against live, entry by entry -- what `publish` would do.
250
- async function status(client, workspace, base) {
271
+ // What this environment changed against live, entry by entry -- what
272
+ // publishing it would do, and which entries live changed underneath.
273
+ async function status({ client, workspace, env }, base) {
251
274
  if (!base) fail("usage: vatio kb status BASE");
252
275
 
253
- const changes = asArray((await client.knowledgeChanges(base)).data);
276
+ const payload = await client.knowledgeChanges(base, { environment: env });
277
+ const changes = asArray(payload.data);
254
278
  if (changes.length === 0) {
255
- console.log(`${base} on workspace ${workspace}: everything is published`);
279
+ console.log(`${base} in ${envName(env)} on workspace ${workspace}: nothing that live does not have`);
256
280
  return;
257
281
  }
258
282
 
259
- console.log(`${base} on workspace ${workspace}: ${changes.length} unpublished change${changes.length === 1 ? "" : "s"}`);
283
+ console.log(`${base} in ${envName(env)} on workspace ${workspace}: ${changes.length} change${changes.length === 1 ? "" : "s"} live does not have`);
260
284
  for (const change of changes) {
261
285
  const from = change.live_version ? `v${change.live_version}` : "—";
262
- const to = change.draft_version ? `v${change.draft_version}` : "—";
263
- console.log(` ${change.change.padEnd(8)} ${change.name} ${from} → ${to}`);
286
+ const to = change.version ?? change.draft_version;
287
+ const conflict = change.conflict ? " CONFLICT: live changed it after you did" : "";
288
+ console.log(` ${change.change.padEnd(8)} ${change.name} ${from} → v${to}${conflict}`);
289
+ }
290
+ if (changes.some((change) => change.conflict)) {
291
+ console.log(`\nRead each conflicting entry from live (\`vatio kb cat ${base} ENTRY --env live\`), write your change on top, and it clears.`);
292
+ if (payload.review_url) console.log(`Or resolve them side by side: ${payload.review_url}`);
293
+ } else {
294
+ console.log("\n`vatio publish` takes them live with the agent; `vatio kb publish` takes them alone.");
264
295
  }
265
- console.log(`\nPublish with \`vatio kb publish ${base} -m "what changed"\``);
266
296
  }
267
297
 
268
- async function publish(client, workspace, args) {
269
- const base = args.shift();
270
- if (!base) fail("usage: vatio kb publish BASE [-m NOTE]");
298
+ async function publish({ client, workspace, env }, base) {
299
+ if (!base) fail("usage: vatio kb publish BASE [--env NAME]");
271
300
 
272
- let note = "";
273
- const flag = args.findIndex((arg) => arg === "-m" || arg === "--note");
274
- if (flag !== -1) note = args[flag + 1] ?? "";
275
-
276
- const snapshot = await client.publishKnowledgeBase({ base, note });
277
- console.log(`Published ${base} on workspace ${workspace} as #${snapshot.number} (${snapshot.entries} entries)`);
278
- console.log(`Live answers from it now. Undo with \`vatio kb rollback ${base} NUMBER\` — see \`vatio kb history ${base}\`.`);
301
+ const result = await client.publishKnowledgeBase({ base, environment: env });
302
+ const names = asArray(result.published);
303
+ console.log(`Published ${names.length} change${names.length === 1 ? "" : "s"} to ${base} from ${envName(env)} on workspace ${workspace}: ${names.join(", ")}`);
304
+ console.log("Live answers from them now.");
279
305
  }
280
306
 
307
+ // Every version that reached live, newest first. Going back is writing one
308
+ // of them again.
281
309
  async function history(client, workspace, base) {
282
310
  if (!base) fail("usage: vatio kb history BASE");
283
311
 
284
- const snapshots = asArray((await client.knowledgeSnapshots(base)).data);
285
- if (snapshots.length === 0) {
286
- console.log(`${base} on workspace ${workspace} was never published — \`vatio kb publish ${base}\``);
312
+ const versions = asArray((await client.knowledgeHistory(base)).data);
313
+ if (versions.length === 0) {
314
+ console.log(`Nothing in ${base} on workspace ${workspace} has reached live yet`);
287
315
  return;
288
316
  }
289
317
 
290
- console.log(`Publications of ${base} on workspace ${workspace}:`);
291
- for (const snapshot of snapshots) {
292
- const live = snapshot.live ? " ← live" : "";
293
- const who = snapshot.author ? ` ${snapshot.author}` : "";
294
- const note = snapshot.note ? ` ${snapshot.note}` : "";
295
- console.log(` #${snapshot.number} ${snapshot.created_at}${who} entries=${snapshot.entries}${note}${live}`);
318
+ console.log(`What reached live in ${base} on workspace ${workspace}:`);
319
+ for (const row of versions) {
320
+ const who = row.author ? ` ${row.author}` : "";
321
+ const what = row.deleted ? " deleted" : "";
322
+ console.log(` ${row.published_at} ${row.name} v${row.version} ${row.source}${who}${what}`);
296
323
  }
297
324
  }
298
325
 
299
- // Only the pointer moves: the draft, and every later publication, stay.
300
- async function rollback(client, workspace, base, number) {
301
- if (!base || !number || !/^\d+$/.test(number)) fail("usage: vatio kb rollback BASE NUMBER");
302
-
303
- await client.rollbackKnowledgeBase({ base, number });
304
- console.log(`Live for ${base} on workspace ${workspace} is publication #${number} again`);
305
- console.log("The draft is untouched; publish again whenever it is ready.");
306
- }
307
-
308
326
  // Blocking on purpose: `vatio kb write docs refunds < refunds.md` should behave
309
327
  // like every other filter, and a terminal with nobody piping into it returns ""
310
328
  // rather than hanging on a read that will never end.
@@ -60,22 +60,31 @@ const COMMANDS = {
60
60
  push: {
61
61
  usage: "[--env NAME]",
62
62
  summary:
63
- "Validate the workspace and update a preview deployment. Never touches live. `--env NAME` " +
64
- "lands it on its own named preview (one per pull request) instead of the default one.",
63
+ "Validate the workspace and update a preview deployment. Never touches live. On a git branch " +
64
+ "other than the default one it lands on that branch's own environment (fix/pagos -> fix-pagos), " +
65
+ "the same one the branch's pull request deploys to; elsewhere on `preview`.",
65
66
  annotations: { readOnlyHint: false }
66
67
  },
67
68
  publish: {
68
69
  usage: "[--env NAME]",
69
70
  summary:
70
- "Promote the latest push to live (or the preview `--env` names), which is what customers " +
71
- "see. Ask the developer first: this is " +
72
- "the one step that changes what real visitors get. Refused when the workspace is connected " +
73
- "to GitHub: there, live changes only by merging to the default branch.",
71
+ "Promote an environment to live, with the knowledge changes made in it — what customers see. " +
72
+ "Ask the developer first: this is the one step that changes what real visitors get. Refused " +
73
+ "when the workspace is connected to GitHub: there, live changes only by merging the branch's " +
74
+ "pull request. Refused too when live changed an entry this environment also changed.",
74
75
  annotations: { readOnlyHint: false, destructiveHint: true }
75
76
  },
77
+ env: {
78
+ usage: "[NAME] | list | rm NAME",
79
+ summary:
80
+ "The environment this workspace is working in — the branch's own on a branch — with its " +
81
+ "publishable token, widget snippet, share link, the knowledge entries it changed and any that " +
82
+ "live changed underneath. Use its token to point an app being developed at this environment.",
83
+ annotations: { readOnlyHint: true }
84
+ },
76
85
  rollback: {
77
86
  usage: "",
78
- summary: "Restore the previous live deployment. Same weight as publish — ask first.",
87
+ summary: "Put live back to the previous snapshot — its configuration and its knowledge together. Same weight as publish — ask first.",
79
88
  annotations: { readOnlyHint: false, destructiveHint: true }
80
89
  },
81
90
  chat: {
@@ -88,21 +97,24 @@ const COMMANDS = {
88
97
  annotations: { readOnlyHint: false }
89
98
  },
90
99
  secrets: {
91
- usage: "list | set KEY VALUE | rm KEY",
100
+ usage: "list | set KEY VALUE | rm KEY [--env NAME]",
92
101
  summary:
93
- "Credentials the workspace's JS tools read as ctx.env. Values are write-only — listing shows " +
94
- "keys, never values.",
102
+ "Credentials the workspace's tools read as $env.KEY. Values are write-only — listing shows " +
103
+ "keys, never values. On a branch (or with --env) a value applies to that environment only, " +
104
+ "replacing the one everywhere else: point a branch's tools at staging without touching live.",
95
105
  annotations: { readOnlyHint: false }
96
106
  },
97
107
  kb: {
98
108
  usage:
99
- "[list] | show NAME | create NAME | write BASE ENTRY [FILE] | cat BASE ENTRY | rm-entry BASE ENTRY | " +
100
- "follow BASE URL | unfollow BASE URL | refresh BASE [URL] | status BASE | publish BASE [-m NOTE] | " +
101
- "history BASE | rollback BASE NUMBER",
109
+ "[list] | show NAME | create NAME | write BASE ENTRY [FILE] | cat BASE ENTRY [--version N] | " +
110
+ "rm-entry BASE ENTRY | follow BASE URL | unfollow BASE URL | refresh BASE [URL] | status BASE | " +
111
+ "publish BASE | history BASE (entry commands take --env NAME)",
102
112
  summary:
103
113
  "Knowledge bases, which belong to the workspace rather than to a deployment: `knowledge:` in " +
104
- "vatio.yml is a list of names, and a push neither fills nor empties one. Writes go to a draft that " +
105
- "preview answers from; live changes only on publish.",
114
+ "vatio.yml is a list of names, and a push neither fills nor empties one. The entries are live; " +
115
+ "writes on a branch are changes only that branch's preview answers from, and they go live with " +
116
+ "vatio_publish or the merge of its pull request. `status` names entries live changed underneath: " +
117
+ "read those from live (`cat --env live`), write your change on top, and the conflict clears.",
106
118
  annotations: { readOnlyHint: false, openWorldHint: true }
107
119
  },
108
120
  tokens: {
@@ -125,7 +137,7 @@ const COMMANDS = {
125
137
  summary:
126
138
  "The workspace's own WhatsApp number, which serves live: whether it is connected, whether Meta " +
127
139
  "delivers to it, and whether it is answering — three things that fail separately. `numbers` is " +
128
- "the other thing: the shared WhatsApp preview, test phones that reach preview and need no Meta " +
140
+ "the other thing: the shared WhatsApp preview, test phones that reach the environment they point at (`point PHONE --env NAME`) and need no Meta " +
129
141
  "account. A code goes to the handset, so a human has to read it back to you.",
130
142
  annotations: { readOnlyHint: false, openWorldHint: true }
131
143
  },
@@ -155,6 +167,7 @@ const WITHHELD = {
155
167
  const BLOCKED_SUBCOMMANDS = {
156
168
  chat: { destroy: "destroys a conversation; ask the developer" },
157
169
  kb: { rm: "deletes an indexed base; ask the developer" },
170
+ env: { rm: "removes an environment with its unpublished knowledge changes; ask the developer" },
158
171
  whatsapp: { connect: "takes Meta credentials the developer holds", disconnect: "ask the developer" },
159
172
  instagram: { connect: "needs Meta's consent screen in a browser", disconnect: "ask the developer" }
160
173
  };
@@ -218,8 +231,13 @@ function initializeResult(params) {
218
231
  "Start with vatio_docs: it is the contract for vatio.yml, tools, auth, visitor identity and",
219
232
  "knowledge, fetched live from the platform, so it is what the platform actually accepts.",
220
233
  "",
221
- "The loop is: edit files, vatio_push (preview only), vatio_chat to read the agent's answer,",
222
- "vatio_publish when the developer says so. Nothing a customer sees changes until publish.",
234
+ "Work on a git branch (a worktree is ideal): every command then uses that branch's own",
235
+ "environment — its agent, its knowledge changes, its secret values — and nothing you do there",
236
+ "reaches live or another branch. vatio_env shows which one and the token to point an app at it.",
237
+ "",
238
+ "The loop is: edit files and knowledge (vatio_kb write), vatio_push, vatio_chat to read the",
239
+ "agent's answer, then publish when the developer says so — by merging the branch's pull request",
240
+ "when GitHub is connected, vatio_publish otherwise. The prompt and the knowledge go live together.",
223
241
  "",
224
242
  "Anything that needs a browser or a person — login, issue, instagram connect — is not here;",
225
243
  "ask the developer to run it in their terminal."
@@ -6,63 +6,71 @@
6
6
  // will actually enforce, edited in the console. None of them are in vatio.yml.
7
7
 
8
8
  import { deployClient } from "./deploy.mjs";
9
- import { askSecret, fail, paint, takeValue } from "../support.mjs";
9
+ import { askSecret, branchDefault, fail, paint, takeValue } from "../support.mjs";
10
10
 
11
- // live, preview, or a named preview like pr-42. This used to accept only the
12
- // first two, which meant a workspace could deploy to `pr-42` and then have no
13
- // way to mint the publishable token that makes it embeddable.
11
+ // live, preview, or a branch's own like fix-pagos.
14
12
  const ENVIRONMENT_NAME = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
15
13
  const ENVIRONMENT_MAX = 40;
16
14
 
17
- function environmentOption(args, fallback) {
18
- const value =
19
- takeValue(args, "--env") ?? takeValue(args, "--environment") ?? takeValue(args, "-e") ?? fallback;
15
+ // On a branch, its environment unless told otherwise (see branch.mjs).
16
+ function environmentOption(args, fallback, cwd) {
17
+ const given = takeValue(args, "--env") ?? takeValue(args, "--environment") ?? takeValue(args, "-e");
18
+ const value = given ?? branchDefault(cwd, fallback);
19
+ if (value === null) return null;
20
20
  if (String(value).length > ENVIRONMENT_MAX || !ENVIRONMENT_NAME.test(String(value))) {
21
- fail(`Unknown environment "${value}" (use live, preview, or a preview name like pr-42)`);
21
+ fail(`Unknown environment "${value}" (use live, preview, or a branch's environment like fix-pagos)`);
22
22
  }
23
23
  return String(value);
24
24
  }
25
25
 
26
+ // Without an environment a secret is the value everywhere. On a branch, or
27
+ // with --env, it is the value only that environment uses -- a preview pointing
28
+ // its tools at staging while live keeps production.
26
29
  export async function secrets(config, args) {
27
30
  const workspace = config.resolveWorkspaceRequired();
28
31
  const client = deployClient(config, workspace);
32
+ const environment = environmentOption(args, null, config.workspaceRoot);
33
+ const where = environment ? `${environment} on workspace ${workspace}` : `workspace ${workspace}`;
29
34
  const sub = args.shift() ?? "list";
30
35
 
31
36
  if (sub === "list") {
32
- const payload = await client.listSecrets();
37
+ const payload = await client.listSecrets({ environment });
33
38
  const keys = asArray(payload.secrets);
34
39
  if (keys.length === 0) {
35
- console.log(`No secrets on workspace ${workspace}`);
40
+ console.log(`No secrets on ${where}`);
36
41
  console.log(" Set one: vatio secrets set STRIPE_KEY sk_live_…");
37
42
  return;
38
43
  }
39
44
  // Keys only, never values. The platform does not hand a value back once it
40
45
  // is stored, and a CLI that printed them would be a CLI that leaks them
41
46
  // into a terminal scrollback.
42
- console.log(`Secrets on workspace ${workspace} (${keys.length}):`);
43
- for (const secret of keys) console.log(` ${secret.key ?? secret}`);
47
+ console.log(`Secrets on ${where} (${keys.length}):`);
48
+ for (const secret of keys) {
49
+ const scope = secret.environment ? ` (only ${secret.environment})` : "";
50
+ console.log(` ${secret.key ?? secret}${scope}`);
51
+ }
44
52
  return;
45
53
  }
46
54
  if (sub === "set") {
47
55
  const key = args.shift();
48
- if (!key) fail("Usage: vatio secrets set KEY [VALUE]");
56
+ if (!key) fail("Usage: vatio secrets set KEY [VALUE] [--env NAME]");
49
57
  // Asked for rather than typed on the command line, where it would stay in
50
58
  // shell history.
51
59
  const value = args.shift() ?? (await askSecret(`${paint("green", "?")} Paste your secret: `));
52
60
  if (value === "") fail(`No value given for ${key}`);
53
- await client.upsertSecret({ key, value });
54
- console.log(`${paint("green", "✓")} Set secret ${key} on workspace ${workspace}`);
61
+ await client.upsertSecret({ key, value, environment });
62
+ console.log(`${paint("green", "✓")} Set secret ${key} on ${where}`);
55
63
  return;
56
64
  }
57
65
  if (sub === "rm" || sub === "remove" || sub === "delete") {
58
66
  const key = args.shift();
59
- if (!key) fail("Usage: vatio secrets rm KEY");
60
- await client.deleteSecret({ key });
61
- console.log(`Removed ${key} from workspace ${workspace}`);
67
+ if (!key) fail("Usage: vatio secrets rm KEY [--env NAME]");
68
+ await client.deleteSecret({ key, environment });
69
+ console.log(`Removed ${key} from ${where}`);
62
70
  return;
63
71
  }
64
72
 
65
- fail("Usage: vatio secrets list|set KEY [VALUE]|rm KEY");
73
+ fail("Usage: vatio secrets list|set KEY [VALUE]|rm KEY [--env NAME]");
66
74
  }
67
75
 
68
76
  // Repeatable: `--origin https://a.com --origin https://b.com`.
@@ -75,7 +83,7 @@ function takeOrigins(args) {
75
83
  }
76
84
 
77
85
  export async function tokens(config, args) {
78
- const environment = environmentOption(args, "live");
86
+ const environment = environmentOption(args, "live", config.workspaceRoot);
79
87
  const label = takeValue(args, "--label") ?? takeValue(args, "-l");
80
88
  const origins = takeOrigins(args);
81
89
  const workspace = config.resolveWorkspaceRequired();
@@ -139,7 +147,7 @@ function originsLine(origins) {
139
147
  }
140
148
 
141
149
  export async function widget(config, args) {
142
- const environment = environmentOption(args, "live");
150
+ const environment = environmentOption(args, "live", config.workspaceRoot);
143
151
  const workspace = config.resolveWorkspaceRequired();
144
152
  const payload = await deployClient(config, workspace).widget();
145
153
 
package/lib/help.mjs CHANGED
@@ -12,7 +12,10 @@ A workspace is any directory with a vatio.yml. Every command except init reads
12
12
  \`workspace:\` from the vatio.yml at or above the cwd — there is no --workspace
13
13
  flag, the directory decides.
14
14
 
15
- --env NAME targets a deployment: live, preview, or a preview name like pr-42.
15
+ --env NAME targets an environment: live, preview, or a branch's own. On a git
16
+ branch other than the default one, the branch's environment is the default
17
+ (fix/pagos -> fix-pagos): its own agent, knowledge changes and secret values,
18
+ and the one its pull request deploys to. Elsewhere commands default as listed.
16
19
 
17
20
  Getting started:
18
21
  init [SLUG] [--name NAME] Create vatio.yml here, and the remote to match
@@ -24,11 +27,15 @@ flag, the directory decides.
24
27
 
25
28
  Deploy:
26
29
  push [--env NAME] Validate the workspace, then update a preview
27
- publish [--env NAME] Promote the latest push to live. --env promotes
28
- a specific preview instead. Refused when GitHub
29
- is connected: merge to the default branch
30
- rollback Restore the previous live deployment
30
+ publish [--env NAME] Promote an environment to live, with its knowledge
31
+ changes. Without --env or a branch, the latest
32
+ push. Refused when GitHub is connected: merge
33
+ the branch's pull request instead
34
+ rollback Put live back to the previous snapshot: config and knowledge
31
35
  status Preview and live deployment state
36
+ env [NAME] This environment: token, widget snippet, link,
37
+ knowledge changes and conflicts
38
+ env list | rm NAME Every environment / remove one and what it holds
32
39
  diff [--env NAME] What this directory would change (default: preview)
33
40
  --env live answers "what would publishing change?"
34
41
  diff --stat|--name-only|--format json|--full
@@ -45,18 +52,23 @@ flag, the directory decides.
45
52
  kb [list] Bases, and whether a deployed agent reads each
46
53
  kb show NAME One base: its sites and its entries
47
54
  kb create NAME | rm NAME
48
- kb write BASE ENTRY [FILE] Write an entry, from FILE or stdin
49
- kb cat BASE ENTRY Print an entry's markdown
50
- kb rm-entry BASE ENTRY Delete one entry
55
+ kb write BASE ENTRY [FILE] Write an entry in this environment, from FILE or stdin
56
+ kb cat BASE ENTRY [--version N]
57
+ Print an entry's markdown, or one old version
58
+ kb rm-entry BASE ENTRY Delete one entry in this environment
51
59
  kb follow BASE URL Read a site into the base, nightly
52
60
  kb unfollow BASE URL | refresh BASE [URL]
53
- kb status BASE What the draft has that live does not
54
- kb publish BASE [-m NOTE] Take the draft live (preview already has it)
55
- kb history BASE | rollback BASE NUMBER
61
+ kb status BASE What this environment changed that live does not
62
+ have, and what live changed underneath
63
+ kb publish BASE Take this environment's changes live, alone
64
+ kb history BASE What reached live, newest first
65
+ (entry commands take --env; --env live writes live)
56
66
 
57
67
  Credentials and the widget:
58
- secrets list|set KEY [VALUE]|rm KEY
59
- Without VALUE, set asks for it with the input hidden
68
+ secrets list|set KEY [VALUE]|rm KEY [--env NAME]
69
+ Without VALUE, set asks for it with the input hidden.
70
+ On a branch or with --env, the value only that
71
+ environment uses; otherwise the value everywhere
60
72
  tokens list|create [--env NAME] [--label NAME] [--origin URL]...|revoke PREFIX
61
73
  tokens origins PREFIX URL...
62
74
  The origins a token is accepted from (replaces the list)
@@ -64,11 +76,13 @@ flag, the directory decides.
64
76
  the look is edited in the console
65
77
  auth --new-key Keypair that signs the JWT saying who a visitor is
66
78
 
67
- Channels (your own account serves live; the shared preview always serves preview):
79
+ Channels (your own account serves live; each test phone or account on the shared
80
+ preview reaches the environment it points at -- add and point take --env, and
81
+ default to the branch you are on):
68
82
  whatsapp [status]|connect|check|activate|deactivate|disconnect
69
- whatsapp numbers list|add PHONE|verify PHONE CODE|resend PHONE|remove PHONE
83
+ whatsapp numbers list|add PHONE|point PHONE|verify PHONE CODE|resend PHONE|remove PHONE
70
84
  instagram [status]|connect|check|disconnect
71
- instagram accounts list|add @HANDLE|verify CODE|resend ID|remove ID
85
+ instagram accounts list|add @HANDLE|point ID|verify CODE|resend ID|remove ID
72
86
 
73
87
  Everything else:
74
88
  issue "what should change"|list|show ID|comment ID "reply"|--template
package/lib/http.mjs CHANGED
@@ -37,7 +37,10 @@ function unprocessableMessage(body) {
37
37
  .map((entry) => (typeof entry === "object" ? String(entry.message ?? "") : String(entry)));
38
38
  if (errors.length > 0) return errors.join("; ");
39
39
 
40
- const message = body.error_message ?? body.error;
40
+ // `{ error: { code, message } }` is how the knowledge and token endpoints
41
+ // say it.
42
+ const error = body.error && typeof body.error === "object" && !Array.isArray(body.error) ? body.error.message : body.error;
43
+ const message = body.error_message ?? error;
41
44
  if (message != null) return Array.isArray(message) ? message.join("; ") : String(message);
42
45
 
43
46
  return "Unprocessable";