daedalion 0.0.1
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 +133 -0
- package/bin/daedalion.js +95 -0
- package/package.json +41 -0
- package/src/commands/build.js +198 -0
- package/src/commands/clean.js +85 -0
- package/src/commands/init.js +88 -0
- package/src/commands/validate.js +141 -0
- package/src/config.js +50 -0
- package/src/generators/agent.js +121 -0
- package/src/generators/instructions.js +65 -0
- package/src/generators/prompt.js +113 -0
- package/src/generators/skill.js +105 -0
- package/src/generators/tools.js +183 -0
- package/src/generators/workflow.js +65 -0
- package/src/index.js +14 -0
- package/src/parsers/proposal.js +52 -0
- package/src/parsers/spec.js +105 -0
- package/src/parsers/tasks.js +46 -0
- package/src/utils.js +51 -0
- package/src/version.js +60 -0
- package/templates/init/daedalion.yaml +18 -0
- package/templates/init/openspec/changes/example-feature/proposal.md +13 -0
- package/templates/init/openspec/changes/example-feature/tasks.md +19 -0
- package/templates/init/openspec/project.md +15 -0
- package/templates/init/openspec/specs/example/spec.md +37 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { ensureDir } from '../utils.js';
|
|
4
|
+
|
|
5
|
+
export function generateTools(spec, outputDir, config = {}, options = {}) {
|
|
6
|
+
const tools = extractTools(spec);
|
|
7
|
+
const language = config.tools?.language || 'python';
|
|
8
|
+
const generated = [];
|
|
9
|
+
|
|
10
|
+
for (const tool of tools) {
|
|
11
|
+
const stubResult = generateToolStub(tool, spec, outputDir, language, options);
|
|
12
|
+
generated.push(stubResult);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return generated;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractTools(spec) {
|
|
19
|
+
const tools = [];
|
|
20
|
+
const frontmatter = spec.frontmatter || {};
|
|
21
|
+
|
|
22
|
+
if (frontmatter.tools && Array.isArray(frontmatter.tools)) {
|
|
23
|
+
for (const tool of frontmatter.tools) {
|
|
24
|
+
if (typeof tool === 'string') {
|
|
25
|
+
tools.push({ name: tool });
|
|
26
|
+
} else if (tool.name) {
|
|
27
|
+
tools.push(tool);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return tools;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mapPythonType(type) {
|
|
36
|
+
const typeMap = {
|
|
37
|
+
'string': 'str',
|
|
38
|
+
'String': 'str',
|
|
39
|
+
'number': 'int | float',
|
|
40
|
+
'Number': 'int | float',
|
|
41
|
+
'object': 'dict',
|
|
42
|
+
'Object': 'dict',
|
|
43
|
+
'array': 'Array',
|
|
44
|
+
'Array': 'Array',
|
|
45
|
+
'boolean': 'bool',
|
|
46
|
+
'Boolean': 'bool'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (typeof type !== 'string') {
|
|
50
|
+
return 'Any';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return typeMap[type] || type || 'Any';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getAllRequirementIds(spec) {
|
|
57
|
+
return spec.requirements.map((req, i) => ({
|
|
58
|
+
id: `REQ-${String(i + 1).padStart(3, '0')}`,
|
|
59
|
+
name: req.name
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function filterRelevantRequirements(spec, tool) {
|
|
64
|
+
if (!tool.requirements || !Array.isArray(tool.requirements)) {
|
|
65
|
+
return getAllRequirementIds(spec).slice(0, 2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const allReqs = getAllRequirementIds(spec);
|
|
69
|
+
const toolReqIds = tool.requirements.map(r => {
|
|
70
|
+
if (typeof r === 'string' && r.startsWith('REQ-')) {
|
|
71
|
+
return r;
|
|
72
|
+
}
|
|
73
|
+
const idx = parseInt(r) || 1;
|
|
74
|
+
return `REQ-${String(idx).padStart(3, '0')}`;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return allReqs.filter(req => toolReqIds.includes(req.id));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function generatePythonStub(tool, spec, filePath, options) {
|
|
81
|
+
const toolReqs = filterRelevantRequirements(spec, tool);
|
|
82
|
+
const toolReqIds = toolReqs.map(r => r.id);
|
|
83
|
+
|
|
84
|
+
let content = `"""
|
|
85
|
+
${tool.description || tool.name}
|
|
86
|
+
Requirements: ${toolReqIds.join(', ') || 'TODO'}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
content += `def ${tool.name}(${generatePythonSignature(tool)}) -> ${mapPythonType(tool.outputs?.[0] || 'dict')}:\n`;
|
|
92
|
+
content += ` """
|
|
93
|
+
${tool.description || 'TODO: Add description'}
|
|
94
|
+
\n`;
|
|
95
|
+
|
|
96
|
+
if (tool.inputs && tool.inputs.length > 0) {
|
|
97
|
+
content += ` Args:\n`;
|
|
98
|
+
for (const input of tool.inputs) {
|
|
99
|
+
content += ` ${input.name}: ${mapPythonType(input.type || 'Any')} - ${input.description || 'TODO'}\n`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (tool.outputs && tool.outputs.length > 0) {
|
|
104
|
+
content += ` \n Returns:\n`;
|
|
105
|
+
content += ` ${mapPythonType(tool.outputs[0] || 'dict')}\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (toolReqIds.length > 0) {
|
|
109
|
+
content += ` \n Spec References:\n`;
|
|
110
|
+
for (const ref of toolReqIds) {
|
|
111
|
+
content += ` - ${ref}\n`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
content += ` """\n`;
|
|
116
|
+
content += ` raise NotImplementedError("Implement ${tool.name} logic")\n`;
|
|
117
|
+
|
|
118
|
+
if (options.dryRun) {
|
|
119
|
+
return { path: filePath, content };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ensureDir(filePath);
|
|
123
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
124
|
+
return { path: filePath, content };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function generateToolStub(tool, spec, outputDir, language, options) {
|
|
128
|
+
const toolsDir = join(outputDir, 'tools');
|
|
129
|
+
const ext = getLanguageExt(language);
|
|
130
|
+
const toolPath = join(toolsDir, `${tool.name.toLowerCase()}${ext}`);
|
|
131
|
+
|
|
132
|
+
if (language === 'python') {
|
|
133
|
+
return generatePythonStub(tool, spec, toolPath, options);
|
|
134
|
+
} else if (language === 'javascript') {
|
|
135
|
+
return generateJavaScriptStub(tool, spec, toolPath, options);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function generatePythonSignature(tool) {
|
|
140
|
+
if (!tool.inputs || tool.inputs.length === 0) {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return tool.inputs.map(input => `${input.name}: ${mapPythonType(input.type)}`).join(', ');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function generateJSSignature(tool) {
|
|
148
|
+
if (!tool.inputs || tool.inputs.length === 0) {
|
|
149
|
+
return '{}';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return `{ ${tool.inputs.map(input => `${input.name}`).join(', ')} }`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getLanguageExt(language) {
|
|
156
|
+
const exts = { python: '.py', javascript: '.js' };
|
|
157
|
+
return exts[language] || '.py';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function mapJSType(type) {
|
|
161
|
+
const typeMap = {
|
|
162
|
+
'string': 'string',
|
|
163
|
+
'String': 'string',
|
|
164
|
+
'number': 'number',
|
|
165
|
+
'Number': 'number',
|
|
166
|
+
'object': 'Object',
|
|
167
|
+
'Object': 'Object',
|
|
168
|
+
'array': 'Array',
|
|
169
|
+
'Array': 'Array',
|
|
170
|
+
'boolean': 'boolean',
|
|
171
|
+
'Boolean': 'boolean'
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (typeof type !== 'string') {
|
|
175
|
+
return 'any';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return typeMap[type] || type || 'any';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function camelCase(str) {
|
|
182
|
+
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase());
|
|
183
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ensureDir } from '../utils.js';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export function generateWorkflow(config, outputDir, options = {}) {
|
|
6
|
+
const workflowPath = join(outputDir, 'workflows', 'daedalion.yml');
|
|
7
|
+
|
|
8
|
+
const content = config.ci?.auto_commit
|
|
9
|
+
? generateAutoCommitWorkflow(config)
|
|
10
|
+
: generateValidateOnlyWorkflow(config);
|
|
11
|
+
|
|
12
|
+
if (options.dryRun) {
|
|
13
|
+
return { path: workflowPath, content };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
ensureDir(workflowPath);
|
|
17
|
+
writeFileSync(workflowPath, content);
|
|
18
|
+
return { path: workflowPath, content };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function generateValidateOnlyWorkflow(config) {
|
|
22
|
+
return `name: Daedalion
|
|
23
|
+
on:
|
|
24
|
+
push:
|
|
25
|
+
paths: ['openspec/**']
|
|
26
|
+
pull_request:
|
|
27
|
+
paths: ['openspec/**']
|
|
28
|
+
jobs:
|
|
29
|
+
validate:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: actions/setup-node@v4
|
|
34
|
+
with:
|
|
35
|
+
node-version: '20'
|
|
36
|
+
- run: npm install -g daedalion
|
|
37
|
+
- run: daedalion build --dry-run
|
|
38
|
+
- run: daedalion validate
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function generateAutoCommitWorkflow(config) {
|
|
43
|
+
const commitMessage = config.ci?.commit_message || 'chore: regenerate agents from specs';
|
|
44
|
+
|
|
45
|
+
return `name: Daedalion
|
|
46
|
+
on:
|
|
47
|
+
push:
|
|
48
|
+
branches: [main]
|
|
49
|
+
paths: ['openspec/**']
|
|
50
|
+
jobs:
|
|
51
|
+
build:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: '20'
|
|
58
|
+
- run: npm install -g daedalion
|
|
59
|
+
- run: daedalion build
|
|
60
|
+
- run: daedalion validate
|
|
61
|
+
- uses: stefanzweifel/git-auto-commit-action@v5
|
|
62
|
+
with:
|
|
63
|
+
commit_message: '${commitMessage}'
|
|
64
|
+
`;
|
|
65
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { init } from './commands/init.js';
|
|
2
|
+
export { build } from './commands/build.js';
|
|
3
|
+
export { validate } from './commands/validate.js';
|
|
4
|
+
export { clean } from './commands/clean.js';
|
|
5
|
+
export { loadConfig } from './config.js';
|
|
6
|
+
export { parseSpec } from './parsers/spec.js';
|
|
7
|
+
export { parseProposal } from './parsers/proposal.js';
|
|
8
|
+
export { parseTasks } from './parsers/tasks.js';
|
|
9
|
+
export { generateSkill } from './generators/skill.js';
|
|
10
|
+
export { generateAgent } from './generators/agent.js';
|
|
11
|
+
export { generatePrompt } from './generators/prompt.js';
|
|
12
|
+
export { generateWorkflow } from './generators/workflow.js';
|
|
13
|
+
export { generateInstructions } from './generators/instructions.js';
|
|
14
|
+
export { generateTools } from './generators/tools.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { basename, dirname } from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
|
|
5
|
+
export function parseProposal(proposalPath) {
|
|
6
|
+
const content = readFileSync(proposalPath, 'utf-8');
|
|
7
|
+
const { data: frontmatter, content: body } = matter(content);
|
|
8
|
+
|
|
9
|
+
const changeName = basename(dirname(proposalPath));
|
|
10
|
+
const title = extractTitle(body);
|
|
11
|
+
const why = extractSection(body, 'Why');
|
|
12
|
+
const what = extractSection(body, 'What');
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
path: proposalPath,
|
|
16
|
+
changeName,
|
|
17
|
+
title,
|
|
18
|
+
frontmatter,
|
|
19
|
+
why,
|
|
20
|
+
what
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractTitle(content) {
|
|
25
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
26
|
+
return match ? match[1].trim() : 'Untitled Proposal';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractSection(content, sectionName) {
|
|
30
|
+
const lines = content.split('\n');
|
|
31
|
+
let inSection = false;
|
|
32
|
+
let sectionContent = [];
|
|
33
|
+
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
// Match ## Why or ## What (case insensitive)
|
|
36
|
+
const sectionMatch = line.match(/^##\s+(.+)$/);
|
|
37
|
+
if (sectionMatch) {
|
|
38
|
+
if (sectionMatch[1].toLowerCase() === sectionName.toLowerCase()) {
|
|
39
|
+
inSection = true;
|
|
40
|
+
continue;
|
|
41
|
+
} else if (inSection) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (inSection && line.trim()) {
|
|
47
|
+
sectionContent.push(line.trim());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return sectionContent.join('\n') || null;
|
|
52
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { basename, dirname } from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
|
|
5
|
+
export function parseSpec(specPath) {
|
|
6
|
+
const content = readFileSync(specPath, 'utf-8');
|
|
7
|
+
const { data: frontmatter, content: body } = matter(content);
|
|
8
|
+
|
|
9
|
+
const domain = basename(dirname(specPath));
|
|
10
|
+
const title = extractTitle(body);
|
|
11
|
+
const requirements = extractRequirements(body);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
path: specPath,
|
|
15
|
+
domain,
|
|
16
|
+
title,
|
|
17
|
+
frontmatter,
|
|
18
|
+
requirements
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractTitle(content) {
|
|
23
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
24
|
+
return match ? match[1].trim() : 'Untitled Specification';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractRequirements(content) {
|
|
28
|
+
const requirements = [];
|
|
29
|
+
const lines = content.split('\n');
|
|
30
|
+
|
|
31
|
+
let currentRequirement = null;
|
|
32
|
+
let currentScenario = null;
|
|
33
|
+
let inRequirementDescription = false;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < lines.length; i++) {
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
|
|
38
|
+
// Match ### Requirement: Name
|
|
39
|
+
const reqMatch = line.match(/^###\s+Requirement:\s*(.+)$/i);
|
|
40
|
+
if (reqMatch) {
|
|
41
|
+
if (currentRequirement) {
|
|
42
|
+
if (currentScenario) {
|
|
43
|
+
currentRequirement.scenarios.push(currentScenario);
|
|
44
|
+
}
|
|
45
|
+
requirements.push(currentRequirement);
|
|
46
|
+
}
|
|
47
|
+
currentRequirement = {
|
|
48
|
+
name: reqMatch[1].trim(),
|
|
49
|
+
description: '',
|
|
50
|
+
scenarios: []
|
|
51
|
+
};
|
|
52
|
+
currentScenario = null;
|
|
53
|
+
inRequirementDescription = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Match #### Scenario: Name
|
|
58
|
+
const scenarioMatch = line.match(/^####\s+Scenario:\s*(.+)$/i);
|
|
59
|
+
if (scenarioMatch && currentRequirement) {
|
|
60
|
+
if (currentScenario) {
|
|
61
|
+
currentRequirement.scenarios.push(currentScenario);
|
|
62
|
+
}
|
|
63
|
+
currentScenario = {
|
|
64
|
+
name: scenarioMatch[1].trim(),
|
|
65
|
+
steps: []
|
|
66
|
+
};
|
|
67
|
+
inRequirementDescription = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Stop description collection at any heading
|
|
72
|
+
if (line.match(/^#{1,4}\s+/)) {
|
|
73
|
+
inRequirementDescription = false;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Collect requirement description
|
|
78
|
+
if (inRequirementDescription && currentRequirement && line.trim()) {
|
|
79
|
+
if (currentRequirement.description) {
|
|
80
|
+
currentRequirement.description += ' ' + line.trim();
|
|
81
|
+
} else {
|
|
82
|
+
currentRequirement.description = line.trim();
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Collect scenario steps (lines starting with -)
|
|
88
|
+
if (currentScenario && line.match(/^\s*-\s+/)) {
|
|
89
|
+
const step = line.replace(/^\s*-\s+/, '').trim();
|
|
90
|
+
if (step) {
|
|
91
|
+
currentScenario.steps.push(step);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Push last requirement/scenario
|
|
97
|
+
if (currentRequirement) {
|
|
98
|
+
if (currentScenario) {
|
|
99
|
+
currentRequirement.scenarios.push(currentScenario);
|
|
100
|
+
}
|
|
101
|
+
requirements.push(currentRequirement);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return requirements;
|
|
105
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
export function parseTasks(tasksPath, maxItems = 10) {
|
|
4
|
+
if (!existsSync(tasksPath)) {
|
|
5
|
+
return { groups: [], items: [], hasMore: false };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const content = readFileSync(tasksPath, 'utf-8');
|
|
9
|
+
return summarizeTasks(content, maxItems);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function summarizeTasks(content, maxItems = 10) {
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
const groups = [];
|
|
15
|
+
const items = [];
|
|
16
|
+
let currentGroup = null;
|
|
17
|
+
let totalItems = 0;
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
// Match headings as groups
|
|
21
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
22
|
+
if (headingMatch) {
|
|
23
|
+
currentGroup = headingMatch[2].trim();
|
|
24
|
+
if (!groups.includes(currentGroup)) {
|
|
25
|
+
groups.push(currentGroup);
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Match top-level task items (- [ ] or - [x] or just -)
|
|
31
|
+
const taskMatch = line.match(/^-\s+(\[[ x]\])?\s*(.+)$/);
|
|
32
|
+
if (taskMatch && !line.match(/^\s{2,}-/)) {
|
|
33
|
+
totalItems++;
|
|
34
|
+
if (items.length < maxItems) {
|
|
35
|
+
const taskText = taskMatch[2].trim();
|
|
36
|
+
items.push(taskText);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
groups,
|
|
43
|
+
items,
|
|
44
|
+
hasMore: totalItems > maxItems
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
export function ensureDir(filePath) {
|
|
5
|
+
const dir = dirname(filePath);
|
|
6
|
+
if (!existsSync(dir)) {
|
|
7
|
+
mkdirSync(dir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function extractFirstHeading(content) {
|
|
12
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
13
|
+
return match ? match[1].trim() : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function extractDescription(content) {
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
let inFrontmatter = false;
|
|
19
|
+
let description = [];
|
|
20
|
+
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (line.trim() === '---') {
|
|
23
|
+
if (inFrontmatter) {
|
|
24
|
+
inFrontmatter = false;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
inFrontmatter = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (inFrontmatter) continue;
|
|
32
|
+
|
|
33
|
+
if (line.startsWith('#')) continue;
|
|
34
|
+
|
|
35
|
+
if (line.trim() && !line.startsWith('##')) {
|
|
36
|
+
description.push(line.trim());
|
|
37
|
+
if (description.length >= 2) break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (line.startsWith('##')) break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return description.join(' ').slice(0, 200) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function slugify(text) {
|
|
47
|
+
return text
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
50
|
+
.replace(/^-|-$/g, '');
|
|
51
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
// Read version from package.json
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
11
|
+
export const VERSION = pkg.version;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get version string with commit hash for dev builds
|
|
15
|
+
* @returns {string} e.g., "0.0.1" or "0.0.1-dev+abc1234"
|
|
16
|
+
*/
|
|
17
|
+
export function getVersionString() {
|
|
18
|
+
const commitHash = getCommitHash();
|
|
19
|
+
if (commitHash && !isReleaseBuild()) {
|
|
20
|
+
return `${VERSION}-dev+${commitHash}`;
|
|
21
|
+
}
|
|
22
|
+
return VERSION;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get short commit hash
|
|
27
|
+
* @returns {string|null}
|
|
28
|
+
*/
|
|
29
|
+
function getCommitHash() {
|
|
30
|
+
try {
|
|
31
|
+
return execSync('git rev-parse --short HEAD', {
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
34
|
+
}).trim();
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if this is a release build (tagged or npm published)
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function isReleaseBuild() {
|
|
45
|
+
// Check if running from node_modules (npm installed)
|
|
46
|
+
if (__dirname.includes('node_modules')) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if current commit is tagged
|
|
51
|
+
try {
|
|
52
|
+
execSync('git describe --exact-match --tags HEAD', {
|
|
53
|
+
encoding: 'utf-8',
|
|
54
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
target: github
|
|
3
|
+
openspec: ./openspec
|
|
4
|
+
output: ./.github
|
|
5
|
+
|
|
6
|
+
# Optional
|
|
7
|
+
# project_name: my-project
|
|
8
|
+
# constitution: ./CONSTITUTION.md
|
|
9
|
+
|
|
10
|
+
# CI behavior
|
|
11
|
+
ci:
|
|
12
|
+
auto_commit: false
|
|
13
|
+
commit_message: 'chore: regenerate agents from specs'
|
|
14
|
+
|
|
15
|
+
# Agent output configuration
|
|
16
|
+
agents:
|
|
17
|
+
target: ide # 'ide' or 'sdk'
|
|
18
|
+
tools: null # null to use IDE defaults, or array of custom tool names
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Add Personalized Dashboard
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
Users need a central place to view their activity and preferences. Currently, they must navigate through multiple pages to find relevant information.
|
|
6
|
+
|
|
7
|
+
## What
|
|
8
|
+
|
|
9
|
+
Create a personalized dashboard that displays:
|
|
10
|
+
- Recent activity summary
|
|
11
|
+
- Quick actions
|
|
12
|
+
- User preferences
|
|
13
|
+
- Notifications
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Tasks
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
- [ ] Create dashboard component structure
|
|
6
|
+
- [ ] Set up routing for /dashboard
|
|
7
|
+
|
|
8
|
+
## Implementation
|
|
9
|
+
|
|
10
|
+
- [ ] Add activity summary widget
|
|
11
|
+
- [ ] Add quick actions panel
|
|
12
|
+
- [ ] Add preferences section
|
|
13
|
+
- [ ] Add notifications component
|
|
14
|
+
|
|
15
|
+
## Testing
|
|
16
|
+
|
|
17
|
+
- [ ] Write unit tests for dashboard components
|
|
18
|
+
- [ ] Write integration tests for data fetching
|
|
19
|
+
- [ ] Manual QA testing
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Project Overview
|
|
2
|
+
|
|
3
|
+
This project uses OpenSpec for specification-driven development.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- Define clear specifications before implementation
|
|
8
|
+
- Use agents and skills to guide development
|
|
9
|
+
- Maintain alignment between specs and code
|
|
10
|
+
|
|
11
|
+
## Conventions
|
|
12
|
+
|
|
13
|
+
- All specifications follow OpenSpec format
|
|
14
|
+
- Changes go through proposal → review → implementation workflow
|
|
15
|
+
- Generated artifacts are kept in sync via Daedalion
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Example Specification
|
|
2
|
+
|
|
3
|
+
This is an example specification demonstrating the OpenSpec format.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
### Requirement: User Greeting
|
|
8
|
+
|
|
9
|
+
The system SHALL greet users by name when they log in.
|
|
10
|
+
|
|
11
|
+
#### Scenario: Known user logs in
|
|
12
|
+
|
|
13
|
+
- GIVEN a user with name "Alice" exists
|
|
14
|
+
- WHEN Alice logs in
|
|
15
|
+
- THEN the system displays "Welcome, Alice!"
|
|
16
|
+
|
|
17
|
+
#### Scenario: Anonymous user
|
|
18
|
+
|
|
19
|
+
- GIVEN a user is not logged in
|
|
20
|
+
- WHEN they access the homepage
|
|
21
|
+
- THEN the system displays "Welcome, Guest!"
|
|
22
|
+
|
|
23
|
+
### Requirement: Session Management
|
|
24
|
+
|
|
25
|
+
The system SHALL maintain user sessions for 24 hours.
|
|
26
|
+
|
|
27
|
+
#### Scenario: Active session
|
|
28
|
+
|
|
29
|
+
- GIVEN a user has an active session
|
|
30
|
+
- WHEN they make a request within 24 hours
|
|
31
|
+
- THEN the session remains valid
|
|
32
|
+
|
|
33
|
+
#### Scenario: Expired session
|
|
34
|
+
|
|
35
|
+
- GIVEN a user's session is older than 24 hours
|
|
36
|
+
- WHEN they make a request
|
|
37
|
+
- THEN they are redirected to login
|