@svgrid/studio 0.1.4 → 0.3.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,4 +1,23 @@
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>
4
+
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
+ ---
2
21
 
3
22
  Scaffold a full CRUD screen for SvelteKit from a **live database** or a
4
23
  **Drizzle / Prisma schema** in **one command**.
@@ -14,9 +33,9 @@ npx @svgrid/studio add User --from prisma/schema.prisma
14
33
 
15
34
  That generates, into your SvelteKit app:
16
35
 
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
36
+ - `src/lib/customers.schema.ts` - the entity schema + row type
37
+ - `src/routes/api/customers/+server.ts` - the API route
38
+ - `src/routes/customers/+page.svelte` - a working screen: grid + edit panel, with
20
39
  search, server sort, pagination, and multi-select (optimistic) delete
21
40
 
22
41
  Then:
@@ -79,11 +98,11 @@ in with any client (Drizzle `db.execute`, postgres.js, better-sqlite3, mysql2).
79
98
 
80
99
  ## Regeneration is safe
81
100
 
82
- Re-running `add` replaces only the `svgrid:managed` regions of each file — your
101
+ Re-running `add` replaces only the `svgrid:managed` regions of each file - your
83
102
  edits outside those markers are preserved.
84
103
 
85
104
  ## Requires
86
105
 
87
106
  `@svgrid/grid` and `@svgrid/enterprise` in your project. The Studio is part of the
88
107
  **Enterprise** license (soft-gate: it works unlicensed, with a nudge). See
89
- [licensing](https://svgrid.com/pricing).
108
+ [licensing](https://svgrid.com/pricing/).
package/dist/cli.js CHANGED
@@ -11,11 +11,13 @@
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 { 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';
16
17
  import { connect } from './db-connect.js';
17
18
  import { DRIVER_FOR, installDriver, isDriverInstalled } from './driver-install.js';
18
19
  import { startDesignerServer } from './designer-server.js';
20
+ import { ensureApp, startAppServer } from './dev.js';
19
21
  const io = {
20
22
  readFile: async (path) => {
21
23
  try {
@@ -61,6 +63,16 @@ function parse(args) {
61
63
  out.template = args[++i];
62
64
  else if (a === '--no-open')
63
65
  out.noOpen = true;
66
+ else if (a === '--target')
67
+ out.target = args[++i];
68
+ else if (a === '--dry-run')
69
+ out.dryRun = true;
70
+ else if (a === '--ai')
71
+ out.ai = true;
72
+ else if (a === '--app-port')
73
+ out.appPort = Number(args[++i]);
74
+ else if (a === '--fragment')
75
+ out.fragment = true;
64
76
  else if (a === '-h' || a === '--help')
65
77
  out.help = true;
66
78
  else if (!a.startsWith('-'))
@@ -73,44 +85,55 @@ function parse(args) {
73
85
  out.table = name;
74
86
  return out;
75
87
  }
76
- const HELP = `svgrid-studio - scaffold CRUD screens from a schema file or a live database
77
-
78
- Usage:
79
- svgrid-studio designer # open the visual designer in your browser
80
- svgrid-studio add <name> --from <schema> # one table/model from a schema file
81
- svgrid-studio add --all --from <schema> # every table/model, linked
82
- svgrid-studio add <name> --db <dialect> --url <conn> # one table from a live database
83
- svgrid-studio add --all --db <dialect> --url <conn> # every table from a live database
84
-
85
- Designer (visual app builder, auto-saves to studio.config.json):
86
- --template <id> open a ready-made sample app (crm | ecommerce | projects | support)
87
- --config <path> studio.config.json to load + auto-save (default: ./studio.config.json)
88
- --out <dir> folder to write the generated app into (default: .)
89
- --port <n> port to serve on (default: 4321)
90
- --no-open don't open the browser
91
-
92
- Schema files: a Drizzle schema.ts or a Prisma schema.prisma (auto-detected).
93
- Foreign keys become searchable relation lookups; enums become select fields.
94
-
95
- Databases: postgres | supabase | mysql | mssql | sqlite
96
- (the matching driver - pg / mysql2 / mssql / better-sqlite3 - must be installed)
97
-
98
- Options:
99
- --from <path> Drizzle (.ts) or Prisma (.prisma) schema file to introspect
100
- --db <dialect> Connect to a live database and read its catalog
101
- --url <conn> Connection string / file path for --db
102
- --all Scaffold a screen for every table/model (+ nav & home)
103
- --table <name> Which table/model to use (defaults to <name>)
104
- --sql Wire a real SQL data source (default with --db)
105
- --route <seg> Route segment (default: <name> / table name)
106
- --api <path> API route path (default: /api/<route>)
107
- -h, --help Show this help
108
-
109
- Examples:
110
- svgrid-studio add customers --from src/lib/db/schema.ts
111
- svgrid-studio add --all --from prisma/schema.prisma
112
- svgrid-studio add customers --db postgres --url $DATABASE_URL --sql
113
- svgrid-studio add --all --db supabase --url $DATABASE_URL
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
114
137
  `;
115
138
  function report(name, written, verifyLine) {
116
139
  process.stdout.write(`Scaffolded "${name}":\n`);
@@ -121,6 +144,89 @@ function report(name, written, verifyLine) {
121
144
  async function main() {
122
145
  const [cmd, ...rest] = process.argv.slice(2);
123
146
  const opts = parse(rest);
147
+ // --- eject: write the app (or a fragment) from studio.config.json ---------
148
+ if (cmd === 'eject' && !opts.help) {
149
+ const configPath = opts.config ?? 'studio.config.json';
150
+ const json = await io.readFile(configPath);
151
+ if (json == null) {
152
+ process.stderr.write(`eject: ${configPath} not found - run \`svgrid-studio designer\` first, or pass --config.\n`);
153
+ process.exit(1);
154
+ }
155
+ const project = parseProject(json);
156
+ const outDir = opts.outDir ?? '.';
157
+ const files = opts.fragment ? emitStudioFragment(project) : emitStudioAppBundle(project);
158
+ for (const f of files) {
159
+ const full = resolve(outDir, f.path);
160
+ await mkdir(dirname(full), { recursive: true });
161
+ await writeFile(full, f.contents);
162
+ }
163
+ process.stdout.write(`Wrote ${files.length} ${opts.fragment ? 'fragment' : 'app'} file(s) -> ${resolve(outDir)}\n`);
164
+ if (opts.fragment) {
165
+ const deps = runtimeDeps(project, files.map((f) => f.contents).join('\n'));
166
+ const install = Object.keys(deps).filter((d) => d !== '@svgrid/grid' && d !== '@svgrid/enterprise');
167
+ 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`);
168
+ }
169
+ return;
170
+ }
171
+ // --- import an OpenAPI spec into a studio.config.json ---------------------
172
+ if (cmd === 'openapi' && !opts.help) {
173
+ const src = opts.from ?? rest.find((a) => !a.startsWith('-'));
174
+ if (!src) {
175
+ process.stderr.write('openapi: pass a spec - `svgrid-studio openapi ./openapi.json` (or a URL)\n');
176
+ process.exit(1);
177
+ }
178
+ const text = /^https?:\/\//.test(src) ? await (await fetch(src)).text() : await readFile(src, 'utf8');
179
+ const { entities, sources, warnings } = introspectOpenApi(text);
180
+ let project = createProject(entities, { title: 'API app' });
181
+ for (const [name, source] of Object.entries(sources))
182
+ project = setEntityDataSource(project, name, source);
183
+ const outPath = opts.config ?? 'studio.config.json';
184
+ await writeFile(outPath, serializeProject(project));
185
+ process.stdout.write(`Imported ${entities.length} REST resource(s) -> ${outPath}\n`);
186
+ for (const w of warnings)
187
+ process.stdout.write(` ! ${w}\n`);
188
+ process.stdout.write(`Next: svgrid-studio designer (open it visually) or svgrid-studio dev\n`);
189
+ return;
190
+ }
191
+ // --- live dev loop: designer + the real app, side by side ----------------
192
+ if (cmd === 'dev' && !opts.help) {
193
+ const configPath = opts.config ?? 'studio.config.json';
194
+ const outDir = opts.outDir ?? '.';
195
+ const port = Number.isFinite(opts.port) ? opts.port : 4321;
196
+ const appPort = Number.isFinite(opts.appPort) ? opts.appPort : port + 1;
197
+ const log = (l) => process.stdout.write(l + '\n');
198
+ const hasApp = await ensureApp(resolve(outDir), resolve(configPath), log);
199
+ let app = null;
200
+ if (hasApp) {
201
+ app = await startAppServer({ outDir, port: appPort, log });
202
+ log(`\n app: ${app.url} (Vite dev server - designer saves hot-reload it)`);
203
+ }
204
+ else {
205
+ log(`\n No app here yet - design something and hit "Save to folder"; the app server starts on the first save.`);
206
+ }
207
+ process.on('SIGINT', () => {
208
+ app?.stop();
209
+ process.exit(0);
210
+ });
211
+ await startDesignerServer({
212
+ configPath,
213
+ outDir,
214
+ port,
215
+ open: !opts.noOpen,
216
+ template: opts.template,
217
+ ai: !!opts.ai,
218
+ appUrl: app?.url,
219
+ onGenerated: async () => {
220
+ if (app)
221
+ await app.syncDeps();
222
+ else {
223
+ app = await startAppServer({ outDir, port: appPort, log });
224
+ log(` app: ${app.url} (started after first save)`);
225
+ }
226
+ },
227
+ });
228
+ return;
229
+ }
124
230
  // --- visual designer -----------------------------------------------------
125
231
  if (cmd === 'designer' && !opts.help) {
126
232
  await startDesignerServer({
@@ -129,10 +235,48 @@ async function main() {
129
235
  port: Number.isFinite(opts.port) ? opts.port : 4321,
130
236
  open: !opts.noOpen,
131
237
  template: opts.template,
238
+ ai: !!opts.ai,
132
239
  });
133
240
  // Keep the process alive; the server holds the event loop until Ctrl+C.
134
241
  return;
135
242
  }
243
+ // --- deploy --------------------------------------------------------------
244
+ if (cmd === 'deploy' && !opts.help) {
245
+ const read = (p) => io.readFile(p);
246
+ const resolution = resolveDeployTarget({
247
+ flag: opts.target,
248
+ configJson: await read('studio.config.json'),
249
+ svelteConfig: (await read('svelte.config.js')) ?? (await read('svelte.config.ts')),
250
+ });
251
+ if (!resolution.ok) {
252
+ process.stderr.write(`deploy: ${resolution.reason}\n`);
253
+ process.exit(1);
254
+ }
255
+ const plan = deployCommands(resolution.provider);
256
+ process.stdout.write(`Deploying to ${plan.provider} (from ${resolution.source === 'flag' ? '--target' : resolution.source === 'config' ? 'studio.config.json' : 'svelte.config.js adapter'}).\n`);
257
+ // Preflight: env keys the app declares but the local .env doesn't set.
258
+ const missing = missingEnvKeys(await read('.env.example'), await read('.env'));
259
+ if (missing.length)
260
+ process.stdout.write(`! Unset env keys (set them on the host too): ${missing.join(', ')}\n`);
261
+ for (const note of plan.notes)
262
+ process.stdout.write(` ${note}\n`);
263
+ const commands = [plan.build, ...(plan.deploy ? [plan.deploy] : [])];
264
+ if (opts.dryRun) {
265
+ process.stdout.write(commands.map((c) => `> ${c}\n`).join(''));
266
+ return;
267
+ }
268
+ for (const command of commands) {
269
+ process.stdout.write(`\n> ${command}\n`);
270
+ const res = spawnSync(command, { stdio: 'inherit', shell: true });
271
+ if (res.status !== 0) {
272
+ if (command !== plan.build) {
273
+ 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`);
274
+ }
275
+ process.exit(res.status ?? 1);
276
+ }
277
+ }
278
+ return;
279
+ }
136
280
  if (cmd !== 'add' || opts.help) {
137
281
  process.stdout.write(HELP);
138
282
  process.exit(opts.help || !cmd ? 0 : 1);
@@ -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, } 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,13 +102,56 @@ 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 = [];
123
+ let written = 0;
100
124
  for (const f of files) {
101
125
  const full = join(outDir, f.path);
126
+ // User-owned companions (a screen's handlers.ts) are scaffolded once, then the
127
+ // developer's forever: never overwrite one that already exists on disk. This is
128
+ // the make-or-break round-trip guarantee. See HANDLERS-DESIGN.md.
129
+ if (skipUserOwned(f, existsSync(full)))
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
+ }
102
148
  await mkdir(dirname(full), { recursive: true });
103
149
  await writeFile(full, f.contents);
150
+ manifest[f.path] = newHash;
151
+ written++;
104
152
  }
153
+ await mkdir(dirname(manifestPath), { recursive: true });
154
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
105
155
  const allSource = files.map((f) => f.contents).join('\n');
106
156
  const pkgPath = join(outDir, 'package.json');
107
157
  if (existsSync(pkgPath)) {
@@ -121,7 +171,7 @@ async function writeBundle(outDir, files) {
121
171
  // leave package.json untouched if it isn't valid JSON
122
172
  }
123
173
  }
124
- return files.length;
174
+ return { written, conflicts };
125
175
  }
126
176
  /** Best-effort open the default browser (no dependency). */
127
177
  function openBrowser(url) {
@@ -145,9 +195,33 @@ export async function startDesignerServer(opts) {
145
195
  send(res, 500, JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
146
196
  });
147
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;
148
202
  async function handle(req, res) {
149
203
  const url = req.url ?? '/';
150
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
+ }
151
225
  if (path === '/api/project' && req.method === 'GET') {
152
226
  const json = existsSync(configPath) ? await readFile(configPath, 'utf8') : null;
153
227
  // Round-trip through parse/serialize so we hand the client a normalized,
@@ -172,9 +246,18 @@ export async function startDesignerServer(opts) {
172
246
  send(res, 400, JSON.stringify({ error: 'no files to write' }));
173
247
  return;
174
248
  }
175
- const count = await writeBundle(outDir, files);
176
- log(` generated ${count} files -> ${outDir}`);
177
- 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 }));
178
261
  return;
179
262
  }
180
263
  // Assemble a connection string from guided-form fields (host/port/user/...).
@@ -242,6 +325,23 @@ export async function startDesignerServer(opts) {
242
325
  }
243
326
  return;
244
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
+ }
245
345
  if (action === 'introspect') {
246
346
  const tables = Array.isArray(reqBody.tables) ? reqBody.tables : [];
247
347
  const schemas = [];
@@ -290,6 +390,10 @@ export async function startDesignerServer(opts) {
290
390
  log(`\n SvGrid Studio designer running at ${url}`);
291
391
  log(` config: ${configPath}`);
292
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`);
293
397
  log(` (edits auto-save; "Save to folder" writes the app. Ctrl+C to stop.)\n`);
294
398
  if (opts.open)
295
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
+ }
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@svgrid/studio",
3
- "version": "0.1.4",
3
+ "funding": {
4
+ "type": "commercial",
5
+ "url": "https://svgrid.com/pricing"
6
+ },
7
+ "version": "0.3.0",
4
8
  "description": "SvGrid Studio CLI: scaffold a full CRUD screen (grid + edit panel + SvelteKit API route) from a Drizzle schema in one command.",
5
9
  "license": "SEE LICENSE IN LICENSE",
6
10
  "author": "jQWidgets <sales@jqwidgets.com>",
7
11
  "homepage": "https://svgrid.com",
8
12
  "repository": {
9
13
  "type": "git",
10
- "url": "https://github.com/sv-grid/sv-grid.git",
14
+ "url": "git+https://github.com/sv-grid/sv-grid.git",
11
15
  "directory": "packages/studio"
12
16
  },
13
17
  "type": "module",
@@ -23,7 +27,7 @@
23
27
  "access": "public"
24
28
  },
25
29
  "dependencies": {
26
- "@svgrid/enterprise": "^2.0.4"
30
+ "@svgrid/enterprise": "^2.3.0"
27
31
  },
28
32
  "devDependencies": {
29
33
  "@types/node": "^22.10.7",
@@ -32,6 +36,24 @@
32
36
  "engines": {
33
37
  "node": ">=18"
34
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
+ ],
35
57
  "scripts": {
36
58
  "build": "tsc -p tsconfig.json",
37
59
  "test:types": "tsc -p tsconfig.json --noEmit"