@askrjs/cli 0.0.10 → 0.0.12

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.
Files changed (50) hide show
  1. package/README.md +50 -0
  2. package/dist/add.d.ts +1 -10
  3. package/dist/add.js +1 -63
  4. package/dist/analyze.d.ts +88 -0
  5. package/dist/analyze.js +97 -0
  6. package/dist/cli.js +16 -0
  7. package/dist/create.js +1 -1
  8. package/dist/{discovery-BX-lnFRK.js → discovery-DUDrZCIC.js} +11 -2
  9. package/dist/file-changes-BAFLhEHZ.js +66 -0
  10. package/dist/file-changes-cmFN-rF8.d.ts +11 -0
  11. package/dist/guardrails-GPM9PJqO.js +121 -0
  12. package/dist/{planner-BDfYDKnI.js → planner-BEfWRd0x.js} +3 -3
  13. package/dist/runner-BRjjPhZY.js +1495 -0
  14. package/dist/runner-Ca43qqzG.js +289 -0
  15. package/dist/skills/askr-ssr-ssg/SKILL.md +2 -0
  16. package/dist/{skills-C2KzfTl9.js → skills-CSGdAZHN.js} +50 -1
  17. package/dist/skills.d.ts +12 -1
  18. package/dist/skills.js +2 -2
  19. package/dist/ssg.d.ts +1 -0
  20. package/dist/ssg.js +5 -4
  21. package/dist/templates/full-stack/AGENTS.md +6 -0
  22. package/dist/templates/full-stack/package.json +2 -1
  23. package/dist/templates/spa/AGENTS.md +7 -1
  24. package/dist/templates/spa/README.md +1 -1
  25. package/dist/templates/spa/package.json +2 -1
  26. package/dist/templates/spa/src/main.tsx +1 -1
  27. package/dist/templates/spa/src/pages/app/_layout.tsx +11 -8
  28. package/dist/templates/spa/src/pages/app/admin-home.tsx +97 -90
  29. package/dist/templates/spa/src/pages/app/agent-runs.tsx +33 -26
  30. package/dist/templates/spa/src/pages/public/home.tsx +12 -9
  31. package/dist/templates/ssg/AGENTS.md +6 -0
  32. package/dist/templates/ssg/package.json +2 -1
  33. package/dist/templates/ssg/src/components/site-shell.tsx +4 -3
  34. package/dist/templates/ssg/src/pages/about.tsx +14 -11
  35. package/dist/templates/ssg/src/pages/content.tsx +10 -7
  36. package/dist/templates/ssg/src/pages/example.tsx +0 -2
  37. package/dist/templates/ssg/src/pages/home.tsx +6 -3
  38. package/dist/templates/ssr/AGENTS.md +6 -0
  39. package/dist/templates/ssr/package.json +2 -1
  40. package/dist/templates/startkit/AGENTS.md +3 -0
  41. package/dist/templates/startkit/package.json +2 -1
  42. package/dist/templates/startkit/src/components/app-sidebar.tsx +23 -18
  43. package/dist/templates/startkit/src/components/data-table.tsx +8 -6
  44. package/dist/templates/startkit/src/pages/workspace/dashboard.tsx +16 -13
  45. package/dist/templates/startkit/src/routes/auth.ts +1 -2
  46. package/dist/templates/startkit/src/routes/public.ts +1 -2
  47. package/dist/templates/startkit/src/routes/workspace/accounts.ts +1 -2
  48. package/dist/templates/startkit/src/routes/workspace/index.ts +2 -3
  49. package/dist/update.js +1 -1
  50. package/package.json +7 -6
@@ -0,0 +1,289 @@
1
+ import { t as inspectBundledSkills } from "./skills-CSGdAZHN.js";
2
+ import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
3
+ import { analysisHasBlockingFindings, runAnalysis } from "./runner-BRjjPhZY.js";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import semver from "semver";
8
+ //#region src/guardrails/runner.ts
9
+ const SUPPORTED_NODE_RANGE = "^20.19.0 || >=22.12.0";
10
+ const VALIDATION_SCRIPTS = [
11
+ "lint",
12
+ "typecheck",
13
+ "test",
14
+ "build"
15
+ ];
16
+ function summary(findings) {
17
+ return {
18
+ passed: findings.filter((entry) => entry.status === "pass").length,
19
+ warnings: findings.filter((entry) => entry.status === "warning").length,
20
+ errors: findings.filter((entry) => entry.status === "error").length
21
+ };
22
+ }
23
+ function dependencyRecord(manifest, section) {
24
+ const value = manifest[section];
25
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
26
+ }
27
+ function hasAskrDependency(manifest) {
28
+ return [
29
+ "dependencies",
30
+ "devDependencies",
31
+ "peerDependencies",
32
+ "optionalDependencies"
33
+ ].some((section) => "@askrjs/askr" in dependencyRecord(manifest, section));
34
+ }
35
+ async function existingLockfiles(root) {
36
+ return (await Promise.all([
37
+ "package-lock.json",
38
+ "pnpm-lock.yaml",
39
+ "yarn.lock",
40
+ "bun.lock",
41
+ "bun.lockb"
42
+ ].map(async (name) => ({
43
+ name,
44
+ exists: Boolean(await fs.stat(path.join(root, name)).catch(() => null))
45
+ })))).filter((entry) => entry.exists).map((entry) => entry.name);
46
+ }
47
+ function declaredPackageManager(manifest) {
48
+ if (typeof manifest.packageManager !== "string") return null;
49
+ return /^(npm|pnpm|yarn|bun)@/.exec(manifest.packageManager)?.[1] ?? null;
50
+ }
51
+ function lockfilePackageManager(lockfile) {
52
+ if (lockfile === "package-lock.json") return "npm";
53
+ if (lockfile === "pnpm-lock.yaml") return "pnpm";
54
+ if (lockfile === "yarn.lock") return "yarn";
55
+ return "bun";
56
+ }
57
+ function packageManagerFinding(lockfiles, manifest) {
58
+ const declared = declaredPackageManager(manifest);
59
+ if (lockfiles.length > 1) return {
60
+ id: "askr/doctor-package-manager",
61
+ status: "error",
62
+ message: `Multiple package-manager lockfiles are present: ${lockfiles.join(", ")}.`,
63
+ remediation: "Keep the lockfile for the project's canonical package manager."
64
+ };
65
+ if (lockfiles.length === 0) return {
66
+ id: "askr/doctor-package-manager",
67
+ status: "warning",
68
+ message: "No package-manager lockfile is present.",
69
+ remediation: "Install dependencies with the project's chosen package manager."
70
+ };
71
+ const detected = lockfilePackageManager(lockfiles[0]);
72
+ if (declared && declared !== detected) return {
73
+ id: "askr/doctor-package-manager",
74
+ status: "error",
75
+ message: `packageManager declares ${declared}, but ${lockfiles[0]} belongs to ${detected}.`,
76
+ remediation: "Align packageManager and the committed lockfile."
77
+ };
78
+ return {
79
+ id: "askr/doctor-package-manager",
80
+ status: "pass",
81
+ message: `Package manager is ${declared ?? detected} (${lockfiles[0]}).`
82
+ };
83
+ }
84
+ async function skillsFinding(root) {
85
+ const status = await inspectBundledSkills({ cwd: root });
86
+ if (status.installed === 0) return {
87
+ id: "askr/doctor-agent-guidance",
88
+ status: "warning",
89
+ message: "Project-local Askr agent skills are not installed.",
90
+ remediation: "Run `askr skills install`, or `askr skills sync` for an existing project."
91
+ };
92
+ if (!status.current) return {
93
+ id: "askr/doctor-agent-guidance",
94
+ status: "warning",
95
+ message: `Project-local Askr agent skills are stale (${[
96
+ status.missing.length > 0 ? `${status.missing.length} missing` : "",
97
+ status.modified.length > 0 ? `${status.modified.length} modified` : "",
98
+ status.obsolete.length > 0 ? `${status.obsolete.length} obsolete` : ""
99
+ ].filter(Boolean).join(", ")}).`,
100
+ remediation: "Run `askr skills sync` to restore the current bundled guidance."
101
+ };
102
+ return {
103
+ id: "askr/doctor-agent-guidance",
104
+ status: "pass",
105
+ message: `${status.bundled} project-local Askr agent skill(s) are current.`
106
+ };
107
+ }
108
+ function analysisFinding(analysis) {
109
+ if (analysis.summary.errors > 0) return {
110
+ id: "askr/doctor-analysis",
111
+ status: "error",
112
+ message: `Static analysis found ${analysis.summary.errors} error(s) and ${analysis.summary.warnings} warning(s).`,
113
+ remediation: "Run `askr repair`, review remaining findings, then run `askr check`."
114
+ };
115
+ if (analysis.summary.warnings > 0) return {
116
+ id: "askr/doctor-analysis",
117
+ status: "warning",
118
+ message: `Static analysis found ${analysis.summary.warnings} warning(s).`,
119
+ remediation: "Review the reported performance or lifecycle guidance."
120
+ };
121
+ return {
122
+ id: "askr/doctor-analysis",
123
+ status: "pass",
124
+ message: `Static analysis is clean across ${analysis.workspaces.length} workspace(s).`
125
+ };
126
+ }
127
+ async function runDoctor(options, runtime = {}) {
128
+ const project = await discoverWorkspaceProject({
129
+ cwd: options.cwd,
130
+ workspacePatterns: [...options.workspacePatterns]
131
+ });
132
+ const analysis = await runAnalysis({
133
+ cwd: options.cwd,
134
+ workspacePatterns: [...options.workspacePatterns],
135
+ check: true
136
+ });
137
+ const rootWorkspace = project.workspaces.find((workspace) => workspace.isRoot);
138
+ if (!rootWorkspace) throw new Error("Discovered project is missing its root workspace.");
139
+ const nodeVersion = runtime.nodeVersion ?? process.versions.node;
140
+ const findings = [semver.satisfies(nodeVersion, SUPPORTED_NODE_RANGE) ? {
141
+ id: "askr/doctor-node",
142
+ status: "pass",
143
+ message: `Node ${nodeVersion} satisfies ${SUPPORTED_NODE_RANGE}.`
144
+ } : {
145
+ id: "askr/doctor-node",
146
+ status: "error",
147
+ message: `Node ${nodeVersion} does not satisfy ${SUPPORTED_NODE_RANGE}.`,
148
+ remediation: `Install a Node version matching ${SUPPORTED_NODE_RANGE}.`
149
+ }, packageManagerFinding(await existingLockfiles(project.root), rootWorkspace.manifest)];
150
+ const sourceWorkspaces = new Set(analysis.workspaces.filter((workspace) => workspace.files > 0).map((workspace) => workspace.name));
151
+ const missingFramework = project.selectedWorkspaces.filter((workspace) => sourceWorkspaces.has(workspace.name) && !hasAskrDependency(workspace.manifest));
152
+ findings.push(missingFramework.length === 0 ? {
153
+ id: "askr/doctor-framework-dependency",
154
+ status: "pass",
155
+ message: "Source workspaces declare @askrjs/askr."
156
+ } : {
157
+ id: "askr/doctor-framework-dependency",
158
+ status: "error",
159
+ message: `Source workspace(s) do not declare @askrjs/askr: ${missingFramework.map((workspace) => workspace.name).join(", ")}.`,
160
+ remediation: "Declare @askrjs/askr in each application workspace."
161
+ });
162
+ findings.push(await skillsFinding(project.root), analysisFinding(analysis));
163
+ return {
164
+ schemaVersion: 1,
165
+ command: "doctor",
166
+ root: project.root,
167
+ findings,
168
+ analysis,
169
+ summary: summary(findings)
170
+ };
171
+ }
172
+ function scriptsFromManifest(manifest) {
173
+ const scripts = manifest.scripts;
174
+ if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) return {};
175
+ return Object.fromEntries(Object.entries(scripts).filter((entry) => typeof entry[1] === "string"));
176
+ }
177
+ async function defaultRunScript(executable, args, cwd) {
178
+ return new Promise((resolve, reject) => {
179
+ const child = spawn(executable, [...args], {
180
+ cwd,
181
+ env: {
182
+ ...process.env,
183
+ NO_COLOR: "1"
184
+ },
185
+ stdio: [
186
+ "ignore",
187
+ "pipe",
188
+ "pipe"
189
+ ]
190
+ });
191
+ let stdout = "";
192
+ let stderr = "";
193
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
194
+ stdout += chunk;
195
+ });
196
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
197
+ stderr += chunk;
198
+ });
199
+ child.on("error", reject);
200
+ child.on("close", (code) => {
201
+ resolve({
202
+ status: code === 0 ? "passed" : "failed",
203
+ exitCode: code,
204
+ stdout,
205
+ stderr
206
+ });
207
+ });
208
+ });
209
+ }
210
+ function managerCommand(lockfiles, manifest) {
211
+ return {
212
+ executable: declaredPackageManager(manifest) ?? (lockfiles.length === 1 ? lockfilePackageManager(lockfiles[0]) : "npm"),
213
+ args: (script) => ["run", script]
214
+ };
215
+ }
216
+ async function runCheck(options, runtime = {}) {
217
+ const project = await discoverWorkspaceProject({
218
+ cwd: options.cwd,
219
+ workspacePatterns: [...options.workspacePatterns]
220
+ });
221
+ const rootWorkspace = project.workspaces.find((workspace) => workspace.isRoot);
222
+ if (!rootWorkspace) throw new Error("Discovered project is missing its root workspace.");
223
+ const analysis = await runAnalysis({
224
+ cwd: options.cwd,
225
+ workspacePatterns: [...options.workspacePatterns],
226
+ check: true
227
+ });
228
+ const scripts = scriptsFromManifest(rootWorkspace.manifest);
229
+ const selected = VALIDATION_SCRIPTS.filter((name) => scripts[name]);
230
+ const manager = managerCommand(await existingLockfiles(project.root), rootWorkspace.manifest);
231
+ const results = [];
232
+ if (analysisHasBlockingFindings(analysis)) for (const name of selected) results.push({
233
+ name,
234
+ status: "skipped",
235
+ command: `${manager.executable} run ${name}`,
236
+ exitCode: null,
237
+ stdout: "",
238
+ stderr: "",
239
+ reason: "static analysis must pass first"
240
+ });
241
+ else for (const [index, name] of selected.entries()) {
242
+ const args = manager.args(name);
243
+ const result = await (runtime.runScript ?? defaultRunScript)(manager.executable, args, project.root);
244
+ results.push({
245
+ name,
246
+ command: [manager.executable, ...args].join(" "),
247
+ ...result
248
+ });
249
+ if (result.status === "failed") {
250
+ for (const skipped of selected.slice(index + 1)) results.push({
251
+ name: skipped,
252
+ status: "skipped",
253
+ command: `${manager.executable} run ${skipped}`,
254
+ exitCode: null,
255
+ stdout: "",
256
+ stderr: "",
257
+ reason: `${name} failed`
258
+ });
259
+ break;
260
+ }
261
+ }
262
+ const failed = analysisHasBlockingFindings(analysis) || results.some((entry) => entry.status === "failed");
263
+ return {
264
+ schemaVersion: 1,
265
+ command: "check",
266
+ root: project.root,
267
+ analysis,
268
+ scripts: results,
269
+ status: failed ? "failed" : "passed"
270
+ };
271
+ }
272
+ async function runRepair(options) {
273
+ const analysis = await runAnalysis({
274
+ cwd: options.cwd,
275
+ workspacePatterns: [...options.workspacePatterns],
276
+ check: false
277
+ });
278
+ const needsReview = analysisHasBlockingFindings(analysis);
279
+ return {
280
+ schemaVersion: 1,
281
+ command: "repair",
282
+ root: analysis.root,
283
+ analysis,
284
+ status: needsReview ? "needs-review" : "clean",
285
+ nextAction: needsReview ? "Review the remaining semantic diagnostics, then run `askr repair` again." : "Run `askr check` to execute the full validation path."
286
+ };
287
+ }
288
+ //#endregion
289
+ export { runCheck, runDoctor, runRepair };
@@ -34,6 +34,8 @@ Use this when the app renders outside the browser or produces static output. The
34
34
  ## Copy This Shape
35
35
 
36
36
  ```ts
37
+ import { createRouteRegistry } from "@askrjs/askr/router";
38
+
37
39
  const registry = createRouteRegistry(() => {
38
40
  page("/docs/{slug}", DocsPage, {
39
41
  entries: async () => [{ slug: "getting-started" }, { slug: "routing" }],
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { constants } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { createHash } from "node:crypto";
7
8
  //#region src/bin/skill-review.ts
8
9
  const TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
9
10
  ".cjs",
@@ -303,6 +304,7 @@ const REVIEW_PROMPTS = [
303
304
  re("local Sidebar", String.raw`export\s+(?:default\s+)?function\s+Sidebar\b`)
304
305
  ]),
305
306
  requireAny("Keeps Askr-native route or state primitives in use.", [
307
+ re("registerRoutes", String.raw`\bregisterRoutes\s*\(`),
306
308
  re("createRouteRegistry", String.raw`\bcreateRouteRegistry\s*\(`),
307
309
  re("state()", String.raw`\bstate\s*\(`),
308
310
  re("resource()", String.raw`\bresource\s*\(`)
@@ -554,6 +556,53 @@ async function listBundledSkills() {
554
556
  const source = await findBundledSkillsDir();
555
557
  return (await fs.readdir(source, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
556
558
  }
559
+ async function directoryFingerprint(directory) {
560
+ if (!(await fs.stat(directory).catch(() => null))?.isDirectory()) return null;
561
+ const hash = createHash("sha256");
562
+ const visit = async (current, relative) => {
563
+ const entries = await fs.readdir(current, { withFileTypes: true });
564
+ entries.sort((left, right) => left.name.localeCompare(right.name));
565
+ for (const entry of entries) {
566
+ const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
567
+ const child = path.join(current, entry.name);
568
+ hash.update(`${entry.isDirectory() ? "d" : "f"}:${childRelative}\0`);
569
+ if (entry.isDirectory()) await visit(child, childRelative);
570
+ else if (entry.isFile()) hash.update(await fs.readFile(child));
571
+ else if (entry.isSymbolicLink()) hash.update(await fs.readlink(child));
572
+ }
573
+ };
574
+ await visit(directory, "");
575
+ return hash.digest("hex");
576
+ }
577
+ async function inspectBundledSkills(options = {}) {
578
+ const root = path.resolve(options.cwd ?? process.cwd());
579
+ const sourceRoot = await findBundledSkillsDir();
580
+ const targetRoot = path.join(root, PROJECT_SKILLS_DIR);
581
+ const bundledNames = await listBundledSkills();
582
+ const targetEntries = await fs.readdir(targetRoot, { withFileTypes: true }).catch(() => []);
583
+ const targetDirectories = new Set(targetEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
584
+ const missing = [];
585
+ const modified = [];
586
+ for (const name of bundledNames) {
587
+ if (!targetDirectories.has(name)) {
588
+ missing.push(name);
589
+ continue;
590
+ }
591
+ const [sourceFingerprint, targetFingerprint] = await Promise.all([directoryFingerprint(path.join(sourceRoot, name)), directoryFingerprint(path.join(targetRoot, name))]);
592
+ if (sourceFingerprint !== targetFingerprint) modified.push(name);
593
+ }
594
+ const bundledSet = new Set(bundledNames);
595
+ const obsolete = [...targetDirectories].filter((name) => name.startsWith(MANAGED_PREFIX) && !bundledSet.has(name)).sort();
596
+ const installed = bundledNames.filter((name) => targetDirectories.has(name)).length;
597
+ return {
598
+ bundled: bundledNames.length,
599
+ installed,
600
+ missing,
601
+ modified,
602
+ obsolete,
603
+ current: missing.length === 0 && modified.length === 0 && obsolete.length === 0
604
+ };
605
+ }
557
606
  async function copyDir(src, dest) {
558
607
  const entries = await fs.readdir(src, { withFileTypes: true });
559
608
  await fs.mkdir(dest, { recursive: true });
@@ -741,4 +790,4 @@ async function main() {
741
790
  }
742
791
  if (isDirectExecution(import.meta.url)) main();
743
792
  //#endregion
744
- export { runSkillsCli as n, syncBundledSkills as r, installBundledSkills as t };
793
+ export { syncBundledSkills as i, installBundledSkills as n, runSkillsCli as r, inspectBundledSkills as t };
package/dist/skills.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  //#region src/bin/skills.d.ts
2
2
  type CliIo = Pick<Console, "error" | "log">;
3
+ interface BundledSkillStatus {
4
+ readonly bundled: number;
5
+ readonly installed: number;
6
+ readonly missing: readonly string[];
7
+ readonly modified: readonly string[];
8
+ readonly obsolete: readonly string[];
9
+ readonly current: boolean;
10
+ }
11
+ declare function inspectBundledSkills(options?: {
12
+ cwd?: string;
13
+ }): Promise<BundledSkillStatus>;
3
14
  declare function installBundledSkills(options?: {
4
15
  cwd?: string;
5
16
  force?: boolean;
@@ -15,4 +26,4 @@ declare function syncBundledSkills(options?: {
15
26
  }>;
16
27
  declare function runSkillsCli(args?: string[], io?: CliIo): Promise<number>;
17
28
  //#endregion
18
- export { installBundledSkills, runSkillsCli, syncBundledSkills };
29
+ export { BundledSkillStatus, inspectBundledSkills, installBundledSkills, runSkillsCli, syncBundledSkills };
package/dist/skills.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { n as runSkillsCli, r as syncBundledSkills, t as installBundledSkills } from "./skills-C2KzfTl9.js";
3
- export { installBundledSkills, runSkillsCli, syncBundledSkills };
2
+ import { i as syncBundledSkills, n as installBundledSkills, r as runSkillsCli, t as inspectBundledSkills } from "./skills-CSGdAZHN.js";
3
+ export { inspectBundledSkills, installBundledSkills, runSkillsCli, syncBundledSkills };
package/dist/ssg.d.ts CHANGED
@@ -38,6 +38,7 @@ interface SsgDeps {
38
38
  existsSync?: typeof existsSync;
39
39
  importConfig?: (filePath: string) => Promise<unknown>;
40
40
  createStaticGen?: (options: {
41
+ routes?: unknown[];
41
42
  registry?: unknown;
42
43
  outputDir: string;
43
44
  seed?: unknown;
package/dist/ssg.js CHANGED
@@ -458,8 +458,9 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
458
458
  }
459
459
  const configModule = imported;
460
460
  const candidate = configModule.default ?? configModule.staticConfig ?? configModule;
461
- if (!(candidate.registry !== void 0) || Object.prototype.hasOwnProperty.call(candidate, "routes")) {
462
- io.error("Error: Config must provide a route registry and no raw routes array");
461
+ const hasRoutes = Array.isArray(candidate.routes);
462
+ if (hasRoutes === (candidate.registry !== void 0)) {
463
+ io.error("Error: Config must provide exactly one route source: routes or registry");
463
464
  return 1;
464
465
  }
465
466
  const config = candidate;
@@ -467,7 +468,7 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
467
468
  io.error("Error: Config must provide siteUrl to generate sitemap.xml, or set sitemap: false");
468
469
  return 1;
469
470
  }
470
- io.log("Generating registered routes...");
471
+ io.log(hasRoutes ? `Generating ${config.routes?.length ?? 0} routes...` : "Generating registered routes...");
471
472
  const createStaticGen = typeof resolvedDeps.createStaticGen === "function" ? resolvedDeps.createStaticGen : await loadCreateStaticGen();
472
473
  cliStagingDir = await createSiblingStage(resolvedOutputDir, "askr-ssg");
473
474
  if (parsed.incremental && !parsed.forceFull && await pathExists(resolvedOutputDir)) await fs$1.cp(resolvedOutputDir, cliStagingDir, {
@@ -476,7 +477,7 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
476
477
  });
477
478
  const generationOutputDir = cliStagingDir;
478
479
  const ssg = createStaticGen({
479
- registry: config.registry,
480
+ ...hasRoutes ? { routes: config.routes } : { registry: config.registry },
480
481
  outputDir: generationOutputDir,
481
482
  seed: config.seed,
482
483
  dataOverrides: config.dataOverrides,
@@ -9,3 +9,9 @@ This project is the progressive full-stack stage of an Askr application.
9
9
  - Keep `index.html` as the only document source. Preserve exactly one `<!--askr-head-->` and one `<!--askr-app-->` marker.
10
10
  - Use native forms first; enhanced submission must preserve the same validation and authorization behavior.
11
11
  - Never log cookies, authorization values, tokens, form fields, request bodies, or personal data.
12
+
13
+ ## Recovery and completion
14
+
15
+ - Run `askr repair` after analyzer failures; it applies only safe mechanical fixes.
16
+ - Resolve remaining semantic diagnostics deliberately.
17
+ - Run `npm run check` before declaring work complete. It requires clean Askr analysis, then runs lint, typecheck, tests, and build.
@@ -10,8 +10,9 @@
10
10
  "test": "vp test run",
11
11
  "typecheck": "tsc --noEmit",
12
12
  "lint": "vp lint src server.ts tests vite.config.ts",
13
+ "analyze": "askr analyze --check",
13
14
  "fmt": "vp fmt .",
14
- "check": "npm run lint && npm run typecheck && npm test && npm run build"
15
+ "check": "askr check"
15
16
  },
16
17
  "dependencies": {
17
18
  "@askrjs/askr": ">=0.0.53 <0.1.0",
@@ -16,7 +16,7 @@ npm run fmt # Prettier
16
16
 
17
17
  ## Architecture
18
18
 
19
- - **Routing:** `src/main.tsx` imports `src/pages/_routes.tsx`, then boots `createSPA()` with the route manifest. Route branches live under `src/pages/public`, `src/pages/auth`, and `src/pages/app`.
19
+ - **Routing:** `src/main.tsx` imports the `pageRegistry` from `src/pages/_routes.tsx`, then passes it to `createSPA()`. Route branches live under `src/pages/public`, `src/pages/auth`, and `src/pages/app`.
20
20
  - **Layouts:** `_layout.tsx` files own shells. The root layout owns `ThemeScope`; public layouts own landing chrome, auth layouts own sign-in chrome, and app layouts own authenticated sidebar chrome.
21
21
  - **UI:** Prefer the `@askrjs/themes/components` catalog before writing local components. Use app-local components only for product concepts such as `MetricCard` and `StatusBadge`; keep charts in `@askrjs/charts`.
22
22
  - **State:** `const [value, setValue] = state(initial)`. Read with `value()`, update with `setValue(...)`. Use `derive()` for computed values and `resource()` for async data.
@@ -58,3 +58,9 @@ tests/
58
58
  - Use `Link` and `navigate` from `@askrjs/askr/router`.
59
59
  - Use headless `@askrjs/ui/*` for behavior primitives and `@askrjs/themes/*` for composed visual surfaces.
60
60
  - Avoid hardcoded color systems, custom component catalogs, and React habits like effect-driven data loading.
61
+
62
+ ## Recovery and completion
63
+
64
+ - Run `askr repair` after analyzer failures; it applies only safe mechanical fixes.
65
+ - Resolve remaining semantic diagnostics deliberately.
66
+ - Run `npm run check` before declaring work complete. It requires clean Askr analysis, then runs lint, typecheck, tests, and build.
@@ -16,7 +16,7 @@ npm test # Run tests with Vitest
16
16
 
17
17
  ```
18
18
  src/
19
- |-- main.tsx # SPA boot and route manifest
19
+ |-- main.tsx # SPA boot and route registry
20
20
  |-- pages/
21
21
  | |-- _routes.tsx # Top-level route branches
22
22
  | |-- _layout.tsx # Theme provider and app root
@@ -11,8 +11,9 @@
11
11
  "typecheck": "tsc --noEmit",
12
12
  "lint": "vp lint .",
13
13
  "lint:fix": "npm run lint -- --fix",
14
+ "analyze": "askr analyze --check",
14
15
  "fmt": "vp fmt .",
15
- "check": "npm run lint && npm run typecheck && npm test && npm run build"
16
+ "check": "askr check"
16
17
  },
17
18
  "dependencies": {
18
19
  "@askrjs/askr": ">=0.0.53 <0.1.0",
@@ -1,7 +1,7 @@
1
1
  import { createSPA } from '@askrjs/askr/boot';
2
- import { pageRegistry } from './pages/_routes';
3
2
 
4
3
  import './styles.css';
4
+ import { pageRegistry } from './pages/_routes';
5
5
 
6
6
  await createSPA({
7
7
  root: document.getElementById('app')!,
@@ -6,6 +6,7 @@ import {
6
6
  SettingsIcon,
7
7
  SunIcon,
8
8
  } from '@askrjs/lucide';
9
+ import { For } from '@askrjs/askr/control';
9
10
  import { Link, navigate } from '@askrjs/askr/router';
10
11
  import { Button } from '@askrjs/themes/components';
11
12
  import { Container, Inline, Stack } from '@askrjs/themes/components';
@@ -42,14 +43,16 @@ export default function AppLayout({ children }: { children?: unknown }) {
42
43
  </Link>
43
44
  </NavBrand>
44
45
  <NavGroup label="Workspace">
45
- {appNavItems.map((item) => (
46
- <NavLink href={item.href} match={item.match}>
47
- <Inline as="span" gap="2" align="center">
48
- {icons[item.icon]}
49
- <span>{item.label}</span>
50
- </Inline>
51
- </NavLink>
52
- ))}
46
+ <For each={[...appNavItems]} by={(item) => item.href}>
47
+ {(item) => (
48
+ <NavLink href={item.href} match={item.match}>
49
+ <Inline as="span" gap="2" align="center">
50
+ {icons[item.icon]}
51
+ <span>{item.label}</span>
52
+ </Inline>
53
+ </NavLink>
54
+ )}
55
+ </For>
53
56
  </NavGroup>
54
57
  <NavGroup label="Session" align="end">
55
58
  <NavLink href="/" match="exact">