@svgrid/studio 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,8 +19,23 @@
19
19
 
20
20
  ---
21
21
 
22
- Scaffold a full CRUD screen for SvelteKit from a **live database** or a
23
- **Drizzle / Prisma schema** in **one command**.
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**:
24
39
 
25
40
  ```bash
26
41
  # from a live database (PostgreSQL / Supabase / MySQL / SQL Server / SQLite)
package/dist/cli.js CHANGED
@@ -12,10 +12,11 @@
12
12
  */
13
13
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
14
14
  import { spawnSync } from 'node:child_process';
15
+ import { createInterface } from 'node:readline/promises';
15
16
  import { dirname, resolve } from 'node:path';
16
- import { buildStudioBugReport, createProject, deployCommands, emitStudioAppBundle, emitStudioFragment, introspectDatabase, introspectOpenApi, runtimeDeps, listDatabaseTables, missingEnvKeys, parseProject, resolveDeployTarget, resolveSchemas, runStudioAdd, runStudioAddApp, serializeProject, setEntityDataSource, summarizeVerify, } from '@svgrid/enterprise/studio';
17
+ import { buildStudioBugReport, createProject, deployCommands, emitStudioAppBundle, emitStudioFragment, introspectDatabase, introspectOpenApi, isUserError, runtimeDeps, listDatabaseTables, missingEnvKeys, parseProject, resolveDeployTarget, resolveSchemas, runStudioAdd, runStudioAddApp, runStudioInit, sampleApps, serializeProject, setEntityDataSource, summarizeVerify, UserError, } from '@svgrid/enterprise/studio';
17
18
  import { connect } from './db-connect.js';
18
- import { DRIVER_FOR, installDriver, isDriverInstalled } from './driver-install.js';
19
+ import { ensureDriverInstalled } from './driver-install.js';
19
20
  import { startDesignerServer } from './designer-server.js';
20
21
  import { ensureApp, startAppServer } from './dev.js';
21
22
  const io = {
@@ -35,44 +36,75 @@ const io = {
35
36
  function parse(args) {
36
37
  const out = {};
37
38
  const positional = [];
38
- for (let i = 0; i < args.length; i++) {
39
+ /**
40
+ * Read the value that follows a flag. A missing or flag-shaped value used to
41
+ * come back as `undefined` and read as "not passed", so `--url` at the end of
42
+ * the line silently fell through to a different code path instead of saying
43
+ * what was wrong.
44
+ */
45
+ let i = 0;
46
+ const value = (flag) => {
47
+ const next = args[i + 1];
48
+ if (next === undefined || next.startsWith('-')) {
49
+ throw new UserError(`${flag} needs a value.`, 'Run `svgrid-studio --help` to see what each flag expects.');
50
+ }
51
+ i++;
52
+ return next;
53
+ };
54
+ for (i = 0; i < args.length; i++) {
39
55
  const a = args[i];
40
56
  if (a === '--from')
41
- out.from = args[++i];
57
+ out.from = value(a);
42
58
  else if (a === '--table')
43
- out.table = args[++i];
59
+ out.table = value(a);
44
60
  else if (a === '--route')
45
- out.route = args[++i];
61
+ out.route = value(a);
46
62
  else if (a === '--api')
47
- out.apiRoute = args[++i];
63
+ out.apiRoute = value(a);
48
64
  else if (a === '--sql')
49
65
  out.dataSource = 'sql';
50
66
  else if (a === '--db')
51
- out.db = args[++i];
67
+ out.db = value(a);
52
68
  else if (a === '--url')
53
- out.url = args[++i];
69
+ out.url = value(a);
54
70
  else if (a === '--all')
55
71
  out.all = true;
56
72
  else if (a === '--config')
57
- out.config = args[++i];
73
+ out.config = value(a);
58
74
  else if (a === '--out')
59
- out.outDir = args[++i];
75
+ out.outDir = value(a);
60
76
  else if (a === '--port')
61
- out.port = Number(args[++i]);
77
+ out.port = Number(value(a));
62
78
  else if (a === '--template')
63
- out.template = args[++i];
79
+ out.template = value(a);
64
80
  else if (a === '--no-open')
65
81
  out.noOpen = true;
66
82
  else if (a === '--target')
67
- out.target = args[++i];
83
+ out.target = value(a);
68
84
  else if (a === '--dry-run')
69
85
  out.dryRun = true;
70
86
  else if (a === '--ai')
71
87
  out.ai = true;
72
88
  else if (a === '--app-port')
73
- out.appPort = Number(args[++i]);
89
+ out.appPort = Number(value(a));
74
90
  else if (a === '--fragment')
75
91
  out.fragment = true;
92
+ else if (a === '--supabase-url')
93
+ out.supabaseUrl = value(a);
94
+ else if (a === '--supabase-key')
95
+ out.supabaseKey = value(a);
96
+ else if (a === '--dataset')
97
+ out.dataset = value(a);
98
+ else if (a === '--theme')
99
+ out.theme = value(a);
100
+ else if (a === '--dark')
101
+ out.dark = true;
102
+ else if (a === '--title')
103
+ out.title = value(a);
104
+ else if (a === '-f' || a === '--force')
105
+ out.force = true;
106
+ else if (a === '-y' || a === '--yes')
107
+ out.yes = true;
76
108
  else if (a === '-h' || a === '--help')
77
109
  out.help = true;
78
110
  else if (!a.startsWith('-'))
@@ -85,56 +117,161 @@ function parse(args) {
85
117
  out.table = name;
86
118
  return out;
87
119
  }
88
- const HELP = `svgrid-studio - scaffold CRUD screens from a schema file or a live database
89
-
90
- Usage:
91
- svgrid-studio designer # open the visual designer in your browser
92
- svgrid-studio add <name> --from <schema> # one table/model from a schema file
93
- svgrid-studio add --all --from <schema> # every table/model, linked
94
- svgrid-studio add <name> --db <dialect> --url <conn> # one table from a live database
95
- svgrid-studio add --all --db <dialect> --url <conn> # every table from a live database
96
- svgrid-studio openapi <file|url> # import an OpenAPI (JSON) spec -> studio.config.json
97
- svgrid-studio eject [--fragment] # write the app (or a drop-in fragment) from studio.config.json
98
- svgrid-studio dev # designer + the RUNNING app, side by side (HMR)
99
- svgrid-studio deploy [--target <provider>] [--dry-run] # build + deploy via the provider CLI
100
-
101
- Deploy (build first, then the provider CLI; target from --target, else
102
- studio.config.json, else the adapter in svelte.config.js):
103
- --target <p> vercel | netlify | cloudflare | node
104
- --dry-run print the resolved commands without running anything
105
-
106
- Designer (visual app builder, auto-saves to studio.config.json):
107
- --template <id> open a ready-made sample app (crm | ecommerce | projects | support)
108
- --config <path> studio.config.json to load + auto-save (default: ./studio.config.json)
109
- --out <dir> folder to write the generated app into (default: .)
110
- --port <n> port to serve on (default: 4321)
111
- --no-open don't open the browser
112
- --ai enable the AI copilot (needs ANTHROPIC_API_KEY in the environment)
113
- --app-port <n> dev: port for the generated app's dev server (default: designer port + 1)
114
-
115
- Schema files: a Drizzle schema.ts or a Prisma schema.prisma (auto-detected).
116
- Foreign keys become searchable relation lookups; enums become select fields.
117
-
118
- Databases: postgres | supabase | mysql | mssql | sqlite
119
- (the matching driver - pg / mysql2 / mssql / better-sqlite3 - must be installed)
120
-
121
- Options:
122
- --from <path> Drizzle (.ts) or Prisma (.prisma) schema file to introspect
123
- --db <dialect> Connect to a live database and read its catalog
124
- --url <conn> Connection string / file path for --db
125
- --all Scaffold a screen for every table/model (+ nav & home)
126
- --table <name> Which table/model to use (defaults to <name>)
127
- --sql Wire a real SQL data source (default with --db)
128
- --route <seg> Route segment (default: <name> / table name)
129
- --api <path> API route path (default: /api/<route>)
130
- -h, --help Show this help
131
-
132
- Examples:
133
- svgrid-studio add customers --from src/lib/db/schema.ts
134
- svgrid-studio add --all --from prisma/schema.prisma
135
- svgrid-studio add customers --db postgres --url $DATABASE_URL --sql
136
- svgrid-studio add --all --db supabase --url $DATABASE_URL
120
+ /**
121
+ * Every sample id, wrapped so the help stays readable. Built from the registry
122
+ * rather than a hand-kept list: a new sample used to be invisible here until
123
+ * someone remembered to update the sentence.
124
+ */
125
+ function templateIds(width = 62) {
126
+ const lines = [];
127
+ let line = '';
128
+ for (const id of sampleApps.map((s) => s.id)) {
129
+ const next = line ? `${line} | ${id}` : id;
130
+ if (next.length > width) {
131
+ lines.push(line);
132
+ line = id;
133
+ }
134
+ else {
135
+ line = next;
136
+ }
137
+ }
138
+ if (line)
139
+ lines.push(line);
140
+ return lines.join('\n ');
141
+ }
142
+ const HELP = `svgrid-studio - scaffold CRUD screens from a schema file or a live database
143
+
144
+ Usage:
145
+ svgrid-studio init # guided: pick your data, get a working CRUD app
146
+ svgrid-studio designer # open the visual designer in your browser
147
+ svgrid-studio add <name> --from <schema> # one table/model from a schema file
148
+ svgrid-studio add --all --from <schema> # every table/model, linked
149
+ svgrid-studio add <name> --db <dialect> --url <conn> # one table from a live database
150
+ svgrid-studio add --all --db <dialect> --url <conn> # every table from a live database
151
+ svgrid-studio openapi <file|url> # import an OpenAPI (JSON) spec -> studio.config.json
152
+ svgrid-studio eject [--fragment] # write the app (or a drop-in fragment) from studio.config.json
153
+ svgrid-studio dev # designer + the RUNNING app, side by side (HMR)
154
+ svgrid-studio deploy [--target <provider>] [--dry-run] # build + deploy via the provider CLI
155
+
156
+ Guided setup (init) - asks where your data lives, which tables you want, and
157
+ which pages each gets, then writes a runnable app + studio.config.json.
158
+ Running \`svgrid-studio\` with no arguments starts it too.
159
+ --db <dialect> --url <conn> skip the questions and read a live database
160
+ --supabase-url <url> --supabase-key <anon> read a Supabase project over its
161
+ REST API (no driver, works without a local database)
162
+ --dataset <id> start from sample data (customers-orders, products-categories,
163
+ projects-tasks, employees-departments, tickets-accounts)
164
+ --title <name> app name
165
+ --out <dir> folder to write the app into (default: .)
166
+ --theme <id> design-system preset --dark dark mode
167
+ -y, --yes take every default, ask nothing
168
+ -f, --force write into a folder that already holds another app
169
+
170
+ Deploy (build first, then the provider CLI; target from --target, else
171
+ studio.config.json, else the adapter in svelte.config.js):
172
+ --target <p> vercel | netlify | cloudflare | node
173
+ --dry-run print the resolved commands without running anything
174
+
175
+ Designer (visual app builder, auto-saves to studio.config.json):
176
+ --template <id> open a ready-made sample app:
177
+ ${templateIds()}
178
+ --config <path> studio.config.json to load + auto-save (default: ./studio.config.json)
179
+ --out <dir> folder to write the generated app into (default: .)
180
+ --port <n> port to serve on (default: 4321)
181
+ --no-open don't open the browser
182
+ --ai enable the AI copilot (needs ANTHROPIC_API_KEY in the environment)
183
+ --app-port <n> dev: port for the generated app's dev server (default: designer port + 1)
184
+
185
+ Schema files: a Drizzle schema.ts or a Prisma schema.prisma (auto-detected).
186
+ Foreign keys become searchable relation lookups; enums become select fields.
187
+
188
+ Databases: postgres | supabase | mysql | mssql | sqlite
189
+ (the matching driver - pg / mysql2 / mssql / better-sqlite3 - must be installed)
190
+
191
+ Options:
192
+ --from <path> Drizzle (.ts) or Prisma (.prisma) schema file to introspect
193
+ --db <dialect> Connect to a live database and read its catalog
194
+ --url <conn> Connection string / file path for --db
195
+ --all Scaffold a screen for every table/model (+ nav & home)
196
+ --table <name> Which table/model to use (defaults to <name>)
197
+ --sql Wire a real SQL data source (default with --db)
198
+ --route <seg> Route segment (default: <name> / table name)
199
+ --api <path> API route path (default: /api/<route>)
200
+ -h, --help Show this help
201
+
202
+ Examples:
203
+ svgrid-studio init
204
+ svgrid-studio init --db postgres --url $DATABASE_URL --out my-app
205
+ svgrid-studio add customers --from src/lib/db/schema.ts
206
+ svgrid-studio add --all --from prisma/schema.prisma
207
+ svgrid-studio add customers --db postgres --url $DATABASE_URL --sql
208
+ svgrid-studio add --all --db supabase --url $DATABASE_URL
137
209
  `;
210
+ /**
211
+ * Terminal prompts for the guided `init`.
212
+ *
213
+ * Lines are queued from a listener attached up front rather than read one at a
214
+ * time with `rl.question()`: on a pipe or a redirected file, readline delivers
215
+ * the whole buffer at once, and anything arriving before the next question is
216
+ * asked would simply be dropped. Once input runs out we answer with each
217
+ * question's default, so a scripted run finishes instead of hanging.
218
+ */
219
+ function terminalPrompts() {
220
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
221
+ const waiting = [];
222
+ const buffered = [];
223
+ let ended = false;
224
+ rl.on('line', (line) => {
225
+ const next = waiting.shift();
226
+ if (next)
227
+ next(line);
228
+ else
229
+ buffered.push(line);
230
+ });
231
+ rl.on('close', () => {
232
+ ended = true;
233
+ for (const resolve of waiting.splice(0))
234
+ resolve(null);
235
+ });
236
+ const nextLine = () => {
237
+ if (buffered.length)
238
+ return Promise.resolve(buffered.shift());
239
+ if (ended)
240
+ return Promise.resolve(null);
241
+ return new Promise((resolve) => waiting.push(resolve));
242
+ };
243
+ return {
244
+ ask: async (question, def) => {
245
+ process.stdout.write(`${question}${def ? ` (${def})` : ''} `);
246
+ const line = await nextLine();
247
+ if (line === null) {
248
+ process.stdout.write(`${def ?? ''}\n`); // echo the default we fell back to
249
+ return def ?? '';
250
+ }
251
+ return line.trim() || def || '';
252
+ },
253
+ say: (line) => process.stdout.write(line + '\n'),
254
+ close: () => rl.close(),
255
+ };
256
+ }
257
+ /** Read an OpenAPI spec / REST response from a URL or a local file. */
258
+ const fetchText = async (source) => /^https?:\/\//.test(source) ? (await fetch(source)).text() : readFile(source, 'utf8');
259
+ /** The guided "build me a CRUD app" flow. */
260
+ async function runInit(flags) {
261
+ const prompts = terminalPrompts();
262
+ try {
263
+ const result = await runStudioInit(flags, prompts, {
264
+ ensureDriver: (dialect) => ensureDriverInstalled(dialect, process.cwd(), prompts.say),
265
+ connect: (dialect, url) => connect(dialect, url),
266
+ }, io, fetchText);
267
+ process.stdout.write('\nNext:\n');
268
+ for (const stepLine of result.nextSteps)
269
+ process.stdout.write(` ${stepLine}\n`);
270
+ }
271
+ finally {
272
+ prompts.close();
273
+ }
274
+ }
138
275
  function report(name, written, verifyLine) {
139
276
  process.stdout.write(`Scaffolded "${name}":\n`);
140
277
  for (const path of written)
@@ -144,6 +281,25 @@ function report(name, written, verifyLine) {
144
281
  async function main() {
145
282
  const [cmd, ...rest] = process.argv.slice(2);
146
283
  const opts = parse(rest);
284
+ // --- guided setup: the few-clicks path from nothing to a CRUD app ---------
285
+ // Bare `svgrid-studio` in a terminal starts here too: with no argument, the
286
+ // most useful thing we can do is walk the user through building an app.
287
+ if ((cmd === 'init' || (!cmd && process.stdin.isTTY)) && !opts.help) {
288
+ await runInit({
289
+ ...(opts.outDir ? { out: opts.outDir } : {}),
290
+ ...(opts.db ? { db: opts.db } : {}),
291
+ ...(opts.url ? { url: opts.url } : {}),
292
+ ...(opts.supabaseUrl ? { supabaseUrl: opts.supabaseUrl } : {}),
293
+ ...(opts.supabaseKey ? { supabaseKey: opts.supabaseKey } : {}),
294
+ ...(opts.dataset ? { dataset: opts.dataset } : {}),
295
+ ...(opts.theme ? { theme: opts.theme } : {}),
296
+ ...(opts.dark ? { dark: true } : {}),
297
+ ...(opts.title ? { title: opts.title } : {}),
298
+ ...(opts.yes ? { yes: true } : {}),
299
+ ...(opts.force ? { force: true } : {}),
300
+ });
301
+ return;
302
+ }
147
303
  // --- eject: write the app (or a fragment) from studio.config.json ---------
148
304
  if (cmd === 'eject' && !opts.help) {
149
305
  const configPath = opts.config ?? 'studio.config.json';
@@ -291,29 +447,10 @@ async function main() {
291
447
  // the user - they never need to know `pg`/`mysql2`/etc. must be present
292
448
  // before --db works (mirrors the designer's "Install driver" flow).
293
449
  const cwd = process.cwd();
294
- if (!isDriverInstalled(opts.db, cwd)) {
295
- const driver = DRIVER_FOR[opts.db];
296
- process.stdout.write(`Installing ${driver} (required for --db ${opts.db})...\n`);
297
- const result = await installDriver(opts.db, cwd);
298
- if (!result.ok) {
299
- process.stderr.write(`svgrid-studio: could not install "${driver}" automatically.\n${result.output}\n` +
300
- `Run \`${result.manager} install ${driver}\` yourself and try again.\n`);
301
- process.exit(1);
302
- }
303
- process.stdout.write(`Installed ${driver}.\n`);
304
- // The install child process can report success before the new module is
305
- // reliably resolvable (seen on Windows - antivirus/file-sync tools can
306
- // briefly hold the just-written files). Poll rather than racing straight
307
- // into connect(), which would resolve the driver too early and crash.
308
- const deadline = Date.now() + 5000;
309
- while (!isDriverInstalled(opts.db, cwd) && Date.now() < deadline) {
310
- await new Promise((r) => setTimeout(r, 200));
311
- }
312
- if (!isDriverInstalled(opts.db, cwd)) {
313
- process.stderr.write(`svgrid-studio: installed "${driver}" but it's still not resolvable from this directory.\n` +
314
- `Something (antivirus, a file-sync tool) may be holding a lock on the new files. Try running the command again.\n`);
315
- process.exit(1);
316
- }
450
+ const driver = await ensureDriverInstalled(opts.db, cwd, (l) => process.stdout.write(l + '\n'));
451
+ if (!driver.ok) {
452
+ process.stderr.write(`svgrid-studio: ${driver.message}\n`);
453
+ process.exit(1);
317
454
  }
318
455
  const execute = await connect(opts.db, opts.url);
319
456
  // A dialect dataSource emits a fully-connected +server.ts (driver + DATABASE_URL);
@@ -399,6 +536,14 @@ async function writeCrashReport(err) {
399
536
  }
400
537
  main().catch(async (err) => {
401
538
  process.stderr.write(`svgrid-studio: ${err instanceof Error ? err.message : String(err)}\n`);
539
+ // A mistyped connection string is not a bug. Only real crashes earn a report -
540
+ // otherwise ordinary fat-fingering ends with "report this on GitHub", which
541
+ // teaches people to distrust the message and file noise.
542
+ if (isUserError(err)) {
543
+ if (err.hint)
544
+ process.stderr.write(` ${err.hint}\n`);
545
+ process.exit(1);
546
+ }
402
547
  try {
403
548
  await writeCrashReport(err);
404
549
  }
@@ -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,7 +4,7 @@
4
4
  "type": "commercial",
5
5
  "url": "https://svgrid.com/pricing"
6
6
  },
7
- "version": "0.3.0",
7
+ "version": "0.5.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>",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@svgrid/enterprise": "^2.3.0"
30
+ "@svgrid/enterprise": "^2.5.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.10.7",
@@ -56,6 +56,7 @@
56
56
  ],
57
57
  "scripts": {
58
58
  "build": "tsc -p tsconfig.json",
59
- "test:types": "tsc -p tsconfig.json --noEmit"
59
+ "test:types": "tsc -p tsconfig.json --noEmit",
60
+ "lint": "eslint ./src"
60
61
  }
61
62
  }