@construct-space/cli 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/index.js +55 -11
- package/dist/templates/space/actions.ts.tmpl +20 -29
- package/dist/templates/space/construct.md.tmpl +238 -0
- package/dist/templates/space/eslint.config.js.tmpl +26 -0
- package/dist/templates/space/index.vue.tmpl +22 -7
- package/dist/templates/space/package.json.tmpl +9 -0
- package/package.json +1 -1
- package/templates/space/actions.ts.tmpl +20 -29
- package/templates/space/construct.md.tmpl +238 -0
- package/templates/space/eslint.config.js.tmpl +26 -0
- package/templates/space/index.vue.tmpl +22 -7
- package/templates/space/package.json.tmpl +9 -0
package/README.md
CHANGED
|
@@ -23,6 +23,25 @@ construct check # Type-check + lint
|
|
|
23
23
|
construct clean # Remove build artifacts
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
### Built-in libraries
|
|
27
|
+
|
|
28
|
+
Scaffolded spaces ship with the full Construct stack pre-wired — import freely, the host resolves them at runtime (never bundled):
|
|
29
|
+
|
|
30
|
+
| Package | Purpose |
|
|
31
|
+
|---------|---------|
|
|
32
|
+
| `@construct-space/ui` | Vue 3 components — `Button`, `Card`, `Modal`, `Table`, `Badge`, `Avatar`, `ToggleGroup`, `ConfirmationModal`, ... |
|
|
33
|
+
| `@construct-space/sdk` | Host composables — `useOrg`, `useOrgMembers`, `useToast`, `useAuth` |
|
|
34
|
+
| `@construct-space/graph` | Multi-tenant data layer — `defineModel`, `useGraph`, scopes, access rules |
|
|
35
|
+
| `lucide-vue-next` | Icon set |
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Button, Card, Modal } from '@construct-space/ui'
|
|
39
|
+
import { useOrg, useOrgMembers } from '@construct-space/sdk'
|
|
40
|
+
import { defineModel, field, useGraph } from '@construct-space/graph'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The scaffolded `src/pages/index.vue` and `src/actions.ts` show the recommended patterns. Use `construct check` (typecheck + ESLint flat config) before commit.
|
|
44
|
+
|
|
26
45
|
### Graph (Data Models)
|
|
27
46
|
|
|
28
47
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -2887,14 +2887,47 @@ function store(creds) {
|
|
|
2887
2887
|
}
|
|
2888
2888
|
function load2() {
|
|
2889
2889
|
const path = credentialsPath();
|
|
2890
|
-
if (
|
|
2891
|
-
|
|
2890
|
+
if (existsSync10(path)) {
|
|
2891
|
+
const data = JSON.parse(readFileSync7(path, "utf-8"));
|
|
2892
|
+
if (data.token)
|
|
2893
|
+
return data;
|
|
2892
2894
|
}
|
|
2893
|
-
const
|
|
2894
|
-
if (
|
|
2895
|
-
|
|
2895
|
+
const fromProfile = loadFromActiveProfile();
|
|
2896
|
+
if (fromProfile)
|
|
2897
|
+
return fromProfile;
|
|
2898
|
+
throw new Error("not logged in \u2014 run 'construct login' first");
|
|
2899
|
+
}
|
|
2900
|
+
function loadFromActiveProfile() {
|
|
2901
|
+
try {
|
|
2902
|
+
const regPath = join12(dataDir(), "profiles.json");
|
|
2903
|
+
if (!existsSync10(regPath))
|
|
2904
|
+
return null;
|
|
2905
|
+
const reg = JSON.parse(readFileSync7(regPath, "utf-8"));
|
|
2906
|
+
const activeId = reg.active_profile;
|
|
2907
|
+
if (!activeId)
|
|
2908
|
+
return null;
|
|
2909
|
+
const authPath = join12(profilesDir(), activeId, "auth.json");
|
|
2910
|
+
if (!existsSync10(authPath))
|
|
2911
|
+
return null;
|
|
2912
|
+
const a = JSON.parse(readFileSync7(authPath, "utf-8"));
|
|
2913
|
+
if (!a.token)
|
|
2914
|
+
return null;
|
|
2915
|
+
if (a.authenticated === false)
|
|
2916
|
+
return null;
|
|
2917
|
+
const u = a.user || {};
|
|
2918
|
+
return {
|
|
2919
|
+
token: a.token,
|
|
2920
|
+
portal: DEFAULT_PORTAL,
|
|
2921
|
+
profileId: activeId,
|
|
2922
|
+
user: {
|
|
2923
|
+
id: u.id || u.uuid || activeId,
|
|
2924
|
+
name: u.name || u.username || activeId,
|
|
2925
|
+
email: u.email || ""
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
} catch {
|
|
2929
|
+
return null;
|
|
2896
2930
|
}
|
|
2897
|
-
return data;
|
|
2898
2931
|
}
|
|
2899
2932
|
function isAuthenticated() {
|
|
2900
2933
|
try {
|
|
@@ -5280,8 +5313,10 @@ async function scaffold(nameArg, options) {
|
|
|
5280
5313
|
"safety.json.tmpl": join2(name, "agent", "hooks", "safety.json"),
|
|
5281
5314
|
"build.yml.tmpl": join2(name, ".github", "workflows", "build.yml"),
|
|
5282
5315
|
"tsconfig.json.tmpl": join2(name, "tsconfig.json"),
|
|
5316
|
+
"eslint.config.js.tmpl": join2(name, "eslint.config.js"),
|
|
5283
5317
|
"gitignore.tmpl": join2(name, ".gitignore"),
|
|
5284
5318
|
"readme.md.tmpl": join2(name, "README.md"),
|
|
5319
|
+
"construct.md.tmpl": join2(name, "CONSTRUCT.md"),
|
|
5285
5320
|
"widgets/2x1.vue.tmpl": join2(name, "widgets", "summary", "2x1.vue"),
|
|
5286
5321
|
"widgets/4x1.vue.tmpl": join2(name, "widgets", "summary", "4x1.vue"),
|
|
5287
5322
|
"actions.ts.tmpl": join2(name, "src", "actions.ts")
|
|
@@ -7956,6 +7991,7 @@ function ora(options) {
|
|
|
7956
7991
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
7957
7992
|
import { join as join3 } from "path";
|
|
7958
7993
|
var MANIFEST_FILE = "space.manifest.json";
|
|
7994
|
+
var HOST_API_VERSION = "0.5.0";
|
|
7959
7995
|
var idRegex = /^[a-z][a-z0-9-]*$/;
|
|
7960
7996
|
var versionRegex = /^\d+\.\d+\.\d+/;
|
|
7961
7997
|
function validate2(m) {
|
|
@@ -8301,7 +8337,7 @@ async function build(options) {
|
|
|
8301
8337
|
writeWithBuild(distDir, raw, {
|
|
8302
8338
|
checksum,
|
|
8303
8339
|
size: bundleData.length,
|
|
8304
|
-
hostApiVersion:
|
|
8340
|
+
hostApiVersion: HOST_API_VERSION,
|
|
8305
8341
|
builtAt: new Date().toISOString()
|
|
8306
8342
|
});
|
|
8307
8343
|
console.log(source_default.green(`Built ${m.name} v${m.version}`));
|
|
@@ -9983,7 +10019,7 @@ async function dev() {
|
|
|
9983
10019
|
writeWithBuild(distDir, raw, {
|
|
9984
10020
|
checksum,
|
|
9985
10021
|
size: bundleData.length,
|
|
9986
|
-
hostApiVersion:
|
|
10022
|
+
hostApiVersion: HOST_API_VERSION,
|
|
9987
10023
|
builtAt: new Date().toISOString()
|
|
9988
10024
|
});
|
|
9989
10025
|
console.log(source_default.green(`Built \u2192 dist/ (${(bundleData.length / 1024).toFixed(1)} KB)`));
|
|
@@ -10966,9 +11002,17 @@ function parseModelFile(content, fileName) {
|
|
|
10966
11002
|
const [, op, level] = accessMatch;
|
|
10967
11003
|
accessRules[op] = level;
|
|
10968
11004
|
}
|
|
11005
|
+
let scope;
|
|
11006
|
+
const scopeMatch = content.match(/scope\s*:\s*['"](app|project|org)['"]/);
|
|
11007
|
+
if (scopeMatch)
|
|
11008
|
+
scope = scopeMatch[1];
|
|
10969
11009
|
const result = { name: modelName, fields };
|
|
10970
|
-
if (Object.keys(accessRules).length > 0) {
|
|
10971
|
-
result.options = {
|
|
11010
|
+
if (Object.keys(accessRules).length > 0 || scope) {
|
|
11011
|
+
result.options = {};
|
|
11012
|
+
if (Object.keys(accessRules).length > 0)
|
|
11013
|
+
result.options.access = accessRules;
|
|
11014
|
+
if (scope)
|
|
11015
|
+
result.options.scope = scope;
|
|
10972
11016
|
}
|
|
10973
11017
|
return result;
|
|
10974
11018
|
}
|
|
@@ -11121,7 +11165,7 @@ function parseModelFields(content, fileName) {
|
|
|
11121
11165
|
// package.json
|
|
11122
11166
|
var package_default = {
|
|
11123
11167
|
name: "@construct-space/cli",
|
|
11124
|
-
version: "1.
|
|
11168
|
+
version: "1.6.0",
|
|
11125
11169
|
description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
|
|
11126
11170
|
type: "module",
|
|
11127
11171
|
bin: {
|
|
@@ -1,35 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Space Actions — exposed to the AI agent via space_run_action
|
|
2
|
+
* Space Actions — exposed to the AI agent via space_run_action.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Each action: { description, params, run }.
|
|
5
|
+
* - `params` describe agent-callable inputs (type, description, required).
|
|
6
|
+
* - `run` receives the payload and returns any JSON-serialisable value.
|
|
7
|
+
*
|
|
8
|
+
* Pair with @construct-space/graph for typed multi-tenant data:
|
|
9
|
+
* import { useGraph } from '@construct-space/graph'
|
|
10
|
+
* import { Item } from './models'
|
|
11
|
+
* const items = useGraph(Item)
|
|
12
|
+
*
|
|
13
|
+
* listItems: {
|
|
14
|
+
* description: 'List items',
|
|
15
|
+
* params: { limit: { type: 'number', required: false } },
|
|
16
|
+
* run: async (p: any) => ({ items: await items.find({ limit: p.limit ?? 50 }) }),
|
|
17
|
+
* }
|
|
6
18
|
*/
|
|
7
19
|
|
|
8
|
-
// --- Graph SDK (data layer) ---
|
|
9
|
-
// Uncomment after running `construct graph init` and defining models:
|
|
10
|
-
//
|
|
11
|
-
// import { useGraph } from '@construct-space/graph'
|
|
12
|
-
// const graph = useGraph()
|
|
13
|
-
//
|
|
14
|
-
// Example action using Graph:
|
|
15
|
-
// fetchItems: {
|
|
16
|
-
// description: 'Fetch items from the graph',
|
|
17
|
-
// params: {
|
|
18
|
-
// limit: { type: 'number', description: 'Max items to return', required: false },
|
|
19
|
-
// },
|
|
20
|
-
// run: async (p: any) => {
|
|
21
|
-
// const items = await graph.query('Item').limit(p.limit ?? 10).exec()
|
|
22
|
-
// return { items }
|
|
23
|
-
// },
|
|
24
|
-
// },
|
|
25
|
-
|
|
26
20
|
export const actions = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// },
|
|
33
|
-
// run: (p: any) => ({ message: `Hello ${p.name}!` }),
|
|
34
|
-
// },
|
|
21
|
+
ping: {
|
|
22
|
+
description: 'Health check — returns pong with the space id.',
|
|
23
|
+
params: {},
|
|
24
|
+
run: () => ({ pong: true, space: '{{.ID}}' }),
|
|
25
|
+
},
|
|
35
26
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: construct-stack
|
|
3
|
+
name: Construct Stack Guide
|
|
4
|
+
description: How to build and extend a Construct space — CLI, UI, SDK, Graph, manifest, agent actions
|
|
5
|
+
trigger: construct|space|graph|ui|sdk|manifest|action
|
|
6
|
+
category: skill
|
|
7
|
+
tools: [read_file, list_dir, glob, grep, write_file, edit_file]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Construct.md — agent guide for `{{.DisplayName}}`
|
|
11
|
+
|
|
12
|
+
This file briefs an AI coding agent on the Construct stack so it can build, extend, and ship this space without external lookups.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Stack at a glance
|
|
17
|
+
|
|
18
|
+
| Layer | Package | Purpose |
|
|
19
|
+
|-------|---------|---------|
|
|
20
|
+
| UI | `@construct-space/ui` | Vue 3 component library |
|
|
21
|
+
| Host | `@construct-space/sdk` | App composables (org, members, toast, auth) |
|
|
22
|
+
| Data | `@construct-space/graph` | Multi-tenant typed data layer |
|
|
23
|
+
| Icons | `lucide-vue-next` | Icon set |
|
|
24
|
+
| Tooling | `@construct-space/cli` | Build / dev / publish |
|
|
25
|
+
|
|
26
|
+
All four are **host-provided externals** — import normally, never bundle. Vite externalises them via `vite.config.ts`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Layout
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{{.Name}}/
|
|
34
|
+
space.manifest.json scope, pages, toolbar, agent, widgets, actions
|
|
35
|
+
src/
|
|
36
|
+
entry.ts auto-generated; do not hand-edit
|
|
37
|
+
actions.ts agent-callable actions (space_run_action)
|
|
38
|
+
pages/ filesystem routing (e.g. settings.vue → /settings)
|
|
39
|
+
components/ local Vue components
|
|
40
|
+
composables/ local composables (use* convention)
|
|
41
|
+
models/ graph models (defineModel)
|
|
42
|
+
agent/
|
|
43
|
+
config.md agent system prompt, tool whitelist, max iterations
|
|
44
|
+
skills/ scoped skills loaded with the agent
|
|
45
|
+
hooks/safety.json tool-call interception rules
|
|
46
|
+
widgets/ home-screen widgets (run inside Shadow DOM sandbox)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
construct dev hot-reload dev (regenerates entry.ts on change)
|
|
55
|
+
construct build production build → dist/
|
|
56
|
+
construct check manifest + typecheck + lint
|
|
57
|
+
construct install install to active profile
|
|
58
|
+
construct publish publish to registry (requires auth)
|
|
59
|
+
construct graph push register models with graph backend
|
|
60
|
+
construct graph install <id> install a published space for current org
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Manifest essentials
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
{
|
|
69
|
+
"id": "{{.ID}}",
|
|
70
|
+
"scope": "app | project | org", // org = multi-tenant per organization
|
|
71
|
+
"minConstructVersion": "0.7.0",
|
|
72
|
+
"pages": [{ "path": "", "label": "Home", "default": true }],
|
|
73
|
+
"toolbar": [{ "id": "...", "icon": "...", "action": "createX" }],
|
|
74
|
+
"agent": "agent/config.md",
|
|
75
|
+
"skills": ["agent/skills/default.md"],
|
|
76
|
+
"actions": "src/actions.ts",
|
|
77
|
+
"widgets": [{ "id": "summary", "defaultSize": "4x1", "sizes": {...} }]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`scope: 'org'` makes everything multi-tenant — graph schemas auto-partition per org. Use `useOrg()` to read current org context.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `@construct-space/ui` — component cheatsheet
|
|
86
|
+
|
|
87
|
+
All exported from the package root:
|
|
88
|
+
|
|
89
|
+
**Layout**: `Card` (slots: header/accessory/default/footer/footer-end; variants: default/outline/muted; modifiers: hoverable/interactive), `SplitPane`, `ScrollArea`, `Separator`, `PanelSection`, `DashboardPanel`
|
|
90
|
+
|
|
91
|
+
**Inputs**: `Input`, `Textarea`, `Select` (options: `{label, value}[]`), `MultiSelect`, `Checkbox`, `Switch`, `RadioGroup`, `DatePicker`, `ColorPicker`, `FileInput`, `Slider`, `Autocomplete`, `FormField` (label/hint wrapper)
|
|
92
|
+
|
|
93
|
+
**Display**: `Badge` (color: primary|neutral|success|warning|error|info; variant: solid|soft|subtle|outline), `Chip`, `Avatar` (`fallback` shows text — pass initials), `Empty`, `Skeleton`, `Progress`, `Tooltip`, `Kbd`, `Alert`, `Notification`/`Toast`
|
|
94
|
+
|
|
95
|
+
**Actions**: `Button` (variant: solid|ghost|outline; color: primary|neutral|error|...; size: xs|sm|md|lg), `Dropdown`, `DropdownMenu`, `ContextMenu`, `Popover`, `ToggleGroup` (segmented control)
|
|
96
|
+
|
|
97
|
+
**Overlays**: `Modal` (`v-model:open`, slots: header/body/footer), `Slideover`, `Drawer`, `ConfirmationModal` (`@confirm`, `@cancel`), `Pagination`
|
|
98
|
+
|
|
99
|
+
**Data**: `Table` (`columns: TableColumn[]`, `rows`, `selectable`, `striped`, `stickyHeader`, slots: `cell-{key}`, `header-{key}`, `empty`), `Tree`, `Timeline`, `Tabs`, `Tab`, `Accordion`, `Calendar`, `Breadcrumbs`
|
|
100
|
+
|
|
101
|
+
**Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
|
|
102
|
+
|
|
103
|
+
```vue
|
|
104
|
+
<script setup lang="ts">
|
|
105
|
+
import { Card, Button, Modal, Badge } from '@construct-space/ui'
|
|
106
|
+
const open = ref(false)
|
|
107
|
+
</script>
|
|
108
|
+
<template>
|
|
109
|
+
<Card title="Hello" hoverable>
|
|
110
|
+
<Badge color="success" variant="subtle" label="Live" />
|
|
111
|
+
<template #footer-end>
|
|
112
|
+
<Button variant="solid" size="sm" @click="open = true">Open</Button>
|
|
113
|
+
</template>
|
|
114
|
+
</Card>
|
|
115
|
+
<Modal v-model:open="open" title="Demo">…</Modal>
|
|
116
|
+
</template>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## `@construct-space/sdk` — host composables
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { useOrg, useOrgMembers, useToast, useAuth } from '@construct-space/sdk'
|
|
125
|
+
|
|
126
|
+
const { orgId, isOrg, org } = useOrg() // current org context (reactive)
|
|
127
|
+
const { members } = useOrgMembers() // [{ user_id, name, email, role }, ...]
|
|
128
|
+
const toast = useToast() // toast.success(...), toast.error(...)
|
|
129
|
+
const { user, isAuthenticated, logout } = useAuth()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use these for org-aware UX (assignee pickers, "shared with N" badges, avatars, member lookups).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## `@construct-space/graph` — data layer
|
|
137
|
+
|
|
138
|
+
Define typed models, get a typed reactive client, multi-tenant by default.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// src/models/Task.ts
|
|
142
|
+
import { defineModel, field, access } from '@construct-space/graph'
|
|
143
|
+
|
|
144
|
+
export const Task = defineModel('task', {
|
|
145
|
+
title: field.string().required(),
|
|
146
|
+
description: field.string(),
|
|
147
|
+
priority: field.string(),
|
|
148
|
+
due_date: field.date(),
|
|
149
|
+
position: field.int(),
|
|
150
|
+
labels: field.json(),
|
|
151
|
+
assignee_id: field.string().index(),
|
|
152
|
+
}, {
|
|
153
|
+
scope: 'org', // partitions schema per organization
|
|
154
|
+
access: {
|
|
155
|
+
read: access.authenticated(),
|
|
156
|
+
create: access.authenticated(),
|
|
157
|
+
update: access.authenticated(),
|
|
158
|
+
delete: access.owner(),
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// src/composables/useTasks.ts
|
|
165
|
+
import { useGraph } from '@construct-space/graph'
|
|
166
|
+
import { Task } from '../models'
|
|
167
|
+
|
|
168
|
+
const tasks = useGraph(Task)
|
|
169
|
+
await tasks.find({ where: { priority: 'high' }, orderBy: { position: 'asc' }, limit: 50 })
|
|
170
|
+
await tasks.create({ title: 'Ship', position: 0 })
|
|
171
|
+
await tasks.update(id, { position: 1 })
|
|
172
|
+
await tasks.remove(id)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Field types**: `string`, `int`, `number`, `boolean`, `date`, `json`, `enum([...])`. Chain `.required()`, `.index()`, `.unique()`, `.default(v)`.
|
|
176
|
+
|
|
177
|
+
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context — use `authenticated()` for org-scoped models that allow any logged-in member.
|
|
178
|
+
|
|
179
|
+
**Scopes**:
|
|
180
|
+
- `app` — single shared schema (rare)
|
|
181
|
+
- `org` — partitioned by organization (most product spaces)
|
|
182
|
+
- `project` — partitioned by project (dev-tooling spaces)
|
|
183
|
+
|
|
184
|
+
**Push models to backend after editing**: `construct graph push`.
|
|
185
|
+
|
|
186
|
+
### Pitfalls
|
|
187
|
+
|
|
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`.
|
|
189
|
+
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
|
|
190
|
+
- **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Actions (agent surface)
|
|
195
|
+
|
|
196
|
+
`src/actions.ts` exports an `actions` object — each entry becomes an agent-callable tool via `space_run_action`.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
export const actions = {
|
|
200
|
+
createTask: {
|
|
201
|
+
description: 'Create a task',
|
|
202
|
+
params: {
|
|
203
|
+
title: { type: 'string', required: true },
|
|
204
|
+
priority: { type: 'string', required: false },
|
|
205
|
+
},
|
|
206
|
+
run: async (p: any) => ({ task: await tasks.create(p) }),
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The agent (configured in `agent/config.md`) calls `space_list_actions` then `space_run_action`. Keep descriptions tight and specific — they're the only docstring the agent sees.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Drag and drop (HTML5)
|
|
216
|
+
|
|
217
|
+
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.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Conventions
|
|
222
|
+
|
|
223
|
+
- **Imports**: `@construct-space/*` packages always external. Local imports use relative paths (no `@/` alias unless you add one).
|
|
224
|
+
- **Composables**: `use*` prefix, keep in `src/composables/`.
|
|
225
|
+
- **Pages**: filename = route path. `index.vue` → `/`, `settings.vue` → `/settings`, `[id].vue` → param.
|
|
226
|
+
- **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.
|
|
227
|
+
- **Comments**: explain *why*, not *what*. Skip TODOs in committed code.
|
|
228
|
+
- **No emojis** in source unless the user requests them.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Build pipeline reminder
|
|
233
|
+
|
|
234
|
+
`construct build` writes:
|
|
235
|
+
- `dist/space-{{.ID}}.iife.js` — the bundled space
|
|
236
|
+
- `dist/manifest.json` — manifest + `build` block (checksum, size, **`hostApiVersion`**, builtAt)
|
|
237
|
+
|
|
238
|
+
The runtime SpaceLoader compares `hostApiVersion` to its own; mismatches log a warning. Bump the CLI to keep them aligned.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
languageOptions: {
|
|
7
|
+
ecmaVersion: 'latest',
|
|
8
|
+
sourceType: 'module',
|
|
9
|
+
globals: {
|
|
10
|
+
console: 'readonly',
|
|
11
|
+
window: 'readonly',
|
|
12
|
+
document: 'readonly',
|
|
13
|
+
globalThis: 'readonly',
|
|
14
|
+
setTimeout: 'readonly',
|
|
15
|
+
clearTimeout: 'readonly',
|
|
16
|
+
history: 'readonly',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
rules: {
|
|
20
|
+
'no-unused-vars': 'off',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
ignores: ['dist/', 'node_modules/', '*.d.ts'],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* {{.DisplayName}} — Home page
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Built-in libraries available at runtime (do not bundle):
|
|
6
|
+
* @construct-space/ui — Vue 3 components (Button, Card, Modal, Table, Badge, ...)
|
|
7
|
+
* @construct-space/sdk — useOrg, useOrgMembers, useToast, useAuth
|
|
8
|
+
* @construct-space/graph — defineModel, useGraph (multi-tenant data layer)
|
|
9
|
+
* lucide-vue-next — icons
|
|
7
10
|
*/
|
|
8
11
|
import { ref } from 'vue'
|
|
12
|
+
import { Button, Card, Empty } from '@construct-space/ui'
|
|
13
|
+
import { Sparkles } from 'lucide-vue-next'
|
|
9
14
|
|
|
10
15
|
const greeting = ref('Your space is ready. Start building!')
|
|
11
16
|
</script>
|
|
12
17
|
|
|
13
18
|
<template>
|
|
14
|
-
<div class="h-full flex items-center justify-center">
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<div class="h-full flex items-center justify-center p-6">
|
|
20
|
+
<Card variant="default" class="max-w-md w-full text-center">
|
|
21
|
+
<template #header>
|
|
22
|
+
<div class="flex items-center justify-center gap-2 w-full">
|
|
23
|
+
<Sparkles class="size-5 text-[var(--app-accent)]" />
|
|
24
|
+
<h1 class="text-lg font-semibold text-[var(--app-foreground)]">{{.DisplayName}}</h1>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<Empty :description="greeting" size="sm" />
|
|
29
|
+
|
|
30
|
+
<template #footer-end>
|
|
31
|
+
<Button variant="solid" size="sm" label="Get Started" />
|
|
32
|
+
</template>
|
|
33
|
+
</Card>
|
|
19
34
|
</div>
|
|
20
35
|
</template>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"build": "construct build",
|
|
9
9
|
"dev": "construct dev",
|
|
10
10
|
"check": "construct check",
|
|
11
|
+
"lint": "eslint .",
|
|
11
12
|
"validate": "construct validate"
|
|
12
13
|
},
|
|
13
14
|
"peerDependencies": {
|
|
@@ -18,10 +19,18 @@
|
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@construct-space/cli": "latest",
|
|
20
21
|
"@construct-space/sdk": "latest",
|
|
22
|
+
"@eslint/js": "^10.0.1",
|
|
21
23
|
"@vitejs/plugin-vue": "^5.2.3",
|
|
24
|
+
"eslint": "^10.2.1",
|
|
25
|
+
"eslint-plugin-vue": "^10.0.0",
|
|
22
26
|
"lucide-vue-next": "^1.0.0",
|
|
23
27
|
"typescript": "^5.9.3",
|
|
28
|
+
"typescript-eslint": "^9.0.0",
|
|
24
29
|
"vite": "^6.3.5",
|
|
25
30
|
"vue-tsc": "^3.2.5"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@construct-space/graph": "^0.5.0",
|
|
34
|
+
"@construct-space/ui": "^0.5.2"
|
|
26
35
|
}
|
|
27
36
|
}
|
package/package.json
CHANGED
|
@@ -1,35 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Space Actions — exposed to the AI agent via space_run_action
|
|
2
|
+
* Space Actions — exposed to the AI agent via space_run_action.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Each action: { description, params, run }.
|
|
5
|
+
* - `params` describe agent-callable inputs (type, description, required).
|
|
6
|
+
* - `run` receives the payload and returns any JSON-serialisable value.
|
|
7
|
+
*
|
|
8
|
+
* Pair with @construct-space/graph for typed multi-tenant data:
|
|
9
|
+
* import { useGraph } from '@construct-space/graph'
|
|
10
|
+
* import { Item } from './models'
|
|
11
|
+
* const items = useGraph(Item)
|
|
12
|
+
*
|
|
13
|
+
* listItems: {
|
|
14
|
+
* description: 'List items',
|
|
15
|
+
* params: { limit: { type: 'number', required: false } },
|
|
16
|
+
* run: async (p: any) => ({ items: await items.find({ limit: p.limit ?? 50 }) }),
|
|
17
|
+
* }
|
|
6
18
|
*/
|
|
7
19
|
|
|
8
|
-
// --- Graph SDK (data layer) ---
|
|
9
|
-
// Uncomment after running `construct graph init` and defining models:
|
|
10
|
-
//
|
|
11
|
-
// import { useGraph } from '@construct-space/graph'
|
|
12
|
-
// const graph = useGraph()
|
|
13
|
-
//
|
|
14
|
-
// Example action using Graph:
|
|
15
|
-
// fetchItems: {
|
|
16
|
-
// description: 'Fetch items from the graph',
|
|
17
|
-
// params: {
|
|
18
|
-
// limit: { type: 'number', description: 'Max items to return', required: false },
|
|
19
|
-
// },
|
|
20
|
-
// run: async (p: any) => {
|
|
21
|
-
// const items = await graph.query('Item').limit(p.limit ?? 10).exec()
|
|
22
|
-
// return { items }
|
|
23
|
-
// },
|
|
24
|
-
// },
|
|
25
|
-
|
|
26
20
|
export const actions = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// },
|
|
33
|
-
// run: (p: any) => ({ message: `Hello ${p.name}!` }),
|
|
34
|
-
// },
|
|
21
|
+
ping: {
|
|
22
|
+
description: 'Health check — returns pong with the space id.',
|
|
23
|
+
params: {},
|
|
24
|
+
run: () => ({ pong: true, space: '{{.ID}}' }),
|
|
25
|
+
},
|
|
35
26
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: construct-stack
|
|
3
|
+
name: Construct Stack Guide
|
|
4
|
+
description: How to build and extend a Construct space — CLI, UI, SDK, Graph, manifest, agent actions
|
|
5
|
+
trigger: construct|space|graph|ui|sdk|manifest|action
|
|
6
|
+
category: skill
|
|
7
|
+
tools: [read_file, list_dir, glob, grep, write_file, edit_file]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Construct.md — agent guide for `{{.DisplayName}}`
|
|
11
|
+
|
|
12
|
+
This file briefs an AI coding agent on the Construct stack so it can build, extend, and ship this space without external lookups.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Stack at a glance
|
|
17
|
+
|
|
18
|
+
| Layer | Package | Purpose |
|
|
19
|
+
|-------|---------|---------|
|
|
20
|
+
| UI | `@construct-space/ui` | Vue 3 component library |
|
|
21
|
+
| Host | `@construct-space/sdk` | App composables (org, members, toast, auth) |
|
|
22
|
+
| Data | `@construct-space/graph` | Multi-tenant typed data layer |
|
|
23
|
+
| Icons | `lucide-vue-next` | Icon set |
|
|
24
|
+
| Tooling | `@construct-space/cli` | Build / dev / publish |
|
|
25
|
+
|
|
26
|
+
All four are **host-provided externals** — import normally, never bundle. Vite externalises them via `vite.config.ts`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Layout
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{{.Name}}/
|
|
34
|
+
space.manifest.json scope, pages, toolbar, agent, widgets, actions
|
|
35
|
+
src/
|
|
36
|
+
entry.ts auto-generated; do not hand-edit
|
|
37
|
+
actions.ts agent-callable actions (space_run_action)
|
|
38
|
+
pages/ filesystem routing (e.g. settings.vue → /settings)
|
|
39
|
+
components/ local Vue components
|
|
40
|
+
composables/ local composables (use* convention)
|
|
41
|
+
models/ graph models (defineModel)
|
|
42
|
+
agent/
|
|
43
|
+
config.md agent system prompt, tool whitelist, max iterations
|
|
44
|
+
skills/ scoped skills loaded with the agent
|
|
45
|
+
hooks/safety.json tool-call interception rules
|
|
46
|
+
widgets/ home-screen widgets (run inside Shadow DOM sandbox)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
construct dev hot-reload dev (regenerates entry.ts on change)
|
|
55
|
+
construct build production build → dist/
|
|
56
|
+
construct check manifest + typecheck + lint
|
|
57
|
+
construct install install to active profile
|
|
58
|
+
construct publish publish to registry (requires auth)
|
|
59
|
+
construct graph push register models with graph backend
|
|
60
|
+
construct graph install <id> install a published space for current org
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Manifest essentials
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
{
|
|
69
|
+
"id": "{{.ID}}",
|
|
70
|
+
"scope": "app | project | org", // org = multi-tenant per organization
|
|
71
|
+
"minConstructVersion": "0.7.0",
|
|
72
|
+
"pages": [{ "path": "", "label": "Home", "default": true }],
|
|
73
|
+
"toolbar": [{ "id": "...", "icon": "...", "action": "createX" }],
|
|
74
|
+
"agent": "agent/config.md",
|
|
75
|
+
"skills": ["agent/skills/default.md"],
|
|
76
|
+
"actions": "src/actions.ts",
|
|
77
|
+
"widgets": [{ "id": "summary", "defaultSize": "4x1", "sizes": {...} }]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`scope: 'org'` makes everything multi-tenant — graph schemas auto-partition per org. Use `useOrg()` to read current org context.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `@construct-space/ui` — component cheatsheet
|
|
86
|
+
|
|
87
|
+
All exported from the package root:
|
|
88
|
+
|
|
89
|
+
**Layout**: `Card` (slots: header/accessory/default/footer/footer-end; variants: default/outline/muted; modifiers: hoverable/interactive), `SplitPane`, `ScrollArea`, `Separator`, `PanelSection`, `DashboardPanel`
|
|
90
|
+
|
|
91
|
+
**Inputs**: `Input`, `Textarea`, `Select` (options: `{label, value}[]`), `MultiSelect`, `Checkbox`, `Switch`, `RadioGroup`, `DatePicker`, `ColorPicker`, `FileInput`, `Slider`, `Autocomplete`, `FormField` (label/hint wrapper)
|
|
92
|
+
|
|
93
|
+
**Display**: `Badge` (color: primary|neutral|success|warning|error|info; variant: solid|soft|subtle|outline), `Chip`, `Avatar` (`fallback` shows text — pass initials), `Empty`, `Skeleton`, `Progress`, `Tooltip`, `Kbd`, `Alert`, `Notification`/`Toast`
|
|
94
|
+
|
|
95
|
+
**Actions**: `Button` (variant: solid|ghost|outline; color: primary|neutral|error|...; size: xs|sm|md|lg), `Dropdown`, `DropdownMenu`, `ContextMenu`, `Popover`, `ToggleGroup` (segmented control)
|
|
96
|
+
|
|
97
|
+
**Overlays**: `Modal` (`v-model:open`, slots: header/body/footer), `Slideover`, `Drawer`, `ConfirmationModal` (`@confirm`, `@cancel`), `Pagination`
|
|
98
|
+
|
|
99
|
+
**Data**: `Table` (`columns: TableColumn[]`, `rows`, `selectable`, `striped`, `stickyHeader`, slots: `cell-{key}`, `header-{key}`, `empty`), `Tree`, `Timeline`, `Tabs`, `Tab`, `Accordion`, `Calendar`, `Breadcrumbs`
|
|
100
|
+
|
|
101
|
+
**Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
|
|
102
|
+
|
|
103
|
+
```vue
|
|
104
|
+
<script setup lang="ts">
|
|
105
|
+
import { Card, Button, Modal, Badge } from '@construct-space/ui'
|
|
106
|
+
const open = ref(false)
|
|
107
|
+
</script>
|
|
108
|
+
<template>
|
|
109
|
+
<Card title="Hello" hoverable>
|
|
110
|
+
<Badge color="success" variant="subtle" label="Live" />
|
|
111
|
+
<template #footer-end>
|
|
112
|
+
<Button variant="solid" size="sm" @click="open = true">Open</Button>
|
|
113
|
+
</template>
|
|
114
|
+
</Card>
|
|
115
|
+
<Modal v-model:open="open" title="Demo">…</Modal>
|
|
116
|
+
</template>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## `@construct-space/sdk` — host composables
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { useOrg, useOrgMembers, useToast, useAuth } from '@construct-space/sdk'
|
|
125
|
+
|
|
126
|
+
const { orgId, isOrg, org } = useOrg() // current org context (reactive)
|
|
127
|
+
const { members } = useOrgMembers() // [{ user_id, name, email, role }, ...]
|
|
128
|
+
const toast = useToast() // toast.success(...), toast.error(...)
|
|
129
|
+
const { user, isAuthenticated, logout } = useAuth()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use these for org-aware UX (assignee pickers, "shared with N" badges, avatars, member lookups).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## `@construct-space/graph` — data layer
|
|
137
|
+
|
|
138
|
+
Define typed models, get a typed reactive client, multi-tenant by default.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// src/models/Task.ts
|
|
142
|
+
import { defineModel, field, access } from '@construct-space/graph'
|
|
143
|
+
|
|
144
|
+
export const Task = defineModel('task', {
|
|
145
|
+
title: field.string().required(),
|
|
146
|
+
description: field.string(),
|
|
147
|
+
priority: field.string(),
|
|
148
|
+
due_date: field.date(),
|
|
149
|
+
position: field.int(),
|
|
150
|
+
labels: field.json(),
|
|
151
|
+
assignee_id: field.string().index(),
|
|
152
|
+
}, {
|
|
153
|
+
scope: 'org', // partitions schema per organization
|
|
154
|
+
access: {
|
|
155
|
+
read: access.authenticated(),
|
|
156
|
+
create: access.authenticated(),
|
|
157
|
+
update: access.authenticated(),
|
|
158
|
+
delete: access.owner(),
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// src/composables/useTasks.ts
|
|
165
|
+
import { useGraph } from '@construct-space/graph'
|
|
166
|
+
import { Task } from '../models'
|
|
167
|
+
|
|
168
|
+
const tasks = useGraph(Task)
|
|
169
|
+
await tasks.find({ where: { priority: 'high' }, orderBy: { position: 'asc' }, limit: 50 })
|
|
170
|
+
await tasks.create({ title: 'Ship', position: 0 })
|
|
171
|
+
await tasks.update(id, { position: 1 })
|
|
172
|
+
await tasks.remove(id)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Field types**: `string`, `int`, `number`, `boolean`, `date`, `json`, `enum([...])`. Chain `.required()`, `.index()`, `.unique()`, `.default(v)`.
|
|
176
|
+
|
|
177
|
+
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context — use `authenticated()` for org-scoped models that allow any logged-in member.
|
|
178
|
+
|
|
179
|
+
**Scopes**:
|
|
180
|
+
- `app` — single shared schema (rare)
|
|
181
|
+
- `org` — partitioned by organization (most product spaces)
|
|
182
|
+
- `project` — partitioned by project (dev-tooling spaces)
|
|
183
|
+
|
|
184
|
+
**Push models to backend after editing**: `construct graph push`.
|
|
185
|
+
|
|
186
|
+
### Pitfalls
|
|
187
|
+
|
|
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`.
|
|
189
|
+
- **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
|
|
190
|
+
- **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Actions (agent surface)
|
|
195
|
+
|
|
196
|
+
`src/actions.ts` exports an `actions` object — each entry becomes an agent-callable tool via `space_run_action`.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
export const actions = {
|
|
200
|
+
createTask: {
|
|
201
|
+
description: 'Create a task',
|
|
202
|
+
params: {
|
|
203
|
+
title: { type: 'string', required: true },
|
|
204
|
+
priority: { type: 'string', required: false },
|
|
205
|
+
},
|
|
206
|
+
run: async (p: any) => ({ task: await tasks.create(p) }),
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The agent (configured in `agent/config.md`) calls `space_list_actions` then `space_run_action`. Keep descriptions tight and specific — they're the only docstring the agent sees.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Drag and drop (HTML5)
|
|
216
|
+
|
|
217
|
+
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.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Conventions
|
|
222
|
+
|
|
223
|
+
- **Imports**: `@construct-space/*` packages always external. Local imports use relative paths (no `@/` alias unless you add one).
|
|
224
|
+
- **Composables**: `use*` prefix, keep in `src/composables/`.
|
|
225
|
+
- **Pages**: filename = route path. `index.vue` → `/`, `settings.vue` → `/settings`, `[id].vue` → param.
|
|
226
|
+
- **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.
|
|
227
|
+
- **Comments**: explain *why*, not *what*. Skip TODOs in committed code.
|
|
228
|
+
- **No emojis** in source unless the user requests them.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Build pipeline reminder
|
|
233
|
+
|
|
234
|
+
`construct build` writes:
|
|
235
|
+
- `dist/space-{{.ID}}.iife.js` — the bundled space
|
|
236
|
+
- `dist/manifest.json` — manifest + `build` block (checksum, size, **`hostApiVersion`**, builtAt)
|
|
237
|
+
|
|
238
|
+
The runtime SpaceLoader compares `hostApiVersion` to its own; mismatches log a warning. Bump the CLI to keep them aligned.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
languageOptions: {
|
|
7
|
+
ecmaVersion: 'latest',
|
|
8
|
+
sourceType: 'module',
|
|
9
|
+
globals: {
|
|
10
|
+
console: 'readonly',
|
|
11
|
+
window: 'readonly',
|
|
12
|
+
document: 'readonly',
|
|
13
|
+
globalThis: 'readonly',
|
|
14
|
+
setTimeout: 'readonly',
|
|
15
|
+
clearTimeout: 'readonly',
|
|
16
|
+
history: 'readonly',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
rules: {
|
|
20
|
+
'no-unused-vars': 'off',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
ignores: ['dist/', 'node_modules/', '*.d.ts'],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* {{.DisplayName}} — Home page
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Built-in libraries available at runtime (do not bundle):
|
|
6
|
+
* @construct-space/ui — Vue 3 components (Button, Card, Modal, Table, Badge, ...)
|
|
7
|
+
* @construct-space/sdk — useOrg, useOrgMembers, useToast, useAuth
|
|
8
|
+
* @construct-space/graph — defineModel, useGraph (multi-tenant data layer)
|
|
9
|
+
* lucide-vue-next — icons
|
|
7
10
|
*/
|
|
8
11
|
import { ref } from 'vue'
|
|
12
|
+
import { Button, Card, Empty } from '@construct-space/ui'
|
|
13
|
+
import { Sparkles } from 'lucide-vue-next'
|
|
9
14
|
|
|
10
15
|
const greeting = ref('Your space is ready. Start building!')
|
|
11
16
|
</script>
|
|
12
17
|
|
|
13
18
|
<template>
|
|
14
|
-
<div class="h-full flex items-center justify-center">
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<div class="h-full flex items-center justify-center p-6">
|
|
20
|
+
<Card variant="default" class="max-w-md w-full text-center">
|
|
21
|
+
<template #header>
|
|
22
|
+
<div class="flex items-center justify-center gap-2 w-full">
|
|
23
|
+
<Sparkles class="size-5 text-[var(--app-accent)]" />
|
|
24
|
+
<h1 class="text-lg font-semibold text-[var(--app-foreground)]">{{.DisplayName}}</h1>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<Empty :description="greeting" size="sm" />
|
|
29
|
+
|
|
30
|
+
<template #footer-end>
|
|
31
|
+
<Button variant="solid" size="sm" label="Get Started" />
|
|
32
|
+
</template>
|
|
33
|
+
</Card>
|
|
19
34
|
</div>
|
|
20
35
|
</template>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"build": "construct build",
|
|
9
9
|
"dev": "construct dev",
|
|
10
10
|
"check": "construct check",
|
|
11
|
+
"lint": "eslint .",
|
|
11
12
|
"validate": "construct validate"
|
|
12
13
|
},
|
|
13
14
|
"peerDependencies": {
|
|
@@ -18,10 +19,18 @@
|
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@construct-space/cli": "latest",
|
|
20
21
|
"@construct-space/sdk": "latest",
|
|
22
|
+
"@eslint/js": "^10.0.1",
|
|
21
23
|
"@vitejs/plugin-vue": "^5.2.3",
|
|
24
|
+
"eslint": "^10.2.1",
|
|
25
|
+
"eslint-plugin-vue": "^10.0.0",
|
|
22
26
|
"lucide-vue-next": "^1.0.0",
|
|
23
27
|
"typescript": "^5.9.3",
|
|
28
|
+
"typescript-eslint": "^9.0.0",
|
|
24
29
|
"vite": "^6.3.5",
|
|
25
30
|
"vue-tsc": "^3.2.5"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@construct-space/graph": "^0.5.0",
|
|
34
|
+
"@construct-space/ui": "^0.5.2"
|
|
26
35
|
}
|
|
27
36
|
}
|