@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,109 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { loadConfig } from "../core/config.js";
|
|
3
|
+
import { EXIT_CODES } from "../core/errors.js";
|
|
4
|
+
import { summarize } from "../core/types.js";
|
|
5
|
+
import { validateProject } from "../validators/project-validator.js";
|
|
6
|
+
import { validateDocs } from "../validators/docs-validator.js";
|
|
7
|
+
import { validateAdrs } from "../validators/adr-validator.js";
|
|
8
|
+
import { validateAgents } from "../validators/agents-validator.js";
|
|
9
|
+
import { validateDomains } from "../validators/domain-validator.js";
|
|
10
|
+
import { validateOpenSpec } from "../validators/openspec-validator.js";
|
|
11
|
+
export function runCheck(root, opts = {}) {
|
|
12
|
+
const { config, configPath } = loadConfig(root);
|
|
13
|
+
const ctx = { root, config, configFound: configPath !== null };
|
|
14
|
+
const scoped = Boolean(opts.docs || opts.adr || opts.agents || opts.openspec);
|
|
15
|
+
const issues = [];
|
|
16
|
+
let checkedFiles = 0;
|
|
17
|
+
const run = (fn) => {
|
|
18
|
+
// A single broken validator must not crash the whole check.
|
|
19
|
+
try {
|
|
20
|
+
const r = fn(ctx);
|
|
21
|
+
issues.push(...r.issues);
|
|
22
|
+
checkedFiles += r.checkedFiles;
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
issues.push({
|
|
26
|
+
code: "CHECK_INTERNAL_ERROR",
|
|
27
|
+
severity: "error",
|
|
28
|
+
message: `Validator crashed: ${e.message}`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
if (!scoped) {
|
|
33
|
+
run(validateProject);
|
|
34
|
+
run(validateDocs);
|
|
35
|
+
run(validateAdrs);
|
|
36
|
+
run(validateAgents);
|
|
37
|
+
run(validateDomains);
|
|
38
|
+
run(validateOpenSpec);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
if (opts.docs)
|
|
42
|
+
run(validateDocs);
|
|
43
|
+
if (opts.adr)
|
|
44
|
+
run(validateAdrs);
|
|
45
|
+
if (opts.agents)
|
|
46
|
+
run(validateAgents);
|
|
47
|
+
if (opts.openspec)
|
|
48
|
+
run(validateOpenSpec);
|
|
49
|
+
}
|
|
50
|
+
return summarize(issues, checkedFiles);
|
|
51
|
+
}
|
|
52
|
+
export function printCheckResult(result, strict) {
|
|
53
|
+
console.log(pc.bold("Agent Meta Check"));
|
|
54
|
+
console.log();
|
|
55
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
56
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
57
|
+
if (result.issues.length === 0) {
|
|
58
|
+
console.log(`${pc.green("✓")} No issues found (${result.summary.checkedFiles} files checked)`);
|
|
59
|
+
}
|
|
60
|
+
for (const issue of errors) {
|
|
61
|
+
console.log(`${pc.red("ERROR")} ${pc.bold(issue.code)}`);
|
|
62
|
+
if (issue.file)
|
|
63
|
+
console.log(`${issue.file}${issue.line ? `:${issue.line}` : ""}`);
|
|
64
|
+
console.log();
|
|
65
|
+
console.log(issue.message);
|
|
66
|
+
if (issue.suggestion) {
|
|
67
|
+
console.log();
|
|
68
|
+
console.log("Fix:");
|
|
69
|
+
console.log(issue.suggestion);
|
|
70
|
+
}
|
|
71
|
+
console.log();
|
|
72
|
+
}
|
|
73
|
+
for (const issue of warnings) {
|
|
74
|
+
console.log(`${pc.yellow("WARN")} ${issue.file ?? ""}${issue.line ? `:${issue.line}` : ""}`);
|
|
75
|
+
console.log(` ${issue.message}`);
|
|
76
|
+
if (issue.suggestion)
|
|
77
|
+
console.log(` Suggestion: ${issue.suggestion}`);
|
|
78
|
+
console.log();
|
|
79
|
+
}
|
|
80
|
+
const label = result.status === "failed"
|
|
81
|
+
? pc.red("FAILED")
|
|
82
|
+
: result.status === "passed_with_warnings"
|
|
83
|
+
? strict
|
|
84
|
+
? pc.red("FAILED (strict: warnings treated as errors)")
|
|
85
|
+
: pc.yellow("PASS WITH WARNINGS")
|
|
86
|
+
: pc.green("PASS");
|
|
87
|
+
console.log(`Result: ${label}`);
|
|
88
|
+
console.log(`Errors: ${result.summary.errors}`);
|
|
89
|
+
console.log(`Warnings: ${result.summary.warnings}`);
|
|
90
|
+
}
|
|
91
|
+
export function checkExitCode(result, strict) {
|
|
92
|
+
if (result.summary.errors > 0)
|
|
93
|
+
return EXIT_CODES.CHECK_FAILED;
|
|
94
|
+
if (strict && result.summary.warnings > 0)
|
|
95
|
+
return EXIT_CODES.CHECK_FAILED;
|
|
96
|
+
return EXIT_CODES.OK;
|
|
97
|
+
}
|
|
98
|
+
export function toJsonOutput(result) {
|
|
99
|
+
return {
|
|
100
|
+
status: result.status,
|
|
101
|
+
errors: result.issues
|
|
102
|
+
.filter((i) => i.severity === "error")
|
|
103
|
+
.map(({ code, file, line, message, suggestion }) => ({ code, file, line, message, suggestion })),
|
|
104
|
+
warnings: result.issues
|
|
105
|
+
.filter((i) => i.severity === "warning")
|
|
106
|
+
.map(({ code, file, line, message, suggestion }) => ({ code, file, line, message, suggestion })),
|
|
107
|
+
summary: result.summary,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { loadConfig, CONFIG_FILENAME } from "../core/config.js";
|
|
5
|
+
import { configSchema } from "../schemas/config.schema.js";
|
|
6
|
+
import { logger } from "../core/logger.js";
|
|
7
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
8
|
+
function flatten(obj, prefix = "") {
|
|
9
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
10
|
+
return [[prefix, obj]];
|
|
11
|
+
}
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
14
|
+
out.push(...flatten(v, prefix ? `${prefix}.${k}` : k));
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
export function runConfigList(root) {
|
|
19
|
+
const { config } = loadConfig(root);
|
|
20
|
+
for (const [key, value] of flatten(config)) {
|
|
21
|
+
const rendered = Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
|
|
22
|
+
logger.info(`${key} = ${rendered}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function coerce(value) {
|
|
26
|
+
if (value === "true")
|
|
27
|
+
return true;
|
|
28
|
+
if (value === "false")
|
|
29
|
+
return false;
|
|
30
|
+
if (/^-?\d+$/.test(value))
|
|
31
|
+
return parseInt(value, 10);
|
|
32
|
+
if (/^-?\d+\.\d+$/.test(value))
|
|
33
|
+
return parseFloat(value);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function runConfigSet(root, key, value) {
|
|
37
|
+
const configPath = path.join(root, CONFIG_FILENAME);
|
|
38
|
+
if (!fs.existsSync(configPath)) {
|
|
39
|
+
throw new MetaError(`${CONFIG_FILENAME} not found. Run \`meta init\` first.`, EXIT_CODES.CONFIG_UNPARSABLE);
|
|
40
|
+
}
|
|
41
|
+
const doc = YAML.parse(fs.readFileSync(configPath, "utf8")) ?? {};
|
|
42
|
+
const parts = key.split(".");
|
|
43
|
+
let cursor = doc;
|
|
44
|
+
for (const part of parts.slice(0, -1)) {
|
|
45
|
+
if (typeof cursor[part] !== "object" || cursor[part] === null)
|
|
46
|
+
cursor[part] = {};
|
|
47
|
+
cursor = cursor[part];
|
|
48
|
+
}
|
|
49
|
+
cursor[parts[parts.length - 1]] = coerce(value);
|
|
50
|
+
const validated = configSchema.safeParse(doc);
|
|
51
|
+
if (!validated.success) {
|
|
52
|
+
const details = validated.error.issues
|
|
53
|
+
.map((i) => ` ${i.path.join(".")}: ${i.message}`)
|
|
54
|
+
.join("\n");
|
|
55
|
+
throw new MetaError(`Invalid value for ${key}:\n${details}`, EXIT_CODES.BAD_ARGS);
|
|
56
|
+
}
|
|
57
|
+
fs.writeFileSync(configPath, YAML.stringify(doc), "utf8");
|
|
58
|
+
logger.ok(`Set ${key} = ${value}`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
yes?: boolean;
|
|
3
|
+
type?: string;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function detectProjectType(root: string): string;
|
|
7
|
+
export declare function detectPackageManager(root: string): string;
|
|
8
|
+
export declare function runInit(root: string, opts: InitOptions): Promise<void>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { input, select } from "@inquirer/prompts";
|
|
5
|
+
import { FileWriter } from "../core/filesystem.js";
|
|
6
|
+
import { logger } from "../core/logger.js";
|
|
7
|
+
import { renderNamedTemplate } from "../core/templates.js";
|
|
8
|
+
import { todayISO } from "../core/paths.js";
|
|
9
|
+
import { configSchema, DEFAULT_CONFIG } from "../schemas/config.schema.js";
|
|
10
|
+
import { CONFIG_FILENAME, configExists } from "../core/config.js";
|
|
11
|
+
import { padAdrNumber } from "../generators/adr-generator.js";
|
|
12
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
13
|
+
const PROJECT_TYPES = ["node", "monorepo", "nextjs", "cocos", "backend", "other"];
|
|
14
|
+
export function detectProjectType(root) {
|
|
15
|
+
if (fs.existsSync(path.join(root, "pnpm-workspace.yaml")))
|
|
16
|
+
return "monorepo";
|
|
17
|
+
const pkgPath = path.join(root, "package.json");
|
|
18
|
+
if (fs.existsSync(pkgPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
21
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
22
|
+
if (deps?.next)
|
|
23
|
+
return "nextjs";
|
|
24
|
+
if (deps?.["@nestjs/core"])
|
|
25
|
+
return "backend";
|
|
26
|
+
return "node";
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "node";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (fs.existsSync(path.join(root, "project.json")) && fs.existsSync(path.join(root, "assets")))
|
|
33
|
+
return "cocos";
|
|
34
|
+
return "other";
|
|
35
|
+
}
|
|
36
|
+
export function detectPackageManager(root) {
|
|
37
|
+
if (fs.existsSync(path.join(root, "pnpm-lock.yaml")))
|
|
38
|
+
return "pnpm";
|
|
39
|
+
if (fs.existsSync(path.join(root, "yarn.lock")))
|
|
40
|
+
return "yarn";
|
|
41
|
+
if (fs.existsSync(path.join(root, "package-lock.json")))
|
|
42
|
+
return "npm";
|
|
43
|
+
if (fs.existsSync(path.join(root, "package.json")))
|
|
44
|
+
return "npm";
|
|
45
|
+
return "none";
|
|
46
|
+
}
|
|
47
|
+
export async function runInit(root, opts) {
|
|
48
|
+
const dryRun = Boolean(opts.dryRun);
|
|
49
|
+
const writer = new FileWriter(root, dryRun);
|
|
50
|
+
const templatesDir = DEFAULT_CONFIG.paths.templates;
|
|
51
|
+
const date = todayISO();
|
|
52
|
+
const detectedType = detectProjectType(root);
|
|
53
|
+
const packageManager = detectPackageManager(root);
|
|
54
|
+
let projectName = path.basename(root);
|
|
55
|
+
let projectType = opts.type ?? detectedType;
|
|
56
|
+
if (opts.type && !PROJECT_TYPES.includes(opts.type)) {
|
|
57
|
+
throw new MetaError(`Unknown project type '${opts.type}'. Valid types: ${PROJECT_TYPES.join(", ")}`, EXIT_CODES.BAD_ARGS);
|
|
58
|
+
}
|
|
59
|
+
if (!opts.yes) {
|
|
60
|
+
projectName = await input({ message: "项目名称:", default: projectName });
|
|
61
|
+
if (!opts.type) {
|
|
62
|
+
projectType = await select({
|
|
63
|
+
message: "项目类型:",
|
|
64
|
+
choices: [
|
|
65
|
+
{ name: "Node.js / TypeScript", value: "node" },
|
|
66
|
+
{ name: "Monorepo", value: "monorepo" },
|
|
67
|
+
{ name: "Next.js / Fullstack", value: "nextjs" },
|
|
68
|
+
{ name: "Cocos / 游戏", value: "cocos" },
|
|
69
|
+
{ name: "Backend Service", value: "backend" },
|
|
70
|
+
{ name: "Other", value: "other" },
|
|
71
|
+
],
|
|
72
|
+
default: detectedType,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Existing AGENTS.md handling.
|
|
77
|
+
const agentsRel = DEFAULT_CONFIG.paths.root_agents;
|
|
78
|
+
let agentsAction = writer.exists(agentsRel) ? "keep" : "create";
|
|
79
|
+
if (agentsAction === "keep" && !opts.yes) {
|
|
80
|
+
const choice = await select({
|
|
81
|
+
message: `发现已有 ${agentsRel}。如何处理?`,
|
|
82
|
+
choices: [
|
|
83
|
+
{ name: "保留,不修改", value: "keep" },
|
|
84
|
+
{ name: "备份为 AGENTS.md.bak 后重新生成", value: "backup" },
|
|
85
|
+
{ name: "退出", value: "exit" },
|
|
86
|
+
],
|
|
87
|
+
default: "keep",
|
|
88
|
+
});
|
|
89
|
+
if (choice === "exit") {
|
|
90
|
+
logger.info("Aborted.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
agentsAction = choice;
|
|
94
|
+
}
|
|
95
|
+
logger.blank();
|
|
96
|
+
if (dryRun)
|
|
97
|
+
logger.info("Dry run — no files will be written.\n");
|
|
98
|
+
// 1. agent-meta.config.yaml
|
|
99
|
+
if (!configExists(root)) {
|
|
100
|
+
const config = configSchema.parse({
|
|
101
|
+
project: { name: projectName, type: projectType, package_manager: packageManager },
|
|
102
|
+
});
|
|
103
|
+
writer.write(CONFIG_FILENAME, YAML.stringify(config));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
writer.actions.push({ type: "skip", path: CONFIG_FILENAME });
|
|
107
|
+
}
|
|
108
|
+
// 2. AGENTS.md
|
|
109
|
+
const agentsVars = {
|
|
110
|
+
project_type: projectType,
|
|
111
|
+
package_manager: packageManager,
|
|
112
|
+
main_paths: "src/",
|
|
113
|
+
};
|
|
114
|
+
if (agentsAction === "backup") {
|
|
115
|
+
writer.backup(agentsRel);
|
|
116
|
+
writer.write(agentsRel, renderNamedTemplate("agents.md", root, templatesDir, agentsVars), {
|
|
117
|
+
overwrite: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else if (agentsAction === "create") {
|
|
121
|
+
writer.write(agentsRel, renderNamedTemplate("agents.md", root, templatesDir, agentsVars));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
writer.actions.push({ type: "skip", path: agentsRel });
|
|
125
|
+
}
|
|
126
|
+
// 3. docs skeleton
|
|
127
|
+
const owner = projectName || "team";
|
|
128
|
+
const vars = { owner, date };
|
|
129
|
+
writer.write("docs/domain/README.md", renderNamedTemplate("domain-index-readme.md", root, templatesDir, vars));
|
|
130
|
+
writer.write("docs/domain/glossary.md", renderNamedTemplate("glossary.md", root, templatesDir, vars));
|
|
131
|
+
writer.write("docs/adr/README.md", renderNamedTemplate("adr-index-readme.md", root, templatesDir, vars));
|
|
132
|
+
writer.write("docs/architecture/system-overview.md", renderNamedTemplate("system-overview.md", root, templatesDir, vars));
|
|
133
|
+
writer.write("docs/api/conventions.md", renderNamedTemplate("api-conventions.md", root, templatesDir, vars));
|
|
134
|
+
writer.write("docs/engineering/testing.md", renderNamedTemplate("engineering-testing.md", root, templatesDir, vars));
|
|
135
|
+
writer.write("docs/runbooks/README.md", renderNamedTemplate("runbooks-readme.md", root, templatesDir, vars));
|
|
136
|
+
// 4. First ADR
|
|
137
|
+
const adrNumber = padAdrNumber(1, DEFAULT_CONFIG.defaults.adr_padding);
|
|
138
|
+
writer.write(`docs/adr/${adrNumber}-repository-conventions.md`, renderNamedTemplate("adr-0001.md", root, templatesDir, { adr_number: adrNumber, owner, date }));
|
|
139
|
+
// Report
|
|
140
|
+
if (dryRun) {
|
|
141
|
+
const created = writer.created();
|
|
142
|
+
if (created.length > 0) {
|
|
143
|
+
logger.info("Would create:");
|
|
144
|
+
for (const a of created)
|
|
145
|
+
logger.info(` ${a.path}`);
|
|
146
|
+
}
|
|
147
|
+
const updated = writer.actions.filter((a) => a.type === "update" || a.type === "backup");
|
|
148
|
+
if (updated.length > 0) {
|
|
149
|
+
logger.info("Would update:");
|
|
150
|
+
for (const a of updated)
|
|
151
|
+
logger.info(` ${a.path}`);
|
|
152
|
+
}
|
|
153
|
+
const skipped = writer.skipped();
|
|
154
|
+
if (skipped.length > 0) {
|
|
155
|
+
logger.info("Would skip (already exists):");
|
|
156
|
+
for (const a of skipped)
|
|
157
|
+
logger.info(` ${a.path}`);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const a of writer.actions) {
|
|
162
|
+
if (a.type === "create")
|
|
163
|
+
logger.ok(`Created ${a.path}`);
|
|
164
|
+
else if (a.type === "update")
|
|
165
|
+
logger.ok(`Updated ${a.path}`);
|
|
166
|
+
else if (a.type === "backup")
|
|
167
|
+
logger.ok(`Backed up to ${a.path}`);
|
|
168
|
+
else
|
|
169
|
+
logger.info(`- Skipped ${a.path} (already exists)`);
|
|
170
|
+
}
|
|
171
|
+
if (fs.existsSync(path.join(root, DEFAULT_CONFIG.paths.openspec))) {
|
|
172
|
+
logger.ok(`OpenSpec detected at ${DEFAULT_CONFIG.paths.openspec}/`);
|
|
173
|
+
}
|
|
174
|
+
logger.blank();
|
|
175
|
+
logger.info("Next steps:");
|
|
176
|
+
logger.info("1. Fill in AGENTS.md project commands.");
|
|
177
|
+
logger.info("2. Add domain docs for core business modules: meta new-domain <name>");
|
|
178
|
+
logger.info("3. Run: meta check");
|
|
179
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FileWriter } from "../core/filesystem.js";
|
|
3
|
+
import { logger } from "../core/logger.js";
|
|
4
|
+
import { loadConfig } from "../core/config.js";
|
|
5
|
+
import { renderNamedTemplate } from "../core/templates.js";
|
|
6
|
+
import { slugify, todayISO } from "../core/paths.js";
|
|
7
|
+
import { nextAdrNumber, padAdrNumber, listAdrFiles } from "../generators/adr-generator.js";
|
|
8
|
+
import { updateAdrIndex } from "../generators/index-generator.js";
|
|
9
|
+
import { VALID_ADR_STATUSES } from "../validators/adr-validator.js";
|
|
10
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
11
|
+
export function runNewAdr(root, name, opts) {
|
|
12
|
+
const { config } = loadConfig(root);
|
|
13
|
+
const slug = slugify(name);
|
|
14
|
+
if (!slug) {
|
|
15
|
+
throw new MetaError(`Cannot derive a slug from '${name}'.`, EXIT_CODES.BAD_ARGS);
|
|
16
|
+
}
|
|
17
|
+
const status = opts.status ?? "proposed";
|
|
18
|
+
if (!VALID_ADR_STATUSES.includes(status)) {
|
|
19
|
+
throw new MetaError(`Invalid ADR status '${status}'. Valid: ${VALID_ADR_STATUSES.join(", ")}`, EXIT_CODES.BAD_ARGS);
|
|
20
|
+
}
|
|
21
|
+
const relDir = opts.path ?? config.paths.adr_docs;
|
|
22
|
+
const absDir = path.join(root, relDir);
|
|
23
|
+
const duplicateSlug = listAdrFiles(absDir).find((f) => f.slug === slug);
|
|
24
|
+
if (duplicateSlug) {
|
|
25
|
+
throw new MetaError(`An ADR with slug '${slug}' already exists: ${path.basename(duplicateSlug.file)}`, EXIT_CODES.CHECK_FAILED);
|
|
26
|
+
}
|
|
27
|
+
const number = nextAdrNumber(absDir);
|
|
28
|
+
const numberStr = padAdrNumber(number, config.defaults.adr_padding);
|
|
29
|
+
const fileRel = path.join(relDir, `${numberStr}-${slug}.md`);
|
|
30
|
+
const title = opts.title ?? slug;
|
|
31
|
+
const owner = opts.owner || config.project.name || "team";
|
|
32
|
+
const content = renderNamedTemplate("adr.md", root, config.paths.templates, {
|
|
33
|
+
adr_number: numberStr,
|
|
34
|
+
title,
|
|
35
|
+
status,
|
|
36
|
+
date: todayISO(),
|
|
37
|
+
owner,
|
|
38
|
+
slug,
|
|
39
|
+
});
|
|
40
|
+
const writer = new FileWriter(root, Boolean(opts.dryRun));
|
|
41
|
+
try {
|
|
42
|
+
// Exclusive create: two concurrent invocations can never share a number.
|
|
43
|
+
writer.writeExclusive(fileRel, content);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
throw new MetaError(e.message, EXIT_CODES.CHECK_FAILED);
|
|
47
|
+
}
|
|
48
|
+
if (config.features.generate_indexes && !opts.dryRun) {
|
|
49
|
+
updateAdrIndex(writer, root, config);
|
|
50
|
+
}
|
|
51
|
+
if (opts.dryRun) {
|
|
52
|
+
logger.info("Would create:");
|
|
53
|
+
logger.info(` ${fileRel}`);
|
|
54
|
+
if (config.features.generate_indexes) {
|
|
55
|
+
logger.info("Would update:");
|
|
56
|
+
logger.info(` ${path.join(relDir, "README.md")}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
logger.ok(`Created ${fileRel}`);
|
|
61
|
+
if (config.features.generate_indexes) {
|
|
62
|
+
logger.ok(`Updated ${path.join(relDir, "README.md")}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface NewDomainOptions {
|
|
2
|
+
title?: string;
|
|
3
|
+
owner?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
withStateMachine?: boolean;
|
|
6
|
+
withGlossary?: boolean;
|
|
7
|
+
withLocalAgents?: boolean;
|
|
8
|
+
/** Commander sets this to false for --no-local-agents. */
|
|
9
|
+
localAgents?: boolean;
|
|
10
|
+
force?: boolean;
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function runNewDomain(root: string, name: string, opts: NewDomainOptions): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FileWriter } from "../core/filesystem.js";
|
|
4
|
+
import { logger } from "../core/logger.js";
|
|
5
|
+
import { loadConfig } from "../core/config.js";
|
|
6
|
+
import { renderNamedTemplate } from "../core/templates.js";
|
|
7
|
+
import { slugify, todayISO } from "../core/paths.js";
|
|
8
|
+
import { matchRiskKeywords } from "../core/risk.js";
|
|
9
|
+
import { updateDomainIndex } from "../generators/index-generator.js";
|
|
10
|
+
import { EXIT_CODES, MetaError } from "../core/errors.js";
|
|
11
|
+
export function runNewDomain(root, name, opts) {
|
|
12
|
+
const { config } = loadConfig(root);
|
|
13
|
+
const slug = slugify(name);
|
|
14
|
+
if (!slug) {
|
|
15
|
+
throw new MetaError(`Cannot derive a kebab-case directory name from '${name}'. Use an ASCII name and set the Chinese title via --title.`, EXIT_CODES.BAD_ARGS);
|
|
16
|
+
}
|
|
17
|
+
const relDir = opts.path ?? path.join(config.paths.domain_docs, slug);
|
|
18
|
+
const absDir = path.join(root, relDir);
|
|
19
|
+
const dirExists = fs.existsSync(absDir);
|
|
20
|
+
if (dirExists && !opts.force) {
|
|
21
|
+
throw new MetaError(`Domain directory already exists: ${relDir}\nUse --force to create missing files only (existing content is never overwritten).`, EXIT_CODES.CHECK_FAILED);
|
|
22
|
+
}
|
|
23
|
+
const title = opts.title ?? name;
|
|
24
|
+
const owner = opts.owner || config.project.name || "team";
|
|
25
|
+
const date = todayISO();
|
|
26
|
+
const templatesDir = config.paths.templates;
|
|
27
|
+
const withStateMachine = opts.withStateMachine ?? config.domain.create_state_machine_by_default;
|
|
28
|
+
const withGlossary = opts.withGlossary ?? config.domain.create_glossary_by_default;
|
|
29
|
+
const withLocalAgents = opts.localAgents === false
|
|
30
|
+
? false
|
|
31
|
+
: (opts.withLocalAgents ?? config.domain.create_local_agents_by_default);
|
|
32
|
+
const withRules = config.domain.create_rules_by_default;
|
|
33
|
+
const vars = {
|
|
34
|
+
domain_name: slug,
|
|
35
|
+
domain_title: title,
|
|
36
|
+
owner,
|
|
37
|
+
date,
|
|
38
|
+
slug,
|
|
39
|
+
state_machine_link: withStateMachine ? "- [状态机](./state-machine.md)\n" : "",
|
|
40
|
+
glossary_link: withGlossary ? "- [术语表](./glossary.md)\n" : "",
|
|
41
|
+
state_machine_reading: withStateMachine ? "- `state-machine.md`" : "",
|
|
42
|
+
};
|
|
43
|
+
const writer = new FileWriter(root, Boolean(opts.dryRun));
|
|
44
|
+
// --force only fills in missing files; FileWriter.write never overwrites here.
|
|
45
|
+
writer.write(path.join(relDir, "README.md"), renderNamedTemplate("domain-readme.md", root, templatesDir, vars));
|
|
46
|
+
if (withRules) {
|
|
47
|
+
writer.write(path.join(relDir, "rules.md"), renderNamedTemplate("domain-rules.md", root, templatesDir, vars));
|
|
48
|
+
}
|
|
49
|
+
if (withStateMachine) {
|
|
50
|
+
writer.write(path.join(relDir, "state-machine.md"), renderNamedTemplate("domain-state-machine.md", root, templatesDir, vars));
|
|
51
|
+
}
|
|
52
|
+
if (withGlossary) {
|
|
53
|
+
writer.write(path.join(relDir, "glossary.md"), renderNamedTemplate("domain-glossary.md", root, templatesDir, vars));
|
|
54
|
+
}
|
|
55
|
+
if (withLocalAgents) {
|
|
56
|
+
writer.write(path.join(relDir, "AGENTS.md"), renderNamedTemplate("domain-agents.md", root, templatesDir, vars));
|
|
57
|
+
}
|
|
58
|
+
if (config.features.generate_indexes && !opts.dryRun) {
|
|
59
|
+
updateDomainIndex(writer, root, config);
|
|
60
|
+
}
|
|
61
|
+
if (opts.dryRun) {
|
|
62
|
+
logger.info("Would create:");
|
|
63
|
+
for (const a of writer.created())
|
|
64
|
+
logger.info(` ${a.path}`);
|
|
65
|
+
for (const a of writer.skipped())
|
|
66
|
+
logger.info(` (skip, exists) ${a.path}`);
|
|
67
|
+
if (config.features.generate_indexes) {
|
|
68
|
+
logger.info("Would update:");
|
|
69
|
+
logger.info(` ${path.join(config.paths.domain_docs, "README.md")}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (const a of writer.created())
|
|
74
|
+
logger.ok(`Created ${a.path}`);
|
|
75
|
+
for (const a of writer.updated())
|
|
76
|
+
logger.ok(`Updated ${a.path}`);
|
|
77
|
+
for (const a of writer.skipped())
|
|
78
|
+
logger.info(`- Skipped ${a.path} (already exists)`);
|
|
79
|
+
}
|
|
80
|
+
const riskHits = matchRiskKeywords(slug, config.risk_keywords);
|
|
81
|
+
if (riskHits.length > 0) {
|
|
82
|
+
logger.blank();
|
|
83
|
+
logger.warn(`⚠ High-risk domain detected: ${slug} (matched: ${riskHits.join(", ")})`);
|
|
84
|
+
logger.blank();
|
|
85
|
+
const missing = [];
|
|
86
|
+
if (!withStateMachine)
|
|
87
|
+
missing.push(" --with-state-machine");
|
|
88
|
+
if (!withLocalAgents)
|
|
89
|
+
missing.push(" --with-local-agents");
|
|
90
|
+
if (!withGlossary)
|
|
91
|
+
missing.push(" --with-glossary");
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
logger.info("Recommended options:");
|
|
94
|
+
for (const m of missing)
|
|
95
|
+
logger.info(m);
|
|
96
|
+
logger.blank();
|
|
97
|
+
}
|
|
98
|
+
logger.info("Also consider creating ADRs for:");
|
|
99
|
+
logger.info(" - idempotency");
|
|
100
|
+
logger.info(" - audit logging");
|
|
101
|
+
logger.info(" - money storage");
|
|
102
|
+
logger.info(" - retry strategy");
|
|
103
|
+
logger.blank();
|
|
104
|
+
logger.info(`Recommended next command:\n meta new-adr ${slug}-idempotency`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { FileWriter } from "../core/filesystem.js";
|
|
5
|
+
import { logger } from "../core/logger.js";
|
|
6
|
+
import { loadConfig } from "../core/config.js";
|
|
7
|
+
import { updateAdrIndex, updateDomainIndex } from "../generators/index-generator.js";
|
|
8
|
+
export function runSync(root, opts) {
|
|
9
|
+
const { config } = loadConfig(root);
|
|
10
|
+
const all = !opts.indexes && !opts.openspec && !opts.templates;
|
|
11
|
+
if (all || opts.indexes) {
|
|
12
|
+
const writer = new FileWriter(root, Boolean(opts.dryRun));
|
|
13
|
+
updateDomainIndex(writer, root, config);
|
|
14
|
+
updateAdrIndex(writer, root, config);
|
|
15
|
+
for (const a of writer.actions) {
|
|
16
|
+
if (a.type === "skip")
|
|
17
|
+
continue;
|
|
18
|
+
if (opts.dryRun)
|
|
19
|
+
logger.info(`Would update: ${a.path}`);
|
|
20
|
+
else
|
|
21
|
+
logger.ok(`Updated ${a.path}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (all || opts.openspec) {
|
|
25
|
+
const openspecDir = path.join(root, config.paths.openspec);
|
|
26
|
+
if (!fs.existsSync(openspecDir)) {
|
|
27
|
+
if (opts.openspec)
|
|
28
|
+
logger.info("No openspec/ directory found; skipping OpenSpec sync.");
|
|
29
|
+
}
|
|
30
|
+
else if (opts.dryRun) {
|
|
31
|
+
logger.info("Would run: openspec update");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
try {
|
|
35
|
+
execSync("openspec update", { cwd: root, stdio: "inherit" });
|
|
36
|
+
logger.ok("OpenSpec updated");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
logger.warn("Could not run `openspec update` (is the OpenSpec CLI installed?). Skipped.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (opts.templates) {
|
|
44
|
+
logger.info("Template sync is planned for a later release (team template packs).");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type MetaConfig } from "../schemas/config.schema.js";
|
|
2
|
+
export declare const CONFIG_FILENAME = "agent-meta.config.yaml";
|
|
3
|
+
export interface LoadedConfig {
|
|
4
|
+
config: MetaConfig;
|
|
5
|
+
/** Absolute path of the config file, or null when running on built-in defaults. */
|
|
6
|
+
configPath: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Load agent-meta.config.yaml from the given directory, falling back to
|
|
10
|
+
* built-in defaults when the file does not exist. An unparsable or invalid
|
|
11
|
+
* config throws MetaError with exit code 3.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadConfig(cwd: string): LoadedConfig;
|
|
14
|
+
export declare function configExists(cwd: string): boolean;
|
|
15
|
+
export declare function saveConfig(cwd: string, config: MetaConfig): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { configSchema, DEFAULT_CONFIG } from "../schemas/config.schema.js";
|
|
5
|
+
import { EXIT_CODES, MetaError } from "./errors.js";
|
|
6
|
+
export const CONFIG_FILENAME = "agent-meta.config.yaml";
|
|
7
|
+
/**
|
|
8
|
+
* Load agent-meta.config.yaml from the given directory, falling back to
|
|
9
|
+
* built-in defaults when the file does not exist. An unparsable or invalid
|
|
10
|
+
* config throws MetaError with exit code 3.
|
|
11
|
+
*/
|
|
12
|
+
export function loadConfig(cwd) {
|
|
13
|
+
const configPath = path.join(cwd, CONFIG_FILENAME);
|
|
14
|
+
if (!fs.existsSync(configPath)) {
|
|
15
|
+
return { config: DEFAULT_CONFIG, configPath: null };
|
|
16
|
+
}
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = YAML.parse(fs.readFileSync(configPath, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
throw new MetaError(`Cannot parse ${CONFIG_FILENAME}: ${e.message}`, EXIT_CODES.CONFIG_UNPARSABLE);
|
|
23
|
+
}
|
|
24
|
+
const parsed = configSchema.safeParse(raw ?? {});
|
|
25
|
+
if (!parsed.success) {
|
|
26
|
+
const details = parsed.error.issues
|
|
27
|
+
.map((i) => ` ${i.path.join(".")}: ${i.message}`)
|
|
28
|
+
.join("\n");
|
|
29
|
+
throw new MetaError(`Invalid ${CONFIG_FILENAME}:\n${details}`, EXIT_CODES.CONFIG_UNPARSABLE);
|
|
30
|
+
}
|
|
31
|
+
return { config: parsed.data, configPath };
|
|
32
|
+
}
|
|
33
|
+
export function configExists(cwd) {
|
|
34
|
+
return fs.existsSync(path.join(cwd, CONFIG_FILENAME));
|
|
35
|
+
}
|
|
36
|
+
export function saveConfig(cwd, config) {
|
|
37
|
+
const configPath = path.join(cwd, CONFIG_FILENAME);
|
|
38
|
+
fs.writeFileSync(configPath, YAML.stringify(config), "utf8");
|
|
39
|
+
return configPath;
|
|
40
|
+
}
|