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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Daedalion Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Daedalion
|
|
2
|
+
|
|
3
|
+
[](https://nodejs.org/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.npmjs.com/package/daedalion)
|
|
6
|
+
|
|
7
|
+
Spec-to-Agent compiler for GitHub Copilot. Write specs; get agents, skills, and prompts — all kept in sync with your source of truth.
|
|
8
|
+
|
|
9
|
+
> *"Write specs, get agents automatically."*
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
Daedalion turns your `openspec/` specifications into native GitHub Copilot artifacts — agents, skills, prompts, and instructions. Your specs stay the source of truth; agents stay in sync automatically.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
OpenSpec (what) ───▶ Daedalion ───▶ GitHub Copilot (how)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Core Goal: Spec-Driven Development with AI
|
|
22
|
+
|
|
23
|
+
**Daedalion's primary mission:** Ensure humans and AIs agree on **what to build** *before* any code is written.
|
|
24
|
+
|
|
25
|
+
When you run `daedalion build`, it generates **`.github/prompts/daedalion-openspec-cycle.prompt.md`** — a guided workflow that:
|
|
26
|
+
|
|
27
|
+
1. Routes AI to draft **proposals** (why, goals, scope) + **spec deltas** (what needs to change)
|
|
28
|
+
2. Blocks coding until the **human explicitly approves specs**
|
|
29
|
+
3. Guides implementation via a clear **task checklist**
|
|
30
|
+
4. Archives completed changes and **merges specs into canonical truth**
|
|
31
|
+
|
|
32
|
+
This prompt is your **AI code coordinator** — it prevents costly rework by enforcing spec clarity and human review at every phase.
|
|
33
|
+
|
|
34
|
+
**The result:** Predictable, spec-aligned development where both humans and AIs follow the same playbook.
|
|
35
|
+
|
|
36
|
+
## Why Daedalion + OpenSpec
|
|
37
|
+
|
|
38
|
+
- Aligns humans and AIs on the “what” before any code.
|
|
39
|
+
- Enforces approval gates with an AI coordinator prompt.
|
|
40
|
+
- Generates native Copilot artifacts from `openspec/` automatically.
|
|
41
|
+
- Prevents drift with `validate` and supports CI.
|
|
42
|
+
|
|
43
|
+
## The Core Idea
|
|
44
|
+
|
|
45
|
+
`daedalion build` generates `.github/prompts/daedalion-openspec-cycle.prompt.md` — your AI code coordinator that:
|
|
46
|
+
- Routes work: proposal → review → implementation → archive
|
|
47
|
+
- Blocks coding until specs are explicitly approved
|
|
48
|
+
- Keeps tasks and deltas in lockstep with human review
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
flowchart LR
|
|
52
|
+
D[Draft: proposal + spec deltas] --> E{Approved?}
|
|
53
|
+
E -- no --> D
|
|
54
|
+
E -- yes --> F[Implement: tasks]
|
|
55
|
+
F --> G{All tasks complete?}
|
|
56
|
+
G -- no --> F
|
|
57
|
+
G -- yes --> H[Archive: merge into openspec/specs]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install -g openspec daedalion
|
|
64
|
+
openspec init # creates openspec/ + AGENTS.md
|
|
65
|
+
daedalion init # adds daedalion.yaml + example specs
|
|
66
|
+
daedalion build # generates Copilot artifacts (agents, skills, prompts)
|
|
67
|
+
daedalion validate # checks specs ↔ artifacts are in sync
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use the coordinator by asking your AI: “Help me work through the OpenSpec cycle.”
|
|
71
|
+
|
|
72
|
+
## What It Generates
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
.github/
|
|
76
|
+
├── prompts/
|
|
77
|
+
│ ├── daedalion-openspec-cycle.prompt.md # AI coordinator (spec-driven workflow)
|
|
78
|
+
│ └── {change}.prompt.md # Slash prompts for active changes
|
|
79
|
+
├── agents/{domain}.agent.md # Domain personas
|
|
80
|
+
├── skills/{domain}/SKILL.md # Auto-loaded skills from specs
|
|
81
|
+
├── copilot-instructions.md # Project context from openspec/project.md
|
|
82
|
+
└── workflows/daedalion.yml # Optional CI (validate / auto-regenerate)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Commands
|
|
86
|
+
|
|
87
|
+
- `daedalion init` - Scaffolds a new project with example specs:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
project/
|
|
91
|
+
├── daedalion.yaml
|
|
92
|
+
└── openspec/
|
|
93
|
+
├── project.md
|
|
94
|
+
├── specs/
|
|
95
|
+
│ └── example/
|
|
96
|
+
│ └── spec.md
|
|
97
|
+
└── changes/
|
|
98
|
+
└── example-feature/
|
|
99
|
+
├── proposal.md
|
|
100
|
+
└── tasks.md
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- `daedalion build` - Generates GitHub Copilot artifacts from your specs:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
.github/
|
|
107
|
+
├── skills/
|
|
108
|
+
│ └── {domain}/
|
|
109
|
+
│ └── SKILL.md
|
|
110
|
+
├── agents/
|
|
111
|
+
│ └── {domain}.agent.md
|
|
112
|
+
├── prompts/
|
|
113
|
+
│ └── {change-name}.prompt.md
|
|
114
|
+
├── workflows/
|
|
115
|
+
│ └── daedalion.yml
|
|
116
|
+
└── copilot-instructions.md
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `daedalion validate` — Verify specs and artifacts are in sync
|
|
120
|
+
- `daedalion clean` — Remove only Daedalion-generated files
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Learn More
|
|
124
|
+
|
|
125
|
+
- OpenSpec format and conventions: [docs/openspec-format.md](docs/openspec-format.md)
|
|
126
|
+
- Generated output details: [docs/generated-output.md](docs/generated-output.md)
|
|
127
|
+
- CI setup and options: [docs/ci-workflow.md](docs/ci-workflow.md)
|
|
128
|
+
- Developer notes: [docs/DEVELOPER.md](docs/DEVELOPER.md)
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
133
|
+
|
package/bin/daedalion.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { init } from '../src/commands/init.js';
|
|
6
|
+
import { build } from '../src/commands/build.js';
|
|
7
|
+
import { validate } from '../src/commands/validate.js';
|
|
8
|
+
import { clean } from '../src/commands/clean.js';
|
|
9
|
+
import { VERSION, getVersionString } from '../src/version.js';
|
|
10
|
+
|
|
11
|
+
function displayLogo() {
|
|
12
|
+
const logo = `
|
|
13
|
+
██████╗ █████╗ ███████╗██████╗ █████╗ ██╗ ██╗ ██████╗ ███╗ ██╗
|
|
14
|
+
██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔══██╗██║ ██║██╔═══██╗████╗ ██║
|
|
15
|
+
██║ ██║███████║█████╗ ██║ ██║███████║██║ ██║██║ ██║██╔██╗ ██║
|
|
16
|
+
██║ ██║██╔══██║██╔══╝ ██║ ██║██╔══██║██║ ██║██║ ██║██║╚██╗██║
|
|
17
|
+
██████╔╝██║ ██║███████╗██████╔╝██║ ██║███████╗██║╚██████╔╝██║ ╚████║
|
|
18
|
+
╚═════╝ ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
console.log(chalk.cyan(logo));
|
|
22
|
+
console.log(chalk.gray(`Author: Henry Bravo`));
|
|
23
|
+
console.log(chalk.gray(`Email: info@henrybravo.nl`));
|
|
24
|
+
console.log(chalk.gray(`Version: ${getVersionString()}`));
|
|
25
|
+
console.log();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Display logo unless help is requested
|
|
29
|
+
if (!process.argv.includes('--help') && !process.argv.includes('-h')) {
|
|
30
|
+
displayLogo();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.name('daedalion')
|
|
35
|
+
.description('OpenSpec-to-Agent compiler for GitHub Copilot')
|
|
36
|
+
.version(VERSION)
|
|
37
|
+
.helpOption('-h, --help', 'display help for command');
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command('init')
|
|
41
|
+
.description('Scaffold config + example spec')
|
|
42
|
+
.option('--target <mode>', 'Agent target mode: ide or sdk')
|
|
43
|
+
.action(async (options) => {
|
|
44
|
+
try {
|
|
45
|
+
await init(process.cwd(), options);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command('build')
|
|
54
|
+
.description('Generate .github/ from openspec/')
|
|
55
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
56
|
+
.option('--verbose', 'Detailed output for debugging')
|
|
57
|
+
.option('--force', 'Overwrite without confirmation')
|
|
58
|
+
.option('--with-tools', 'Generate tool stub files from specs')
|
|
59
|
+
.action(async (options) => {
|
|
60
|
+
try {
|
|
61
|
+
await build(process.cwd(), options);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command('validate')
|
|
70
|
+
.description('Check specs and generated output')
|
|
71
|
+
.action(async () => {
|
|
72
|
+
try {
|
|
73
|
+
const valid = await validate(process.cwd());
|
|
74
|
+
if (!valid) {
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
program
|
|
84
|
+
.command('clean')
|
|
85
|
+
.description('Remove generated files')
|
|
86
|
+
.action(async () => {
|
|
87
|
+
try {
|
|
88
|
+
await clean(process.cwd());
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "daedalion",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "OpenSpec-to-Agent compiler for GitHub Copilot",
|
|
5
|
+
"bin": {
|
|
6
|
+
"daedalion": "./bin/daedalion.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"src/",
|
|
11
|
+
"templates/"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"test:coverage": "vitest run --coverage"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"openspec",
|
|
24
|
+
"github-copilot",
|
|
25
|
+
"agents",
|
|
26
|
+
"skills",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"author": "Henry Bravo <info@henrybravo.nl>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "^5.3.0",
|
|
33
|
+
"commander": "^12.0.0",
|
|
34
|
+
"glob": "^10.3.0",
|
|
35
|
+
"gray-matter": "^4.0.3",
|
|
36
|
+
"yaml": "^2.3.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"vitest": "^1.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { VERSION } from '../version.js';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
import { loadConfig, resolveOpenspecPath, resolveOutputPath } from '../config.js';
|
|
7
|
+
import { parseSpec } from '../parsers/spec.js';
|
|
8
|
+
import { parseProposal } from '../parsers/proposal.js';
|
|
9
|
+
import { parseTasks } from '../parsers/tasks.js';
|
|
10
|
+
import { generateSkill } from '../generators/skill.js';
|
|
11
|
+
import { generateAgent } from '../generators/agent.js';
|
|
12
|
+
import { generatePrompt, generateCyclePrompt } from '../generators/prompt.js';
|
|
13
|
+
import { generateWorkflow } from '../generators/workflow.js';
|
|
14
|
+
import { generateInstructions } from '../generators/instructions.js';
|
|
15
|
+
import { generateTools } from '../generators/tools.js';
|
|
16
|
+
import { ensureDir } from '../utils.js';
|
|
17
|
+
|
|
18
|
+
const MANIFEST_FILENAME = '.daedalion-manifest.json';
|
|
19
|
+
|
|
20
|
+
export async function build(cwd, options = {}) {
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(chalk.bold(` Daedalion v${VERSION}`));
|
|
23
|
+
console.log();
|
|
24
|
+
|
|
25
|
+
const config = loadConfig(cwd);
|
|
26
|
+
const openspecDir = resolveOpenspecPath(cwd, config);
|
|
27
|
+
const outputDir = resolveOutputPath(cwd, config);
|
|
28
|
+
|
|
29
|
+
if (!existsSync(openspecDir)) {
|
|
30
|
+
throw new Error(`OpenSpec directory not found: ${openspecDir}\n Run 'daedalion init' first.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const generatedFiles = [];
|
|
34
|
+
|
|
35
|
+
// Parse specs
|
|
36
|
+
console.log(' Parsing specs...');
|
|
37
|
+
const specs = await findAndParseSpecs(openspecDir, options);
|
|
38
|
+
for (const spec of specs) {
|
|
39
|
+
console.log(chalk.gray(` ✓ ${relative(cwd, spec.path)}`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse changes
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(' Parsing changes...');
|
|
45
|
+
const changes = await findAndParseChanges(openspecDir, options);
|
|
46
|
+
for (const change of changes) {
|
|
47
|
+
console.log(chalk.gray(` ✓ ${relative(cwd, change.proposal.path)}`));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Aggregate tasks from all changes for skills
|
|
51
|
+
const allTasks = aggregateTasks(changes);
|
|
52
|
+
|
|
53
|
+
// Generate outputs
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(' Generating...');
|
|
56
|
+
|
|
57
|
+
// Generate skills and agents from specs
|
|
58
|
+
for (const spec of specs) {
|
|
59
|
+
const tasks = allTasks[spec.domain] || null;
|
|
60
|
+
const skillResult = generateSkill(spec, tasks, outputDir, options);
|
|
61
|
+
generatedFiles.push(skillResult);
|
|
62
|
+
logGenerated(skillResult.path, cwd, options);
|
|
63
|
+
|
|
64
|
+
const agentResult = generateAgent(spec, outputDir, options, config);
|
|
65
|
+
generatedFiles.push(agentResult);
|
|
66
|
+
logGenerated(agentResult.path, cwd, options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate tool stubs if --with-tools flag is set
|
|
70
|
+
if (options.withTools) {
|
|
71
|
+
console.log(chalk.gray(` (generating tool stubs)`));
|
|
72
|
+
for (const spec of specs) {
|
|
73
|
+
const toolResults = generateTools(spec, outputDir, config, options);
|
|
74
|
+
for (const toolResult of toolResults) {
|
|
75
|
+
generatedFiles.push(toolResult);
|
|
76
|
+
logGenerated(toolResult.path, cwd, options);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Generate prompts from changes
|
|
82
|
+
for (const change of changes) {
|
|
83
|
+
const domain = findDomainForChange(change, specs);
|
|
84
|
+
const promptResult = generatePrompt(change.proposal, change.tasks, domain, outputDir, options);
|
|
85
|
+
generatedFiles.push(promptResult);
|
|
86
|
+
logGenerated(promptResult.path, cwd, options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Generate OpenSpec cycle prompt
|
|
90
|
+
const cyclePromptResult = generateCyclePrompt(outputDir, options);
|
|
91
|
+
generatedFiles.push(cyclePromptResult);
|
|
92
|
+
logGenerated(cyclePromptResult.path, cwd, options);
|
|
93
|
+
|
|
94
|
+
// Generate workflow
|
|
95
|
+
const workflowResult = generateWorkflow(config, outputDir, options);
|
|
96
|
+
generatedFiles.push(workflowResult);
|
|
97
|
+
logGenerated(workflowResult.path, cwd, options);
|
|
98
|
+
|
|
99
|
+
// Generate copilot-instructions.md
|
|
100
|
+
const instructionsResult = generateInstructions(openspecDir, outputDir, options);
|
|
101
|
+
generatedFiles.push(instructionsResult);
|
|
102
|
+
logGenerated(instructionsResult.path, cwd, options);
|
|
103
|
+
|
|
104
|
+
// Write manifest of generated files (for clean command)
|
|
105
|
+
if (!options.dryRun) {
|
|
106
|
+
writeManifest(outputDir, generatedFiles, cwd);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log();
|
|
110
|
+
if (options.dryRun) {
|
|
111
|
+
console.log(chalk.yellow(` Dry run complete. ${generatedFiles.length} files would be generated.`));
|
|
112
|
+
} else {
|
|
113
|
+
console.log(chalk.green(` Done. ${generatedFiles.length} files generated.`));
|
|
114
|
+
}
|
|
115
|
+
console.log();
|
|
116
|
+
|
|
117
|
+
return generatedFiles;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeManifest(outputDir, generatedFiles, cwd) {
|
|
121
|
+
const manifestPath = join(outputDir, MANIFEST_FILENAME);
|
|
122
|
+
const manifest = {
|
|
123
|
+
version: 1,
|
|
124
|
+
generatedAt: new Date().toISOString(),
|
|
125
|
+
files: generatedFiles.map(f => relative(cwd, f.path))
|
|
126
|
+
};
|
|
127
|
+
ensureDir(manifestPath);
|
|
128
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function findAndParseSpecs(openspecDir, options) {
|
|
132
|
+
const specsDir = join(openspecDir, 'specs');
|
|
133
|
+
if (!existsSync(specsDir)) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const specFiles = await glob('*/spec.md', { cwd: specsDir });
|
|
138
|
+
return specFiles.map(file => parseSpec(join(specsDir, file)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function findAndParseChanges(openspecDir, options) {
|
|
142
|
+
const changesDir = join(openspecDir, 'changes');
|
|
143
|
+
if (!existsSync(changesDir)) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const changes = [];
|
|
148
|
+
const changeDirs = readdirSync(changesDir).filter(name => {
|
|
149
|
+
const fullPath = join(changesDir, name);
|
|
150
|
+
return statSync(fullPath).isDirectory();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
for (const changeDir of changeDirs) {
|
|
154
|
+
const proposalPath = join(changesDir, changeDir, 'proposal.md');
|
|
155
|
+
const tasksPath = join(changesDir, changeDir, 'tasks.md');
|
|
156
|
+
|
|
157
|
+
if (existsSync(proposalPath)) {
|
|
158
|
+
changes.push({
|
|
159
|
+
proposal: parseProposal(proposalPath),
|
|
160
|
+
tasks: parseTasks(tasksPath)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return changes;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function aggregateTasks(changes) {
|
|
169
|
+
const tasksByDomain = {};
|
|
170
|
+
for (const change of changes) {
|
|
171
|
+
// For now, associate all tasks with a generic key
|
|
172
|
+
// In a real implementation, you might parse domain from proposal
|
|
173
|
+
if (!tasksByDomain['default']) {
|
|
174
|
+
tasksByDomain['default'] = { groups: [], items: [], hasMore: false };
|
|
175
|
+
}
|
|
176
|
+
tasksByDomain['default'].items.push(...change.tasks.items);
|
|
177
|
+
tasksByDomain['default'].hasMore = tasksByDomain['default'].hasMore || change.tasks.hasMore;
|
|
178
|
+
}
|
|
179
|
+
return tasksByDomain;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function findDomainForChange(change, specs) {
|
|
183
|
+
// Try to match change to a spec domain
|
|
184
|
+
// For now, use the first spec's domain or 'default'
|
|
185
|
+
if (specs.length > 0) {
|
|
186
|
+
return specs[0].domain;
|
|
187
|
+
}
|
|
188
|
+
return 'default';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function logGenerated(filePath, cwd, options) {
|
|
192
|
+
const relativePath = relative(cwd, filePath);
|
|
193
|
+
if (options.dryRun) {
|
|
194
|
+
console.log(chalk.yellow(` → ${relativePath}`));
|
|
195
|
+
} else {
|
|
196
|
+
console.log(chalk.green(` ✓ ${relativePath}`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, rmSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { VERSION } from '../version.js';
|
|
5
|
+
import { loadConfig, resolveOutputPath } from '../config.js';
|
|
6
|
+
|
|
7
|
+
const MANIFEST_FILENAME = '.daedalion-manifest.json';
|
|
8
|
+
|
|
9
|
+
export async function clean(cwd) {
|
|
10
|
+
console.log();
|
|
11
|
+
console.log(chalk.bold(` Daedalion v${VERSION} - Clean`));
|
|
12
|
+
console.log();
|
|
13
|
+
|
|
14
|
+
const config = loadConfig(cwd);
|
|
15
|
+
const outputDir = resolveOutputPath(cwd, config);
|
|
16
|
+
|
|
17
|
+
if (!existsSync(outputDir)) {
|
|
18
|
+
console.log(chalk.yellow(' No output directory found. Nothing to clean.'));
|
|
19
|
+
console.log();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const manifestPath = join(outputDir, MANIFEST_FILENAME);
|
|
24
|
+
|
|
25
|
+
if (!existsSync(manifestPath)) {
|
|
26
|
+
console.log(chalk.yellow(' No manifest found. Run `daedalion build` first to generate a manifest.'));
|
|
27
|
+
console.log(chalk.gray(' (Clean requires a manifest to avoid deleting non-Daedalion files)'));
|
|
28
|
+
console.log();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
33
|
+
const filesToRemove = manifest.files || [];
|
|
34
|
+
|
|
35
|
+
let removed = 0;
|
|
36
|
+
const cleanedDirs = new Set();
|
|
37
|
+
|
|
38
|
+
for (const relativePath of filesToRemove) {
|
|
39
|
+
const fullPath = join(cwd, relativePath);
|
|
40
|
+
|
|
41
|
+
if (existsSync(fullPath)) {
|
|
42
|
+
rmSync(fullPath, { force: true });
|
|
43
|
+
console.log(chalk.green(` ✓ Removed ${relativePath}`));
|
|
44
|
+
removed++;
|
|
45
|
+
|
|
46
|
+
// Track parent directories for cleanup
|
|
47
|
+
cleanedDirs.add(dirname(fullPath));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Remove the manifest itself
|
|
52
|
+
rmSync(manifestPath, { force: true });
|
|
53
|
+
console.log(chalk.green(` ✓ Removed .github/${MANIFEST_FILENAME}`));
|
|
54
|
+
|
|
55
|
+
// Clean up empty directories (skills/domain/, agents/, etc.)
|
|
56
|
+
for (const dir of cleanedDirs) {
|
|
57
|
+
cleanEmptyDirs(dir, outputDir);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log();
|
|
61
|
+
if (removed > 0) {
|
|
62
|
+
console.log(chalk.green(` Done. ${removed} files removed.`));
|
|
63
|
+
} else {
|
|
64
|
+
console.log(chalk.yellow(' No generated files found to clean.'));
|
|
65
|
+
}
|
|
66
|
+
console.log();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Recursively remove empty directories up to (but not including) the output dir
|
|
70
|
+
function cleanEmptyDirs(dir, stopAt) {
|
|
71
|
+
if (dir === stopAt || !existsSync(dir)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const contents = readdirSync(dir);
|
|
77
|
+
if (contents.length === 0) {
|
|
78
|
+
rmSync(dir, { recursive: true, force: true });
|
|
79
|
+
// Check parent directory too
|
|
80
|
+
cleanEmptyDirs(dirname(dir), stopAt);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Directory might have been removed already
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join, dirname, relative } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { VERSION } from '../version.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
export async function init(cwd, options = {}) {
|
|
11
|
+
console.log();
|
|
12
|
+
console.log(chalk.bold(` Daedalion v${VERSION}`));
|
|
13
|
+
console.log();
|
|
14
|
+
|
|
15
|
+
const templatesDir = join(__dirname, '../../templates/init');
|
|
16
|
+
const files = getAllFiles(templatesDir);
|
|
17
|
+
const { agentTarget } = options;
|
|
18
|
+
|
|
19
|
+
let created = 0;
|
|
20
|
+
let skipped = 0;
|
|
21
|
+
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const relativePath = relative(templatesDir, file);
|
|
24
|
+
const targetPath = join(cwd, relativePath);
|
|
25
|
+
|
|
26
|
+
if (existsSync(targetPath)) {
|
|
27
|
+
console.log(chalk.yellow(` ⚠ ${relativePath} already exists, skipping`));
|
|
28
|
+
skipped++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const dir = dirname(targetPath);
|
|
33
|
+
if (!existsSync(dir)) {
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
copyFileSync(file, targetPath);
|
|
38
|
+
console.log(chalk.green(` ✓ ${relativePath}`));
|
|
39
|
+
created++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (agentTarget === 'sdk') {
|
|
43
|
+
const configPath = join(cwd, 'daedalion.yaml');
|
|
44
|
+
if (existsSync(configPath)) {
|
|
45
|
+
let config = readFileSync(configPath, 'utf-8');
|
|
46
|
+
config = config.replace(/agents:\s*\n\s*target:\s*ide/, 'agents:\n target: sdk');
|
|
47
|
+
writeFileSync(configPath, config);
|
|
48
|
+
console.log(chalk.cyan(` ✓ Updated daedalion.yaml with SDK target`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log();
|
|
53
|
+
if (created > 0) {
|
|
54
|
+
console.log(chalk.green(` Done. ${created} files created.`));
|
|
55
|
+
}
|
|
56
|
+
if (skipped > 0) {
|
|
57
|
+
console.log(chalk.yellow(` ${skipped} files skipped (already exist).`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (agentTarget === 'sdk') {
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(chalk.cyan(` Agent target set to SDK (for CI pipelines)`));
|
|
63
|
+
console.log(chalk.gray(` Edit daedalion.yaml to add custom tool names under agents.tools`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(' Next steps:');
|
|
68
|
+
console.log(' 1. Edit openspec/specs/example/spec.md with your specifications');
|
|
69
|
+
console.log(' 2. Run `daedalion build` to generate GitHub Copilot artifacts');
|
|
70
|
+
console.log();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getAllFiles(dir, files = []) {
|
|
74
|
+
const entries = readdirSync(dir);
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const fullPath = join(dir, entry);
|
|
78
|
+
const stat = statSync(fullPath);
|
|
79
|
+
|
|
80
|
+
if (stat.isDirectory()) {
|
|
81
|
+
getAllFiles(fullPath, files);
|
|
82
|
+
} else {
|
|
83
|
+
files.push(fullPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return files;
|
|
88
|
+
}
|