@construct-space/cli 1.5.1 → 1.6.2
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 +19 -0
- package/dist/index.js +251 -33
- package/dist/templates/space/actions.ts.tmpl +20 -29
- package/dist/templates/space/construct.md.tmpl +260 -0
- package/dist/templates/space/eslint.config.js.tmpl +26 -0
- package/dist/templates/space/index.vue.tmpl +22 -7
- package/dist/templates/space/package.json.tmpl +9 -0
- package/package.json +1 -1
- package/templates/space/actions.ts.tmpl +20 -29
- package/templates/space/construct.md.tmpl +260 -0
- package/templates/space/eslint.config.js.tmpl +26 -0
- package/templates/space/index.vue.tmpl +22 -7
- package/templates/space/package.json.tmpl +9 -0
package/README.md
CHANGED
|
@@ -23,6 +23,25 @@ construct check # Type-check + lint
|
|
|
23
23
|
construct clean # Remove build artifacts
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
### Built-in libraries
|
|
27
|
+
|
|
28
|
+
Scaffolded spaces ship with the full Construct stack pre-wired — import freely, the host resolves them at runtime (never bundled):
|
|
29
|
+
|
|
30
|
+
| Package | Purpose |
|
|
31
|
+
|---------|---------|
|
|
32
|
+
| `@construct-space/ui` | Vue 3 components — `Button`, `Card`, `Modal`, `Table`, `Badge`, `Avatar`, `ToggleGroup`, `ConfirmationModal`, ... |
|
|
33
|
+
| `@construct-space/sdk` | Host composables — `useOrg`, `useOrgMembers`, `useToast`, `useAuth` |
|
|
34
|
+
| `@construct-space/graph` | Multi-tenant data layer — `defineModel`, `useGraph`, scopes, access rules |
|
|
35
|
+
| `lucide-vue-next` | Icon set |
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Button, Card, Modal } from '@construct-space/ui'
|
|
39
|
+
import { useOrg, useOrgMembers } from '@construct-space/sdk'
|
|
40
|
+
import { defineModel, field, useGraph } from '@construct-space/graph'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The scaffolded `src/pages/index.vue` and `src/actions.ts` show the recommended patterns. Use `construct check` (typecheck + ESLint flat config) before commit.
|
|
44
|
+
|
|
26
45
|
### Graph (Data Models)
|
|
27
46
|
|
|
28
47
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -5313,8 +5313,10 @@ async function scaffold(nameArg, options) {
|
|
|
5313
5313
|
"safety.json.tmpl": join2(name, "agent", "hooks", "safety.json"),
|
|
5314
5314
|
"build.yml.tmpl": join2(name, ".github", "workflows", "build.yml"),
|
|
5315
5315
|
"tsconfig.json.tmpl": join2(name, "tsconfig.json"),
|
|
5316
|
+
"eslint.config.js.tmpl": join2(name, "eslint.config.js"),
|
|
5316
5317
|
"gitignore.tmpl": join2(name, ".gitignore"),
|
|
5317
5318
|
"readme.md.tmpl": join2(name, "README.md"),
|
|
5319
|
+
"construct.md.tmpl": join2(name, "CONSTRUCT.md"),
|
|
5318
5320
|
"widgets/2x1.vue.tmpl": join2(name, "widgets", "summary", "2x1.vue"),
|
|
5319
5321
|
"widgets/4x1.vue.tmpl": join2(name, "widgets", "summary", "4x1.vue"),
|
|
5320
5322
|
"actions.ts.tmpl": join2(name, "src", "actions.ts")
|
|
@@ -7989,6 +7991,7 @@ function ora(options) {
|
|
|
7989
7991
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
7990
7992
|
import { join as join3 } from "path";
|
|
7991
7993
|
var MANIFEST_FILE = "space.manifest.json";
|
|
7994
|
+
var HOST_API_VERSION = "0.5.0";
|
|
7992
7995
|
var idRegex = /^[a-z][a-z0-9-]*$/;
|
|
7993
7996
|
var versionRegex = /^\d+\.\d+\.\d+/;
|
|
7994
7997
|
function validate2(m) {
|
|
@@ -8023,6 +8026,11 @@ function readRaw(dir) {
|
|
|
8023
8026
|
const data = readFileSync2(path, "utf-8");
|
|
8024
8027
|
return JSON.parse(data);
|
|
8025
8028
|
}
|
|
8029
|
+
function write(dir, m) {
|
|
8030
|
+
const path = join3(dir, MANIFEST_FILE);
|
|
8031
|
+
writeFileSync2(path, JSON.stringify(m, null, 2) + `
|
|
8032
|
+
`);
|
|
8033
|
+
}
|
|
8026
8034
|
function writeWithBuild(dir, raw, build) {
|
|
8027
8035
|
raw.build = build;
|
|
8028
8036
|
writeFileSync2(join3(dir, "manifest.json"), JSON.stringify(raw, null, 2) + `
|
|
@@ -8106,7 +8114,7 @@ function resolvePages(m, root, prefix) {
|
|
|
8106
8114
|
if (existsSync4(legacyFullPath)) {
|
|
8107
8115
|
let varName = "IndexPage";
|
|
8108
8116
|
if (p.path) {
|
|
8109
|
-
varName = capitalize(p.path.replace(/[\/:]/g, "-").replace(/-+/g, "-")) + "Page";
|
|
8117
|
+
varName = capitalize(p.path.replace(/\[([^\]]+)\]/g, "$1").replace(/[\/:]/g, "-").replace(/-+/g, "-")) + "Page";
|
|
8110
8118
|
}
|
|
8111
8119
|
return { varName, importPath: prefix + legacyComponent, path: p.path };
|
|
8112
8120
|
}
|
|
@@ -8240,35 +8248,174 @@ function bundleAgentDir(srcDir, distDir) {
|
|
|
8240
8248
|
}
|
|
8241
8249
|
|
|
8242
8250
|
// src/commands/build.ts
|
|
8251
|
+
function stripComments(source) {
|
|
8252
|
+
let out = "";
|
|
8253
|
+
let i = 0;
|
|
8254
|
+
let quote = null;
|
|
8255
|
+
let escaped = false;
|
|
8256
|
+
while (i < source.length) {
|
|
8257
|
+
const ch = source[i];
|
|
8258
|
+
const next = source[i + 1];
|
|
8259
|
+
if (quote) {
|
|
8260
|
+
out += ch;
|
|
8261
|
+
if (escaped) {
|
|
8262
|
+
escaped = false;
|
|
8263
|
+
} else if (ch === "\\") {
|
|
8264
|
+
escaped = true;
|
|
8265
|
+
} else if (ch === quote) {
|
|
8266
|
+
quote = null;
|
|
8267
|
+
}
|
|
8268
|
+
i++;
|
|
8269
|
+
continue;
|
|
8270
|
+
}
|
|
8271
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
8272
|
+
quote = ch;
|
|
8273
|
+
out += ch;
|
|
8274
|
+
i++;
|
|
8275
|
+
continue;
|
|
8276
|
+
}
|
|
8277
|
+
if (ch === "/" && next === "/") {
|
|
8278
|
+
while (i < source.length && source[i] !== `
|
|
8279
|
+
`) {
|
|
8280
|
+
out += " ";
|
|
8281
|
+
i++;
|
|
8282
|
+
}
|
|
8283
|
+
continue;
|
|
8284
|
+
}
|
|
8285
|
+
if (ch === "/" && next === "*") {
|
|
8286
|
+
out += " ";
|
|
8287
|
+
i += 2;
|
|
8288
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) {
|
|
8289
|
+
out += source[i] === `
|
|
8290
|
+
` ? `
|
|
8291
|
+
` : " ";
|
|
8292
|
+
i++;
|
|
8293
|
+
}
|
|
8294
|
+
if (i < source.length) {
|
|
8295
|
+
out += " ";
|
|
8296
|
+
i += 2;
|
|
8297
|
+
}
|
|
8298
|
+
continue;
|
|
8299
|
+
}
|
|
8300
|
+
out += ch;
|
|
8301
|
+
i++;
|
|
8302
|
+
}
|
|
8303
|
+
return out;
|
|
8304
|
+
}
|
|
8305
|
+
function findMatchingBrace(source, openIndex) {
|
|
8306
|
+
let depth = 0;
|
|
8307
|
+
let quote = null;
|
|
8308
|
+
let escaped = false;
|
|
8309
|
+
for (let i = openIndex;i < source.length; i++) {
|
|
8310
|
+
const ch = source[i];
|
|
8311
|
+
if (quote) {
|
|
8312
|
+
if (escaped)
|
|
8313
|
+
escaped = false;
|
|
8314
|
+
else if (ch === "\\")
|
|
8315
|
+
escaped = true;
|
|
8316
|
+
else if (ch === quote)
|
|
8317
|
+
quote = null;
|
|
8318
|
+
continue;
|
|
8319
|
+
}
|
|
8320
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
8321
|
+
quote = ch;
|
|
8322
|
+
continue;
|
|
8323
|
+
}
|
|
8324
|
+
if (ch === "{")
|
|
8325
|
+
depth++;
|
|
8326
|
+
else if (ch === "}") {
|
|
8327
|
+
depth--;
|
|
8328
|
+
if (depth === 0)
|
|
8329
|
+
return i;
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8332
|
+
return -1;
|
|
8333
|
+
}
|
|
8334
|
+
function objectLiteralAfter(source, start) {
|
|
8335
|
+
const open = source.indexOf("{", start);
|
|
8336
|
+
if (open < 0)
|
|
8337
|
+
return null;
|
|
8338
|
+
const close = findMatchingBrace(source, open);
|
|
8339
|
+
if (close < 0)
|
|
8340
|
+
return null;
|
|
8341
|
+
return source.slice(open + 1, close);
|
|
8342
|
+
}
|
|
8343
|
+
function actionsObject(source) {
|
|
8344
|
+
const stripped = stripComments(source);
|
|
8345
|
+
const match = /export\s+const\s+actions\s*=/.exec(stripped);
|
|
8346
|
+
if (!match)
|
|
8347
|
+
return null;
|
|
8348
|
+
return objectLiteralAfter(stripped, match.index + match[0].length);
|
|
8349
|
+
}
|
|
8350
|
+
function objectEntries(body) {
|
|
8351
|
+
const entries = [];
|
|
8352
|
+
let i = 0;
|
|
8353
|
+
while (i < body.length) {
|
|
8354
|
+
const match = /\s*,?\s*([A-Za-z_$][\w$]*)\s*:/.exec(body.slice(i));
|
|
8355
|
+
if (!match)
|
|
8356
|
+
break;
|
|
8357
|
+
const name = match[1];
|
|
8358
|
+
const valueStart = i + match.index + match[0].length;
|
|
8359
|
+
const open = body.indexOf("{", valueStart);
|
|
8360
|
+
if (open < 0)
|
|
8361
|
+
break;
|
|
8362
|
+
if (body.slice(valueStart, open).trim()) {
|
|
8363
|
+
i = valueStart;
|
|
8364
|
+
continue;
|
|
8365
|
+
}
|
|
8366
|
+
const close = findMatchingBrace(body, open);
|
|
8367
|
+
if (close < 0)
|
|
8368
|
+
break;
|
|
8369
|
+
entries.push({ name, body: body.slice(open + 1, close) });
|
|
8370
|
+
i = close + 1;
|
|
8371
|
+
}
|
|
8372
|
+
return entries;
|
|
8373
|
+
}
|
|
8374
|
+
function stringProperty(body, name) {
|
|
8375
|
+
const match = new RegExp(`${name}\\s*:\\s*['"\`]([^'"\`]*)['"\`]`).exec(body);
|
|
8376
|
+
return match?.[1];
|
|
8377
|
+
}
|
|
8378
|
+
function booleanProperty(body, name) {
|
|
8379
|
+
const match = new RegExp(`${name}\\s*:\\s*(true|false)`).exec(body);
|
|
8380
|
+
if (!match)
|
|
8381
|
+
return;
|
|
8382
|
+
return match[1] === "true";
|
|
8383
|
+
}
|
|
8384
|
+
function propertyObject(body, name) {
|
|
8385
|
+
const match = new RegExp(`${name}\\s*:`).exec(body);
|
|
8386
|
+
if (!match)
|
|
8387
|
+
return null;
|
|
8388
|
+
return objectLiteralAfter(body, match.index + match[0].length);
|
|
8389
|
+
}
|
|
8243
8390
|
function extractActionMetadata(actionsPath) {
|
|
8244
8391
|
try {
|
|
8245
|
-
const
|
|
8392
|
+
const actionsBody = actionsObject(readFileSync4(actionsPath, "utf-8"));
|
|
8393
|
+
if (!actionsBody)
|
|
8394
|
+
return null;
|
|
8246
8395
|
const result = {};
|
|
8247
|
-
const
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
const
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
for (const actionId of Object.keys(result)) {
|
|
8255
|
-
const paramBlockPattern = new RegExp(`${actionId}\\s*:\\s*\\{[\\s\\S]*?params\\s*:\\s*\\{([\\s\\S]*?)\\}\\s*,?\\s*(?:run|\\})`);
|
|
8256
|
-
const paramMatch = source.match(paramBlockPattern);
|
|
8257
|
-
if (paramMatch?.[1]) {
|
|
8396
|
+
for (const action of objectEntries(actionsBody)) {
|
|
8397
|
+
const description = stringProperty(action.body, "description");
|
|
8398
|
+
if (!description)
|
|
8399
|
+
continue;
|
|
8400
|
+
const metadata = { description };
|
|
8401
|
+
const paramsBody = propertyObject(action.body, "params");
|
|
8402
|
+
if (paramsBody) {
|
|
8258
8403
|
const params = {};
|
|
8259
|
-
const
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8404
|
+
for (const param of objectEntries(paramsBody)) {
|
|
8405
|
+
const type = stringProperty(param.body, "type");
|
|
8406
|
+
if (!type)
|
|
8407
|
+
continue;
|
|
8408
|
+
const required = booleanProperty(param.body, "required");
|
|
8409
|
+
params[param.name] = {
|
|
8410
|
+
type,
|
|
8411
|
+
...stringProperty(param.body, "description") ? { description: stringProperty(param.body, "description") } : {},
|
|
8412
|
+
...required === true ? { required: true } : {}
|
|
8266
8413
|
};
|
|
8267
8414
|
}
|
|
8268
|
-
if (Object.keys(params).length > 0)
|
|
8269
|
-
|
|
8270
|
-
}
|
|
8415
|
+
if (Object.keys(params).length > 0)
|
|
8416
|
+
metadata.params = params;
|
|
8271
8417
|
}
|
|
8418
|
+
result[action.name] = metadata;
|
|
8272
8419
|
}
|
|
8273
8420
|
return Object.keys(result).length > 0 ? result : null;
|
|
8274
8421
|
} catch {
|
|
@@ -8334,7 +8481,7 @@ async function build(options) {
|
|
|
8334
8481
|
writeWithBuild(distDir, raw, {
|
|
8335
8482
|
checksum,
|
|
8336
8483
|
size: bundleData.length,
|
|
8337
|
-
hostApiVersion:
|
|
8484
|
+
hostApiVersion: HOST_API_VERSION,
|
|
8338
8485
|
builtAt: new Date().toISOString()
|
|
8339
8486
|
});
|
|
8340
8487
|
console.log(source_default.green(`Built ${m.name} v${m.version}`));
|
|
@@ -10016,7 +10163,7 @@ async function dev() {
|
|
|
10016
10163
|
writeWithBuild(distDir, raw, {
|
|
10017
10164
|
checksum,
|
|
10018
10165
|
size: bundleData.length,
|
|
10019
|
-
hostApiVersion:
|
|
10166
|
+
hostApiVersion: HOST_API_VERSION,
|
|
10020
10167
|
builtAt: new Date().toISOString()
|
|
10021
10168
|
});
|
|
10022
10169
|
console.log(source_default.green(`Built \u2192 dist/ (${(bundleData.length / 1024).toFixed(1)} KB)`));
|
|
@@ -10316,6 +10463,22 @@ async function publish(options) {
|
|
|
10316
10463
|
init_source();
|
|
10317
10464
|
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
10318
10465
|
import { join as join15 } from "path";
|
|
10466
|
+
|
|
10467
|
+
// src/lib/pagePaths.ts
|
|
10468
|
+
function pageComponentFromPath(path) {
|
|
10469
|
+
const clean = path.replace(/^\/+|\/+$/g, "");
|
|
10470
|
+
if (!clean)
|
|
10471
|
+
return "pages/index.vue";
|
|
10472
|
+
const segments = clean.split("/").map((segment) => {
|
|
10473
|
+
if (segment.startsWith(":") && segment.length > 1) {
|
|
10474
|
+
return `[${segment.slice(1)}]`;
|
|
10475
|
+
}
|
|
10476
|
+
return segment;
|
|
10477
|
+
});
|
|
10478
|
+
return `pages/${segments.join("/")}.vue`;
|
|
10479
|
+
}
|
|
10480
|
+
|
|
10481
|
+
// src/commands/validate.ts
|
|
10319
10482
|
function validate3() {
|
|
10320
10483
|
const root = process.cwd();
|
|
10321
10484
|
if (!exists(root)) {
|
|
@@ -10332,7 +10495,7 @@ function validate3() {
|
|
|
10332
10495
|
}
|
|
10333
10496
|
let warnings = 0;
|
|
10334
10497
|
for (const page of m.pages) {
|
|
10335
|
-
const component = page.component || (page.path
|
|
10498
|
+
const component = page.component || pageComponentFromPath(page.path);
|
|
10336
10499
|
const fullPath = join15(root, "src", component);
|
|
10337
10500
|
if (!existsSync12(fullPath)) {
|
|
10338
10501
|
console.log(source_default.yellow(` \u26A0 Page component not found: src/${component}`));
|
|
@@ -10383,7 +10546,7 @@ function check() {
|
|
|
10383
10546
|
}
|
|
10384
10547
|
let warnings = 0;
|
|
10385
10548
|
for (const page of m.pages) {
|
|
10386
|
-
const component = page.component || (page.path
|
|
10549
|
+
const component = page.component || pageComponentFromPath(page.path);
|
|
10387
10550
|
if (!existsSync13(join16(root, "src", component))) {
|
|
10388
10551
|
console.log(source_default.yellow(` \u26A0 Page not found: src/${component}`));
|
|
10389
10552
|
warnings++;
|
|
@@ -10946,6 +11109,33 @@ async function graphPush() {
|
|
|
10946
11109
|
process.exit(1);
|
|
10947
11110
|
}
|
|
10948
11111
|
}
|
|
11112
|
+
function parseDefaultValue(raw) {
|
|
11113
|
+
const value = raw.trim();
|
|
11114
|
+
try {
|
|
11115
|
+
return JSON.parse(value);
|
|
11116
|
+
} catch {}
|
|
11117
|
+
const quote = value[0];
|
|
11118
|
+
if ((quote === "'" || quote === '"') && value.endsWith(quote)) {
|
|
11119
|
+
return value.slice(1, -1).replace(/\\(['"\\bfnrt])/g, (_, ch) => {
|
|
11120
|
+
switch (ch) {
|
|
11121
|
+
case "b":
|
|
11122
|
+
return "\b";
|
|
11123
|
+
case "f":
|
|
11124
|
+
return "\f";
|
|
11125
|
+
case "n":
|
|
11126
|
+
return `
|
|
11127
|
+
`;
|
|
11128
|
+
case "r":
|
|
11129
|
+
return "\r";
|
|
11130
|
+
case "t":
|
|
11131
|
+
return "\t";
|
|
11132
|
+
default:
|
|
11133
|
+
return ch;
|
|
11134
|
+
}
|
|
11135
|
+
});
|
|
11136
|
+
}
|
|
11137
|
+
return value;
|
|
11138
|
+
}
|
|
10949
11139
|
function parseModelFile(content, fileName) {
|
|
10950
11140
|
const modelMatch = content.match(/defineModel\s*\(\s*['"](\w+)['"]/);
|
|
10951
11141
|
if (!modelMatch)
|
|
@@ -10973,11 +11163,7 @@ function parseModelFile(content, fileName) {
|
|
|
10973
11163
|
field.validation = "url";
|
|
10974
11164
|
const defaultMatch = modifiers.match(/\.default\((.+?)\)/);
|
|
10975
11165
|
if (defaultMatch) {
|
|
10976
|
-
|
|
10977
|
-
field.default = JSON.parse(defaultMatch[1]);
|
|
10978
|
-
} catch {
|
|
10979
|
-
field.default = defaultMatch[1];
|
|
10980
|
-
}
|
|
11166
|
+
field.default = parseDefaultValue(defaultMatch[1]);
|
|
10981
11167
|
}
|
|
10982
11168
|
}
|
|
10983
11169
|
fields.push(field);
|
|
@@ -11159,10 +11345,41 @@ function parseModelFields(content, fileName) {
|
|
|
11159
11345
|
}
|
|
11160
11346
|
return { name: modelMatch[1], fields };
|
|
11161
11347
|
}
|
|
11348
|
+
|
|
11349
|
+
// src/commands/graph/fork.ts
|
|
11350
|
+
init_source();
|
|
11351
|
+
var spaceIDRegex = /^[a-z][a-z0-9-]*$/;
|
|
11352
|
+
function forkManifest(root, newSpaceID) {
|
|
11353
|
+
const id = newSpaceID.trim();
|
|
11354
|
+
if (!spaceIDRegex.test(id)) {
|
|
11355
|
+
throw new Error("space id must be lowercase alphanumeric with hyphens, starting with a letter");
|
|
11356
|
+
}
|
|
11357
|
+
if (!exists(root)) {
|
|
11358
|
+
throw new Error("No space.manifest.json found in current directory");
|
|
11359
|
+
}
|
|
11360
|
+
const m = read(root);
|
|
11361
|
+
if (m.id === id) {
|
|
11362
|
+
throw new Error(`space.manifest.json already uses id "${id}"`);
|
|
11363
|
+
}
|
|
11364
|
+
const oldSpaceID = m.id;
|
|
11365
|
+
m.id = id;
|
|
11366
|
+
write(root, m);
|
|
11367
|
+
return { oldSpaceID, newSpaceID: id };
|
|
11368
|
+
}
|
|
11369
|
+
function graphFork(newSpaceID) {
|
|
11370
|
+
try {
|
|
11371
|
+
const result = forkManifest(process.cwd(), newSpaceID);
|
|
11372
|
+
console.log(source_default.green(`Forked graph space id: ${result.oldSpaceID} -> ${result.newSpaceID}`));
|
|
11373
|
+
console.log(source_default.dim("Run `construct graph push` to register the forked schema."));
|
|
11374
|
+
} catch (err) {
|
|
11375
|
+
console.error(source_default.red(err.message));
|
|
11376
|
+
process.exit(1);
|
|
11377
|
+
}
|
|
11378
|
+
}
|
|
11162
11379
|
// package.json
|
|
11163
11380
|
var package_default = {
|
|
11164
11381
|
name: "@construct-space/cli",
|
|
11165
|
-
version: "1.
|
|
11382
|
+
version: "1.6.2",
|
|
11166
11383
|
description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
|
|
11167
11384
|
type: "module",
|
|
11168
11385
|
bin: {
|
|
@@ -11226,6 +11443,7 @@ var graph = program2.command("graph").description("Construct Graph \u2014 data m
|
|
|
11226
11443
|
graph.command("init").description("Initialize Graph in a space project").action(() => graphInit());
|
|
11227
11444
|
graph.command("generate <model> [fields...]").alias("g").description("Generate a data model").option("--access <rules>", "Access rules (e.g. read:member,create:member,update:owner,delete:admin)").action((model, fields, opts) => generate2(model, fields, opts));
|
|
11228
11445
|
graph.command("push").description("Register models with the Graph service").action(async () => graphPush());
|
|
11446
|
+
graph.command("fork <new-space-id>").description("Rewrite this space manifest to a new graph space id").action((newSpaceID) => graphFork(newSpaceID));
|
|
11229
11447
|
graph.command("migrate").description("Compare local models with server schema and apply changes").option("--apply", "Apply destructive changes (drop columns, alter constraints)").action(async (opts) => graphMigrate(opts));
|
|
11230
11448
|
graph.command("spaces").alias("list").alias("ls").description("List spaces published by your org").option("--org <id>", "Override org id").option("--bundle <id>", "Filter to a single bundle").option("--json", "Output JSON").action(async (opts) => (await Promise.resolve().then(() => (init_spaces(), exports_spaces))).spacesList(opts));
|
|
11231
11449
|
var bundles = graph.command("bundles").description("Manage space bundles (publisher grouping)");
|
|
@@ -1,35 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Space Actions — exposed to the AI agent via space_run_action
|
|
2
|
+
* Space Actions — exposed to the AI agent via space_run_action.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Each action: { description, params, run }.
|
|
5
|
+
* - `params` describe agent-callable inputs (type, description, required).
|
|
6
|
+
* - `run` receives the payload and returns any JSON-serialisable value.
|
|
7
|
+
*
|
|
8
|
+
* Pair with @construct-space/graph for typed multi-tenant data:
|
|
9
|
+
* import { useGraph } from '@construct-space/graph'
|
|
10
|
+
* import { Item } from './models'
|
|
11
|
+
* const items = useGraph(Item)
|
|
12
|
+
*
|
|
13
|
+
* listItems: {
|
|
14
|
+
* description: 'List items',
|
|
15
|
+
* params: { limit: { type: 'number', required: false } },
|
|
16
|
+
* run: async (p: any) => ({ items: await items.find({ limit: p.limit ?? 50 }) }),
|
|
17
|
+
* }
|
|
6
18
|
*/
|
|
7
19
|
|
|
8
|
-
// --- Graph SDK (data layer) ---
|
|
9
|
-
// Uncomment after running `construct graph init` and defining models:
|
|
10
|
-
//
|
|
11
|
-
// import { useGraph } from '@construct-space/graph'
|
|
12
|
-
// const graph = useGraph()
|
|
13
|
-
//
|
|
14
|
-
// Example action using Graph:
|
|
15
|
-
// fetchItems: {
|
|
16
|
-
// description: 'Fetch items from the graph',
|
|
17
|
-
// params: {
|
|
18
|
-
// limit: { type: 'number', description: 'Max items to return', required: false },
|
|
19
|
-
// },
|
|
20
|
-
// run: async (p: any) => {
|
|
21
|
-
// const items = await graph.query('Item').limit(p.limit ?? 10).exec()
|
|
22
|
-
// return { items }
|
|
23
|
-
// },
|
|
24
|
-
// },
|
|
25
|
-
|
|
26
20
|
export const actions = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// },
|
|
33
|
-
// run: (p: any) => ({ message: `Hello ${p.name}!` }),
|
|
34
|
-
// },
|
|
21
|
+
ping: {
|
|
22
|
+
description: 'Health check — returns pong with the space id.',
|
|
23
|
+
params: {},
|
|
24
|
+
run: () => ({ pong: true, space: '{{.ID}}' }),
|
|
25
|
+
},
|
|
35
26
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: construct-stack
|
|
3
|
+
name: Construct Stack Guide
|
|
4
|
+
description: How to build and extend a Construct space — CLI, UI, SDK, Graph, manifest, agent actions
|
|
5
|
+
trigger: construct|space|graph|ui|sdk|manifest|action
|
|
6
|
+
category: skill
|
|
7
|
+
tools: [read_file, list_dir, glob, grep, write_file, edit_file]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Construct.md — agent guide for `{{.DisplayName}}`
|
|
11
|
+
|
|
12
|
+
This file briefs an AI coding agent on the Construct stack so it can build, extend, and ship this space without external lookups.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Stack at a glance
|
|
17
|
+
|
|
18
|
+
| Layer | Package | Purpose |
|
|
19
|
+
|-------|---------|---------|
|
|
20
|
+
| UI | `@construct-space/ui` | Vue 3 component library |
|
|
21
|
+
| Host | `@construct-space/sdk` | App composables (org, members, toast, auth) |
|
|
22
|
+
| Data | `@construct-space/graph` | Multi-tenant typed data layer |
|
|
23
|
+
| Icons | `lucide-vue-next` | Icon set |
|
|
24
|
+
| Tooling | `@construct-space/cli` | Build / dev / publish |
|
|
25
|
+
|
|
26
|
+
All four are **host-provided externals** — import normally, never bundle. Vite externalises them via `vite.config.ts`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Layout
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{{.Name}}/
|
|
34
|
+
space.manifest.json scope, pages, toolbar, agent, widgets, actions
|
|
35
|
+
src/
|
|
36
|
+
entry.ts auto-generated; do not hand-edit
|
|
37
|
+
actions.ts agent-callable actions (space_run_action)
|
|
38
|
+
pages/ filesystem routing (e.g. settings.vue → /settings)
|
|
39
|
+
components/ local Vue components
|
|
40
|
+
composables/ local composables (use* convention)
|
|
41
|
+
models/ graph models (defineModel)
|
|
42
|
+
agent/
|
|
43
|
+
config.md agent system prompt, tool whitelist, max iterations
|
|
44
|
+
skills/ scoped skills loaded with the agent
|
|
45
|
+
hooks/safety.json tool-call interception rules
|
|
46
|
+
widgets/ home-screen widgets (run inside Shadow DOM sandbox)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
construct dev hot-reload dev (regenerates entry.ts on change)
|
|
55
|
+
construct build production build → dist/
|
|
56
|
+
construct check manifest + typecheck + lint
|
|
57
|
+
construct install install to active profile
|
|
58
|
+
construct publish publish to registry (requires auth)
|
|
59
|
+
construct graph push register models with graph backend
|
|
60
|
+
construct graph install <id> install a published space for current org
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Manifest essentials
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
{
|
|
69
|
+
"id": "{{.ID}}",
|
|
70
|
+
"scope": "app | project | org", // org = multi-tenant per organization
|
|
71
|
+
"minConstructVersion": "0.7.0",
|
|
72
|
+
"pages": [{ "path": "", "label": "Home", "default": true }],
|
|
73
|
+
"toolbar": [{ "id": "...", "icon": "...", "action": "createX" }],
|
|
74
|
+
"agent": "agent/config.md",
|
|
75
|
+
"skills": ["agent/skills/default.md"],
|
|
76
|
+
"actions": "src/actions.ts",
|
|
77
|
+
"widgets": [{ "id": "summary", "defaultSize": "4x1", "sizes": {...} }]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`scope: 'org'` makes everything multi-tenant — graph schemas auto-partition per org. Use `useOrg()` to read current org context.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `@construct-space/ui` — component cheatsheet
|
|
86
|
+
|
|
87
|
+
All exported from the package root:
|
|
88
|
+
|
|
89
|
+
**Layout**: `Card` (slots: header/accessory/default/footer/footer-end; variants: default/outline/muted; modifiers: hoverable/interactive), `SplitPane`, `ScrollArea`, `Separator`, `PanelSection`, `DashboardPanel`
|
|
90
|
+
|
|
91
|
+
**Inputs**: `Input`, `Textarea`, `Select` (options: `{label, value}[]`), `MultiSelect`, `Checkbox`, `Switch`, `RadioGroup`, `DatePicker`, `ColorPicker`, `FileInput`, `Slider`, `Autocomplete`, `FormField` (label/hint wrapper)
|
|
92
|
+
|
|
93
|
+
**Display**: `Badge` (color: primary|neutral|success|warning|error|info; variant: solid|soft|subtle|outline), `Chip`, `Avatar` (`fallback` shows text — pass initials), `Empty`, `Skeleton`, `Progress`, `Tooltip`, `Kbd`, `Alert`, `Notification`/`Toast`
|
|
94
|
+
|
|
95
|
+
**Actions**: `Button` (variant: solid|ghost|outline; color: primary|neutral|error|...; size: xs|sm|md|lg), `Dropdown`, `DropdownMenu`, `ContextMenu`, `Popover`, `ToggleGroup` (segmented control)
|
|
96
|
+
|
|
97
|
+
**Overlays**: `Modal` (`v-model:open`, slots: header/body/footer), `Slideover`, `Drawer`, `ConfirmationModal` (`@confirm`, `@cancel`), `Pagination`
|
|
98
|
+
|
|
99
|
+
**Data**: `Table` (`columns: TableColumn[]`, `rows`, `selectable`, `striped`, `stickyHeader`, slots: `cell-{key}`, `header-{key}`, `empty`), `Tree`, `Timeline`, `Tabs`, `Tab`, `Accordion`, `Calendar`, `Breadcrumbs`
|
|
100
|
+
|
|
101
|
+
**Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
|
|
102
|
+
|
|
103
|
+
### Component gotchas
|
|
104
|
+
|
|
105
|
+
- **`Avatar` sizes**: only `sm | md | lg` (no `xs`).
|
|
106
|
+
- **`Select`** doesn't accept `multiple` — render a chip-toggle list yourself for multi-select.
|
|
107
|
+
- **`Table`** column `align` literals must be `as const` to typecheck (`align: 'right' as const`).
|
|
108
|
+
- **`Table` rowKey** is a string field name on the row object, not a function.
|
|
109
|
+
- **`Modal`** uses `v-model:open` (not `v-model`); slots: `header | body | footer | accessory`.
|
|
110
|
+
- **`Card`** slots: `header | accessory | default | footer | footer-end`. Use `title`/`description` props for the header to get standard styling; put right-side actions in `accessory`.
|
|
111
|
+
- **`ConfirmationModal`** emits `@confirm` and `@cancel` — wire both, otherwise dismissal does nothing.
|
|
112
|
+
- **Settings pages**: never raw `<select>`/`<input>`/`<textarea>` — use `Select`, `Switch`, `Input`, `Textarea` with `FormField` labels. Persist with `localStorage` (key prefix per space).
|
|
113
|
+
|
|
114
|
+
```vue
|
|
115
|
+
<script setup lang="ts">
|
|
116
|
+
import { Card, Button, Modal, Badge } from '@construct-space/ui'
|
|
117
|
+
const open = ref(false)
|
|
118
|
+
</script>
|
|
119
|
+
<template>
|
|
120
|
+
<Card title="Hello" hoverable>
|
|
121
|
+
<Badge color="success" variant="subtle" label="Live" />
|
|
122
|
+
<template #footer-end>
|
|
123
|
+
<Button variant="solid" size="sm" @click="open = true">Open</Button>
|
|
124
|
+
</template>
|
|
125
|
+
</Card>
|
|
126
|
+
<Modal v-model:open="open" title="Demo">…</Modal>
|
|
127
|
+
</template>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## `@construct-space/sdk` — host composables
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { useOrg, useOrgMembers, useToast, useAuth } from '@construct-space/sdk'
|
|
136
|
+
|
|
137
|
+
const { orgId, isOrg, org } = useOrg() // current org context (reactive)
|
|
138
|
+
const { members } = useOrgMembers() // [{ user_id, name, email, role }, ...]
|
|
139
|
+
const toast = useToast() // toast.success(...), toast.error(...)
|
|
140
|
+
const { user, isAuthenticated, logout } = useAuth()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use these for org-aware UX (assignee pickers, "shared with N" badges, avatars, member lookups).
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## `@construct-space/graph` — data layer
|
|
148
|
+
|
|
149
|
+
Define typed models, get a typed reactive client, multi-tenant by default.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// src/models/Task.ts
|
|
153
|
+
import { defineModel, field, access } from '@construct-space/graph'
|
|
154
|
+
|
|
155
|
+
export const Task = defineModel('task', {
|
|
156
|
+
title: field.string().required(),
|
|
157
|
+
description: field.string(),
|
|
158
|
+
priority: field.string(),
|
|
159
|
+
due_date: field.date(),
|
|
160
|
+
position: field.int(),
|
|
161
|
+
labels: field.json(),
|
|
162
|
+
assignee_id: field.string().index(),
|
|
163
|
+
}, {
|
|
164
|
+
scope: 'org', // partitions schema per organization
|
|
165
|
+
access: {
|
|
166
|
+
read: access.authenticated(),
|
|
167
|
+
create: access.authenticated(),
|
|
168
|
+
update: access.authenticated(),
|
|
169
|
+
delete: access.owner(),
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// src/composables/useTasks.ts
|
|
176
|
+
import { useGraph } from '@construct-space/graph'
|
|
177
|
+
import { Task } from '../models'
|
|
178
|
+
|
|
179
|
+
const tasks = useGraph(Task)
|
|
180
|
+
await tasks.find({ where: { priority: 'high' }, orderBy: { position: 'asc' }, limit: 50 })
|
|
181
|
+
await tasks.create({ title: 'Ship', position: 0 })
|
|
182
|
+
await tasks.update(id, { position: 1 })
|
|
183
|
+
await tasks.remove(id)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Field types**: `string`, `int`, `number`, `boolean`, `date`, `json`, `enum([...])`. Chain `.required()`, `.index()`, `.unique()`, `.default(v)`.
|
|
187
|
+
|
|
188
|
+
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context — use `authenticated()` for org-scoped models that allow any logged-in member.
|
|
189
|
+
|
|
190
|
+
**Scopes**:
|
|
191
|
+
- `app` — single shared schema (rare)
|
|
192
|
+
- `org` — partitioned by organization (most product spaces)
|
|
193
|
+
- `project` — partitioned by project (dev-tooling spaces)
|
|
194
|
+
|
|
195
|
+
**Push models to backend after editing**: `construct graph push`.
|
|
196
|
+
|
|
197
|
+
### Pitfalls
|
|
198
|
+
|
|
199
|
+
- **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`. Pattern:
|
|
200
|
+
```ts
|
|
201
|
+
function parseLabels(raw: unknown): string[] {
|
|
202
|
+
if (Array.isArray(raw)) return raw as string[]
|
|
203
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
204
|
+
try { const v = JSON.parse(raw); return Array.isArray(v) ? v : [] } catch { return [] }
|
|
205
|
+
}
|
|
206
|
+
return []
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
|
|
210
|
+
- **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
|
|
211
|
+
- **Schema ownership is sticky**: the first profile to `construct graph push` becomes the schema owner. Subsequent pushes from a different profile fail with "Ownership check failed". Bump the space `id` to fork, or push from the original owner profile.
|
|
212
|
+
- **`access.member()` requires org context** — use `access.authenticated()` for org-scoped models that any logged-in member should be able to read/write. `member()` is for stricter membership checks.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Actions (agent surface)
|
|
217
|
+
|
|
218
|
+
`src/actions.ts` exports an `actions` object — each entry becomes an agent-callable tool via `space_run_action`.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
export const actions = {
|
|
222
|
+
createTask: {
|
|
223
|
+
description: 'Create a task',
|
|
224
|
+
params: {
|
|
225
|
+
title: { type: 'string', required: true },
|
|
226
|
+
priority: { type: 'string', required: false },
|
|
227
|
+
},
|
|
228
|
+
run: async (p: any) => ({ task: await tasks.create(p) }),
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The agent (configured in `agent/config.md`) calls `space_list_actions` then `space_run_action`. Keep descriptions tight and specific — they're the only docstring the agent sees.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Drag and drop (HTML5)
|
|
238
|
+
|
|
239
|
+
Tauri intercepts native drag-drop by default. The Construct host sets `dragDropEnabled: false` so HTML5 events fire normally — use `draggable="true"`, `@dragstart`, `@dragover.prevent`, `@drop`. No extra deps required; reach for `vuedraggable` only if reordering needs auto-scroll/animation.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Conventions
|
|
244
|
+
|
|
245
|
+
- **Imports**: `@construct-space/*` packages always external. Local imports use relative paths (no `@/` alias unless you add one).
|
|
246
|
+
- **Composables**: `use*` prefix, keep in `src/composables/`.
|
|
247
|
+
- **Pages**: filename = route path. `index.vue` → `/`, `settings.vue` → `/settings`, `[id].vue` → param.
|
|
248
|
+
- **Styling**: Tailwind utilities + CSS vars `--app-foreground`, `--app-background`, `--app-muted`, `--app-border`, `--app-accent`. Don't hard-code colors; the host themes via these variables.
|
|
249
|
+
- **Comments**: explain *why*, not *what*. Skip TODOs in committed code.
|
|
250
|
+
- **No emojis** in source unless the user requests them.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Build pipeline reminder
|
|
255
|
+
|
|
256
|
+
`construct build` writes:
|
|
257
|
+
- `dist/space-{{.ID}}.iife.js` — the bundled space
|
|
258
|
+
- `dist/manifest.json` — manifest + `build` block (checksum, size, **`hostApiVersion`**, builtAt)
|
|
259
|
+
|
|
260
|
+
The runtime SpaceLoader compares `hostApiVersion` to its own; mismatches log a warning. Bump the CLI to keep them aligned.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
languageOptions: {
|
|
7
|
+
ecmaVersion: 'latest',
|
|
8
|
+
sourceType: 'module',
|
|
9
|
+
globals: {
|
|
10
|
+
console: 'readonly',
|
|
11
|
+
window: 'readonly',
|
|
12
|
+
document: 'readonly',
|
|
13
|
+
globalThis: 'readonly',
|
|
14
|
+
setTimeout: 'readonly',
|
|
15
|
+
clearTimeout: 'readonly',
|
|
16
|
+
history: 'readonly',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
rules: {
|
|
20
|
+
'no-unused-vars': 'off',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
ignores: ['dist/', 'node_modules/', '*.d.ts'],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* {{.DisplayName}} — Home page
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Built-in libraries available at runtime (do not bundle):
|
|
6
|
+
* @construct-space/ui — Vue 3 components (Button, Card, Modal, Table, Badge, ...)
|
|
7
|
+
* @construct-space/sdk — useOrg, useOrgMembers, useToast, useAuth
|
|
8
|
+
* @construct-space/graph — defineModel, useGraph (multi-tenant data layer)
|
|
9
|
+
* lucide-vue-next — icons
|
|
7
10
|
*/
|
|
8
11
|
import { ref } from 'vue'
|
|
12
|
+
import { Button, Card, Empty } from '@construct-space/ui'
|
|
13
|
+
import { Sparkles } from 'lucide-vue-next'
|
|
9
14
|
|
|
10
15
|
const greeting = ref('Your space is ready. Start building!')
|
|
11
16
|
</script>
|
|
12
17
|
|
|
13
18
|
<template>
|
|
14
|
-
<div class="h-full flex items-center justify-center">
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<div class="h-full flex items-center justify-center p-6">
|
|
20
|
+
<Card variant="default" class="max-w-md w-full text-center">
|
|
21
|
+
<template #header>
|
|
22
|
+
<div class="flex items-center justify-center gap-2 w-full">
|
|
23
|
+
<Sparkles class="size-5 text-[var(--app-accent)]" />
|
|
24
|
+
<h1 class="text-lg font-semibold text-[var(--app-foreground)]">{{.DisplayName}}</h1>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<Empty :description="greeting" size="sm" />
|
|
29
|
+
|
|
30
|
+
<template #footer-end>
|
|
31
|
+
<Button variant="solid" size="sm" label="Get Started" />
|
|
32
|
+
</template>
|
|
33
|
+
</Card>
|
|
19
34
|
</div>
|
|
20
35
|
</template>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"build": "construct build",
|
|
9
9
|
"dev": "construct dev",
|
|
10
10
|
"check": "construct check",
|
|
11
|
+
"lint": "eslint .",
|
|
11
12
|
"validate": "construct validate"
|
|
12
13
|
},
|
|
13
14
|
"peerDependencies": {
|
|
@@ -18,10 +19,18 @@
|
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@construct-space/cli": "latest",
|
|
20
21
|
"@construct-space/sdk": "latest",
|
|
22
|
+
"@eslint/js": "^10.0.1",
|
|
21
23
|
"@vitejs/plugin-vue": "^5.2.3",
|
|
24
|
+
"eslint": "^10.2.1",
|
|
25
|
+
"eslint-plugin-vue": "^10.0.0",
|
|
22
26
|
"lucide-vue-next": "^1.0.0",
|
|
23
27
|
"typescript": "^5.9.3",
|
|
28
|
+
"typescript-eslint": "^8.0.0",
|
|
24
29
|
"vite": "^6.3.5",
|
|
25
30
|
"vue-tsc": "^3.2.5"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@construct-space/graph": "^0.5.0",
|
|
34
|
+
"@construct-space/ui": "^0.5.2"
|
|
26
35
|
}
|
|
27
36
|
}
|
package/package.json
CHANGED
|
@@ -1,35 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Space Actions — exposed to the AI agent via space_run_action
|
|
2
|
+
* Space Actions — exposed to the AI agent via space_run_action.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Each action: { description, params, run }.
|
|
5
|
+
* - `params` describe agent-callable inputs (type, description, required).
|
|
6
|
+
* - `run` receives the payload and returns any JSON-serialisable value.
|
|
7
|
+
*
|
|
8
|
+
* Pair with @construct-space/graph for typed multi-tenant data:
|
|
9
|
+
* import { useGraph } from '@construct-space/graph'
|
|
10
|
+
* import { Item } from './models'
|
|
11
|
+
* const items = useGraph(Item)
|
|
12
|
+
*
|
|
13
|
+
* listItems: {
|
|
14
|
+
* description: 'List items',
|
|
15
|
+
* params: { limit: { type: 'number', required: false } },
|
|
16
|
+
* run: async (p: any) => ({ items: await items.find({ limit: p.limit ?? 50 }) }),
|
|
17
|
+
* }
|
|
6
18
|
*/
|
|
7
19
|
|
|
8
|
-
// --- Graph SDK (data layer) ---
|
|
9
|
-
// Uncomment after running `construct graph init` and defining models:
|
|
10
|
-
//
|
|
11
|
-
// import { useGraph } from '@construct-space/graph'
|
|
12
|
-
// const graph = useGraph()
|
|
13
|
-
//
|
|
14
|
-
// Example action using Graph:
|
|
15
|
-
// fetchItems: {
|
|
16
|
-
// description: 'Fetch items from the graph',
|
|
17
|
-
// params: {
|
|
18
|
-
// limit: { type: 'number', description: 'Max items to return', required: false },
|
|
19
|
-
// },
|
|
20
|
-
// run: async (p: any) => {
|
|
21
|
-
// const items = await graph.query('Item').limit(p.limit ?? 10).exec()
|
|
22
|
-
// return { items }
|
|
23
|
-
// },
|
|
24
|
-
// },
|
|
25
|
-
|
|
26
20
|
export const actions = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// },
|
|
33
|
-
// run: (p: any) => ({ message: `Hello ${p.name}!` }),
|
|
34
|
-
// },
|
|
21
|
+
ping: {
|
|
22
|
+
description: 'Health check — returns pong with the space id.',
|
|
23
|
+
params: {},
|
|
24
|
+
run: () => ({ pong: true, space: '{{.ID}}' }),
|
|
25
|
+
},
|
|
35
26
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: construct-stack
|
|
3
|
+
name: Construct Stack Guide
|
|
4
|
+
description: How to build and extend a Construct space — CLI, UI, SDK, Graph, manifest, agent actions
|
|
5
|
+
trigger: construct|space|graph|ui|sdk|manifest|action
|
|
6
|
+
category: skill
|
|
7
|
+
tools: [read_file, list_dir, glob, grep, write_file, edit_file]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Construct.md — agent guide for `{{.DisplayName}}`
|
|
11
|
+
|
|
12
|
+
This file briefs an AI coding agent on the Construct stack so it can build, extend, and ship this space without external lookups.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Stack at a glance
|
|
17
|
+
|
|
18
|
+
| Layer | Package | Purpose |
|
|
19
|
+
|-------|---------|---------|
|
|
20
|
+
| UI | `@construct-space/ui` | Vue 3 component library |
|
|
21
|
+
| Host | `@construct-space/sdk` | App composables (org, members, toast, auth) |
|
|
22
|
+
| Data | `@construct-space/graph` | Multi-tenant typed data layer |
|
|
23
|
+
| Icons | `lucide-vue-next` | Icon set |
|
|
24
|
+
| Tooling | `@construct-space/cli` | Build / dev / publish |
|
|
25
|
+
|
|
26
|
+
All four are **host-provided externals** — import normally, never bundle. Vite externalises them via `vite.config.ts`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Layout
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{{.Name}}/
|
|
34
|
+
space.manifest.json scope, pages, toolbar, agent, widgets, actions
|
|
35
|
+
src/
|
|
36
|
+
entry.ts auto-generated; do not hand-edit
|
|
37
|
+
actions.ts agent-callable actions (space_run_action)
|
|
38
|
+
pages/ filesystem routing (e.g. settings.vue → /settings)
|
|
39
|
+
components/ local Vue components
|
|
40
|
+
composables/ local composables (use* convention)
|
|
41
|
+
models/ graph models (defineModel)
|
|
42
|
+
agent/
|
|
43
|
+
config.md agent system prompt, tool whitelist, max iterations
|
|
44
|
+
skills/ scoped skills loaded with the agent
|
|
45
|
+
hooks/safety.json tool-call interception rules
|
|
46
|
+
widgets/ home-screen widgets (run inside Shadow DOM sandbox)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
construct dev hot-reload dev (regenerates entry.ts on change)
|
|
55
|
+
construct build production build → dist/
|
|
56
|
+
construct check manifest + typecheck + lint
|
|
57
|
+
construct install install to active profile
|
|
58
|
+
construct publish publish to registry (requires auth)
|
|
59
|
+
construct graph push register models with graph backend
|
|
60
|
+
construct graph install <id> install a published space for current org
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Manifest essentials
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
{
|
|
69
|
+
"id": "{{.ID}}",
|
|
70
|
+
"scope": "app | project | org", // org = multi-tenant per organization
|
|
71
|
+
"minConstructVersion": "0.7.0",
|
|
72
|
+
"pages": [{ "path": "", "label": "Home", "default": true }],
|
|
73
|
+
"toolbar": [{ "id": "...", "icon": "...", "action": "createX" }],
|
|
74
|
+
"agent": "agent/config.md",
|
|
75
|
+
"skills": ["agent/skills/default.md"],
|
|
76
|
+
"actions": "src/actions.ts",
|
|
77
|
+
"widgets": [{ "id": "summary", "defaultSize": "4x1", "sizes": {...} }]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`scope: 'org'` makes everything multi-tenant — graph schemas auto-partition per org. Use `useOrg()` to read current org context.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `@construct-space/ui` — component cheatsheet
|
|
86
|
+
|
|
87
|
+
All exported from the package root:
|
|
88
|
+
|
|
89
|
+
**Layout**: `Card` (slots: header/accessory/default/footer/footer-end; variants: default/outline/muted; modifiers: hoverable/interactive), `SplitPane`, `ScrollArea`, `Separator`, `PanelSection`, `DashboardPanel`
|
|
90
|
+
|
|
91
|
+
**Inputs**: `Input`, `Textarea`, `Select` (options: `{label, value}[]`), `MultiSelect`, `Checkbox`, `Switch`, `RadioGroup`, `DatePicker`, `ColorPicker`, `FileInput`, `Slider`, `Autocomplete`, `FormField` (label/hint wrapper)
|
|
92
|
+
|
|
93
|
+
**Display**: `Badge` (color: primary|neutral|success|warning|error|info; variant: solid|soft|subtle|outline), `Chip`, `Avatar` (`fallback` shows text — pass initials), `Empty`, `Skeleton`, `Progress`, `Tooltip`, `Kbd`, `Alert`, `Notification`/`Toast`
|
|
94
|
+
|
|
95
|
+
**Actions**: `Button` (variant: solid|ghost|outline; color: primary|neutral|error|...; size: xs|sm|md|lg), `Dropdown`, `DropdownMenu`, `ContextMenu`, `Popover`, `ToggleGroup` (segmented control)
|
|
96
|
+
|
|
97
|
+
**Overlays**: `Modal` (`v-model:open`, slots: header/body/footer), `Slideover`, `Drawer`, `ConfirmationModal` (`@confirm`, `@cancel`), `Pagination`
|
|
98
|
+
|
|
99
|
+
**Data**: `Table` (`columns: TableColumn[]`, `rows`, `selectable`, `striped`, `stickyHeader`, slots: `cell-{key}`, `header-{key}`, `empty`), `Tree`, `Timeline`, `Tabs`, `Tab`, `Accordion`, `Calendar`, `Breadcrumbs`
|
|
100
|
+
|
|
101
|
+
**Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
|
|
102
|
+
|
|
103
|
+
### Component gotchas
|
|
104
|
+
|
|
105
|
+
- **`Avatar` sizes**: only `sm | md | lg` (no `xs`).
|
|
106
|
+
- **`Select`** doesn't accept `multiple` — render a chip-toggle list yourself for multi-select.
|
|
107
|
+
- **`Table`** column `align` literals must be `as const` to typecheck (`align: 'right' as const`).
|
|
108
|
+
- **`Table` rowKey** is a string field name on the row object, not a function.
|
|
109
|
+
- **`Modal`** uses `v-model:open` (not `v-model`); slots: `header | body | footer | accessory`.
|
|
110
|
+
- **`Card`** slots: `header | accessory | default | footer | footer-end`. Use `title`/`description` props for the header to get standard styling; put right-side actions in `accessory`.
|
|
111
|
+
- **`ConfirmationModal`** emits `@confirm` and `@cancel` — wire both, otherwise dismissal does nothing.
|
|
112
|
+
- **Settings pages**: never raw `<select>`/`<input>`/`<textarea>` — use `Select`, `Switch`, `Input`, `Textarea` with `FormField` labels. Persist with `localStorage` (key prefix per space).
|
|
113
|
+
|
|
114
|
+
```vue
|
|
115
|
+
<script setup lang="ts">
|
|
116
|
+
import { Card, Button, Modal, Badge } from '@construct-space/ui'
|
|
117
|
+
const open = ref(false)
|
|
118
|
+
</script>
|
|
119
|
+
<template>
|
|
120
|
+
<Card title="Hello" hoverable>
|
|
121
|
+
<Badge color="success" variant="subtle" label="Live" />
|
|
122
|
+
<template #footer-end>
|
|
123
|
+
<Button variant="solid" size="sm" @click="open = true">Open</Button>
|
|
124
|
+
</template>
|
|
125
|
+
</Card>
|
|
126
|
+
<Modal v-model:open="open" title="Demo">…</Modal>
|
|
127
|
+
</template>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## `@construct-space/sdk` — host composables
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { useOrg, useOrgMembers, useToast, useAuth } from '@construct-space/sdk'
|
|
136
|
+
|
|
137
|
+
const { orgId, isOrg, org } = useOrg() // current org context (reactive)
|
|
138
|
+
const { members } = useOrgMembers() // [{ user_id, name, email, role }, ...]
|
|
139
|
+
const toast = useToast() // toast.success(...), toast.error(...)
|
|
140
|
+
const { user, isAuthenticated, logout } = useAuth()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use these for org-aware UX (assignee pickers, "shared with N" badges, avatars, member lookups).
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## `@construct-space/graph` — data layer
|
|
148
|
+
|
|
149
|
+
Define typed models, get a typed reactive client, multi-tenant by default.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// src/models/Task.ts
|
|
153
|
+
import { defineModel, field, access } from '@construct-space/graph'
|
|
154
|
+
|
|
155
|
+
export const Task = defineModel('task', {
|
|
156
|
+
title: field.string().required(),
|
|
157
|
+
description: field.string(),
|
|
158
|
+
priority: field.string(),
|
|
159
|
+
due_date: field.date(),
|
|
160
|
+
position: field.int(),
|
|
161
|
+
labels: field.json(),
|
|
162
|
+
assignee_id: field.string().index(),
|
|
163
|
+
}, {
|
|
164
|
+
scope: 'org', // partitions schema per organization
|
|
165
|
+
access: {
|
|
166
|
+
read: access.authenticated(),
|
|
167
|
+
create: access.authenticated(),
|
|
168
|
+
update: access.authenticated(),
|
|
169
|
+
delete: access.owner(),
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// src/composables/useTasks.ts
|
|
176
|
+
import { useGraph } from '@construct-space/graph'
|
|
177
|
+
import { Task } from '../models'
|
|
178
|
+
|
|
179
|
+
const tasks = useGraph(Task)
|
|
180
|
+
await tasks.find({ where: { priority: 'high' }, orderBy: { position: 'asc' }, limit: 50 })
|
|
181
|
+
await tasks.create({ title: 'Ship', position: 0 })
|
|
182
|
+
await tasks.update(id, { position: 1 })
|
|
183
|
+
await tasks.remove(id)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Field types**: `string`, `int`, `number`, `boolean`, `date`, `json`, `enum([...])`. Chain `.required()`, `.index()`, `.unique()`, `.default(v)`.
|
|
187
|
+
|
|
188
|
+
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context — use `authenticated()` for org-scoped models that allow any logged-in member.
|
|
189
|
+
|
|
190
|
+
**Scopes**:
|
|
191
|
+
- `app` — single shared schema (rare)
|
|
192
|
+
- `org` — partitioned by organization (most product spaces)
|
|
193
|
+
- `project` — partitioned by project (dev-tooling spaces)
|
|
194
|
+
|
|
195
|
+
**Push models to backend after editing**: `construct graph push`.
|
|
196
|
+
|
|
197
|
+
### Pitfalls
|
|
198
|
+
|
|
199
|
+
- **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`. Pattern:
|
|
200
|
+
```ts
|
|
201
|
+
function parseLabels(raw: unknown): string[] {
|
|
202
|
+
if (Array.isArray(raw)) return raw as string[]
|
|
203
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
204
|
+
try { const v = JSON.parse(raw); return Array.isArray(v) ? v : [] } catch { return [] }
|
|
205
|
+
}
|
|
206
|
+
return []
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
|
|
210
|
+
- **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
|
|
211
|
+
- **Schema ownership is sticky**: the first profile to `construct graph push` becomes the schema owner. Subsequent pushes from a different profile fail with "Ownership check failed". Bump the space `id` to fork, or push from the original owner profile.
|
|
212
|
+
- **`access.member()` requires org context** — use `access.authenticated()` for org-scoped models that any logged-in member should be able to read/write. `member()` is for stricter membership checks.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Actions (agent surface)
|
|
217
|
+
|
|
218
|
+
`src/actions.ts` exports an `actions` object — each entry becomes an agent-callable tool via `space_run_action`.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
export const actions = {
|
|
222
|
+
createTask: {
|
|
223
|
+
description: 'Create a task',
|
|
224
|
+
params: {
|
|
225
|
+
title: { type: 'string', required: true },
|
|
226
|
+
priority: { type: 'string', required: false },
|
|
227
|
+
},
|
|
228
|
+
run: async (p: any) => ({ task: await tasks.create(p) }),
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The agent (configured in `agent/config.md`) calls `space_list_actions` then `space_run_action`. Keep descriptions tight and specific — they're the only docstring the agent sees.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Drag and drop (HTML5)
|
|
238
|
+
|
|
239
|
+
Tauri intercepts native drag-drop by default. The Construct host sets `dragDropEnabled: false` so HTML5 events fire normally — use `draggable="true"`, `@dragstart`, `@dragover.prevent`, `@drop`. No extra deps required; reach for `vuedraggable` only if reordering needs auto-scroll/animation.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Conventions
|
|
244
|
+
|
|
245
|
+
- **Imports**: `@construct-space/*` packages always external. Local imports use relative paths (no `@/` alias unless you add one).
|
|
246
|
+
- **Composables**: `use*` prefix, keep in `src/composables/`.
|
|
247
|
+
- **Pages**: filename = route path. `index.vue` → `/`, `settings.vue` → `/settings`, `[id].vue` → param.
|
|
248
|
+
- **Styling**: Tailwind utilities + CSS vars `--app-foreground`, `--app-background`, `--app-muted`, `--app-border`, `--app-accent`. Don't hard-code colors; the host themes via these variables.
|
|
249
|
+
- **Comments**: explain *why*, not *what*. Skip TODOs in committed code.
|
|
250
|
+
- **No emojis** in source unless the user requests them.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Build pipeline reminder
|
|
255
|
+
|
|
256
|
+
`construct build` writes:
|
|
257
|
+
- `dist/space-{{.ID}}.iife.js` — the bundled space
|
|
258
|
+
- `dist/manifest.json` — manifest + `build` block (checksum, size, **`hostApiVersion`**, builtAt)
|
|
259
|
+
|
|
260
|
+
The runtime SpaceLoader compares `hostApiVersion` to its own; mismatches log a warning. Bump the CLI to keep them aligned.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
languageOptions: {
|
|
7
|
+
ecmaVersion: 'latest',
|
|
8
|
+
sourceType: 'module',
|
|
9
|
+
globals: {
|
|
10
|
+
console: 'readonly',
|
|
11
|
+
window: 'readonly',
|
|
12
|
+
document: 'readonly',
|
|
13
|
+
globalThis: 'readonly',
|
|
14
|
+
setTimeout: 'readonly',
|
|
15
|
+
clearTimeout: 'readonly',
|
|
16
|
+
history: 'readonly',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
rules: {
|
|
20
|
+
'no-unused-vars': 'off',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
ignores: ['dist/', 'node_modules/', '*.d.ts'],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* {{.DisplayName}} — Home page
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Built-in libraries available at runtime (do not bundle):
|
|
6
|
+
* @construct-space/ui — Vue 3 components (Button, Card, Modal, Table, Badge, ...)
|
|
7
|
+
* @construct-space/sdk — useOrg, useOrgMembers, useToast, useAuth
|
|
8
|
+
* @construct-space/graph — defineModel, useGraph (multi-tenant data layer)
|
|
9
|
+
* lucide-vue-next — icons
|
|
7
10
|
*/
|
|
8
11
|
import { ref } from 'vue'
|
|
12
|
+
import { Button, Card, Empty } from '@construct-space/ui'
|
|
13
|
+
import { Sparkles } from 'lucide-vue-next'
|
|
9
14
|
|
|
10
15
|
const greeting = ref('Your space is ready. Start building!')
|
|
11
16
|
</script>
|
|
12
17
|
|
|
13
18
|
<template>
|
|
14
|
-
<div class="h-full flex items-center justify-center">
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<div class="h-full flex items-center justify-center p-6">
|
|
20
|
+
<Card variant="default" class="max-w-md w-full text-center">
|
|
21
|
+
<template #header>
|
|
22
|
+
<div class="flex items-center justify-center gap-2 w-full">
|
|
23
|
+
<Sparkles class="size-5 text-[var(--app-accent)]" />
|
|
24
|
+
<h1 class="text-lg font-semibold text-[var(--app-foreground)]">{{.DisplayName}}</h1>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<Empty :description="greeting" size="sm" />
|
|
29
|
+
|
|
30
|
+
<template #footer-end>
|
|
31
|
+
<Button variant="solid" size="sm" label="Get Started" />
|
|
32
|
+
</template>
|
|
33
|
+
</Card>
|
|
19
34
|
</div>
|
|
20
35
|
</template>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"build": "construct build",
|
|
9
9
|
"dev": "construct dev",
|
|
10
10
|
"check": "construct check",
|
|
11
|
+
"lint": "eslint .",
|
|
11
12
|
"validate": "construct validate"
|
|
12
13
|
},
|
|
13
14
|
"peerDependencies": {
|
|
@@ -18,10 +19,18 @@
|
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@construct-space/cli": "latest",
|
|
20
21
|
"@construct-space/sdk": "latest",
|
|
22
|
+
"@eslint/js": "^10.0.1",
|
|
21
23
|
"@vitejs/plugin-vue": "^5.2.3",
|
|
24
|
+
"eslint": "^10.2.1",
|
|
25
|
+
"eslint-plugin-vue": "^10.0.0",
|
|
22
26
|
"lucide-vue-next": "^1.0.0",
|
|
23
27
|
"typescript": "^5.9.3",
|
|
28
|
+
"typescript-eslint": "^8.0.0",
|
|
24
29
|
"vite": "^6.3.5",
|
|
25
30
|
"vue-tsc": "^3.2.5"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@construct-space/graph": "^0.5.0",
|
|
34
|
+
"@construct-space/ui": "^0.5.2"
|
|
26
35
|
}
|
|
27
36
|
}
|