feat(services): add decision service and refactor CLI
This commit is contained in:
parent
6c8ee37a08
commit
fff8a61ad9
@ -1,54 +1,22 @@
|
|||||||
import { input } from '@inquirer/prompts';
|
import { input } from '@inquirer/prompts';
|
||||||
import { join } from 'path';
|
import { createDecision, listDecisions } from '../../core/services/decisionService.js';
|
||||||
import { getEntityDir } from '../../core/paths.js';
|
import type { Decision } from '../../core/schema.js';
|
||||||
import { getNextId } from '../../core/counter.js';
|
|
||||||
import { readEntity, writeEntity } from '../../core/files.js';
|
|
||||||
import { DecisionSchema, type Decision } from '../../core/schema.js';
|
|
||||||
import { Index } from '../../core/index.js';
|
|
||||||
|
|
||||||
export async function decisionCreate(cwd: string, options: Partial<Decision> = {}): Promise<void> {
|
export async function decisionCreate(cwd: string, options: Partial<Decision> = {}): Promise<void> {
|
||||||
const title = options.title ?? await input({ message: 'Decision title:' });
|
const title = options.title ?? await input({ message: 'Decision title:' });
|
||||||
const context = options.context ?? await input({ message: 'Context:' });
|
const context = options.context ?? await input({ message: 'Context:' });
|
||||||
const decision = options.decision ?? await input({ message: 'Decision:' });
|
const decision = options.decision ?? await input({ message: 'Decision:' });
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const record = createDecision(cwd, { title, context, decision });
|
||||||
const record: Decision = DecisionSchema.parse({
|
|
||||||
id: getNextId(cwd, 'decision'),
|
|
||||||
title,
|
|
||||||
context,
|
|
||||||
decision,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
});
|
|
||||||
|
|
||||||
const filePath = join(getEntityDir(cwd, 'decisions'), `${record.id}.md`);
|
|
||||||
writeEntity(filePath, record, `# ${record.title}\n\n## Decision\n\n${record.decision}\n\n## Context\n\n${record.context}`);
|
|
||||||
|
|
||||||
const index = new Index(cwd);
|
|
||||||
index.upsert({
|
|
||||||
id: record.id,
|
|
||||||
type: 'decision',
|
|
||||||
title: record.title,
|
|
||||||
content: `${record.context} ${record.decision}`,
|
|
||||||
filePath,
|
|
||||||
createdAt: record.createdAt,
|
|
||||||
updatedAt: record.updatedAt,
|
|
||||||
});
|
|
||||||
index.close();
|
|
||||||
|
|
||||||
console.log(`Decision recorded: ${record.id}`);
|
console.log(`Decision recorded: ${record.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decisionList(cwd: string): void {
|
export function decisionList(cwd: string): void {
|
||||||
const index = new Index(cwd);
|
const decisions = listDecisions(cwd);
|
||||||
const decisions = index.list('decision');
|
|
||||||
index.close();
|
|
||||||
|
|
||||||
if (decisions.length === 0) {
|
if (decisions.length === 0) {
|
||||||
console.log('No decisions found.');
|
console.log('No decisions found.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const d of decisions) {
|
for (const d of decisions) {
|
||||||
console.log(`${d.id}: ${d.title}`);
|
console.log(`${d.id}: ${d.title}`);
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/core/services/decisionService.ts
Normal file
52
src/core/services/decisionService.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { getEntityDir } from '../paths.js';
|
||||||
|
import { getNextId } from '../counter.js';
|
||||||
|
import { readEntity, writeEntity } from '../files.js';
|
||||||
|
import { DecisionSchema, type Decision } from '../schema.js';
|
||||||
|
import { Index } from '../index.js';
|
||||||
|
|
||||||
|
export function createDecision(cwd: string, options: Partial<Decision> = {}): Decision {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const record: Decision = DecisionSchema.parse({
|
||||||
|
id: getNextId(cwd, 'decision'),
|
||||||
|
title: options.title ?? 'Decision',
|
||||||
|
context: options.context ?? '',
|
||||||
|
decision: options.decision ?? '',
|
||||||
|
status: options.status ?? 'accepted',
|
||||||
|
consequences: options.consequences ?? [],
|
||||||
|
alternatives: options.alternatives ?? [],
|
||||||
|
relatedDecisions: options.relatedDecisions ?? [],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePath = join(getEntityDir(cwd, 'decisions'), `${record.id}.md`);
|
||||||
|
writeEntity(filePath, record, `# ${record.title}\n\n## Decision\n\n${record.decision}\n\n## Context\n\n${record.context}`);
|
||||||
|
|
||||||
|
const index = new Index(cwd);
|
||||||
|
index.upsert({
|
||||||
|
id: record.id,
|
||||||
|
type: 'decision',
|
||||||
|
title: record.title,
|
||||||
|
content: `${record.context} ${record.decision}`,
|
||||||
|
filePath,
|
||||||
|
createdAt: record.createdAt,
|
||||||
|
updatedAt: record.updatedAt,
|
||||||
|
});
|
||||||
|
index.close();
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDecisions(cwd: string): ReturnType<Index['list']> {
|
||||||
|
const index = new Index(cwd);
|
||||||
|
const decisions = index.list('decision');
|
||||||
|
index.close();
|
||||||
|
return decisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDecision(cwd: string, id: string): { decision: Decision; body: string; filePath: string } {
|
||||||
|
const filePath = join(getEntityDir(cwd, 'decisions'), `${id}.md`);
|
||||||
|
const { frontmatter, body } = readEntity(filePath);
|
||||||
|
return { decision: DecisionSchema.parse(frontmatter), body, filePath };
|
||||||
|
}
|
||||||
19
tests/decisionService.test.ts
Normal file
19
tests/decisionService.test.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { mkdtempSync, rmSync } from 'fs';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { createDecision, listDecisions, getDecision } from '../src/core/services/decisionService.js';
|
||||||
|
|
||||||
|
describe('decisionService', () => {
|
||||||
|
let cwd: string;
|
||||||
|
|
||||||
|
beforeEach(() => { cwd = mkdtempSync(join(tmpdir(), 'ah-dec-')); });
|
||||||
|
afterEach(() => { rmSync(cwd, { recursive: true, force: true }); });
|
||||||
|
|
||||||
|
it('creates and reads a decision', () => {
|
||||||
|
const d = createDecision(cwd, { title: 'Use SQLite', context: 'c', decision: 'd' });
|
||||||
|
expect(d.id).toBe('DEC-0001');
|
||||||
|
expect(getDecision(cwd, d.id).decision.title).toBe('Use SQLite');
|
||||||
|
expect(listDecisions(cwd)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user