@bossmissing/agent-meta 0.1.0
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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/analyzers/duplicate-rule-analyzer.d.ts +31 -0
- package/dist/analyzers/duplicate-rule-analyzer.js +80 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +177 -0
- package/dist/commands/audit.d.ts +7 -0
- package/dist/commands/audit.js +187 -0
- package/dist/commands/check.d.ts +35 -0
- package/dist/commands/check.js +109 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +59 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +179 -0
- package/dist/commands/new-adr.d.ts +8 -0
- package/dist/commands/new-adr.js +65 -0
- package/dist/commands/new-domain.d.ts +13 -0
- package/dist/commands/new-domain.js +106 -0
- package/dist/commands/sync.d.ts +7 -0
- package/dist/commands/sync.js +46 -0
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +40 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +16 -0
- package/dist/core/filesystem.d.ts +33 -0
- package/dist/core/filesystem.js +92 -0
- package/dist/core/frontmatter.d.ts +19 -0
- package/dist/core/frontmatter.js +84 -0
- package/dist/core/logger.d.ts +8 -0
- package/dist/core/logger.js +22 -0
- package/dist/core/paths.d.ts +8 -0
- package/dist/core/paths.js +39 -0
- package/dist/core/risk.d.ts +3 -0
- package/dist/core/risk.js +8 -0
- package/dist/core/templates.d.ts +10 -0
- package/dist/core/templates.js +31 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.js +7 -0
- package/dist/generators/adr-generator.d.ts +11 -0
- package/dist/generators/adr-generator.js +25 -0
- package/dist/generators/index-generator.d.ts +6 -0
- package/dist/generators/index-generator.js +87 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/schemas/config.schema.d.ts +208 -0
- package/dist/schemas/config.schema.js +83 -0
- package/dist/templates/index.d.ts +23 -0
- package/dist/templates/index.js +445 -0
- package/dist/validators/adr-validator.d.ts +3 -0
- package/dist/validators/adr-validator.js +182 -0
- package/dist/validators/agents-validator.d.ts +3 -0
- package/dist/validators/agents-validator.js +82 -0
- package/dist/validators/context.d.ts +12 -0
- package/dist/validators/context.js +1 -0
- package/dist/validators/docs-validator.d.ts +3 -0
- package/dist/validators/docs-validator.js +129 -0
- package/dist/validators/domain-validator.d.ts +3 -0
- package/dist/validators/domain-validator.js +132 -0
- package/dist/validators/openspec-validator.d.ts +2 -0
- package/dist/validators/openspec-validator.js +76 -0
- package/dist/validators/project-validator.d.ts +2 -0
- package/dist/validators/project-validator.js +41 -0
- package/package.json +64 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const EXIT_CODES: {
|
|
2
|
+
readonly OK: 0;
|
|
3
|
+
readonly CHECK_FAILED: 1;
|
|
4
|
+
readonly BAD_ARGS: 2;
|
|
5
|
+
readonly CONFIG_UNPARSABLE: 3;
|
|
6
|
+
readonly IO_ERROR: 4;
|
|
7
|
+
readonly TEMPLATE_ERROR: 5;
|
|
8
|
+
};
|
|
9
|
+
export declare class MetaError extends Error {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
constructor(message: string, exitCode?: number);
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const EXIT_CODES = {
|
|
2
|
+
OK: 0,
|
|
3
|
+
CHECK_FAILED: 1,
|
|
4
|
+
BAD_ARGS: 2,
|
|
5
|
+
CONFIG_UNPARSABLE: 3,
|
|
6
|
+
IO_ERROR: 4,
|
|
7
|
+
TEMPLATE_ERROR: 5,
|
|
8
|
+
};
|
|
9
|
+
export class MetaError extends Error {
|
|
10
|
+
exitCode;
|
|
11
|
+
constructor(message, exitCode = EXIT_CODES.CHECK_FAILED) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "MetaError";
|
|
14
|
+
this.exitCode = exitCode;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type FileActionType = "create" | "update" | "skip" | "backup";
|
|
2
|
+
export interface FileAction {
|
|
3
|
+
type: FileActionType;
|
|
4
|
+
/** Path relative to the writer's root. */
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* All file writes go through this class so that --dry-run can report the
|
|
9
|
+
* exact set of files that would be created or updated without touching disk.
|
|
10
|
+
*/
|
|
11
|
+
export declare class FileWriter {
|
|
12
|
+
readonly root: string;
|
|
13
|
+
readonly dryRun: boolean;
|
|
14
|
+
readonly actions: FileAction[];
|
|
15
|
+
constructor(root: string, dryRun?: boolean);
|
|
16
|
+
private rel;
|
|
17
|
+
private abs;
|
|
18
|
+
exists(p: string): boolean;
|
|
19
|
+
/** Write a new file; skips when the file exists (unless overwrite). */
|
|
20
|
+
write(p: string, content: string, opts?: {
|
|
21
|
+
overwrite?: boolean;
|
|
22
|
+
}): FileAction;
|
|
23
|
+
/**
|
|
24
|
+
* Exclusive create: fails if the file already exists, even in a race
|
|
25
|
+
* between two concurrent processes (uses the wx flag).
|
|
26
|
+
*/
|
|
27
|
+
writeExclusive(p: string, content: string): FileAction;
|
|
28
|
+
backup(p: string): FileAction;
|
|
29
|
+
mkdir(p: string): void;
|
|
30
|
+
created(): FileAction[];
|
|
31
|
+
updated(): FileAction[];
|
|
32
|
+
skipped(): FileAction[];
|
|
33
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* All file writes go through this class so that --dry-run can report the
|
|
5
|
+
* exact set of files that would be created or updated without touching disk.
|
|
6
|
+
*/
|
|
7
|
+
export class FileWriter {
|
|
8
|
+
root;
|
|
9
|
+
dryRun;
|
|
10
|
+
actions = [];
|
|
11
|
+
constructor(root, dryRun = false) {
|
|
12
|
+
this.root = root;
|
|
13
|
+
this.dryRun = dryRun;
|
|
14
|
+
}
|
|
15
|
+
rel(p) {
|
|
16
|
+
return path.isAbsolute(p) ? path.relative(this.root, p) : p;
|
|
17
|
+
}
|
|
18
|
+
abs(p) {
|
|
19
|
+
return path.isAbsolute(p) ? p : path.join(this.root, p);
|
|
20
|
+
}
|
|
21
|
+
exists(p) {
|
|
22
|
+
return fs.existsSync(this.abs(p));
|
|
23
|
+
}
|
|
24
|
+
/** Write a new file; skips when the file exists (unless overwrite). */
|
|
25
|
+
write(p, content, opts = {}) {
|
|
26
|
+
const abs = this.abs(p);
|
|
27
|
+
const rel = this.rel(p);
|
|
28
|
+
const exists = fs.existsSync(abs);
|
|
29
|
+
if (exists && !opts.overwrite) {
|
|
30
|
+
const action = { type: "skip", path: rel };
|
|
31
|
+
this.actions.push(action);
|
|
32
|
+
return action;
|
|
33
|
+
}
|
|
34
|
+
const action = { type: exists ? "update" : "create", path: rel };
|
|
35
|
+
if (!this.dryRun) {
|
|
36
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
37
|
+
fs.writeFileSync(abs, content, "utf8");
|
|
38
|
+
}
|
|
39
|
+
this.actions.push(action);
|
|
40
|
+
return action;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Exclusive create: fails if the file already exists, even in a race
|
|
44
|
+
* between two concurrent processes (uses the wx flag).
|
|
45
|
+
*/
|
|
46
|
+
writeExclusive(p, content) {
|
|
47
|
+
const abs = this.abs(p);
|
|
48
|
+
const rel = this.rel(p);
|
|
49
|
+
if (this.dryRun) {
|
|
50
|
+
if (fs.existsSync(abs))
|
|
51
|
+
throw new Error(`File already exists: ${rel}`);
|
|
52
|
+
const action = { type: "create", path: rel };
|
|
53
|
+
this.actions.push(action);
|
|
54
|
+
return action;
|
|
55
|
+
}
|
|
56
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(abs, content, { encoding: "utf8", flag: "wx" });
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (e.code === "EEXIST") {
|
|
62
|
+
throw new Error(`File already exists: ${rel}`);
|
|
63
|
+
}
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
const action = { type: "create", path: rel };
|
|
67
|
+
this.actions.push(action);
|
|
68
|
+
return action;
|
|
69
|
+
}
|
|
70
|
+
backup(p) {
|
|
71
|
+
const abs = this.abs(p);
|
|
72
|
+
const rel = this.rel(p);
|
|
73
|
+
const action = { type: "backup", path: `${rel}.bak` };
|
|
74
|
+
if (!this.dryRun)
|
|
75
|
+
fs.copyFileSync(abs, `${abs}.bak`);
|
|
76
|
+
this.actions.push(action);
|
|
77
|
+
return action;
|
|
78
|
+
}
|
|
79
|
+
mkdir(p) {
|
|
80
|
+
if (!this.dryRun)
|
|
81
|
+
fs.mkdirSync(this.abs(p), { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
created() {
|
|
84
|
+
return this.actions.filter((a) => a.type === "create");
|
|
85
|
+
}
|
|
86
|
+
updated() {
|
|
87
|
+
return this.actions.filter((a) => a.type === "update");
|
|
88
|
+
}
|
|
89
|
+
skipped() {
|
|
90
|
+
return this.actions.filter((a) => a.type === "skip");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ParsedDoc {
|
|
2
|
+
file: string;
|
|
3
|
+
data: Record<string, unknown>;
|
|
4
|
+
content: string;
|
|
5
|
+
hasFrontmatter: boolean;
|
|
6
|
+
parseError: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseDocFile(absPath: string): ParsedDoc;
|
|
9
|
+
/** Extract markdown heading texts (any level) from content. */
|
|
10
|
+
export declare function extractHeadings(content: string): string[];
|
|
11
|
+
/** True if any heading contains one of the given texts (case-insensitive). */
|
|
12
|
+
export declare function hasSection(content: string, names: string[]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* True if there is any non-empty, non-comment body text under a matching
|
|
15
|
+
* heading. Sub-headings (deeper levels) belong to the section, so their body
|
|
16
|
+
* text counts; the section ends at the next heading of the same or shallower
|
|
17
|
+
* level.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sectionHasContent(content: string, names: string[]): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import matter from "gray-matter";
|
|
3
|
+
export function parseDocFile(absPath) {
|
|
4
|
+
const raw = fs.readFileSync(absPath, "utf8");
|
|
5
|
+
const hasFrontmatter = /^---\r?\n/.test(raw);
|
|
6
|
+
try {
|
|
7
|
+
const parsed = matter(raw);
|
|
8
|
+
return {
|
|
9
|
+
file: absPath,
|
|
10
|
+
data: parsed.data ?? {},
|
|
11
|
+
content: parsed.content,
|
|
12
|
+
hasFrontmatter,
|
|
13
|
+
parseError: null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return {
|
|
18
|
+
file: absPath,
|
|
19
|
+
data: {},
|
|
20
|
+
content: raw,
|
|
21
|
+
hasFrontmatter,
|
|
22
|
+
parseError: e.message,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Extract markdown heading texts (any level) from content. */
|
|
27
|
+
export function extractHeadings(content) {
|
|
28
|
+
const headings = [];
|
|
29
|
+
let inFence = false;
|
|
30
|
+
for (const line of content.split(/\r?\n/)) {
|
|
31
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
32
|
+
inFence = !inFence;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (inFence)
|
|
36
|
+
continue;
|
|
37
|
+
const m = /^#{1,6}\s+(.+?)\s*$/.exec(line);
|
|
38
|
+
if (m)
|
|
39
|
+
headings.push(m[1]);
|
|
40
|
+
}
|
|
41
|
+
return headings;
|
|
42
|
+
}
|
|
43
|
+
/** True if any heading contains one of the given texts (case-insensitive). */
|
|
44
|
+
export function hasSection(content, names) {
|
|
45
|
+
const headings = extractHeadings(content).map((h) => h.toLowerCase());
|
|
46
|
+
return names.some((n) => headings.some((h) => h.includes(n.toLowerCase())));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* True if there is any non-empty, non-comment body text under a matching
|
|
50
|
+
* heading. Sub-headings (deeper levels) belong to the section, so their body
|
|
51
|
+
* text counts; the section ends at the next heading of the same or shallower
|
|
52
|
+
* level.
|
|
53
|
+
*/
|
|
54
|
+
export function sectionHasContent(content, names) {
|
|
55
|
+
const lines = content.split(/\r?\n/);
|
|
56
|
+
let capturing = false;
|
|
57
|
+
let sectionLevel = 0;
|
|
58
|
+
let inFence = false;
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
if (/^\s*(```|~~~)/.test(line))
|
|
61
|
+
inFence = !inFence;
|
|
62
|
+
const m = !inFence ? /^(#{1,6})\s+(.+?)\s*$/.exec(line) : null;
|
|
63
|
+
if (m) {
|
|
64
|
+
const level = m[1].length;
|
|
65
|
+
if (names.some((n) => m[2].toLowerCase().includes(n.toLowerCase()))) {
|
|
66
|
+
capturing = true;
|
|
67
|
+
sectionLevel = level;
|
|
68
|
+
}
|
|
69
|
+
else if (capturing && level <= sectionLevel) {
|
|
70
|
+
capturing = false;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!capturing)
|
|
75
|
+
continue;
|
|
76
|
+
const text = line
|
|
77
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
78
|
+
.replace(/^[-*+]\s*/, "")
|
|
79
|
+
.trim();
|
|
80
|
+
if (text.length > 0 && !/^<!--/.test(line.trim()))
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
export const logger = {
|
|
3
|
+
ok(msg) {
|
|
4
|
+
console.log(`${pc.green("✓")} ${msg}`);
|
|
5
|
+
},
|
|
6
|
+
warn(msg) {
|
|
7
|
+
console.log(`${pc.yellow("WARN")} ${msg}`);
|
|
8
|
+
},
|
|
9
|
+
error(msg) {
|
|
10
|
+
console.error(`${pc.red("✗")} ${msg}`);
|
|
11
|
+
},
|
|
12
|
+
info(msg) {
|
|
13
|
+
console.log(msg);
|
|
14
|
+
},
|
|
15
|
+
heading(msg) {
|
|
16
|
+
console.log(pc.bold(msg));
|
|
17
|
+
console.log();
|
|
18
|
+
},
|
|
19
|
+
blank() {
|
|
20
|
+
console.log();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Convert an arbitrary name ("Payment Callback", "withdrawal_v2") to kebab-case. */
|
|
2
|
+
export declare function slugify(input: string): string;
|
|
3
|
+
export declare function todayISO(): string;
|
|
4
|
+
/** Days elapsed since the given YYYY-MM-DD date; NaN when unparsable. */
|
|
5
|
+
export declare function daysSince(dateStr: string): number;
|
|
6
|
+
export declare function isValidISODate(value: unknown): boolean;
|
|
7
|
+
/** Normalize a frontmatter date value (gray-matter may parse it as Date). */
|
|
8
|
+
export declare function dateValueToString(value: unknown): string | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Convert an arbitrary name ("Payment Callback", "withdrawal_v2") to kebab-case. */
|
|
2
|
+
export function slugify(input) {
|
|
3
|
+
const slug = input
|
|
4
|
+
.trim()
|
|
5
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
8
|
+
.replace(/-+/g, "-")
|
|
9
|
+
.replace(/^-|-$/g, "");
|
|
10
|
+
return slug;
|
|
11
|
+
}
|
|
12
|
+
export function todayISO() {
|
|
13
|
+
return new Date().toISOString().slice(0, 10);
|
|
14
|
+
}
|
|
15
|
+
/** Days elapsed since the given YYYY-MM-DD date; NaN when unparsable. */
|
|
16
|
+
export function daysSince(dateStr) {
|
|
17
|
+
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
18
|
+
if (Number.isNaN(d.getTime()))
|
|
19
|
+
return NaN;
|
|
20
|
+
return Math.floor((Date.now() - d.getTime()) / 86_400_000);
|
|
21
|
+
}
|
|
22
|
+
export function isValidISODate(value) {
|
|
23
|
+
if (value instanceof Date)
|
|
24
|
+
return !Number.isNaN(value.getTime());
|
|
25
|
+
if (typeof value !== "string")
|
|
26
|
+
return false;
|
|
27
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
28
|
+
return false;
|
|
29
|
+
return !Number.isNaN(new Date(`${value}T00:00:00Z`).getTime());
|
|
30
|
+
}
|
|
31
|
+
/** Normalize a frontmatter date value (gray-matter may parse it as Date). */
|
|
32
|
+
export function dateValueToString(value) {
|
|
33
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
34
|
+
return value.toISOString().slice(0, 10);
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "string" && isValidISODate(value))
|
|
37
|
+
return value;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Returns the risk keywords matched by the given name/title, empty when low-risk. */
|
|
2
|
+
export function matchRiskKeywords(name, keywords) {
|
|
3
|
+
const lower = name.toLowerCase();
|
|
4
|
+
return keywords.filter((k) => lower.includes(k.toLowerCase()));
|
|
5
|
+
}
|
|
6
|
+
export function isHighRisk(name, keywords) {
|
|
7
|
+
return matchRiskKeywords(name, keywords).length > 0;
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TemplateName } from "../templates/index.js";
|
|
2
|
+
export type TemplateVars = Record<string, string>;
|
|
3
|
+
/** Simple {{variable}} substitution; unknown variables are replaced with "". */
|
|
4
|
+
export declare function renderTemplate(template: string, vars: TemplateVars): string;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a template by name: a project-level override under
|
|
7
|
+
* `<templatesDir>/<name>.tmpl` wins over the built-in template.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadTemplate(name: TemplateName, root: string, templatesDir: string): string;
|
|
10
|
+
export declare function renderNamedTemplate(name: TemplateName, root: string, templatesDir: string, vars: TemplateVars): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { EXIT_CODES, MetaError } from "./errors.js";
|
|
4
|
+
import { BUILTIN_TEMPLATES } from "../templates/index.js";
|
|
5
|
+
/** Simple {{variable}} substitution; unknown variables are replaced with "". */
|
|
6
|
+
export function renderTemplate(template, vars) {
|
|
7
|
+
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => vars[key] ?? "");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a template by name: a project-level override under
|
|
11
|
+
* `<templatesDir>/<name>.tmpl` wins over the built-in template.
|
|
12
|
+
*/
|
|
13
|
+
export function loadTemplate(name, root, templatesDir) {
|
|
14
|
+
const override = path.join(root, templatesDir, `${name}.tmpl`);
|
|
15
|
+
if (fs.existsSync(override)) {
|
|
16
|
+
try {
|
|
17
|
+
return fs.readFileSync(override, "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
throw new MetaError(`Cannot read template override ${override}: ${e.message}`, EXIT_CODES.TEMPLATE_ERROR);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const builtin = BUILTIN_TEMPLATES[name];
|
|
24
|
+
if (!builtin) {
|
|
25
|
+
throw new MetaError(`Unknown template: ${name}`, EXIT_CODES.TEMPLATE_ERROR);
|
|
26
|
+
}
|
|
27
|
+
return builtin;
|
|
28
|
+
}
|
|
29
|
+
export function renderNamedTemplate(name, root, templatesDir, vars) {
|
|
30
|
+
return renderTemplate(loadTemplate(name, root, templatesDir), vars);
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Severity = "error" | "warning" | "info";
|
|
2
|
+
export interface MetaIssue {
|
|
3
|
+
code: string;
|
|
4
|
+
severity: Severity;
|
|
5
|
+
file?: string;
|
|
6
|
+
line?: number;
|
|
7
|
+
message: string;
|
|
8
|
+
suggestion?: string;
|
|
9
|
+
}
|
|
10
|
+
export type CheckStatus = "passed" | "passed_with_warnings" | "failed";
|
|
11
|
+
export interface MetaCheckResult {
|
|
12
|
+
status: CheckStatus;
|
|
13
|
+
issues: MetaIssue[];
|
|
14
|
+
summary: {
|
|
15
|
+
errors: number;
|
|
16
|
+
warnings: number;
|
|
17
|
+
infos: number;
|
|
18
|
+
checkedFiles: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function summarize(issues: MetaIssue[], checkedFiles: number): MetaCheckResult;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function summarize(issues, checkedFiles) {
|
|
2
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
3
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
4
|
+
const infos = issues.filter((i) => i.severity === "info").length;
|
|
5
|
+
const status = errors > 0 ? "failed" : warnings > 0 ? "passed_with_warnings" : "passed";
|
|
6
|
+
return { status, issues, summary: { errors, warnings, infos, checkedFiles } };
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const ADR_FILE_RE: RegExp;
|
|
2
|
+
export interface AdrFileInfo {
|
|
3
|
+
file: string;
|
|
4
|
+
number: number;
|
|
5
|
+
numberStr: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function listAdrFiles(adrDirAbs: string): AdrFileInfo[];
|
|
9
|
+
/** Next ADR number = max existing valid number + 1. Deleted numbers are never reused. */
|
|
10
|
+
export declare function nextAdrNumber(adrDirAbs: string): number;
|
|
11
|
+
export declare function padAdrNumber(n: number, padding: number): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const ADR_FILE_RE = /^(\d{4,})-(.+)\.md$/;
|
|
4
|
+
export function listAdrFiles(adrDirAbs) {
|
|
5
|
+
if (!fs.existsSync(adrDirAbs))
|
|
6
|
+
return [];
|
|
7
|
+
return fs
|
|
8
|
+
.readdirSync(adrDirAbs)
|
|
9
|
+
.filter((f) => ADR_FILE_RE.test(f))
|
|
10
|
+
.map((f) => {
|
|
11
|
+
const m = ADR_FILE_RE.exec(f);
|
|
12
|
+
return { file: path.join(adrDirAbs, f), number: parseInt(m[1], 10), numberStr: m[1], slug: m[2] };
|
|
13
|
+
})
|
|
14
|
+
.sort((a, b) => a.number - b.number);
|
|
15
|
+
}
|
|
16
|
+
/** Next ADR number = max existing valid number + 1. Deleted numbers are never reused. */
|
|
17
|
+
export function nextAdrNumber(adrDirAbs) {
|
|
18
|
+
const files = listAdrFiles(adrDirAbs);
|
|
19
|
+
if (files.length === 0)
|
|
20
|
+
return 1;
|
|
21
|
+
return Math.max(...files.map((f) => f.number)) + 1;
|
|
22
|
+
}
|
|
23
|
+
export function padAdrNumber(n, padding) {
|
|
24
|
+
return String(n).padStart(padding, "0");
|
|
25
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MetaConfig } from "../schemas/config.schema.js";
|
|
2
|
+
import type { FileWriter } from "../core/filesystem.js";
|
|
3
|
+
export declare function buildDomainIndexTable(root: string, config: MetaConfig): string;
|
|
4
|
+
export declare function buildAdrIndexTable(root: string, config: MetaConfig): string;
|
|
5
|
+
export declare function updateDomainIndex(writer: FileWriter, root: string, config: MetaConfig): void;
|
|
6
|
+
export declare function updateAdrIndex(writer: FileWriter, root: string, config: MetaConfig): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseDocFile } from "../core/frontmatter.js";
|
|
4
|
+
import { dateValueToString } from "../core/paths.js";
|
|
5
|
+
import { listAdrFiles } from "./adr-generator.js";
|
|
6
|
+
import { renderNamedTemplate } from "../core/templates.js";
|
|
7
|
+
import { todayISO } from "../core/paths.js";
|
|
8
|
+
const DOMAIN_MARKERS = ["<!-- meta:domain-index:start -->", "<!-- meta:domain-index:end -->"];
|
|
9
|
+
const ADR_MARKERS = ["<!-- meta:adr-index:start -->", "<!-- meta:adr-index:end -->"];
|
|
10
|
+
function replaceBetweenMarkers(content, [start, end], body) {
|
|
11
|
+
const startIdx = content.indexOf(start);
|
|
12
|
+
const endIdx = content.indexOf(end);
|
|
13
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
|
14
|
+
// No markers: append a managed section at the end.
|
|
15
|
+
return `${content.trimEnd()}\n\n${start}\n${body}\n${end}\n`;
|
|
16
|
+
}
|
|
17
|
+
return (content.slice(0, startIdx + start.length) + "\n" + body + "\n" + content.slice(endIdx));
|
|
18
|
+
}
|
|
19
|
+
function frontmatterStr(data, key) {
|
|
20
|
+
const v = data[key];
|
|
21
|
+
if (Array.isArray(v))
|
|
22
|
+
return v.join(", ");
|
|
23
|
+
if (v instanceof Date)
|
|
24
|
+
return v.toISOString().slice(0, 10);
|
|
25
|
+
if (v === undefined || v === null)
|
|
26
|
+
return "";
|
|
27
|
+
return String(v);
|
|
28
|
+
}
|
|
29
|
+
export function buildDomainIndexTable(root, config) {
|
|
30
|
+
const domainDir = path.join(root, config.paths.domain_docs);
|
|
31
|
+
const rows = [];
|
|
32
|
+
if (fs.existsSync(domainDir)) {
|
|
33
|
+
const dirs = fs
|
|
34
|
+
.readdirSync(domainDir, { withFileTypes: true })
|
|
35
|
+
.filter((d) => d.isDirectory())
|
|
36
|
+
.map((d) => d.name)
|
|
37
|
+
.sort();
|
|
38
|
+
for (const dir of dirs) {
|
|
39
|
+
const readme = path.join(domainDir, dir, "README.md");
|
|
40
|
+
if (!fs.existsSync(readme)) {
|
|
41
|
+
rows.push(`| [${dir}](./${dir}/) | — | — | — |`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const doc = parseDocFile(readme);
|
|
45
|
+
const title = frontmatterStr(doc.data, "title") || dir;
|
|
46
|
+
rows.push(`| [${title}](./${dir}/README.md) | ${frontmatterStr(doc.data, "status")} | ${frontmatterStr(doc.data, "owner")} | ${dateValueToString(doc.data["last_reviewed"]) ?? frontmatterStr(doc.data, "last_reviewed")} |`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
"| Domain | Status | Owner | Last Reviewed |",
|
|
51
|
+
"|---|---|---|---|",
|
|
52
|
+
...rows,
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
export function buildAdrIndexTable(root, config) {
|
|
56
|
+
const adrDir = path.join(root, config.paths.adr_docs);
|
|
57
|
+
const rows = listAdrFiles(adrDir).map((info) => {
|
|
58
|
+
const doc = parseDocFile(info.file);
|
|
59
|
+
const title = frontmatterStr(doc.data, "title") || info.slug;
|
|
60
|
+
const rel = path.basename(info.file);
|
|
61
|
+
return `| ${info.numberStr} | [${title}](./${rel}) | ${frontmatterStr(doc.data, "status")} | ${dateValueToString(doc.data["date"]) ?? frontmatterStr(doc.data, "date")} |`;
|
|
62
|
+
});
|
|
63
|
+
return ["| # | Title | Status | Date |", "|---|---|---|---|", ...rows].join("\n");
|
|
64
|
+
}
|
|
65
|
+
function updateIndexFile(writer, relPath, markers, table, templateName, root, config) {
|
|
66
|
+
const abs = path.join(root, relPath);
|
|
67
|
+
let content;
|
|
68
|
+
if (fs.existsSync(abs)) {
|
|
69
|
+
content = fs.readFileSync(abs, "utf8");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
content = renderNamedTemplate(templateName, root, config.paths.templates, {
|
|
73
|
+
owner: config.project.name || "team",
|
|
74
|
+
date: todayISO(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const updated = replaceBetweenMarkers(content, markers, table);
|
|
78
|
+
writer.write(relPath, updated, { overwrite: true });
|
|
79
|
+
}
|
|
80
|
+
export function updateDomainIndex(writer, root, config) {
|
|
81
|
+
const rel = path.join(config.paths.domain_docs, "README.md");
|
|
82
|
+
updateIndexFile(writer, rel, DOMAIN_MARKERS, buildDomainIndexTable(root, config), "domain-index-readme.md", root, config);
|
|
83
|
+
}
|
|
84
|
+
export function updateAdrIndex(writer, root, config) {
|
|
85
|
+
const rel = path.join(config.paths.adr_docs, "README.md");
|
|
86
|
+
updateIndexFile(writer, rel, ADR_MARKERS, buildAdrIndexTable(root, config), "adr-index-readme.md", root, config);
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { runCheck, checkExitCode, toJsonOutput } from "./commands/check.js";
|
|
2
|
+
export { runInit } from "./commands/init.js";
|
|
3
|
+
export { runNewDomain } from "./commands/new-domain.js";
|
|
4
|
+
export { runNewAdr } from "./commands/new-adr.js";
|
|
5
|
+
export { runAudit } from "./commands/audit.js";
|
|
6
|
+
export { runSync } from "./commands/sync.js";
|
|
7
|
+
export { loadConfig, CONFIG_FILENAME } from "./core/config.js";
|
|
8
|
+
export { configSchema, DEFAULT_CONFIG, type MetaConfig } from "./schemas/config.schema.js";
|
|
9
|
+
export type { MetaIssue, MetaCheckResult, Severity } from "./core/types.js";
|
|
10
|
+
export { slugify } from "./core/paths.js";
|
|
11
|
+
export { nextAdrNumber, padAdrNumber, listAdrFiles } from "./generators/adr-generator.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { runCheck, checkExitCode, toJsonOutput } from "./commands/check.js";
|
|
2
|
+
export { runInit } from "./commands/init.js";
|
|
3
|
+
export { runNewDomain } from "./commands/new-domain.js";
|
|
4
|
+
export { runNewAdr } from "./commands/new-adr.js";
|
|
5
|
+
export { runAudit } from "./commands/audit.js";
|
|
6
|
+
export { runSync } from "./commands/sync.js";
|
|
7
|
+
export { loadConfig, CONFIG_FILENAME } from "./core/config.js";
|
|
8
|
+
export { configSchema, DEFAULT_CONFIG } from "./schemas/config.schema.js";
|
|
9
|
+
export { slugify } from "./core/paths.js";
|
|
10
|
+
export { nextAdrNumber, padAdrNumber, listAdrFiles } from "./generators/adr-generator.js";
|