@askrjs/cli 0.0.15 → 0.0.17

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
@@ -45,11 +45,12 @@ unless you opt out with `--no-skills`.
45
45
  - `askr doctor [--cwd <dir>] [--workspace <pattern>]... [--json]`
46
46
  - `askr repair [--cwd <dir>] [--workspace <pattern>]... [--json]`
47
47
  - `askr check [--cwd <dir>] [--workspace <pattern>]... [--json]`
48
+ - `askr database validate|generate [--database <name>] [--json]`
49
+ - `askr database migration create|status|plan|apply|resolve ...`
48
50
  - `askr skills list`
49
51
  - `askr skills install [--cwd <dir>] [--force]`
50
52
  - `askr skills sync [--cwd <dir>]`
51
53
  - `askr ssg --config <path> --output <dir> [--incremental]`
52
- - `askr verify-hydration [--output ./dist] [--route <path>]...`
53
54
  - `askr openapi [--entry ./src/api.ts] [--output ./openapi.yml] [--check]`
54
55
  - `askr outdated [packages...] [--workspace <glob>] [--tag <tag>] [--json]`
55
56
  - `askr update [packages...] [--workspace <glob>] [--tag <tag>] [--json]`
@@ -74,10 +75,8 @@ askr analyze --json --check
74
75
 
75
76
  All diagnostics include a stable rule ID and workspace-relative source
76
77
  location. The analyzer distinguishes canonical Askr imports from unrelated
77
- same-named functions and recommends `<For>` only when a `.map()` result is
78
- rendered directly as JSX children, so ordinary data transforms remain valid.
79
- It reports eager `<For>`/`<Show>`/`<Case>` controls behind changing ternaries
80
- while accepting conditionally mounted components with their own render scope.
78
+ same-named functions and only recommends `<For>` for state-backed reactive JSX
79
+ collections, so static transforms with `.map()` remain valid.
81
80
 
82
81
  By default it transactionally applies only mechanical route-parameter and
83
82
  plain-JSON JSX configuration fixes. `--check` is read-only for CI. Semantic
@@ -99,7 +98,17 @@ askr check
99
98
  and static-analysis inspection. `repair` transactionally applies only safe
100
99
  mechanical fixes and reports remaining semantic work. `check` requires clean
101
100
  analysis before running the project's declared lint, typecheck, test, and build
102
- scripts in order. Generated projects expose that final gate as `npm run check`.
101
+ scripts in order. When `database/index.ts` exists, `check` first delegates
102
+ database validation to the project's installed `@askrjs/orm` tooling.
103
+ Generated projects expose that final gate as `npm run check`.
104
+
105
+ ## Database tooling
106
+
107
+ `askr database ...` is a lazy front end. The CLI resolves
108
+ `@askrjs/orm/tooling` from the target project and delegates the complete
109
+ command, so schema generation and migration semantics always match the
110
+ project's installed ORM version. See the
111
+ [database command reference](./docs/database.md).
103
112
 
104
113
  See the [project guardrails reference](./docs/guardrails.md) for command and JSON
105
114
  contracts.
@@ -141,24 +150,6 @@ owned files so stale chunks and previous output paths are removed. Full and
141
150
  incremental builds publish through a sibling staging directory, so route output,
142
151
  metadata, assets, and sitemap artifacts change together or not at all.
143
152
 
144
- ## Hydration verification
145
-
146
- `askr verify-hydration` builds SSG output, serves the generated route set, and
147
- loads every successful metadata route in a real headless browser both with and
148
- without JavaScript. It compares normalized tag-and-child topology under `#app`
149
- after hydration, so text, classes, and mutable ARIA state do not create noise
150
- while nodes migrating into the wrong sibling container fail with an actionable
151
- static-versus-hydrated path diff.
152
-
153
- ```bash
154
- askr verify-hydration
155
- askr verify-hydration --route / --route /docs
156
- askr verify-hydration --no-build --output ./dist
157
- ```
158
-
159
- See the [hydration verification reference](./docs/verify-hydration.md) for
160
- browser installation, timeout, route, and root-selector options.
161
-
162
153
  ## OpenAPI artifacts
163
154
 
164
155
  `askr openapi` loads a TypeScript module whose default export exposes
package/dist/analyze.js CHANGED
@@ -71,7 +71,7 @@ async function runAnalyzeCli(args = process.argv.slice(2), io = console, runtime
71
71
  io.log(helpText.trimEnd());
72
72
  return 0;
73
73
  }
74
- const report = await (runtime.analyze ?? (await import("./runner-BSs9Ow3t.js")).runAnalysis)({
74
+ const report = await (runtime.analyze ?? (await import("./runner-SJnfoNvK.js")).runAnalysis)({
75
75
  cwd: parsed.cwd,
76
76
  workspacePatterns: parsed.workspacePatterns,
77
77
  check: parsed.check
package/dist/cli.js CHANGED
@@ -36,12 +36,12 @@ function printHelp(io = console) {
36
36
  io.log(" analyze Analyze Askr correctness and performance");
37
37
  io.log(" check Run the complete project validation path");
38
38
  io.log(" create Create a new Askr app from a template or product prompt");
39
+ io.log(" database Generate, validate, and migrate project databases");
39
40
  io.log(" doctor Diagnose environment and Askr project health");
40
41
  io.log(" generate Generate an @askrjs/fetch client from OpenAPI");
41
42
  io.log(" openapi Generate or check an OpenAPI YAML artifact");
42
43
  io.log(" skills Install or sync Askr agent skills");
43
44
  io.log(" ssg Run static-site generation");
44
- io.log(" verify-hydration Verify SSG DOM structure in a real browser");
45
45
  io.log(" outdated List available dependency updates");
46
46
  io.log(" repair Apply safe fixes and identify remaining semantic work");
47
47
  io.log(" update Apply safe dependency updates");
@@ -60,11 +60,11 @@ function printHelp(io = console) {
60
60
  io.log(" askr doctor");
61
61
  io.log(" askr repair");
62
62
  io.log(" askr check");
63
+ io.log(" askr database validate");
63
64
  io.log(" askr skills install");
64
65
  io.log(" askr openapi --check");
65
66
  io.log(" askr skills review foundation --cwd ./candidate-app");
66
67
  io.log(" askr ssg --config ./ssg.config.ts --output ./dist/static");
67
- io.log(" askr verify-hydration --output ./dist --route /");
68
68
  io.log(" askr outdated");
69
69
  io.log(" askr update");
70
70
  io.log(" askr upgrade");
@@ -87,12 +87,16 @@ async function runCli(args = process.argv.slice(2), io = console) {
87
87
  const { runAddCli } = await import("./add.js");
88
88
  return runAddCli(args.slice(1), io);
89
89
  }
90
+ if (command === "database") {
91
+ const { runDatabaseCommand } = await import("./database.js");
92
+ return runDatabaseCommand(args.slice(1), io);
93
+ }
90
94
  if (command === "analyze") {
91
95
  const { runAnalyzeCli } = await import("./analyze.js");
92
96
  return runAnalyzeCli(args.slice(1), io);
93
97
  }
94
98
  if (command === "check" || command === "doctor" || command === "repair") {
95
- const { runGuardrailCli } = await import("./guardrails-D6KCKS-H.js");
99
+ const { runGuardrailCli } = await import("./guardrails-Dq4-Rglh.js");
96
100
  return runGuardrailCli(command, args.slice(1), io);
97
101
  }
98
102
  if (command === "generate") {
@@ -103,10 +107,6 @@ async function runCli(args = process.argv.slice(2), io = console) {
103
107
  const { runSsgCli } = await import("./ssg.js");
104
108
  return runSsgCli(args.slice(1), void 0, io);
105
109
  }
106
- if (command === "verify-hydration") {
107
- const { runVerifyHydrationCli } = await import("./verify-hydration.js");
108
- return runVerifyHydrationCli(args.slice(1), void 0, io);
109
- }
110
110
  if (command === "openapi") {
111
111
  const { runOpenApiCli } = await import("./openapi.js");
112
112
  return runOpenApiCli(args.slice(1), io);
@@ -0,0 +1,18 @@
1
+ //#region src/bin/database.d.ts
2
+ type CliIo = Pick<Console, "error" | "log">;
3
+ interface OrmTooling {
4
+ runDatabaseCli(args: readonly string[], options: {
5
+ readonly cwd: string;
6
+ readonly io: CliIo;
7
+ }): Promise<number>;
8
+ }
9
+ declare function loadOrmTooling(cwd: string): Promise<OrmTooling>;
10
+ declare function runDatabaseCommand(args: readonly string[], io?: CliIo, cwd?: string, loader?: (cwd: string) => Promise<OrmTooling>): Promise<number>;
11
+ declare function runDatabaseValidation(cwd: string, loader?: (cwd: string) => Promise<OrmTooling>): Promise<{
12
+ readonly status: "passed" | "failed";
13
+ readonly exitCode: number;
14
+ readonly stdout: string;
15
+ readonly stderr: string;
16
+ }>;
17
+ //#endregion
18
+ export { OrmTooling, loadOrmTooling, runDatabaseCommand, runDatabaseValidation };
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ //#region src/bin/database.ts
7
+ async function loadOrmTooling(cwd) {
8
+ const require = createRequire(path.join(path.resolve(cwd), "package.json"));
9
+ let entry;
10
+ try {
11
+ const manifestPath = require.resolve("@askrjs/orm/package.json");
12
+ const toolingExport = JSON.parse(await fs.readFile(manifestPath, "utf8")).exports?.["./tooling"];
13
+ const relative = typeof toolingExport === "string" ? toolingExport : toolingExport?.import;
14
+ if (!relative) throw new Error("Missing import export for @askrjs/orm/tooling.");
15
+ entry = path.resolve(path.dirname(manifestPath), relative);
16
+ } catch (error) {
17
+ throw new Error("This project does not have @askrjs/orm installed. Install it before running `askr database`.", { cause: error });
18
+ }
19
+ const tooling = await import(pathToFileURL(entry).href);
20
+ if (typeof tooling.runDatabaseCli !== "function") throw new Error("The installed @askrjs/orm package does not expose compatible database tooling.");
21
+ return tooling;
22
+ }
23
+ async function runDatabaseCommand(args, io = console, cwd = process.cwd(), loader = loadOrmTooling) {
24
+ try {
25
+ return (await loader(cwd)).runDatabaseCli(args, {
26
+ cwd,
27
+ io
28
+ });
29
+ } catch (error) {
30
+ io.error(error instanceof Error ? error.message : String(error));
31
+ return 1;
32
+ }
33
+ }
34
+ async function runDatabaseValidation(cwd, loader = loadOrmTooling) {
35
+ const stdout = [];
36
+ const stderr = [];
37
+ const code = await runDatabaseCommand([
38
+ "validate",
39
+ "--cwd",
40
+ cwd
41
+ ], {
42
+ log: (...values) => stdout.push(values.join(" ")),
43
+ error: (...values) => stderr.push(values.join(" "))
44
+ }, cwd, loader);
45
+ return {
46
+ status: code === 0 ? "passed" : "failed",
47
+ exitCode: code,
48
+ stdout: stdout.join("\n"),
49
+ stderr: stderr.join("\n")
50
+ };
51
+ }
52
+ //#endregion
53
+ export { loadOrmTooling, runDatabaseCommand, runDatabaseValidation };
package/dist/generate.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
2
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import { lookup } from "node:dns/promises";
5
5
  import { request } from "node:https";
@@ -296,10 +296,16 @@ async function fetchSource(uri, options) {
296
296
  }
297
297
  async function readSource(uri, options) {
298
298
  if (uri.startsWith("http://") || uri.startsWith("https://")) return fetchSource(uri, options);
299
- const path = fileURLToPath(uri);
299
+ let path;
300
+ try {
301
+ path = fileURLToPath(uri);
302
+ } catch {
303
+ throw new GenerationError(`Local OpenAPI reference escapes the specification directory: ${uri}`);
304
+ }
300
305
  const canonical = await realpath(path);
301
- const relativePath = options.localRootDirectory ? relative(options.localRootDirectory, canonical) : "..";
302
- if (!options.localRootDirectory || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) throw new GenerationError(`Local OpenAPI reference escapes the specification directory: ${path}`);
306
+ if (!options.localRootDirectory) throw new GenerationError(`Local OpenAPI reference escapes the specification directory: ${path}`);
307
+ const relativePath = relative(options.localRootDirectory, canonical);
308
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) throw new GenerationError(`Local OpenAPI reference escapes the specification directory: ${path}`);
303
309
  const contents = await readFile(canonical);
304
310
  if (contents.byteLength > options.maxBytes) throw new GenerationError(`OpenAPI reference exceeds ${options.maxBytes} bytes: ${path}`);
305
311
  return {
@@ -98,7 +98,7 @@ async function runGuardrailCli(command, args = process.argv.slice(2), io = conso
98
98
  cwd: parsed.cwd,
99
99
  workspacePatterns: parsed.workspacePatterns
100
100
  };
101
- const { runCheck, runDoctor, runRepair } = await import("./runner-DetGSvGf.js");
101
+ const { runCheck, runDoctor, runRepair } = await import("./runner-H-TW4lHZ.js");
102
102
  const report = command === "doctor" ? await runDoctor(options, runtime) : command === "repair" ? await runRepair(options) : await runCheck(options, runtime);
103
103
  if (parsed.json) io.log(JSON.stringify(report));
104
104
  else if (report.command === "doctor") printDoctor(report, io);
@@ -294,11 +294,7 @@ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tag
294
294
  return;
295
295
  }
296
296
  const next = remaining.map((name) => [name, pruned.get(name)]).sort(([leftName, left], [rightName, right]) => left.length - right.length || leftName.localeCompare(rightName))[0];
297
- const signature = component.map((name) => {
298
- const assignedVersion = assigned.get(name);
299
- const domain = pruned.get(name) ?? [];
300
- return `${name}:assigned=${JSON.stringify(assignedVersion ?? null)}:domain=${JSON.stringify(domain)}`;
301
- }).join("|");
297
+ const signature = component.map((name) => assigned.has(name) ? `${name}=assigned:${assigned.get(name)}` : `${name}=domain:${(pruned.get(name) ?? []).join(",")}`).join("|");
302
298
  if (memo.has(signature)) return;
303
299
  memo.add(signature);
304
300
  for (const version of next[1]) {
@@ -1,6 +1,6 @@
1
1
  import { t as inspectBundledSkills } from "./skills-CSGdAZHN.js";
2
2
  import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
3
- import { analysisHasBlockingFindings, runAnalysis } from "./runner-BSs9Ow3t.js";
3
+ import { analysisHasBlockingFindings, runAnalysis } from "./runner-SJnfoNvK.js";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { spawn } from "node:child_process";
@@ -229,16 +229,38 @@ async function runCheck(options, runtime = {}) {
229
229
  const selected = VALIDATION_SCRIPTS.filter((name) => scripts[name]);
230
230
  const manager = managerCommand(await existingLockfiles(project.root), rootWorkspace.manifest);
231
231
  const results = [];
232
- if (analysisHasBlockingFindings(analysis)) for (const name of selected) results.push({
232
+ const selectedChecks = [...Boolean(await fs.stat(path.join(project.root, "database", "index.ts")).catch(() => null)) ? ["database"] : [], ...selected];
233
+ if (analysisHasBlockingFindings(analysis)) for (const name of selectedChecks) results.push({
233
234
  name,
234
235
  status: "skipped",
235
- command: `${manager.executable} run ${name}`,
236
+ command: name === "database" ? "askr database validate" : `${manager.executable} run ${name}`,
236
237
  exitCode: null,
237
238
  stdout: "",
238
239
  stderr: "",
239
240
  reason: "static analysis must pass first"
240
241
  });
241
- else for (const [index, name] of selected.entries()) {
242
+ else for (const [index, name] of selectedChecks.entries()) {
243
+ if (name === "database") {
244
+ const result = await (runtime.runDatabaseValidation ?? (await import("./database.js")).runDatabaseValidation)(project.root);
245
+ results.push({
246
+ name,
247
+ command: "askr database validate",
248
+ ...result
249
+ });
250
+ if (result.status === "failed") {
251
+ for (const skipped of selectedChecks.slice(index + 1)) results.push({
252
+ name: skipped,
253
+ status: "skipped",
254
+ command: `${manager.executable} run ${skipped}`,
255
+ exitCode: null,
256
+ stdout: "",
257
+ stderr: "",
258
+ reason: "database validation failed"
259
+ });
260
+ break;
261
+ }
262
+ continue;
263
+ }
242
264
  const args = manager.args(name);
243
265
  const result = await (runtime.runScript ?? defaultRunScript)(manager.executable, args, project.root);
244
266
  results.push({
@@ -247,7 +269,7 @@ async function runCheck(options, runtime = {}) {
247
269
  ...result
248
270
  });
249
271
  if (result.status === "failed") {
250
- for (const skipped of selected.slice(index + 1)) results.push({
272
+ for (const skipped of selectedChecks.slice(index + 1)) results.push({
251
273
  name: skipped,
252
274
  status: "skipped",
253
275
  command: `${manager.executable} run ${skipped}`,