@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 +17 -2
- package/dist/cli.js +232 -87
- package/dist/driver-install.js +36 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -19,8 +19,23 @@
|
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
57
|
+
out.from = value(a);
|
|
42
58
|
else if (a === '--table')
|
|
43
|
-
out.table =
|
|
59
|
+
out.table = value(a);
|
|
44
60
|
else if (a === '--route')
|
|
45
|
-
out.route =
|
|
61
|
+
out.route = value(a);
|
|
46
62
|
else if (a === '--api')
|
|
47
|
-
out.apiRoute =
|
|
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 =
|
|
67
|
+
out.db = value(a);
|
|
52
68
|
else if (a === '--url')
|
|
53
|
-
out.url =
|
|
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 =
|
|
73
|
+
out.config = value(a);
|
|
58
74
|
else if (a === '--out')
|
|
59
|
-
out.outDir =
|
|
75
|
+
out.outDir = value(a);
|
|
60
76
|
else if (a === '--port')
|
|
61
|
-
out.port = Number(
|
|
77
|
+
out.port = Number(value(a));
|
|
62
78
|
else if (a === '--template')
|
|
63
|
-
out.template =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
--
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
--
|
|
128
|
-
--
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
process.
|
|
297
|
-
|
|
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
|
}
|
package/dist/driver-install.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
}
|