@svgrid/studio 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,46 +1,85 @@
1
- This repository is a multi-package workspace. Different packages ship
2
- under different licenses. The authoritative license for each package is
3
- the LICENSE file inside that package directory.
1
+ SVGRID STUDIO COMMERCIAL LICENSE
2
+ Version 1.0
3
+
4
+ Copyright (c) 2026 jQWidgets Ltd
5
+ Project: @svgrid/studio
6
+
7
+ This software is licensed, not sold. Use requires a valid, paid SvGrid
8
+ Pro or Enterprise license. The authoritative agreement is the SvGrid
9
+ End User License Agreement at https://svgrid.com/eula; this file
10
+ summarises the terms that apply to this package.
4
11
 
5
12
  ------------------------------------------------------------------------
6
- Per-package licensing
13
+ 1. GRANT (with valid SvGrid Pro or Enterprise license)
7
14
  ------------------------------------------------------------------------
8
15
 
9
- packages/grid/ MIT (terms below, and packages/grid/LICENSE)
10
- packages/enterprise/ Commercial - see packages/enterprise/LICENSE
11
- packages/mcp/ Commercial - see packages/mcp/LICENSE
12
- website/ Proprietary - see website/LICENSE
16
+ Subject to the Subscriber's compliance with this License and continued
17
+ payment of the applicable license fee, the Author grants the Subscriber
18
+ a non-exclusive, non-transferable, non-sublicensable license to:
19
+
20
+ (a) install and run the Software on developer workstations and on
21
+ build / CI machines belonging to the Subscriber;
22
+ (b) use the Software to generate application source code, and to
23
+ use, modify, and distribute that generated output as part of the
24
+ Subscriber's own applications;
25
+ (c) modify the Software for the Subscriber's internal use, provided
26
+ that all copyright notices and license terms are preserved.
27
+
28
+ The number of developer seats covered is determined by the
29
+ Subscriber's SvGrid Pro / Enterprise order. A "developer" is any
30
+ individual who runs the Software, or who writes or modifies source
31
+ code it generated.
13
32
 
14
- Only `@svgrid/grid` is open source. Everything else in this
15
- repository (the Pro feature pack, the MCP server, the marketing +
16
- docs website, and any other package added later that does not ship
17
- its own MIT LICENSE) is proprietary and may not be copied, modified,
18
- or redistributed without a paid license. Source is visible in this
19
- repository for transparency and for paying customers; visibility does
20
- not grant a license.
33
+ Code generated by the Software is owned by the Subscriber and is not
34
+ subject to Section 2, except that generated code which imports
35
+ `@svgrid/enterprise` still requires an Enterprise license at runtime.
21
36
 
22
37
  ------------------------------------------------------------------------
23
- MIT License (applies ONLY to packages/grid)
38
+ 2. RESTRICTIONS
24
39
  ------------------------------------------------------------------------
25
40
 
26
- MIT License
41
+ The Subscriber may NOT:
27
42
 
28
- Copyright (c) 2026 jQWidgets Ltd
43
+ (a) redistribute the Software as a standalone package, library, CLI,
44
+ or hosted service;
45
+ (b) sublicense, rent, lease, or sell the Software to third parties;
46
+ (c) expose the Software's code generation as a service to users
47
+ outside the Subscriber's organisation;
48
+ (d) use the Software to develop a directly competing application
49
+ builder, code generator, or data-grid product.
50
+
51
+ ------------------------------------------------------------------------
52
+ 3. SUBSCRIPTION AND TERMINATION
53
+ ------------------------------------------------------------------------
54
+
55
+ The license is granted for the term specified in the Subscriber's
56
+ SvGrid Pro or Enterprise order. On non-renewal or material breach of
57
+ Sections 1 or 2, the license terminates automatically and the
58
+ Subscriber must cease all use of the Software and remove it from
59
+ development environments and CI infrastructure. Applications already
60
+ generated and shipped are not affected.
61
+
62
+ ------------------------------------------------------------------------
63
+ 4. WARRANTY DISCLAIMER AND LIABILITY
64
+ ------------------------------------------------------------------------
65
+
66
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
67
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
68
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
69
+ NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
70
+ CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM USE OF THE SOFTWARE,
71
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE.
72
+
73
+ The Author's total cumulative liability under this License shall not
74
+ exceed the license fees paid by the Subscriber in the twelve months
75
+ preceding the event giving rise to the claim.
76
+
77
+ ------------------------------------------------------------------------
78
+ 5. CONTACT
79
+ ------------------------------------------------------------------------
80
+
81
+ To purchase a license, request a quote, or report unauthorized use,
82
+ contact: boikom@jqwidgets.com
83
+ Pricing: https://svgrid.com/pricing
29
84
 
30
- Permission is hereby granted, free of charge, to any person obtaining a copy
31
- of this software and associated documentation files (the "Software"), to deal
32
- in the Software without restriction, including without limitation the rights
33
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
- copies of the Software, and to permit persons to whom the Software is
35
- furnished to do so, subject to the following conditions:
36
-
37
- The above copyright notice and this permission notice shall be included in all
38
- copies or substantial portions of the Software.
39
-
40
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46
- SOFTWARE.
85
+ END OF LICENSE
package/README.md CHANGED
@@ -1,7 +1,41 @@
1
- # @svgrid/studio
1
+ <p align="center">
2
+ <img src="https://svgrid.com/brand/svgrid-logo-icon-1200.png" alt="SvGrid" width="100" height="100" />
3
+ </p>
2
4
 
3
- Scaffold a full CRUD screen for SvelteKit from a **live database** or a
4
- **Drizzle / Prisma schema** in **one command**.
5
+ <h1 align="center">@svgrid/studio</h1>
6
+
7
+ <p align="center"><strong>Generate SvelteKit CRUD screens from your database schema.</strong></p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@svgrid/studio"><img src="https://img.shields.io/npm/v/%40svgrid%2Fstudio.svg?label=%40svgrid%2Fstudio" alt="npm version" /></a>
11
+ <a href="https://svgrid.com/pricing/"><img src="https://img.shields.io/badge/license-commercial-blue.svg" alt="Commercial license" /></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://svgrid.com">Website</a> ·
16
+ <a href="https://svgrid.com/docs/">Docs</a> ·
17
+ <a href="https://svgrid.com/pricing/">Pricing</a>
18
+ </p>
19
+
20
+ ---
21
+
22
+ Not sure where to start? Let it ask:
23
+
24
+ ```bash
25
+ npx @svgrid/studio init
26
+ ```
27
+
28
+ `init` walks you through it - where your data lives, which tables you want,
29
+ which pages each gets - and writes a runnable SvelteKit app: a searchable list,
30
+ an edit form and a record page per table, plus an overview dashboard. Point it
31
+ straight at a database and it installs the driver for you:
32
+
33
+ ```bash
34
+ npx @svgrid/studio init --db postgres --url "$DATABASE_URL" --out my-app
35
+ ```
36
+
37
+ Or scaffold a single CRUD screen into an existing app from a **live database**
38
+ or a **Drizzle / Prisma schema** in **one command**:
5
39
 
6
40
  ```bash
7
41
  # from a live database (PostgreSQL / Supabase / MySQL / SQL Server / SQLite)
@@ -14,9 +48,9 @@ npx @svgrid/studio add User --from prisma/schema.prisma
14
48
 
15
49
  That generates, into your SvelteKit app:
16
50
 
17
- - `src/lib/customers.schema.ts` — the entity schema + row type
18
- - `src/routes/api/customers/+server.ts` — the API route
19
- - `src/routes/customers/+page.svelte` — a working screen: grid + edit panel, with
51
+ - `src/lib/customers.schema.ts` - the entity schema + row type
52
+ - `src/routes/api/customers/+server.ts` - the API route
53
+ - `src/routes/customers/+page.svelte` - a working screen: grid + edit panel, with
20
54
  search, server sort, pagination, and multi-select (optimistic) delete
21
55
 
22
56
  Then:
@@ -79,11 +113,11 @@ in with any client (Drizzle `db.execute`, postgres.js, better-sqlite3, mysql2).
79
113
 
80
114
  ## Regeneration is safe
81
115
 
82
- Re-running `add` replaces only the `svgrid:managed` regions of each file — your
116
+ Re-running `add` replaces only the `svgrid:managed` regions of each file - your
83
117
  edits outside those markers are preserved.
84
118
 
85
119
  ## Requires
86
120
 
87
121
  `@svgrid/grid` and `@svgrid/enterprise` in your project. The Studio is part of the
88
122
  **Enterprise** license (soft-gate: it works unlicensed, with a nudge). See
89
- [licensing](https://svgrid.com/pricing).
123
+ [licensing](https://svgrid.com/pricing/).
package/dist/cli.js CHANGED
@@ -11,11 +11,14 @@
11
11
  * the node:fs wiring.
12
12
  */
13
13
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
14
- import { dirname } from 'node:path';
15
- import { buildStudioBugReport, introspectDatabase, listDatabaseTables, parseProject, resolveSchemas, runStudioAdd, runStudioAddApp, summarizeVerify, } from '@svgrid/enterprise/studio';
14
+ import { spawnSync } from 'node:child_process';
15
+ import { createInterface } from 'node:readline/promises';
16
+ import { dirname, resolve } from 'node:path';
17
+ import { buildStudioBugReport, createProject, deployCommands, emitStudioAppBundle, emitStudioFragment, introspectDatabase, introspectOpenApi, runtimeDeps, listDatabaseTables, missingEnvKeys, parseProject, resolveDeployTarget, resolveSchemas, runStudioAdd, runStudioAddApp, runStudioInit, serializeProject, setEntityDataSource, summarizeVerify, } from '@svgrid/enterprise/studio';
16
18
  import { connect } from './db-connect.js';
17
- import { DRIVER_FOR, installDriver, isDriverInstalled } from './driver-install.js';
19
+ import { ensureDriverInstalled } from './driver-install.js';
18
20
  import { startDesignerServer } from './designer-server.js';
21
+ import { ensureApp, startAppServer } from './dev.js';
19
22
  const io = {
20
23
  readFile: async (path) => {
21
24
  try {
@@ -61,6 +64,30 @@ function parse(args) {
61
64
  out.template = args[++i];
62
65
  else if (a === '--no-open')
63
66
  out.noOpen = true;
67
+ else if (a === '--target')
68
+ out.target = args[++i];
69
+ else if (a === '--dry-run')
70
+ out.dryRun = true;
71
+ else if (a === '--ai')
72
+ out.ai = true;
73
+ else if (a === '--app-port')
74
+ out.appPort = Number(args[++i]);
75
+ else if (a === '--fragment')
76
+ out.fragment = true;
77
+ else if (a === '--supabase-url')
78
+ out.supabaseUrl = args[++i];
79
+ else if (a === '--supabase-key')
80
+ out.supabaseKey = args[++i];
81
+ else if (a === '--dataset')
82
+ out.dataset = args[++i];
83
+ else if (a === '--theme')
84
+ out.theme = args[++i];
85
+ else if (a === '--dark')
86
+ out.dark = true;
87
+ else if (a === '--title')
88
+ out.title = args[++i];
89
+ else if (a === '-y' || a === '--yes')
90
+ out.yes = true;
64
91
  else if (a === '-h' || a === '--help')
65
92
  out.help = true;
66
93
  else if (!a.startsWith('-'))
@@ -76,11 +103,34 @@ function parse(args) {
76
103
  const HELP = `svgrid-studio - scaffold CRUD screens from a schema file or a live database
77
104
 
78
105
  Usage:
106
+ svgrid-studio init # guided: pick your data, get a working CRUD app
79
107
  svgrid-studio designer # open the visual designer in your browser
80
108
  svgrid-studio add <name> --from <schema> # one table/model from a schema file
81
109
  svgrid-studio add --all --from <schema> # every table/model, linked
82
110
  svgrid-studio add <name> --db <dialect> --url <conn> # one table from a live database
83
111
  svgrid-studio add --all --db <dialect> --url <conn> # every table from a live database
112
+ svgrid-studio openapi <file|url> # import an OpenAPI (JSON) spec -> studio.config.json
113
+ svgrid-studio eject [--fragment] # write the app (or a drop-in fragment) from studio.config.json
114
+ svgrid-studio dev # designer + the RUNNING app, side by side (HMR)
115
+ svgrid-studio deploy [--target <provider>] [--dry-run] # build + deploy via the provider CLI
116
+
117
+ Guided setup (init) - asks where your data lives, which tables you want, and
118
+ which pages each gets, then writes a runnable app + studio.config.json.
119
+ Running \`svgrid-studio\` with no arguments starts it too.
120
+ --db <dialect> --url <conn> skip the questions and read a live database
121
+ --supabase-url <url> --supabase-key <anon> read a Supabase project over its
122
+ REST API (no driver, works without a local database)
123
+ --dataset <id> start from sample data (customers-orders, products-categories,
124
+ projects-tasks, employees-departments, tickets-accounts)
125
+ --title <name> app name
126
+ --out <dir> folder to write the app into (default: .)
127
+ --theme <id> design-system preset --dark dark mode
128
+ -y, --yes take every default, ask nothing
129
+
130
+ Deploy (build first, then the provider CLI; target from --target, else
131
+ studio.config.json, else the adapter in svelte.config.js):
132
+ --target <p> vercel | netlify | cloudflare | node
133
+ --dry-run print the resolved commands without running anything
84
134
 
85
135
  Designer (visual app builder, auto-saves to studio.config.json):
86
136
  --template <id> open a ready-made sample app (crm | ecommerce | projects | support)
@@ -88,6 +138,8 @@ Designer (visual app builder, auto-saves to studio.config.json):
88
138
  --out <dir> folder to write the generated app into (default: .)
89
139
  --port <n> port to serve on (default: 4321)
90
140
  --no-open don't open the browser
141
+ --ai enable the AI copilot (needs ANTHROPIC_API_KEY in the environment)
142
+ --app-port <n> dev: port for the generated app's dev server (default: designer port + 1)
91
143
 
92
144
  Schema files: a Drizzle schema.ts or a Prisma schema.prisma (auto-detected).
93
145
  Foreign keys become searchable relation lookups; enums become select fields.
@@ -107,11 +159,78 @@ Options:
107
159
  -h, --help Show this help
108
160
 
109
161
  Examples:
162
+ svgrid-studio init
163
+ svgrid-studio init --db postgres --url $DATABASE_URL --out my-app
110
164
  svgrid-studio add customers --from src/lib/db/schema.ts
111
165
  svgrid-studio add --all --from prisma/schema.prisma
112
166
  svgrid-studio add customers --db postgres --url $DATABASE_URL --sql
113
167
  svgrid-studio add --all --db supabase --url $DATABASE_URL
114
168
  `;
169
+ /**
170
+ * Terminal prompts for the guided `init`.
171
+ *
172
+ * Lines are queued from a listener attached up front rather than read one at a
173
+ * time with `rl.question()`: on a pipe or a redirected file, readline delivers
174
+ * the whole buffer at once, and anything arriving before the next question is
175
+ * asked would simply be dropped. Once input runs out we answer with each
176
+ * question's default, so a scripted run finishes instead of hanging.
177
+ */
178
+ function terminalPrompts() {
179
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
180
+ const waiting = [];
181
+ const buffered = [];
182
+ let ended = false;
183
+ rl.on('line', (line) => {
184
+ const next = waiting.shift();
185
+ if (next)
186
+ next(line);
187
+ else
188
+ buffered.push(line);
189
+ });
190
+ rl.on('close', () => {
191
+ ended = true;
192
+ for (const resolve of waiting.splice(0))
193
+ resolve(null);
194
+ });
195
+ const nextLine = () => {
196
+ if (buffered.length)
197
+ return Promise.resolve(buffered.shift());
198
+ if (ended)
199
+ return Promise.resolve(null);
200
+ return new Promise((resolve) => waiting.push(resolve));
201
+ };
202
+ return {
203
+ ask: async (question, def) => {
204
+ process.stdout.write(`${question}${def ? ` (${def})` : ''} `);
205
+ const line = await nextLine();
206
+ if (line === null) {
207
+ process.stdout.write(`${def ?? ''}\n`); // echo the default we fell back to
208
+ return def ?? '';
209
+ }
210
+ return line.trim() || def || '';
211
+ },
212
+ say: (line) => process.stdout.write(line + '\n'),
213
+ close: () => rl.close(),
214
+ };
215
+ }
216
+ /** Read an OpenAPI spec / REST response from a URL or a local file. */
217
+ const fetchText = async (source) => /^https?:\/\//.test(source) ? (await fetch(source)).text() : readFile(source, 'utf8');
218
+ /** The guided "build me a CRUD app" flow. */
219
+ async function runInit(flags) {
220
+ const prompts = terminalPrompts();
221
+ try {
222
+ const result = await runStudioInit(flags, prompts, {
223
+ ensureDriver: (dialect) => ensureDriverInstalled(dialect, process.cwd(), prompts.say),
224
+ connect: (dialect, url) => connect(dialect, url),
225
+ }, io, fetchText);
226
+ process.stdout.write('\nNext:\n');
227
+ for (const stepLine of result.nextSteps)
228
+ process.stdout.write(` ${stepLine}\n`);
229
+ }
230
+ finally {
231
+ prompts.close();
232
+ }
233
+ }
115
234
  function report(name, written, verifyLine) {
116
235
  process.stdout.write(`Scaffolded "${name}":\n`);
117
236
  for (const path of written)
@@ -121,6 +240,107 @@ function report(name, written, verifyLine) {
121
240
  async function main() {
122
241
  const [cmd, ...rest] = process.argv.slice(2);
123
242
  const opts = parse(rest);
243
+ // --- guided setup: the few-clicks path from nothing to a CRUD app ---------
244
+ // Bare `svgrid-studio` in a terminal starts here too: with no argument, the
245
+ // most useful thing we can do is walk the user through building an app.
246
+ if ((cmd === 'init' || (!cmd && process.stdin.isTTY)) && !opts.help) {
247
+ await runInit({
248
+ ...(opts.outDir ? { out: opts.outDir } : {}),
249
+ ...(opts.db ? { db: opts.db } : {}),
250
+ ...(opts.url ? { url: opts.url } : {}),
251
+ ...(opts.supabaseUrl ? { supabaseUrl: opts.supabaseUrl } : {}),
252
+ ...(opts.supabaseKey ? { supabaseKey: opts.supabaseKey } : {}),
253
+ ...(opts.dataset ? { dataset: opts.dataset } : {}),
254
+ ...(opts.theme ? { theme: opts.theme } : {}),
255
+ ...(opts.dark ? { dark: true } : {}),
256
+ ...(opts.title ? { title: opts.title } : {}),
257
+ ...(opts.yes ? { yes: true } : {}),
258
+ });
259
+ return;
260
+ }
261
+ // --- eject: write the app (or a fragment) from studio.config.json ---------
262
+ if (cmd === 'eject' && !opts.help) {
263
+ const configPath = opts.config ?? 'studio.config.json';
264
+ const json = await io.readFile(configPath);
265
+ if (json == null) {
266
+ process.stderr.write(`eject: ${configPath} not found - run \`svgrid-studio designer\` first, or pass --config.\n`);
267
+ process.exit(1);
268
+ }
269
+ const project = parseProject(json);
270
+ const outDir = opts.outDir ?? '.';
271
+ const files = opts.fragment ? emitStudioFragment(project) : emitStudioAppBundle(project);
272
+ for (const f of files) {
273
+ const full = resolve(outDir, f.path);
274
+ await mkdir(dirname(full), { recursive: true });
275
+ await writeFile(full, f.contents);
276
+ }
277
+ process.stdout.write(`Wrote ${files.length} ${opts.fragment ? 'fragment' : 'app'} file(s) -> ${resolve(outDir)}\n`);
278
+ if (opts.fragment) {
279
+ const deps = runtimeDeps(project, files.map((f) => f.contents).join('\n'));
280
+ const install = Object.keys(deps).filter((d) => d !== '@svgrid/grid' && d !== '@svgrid/enterprise');
281
+ process.stdout.write(`Fragment: merge src/routes + src/lib into your app, import src/app.css, then:\n npm install ${install.join(' ') || '@svgrid/grid @svgrid/enterprise'}\nSee FRAGMENT.md for the full runbook.\n`);
282
+ }
283
+ return;
284
+ }
285
+ // --- import an OpenAPI spec into a studio.config.json ---------------------
286
+ if (cmd === 'openapi' && !opts.help) {
287
+ const src = opts.from ?? rest.find((a) => !a.startsWith('-'));
288
+ if (!src) {
289
+ process.stderr.write('openapi: pass a spec - `svgrid-studio openapi ./openapi.json` (or a URL)\n');
290
+ process.exit(1);
291
+ }
292
+ const text = /^https?:\/\//.test(src) ? await (await fetch(src)).text() : await readFile(src, 'utf8');
293
+ const { entities, sources, warnings } = introspectOpenApi(text);
294
+ let project = createProject(entities, { title: 'API app' });
295
+ for (const [name, source] of Object.entries(sources))
296
+ project = setEntityDataSource(project, name, source);
297
+ const outPath = opts.config ?? 'studio.config.json';
298
+ await writeFile(outPath, serializeProject(project));
299
+ process.stdout.write(`Imported ${entities.length} REST resource(s) -> ${outPath}\n`);
300
+ for (const w of warnings)
301
+ process.stdout.write(` ! ${w}\n`);
302
+ process.stdout.write(`Next: svgrid-studio designer (open it visually) or svgrid-studio dev\n`);
303
+ return;
304
+ }
305
+ // --- live dev loop: designer + the real app, side by side ----------------
306
+ if (cmd === 'dev' && !opts.help) {
307
+ const configPath = opts.config ?? 'studio.config.json';
308
+ const outDir = opts.outDir ?? '.';
309
+ const port = Number.isFinite(opts.port) ? opts.port : 4321;
310
+ const appPort = Number.isFinite(opts.appPort) ? opts.appPort : port + 1;
311
+ const log = (l) => process.stdout.write(l + '\n');
312
+ const hasApp = await ensureApp(resolve(outDir), resolve(configPath), log);
313
+ let app = null;
314
+ if (hasApp) {
315
+ app = await startAppServer({ outDir, port: appPort, log });
316
+ log(`\n app: ${app.url} (Vite dev server - designer saves hot-reload it)`);
317
+ }
318
+ else {
319
+ log(`\n No app here yet - design something and hit "Save to folder"; the app server starts on the first save.`);
320
+ }
321
+ process.on('SIGINT', () => {
322
+ app?.stop();
323
+ process.exit(0);
324
+ });
325
+ await startDesignerServer({
326
+ configPath,
327
+ outDir,
328
+ port,
329
+ open: !opts.noOpen,
330
+ template: opts.template,
331
+ ai: !!opts.ai,
332
+ appUrl: app?.url,
333
+ onGenerated: async () => {
334
+ if (app)
335
+ await app.syncDeps();
336
+ else {
337
+ app = await startAppServer({ outDir, port: appPort, log });
338
+ log(` app: ${app.url} (started after first save)`);
339
+ }
340
+ },
341
+ });
342
+ return;
343
+ }
124
344
  // --- visual designer -----------------------------------------------------
125
345
  if (cmd === 'designer' && !opts.help) {
126
346
  await startDesignerServer({
@@ -129,10 +349,48 @@ async function main() {
129
349
  port: Number.isFinite(opts.port) ? opts.port : 4321,
130
350
  open: !opts.noOpen,
131
351
  template: opts.template,
352
+ ai: !!opts.ai,
132
353
  });
133
354
  // Keep the process alive; the server holds the event loop until Ctrl+C.
134
355
  return;
135
356
  }
357
+ // --- deploy --------------------------------------------------------------
358
+ if (cmd === 'deploy' && !opts.help) {
359
+ const read = (p) => io.readFile(p);
360
+ const resolution = resolveDeployTarget({
361
+ flag: opts.target,
362
+ configJson: await read('studio.config.json'),
363
+ svelteConfig: (await read('svelte.config.js')) ?? (await read('svelte.config.ts')),
364
+ });
365
+ if (!resolution.ok) {
366
+ process.stderr.write(`deploy: ${resolution.reason}\n`);
367
+ process.exit(1);
368
+ }
369
+ const plan = deployCommands(resolution.provider);
370
+ process.stdout.write(`Deploying to ${plan.provider} (from ${resolution.source === 'flag' ? '--target' : resolution.source === 'config' ? 'studio.config.json' : 'svelte.config.js adapter'}).\n`);
371
+ // Preflight: env keys the app declares but the local .env doesn't set.
372
+ const missing = missingEnvKeys(await read('.env.example'), await read('.env'));
373
+ if (missing.length)
374
+ process.stdout.write(`! Unset env keys (set them on the host too): ${missing.join(', ')}\n`);
375
+ for (const note of plan.notes)
376
+ process.stdout.write(` ${note}\n`);
377
+ const commands = [plan.build, ...(plan.deploy ? [plan.deploy] : [])];
378
+ if (opts.dryRun) {
379
+ process.stdout.write(commands.map((c) => `> ${c}\n`).join(''));
380
+ return;
381
+ }
382
+ for (const command of commands) {
383
+ process.stdout.write(`\n> ${command}\n`);
384
+ const res = spawnSync(command, { stdio: 'inherit', shell: true });
385
+ if (res.status !== 0) {
386
+ if (command !== plan.build) {
387
+ process.stderr.write(`deploy: "${command}" failed. If the provider CLI isn't set up yet, log in first (see the note above) and re-run.\n`);
388
+ }
389
+ process.exit(res.status ?? 1);
390
+ }
391
+ }
392
+ return;
393
+ }
136
394
  if (cmd !== 'add' || opts.help) {
137
395
  process.stdout.write(HELP);
138
396
  process.exit(opts.help || !cmd ? 0 : 1);
@@ -147,29 +405,10 @@ async function main() {
147
405
  // the user - they never need to know `pg`/`mysql2`/etc. must be present
148
406
  // before --db works (mirrors the designer's "Install driver" flow).
149
407
  const cwd = process.cwd();
150
- if (!isDriverInstalled(opts.db, cwd)) {
151
- const driver = DRIVER_FOR[opts.db];
152
- process.stdout.write(`Installing ${driver} (required for --db ${opts.db})...\n`);
153
- const result = await installDriver(opts.db, cwd);
154
- if (!result.ok) {
155
- process.stderr.write(`svgrid-studio: could not install "${driver}" automatically.\n${result.output}\n` +
156
- `Run \`${result.manager} install ${driver}\` yourself and try again.\n`);
157
- process.exit(1);
158
- }
159
- process.stdout.write(`Installed ${driver}.\n`);
160
- // The install child process can report success before the new module is
161
- // reliably resolvable (seen on Windows - antivirus/file-sync tools can
162
- // briefly hold the just-written files). Poll rather than racing straight
163
- // into connect(), which would resolve the driver too early and crash.
164
- const deadline = Date.now() + 5000;
165
- while (!isDriverInstalled(opts.db, cwd) && Date.now() < deadline) {
166
- await new Promise((r) => setTimeout(r, 200));
167
- }
168
- if (!isDriverInstalled(opts.db, cwd)) {
169
- process.stderr.write(`svgrid-studio: installed "${driver}" but it's still not resolvable from this directory.\n` +
170
- `Something (antivirus, a file-sync tool) may be holding a lock on the new files. Try running the command again.\n`);
171
- process.exit(1);
172
- }
408
+ const driver = await ensureDriverInstalled(opts.db, cwd, (l) => process.stdout.write(l + '\n'));
409
+ if (!driver.ok) {
410
+ process.stderr.write(`svgrid-studio: ${driver.message}\n`);
411
+ process.exit(1);
173
412
  }
174
413
  const execute = await connect(opts.db, opts.url);
175
414
  // A dialect dataSource emits a fully-connected +server.ts (driver + DATABASE_URL);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Copilot backend for the CLI designer (`svgrid-studio designer --ai`): calls
3
+ * Anthropic's Messages API with the request built by the tested enterprise core
4
+ * and returns the validated, serialized project. Dependency-free by design -
5
+ * a plain fetch, no SDK - so the CLI keeps running on bare Node. The API key
6
+ * stays server-side (read from ANTHROPIC_API_KEY by the designer server).
7
+ */
8
+ import { buildCopilotMessages, projectFromModelText, parseProject, serializeProject } from '@svgrid/enterprise/studio';
9
+ const API_URL = 'https://api.anthropic.com/v1/messages';
10
+ /** Overridable for teams pinning a model (SVGRID_COPILOT_MODEL). */
11
+ const DEFAULT_MODEL = 'claude-sonnet-5';
12
+ /** One copilot round: instruction + current project JSON -> updated project JSON. */
13
+ export async function runCopilot(prompt, projectJson, opts) {
14
+ const project = parseProject(projectJson);
15
+ const { system, messages } = buildCopilotMessages(prompt, project);
16
+ const doFetch = opts.fetchImpl ?? fetch;
17
+ const res = await doFetch(API_URL, {
18
+ method: 'POST',
19
+ headers: {
20
+ 'content-type': 'application/json',
21
+ 'x-api-key': opts.apiKey,
22
+ 'anthropic-version': '2023-06-01',
23
+ },
24
+ body: JSON.stringify({
25
+ model: opts.model ?? process.env.SVGRID_COPILOT_MODEL ?? DEFAULT_MODEL,
26
+ max_tokens: 32000,
27
+ system,
28
+ messages,
29
+ }),
30
+ });
31
+ if (!res.ok) {
32
+ const detail = await res.text().catch(() => '');
33
+ throw new Error(`Anthropic API ${res.status}: ${detail.slice(0, 300)}`);
34
+ }
35
+ const body = (await res.json());
36
+ const text = (body.content ?? [])
37
+ .filter((c) => c.type === 'text')
38
+ .map((c) => c.text ?? '')
39
+ .join('');
40
+ if (!text)
41
+ throw new Error('Anthropic API returned no text content.');
42
+ return serializeProject(projectFromModelText(text));
43
+ }
@@ -15,9 +15,11 @@ import { createReadStream, existsSync } from 'node:fs';
15
15
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
16
16
  import { dirname, join, normalize, resolve, extname } from 'node:path';
17
17
  import { createRequire } from 'node:module';
18
+ import { createHash } from 'node:crypto';
18
19
  import { spawn } from 'node:child_process';
19
- import { createProject, parseProject, serializeProject, introspectDatabase, listDatabaseTables, probeConnection, linkRelationLabels, buildConnectionString, getSampleApp, skipUserOwned, } from '@svgrid/enterprise/studio';
20
+ import { parseProject, serializeProject, introspectDatabase, listDatabaseTables, probeConnection, linkRelationLabels, buildConnectionString, getSampleApp, starterProject as buildStarterProject, skipUserOwned, } from '@svgrid/enterprise/studio';
20
21
  import { connect } from './db-connect.js';
22
+ import { runCopilot } from './copilot.js';
21
23
  import { DRIVER_FOR, installDriver, isDriverInstalled } from './driver-install.js';
22
24
  const MIME = {
23
25
  '.html': 'text/html; charset=utf-8',
@@ -32,6 +34,21 @@ const MIME = {
32
34
  '.woff2': 'font/woff2',
33
35
  '.map': 'application/json; charset=utf-8',
34
36
  };
37
+ /** Quote a SQL identifier for the dialect (doubling the closing quote char). */
38
+ function quoteIdent(dialect, name) {
39
+ if (dialect === 'mysql')
40
+ return '`' + name.replace(/`/g, '``') + '`';
41
+ if (dialect === 'mssql')
42
+ return '[' + name.replace(/]/g, ']]') + ']';
43
+ return '"' + name.replace(/"/g, '""') + '"'; // postgres / supabase / sqlite
44
+ }
45
+ /** A read-only preview SELECT for one table (optionally schema-qualified). No
46
+ * params - it's a fixed `SELECT * ... LIMIT n` (MSSQL uses `TOP n`). */
47
+ function buildPreviewSql(dialect, table, schema, limit) {
48
+ const t = schema ? `${quoteIdent(dialect, schema)}.${quoteIdent(dialect, table)}` : quoteIdent(dialect, table);
49
+ const n = Math.max(1, Math.min(200, Math.floor(limit)));
50
+ return dialect === 'mssql' ? `SELECT TOP ${n} * FROM ${t}` : `SELECT * FROM ${t} LIMIT ${n}`;
51
+ }
35
52
  /** The runtime deps the generated code imports, keyed by a source needle. */
36
53
  const RUNTIME_DEPS = [
37
54
  ["from '@supabase/supabase-js'", '@supabase/supabase-js', '^2.45.0'],
@@ -50,26 +67,16 @@ function resolveDesignerDir() {
50
67
  }
51
68
  return dir;
52
69
  }
53
- /** The project a fresh session opens with: a chosen sample app (`--template`), or
54
- * a minimal tasks starter. */
70
+ /** The project a fresh session opens with: a chosen sample app (`--template`), or the
71
+ * default enterprise-ready Customers CRUD showcase (rich grid + export + popup form,
72
+ * seeded) - so the first screen is a complete, generate-and-copy-ready SvGrid app. */
55
73
  function starterProject(templateId) {
56
74
  if (templateId) {
57
75
  const app = getSampleApp(templateId);
58
76
  if (app)
59
77
  return app.build();
60
78
  }
61
- const tasks = {
62
- name: 'tasks',
63
- label: 'Task',
64
- idField: 'id',
65
- fields: [
66
- { field: 'id', type: 'number', primaryKey: true, readonly: true },
67
- { field: 'title', type: 'text', required: true },
68
- { field: 'status', type: 'enum', options: [{ value: 'todo', label: 'To do' }, { value: 'doing', label: 'Doing' }, { value: 'done', label: 'Done' }] },
69
- { field: 'due', type: 'date' },
70
- ],
71
- };
72
- return createProject([tasks], { title: 'My App' });
79
+ return buildStarterProject();
73
80
  }
74
81
  function send(res, status, body, type = 'application/json; charset=utf-8') {
75
82
  res.writeHead(status, { 'content-type': type });
@@ -95,8 +102,24 @@ function serveStatic(res, dir, urlPath) {
95
102
  res.writeHead(200, { 'content-type': MIME[extname(filePath)] ?? 'application/octet-stream' });
96
103
  createReadStream(filePath).pipe(res);
97
104
  }
98
- /** Write the generated app bundle to disk and add any runtime deps it imports. */
99
- async function writeBundle(outDir, files) {
105
+ /** Write the generated app bundle to disk and add any runtime deps it imports.
106
+ *
107
+ * Clobber guard: `.studio/manifest.json` records the hash of every file Studio
108
+ * last wrote. On regenerate, a file the USER has since edited (disk differs from
109
+ * the manifest) gets the new version written as `<file>.new` + reported as a
110
+ * conflict, instead of silently overwriting their work. Files Studio wrote and
111
+ * nobody touched are overwritten as before. */
112
+ export async function writeBundle(outDir, files) {
113
+ const manifestPath = join(outDir, '.studio', 'manifest.json');
114
+ let manifest = {};
115
+ try {
116
+ manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
117
+ }
118
+ catch {
119
+ /* first generate (or pre-manifest project) - guard activates from now on */
120
+ }
121
+ const sha = (s) => createHash('sha256').update(s).digest('hex');
122
+ const conflicts = [];
100
123
  let written = 0;
101
124
  for (const f of files) {
102
125
  const full = join(outDir, f.path);
@@ -105,10 +128,30 @@ async function writeBundle(outDir, files) {
105
128
  // the make-or-break round-trip guarantee. See HANDLERS-DESIGN.md.
106
129
  if (skipUserOwned(f, existsSync(full)))
107
130
  continue;
131
+ const newHash = sha(f.contents);
132
+ if (existsSync(full)) {
133
+ const onDisk = await readFile(full, 'utf8').catch(() => null);
134
+ const diskHash = onDisk == null ? null : sha(onDisk);
135
+ if (diskHash === newHash) {
136
+ manifest[f.path] = newHash; // already identical - nothing to write
137
+ continue;
138
+ }
139
+ const lastGenerated = manifest[f.path];
140
+ if (lastGenerated !== undefined && diskHash !== null && diskHash !== lastGenerated) {
141
+ // Locally modified since the last generate AND the regenerated content
142
+ // differs: park it next to the user's version instead of clobbering.
143
+ await writeFile(full + '.new', f.contents);
144
+ conflicts.push(f.path);
145
+ continue;
146
+ }
147
+ }
108
148
  await mkdir(dirname(full), { recursive: true });
109
149
  await writeFile(full, f.contents);
150
+ manifest[f.path] = newHash;
110
151
  written++;
111
152
  }
153
+ await mkdir(dirname(manifestPath), { recursive: true });
154
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
112
155
  const allSource = files.map((f) => f.contents).join('\n');
113
156
  const pkgPath = join(outDir, 'package.json');
114
157
  if (existsSync(pkgPath)) {
@@ -128,7 +171,7 @@ async function writeBundle(outDir, files) {
128
171
  // leave package.json untouched if it isn't valid JSON
129
172
  }
130
173
  }
131
- return written;
174
+ return { written, conflicts };
132
175
  }
133
176
  /** Best-effort open the default browser (no dependency). */
134
177
  function openBrowser(url) {
@@ -152,9 +195,33 @@ export async function startDesignerServer(opts) {
152
195
  send(res, 500, JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
153
196
  });
154
197
  });
198
+ // Copilot is live only when BOTH the flag and the key are present; the SPA
199
+ // probes /api/capabilities and only then wires (and shows) the Copilot button.
200
+ const copilotKey = process.env.ANTHROPIC_API_KEY;
201
+ const copilotOn = !!opts.ai && !!copilotKey;
155
202
  async function handle(req, res) {
156
203
  const url = req.url ?? '/';
157
204
  const path = url.split('?')[0];
205
+ if (path === '/api/capabilities' && req.method === 'GET') {
206
+ send(res, 200, JSON.stringify({ copilot: copilotOn, ...(opts.appUrl ? { appUrl: opts.appUrl } : {}) }));
207
+ return;
208
+ }
209
+ if (path === '/api/copilot' && req.method === 'POST') {
210
+ if (!copilotOn) {
211
+ send(res, 404, JSON.stringify({ error: 'Copilot is not enabled. Start with --ai and set ANTHROPIC_API_KEY.' }));
212
+ return;
213
+ }
214
+ const body = await readBody(req);
215
+ const { prompt, project } = JSON.parse(body);
216
+ if (!prompt || !project) {
217
+ send(res, 400, JSON.stringify({ error: 'prompt and project are required' }));
218
+ return;
219
+ }
220
+ log(` copilot: ${prompt.slice(0, 80)}`);
221
+ const updated = await runCopilot(prompt, project, { apiKey: copilotKey });
222
+ send(res, 200, updated);
223
+ return;
224
+ }
158
225
  if (path === '/api/project' && req.method === 'GET') {
159
226
  const json = existsSync(configPath) ? await readFile(configPath, 'utf8') : null;
160
227
  // Round-trip through parse/serialize so we hand the client a normalized,
@@ -179,9 +246,18 @@ export async function startDesignerServer(opts) {
179
246
  send(res, 400, JSON.stringify({ error: 'no files to write' }));
180
247
  return;
181
248
  }
182
- const count = await writeBundle(outDir, files);
183
- log(` generated ${count} files -> ${outDir}`);
184
- send(res, 200, JSON.stringify({ ok: true, count, outDir }));
249
+ const { written, conflicts } = await writeBundle(outDir, files);
250
+ log(` generated ${written} files -> ${outDir}`);
251
+ if (conflicts.length) {
252
+ log(` ! ${conflicts.length} file(s) were edited by hand since the last generate - the new`);
253
+ log(` version was written NEXT TO yours as "<file>.new" (merge or replace, then delete it):`);
254
+ for (const c of conflicts)
255
+ log(` - ${c} -> ${c}.new`);
256
+ }
257
+ // `studio dev`: give the app-server loop a chance to install newly added
258
+ // deps + restart; plain file rewrites are picked up by Vite's HMR.
259
+ await opts.onGenerated?.();
260
+ send(res, 200, JSON.stringify({ ok: true, count: written, conflicts, outDir }));
185
261
  return;
186
262
  }
187
263
  // Assemble a connection string from guided-form fields (host/port/user/...).
@@ -249,6 +325,23 @@ export async function startDesignerServer(opts) {
249
325
  }
250
326
  return;
251
327
  }
328
+ if (action === 'preview') {
329
+ // "Preview data": run a real SELECT so the designer canvas shows the user's
330
+ // actual rows (the SQL equivalent of the Supabase/REST live preview).
331
+ const table = Array.isArray(reqBody.tables) ? reqBody.tables[0] : undefined;
332
+ if (!table) {
333
+ send(res, 400, JSON.stringify({ error: 'a table is required for preview' }));
334
+ return;
335
+ }
336
+ try {
337
+ const rows = await execute(buildPreviewSql(dialect, table, reqBody.schema, 25), []);
338
+ send(res, 200, JSON.stringify({ rows }));
339
+ }
340
+ catch (err) {
341
+ send(res, 502, JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
342
+ }
343
+ return;
344
+ }
252
345
  if (action === 'introspect') {
253
346
  const tables = Array.isArray(reqBody.tables) ? reqBody.tables : [];
254
347
  const schemas = [];
@@ -297,6 +390,10 @@ export async function startDesignerServer(opts) {
297
390
  log(`\n SvGrid Studio designer running at ${url}`);
298
391
  log(` config: ${configPath}`);
299
392
  log(` output: ${outDir}`);
393
+ if (copilotOn)
394
+ log(` copilot: ON (Anthropic)`);
395
+ else if (opts.ai)
396
+ log(` copilot: OFF - --ai was passed but ANTHROPIC_API_KEY is not set`);
300
397
  log(` (edits auto-save; "Save to folder" writes the app. Ctrl+C to stop.)\n`);
301
398
  if (opts.open)
302
399
  openBrowser(url);
package/dist/dev.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * `svgrid-studio dev` - the live loop: the visual designer AND the real generated
3
+ * app's Vite dev server running side by side. Designer saves write real files
4
+ * into the app folder (targeted, hash-guarded writes), Vite picks them up as
5
+ * HMR - so the designer edits the actual running app, not a simulation.
6
+ *
7
+ * Dependency-free like the rest of the CLI: node:child_process + the existing
8
+ * package-manager/driver plumbing.
9
+ */
10
+ import { spawn, spawnSync } from 'node:child_process';
11
+ import { existsSync } from 'node:fs';
12
+ import { mkdir } from 'node:fs/promises';
13
+ import { join, resolve } from 'node:path';
14
+ import { createRequire } from 'node:module';
15
+ import { emitStudioAppBundle, parseProject } from '@svgrid/enterprise/studio';
16
+ import { readFile } from 'node:fs/promises';
17
+ import { writeBundle } from './designer-server.js';
18
+ import { detectPackageManager } from './driver-install.js';
19
+ const isWin = process.platform === 'win32';
20
+ /** Ensure the folder holds a generated app; generate from studio.config.json when
21
+ * it doesn't. Returns false when there's nothing to run yet (no app, no config). */
22
+ export async function ensureApp(outDir, configPath, log) {
23
+ if (existsSync(join(outDir, 'package.json')))
24
+ return true;
25
+ if (!existsSync(configPath))
26
+ return false;
27
+ const project = parseProject(await readFile(configPath, 'utf8'));
28
+ await mkdir(outDir, { recursive: true });
29
+ const { written } = await writeBundle(outDir, emitStudioAppBundle(project));
30
+ log(` generated the app from ${configPath} (${written} files)`);
31
+ return true;
32
+ }
33
+ /** Deps declared in the app's package.json that aren't installed yet. Includes
34
+ * devDependencies - `vite`/`svelte-kit` live there and `dev` can't run without
35
+ * them (a wiped-then-partially-restored node_modules is a real scenario). */
36
+ export function missingDeps(outDir) {
37
+ try {
38
+ const req = createRequire(join(resolve(outDir), 'package.json'));
39
+ const pkg = req('./package.json');
40
+ return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).filter((dep) => {
41
+ try {
42
+ req.resolve(join(dep, 'package.json'));
43
+ return false;
44
+ }
45
+ catch {
46
+ try {
47
+ req.resolve(dep);
48
+ return false;
49
+ }
50
+ catch {
51
+ return true;
52
+ }
53
+ }
54
+ });
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ }
60
+ function install(outDir, log) {
61
+ const pm = detectPackageManager(outDir);
62
+ log(` installing dependencies (${pm} install)...`);
63
+ const res = spawnSync(`${pm} install`, { cwd: outDir, stdio: 'inherit', shell: true });
64
+ if (res.status !== 0)
65
+ throw new Error(`${pm} install failed (exit ${res.status ?? 'signal'})`);
66
+ }
67
+ function spawnVite(outDir, port, log) {
68
+ const pm = detectPackageManager(outDir);
69
+ // `-- --port` threads through the package script to vite; strictPort so a
70
+ // taken port errors loudly instead of silently drifting to another one.
71
+ const child = spawn(`${pm} run dev -- --port ${port} --strictPort`, { cwd: outDir, shell: true });
72
+ const prefix = (data) => {
73
+ for (const line of String(data).split(/\r?\n/))
74
+ if (line.trim())
75
+ log(` [app] ${line}`);
76
+ };
77
+ child.stdout?.on('data', prefix);
78
+ child.stderr?.on('data', prefix);
79
+ return child;
80
+ }
81
+ /** Kill the dev-server process tree (shell:true leaves grandchildren on Windows). */
82
+ function killTree(child) {
83
+ if (child.pid == null)
84
+ return;
85
+ if (isWin)
86
+ spawnSync(`taskkill /pid ${child.pid} /T /F`, { shell: true, stdio: 'ignore' });
87
+ else
88
+ child.kill('SIGTERM');
89
+ }
90
+ /** The `npm run dev` launcher must actually exist - a package can resolve while
91
+ * its .bin shim is gone (partially wiped node_modules), which fails with a
92
+ * cryptic "'vite' is not recognized". */
93
+ function viteBinMissing(outDir) {
94
+ const bin = join(outDir, 'node_modules', '.bin');
95
+ return !existsSync(join(bin, 'vite')) && !existsSync(join(bin, 'vite.cmd')) && !existsSync(join(bin, 'vite.CMD'));
96
+ }
97
+ /** Start the generated app's dev server (installing first when needed). */
98
+ export async function startAppServer(opts) {
99
+ const log = opts.log ?? ((l) => process.stdout.write(l + '\n'));
100
+ const outDir = resolve(opts.outDir);
101
+ if (!existsSync(join(outDir, 'node_modules')) || viteBinMissing(outDir) || missingDeps(outDir).length)
102
+ install(outDir, log);
103
+ let child = spawnVite(outDir, opts.port, log);
104
+ let stopped = false;
105
+ child.on('exit', (code) => {
106
+ if (!stopped && code !== 0 && code != null)
107
+ log(` [app] dev server exited with code ${code}`);
108
+ });
109
+ return {
110
+ url: `http://localhost:${opts.port}`,
111
+ async syncDeps() {
112
+ const missing = missingDeps(outDir);
113
+ if (!missing.length)
114
+ return; // nothing new - the running server HMRs the file writes
115
+ log(` new dependenc${missing.length === 1 ? 'y' : 'ies'} (${missing.join(', ')}) - installing + restarting the app server...`);
116
+ killTree(child);
117
+ install(outDir, log);
118
+ child = spawnVite(outDir, opts.port, log);
119
+ },
120
+ stop() {
121
+ stopped = true;
122
+ killTree(child);
123
+ },
124
+ };
125
+ }
@@ -61,6 +61,42 @@ export function isDriverInstalled(dialect, cwd) {
61
61
  return false;
62
62
  }
63
63
  }
64
+ /**
65
+ * Make sure the dialect's driver is installed and actually resolvable, installing
66
+ * it if needed. Shared by `add --db` and the guided `init`, so a user connecting
67
+ * to Postgres never has to know `pg` must be present first.
68
+ *
69
+ * Never rejects: reports `{ ok: false, message }` the caller can print.
70
+ */
71
+ export async function ensureDriverInstalled(dialect, cwd, log = () => { }) {
72
+ if (isDriverInstalled(dialect, cwd))
73
+ return { ok: true };
74
+ const driver = DRIVER_FOR[dialect];
75
+ log(`Installing ${driver} (needed to connect to ${dialect})...`);
76
+ const result = await installDriver(dialect, cwd);
77
+ if (!result.ok) {
78
+ return {
79
+ ok: false,
80
+ message: `Could not install "${driver}" automatically.\n${result.output}\nRun \`${result.manager} install ${driver}\` yourself and try again.`,
81
+ };
82
+ }
83
+ log(`Installed ${driver}.`);
84
+ // The install child process can report success before the new module is
85
+ // reliably resolvable (seen on Windows - antivirus/file-sync tools can briefly
86
+ // hold the just-written files). Poll rather than racing straight into
87
+ // connect(), which would resolve the driver too early and crash.
88
+ const deadline = Date.now() + 5000;
89
+ while (!isDriverInstalled(dialect, cwd) && Date.now() < deadline) {
90
+ await new Promise((r) => setTimeout(r, 200));
91
+ }
92
+ if (!isDriverInstalled(dialect, cwd)) {
93
+ return {
94
+ ok: false,
95
+ message: `Installed "${driver}" but it's still not resolvable from this directory.\nSomething (antivirus, a file-sync tool) may be holding a lock on the new files. Try running the command again.`,
96
+ };
97
+ }
98
+ return { ok: true };
99
+ }
64
100
  /**
65
101
  * Install the dialect's driver into `cwd`. No-ops (ok:true) when it's already
66
102
  * present. Never rejects: a failed install resolves with `ok:false` and the
package/package.json CHANGED
@@ -4,14 +4,14 @@
4
4
  "type": "commercial",
5
5
  "url": "https://svgrid.com/pricing"
6
6
  },
7
- "version": "0.2.0",
7
+ "version": "0.4.0",
8
8
  "description": "SvGrid Studio CLI: scaffold a full CRUD screen (grid + edit panel + SvelteKit API route) from a Drizzle schema in one command.",
9
9
  "license": "SEE LICENSE IN LICENSE",
10
10
  "author": "jQWidgets <sales@jqwidgets.com>",
11
11
  "homepage": "https://svgrid.com",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "https://github.com/sv-grid/sv-grid.git",
14
+ "url": "git+https://github.com/sv-grid/sv-grid.git",
15
15
  "directory": "packages/studio"
16
16
  },
17
17
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@svgrid/enterprise": "^2.2.0"
30
+ "@svgrid/enterprise": "^2.4.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.10.7",
@@ -36,8 +36,27 @@
36
36
  "engines": {
37
37
  "node": ">=18"
38
38
  },
39
+ "keywords": [
40
+ "svelte",
41
+ "svelte5",
42
+ "sveltekit",
43
+ "crud",
44
+ "scaffold",
45
+ "codegen",
46
+ "drizzle",
47
+ "prisma",
48
+ "admin panel",
49
+ "low-code",
50
+ "data app",
51
+ "data grid",
52
+ "datagrid",
53
+ "studio",
54
+ "svgrid",
55
+ "sv-grid"
56
+ ],
39
57
  "scripts": {
40
58
  "build": "tsc -p tsconfig.json",
41
- "test:types": "tsc -p tsconfig.json --noEmit"
59
+ "test:types": "tsc -p tsconfig.json --noEmit",
60
+ "lint": "eslint ./src"
42
61
  }
43
62
  }