@spooky-sync/cli 0.0.1-canary.63 → 0.0.1-canary.65

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/AGENTS.md ADDED
@@ -0,0 +1,76 @@
1
+ # `@spooky-sync/cli` (`spky`) — agent guide
2
+
3
+ ## What this package is
4
+
5
+ The sp00ky toolchain. A Rust binary (`spky`) plus a thin npm wrapper. It parses `.surql` schemas, emits typed `schema.gen.ts` (and `.dart`), runs migrations, manages buckets and API backends, drives the local dev environment, and orchestrates Sp00ky Cloud deployments.
6
+
7
+ ## Binary
8
+
9
+ ```
10
+ spky <subcommand> [flags]
11
+ ```
12
+
13
+ Installed via `npx @spooky-sync/cli` or globally as `spky`. The `bin` field in `package.json` is `spky` — *not* `sp00ky`.
14
+
15
+ ## Project layout it expects
16
+
17
+ ```
18
+ your-app/
19
+ ├── sp00ky.yml # config: schema path, generated outputs, backends, buckets
20
+ ├── schema/
21
+ │ └── schema.surql # source of truth — your domain model
22
+ ├── src/
23
+ │ └── schema.gen.ts # GENERATED — never hand-edit
24
+ └── migrations/ # GENERATED migrations; modified files are tracked by checksum
25
+ ```
26
+
27
+ `spky` finds `sp00ky.yml` in the current directory by default; pass `--config <path>` to override.
28
+
29
+ ## Subcommands an app developer/agent uses most
30
+
31
+ - **`spky generate` / `spky gen`** — read `sp00ky.yml`, parse all `.surql`, emit `schema.gen.ts` (and Dart equivalents per config). **Run this after every schema edit.**
32
+ - **`spky migrate create <name>`** — diff current schema against the last applied migration and emit a new `.surql` migration file.
33
+ - **`spky migrate apply`** — apply pending migrations against the configured database. `--fix-checksums` updates stored checksums for legitimately-modified migration files.
34
+ - **`spky migrate status`** — show pending vs applied vs modified-but-applied migrations.
35
+ - **`spky migrate fix [--fix-checksums]`** — repair schema drift / checksum mismatches.
36
+ - **`spky verify [--fix]`** — confirm SSP/scheduler snapshot matches upstream SurrealDB. `--fix` triggers a resync.
37
+ - **`spky lint`** — validate `sp00ky.yml` and referenced files exist.
38
+ - **`spky dev [--apply-migrations] [--clean]`** — boots a local SurrealDB + SSP + scheduler stack via Docker. `--clean` wipes SSP/scheduler state but preserves user data in SurrealDB.
39
+ - **`spky create`** — scaffold a new sp00ky project.
40
+ - **`spky bucket add`** / **`spky api add`** — append a bucket or backend definition to `sp00ky.yml`.
41
+ - **`spky mcp`** — start the bundled `@spooky-sync/devtools-mcp` server (so AI assistants can introspect the running app).
42
+
43
+ ## Cloud subcommands (deployment)
44
+
45
+ `spky cloud login | create | deploy | status | logs | scale | restart | destroy | backup | env | keys | link | team | vault | credentials`. See `spky cloud --help`. Most app code agents touch never need these.
46
+
47
+ ## Schema annotations the parser recognizes
48
+
49
+ In your `.surql` source, comment annotations attached to `DEFINE FIELD` / `DEFINE TABLE` change codegen output:
50
+
51
+ - `-- @crdt text` (above a `DEFINE FIELD`) — marks a field as a Loro CRDT text field. Consumers must use `useCrdtField` to read/write it; plain `useQuery` will see stale or unmerged content.
52
+ - `-- @parent` (suffix on `DEFINE FIELD ... TYPE record<...>`) — marks the column as the parent side of a relationship; written automatically from the auth context, never by client code.
53
+
54
+ Example:
55
+ ```sql
56
+ DEFINE TABLE thread SCHEMAFULL ...;
57
+
58
+ -- @crdt text
59
+ DEFINE FIELD content ON TABLE thread TYPE string ASSERT $value != NONE;
60
+
61
+ DEFINE FIELD author ON TABLE thread TYPE record<user>; -- @parent
62
+ ```
63
+
64
+ ## Common gotchas
65
+
66
+ - **`schema.gen.ts` must be regenerated after every `.surql` change.** `spky generate`. CI typically asserts no drift.
67
+ - **Migrations are checksum-tracked.** Editing a previously-applied migration file won't silently re-run; `spky migrate status` flags it. Use `--fix-checksums` only when you're sure the change is semantically a no-op.
68
+ - **`sp00ky.yml` is the entry point.** The CLI never crawls for `.surql`; everything is wired explicitly through the config.
69
+ - **Generation modes matter.** The `--mode` flag (`singlenode`, `cluster`, `surrealism`) changes what the generated client connects to. Default is `singlenode` (HTTP to a single SSP). `surrealism` embeds the WASM stream processor in-browser.
70
+ - **Don't commit the bin output.** The Rust binary is built per-platform and shipped via the npm tarball under `dist/`.
71
+
72
+ ## Pointers
73
+
74
+ - Sync engine the generated client targets: `node_modules/@spooky-sync/core/AGENTS.md`
75
+ - Reactive UI bindings: `node_modules/@spooky-sync/client-solid/AGENTS.md`
76
+ - Live MCP introspection during dev: `node_modules/@spooky-sync/devtools-mcp/AGENTS.md`
@@ -142,6 +142,92 @@ export function createServer(bridge, surreal) {
142
142
  await bridge.request(BRIDGE_METHODS.CLEAR_HISTORY, {}, tabId);
143
143
  return { content: [{ type: 'text', text: 'History cleared.' }] };
144
144
  });
145
+ server.tool('describe_schema', 'Describe all tables with columns, types, and sp00ky annotations (@crdt, @parent). Stitches INFO FOR DB with parsed schema metadata. With the browser extension this returns @crdt/@parent semantics; direct-DB mode returns raw column info only.', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
146
+ if (bridge.isConnected) {
147
+ const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
148
+ const dbState = state?.database ?? {};
149
+ return json({
150
+ source: 'extension',
151
+ tables: dbState.tables ?? [],
152
+ relationships: dbState.relationships ?? [],
153
+ });
154
+ }
155
+ if (surreal) {
156
+ const dbInfo = (await surreal.query('INFO FOR DB;'));
157
+ const tablesObj = dbInfo?.[0]?.result?.tables ?? dbInfo?.[0]?.tables ?? {};
158
+ const tableNames = Object.keys(tablesObj);
159
+ const tables = await Promise.all(tableNames.map(async (name) => {
160
+ try {
161
+ const info = (await surreal.query(`INFO FOR TABLE \`${name}\`;`));
162
+ const fieldsObj = info?.[0]?.result?.fields ?? info?.[0]?.fields ?? {};
163
+ const columns = Object.entries(fieldsObj).map(([fname, def]) => ({
164
+ name: fname,
165
+ definition: typeof def === 'string' ? def : JSON.stringify(def),
166
+ }));
167
+ return { name, columns };
168
+ }
169
+ catch (e) {
170
+ return { name, columns: [], error: e instanceof Error ? e.message : String(e) };
171
+ }
172
+ }));
173
+ return json({
174
+ source: 'direct-db',
175
+ note: '@crdt / @parent annotations are not visible in direct-DB mode; connect the browser extension to see them.',
176
+ tables,
177
+ });
178
+ }
179
+ throw new Error('No extension connected and no direct database configured.');
180
+ });
181
+ server.tool('lint_query', 'Validate a SurrealQL query without running it. Sends EXPLAIN <query> through the connected channel; returns parse / plan errors with location when SurrealDB provides them.', {
182
+ query: z.string().describe('SurrealQL query to validate'),
183
+ target: z
184
+ .enum(['local', 'remote'])
185
+ .optional()
186
+ .default('remote')
187
+ .describe('When using the extension: lint against local (cache) or remote DB'),
188
+ tabId: z.number().optional().describe('Browser tab ID'),
189
+ }, async ({ query, target, tabId }) => {
190
+ const trimmed = query.trim().replace(/;\s*$/, '');
191
+ const explainQuery = /^\s*EXPLAIN\b/i.test(trimmed) ? trimmed : `EXPLAIN ${trimmed};`;
192
+ const parseError = (msg) => {
193
+ const m = msg.match(/line\s+(\d+)(?:[,\s]+col(?:umn)?\s+(\d+))?/i);
194
+ return {
195
+ ok: false,
196
+ errors: [
197
+ {
198
+ message: msg,
199
+ line: m ? Number(m[1]) : undefined,
200
+ column: m && m[2] ? Number(m[2]) : undefined,
201
+ },
202
+ ],
203
+ };
204
+ };
205
+ const inspectResult = (raw) => {
206
+ const arr = Array.isArray(raw) ? raw : [raw];
207
+ const errors = arr
208
+ .map((r) => (r && r.status === 'ERR' ? r.result ?? r.message : null))
209
+ .filter(Boolean);
210
+ if (errors.length > 0) {
211
+ return { ok: false, errors: errors.map((m) => parseError(m).errors[0]) };
212
+ }
213
+ return { ok: true, plan: arr };
214
+ };
215
+ try {
216
+ if (bridge.isConnected) {
217
+ const result = await bridge.request(BRIDGE_METHODS.RUN_QUERY, { query: explainQuery, target }, tabId);
218
+ return json(inspectResult(result));
219
+ }
220
+ if (surreal) {
221
+ const result = await surreal.query(explainQuery);
222
+ return json(inspectResult(result));
223
+ }
224
+ throw new Error('No extension connected and no direct database configured.');
225
+ }
226
+ catch (e) {
227
+ const msg = e instanceof Error ? e.message : String(e);
228
+ return json(parseError(msg));
229
+ }
230
+ });
145
231
  // --- Resources ---
146
232
  server.resource('state', 'sp00ky://state', { description: 'Full Sp00ky DevTools state' }, async (uri) => {
147
233
  if (!bridge.isConnected) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/cli",
3
- "version": "0.0.1-canary.63",
3
+ "version": "0.0.1-canary.65",
4
4
  "description": "Generate TypeScript/Dart types from SurrealDB schema files",
5
5
  "type": "module",
6
6
  "main": "./dist/syncgen.cjs",
@@ -18,7 +18,9 @@
18
18
  },
19
19
  "files": [
20
20
  "dist",
21
- "devtools-mcp"
21
+ "devtools-mcp",
22
+ "templates/cookbook",
23
+ "AGENTS.md"
22
24
  ],
23
25
  "scripts": {
24
26
  "dev": "vite",
@@ -58,10 +60,10 @@
58
60
  "vitest": "^1.0.0"
59
61
  },
60
62
  "optionalDependencies": {
61
- "@spooky-sync/cli-darwin-arm64": "0.0.1-canary.63",
62
- "@spooky-sync/cli-darwin-x64": "0.0.1-canary.63",
63
- "@spooky-sync/cli-linux-arm64": "0.0.1-canary.63",
64
- "@spooky-sync/cli-linux-x64": "0.0.1-canary.63",
65
- "@spooky-sync/cli-win32-x64": "0.0.1-canary.63"
63
+ "@spooky-sync/cli-darwin-arm64": "0.0.1-canary.65",
64
+ "@spooky-sync/cli-darwin-x64": "0.0.1-canary.65",
65
+ "@spooky-sync/cli-linux-arm64": "0.0.1-canary.65",
66
+ "@spooky-sync/cli-linux-x64": "0.0.1-canary.65",
67
+ "@spooky-sync/cli-win32-x64": "0.0.1-canary.65"
66
68
  }
67
69
  }
@@ -0,0 +1,31 @@
1
+ # sp00ky cookbook
2
+
3
+ A short, scannable list of patterns an AI agent (or human) reaches for when writing code against a sp00ky-backed app. Each entry has a one-sentence "when to use," the canonical snippet, and one gotcha.
4
+
5
+ Render any recipe with:
6
+ ```
7
+ spky scaffold <recipe> --table <your-table>
8
+ ```
9
+ or pass `--out path/to/file.tsx` to write the snippet directly.
10
+
11
+ ## Recipes
12
+
13
+ ### `live-list`
14
+ **When to use:** you want a reactive, sorted list of rows from a table that updates as records change.
15
+ **Render:** `spky scaffold live-list --table thread`
16
+ **Gotcha:** end the query with `.build()` — `useQuery` will hang on a bare builder.
17
+
18
+ ### `optimistic-mutation`
19
+ **When to use:** you're inserting a new record from a UI action (form submit, button click) and want the local cache to update immediately while the mutation drains to the remote.
20
+ **Render:** `spky scaffold optimistic-mutation --table thread`
21
+ **Gotcha:** `db.create` takes a *full* record-ID string (`'thread:abc'`), not `(table, payload)`. Use `Uuid` to mint IDs.
22
+
23
+ ### `crdt-text-field`
24
+ **When to use:** a text column is annotated `-- @crdt text` in your schema and you need a collaborative editor wired to it.
25
+ **Render:** `spky scaffold crdt-text-field --table thread --field content`
26
+ **Gotcha:** never read the field via `useQuery`; always `useCrdtField`. Writes must pass `{ debounced: true }` to `db.update` so rapid keystrokes coalesce.
27
+
28
+ ## Related
29
+
30
+ - See `AGENTS.md` (in your project root or `node_modules/@spooky-sync/*/AGENTS.md`) for the broader mental model and gotchas.
31
+ - After editing the schema: `spky generate` then `spky doctor` to confirm everything is in sync.
@@ -0,0 +1,36 @@
1
+ // Recipe: crdt-text-field
2
+ // Wire a CRDT text column (`{{table}}.{{field}}` annotated `-- @crdt text` in the schema)
3
+ // to a textarea. Concurrent edits from multiple clients merge via Loro.
4
+
5
+ import { useDb, useQuery, useCrdtField } from '@spooky-sync/client-solid';
6
+ import { schema } from '../schema.gen';
7
+
8
+ export function {{TablePascal}}{{FieldPascal}}Editor(props: { id: string }) {
9
+ const db = useDb<typeof schema>();
10
+
11
+ // Pull the surrounding row so we know the field is loaded.
12
+ const rowResult = useQuery(() =>
13
+ db.query('{{table}}').where({ id: props.id } as any).build()
14
+ );
15
+ const row = () => rowResult.data()?.[0];
16
+
17
+ // Bind the CRDT field. All four args take accessor functions for SolidJS tracking.
18
+ const field = useCrdtField(
19
+ '{{table}}',
20
+ () => row()?.id,
21
+ '{{field}}',
22
+ () => row()?.{{field}}
23
+ );
24
+
25
+ const handleInput = async (e: InputEvent) => {
26
+ const next = (e.currentTarget as HTMLTextAreaElement).value;
27
+ const r = row();
28
+ if (!r?.id) return;
29
+ // `debounced` coalesces rapid keystrokes into a single mutation.
30
+ await db.update('{{table}}', r.id, { {{field}}: next }, { debounced: true });
31
+ };
32
+
33
+ return (
34
+ <textarea value={field.value() ?? ''} onInput={handleInput} />
35
+ );
36
+ }
@@ -0,0 +1,22 @@
1
+ // Recipe: live-list
2
+ // Reactive, sorted list of `{{table}}` rows. Re-runs automatically as records change.
3
+
4
+ import { For } from 'solid-js';
5
+ import { useQuery, useDb } from '@spooky-sync/client-solid';
6
+ import { schema } from '../schema.gen';
7
+
8
+ export function {{TablePascal}}List() {
9
+ const db = useDb<typeof schema>();
10
+
11
+ const result = useQuery(() =>
12
+ db.query('{{table}}').orderBy('id', 'asc').limit(50).build()
13
+ );
14
+
15
+ return (
16
+ <ul>
17
+ <For each={result.data() ?? []}>
18
+ {(row) => <li>{row.id}</li>}
19
+ </For>
20
+ </ul>
21
+ );
22
+ }
@@ -0,0 +1,33 @@
1
+ // Recipe: optimistic-mutation
2
+ // Insert a new `{{table}}` row from a UI action. Local cache updates immediately;
3
+ // the mutation drains to the remote in the background.
4
+
5
+ import { createSignal } from 'solid-js';
6
+ import { Uuid, useDb } from '@spooky-sync/client-solid';
7
+ import { schema } from '../schema.gen';
8
+
9
+ export function Create{{TablePascal}}Form() {
10
+ const db = useDb<typeof schema>();
11
+ const [submitting, setSubmitting] = createSignal(false);
12
+
13
+ const handleSubmit = async (e: SubmitEvent) => {
14
+ e.preventDefault();
15
+ setSubmitting(true);
16
+ try {
17
+ const id = new Uuid().toString();
18
+ await db.create(`{{table}}:${id}`, {
19
+ // TODO: fill in the fields your `{{table}}` schema requires.
20
+ });
21
+ } finally {
22
+ setSubmitting(false);
23
+ }
24
+ };
25
+
26
+ return (
27
+ <form onSubmit={handleSubmit}>
28
+ <button type="submit" disabled={submitting()}>
29
+ {submitting() ? 'Creating…' : 'Create {{table}}'}
30
+ </button>
31
+ </form>
32
+ );
33
+ }