@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.
- package/README.md +50 -0
- package/dist/add.d.ts +1 -10
- package/dist/add.js +1 -63
- package/dist/analyze.d.ts +88 -0
- package/dist/analyze.js +97 -0
- package/dist/cli.js +16 -0
- package/dist/create.js +1 -1
- package/dist/{discovery-BX-lnFRK.js → discovery-DUDrZCIC.js} +11 -2
- package/dist/file-changes-BAFLhEHZ.js +66 -0
- package/dist/file-changes-cmFN-rF8.d.ts +11 -0
- package/dist/guardrails-GPM9PJqO.js +121 -0
- package/dist/{planner-BDfYDKnI.js → planner-BEfWRd0x.js} +3 -3
- package/dist/runner-BRjjPhZY.js +1495 -0
- package/dist/runner-Ca43qqzG.js +289 -0
- package/dist/skills/askr-ssr-ssg/SKILL.md +2 -0
- package/dist/{skills-C2KzfTl9.js → skills-CSGdAZHN.js} +50 -1
- package/dist/skills.d.ts +12 -1
- package/dist/skills.js +2 -2
- package/dist/ssg.d.ts +1 -0
- package/dist/ssg.js +5 -4
- package/dist/templates/full-stack/AGENTS.md +6 -0
- package/dist/templates/full-stack/package.json +2 -1
- package/dist/templates/spa/AGENTS.md +7 -1
- package/dist/templates/spa/README.md +1 -1
- package/dist/templates/spa/package.json +2 -1
- package/dist/templates/spa/src/main.tsx +1 -1
- package/dist/templates/spa/src/pages/app/_layout.tsx +11 -8
- package/dist/templates/spa/src/pages/app/admin-home.tsx +97 -90
- package/dist/templates/spa/src/pages/app/agent-runs.tsx +33 -26
- package/dist/templates/spa/src/pages/public/home.tsx +12 -9
- package/dist/templates/ssg/AGENTS.md +6 -0
- package/dist/templates/ssg/package.json +2 -1
- package/dist/templates/ssg/src/components/site-shell.tsx +4 -3
- package/dist/templates/ssg/src/pages/about.tsx +14 -11
- package/dist/templates/ssg/src/pages/content.tsx +10 -7
- package/dist/templates/ssg/src/pages/example.tsx +0 -2
- package/dist/templates/ssg/src/pages/home.tsx +6 -3
- package/dist/templates/ssr/AGENTS.md +6 -0
- package/dist/templates/ssr/package.json +2 -1
- package/dist/templates/startkit/AGENTS.md +3 -0
- package/dist/templates/startkit/package.json +2 -1
- package/dist/templates/startkit/src/components/app-sidebar.tsx +23 -18
- package/dist/templates/startkit/src/components/data-table.tsx +8 -6
- package/dist/templates/startkit/src/pages/workspace/dashboard.tsx +16 -13
- package/dist/templates/startkit/src/routes/auth.ts +1 -2
- package/dist/templates/startkit/src/routes/public.ts +1 -2
- package/dist/templates/startkit/src/routes/workspace/accounts.ts +1 -2
- package/dist/templates/startkit/src/routes/workspace/index.ts +2 -3
- package/dist/update.js +1 -1
- 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,52 @@ 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 recommends `<For>` only when a `.map()` result is
|
|
77
|
+
rendered directly as JSX children, so ordinary data transforms remain valid.
|
|
78
|
+
It reports eager `<For>`/`<Show>`/`<Case>` controls behind changing ternaries
|
|
79
|
+
while accepting conditionally mounted components with their own render scope.
|
|
80
|
+
|
|
81
|
+
By default it transactionally applies only mechanical route-parameter and
|
|
82
|
+
plain-JSON JSX configuration fixes. `--check` is read-only for CI. Semantic
|
|
83
|
+
rewrites, including `.map()` to `<For>`, conditional primitives, and key
|
|
84
|
+
changes, are always report-only. See the
|
|
85
|
+
[analyze command reference](./docs/analyze.md) for rules and configuration.
|
|
86
|
+
|
|
87
|
+
## Project recovery loop
|
|
88
|
+
|
|
89
|
+
Every generated project uses the same guardrail loop:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
askr doctor
|
|
93
|
+
askr repair
|
|
94
|
+
askr check
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`doctor` performs a read-only environment, package-manager, framework, skills,
|
|
98
|
+
and static-analysis inspection. `repair` transactionally applies only safe
|
|
99
|
+
mechanical fixes and reports remaining semantic work. `check` requires clean
|
|
100
|
+
analysis before running the project's declared lint, typecheck, test, and build
|
|
101
|
+
scripts in order. Generated projects expose that final gate as `npm run check`.
|
|
102
|
+
|
|
103
|
+
See the [project guardrails reference](./docs/guardrails.md) for command and JSON
|
|
104
|
+
contracts.
|
|
105
|
+
|
|
56
106
|
## Static sitemap generation
|
|
57
107
|
|
|
58
108
|
`askr ssg` writes `sitemap.xml` from the concrete routes generated during the
|
package/dist/add.d.ts
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
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 };
|
package/dist/analyze.js
ADDED
|
@@ -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-BRjjPhZY.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-GPM9PJqO.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 {
|
|
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
|
|
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-Ca43qqzG.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
|
-
|
|
300
|
-
return `${name}
|
|
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));
|