@svgrid/studio 0.3.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/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, runtimeDeps, listDatabaseTables, missingEnvKeys, parseProject, resolveDeployTarget, resolveSchemas, runStudioAdd, runStudioAddApp, runStudioInit, serializeProject, setEntityDataSource, summarizeVerify, } 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 = {
@@ -73,6 +74,20 @@ function parse(args) {
73
74
  out.appPort = Number(args[++i]);
74
75
  else if (a === '--fragment')
75
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;
76
91
  else if (a === '-h' || a === '--help')
77
92
  out.help = true;
78
93
  else if (!a.startsWith('-'))
@@ -85,56 +100,137 @@ function parse(args) {
85
100
  out.table = name;
86
101
  return out;
87
102
  }
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
103
+ const HELP = `svgrid-studio - scaffold CRUD screens from a schema file or a live database
104
+
105
+ Usage:
106
+ svgrid-studio init # guided: pick your data, get a working CRUD app
107
+ svgrid-studio designer # open the visual designer in your browser
108
+ svgrid-studio add <name> --from <schema> # one table/model from a schema file
109
+ svgrid-studio add --all --from <schema> # every table/model, linked
110
+ svgrid-studio add <name> --db <dialect> --url <conn> # one table from a live database
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
134
+
135
+ Designer (visual app builder, auto-saves to studio.config.json):
136
+ --template <id> open a ready-made sample app (crm | ecommerce | projects | support)
137
+ --config <path> studio.config.json to load + auto-save (default: ./studio.config.json)
138
+ --out <dir> folder to write the generated app into (default: .)
139
+ --port <n> port to serve on (default: 4321)
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)
143
+
144
+ Schema files: a Drizzle schema.ts or a Prisma schema.prisma (auto-detected).
145
+ Foreign keys become searchable relation lookups; enums become select fields.
146
+
147
+ Databases: postgres | supabase | mysql | mssql | sqlite
148
+ (the matching driver - pg / mysql2 / mssql / better-sqlite3 - must be installed)
149
+
150
+ Options:
151
+ --from <path> Drizzle (.ts) or Prisma (.prisma) schema file to introspect
152
+ --db <dialect> Connect to a live database and read its catalog
153
+ --url <conn> Connection string / file path for --db
154
+ --all Scaffold a screen for every table/model (+ nav & home)
155
+ --table <name> Which table/model to use (defaults to <name>)
156
+ --sql Wire a real SQL data source (default with --db)
157
+ --route <seg> Route segment (default: <name> / table name)
158
+ --api <path> API route path (default: /api/<route>)
159
+ -h, --help Show this help
160
+
161
+ Examples:
162
+ svgrid-studio init
163
+ svgrid-studio init --db postgres --url $DATABASE_URL --out my-app
164
+ svgrid-studio add customers --from src/lib/db/schema.ts
165
+ svgrid-studio add --all --from prisma/schema.prisma
166
+ svgrid-studio add customers --db postgres --url $DATABASE_URL --sql
167
+ svgrid-studio add --all --db supabase --url $DATABASE_URL
137
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
+ }
138
234
  function report(name, written, verifyLine) {
139
235
  process.stdout.write(`Scaffolded "${name}":\n`);
140
236
  for (const path of written)
@@ -144,6 +240,24 @@ function report(name, written, verifyLine) {
144
240
  async function main() {
145
241
  const [cmd, ...rest] = process.argv.slice(2);
146
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
+ }
147
261
  // --- eject: write the app (or a fragment) from studio.config.json ---------
148
262
  if (cmd === 'eject' && !opts.help) {
149
263
  const configPath = opts.config ?? 'studio.config.json';
@@ -291,29 +405,10 @@ async function main() {
291
405
  // the user - they never need to know `pg`/`mysql2`/etc. must be present
292
406
  // before --db works (mirrors the designer's "Install driver" flow).
293
407
  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
- }
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);
317
412
  }
318
413
  const execute = await connect(opts.db, opts.url);
319
414
  // A dialect dataSource emits a fully-connected +server.ts (driver + DATABASE_URL);
@@ -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.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>",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@svgrid/enterprise": "^2.3.0"
30
+ "@svgrid/enterprise": "^2.4.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
  }