@askrjs/cli 0.0.10 → 0.0.11

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 (39) hide show
  1. package/README.md +48 -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-vH9lYg05.js +121 -0
  12. package/dist/{planner-BDfYDKnI.js → planner-BEfWRd0x.js} +3 -3
  13. package/dist/runner-42nqhW9u.js +1452 -0
  14. package/dist/runner-tPGn2CQC.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/ssg/AGENTS.md +6 -0
  28. package/dist/templates/ssg/package.json +2 -1
  29. package/dist/templates/ssg/src/pages/example.tsx +0 -2
  30. package/dist/templates/ssr/AGENTS.md +6 -0
  31. package/dist/templates/ssr/package.json +2 -1
  32. package/dist/templates/startkit/AGENTS.md +3 -0
  33. package/dist/templates/startkit/package.json +2 -1
  34. package/dist/templates/startkit/src/routes/auth.ts +1 -2
  35. package/dist/templates/startkit/src/routes/public.ts +1 -2
  36. package/dist/templates/startkit/src/routes/workspace/accounts.ts +1 -2
  37. package/dist/templates/startkit/src/routes/workspace/index.ts +2 -3
  38. package/dist/update.js +1 -1
  39. package/package.json +7 -6
package/README.md CHANGED
@@ -41,6 +41,10 @@ unless you opt out with `--no-skills`.
41
41
  - `askr create [template] <name> [--prompt <text>] [--no-install] [--no-skills]`
42
42
  - `askr add page <name> [--branch app|public]`
43
43
  - `askr add action <name> --route <path>`
44
+ - `askr analyze [--cwd <dir>] [--workspace <pattern>]... [--json] [--check]`
45
+ - `askr doctor [--cwd <dir>] [--workspace <pattern>]... [--json]`
46
+ - `askr repair [--cwd <dir>] [--workspace <pattern>]... [--json]`
47
+ - `askr check [--cwd <dir>] [--workspace <pattern>]... [--json]`
44
48
  - `askr skills list`
45
49
  - `askr skills install [--cwd <dir>] [--force]`
46
50
  - `askr skills sync [--cwd <dir>]`
@@ -53,6 +57,50 @@ unless you opt out with `--no-skills`.
53
57
  The installed command is `askr`. Subcommands are intentionally not published
54
58
  as compatibility binaries.
55
59
 
60
+ ## Static analysis
61
+
62
+ `askr analyze` discovers the containing npm or pnpm workspace, builds a
63
+ TypeScript compiler program for every selected workspace, and checks current
64
+ Askr state, lifecycle, data, control-flow, routing, boot, SSR/SSG, action, and
65
+ configuration contracts.
66
+
67
+ ```bash
68
+ askr analyze
69
+ askr analyze --workspace "@scope/web"
70
+ askr analyze --check
71
+ askr analyze --json --check
72
+ ```
73
+
74
+ All diagnostics include a stable rule ID and workspace-relative source
75
+ location. The analyzer distinguishes canonical Askr imports from unrelated
76
+ same-named functions and only recommends `<For>` for state-backed reactive JSX
77
+ collections, so static transforms with `.map()` remain valid.
78
+
79
+ By default it transactionally applies only mechanical route-parameter and
80
+ plain-JSON JSX configuration fixes. `--check` is read-only for CI. Semantic
81
+ rewrites, including `.map()` to `<For>`, conditional primitives, and key
82
+ changes, are always report-only. See the
83
+ [analyze command reference](./docs/analyze.md) for rules and configuration.
84
+
85
+ ## Project recovery loop
86
+
87
+ Every generated project uses the same guardrail loop:
88
+
89
+ ```bash
90
+ askr doctor
91
+ askr repair
92
+ askr check
93
+ ```
94
+
95
+ `doctor` performs a read-only environment, package-manager, framework, skills,
96
+ and static-analysis inspection. `repair` transactionally applies only safe
97
+ mechanical fixes and reports remaining semantic work. `check` requires clean
98
+ analysis before running the project's declared lint, typecheck, test, and build
99
+ scripts in order. Generated projects expose that final gate as `npm run check`.
100
+
101
+ See the [project guardrails reference](./docs/guardrails.md) for command and JSON
102
+ contracts.
103
+
56
104
  ## Static sitemap generation
57
105
 
58
106
  `askr ssg` writes `sitemap.xml` from the concrete routes generated during the
package/dist/add.d.ts CHANGED
@@ -1,13 +1,4 @@
1
- //#region src/file-changes.d.ts
2
- interface FileChange {
3
- readonly filePath: string;
4
- readonly content: string;
5
- }
6
- interface FileChangeWriterOptions {
7
- readonly replace?: (temporaryPath: string, filePath: string) => Promise<void>;
8
- }
9
- declare function writeFileChanges(changes: readonly FileChange[], options?: FileChangeWriterOptions): Promise<void>;
10
- //#endregion
1
+ import { n as writeFileChanges } from "./file-changes-cmFN-rF8.js";
11
2
  //#region src/bin/add.d.ts
12
3
  type CliIo = Pick<Console, "error" | "log">;
13
4
  type WriteChanges = typeof writeFileChanges;
package/dist/add.js CHANGED
@@ -1,70 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
3
+ import { t as writeFileChanges } from "./file-changes-BAFLhEHZ.js";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "node:path";
5
- import { randomUUID } from "node:crypto";
6
- //#region src/file-changes.ts
7
- async function remove(paths) {
8
- await Promise.all(paths.map((filePath) => fs.rm(filePath, { force: true }).catch(() => void 0)));
9
- }
10
- async function restore(changes) {
11
- let complete = true;
12
- for (const change of [...changes].reverse()) try {
13
- if (change.original === null) {
14
- await fs.rm(change.filePath, { force: true });
15
- continue;
16
- }
17
- const rollbackPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-rollback-${randomUUID()}`);
18
- await fs.writeFile(rollbackPath, change.original, {
19
- flag: "wx",
20
- mode: change.mode
21
- });
22
- await fs.rename(rollbackPath, change.filePath);
23
- } catch {
24
- complete = false;
25
- }
26
- return complete;
27
- }
28
- async function writeFileChanges(changes, options = {}) {
29
- const ordered = [...changes].sort((left, right) => left.filePath.localeCompare(right.filePath));
30
- if (new Set(ordered.map((change) => change.filePath)).size !== ordered.length) throw new Error("File changes contain duplicate target paths.");
31
- const replace = options.replace ?? fs.rename;
32
- const staged = [];
33
- try {
34
- for (const change of ordered) {
35
- await fs.mkdir(path.dirname(change.filePath), { recursive: true });
36
- const stat = await fs.stat(change.filePath).catch(() => null);
37
- const original = stat ? await fs.readFile(change.filePath) : null;
38
- const temporaryPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-change-${randomUUID()}`);
39
- const mode = stat?.mode ?? 420;
40
- await fs.writeFile(temporaryPath, change.content, {
41
- flag: "wx",
42
- mode
43
- });
44
- staged.push({
45
- ...change,
46
- original,
47
- mode,
48
- temporaryPath
49
- });
50
- }
51
- } catch (error) {
52
- await remove(staged.map((change) => change.temporaryPath));
53
- throw error;
54
- }
55
- const replaced = [];
56
- try {
57
- for (const change of staged) {
58
- await replace(change.temporaryPath, change.filePath);
59
- replaced.push(change);
60
- }
61
- } catch {
62
- const complete = await restore(replaced);
63
- await remove(staged.map((change) => change.temporaryPath));
64
- throw new Error(complete ? "File replacement failed; completed changes were rolled back." : "File replacement failed and rollback was incomplete.");
65
- }
66
- }
67
- //#endregion
68
6
  //#region src/bin/add.ts
69
7
  const BRANCH_CONFIG = {
70
8
  app: {
@@ -0,0 +1,88 @@
1
+ import { t as FileChange } from "./file-changes-cmFN-rF8.js";
2
+ import ts from "typescript";
3
+ //#region src/analyze/types.d.ts
4
+ declare const ANALYZE_SCHEMA_VERSION = 1;
5
+ type AnalyzeSeverity = "error" | "warning" | "info";
6
+ type AnalyzeCategory = "correctness" | "performance" | "configuration";
7
+ interface AnalyzeFix {
8
+ readonly description: string;
9
+ readonly filePath: string;
10
+ readonly start: number;
11
+ readonly end: number;
12
+ readonly replacement: string;
13
+ }
14
+ interface AnalyzeDiagnostic {
15
+ readonly ruleId: string;
16
+ readonly category: AnalyzeCategory;
17
+ readonly severity: AnalyzeSeverity;
18
+ readonly message: string;
19
+ readonly workspace: string;
20
+ readonly file: string;
21
+ readonly line: number;
22
+ readonly column: number;
23
+ readonly remediation?: string;
24
+ readonly fix?: AnalyzeFix;
25
+ }
26
+ interface PublicDiagnostic extends Omit<AnalyzeDiagnostic, "fix"> {
27
+ readonly fix?: {
28
+ readonly description: string;
29
+ readonly safe: true;
30
+ };
31
+ }
32
+ interface AnalyzeWorkspaceResult {
33
+ readonly name: string;
34
+ readonly path: string;
35
+ readonly tsconfig: string | null;
36
+ readonly files: number;
37
+ }
38
+ interface AnalyzeFixResult {
39
+ readonly ruleId: string;
40
+ readonly workspace: string;
41
+ readonly file: string;
42
+ readonly description: string;
43
+ readonly reason?: string;
44
+ }
45
+ interface AnalyzeSummary {
46
+ readonly errors: number;
47
+ readonly warnings: number;
48
+ readonly info: number;
49
+ readonly diagnostics: number;
50
+ readonly appliedFixes: number;
51
+ readonly skippedFixes: number;
52
+ }
53
+ interface AnalyzeReport {
54
+ readonly schemaVersion: typeof ANALYZE_SCHEMA_VERSION;
55
+ readonly root: string;
56
+ readonly discoveredWorkspaces: readonly string[];
57
+ readonly selectedWorkspaces: readonly string[];
58
+ readonly workspaces: readonly AnalyzeWorkspaceResult[];
59
+ readonly appliedFixes: readonly AnalyzeFixResult[];
60
+ readonly skippedFixes: readonly AnalyzeFixResult[];
61
+ readonly diagnostics: readonly PublicDiagnostic[];
62
+ readonly summary: AnalyzeSummary;
63
+ }
64
+ //#endregion
65
+ //#region src/analyze/runner.d.ts
66
+ interface RunAnalysisOptions {
67
+ readonly cwd: string;
68
+ readonly workspacePatterns: string[];
69
+ readonly check: boolean;
70
+ readonly writer?: (changes: readonly FileChange[]) => Promise<void>;
71
+ }
72
+ //#endregion
73
+ //#region src/bin/analyze.d.ts
74
+ type CliIo = Pick<Console, "error" | "log">;
75
+ interface ParsedAnalyzeArgs {
76
+ cwd: string;
77
+ workspacePatterns: string[];
78
+ json: boolean;
79
+ check: boolean;
80
+ help: boolean;
81
+ }
82
+ interface AnalyzeRuntime {
83
+ readonly analyze?: (options: RunAnalysisOptions) => Promise<AnalyzeReport>;
84
+ }
85
+ declare function parseAnalyzeArgs(args: readonly string[]): ParsedAnalyzeArgs;
86
+ declare function runAnalyzeCli(args?: string[], io?: CliIo, runtime?: AnalyzeRuntime): Promise<number>;
87
+ //#endregion
88
+ export { parseAnalyzeArgs, runAnalyzeCli };
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
3
+ import path from "node:path";
4
+ //#region src/bin/analyze.ts
5
+ const helpText = `askr analyze - Analyze Askr correctness and performance
6
+
7
+ Usage:
8
+ askr analyze [--cwd <dir>] [--workspace <pattern>]... [--json] [--check]
9
+
10
+ Options:
11
+ --cwd <dir> Resolve a project from another directory
12
+ --workspace <pattern> Select workspace names (repeatable)
13
+ --json Emit one deterministic JSON object on stdout
14
+ --check Report safe fixes without writing files
15
+ --help, -h Show this help message
16
+
17
+ By default every declared workspace is scanned and explicitly safe fixes are
18
+ applied transactionally. Semantic rewrites are always report-only.
19
+ `;
20
+ function optionValue(args, index, option) {
21
+ const value = args[index + 1];
22
+ if (!value || value.startsWith("-")) throw new Error(`${option} requires a value`);
23
+ return value;
24
+ }
25
+ function parseAnalyzeArgs(args) {
26
+ const parsed = {
27
+ cwd: process.cwd(),
28
+ workspacePatterns: [],
29
+ json: false,
30
+ check: false,
31
+ help: false
32
+ };
33
+ for (let index = 0; index < args.length; index += 1) {
34
+ const argument = args[index];
35
+ if (argument === "--help" || argument === "-h") parsed.help = true;
36
+ else if (argument === "--json") parsed.json = true;
37
+ else if (argument === "--check") parsed.check = true;
38
+ else if (argument === "--cwd") {
39
+ parsed.cwd = path.resolve(optionValue(args, index, "--cwd"));
40
+ index += 1;
41
+ } else if (argument.startsWith("--cwd=")) {
42
+ const value = argument.slice(6);
43
+ if (!value) throw new Error("--cwd requires a value");
44
+ parsed.cwd = path.resolve(value);
45
+ } else if (argument === "--workspace") {
46
+ parsed.workspacePatterns.push(optionValue(args, index, "--workspace"));
47
+ index += 1;
48
+ } else if (argument.startsWith("--workspace=")) {
49
+ const value = argument.slice(12);
50
+ if (!value) throw new Error("--workspace requires a value");
51
+ parsed.workspacePatterns.push(value);
52
+ } else throw new Error(`Unknown option: ${argument}`);
53
+ }
54
+ return parsed;
55
+ }
56
+ function formatDiagnostic(diagnostic) {
57
+ const severity = diagnostic.severity.toUpperCase();
58
+ const remedy = diagnostic.remediation ? `\n ${diagnostic.remediation}` : "";
59
+ return `${diagnostic.workspace}:${diagnostic.file}:${diagnostic.line}:${diagnostic.column} ${severity} ${diagnostic.ruleId} ${diagnostic.message}${remedy}`;
60
+ }
61
+ function printHuman(report, io) {
62
+ for (const fix of report.appliedFixes) io.log(`fixed ${fix.file} ${fix.ruleId}: ${fix.description}`);
63
+ for (const diagnostic of report.diagnostics) io.log(formatDiagnostic(diagnostic));
64
+ io.log(`Analyzed ${report.workspaces.length} workspace(s), ${report.workspaces.reduce((count, workspace) => count + workspace.files, 0)} file(s): ${report.summary.errors} error(s), ${report.summary.warnings} warning(s), ${report.summary.info} info, ${report.summary.appliedFixes} fix(es) applied.`);
65
+ }
66
+ async function runAnalyzeCli(args = process.argv.slice(2), io = console, runtime = {}) {
67
+ const wantsJson = args.includes("--json");
68
+ try {
69
+ const parsed = parseAnalyzeArgs(args);
70
+ if (parsed.help) {
71
+ io.log(helpText.trimEnd());
72
+ return 0;
73
+ }
74
+ const report = await (runtime.analyze ?? (await import("./runner-42nqhW9u.js")).runAnalysis)({
75
+ cwd: parsed.cwd,
76
+ workspacePatterns: parsed.workspacePatterns,
77
+ check: parsed.check
78
+ });
79
+ if (parsed.json) io.log(JSON.stringify(report));
80
+ else printHuman(report, io);
81
+ return report.summary.errors > 0 || report.summary.warnings > 0 ? 1 : 0;
82
+ } catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ io.error(wantsJson ? JSON.stringify({
85
+ schemaVersion: 1,
86
+ status: "error",
87
+ error: message
88
+ }) : message);
89
+ return 1;
90
+ }
91
+ }
92
+ async function main() {
93
+ process.exit(await runAnalyzeCli(process.argv.slice(2)));
94
+ }
95
+ if (isDirectExecution(import.meta.url)) main();
96
+ //#endregion
97
+ export { parseAnalyzeArgs, runAnalyzeCli };
package/dist/cli.js CHANGED
@@ -33,12 +33,16 @@ function printHelp(io = console) {
33
33
  io.log("");
34
34
  io.log("Commands:");
35
35
  io.log(" add Generate pages or declared actions into an Askr project");
36
+ io.log(" analyze Analyze Askr correctness and performance");
37
+ io.log(" check Run the complete project validation path");
36
38
  io.log(" create Create a new Askr app from a template or product prompt");
39
+ io.log(" doctor Diagnose environment and Askr project health");
37
40
  io.log(" generate Generate an @askrjs/fetch client from OpenAPI");
38
41
  io.log(" openapi Generate or check an OpenAPI YAML artifact");
39
42
  io.log(" skills Install or sync Askr agent skills");
40
43
  io.log(" ssg Run static-site generation");
41
44
  io.log(" outdated List available dependency updates");
45
+ io.log(" repair Apply safe fixes and identify remaining semantic work");
42
46
  io.log(" update Apply safe dependency updates");
43
47
  io.log(" upgrade Apply latest peer-compatible dependency upgrades");
44
48
  io.log("");
@@ -51,6 +55,10 @@ function printHelp(io = console) {
51
55
  io.log(" askr create --prompt \"Agent workflow console with approvals\"");
52
56
  io.log(" askr add page audit-log");
53
57
  io.log(" askr add action approve-request --route /requests/{id}");
58
+ io.log(" askr analyze --check");
59
+ io.log(" askr doctor");
60
+ io.log(" askr repair");
61
+ io.log(" askr check");
54
62
  io.log(" askr skills install");
55
63
  io.log(" askr openapi --check");
56
64
  io.log(" askr skills review foundation --cwd ./candidate-app");
@@ -77,6 +85,14 @@ async function runCli(args = process.argv.slice(2), io = console) {
77
85
  const { runAddCli } = await import("./add.js");
78
86
  return runAddCli(args.slice(1), io);
79
87
  }
88
+ if (command === "analyze") {
89
+ const { runAnalyzeCli } = await import("./analyze.js");
90
+ return runAnalyzeCli(args.slice(1), io);
91
+ }
92
+ if (command === "check" || command === "doctor" || command === "repair") {
93
+ const { runGuardrailCli } = await import("./guardrails-vH9lYg05.js");
94
+ return runGuardrailCli(command, args.slice(1), io);
95
+ }
80
96
  if (command === "generate") {
81
97
  const { runGenerateCli } = await import("./generate.js");
82
98
  return runGenerateCli(args.slice(1), io);
package/dist/create.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
3
- import { t as installBundledSkills } from "./skills-C2KzfTl9.js";
3
+ import { n as installBundledSkills } from "./skills-CSGdAZHN.js";
4
4
  import { n as publishStagedDirectory, t as createSiblingStage } from "./directory-swap-DWoHtx7C.js";
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
@@ -301,11 +301,20 @@ function collectOccurrences(root, workspaces, localNames, policy, packagePattern
301
301
  occurrences.sort((left, right) => left.package.localeCompare(right.package) || left.workspace.localeCompare(right.workspace) || left.relativeManifestPath.localeCompare(right.relativeManifestPath) || sectionOrder.get(left.section) - sectionOrder.get(right.section));
302
302
  return occurrences;
303
303
  }
304
- async function discoverProject(options) {
304
+ async function discoverWorkspaceProject(options) {
305
305
  const root = await findProjectRoot(options.cwd);
306
306
  const { policy, workspaces } = await discoverWorkspaces(root);
307
307
  const selectedWorkspaces = options.workspacePatterns.length === 0 ? workspaces : workspaces.filter((workspace) => matchesAny(workspace.name, options.workspacePatterns));
308
308
  if (selectedWorkspaces.length === 0) throw new Error("No discovered workspace matches the requested --workspace filters.");
309
+ return {
310
+ root,
311
+ workspaces,
312
+ selectedWorkspaces,
313
+ policy
314
+ };
315
+ }
316
+ async function discoverProject(options) {
317
+ const { root, workspaces, selectedWorkspaces, policy } = await discoverWorkspaceProject(options);
309
318
  const localNames = new Set(workspaces.map((workspace) => workspace.name));
310
319
  const localVersions = new Map(workspaces.flatMap((workspace) => typeof workspace.manifest.version === "string" && semver.valid(workspace.manifest.version) ? [[workspace.name, workspace.manifest.version]] : []));
311
320
  return {
@@ -322,4 +331,4 @@ async function discoverProject(options) {
322
331
  };
323
332
  }
324
333
  //#endregion
325
- export { discoverProject };
334
+ export { discoverProject, discoverWorkspaceProject };
@@ -0,0 +1,66 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ //#region src/file-changes.ts
5
+ async function remove(paths) {
6
+ await Promise.all(paths.map((filePath) => fs.rm(filePath, { force: true }).catch(() => void 0)));
7
+ }
8
+ async function restore(changes) {
9
+ let complete = true;
10
+ for (const change of [...changes].reverse()) try {
11
+ if (change.original === null) {
12
+ await fs.rm(change.filePath, { force: true });
13
+ continue;
14
+ }
15
+ const rollbackPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-rollback-${randomUUID()}`);
16
+ await fs.writeFile(rollbackPath, change.original, {
17
+ flag: "wx",
18
+ mode: change.mode
19
+ });
20
+ await fs.rename(rollbackPath, change.filePath);
21
+ } catch {
22
+ complete = false;
23
+ }
24
+ return complete;
25
+ }
26
+ async function writeFileChanges(changes, options = {}) {
27
+ const ordered = [...changes].sort((left, right) => left.filePath.localeCompare(right.filePath));
28
+ if (new Set(ordered.map((change) => change.filePath)).size !== ordered.length) throw new Error("File changes contain duplicate target paths.");
29
+ const replace = options.replace ?? fs.rename;
30
+ const staged = [];
31
+ try {
32
+ for (const change of ordered) {
33
+ await fs.mkdir(path.dirname(change.filePath), { recursive: true });
34
+ const stat = await fs.stat(change.filePath).catch(() => null);
35
+ const original = stat ? await fs.readFile(change.filePath) : null;
36
+ const temporaryPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-change-${randomUUID()}`);
37
+ const mode = stat?.mode ?? 420;
38
+ await fs.writeFile(temporaryPath, change.content, {
39
+ flag: "wx",
40
+ mode
41
+ });
42
+ staged.push({
43
+ ...change,
44
+ original,
45
+ mode,
46
+ temporaryPath
47
+ });
48
+ }
49
+ } catch (error) {
50
+ await remove(staged.map((change) => change.temporaryPath));
51
+ throw error;
52
+ }
53
+ const replaced = [];
54
+ try {
55
+ for (const change of staged) {
56
+ await replace(change.temporaryPath, change.filePath);
57
+ replaced.push(change);
58
+ }
59
+ } catch {
60
+ const complete = await restore(replaced);
61
+ await remove(staged.map((change) => change.temporaryPath));
62
+ throw new Error(complete ? "File replacement failed; completed changes were rolled back." : "File replacement failed and rollback was incomplete.");
63
+ }
64
+ }
65
+ //#endregion
66
+ export { writeFileChanges as t };
@@ -0,0 +1,11 @@
1
+ //#region src/file-changes.d.ts
2
+ interface FileChange {
3
+ readonly filePath: string;
4
+ readonly content: string;
5
+ }
6
+ interface FileChangeWriterOptions {
7
+ readonly replace?: (temporaryPath: string, filePath: string) => Promise<void>;
8
+ }
9
+ declare function writeFileChanges(changes: readonly FileChange[], options?: FileChangeWriterOptions): Promise<void>;
10
+ //#endregion
11
+ export { writeFileChanges as n, FileChange as t };
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+ //#region src/bin/guardrails.ts
3
+ const descriptions = {
4
+ check: "Run analysis, lint, typecheck, tests, and build",
5
+ doctor: "Diagnose environment and Askr project health",
6
+ repair: "Apply safe fixes and report remaining semantic work"
7
+ };
8
+ function helpText(command) {
9
+ return `askr ${command} - ${descriptions[command]}
10
+
11
+ Usage:
12
+ askr ${command} [--cwd <dir>] [--workspace <pattern>]... [--json]
13
+
14
+ Options:
15
+ --cwd <dir> Resolve a project from another directory
16
+ --workspace <pattern> Select workspace names for analysis (repeatable)
17
+ --json Emit one deterministic JSON object
18
+ --help, -h Show this help message
19
+ `;
20
+ }
21
+ function optionValue(args, index, option) {
22
+ const value = args[index + 1];
23
+ if (!value || value.startsWith("-")) throw new Error(`${option} requires a value`);
24
+ return value;
25
+ }
26
+ function parseGuardrailArgs(args) {
27
+ let cwd = process.cwd();
28
+ const workspacePatterns = [];
29
+ let json = false;
30
+ let help = false;
31
+ for (let index = 0; index < args.length; index += 1) {
32
+ const argument = args[index];
33
+ if (argument === "--help" || argument === "-h") help = true;
34
+ else if (argument === "--json") json = true;
35
+ else if (argument === "--cwd") {
36
+ cwd = path.resolve(optionValue(args, index, "--cwd"));
37
+ index += 1;
38
+ } else if (argument.startsWith("--cwd=")) {
39
+ const value = argument.slice(6);
40
+ if (!value) throw new Error("--cwd requires a value");
41
+ cwd = path.resolve(value);
42
+ } else if (argument === "--workspace") {
43
+ workspacePatterns.push(optionValue(args, index, "--workspace"));
44
+ index += 1;
45
+ } else if (argument.startsWith("--workspace=")) {
46
+ const value = argument.slice(12);
47
+ if (!value) throw new Error("--workspace requires a value");
48
+ workspacePatterns.push(value);
49
+ } else throw new Error(`Unknown option: ${argument}`);
50
+ }
51
+ return {
52
+ cwd,
53
+ workspacePatterns,
54
+ json,
55
+ help
56
+ };
57
+ }
58
+ function formatDiagnostic(diagnostic) {
59
+ const remediation = diagnostic.remediation ? `\n ${diagnostic.remediation}` : "";
60
+ return `${diagnostic.workspace}:${diagnostic.file}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity.toUpperCase()} ${diagnostic.ruleId} ${diagnostic.message}${remediation}`;
61
+ }
62
+ function printDiagnostics(analysis, io) {
63
+ for (const diagnostic of analysis.diagnostics) io.log(formatDiagnostic(diagnostic));
64
+ }
65
+ function formatFinding(finding) {
66
+ return `${finding.status === "pass" ? "PASS" : finding.status === "warning" ? "WARN" : "FAIL"} ${finding.id} ${finding.message}${finding.remediation ? `\n ${finding.remediation}` : ""}`;
67
+ }
68
+ function printDoctor(report, io) {
69
+ for (const finding of report.findings) io.log(formatFinding(finding));
70
+ printDiagnostics(report.analysis, io);
71
+ io.log(`Doctor: ${report.summary.passed} passed, ${report.summary.warnings} warning(s), ${report.summary.errors} error(s).`);
72
+ }
73
+ function printRepair(report, io) {
74
+ for (const fix of report.analysis.appliedFixes) io.log(`fixed ${fix.file} ${fix.ruleId}: ${fix.description}`);
75
+ printDiagnostics(report.analysis, io);
76
+ io.log(`Repair: ${report.analysis.summary.appliedFixes} safe fix(es) applied, ${report.analysis.summary.diagnostics} finding(s) remain.`);
77
+ io.log(report.nextAction);
78
+ }
79
+ function printCheck(report, io) {
80
+ printDiagnostics(report.analysis, io);
81
+ for (const script of report.scripts) {
82
+ if (script.stdout.trim()) io.log(script.stdout.trimEnd());
83
+ if (script.stderr.trim()) io.error(script.stderr.trimEnd());
84
+ const marker = script.status === "passed" ? "PASS" : script.status === "failed" ? "FAIL" : "SKIP";
85
+ io.log(`${marker} ${script.command}${script.reason ? ` (${script.reason})` : ""}`);
86
+ }
87
+ io.log(`Check ${report.status}: ${report.analysis.summary.errors} analysis error(s), ${report.analysis.summary.warnings} warning(s), ${report.scripts.filter((entry) => entry.status === "passed").length} script(s) passed.`);
88
+ }
89
+ async function runGuardrailCli(command, args = process.argv.slice(2), io = console, runtime = {}) {
90
+ const wantsJson = args.includes("--json");
91
+ try {
92
+ const parsed = parseGuardrailArgs(args);
93
+ if (parsed.help) {
94
+ io.log(helpText(command).trimEnd());
95
+ return 0;
96
+ }
97
+ const options = {
98
+ cwd: parsed.cwd,
99
+ workspacePatterns: parsed.workspacePatterns
100
+ };
101
+ const { runCheck, runDoctor, runRepair } = await import("./runner-tPGn2CQC.js");
102
+ const report = command === "doctor" ? await runDoctor(options, runtime) : command === "repair" ? await runRepair(options) : await runCheck(options, runtime);
103
+ if (parsed.json) io.log(JSON.stringify(report));
104
+ else if (report.command === "doctor") printDoctor(report, io);
105
+ else if (report.command === "repair") printRepair(report, io);
106
+ else printCheck(report, io);
107
+ if (report.command === "doctor") return report.summary.errors > 0 ? 1 : 0;
108
+ if (report.command === "repair") return report.status === "clean" ? 0 : 1;
109
+ return report.status === "passed" ? 0 : 1;
110
+ } catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ io.error(wantsJson ? JSON.stringify({
113
+ schemaVersion: 1,
114
+ status: "error",
115
+ error: message
116
+ }) : message);
117
+ return 1;
118
+ }
119
+ }
120
+ //#endregion
121
+ export { runGuardrailCli };
@@ -296,8 +296,8 @@ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tag
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
297
  const signature = component.map((name) => {
298
298
  const assignedVersion = assigned.get(name);
299
- if (assignedVersion !== void 0) return `${name}=assigned:${assignedVersion}`;
300
- return `${name}=domain:${(pruned.get(name) ?? []).join(",")}`;
299
+ const domain = pruned.get(name) ?? [];
300
+ return `${name}:assigned=${JSON.stringify(assignedVersion ?? null)}:domain=${JSON.stringify(domain)}`;
301
301
  }).join("|");
302
302
  if (memo.has(signature)) return;
303
303
  memo.add(signature);
@@ -352,7 +352,7 @@ function summarize(decisions) {
352
352
  function planUpdates(options) {
353
353
  const failures = options.failures ?? /* @__PURE__ */ new Map();
354
354
  const tags = options.tags ?? {};
355
- const mode = options.mode ?? "update";
355
+ const mode = options.mode ?? (options.force ? "upgrade" : "update");
356
356
  const context = options.contextOccurrences ?? options.occurrences;
357
357
  const workspaceSolutions = /* @__PURE__ */ new Map();
358
358
  if (mode !== "force") for (const workspace of new Set(options.occurrences.map((entry) => entry.workspace))) workspaceSolutions.set(workspace, solveWorkspace(workspace, options.occurrences.filter((entry) => entry.workspace === workspace), context, options.packuments, tags, options.cliTag, options.localVersions ?? /* @__PURE__ */ new Map(), mode));