@construct-space/cli 1.9.2 → 1.9.4
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 +52 -52
- package/dist/templates/space/actions.ts.tmpl +7 -7
- package/dist/templates/space/config.md.tmpl +1 -1
- package/dist/templates/space/construct.md.tmpl +30 -30
- package/dist/templates/space/entry.ts.tmpl +1 -1
- package/dist/templates/space/full/entry.ts.tmpl +1 -1
- package/dist/templates/space/full/settings.vue.tmpl +1 -1
- package/dist/templates/space/full/skill-data.md.tmpl +3 -3
- package/dist/templates/space/full/skill-ui.md.tmpl +3 -3
- package/dist/templates/space/index.vue.tmpl +5 -5
- package/dist/templates/space/skill.md.tmpl +3 -3
- package/dist/templates/space/vite.config.ts.tmpl +4 -4
- package/dist/templates/space/widgets/2x1.vue.tmpl +2 -2
- package/dist/templates/space/widgets/4x1.vue.tmpl +2 -2
- package/package.json +1 -1
- package/templates/space/actions.ts.tmpl +7 -7
- package/templates/space/config.md.tmpl +1 -1
- package/templates/space/construct.md.tmpl +30 -30
- package/templates/space/entry.ts.tmpl +1 -1
- package/templates/space/full/entry.ts.tmpl +1 -1
- package/templates/space/full/settings.vue.tmpl +1 -1
- package/templates/space/full/skill-data.md.tmpl +3 -3
- package/templates/space/full/skill-ui.md.tmpl +3 -3
- package/templates/space/index.vue.tmpl +5 -5
- package/templates/space/skill.md.tmpl +3 -3
- package/templates/space/vite.config.ts.tmpl +4 -4
- package/templates/space/widgets/2x1.vue.tmpl +2 -2
- package/templates/space/widgets/4x1.vue.tmpl +2 -2
package/dist/index.js
CHANGED
|
@@ -2845,7 +2845,7 @@ function spaceDir(spaceId) {
|
|
|
2845
2845
|
var init_appdir = () => {};
|
|
2846
2846
|
|
|
2847
2847
|
// src/lib/auth.ts
|
|
2848
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, unlinkSync, existsSync as existsSync10, readdirSync as readdirSync5, statSync as
|
|
2848
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, unlinkSync, existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
2849
2849
|
import { join as join12, dirname as dirname4 } from "path";
|
|
2850
2850
|
function listDesktopProfiles() {
|
|
2851
2851
|
const dir = profilesDir();
|
|
@@ -2855,7 +2855,7 @@ function listDesktopProfiles() {
|
|
|
2855
2855
|
for (const entry of readdirSync5(dir)) {
|
|
2856
2856
|
const full = join12(dir, entry);
|
|
2857
2857
|
try {
|
|
2858
|
-
if (!
|
|
2858
|
+
if (!statSync3(full).isDirectory())
|
|
2859
2859
|
continue;
|
|
2860
2860
|
const authPath = join12(full, "auth.json");
|
|
2861
2861
|
if (!existsSync10(authPath))
|
|
@@ -2965,7 +2965,7 @@ function load2() {
|
|
|
2965
2965
|
const fromProfile = loadFromActiveProfile();
|
|
2966
2966
|
if (fromProfile)
|
|
2967
2967
|
return fromProfile;
|
|
2968
|
-
throw new Error("not logged in
|
|
2968
|
+
throw new Error("not logged in -- run 'construct login' first");
|
|
2969
2969
|
}
|
|
2970
2970
|
function loadFromActiveProfile() {
|
|
2971
2971
|
try {
|
|
@@ -3124,7 +3124,7 @@ async function orgStatus() {
|
|
|
3124
3124
|
process.exit(1);
|
|
3125
3125
|
}
|
|
3126
3126
|
if (s.scope === "org" && s.org) {
|
|
3127
|
-
console.log(source_default.cyan("Scope: ") + source_default.bold(`org
|
|
3127
|
+
console.log(source_default.cyan("Scope: ") + source_default.bold(`org -- ${s.org.name || s.org.slug || s.org.id}`));
|
|
3128
3128
|
if (s.org.slug)
|
|
3129
3129
|
console.log(source_default.dim(` slug: ${s.org.slug}`));
|
|
3130
3130
|
if (s.org.id)
|
|
@@ -3148,13 +3148,13 @@ async function orgStatus() {
|
|
|
3148
3148
|
const expected = s.scope === "org" ? "org" : "user";
|
|
3149
3149
|
const matches = kind === expected;
|
|
3150
3150
|
console.log(` ${source_default.bold(creds.publisherName || "(unnamed)")} ${source_default.dim(`[${kind}]`)}`);
|
|
3151
|
-
console.log(source_default.dim(` key: ${creds.publisherKey.slice(0, 14)}
|
|
3151
|
+
console.log(source_default.dim(` key: ${creds.publisherKey.slice(0, 14)}...`));
|
|
3152
3152
|
if (!matches) {
|
|
3153
3153
|
console.log(source_default.yellow(` \u26A0 publisher kind (${kind}) doesn't match active scope (${expected}).`));
|
|
3154
3154
|
console.log(source_default.dim(" Restart the desktop app or run 'construct login' to re-sync."));
|
|
3155
3155
|
}
|
|
3156
3156
|
} else {
|
|
3157
|
-
console.log(source_default.dim(" (none
|
|
3157
|
+
console.log(source_default.dim(" (none -- enroll as a developer to get a publisher key)"));
|
|
3158
3158
|
}
|
|
3159
3159
|
if (s.developer) {
|
|
3160
3160
|
console.log();
|
|
@@ -3255,7 +3255,7 @@ async function spacesList(opts = {}) {
|
|
|
3255
3255
|
const rows = spaces.map((s) => ({
|
|
3256
3256
|
id: s.id,
|
|
3257
3257
|
version: s.latest_version || "0.0.0",
|
|
3258
|
-
bundle: s.bundle_id || source_default.dim("
|
|
3258
|
+
bundle: s.bundle_id || source_default.dim("--"),
|
|
3259
3259
|
distribution: colorizeDistribution(s.distribution),
|
|
3260
3260
|
installs: String(s.install_count)
|
|
3261
3261
|
}));
|
|
@@ -3342,7 +3342,7 @@ async function bundlesList(opts = {}) {
|
|
|
3342
3342
|
}
|
|
3343
3343
|
console.log(source_default.bold(`Bundles for org ${orgId}:`));
|
|
3344
3344
|
for (const b of bundles) {
|
|
3345
|
-
console.log(` ${source_default.cyan(b.id)}
|
|
3345
|
+
console.log(` ${source_default.cyan(b.id)} -- ${b.name}`);
|
|
3346
3346
|
}
|
|
3347
3347
|
} catch (err) {
|
|
3348
3348
|
fail(err);
|
|
@@ -3365,7 +3365,7 @@ async function bundleCreate(id, name, opts = {}) {
|
|
|
3365
3365
|
console.log(JSON.stringify(resp, null, 2));
|
|
3366
3366
|
return;
|
|
3367
3367
|
}
|
|
3368
|
-
console.log(source_default.green(`\u2713 Bundle created: ${source_default.cyan(resp.id)}
|
|
3368
|
+
console.log(source_default.green(`\u2713 Bundle created: ${source_default.cyan(resp.id)} -- ${resp.name}`));
|
|
3369
3369
|
console.log(source_default.dim(` Next: add bundle_id: "${resp.id}" to your data.manifest.json and publish.`));
|
|
3370
3370
|
} catch (err) {
|
|
3371
3371
|
fail(err);
|
|
@@ -3572,7 +3572,7 @@ async function allowlistRemove(spaceId, targetOrgId, opts = {}) {
|
|
|
3572
3572
|
return;
|
|
3573
3573
|
}
|
|
3574
3574
|
console.log(source_default.green(`\u2713 Removed ${source_default.cyan(targetOrgId)} from ${spaceId} allowlist`));
|
|
3575
|
-
console.log(source_default.dim(" Existing installs by this org are preserved
|
|
3575
|
+
console.log(source_default.dim(" Existing installs by this org are preserved -- uninstall separately if needed."));
|
|
3576
3576
|
} catch (err) {
|
|
3577
3577
|
fail3(err);
|
|
3578
3578
|
}
|
|
@@ -5516,7 +5516,7 @@ async function scaffold(nameArg, options) {
|
|
|
5516
5516
|
|
|
5517
5517
|
// src/commands/build.ts
|
|
5518
5518
|
init_source();
|
|
5519
|
-
import { cpSync, existsSync as existsSync6, readFileSync as readFileSync4, readdirSync as readdirSync3, renameSync, statSync as
|
|
5519
|
+
import { cpSync, existsSync as existsSync6, readFileSync as readFileSync4, readdirSync as readdirSync3, renameSync, statSync as statSync2 } from "fs";
|
|
5520
5520
|
import { join as join6 } from "path";
|
|
5521
5521
|
import { createHash } from "crypto";
|
|
5522
5522
|
|
|
@@ -8310,7 +8310,7 @@ function generate(root, m) {
|
|
|
8310
8310
|
const hasActions = existsSync4(actionsPath);
|
|
8311
8311
|
console.log(`[entry] root=${root} actionsPath=${actionsPath} hasActions=${hasActions}`);
|
|
8312
8312
|
const lines = [
|
|
8313
|
-
"// Auto-generated entry
|
|
8313
|
+
"// Auto-generated entry -- do not edit manually",
|
|
8314
8314
|
"// Generated from space.manifest.json"
|
|
8315
8315
|
];
|
|
8316
8316
|
if (existsSync4(join4(root, "src", "style.css"))) {
|
|
@@ -8423,7 +8423,7 @@ function copyAssetDirs(root, distDir) {
|
|
|
8423
8423
|
const copied = [];
|
|
8424
8424
|
for (const name of ASSET_DIRS) {
|
|
8425
8425
|
const src = join6(root, name);
|
|
8426
|
-
if (!existsSync6(src) || !
|
|
8426
|
+
if (!existsSync6(src) || !statSync2(src).isDirectory())
|
|
8427
8427
|
continue;
|
|
8428
8428
|
cpSync(src, join6(distDir, name), { recursive: true, verbatimSymlinks: true });
|
|
8429
8429
|
copied.push(name);
|
|
@@ -8630,7 +8630,7 @@ async function build(options) {
|
|
|
8630
8630
|
}
|
|
8631
8631
|
runHook(m.hooks, "postBuild", root);
|
|
8632
8632
|
const agentDir = join6(root, "agent");
|
|
8633
|
-
if (existsSync6(agentDir) &&
|
|
8633
|
+
if (existsSync6(agentDir) && statSync2(agentDir).isDirectory()) {
|
|
8634
8634
|
const distDir2 = join6(root, "dist");
|
|
8635
8635
|
bundleAgentDir(agentDir, distDir2);
|
|
8636
8636
|
bundleAgentDir(agentDir, root);
|
|
@@ -10270,7 +10270,7 @@ async function dev() {
|
|
|
10270
10270
|
}
|
|
10271
10271
|
const m = read(root);
|
|
10272
10272
|
const rt = detect();
|
|
10273
|
-
console.log(source_default.blue(`Dev mode
|
|
10273
|
+
console.log(source_default.blue(`Dev mode -- ${m.id} (${rt.name} ${rt.version})`));
|
|
10274
10274
|
ensureDeps(root, rt);
|
|
10275
10275
|
writeEntry(root, m);
|
|
10276
10276
|
const vite = watchCmd(root, rt);
|
|
@@ -10293,10 +10293,10 @@ async function dev() {
|
|
|
10293
10293
|
entryWatcher.on("all", (_, changedPath) => {
|
|
10294
10294
|
regenerateEntry();
|
|
10295
10295
|
if (changedPath.endsWith(MANIFEST_FILE)) {
|
|
10296
|
-
console.log(source_default.blue("Manifest changed
|
|
10296
|
+
console.log(source_default.blue("Manifest changed -- entry regenerated"));
|
|
10297
10297
|
return;
|
|
10298
10298
|
}
|
|
10299
|
-
console.log(source_default.blue("Actions changed
|
|
10299
|
+
console.log(source_default.blue("Actions changed -- entry regenerated"));
|
|
10300
10300
|
});
|
|
10301
10301
|
const distDir = join9(root, "dist");
|
|
10302
10302
|
const bundleFile = join9(distDir, `space-${m.id}.iife.js`);
|
|
@@ -10317,7 +10317,7 @@ async function dev() {
|
|
|
10317
10317
|
hostApiVersion: "0.5.0",
|
|
10318
10318
|
builtAt: new Date().toISOString()
|
|
10319
10319
|
});
|
|
10320
|
-
console.log(source_default.green(`Built
|
|
10320
|
+
console.log(source_default.green(`Built -> dist/ (${(bundleData.length / 1024).toFixed(1)} KB)`));
|
|
10321
10321
|
});
|
|
10322
10322
|
console.log(source_default.green("Watching for changes... (Ctrl+C to stop)"));
|
|
10323
10323
|
console.log(source_default.dim("Use the Preview button in Construct to open the Space Runner"));
|
|
@@ -10371,18 +10371,18 @@ function install() {
|
|
|
10371
10371
|
mkdirSync3(installDir, { recursive: true });
|
|
10372
10372
|
cpSync2(distDir, installDir, { recursive: true, verbatimSymlinks: true, force: true });
|
|
10373
10373
|
ensureBinExecutable(installDir);
|
|
10374
|
-
console.log(source_default.green(`Installed ${m.name}
|
|
10374
|
+
console.log(source_default.green(`Installed ${m.name} -> ${installDir}`));
|
|
10375
10375
|
console.log(source_default.dim(" Restart Construct to load the updated space."));
|
|
10376
10376
|
}
|
|
10377
10377
|
|
|
10378
10378
|
// src/commands/publish.ts
|
|
10379
10379
|
init_source();
|
|
10380
|
-
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, statSync as
|
|
10381
|
-
import { join as join14, basename as
|
|
10380
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, statSync as statSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
10381
|
+
import { join as join14, basename as basename5 } from "path";
|
|
10382
10382
|
init_auth();
|
|
10383
10383
|
|
|
10384
10384
|
// src/lib/pack.ts
|
|
10385
|
-
import { readdirSync as readdirSync6, statSync as
|
|
10385
|
+
import { readdirSync as readdirSync6, statSync as statSync4, existsSync as existsSync11 } from "fs";
|
|
10386
10386
|
import { join as join13 } from "path";
|
|
10387
10387
|
import { tmpdir } from "os";
|
|
10388
10388
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -10429,7 +10429,7 @@ async function packSource(root) {
|
|
|
10429
10429
|
entries.push(name);
|
|
10430
10430
|
}
|
|
10431
10431
|
for (const entry of readdirSync6(root)) {
|
|
10432
|
-
if (
|
|
10432
|
+
if (statSync4(join13(root, entry)).isDirectory())
|
|
10433
10433
|
continue;
|
|
10434
10434
|
if (allowedRootFiles.includes(entry))
|
|
10435
10435
|
continue;
|
|
@@ -10456,7 +10456,7 @@ async function packSource(root) {
|
|
|
10456
10456
|
"--exclude=*.lockb"
|
|
10457
10457
|
];
|
|
10458
10458
|
execFileSync2("tar", ["czf", tarballPath, ...excludes, ...validEntries], { cwd: root });
|
|
10459
|
-
const size =
|
|
10459
|
+
const size = statSync4(tarballPath).size;
|
|
10460
10460
|
if (size > MAX_SIZE) {
|
|
10461
10461
|
throw new Error(`Source exceeds maximum size of ${MAX_SIZE / 1024 / 1024}MB`);
|
|
10462
10462
|
}
|
|
@@ -10469,7 +10469,7 @@ async function uploadSource(portalURL, identityToken, publisherKey, tarballPath,
|
|
|
10469
10469
|
formData.append("manifest", JSON.stringify(m));
|
|
10470
10470
|
const fileData = readFileSync8(tarballPath);
|
|
10471
10471
|
const blob = new Blob([fileData]);
|
|
10472
|
-
formData.append("source", blob,
|
|
10472
|
+
formData.append("source", blob, basename5(tarballPath));
|
|
10473
10473
|
if (opts.private) {
|
|
10474
10474
|
formData.append("private", "true");
|
|
10475
10475
|
}
|
|
@@ -10489,7 +10489,7 @@ async function uploadSource(portalURL, identityToken, publisherKey, tarballPath,
|
|
|
10489
10489
|
});
|
|
10490
10490
|
const result = await resp.json();
|
|
10491
10491
|
if (resp.status === 401) {
|
|
10492
|
-
throw new Error("authentication failed
|
|
10492
|
+
throw new Error("authentication failed -- run 'construct login' to re-authenticate");
|
|
10493
10493
|
}
|
|
10494
10494
|
if (resp.status === 403) {
|
|
10495
10495
|
let msg = result.error || "You are not the owner of this space";
|
|
@@ -10644,7 +10644,7 @@ async function publish(options) {
|
|
|
10644
10644
|
console.log(source_default.yellow("No publisher key in active profile."));
|
|
10645
10645
|
console.log(source_default.dim(" Spaces will be attributed to your personal user identity."));
|
|
10646
10646
|
console.log(source_default.dim(" To publish as an org: enroll the org from the desktop app"));
|
|
10647
|
-
console.log(source_default.dim(" (Org Settings
|
|
10647
|
+
console.log(source_default.dim(" (Org Settings -> Developer), then re-run this command."));
|
|
10648
10648
|
console.log();
|
|
10649
10649
|
}
|
|
10650
10650
|
if (wantUserScope && creds.publisherKind === "org" && !options?.apiKey) {
|
|
@@ -10659,7 +10659,7 @@ async function publish(options) {
|
|
|
10659
10659
|
console.error(source_default.red("--private requires an org publisher key."));
|
|
10660
10660
|
console.error(source_default.dim(" Personal publishes are always public."));
|
|
10661
10661
|
console.error(source_default.dim(" Pass --scope=org to publish as your active org, or enrol an org"));
|
|
10662
|
-
console.error(source_default.dim(" from the desktop app (Org Settings
|
|
10662
|
+
console.error(source_default.dim(" from the desktop app (Org Settings -> Developer) first."));
|
|
10663
10663
|
process.exit(1);
|
|
10664
10664
|
}
|
|
10665
10665
|
console.log();
|
|
@@ -10694,7 +10694,7 @@ async function publish(options) {
|
|
|
10694
10694
|
let tarballPath;
|
|
10695
10695
|
try {
|
|
10696
10696
|
tarballPath = await packSource(root);
|
|
10697
|
-
const size =
|
|
10697
|
+
const size = statSync5(tarballPath).size;
|
|
10698
10698
|
spinner.succeed(`Source packed (${formatBytes(size)})`);
|
|
10699
10699
|
} catch (err) {
|
|
10700
10700
|
spinner.fail("Pack failed");
|
|
@@ -10706,7 +10706,7 @@ async function publish(options) {
|
|
|
10706
10706
|
const explicitKey = options?.apiKey || process.env.CONSTRUCT_PUBLISHER_KEY;
|
|
10707
10707
|
let initialKey = explicitKey || creds.publisherKey;
|
|
10708
10708
|
if (wantOrgScope && !explicitKey && !initialKey) {
|
|
10709
|
-
uploadSpinner.text = "Fetching org publisher key
|
|
10709
|
+
uploadSpinner.text = "Fetching org publisher key...";
|
|
10710
10710
|
const orgKey = await fetchOrgPublisherKey(creds.portal, creds.token);
|
|
10711
10711
|
if (!orgKey) {
|
|
10712
10712
|
uploadSpinner.fail("No org publisher available for --scope=org");
|
|
@@ -10722,11 +10722,11 @@ async function publish(options) {
|
|
|
10722
10722
|
result = await uploadSource(creds.portal, creds.token, initialKey, tarballPath, m, { private: wantPrivate, public: wantPublic });
|
|
10723
10723
|
} catch (e) {
|
|
10724
10724
|
if (e?.ownerKind === "org" && !creds.publisherKey && !wantUserScope) {
|
|
10725
|
-
uploadSpinner.text = "Fetching org publisher key
|
|
10725
|
+
uploadSpinner.text = "Fetching org publisher key...";
|
|
10726
10726
|
const orgKey = await fetchOrgPublisherKey(creds.portal, creds.token);
|
|
10727
10727
|
if (!orgKey)
|
|
10728
10728
|
throw e;
|
|
10729
|
-
uploadSpinner.text = "Uploading & building (as org)
|
|
10729
|
+
uploadSpinner.text = "Uploading & building (as org)...";
|
|
10730
10730
|
result = await uploadSource(creds.portal, creds.token, orgKey, tarballPath, m, { private: wantPrivate, public: wantPublic });
|
|
10731
10731
|
} else {
|
|
10732
10732
|
throw e;
|
|
@@ -10738,7 +10738,7 @@ async function publish(options) {
|
|
|
10738
10738
|
if (result.status === "approved" || result.status === "pending_review") {
|
|
10739
10739
|
uploadSpinner.succeed(`Published ${m.name} v${m.version}`);
|
|
10740
10740
|
if (result.status === "pending_review") {
|
|
10741
|
-
console.log(source_default.dim(" Status: pending review
|
|
10741
|
+
console.log(source_default.dim(" Status: pending review -- your space will be available after approval."));
|
|
10742
10742
|
}
|
|
10743
10743
|
} else if (result.status === "build_failed") {
|
|
10744
10744
|
uploadSpinner.fail("Build failed on server");
|
|
@@ -10886,7 +10886,7 @@ function check() {
|
|
|
10886
10886
|
process.exit(1);
|
|
10887
10887
|
}
|
|
10888
10888
|
console.log();
|
|
10889
|
-
console.log(source_default.green(`\u2713 ${m.name} v${m.version}
|
|
10889
|
+
console.log(source_default.green(`\u2713 ${m.name} v${m.version} -- all checks passed`));
|
|
10890
10890
|
}
|
|
10891
10891
|
|
|
10892
10892
|
// src/commands/clean.ts
|
|
@@ -11192,7 +11192,7 @@ function generateFieldCode(field) {
|
|
|
11192
11192
|
if (MODIFIERS[mod]) {
|
|
11193
11193
|
code += MODIFIERS[mod];
|
|
11194
11194
|
} else {
|
|
11195
|
-
console.log(source_default.yellow(` \u26A0 Unknown modifier '${mod}'
|
|
11195
|
+
console.log(source_default.yellow(` \u26A0 Unknown modifier '${mod}' -- skipped`));
|
|
11196
11196
|
}
|
|
11197
11197
|
}
|
|
11198
11198
|
return code;
|
|
@@ -11317,7 +11317,7 @@ function updateBarrel(modelsDir, modelName) {
|
|
|
11317
11317
|
// src/commands/graph/push.ts
|
|
11318
11318
|
init_source();
|
|
11319
11319
|
import { existsSync as existsSync17, readdirSync as readdirSync7, readFileSync as readFileSync13 } from "fs";
|
|
11320
|
-
import { join as join20
|
|
11320
|
+
import { join as join20 } from "path";
|
|
11321
11321
|
init_auth();
|
|
11322
11322
|
async function graphPush() {
|
|
11323
11323
|
const root = process.cwd();
|
|
@@ -11341,7 +11341,7 @@ async function graphPush() {
|
|
|
11341
11341
|
const models = [];
|
|
11342
11342
|
for (const file of modelFiles) {
|
|
11343
11343
|
const content = readFileSync13(join20(modelsDir, file), "utf-8");
|
|
11344
|
-
const model = parseModelFile(content
|
|
11344
|
+
const model = parseModelFile(content);
|
|
11345
11345
|
if (model)
|
|
11346
11346
|
models.push(model);
|
|
11347
11347
|
}
|
|
@@ -11405,7 +11405,7 @@ async function graphPush() {
|
|
|
11405
11405
|
}
|
|
11406
11406
|
console.error(source_default.dim(" Fork to a new space_id to publish your own version."));
|
|
11407
11407
|
} catch {
|
|
11408
|
-
console.error(source_default.red(` 403: Forbidden
|
|
11408
|
+
console.error(source_default.red(` 403: Forbidden -- ownership check failed`));
|
|
11409
11409
|
}
|
|
11410
11410
|
process.exit(1);
|
|
11411
11411
|
}
|
|
@@ -11415,11 +11415,11 @@ async function graphPush() {
|
|
|
11415
11415
|
console.error(source_default.red(` ${resp.status}: ${body}`));
|
|
11416
11416
|
process.exit(1);
|
|
11417
11417
|
}
|
|
11418
|
-
|
|
11418
|
+
await resp.json();
|
|
11419
11419
|
spinner.succeed("Models registered");
|
|
11420
11420
|
console.log();
|
|
11421
11421
|
for (const model of models) {
|
|
11422
|
-
console.log(` ${source_default.cyan(model.name)}
|
|
11422
|
+
console.log(` ${source_default.cyan(model.name)} -- ${model.fields.length} field(s)`);
|
|
11423
11423
|
}
|
|
11424
11424
|
console.log();
|
|
11425
11425
|
console.log(source_default.dim(` GraphQL endpoint: ${graphURL}/graphql`));
|
|
@@ -11457,7 +11457,7 @@ function parseDefaultValue(raw) {
|
|
|
11457
11457
|
}
|
|
11458
11458
|
return value;
|
|
11459
11459
|
}
|
|
11460
|
-
function parseModelFile(content
|
|
11460
|
+
function parseModelFile(content) {
|
|
11461
11461
|
const modelMatch = content.match(/defineModel\s*\(\s*['"](\w+)['"]/);
|
|
11462
11462
|
if (!modelMatch)
|
|
11463
11463
|
return null;
|
|
@@ -11524,7 +11524,7 @@ function parseModelFile(content, fileName) {
|
|
|
11524
11524
|
// src/commands/graph/migrate.ts
|
|
11525
11525
|
init_source();
|
|
11526
11526
|
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync14 } from "fs";
|
|
11527
|
-
import { join as join21
|
|
11527
|
+
import { join as join21 } from "path";
|
|
11528
11528
|
init_auth();
|
|
11529
11529
|
async function graphMigrate(options) {
|
|
11530
11530
|
const root = process.cwd();
|
|
@@ -11565,7 +11565,7 @@ async function graphMigrate(options) {
|
|
|
11565
11565
|
const localModels = [];
|
|
11566
11566
|
for (const file of modelFiles) {
|
|
11567
11567
|
const content = readFileSync14(join21(modelsDir, file), "utf-8");
|
|
11568
|
-
const model = parseModelFields(content
|
|
11568
|
+
const model = parseModelFields(content);
|
|
11569
11569
|
if (model)
|
|
11570
11570
|
localModels.push(model);
|
|
11571
11571
|
}
|
|
@@ -11582,25 +11582,25 @@ async function graphMigrate(options) {
|
|
|
11582
11582
|
const localFields = local.fields.map((f) => f.name);
|
|
11583
11583
|
for (const sf of serverFields) {
|
|
11584
11584
|
if (!localFields.includes(sf)) {
|
|
11585
|
-
console.log(source_default.red(` - ${server.name}.${sf}`), source_default.dim("(on server, not in local model
|
|
11585
|
+
console.log(source_default.red(` - ${server.name}.${sf}`), source_default.dim("(on server, not in local model -- can drop)"));
|
|
11586
11586
|
hasChanges = true;
|
|
11587
11587
|
}
|
|
11588
11588
|
}
|
|
11589
11589
|
for (const lf of localFields) {
|
|
11590
11590
|
if (!serverFields.includes(lf)) {
|
|
11591
|
-
console.log(source_default.green(` + ${server.name}.${lf}`), source_default.dim("(new
|
|
11591
|
+
console.log(source_default.green(` + ${server.name}.${lf}`), source_default.dim("(new -- will be added on push)"));
|
|
11592
11592
|
hasChanges = true;
|
|
11593
11593
|
}
|
|
11594
11594
|
}
|
|
11595
11595
|
}
|
|
11596
11596
|
for (const local of localModels) {
|
|
11597
11597
|
if (!serverModels.find((m2) => m2.name === local.name)) {
|
|
11598
|
-
console.log(source_default.green(` + Model "${local.name}"`), source_default.dim("(new
|
|
11598
|
+
console.log(source_default.green(` + Model "${local.name}"`), source_default.dim("(new -- will be created on push)"));
|
|
11599
11599
|
hasChanges = true;
|
|
11600
11600
|
}
|
|
11601
11601
|
}
|
|
11602
11602
|
if (!hasChanges) {
|
|
11603
|
-
console.log(source_default.green(" Schema is in sync
|
|
11603
|
+
console.log(source_default.green(" Schema is in sync -- no changes needed"));
|
|
11604
11604
|
return;
|
|
11605
11605
|
}
|
|
11606
11606
|
console.log();
|
|
@@ -11653,7 +11653,7 @@ async function graphMigrate(options) {
|
|
|
11653
11653
|
process.exit(1);
|
|
11654
11654
|
}
|
|
11655
11655
|
}
|
|
11656
|
-
function parseModelFields(content
|
|
11656
|
+
function parseModelFields(content) {
|
|
11657
11657
|
const modelMatch = content.match(/defineModel\s*\(\s*['"](\w+)['"]/);
|
|
11658
11658
|
if (!modelMatch)
|
|
11659
11659
|
return null;
|
|
@@ -11700,7 +11700,7 @@ function graphFork(newSpaceID) {
|
|
|
11700
11700
|
// package.json
|
|
11701
11701
|
var package_default = {
|
|
11702
11702
|
name: "@construct-space/cli",
|
|
11703
|
-
version: "1.9.
|
|
11703
|
+
version: "1.9.4",
|
|
11704
11704
|
description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
|
|
11705
11705
|
type: "module",
|
|
11706
11706
|
bin: {
|
|
@@ -11747,12 +11747,12 @@ var package_default = {
|
|
|
11747
11747
|
// src/index.ts
|
|
11748
11748
|
var VERSION = package_default.version;
|
|
11749
11749
|
var program2 = new Command;
|
|
11750
|
-
program2.name("construct").description("Construct CLI
|
|
11750
|
+
program2.name("construct").description("Construct CLI -- scaffold, build, develop, and publish spaces").version(VERSION);
|
|
11751
11751
|
program2.command("scaffold [name]").alias("new").alias("create").description("Create a new Construct space project").option("--with-tests", "Include E2E testing boilerplate").option("--full", "Full preset: multiple pages, extra skills, widget templates").action(async (name, opts) => scaffold(name, opts));
|
|
11752
11752
|
program2.command("build").description("Build the space (generate entry + run Vite)").option("--entry-only", "Only generate src/entry.ts").action(async (opts) => build(opts));
|
|
11753
11753
|
program2.command("dev").description("Start dev mode with file watching and live reload").action(async () => dev());
|
|
11754
11754
|
program2.command("install").alias("run").description("Install built space to Construct spaces directory").action(() => install());
|
|
11755
|
-
program2.command("publish").description("Publish a space to the Construct registry").option("-y, --yes", "Skip all confirmation prompts").option("--bump <type>", "Auto-bump version (patch, minor, major)").option("--private", "Publish as org-private (catalog-listed only inside the owning org). Requires an org publisher key.").option("--public", "Flip a previously-private space back to the public catalog on this publish. Without --private or --public, visibility is preserved on update (and defaults to public on first publish).").option("--scope <scope>", "Publish as 'user' (personal) or 'org'. Without this flag, the active publisher in auth.json decides.").option("--api-key <key>", "Publisher API key (csk_live_
|
|
11755
|
+
program2.command("publish").description("Publish a space to the Construct registry").option("-y, --yes", "Skip all confirmation prompts").option("--bump <type>", "Auto-bump version (patch, minor, major)").option("--private", "Publish as org-private (catalog-listed only inside the owning org). Requires an org publisher key.").option("--public", "Flip a previously-private space back to the public catalog on this publish. Without --private or --public, visibility is preserved on update (and defaults to public on first publish).").option("--scope <scope>", "Publish as 'user' (personal) or 'org'. Without this flag, the active publisher in auth.json decides.").option("--api-key <key>", "Publisher API key (csk_live_...). Overrides any key stored in the active profile. Also reads CONSTRUCT_PUBLISHER_KEY.").action(async (opts) => publish(opts));
|
|
11756
11756
|
program2.command("validate").description("Validate space.manifest.json").action(() => validate3());
|
|
11757
11757
|
program2.command("check").description("Run type-check (vue-tsc) and linter (eslint)").action(() => check());
|
|
11758
11758
|
program2.command("clean").description("Remove build artifacts").option("--all", "Also remove node_modules and lockfiles").action((opts) => clean(opts));
|
|
@@ -11762,7 +11762,7 @@ program2.command("update").description("Update the CLI to the latest version").a
|
|
|
11762
11762
|
program2.command("whoami").alias("status").description("Show the signed-in user + current org scope").action(async () => (await Promise.resolve().then(() => (init_whoami(), exports_whoami))).whoami());
|
|
11763
11763
|
var org = program2.command("org").description("Inspect the active organization context");
|
|
11764
11764
|
org.command("status").description("Show active scope, publisher, and roles").action(async () => (await Promise.resolve().then(() => (init_org(), exports_org))).orgStatus());
|
|
11765
|
-
var graph = program2.command("graph").description("Construct Graph
|
|
11765
|
+
var graph = program2.command("graph").description("Construct Graph -- data models and GraphQL");
|
|
11766
11766
|
graph.command("init").description("Initialize Graph in a space project").action(() => graphInit());
|
|
11767
11767
|
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));
|
|
11768
11768
|
graph.command("push").description("Register models with the Graph service").action(async () => graphPush());
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Space Actions
|
|
2
|
+
* Space Actions -- exposed to the AI agent via space_run_action.
|
|
3
3
|
*
|
|
4
4
|
* Each action: { description, params, run, tier? }.
|
|
5
5
|
* - `params` JSON-schema-ish input shape; each: { type, description?, required? }.
|
|
6
6
|
* - `run` receives the validated payload, returns any JSON-serialisable value.
|
|
7
7
|
* - `tier` (optional) default model bucket for useBrain() calls inside `run`:
|
|
8
|
-
* 'small' fast/cheap
|
|
9
|
-
* 'medium' balanced
|
|
10
|
-
* 'large' reasoning
|
|
11
|
-
* The host resolves tier
|
|
12
|
-
* (Settings
|
|
8
|
+
* 'small' fast/cheap -- summarisation, classification, short Q&A
|
|
9
|
+
* 'medium' balanced -- general help, edits, structured output
|
|
10
|
+
* 'large' reasoning -- long-form writing, deep code, planning
|
|
11
|
+
* The host resolves tier -> provider+model via the user's tier config
|
|
12
|
+
* (Settings -> LLM Providers). Per-call `brain.complete({ tier })`
|
|
13
13
|
* wins over the action default.
|
|
14
14
|
*
|
|
15
15
|
* Permission for useBrain():
|
|
@@ -31,7 +31,7 @@ import type { SpaceActions } from '@construct-space/sdk'
|
|
|
31
31
|
|
|
32
32
|
export const actions: SpaceActions = {
|
|
33
33
|
ping: {
|
|
34
|
-
description: 'Health check
|
|
34
|
+
description: 'Health check -- returns pong with the space id.',
|
|
35
35
|
params: {},
|
|
36
36
|
run: () => ({ pong: true, space: '{{.ID}}' }),
|
|
37
37
|
},
|
|
@@ -21,7 +21,7 @@ Use the listed tools above first. If you need an action that isn't whitelisted,
|
|
|
21
21
|
call space_list_actions to discover what else this space exposes, then
|
|
22
22
|
space_run_action to invoke it.
|
|
23
23
|
|
|
24
|
-
Do NOT call get_project_context
|
|
24
|
+
Do NOT call get_project_context -- you work with space content, not project files.
|
|
25
25
|
|
|
26
26
|
## Behavior
|
|
27
27
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: construct-stack
|
|
3
3
|
name: Construct Stack Guide
|
|
4
|
-
description: How to build and extend a Construct space
|
|
4
|
+
description: How to build and extend a Construct space -- CLI, UI, SDK, Graph, manifest, agent actions
|
|
5
5
|
trigger: construct|space|graph|ui|sdk|manifest|action
|
|
6
6
|
category: skill
|
|
7
7
|
tools: [read_file, list_dir, glob, grep, write_file, edit_file]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
# Construct.md
|
|
10
|
+
# Construct.md -- agent guide for `{{.DisplayName}}`
|
|
11
11
|
|
|
12
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
13
|
|
|
@@ -23,7 +23,7 @@ This file briefs an AI coding agent on the Construct stack so it can build, exte
|
|
|
23
23
|
| Icons | `lucide-vue-next` | Icon set |
|
|
24
24
|
| Tooling | `@construct-space/cli` | Build / dev / publish |
|
|
25
25
|
|
|
26
|
-
All four are **host-provided externals**
|
|
26
|
+
All four are **host-provided externals** -- import normally, never bundle. Vite externalises them via `vite.config.ts`.
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -35,7 +35,7 @@ All four are **host-provided externals** — import normally, never bundle. Vite
|
|
|
35
35
|
src/
|
|
36
36
|
entry.ts auto-generated; do not hand-edit
|
|
37
37
|
actions.ts agent-callable actions (space_run_action)
|
|
38
|
-
pages/ filesystem routing (e.g. settings.vue
|
|
38
|
+
pages/ filesystem routing (e.g. settings.vue -> /settings)
|
|
39
39
|
components/ local Vue components
|
|
40
40
|
composables/ local composables (use* convention)
|
|
41
41
|
models/ graph models (defineModel)
|
|
@@ -52,7 +52,7 @@ All four are **host-provided externals** — import normally, never bundle. Vite
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
construct dev hot-reload dev (regenerates entry.ts on change)
|
|
55
|
-
construct build production build
|
|
55
|
+
construct build production build -> dist/
|
|
56
56
|
construct check manifest + typecheck + lint
|
|
57
57
|
construct install install to active profile
|
|
58
58
|
construct publish publish to registry (requires auth)
|
|
@@ -79,11 +79,11 @@ construct graph install <id> install a published space for current org
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
`scopes: ['org']` makes everything multi-tenant
|
|
82
|
+
`scopes: ['org']` makes everything multi-tenant -- graph schemas auto-partition per org. Use `useOrg()` to read current org context. `projectAware: true` is the orthogonal flag for spaces that also live inside a project.
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
|
-
## `@construct-space/ui`
|
|
86
|
+
## `@construct-space/ui` -- component cheatsheet
|
|
87
87
|
|
|
88
88
|
All exported from the package root:
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@ All exported from the package root:
|
|
|
91
91
|
|
|
92
92
|
**Inputs**: `Input`, `Textarea`, `Select` (options: `{label, value}[]`), `MultiSelect`, `Checkbox`, `Switch`, `RadioGroup`, `DatePicker`, `ColorPicker`, `FileInput`, `Slider`, `Autocomplete`, `FormField` (label/hint wrapper)
|
|
93
93
|
|
|
94
|
-
**Display**: `Badge` (color: primary|neutral|success|warning|error|info; variant: solid|soft|subtle|outline), `Chip`, `Avatar` (`fallback` shows text
|
|
94
|
+
**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`
|
|
95
95
|
|
|
96
96
|
**Actions**: `Button` (variant: solid|ghost|outline; color: primary|neutral|error|...; size: xs|sm|md|lg), `Dropdown`, `DropdownMenu`, `ContextMenu`, `Popover`, `ToggleGroup` (segmented control)
|
|
97
97
|
|
|
@@ -104,13 +104,13 @@ All exported from the package root:
|
|
|
104
104
|
### Component gotchas
|
|
105
105
|
|
|
106
106
|
- **`Avatar` sizes**: only `sm | md | lg` (no `xs`).
|
|
107
|
-
- **`Select`** doesn't accept `multiple`
|
|
107
|
+
- **`Select`** doesn't accept `multiple` -- render a chip-toggle list yourself for multi-select.
|
|
108
108
|
- **`Table`** column `align` literals must be `as const` to typecheck (`align: 'right' as const`).
|
|
109
109
|
- **`Table` rowKey** is a string field name on the row object, not a function.
|
|
110
110
|
- **`Modal`** uses `v-model:open` (not `v-model`); slots: `header | body | footer | accessory`.
|
|
111
111
|
- **`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`.
|
|
112
|
-
- **`ConfirmationModal`** emits `@confirm` and `@cancel`
|
|
113
|
-
- **Settings pages**: never raw `<select>`/`<input>`/`<textarea>`
|
|
112
|
+
- **`ConfirmationModal`** emits `@confirm` and `@cancel` -- wire both, otherwise dismissal does nothing.
|
|
113
|
+
- **Settings pages**: never raw `<select>`/`<input>`/`<textarea>` -- use `Select`, `Switch`, `Input`, `Textarea` with `FormField` labels. Persist with `localStorage` (key prefix per space).
|
|
114
114
|
|
|
115
115
|
```vue
|
|
116
116
|
<script setup lang="ts">
|
|
@@ -124,13 +124,13 @@ const open = ref(false)
|
|
|
124
124
|
<Button variant="solid" size="sm" @click="open = true">Open</Button>
|
|
125
125
|
</template>
|
|
126
126
|
</Card>
|
|
127
|
-
<Modal v-model:open="open" title="Demo"
|
|
127
|
+
<Modal v-model:open="open" title="Demo">...</Modal>
|
|
128
128
|
</template>
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
133
|
-
## `@construct-space/sdk`
|
|
133
|
+
## `@construct-space/sdk` -- host composables
|
|
134
134
|
|
|
135
135
|
```ts
|
|
136
136
|
import { useOrg, useOrgMembers, useToast, useAuth } from '@construct-space/sdk'
|
|
@@ -145,7 +145,7 @@ Use these for org-aware UX (assignee pickers, "shared with N" badges, avatars, m
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
148
|
-
## `@construct-space/graph`
|
|
148
|
+
## `@construct-space/graph` -- data layer
|
|
149
149
|
|
|
150
150
|
Define typed models, get a typed reactive client, multi-tenant by default.
|
|
151
151
|
|
|
@@ -186,12 +186,12 @@ await tasks.remove(id)
|
|
|
186
186
|
|
|
187
187
|
**Field types**: `string`, `int`, `number`, `boolean`, `date`, `json`, `enum([...])`. Chain `.required()`, `.index()`, `.unique()`, `.default(v)`.
|
|
188
188
|
|
|
189
|
-
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context
|
|
189
|
+
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context -- use `authenticated()` for org-scoped models that allow any logged-in member.
|
|
190
190
|
|
|
191
191
|
**Scopes** (must match `scopes` in space.manifest.json):
|
|
192
|
-
- `'app'`
|
|
193
|
-
- `'org'`
|
|
194
|
-
- both: `scopes: ['app', 'org']`
|
|
192
|
+
- `'app'` -- per-user bucket (personal install)
|
|
193
|
+
- `'org'` -- shared across the active organization (org install)
|
|
194
|
+
- both: `scopes: ['app', 'org']` -- host picks bucket at install time
|
|
195
195
|
|
|
196
196
|
`'project'` was removed in SDK 1.0. Older docs that show it are stale.
|
|
197
197
|
|
|
@@ -209,16 +209,16 @@ await tasks.remove(id)
|
|
|
209
209
|
return []
|
|
210
210
|
}
|
|
211
211
|
```
|
|
212
|
-
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails
|
|
212
|
+
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails -- use a `field.string()` foreign-key column instead and join manually in composables.
|
|
213
213
|
- **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
|
|
214
214
|
- **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.
|
|
215
|
-
- **`access.member()` requires org context**
|
|
215
|
+
- **`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.
|
|
216
216
|
|
|
217
217
|
---
|
|
218
218
|
|
|
219
219
|
## Actions (agent surface)
|
|
220
220
|
|
|
221
|
-
`src/actions.ts` exports an `actions` object
|
|
221
|
+
`src/actions.ts` exports an `actions` object -- each entry is exposed to the space's agent as a first-class tool.
|
|
222
222
|
|
|
223
223
|
```ts
|
|
224
224
|
import type { SpaceActions } from '@construct-space/sdk'
|
|
@@ -239,7 +239,7 @@ export const actions: SpaceActions = {
|
|
|
239
239
|
|
|
240
240
|
Actions can call the model mid-execution via `useBrain()`. The action picks
|
|
241
241
|
the **cost bucket** (small / medium / large), the **user** picks the slot
|
|
242
|
-
in Settings
|
|
242
|
+
in Settings -> LLM Providers. The host maps tier -> provider + model.
|
|
243
243
|
|
|
244
244
|
```ts
|
|
245
245
|
import { useBrain } from '@construct-space/sdk'
|
|
@@ -261,7 +261,7 @@ export const actions: SpaceActions = {
|
|
|
261
261
|
|
|
262
262
|
composeReply: {
|
|
263
263
|
description: 'Draft a polished reply to the given message.',
|
|
264
|
-
tier: 'large', // long-form writing
|
|
264
|
+
tier: 'large', // long-form writing -> opus-class
|
|
265
265
|
params: {
|
|
266
266
|
original: { type: 'string', required: true },
|
|
267
267
|
style: { type: 'string', required: false, description: 'e.g. "warm", "formal"' },
|
|
@@ -304,7 +304,7 @@ to it under `permissions.actions`. The user grants this at install time.
|
|
|
304
304
|
Without the grant, `useBrain()` throws `BrainPermissionDenied` synchronously
|
|
305
305
|
on the first method call so the action can fall back gracefully.
|
|
306
306
|
|
|
307
|
-
### Wiring (REQUIRED
|
|
307
|
+
### Wiring (REQUIRED -- easy to miss)
|
|
308
308
|
|
|
309
309
|
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".
|
|
310
310
|
|
|
@@ -319,15 +319,15 @@ tools:
|
|
|
319
319
|
---
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
-
Fallback: agents can also discover/invoke actions via the generic `space_list_actions` + `space_run_action` bridge tools. Prefer the whitelist
|
|
322
|
+
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.
|
|
323
323
|
|
|
324
|
-
Keep action descriptions tight and specific
|
|
324
|
+
Keep action descriptions tight and specific -- they're the only docstring the agent sees.
|
|
325
325
|
|
|
326
326
|
---
|
|
327
327
|
|
|
328
328
|
## Drag and drop (HTML5)
|
|
329
329
|
|
|
330
|
-
Tauri intercepts native drag-drop by default. The Construct host sets `dragDropEnabled: false` so HTML5 events fire normally
|
|
330
|
+
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.
|
|
331
331
|
|
|
332
332
|
---
|
|
333
333
|
|
|
@@ -335,7 +335,7 @@ Tauri intercepts native drag-drop by default. The Construct host sets `dragDropE
|
|
|
335
335
|
|
|
336
336
|
- **Imports**: `@construct-space/*` packages always external. Local imports use relative paths (no `@/` alias unless you add one).
|
|
337
337
|
- **Composables**: `use*` prefix, keep in `src/composables/`.
|
|
338
|
-
- **Pages**: filename = route path. `index.vue`
|
|
338
|
+
- **Pages**: filename = route path. `index.vue` -> `/`, `settings.vue` -> `/settings`, `[id].vue` -> param.
|
|
339
339
|
- **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.
|
|
340
340
|
- **Comments**: explain *why*, not *what*. Skip TODOs in committed code.
|
|
341
341
|
- **No emojis** in source unless the user requests them.
|
|
@@ -345,7 +345,7 @@ Tauri intercepts native drag-drop by default. The Construct host sets `dragDropE
|
|
|
345
345
|
## Build pipeline reminder
|
|
346
346
|
|
|
347
347
|
`construct build` writes:
|
|
348
|
-
- `dist/space-{{.ID}}.iife.js`
|
|
349
|
-
- `dist/manifest.json`
|
|
348
|
+
- `dist/space-{{.ID}}.iife.js` -- the bundled space
|
|
349
|
+
- `dist/manifest.json` -- manifest + `build` block (checksum, size, **`hostApiVersion`**, builtAt)
|
|
350
350
|
|
|
351
351
|
The runtime SpaceLoader compares `hostApiVersion` to its own; mismatches log a warning. Bump the CLI to keep them aligned.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Space entry
|
|
1
|
+
// Space entry -- exports pages, widgets, and actions for the host loader.
|
|
2
2
|
// `construct dev` regenerates this from space.manifest.json on changes.
|
|
3
3
|
import './style.css'
|
|
4
4
|
import IndexPage from './pages/index.vue'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Space entry
|
|
1
|
+
// Space entry -- exports pages, widgets, and actions for the host loader.
|
|
2
2
|
// `construct dev` regenerates this from space.manifest.json on changes.
|
|
3
3
|
import './style.css'
|
|
4
4
|
import IndexPage from './pages/index.vue'
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# {{.DisplayName}}
|
|
1
|
+
# {{.DisplayName}} -- data
|
|
2
2
|
|
|
3
3
|
Reference doc for the agent when working with {{.DisplayName}} data.
|
|
4
4
|
|
|
5
5
|
## Storage model
|
|
6
6
|
|
|
7
|
-
Describe where {{.DisplayName}} data lives (Graph models, local storage, server API
|
|
7
|
+
Describe where {{.DisplayName}} data lives (Graph models, local storage, server API...). Edit this section to match the actual model.
|
|
8
8
|
|
|
9
9
|
## CRUD conventions
|
|
10
10
|
|
|
11
11
|
- Validate inputs before writing
|
|
12
12
|
- Prefer the space's actions API over raw file or DB writes
|
|
13
13
|
- Batch bulk changes when possible
|
|
14
|
-
- Read existing rows before modifying
|
|
14
|
+
- Read existing rows before modifying -- changes should be additive when in doubt
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# {{.DisplayName}}
|
|
1
|
+
# {{.DisplayName}} -- UI
|
|
2
2
|
|
|
3
3
|
Reference doc for the agent when adjusting {{.DisplayName}}'s UI.
|
|
4
4
|
|
|
@@ -12,8 +12,8 @@ Use Construct CSS variables instead of hardcoded values:
|
|
|
12
12
|
|
|
13
13
|
## Layout
|
|
14
14
|
|
|
15
|
-
- Live inside the host shell
|
|
16
|
-
- Use the `@construct-space/ui` primitives (`Card`, `Button`, `Badge`,
|
|
15
|
+
- Live inside the host shell -- avoid full-bleed layouts that conflict with the sidebar/header
|
|
16
|
+
- Use the `@construct-space/ui` primitives (`Card`, `Button`, `Badge`, ...) for consistency
|
|
17
17
|
- Prefer flex/grid over absolute positioning
|
|
18
18
|
|
|
19
19
|
## Accessibility
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* {{.DisplayName}}
|
|
3
|
+
* {{.DisplayName}} -- Home page
|
|
4
4
|
*
|
|
5
5
|
* Built-in libraries available at runtime (do not bundle):
|
|
6
|
-
* @construct-space/ui
|
|
7
|
-
* @construct-space/sdk
|
|
8
|
-
* @construct-space/graph
|
|
9
|
-
* lucide-vue-next
|
|
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
|
|
10
10
|
*/
|
|
11
11
|
import { ref } from 'vue'
|
|
12
12
|
import { Button, Card, Empty } from '@construct-space/ui'
|
|
@@ -26,6 +26,6 @@ Help the user with tasks inside the {{.DisplayName}} space.
|
|
|
26
26
|
|
|
27
27
|
The agent loads these on demand:
|
|
28
28
|
|
|
29
|
-
- `references/`
|
|
30
|
-
- `scripts/`
|
|
31
|
-
- `assets/`
|
|
29
|
+
- `references/` -- domain-specific docs (data model, UI conventions, ...)
|
|
30
|
+
- `scripts/` -- supporting executables
|
|
31
|
+
- `assets/` -- templates and static resources
|
|
@@ -4,19 +4,19 @@ import vue from '@vitejs/plugin-vue'
|
|
|
4
4
|
import { resolve } from 'path'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Host-provided externals
|
|
7
|
+
* Host-provided externals -- these are supplied by Construct at runtime
|
|
8
8
|
* via window.__CONSTRUCT__. Do NOT bundle them; they must stay external.
|
|
9
9
|
*
|
|
10
10
|
* Source of truth: construct-app/frontend/lib/spaceHost.ts
|
|
11
11
|
*
|
|
12
|
-
* IMPORTANT
|
|
12
|
+
* IMPORTANT -- only add a package here AFTER confirming spaceHost.ts
|
|
13
13
|
* actually puts it on window.__CONSTRUCT__. Externalising a package the
|
|
14
14
|
* host doesn't expose silently substitutes `undefined`; you'll see
|
|
15
15
|
* "X is not a function" at runtime. When in doubt, bundle it (omit from
|
|
16
|
-
* this list)
|
|
16
|
+
* this list) -- bundle bloat is cheaper than a broken space.
|
|
17
17
|
*
|
|
18
18
|
* Notably NOT externalised:
|
|
19
|
-
* @construct-space/graph
|
|
19
|
+
* @construct-space/graph -- host does not expose this. Bundle it.
|
|
20
20
|
*/
|
|
21
21
|
const hostExternals = [
|
|
22
22
|
'vue',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* {{.DisplayName}} Summary Widget
|
|
3
|
+
* {{.DisplayName}} Summary Widget -- 2x1 compact view
|
|
4
4
|
*
|
|
5
5
|
* Widgets run inside a closed Shadow DOM sandbox.
|
|
6
|
-
* Use the injected widgetApi for theme and actions
|
|
6
|
+
* Use the injected widgetApi for theme and actions -- do not access
|
|
7
7
|
* window, document, or global stores directly.
|
|
8
8
|
*/
|
|
9
9
|
import { inject } from 'vue'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* {{.DisplayName}} Summary Widget
|
|
3
|
+
* {{.DisplayName}} Summary Widget -- 4x1 wide view
|
|
4
4
|
*
|
|
5
5
|
* Widgets run inside a closed Shadow DOM sandbox.
|
|
6
|
-
* Use the injected widgetApi for theme and actions
|
|
6
|
+
* Use the injected widgetApi for theme and actions -- do not access
|
|
7
7
|
* window, document, or global stores directly.
|
|
8
8
|
*/
|
|
9
9
|
import { inject } from 'vue'
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Space Actions
|
|
2
|
+
* Space Actions -- exposed to the AI agent via space_run_action.
|
|
3
3
|
*
|
|
4
4
|
* Each action: { description, params, run, tier? }.
|
|
5
5
|
* - `params` JSON-schema-ish input shape; each: { type, description?, required? }.
|
|
6
6
|
* - `run` receives the validated payload, returns any JSON-serialisable value.
|
|
7
7
|
* - `tier` (optional) default model bucket for useBrain() calls inside `run`:
|
|
8
|
-
* 'small' fast/cheap
|
|
9
|
-
* 'medium' balanced
|
|
10
|
-
* 'large' reasoning
|
|
11
|
-
* The host resolves tier
|
|
12
|
-
* (Settings
|
|
8
|
+
* 'small' fast/cheap -- summarisation, classification, short Q&A
|
|
9
|
+
* 'medium' balanced -- general help, edits, structured output
|
|
10
|
+
* 'large' reasoning -- long-form writing, deep code, planning
|
|
11
|
+
* The host resolves tier -> provider+model via the user's tier config
|
|
12
|
+
* (Settings -> LLM Providers). Per-call `brain.complete({ tier })`
|
|
13
13
|
* wins over the action default.
|
|
14
14
|
*
|
|
15
15
|
* Permission for useBrain():
|
|
@@ -31,7 +31,7 @@ import type { SpaceActions } from '@construct-space/sdk'
|
|
|
31
31
|
|
|
32
32
|
export const actions: SpaceActions = {
|
|
33
33
|
ping: {
|
|
34
|
-
description: 'Health check
|
|
34
|
+
description: 'Health check -- returns pong with the space id.',
|
|
35
35
|
params: {},
|
|
36
36
|
run: () => ({ pong: true, space: '{{.ID}}' }),
|
|
37
37
|
},
|
|
@@ -21,7 +21,7 @@ Use the listed tools above first. If you need an action that isn't whitelisted,
|
|
|
21
21
|
call space_list_actions to discover what else this space exposes, then
|
|
22
22
|
space_run_action to invoke it.
|
|
23
23
|
|
|
24
|
-
Do NOT call get_project_context
|
|
24
|
+
Do NOT call get_project_context -- you work with space content, not project files.
|
|
25
25
|
|
|
26
26
|
## Behavior
|
|
27
27
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: construct-stack
|
|
3
3
|
name: Construct Stack Guide
|
|
4
|
-
description: How to build and extend a Construct space
|
|
4
|
+
description: How to build and extend a Construct space -- CLI, UI, SDK, Graph, manifest, agent actions
|
|
5
5
|
trigger: construct|space|graph|ui|sdk|manifest|action
|
|
6
6
|
category: skill
|
|
7
7
|
tools: [read_file, list_dir, glob, grep, write_file, edit_file]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
# Construct.md
|
|
10
|
+
# Construct.md -- agent guide for `{{.DisplayName}}`
|
|
11
11
|
|
|
12
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
13
|
|
|
@@ -23,7 +23,7 @@ This file briefs an AI coding agent on the Construct stack so it can build, exte
|
|
|
23
23
|
| Icons | `lucide-vue-next` | Icon set |
|
|
24
24
|
| Tooling | `@construct-space/cli` | Build / dev / publish |
|
|
25
25
|
|
|
26
|
-
All four are **host-provided externals**
|
|
26
|
+
All four are **host-provided externals** -- import normally, never bundle. Vite externalises them via `vite.config.ts`.
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -35,7 +35,7 @@ All four are **host-provided externals** — import normally, never bundle. Vite
|
|
|
35
35
|
src/
|
|
36
36
|
entry.ts auto-generated; do not hand-edit
|
|
37
37
|
actions.ts agent-callable actions (space_run_action)
|
|
38
|
-
pages/ filesystem routing (e.g. settings.vue
|
|
38
|
+
pages/ filesystem routing (e.g. settings.vue -> /settings)
|
|
39
39
|
components/ local Vue components
|
|
40
40
|
composables/ local composables (use* convention)
|
|
41
41
|
models/ graph models (defineModel)
|
|
@@ -52,7 +52,7 @@ All four are **host-provided externals** — import normally, never bundle. Vite
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
construct dev hot-reload dev (regenerates entry.ts on change)
|
|
55
|
-
construct build production build
|
|
55
|
+
construct build production build -> dist/
|
|
56
56
|
construct check manifest + typecheck + lint
|
|
57
57
|
construct install install to active profile
|
|
58
58
|
construct publish publish to registry (requires auth)
|
|
@@ -79,11 +79,11 @@ construct graph install <id> install a published space for current org
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
`scopes: ['org']` makes everything multi-tenant
|
|
82
|
+
`scopes: ['org']` makes everything multi-tenant -- graph schemas auto-partition per org. Use `useOrg()` to read current org context. `projectAware: true` is the orthogonal flag for spaces that also live inside a project.
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
|
-
## `@construct-space/ui`
|
|
86
|
+
## `@construct-space/ui` -- component cheatsheet
|
|
87
87
|
|
|
88
88
|
All exported from the package root:
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@ All exported from the package root:
|
|
|
91
91
|
|
|
92
92
|
**Inputs**: `Input`, `Textarea`, `Select` (options: `{label, value}[]`), `MultiSelect`, `Checkbox`, `Switch`, `RadioGroup`, `DatePicker`, `ColorPicker`, `FileInput`, `Slider`, `Autocomplete`, `FormField` (label/hint wrapper)
|
|
93
93
|
|
|
94
|
-
**Display**: `Badge` (color: primary|neutral|success|warning|error|info; variant: solid|soft|subtle|outline), `Chip`, `Avatar` (`fallback` shows text
|
|
94
|
+
**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`
|
|
95
95
|
|
|
96
96
|
**Actions**: `Button` (variant: solid|ghost|outline; color: primary|neutral|error|...; size: xs|sm|md|lg), `Dropdown`, `DropdownMenu`, `ContextMenu`, `Popover`, `ToggleGroup` (segmented control)
|
|
97
97
|
|
|
@@ -104,13 +104,13 @@ All exported from the package root:
|
|
|
104
104
|
### Component gotchas
|
|
105
105
|
|
|
106
106
|
- **`Avatar` sizes**: only `sm | md | lg` (no `xs`).
|
|
107
|
-
- **`Select`** doesn't accept `multiple`
|
|
107
|
+
- **`Select`** doesn't accept `multiple` -- render a chip-toggle list yourself for multi-select.
|
|
108
108
|
- **`Table`** column `align` literals must be `as const` to typecheck (`align: 'right' as const`).
|
|
109
109
|
- **`Table` rowKey** is a string field name on the row object, not a function.
|
|
110
110
|
- **`Modal`** uses `v-model:open` (not `v-model`); slots: `header | body | footer | accessory`.
|
|
111
111
|
- **`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`.
|
|
112
|
-
- **`ConfirmationModal`** emits `@confirm` and `@cancel`
|
|
113
|
-
- **Settings pages**: never raw `<select>`/`<input>`/`<textarea>`
|
|
112
|
+
- **`ConfirmationModal`** emits `@confirm` and `@cancel` -- wire both, otherwise dismissal does nothing.
|
|
113
|
+
- **Settings pages**: never raw `<select>`/`<input>`/`<textarea>` -- use `Select`, `Switch`, `Input`, `Textarea` with `FormField` labels. Persist with `localStorage` (key prefix per space).
|
|
114
114
|
|
|
115
115
|
```vue
|
|
116
116
|
<script setup lang="ts">
|
|
@@ -124,13 +124,13 @@ const open = ref(false)
|
|
|
124
124
|
<Button variant="solid" size="sm" @click="open = true">Open</Button>
|
|
125
125
|
</template>
|
|
126
126
|
</Card>
|
|
127
|
-
<Modal v-model:open="open" title="Demo"
|
|
127
|
+
<Modal v-model:open="open" title="Demo">...</Modal>
|
|
128
128
|
</template>
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
133
|
-
## `@construct-space/sdk`
|
|
133
|
+
## `@construct-space/sdk` -- host composables
|
|
134
134
|
|
|
135
135
|
```ts
|
|
136
136
|
import { useOrg, useOrgMembers, useToast, useAuth } from '@construct-space/sdk'
|
|
@@ -145,7 +145,7 @@ Use these for org-aware UX (assignee pickers, "shared with N" badges, avatars, m
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
148
|
-
## `@construct-space/graph`
|
|
148
|
+
## `@construct-space/graph` -- data layer
|
|
149
149
|
|
|
150
150
|
Define typed models, get a typed reactive client, multi-tenant by default.
|
|
151
151
|
|
|
@@ -186,12 +186,12 @@ await tasks.remove(id)
|
|
|
186
186
|
|
|
187
187
|
**Field types**: `string`, `int`, `number`, `boolean`, `date`, `json`, `enum([...])`. Chain `.required()`, `.index()`, `.unique()`, `.default(v)`.
|
|
188
188
|
|
|
189
|
-
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context
|
|
189
|
+
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context -- use `authenticated()` for org-scoped models that allow any logged-in member.
|
|
190
190
|
|
|
191
191
|
**Scopes** (must match `scopes` in space.manifest.json):
|
|
192
|
-
- `'app'`
|
|
193
|
-
- `'org'`
|
|
194
|
-
- both: `scopes: ['app', 'org']`
|
|
192
|
+
- `'app'` -- per-user bucket (personal install)
|
|
193
|
+
- `'org'` -- shared across the active organization (org install)
|
|
194
|
+
- both: `scopes: ['app', 'org']` -- host picks bucket at install time
|
|
195
195
|
|
|
196
196
|
`'project'` was removed in SDK 1.0. Older docs that show it are stale.
|
|
197
197
|
|
|
@@ -209,16 +209,16 @@ await tasks.remove(id)
|
|
|
209
209
|
return []
|
|
210
210
|
}
|
|
211
211
|
```
|
|
212
|
-
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails
|
|
212
|
+
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails -- use a `field.string()` foreign-key column instead and join manually in composables.
|
|
213
213
|
- **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
|
|
214
214
|
- **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.
|
|
215
|
-
- **`access.member()` requires org context**
|
|
215
|
+
- **`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.
|
|
216
216
|
|
|
217
217
|
---
|
|
218
218
|
|
|
219
219
|
## Actions (agent surface)
|
|
220
220
|
|
|
221
|
-
`src/actions.ts` exports an `actions` object
|
|
221
|
+
`src/actions.ts` exports an `actions` object -- each entry is exposed to the space's agent as a first-class tool.
|
|
222
222
|
|
|
223
223
|
```ts
|
|
224
224
|
import type { SpaceActions } from '@construct-space/sdk'
|
|
@@ -239,7 +239,7 @@ export const actions: SpaceActions = {
|
|
|
239
239
|
|
|
240
240
|
Actions can call the model mid-execution via `useBrain()`. The action picks
|
|
241
241
|
the **cost bucket** (small / medium / large), the **user** picks the slot
|
|
242
|
-
in Settings
|
|
242
|
+
in Settings -> LLM Providers. The host maps tier -> provider + model.
|
|
243
243
|
|
|
244
244
|
```ts
|
|
245
245
|
import { useBrain } from '@construct-space/sdk'
|
|
@@ -261,7 +261,7 @@ export const actions: SpaceActions = {
|
|
|
261
261
|
|
|
262
262
|
composeReply: {
|
|
263
263
|
description: 'Draft a polished reply to the given message.',
|
|
264
|
-
tier: 'large', // long-form writing
|
|
264
|
+
tier: 'large', // long-form writing -> opus-class
|
|
265
265
|
params: {
|
|
266
266
|
original: { type: 'string', required: true },
|
|
267
267
|
style: { type: 'string', required: false, description: 'e.g. "warm", "formal"' },
|
|
@@ -304,7 +304,7 @@ to it under `permissions.actions`. The user grants this at install time.
|
|
|
304
304
|
Without the grant, `useBrain()` throws `BrainPermissionDenied` synchronously
|
|
305
305
|
on the first method call so the action can fall back gracefully.
|
|
306
306
|
|
|
307
|
-
### Wiring (REQUIRED
|
|
307
|
+
### Wiring (REQUIRED -- easy to miss)
|
|
308
308
|
|
|
309
309
|
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".
|
|
310
310
|
|
|
@@ -319,15 +319,15 @@ tools:
|
|
|
319
319
|
---
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
-
Fallback: agents can also discover/invoke actions via the generic `space_list_actions` + `space_run_action` bridge tools. Prefer the whitelist
|
|
322
|
+
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.
|
|
323
323
|
|
|
324
|
-
Keep action descriptions tight and specific
|
|
324
|
+
Keep action descriptions tight and specific -- they're the only docstring the agent sees.
|
|
325
325
|
|
|
326
326
|
---
|
|
327
327
|
|
|
328
328
|
## Drag and drop (HTML5)
|
|
329
329
|
|
|
330
|
-
Tauri intercepts native drag-drop by default. The Construct host sets `dragDropEnabled: false` so HTML5 events fire normally
|
|
330
|
+
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.
|
|
331
331
|
|
|
332
332
|
---
|
|
333
333
|
|
|
@@ -335,7 +335,7 @@ Tauri intercepts native drag-drop by default. The Construct host sets `dragDropE
|
|
|
335
335
|
|
|
336
336
|
- **Imports**: `@construct-space/*` packages always external. Local imports use relative paths (no `@/` alias unless you add one).
|
|
337
337
|
- **Composables**: `use*` prefix, keep in `src/composables/`.
|
|
338
|
-
- **Pages**: filename = route path. `index.vue`
|
|
338
|
+
- **Pages**: filename = route path. `index.vue` -> `/`, `settings.vue` -> `/settings`, `[id].vue` -> param.
|
|
339
339
|
- **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.
|
|
340
340
|
- **Comments**: explain *why*, not *what*. Skip TODOs in committed code.
|
|
341
341
|
- **No emojis** in source unless the user requests them.
|
|
@@ -345,7 +345,7 @@ Tauri intercepts native drag-drop by default. The Construct host sets `dragDropE
|
|
|
345
345
|
## Build pipeline reminder
|
|
346
346
|
|
|
347
347
|
`construct build` writes:
|
|
348
|
-
- `dist/space-{{.ID}}.iife.js`
|
|
349
|
-
- `dist/manifest.json`
|
|
348
|
+
- `dist/space-{{.ID}}.iife.js` -- the bundled space
|
|
349
|
+
- `dist/manifest.json` -- manifest + `build` block (checksum, size, **`hostApiVersion`**, builtAt)
|
|
350
350
|
|
|
351
351
|
The runtime SpaceLoader compares `hostApiVersion` to its own; mismatches log a warning. Bump the CLI to keep them aligned.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Space entry
|
|
1
|
+
// Space entry -- exports pages, widgets, and actions for the host loader.
|
|
2
2
|
// `construct dev` regenerates this from space.manifest.json on changes.
|
|
3
3
|
import './style.css'
|
|
4
4
|
import IndexPage from './pages/index.vue'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Space entry
|
|
1
|
+
// Space entry -- exports pages, widgets, and actions for the host loader.
|
|
2
2
|
// `construct dev` regenerates this from space.manifest.json on changes.
|
|
3
3
|
import './style.css'
|
|
4
4
|
import IndexPage from './pages/index.vue'
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# {{.DisplayName}}
|
|
1
|
+
# {{.DisplayName}} -- data
|
|
2
2
|
|
|
3
3
|
Reference doc for the agent when working with {{.DisplayName}} data.
|
|
4
4
|
|
|
5
5
|
## Storage model
|
|
6
6
|
|
|
7
|
-
Describe where {{.DisplayName}} data lives (Graph models, local storage, server API
|
|
7
|
+
Describe where {{.DisplayName}} data lives (Graph models, local storage, server API...). Edit this section to match the actual model.
|
|
8
8
|
|
|
9
9
|
## CRUD conventions
|
|
10
10
|
|
|
11
11
|
- Validate inputs before writing
|
|
12
12
|
- Prefer the space's actions API over raw file or DB writes
|
|
13
13
|
- Batch bulk changes when possible
|
|
14
|
-
- Read existing rows before modifying
|
|
14
|
+
- Read existing rows before modifying -- changes should be additive when in doubt
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# {{.DisplayName}}
|
|
1
|
+
# {{.DisplayName}} -- UI
|
|
2
2
|
|
|
3
3
|
Reference doc for the agent when adjusting {{.DisplayName}}'s UI.
|
|
4
4
|
|
|
@@ -12,8 +12,8 @@ Use Construct CSS variables instead of hardcoded values:
|
|
|
12
12
|
|
|
13
13
|
## Layout
|
|
14
14
|
|
|
15
|
-
- Live inside the host shell
|
|
16
|
-
- Use the `@construct-space/ui` primitives (`Card`, `Button`, `Badge`,
|
|
15
|
+
- Live inside the host shell -- avoid full-bleed layouts that conflict with the sidebar/header
|
|
16
|
+
- Use the `@construct-space/ui` primitives (`Card`, `Button`, `Badge`, ...) for consistency
|
|
17
17
|
- Prefer flex/grid over absolute positioning
|
|
18
18
|
|
|
19
19
|
## Accessibility
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* {{.DisplayName}}
|
|
3
|
+
* {{.DisplayName}} -- Home page
|
|
4
4
|
*
|
|
5
5
|
* Built-in libraries available at runtime (do not bundle):
|
|
6
|
-
* @construct-space/ui
|
|
7
|
-
* @construct-space/sdk
|
|
8
|
-
* @construct-space/graph
|
|
9
|
-
* lucide-vue-next
|
|
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
|
|
10
10
|
*/
|
|
11
11
|
import { ref } from 'vue'
|
|
12
12
|
import { Button, Card, Empty } from '@construct-space/ui'
|
|
@@ -26,6 +26,6 @@ Help the user with tasks inside the {{.DisplayName}} space.
|
|
|
26
26
|
|
|
27
27
|
The agent loads these on demand:
|
|
28
28
|
|
|
29
|
-
- `references/`
|
|
30
|
-
- `scripts/`
|
|
31
|
-
- `assets/`
|
|
29
|
+
- `references/` -- domain-specific docs (data model, UI conventions, ...)
|
|
30
|
+
- `scripts/` -- supporting executables
|
|
31
|
+
- `assets/` -- templates and static resources
|
|
@@ -4,19 +4,19 @@ import vue from '@vitejs/plugin-vue'
|
|
|
4
4
|
import { resolve } from 'path'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Host-provided externals
|
|
7
|
+
* Host-provided externals -- these are supplied by Construct at runtime
|
|
8
8
|
* via window.__CONSTRUCT__. Do NOT bundle them; they must stay external.
|
|
9
9
|
*
|
|
10
10
|
* Source of truth: construct-app/frontend/lib/spaceHost.ts
|
|
11
11
|
*
|
|
12
|
-
* IMPORTANT
|
|
12
|
+
* IMPORTANT -- only add a package here AFTER confirming spaceHost.ts
|
|
13
13
|
* actually puts it on window.__CONSTRUCT__. Externalising a package the
|
|
14
14
|
* host doesn't expose silently substitutes `undefined`; you'll see
|
|
15
15
|
* "X is not a function" at runtime. When in doubt, bundle it (omit from
|
|
16
|
-
* this list)
|
|
16
|
+
* this list) -- bundle bloat is cheaper than a broken space.
|
|
17
17
|
*
|
|
18
18
|
* Notably NOT externalised:
|
|
19
|
-
* @construct-space/graph
|
|
19
|
+
* @construct-space/graph -- host does not expose this. Bundle it.
|
|
20
20
|
*/
|
|
21
21
|
const hostExternals = [
|
|
22
22
|
'vue',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* {{.DisplayName}} Summary Widget
|
|
3
|
+
* {{.DisplayName}} Summary Widget -- 2x1 compact view
|
|
4
4
|
*
|
|
5
5
|
* Widgets run inside a closed Shadow DOM sandbox.
|
|
6
|
-
* Use the injected widgetApi for theme and actions
|
|
6
|
+
* Use the injected widgetApi for theme and actions -- do not access
|
|
7
7
|
* window, document, or global stores directly.
|
|
8
8
|
*/
|
|
9
9
|
import { inject } from 'vue'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* {{.DisplayName}} Summary Widget
|
|
3
|
+
* {{.DisplayName}} Summary Widget -- 4x1 wide view
|
|
4
4
|
*
|
|
5
5
|
* Widgets run inside a closed Shadow DOM sandbox.
|
|
6
|
-
* Use the injected widgetApi for theme and actions
|
|
6
|
+
* Use the injected widgetApi for theme and actions -- do not access
|
|
7
7
|
* window, document, or global stores directly.
|
|
8
8
|
*/
|
|
9
9
|
import { inject } from 'vue'
|