@vxil/cli 0.30.0 → 0.31.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.
package/README.md CHANGED
@@ -52,7 +52,7 @@ Verbs: `init templates quickstart try login link keys account logout invites mem
52
52
  - `vxil doctor` — preflight: secrets referenced-vs-stored (advisory, names only) plus a served / published / local hash verdict per function. Under `--json` stdout is one JSON document; notices go to stderr.
53
53
  - `vxil link <slug>` — reuses the key already stored for the slug on this machine; `--mint` for a fresh one, `--key` to hand one in.
54
54
  - `vxil keys mint --name <name> --scopes <a,b,c> [--env live|test] [--store]` — mint a **narrow** key for a server (scopes validated locally; `admin` refused); the key prints **once** to stdout (`--json` → `{ api_key, key_id, scopes }`) and is never stored unless `--store`. `vxil keys list` shows names, ids, scopes, created and last-used (never secrets); `vxil keys revoke <key_id> [--yes]` asks you to type the id, and refuses the key the CLI itself is using unless `--yes`. Needs a dashboard session (`vxil login`, or it prompts at a terminal; a piped / `--json` run fails with that remedy instead — pass `--email`/`--password` to script it, which never stores the session).
55
- - `vxil cms drop <collection> [--yes]` — remove one collection definition through the same guarded ack `push --allow-destructive` uses (rows are retained).
55
+ - `vxil cms drop <collection> [--purge-rows] [--yes]` — remove one collection definition through the same guarded ack `push --allow-destructive` uses (rows are retained, and a re-create brings them back; `--purge-rows` hard-deletes them too, and on a collection dropped earlier deletes its retained rows).
56
56
  - `vxil members list|invite|add|rm|uninvite` and `vxil invites accept <token|url>` — the bound project's team from the terminal (your `vxil login` session): invite by e-mail with a role, add an existing account (also how you change a role), remove (typed e-mail, refuses the last owner), revoke a pending invitation, accept one.
57
57
  - `vxil projects list|create|rm` and `vxil billing status|upgrade --tier <t>` — your projects and the bound project's plan. `projects rm` deletes any project you own after you type its slug; `billing upgrade` prints the hosted checkout URL (never opens a browser) and also downgrades (`--tier free`).
58
58
  - `vxil files put <path> --user <id>`, `vxil webhooks sources add|list|rm|test`, `vxil webhooks events replay|runs`, `vxil cms bulk-rm <collection> --filter '<json>' [--dry-run]`, `vxil search sync run|reset <key> [--purge]`, `vxil users import <file>` — the operator actions where the CLI adds something a raw call cannot (bytes from disk, a print-once receiver URL, a dry run before a typed confirm, a chunked bulk). Everything else the dashboard does per feature is one `vxil api` line: `vxil api --help`.
@@ -685,6 +685,11 @@ export declare const DeclaredAiTemplateSchema: import("@sinclair/typebox").TObje
685
685
  user: import("@sinclair/typebox").TString;
686
686
  system: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
687
687
  schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
688
+ /** MX-1023-MEDIA-RESOLUTION: this template's Gemini media resolution (sent
689
+ * to ai-v1 as `media_resolution`). A request's media_resolution wins; absent
690
+ * falls back to defaults.mediaResolution. Part of the content hash only
691
+ * when set, so an existing declared template is not re-versioned. */
692
+ mediaResolution: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"low">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"high">]>>;
688
693
  }>;
689
694
  export type DeclaredAiTemplate = Static<typeof DeclaredAiTemplateSchema>;
690
695
  export declare const AiConfigSchema: import("@sinclair/typebox").TObject<{
@@ -723,6 +728,11 @@ export declare const AiConfigSchema: import("@sinclair/typebox").TObject<{
723
728
  user: import("@sinclair/typebox").TString;
724
729
  system: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
725
730
  schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
731
+ /** MX-1023-MEDIA-RESOLUTION: this template's Gemini media resolution (sent
732
+ * to ai-v1 as `media_resolution`). A request's media_resolution wins; absent
733
+ * falls back to defaults.mediaResolution. Part of the content hash only
734
+ * when set, so an existing declared template is not re-versioned. */
735
+ mediaResolution: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"low">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"high">]>>;
726
736
  }>>>;
727
737
  testMarkers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
728
738
  }>;
package/dist/vxil-main.js CHANGED
@@ -4717,7 +4717,12 @@ var DeclaredAiTemplateSchema = Type.Object({
4717
4717
  template: Type.String({ minLength: 1, maxLength: 128, pattern: AI_TEMPLATE_NAME_PATTERN }),
4718
4718
  user: Type.String({ minLength: 1, maxLength: 64e3 }),
4719
4719
  system: Type.Optional(Type.String({ maxLength: 32e3 })),
4720
- schema: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
4720
+ schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
4721
+ /** MX-1023-MEDIA-RESOLUTION: this template's Gemini media resolution (sent
4722
+ * to ai-v1 as `media_resolution`). A request's media_resolution wins; absent
4723
+ * falls back to defaults.mediaResolution. Part of the content hash only
4724
+ * when set, so an existing declared template is not re-versioned. */
4725
+ mediaResolution: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")]))
4721
4726
  });
4722
4727
  var AiConfigSchema = Type.Object({
4723
4728
  enabled: Type.Boolean({ default: true }),
@@ -4802,8 +4807,9 @@ var AiConfigSchema = Type.Object({
4802
4807
  // aiTemplateContentSha256) against the `content_sha256` GET /v1/ai/templates
4803
4808
  // stamps on the latest version, and POSTs a new version ONLY when the content
4804
4809
  // differs — so a push is idempotent and versions stay monotonic per name.
4805
- // Item shape mirrors ai-v1 core.ts TemplateBody exactly (`template` is the
4806
- // name). Stored templates the config does not declare are reported and left
4810
+ // Item shape mirrors ai-v1 core.ts TemplateBody (`template` is the name;
4811
+ // `mediaResolution` is sent as the body's `media_resolution`).
4812
+ // Stored templates the config does not declare are reported and left
4807
4813
  // in place — RETIRED (soft: hidden from list + render, history kept) only
4808
4814
  // under --allow-destructive (2026-10-01). Bounded to 50 entries: the
4809
4815
  // manifest rides the 1 MiB config body cap. An Optional ARRAY is ONE leaf.
@@ -5666,9 +5672,9 @@ function lowerAllTriggerBindings(def, name) {
5666
5672
  return bindings;
5667
5673
  }
5668
5674
  var CONFIG_FILENAMES = ["vxil.config.ts", "vxil.config.mjs", "vxil.config.js"];
5669
- var VXIL_CONFIG_PKG_VERSION = "0.22.0";
5670
- var VXIL_SDK_PKG_VERSION = "0.29.0";
5671
- var VXIL_CLI_PKG_VERSION = "0.30.0";
5675
+ var VXIL_CONFIG_PKG_VERSION = "0.23.0";
5676
+ var VXIL_SDK_PKG_VERSION = "0.30.0";
5677
+ var VXIL_CLI_PKG_VERSION = "0.31.0";
5672
5678
  function ensureScaffoldPackageJson(cwd, opts = {}) {
5673
5679
  const file = resolve(cwd, "package.json");
5674
5680
  const wanted = {
@@ -8569,6 +8575,7 @@ async function aiTemplateContentSha256(t) {
8569
8575
  return Object.fromEntries(Object.keys(o).sort().map((k) => [k, canon(o[k], depth + 1)]));
8570
8576
  };
8571
8577
  return sha256Hex(JSON.stringify({
8578
+ ...t.mediaResolution ? { media_resolution: t.mediaResolution } : {},
8572
8579
  schema: canon(t.schema ?? null, 1),
8573
8580
  system: t.system ?? null,
8574
8581
  user: t.user
@@ -9014,7 +9021,7 @@ async function readOnlyPlan(api, block, allowDestructive) {
9014
9021
  } else {
9015
9022
  const hashed = await Promise.all(manifest.templates.map(async (t) => ({
9016
9023
  template: t.template,
9017
- content_sha256: await aiTemplateContentSha256({ system: t.system ?? null, user: t.user, schema: t.schema ?? null })
9024
+ content_sha256: await aiTemplateContentSha256({ system: t.system ?? null, user: t.user, schema: t.schema ?? null, mediaResolution: t.mediaResolution ?? null })
9018
9025
  })));
9019
9026
  const plan = planAiTemplates(hashed, listOf(data2.templates));
9020
9027
  s.created.push(...plan.create);
@@ -10396,14 +10403,43 @@ async function dropCollection(api, collection) {
10396
10403
  throw new Error(`cms: drop collection ${collection} failed: ${e.code ?? res.status} ${e.message ?? ""}`);
10397
10404
  }
10398
10405
  }
10399
- function formatCollectionDrop(collection, plan, declaredLocally) {
10406
+ async function dropCollectionPurgingRows(api, collection) {
10407
+ let purgedRows = 0;
10408
+ for (let round = 0; ; round++) {
10409
+ if (round > 0) {
10410
+ const read = await readRemote2(api);
10411
+ if (read.unavailable) {
10412
+ throw new Error(`cms: purge of ${collection} stopped after ${purgedRows} purged row(s): cannot read ${read.unavailable.route}`);
10413
+ }
10414
+ if (read.map.has(collection)) {
10415
+ throw new Error(`cms: purge of ${collection} stopped after ${purgedRows} purged row(s): the collection was re-created meanwhile \u2014 its rows are kept`);
10416
+ }
10417
+ }
10418
+ const res = await api("POST", "/v1/cms/collections", { drop_collection: collection, allow_destructive: true, purge_rows: true });
10419
+ if (res.body.error?.code === "not_found") return { purgedRows };
10420
+ if (res.status < 200 || res.status >= 300) {
10421
+ const e = res.body.error ?? {};
10422
+ throw new Error(`cms: drop collection ${collection} failed after ${purgedRows} purged row(s): ${e.code ?? res.status} ${e.message ?? ""}`);
10423
+ }
10424
+ const d = res.body.data ?? {};
10425
+ const n = typeof d.purged_rows === "number" ? d.purged_rows : 0;
10426
+ purgedRows += n;
10427
+ if (d.purge_complete === true) return { purgedRows };
10428
+ if (n === 0) throw new Error(`cms: purge of ${collection} made no progress after ${purgedRows} purged row(s) \u2014 re-run \`vxil cms drop ${collection} --purge-rows\``);
10429
+ }
10430
+ }
10431
+ function formatCollectionDrop(collection, plan, declaredLocally, purgeRows = false) {
10400
10432
  const rows = plan.rows === null ? "an unknown number of live rows" : `${plan.rows} live row(s)`;
10401
- const lines = [
10402
- ` ! drop collection ${collection} (${plan.fields} field(s), ${rows}) \u2014 the definition is removed; stored item rows are NOT deleted (they become unreachable through the API until a collection of that name exists again)`
10433
+ const lines = !plan.exists && purgeRows ? [` ! collection ${collection} has no definition \u2014 its retained rows from an earlier drop (live and deleted) are HARD-DELETED; they cannot be restored`] : [
10434
+ ` ! drop collection ${collection} (${plan.fields} field(s), ${rows}) \u2014 the definition is removed; ` + (purgeRows ? "its stored item rows (live and deleted) are HARD-DELETED with it; they cannot be restored" : "stored item rows are NOT deleted (they become unreachable through the API until a collection of that name exists again)")
10403
10435
  ];
10404
10436
  if (declaredLocally) {
10405
- const back = plan.rows === null ? "the retained rows" : `the ${plan.rows} retained row(s)`;
10406
- lines.push(` \u26A0 '${collection}' is still declared in vxil.config.ts \u2014 the next \`vxil push\` re-creates it and ${back} come back with it; remove it from the config too`);
10437
+ if (purgeRows) {
10438
+ lines.push(` \u26A0 '${collection}' is still declared in vxil.config.ts \u2014 the next \`vxil push\` re-creates it empty; remove it from the config too`);
10439
+ } else {
10440
+ const back = plan.rows === null ? "the retained rows" : `the ${plan.rows} retained row(s)`;
10441
+ lines.push(` \u26A0 '${collection}' is still declared in vxil.config.ts \u2014 the next \`vxil push\` re-creates it and ${back} come back with it; remove it from the config too`);
10442
+ }
10407
10443
  }
10408
10444
  return lines.join("\n");
10409
10445
  }
@@ -14914,7 +14950,7 @@ var TOOLS = [
14914
14950
  media_resolution: {
14915
14951
  type: "string",
14916
14952
  enum: ["low", "medium", "high"],
14917
- description: "Gemini token budget per image or PDF page (ignored by other providers): lower is faster and bills fewer input tokens. Omitted: the tenant's defaults.mediaResolution, then the model default."
14953
+ description: "Gemini token budget per image or PDF page (ignored by other providers): lower is faster and bills fewer input tokens. Omitted: the stored template's own level (when template is set), then the tenant's defaults.mediaResolution, then the model default."
14918
14954
  },
14919
14955
  mode: {
14920
14956
  type: "string",
@@ -16932,7 +16968,7 @@ var VERB_USAGE = {
16932
16968
  import: "usage: vxil import <feature> --file <bundle.json|export.ndjson> [--mode merge|replace] [--allow-split-links]",
16933
16969
  migrate: "usage: vxil migrate plan|schema|data|verify|payments \u2026 (run `vxil migrate --help`)",
16934
16970
  functions: "usage: vxil functions new <name> [--force]|deploy [<name>] [--force]|list|delete|enable|disable [--yes]|invoke ['<json>' | --data '<json>'] [--async]|logs [--tail]|dev <name> [--port <n>] [--trigger <lane>] [--event <file.json>]",
16935
- cms: "usage: vxil cms status|pull|reindex <collection> [--field <field>]|drop <collection> [--yes]|bulk-rm <collection> --filter '<json>' [--limit <1..100>] [--dry-run] [--yes]",
16971
+ cms: "usage: vxil cms status|pull|reindex <collection> [--field <field>]|drop <collection> [--purge-rows] [--yes]|bulk-rm <collection> --filter '<json>' [--limit <1..100>] [--dry-run] [--yes]",
16936
16972
  payments: "usage: vxil payments simulate --scenario <refund-pair|cross-platform-unlock|renewal|expiry|past-due-grace|transfer> --user <id> [--tier <t>] [--dev] \u2014 mock/dev projects only",
16937
16973
  dev: "usage: vxil dev up [--ttl <h>] [--new] [--name <n>] [--reuse-slug <slug>] [--email <e> --password <p>] [--invite <code>] | down [--yes] | seed | reset | branch [<name>] [--ttl <h=24>] [--no-env] [--print] | --list | --rm <name> [--yes] | --watch [--target <name>] \u2014 dev up signs in with --email/--password (or VXIL_DEV_EMAIL/VXIL_DEV_PASSWORD, + VXIL_DEV_INVITE), else your `vxil login` session, else a prompt",
16938
16974
  config: "usage: vxil config get <feature> \xB7 vxil config draft show|save [--file <f>]|publish|discard <feature> \xB7 vxil config draft duplicate <feature> --from <version>",
@@ -17052,6 +17088,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17052
17088
  "check",
17053
17089
  "all-projects",
17054
17090
  "allow-destructive",
17091
+ "purge-rows",
17055
17092
  "exit-code",
17056
17093
  "show-runs",
17057
17094
  "skip-functions",
@@ -26134,25 +26171,35 @@ export default defineConfig(${JSON.stringify({ features: featBlocks, cms: { coll
26134
26171
  const [sub] = positional(0);
26135
26172
  if (sub === "drop") {
26136
26173
  const [collection] = positional(1);
26137
- if (!collection) fail("usage: vxil cms drop <collection> [--yes]");
26174
+ if (!collection) fail("usage: vxil cms drop <collection> [--purge-rows] [--yes]");
26175
+ const purgeRows = hasFlag("purge-rows");
26138
26176
  const { api: api2, target } = requireApi(selector(), { write: `cms drop ${collection}` });
26139
26177
  const plan = await planCollectionDrop(api2, collection);
26140
26178
  if (plan.remoteUnavailable) {
26141
26179
  fail(`cannot read ${plan.remoteUnavailable.route} (${plan.remoteUnavailable.code ?? plan.remoteUnavailable.status}) \u2014 refusing to drop against an unread target`);
26142
26180
  }
26143
- if (!plan.exists) fail(`collection '${collection}' does not exist on tenant '${target.tenantSlug ?? "(env key)"}' \u2014 nothing to drop`);
26181
+ if (!plan.exists && !purgeRows) fail(`collection '${collection}' does not exist on tenant '${target.tenantSlug ?? "(env key)"}' \u2014 nothing to drop (\`--purge-rows\` deletes the rows an earlier drop retained)`);
26144
26182
  let declaredLocally = false;
26145
26183
  try {
26146
26184
  const cfg = await loadVxilConfig();
26147
26185
  declaredLocally = Object.prototype.hasOwnProperty.call(cfg.cms?.collections ?? {}, collection);
26148
26186
  } catch {
26149
26187
  }
26150
- console.error(formatCollectionDrop(collection, plan, declaredLocally));
26188
+ console.error(formatCollectionDrop(collection, plan, declaredLocally, purgeRows));
26151
26189
  if (!hasFlag("yes")) {
26152
26190
  if (!process.stdin.isTTY) fail("cms drop is destructive \u2014 pass --yes to confirm in a non-interactive run");
26153
- const ans = await prompt(`drop collection '${collection}'? type the collection name to confirm: `);
26191
+ const ans = await prompt(`${purgeRows ? "drop collection and hard-delete the rows of" : "drop collection"} '${collection}'? type the collection name to confirm: `);
26154
26192
  if (ans !== collection) fail("confirmation did not match \u2014 aborted.");
26155
26193
  }
26194
+ if (purgeRows) {
26195
+ const { purgedRows } = await dropCollectionPurgingRows(api2, collection);
26196
+ if (jsonOut) {
26197
+ console.log(JSON.stringify({ ok: true, collection, dropped: plan.exists, fields: plan.fields, purged_rows: purgedRows, declared_locally: declaredLocally }));
26198
+ } else {
26199
+ console.log(`\u2713 ${plan.exists ? `dropped collection ${collection} (${plan.fields} field(s)) and ` : `${collection}: `}hard-deleted ${purgedRows} stored row(s)`);
26200
+ }
26201
+ break;
26202
+ }
26156
26203
  await dropCollection(api2, collection);
26157
26204
  if (jsonOut) {
26158
26205
  console.log(JSON.stringify({ ok: true, collection, fields: plan.fields, rows: plan.rows, declared_locally: declaredLocally }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/cli",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "private": false,
5
5
  "description": "The vxil CLI — init, quickstart, push, gen, secrets, keys, migrate, doctor, diff, functions, payments; installs the `vxil` command (npm i -g @vxil/cli).",
6
6
  "license": "MIT",
@@ -45,8 +45,8 @@
45
45
  }
46
46
  },
47
47
  "devDependencies": {
48
- "@vxil/config": "0.22.0",
49
- "@vxil/feature-configs": "0.20.0",
48
+ "@vxil/config": "0.23.0",
49
+ "@vxil/feature-configs": "0.21.0",
50
50
  "@vxil/runtime": "0.0.1",
51
51
  "@vxil/types": "0.0.1",
52
52
  "miniflare": "^4.20260730.0"