@impetik/xeer-mcp 0.2.29 → 0.2.32

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
@@ -63,7 +63,7 @@ The generated registry is the complete MCP tool and exclusion surface:
63
63
  | `xeer_check` | `check` | `author` / `operator` | Validate a project and report structured diagnostics. | `directory?`: `string` | `read-source`<br>`write-generated` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
64
64
  | `xeer_build` | `build` | `author` / `operator` | Build and verify a content-addressed project artifact. | `directory?`: `string` | `read-source`<br>`write-generated`<br>`run-local` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
65
65
  | `xeer_test` | `test` | `author` / `operator` | Run the project test suite and record review evidence. | `directory?`: `string`<br>`timeoutMilliseconds?`: `integer` [1000..1800000] | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | none |
66
- | `xeer_new` | `new` | `author` / `operator` | Create a new project from a supported scaffold. | `directory`: `string`<br>`template?`: `string`<br>`framework?`: `sveltekit`<br>`ui?`: `preact` / `react` | `write-source`<br>`network-read` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
66
+ | `xeer_new` | `new` | `author` / `operator` | Create a new project from a supported scaffold. | `directory`: `string`<br>`template?`: `string`<br>`framework?`: `sveltekit` / `astro`<br>`ui?`: `preact` / `react` | `write-source`<br>`network-read` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
67
67
  | `xeer_agent_context` | `agent.context` | `author` / `operator` | Read normalized project facts, operations, diagnostics, tests, and safe next actions. | `directory?`: `string` | `read-source` | read-only; idempotent; reversible; non-destructive | `project-relative` | `xeer.agent-context.v0` | none |
68
68
  | `xeer_docs_search` | `docs.search` | `author` / `operator` | Search the installed-version Xeer documentation index. | `query`: `string`<br>`limit?`: `integer` [1..20]; default `5` | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.docs-search.v0` | none |
69
69
  | `xeer_doctor` | `doctor` | `author` / `operator` | Diagnose the toolchain and generated project state. | `directory?`: `string` | `read-source`<br>`write-generated` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
@@ -76,11 +76,12 @@ The generated registry is the complete MCP tool and exclusion surface:
76
76
  | `xeer_dev_stop` | `dev.stop` | `author` / `operator` | Stop a local development session and release its lease. | `sessionId?`: `string`<br>`cursor?`: `integer` [0..9007199254740991]; default `0` | `run-local` | writes; idempotent; reversible; non-destructive | `none` | `xeer.dev.v0` | none |
77
77
  | `xeer_diagnostics` | `diagnostics` | `author` / `operator` | Explain one emitted diagnostic code. | `code`: `string` | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | none |
78
78
 
79
- **CLI actions intentionally excluded from MCP (36).**
79
+ **CLI actions intentionally excluded from MCP (39).**
80
80
 
81
81
  | CLI action | Action | Summary | Effects | Safety | Path policy | Output | Why no MCP tool |
82
82
  | --- | --- | --- | --- | --- | --- | --- | --- |
83
- | `xeer init` | `init` | Initialize Xeer in an existing SvelteKit project. | `read-source`<br>`write-source`<br>`run-local`<br>`network-read` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
83
+ | `xeer init` | `init` | Initialize Xeer in an existing SvelteKit or Astro project. | `read-source`<br>`write-source`<br>`run-local`<br>`network-read` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
84
+ | `xeer bootstrap` | `bootstrap` | Open owner-only EmDash setup for a deployed Astro project. | `read-source`<br>`network-read`<br>`network-write` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
84
85
  | `xeer agent setup` | `agent.setup` | Install or verify project-confined agent adapters. | `read-source`<br>`write-source` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
85
86
  | `xeer deploy` | `deploy` | Build and deploy a project artifact. | `read-source`<br>`write-generated`<br>`run-local`<br>`network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | MCP exposes a separate preview-only deploy action; direct production deploy stays CLI-only. |
86
87
  | `xeer promote` | `promote` | Promote a preview artifact to production. | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | MCP promotion is a separate action available only when the server starts in operator profile. |
@@ -115,7 +116,9 @@ The generated registry is the complete MCP tool and exclusion surface:
115
116
  | `xeer actions` | `actions` | Print the machine-readable action and safety catalogue. | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
116
117
  | `xeer db tables` | `db.tables` | List the tables of a running application database. | `read-state` | read-only; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
117
118
  | `xeer db schema` | `db.schema` | Print the declared schema of a running application database. | `read-state` | read-only; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
118
- | `xeer db exec` | `db.exec` | Run one SQL statement against a running application database. | `read-state`<br>`write-state` | writes; non-idempotent; irreversible; non-destructive | `project-relative` | `xeer.command.v0` | Destructive state replacement requires an explicit CLI invocation. |
119
+ | `xeer db exec` | `db.exec` | Run SQL against a running or deployed application database. | `read-state`<br>`write-state`<br>`network-read`<br>`network-write` | writes; non-idempotent; irreversible; destructive | `project-relative` | `xeer.command.v0` | Destructive state replacement requires an explicit CLI invocation. |
120
+ | `xeer db bookmark` | `db.bookmark` | Print the current Time Travel bookmark of a deployed application database. | `network-read` | read-only; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
121
+ | `xeer db restore` | `db.restore` | Restore a deployed application database to a bookmark or a time. | `network-write`<br>`write-state` | writes; non-idempotent; reversible; destructive | `project-relative` | `xeer.command.v0` | Destructive state replacement requires an explicit CLI invocation. |
119
122
  <!-- xeer-action-reference:end -->
120
123
 
121
124
  Every tool returns `{ protocol: "xeer.mcp-result.v0", action, ok, result?, error? }` both as
@@ -25,6 +25,10 @@ export type DevSessionStatus = 'starting' | 'ready' | 'compile_failed' | 'stoppe
25
25
  */
26
26
  export interface PreviewUrls {
27
27
  url: string;
28
+ /** A framework project's framework, `sveltekit` or `astro`; absent for a Xeer application. */
29
+ framework?: string;
30
+ /** An EmDash project's local admin, on `localhost` because passkeys refuse an IP address. */
31
+ adminUrl?: string;
28
32
  healthUrl?: string;
29
33
  inspectorUrl?: string;
30
34
  debugUrl?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impetik/xeer-mcp",
3
- "version": "0.2.29",
3
+ "version": "0.2.32",
4
4
  "type": "module",
5
5
  "description": "Model Context Protocol server for Xeer project context, diagnostics, scaffold, check, test, dev, build, and preview deployment.",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
49
  "zod": "^4.0.10",
50
- "@impetik/xeer": "0.2.29"
50
+ "@impetik/xeer": "0.2.32"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^24.1.0"
@@ -194,7 +194,7 @@ declare const actions: readonly [{
194
194
  readonly command: readonly ["new"];
195
195
  readonly summary: "Create a new project from a supported scaffold.";
196
196
  readonly description: string;
197
- readonly usage: readonly ["new [directory] [--template <id>] [--framework sveltekit] [--ui preact|react] [--json]"];
197
+ readonly usage: readonly ["new [directory] [--template <id>] [--framework sveltekit|astro] [--ui preact|react] [--json]"];
198
198
  readonly helpOrder: 30;
199
199
  readonly outputProtocol: "xeer.command.v0";
200
200
  readonly effects: readonly ["write-source", "network-read"];
@@ -218,7 +218,7 @@ declare const actions: readonly [{
218
218
  readonly framework: {
219
219
  readonly description: string;
220
220
  readonly type: "string";
221
- readonly enum: readonly ["sveltekit"];
221
+ readonly enum: readonly ["sveltekit", "astro"];
222
222
  };
223
223
  readonly ui: {
224
224
  readonly description: string;
@@ -237,7 +237,7 @@ declare const actions: readonly [{
237
237
  }, {
238
238
  readonly id: "init";
239
239
  readonly command: readonly ["init"];
240
- readonly summary: "Initialize Xeer in an existing SvelteKit project.";
240
+ readonly summary: "Initialize Xeer in an existing SvelteKit or Astro project.";
241
241
  readonly description: string;
242
242
  readonly usage: readonly ["init [directory] [--dry-run] [--json]"];
243
243
  readonly helpOrder: 32;
@@ -252,6 +252,24 @@ declare const actions: readonly [{
252
252
  readonly cli: true;
253
253
  readonly mcpExclusion: "Not exposed through MCP v0; use the CLI deliberately.";
254
254
  };
255
+ }, {
256
+ readonly id: "bootstrap";
257
+ readonly command: readonly ["bootstrap"];
258
+ readonly summary: "Open owner-only EmDash setup for a deployed Astro project.";
259
+ readonly description: string;
260
+ readonly usage: readonly ["bootstrap [directory] [--environment prod|preview] [--control-url <url>] [--no-open] [--json]"];
261
+ readonly helpOrder: 51;
262
+ readonly outputProtocol: "xeer.command.v0";
263
+ readonly effects: readonly ["read-source", "network-read", "network-write"];
264
+ readonly idempotent: false;
265
+ readonly reversible: true;
266
+ readonly destructive: false;
267
+ readonly humanPrerequisites: readonly [string];
268
+ readonly pathPolicy: "project-relative";
269
+ readonly surfaces: {
270
+ readonly cli: true;
271
+ readonly mcpExclusion: "Not exposed through MCP v0; use the CLI deliberately.";
272
+ };
255
273
  }, {
256
274
  readonly id: "agent.setup";
257
275
  readonly command: readonly ["agent", "setup"];
@@ -1247,15 +1265,52 @@ declare const actions: readonly [{
1247
1265
  }, {
1248
1266
  readonly id: "db.exec";
1249
1267
  readonly command: readonly ["db", "exec"];
1250
- readonly summary: "Run one SQL statement against a running application database.";
1251
- readonly usage: readonly ["db exec [directory] <sql> [--state <dev|preview>] [--write] [--json]"];
1268
+ readonly summary: "Run SQL against a running or deployed application database.";
1269
+ readonly description: string;
1270
+ readonly usage: readonly ["db exec [directory] <sql> [--state <dev|preview>] [--write] [--json]", "db exec [directory] <sql> --remote --environment <prod|preview> [--param <value>]... [--control-url <url>] [--json]", "db exec [directory] --remote --environment <prod|preview> --file <batch.json> [--control-url <url>] [--json]"];
1252
1271
  readonly helpOrder: 340;
1253
1272
  readonly outputProtocol: "xeer.command.v0";
1254
- readonly effects: readonly ["read-state", "write-state"];
1273
+ readonly effects: readonly ["read-state", "write-state", "network-read", "network-write"];
1255
1274
  readonly idempotent: false;
1256
1275
  readonly reversible: false;
1276
+ readonly destructive: true;
1277
+ readonly humanPrerequisites: readonly ["Locally, a human must pass --write before any statement that modifies rows.", string];
1278
+ readonly pathPolicy: "project-relative";
1279
+ readonly surfaces: {
1280
+ readonly cli: true;
1281
+ readonly mcpExclusion: "Destructive state replacement requires an explicit CLI invocation.";
1282
+ };
1283
+ }, {
1284
+ readonly id: "db.bookmark";
1285
+ readonly command: readonly ["db", "bookmark"];
1286
+ readonly summary: "Print the current Time Travel bookmark of a deployed application database.";
1287
+ readonly description: string;
1288
+ readonly usage: readonly ["db bookmark [directory] --remote --environment <prod|preview> [--control-url <url>] [--json]"];
1289
+ readonly helpOrder: 345;
1290
+ readonly outputProtocol: "xeer.command.v0";
1291
+ readonly effects: readonly ["network-read"];
1292
+ readonly idempotent: true;
1293
+ readonly reversible: true;
1257
1294
  readonly destructive: false;
1258
- readonly humanPrerequisites: readonly ["A human must pass --write before any statement that modifies rows."];
1295
+ readonly humanPrerequisites: readonly [string];
1296
+ readonly pathPolicy: "project-relative";
1297
+ readonly surfaces: {
1298
+ readonly cli: true;
1299
+ readonly mcpExclusion: "Not exposed through MCP v0; use the CLI deliberately.";
1300
+ };
1301
+ }, {
1302
+ readonly id: "db.restore";
1303
+ readonly command: readonly ["db", "restore"];
1304
+ readonly summary: "Restore a deployed application database to a bookmark or a time.";
1305
+ readonly description: string;
1306
+ readonly usage: readonly ["db restore [directory] --remote --environment <prod|preview> --to <bookmark|time> [--control-url <url>] [--json]"];
1307
+ readonly helpOrder: 346;
1308
+ readonly outputProtocol: "xeer.command.v0";
1309
+ readonly effects: readonly ["network-write", "write-state"];
1310
+ readonly idempotent: false;
1311
+ readonly reversible: true;
1312
+ readonly destructive: true;
1313
+ readonly humanPrerequisites: readonly [string];
1259
1314
  readonly pathPolicy: "project-relative";
1260
1315
  readonly surfaces: {
1261
1316
  readonly cli: true;
@@ -1,6 +1,6 @@
1
1
  /** Stable protocol for the action catalogue consumed by CLI, MCP, and agent context surfaces. */
2
2
  import { REVIEW_RECEIPT_ID_SOURCE } from './review.js';
3
- import { FRAMEWORK_NAMES } from './scaffold-names.js';
3
+ import { SCAFFOLDABLE_FRAMEWORK_NAMES } from './scaffold-names.js';
4
4
  export const ACTION_REGISTRY_PROTOCOL = 'xeer.actions.v0';
5
5
  export const XEER_MCP_PROFILES = ['author', 'operator'];
6
6
  export const XEER_ARTIFACT_ID_PATTERN = '^sha256:[a-f0-9]{64}$';
@@ -17,7 +17,7 @@ export const XEER_CLI_COMMAND_GROUPS = Object.freeze([
17
17
  { command: 'state', summary: 'Inspect running application state or reset local state.' },
18
18
  { command: 'env', summary: 'Manage application environment values and secrets.' },
19
19
  { command: 'token', summary: 'Manage service builder tokens.' },
20
- { command: 'db', summary: 'Inspect and query a running application database.' },
20
+ { command: 'db', summary: 'Inspect, query and restore a running or deployed application database.' },
21
21
  ]);
22
22
  const NOT_EXPOSED_V0 = 'Not exposed through MCP v0; use the CLI deliberately.';
23
23
  const HUMAN_IDENTITY = 'Human identity and project ownership decisions are not delegated through MCP.';
@@ -216,12 +216,14 @@ const actions = [
216
216
  + 'scaffold sign-in UI: every visitor already has a verified identity, and the generated README '
217
217
  + 'explains how to opt in to a persistent account. The four bundled starter names resolve '
218
218
  + 'offline and accept `--ui`; every other exact template id resolves through the public catalog '
219
- + 'and owns its renderer. `--framework sveltekit` writes a framework '
220
- + 'project instead: SvelteKit owns the renderer and the routes, Xeer owns `xeer.config.json` '
221
- + 'and the deployment, so `result.framework` replaces both fields, `--ui` does not apply and '
222
- + 'is refused with `XE3004`, and `--template` alongside it is `XE3005`. To adopt a framework '
219
+ + 'and owns its renderer. `--template emdash` is the bundled EmDash blog: a complete Astro '
220
+ + 'framework project that resolves offline, declares `integration: "emdash"`, and refuses `--ui` '
221
+ + 'with `XE3013`. `--framework sveltekit|astro` writes a plain framework project instead: the '
222
+ + 'framework owns the renderer and the routes, Xeer owns `xeer.config.json` and the '
223
+ + 'deployment, so `result.framework` replaces both fields, `--ui` does not apply and is '
224
+ + 'refused with `XE3004`, and `--template` alongside it is `XE3005`. To adopt a framework '
223
225
  + 'project that already exists, use `xeer init` rather than this command.',
224
- usage: ['new [directory] [--template <id>] [--framework sveltekit] [--ui preact|react] [--json]'],
226
+ usage: ['new [directory] [--template <id>] [--framework sveltekit|astro] [--ui preact|react] [--json]'],
225
227
  helpOrder: 30,
226
228
  outputProtocol: 'xeer.command.v0', effects: ['write-source', 'network-read'], idempotent: false,
227
229
  reversible: true, destructive: false, humanPrerequisites: [], pathPolicy: 'project-relative',
@@ -235,15 +237,16 @@ const actions = [
235
237
  type: 'string', description: 'Target directory, relative to the server root. Must be empty or absent.',
236
238
  },
237
239
  template: {
238
- description: 'A bundled starter name or exact template catalog id. Defaults to notes. '
239
- + 'Bundled starters resolve offline. Exclusive with framework, which is refused with XE3005.',
240
+ description: 'A bundled starter name, the bundled emdash framework template, or an exact template '
241
+ + 'catalog id. Defaults to notes. Bundled names resolve offline. Exclusive with framework, '
242
+ + 'which is refused with XE3005.',
240
243
  type: 'string',
241
244
  },
242
245
  framework: {
243
246
  description: 'Scaffold a fresh project for this framework instead of a Xeer '
244
247
  + 'application: it declares xeer.config.json, owns its own renderer and routes, and '
245
248
  + 'delegates check, dev, test, and build to its package scripts. Exclusive with template and ui.',
246
- type: 'string', enum: FRAMEWORK_NAMES,
249
+ type: 'string', enum: SCAFFOLDABLE_FRAMEWORK_NAMES,
247
250
  },
248
251
  ui: {
249
252
  description: 'Which UI provider the written manifest selects. Defaults to preact, and '
@@ -257,7 +260,7 @@ const actions = [
257
260
  },
258
261
  },
259
262
  {
260
- id: 'init', command: ['init'], summary: 'Initialize Xeer in an existing SvelteKit project.',
263
+ id: 'init', command: ['init'], summary: 'Initialize Xeer in an existing SvelteKit or Astro project.',
261
264
  description: 'Detects an existing SvelteKit project, runs Wrangler setup when Cloudflare is not '
262
265
  + 'configured, preserves the project\'s native dev/check/test/build scripts and application '
263
266
  + 'source, and creates the Xeer framework configuration. Use --dry-run to inspect the '
@@ -267,6 +270,19 @@ const actions = [
267
270
  idempotent: true, reversible: true, destructive: false, humanPrerequisites: [],
268
271
  pathPolicy: 'project-relative', surfaces: { cli: true, mcpExclusion: NOT_EXPOSED_V0 },
269
272
  },
273
+ {
274
+ id: 'bootstrap', command: ['bootstrap'], summary: 'Open owner-only EmDash setup for a deployed Astro project.',
275
+ description: 'Verifies builder ownership, obtains a private setup grant, then polls the deployed '
276
+ + 'site\x27s read-only `GET /_emdash/api/setup/status` for up to 240 seconds so a cold EmDash '
277
+ + 'database can finish its migrations. A ready site returns `setup: "required"` with the private '
278
+ + 'setup URL, or `setup: "complete"` with the admin URL once an owner exists. If the site is '
279
+ + 'still initializing when the wait ends, the command exits 1 with `XE5135` and a '
280
+ + '`readiness: "pending"` result naming the retry command. `--json` never opens a browser.',
281
+ usage: ['bootstrap [directory] [--environment prod|preview] [--control-url <url>] [--no-open] [--json]'],
282
+ helpOrder: 51, outputProtocol: 'xeer.command.v0', effects: ['read-source', 'network-read', 'network-write'],
283
+ idempotent: false, reversible: true, destructive: false, humanPrerequisites: [BUILDER_CREDENTIAL],
284
+ pathPolicy: 'project-relative', surfaces: { cli: true, mcpExclusion: NOT_EXPOSED_V0 },
285
+ },
270
286
  {
271
287
  id: 'agent.setup', command: ['agent', 'setup'],
272
288
  summary: 'Install or verify project-confined agent adapters.',
@@ -755,16 +771,48 @@ const actions = [
755
771
  {
756
772
  // Not idempotent and not reversible, because `--write` admits UPDATE and DELETE and the action
757
773
  // catalogue describes what an invocation may do rather than what a particular statement does.
758
- // `destructive: false` all the same: a destructive action here is one that discards state
759
- // wholesale, and this refuses DDL outright — the worst a statement can do is edit rows the
760
- // schema still constrains.
761
- id: 'db.exec', command: ['db', 'exec'], summary: 'Run one SQL statement against a running application database.',
762
- usage: ['db exec [directory] <sql> [--state <dev|preview>] [--write] [--json]'], helpOrder: 340,
763
- outputProtocol: 'xeer.command.v0', effects: ['read-state', 'write-state'], idempotent: false,
764
- reversible: false, destructive: false,
765
- humanPrerequisites: ['A human must pass --write before any statement that modifies rows.'],
774
+ // Destructive because `--remote` runs any SQL the owner sends, DDL included: on a deployed
775
+ // database the owner may do anything to their own data, and `db restore` is the undo.
776
+ id: 'db.exec', command: ['db', 'exec'], summary: 'Run SQL against a running or deployed application database.',
777
+ description: 'Locally (`--state`), runs one statement against a running `xeer dev` or `xeer preview`: '
778
+ + 'reads by default, row writes with --write, and never DDL. With --remote it runs as the owner '
779
+ + 'against the D1 database of a deployed framework project, in the --environment named (there is no '
780
+ + 'default), through the control plane: any SQL, one statement or a --file batch (a JSON array of '
781
+ + '{"sql","params"}) that runs as one transaction, so a failing statement rolls every statement '
782
+ + 'back. Undo with `xeer db restore --remote`.',
783
+ usage: ['db exec [directory] <sql> [--state <dev|preview>] [--write] [--json]',
784
+ 'db exec [directory] <sql> --remote --environment <prod|preview> [--param <value>]... [--control-url <url>] [--json]',
785
+ 'db exec [directory] --remote --environment <prod|preview> --file <batch.json> [--control-url <url>] [--json]'],
786
+ helpOrder: 340,
787
+ outputProtocol: 'xeer.command.v0', effects: ['read-state', 'write-state', 'network-read', 'network-write'],
788
+ idempotent: false, reversible: false, destructive: true,
789
+ humanPrerequisites: ['Locally, a human must pass --write before any statement that modifies rows.',
790
+ BUILDER_CREDENTIAL],
766
791
  pathPolicy: 'project-relative', surfaces: { cli: true, mcpExclusion: DESTRUCTIVE_STATE },
767
792
  },
793
+ {
794
+ id: 'db.bookmark', command: ['db', 'bookmark'],
795
+ summary: 'Print the current Time Travel bookmark of a deployed application database.',
796
+ description: 'The bookmark names the database as it is now; `xeer db restore --remote --to <bookmark>` '
797
+ + 'returns to it for 30 days (7 on the Workers Free plan).',
798
+ usage: ['db bookmark [directory] --remote --environment <prod|preview> [--control-url <url>] [--json]'], helpOrder: 345,
799
+ outputProtocol: 'xeer.command.v0', effects: ['network-read'], idempotent: true, reversible: true,
800
+ destructive: false, humanPrerequisites: [BUILDER_CREDENTIAL], pathPolicy: 'project-relative',
801
+ surfaces: { cli: true, mcpExclusion: NOT_EXPOSED_V0 },
802
+ },
803
+ {
804
+ // Reversible although destructive: the answer carries previousBookmark, and restoring to it
805
+ // brings back everything this restore discarded.
806
+ id: 'db.restore', command: ['db', 'restore'],
807
+ summary: 'Restore a deployed application database to a bookmark or a time.',
808
+ description: 'Replaces the whole database in --environment with its state at the bookmark or ISO time '
809
+ + '(with a zone) given to --to. Every later write is discarded, including the owner writes; uploaded '
810
+ + 'media, code and sessions are not touched. The previousBookmark in the result undoes the restore.',
811
+ usage: ['db restore [directory] --remote --environment <prod|preview> --to <bookmark|time> [--control-url <url>] [--json]'],
812
+ helpOrder: 346, outputProtocol: 'xeer.command.v0', effects: ['network-write', 'write-state'], idempotent: false,
813
+ reversible: true, destructive: true, humanPrerequisites: [BUILDER_CREDENTIAL], pathPolicy: 'project-relative',
814
+ surfaces: { cli: true, mcpExclusion: DESTRUCTIVE_STATE },
815
+ },
768
816
  ];
769
817
  export const XEER_ACTIONS = Object.freeze(actions);
770
818
  export function actionDefinition(id) {
@@ -24,8 +24,13 @@
24
24
  * change enforcement at all.
25
25
  */
26
26
  import { ADMIN_PROTOCOL } from './admin.js';
27
- /** Statements that only read. `EXPLAIN` is here because it plans a statement without running it. */
28
- const READ_VERBS = new Set(['SELECT', 'WITH', 'VALUES', 'EXPLAIN']);
27
+ /**
28
+ * Statements that only read. A strict allowlist of leading verbs: `WITH` and `EXPLAIN` are not here
29
+ * because the verb after them decides what they do — `WITH x AS (SELECT 1) UPDATE posts …` writes
30
+ * without `--write`, and `EXPLAIN PRAGMA ignore_check_constraints=ON` sets the pragma while the
31
+ * statement is prepared.
32
+ */
33
+ const READ_VERBS = new Set(['SELECT', 'VALUES']);
29
34
  const WRITE_VERBS = new Set(['INSERT', 'UPDATE', 'DELETE', 'REPLACE']);
30
35
  /**
31
36
  * Pragmas that only report. An allowlist rather than a denylist, because the case that must never
@@ -13,7 +13,7 @@
13
13
  * undocumented and a retired code cannot linger here.
14
14
  */
15
15
  /** Where an agent observes a code, in the vocabulary of the CLI commands. */
16
- export type DiagnosticSurface = 'any' | 'check' | 'build' | 'dev' | 'preview' | 'test' | 'new' | 'init' | 'agent' | 'doctor' | 'inspect' | 'state' | 'auth' | 'deploy' | 'deployments' | 'promote' | 'rollback' | 'disable' | 'enable' | 'delete' | 'link' | 'env' | 'token' | 'domains' | 'export' | 'import' | 'db';
16
+ export type DiagnosticSurface = 'any' | 'check' | 'build' | 'dev' | 'preview' | 'test' | 'new' | 'init' | 'agent' | 'doctor' | 'inspect' | 'state' | 'auth' | 'deploy' | 'deployments' | 'promote' | 'rollback' | 'disable' | 'enable' | 'delete' | 'link' | 'env' | 'bootstrap' | 'token' | 'domains' | 'export' | 'import' | 'db';
17
17
  export interface DiagnosticFamily {
18
18
  /** Numeric prefix the family owns, as it appears in a code. */
19
19
  readonly prefix: string;
@@ -357,6 +357,12 @@ export const DIAGNOSTIC_DEFINITIONS = [
357
357
  define('XE1841', 'The running server published no admin session secret, so its database surface is '
358
358
  + 'not reachable. The secret is minted per run and shared through the state lease.', 'Restart the server with a build that publishes one; a server older than this CLI does not.', ['db']),
359
359
  define('XE1842', 'The server answered the database request with a protocol this CLI does not know.', 'The CLI and the running server are different versions. Restart the server from this checkout.', ['db']),
360
+ define('XE1843', 'The control plane refused, or did not complete, a `xeer db --remote` request.', 'Read `message` and `hint`: they carry the refusal verbatim. `sql_error` means the whole batch was '
361
+ + 'rolled back; an unknown outcome means it may have been applied, so read before running writes again.', ['db']),
362
+ define('XE1844', 'The control plane returned a database response the CLI will not trust.', 'Retry the read; if it persists the control plane and CLI versions are incompatible.', ['db']),
363
+ define('XE1845', 'A `xeer db --remote` invocation is wrong: no --remote, no --environment, a --state '
364
+ + 'alongside it, an unreadable --file, a bad --to, or no linked application.', 'Pass --remote --environment prod|preview in the project directory. A --file batch is a JSON array '
365
+ + 'of {"sql","params"}; --to takes a bookmark or an ISO time with a zone.', ['db']),
360
366
  define('XE1821', 'The export file could not be read or written.', 'Read `message`: usually a missing path or permissions. `--out` creates parent directories.', ['export', 'import']),
361
367
  define('XE1822', 'The document is not a usable xeer.state-export.v0 export: wrong protocol, or it '
362
368
  + 'contradicts itself (counts, schema, or an encoded value).', 'Import the unmodified file `xeer export --out` produced. `detail.code` names the exact defect.', ['export', 'import']),
@@ -404,15 +410,16 @@ export const DIAGNOSTIC_DEFINITIONS = [
404
410
  + 'fix: wait out the advertised retry and read again. '
405
411
  + "Quote `detail.errorId` when correlating with the Worker's console output.", ['inspect', 'state', 'export']),
406
412
  define('XE3001', 'xeer new could not scaffold the project — most often a non-empty target directory.', 'Choose an empty or non-existent directory.', ['new']),
407
- define('XE3002', '`--template` named neither a bundled starter nor an exact id in the reachable catalog.', 'Check the exact catalog id, use a bundled starter, or omit --template for the default. A framework '
408
- + 'scaffold is selected with `--framework`.', ['new']),
413
+ define('XE3002', '`--template` named neither a bundled starter nor an exact id in the reachable catalog.', 'Check the exact catalog id, use a bundled name (notes, todo, blog, personal-site, or the emdash '
414
+ + 'framework template), or omit --template for the default. A plain framework scaffold is '
415
+ + 'selected with `--framework`.', ['new']),
409
416
  define('XE3003', '`--ui` named a UI provider that does not exist. Refused before the target '
410
417
  + 'directory is read, so nothing was written.', 'Use preact or react, or omit --ui for the default (preact). The choice is orthogonal to '
411
418
  + '--template for every Xeer application template: each scaffolds on either provider from the '
412
419
  + 'same client sources.', ['new']),
413
420
  define('XE3004', '`--ui` was passed with `--framework`, and a framework renders through its own '
414
421
  + 'toolchain rather than through a Xeer UI provider. Refused before the target directory is '
415
- + 'read, so nothing was written.', 'Drop --ui. `--framework sveltekit` owns its renderer: it writes no client.runtime.provider to '
422
+ + 'read, so nothing was written.', 'Drop --ui. A framework project owns its renderer: it writes no client.runtime.provider to '
416
423
  + 'select, and no value of --ui would change a file it writes.', ['new']),
417
424
  define('XE3005', '`--template` and `--framework` were passed together. They are two exclusive '
418
425
  + 'axes: one writes a Xeer application, the other a project Xeer runs rather than compiles. '
@@ -427,7 +434,8 @@ export const DIAGNOSTIC_DEFINITIONS = [
427
434
  define('XE3010', 'The downloaded template archive does not match the catalog SHA-256.', 'Do not use the bytes. Repeat later or report the template artifact.', ['new']),
428
435
  define('XE3011', 'The downloaded template archive contains unsafe or unsupported content.', 'Report the template artifact and choose another template.', ['new']),
429
436
  define('XE3012', 'The extracted project root does not match the project metadata declared by the catalog.', 'Report the template artifact and choose another template.', ['new']),
430
- define('XE3013', '`--ui` was passed with a catalog template, which owns its project kind and renderer.', 'Drop --ui and install the catalog template unchanged.', ['new']),
437
+ define('XE3013', '`--ui` was passed with a template that owns its renderer: a catalog template, or the '
438
+ + 'bundled emdash framework template. Refused before the target directory is read, so nothing was written.', 'Drop --ui and create the template unchanged.', ['new']),
431
439
  define('XE3101', 'Agent setup check found one or more generated files missing or stale. No files were written.', 'Run `xeer agent setup`, then repeat `xeer agent setup --check --json`.', ['agent']),
432
440
  define('XE3102', 'Agent setup found a path owned by the user or another tool. The entire write was refused.', 'Move, rename, or deliberately remove the conflicting path; setup never overwrites an unowned file.', ['agent']),
433
441
  define('XE3103', '`--target` named an agent adapter that Xeer does not support.', 'Use auto, agents, claude, codex, cursor, vscode, or mcp.', ['agent']),
@@ -509,6 +517,11 @@ export const DIAGNOSTIC_DEFINITIONS = [
509
517
  + 'entries have names or values a local run cannot load — so `ctx.env` is missing them.', 'Read `message`: it names the file and the exact problem — the shape to write is '
510
518
  + '{"format": "xeer.env-local.v0", "values": {"NAME": "value"}} with UPPER_SNAKE_CASE names '
511
519
  + 'and string values. `xeer env pull` writes the file correctly.', ['dev', 'preview', 'test']),
520
+ define('XE5135', 'xeer bootstrap could not confirm EmDash setup readiness within its fixed 240-second wait: '
521
+ + 'the deployed site never answered `GET /_emdash/api/setup/status` with a ready response. A fresh '
522
+ + 'database can take several minutes to run its migrations; no probe has a shorter deadline than what remains of that budget.', 'Wait for the site to finish initializing, then run the `retry` command from the result. The result '
523
+ + 'carries `readiness: "pending"` and `retryable: true`; no setup URL was returned, and a retry obtains a '
524
+ + 'fresh grant. Do not release a migration lock while initialization is still running.', ['bootstrap']),
512
525
  define('XE5139', 'xeer env failed without a more specific code.', 'Read `message`; it is the underlying error verbatim.', ['env']),
513
526
  define('XE5140', 'An `xeer deployments` invocation is wrong: an unusable --limit, or a directory that '
514
527
  + 'declares no app identity.', 'Read `message`. --limit takes a positive integer up to 200. In a directory with no appId, run '
@@ -1,18 +1,53 @@
1
1
  import { z } from 'zod';
2
2
  export declare const FRAMEWORK_CONFIG_SCHEMA_URL: "https://docs.xeer.run/config-v0.schema.json";
3
3
  export declare const FRAMEWORK_CONFIG_FORMAT: "xeer.config.v0";
4
+ /**
5
+ * What an EmDash deployment must bind. EmDash's own adapters fix the D1 and R2 binding names, its
6
+ * sign-in needs Astro's session KV, and its scheduled publishing needs the platform's one supported
7
+ * schedule. Stated once here so the config, the build, and the control plane refuse the same shape.
8
+ */
9
+ export declare const EMDASH_DEPLOYMENT: Readonly<{
10
+ readonly framework: "astro";
11
+ readonly database: "DB";
12
+ readonly storage: "MEDIA";
13
+ readonly schedule: "* * * * *";
14
+ }>;
4
15
  declare const frameworkConfigSchema: z.ZodObject<{
5
16
  $schema: z.ZodOptional<z.ZodString>;
6
17
  format: z.ZodLiteral<"xeer.config.v0">;
7
18
  name: z.ZodString;
8
19
  framework: z.ZodEnum<{
20
+ astro: "astro";
9
21
  sveltekit: "sveltekit";
10
22
  }>;
23
+ integration: z.ZodOptional<z.ZodEnum<{
24
+ emdash: "emdash";
25
+ }>>;
11
26
  capabilities: z.ZodObject<{
12
27
  auth: z.ZodOptional<z.ZodObject<{}, z.core.$strict>>;
28
+ database: z.ZodOptional<z.ZodObject<{}, z.core.$strict>>;
29
+ storage: z.ZodOptional<z.ZodObject<{}, z.core.$strict>>;
13
30
  }, z.core.$strict>;
14
31
  }, z.core.$strict>;
15
32
  export type FrameworkConfigV0 = z.infer<typeof frameworkConfigSchema>;
16
33
  export declare const frameworkConfigJsonSchema: Readonly<Record<string, unknown>>;
17
34
  export declare function parseFrameworkConfig(value: unknown): FrameworkConfigV0;
35
+ declare const frameworkDeploymentSchema: z.ZodObject<{
36
+ framework: z.ZodOptional<z.ZodEnum<{
37
+ astro: "astro";
38
+ sveltekit: "sveltekit";
39
+ }>>;
40
+ integration: z.ZodOptional<z.ZodEnum<{
41
+ emdash: "emdash";
42
+ }>>;
43
+ bindings: z.ZodOptional<z.ZodObject<{
44
+ database: z.ZodOptional<z.ZodString>;
45
+ storage: z.ZodOptional<z.ZodString>;
46
+ session: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strict>>;
48
+ schedule: z.ZodOptional<z.ZodLiteral<"* * * * *">>;
49
+ }, z.core.$strip>;
50
+ export type FrameworkDeployment = z.infer<typeof frameworkDeploymentSchema>;
51
+ export type FrameworkBindings = NonNullable<FrameworkDeployment['bindings']>;
52
+ export declare function parseFrameworkDeployment(value: unknown): FrameworkDeployment;
18
53
  export {};
@@ -1,16 +1,37 @@
1
1
  import { z } from 'zod';
2
- import { FRAMEWORK_NAMES } from './scaffold-names.js';
2
+ import { FRAMEWORK_NAMES, INTEGRATION_NAMES } from './scaffold-names.js';
3
3
  export const FRAMEWORK_CONFIG_SCHEMA_URL = 'https://docs.xeer.run/config-v0.schema.json';
4
4
  export const FRAMEWORK_CONFIG_FORMAT = 'xeer.config.v0';
5
+ /**
6
+ * What an EmDash deployment must bind. EmDash's own adapters fix the D1 and R2 binding names, its
7
+ * sign-in needs Astro's session KV, and its scheduled publishing needs the platform's one supported
8
+ * schedule. Stated once here so the config, the build, and the control plane refuse the same shape.
9
+ */
10
+ export const EMDASH_DEPLOYMENT = Object.freeze({
11
+ framework: 'astro', database: 'DB', storage: 'MEDIA', schedule: '* * * * *',
12
+ });
13
+ function requireIntegrationFramework(value, context) {
14
+ if (value.integration === 'emdash' && value.framework !== 'astro') {
15
+ context.addIssue({ code: 'custom', message: 'The emdash integration requires the astro framework.' });
16
+ }
17
+ }
5
18
  const frameworkConfigSchema = z.object({
6
19
  $schema: z.string().url().optional(),
7
20
  format: z.literal(FRAMEWORK_CONFIG_FORMAT),
8
21
  name: z.string().regex(/^[a-z][a-z0-9-]{0,61}[a-z0-9]$/u),
9
22
  framework: z.enum(FRAMEWORK_NAMES),
23
+ integration: z.enum(INTEGRATION_NAMES).optional(),
10
24
  capabilities: z.object({
11
25
  auth: z.object({}).strict().optional(),
26
+ database: z.object({}).strict().optional(),
27
+ storage: z.object({}).strict().optional(),
12
28
  }).strict(),
13
- }).strict();
29
+ }).strict().superRefine((value, context) => {
30
+ requireIntegrationFramework(value, context);
31
+ if (value.integration === 'emdash' && (!value.capabilities.database || !value.capabilities.storage)) {
32
+ context.addIssue({ code: 'custom', message: 'The emdash integration requires database and storage capabilities.' });
33
+ }
34
+ });
14
35
  export const frameworkConfigJsonSchema = Object.freeze({
15
36
  ...z.toJSONSchema(frameworkConfigSchema),
16
37
  $id: FRAMEWORK_CONFIG_SCHEMA_URL,
@@ -20,3 +41,29 @@ export const frameworkConfigJsonSchema = Object.freeze({
20
41
  export function parseFrameworkConfig(value) {
21
42
  return frameworkConfigSchema.parse(value);
22
43
  }
44
+ const bindingName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/u)
45
+ .refine((name) => !name.startsWith('XEER_') && !['ASSETS', 'XeerState'].includes(name), 'Binding names must not shadow platform bindings.');
46
+ const frameworkDeploymentSchema = z.object({
47
+ framework: z.enum(FRAMEWORK_NAMES).optional(),
48
+ integration: z.enum(INTEGRATION_NAMES).optional(),
49
+ bindings: z.object({ database: bindingName.optional(), storage: bindingName.optional(),
50
+ session: bindingName.optional() }).strict().optional(),
51
+ schedule: z.literal('* * * * *').optional(),
52
+ }).superRefine((value, context) => {
53
+ const names = Object.values(value.bindings ?? {});
54
+ if (new Set(names).size !== names.length)
55
+ context.addIssue({ code: 'custom', message: 'Binding names must be distinct.' });
56
+ if ((value.bindings !== undefined || value.schedule !== undefined) && value.framework !== 'astro') {
57
+ context.addIssue({ code: 'custom', message: 'Declared bindings and schedules require an Astro framework bundle.' });
58
+ }
59
+ requireIntegrationFramework(value, context);
60
+ if (value.integration === 'emdash' && (value.bindings?.database !== EMDASH_DEPLOYMENT.database
61
+ || value.bindings.storage !== EMDASH_DEPLOYMENT.storage || value.bindings.session === undefined
62
+ || value.schedule !== EMDASH_DEPLOYMENT.schedule)) {
63
+ context.addIssue({ code: 'custom', message: 'The emdash integration requires the DB database binding, the '
64
+ + 'MEDIA storage binding, a session binding, and the * * * * * schedule.' });
65
+ }
66
+ });
67
+ export function parseFrameworkDeployment(value) {
68
+ return frameworkDeploymentSchema.parse(value);
69
+ }
@@ -26,3 +26,4 @@ export * from './framework-config.js';
26
26
  export * from './tunnel.js';
27
27
  export * from './type-check-profile.js';
28
28
  export * from './editor-settings.js';
29
+ export * from './platform-grants.js';
@@ -26,3 +26,4 @@ export * from './framework-config.js';
26
26
  export * from './tunnel.js';
27
27
  export * from './type-check-profile.js';
28
28
  export * from './editor-settings.js';
29
+ export * from './platform-grants.js';
@@ -0,0 +1,25 @@
1
+ /** Fixed by the endpoint, never selected by an incoming grant. */
2
+ export declare const PLATFORM_GRANTS: Readonly<{
3
+ inspector: Readonly<{
4
+ audience: "xeer-inspector";
5
+ maxTtlSeconds: 60;
6
+ }>;
7
+ scheduler: Readonly<{
8
+ audience: "xeer-scheduler";
9
+ maxTtlSeconds: 60;
10
+ }>;
11
+ bootstrap: Readonly<{
12
+ audience: "xeer-bootstrap";
13
+ maxTtlSeconds: 1800;
14
+ }>;
15
+ }>;
16
+ export type PlatformGrantScope = keyof typeof PLATFORM_GRANTS;
17
+ export type PlatformGrantEnvironment = 'production' | 'preview';
18
+ /** Includes cold framework migrations; four batches remain below a cron event's 15-minute lifetime. */
19
+ export declare const PLATFORM_SCHEDULE: Readonly<{
20
+ invocationDeadlineMs: 180000;
21
+ dispatchTimeoutMs: 185000;
22
+ leaseMs: 210000;
23
+ maxApplications: 20;
24
+ concurrency: 5;
25
+ }>;
@@ -0,0 +1,14 @@
1
+ /** Fixed by the endpoint, never selected by an incoming grant. */
2
+ export const PLATFORM_GRANTS = Object.freeze({
3
+ inspector: Object.freeze({ audience: 'xeer-inspector', maxTtlSeconds: 60 }),
4
+ scheduler: Object.freeze({ audience: 'xeer-scheduler', maxTtlSeconds: 60 }),
5
+ bootstrap: Object.freeze({ audience: 'xeer-bootstrap', maxTtlSeconds: 1800 }),
6
+ });
7
+ /** Includes cold framework migrations; four batches remain below a cron event's 15-minute lifetime. */
8
+ export const PLATFORM_SCHEDULE = Object.freeze({
9
+ invocationDeadlineMs: 180_000,
10
+ dispatchTimeoutMs: 185_000,
11
+ leaseMs: 210_000,
12
+ maxApplications: 20,
13
+ concurrency: 5,
14
+ });
@@ -110,6 +110,7 @@ export const LIVE_STREAM_PATH = '/__xeer/events';
110
110
  export const PUBLIC_RESERVED_ROUTES = new Set([
111
111
  '/__xeer/health',
112
112
  '/__xeer/identity',
113
+ '/__xeer/bootstrap',
113
114
  '/__xeer/rpc/query',
114
115
  '/__xeer/rpc/mutation',
115
116
  LIVE_STREAM_PATH,
@@ -13,6 +13,19 @@
13
13
  */
14
14
  export declare const BUNDLED_TEMPLATES: readonly ["notes", "todo", "blog", "personal-site"];
15
15
  export type BundledTemplateName = (typeof BUNDLED_TEMPLATES)[number];
16
+ /**
17
+ * The bundled framework templates: a complete framework project, written offline like the
18
+ * application starters above, but holding `xeer.config.json` and declaring which framework runs it.
19
+ *
20
+ * A template rather than a framework name because EmDash is an Astro application, not another
21
+ * runtime: `--template emdash` writes an Astro project whose config declares `framework: "astro"`
22
+ * and `integration: "emdash"`. Kept apart from {@link BUNDLED_TEMPLATES} because the two write
23
+ * different root contracts, and every application-only consumer of that list must stay narrow.
24
+ */
25
+ export declare const BUNDLED_FRAMEWORK_TEMPLATES: Readonly<{
26
+ readonly emdash: "astro";
27
+ }>;
28
+ export type BundledFrameworkTemplateName = keyof typeof BUNDLED_FRAMEWORK_TEMPLATES;
16
29
  /**
17
30
  * The frameworks Xeer runs rather than compiles: a `xeer.config.json` beside a project whose own
18
31
  * toolchain owns the renderer, the routes, and the build.
@@ -21,5 +34,14 @@ export type BundledTemplateName = (typeof BUNDLED_TEMPLATES)[number];
21
34
  * and every later command dispatches on which one is present. `--framework` selects one for a fresh
22
35
  * project; `xeer init` adopts an existing one; `xeer.config.json` declares which one a directory is.
23
36
  */
24
- export declare const FRAMEWORK_NAMES: readonly ["sveltekit"];
37
+ export declare const FRAMEWORK_NAMES: readonly ["sveltekit", "astro"];
25
38
  export type FrameworkName = (typeof FRAMEWORK_NAMES)[number];
39
+ export declare const SCAFFOLDABLE_FRAMEWORK_NAMES: readonly ["sveltekit", "astro"];
40
+ export type ScaffoldableFrameworkName = (typeof SCAFFOLDABLE_FRAMEWORK_NAMES)[number];
41
+ /**
42
+ * The integrations a framework project may declare on top of its framework. One today: EmDash, an
43
+ * Astro CMS whose deployment needs a fixed binding set and a computed site URL. Declared explicitly
44
+ * in `xeer.config.json`, never inferred from packages, routes, or resource names.
45
+ */
46
+ export declare const INTEGRATION_NAMES: readonly ["emdash"];
47
+ export type IntegrationName = (typeof INTEGRATION_NAMES)[number];
@@ -12,6 +12,16 @@
12
12
  * it names. Offline, deterministic, and resolved before any catalog request.
13
13
  */
14
14
  export const BUNDLED_TEMPLATES = Object.freeze(['notes', 'todo', 'blog', 'personal-site']);
15
+ /**
16
+ * The bundled framework templates: a complete framework project, written offline like the
17
+ * application starters above, but holding `xeer.config.json` and declaring which framework runs it.
18
+ *
19
+ * A template rather than a framework name because EmDash is an Astro application, not another
20
+ * runtime: `--template emdash` writes an Astro project whose config declares `framework: "astro"`
21
+ * and `integration: "emdash"`. Kept apart from {@link BUNDLED_TEMPLATES} because the two write
22
+ * different root contracts, and every application-only consumer of that list must stay narrow.
23
+ */
24
+ export const BUNDLED_FRAMEWORK_TEMPLATES = Object.freeze({ emdash: 'astro' });
15
25
  /**
16
26
  * The frameworks Xeer runs rather than compiles: a `xeer.config.json` beside a project whose own
17
27
  * toolchain owns the renderer, the routes, and the build.
@@ -20,4 +30,11 @@ export const BUNDLED_TEMPLATES = Object.freeze(['notes', 'todo', 'blog', 'person
20
30
  * and every later command dispatches on which one is present. `--framework` selects one for a fresh
21
31
  * project; `xeer init` adopts an existing one; `xeer.config.json` declares which one a directory is.
22
32
  */
23
- export const FRAMEWORK_NAMES = Object.freeze(['sveltekit']);
33
+ export const FRAMEWORK_NAMES = Object.freeze(['sveltekit', 'astro']);
34
+ export const SCAFFOLDABLE_FRAMEWORK_NAMES = Object.freeze(['sveltekit', 'astro']);
35
+ /**
36
+ * The integrations a framework project may declare on top of its framework. One today: EmDash, an
37
+ * Astro CMS whose deployment needs a fixed binding set and a computed site URL. Declared explicitly
38
+ * in `xeer.config.json`, never inferred from packages, routes, or resource names.
39
+ */
40
+ export const INTEGRATION_NAMES = Object.freeze(['emdash']);
@@ -34,6 +34,15 @@ export declare const templateDistributionEntrySchema: z.ZodObject<{
34
34
  url: z.ZodString;
35
35
  sha256: z.ZodString;
36
36
  }, z.core.$strict>;
37
+ admin: z.ZodOptional<z.ZodObject<{
38
+ path: z.ZodString;
39
+ signIn: z.ZodEnum<{
40
+ code: "code";
41
+ owner: "owner";
42
+ }>;
43
+ codeSecret: z.ZodOptional<z.ZodString>;
44
+ tasks: z.ZodString;
45
+ }, z.core.$strict>>;
37
46
  author: z.ZodObject<{
38
47
  name: z.ZodString;
39
48
  url: z.ZodOptional<z.ZodString>;
@@ -63,8 +72,12 @@ export declare const templateDistributionEntrySchema: z.ZodObject<{
63
72
  "xeer-application": "xeer-application";
64
73
  }>;
65
74
  framework: z.ZodOptional<z.ZodEnum<{
75
+ astro: "astro";
66
76
  sveltekit: "sveltekit";
67
77
  }>>;
78
+ integration: z.ZodOptional<z.ZodEnum<{
79
+ emdash: "emdash";
80
+ }>>;
68
81
  }, z.core.$strict>>;
69
82
  }, z.core.$strict>;
70
83
  export type TemplateDistributionEntryV0 = z.infer<typeof templateDistributionEntrySchema>;
@@ -86,6 +99,15 @@ export declare const templateDistributionCatalogSchema: z.ZodObject<{
86
99
  url: z.ZodString;
87
100
  sha256: z.ZodString;
88
101
  }, z.core.$strict>;
102
+ admin: z.ZodOptional<z.ZodObject<{
103
+ path: z.ZodString;
104
+ signIn: z.ZodEnum<{
105
+ code: "code";
106
+ owner: "owner";
107
+ }>;
108
+ codeSecret: z.ZodOptional<z.ZodString>;
109
+ tasks: z.ZodString;
110
+ }, z.core.$strict>>;
89
111
  author: z.ZodObject<{
90
112
  name: z.ZodString;
91
113
  url: z.ZodOptional<z.ZodString>;
@@ -115,8 +137,12 @@ export declare const templateDistributionCatalogSchema: z.ZodObject<{
115
137
  "xeer-application": "xeer-application";
116
138
  }>;
117
139
  framework: z.ZodOptional<z.ZodEnum<{
140
+ astro: "astro";
118
141
  sveltekit: "sveltekit";
119
142
  }>>;
143
+ integration: z.ZodOptional<z.ZodEnum<{
144
+ emdash: "emdash";
145
+ }>>;
120
146
  }, z.core.$strict>>;
121
147
  }, z.core.$strict>>;
122
148
  }, z.core.$strict>;
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { FRAMEWORK_NAMES } from './scaffold-names.js';
3
- import { checkTemplateMetadataUniqueness, templateAltTextSchema, templateIdentityShape, templateMetadataShape, templateSlugSchema, } from './template.js';
2
+ import { FRAMEWORK_NAMES, INTEGRATION_NAMES } from './scaffold-names.js';
3
+ import { checkTemplateMetadataUniqueness, templateAdminPathSchema, templateAdminTasksSchema, templateAltTextSchema, templateIdentityShape, templateMetadataShape, templateSlugSchema, } from './template.js';
4
4
  export const TEMPLATE_DISTRIBUTION_CATALOG_FORMAT = 'xeer.template-distribution-catalog.v0';
5
5
  export const TEMPLATE_DISTRIBUTION_CATALOG_SCHEMA_URL = 'https://docs.xeer.run/template-distribution-catalog-v0.schema.json';
6
6
  /** Exact semantic version: no range operator, no `v` prefix, no wildcard. */
@@ -40,7 +40,12 @@ export const TEMPLATE_PROJECT_KINDS = ['xeer-application', 'framework'];
40
40
  const templateProjectSchema = z.strictObject({
41
41
  kind: z.enum(TEMPLATE_PROJECT_KINDS),
42
42
  framework: z.enum(FRAMEWORK_NAMES).optional(),
43
+ /** Copied from the framework configuration; EmDash is the only integration, and it runs on Astro. */
44
+ integration: z.enum(INTEGRATION_NAMES).optional(),
43
45
  }).superRefine((project, ctx) => {
46
+ if (project.integration === 'emdash' && project.framework !== 'astro') {
47
+ ctx.addIssue({ code: 'custom', path: ['integration'], message: 'the emdash integration runs on the astro framework' });
48
+ }
44
49
  if ((project.framework !== undefined) === (project.kind === 'framework'))
45
50
  return;
46
51
  ctx.addIssue({
@@ -51,6 +56,17 @@ const templateProjectSchema = z.strictObject({
51
56
  : `only a framework project names a framework; a ${project.kind} project does not`,
52
57
  });
53
58
  });
59
+ /**
60
+ * The Site admin an entry resolves to: an owner dashboard unlocked by one of the entry's invite codes
61
+ * (`code`), or EmDash's admin, which the owner opens through the platform's owner sign-in (`owner`).
62
+ * Authored in `template.json` for the first, derived from the EmDash integration for the second.
63
+ */
64
+ const distributedAdminSchema = z.strictObject({
65
+ path: templateAdminPathSchema,
66
+ signIn: z.enum(['code', 'owner']),
67
+ codeSecret: z.string().optional(),
68
+ tasks: templateAdminTasksSchema,
69
+ });
54
70
  /**
55
71
  * One published template. The authored metadata is copied verbatim from `template.json`; everything
56
72
  * else is produced by the release job that packed, deployed, and verified this exact artifact.
@@ -77,7 +93,29 @@ export const templateDistributionEntrySchema = z.strictObject({
77
93
  /** Release version of the template artifact itself, independent of the platform version. */
78
94
  artifactVersion: exactVersion,
79
95
  sourceArchive: sourceArchiveSchema,
80
- }).superRefine(checkTemplateMetadataUniqueness);
96
+ /** Absent for a template without an admin, and on every entry published before the field existed. */
97
+ admin: distributedAdminSchema.optional(),
98
+ }).superRefine((entry, ctx) => {
99
+ checkTemplateMetadataUniqueness(entry, ctx);
100
+ const { admin } = entry;
101
+ const emdash = entry.project?.integration === 'emdash';
102
+ if (emdash && admin?.signIn !== 'owner') {
103
+ ctx.addIssue({ code: 'custom', path: ['admin'], message: 'an emdash project carries the owner admin' });
104
+ }
105
+ if (!admin)
106
+ return;
107
+ if (admin.signIn === 'owner' && !emdash) {
108
+ ctx.addIssue({ code: 'custom', path: ['admin', 'signIn'], message: 'the owner sign-in belongs to an emdash project' });
109
+ }
110
+ if ((admin.codeSecret !== undefined) !== (admin.signIn === 'code')) {
111
+ ctx.addIssue({ code: 'custom', path: ['admin', 'codeSecret'],
112
+ message: 'a code admin names its codeSecret, and only a code admin does' });
113
+ }
114
+ else if (admin.codeSecret !== undefined && !entry.requiredSecrets.some((secret) => secret.name === admin.codeSecret)) {
115
+ ctx.addIssue({ code: 'custom', path: ['admin', 'codeSecret'],
116
+ message: 'admin codeSecret must name one of the entry requiredSecrets' });
117
+ }
118
+ });
81
119
  /**
82
120
  * Versioned distribution catalog published atomically after a platform release. Entries are keyed by
83
121
  * template id, sorted by codepoint so a regenerated catalog is byte-stable, and a template with no
@@ -3,6 +3,13 @@ export declare const TEMPLATE_FORMAT: "xeer.template.v0";
3
3
  export declare const TEMPLATE_MANIFEST_SCHEMA_URL: "https://docs.xeer.run/template-v0.schema.json";
4
4
  /** Catalog identifier shape shared by template directory names and categories. */
5
5
  export declare const templateSlugSchema: z.ZodString;
6
+ /**
7
+ * Where a template's own admin lives, relative to the site's origin: one absolute path, nothing that
8
+ * could leave the origin (`//host`) or carry a query or fragment.
9
+ */
10
+ export declare const templateAdminPathSchema: z.ZodString;
11
+ /** What the owner does in the admin, shown next to its button: one short sentence. */
12
+ export declare const templateAdminTasksSchema: z.ZodString;
6
13
  /** Alt text carried by a gallery image in either the source or the distribution catalog. */
7
14
  export declare const templateAltTextSchema: z.ZodString;
8
15
  /**
@@ -50,6 +57,12 @@ export declare function checkTemplateMetadataUniqueness(metadata: {
50
57
  }[];
51
58
  }, ctx: z.RefinementCtx): void;
52
59
  export declare const templateManifestSchema: z.ZodObject<{
60
+ admin: z.ZodOptional<z.ZodObject<{
61
+ path: z.ZodOptional<z.ZodString>;
62
+ signIn: z.ZodOptional<z.ZodLiteral<"code">>;
63
+ codeSecret: z.ZodOptional<z.ZodString>;
64
+ tasks: z.ZodOptional<z.ZodString>;
65
+ }, z.core.$strict>>;
53
66
  author: z.ZodObject<{
54
67
  name: z.ZodString;
55
68
  url: z.ZodOptional<z.ZodString>;
@@ -18,6 +18,27 @@ const requiredSecretSchema = z.strictObject({
18
18
  name: environmentName,
19
19
  description: trimmed(200),
20
20
  });
21
+ /**
22
+ * Where a template's own admin lives, relative to the site's origin: one absolute path, nothing that
23
+ * could leave the origin (`//host`) or carry a query or fragment.
24
+ */
25
+ export const templateAdminPathSchema = z.string().max(200)
26
+ .regex(/^\/(?!\/)[^\s?#\\]*$/u, 'admin path must be a plain site path starting with one "/"');
27
+ /** What the owner does in the admin, shown next to its button: one short sentence. */
28
+ export const templateAdminTasksSchema = trimmed(140)
29
+ .regex(/^[^.!?]*(?:[.!?](?! )[^.!?]*)*\.$/u, 'admin tasks must be one short sentence ending in a full stop');
30
+ /**
31
+ * The Site admin a template offers. An application template authors all of it: the path of its owner
32
+ * dashboard, and the invite code (one of its `requiredSecrets`) that unlocks it. An EmDash template's
33
+ * admin is derived from its integration by the catalog, so there only `tasks` may be authored; which
34
+ * of the two applies is known only to the catalog generator, which reads the root contract.
35
+ */
36
+ const templateAdminSchema = z.strictObject({
37
+ path: templateAdminPathSchema.optional(),
38
+ signIn: z.literal('code').optional(),
39
+ codeSecret: environmentName.optional(),
40
+ tasks: templateAdminTasksSchema.optional(),
41
+ });
21
42
  /** Alt text carried by a gallery image in either the source or the distribution catalog. */
22
43
  export const templateAltTextSchema = trimmed(200);
23
44
  const screenshotSchema = z.strictObject({
@@ -70,7 +91,21 @@ export const templateManifestSchema = z.strictObject({
70
91
  ...templateMetadataShape,
71
92
  screenshots: z.array(screenshotSchema).min(1).max(8),
72
93
  ...templateIdentityShape,
73
- }).superRefine(checkTemplateMetadataUniqueness);
94
+ admin: templateAdminSchema.optional(),
95
+ }).superRefine((manifest, ctx) => {
96
+ checkTemplateMetadataUniqueness(manifest, ctx);
97
+ const { admin } = manifest;
98
+ if (!admin)
99
+ return;
100
+ const code = [admin.path, admin.signIn, admin.codeSecret];
101
+ if (code.some((value) => value !== undefined) && code.some((value) => value === undefined)) {
102
+ ctx.addIssue({ code: 'custom', path: ['admin'], message: 'admin needs path, signIn, and codeSecret together' });
103
+ }
104
+ if (admin.codeSecret !== undefined && !manifest.requiredSecrets.some((secret) => secret.name === admin.codeSecret)) {
105
+ ctx.addIssue({ code: 'custom', path: ['admin', 'codeSecret'],
106
+ message: 'admin codeSecret must name one of the template requiredSecrets' });
107
+ }
108
+ });
74
109
  const generatedTemplateManifestJsonSchema = z.toJSONSchema(templateManifestSchema);
75
110
  const generatedProperties = generatedTemplateManifestJsonSchema.properties;
76
111
  export const templateManifestJsonSchema = Object.freeze({