@lumerahq/cli 0.19.9-dev.2 → 0.19.9-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -219,25 +219,25 @@ async function main() {
219
219
  switch (command) {
220
220
  // Resource commands
221
221
  case "plan":
222
- await import("./resources-ZFGJITDH.js").then((m) => m.plan(args.slice(1)));
222
+ await import("./resources-4M4LMSSN.js").then((m) => m.plan(args.slice(1)));
223
223
  break;
224
224
  case "apply":
225
- await import("./resources-ZFGJITDH.js").then((m) => m.apply(args.slice(1)));
225
+ await import("./resources-4M4LMSSN.js").then((m) => m.apply(args.slice(1)));
226
226
  break;
227
227
  case "pull":
228
- await import("./resources-ZFGJITDH.js").then((m) => m.pull(args.slice(1)));
228
+ await import("./resources-4M4LMSSN.js").then((m) => m.pull(args.slice(1)));
229
229
  break;
230
230
  case "destroy":
231
- await import("./resources-ZFGJITDH.js").then((m) => m.destroy(args.slice(1)));
231
+ await import("./resources-4M4LMSSN.js").then((m) => m.destroy(args.slice(1)));
232
232
  break;
233
233
  case "list":
234
- await import("./resources-ZFGJITDH.js").then((m) => m.list(args.slice(1)));
234
+ await import("./resources-4M4LMSSN.js").then((m) => m.list(args.slice(1)));
235
235
  break;
236
236
  case "show":
237
- await import("./resources-ZFGJITDH.js").then((m) => m.show(args.slice(1)));
237
+ await import("./resources-4M4LMSSN.js").then((m) => m.show(args.slice(1)));
238
238
  break;
239
239
  case "diff":
240
- await import("./resources-ZFGJITDH.js").then((m) => m.diff(args.slice(1)));
240
+ await import("./resources-4M4LMSSN.js").then((m) => m.diff(args.slice(1)));
241
241
  break;
242
242
  // Development
243
243
  case "dev":
@@ -121,6 +121,7 @@ var collectionSchemaRule = {
121
121
  };
122
122
 
123
123
  // src/lib/hooks-parse.ts
124
+ import { Parser } from "acorn";
124
125
  var VALID_HOOK_TRIGGERS = [
125
126
  "before_create",
126
127
  "after_create",
@@ -137,217 +138,272 @@ var KNOWN_HOOK_CONFIG_KEYS = /* @__PURE__ */ new Set([
137
138
  "enabled",
138
139
  "metadata"
139
140
  ]);
140
- var CONFIG_BLOCK_RE = /export\s+const\s+config\s*[=:]\s*(\{[\s\S]*?\});?/;
141
- function extractConfigBody(content) {
142
- const m = content.match(CONFIG_BLOCK_RE);
143
- return m ? m[1] : null;
144
- }
145
- function stripJsComments(source) {
146
- let out = "";
147
- let inString = null;
148
- let prev = "";
149
- let i = 0;
150
- while (i < source.length) {
151
- const ch = source[i];
152
- const next = source[i + 1];
153
- if (inString) {
154
- out += ch;
155
- if (ch === inString && prev !== "\\") inString = null;
156
- prev = ch;
157
- i++;
158
- continue;
141
+ function isLiteralString(n) {
142
+ return !!n && n.type === "Literal" && typeof n.value === "string";
143
+ }
144
+ function getPropertyKey(prop) {
145
+ const key = prop.key;
146
+ if (!key) return void 0;
147
+ if (key.type === "Identifier") return key.name;
148
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
149
+ return void 0;
150
+ }
151
+ function objectExpressionToLiteral(node) {
152
+ const result = {};
153
+ for (const prop of node.properties) {
154
+ if (prop.type !== "Property") {
155
+ throw new Error("spread or shorthand properties are not supported");
159
156
  }
160
- if (ch === '"' || ch === "'" || ch === "`") {
161
- inString = ch;
162
- out += ch;
163
- prev = ch;
164
- i++;
165
- continue;
157
+ if (prop.computed) {
158
+ throw new Error("computed keys are not supported");
166
159
  }
167
- if (ch === "/" && next === "/") {
168
- while (i < source.length && source[i] !== "\n") i++;
169
- continue;
160
+ const key = getPropertyKey(prop);
161
+ if (key === void 0) throw new Error("non-string property key");
162
+ result[key] = literalNodeToValue(prop.value);
163
+ }
164
+ return result;
165
+ }
166
+ function literalNodeToValue(node) {
167
+ switch (node.type) {
168
+ case "Literal":
169
+ return node.value;
170
+ case "ObjectExpression":
171
+ return objectExpressionToLiteral(node);
172
+ case "ArrayExpression":
173
+ return node.elements.map(
174
+ (el) => el === null ? null : literalNodeToValue(el)
175
+ );
176
+ case "UnaryExpression": {
177
+ const arg = node.argument;
178
+ if (node.operator === "-" && arg.type === "Literal" && typeof arg.value === "number") {
179
+ return -arg.value;
180
+ }
181
+ throw new Error(`unsupported unary operator '${String(node.operator)}'`);
170
182
  }
171
- if (ch === "/" && next === "*") {
172
- i += 2;
173
- while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
174
- i += 2;
175
- continue;
183
+ case "TemplateLiteral": {
184
+ const expressions = node.expressions;
185
+ const quasis = node.quasis;
186
+ if (expressions.length === 0 && quasis.length === 1) {
187
+ return quasis[0].value.cooked;
188
+ }
189
+ throw new Error("template literals with interpolation are not supported");
176
190
  }
177
- out += ch;
178
- prev = ch;
179
- i++;
191
+ default:
192
+ throw new Error(`expected a literal value, got ${node.type}`);
180
193
  }
181
- return out;
182
194
  }
183
- function extractTopLevelConfigKeys(configBody) {
184
- if (!configBody.startsWith("{") || !configBody.endsWith("}")) return [];
185
- const inner = stripJsComments(configBody.slice(1, -1));
186
- let stripped = "";
187
- let depth = 0;
188
- let inString = null;
189
- let prev = "";
190
- for (const ch of inner) {
191
- if (inString) {
192
- if (ch === inString && prev !== "\\") inString = null;
193
- prev = ch;
194
- continue;
195
+ function parseHookFile(content) {
196
+ const finding = {
197
+ configFound: false,
198
+ configKeys: [],
199
+ configIssues: [],
200
+ defaultHandlerFound: false
201
+ };
202
+ let ast;
203
+ try {
204
+ ast = Parser.parse(content, {
205
+ ecmaVersion: "latest",
206
+ sourceType: "module",
207
+ locations: true
208
+ });
209
+ } catch (err) {
210
+ const e = err;
211
+ finding.syntaxError = {
212
+ message: e.message ?? String(err),
213
+ line: e.loc?.line,
214
+ column: e.loc?.column
215
+ };
216
+ return finding;
217
+ }
218
+ let configNode;
219
+ let handlerNode;
220
+ for (const node of ast.body) {
221
+ if (node.type === "ExportNamedDeclaration") {
222
+ const decl = node.declaration;
223
+ if (decl && decl.type === "VariableDeclaration") {
224
+ for (const d of decl.declarations) {
225
+ if (d.id?.type === "Identifier" && d.id.name === "config" && d.init?.type === "ObjectExpression") {
226
+ configNode = d.init;
227
+ }
228
+ }
229
+ }
230
+ } else if (node.type === "ExportDefaultDeclaration") {
231
+ const d = node.declaration;
232
+ if (d.type === "FunctionDeclaration" || d.type === "FunctionExpression" || d.type === "ArrowFunctionExpression") {
233
+ handlerNode = d;
234
+ }
195
235
  }
196
- if (ch === '"' || ch === "'" || ch === "`") {
197
- inString = ch;
198
- prev = ch;
199
- continue;
236
+ }
237
+ if (configNode) {
238
+ finding.configFound = true;
239
+ const valueNodes = {};
240
+ for (const prop of configNode.properties) {
241
+ if (prop.type !== "Property") continue;
242
+ const key = getPropertyKey(prop);
243
+ if (key === void 0) continue;
244
+ finding.configKeys.push(key);
245
+ valueNodes[key] = prop.value;
200
246
  }
201
- if (ch === "{" || ch === "[") {
202
- depth++;
203
- prev = ch;
204
- continue;
247
+ for (const k of finding.configKeys) {
248
+ if (!KNOWN_HOOK_CONFIG_KEYS.has(k)) {
249
+ finding.configIssues.push({ kind: "unknown-key", key: k });
250
+ }
205
251
  }
206
- if (ch === "}" || ch === "]") {
207
- depth--;
208
- prev = ch;
209
- continue;
252
+ const readString = (field) => {
253
+ const n = valueNodes[field];
254
+ if (!n) return void 0;
255
+ if (isLiteralString(n)) return n.value;
256
+ finding.configIssues.push({ kind: "non-literal-field", field });
257
+ return void 0;
258
+ };
259
+ const external_id = readString("external_id");
260
+ const collection = readString("collection");
261
+ const trigger = readString("trigger");
262
+ const name = readString("name");
263
+ if (!collection && !("collection" in valueNodes)) {
264
+ finding.configIssues.push({ kind: "missing-required-field", field: "collection" });
265
+ }
266
+ if (!trigger && !("trigger" in valueNodes)) {
267
+ finding.configIssues.push({ kind: "missing-required-field", field: "trigger" });
268
+ }
269
+ if (trigger && !VALID_HOOK_TRIGGERS.includes(trigger)) {
270
+ finding.configIssues.push({ kind: "invalid-trigger", value: trigger });
271
+ }
272
+ let enabled;
273
+ if ("enabled" in valueNodes) {
274
+ const n = valueNodes.enabled;
275
+ if (n.type === "Literal" && typeof n.value === "boolean") {
276
+ enabled = n.value;
277
+ } else {
278
+ finding.configIssues.push({
279
+ kind: "invalid-enabled",
280
+ rawSource: content.slice(n.start, n.end)
281
+ });
282
+ }
283
+ }
284
+ let metadata;
285
+ if ("metadata" in valueNodes) {
286
+ const n = valueNodes.metadata;
287
+ if (n.type === "ObjectExpression") {
288
+ try {
289
+ metadata = objectExpressionToLiteral(n);
290
+ } catch (e) {
291
+ finding.configIssues.push({
292
+ kind: "metadata-unparseable",
293
+ reason: e.message
294
+ });
295
+ }
296
+ } else {
297
+ finding.configIssues.push({
298
+ kind: "metadata-unparseable",
299
+ reason: "metadata must be an inline object literal"
300
+ });
301
+ }
302
+ }
303
+ if (collection && trigger) {
304
+ finding.hook = {
305
+ external_id: external_id ?? "",
306
+ collection,
307
+ trigger,
308
+ enabled: enabled ?? true,
309
+ ...name !== void 0 ? { name } : {},
310
+ ...metadata !== void 0 ? { metadata } : {}
311
+ };
210
312
  }
211
- if (depth === 0) stripped += ch;
212
- prev = ch;
213
313
  }
214
- const keys = [];
215
- const keyRe = /(?:^|,)\s*(?:['"]([^'"]+)['"]|(\w+))\s*:/g;
216
- let m;
217
- while ((m = keyRe.exec(stripped)) !== null) {
218
- keys.push(m[1] || m[2]);
314
+ if (handlerNode) {
315
+ finding.defaultHandlerFound = true;
316
+ const body = handlerNode.body;
317
+ if (body) {
318
+ if (body.type === "BlockStatement") {
319
+ finding.scriptBody = content.slice(body.start + 1, body.end - 1).trim();
320
+ } else {
321
+ finding.scriptBody = `return ${content.slice(body.start, body.end)};`;
322
+ }
323
+ }
219
324
  }
220
- return keys;
325
+ return finding;
221
326
  }
222
327
  function parseHookConfig(content) {
223
- const configMatch = content.match(CONFIG_BLOCK_RE);
224
- if (!configMatch) return null;
225
- const externalId = configMatch[1].match(/external_id\s*:\s*['"]([^'"]+)['"]/)?.[1];
226
- const collection = configMatch[1].match(/collection\s*:\s*['"]([^'"]+)['"]/)?.[1];
227
- const trigger = configMatch[1].match(/trigger\s*:\s*['"]([^'"]+)['"]/)?.[1];
228
- const enabled = configMatch[1].match(/enabled\s*:\s*(true|false)/)?.[1];
229
- const name = configMatch[1].match(/name\s*:\s*['"]([^'"]+)['"]/)?.[1];
230
- if (!collection || !trigger) return null;
231
- const hook = {
232
- external_id: externalId || "",
233
- collection,
234
- trigger,
235
- enabled: enabled !== "false"
236
- };
237
- if (name) hook.name = name;
238
- const metadataMatch = configMatch[1].match(/metadata\s*:\s*(\{[^}]*\})/);
239
- if (metadataMatch) {
240
- try {
241
- const metaStr = metadataMatch[1].replace(/'/g, '"').replace(/(\w+)\s*:/g, '"$1":').replace(/,\s*}/g, "}");
242
- hook.metadata = JSON.parse(metaStr);
243
- } catch {
244
- }
245
- }
246
- return hook;
328
+ const finding = parseHookFile(content);
329
+ return finding.hook ?? null;
247
330
  }
248
331
  function extractHookScript(content) {
249
- const handlerMatch = content.match(
250
- /export\s+default\s+(?:async\s+)?function\s*(?:\w+)?\s*\([^)]*\)\s*\{([\s\S]*)\}[\s\n]*$/
251
- );
252
- if (handlerMatch) {
253
- return handlerMatch[1].trim();
254
- }
255
- const simpleMatch = content.match(
256
- /export\s+default\s+async\s+function[^{]*\{([\s\S]*)\}[\s\n]*$/
257
- );
258
- if (simpleMatch) {
259
- return simpleMatch[1].trim();
260
- }
261
- return content.replace(/export\s+const\s+config[\s\S]*?;/, "").trim();
262
- }
263
- function hasDefaultHandler(content) {
264
- return /export\s+default\s+(?:async\s+)?function\b/.test(content);
332
+ const finding = parseHookFile(content);
333
+ return finding.scriptBody ?? "";
265
334
  }
266
335
 
267
336
  // src/lib/lint/rules/hook-verify.ts
268
- function error2(target, message) {
337
+ function error2(target, message, line) {
269
338
  return {
270
339
  ruleId: "hook-verify",
271
340
  target,
272
341
  severity: "error",
273
- message
342
+ message,
343
+ ...line !== void 0 ? { line } : {}
274
344
  };
275
345
  }
346
+ function messageForIssue(issue) {
347
+ switch (issue.kind) {
348
+ case "missing-required-field":
349
+ return `Hook config is missing required field '${issue.field}'.`;
350
+ case "unknown-key":
351
+ return `Unknown config key '${issue.key}'. Known keys: ${[...KNOWN_HOOK_CONFIG_KEYS].join(", ")}.`;
352
+ case "invalid-trigger":
353
+ return `Invalid trigger '${issue.value}'. Must be one of: ${VALID_HOOK_TRIGGERS.join(", ")}.`;
354
+ case "invalid-enabled":
355
+ return `Invalid 'enabled' value \`${issue.rawSource}\`. Must be a literal \`true\` or \`false\`.`;
356
+ case "metadata-unparseable":
357
+ return `Could not parse 'metadata': ${issue.reason}. It must be an inline object literal with literal values (strings, numbers, booleans, null, arrays, or nested literal objects).`;
358
+ case "non-literal-field":
359
+ return `Field '${issue.field}' must be a string literal.`;
360
+ }
361
+ }
276
362
  var hookVerifyRule = {
277
363
  id: "hook-verify",
278
364
  description: "Validates hook file structure (config export, trigger, known keys, default handler) and verifies the script compiles via the backend.",
279
365
  appliesTo: ["hook"],
280
366
  async check(target, ctx) {
281
- const config = parseHookConfig(target.source);
282
- if (!config) {
367
+ const finding = parseHookFile(target.source);
368
+ if (finding.syntaxError) {
283
369
  return [
284
370
  error2(
285
371
  target,
286
- "Missing or unparseable `export const config = {...}`. Hook files must export a config object with at least `collection` and `trigger`."
372
+ `Could not parse hook file: ${finding.syntaxError.message}`,
373
+ finding.syntaxError.line
287
374
  )
288
375
  ];
289
376
  }
290
- const errors = [];
291
- if (!config.external_id && !ctx.appName) {
292
- errors.push(
377
+ if (!finding.configFound) {
378
+ return [
293
379
  error2(
294
380
  target,
295
- "Hook config is missing `external_id` and no app name is configured to derive one from the file name."
381
+ "Missing `export const config = {...}`. Hook files must export a config object with at least `collection` and `trigger`."
296
382
  )
297
- );
383
+ ];
298
384
  }
299
- if (!hasDefaultHandler(target.source)) {
385
+ const errors = [];
386
+ if (!finding.hook?.external_id && !ctx.appName) {
300
387
  errors.push(
301
388
  error2(
302
389
  target,
303
- "Missing default exported handler. Hook files must `export default function(ctx) { ... }` (async allowed)."
390
+ "Hook config is missing `external_id` and no app name is configured to derive one from the file name."
304
391
  )
305
392
  );
306
393
  }
307
- if (!VALID_HOOK_TRIGGERS.includes(config.trigger)) {
394
+ if (!finding.defaultHandlerFound) {
308
395
  errors.push(
309
396
  error2(
310
397
  target,
311
- `Invalid trigger '${config.trigger}'. Must be one of: ${VALID_HOOK_TRIGGERS.join(", ")}.`
398
+ "Missing default exported handler. Hook files must `export default function(ctx) { ... }` (async or arrow allowed)."
312
399
  )
313
400
  );
314
401
  }
315
- const configBody = extractConfigBody(target.source);
316
- if (configBody) {
317
- const keys = extractTopLevelConfigKeys(configBody);
318
- for (const key of keys) {
319
- if (!KNOWN_HOOK_CONFIG_KEYS.has(key)) {
320
- errors.push(
321
- error2(
322
- target,
323
- `Unknown config key '${key}'. Known keys: ${[...KNOWN_HOOK_CONFIG_KEYS].join(", ")}.`
324
- )
325
- );
326
- }
327
- }
328
- if (keys.includes("enabled")) {
329
- const m = configBody.match(/enabled\s*:\s*([^,}\n]+)/);
330
- const raw = m ? m[1].trim().replace(/,\s*$/, "").trim() : "";
331
- if (raw !== "true" && raw !== "false") {
332
- errors.push(
333
- error2(
334
- target,
335
- `Invalid 'enabled' value '${raw}'. Must be a literal \`true\` or \`false\`.`
336
- )
337
- );
338
- }
339
- }
340
- if (keys.includes("metadata") && !config.metadata) {
341
- errors.push(
342
- error2(
343
- target,
344
- "Could not parse 'metadata'. It must be an inline object literal with simple key/value pairs (e.g. `{ threshold: 100 }`); nested objects and non-literal values are not supported."
345
- )
346
- );
347
- }
402
+ for (const issue of finding.configIssues) {
403
+ errors.push(error2(target, messageForIssue(issue)));
348
404
  }
349
405
  if (errors.length > 0) return errors;
350
- let script = extractHookScript(target.source);
406
+ let script = finding.scriptBody ?? "";
351
407
  if (ctx.appName) {
352
408
  script = script.replaceAll("{{app}}", ctx.appName);
353
409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.19.9-dev.2",
3
+ "version": "0.19.9-dev.3",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -14,6 +14,7 @@
14
14
  "templates"
15
15
  ],
16
16
  "dependencies": {
17
+ "acorn": "^8.16.0",
17
18
  "archiver": "^7.0.1",
18
19
  "dotenv": "^16.4.7",
19
20
  "open": "^10.1.0",