speci 0.1.0 → 0.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/README.md +46 -43
- package/bin/speci.ts +40 -12
- package/lib/commands/init.ts +138 -129
- package/lib/commands/plan.ts +91 -12
- package/lib/commands/refactor.ts +13 -9
- package/lib/commands/run.ts +1 -1
- package/lib/commands/task.ts +15 -10
- package/lib/config.ts +40 -40
- package/lib/copilot.ts +29 -36
- package/lib/state.ts +26 -7
- package/lib/ui/banner-animation.ts +1019 -0
- package/lib/ui/banner.ts +62 -11
- package/lib/utils/logger.ts +8 -0
- package/package.json +1 -1
- package/templates/agents/{speci-impl.md → speci-impl.agent.md} +1 -1
- package/templates/agents/speci-plan.agent.md +541 -0
- package/templates/agents/{speci-refactor.md → speci-refactor.agent.md} +6 -1
- package/templates/agents/{speci-task.md → speci-task.agent.md} +23 -28
- package/templates/speci.config.json +9 -0
- package/templates/agents/speci-plan.md +0 -771
- /package/templates/agents/{speci-fix.md → speci-fix.agent.md} +0 -0
- /package/templates/agents/{speci-review.md → speci-review.agent.md} +0 -0
- /package/templates/agents/{speci-tidy.md → speci-tidy.agent.md} +0 -0
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ speci init [options]
|
|
|
81
81
|
|
|
82
82
|
**Options:**
|
|
83
83
|
|
|
84
|
-
- `-
|
|
84
|
+
- `-u, --update-agents` - Update agent files even if they already exist
|
|
85
85
|
- `-v, --verbose` - Show detailed output
|
|
86
86
|
|
|
87
87
|
**Creates:**
|
|
@@ -89,23 +89,24 @@ speci init [options]
|
|
|
89
89
|
- `speci.config.json` - Configuration file
|
|
90
90
|
- `docs/tasks/` - Task definition directory
|
|
91
91
|
- `.speci-logs/` - Log file directory
|
|
92
|
+
- `.github/copilot/agents/` - Copilot agent definitions
|
|
92
93
|
|
|
93
94
|
**Examples:**
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
|
-
#
|
|
97
|
+
# Initialize with defaults
|
|
97
98
|
speci init
|
|
98
99
|
|
|
99
|
-
#
|
|
100
|
-
speci init --
|
|
100
|
+
# Update agent files to latest version
|
|
101
|
+
speci init --update-agents
|
|
101
102
|
|
|
102
103
|
# Short alias version
|
|
103
|
-
speci i
|
|
104
|
+
speci i
|
|
104
105
|
```
|
|
105
106
|
|
|
106
107
|
### `speci plan` (alias: `p`)
|
|
107
108
|
|
|
108
|
-
Generate an implementation plan
|
|
109
|
+
Generate an implementation plan using Copilot with an initial prompt or input files.
|
|
109
110
|
|
|
110
111
|
**Usage:**
|
|
111
112
|
|
|
@@ -115,21 +116,31 @@ speci plan [options]
|
|
|
115
116
|
|
|
116
117
|
**Options:**
|
|
117
118
|
|
|
118
|
-
- `-
|
|
119
|
+
- `-p, --prompt <text>` - Initial prompt describing what to plan
|
|
120
|
+
- `-i, --input <files...>` - Input files for context (design docs, specs)
|
|
121
|
+
- `-a, --agent <filename>` - Use custom agent file from `.github/copilot/agents/`
|
|
119
122
|
- `-o, --output <path>` - Save plan to specific file
|
|
120
123
|
- `-v, --verbose` - Show detailed output
|
|
121
124
|
|
|
125
|
+
**Note:** At least `--prompt` or `--input` must be provided.
|
|
126
|
+
|
|
122
127
|
**Examples:**
|
|
123
128
|
|
|
124
129
|
```bash
|
|
125
|
-
#
|
|
126
|
-
speci plan
|
|
130
|
+
# Plan with an initial prompt
|
|
131
|
+
speci plan -p "Build a REST API for user authentication"
|
|
132
|
+
|
|
133
|
+
# Plan using a design doc as context
|
|
134
|
+
speci plan -i docs/design.md
|
|
127
135
|
|
|
128
|
-
#
|
|
129
|
-
speci plan
|
|
136
|
+
# Combine input files with a prompt
|
|
137
|
+
speci plan -i spec.md -p "Focus on the authentication module"
|
|
130
138
|
|
|
131
|
-
#
|
|
132
|
-
speci
|
|
139
|
+
# Save plan to a specific file
|
|
140
|
+
speci plan -i design.md -o docs/plan.md
|
|
141
|
+
|
|
142
|
+
# Use custom agent from .github/copilot/agents/
|
|
143
|
+
speci p -a my-custom-plan.agent.md -p "My feature"
|
|
133
144
|
```
|
|
134
145
|
|
|
135
146
|
### `speci task` (alias: `t`)
|
|
@@ -145,7 +156,7 @@ speci task --plan <path> [options]
|
|
|
145
156
|
**Options:**
|
|
146
157
|
|
|
147
158
|
- `-p, --plan <path>` - Path to plan file (required)
|
|
148
|
-
- `-a, --agent <
|
|
159
|
+
- `-a, --agent <filename>` - Use custom agent file from `.github/copilot/agents/`
|
|
149
160
|
- `-v, --verbose` - Show detailed output
|
|
150
161
|
|
|
151
162
|
**Examples:**
|
|
@@ -172,7 +183,7 @@ speci refactor [options]
|
|
|
172
183
|
|
|
173
184
|
- `-s, --scope <path>` - Directory or glob pattern to analyze
|
|
174
185
|
- `-o, --output <path>` - Save refactoring plan to file
|
|
175
|
-
- `-a, --agent <
|
|
186
|
+
- `-a, --agent <filename>` - Use custom agent file from `.github/copilot/agents/`
|
|
176
187
|
- `-v, --verbose` - Show detailed output
|
|
177
188
|
|
|
178
189
|
**Examples:**
|
|
@@ -306,15 +317,6 @@ The configuration file is created by `speci init` and can be customized:
|
|
|
306
317
|
"logs": ".speci-logs",
|
|
307
318
|
"lock": ".speci.lock"
|
|
308
319
|
},
|
|
309
|
-
"agents": {
|
|
310
|
-
"plan": null,
|
|
311
|
-
"task": null,
|
|
312
|
-
"refactor": null,
|
|
313
|
-
"impl": null,
|
|
314
|
-
"review": null,
|
|
315
|
-
"fix": null,
|
|
316
|
-
"tidy": null
|
|
317
|
-
},
|
|
318
320
|
"copilot": {
|
|
319
321
|
"permissions": "allow-all",
|
|
320
322
|
"model": null,
|
|
@@ -334,18 +336,19 @@ The configuration file is created by `speci init` and can be customized:
|
|
|
334
336
|
|
|
335
337
|
Environment variables can override configuration file settings:
|
|
336
338
|
|
|
337
|
-
| Variable | Config Path | Description
|
|
338
|
-
| ------------------------ | --------------------- |
|
|
339
|
-
| `SPECI_PROGRESS_PATH` | `paths.progress` | Path to PROGRESS.md file
|
|
340
|
-
| `SPECI_TASKS_PATH` | `paths.tasks` | Path to tasks directory
|
|
341
|
-
| `SPECI_LOG_PATH` | `paths.logs` | Path to log directory
|
|
342
|
-
| `SPECI_LOCK_PATH` | `paths.lock` | Path to lock file
|
|
343
|
-
| `SPECI_COPILOT_MODEL` | `copilot.model` | Copilot model to use
|
|
344
|
-
| `SPECI_MAX_ITERATIONS` | `loop.maxIterations` | Maximum loop iterations
|
|
345
|
-
| `SPECI_ENABLE_AUTO_FIX` | `loop.enableAutoFix` | Enable automatic gate fix attempts
|
|
346
|
-
| `SPECI_MAX_FIX_ATTEMPTS` | `gate.maxFixAttempts` | Maximum fix attempts
|
|
347
|
-
| `SPECI_DEBUG` | N/A | Enable debug logging (1 or true)
|
|
348
|
-
| `
|
|
339
|
+
| Variable | Config Path | Description |
|
|
340
|
+
| ------------------------ | --------------------- | --------------------------------------------- |
|
|
341
|
+
| `SPECI_PROGRESS_PATH` | `paths.progress` | Path to PROGRESS.md file |
|
|
342
|
+
| `SPECI_TASKS_PATH` | `paths.tasks` | Path to tasks directory |
|
|
343
|
+
| `SPECI_LOG_PATH` | `paths.logs` | Path to log directory |
|
|
344
|
+
| `SPECI_LOCK_PATH` | `paths.lock` | Path to lock file |
|
|
345
|
+
| `SPECI_COPILOT_MODEL` | `copilot.model` | Copilot model to use |
|
|
346
|
+
| `SPECI_MAX_ITERATIONS` | `loop.maxIterations` | Maximum loop iterations |
|
|
347
|
+
| `SPECI_ENABLE_AUTO_FIX` | `loop.enableAutoFix` | Enable automatic gate fix attempts |
|
|
348
|
+
| `SPECI_MAX_FIX_ATTEMPTS` | `gate.maxFixAttempts` | Maximum fix attempts |
|
|
349
|
+
| `SPECI_DEBUG` | N/A | Enable debug logging (1 or true) |
|
|
350
|
+
| `SPECI_NO_ANIMATION` | N/A | Disable banner animation (any value disables) |
|
|
351
|
+
| `NO_COLOR` | N/A | Disable colored output |
|
|
349
352
|
|
|
350
353
|
## Error Codes
|
|
351
354
|
|
|
@@ -363,13 +366,13 @@ speci uses structured error codes for clear diagnostics:
|
|
|
363
366
|
|
|
364
367
|
### Input Errors (ERR-INP-\*)
|
|
365
368
|
|
|
366
|
-
| Code | Message | Solution
|
|
367
|
-
| ---------- | ------------------------- |
|
|
368
|
-
| ERR-INP-01 | Required argument missing | Check command usage with `--help`
|
|
369
|
-
| ERR-INP-02 | Agent file not found |
|
|
370
|
-
| ERR-INP-03 | Config file is malformed | Fix JSON syntax in speci.config.json
|
|
371
|
-
| ERR-INP-04 | Config validation failed | Check config against schema
|
|
372
|
-
| ERR-INP-05 | Plan file not found | Provide valid path with `--plan`
|
|
369
|
+
| Code | Message | Solution |
|
|
370
|
+
| ---------- | ------------------------- | ---------------------------------------------------------- |
|
|
371
|
+
| ERR-INP-01 | Required argument missing | Check command usage with `--help` |
|
|
372
|
+
| ERR-INP-02 | Agent file not found | Run `speci init` or add agent to `.github/copilot/agents/` |
|
|
373
|
+
| ERR-INP-03 | Config file is malformed | Fix JSON syntax in speci.config.json |
|
|
374
|
+
| ERR-INP-04 | Config validation failed | Check config against schema |
|
|
375
|
+
| ERR-INP-05 | Plan file not found | Provide valid path with `--plan` |
|
|
373
376
|
|
|
374
377
|
### State Errors (ERR-STA-\*)
|
|
375
378
|
|
package/bin/speci.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { Command } from 'commander';
|
|
10
10
|
import { renderBanner, VERSION } from '../lib/ui/banner.js';
|
|
11
|
+
import { animateBanner, shouldAnimate } from '../lib/ui/banner-animation.js';
|
|
11
12
|
import { init } from '../lib/commands/init.js';
|
|
12
13
|
import { plan } from '../lib/commands/plan.js';
|
|
13
14
|
import { task } from '../lib/commands/task.js';
|
|
@@ -20,9 +21,19 @@ import { setVerbose, debug } from '../lib/utils/logger.js';
|
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Display the application banner
|
|
24
|
+
*
|
|
25
|
+
* Conditionally animates the banner when appropriate conditions are met.
|
|
26
|
+
* Returns a Promise when animation is enabled, or void when displaying static banner.
|
|
27
|
+
*
|
|
28
|
+
* @param options - Optional configuration for banner display
|
|
23
29
|
*/
|
|
24
|
-
function displayBanner(): void {
|
|
25
|
-
|
|
30
|
+
function displayBanner(options?: { color?: boolean }): Promise<void> | void {
|
|
31
|
+
if (shouldAnimate(options)) {
|
|
32
|
+
console.log();
|
|
33
|
+
return animateBanner().then(() => console.log());
|
|
34
|
+
} else {
|
|
35
|
+
console.log('\n' + renderBanner({ showVersion: true }) + '\n');
|
|
36
|
+
}
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
const program = new Command();
|
|
@@ -34,7 +45,7 @@ program
|
|
|
34
45
|
.description('Speci CLI - AI-powered implementation loop orchestrator')
|
|
35
46
|
.option('-v, --verbose', 'Enable verbose output')
|
|
36
47
|
.option('--no-color', 'Disable colored output')
|
|
37
|
-
.hook('preAction', (_thisCommand) => {
|
|
48
|
+
.hook('preAction', async (_thisCommand) => {
|
|
38
49
|
// Enable verbose mode if --verbose flag is set
|
|
39
50
|
const opts = _thisCommand.opts();
|
|
40
51
|
if (opts.verbose) {
|
|
@@ -53,7 +64,10 @@ program
|
|
|
53
64
|
args.includes('--version') ||
|
|
54
65
|
args.includes('-V');
|
|
55
66
|
if (!isHelpOrVersion) {
|
|
56
|
-
displayBanner();
|
|
67
|
+
const result = displayBanner({ color: opts.color });
|
|
68
|
+
if (result instanceof Promise) {
|
|
69
|
+
await result;
|
|
70
|
+
}
|
|
57
71
|
}
|
|
58
72
|
});
|
|
59
73
|
|
|
@@ -63,6 +77,7 @@ program
|
|
|
63
77
|
.alias('i')
|
|
64
78
|
.description('Initialize Speci in current project')
|
|
65
79
|
.option('-y, --yes', 'Accept all defaults')
|
|
80
|
+
.option('-u, --update-agents', 'Update agent files even if they exist')
|
|
66
81
|
.option('-v, --verbose', 'Show detailed output')
|
|
67
82
|
.addHelpText(
|
|
68
83
|
'after',
|
|
@@ -71,6 +86,7 @@ Examples:
|
|
|
71
86
|
$ speci init Interactive setup wizard
|
|
72
87
|
$ speci init --yes Quick setup with defaults
|
|
73
88
|
$ speci i -y Force initialize with defaults
|
|
89
|
+
$ speci init -u Update agent files to latest version
|
|
74
90
|
`
|
|
75
91
|
)
|
|
76
92
|
.action(init);
|
|
@@ -80,6 +96,11 @@ program
|
|
|
80
96
|
.command('plan')
|
|
81
97
|
.alias('p')
|
|
82
98
|
.description('Generate implementation plan interactively')
|
|
99
|
+
.option('-p, --prompt <text>', 'Initial prompt describing what to plan')
|
|
100
|
+
.option(
|
|
101
|
+
'-i, --input <files...>',
|
|
102
|
+
'Input files for context (design docs, specs)'
|
|
103
|
+
)
|
|
83
104
|
.option('-a, --agent <path>', 'Use custom agent file')
|
|
84
105
|
.option('-o, --output <path>', 'Output plan to file')
|
|
85
106
|
.option('-v, --verbose', 'Show detailed output')
|
|
@@ -87,9 +108,11 @@ program
|
|
|
87
108
|
'after',
|
|
88
109
|
`
|
|
89
110
|
Examples:
|
|
90
|
-
$ speci plan
|
|
91
|
-
$ speci plan
|
|
92
|
-
$ speci
|
|
111
|
+
$ speci plan -p "Build a REST API" Plan with initial prompt
|
|
112
|
+
$ speci plan -i docs/design.md Plan using design doc as context
|
|
113
|
+
$ speci plan -i spec.md -p "Focus on auth" Combine input files with prompt
|
|
114
|
+
$ speci plan -i design.md -o docs/plan.md Save plan to specific file
|
|
115
|
+
$ speci p -a custom-agent.md -p "My feature" Use custom agent
|
|
93
116
|
`
|
|
94
117
|
)
|
|
95
118
|
.action(plan);
|
|
@@ -220,9 +243,14 @@ program.on('command:*', (operands) => {
|
|
|
220
243
|
|
|
221
244
|
// Show banner and help when no arguments provided
|
|
222
245
|
if (process.argv.length <= 2) {
|
|
223
|
-
|
|
224
|
-
|
|
246
|
+
(async () => {
|
|
247
|
+
const result = displayBanner({ color: program.opts().color });
|
|
248
|
+
if (result instanceof Promise) {
|
|
249
|
+
await result;
|
|
250
|
+
}
|
|
251
|
+
program.help();
|
|
252
|
+
})();
|
|
253
|
+
} else {
|
|
254
|
+
// Parse command line arguments (only when a command is provided)
|
|
255
|
+
program.parse(process.argv);
|
|
225
256
|
}
|
|
226
|
-
|
|
227
|
-
// Parse command line arguments
|
|
228
|
-
program.parse(process.argv);
|
package/lib/commands/init.ts
CHANGED
|
@@ -1,126 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Init Command Module
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
4
|
+
* Provides a project setup for new Speci users.
|
|
5
5
|
* Creates speci.config.json, directory structure, and initial files.
|
|
6
|
-
* Uses Node.js readline for zero-dependency interactive prompts.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
copyFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
statSync,
|
|
15
|
+
} from 'node:fs';
|
|
16
|
+
import { join, relative } from 'node:path';
|
|
12
17
|
import { log } from '../utils/logger.js';
|
|
13
18
|
import { renderBanner } from '../ui/banner.js';
|
|
14
19
|
import { colorize } from '../ui/colors.js';
|
|
15
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getDefaults,
|
|
22
|
+
getAgentsTemplatePath,
|
|
23
|
+
GITHUB_AGENTS_DIR,
|
|
24
|
+
type SpeciConfig,
|
|
25
|
+
} from '../config.js';
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
28
|
* Options for the init command
|
|
19
29
|
*/
|
|
20
30
|
export interface InitOptions {
|
|
21
|
-
yes?: boolean; // Skip prompts, use defaults
|
|
22
31
|
verbose?: boolean; // Show detailed output
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Prompt user for input with default value
|
|
27
|
-
* @param question - Question to ask
|
|
28
|
-
* @param defaultValue - Default value if no input provided
|
|
29
|
-
* @returns User's answer or default value
|
|
30
|
-
*/
|
|
31
|
-
async function prompt(question: string, defaultValue: string): Promise<string> {
|
|
32
|
-
const rl = createInterface({
|
|
33
|
-
input: process.stdin,
|
|
34
|
-
output: process.stdout,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
return new Promise((resolve) => {
|
|
38
|
-
const styled = `${colorize(question, 'sky400')} ${colorize(`(${defaultValue})`, 'dim')}: `;
|
|
39
|
-
rl.question(styled, (answer) => {
|
|
40
|
-
rl.close();
|
|
41
|
-
resolve(answer.trim() || defaultValue);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Validate path to ensure it's safe (no path traversal outside project)
|
|
48
|
-
* @param input - Path to validate
|
|
49
|
-
* @returns Validated path
|
|
50
|
-
* @throws Error if path attempts to escape project directory
|
|
51
|
-
*/
|
|
52
|
-
function validatePath(input: string): string {
|
|
53
|
-
const normalized = resolve(process.cwd(), input);
|
|
54
|
-
const projectRoot = process.cwd();
|
|
55
|
-
|
|
56
|
-
// Check if normalized path starts with project root
|
|
57
|
-
if (!normalized.startsWith(projectRoot)) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
`Invalid path: ${input} attempts to escape project directory`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return input;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Gather configuration through interactive prompts
|
|
68
|
-
* @returns Partial config with user answers
|
|
69
|
-
*/
|
|
70
|
-
async function gatherConfig(): Promise<Partial<SpeciConfig>> {
|
|
71
|
-
// Project name is prompted but not used in config (for future enhancement)
|
|
72
|
-
await prompt('Project name', basename(process.cwd()));
|
|
73
|
-
|
|
74
|
-
const progressPath = validatePath(
|
|
75
|
-
await prompt('Progress file path', 'docs/PROGRESS.md')
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const tasksPath = validatePath(
|
|
79
|
-
await prompt('Tasks directory path', 'docs/tasks')
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const logsPath = validatePath(
|
|
83
|
-
await prompt('Logs directory path', '.speci-logs')
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const gateCommands = await prompt(
|
|
87
|
-
'Gate commands (comma-separated)',
|
|
88
|
-
'npm run lint, npm run typecheck, npm test'
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
paths: {
|
|
93
|
-
progress: progressPath,
|
|
94
|
-
tasks: tasksPath,
|
|
95
|
-
logs: logsPath,
|
|
96
|
-
lock: '.speci-lock',
|
|
97
|
-
},
|
|
98
|
-
gate: {
|
|
99
|
-
commands: gateCommands.split(',').map((cmd) => cmd.trim()),
|
|
100
|
-
maxFixAttempts: 3,
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Merge user config with defaults
|
|
107
|
-
* @param userConfig - Partial config from user
|
|
108
|
-
* @returns Full config merged with defaults
|
|
109
|
-
*/
|
|
110
|
-
function mergeWithDefaults(userConfig: Partial<SpeciConfig>): SpeciConfig {
|
|
111
|
-
const defaults = getDefaults();
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
...defaults,
|
|
115
|
-
paths: {
|
|
116
|
-
...defaults.paths,
|
|
117
|
-
...userConfig.paths,
|
|
118
|
-
},
|
|
119
|
-
gate: {
|
|
120
|
-
...defaults.gate,
|
|
121
|
-
...userConfig.gate,
|
|
122
|
-
},
|
|
123
|
-
};
|
|
32
|
+
updateAgents?: boolean; // Force update agent files even if they exist
|
|
124
33
|
}
|
|
125
34
|
|
|
126
35
|
/**
|
|
@@ -132,11 +41,13 @@ function checkExistingFiles(config: SpeciConfig): {
|
|
|
132
41
|
configExists: boolean;
|
|
133
42
|
tasksExists: boolean;
|
|
134
43
|
logsExists: boolean;
|
|
44
|
+
agentsExist: boolean;
|
|
135
45
|
} {
|
|
136
46
|
return {
|
|
137
47
|
configExists: existsSync('speci.config.json'),
|
|
138
48
|
tasksExists: existsSync(config.paths.tasks),
|
|
139
49
|
logsExists: existsSync(config.paths.logs),
|
|
50
|
+
agentsExist: existsSync(GITHUB_AGENTS_DIR),
|
|
140
51
|
};
|
|
141
52
|
}
|
|
142
53
|
|
|
@@ -144,26 +55,27 @@ function checkExistingFiles(config: SpeciConfig): {
|
|
|
144
55
|
* Display summary of actions to be taken
|
|
145
56
|
* @param config - Config to display
|
|
146
57
|
* @param existing - Existing files flags
|
|
58
|
+
* @param updateAgents - Whether to force update agent files
|
|
147
59
|
*/
|
|
148
60
|
function displayActionSummary(
|
|
149
61
|
config: SpeciConfig,
|
|
150
|
-
existing: ReturnType<typeof checkExistingFiles
|
|
62
|
+
existing: ReturnType<typeof checkExistingFiles>,
|
|
63
|
+
updateAgents: boolean = false
|
|
151
64
|
): void {
|
|
152
|
-
console.log();
|
|
153
|
-
log.info('The following actions will be performed:');
|
|
154
|
-
console.log();
|
|
155
|
-
|
|
156
65
|
if (existing.configExists) {
|
|
157
66
|
log.warn(' speci.config.json already exists (will skip)');
|
|
158
67
|
} else {
|
|
159
|
-
console.log(colorize('
|
|
68
|
+
console.log(colorize(' speci.config.json will be created', 'success'));
|
|
160
69
|
}
|
|
161
70
|
|
|
162
71
|
if (existing.tasksExists) {
|
|
163
72
|
log.warn(` ${config.paths.tasks}/ already exists (will skip)`);
|
|
164
73
|
} else {
|
|
165
74
|
console.log(
|
|
166
|
-
colorize(
|
|
75
|
+
colorize(
|
|
76
|
+
` ${config.paths.tasks}/ directory will be created`,
|
|
77
|
+
'success'
|
|
78
|
+
)
|
|
167
79
|
);
|
|
168
80
|
}
|
|
169
81
|
|
|
@@ -171,7 +83,26 @@ function displayActionSummary(
|
|
|
171
83
|
log.warn(` ${config.paths.logs}/ already exists (will skip)`);
|
|
172
84
|
} else {
|
|
173
85
|
console.log(
|
|
174
|
-
colorize(`
|
|
86
|
+
colorize(` ${config.paths.logs}/ directory will be created`, 'success')
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (existing.agentsExist) {
|
|
91
|
+
if (updateAgents) {
|
|
92
|
+
console.log(
|
|
93
|
+
colorize(
|
|
94
|
+
` ${GITHUB_AGENTS_DIR}/ directory will be updated`,
|
|
95
|
+
'success'
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
log.warn(
|
|
100
|
+
` ${GITHUB_AGENTS_DIR}/ already exists (will skip, use --update-agents to overwrite)`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
console.log(
|
|
105
|
+
colorize(` ${GITHUB_AGENTS_DIR}/ directory will be updated`, 'success')
|
|
175
106
|
);
|
|
176
107
|
}
|
|
177
108
|
|
|
@@ -237,17 +168,97 @@ async function createFiles(
|
|
|
237
168
|
}
|
|
238
169
|
}
|
|
239
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Recursively copy a directory
|
|
173
|
+
* @param src - Source directory path
|
|
174
|
+
* @param dest - Destination directory path
|
|
175
|
+
* @returns Number of files copied
|
|
176
|
+
*/
|
|
177
|
+
function copyDirectoryRecursive(src: string, dest: string): number {
|
|
178
|
+
let fileCount = 0;
|
|
179
|
+
|
|
180
|
+
// Create destination directory
|
|
181
|
+
mkdirSync(dest, { recursive: true, mode: 0o755 });
|
|
182
|
+
|
|
183
|
+
// Read source directory contents
|
|
184
|
+
const entries = readdirSync(src);
|
|
185
|
+
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
const srcPath = join(src, entry);
|
|
188
|
+
const destPath = join(dest, entry);
|
|
189
|
+
const stat = statSync(srcPath);
|
|
190
|
+
|
|
191
|
+
if (stat.isDirectory()) {
|
|
192
|
+
// Recursively copy subdirectories
|
|
193
|
+
fileCount += copyDirectoryRecursive(srcPath, destPath);
|
|
194
|
+
} else if (stat.isFile()) {
|
|
195
|
+
// Copy file
|
|
196
|
+
copyFileSync(srcPath, destPath);
|
|
197
|
+
const relativePath = relative(getAgentsTemplatePath(), srcPath);
|
|
198
|
+
log.debug(`Copied: ${relativePath}`);
|
|
199
|
+
fileCount++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return fileCount;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Copy agent files to .github/copilot/agents/
|
|
208
|
+
* This allows copilot CLI to use --agent flag with agent names
|
|
209
|
+
* Recursively copies entire agents template directory including subagents
|
|
210
|
+
* @param existing - Existing files flags
|
|
211
|
+
* @param forceUpdate - Force update even if agents exist
|
|
212
|
+
*/
|
|
213
|
+
async function copyAgentFiles(
|
|
214
|
+
existing: ReturnType<typeof checkExistingFiles>,
|
|
215
|
+
forceUpdate: boolean = false
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
if (existing.agentsExist && !forceUpdate) {
|
|
218
|
+
log.debug(`Skipping agent files copy: ${GITHUB_AGENTS_DIR} already exists`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const templateDir = getAgentsTemplatePath();
|
|
224
|
+
|
|
225
|
+
if (!existsSync(templateDir)) {
|
|
226
|
+
throw new Error(`Agent templates directory not found: ${templateDir}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Recursively copy entire agents directory
|
|
230
|
+
const fileCount = copyDirectoryRecursive(templateDir, GITHUB_AGENTS_DIR);
|
|
231
|
+
|
|
232
|
+
const action = existing.agentsExist ? 'Updated' : 'Copied';
|
|
233
|
+
log.success(
|
|
234
|
+
`${action} ${fileCount} agent files inside ${GITHUB_AGENTS_DIR}/`
|
|
235
|
+
);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Failed to copy agent files: ${error instanceof Error ? error.message : String(error)}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
240
243
|
/**
|
|
241
244
|
* Display success message and next steps
|
|
242
245
|
*/
|
|
243
246
|
function displaySuccess(): void {
|
|
244
|
-
console.log();
|
|
245
|
-
log.success('Speci initialization complete!');
|
|
246
247
|
console.log();
|
|
247
248
|
log.info('Next steps:');
|
|
248
|
-
console.log(colorize(' 1.
|
|
249
|
-
console.log(
|
|
250
|
-
|
|
249
|
+
console.log(colorize(' 1. Generate your plan with: speci plan', 'dim'));
|
|
250
|
+
console.log(
|
|
251
|
+
colorize(
|
|
252
|
+
' 2. Generate your tasks and PROGRESS.md with: speci tasks',
|
|
253
|
+
'dim'
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
console.log(
|
|
257
|
+
colorize(
|
|
258
|
+
' 3. After a manual check start the implementation loop: speci run',
|
|
259
|
+
'dim'
|
|
260
|
+
)
|
|
261
|
+
);
|
|
251
262
|
console.log();
|
|
252
263
|
}
|
|
253
264
|
|
|
@@ -264,19 +275,14 @@ export async function init(options: InitOptions = {}): Promise<void> {
|
|
|
264
275
|
log.info('Initializing Speci in current directory...');
|
|
265
276
|
console.log();
|
|
266
277
|
|
|
267
|
-
//
|
|
268
|
-
const
|
|
269
|
-
? { paths: getDefaults().paths, gate: getDefaults().gate }
|
|
270
|
-
: await gatherConfig();
|
|
271
|
-
|
|
272
|
-
// Merge with defaults
|
|
273
|
-
const config = mergeWithDefaults(userConfig);
|
|
278
|
+
// Use default configuration
|
|
279
|
+
const config = getDefaults();
|
|
274
280
|
|
|
275
281
|
// Check existing files
|
|
276
282
|
const existing = checkExistingFiles(config);
|
|
277
283
|
|
|
278
284
|
// Display action summary
|
|
279
|
-
displayActionSummary(config, existing);
|
|
285
|
+
displayActionSummary(config, existing, options.updateAgents);
|
|
280
286
|
|
|
281
287
|
// Create directories
|
|
282
288
|
await createDirectories(config, existing);
|
|
@@ -284,6 +290,9 @@ export async function init(options: InitOptions = {}): Promise<void> {
|
|
|
284
290
|
// Create files
|
|
285
291
|
await createFiles(config, existing);
|
|
286
292
|
|
|
293
|
+
// Copy agent files to .github/copilot/agents/
|
|
294
|
+
await copyAgentFiles(existing, options.updateAgents);
|
|
295
|
+
|
|
287
296
|
// Display success and next steps
|
|
288
297
|
displaySuccess();
|
|
289
298
|
} catch (error) {
|