@spooky-sync/cli 0.0.1-canary.21 → 0.0.1-canary.211
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 +138 -0
- package/README.md +9 -9
- package/devtools-mcp/bridge.d.ts +23 -0
- package/devtools-mcp/bridge.js +155 -0
- package/devtools-mcp/dist/bridge.d.ts +23 -0
- package/devtools-mcp/dist/bridge.js +155 -0
- package/devtools-mcp/dist/index.d.ts +2 -0
- package/devtools-mcp/dist/index.js +37 -0
- package/devtools-mcp/dist/protocol.d.ts +35 -0
- package/devtools-mcp/dist/protocol.js +38 -0
- package/devtools-mcp/dist/server.d.ts +4 -0
- package/devtools-mcp/dist/server.js +330 -0
- package/devtools-mcp/dist/surreal.d.ts +13 -0
- package/devtools-mcp/dist/surreal.js +27 -0
- package/devtools-mcp/index.d.ts +2 -0
- package/devtools-mcp/index.js +37 -0
- package/devtools-mcp/protocol.d.ts +34 -0
- package/devtools-mcp/protocol.js +37 -0
- package/devtools-mcp/server.d.ts +4 -0
- package/devtools-mcp/server.js +290 -0
- package/devtools-mcp/surreal.d.ts +13 -0
- package/devtools-mcp/surreal.js +27 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +2 -2
- package/dist/resolve-binary-J8VAJRMF.cjs +8 -0
- package/dist/resolve-binary-kH1Jwk0z.js +60 -0
- package/dist/resolve-binary.d.ts.map +1 -1
- package/dist/syncgen.cjs +1 -1
- package/dist/syncgen.js +7 -7
- package/package.json +15 -10
- package/templates/cookbook/INDEX.md +31 -0
- package/templates/cookbook/crdt-text-field.tsx +36 -0
- package/templates/cookbook/live-list.tsx +22 -0
- package/templates/cookbook/optimistic-mutation.tsx +33 -0
- package/dist/resolve-binary-BYotYL89.js +0 -51
- package/dist/resolve-binary-CFmJcXEj.cjs +0 -8
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { BRIDGE_METHODS } from './protocol.js';
|
|
4
|
+
function json(data) {
|
|
5
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
6
|
+
}
|
|
7
|
+
export function createServer(bridge, surreal) {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: 'sp00ky-devtools',
|
|
10
|
+
version: '0.0.1',
|
|
11
|
+
});
|
|
12
|
+
// --- Tools ---
|
|
13
|
+
server.tool('list_connections', 'List browser tabs connected to Sp00ky DevTools', {}, async () => {
|
|
14
|
+
return json({ connected: bridge.isConnected, tabs: bridge.getConnectedTabs() });
|
|
15
|
+
});
|
|
16
|
+
server.tool('get_state', 'Get the full Sp00ky DevTools state (events, queries, auth, database)', { tabId: z.number().optional().describe('Browser tab ID (uses first connected tab if omitted)') }, async ({ tabId }) => {
|
|
17
|
+
if (!bridge.isConnected) {
|
|
18
|
+
throw new Error('No extension connected. get_state requires the Sp00ky DevTools browser extension.');
|
|
19
|
+
}
|
|
20
|
+
const result = await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId);
|
|
21
|
+
return json(result);
|
|
22
|
+
});
|
|
23
|
+
server.tool('run_query', 'Execute a SurrealQL query against the database', {
|
|
24
|
+
query: z.string().describe('SurrealQL query to execute'),
|
|
25
|
+
target: z.enum(['local', 'remote']).optional().default('remote').describe('Query target: local or remote database'),
|
|
26
|
+
tabId: z.number().optional().describe('Browser tab ID'),
|
|
27
|
+
}, async ({ query, target, tabId }) => {
|
|
28
|
+
if (bridge.isConnected) {
|
|
29
|
+
const result = await bridge.request(BRIDGE_METHODS.RUN_QUERY, { query, target }, tabId);
|
|
30
|
+
return json(result);
|
|
31
|
+
}
|
|
32
|
+
if (surreal) {
|
|
33
|
+
const result = await surreal.query(query);
|
|
34
|
+
return json(result);
|
|
35
|
+
}
|
|
36
|
+
throw new Error('No extension connected and no direct database configured. Set SURREAL_URL or connect the browser extension.');
|
|
37
|
+
});
|
|
38
|
+
server.tool('list_tables', 'List all database tables', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
|
|
39
|
+
if (bridge.isConnected) {
|
|
40
|
+
const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
|
|
41
|
+
const tables = state?.database?.tables ?? [];
|
|
42
|
+
return json(tables);
|
|
43
|
+
}
|
|
44
|
+
if (surreal) {
|
|
45
|
+
const result = await surreal.query('INFO FOR DB;');
|
|
46
|
+
const info = result;
|
|
47
|
+
const tables = info?.[0]?.result?.tables ?? info?.[0]?.tables ?? {};
|
|
48
|
+
return json(Object.keys(tables));
|
|
49
|
+
}
|
|
50
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
51
|
+
});
|
|
52
|
+
server.tool('get_table_data', 'Fetch records from a database table', {
|
|
53
|
+
tableName: z.string().describe('Name of the table'),
|
|
54
|
+
limit: z.number().optional().default(100).describe('Max number of records to return'),
|
|
55
|
+
tabId: z.number().optional().describe('Browser tab ID'),
|
|
56
|
+
}, async ({ tableName, limit, tabId }) => {
|
|
57
|
+
if (bridge.isConnected) {
|
|
58
|
+
const result = await bridge.request(BRIDGE_METHODS.GET_TABLE_DATA, { tableName }, tabId);
|
|
59
|
+
return json(result);
|
|
60
|
+
}
|
|
61
|
+
if (surreal) {
|
|
62
|
+
const result = await surreal.query(`SELECT * FROM \`${tableName}\` LIMIT ${limit};`);
|
|
63
|
+
return json(result);
|
|
64
|
+
}
|
|
65
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
66
|
+
});
|
|
67
|
+
server.tool('update_table_row', 'Update a record in a database table', {
|
|
68
|
+
tableName: z.string().optional().describe('Name of the table (used when browser extension is connected)'),
|
|
69
|
+
recordId: z.string().describe('Record ID to update (e.g. "users:abc123")'),
|
|
70
|
+
updates: z.record(z.unknown()).describe('Fields to update'),
|
|
71
|
+
tabId: z.number().optional().describe('Browser tab ID'),
|
|
72
|
+
}, async ({ tableName, recordId, updates, tabId }) => {
|
|
73
|
+
if (bridge.isConnected) {
|
|
74
|
+
const result = await bridge.request(BRIDGE_METHODS.UPDATE_TABLE_ROW, { tableName, recordId, updates }, tabId);
|
|
75
|
+
return json(result);
|
|
76
|
+
}
|
|
77
|
+
if (surreal) {
|
|
78
|
+
const result = await surreal.query(`UPDATE ${recordId} MERGE ${JSON.stringify(updates)};`);
|
|
79
|
+
return json(result);
|
|
80
|
+
}
|
|
81
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
82
|
+
});
|
|
83
|
+
server.tool('delete_table_row', 'Delete a record from a database table', {
|
|
84
|
+
tableName: z.string().optional().describe('Name of the table (used when browser extension is connected)'),
|
|
85
|
+
recordId: z.string().describe('Record ID to delete (e.g. "users:abc123")'),
|
|
86
|
+
tabId: z.number().optional().describe('Browser tab ID'),
|
|
87
|
+
}, async ({ tableName, recordId, tabId }) => {
|
|
88
|
+
if (bridge.isConnected) {
|
|
89
|
+
const result = await bridge.request(BRIDGE_METHODS.DELETE_TABLE_ROW, { tableName, recordId }, tabId);
|
|
90
|
+
return json(result);
|
|
91
|
+
}
|
|
92
|
+
if (surreal) {
|
|
93
|
+
const result = await surreal.query(`DELETE ${recordId};`);
|
|
94
|
+
return json(result);
|
|
95
|
+
}
|
|
96
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
97
|
+
});
|
|
98
|
+
server.tool('get_active_queries', 'Get all active live queries and their data', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
|
|
99
|
+
if (bridge.isConnected) {
|
|
100
|
+
const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
|
|
101
|
+
return json(state?.activeQueries ?? []);
|
|
102
|
+
}
|
|
103
|
+
if (surreal) {
|
|
104
|
+
const result = await surreal.query('SELECT * FROM _00_query;');
|
|
105
|
+
return json(result);
|
|
106
|
+
}
|
|
107
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
108
|
+
});
|
|
109
|
+
server.tool('get_events', 'Get event history, optionally filtered by type', {
|
|
110
|
+
eventType: z.string().optional().describe('Filter by event type'),
|
|
111
|
+
limit: z.number().optional().default(50).describe('Max number of events to return'),
|
|
112
|
+
tabId: z.number().optional().describe('Browser tab ID'),
|
|
113
|
+
}, async ({ eventType, limit, tabId }) => {
|
|
114
|
+
if (bridge.isConnected) {
|
|
115
|
+
const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
|
|
116
|
+
let events = state?.eventsHistory ?? [];
|
|
117
|
+
if (eventType) {
|
|
118
|
+
events = events.filter((e) => e.eventType === eventType);
|
|
119
|
+
}
|
|
120
|
+
if (limit) {
|
|
121
|
+
events = events.slice(-limit);
|
|
122
|
+
}
|
|
123
|
+
return json(events);
|
|
124
|
+
}
|
|
125
|
+
if (surreal) {
|
|
126
|
+
const result = await surreal.query(`SELECT * FROM _00_events ORDER BY timestamp DESC LIMIT ${limit};`);
|
|
127
|
+
return json(result);
|
|
128
|
+
}
|
|
129
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
130
|
+
});
|
|
131
|
+
server.tool('get_auth_state', 'Get the current authentication state', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
|
|
132
|
+
if (!bridge.isConnected) {
|
|
133
|
+
throw new Error('No extension connected. get_auth_state requires the Sp00ky DevTools browser extension.');
|
|
134
|
+
}
|
|
135
|
+
const state = (await bridge.request(BRIDGE_METHODS.GET_STATE, {}, tabId));
|
|
136
|
+
return json(state?.auth ?? null);
|
|
137
|
+
});
|
|
138
|
+
server.tool('clear_history', 'Clear the event history', { tabId: z.number().optional().describe('Browser tab ID') }, async ({ tabId }) => {
|
|
139
|
+
if (!bridge.isConnected) {
|
|
140
|
+
throw new Error('No extension connected. clear_history requires the Sp00ky DevTools browser extension.');
|
|
141
|
+
}
|
|
142
|
+
await bridge.request(BRIDGE_METHODS.CLEAR_HISTORY, {}, tabId);
|
|
143
|
+
return { content: [{ type: 'text', text: 'History cleared.' }] };
|
|
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
|
+
});
|
|
231
|
+
// --- Resources ---
|
|
232
|
+
server.resource('state', 'sp00ky://state', { description: 'Full Sp00ky DevTools state' }, async (uri) => {
|
|
233
|
+
if (!bridge.isConnected) {
|
|
234
|
+
throw new Error('No extension connected. State resource requires the browser extension.');
|
|
235
|
+
}
|
|
236
|
+
const state = await bridge.request(BRIDGE_METHODS.GET_STATE);
|
|
237
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(state, null, 2) }] };
|
|
238
|
+
});
|
|
239
|
+
server.resource('tables', 'sp00ky://tables', { description: 'List of database tables' }, async (uri) => {
|
|
240
|
+
if (bridge.isConnected) {
|
|
241
|
+
const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
|
|
242
|
+
const tables = state?.database?.tables ?? [];
|
|
243
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(tables, null, 2) }] };
|
|
244
|
+
}
|
|
245
|
+
if (surreal) {
|
|
246
|
+
const result = await surreal.query('INFO FOR DB;');
|
|
247
|
+
const info = result;
|
|
248
|
+
const tables = info?.[0]?.result?.tables ?? info?.[0]?.tables ?? {};
|
|
249
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(Object.keys(tables), null, 2) }] };
|
|
250
|
+
}
|
|
251
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
252
|
+
});
|
|
253
|
+
server.resource('table-data', new ResourceTemplate('sp00ky://tables/{tableName}', { list: undefined }), { description: 'Contents of a specific database table' }, async (uri, variables) => {
|
|
254
|
+
const tableName = variables.tableName;
|
|
255
|
+
if (bridge.isConnected) {
|
|
256
|
+
const result = await bridge.request(BRIDGE_METHODS.GET_TABLE_DATA, { tableName });
|
|
257
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
258
|
+
}
|
|
259
|
+
if (surreal) {
|
|
260
|
+
const result = await surreal.query(`SELECT * FROM \`${tableName}\` LIMIT 100;`);
|
|
261
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
262
|
+
}
|
|
263
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
264
|
+
});
|
|
265
|
+
server.resource('queries', 'sp00ky://queries', { description: 'Active live queries' }, async (uri) => {
|
|
266
|
+
if (bridge.isConnected) {
|
|
267
|
+
const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
|
|
268
|
+
const queries = state?.activeQueries ?? [];
|
|
269
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(queries, null, 2) }] };
|
|
270
|
+
}
|
|
271
|
+
if (surreal) {
|
|
272
|
+
const result = await surreal.query('SELECT * FROM _00_query;');
|
|
273
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
274
|
+
}
|
|
275
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
276
|
+
});
|
|
277
|
+
server.resource('events', 'sp00ky://events', { description: 'Event history' }, async (uri) => {
|
|
278
|
+
if (bridge.isConnected) {
|
|
279
|
+
const state = (await bridge.request(BRIDGE_METHODS.GET_STATE));
|
|
280
|
+
const events = state?.eventsHistory ?? [];
|
|
281
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(events, null, 2) }] };
|
|
282
|
+
}
|
|
283
|
+
if (surreal) {
|
|
284
|
+
const result = await surreal.query('SELECT * FROM _00_events ORDER BY timestamp DESC LIMIT 50;');
|
|
285
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
286
|
+
}
|
|
287
|
+
throw new Error('No extension connected and no direct database configured.');
|
|
288
|
+
});
|
|
289
|
+
return server;
|
|
290
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SurrealConfig {
|
|
2
|
+
url: string;
|
|
3
|
+
namespace: string;
|
|
4
|
+
database: string;
|
|
5
|
+
username: string;
|
|
6
|
+
password: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class SurrealClient {
|
|
9
|
+
private config;
|
|
10
|
+
private authHeader;
|
|
11
|
+
constructor(config: SurrealConfig);
|
|
12
|
+
query(surql: string): Promise<unknown[]>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class SurrealClient {
|
|
2
|
+
config;
|
|
3
|
+
authHeader;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.authHeader =
|
|
7
|
+
'Basic ' + Buffer.from(`${config.username}:${config.password}`).toString('base64');
|
|
8
|
+
}
|
|
9
|
+
async query(surql) {
|
|
10
|
+
const res = await fetch(`${this.config.url}/sql`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
Authorization: this.authHeader,
|
|
15
|
+
'surreal-ns': this.config.namespace,
|
|
16
|
+
'surreal-db': this.config.database,
|
|
17
|
+
Accept: 'application/json',
|
|
18
|
+
},
|
|
19
|
+
body: surql,
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
throw new Error(`SurrealDB query failed (${res.status}): ${text}`);
|
|
24
|
+
}
|
|
25
|
+
return res.json();
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/cli.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";const r=require("child_process"),s=require("./resolve-binary-
|
|
2
|
+
"use strict";const r=require("child_process"),s=require("./resolve-binary-J8VAJRMF.cjs"),i=s.findBinary(),e=r.spawnSync(i,process.argv.slice(2),{stdio:"inherit"});e.error&&(console.error(`Failed to execute spky: ${e.error.message}`),process.exit(1));process.exit(e.status??1);
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync as e } from "child_process";
|
|
3
|
-
import { f as s } from "./resolve-binary-
|
|
3
|
+
import { f as s } from "./resolve-binary-kH1Jwk0z.js";
|
|
4
4
|
const o = s(), r = e(o, process.argv.slice(2), { stdio: "inherit" });
|
|
5
|
-
r.error && (console.error(`Failed to execute
|
|
5
|
+
r.error && (console.error(`Failed to execute spky: ${r.error.message}`), process.exit(1));
|
|
6
6
|
process.exit(r.status ?? 1);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";const o=require("os"),e=require("path"),t=require("fs"),g=require("url"),k=require("module");var s=typeof document<"u"?document.currentScript:null;const x=g.fileURLToPath(typeof document>"u"?require("url").pathToFileURL(__filename).href:s&&s.tagName.toUpperCase()==="SCRIPT"&&s.src||new URL("resolve-binary-J8VAJRMF.cjs",document.baseURI).href),c=e.dirname(x),d={"darwin-arm64":"@spooky-sync/cli-darwin-arm64","darwin-x64":"@spooky-sync/cli-darwin-x64","linux-arm64":"@spooky-sync/cli-linux-arm64","linux-x64":"@spooky-sync/cli-linux-x64","win32-x64":"@spooky-sync/cli-win32-x64"};function $(){const r=`${o.platform()}-${o.arch()}`,n=d[r];if(!n)return;const l=o.platform()==="win32"?"spky.exe":"spky";try{const i=k.createRequire(typeof document>"u"?require("url").pathToFileURL(__filename).href:s&&s.tagName.toUpperCase()==="SCRIPT"&&s.src||new URL("resolve-binary-J8VAJRMF.cjs",document.baseURI).href).resolve(`${n}/package.json`);return e.resolve(e.dirname(i),l)}catch{return}}function w(){const r=o.platform()==="win32"?"spky.exe":"spky",n=$();if(n&&t.existsSync(n))return n;const l=[e.resolve(c,"../../../target/release",r),e.resolve(c,"../target/release",r),e.resolve(c,"../../../target/debug",r),e.resolve(c,"../target/debug",r)],u=l.filter(a=>t.existsSync(a));if(u.length>0)return u.sort((a,h)=>t.statSync(h).mtimeMs-t.statSync(a).mtimeMs),u[0];const i=e.resolve(c,"..",r);if(t.existsSync(i))return i;const p=e.resolve(process.cwd(),r);if(t.existsSync(p))return p;const m=`${o.platform()}-${o.arch()}`,y=d[m],f=y?`
|
|
2
|
+
Try installing the platform package: npm install ${y}`:`
|
|
3
|
+
Your platform (${m}) is not supported.`;throw new Error(`Could not find spky binary. Checked paths:
|
|
4
|
+
- Platform package (${y??"none"})
|
|
5
|
+
`+l.map(a=>` - ${a}
|
|
6
|
+
`).join("")+` - ${i}
|
|
7
|
+
- ${p}
|
|
8
|
+
`+f)}exports.findBinary=w;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { platform as m, arch as u } from "os";
|
|
2
|
+
import { dirname as d, resolve as t } from "path";
|
|
3
|
+
import { existsSync as c, statSync as f } from "fs";
|
|
4
|
+
import { fileURLToPath as h } from "url";
|
|
5
|
+
import { createRequire as w } from "module";
|
|
6
|
+
const $ = h(import.meta.url), i = d($), k = {
|
|
7
|
+
"darwin-arm64": "@spooky-sync/cli-darwin-arm64",
|
|
8
|
+
"darwin-x64": "@spooky-sync/cli-darwin-x64",
|
|
9
|
+
"linux-arm64": "@spooky-sync/cli-linux-arm64",
|
|
10
|
+
"linux-x64": "@spooky-sync/cli-linux-x64",
|
|
11
|
+
"win32-x64": "@spooky-sync/cli-win32-x64"
|
|
12
|
+
};
|
|
13
|
+
function P() {
|
|
14
|
+
const r = `${m()}-${u()}`, n = k[r];
|
|
15
|
+
if (!n) return;
|
|
16
|
+
const a = m() === "win32" ? "spky.exe" : "spky";
|
|
17
|
+
try {
|
|
18
|
+
const e = w(import.meta.url).resolve(`${n}/package.json`);
|
|
19
|
+
return t(d(e), a);
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function B() {
|
|
25
|
+
const r = m() === "win32" ? "spky.exe" : "spky", n = P();
|
|
26
|
+
if (n && c(n))
|
|
27
|
+
return n;
|
|
28
|
+
const a = [
|
|
29
|
+
t(i, "../../../target/release", r),
|
|
30
|
+
// workspace release
|
|
31
|
+
t(i, "../target/release", r),
|
|
32
|
+
// per-package release
|
|
33
|
+
t(i, "../../../target/debug", r),
|
|
34
|
+
// workspace debug
|
|
35
|
+
t(i, "../target/debug", r)
|
|
36
|
+
// per-package debug
|
|
37
|
+
], s = a.filter((o) => c(o));
|
|
38
|
+
if (s.length > 0)
|
|
39
|
+
return s.sort((o, x) => f(x).mtimeMs - f(o).mtimeMs), s[0];
|
|
40
|
+
const e = t(i, "..", r);
|
|
41
|
+
if (c(e))
|
|
42
|
+
return e;
|
|
43
|
+
const p = t(process.cwd(), r);
|
|
44
|
+
if (c(p))
|
|
45
|
+
return p;
|
|
46
|
+
const y = `${m()}-${u()}`, l = k[y], g = l ? `
|
|
47
|
+
Try installing the platform package: npm install ${l}` : `
|
|
48
|
+
Your platform (${y}) is not supported.`;
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Could not find spky binary. Checked paths:
|
|
51
|
+
- Platform package (${l ?? "none"})
|
|
52
|
+
` + a.map((o) => ` - ${o}
|
|
53
|
+
`).join("") + ` - ${e}
|
|
54
|
+
- ${p}
|
|
55
|
+
` + g
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
B as f
|
|
60
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-binary.d.ts","sourceRoot":"","sources":["../src/resolve-binary.ts"],"names":[],"mappings":"AAiCA,wBAAgB,UAAU,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"resolve-binary.d.ts","sourceRoot":"","sources":["../src/resolve-binary.ts"],"names":[],"mappings":"AAiCA,wBAAgB,UAAU,IAAI,MAAM,CAuDnC"}
|
package/dist/syncgen.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("child_process"),a=require("util"),i=require("./resolve-binary-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("child_process"),a=require("util"),i=require("./resolve-binary-J8VAJRMF.cjs"),d=a.promisify(t.execFile);async function f(e){const n=i.findBinary(),r=[];r.push("--input",e.input),r.push("--output",e.output),e.format&&r.push("--format",e.format),e.pretty&&r.push("--pretty"),e.all&&r.push("--all"),e.noHeader&&r.push("--no-header"),e.append&&r.push("--append",e.append),e.modulesDir&&r.push("--modules-dir",e.modulesDir),e.mode&&r.push("--mode",e.mode),e.endpoint&&r.push("--endpoint",e.endpoint),e.secret&&r.push("--secret",e.secret),e.config&&r.push("--config",e.config);try{const{stdout:u,stderr:c}=await d(n,r);return c&&console.error(c),u}catch(u){throw new Error(`Syncgen failed: ${u.message}`,{cause:u})}}exports.runSyncgen=f;
|
package/dist/syncgen.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { execFile as c } from "child_process";
|
|
2
2
|
import { promisify as d } from "util";
|
|
3
|
-
import { f as t } from "./resolve-binary-
|
|
3
|
+
import { f as t } from "./resolve-binary-kH1Jwk0z.js";
|
|
4
4
|
const i = d(c);
|
|
5
|
-
async function
|
|
5
|
+
async function s(e) {
|
|
6
6
|
const a = t(), r = [];
|
|
7
7
|
r.push("--input", e.input), r.push("--output", e.output), e.format && r.push("--format", e.format), e.pretty && r.push("--pretty"), e.all && r.push("--all"), e.noHeader && r.push("--no-header"), e.append && r.push("--append", e.append), e.modulesDir && r.push("--modules-dir", e.modulesDir), e.mode && r.push("--mode", e.mode), e.endpoint && r.push("--endpoint", e.endpoint), e.secret && r.push("--secret", e.secret), e.config && r.push("--config", e.config);
|
|
8
8
|
try {
|
|
9
|
-
const { stdout:
|
|
10
|
-
return
|
|
11
|
-
} catch (
|
|
12
|
-
throw new Error(`Syncgen failed: ${
|
|
9
|
+
const { stdout: u, stderr: f } = await i(a, r);
|
|
10
|
+
return f && console.error(f), u;
|
|
11
|
+
} catch (u) {
|
|
12
|
+
throw new Error(`Syncgen failed: ${u.message}`, { cause: u });
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
export {
|
|
16
|
-
|
|
16
|
+
s as runSyncgen
|
|
17
17
|
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/cli",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.211",
|
|
4
4
|
"description": "Generate TypeScript/Dart types from SurrealDB schema files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/syncgen.cjs",
|
|
7
7
|
"module": "./dist/syncgen.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"bin": {
|
|
10
|
-
"
|
|
10
|
+
"spky": "./dist/cli.js"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
@@ -17,15 +17,20 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"devtools-mcp",
|
|
22
|
+
"templates/cookbook",
|
|
23
|
+
"AGENTS.md"
|
|
21
24
|
],
|
|
22
25
|
"scripts": {
|
|
23
26
|
"dev": "vite",
|
|
27
|
+
"dev:build": "cargo build -p sp00ky-cli",
|
|
24
28
|
"build:rust": "cargo build --release",
|
|
25
29
|
"build:vite": "vite build",
|
|
26
30
|
"build:types": "tsc --emitDeclarationOnly --declaration --declarationDir dist",
|
|
31
|
+
"build:devtools-mcp": "cd ../devtools-mcp && npm run build && cp -r dist ../cli/devtools-mcp",
|
|
27
32
|
"build:js": "npm run build:vite && npm run build:types",
|
|
28
|
-
"build": "npm run build:rust && npm run build:js",
|
|
33
|
+
"build": "npm run build:rust && npm run build:devtools-mcp && npm run build:js",
|
|
29
34
|
"preview": "vite preview",
|
|
30
35
|
"test": "vitest",
|
|
31
36
|
"lint": "eslint src",
|
|
@@ -42,7 +47,7 @@
|
|
|
42
47
|
"license": "MIT",
|
|
43
48
|
"repository": {
|
|
44
49
|
"type": "git",
|
|
45
|
-
"url": "https://github.com/mono424/
|
|
50
|
+
"url": "https://github.com/mono424/sp00ky.git",
|
|
46
51
|
"directory": "apps/cli"
|
|
47
52
|
},
|
|
48
53
|
"publishConfig": {
|
|
@@ -56,10 +61,10 @@
|
|
|
56
61
|
"vitest": "^1.0.0"
|
|
57
62
|
},
|
|
58
63
|
"optionalDependencies": {
|
|
59
|
-
"@spooky-sync/cli-darwin-arm64": "0.0.1-canary.
|
|
60
|
-
"@spooky-sync/cli-darwin-x64": "0.0.1-canary.
|
|
61
|
-
"@spooky-sync/cli-linux-arm64": "0.0.1-canary.
|
|
62
|
-
"@spooky-sync/cli-linux-x64": "0.0.1-canary.
|
|
63
|
-
"@spooky-sync/cli-win32-x64": "0.0.1-canary.
|
|
64
|
+
"@spooky-sync/cli-darwin-arm64": "0.0.1-canary.211",
|
|
65
|
+
"@spooky-sync/cli-darwin-x64": "0.0.1-canary.211",
|
|
66
|
+
"@spooky-sync/cli-linux-arm64": "0.0.1-canary.211",
|
|
67
|
+
"@spooky-sync/cli-linux-x64": "0.0.1-canary.211",
|
|
68
|
+
"@spooky-sync/cli-win32-x64": "0.0.1-canary.211"
|
|
64
69
|
}
|
|
65
70
|
}
|
|
@@ -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
|
+
}
|