@construct-space/cli 1.6.0 → 1.6.3
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/dist/index.js
CHANGED
|
@@ -5354,7 +5354,7 @@ async function scaffold(nameArg, options) {
|
|
|
5354
5354
|
// src/commands/build.ts
|
|
5355
5355
|
init_source();
|
|
5356
5356
|
import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync as readdirSync3, renameSync, statSync as statSync3 } from "fs";
|
|
5357
|
-
import { join as join6 } from "path";
|
|
5357
|
+
import { extname as extname3, join as join6 } from "path";
|
|
5358
5358
|
import { createHash } from "crypto";
|
|
5359
5359
|
|
|
5360
5360
|
// node_modules/ora/index.js
|
|
@@ -8026,6 +8026,11 @@ function readRaw(dir) {
|
|
|
8026
8026
|
const data = readFileSync2(path, "utf-8");
|
|
8027
8027
|
return JSON.parse(data);
|
|
8028
8028
|
}
|
|
8029
|
+
function write(dir, m) {
|
|
8030
|
+
const path = join3(dir, MANIFEST_FILE);
|
|
8031
|
+
writeFileSync2(path, JSON.stringify(m, null, 2) + `
|
|
8032
|
+
`);
|
|
8033
|
+
}
|
|
8029
8034
|
function writeWithBuild(dir, raw, build) {
|
|
8030
8035
|
raw.build = build;
|
|
8031
8036
|
writeFileSync2(join3(dir, "manifest.json"), JSON.stringify(raw, null, 2) + `
|
|
@@ -8109,7 +8114,7 @@ function resolvePages(m, root, prefix) {
|
|
|
8109
8114
|
if (existsSync4(legacyFullPath)) {
|
|
8110
8115
|
let varName = "IndexPage";
|
|
8111
8116
|
if (p.path) {
|
|
8112
|
-
varName = capitalize(p.path.replace(/[\/:]/g, "-").replace(/-+/g, "-")) + "Page";
|
|
8117
|
+
varName = capitalize(p.path.replace(/\[([^\]]+)\]/g, "$1").replace(/[\/:]/g, "-").replace(/-+/g, "-")) + "Page";
|
|
8113
8118
|
}
|
|
8114
8119
|
return { varName, importPath: prefix + legacyComponent, path: p.path };
|
|
8115
8120
|
}
|
|
@@ -8243,41 +8248,245 @@ function bundleAgentDir(srcDir, distDir) {
|
|
|
8243
8248
|
}
|
|
8244
8249
|
|
|
8245
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
|
+
}
|
|
8246
8390
|
function extractActionMetadata(actionsPath) {
|
|
8247
8391
|
try {
|
|
8248
|
-
const
|
|
8392
|
+
const actionsBody = actionsObject(readFileSync4(actionsPath, "utf-8"));
|
|
8393
|
+
if (!actionsBody)
|
|
8394
|
+
return null;
|
|
8249
8395
|
const result = {};
|
|
8250
|
-
const
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
const
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
for (const actionId of Object.keys(result)) {
|
|
8258
|
-
const paramBlockPattern = new RegExp(`${actionId}\\s*:\\s*\\{[\\s\\S]*?params\\s*:\\s*\\{([\\s\\S]*?)\\}\\s*,?\\s*(?:run|\\})`);
|
|
8259
|
-
const paramMatch = source.match(paramBlockPattern);
|
|
8260
|
-
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) {
|
|
8261
8403
|
const params = {};
|
|
8262
|
-
const
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
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 } : {}
|
|
8269
8413
|
};
|
|
8270
8414
|
}
|
|
8271
|
-
if (Object.keys(params).length > 0)
|
|
8272
|
-
|
|
8273
|
-
}
|
|
8415
|
+
if (Object.keys(params).length > 0)
|
|
8416
|
+
metadata.params = params;
|
|
8274
8417
|
}
|
|
8418
|
+
result[action.name] = metadata;
|
|
8275
8419
|
}
|
|
8276
8420
|
return Object.keys(result).length > 0 ? result : null;
|
|
8277
8421
|
} catch {
|
|
8278
8422
|
return null;
|
|
8279
8423
|
}
|
|
8280
8424
|
}
|
|
8425
|
+
var ICON_MIME = {
|
|
8426
|
+
".svg": "image/svg+xml",
|
|
8427
|
+
".png": "image/png",
|
|
8428
|
+
".jpg": "image/jpeg",
|
|
8429
|
+
".jpeg": "image/jpeg",
|
|
8430
|
+
".webp": "image/webp",
|
|
8431
|
+
".gif": "image/gif",
|
|
8432
|
+
".ico": "image/x-icon"
|
|
8433
|
+
};
|
|
8434
|
+
function isInlineableIcon(value) {
|
|
8435
|
+
if (typeof value !== "string" || !value)
|
|
8436
|
+
return false;
|
|
8437
|
+
if (value.startsWith("i-") || value.startsWith("lucide:"))
|
|
8438
|
+
return false;
|
|
8439
|
+
if (value.startsWith("data:"))
|
|
8440
|
+
return false;
|
|
8441
|
+
if (/^https?:\/\//.test(value))
|
|
8442
|
+
return false;
|
|
8443
|
+
return extname3(value).toLowerCase() in ICON_MIME;
|
|
8444
|
+
}
|
|
8445
|
+
function resolveIconPath(root, ref) {
|
|
8446
|
+
const rel = ref.replace(/^\.?\//, "");
|
|
8447
|
+
for (const candidate of [join6(root, rel), join6(root, "src", rel)]) {
|
|
8448
|
+
if (existsSync6(candidate))
|
|
8449
|
+
return candidate;
|
|
8450
|
+
}
|
|
8451
|
+
return null;
|
|
8452
|
+
}
|
|
8453
|
+
function inlineIcon(root, ref) {
|
|
8454
|
+
const abs = resolveIconPath(root, ref);
|
|
8455
|
+
if (!abs)
|
|
8456
|
+
return null;
|
|
8457
|
+
const mime = ICON_MIME[extname3(abs).toLowerCase()] || "application/octet-stream";
|
|
8458
|
+
const b64 = readFileSync4(abs).toString("base64");
|
|
8459
|
+
return `data:${mime};base64,${b64}`;
|
|
8460
|
+
}
|
|
8461
|
+
function inlineManifestIcons(root, raw) {
|
|
8462
|
+
if (isInlineableIcon(raw.icon)) {
|
|
8463
|
+
const data = inlineIcon(root, raw.icon);
|
|
8464
|
+
if (data)
|
|
8465
|
+
raw.icon = data;
|
|
8466
|
+
else
|
|
8467
|
+
console.warn(source_default.yellow(` Icon not found: ${raw.icon}`));
|
|
8468
|
+
}
|
|
8469
|
+
const nav = raw.navigation;
|
|
8470
|
+
if (nav && isInlineableIcon(nav.icon)) {
|
|
8471
|
+
const data = inlineIcon(root, nav.icon);
|
|
8472
|
+
if (data)
|
|
8473
|
+
nav.icon = data;
|
|
8474
|
+
else
|
|
8475
|
+
console.warn(source_default.yellow(` navigation.icon not found: ${nav.icon}`));
|
|
8476
|
+
}
|
|
8477
|
+
const pages = raw.pages;
|
|
8478
|
+
if (Array.isArray(pages)) {
|
|
8479
|
+
for (const p of pages) {
|
|
8480
|
+
if (isInlineableIcon(p.icon)) {
|
|
8481
|
+
const data = inlineIcon(root, p.icon);
|
|
8482
|
+
if (data)
|
|
8483
|
+
p.icon = data;
|
|
8484
|
+
else
|
|
8485
|
+
console.warn(source_default.yellow(` page icon not found: ${p.icon}`));
|
|
8486
|
+
}
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8489
|
+
}
|
|
8281
8490
|
async function build(options) {
|
|
8282
8491
|
const root = process.cwd();
|
|
8283
8492
|
if (!exists(root)) {
|
|
@@ -8326,6 +8535,7 @@ async function build(options) {
|
|
|
8326
8535
|
const bundleData = readFileSync4(bundlePath);
|
|
8327
8536
|
const checksum = createHash("sha256").update(bundleData).digest("hex");
|
|
8328
8537
|
const raw = readRaw(root);
|
|
8538
|
+
inlineManifestIcons(root, raw);
|
|
8329
8539
|
const actionsPath = join6(root, "src", "actions.ts");
|
|
8330
8540
|
if (existsSync6(actionsPath)) {
|
|
8331
8541
|
const actionMeta = extractActionMetadata(actionsPath);
|
|
@@ -10319,6 +10529,22 @@ async function publish(options) {
|
|
|
10319
10529
|
init_source();
|
|
10320
10530
|
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
10321
10531
|
import { join as join15 } from "path";
|
|
10532
|
+
|
|
10533
|
+
// src/lib/pagePaths.ts
|
|
10534
|
+
function pageComponentFromPath(path) {
|
|
10535
|
+
const clean = path.replace(/^\/+|\/+$/g, "");
|
|
10536
|
+
if (!clean)
|
|
10537
|
+
return "pages/index.vue";
|
|
10538
|
+
const segments = clean.split("/").map((segment) => {
|
|
10539
|
+
if (segment.startsWith(":") && segment.length > 1) {
|
|
10540
|
+
return `[${segment.slice(1)}]`;
|
|
10541
|
+
}
|
|
10542
|
+
return segment;
|
|
10543
|
+
});
|
|
10544
|
+
return `pages/${segments.join("/")}.vue`;
|
|
10545
|
+
}
|
|
10546
|
+
|
|
10547
|
+
// src/commands/validate.ts
|
|
10322
10548
|
function validate3() {
|
|
10323
10549
|
const root = process.cwd();
|
|
10324
10550
|
if (!exists(root)) {
|
|
@@ -10335,7 +10561,7 @@ function validate3() {
|
|
|
10335
10561
|
}
|
|
10336
10562
|
let warnings = 0;
|
|
10337
10563
|
for (const page of m.pages) {
|
|
10338
|
-
const component = page.component || (page.path
|
|
10564
|
+
const component = page.component || pageComponentFromPath(page.path);
|
|
10339
10565
|
const fullPath = join15(root, "src", component);
|
|
10340
10566
|
if (!existsSync12(fullPath)) {
|
|
10341
10567
|
console.log(source_default.yellow(` \u26A0 Page component not found: src/${component}`));
|
|
@@ -10386,7 +10612,7 @@ function check() {
|
|
|
10386
10612
|
}
|
|
10387
10613
|
let warnings = 0;
|
|
10388
10614
|
for (const page of m.pages) {
|
|
10389
|
-
const component = page.component || (page.path
|
|
10615
|
+
const component = page.component || pageComponentFromPath(page.path);
|
|
10390
10616
|
if (!existsSync13(join16(root, "src", component))) {
|
|
10391
10617
|
console.log(source_default.yellow(` \u26A0 Page not found: src/${component}`));
|
|
10392
10618
|
warnings++;
|
|
@@ -10949,6 +11175,33 @@ async function graphPush() {
|
|
|
10949
11175
|
process.exit(1);
|
|
10950
11176
|
}
|
|
10951
11177
|
}
|
|
11178
|
+
function parseDefaultValue(raw) {
|
|
11179
|
+
const value = raw.trim();
|
|
11180
|
+
try {
|
|
11181
|
+
return JSON.parse(value);
|
|
11182
|
+
} catch {}
|
|
11183
|
+
const quote = value[0];
|
|
11184
|
+
if ((quote === "'" || quote === '"') && value.endsWith(quote)) {
|
|
11185
|
+
return value.slice(1, -1).replace(/\\(['"\\bfnrt])/g, (_, ch) => {
|
|
11186
|
+
switch (ch) {
|
|
11187
|
+
case "b":
|
|
11188
|
+
return "\b";
|
|
11189
|
+
case "f":
|
|
11190
|
+
return "\f";
|
|
11191
|
+
case "n":
|
|
11192
|
+
return `
|
|
11193
|
+
`;
|
|
11194
|
+
case "r":
|
|
11195
|
+
return "\r";
|
|
11196
|
+
case "t":
|
|
11197
|
+
return "\t";
|
|
11198
|
+
default:
|
|
11199
|
+
return ch;
|
|
11200
|
+
}
|
|
11201
|
+
});
|
|
11202
|
+
}
|
|
11203
|
+
return value;
|
|
11204
|
+
}
|
|
10952
11205
|
function parseModelFile(content, fileName) {
|
|
10953
11206
|
const modelMatch = content.match(/defineModel\s*\(\s*['"](\w+)['"]/);
|
|
10954
11207
|
if (!modelMatch)
|
|
@@ -10976,11 +11229,7 @@ function parseModelFile(content, fileName) {
|
|
|
10976
11229
|
field.validation = "url";
|
|
10977
11230
|
const defaultMatch = modifiers.match(/\.default\((.+?)\)/);
|
|
10978
11231
|
if (defaultMatch) {
|
|
10979
|
-
|
|
10980
|
-
field.default = JSON.parse(defaultMatch[1]);
|
|
10981
|
-
} catch {
|
|
10982
|
-
field.default = defaultMatch[1];
|
|
10983
|
-
}
|
|
11232
|
+
field.default = parseDefaultValue(defaultMatch[1]);
|
|
10984
11233
|
}
|
|
10985
11234
|
}
|
|
10986
11235
|
fields.push(field);
|
|
@@ -11162,10 +11411,41 @@ function parseModelFields(content, fileName) {
|
|
|
11162
11411
|
}
|
|
11163
11412
|
return { name: modelMatch[1], fields };
|
|
11164
11413
|
}
|
|
11414
|
+
|
|
11415
|
+
// src/commands/graph/fork.ts
|
|
11416
|
+
init_source();
|
|
11417
|
+
var spaceIDRegex = /^[a-z][a-z0-9-]*$/;
|
|
11418
|
+
function forkManifest(root, newSpaceID) {
|
|
11419
|
+
const id = newSpaceID.trim();
|
|
11420
|
+
if (!spaceIDRegex.test(id)) {
|
|
11421
|
+
throw new Error("space id must be lowercase alphanumeric with hyphens, starting with a letter");
|
|
11422
|
+
}
|
|
11423
|
+
if (!exists(root)) {
|
|
11424
|
+
throw new Error("No space.manifest.json found in current directory");
|
|
11425
|
+
}
|
|
11426
|
+
const m = read(root);
|
|
11427
|
+
if (m.id === id) {
|
|
11428
|
+
throw new Error(`space.manifest.json already uses id "${id}"`);
|
|
11429
|
+
}
|
|
11430
|
+
const oldSpaceID = m.id;
|
|
11431
|
+
m.id = id;
|
|
11432
|
+
write(root, m);
|
|
11433
|
+
return { oldSpaceID, newSpaceID: id };
|
|
11434
|
+
}
|
|
11435
|
+
function graphFork(newSpaceID) {
|
|
11436
|
+
try {
|
|
11437
|
+
const result = forkManifest(process.cwd(), newSpaceID);
|
|
11438
|
+
console.log(source_default.green(`Forked graph space id: ${result.oldSpaceID} -> ${result.newSpaceID}`));
|
|
11439
|
+
console.log(source_default.dim("Run `construct graph push` to register the forked schema."));
|
|
11440
|
+
} catch (err) {
|
|
11441
|
+
console.error(source_default.red(err.message));
|
|
11442
|
+
process.exit(1);
|
|
11443
|
+
}
|
|
11444
|
+
}
|
|
11165
11445
|
// package.json
|
|
11166
11446
|
var package_default = {
|
|
11167
11447
|
name: "@construct-space/cli",
|
|
11168
|
-
version: "1.6.
|
|
11448
|
+
version: "1.6.3",
|
|
11169
11449
|
description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
|
|
11170
11450
|
type: "module",
|
|
11171
11451
|
bin: {
|
|
@@ -11229,6 +11509,7 @@ var graph = program2.command("graph").description("Construct Graph \u2014 data m
|
|
|
11229
11509
|
graph.command("init").description("Initialize Graph in a space project").action(() => graphInit());
|
|
11230
11510
|
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));
|
|
11231
11511
|
graph.command("push").description("Register models with the Graph service").action(async () => graphPush());
|
|
11512
|
+
graph.command("fork <new-space-id>").description("Rewrite this space manifest to a new graph space id").action((newSpaceID) => graphFork(newSpaceID));
|
|
11232
11513
|
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));
|
|
11233
11514
|
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));
|
|
11234
11515
|
var bundles = graph.command("bundles").description("Manage space bundles (publisher grouping)");
|
|
@@ -100,6 +100,17 @@ All exported from the package root:
|
|
|
100
100
|
|
|
101
101
|
**Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
|
|
102
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
|
+
|
|
103
114
|
```vue
|
|
104
115
|
<script setup lang="ts">
|
|
105
116
|
import { Card, Button, Modal, Badge } from '@construct-space/ui'
|
|
@@ -185,15 +196,26 @@ await tasks.remove(id)
|
|
|
185
196
|
|
|
186
197
|
### Pitfalls
|
|
187
198
|
|
|
188
|
-
- **`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`.
|
|
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
|
+
```
|
|
189
209
|
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
|
|
190
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.
|
|
191
213
|
|
|
192
214
|
---
|
|
193
215
|
|
|
194
216
|
## Actions (agent surface)
|
|
195
217
|
|
|
196
|
-
`src/actions.ts` exports an `actions` object — each entry
|
|
218
|
+
`src/actions.ts` exports an `actions` object — each entry is exposed to the space's agent as a first-class tool.
|
|
197
219
|
|
|
198
220
|
```ts
|
|
199
221
|
export const actions = {
|
|
@@ -208,7 +230,24 @@ export const actions = {
|
|
|
208
230
|
}
|
|
209
231
|
```
|
|
210
232
|
|
|
211
|
-
|
|
233
|
+
### Wiring (REQUIRED — easy to miss)
|
|
234
|
+
|
|
235
|
+
Each action becomes a tool named **`{spaceID}.{actionName}`** (dot, not underscore). The operator only pre-registers actions that are **explicitly whitelisted** in the agent config. An empty `tools: []` means the agent sees zero action tools and will report "unknown tool".
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
# agent/config.md frontmatter
|
|
239
|
+
---
|
|
240
|
+
id: {{.ID}}
|
|
241
|
+
tools:
|
|
242
|
+
- {{.ID}}.createTask
|
|
243
|
+
- {{.ID}}.listTasks
|
|
244
|
+
- {{.ID}}.updateTask
|
|
245
|
+
---
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Fallback: agents can also discover/invoke actions via the generic `space_list_actions` + `space_run_action` bridge tools. Prefer the whitelist — it gives the model proper tool definitions instead of stringly-typed action names.
|
|
249
|
+
|
|
250
|
+
Keep action descriptions tight and specific — they're the only docstring the agent sees.
|
|
212
251
|
|
|
213
252
|
---
|
|
214
253
|
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"eslint-plugin-vue": "^10.0.0",
|
|
26
26
|
"lucide-vue-next": "^1.0.0",
|
|
27
27
|
"typescript": "^5.9.3",
|
|
28
|
-
"typescript-eslint": "^
|
|
28
|
+
"typescript-eslint": "^8.0.0",
|
|
29
29
|
"vite": "^6.3.5",
|
|
30
30
|
"vue-tsc": "^3.2.5"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@construct-space/graph": "^0.
|
|
34
|
-
"@construct-space/ui": "^0.
|
|
33
|
+
"@construct-space/graph": "^0.6.2",
|
|
34
|
+
"@construct-space/ui": "^0.6.1"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -100,6 +100,17 @@ All exported from the package root:
|
|
|
100
100
|
|
|
101
101
|
**Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
|
|
102
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
|
+
|
|
103
114
|
```vue
|
|
104
115
|
<script setup lang="ts">
|
|
105
116
|
import { Card, Button, Modal, Badge } from '@construct-space/ui'
|
|
@@ -185,15 +196,26 @@ await tasks.remove(id)
|
|
|
185
196
|
|
|
186
197
|
### Pitfalls
|
|
187
198
|
|
|
188
|
-
- **`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`.
|
|
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
|
+
```
|
|
189
209
|
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
|
|
190
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.
|
|
191
213
|
|
|
192
214
|
---
|
|
193
215
|
|
|
194
216
|
## Actions (agent surface)
|
|
195
217
|
|
|
196
|
-
`src/actions.ts` exports an `actions` object — each entry
|
|
218
|
+
`src/actions.ts` exports an `actions` object — each entry is exposed to the space's agent as a first-class tool.
|
|
197
219
|
|
|
198
220
|
```ts
|
|
199
221
|
export const actions = {
|
|
@@ -208,7 +230,24 @@ export const actions = {
|
|
|
208
230
|
}
|
|
209
231
|
```
|
|
210
232
|
|
|
211
|
-
|
|
233
|
+
### Wiring (REQUIRED — easy to miss)
|
|
234
|
+
|
|
235
|
+
Each action becomes a tool named **`{spaceID}.{actionName}`** (dot, not underscore). The operator only pre-registers actions that are **explicitly whitelisted** in the agent config. An empty `tools: []` means the agent sees zero action tools and will report "unknown tool".
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
# agent/config.md frontmatter
|
|
239
|
+
---
|
|
240
|
+
id: {{.ID}}
|
|
241
|
+
tools:
|
|
242
|
+
- {{.ID}}.createTask
|
|
243
|
+
- {{.ID}}.listTasks
|
|
244
|
+
- {{.ID}}.updateTask
|
|
245
|
+
---
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Fallback: agents can also discover/invoke actions via the generic `space_list_actions` + `space_run_action` bridge tools. Prefer the whitelist — it gives the model proper tool definitions instead of stringly-typed action names.
|
|
249
|
+
|
|
250
|
+
Keep action descriptions tight and specific — they're the only docstring the agent sees.
|
|
212
251
|
|
|
213
252
|
---
|
|
214
253
|
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"eslint-plugin-vue": "^10.0.0",
|
|
26
26
|
"lucide-vue-next": "^1.0.0",
|
|
27
27
|
"typescript": "^5.9.3",
|
|
28
|
-
"typescript-eslint": "^
|
|
28
|
+
"typescript-eslint": "^8.0.0",
|
|
29
29
|
"vite": "^6.3.5",
|
|
30
30
|
"vue-tsc": "^3.2.5"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@construct-space/graph": "^0.
|
|
34
|
-
"@construct-space/ui": "^0.
|
|
33
|
+
"@construct-space/graph": "^0.6.2",
|
|
34
|
+
"@construct-space/ui": "^0.6.1"
|
|
35
35
|
}
|
|
36
36
|
}
|