@construct-space/cli 1.5.1 → 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 CHANGED
@@ -23,6 +23,25 @@ construct check # Type-check + lint
23
23
  construct clean # Remove build artifacts
24
24
  ```
25
25
 
26
+ ### Built-in libraries
27
+
28
+ Scaffolded spaces ship with the full Construct stack pre-wired — import freely, the host resolves them at runtime (never bundled):
29
+
30
+ | Package | Purpose |
31
+ |---------|---------|
32
+ | `@construct-space/ui` | Vue 3 components — `Button`, `Card`, `Modal`, `Table`, `Badge`, `Avatar`, `ToggleGroup`, `ConfirmationModal`, ... |
33
+ | `@construct-space/sdk` | Host composables — `useOrg`, `useOrgMembers`, `useToast`, `useAuth` |
34
+ | `@construct-space/graph` | Multi-tenant data layer — `defineModel`, `useGraph`, scopes, access rules |
35
+ | `lucide-vue-next` | Icon set |
36
+
37
+ ```ts
38
+ import { Button, Card, Modal } from '@construct-space/ui'
39
+ import { useOrg, useOrgMembers } from '@construct-space/sdk'
40
+ import { defineModel, field, useGraph } from '@construct-space/graph'
41
+ ```
42
+
43
+ The scaffolded `src/pages/index.vue` and `src/actions.ts` show the recommended patterns. Use `construct check` (typecheck + ESLint flat config) before commit.
44
+
26
45
  ### Graph (Data Models)
27
46
 
28
47
  ```bash
package/dist/index.js CHANGED
@@ -5313,8 +5313,10 @@ async function scaffold(nameArg, options) {
5313
5313
  "safety.json.tmpl": join2(name, "agent", "hooks", "safety.json"),
5314
5314
  "build.yml.tmpl": join2(name, ".github", "workflows", "build.yml"),
5315
5315
  "tsconfig.json.tmpl": join2(name, "tsconfig.json"),
5316
+ "eslint.config.js.tmpl": join2(name, "eslint.config.js"),
5316
5317
  "gitignore.tmpl": join2(name, ".gitignore"),
5317
5318
  "readme.md.tmpl": join2(name, "README.md"),
5319
+ "construct.md.tmpl": join2(name, "CONSTRUCT.md"),
5318
5320
  "widgets/2x1.vue.tmpl": join2(name, "widgets", "summary", "2x1.vue"),
5319
5321
  "widgets/4x1.vue.tmpl": join2(name, "widgets", "summary", "4x1.vue"),
5320
5322
  "actions.ts.tmpl": join2(name, "src", "actions.ts")
@@ -7989,6 +7991,7 @@ function ora(options) {
7989
7991
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
7990
7992
  import { join as join3 } from "path";
7991
7993
  var MANIFEST_FILE = "space.manifest.json";
7994
+ var HOST_API_VERSION = "0.5.0";
7992
7995
  var idRegex = /^[a-z][a-z0-9-]*$/;
7993
7996
  var versionRegex = /^\d+\.\d+\.\d+/;
7994
7997
  function validate2(m) {
@@ -8334,7 +8337,7 @@ async function build(options) {
8334
8337
  writeWithBuild(distDir, raw, {
8335
8338
  checksum,
8336
8339
  size: bundleData.length,
8337
- hostApiVersion: "0.2.0",
8340
+ hostApiVersion: HOST_API_VERSION,
8338
8341
  builtAt: new Date().toISOString()
8339
8342
  });
8340
8343
  console.log(source_default.green(`Built ${m.name} v${m.version}`));
@@ -10016,7 +10019,7 @@ async function dev() {
10016
10019
  writeWithBuild(distDir, raw, {
10017
10020
  checksum,
10018
10021
  size: bundleData.length,
10019
- hostApiVersion: "0.2.0",
10022
+ hostApiVersion: HOST_API_VERSION,
10020
10023
  builtAt: new Date().toISOString()
10021
10024
  });
10022
10025
  console.log(source_default.green(`Built \u2192 dist/ (${(bundleData.length / 1024).toFixed(1)} KB)`));
@@ -11162,7 +11165,7 @@ function parseModelFields(content, fileName) {
11162
11165
  // package.json
11163
11166
  var package_default = {
11164
11167
  name: "@construct-space/cli",
11165
- version: "1.5.1",
11168
+ version: "1.6.0",
11166
11169
  description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
11167
11170
  type: "module",
11168
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
- * Define actions here and they'll be automatically available as agent tools.
5
- * The agent calls: space_run_action(action: "action_id", payload: {...})
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
- // Example action:
28
- // hello: {
29
- // description: 'Say hello',
30
- // params: {
31
- // name: { type: 'string', description: 'Name to greet', required: true },
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
- * Host-provided packages (vue, pinia, @vueuse/core, @construct/sdk, etc.)
6
- * are available as imports they resolve to the host at runtime.
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
- <div class="text-center">
16
- <h1 class="text-2xl font-bold text-[var(--app-foreground)] mb-2">{{.DisplayName}}</h1>
17
- <p class="text-sm text-[var(--app-muted)]">{{ greeting }}</p>
18
- </div>
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@construct-space/cli",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Construct CLI — scaffold, build, develop, and publish spaces",
5
5
  "type": "module",
6
6
  "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
- * Define actions here and they'll be automatically available as agent tools.
5
- * The agent calls: space_run_action(action: "action_id", payload: {...})
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
- // Example action:
28
- // hello: {
29
- // description: 'Say hello',
30
- // params: {
31
- // name: { type: 'string', description: 'Name to greet', required: true },
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
- * Host-provided packages (vue, pinia, @vueuse/core, @construct/sdk, etc.)
6
- * are available as imports they resolve to the host at runtime.
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
- <div class="text-center">
16
- <h1 class="text-2xl font-bold text-[var(--app-foreground)] mb-2">{{.DisplayName}}</h1>
17
- <p class="text-sm text-[var(--app-muted)]">{{ greeting }}</p>
18
- </div>
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
  }