agent-workflow-kit-cli 1.2.0 ā 1.3.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/dist/cli/commands/add.js +50 -12
- package/dist/cli/commands/adr.js +98 -0
- package/dist/cli/commands/export.js +13 -0
- package/dist/cli/commands/init.js +136 -30
- package/dist/cli/commands/profile.js +35 -0
- package/dist/cli/commands/role.js +75 -0
- package/dist/cli/commands/run.js +78 -0
- package/dist/cli/commands/workflow.js +95 -0
- package/dist/cli/index.js +193 -2
- package/dist/core/awos/adr.js +208 -0
- package/dist/core/awos/intelligence.js +235 -0
- package/dist/core/awos/profiles.js +272 -0
- package/dist/core/awos/registry.js +224 -0
- package/dist/core/awos/runtime.js +322 -0
- package/dist/core/awos/types.js +5 -0
- package/dist/core/config.js +27 -0
- package/dist/core/parser.js +143 -0
- package/dist/core/renderer.js +74 -23
- package/package.json +3 -2
- package/templates/common/ide-rules.hbs +12 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { WorkflowRegistry } from "../../core/awos/registry.js";
|
|
9
|
+
import { WorkflowRuntime } from "../../core/awos/runtime.js";
|
|
10
|
+
const DEFAULT_WORKFLOW_DIR = ".agents/workflows";
|
|
11
|
+
export async function listWorkflows() {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
console.log(chalk.bold.cyan("\nš Discovered AWOS Workflow Packs"));
|
|
14
|
+
console.log(chalk.dim("------------------------------------------"));
|
|
15
|
+
const dir = path.join(cwd, DEFAULT_WORKFLOW_DIR);
|
|
16
|
+
const list = await WorkflowRegistry.discover(dir);
|
|
17
|
+
if (list.length === 0) {
|
|
18
|
+
console.log(chalk.gray(`No workflows found under '${DEFAULT_WORKFLOW_DIR}/'.`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const wf of list) {
|
|
22
|
+
console.log(`${chalk.bold.green(`- ${wf.id}`)} v${wf.version} : ${wf.name}`);
|
|
23
|
+
console.log(chalk.gray(` ${wf.description}`));
|
|
24
|
+
console.log(chalk.gray(` Roles: ${wf.requiredRoles.join(", ")} | Architectures: ${wf.supportedArchitectures.join(", ")}\n`));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function showWorkflow(id) {
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const dir = path.join(cwd, DEFAULT_WORKFLOW_DIR);
|
|
30
|
+
const wf = await WorkflowRegistry.load(id, dir);
|
|
31
|
+
console.log(chalk.bold.cyan(`\nš Workflow Details: ${wf.id}`));
|
|
32
|
+
console.log(chalk.dim("------------------------------------------"));
|
|
33
|
+
console.log(`${chalk.bold("Name:")} ${wf.name}`);
|
|
34
|
+
console.log(`${chalk.bold("Version:")} ${wf.version}`);
|
|
35
|
+
console.log(`${chalk.bold("Description:")} ${wf.description}`);
|
|
36
|
+
console.log(`${chalk.bold("Architectures:")} ${wf.supportedArchitectures.join(", ")}`);
|
|
37
|
+
console.log(`${chalk.bold("Required Roles:")} ${wf.requiredRoles.join(", ")}`);
|
|
38
|
+
console.log(chalk.dim("------------------------------------------"));
|
|
39
|
+
console.log(chalk.bold("Graph Execution Steps:"));
|
|
40
|
+
for (const node of wf.graph.nodes) {
|
|
41
|
+
const roleStr = node.role ? ` (Role: ${node.role})` : "";
|
|
42
|
+
const execStr = node.executor ? ` [Executor: ${node.executor}]` : "";
|
|
43
|
+
console.log(` - [${node.type.toUpperCase()}] ${chalk.green(node.id)}: "${node.name}"${roleStr}${execStr}`);
|
|
44
|
+
}
|
|
45
|
+
console.log(chalk.dim("------------------------------------------\n"));
|
|
46
|
+
}
|
|
47
|
+
export async function validateWorkflow(id) {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const dir = path.join(cwd, DEFAULT_WORKFLOW_DIR);
|
|
50
|
+
try {
|
|
51
|
+
const wf = await WorkflowRegistry.load(id, dir);
|
|
52
|
+
WorkflowRegistry.validate(wf);
|
|
53
|
+
console.log(chalk.bold.green(`\nāļø Workflow '${id}' is structurally valid and acyclic!`));
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error(chalk.bold.red(`\nā Workflow '${id}' is invalid:`));
|
|
57
|
+
console.error(chalk.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function runWorkflowById(id, options) {
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
const dir = path.join(cwd, DEFAULT_WORKFLOW_DIR);
|
|
64
|
+
const wf = await WorkflowRegistry.load(id, dir);
|
|
65
|
+
let inputs = {};
|
|
66
|
+
if (options.inputs) {
|
|
67
|
+
try {
|
|
68
|
+
const inputsPath = path.resolve(cwd, options.inputs);
|
|
69
|
+
const content = await fs.readFile(inputsPath, "utf8");
|
|
70
|
+
inputs = JSON.parse(content);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
try {
|
|
74
|
+
inputs = JSON.parse(options.inputs);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw new Error(`Failed to parse inputs parameters: ${err instanceof Error ? err.message : String(err)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.bold.cyan(`\nš Executing Discovered Workflow: ${wf.id}`));
|
|
82
|
+
console.log(chalk.dim("------------------------------------------"));
|
|
83
|
+
const runtime = new WorkflowRuntime(cwd);
|
|
84
|
+
const result = await runtime.run(wf.graph, inputs, { dryRun: options.dryRun });
|
|
85
|
+
console.log(chalk.dim("\n------------------------------------------"));
|
|
86
|
+
if (result.status === "SUCCESS") {
|
|
87
|
+
console.log(chalk.bold.green(`āļø Workflow execution succeeded! Run ID: ${result.runId}`));
|
|
88
|
+
}
|
|
89
|
+
else if (result.status === "PAUSED") {
|
|
90
|
+
console.log(chalk.bold.yellow(`ā ļø Workflow execution paused. Resume using: awk resume ${result.runId}`));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(chalk.bold.red(`ā Workflow execution failed. Run ID: ${result.runId}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -9,12 +9,17 @@ import { runAdd } from "./commands/add.js";
|
|
|
9
9
|
import { runSync } from "./commands/sync.js";
|
|
10
10
|
import { runDoctor } from "./commands/doctor.js";
|
|
11
11
|
import { runExport } from "./commands/export.js";
|
|
12
|
+
import { runWorkflowCommand, resumeWorkflowCommand } from "./commands/run.js";
|
|
13
|
+
import { runProfileCheck } from "./commands/profile.js";
|
|
14
|
+
import { listWorkflows, showWorkflow, validateWorkflow, runWorkflowById } from "./commands/workflow.js";
|
|
15
|
+
import { listRoles, showRole, validateRoles } from "./commands/role.js";
|
|
16
|
+
import { createAdrCommand, listAdrsCommand, showAdrCommand, searchAdrsCommand } from "./commands/adr.js";
|
|
12
17
|
const program = new Command();
|
|
13
18
|
export function runCli() {
|
|
14
19
|
program
|
|
15
20
|
.name("agent-workflow-kit")
|
|
16
21
|
.description("Generate AI coding workflows/rules/templates for Codex and Antigravity")
|
|
17
|
-
.version("1.
|
|
22
|
+
.version("1.3.0");
|
|
18
23
|
program
|
|
19
24
|
.command("init")
|
|
20
25
|
.description("Initialize agent guidelines and skills for the repository")
|
|
@@ -75,14 +80,200 @@ export function runCli() {
|
|
|
75
80
|
.command("export <target>")
|
|
76
81
|
.description("Export custom workflows/skills for the target agent (e.g., 'antigravity')")
|
|
77
82
|
.option("--no-clipboard", "Do not copy the exported instructions to clipboard", false)
|
|
83
|
+
.option("-o, --output <file>", "Write the exported guidelines to a specific file")
|
|
78
84
|
.action(async (target, options) => {
|
|
79
85
|
try {
|
|
80
|
-
await runExport(target, { clipboard: options.clipboard });
|
|
86
|
+
await runExport(target, { clipboard: options.clipboard, output: options.output });
|
|
81
87
|
}
|
|
82
88
|
catch (err) {
|
|
83
89
|
console.error(chalk.red(`Error running export: ${err instanceof Error ? err.message : String(err)}`));
|
|
84
90
|
process.exit(1);
|
|
85
91
|
}
|
|
86
92
|
});
|
|
93
|
+
program
|
|
94
|
+
.command("run <workflow>")
|
|
95
|
+
.description("Execute an AWOS graph workflow")
|
|
96
|
+
.option("--inputs <inputs>", "Path to input JSON file or inline JSON string")
|
|
97
|
+
.option("--dry-run", "Execute workflow steps in simulation mode", false)
|
|
98
|
+
.action(async (workflow, options) => {
|
|
99
|
+
try {
|
|
100
|
+
await runWorkflowCommand(workflow, options);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error(chalk.red(`Error running workflow: ${err instanceof Error ? err.message : String(err)}`));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
program
|
|
108
|
+
.command("resume <runId>")
|
|
109
|
+
.description("Resume a suspended AWOS workflow")
|
|
110
|
+
.action(async (runId) => {
|
|
111
|
+
try {
|
|
112
|
+
await resumeWorkflowCommand(runId);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
console.error(chalk.red(`Error resuming workflow: ${err instanceof Error ? err.message : String(err)}`));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
program
|
|
120
|
+
.command("profile")
|
|
121
|
+
.description("Validate directory architecture structure and rules boundaries")
|
|
122
|
+
.option("--profile <profile>", "Target profile name (e.g. clean-architecture)")
|
|
123
|
+
.action(async (options) => {
|
|
124
|
+
try {
|
|
125
|
+
await runProfileCheck(options);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.error(chalk.red(`Error validation profile rules: ${err instanceof Error ? err.message : String(err)}`));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// --- Workflow Group Subcommands ---
|
|
133
|
+
const workflowCmd = program.command("workflow").description("Manage and execute AWOS workflow packs");
|
|
134
|
+
workflowCmd
|
|
135
|
+
.command("list")
|
|
136
|
+
.description("List discovered workflow packs")
|
|
137
|
+
.action(async () => {
|
|
138
|
+
try {
|
|
139
|
+
await listWorkflows();
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
workflowCmd
|
|
147
|
+
.command("show <id>")
|
|
148
|
+
.description("Show workflow pack steps and metadata")
|
|
149
|
+
.action(async (id) => {
|
|
150
|
+
try {
|
|
151
|
+
await showWorkflow(id);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
workflowCmd
|
|
159
|
+
.command("validate <id>")
|
|
160
|
+
.description("Validate a workflow pack schema and graph cyclic constraints")
|
|
161
|
+
.action(async (id) => {
|
|
162
|
+
try {
|
|
163
|
+
await validateWorkflow(id);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
workflowCmd
|
|
171
|
+
.command("run <id>")
|
|
172
|
+
.description("Run a registered workflow pack by ID")
|
|
173
|
+
.option("--inputs <inputs>", "Path to inputs parameter JSON file or inline string")
|
|
174
|
+
.option("--dry-run", "Run nodes in dry-run mode", false)
|
|
175
|
+
.action(async (id, options) => {
|
|
176
|
+
try {
|
|
177
|
+
await runWorkflowById(id, options);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.error(chalk.red(`Error running workflow: ${err instanceof Error ? err.message : String(err)}`));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// --- Role Group Subcommands ---
|
|
185
|
+
const roleCmd = program.command("role").description("Manage agent role profiles");
|
|
186
|
+
roleCmd
|
|
187
|
+
.command("list")
|
|
188
|
+
.description("List all discovered role profiles in catalog")
|
|
189
|
+
.action(async () => {
|
|
190
|
+
try {
|
|
191
|
+
await listRoles();
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
roleCmd
|
|
199
|
+
.command("show <id>")
|
|
200
|
+
.description("Show agent role details, checklists and inputs schema")
|
|
201
|
+
.action(async (id) => {
|
|
202
|
+
try {
|
|
203
|
+
await showRole(id);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
roleCmd
|
|
211
|
+
.command("validate")
|
|
212
|
+
.description("Validate all roles catalog files schema parameters")
|
|
213
|
+
.action(async () => {
|
|
214
|
+
try {
|
|
215
|
+
await validateRoles();
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
// --- ADR Group Subcommands ---
|
|
223
|
+
const adrCmd = program.command("adr").description("Manage Architectural Decision Records");
|
|
224
|
+
adrCmd
|
|
225
|
+
.command("create")
|
|
226
|
+
.description("Create a new numbered architectural decision record (ADR)")
|
|
227
|
+
.option("--title <title>", "ADR decision title")
|
|
228
|
+
.option("--status <status>", "Initial status: proposed | accepted | rejected | superseded", "proposed")
|
|
229
|
+
.option("--context <context>", "Context background explanation")
|
|
230
|
+
.option("--decision <decision>", "Architectural decision detail statement")
|
|
231
|
+
.option("--consequences <consequences>", "Repercussions of decision taken")
|
|
232
|
+
.option("--decision-maker <maker>", "Role or name of decider", "AWOS System")
|
|
233
|
+
.action(async (options) => {
|
|
234
|
+
try {
|
|
235
|
+
await createAdrCommand(options);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
adrCmd
|
|
243
|
+
.command("list")
|
|
244
|
+
.description("List all saved ADR documents")
|
|
245
|
+
.action(async () => {
|
|
246
|
+
try {
|
|
247
|
+
await listAdrsCommand();
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
adrCmd
|
|
255
|
+
.command("show <id>")
|
|
256
|
+
.description("Display details of a numbered ADR document")
|
|
257
|
+
.action(async (id) => {
|
|
258
|
+
try {
|
|
259
|
+
await showAdrCommand(id);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
adrCmd
|
|
267
|
+
.command("search <keyword>")
|
|
268
|
+
.description("Search all ADR documents for keyword text matches")
|
|
269
|
+
.action(async (keyword) => {
|
|
270
|
+
try {
|
|
271
|
+
await searchAdrsCommand(keyword);
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
87
278
|
program.parse(process.argv);
|
|
88
279
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import Handlebars from "handlebars";
|
|
8
|
+
const ADR_DIR = "docs/adr";
|
|
9
|
+
const DEFAULT_TEMPLATE = `---
|
|
10
|
+
id: {{id}}
|
|
11
|
+
title: {{{title}}}
|
|
12
|
+
status: {{status}}
|
|
13
|
+
date: {{date}}
|
|
14
|
+
{{#if metadata.decisionMakerRole}}
|
|
15
|
+
decisionMakerRole: {{metadata.decisionMakerRole}}
|
|
16
|
+
{{/if}}
|
|
17
|
+
{{#if metadata.affectedModules}}
|
|
18
|
+
affectedModules: {{#each metadata.affectedModules}}{{#if @index}}, {{/if}}{{this}}{{/each}}
|
|
19
|
+
{{/if}}
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# {{{title}}}
|
|
23
|
+
|
|
24
|
+
## Context
|
|
25
|
+
{{{context}}}
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
{{{decision}}}
|
|
29
|
+
|
|
30
|
+
## Consequences
|
|
31
|
+
{{{consequences}}}
|
|
32
|
+
`;
|
|
33
|
+
export class ADRService {
|
|
34
|
+
/**
|
|
35
|
+
* Helper to parse a markdown ADR file back into a structured ADRDefinition.
|
|
36
|
+
*/
|
|
37
|
+
static parseMarkdown(content, id) {
|
|
38
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
39
|
+
const match = content.match(frontmatterRegex);
|
|
40
|
+
let title = "Untitled ADR";
|
|
41
|
+
let status = 'proposed';
|
|
42
|
+
let date = new Date().toISOString().split("T")[0];
|
|
43
|
+
let body = content;
|
|
44
|
+
const metadata = {};
|
|
45
|
+
if (match) {
|
|
46
|
+
const frontmatter = match[1];
|
|
47
|
+
body = match[2];
|
|
48
|
+
const lines = frontmatter.split("\n");
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
52
|
+
continue;
|
|
53
|
+
const eqIdx = trimmed.indexOf(":");
|
|
54
|
+
if (eqIdx !== -1) {
|
|
55
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
56
|
+
const value = trimmed.substring(eqIdx + 1).trim();
|
|
57
|
+
if (key === "title")
|
|
58
|
+
title = value.replace(/^['"]|['"]$/g, "");
|
|
59
|
+
else if (key === "status") {
|
|
60
|
+
const parsedStatus = value.replace(/^['"]|['"]$/g, "");
|
|
61
|
+
if (['proposed', 'accepted', 'rejected', 'superseded'].includes(parsedStatus)) {
|
|
62
|
+
status = parsedStatus;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (key === "date")
|
|
66
|
+
date = value;
|
|
67
|
+
else if (key === "decisionMakerRole")
|
|
68
|
+
metadata.decisionMakerRole = value;
|
|
69
|
+
else if (key === "affectedModules") {
|
|
70
|
+
metadata.affectedModules = value.split(",").map((s) => s.trim());
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Extract headers for sections (Context, Decision, Consequences)
|
|
76
|
+
let context = "";
|
|
77
|
+
let decision = "";
|
|
78
|
+
let consequences = "";
|
|
79
|
+
const contextMatch = body.match(/##\s+Context\r?\n([\s\S]*?)(?=##\s+Decision|##\s+Consequences|$)/i);
|
|
80
|
+
const decisionMatch = body.match(/##\s+Decision\r?\n([\s\S]*?)(?=##\s+Context|##\s+Consequences|$)/i);
|
|
81
|
+
const consequencesMatch = body.match(/##\s+Consequences\r?\n([\s\S]*?)(?=##\s+Context|##\s+Decision|$)/i);
|
|
82
|
+
if (contextMatch)
|
|
83
|
+
context = contextMatch[1].trim();
|
|
84
|
+
if (decisionMatch)
|
|
85
|
+
decision = decisionMatch[1].trim();
|
|
86
|
+
if (consequencesMatch)
|
|
87
|
+
consequences = consequencesMatch[1].trim();
|
|
88
|
+
// In case of headers missing
|
|
89
|
+
if (!context && !decision && !consequences) {
|
|
90
|
+
context = body.trim();
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
title,
|
|
95
|
+
date,
|
|
96
|
+
status,
|
|
97
|
+
context,
|
|
98
|
+
decision,
|
|
99
|
+
consequences,
|
|
100
|
+
metadata,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Scans docs/adr/ and calculates the next incremental number (e.g. ADR-0003).
|
|
105
|
+
*/
|
|
106
|
+
static async getNextId(workspaceRoot) {
|
|
107
|
+
const fullDir = path.join(workspaceRoot, ADR_DIR);
|
|
108
|
+
try {
|
|
109
|
+
const files = await fs.readdir(fullDir);
|
|
110
|
+
let maxNum = 0;
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
const match = file.match(/^ADR-(\d{4})\.md$/);
|
|
113
|
+
if (match) {
|
|
114
|
+
const num = parseInt(match[1], 10);
|
|
115
|
+
if (num > maxNum) {
|
|
116
|
+
maxNum = num;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const nextNum = maxNum + 1;
|
|
121
|
+
return `ADR-${String(nextNum).padStart(4, "0")}`;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return "ADR-0001";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Generates a new ADR file in the docs/adr/ directory.
|
|
129
|
+
*/
|
|
130
|
+
static async create(workspaceRoot, adr, templateString) {
|
|
131
|
+
const id = await ADRService.getNextId(workspaceRoot);
|
|
132
|
+
const date = new Date().toISOString().split("T")[0];
|
|
133
|
+
const fullDir = path.join(workspaceRoot, ADR_DIR);
|
|
134
|
+
const definition = {
|
|
135
|
+
...adr,
|
|
136
|
+
id,
|
|
137
|
+
date,
|
|
138
|
+
};
|
|
139
|
+
const template = Handlebars.compile(templateString || DEFAULT_TEMPLATE);
|
|
140
|
+
const rendered = template(definition);
|
|
141
|
+
await fs.mkdir(fullDir, { recursive: true });
|
|
142
|
+
const filePath = path.join(fullDir, `${id}.md`);
|
|
143
|
+
await fs.writeFile(filePath, rendered, "utf8");
|
|
144
|
+
return definition;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Loads a specific ADR by ID.
|
|
148
|
+
*/
|
|
149
|
+
static async load(workspaceRoot, id) {
|
|
150
|
+
const fullDir = path.join(workspaceRoot, ADR_DIR);
|
|
151
|
+
const filePath = path.join(fullDir, `${id}.md`);
|
|
152
|
+
try {
|
|
153
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
154
|
+
return ADRService.parseMarkdown(content, id);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
throw new Error(`Failed to load ADR '${id}': ${err instanceof Error ? err.message : String(err)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Lists all ADRs sorted by ID.
|
|
162
|
+
*/
|
|
163
|
+
static async list(workspaceRoot) {
|
|
164
|
+
const fullDir = path.join(workspaceRoot, ADR_DIR);
|
|
165
|
+
const adrs = [];
|
|
166
|
+
try {
|
|
167
|
+
const files = await fs.readdir(fullDir);
|
|
168
|
+
for (const file of files) {
|
|
169
|
+
if (file.match(/^ADR-\d{4}\.md$/)) {
|
|
170
|
+
const id = path.basename(file, ".md");
|
|
171
|
+
const adr = await ADRService.load(workspaceRoot, id);
|
|
172
|
+
adrs.push(adr);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Directory may not exist yet
|
|
178
|
+
}
|
|
179
|
+
return adrs.sort((a, b) => a.id.localeCompare(b.id));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Performs case-insensitive search across titles, contexts, decisions, and consequences.
|
|
183
|
+
*/
|
|
184
|
+
static async search(workspaceRoot, keyword) {
|
|
185
|
+
const all = await ADRService.list(workspaceRoot);
|
|
186
|
+
const searchLower = keyword.toLowerCase();
|
|
187
|
+
return all.filter((adr) => {
|
|
188
|
+
return (adr.title.toLowerCase().includes(searchLower) ||
|
|
189
|
+
adr.context.toLowerCase().includes(searchLower) ||
|
|
190
|
+
adr.decision.toLowerCase().includes(searchLower) ||
|
|
191
|
+
adr.consequences.toLowerCase().includes(searchLower));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Updates status metadata parameter for an ADR.
|
|
196
|
+
*/
|
|
197
|
+
static async updateStatus(workspaceRoot, id, newStatus) {
|
|
198
|
+
const adr = await ADRService.load(workspaceRoot, id);
|
|
199
|
+
adr.status = newStatus;
|
|
200
|
+
// Render it back
|
|
201
|
+
const template = Handlebars.compile(DEFAULT_TEMPLATE);
|
|
202
|
+
const rendered = template(adr);
|
|
203
|
+
const fullDir = path.join(workspaceRoot, ADR_DIR);
|
|
204
|
+
const filePath = path.join(fullDir, `${id}.md`);
|
|
205
|
+
await fs.writeFile(filePath, rendered, "utf8");
|
|
206
|
+
return adr;
|
|
207
|
+
}
|
|
208
|
+
}
|