sequant 1.10.1 ā 1.11.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 +6 -1
- package/dist/bin/cli.js +55 -2
- package/dist/dashboard/server.d.ts +37 -0
- package/dist/dashboard/server.js +968 -0
- package/dist/src/commands/dashboard.d.ts +25 -0
- package/dist/src/commands/dashboard.js +44 -0
- package/dist/src/commands/doctor.d.ts +18 -1
- package/dist/src/commands/doctor.js +105 -2
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +26 -2
- package/dist/src/commands/run.d.ts +20 -0
- package/dist/src/commands/run.js +151 -3
- package/dist/src/commands/state.d.ts +60 -0
- package/dist/src/commands/state.js +267 -0
- package/dist/src/commands/stats.d.ts +3 -2
- package/dist/src/commands/stats.js +246 -38
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +28 -3
- package/dist/src/lib/ac-parser.d.ts +61 -0
- package/dist/src/lib/ac-parser.js +156 -0
- package/dist/src/lib/fs.d.ts +19 -0
- package/dist/src/lib/fs.js +58 -1
- package/dist/src/lib/settings.d.ts +7 -0
- package/dist/src/lib/settings.js +1 -0
- package/dist/src/lib/system.d.ts +19 -0
- package/dist/src/lib/system.js +26 -0
- package/dist/src/lib/templates.d.ts +34 -1
- package/dist/src/lib/templates.js +109 -5
- package/dist/src/lib/workflow/metrics-schema.d.ts +153 -0
- package/dist/src/lib/workflow/metrics-schema.js +138 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +102 -0
- package/dist/src/lib/workflow/metrics-writer.js +189 -0
- package/dist/src/lib/workflow/state-manager.d.ts +18 -1
- package/dist/src/lib/workflow/state-manager.js +61 -1
- package/dist/src/lib/workflow/state-schema.d.ts +152 -1
- package/dist/src/lib/workflow/state-schema.js +99 -0
- package/dist/src/lib/workflow/state-utils.d.ts +67 -3
- package/dist/src/lib/workflow/state-utils.js +289 -8
- package/dist/src/lib/workflow/types.d.ts +2 -0
- package/dist/src/lib/workflow/types.js +1 -0
- package/package.json +5 -1
|
@@ -16,6 +16,8 @@ function colorStatus(status) {
|
|
|
16
16
|
return chalk.gray(status);
|
|
17
17
|
case "in_progress":
|
|
18
18
|
return chalk.blue(status);
|
|
19
|
+
case "waiting_for_qa_gate":
|
|
20
|
+
return chalk.yellow(status);
|
|
19
21
|
case "ready_for_merge":
|
|
20
22
|
return chalk.green(status);
|
|
21
23
|
case "merged":
|
|
@@ -124,6 +126,7 @@ function displayIssueSummary(issues) {
|
|
|
124
126
|
// Group by status
|
|
125
127
|
const byStatus = {
|
|
126
128
|
in_progress: [],
|
|
129
|
+
waiting_for_qa_gate: [],
|
|
127
130
|
ready_for_merge: [],
|
|
128
131
|
blocked: [],
|
|
129
132
|
not_started: [],
|
|
@@ -136,6 +139,7 @@ function displayIssueSummary(issues) {
|
|
|
136
139
|
// Display in priority order
|
|
137
140
|
const statusOrder = [
|
|
138
141
|
"in_progress",
|
|
142
|
+
"waiting_for_qa_gate",
|
|
139
143
|
"ready_for_merge",
|
|
140
144
|
"blocked",
|
|
141
145
|
"not_started",
|
|
@@ -157,6 +161,9 @@ function displayIssueSummary(issues) {
|
|
|
157
161
|
byStatus.in_progress.length > 0
|
|
158
162
|
? chalk.blue(`In Progress: ${byStatus.in_progress.length}`)
|
|
159
163
|
: null,
|
|
164
|
+
byStatus.waiting_for_qa_gate.length > 0
|
|
165
|
+
? chalk.yellow(`QA Gate: ${byStatus.waiting_for_qa_gate.length}`)
|
|
166
|
+
: null,
|
|
160
167
|
byStatus.ready_for_merge.length > 0
|
|
161
168
|
? chalk.green(`Ready: ${byStatus.ready_for_merge.length}`)
|
|
162
169
|
: null,
|
|
@@ -316,10 +323,14 @@ async function handleRebuild(options) {
|
|
|
316
323
|
*/
|
|
317
324
|
async function handleCleanup(options) {
|
|
318
325
|
const dryRun = options.dryRun ?? false;
|
|
326
|
+
const removeAll = options.all ?? false;
|
|
319
327
|
if (!options.json) {
|
|
320
328
|
if (dryRun) {
|
|
321
329
|
console.log(chalk.bold("\nš§¹ Cleanup preview (dry run)...\n"));
|
|
322
330
|
}
|
|
331
|
+
else if (removeAll) {
|
|
332
|
+
console.log(chalk.bold("\nš§¹ Cleaning up all orphaned entries...\n"));
|
|
333
|
+
}
|
|
323
334
|
else {
|
|
324
335
|
console.log(chalk.bold("\nš§¹ Cleaning up stale entries...\n"));
|
|
325
336
|
}
|
|
@@ -327,6 +338,7 @@ async function handleCleanup(options) {
|
|
|
327
338
|
const result = await cleanupStaleEntries({
|
|
328
339
|
dryRun,
|
|
329
340
|
maxAgeDays: options.maxAge,
|
|
341
|
+
removeAll,
|
|
330
342
|
verbose: !options.json,
|
|
331
343
|
});
|
|
332
344
|
if (options.json) {
|
|
@@ -336,7 +348,8 @@ async function handleCleanup(options) {
|
|
|
336
348
|
if (result.success) {
|
|
337
349
|
const orphanedCount = result.orphaned.length;
|
|
338
350
|
const removedCount = result.removed.length;
|
|
339
|
-
|
|
351
|
+
const mergedCount = result.merged.length;
|
|
352
|
+
if (orphanedCount === 0 && removedCount === 0 && mergedCount === 0) {
|
|
340
353
|
console.log(chalk.green("ā No stale entries found"));
|
|
341
354
|
}
|
|
342
355
|
else {
|
|
@@ -346,14 +359,26 @@ async function handleCleanup(options) {
|
|
|
346
359
|
else {
|
|
347
360
|
console.log(chalk.green("ā Cleanup completed"));
|
|
348
361
|
}
|
|
362
|
+
if (mergedCount > 0) {
|
|
363
|
+
console.log(chalk.green(` Merged PRs (auto-removed): ${result.merged.map((n) => `#${n}`).join(", ")}`));
|
|
364
|
+
}
|
|
349
365
|
if (orphanedCount > 0) {
|
|
350
|
-
|
|
366
|
+
const orphanedNotMerged = result.orphaned.filter((n) => !result.merged.includes(n));
|
|
367
|
+
if (orphanedNotMerged.length > 0) {
|
|
368
|
+
console.log(chalk.yellow(` Abandoned (no merge): ${orphanedNotMerged.map((n) => `#${n}`).join(", ")}`));
|
|
369
|
+
}
|
|
351
370
|
}
|
|
352
371
|
if (removedCount > 0) {
|
|
353
|
-
|
|
372
|
+
const removedNotMerged = result.removed.filter((n) => !result.merged.includes(n));
|
|
373
|
+
if (removedNotMerged.length > 0) {
|
|
374
|
+
console.log(chalk.gray(` Removed: ${removedNotMerged.map((n) => `#${n}`).join(", ")}`));
|
|
375
|
+
}
|
|
354
376
|
}
|
|
355
377
|
if (dryRun) {
|
|
356
378
|
console.log(chalk.gray("\nRun without --dry-run to apply these changes."));
|
|
379
|
+
if (!removeAll && orphanedCount > 0) {
|
|
380
|
+
console.log(chalk.gray("Use --all to remove both merged and abandoned entries."));
|
|
381
|
+
}
|
|
357
382
|
}
|
|
358
383
|
}
|
|
359
384
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acceptance Criteria Parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts acceptance criteria from GitHub issue markdown.
|
|
5
|
+
* Supports checkbox format: `- [ ] **AC-1:** Description`
|
|
6
|
+
* Also supports alternate formats: `- [ ] **B2:** Description`
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { parseAcceptanceCriteria } from './ac-parser';
|
|
11
|
+
*
|
|
12
|
+
* const issueBody = `
|
|
13
|
+
* ## Acceptance Criteria
|
|
14
|
+
* - [ ] **AC-1:** User can login
|
|
15
|
+
* - [ ] **AC-2:** Session persists
|
|
16
|
+
* `;
|
|
17
|
+
*
|
|
18
|
+
* const criteria = parseAcceptanceCriteria(issueBody);
|
|
19
|
+
* // Returns: [
|
|
20
|
+
* // { id: 'AC-1', description: 'User can login', verificationMethod: 'manual', status: 'pending' },
|
|
21
|
+
* // { id: 'AC-2', description: 'Session persists', verificationMethod: 'manual', status: 'pending' }
|
|
22
|
+
* // ]
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { type AcceptanceCriterion, type AcceptanceCriteria, type ACVerificationMethod } from "./workflow/state-schema.js";
|
|
26
|
+
/**
|
|
27
|
+
* Infer verification method from description text
|
|
28
|
+
*
|
|
29
|
+
* @param description - The AC description text
|
|
30
|
+
* @returns The inferred verification method (defaults to 'manual')
|
|
31
|
+
*/
|
|
32
|
+
export declare function inferVerificationMethod(description: string): ACVerificationMethod;
|
|
33
|
+
/**
|
|
34
|
+
* Parse acceptance criteria from GitHub issue markdown
|
|
35
|
+
*
|
|
36
|
+
* Extracts AC items from checkbox format in the issue body.
|
|
37
|
+
* Supports multiple formats:
|
|
38
|
+
* - `- [ ] **AC-1:** Description`
|
|
39
|
+
* - `- [ ] **B2:** Description`
|
|
40
|
+
* - `- [ ] AC-1: Description`
|
|
41
|
+
*
|
|
42
|
+
* @param issueBody - The full GitHub issue body markdown
|
|
43
|
+
* @returns Array of parsed acceptance criteria
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseAcceptanceCriteria(issueBody: string): AcceptanceCriterion[];
|
|
46
|
+
/**
|
|
47
|
+
* Extract and create full AcceptanceCriteria object from issue body
|
|
48
|
+
*
|
|
49
|
+
* This is the main entry point for the /spec skill to use.
|
|
50
|
+
*
|
|
51
|
+
* @param issueBody - The full GitHub issue body markdown
|
|
52
|
+
* @returns Complete AcceptanceCriteria object with items and summary
|
|
53
|
+
*/
|
|
54
|
+
export declare function extractAcceptanceCriteria(issueBody: string): AcceptanceCriteria;
|
|
55
|
+
/**
|
|
56
|
+
* Check if an issue body contains acceptance criteria
|
|
57
|
+
*
|
|
58
|
+
* @param issueBody - The full GitHub issue body markdown
|
|
59
|
+
* @returns True if AC items are found
|
|
60
|
+
*/
|
|
61
|
+
export declare function hasAcceptanceCriteria(issueBody: string): boolean;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acceptance Criteria Parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts acceptance criteria from GitHub issue markdown.
|
|
5
|
+
* Supports checkbox format: `- [ ] **AC-1:** Description`
|
|
6
|
+
* Also supports alternate formats: `- [ ] **B2:** Description`
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { parseAcceptanceCriteria } from './ac-parser';
|
|
11
|
+
*
|
|
12
|
+
* const issueBody = `
|
|
13
|
+
* ## Acceptance Criteria
|
|
14
|
+
* - [ ] **AC-1:** User can login
|
|
15
|
+
* - [ ] **AC-2:** Session persists
|
|
16
|
+
* `;
|
|
17
|
+
*
|
|
18
|
+
* const criteria = parseAcceptanceCriteria(issueBody);
|
|
19
|
+
* // Returns: [
|
|
20
|
+
* // { id: 'AC-1', description: 'User can login', verificationMethod: 'manual', status: 'pending' },
|
|
21
|
+
* // { id: 'AC-2', description: 'Session persists', verificationMethod: 'manual', status: 'pending' }
|
|
22
|
+
* // ]
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { createAcceptanceCriterion, createAcceptanceCriteria, } from "./workflow/state-schema.js";
|
|
26
|
+
/**
|
|
27
|
+
* Regex patterns for AC extraction
|
|
28
|
+
*
|
|
29
|
+
* Matches:
|
|
30
|
+
* - `- [ ] **AC-1:** Description`
|
|
31
|
+
* - `- [x] **AC-1:** Description`
|
|
32
|
+
* - `- [ ] **B2:** Description`
|
|
33
|
+
* - `- [ ] **AC1:** Description`
|
|
34
|
+
*/
|
|
35
|
+
const AC_PATTERNS = [
|
|
36
|
+
// Pattern 1: `- [ ] **AC-1:** Description` or `- [x] **AC-1:** Description`
|
|
37
|
+
/^-\s*\[[x\s]\]\s*\*\*([A-Za-z]+-?\d+):\*\*\s*(.+)$/gim,
|
|
38
|
+
// Pattern 2: `- [ ] **B2:** Description` (letter + number without hyphen)
|
|
39
|
+
/^-\s*\[[x\s]\]\s*\*\*([A-Za-z]\d+):\*\*\s*(.+)$/gim,
|
|
40
|
+
// Pattern 3: `- [ ] AC-1: Description` (no bold)
|
|
41
|
+
/^-\s*\[[x\s]\]\s*([A-Za-z]+-?\d+):\s*(.+)$/gim,
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* Keywords that suggest verification method
|
|
45
|
+
*/
|
|
46
|
+
const VERIFICATION_KEYWORDS = {
|
|
47
|
+
// Unit test keywords
|
|
48
|
+
unit: "unit_test",
|
|
49
|
+
"unit test": "unit_test",
|
|
50
|
+
unittest: "unit_test",
|
|
51
|
+
// Integration test keywords
|
|
52
|
+
integration: "integration_test",
|
|
53
|
+
"integration test": "integration_test",
|
|
54
|
+
api: "integration_test",
|
|
55
|
+
endpoint: "integration_test",
|
|
56
|
+
// Browser test keywords
|
|
57
|
+
browser: "browser_test",
|
|
58
|
+
"browser test": "browser_test",
|
|
59
|
+
e2e: "browser_test",
|
|
60
|
+
"end-to-end": "browser_test",
|
|
61
|
+
ui: "browser_test",
|
|
62
|
+
click: "browser_test",
|
|
63
|
+
navigate: "browser_test",
|
|
64
|
+
display: "browser_test",
|
|
65
|
+
dashboard: "browser_test",
|
|
66
|
+
// Manual keywords (explicit)
|
|
67
|
+
manual: "manual",
|
|
68
|
+
"manual test": "manual",
|
|
69
|
+
verify: "manual",
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Infer verification method from description text
|
|
73
|
+
*
|
|
74
|
+
* @param description - The AC description text
|
|
75
|
+
* @returns The inferred verification method (defaults to 'manual')
|
|
76
|
+
*/
|
|
77
|
+
export function inferVerificationMethod(description) {
|
|
78
|
+
const lowerDesc = description.toLowerCase();
|
|
79
|
+
// Check for explicit keywords (longer phrases first)
|
|
80
|
+
const sortedKeywords = Object.keys(VERIFICATION_KEYWORDS).sort((a, b) => b.length - a.length);
|
|
81
|
+
for (const keyword of sortedKeywords) {
|
|
82
|
+
if (lowerDesc.includes(keyword)) {
|
|
83
|
+
return VERIFICATION_KEYWORDS[keyword];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return "manual";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parse a single line and extract AC if present
|
|
90
|
+
*
|
|
91
|
+
* @param line - A single line from the issue body
|
|
92
|
+
* @returns Parsed AC or null if line doesn't match
|
|
93
|
+
*/
|
|
94
|
+
function parseACLine(line) {
|
|
95
|
+
for (const pattern of AC_PATTERNS) {
|
|
96
|
+
// Reset regex lastIndex for global patterns
|
|
97
|
+
pattern.lastIndex = 0;
|
|
98
|
+
const match = pattern.exec(line);
|
|
99
|
+
if (match) {
|
|
100
|
+
return {
|
|
101
|
+
id: match[1].toUpperCase(),
|
|
102
|
+
description: match[2].trim(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parse acceptance criteria from GitHub issue markdown
|
|
110
|
+
*
|
|
111
|
+
* Extracts AC items from checkbox format in the issue body.
|
|
112
|
+
* Supports multiple formats:
|
|
113
|
+
* - `- [ ] **AC-1:** Description`
|
|
114
|
+
* - `- [ ] **B2:** Description`
|
|
115
|
+
* - `- [ ] AC-1: Description`
|
|
116
|
+
*
|
|
117
|
+
* @param issueBody - The full GitHub issue body markdown
|
|
118
|
+
* @returns Array of parsed acceptance criteria
|
|
119
|
+
*/
|
|
120
|
+
export function parseAcceptanceCriteria(issueBody) {
|
|
121
|
+
const criteria = [];
|
|
122
|
+
const seenIds = new Set();
|
|
123
|
+
// Split into lines and process each
|
|
124
|
+
const lines = issueBody.split("\n");
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const parsed = parseACLine(line);
|
|
127
|
+
if (parsed && !seenIds.has(parsed.id)) {
|
|
128
|
+
seenIds.add(parsed.id);
|
|
129
|
+
const verificationMethod = inferVerificationMethod(parsed.description);
|
|
130
|
+
criteria.push(createAcceptanceCriterion(parsed.id, parsed.description, verificationMethod));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return criteria;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Extract and create full AcceptanceCriteria object from issue body
|
|
137
|
+
*
|
|
138
|
+
* This is the main entry point for the /spec skill to use.
|
|
139
|
+
*
|
|
140
|
+
* @param issueBody - The full GitHub issue body markdown
|
|
141
|
+
* @returns Complete AcceptanceCriteria object with items and summary
|
|
142
|
+
*/
|
|
143
|
+
export function extractAcceptanceCriteria(issueBody) {
|
|
144
|
+
const items = parseAcceptanceCriteria(issueBody);
|
|
145
|
+
return createAcceptanceCriteria(items);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check if an issue body contains acceptance criteria
|
|
149
|
+
*
|
|
150
|
+
* @param issueBody - The full GitHub issue body markdown
|
|
151
|
+
* @returns True if AC items are found
|
|
152
|
+
*/
|
|
153
|
+
export function hasAcceptanceCriteria(issueBody) {
|
|
154
|
+
const items = parseAcceptanceCriteria(issueBody);
|
|
155
|
+
return items.length > 0;
|
|
156
|
+
}
|
package/dist/src/lib/fs.d.ts
CHANGED
|
@@ -7,3 +7,22 @@ export declare function ensureDir(path: string): Promise<void>;
|
|
|
7
7
|
export declare function readFile(path: string): Promise<string>;
|
|
8
8
|
export declare function writeFile(path: string, content: string): Promise<void>;
|
|
9
9
|
export declare function getFileStats(path: string): Promise<import("fs").Stats>;
|
|
10
|
+
/**
|
|
11
|
+
* Check if a path is a symbolic link
|
|
12
|
+
*/
|
|
13
|
+
export declare function isSymlink(path: string): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Get the target of a symbolic link
|
|
16
|
+
*/
|
|
17
|
+
export declare function getSymlinkTarget(path: string): Promise<string | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Remove a file or symbolic link safely
|
|
20
|
+
*/
|
|
21
|
+
export declare function removeFileOrSymlink(path: string): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Create a symbolic link with cross-platform handling
|
|
24
|
+
* @param target The path the symlink should point to
|
|
25
|
+
* @param path The path where the symlink will be created
|
|
26
|
+
* @returns true if symlink was created, false if fallback to copy is needed
|
|
27
|
+
*/
|
|
28
|
+
export declare function createSymlink(target: string, path: string): Promise<boolean>;
|
package/dist/src/lib/fs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File system utilities
|
|
3
3
|
*/
|
|
4
|
-
import { access, constants, mkdir, readFile as fsReadFile, writeFile as fsWriteFile, stat, } from "fs/promises";
|
|
4
|
+
import { access, constants, mkdir, readFile as fsReadFile, writeFile as fsWriteFile, stat, lstat, symlink, unlink, readlink, } from "fs/promises";
|
|
5
5
|
import { dirname } from "path";
|
|
6
6
|
export async function fileExists(path) {
|
|
7
7
|
try {
|
|
@@ -41,3 +41,60 @@ export async function writeFile(path, content) {
|
|
|
41
41
|
export async function getFileStats(path) {
|
|
42
42
|
return stat(path);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if a path is a symbolic link
|
|
46
|
+
*/
|
|
47
|
+
export async function isSymlink(path) {
|
|
48
|
+
try {
|
|
49
|
+
const stats = await lstat(path);
|
|
50
|
+
return stats.isSymbolicLink();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the target of a symbolic link
|
|
58
|
+
*/
|
|
59
|
+
export async function getSymlinkTarget(path) {
|
|
60
|
+
try {
|
|
61
|
+
return await readlink(path);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Remove a file or symbolic link safely
|
|
69
|
+
*/
|
|
70
|
+
export async function removeFileOrSymlink(path) {
|
|
71
|
+
try {
|
|
72
|
+
await unlink(path);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a symbolic link with cross-platform handling
|
|
81
|
+
* @param target The path the symlink should point to
|
|
82
|
+
* @param path The path where the symlink will be created
|
|
83
|
+
* @returns true if symlink was created, false if fallback to copy is needed
|
|
84
|
+
*/
|
|
85
|
+
export async function createSymlink(target, path) {
|
|
86
|
+
try {
|
|
87
|
+
await symlink(target, path);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const err = error;
|
|
92
|
+
// On Windows, symlinks may require admin privileges
|
|
93
|
+
// EPERM: Operation not permitted (Windows without privileges)
|
|
94
|
+
// Return false to signal that caller should fall back to copy
|
|
95
|
+
if (err.code === "EPERM" || err.code === "EACCES") {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -73,6 +73,13 @@ export interface RunSettings {
|
|
|
73
73
|
* Example: "feature/dashboard" for feature integration branches
|
|
74
74
|
*/
|
|
75
75
|
defaultBase?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Enable MCP servers in headless mode.
|
|
78
|
+
* When true, reads MCP config from Claude Desktop and passes to SDK.
|
|
79
|
+
* When false or --no-mcp flag is used, MCPs are disabled.
|
|
80
|
+
* Default: true
|
|
81
|
+
*/
|
|
82
|
+
mcp: boolean;
|
|
76
83
|
}
|
|
77
84
|
/**
|
|
78
85
|
* Full settings schema
|
package/dist/src/lib/settings.js
CHANGED
package/dist/src/lib/system.d.ts
CHANGED
|
@@ -42,6 +42,25 @@ export declare function getClaudeConfigPath(): string;
|
|
|
42
42
|
* Read configured MCP servers from Claude Desktop config
|
|
43
43
|
*/
|
|
44
44
|
export declare function getConfiguredMcpServers(): string[];
|
|
45
|
+
/**
|
|
46
|
+
* MCP server configuration type (matches Claude Desktop config format)
|
|
47
|
+
* This is the format expected by the Claude Agent SDK mcpServers option
|
|
48
|
+
*/
|
|
49
|
+
export interface McpServerConfig {
|
|
50
|
+
command: string;
|
|
51
|
+
args?: string[];
|
|
52
|
+
env?: Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get full MCP server configurations from Claude Desktop config
|
|
56
|
+
*
|
|
57
|
+
* Returns the complete mcpServers object suitable for passing to the
|
|
58
|
+
* Claude Agent SDK query() options. Returns undefined if config doesn't
|
|
59
|
+
* exist or is invalid (graceful degradation for AC-3).
|
|
60
|
+
*
|
|
61
|
+
* @returns MCP server configurations or undefined
|
|
62
|
+
*/
|
|
63
|
+
export declare function getMcpServersConfig(): Record<string, McpServerConfig> | undefined;
|
|
45
64
|
/**
|
|
46
65
|
* Check which optional MCP servers are configured
|
|
47
66
|
* Returns an object with server names as keys and configured status as values
|
package/dist/src/lib/system.js
CHANGED
|
@@ -137,6 +137,32 @@ export function getConfiguredMcpServers() {
|
|
|
137
137
|
return [];
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Get full MCP server configurations from Claude Desktop config
|
|
142
|
+
*
|
|
143
|
+
* Returns the complete mcpServers object suitable for passing to the
|
|
144
|
+
* Claude Agent SDK query() options. Returns undefined if config doesn't
|
|
145
|
+
* exist or is invalid (graceful degradation for AC-3).
|
|
146
|
+
*
|
|
147
|
+
* @returns MCP server configurations or undefined
|
|
148
|
+
*/
|
|
149
|
+
export function getMcpServersConfig() {
|
|
150
|
+
const configPath = getClaudeConfigPath();
|
|
151
|
+
try {
|
|
152
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
153
|
+
const config = JSON.parse(content);
|
|
154
|
+
const servers = config.mcpServers;
|
|
155
|
+
// Return undefined if no mcpServers section exists
|
|
156
|
+
if (!servers || typeof servers !== "object") {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
return servers;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Config file doesn't exist or is invalid - graceful degradation
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
140
166
|
/**
|
|
141
167
|
* Check which optional MCP servers are configured
|
|
142
168
|
* Returns an object with server names as keys and configured status as values
|
|
@@ -13,7 +13,40 @@ export declare function listTemplateFiles(): Promise<string[]>;
|
|
|
13
13
|
* Get content of a template file
|
|
14
14
|
*/
|
|
15
15
|
export declare function getTemplateContent(templatePath: string): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Result of symlink creation attempt
|
|
18
|
+
*/
|
|
19
|
+
export interface SymlinkResult {
|
|
20
|
+
created: boolean;
|
|
21
|
+
path: string;
|
|
22
|
+
target: string;
|
|
23
|
+
fallbackToCopy: boolean;
|
|
24
|
+
skipped: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for copyTemplates
|
|
29
|
+
*/
|
|
30
|
+
export interface CopyTemplatesOptions {
|
|
31
|
+
/** Use copies instead of symlinks for scripts (Windows default or user preference) */
|
|
32
|
+
noSymlinks?: boolean;
|
|
33
|
+
/** Force replacement of existing files/symlinks */
|
|
34
|
+
force?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create symlinks for files in a directory, with fallback to copy
|
|
38
|
+
* @param srcDir Source directory containing template files
|
|
39
|
+
* @param destDir Destination directory for symlinks
|
|
40
|
+
* @param options Options controlling symlink behavior
|
|
41
|
+
* @returns Array of results for each file
|
|
42
|
+
*/
|
|
43
|
+
export declare function symlinkDir(srcDir: string, destDir: string, options?: {
|
|
44
|
+
force?: boolean;
|
|
45
|
+
}): Promise<SymlinkResult[]>;
|
|
16
46
|
/**
|
|
17
47
|
* Copy all templates to .claude/ directory
|
|
18
48
|
*/
|
|
19
|
-
export declare function copyTemplates(stack: string, tokens?: Record<string, string
|
|
49
|
+
export declare function copyTemplates(stack: string, tokens?: Record<string, string>, options?: CopyTemplatesOptions): Promise<{
|
|
50
|
+
scriptsSymlinked: boolean;
|
|
51
|
+
symlinkResults?: SymlinkResult[];
|
|
52
|
+
}>;
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Template management - copy and process templates
|
|
3
3
|
*/
|
|
4
4
|
import { readdir, chmod } from "fs/promises";
|
|
5
|
-
import { join, dirname, basename } from "path";
|
|
5
|
+
import { join, dirname, basename, relative, isAbsolute } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { readFile, writeFile, ensureDir, fileExists } from "./fs.js";
|
|
7
|
+
import { readFile, writeFile, ensureDir, fileExists, isSymlink, createSymlink, removeFileOrSymlink, } from "./fs.js";
|
|
8
8
|
import { getStackConfig } from "./stacks.js";
|
|
9
|
+
import { isNativeWindows } from "./system.js";
|
|
9
10
|
// Get the package templates directory
|
|
10
11
|
function getTemplatesDir() {
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -60,10 +61,100 @@ export async function getTemplateContent(templatePath) {
|
|
|
60
61
|
const fullPath = join(templatesDir, relativePath);
|
|
61
62
|
return readFile(fullPath);
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Create symlinks for files in a directory, with fallback to copy
|
|
66
|
+
* @param srcDir Source directory containing template files
|
|
67
|
+
* @param destDir Destination directory for symlinks
|
|
68
|
+
* @param options Options controlling symlink behavior
|
|
69
|
+
* @returns Array of results for each file
|
|
70
|
+
*/
|
|
71
|
+
export async function symlinkDir(srcDir, destDir, options = {}) {
|
|
72
|
+
const results = [];
|
|
73
|
+
try {
|
|
74
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
75
|
+
await ensureDir(destDir);
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
// Recursively handle subdirectories
|
|
79
|
+
const subResults = await symlinkDir(join(srcDir, entry.name), join(destDir, entry.name), options);
|
|
80
|
+
results.push(...subResults);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const srcPath = join(srcDir, entry.name);
|
|
84
|
+
const destPath = join(destDir, entry.name);
|
|
85
|
+
// Calculate relative path from destDir to srcPath for portable symlinks
|
|
86
|
+
// Note: srcPath may already be absolute (when srcDir is absolute), so check first
|
|
87
|
+
const absoluteDest = isAbsolute(destPath)
|
|
88
|
+
? destPath
|
|
89
|
+
: join(process.cwd(), destPath);
|
|
90
|
+
const absoluteSrc = isAbsolute(srcPath)
|
|
91
|
+
? srcPath
|
|
92
|
+
: join(process.cwd(), srcPath);
|
|
93
|
+
const relativeTarget = relative(dirname(absoluteDest), absoluteSrc);
|
|
94
|
+
// Check if destination already exists
|
|
95
|
+
// Note: isSymlink uses lstat and works on broken symlinks,
|
|
96
|
+
// while fileExists uses access which fails on broken symlinks
|
|
97
|
+
const destIsSymlink = await isSymlink(destPath);
|
|
98
|
+
const destExists = destIsSymlink || (await fileExists(destPath));
|
|
99
|
+
if (destExists && !destIsSymlink && !options.force) {
|
|
100
|
+
// Regular file exists and force not specified - skip
|
|
101
|
+
results.push({
|
|
102
|
+
created: false,
|
|
103
|
+
path: destPath,
|
|
104
|
+
target: relativeTarget,
|
|
105
|
+
fallbackToCopy: false,
|
|
106
|
+
skipped: true,
|
|
107
|
+
reason: "existing file (use --force to replace)",
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Remove existing file/symlink if force or if it's already a symlink
|
|
112
|
+
// (symlinks are always replaced to ensure they point to correct target)
|
|
113
|
+
if (destExists && (options.force || destIsSymlink)) {
|
|
114
|
+
await removeFileOrSymlink(destPath);
|
|
115
|
+
}
|
|
116
|
+
// Try to create symlink
|
|
117
|
+
const symlinkCreated = await createSymlink(relativeTarget, destPath);
|
|
118
|
+
if (symlinkCreated) {
|
|
119
|
+
results.push({
|
|
120
|
+
created: true,
|
|
121
|
+
path: destPath,
|
|
122
|
+
target: relativeTarget,
|
|
123
|
+
fallbackToCopy: false,
|
|
124
|
+
skipped: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Symlink failed (likely Windows without privileges) - fall back to copy
|
|
129
|
+
const content = await readFile(srcPath);
|
|
130
|
+
await writeFile(destPath, content);
|
|
131
|
+
// Make shell scripts executable
|
|
132
|
+
if (entry.name.endsWith(".sh")) {
|
|
133
|
+
await chmod(destPath, 0o755);
|
|
134
|
+
}
|
|
135
|
+
results.push({
|
|
136
|
+
created: true,
|
|
137
|
+
path: destPath,
|
|
138
|
+
target: relativeTarget,
|
|
139
|
+
fallbackToCopy: true,
|
|
140
|
+
skipped: false,
|
|
141
|
+
reason: "symlink not supported, copied instead",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
// Skip if source doesn't exist
|
|
148
|
+
if (error.code !== "ENOENT") {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
63
154
|
/**
|
|
64
155
|
* Copy all templates to .claude/ directory
|
|
65
156
|
*/
|
|
66
|
-
export async function copyTemplates(stack, tokens) {
|
|
157
|
+
export async function copyTemplates(stack, tokens, options = {}) {
|
|
67
158
|
const templatesDir = getTemplatesDir();
|
|
68
159
|
const stackConfig = getStackConfig(stack);
|
|
69
160
|
const variables = {
|
|
@@ -107,12 +198,25 @@ export async function copyTemplates(stack, tokens) {
|
|
|
107
198
|
await copyDir(join(templatesDir, "hooks"), ".claude/hooks");
|
|
108
199
|
// Copy memory (constitution, etc.)
|
|
109
200
|
await copyDir(join(templatesDir, "memory"), ".claude/memory");
|
|
110
|
-
//
|
|
111
|
-
|
|
201
|
+
// Handle scripts directory - use symlinks unless disabled
|
|
202
|
+
const useSymlinks = !options.noSymlinks && !isNativeWindows();
|
|
203
|
+
let scriptsSymlinked = false;
|
|
204
|
+
let symlinkResults;
|
|
205
|
+
if (useSymlinks) {
|
|
206
|
+
// Use symlinks for scripts - they don't need template variable processing
|
|
207
|
+
symlinkResults = await symlinkDir(join(templatesDir, "scripts"), "scripts/dev", { force: options.force });
|
|
208
|
+
// Check if any symlinks were actually created (not all fell back to copy)
|
|
209
|
+
scriptsSymlinked = symlinkResults.some((r) => r.created && !r.fallbackToCopy);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Fall back to copies (Windows or --no-symlinks flag)
|
|
213
|
+
await copyDir(join(templatesDir, "scripts"), "scripts/dev");
|
|
214
|
+
}
|
|
112
215
|
// Copy settings.json
|
|
113
216
|
const settingsPath = join(templatesDir, "settings.json");
|
|
114
217
|
if (await fileExists(settingsPath)) {
|
|
115
218
|
const content = await readFile(settingsPath);
|
|
116
219
|
await writeFile(".claude/settings.json", processTemplate(content, variables));
|
|
117
220
|
}
|
|
221
|
+
return { scriptsSymlinked, symlinkResults };
|
|
118
222
|
}
|