@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 +74 -35
- package/README.md +25 -6
- package/dist/cli.js +184 -40
- package/dist/copilot.js +43 -0
- package/dist/designer-server.js +125 -21
- package/dist/dev.js +125 -0
- package/package.json +25 -3
package/LICENSE
CHANGED
|
@@ -1,46 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
13
|
+
1. GRANT (with valid SvGrid Pro or Enterprise license)
|
|
7
14
|
------------------------------------------------------------------------
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
38
|
+
2. RESTRICTIONS
|
|
24
39
|
------------------------------------------------------------------------
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
The Subscriber may NOT:
|
|
27
42
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
18
|
-
- `src/routes/api/customers/+server.ts`
|
|
19
|
-
- `src/routes/customers/+page.svelte`
|
|
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
|
|
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 {
|
|
15
|
-
import {
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
--
|
|
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
|
-
|
|
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);
|
package/dist/copilot.js
ADDED
|
@@ -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
|
+
}
|
package/dist/designer-server.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
176
|
-
log(` generated ${
|
|
177
|
-
|
|
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
|
-
"
|
|
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
|
|
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"
|