@construct-space/cli 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8026,6 +8026,11 @@ function readRaw(dir) {
8026
8026
  const data = readFileSync2(path, "utf-8");
8027
8027
  return JSON.parse(data);
8028
8028
  }
8029
+ function write(dir, m) {
8030
+ const path = join3(dir, MANIFEST_FILE);
8031
+ writeFileSync2(path, JSON.stringify(m, null, 2) + `
8032
+ `);
8033
+ }
8029
8034
  function writeWithBuild(dir, raw, build) {
8030
8035
  raw.build = build;
8031
8036
  writeFileSync2(join3(dir, "manifest.json"), JSON.stringify(raw, null, 2) + `
@@ -8109,7 +8114,7 @@ function resolvePages(m, root, prefix) {
8109
8114
  if (existsSync4(legacyFullPath)) {
8110
8115
  let varName = "IndexPage";
8111
8116
  if (p.path) {
8112
- varName = capitalize(p.path.replace(/[\/:]/g, "-").replace(/-+/g, "-")) + "Page";
8117
+ varName = capitalize(p.path.replace(/\[([^\]]+)\]/g, "$1").replace(/[\/:]/g, "-").replace(/-+/g, "-")) + "Page";
8113
8118
  }
8114
8119
  return { varName, importPath: prefix + legacyComponent, path: p.path };
8115
8120
  }
@@ -8243,35 +8248,174 @@ function bundleAgentDir(srcDir, distDir) {
8243
8248
  }
8244
8249
 
8245
8250
  // src/commands/build.ts
8251
+ function stripComments(source) {
8252
+ let out = "";
8253
+ let i = 0;
8254
+ let quote = null;
8255
+ let escaped = false;
8256
+ while (i < source.length) {
8257
+ const ch = source[i];
8258
+ const next = source[i + 1];
8259
+ if (quote) {
8260
+ out += ch;
8261
+ if (escaped) {
8262
+ escaped = false;
8263
+ } else if (ch === "\\") {
8264
+ escaped = true;
8265
+ } else if (ch === quote) {
8266
+ quote = null;
8267
+ }
8268
+ i++;
8269
+ continue;
8270
+ }
8271
+ if (ch === '"' || ch === "'" || ch === "`") {
8272
+ quote = ch;
8273
+ out += ch;
8274
+ i++;
8275
+ continue;
8276
+ }
8277
+ if (ch === "/" && next === "/") {
8278
+ while (i < source.length && source[i] !== `
8279
+ `) {
8280
+ out += " ";
8281
+ i++;
8282
+ }
8283
+ continue;
8284
+ }
8285
+ if (ch === "/" && next === "*") {
8286
+ out += " ";
8287
+ i += 2;
8288
+ while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) {
8289
+ out += source[i] === `
8290
+ ` ? `
8291
+ ` : " ";
8292
+ i++;
8293
+ }
8294
+ if (i < source.length) {
8295
+ out += " ";
8296
+ i += 2;
8297
+ }
8298
+ continue;
8299
+ }
8300
+ out += ch;
8301
+ i++;
8302
+ }
8303
+ return out;
8304
+ }
8305
+ function findMatchingBrace(source, openIndex) {
8306
+ let depth = 0;
8307
+ let quote = null;
8308
+ let escaped = false;
8309
+ for (let i = openIndex;i < source.length; i++) {
8310
+ const ch = source[i];
8311
+ if (quote) {
8312
+ if (escaped)
8313
+ escaped = false;
8314
+ else if (ch === "\\")
8315
+ escaped = true;
8316
+ else if (ch === quote)
8317
+ quote = null;
8318
+ continue;
8319
+ }
8320
+ if (ch === '"' || ch === "'" || ch === "`") {
8321
+ quote = ch;
8322
+ continue;
8323
+ }
8324
+ if (ch === "{")
8325
+ depth++;
8326
+ else if (ch === "}") {
8327
+ depth--;
8328
+ if (depth === 0)
8329
+ return i;
8330
+ }
8331
+ }
8332
+ return -1;
8333
+ }
8334
+ function objectLiteralAfter(source, start) {
8335
+ const open = source.indexOf("{", start);
8336
+ if (open < 0)
8337
+ return null;
8338
+ const close = findMatchingBrace(source, open);
8339
+ if (close < 0)
8340
+ return null;
8341
+ return source.slice(open + 1, close);
8342
+ }
8343
+ function actionsObject(source) {
8344
+ const stripped = stripComments(source);
8345
+ const match = /export\s+const\s+actions\s*=/.exec(stripped);
8346
+ if (!match)
8347
+ return null;
8348
+ return objectLiteralAfter(stripped, match.index + match[0].length);
8349
+ }
8350
+ function objectEntries(body) {
8351
+ const entries = [];
8352
+ let i = 0;
8353
+ while (i < body.length) {
8354
+ const match = /\s*,?\s*([A-Za-z_$][\w$]*)\s*:/.exec(body.slice(i));
8355
+ if (!match)
8356
+ break;
8357
+ const name = match[1];
8358
+ const valueStart = i + match.index + match[0].length;
8359
+ const open = body.indexOf("{", valueStart);
8360
+ if (open < 0)
8361
+ break;
8362
+ if (body.slice(valueStart, open).trim()) {
8363
+ i = valueStart;
8364
+ continue;
8365
+ }
8366
+ const close = findMatchingBrace(body, open);
8367
+ if (close < 0)
8368
+ break;
8369
+ entries.push({ name, body: body.slice(open + 1, close) });
8370
+ i = close + 1;
8371
+ }
8372
+ return entries;
8373
+ }
8374
+ function stringProperty(body, name) {
8375
+ const match = new RegExp(`${name}\\s*:\\s*['"\`]([^'"\`]*)['"\`]`).exec(body);
8376
+ return match?.[1];
8377
+ }
8378
+ function booleanProperty(body, name) {
8379
+ const match = new RegExp(`${name}\\s*:\\s*(true|false)`).exec(body);
8380
+ if (!match)
8381
+ return;
8382
+ return match[1] === "true";
8383
+ }
8384
+ function propertyObject(body, name) {
8385
+ const match = new RegExp(`${name}\\s*:`).exec(body);
8386
+ if (!match)
8387
+ return null;
8388
+ return objectLiteralAfter(body, match.index + match[0].length);
8389
+ }
8246
8390
  function extractActionMetadata(actionsPath) {
8247
8391
  try {
8248
- const source = readFileSync4(actionsPath, "utf-8");
8392
+ const actionsBody = actionsObject(readFileSync4(actionsPath, "utf-8"));
8393
+ if (!actionsBody)
8394
+ return null;
8249
8395
  const result = {};
8250
- const actionPattern = /(\w+)\s*:\s*\{[^}]*description\s*:\s*['"`]([^'"`]+)['"`]/g;
8251
- let match;
8252
- while ((match = actionPattern.exec(source)) !== null) {
8253
- const actionId = match[1];
8254
- const description = match[2];
8255
- result[actionId] = { description };
8256
- }
8257
- for (const actionId of Object.keys(result)) {
8258
- const paramBlockPattern = new RegExp(`${actionId}\\s*:\\s*\\{[\\s\\S]*?params\\s*:\\s*\\{([\\s\\S]*?)\\}\\s*,?\\s*(?:run|\\})`);
8259
- const paramMatch = source.match(paramBlockPattern);
8260
- if (paramMatch?.[1]) {
8396
+ for (const action of objectEntries(actionsBody)) {
8397
+ const description = stringProperty(action.body, "description");
8398
+ if (!description)
8399
+ continue;
8400
+ const metadata = { description };
8401
+ const paramsBody = propertyObject(action.body, "params");
8402
+ if (paramsBody) {
8261
8403
  const params = {};
8262
- const paramEntryPattern = /(\w+)\s*:\s*\{\s*type\s*:\s*['"`](\w+)['"`](?:\s*,\s*description\s*:\s*['"`]([^'"`]*)['"`])?(?:\s*,\s*required\s*:\s*(true|false))?\s*\}/g;
8263
- let pm;
8264
- while ((pm = paramEntryPattern.exec(paramMatch[1])) !== null) {
8265
- params[pm[1]] = {
8266
- type: pm[2],
8267
- ...pm[3] ? { description: pm[3] } : {},
8268
- ...pm[4] === "true" ? { required: true } : {}
8404
+ for (const param of objectEntries(paramsBody)) {
8405
+ const type = stringProperty(param.body, "type");
8406
+ if (!type)
8407
+ continue;
8408
+ const required = booleanProperty(param.body, "required");
8409
+ params[param.name] = {
8410
+ type,
8411
+ ...stringProperty(param.body, "description") ? { description: stringProperty(param.body, "description") } : {},
8412
+ ...required === true ? { required: true } : {}
8269
8413
  };
8270
8414
  }
8271
- if (Object.keys(params).length > 0) {
8272
- result[actionId].params = params;
8273
- }
8415
+ if (Object.keys(params).length > 0)
8416
+ metadata.params = params;
8274
8417
  }
8418
+ result[action.name] = metadata;
8275
8419
  }
8276
8420
  return Object.keys(result).length > 0 ? result : null;
8277
8421
  } catch {
@@ -10319,6 +10463,22 @@ async function publish(options) {
10319
10463
  init_source();
10320
10464
  import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
10321
10465
  import { join as join15 } from "path";
10466
+
10467
+ // src/lib/pagePaths.ts
10468
+ function pageComponentFromPath(path) {
10469
+ const clean = path.replace(/^\/+|\/+$/g, "");
10470
+ if (!clean)
10471
+ return "pages/index.vue";
10472
+ const segments = clean.split("/").map((segment) => {
10473
+ if (segment.startsWith(":") && segment.length > 1) {
10474
+ return `[${segment.slice(1)}]`;
10475
+ }
10476
+ return segment;
10477
+ });
10478
+ return `pages/${segments.join("/")}.vue`;
10479
+ }
10480
+
10481
+ // src/commands/validate.ts
10322
10482
  function validate3() {
10323
10483
  const root = process.cwd();
10324
10484
  if (!exists(root)) {
@@ -10335,7 +10495,7 @@ function validate3() {
10335
10495
  }
10336
10496
  let warnings = 0;
10337
10497
  for (const page of m.pages) {
10338
- const component = page.component || (page.path === "" ? "pages/index.vue" : `pages/${page.path}.vue`);
10498
+ const component = page.component || pageComponentFromPath(page.path);
10339
10499
  const fullPath = join15(root, "src", component);
10340
10500
  if (!existsSync12(fullPath)) {
10341
10501
  console.log(source_default.yellow(` \u26A0 Page component not found: src/${component}`));
@@ -10386,7 +10546,7 @@ function check() {
10386
10546
  }
10387
10547
  let warnings = 0;
10388
10548
  for (const page of m.pages) {
10389
- const component = page.component || (page.path === "" ? "pages/index.vue" : `pages/${page.path}.vue`);
10549
+ const component = page.component || pageComponentFromPath(page.path);
10390
10550
  if (!existsSync13(join16(root, "src", component))) {
10391
10551
  console.log(source_default.yellow(` \u26A0 Page not found: src/${component}`));
10392
10552
  warnings++;
@@ -10949,6 +11109,33 @@ async function graphPush() {
10949
11109
  process.exit(1);
10950
11110
  }
10951
11111
  }
11112
+ function parseDefaultValue(raw) {
11113
+ const value = raw.trim();
11114
+ try {
11115
+ return JSON.parse(value);
11116
+ } catch {}
11117
+ const quote = value[0];
11118
+ if ((quote === "'" || quote === '"') && value.endsWith(quote)) {
11119
+ return value.slice(1, -1).replace(/\\(['"\\bfnrt])/g, (_, ch) => {
11120
+ switch (ch) {
11121
+ case "b":
11122
+ return "\b";
11123
+ case "f":
11124
+ return "\f";
11125
+ case "n":
11126
+ return `
11127
+ `;
11128
+ case "r":
11129
+ return "\r";
11130
+ case "t":
11131
+ return "\t";
11132
+ default:
11133
+ return ch;
11134
+ }
11135
+ });
11136
+ }
11137
+ return value;
11138
+ }
10952
11139
  function parseModelFile(content, fileName) {
10953
11140
  const modelMatch = content.match(/defineModel\s*\(\s*['"](\w+)['"]/);
10954
11141
  if (!modelMatch)
@@ -10976,11 +11163,7 @@ function parseModelFile(content, fileName) {
10976
11163
  field.validation = "url";
10977
11164
  const defaultMatch = modifiers.match(/\.default\((.+?)\)/);
10978
11165
  if (defaultMatch) {
10979
- try {
10980
- field.default = JSON.parse(defaultMatch[1]);
10981
- } catch {
10982
- field.default = defaultMatch[1];
10983
- }
11166
+ field.default = parseDefaultValue(defaultMatch[1]);
10984
11167
  }
10985
11168
  }
10986
11169
  fields.push(field);
@@ -11162,10 +11345,41 @@ function parseModelFields(content, fileName) {
11162
11345
  }
11163
11346
  return { name: modelMatch[1], fields };
11164
11347
  }
11348
+
11349
+ // src/commands/graph/fork.ts
11350
+ init_source();
11351
+ var spaceIDRegex = /^[a-z][a-z0-9-]*$/;
11352
+ function forkManifest(root, newSpaceID) {
11353
+ const id = newSpaceID.trim();
11354
+ if (!spaceIDRegex.test(id)) {
11355
+ throw new Error("space id must be lowercase alphanumeric with hyphens, starting with a letter");
11356
+ }
11357
+ if (!exists(root)) {
11358
+ throw new Error("No space.manifest.json found in current directory");
11359
+ }
11360
+ const m = read(root);
11361
+ if (m.id === id) {
11362
+ throw new Error(`space.manifest.json already uses id "${id}"`);
11363
+ }
11364
+ const oldSpaceID = m.id;
11365
+ m.id = id;
11366
+ write(root, m);
11367
+ return { oldSpaceID, newSpaceID: id };
11368
+ }
11369
+ function graphFork(newSpaceID) {
11370
+ try {
11371
+ const result = forkManifest(process.cwd(), newSpaceID);
11372
+ console.log(source_default.green(`Forked graph space id: ${result.oldSpaceID} -> ${result.newSpaceID}`));
11373
+ console.log(source_default.dim("Run `construct graph push` to register the forked schema."));
11374
+ } catch (err) {
11375
+ console.error(source_default.red(err.message));
11376
+ process.exit(1);
11377
+ }
11378
+ }
11165
11379
  // package.json
11166
11380
  var package_default = {
11167
11381
  name: "@construct-space/cli",
11168
- version: "1.6.0",
11382
+ version: "1.6.2",
11169
11383
  description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
11170
11384
  type: "module",
11171
11385
  bin: {
@@ -11229,6 +11443,7 @@ var graph = program2.command("graph").description("Construct Graph \u2014 data m
11229
11443
  graph.command("init").description("Initialize Graph in a space project").action(() => graphInit());
11230
11444
  graph.command("generate <model> [fields...]").alias("g").description("Generate a data model").option("--access <rules>", "Access rules (e.g. read:member,create:member,update:owner,delete:admin)").action((model, fields, opts) => generate2(model, fields, opts));
11231
11445
  graph.command("push").description("Register models with the Graph service").action(async () => graphPush());
11446
+ graph.command("fork <new-space-id>").description("Rewrite this space manifest to a new graph space id").action((newSpaceID) => graphFork(newSpaceID));
11232
11447
  graph.command("migrate").description("Compare local models with server schema and apply changes").option("--apply", "Apply destructive changes (drop columns, alter constraints)").action(async (opts) => graphMigrate(opts));
11233
11448
  graph.command("spaces").alias("list").alias("ls").description("List spaces published by your org").option("--org <id>", "Override org id").option("--bundle <id>", "Filter to a single bundle").option("--json", "Output JSON").action(async (opts) => (await Promise.resolve().then(() => (init_spaces(), exports_spaces))).spacesList(opts));
11234
11449
  var bundles = graph.command("bundles").description("Manage space bundles (publisher grouping)");
@@ -100,6 +100,17 @@ All exported from the package root:
100
100
 
101
101
  **Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
102
102
 
103
+ ### Component gotchas
104
+
105
+ - **`Avatar` sizes**: only `sm | md | lg` (no `xs`).
106
+ - **`Select`** doesn't accept `multiple` — render a chip-toggle list yourself for multi-select.
107
+ - **`Table`** column `align` literals must be `as const` to typecheck (`align: 'right' as const`).
108
+ - **`Table` rowKey** is a string field name on the row object, not a function.
109
+ - **`Modal`** uses `v-model:open` (not `v-model`); slots: `header | body | footer | accessory`.
110
+ - **`Card`** slots: `header | accessory | default | footer | footer-end`. Use `title`/`description` props for the header to get standard styling; put right-side actions in `accessory`.
111
+ - **`ConfirmationModal`** emits `@confirm` and `@cancel` — wire both, otherwise dismissal does nothing.
112
+ - **Settings pages**: never raw `<select>`/`<input>`/`<textarea>` — use `Select`, `Switch`, `Input`, `Textarea` with `FormField` labels. Persist with `localStorage` (key prefix per space).
113
+
103
114
  ```vue
104
115
  <script setup lang="ts">
105
116
  import { Card, Button, Modal, Badge } from '@construct-space/ui'
@@ -185,9 +196,20 @@ await tasks.remove(id)
185
196
 
186
197
  ### Pitfalls
187
198
 
188
- - **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`.
199
+ - **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`. Pattern:
200
+ ```ts
201
+ function parseLabels(raw: unknown): string[] {
202
+ if (Array.isArray(raw)) return raw as string[]
203
+ if (typeof raw === 'string' && raw.trim()) {
204
+ try { const v = JSON.parse(raw); return Array.isArray(v) ? v : [] } catch { return [] }
205
+ }
206
+ return []
207
+ }
208
+ ```
189
209
  - **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
190
210
  - **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
211
+ - **Schema ownership is sticky**: the first profile to `construct graph push` becomes the schema owner. Subsequent pushes from a different profile fail with "Ownership check failed". Bump the space `id` to fork, or push from the original owner profile.
212
+ - **`access.member()` requires org context** — use `access.authenticated()` for org-scoped models that any logged-in member should be able to read/write. `member()` is for stricter membership checks.
191
213
 
192
214
  ---
193
215
 
@@ -25,7 +25,7 @@
25
25
  "eslint-plugin-vue": "^10.0.0",
26
26
  "lucide-vue-next": "^1.0.0",
27
27
  "typescript": "^5.9.3",
28
- "typescript-eslint": "^9.0.0",
28
+ "typescript-eslint": "^8.0.0",
29
29
  "vite": "^6.3.5",
30
30
  "vue-tsc": "^3.2.5"
31
31
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@construct-space/cli",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Construct CLI — scaffold, build, develop, and publish spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,6 +100,17 @@ All exported from the package root:
100
100
 
101
101
  **Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
102
102
 
103
+ ### Component gotchas
104
+
105
+ - **`Avatar` sizes**: only `sm | md | lg` (no `xs`).
106
+ - **`Select`** doesn't accept `multiple` — render a chip-toggle list yourself for multi-select.
107
+ - **`Table`** column `align` literals must be `as const` to typecheck (`align: 'right' as const`).
108
+ - **`Table` rowKey** is a string field name on the row object, not a function.
109
+ - **`Modal`** uses `v-model:open` (not `v-model`); slots: `header | body | footer | accessory`.
110
+ - **`Card`** slots: `header | accessory | default | footer | footer-end`. Use `title`/`description` props for the header to get standard styling; put right-side actions in `accessory`.
111
+ - **`ConfirmationModal`** emits `@confirm` and `@cancel` — wire both, otherwise dismissal does nothing.
112
+ - **Settings pages**: never raw `<select>`/`<input>`/`<textarea>` — use `Select`, `Switch`, `Input`, `Textarea` with `FormField` labels. Persist with `localStorage` (key prefix per space).
113
+
103
114
  ```vue
104
115
  <script setup lang="ts">
105
116
  import { Card, Button, Modal, Badge } from '@construct-space/ui'
@@ -185,9 +196,20 @@ await tasks.remove(id)
185
196
 
186
197
  ### Pitfalls
187
198
 
188
- - **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`.
199
+ - **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`. Pattern:
200
+ ```ts
201
+ function parseLabels(raw: unknown): string[] {
202
+ if (Array.isArray(raw)) return raw as string[]
203
+ if (typeof raw === 'string' && raw.trim()) {
204
+ try { const v = JSON.parse(raw); return Array.isArray(v) ? v : [] } catch { return [] }
205
+ }
206
+ return []
207
+ }
208
+ ```
189
209
  - **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
190
210
  - **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
211
+ - **Schema ownership is sticky**: the first profile to `construct graph push` becomes the schema owner. Subsequent pushes from a different profile fail with "Ownership check failed". Bump the space `id` to fork, or push from the original owner profile.
212
+ - **`access.member()` requires org context** — use `access.authenticated()` for org-scoped models that any logged-in member should be able to read/write. `member()` is for stricter membership checks.
191
213
 
192
214
  ---
193
215
 
@@ -25,7 +25,7 @@
25
25
  "eslint-plugin-vue": "^10.0.0",
26
26
  "lucide-vue-next": "^1.0.0",
27
27
  "typescript": "^5.9.3",
28
- "typescript-eslint": "^9.0.0",
28
+ "typescript-eslint": "^8.0.0",
29
29
  "vite": "^6.3.5",
30
30
  "vue-tsc": "^3.2.5"
31
31
  },