specweave 0.17.19 → 0.18.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/dist/plugins/specweave-ado/lib/ado-spec-content-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js +6 -65
- package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +6 -8
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +117 -78
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -1
- package/dist/src/core/types/config.d.ts +0 -20
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js +0 -7
- package/dist/src/core/types/config.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/hooks.json +0 -4
- package/plugins/specweave/hooks/lib/sync-spec-content.sh +2 -2
- package/plugins/specweave-ado/commands/specweave-ado-sync-spec.md +1 -1
- package/plugins/specweave-ado/lib/ado-spec-content-sync.js +5 -49
- package/plugins/specweave-ado/lib/ado-spec-content-sync.ts +6 -72
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +1 -1
- package/plugins/specweave-github/commands/specweave-github-sync-epic.md +1 -1
- package/plugins/specweave-github/commands/specweave-github-sync-spec.md +1 -1
- package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +1 -1
- package/plugins/specweave-jira/commands/specweave-jira-sync-spec.md +1 -1
- package/plugins/specweave-jira/lib/{enhanced-jira-sync.ts → enhanced-jira-sync.ts.disabled} +52 -26
- package/plugins/specweave-release/commands/specweave-release-platform.md +1 -1
- package/plugins/specweave-release/hooks/post-task-completion.sh +2 -2
- package/dist/src/core/deduplication/command-deduplicator.d.ts +0 -166
- package/dist/src/core/deduplication/command-deduplicator.d.ts.map +0 -1
- package/dist/src/core/deduplication/command-deduplicator.js +0 -254
- package/dist/src/core/deduplication/command-deduplicator.js.map +0 -1
- package/plugins/specweave/hooks/pre-command-deduplication.sh +0 -86
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +0 -134
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global Command Deduplication System
|
|
3
|
-
*
|
|
4
|
-
* Prevents ANY command/tool from being invoked twice within a configurable time window.
|
|
5
|
-
* Tracks all SlashCommand, Write, Edit, and other tool invocations.
|
|
6
|
-
*
|
|
7
|
-
* Architecture:
|
|
8
|
-
* - File-based cache: `.specweave/state/command-invocations.json`
|
|
9
|
-
* - Hash-based deduplication: command + args → unique fingerprint
|
|
10
|
-
* - Time-windowed checks: Configurable window (default: 1000ms)
|
|
11
|
-
* - Automatic cleanup: Removes old entries to prevent bloat
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* ```typescript
|
|
15
|
-
* import { CommandDeduplicator } from './command-deduplicator.js';
|
|
16
|
-
*
|
|
17
|
-
* const dedup = new CommandDeduplicator();
|
|
18
|
-
* const isDuplicate = await dedup.checkDuplicate('/specweave:do', ['0031']);
|
|
19
|
-
*
|
|
20
|
-
* if (isDuplicate) {
|
|
21
|
-
* console.log('⚠️ Duplicate invocation blocked!');
|
|
22
|
-
* return;
|
|
23
|
-
* }
|
|
24
|
-
*
|
|
25
|
-
* await dedup.recordInvocation('/specweave:do', ['0031']);
|
|
26
|
-
* // ... execute command
|
|
27
|
-
* ```
|
|
28
|
-
*
|
|
29
|
-
* @module core/deduplication
|
|
30
|
-
*/
|
|
31
|
-
import * as fs from 'fs-extra';
|
|
32
|
-
import * as path from 'path';
|
|
33
|
-
import * as crypto from 'crypto';
|
|
34
|
-
/**
|
|
35
|
-
* Global command deduplication system
|
|
36
|
-
*/
|
|
37
|
-
export class CommandDeduplicator {
|
|
38
|
-
/**
|
|
39
|
-
* Create new deduplicator instance
|
|
40
|
-
*
|
|
41
|
-
* @param config - Configuration options
|
|
42
|
-
* @param projectRoot - Project root directory (default: process.cwd())
|
|
43
|
-
*/
|
|
44
|
-
constructor(config = {}, projectRoot = process.cwd()) {
|
|
45
|
-
this.projectRoot = projectRoot;
|
|
46
|
-
this.lastCleanupCheck = 0;
|
|
47
|
-
this.config = {
|
|
48
|
-
windowMs: config.windowMs ?? 1000,
|
|
49
|
-
cachePath: config.cachePath ?? path.join(projectRoot, '.specweave', 'state', 'command-invocations.json'),
|
|
50
|
-
maxCacheSize: config.maxCacheSize ?? 1000,
|
|
51
|
-
debug: config.debug ?? false,
|
|
52
|
-
cleanupIntervalMs: config.cleanupIntervalMs ?? 60000
|
|
53
|
-
};
|
|
54
|
-
this.cache = this.loadCache();
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Check if command invocation is a duplicate
|
|
58
|
-
*
|
|
59
|
-
* @param command - Command name (e.g., '/specweave:do')
|
|
60
|
-
* @param args - Command arguments
|
|
61
|
-
* @returns true if duplicate detected, false otherwise
|
|
62
|
-
*/
|
|
63
|
-
async checkDuplicate(command, args = []) {
|
|
64
|
-
const fingerprint = this.createFingerprint(command, args);
|
|
65
|
-
const now = Date.now();
|
|
66
|
-
const windowStart = now - this.config.windowMs;
|
|
67
|
-
// Check for recent invocations with same fingerprint
|
|
68
|
-
const recentDuplicates = this.cache.invocations.filter(inv => inv.fingerprint === fingerprint &&
|
|
69
|
-
inv.timestamp >= windowStart);
|
|
70
|
-
if (recentDuplicates.length > 0) {
|
|
71
|
-
const mostRecent = recentDuplicates[recentDuplicates.length - 1];
|
|
72
|
-
const timeSince = now - mostRecent.timestamp;
|
|
73
|
-
if (this.config.debug) {
|
|
74
|
-
console.log(`[CommandDeduplicator] 🚫 DUPLICATE DETECTED!`);
|
|
75
|
-
console.log(` Command: ${command}`);
|
|
76
|
-
console.log(` Args: ${JSON.stringify(args)}`);
|
|
77
|
-
console.log(` Time since last: ${timeSince}ms`);
|
|
78
|
-
console.log(` Window: ${this.config.windowMs}ms`);
|
|
79
|
-
}
|
|
80
|
-
// Increment duplicate counter
|
|
81
|
-
this.cache.totalDuplicatesBlocked++;
|
|
82
|
-
await this.saveCache();
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Record command invocation
|
|
89
|
-
*
|
|
90
|
-
* @param command - Command name
|
|
91
|
-
* @param args - Command arguments
|
|
92
|
-
*/
|
|
93
|
-
async recordInvocation(command, args = []) {
|
|
94
|
-
const fingerprint = this.createFingerprint(command, args);
|
|
95
|
-
const now = Date.now();
|
|
96
|
-
const record = {
|
|
97
|
-
fingerprint,
|
|
98
|
-
command,
|
|
99
|
-
args,
|
|
100
|
-
timestamp: now,
|
|
101
|
-
date: new Date(now).toISOString()
|
|
102
|
-
};
|
|
103
|
-
this.cache.invocations.push(record);
|
|
104
|
-
this.cache.totalInvocations++;
|
|
105
|
-
if (this.config.debug) {
|
|
106
|
-
console.log(`[CommandDeduplicator] ✅ Recorded invocation: ${command} ${args.join(' ')}`);
|
|
107
|
-
}
|
|
108
|
-
// Trigger cleanup if needed
|
|
109
|
-
if (now - this.lastCleanupCheck > this.config.cleanupIntervalMs) {
|
|
110
|
-
await this.cleanup();
|
|
111
|
-
this.lastCleanupCheck = now;
|
|
112
|
-
}
|
|
113
|
-
await this.saveCache();
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Create unique fingerprint for command + args
|
|
117
|
-
*
|
|
118
|
-
* @param command - Command name
|
|
119
|
-
* @param args - Command arguments
|
|
120
|
-
* @returns SHA256 hash of command + args
|
|
121
|
-
*/
|
|
122
|
-
createFingerprint(command, args) {
|
|
123
|
-
const data = JSON.stringify({ command, args });
|
|
124
|
-
return crypto.createHash('sha256').update(data).digest('hex');
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Load cache from disk
|
|
128
|
-
*
|
|
129
|
-
* @returns Invocation cache
|
|
130
|
-
*/
|
|
131
|
-
loadCache() {
|
|
132
|
-
try {
|
|
133
|
-
if (fs.existsSync(this.config.cachePath)) {
|
|
134
|
-
const data = fs.readJsonSync(this.config.cachePath);
|
|
135
|
-
if (this.config.debug) {
|
|
136
|
-
console.log(`[CommandDeduplicator] 📂 Loaded cache: ${data.invocations.length} invocations`);
|
|
137
|
-
}
|
|
138
|
-
return data;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
if (this.config.debug) {
|
|
143
|
-
console.log(`[CommandDeduplicator] ⚠️ Failed to load cache: ${error}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
// Return empty cache
|
|
147
|
-
return {
|
|
148
|
-
invocations: [],
|
|
149
|
-
lastCleanup: Date.now(),
|
|
150
|
-
totalInvocations: 0,
|
|
151
|
-
totalDuplicatesBlocked: 0
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Save cache to disk
|
|
156
|
-
*/
|
|
157
|
-
async saveCache() {
|
|
158
|
-
try {
|
|
159
|
-
await fs.ensureDir(path.dirname(this.config.cachePath));
|
|
160
|
-
await fs.writeJson(this.config.cachePath, this.cache, { spaces: 2 });
|
|
161
|
-
if (this.config.debug) {
|
|
162
|
-
console.log(`[CommandDeduplicator] 💾 Saved cache: ${this.cache.invocations.length} invocations`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
console.error(`[CommandDeduplicator] ❌ Failed to save cache: ${error}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Clean up old invocation records
|
|
171
|
-
*
|
|
172
|
-
* Removes records older than 10x the deduplication window to prevent cache bloat.
|
|
173
|
-
*/
|
|
174
|
-
async cleanup() {
|
|
175
|
-
const now = Date.now();
|
|
176
|
-
const cutoff = now - (this.config.windowMs * 10); // Keep 10x window
|
|
177
|
-
const before = this.cache.invocations.length;
|
|
178
|
-
this.cache.invocations = this.cache.invocations.filter(inv => inv.timestamp >= cutoff);
|
|
179
|
-
const after = this.cache.invocations.length;
|
|
180
|
-
// Also enforce max cache size
|
|
181
|
-
if (this.cache.invocations.length > this.config.maxCacheSize) {
|
|
182
|
-
this.cache.invocations = this.cache.invocations.slice(-this.config.maxCacheSize);
|
|
183
|
-
}
|
|
184
|
-
this.cache.lastCleanup = now;
|
|
185
|
-
if (this.config.debug && before > after) {
|
|
186
|
-
console.log(`[CommandDeduplicator] 🧹 Cleanup: Removed ${before - after} old records`);
|
|
187
|
-
}
|
|
188
|
-
await this.saveCache();
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Get statistics about deduplication
|
|
192
|
-
*
|
|
193
|
-
* @returns Statistics object
|
|
194
|
-
*/
|
|
195
|
-
getStats() {
|
|
196
|
-
return {
|
|
197
|
-
totalInvocations: this.cache.totalInvocations,
|
|
198
|
-
totalDuplicatesBlocked: this.cache.totalDuplicatesBlocked,
|
|
199
|
-
currentCacheSize: this.cache.invocations.length,
|
|
200
|
-
lastCleanup: new Date(this.cache.lastCleanup).toISOString()
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Clear all cached invocations (useful for testing)
|
|
205
|
-
*/
|
|
206
|
-
async clear() {
|
|
207
|
-
this.cache = {
|
|
208
|
-
invocations: [],
|
|
209
|
-
lastCleanup: Date.now(),
|
|
210
|
-
totalInvocations: 0,
|
|
211
|
-
totalDuplicatesBlocked: 0
|
|
212
|
-
};
|
|
213
|
-
await this.saveCache();
|
|
214
|
-
if (this.config.debug) {
|
|
215
|
-
console.log(`[CommandDeduplicator] 🗑️ Cache cleared`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Global singleton instance (convenience)
|
|
221
|
-
*/
|
|
222
|
-
let globalInstance = null;
|
|
223
|
-
/**
|
|
224
|
-
* Get global deduplicator instance
|
|
225
|
-
*
|
|
226
|
-
* @param config - Configuration (only used on first call)
|
|
227
|
-
* @returns Global deduplicator instance
|
|
228
|
-
*/
|
|
229
|
-
export function getGlobalDeduplicator(config) {
|
|
230
|
-
if (!globalInstance) {
|
|
231
|
-
globalInstance = new CommandDeduplicator(config);
|
|
232
|
-
}
|
|
233
|
-
return globalInstance;
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Convenience function: Check if command is duplicate
|
|
237
|
-
*
|
|
238
|
-
* @param command - Command name
|
|
239
|
-
* @param args - Command arguments
|
|
240
|
-
* @returns true if duplicate, false otherwise
|
|
241
|
-
*/
|
|
242
|
-
export async function isDuplicate(command, args = []) {
|
|
243
|
-
return await getGlobalDeduplicator().checkDuplicate(command, args);
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Convenience function: Record command invocation
|
|
247
|
-
*
|
|
248
|
-
* @param command - Command name
|
|
249
|
-
* @param args - Command arguments
|
|
250
|
-
*/
|
|
251
|
-
export async function recordCommand(command, args = []) {
|
|
252
|
-
await getGlobalDeduplicator().recordInvocation(command, args);
|
|
253
|
-
}
|
|
254
|
-
//# sourceMappingURL=command-deduplicator.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"command-deduplicator.js","sourceRoot":"","sources":["../../../../src/core/deduplication/command-deduplicator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# SpecWeave Pre-Command Deduplication Hook
|
|
4
|
-
# Fires BEFORE any command executes (UserPromptSubmit hook)
|
|
5
|
-
# Purpose: Prevent duplicate command invocations within configurable time window
|
|
6
|
-
|
|
7
|
-
set -euo pipefail
|
|
8
|
-
|
|
9
|
-
# Read input JSON from stdin
|
|
10
|
-
INPUT=$(cat)
|
|
11
|
-
|
|
12
|
-
# Extract prompt from JSON
|
|
13
|
-
PROMPT=$(echo "$INPUT" | node -e "
|
|
14
|
-
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
|
15
|
-
console.log(input.prompt || '');
|
|
16
|
-
")
|
|
17
|
-
|
|
18
|
-
# ==============================================================================
|
|
19
|
-
# DEDUPLICATION CHECK: Block duplicate commands within 1 second
|
|
20
|
-
# ==============================================================================
|
|
21
|
-
|
|
22
|
-
# Extract command name from prompt (if slash command)
|
|
23
|
-
COMMAND=$(echo "$PROMPT" | grep -oE "^/[a-z0-9:-]+" | head -1 || echo "")
|
|
24
|
-
|
|
25
|
-
if [[ -n "$COMMAND" ]]; then
|
|
26
|
-
# Check deduplication using TypeScript module
|
|
27
|
-
if command -v node >/dev/null 2>&1 && [[ -f "dist/src/core/deduplication/command-deduplicator.js" ]]; then
|
|
28
|
-
# Run deduplication check
|
|
29
|
-
DEDUP_RESULT=$(node -e "
|
|
30
|
-
(async () => {
|
|
31
|
-
try {
|
|
32
|
-
const { CommandDeduplicator } = require('./dist/src/core/deduplication/command-deduplicator.js');
|
|
33
|
-
const dedup = new CommandDeduplicator({ debug: false });
|
|
34
|
-
|
|
35
|
-
// Parse command and args
|
|
36
|
-
const fullCommand = '${COMMAND}';
|
|
37
|
-
const args = '${PROMPT}'.replace(fullCommand, '').trim().split(/\\s+/).filter(Boolean);
|
|
38
|
-
|
|
39
|
-
// Check for duplicate
|
|
40
|
-
const isDuplicate = await dedup.checkDuplicate(fullCommand, args);
|
|
41
|
-
|
|
42
|
-
if (isDuplicate) {
|
|
43
|
-
const stats = dedup.getStats();
|
|
44
|
-
console.log('DUPLICATE');
|
|
45
|
-
console.log(JSON.stringify(stats));
|
|
46
|
-
} else {
|
|
47
|
-
// Record invocation
|
|
48
|
-
await dedup.recordInvocation(fullCommand, args);
|
|
49
|
-
console.log('OK');
|
|
50
|
-
}
|
|
51
|
-
} catch (e) {
|
|
52
|
-
console.error('Error in deduplication:', e.message);
|
|
53
|
-
console.log('OK'); // Fail-open (don't block on errors)
|
|
54
|
-
}
|
|
55
|
-
})();
|
|
56
|
-
" 2>/dev/null || echo "OK")
|
|
57
|
-
|
|
58
|
-
# Parse result
|
|
59
|
-
STATUS=$(echo "$DEDUP_RESULT" | head -1)
|
|
60
|
-
|
|
61
|
-
if [[ "$STATUS" == "DUPLICATE" ]]; then
|
|
62
|
-
# Get stats
|
|
63
|
-
STATS=$(echo "$DEDUP_RESULT" | tail -1)
|
|
64
|
-
|
|
65
|
-
cat <<EOF
|
|
66
|
-
{
|
|
67
|
-
"decision": "block",
|
|
68
|
-
"reason": "🚫 DUPLICATE COMMAND DETECTED\\n\\nCommand: \`$COMMAND\`\\nTime window: 1 second\\n\\nThis command was just executed! To prevent unintended duplicates, this invocation has been blocked.\\n\\n💡 If you meant to run this command again:\\n 1. Wait 1 second\\n 2. Run the command again\\n\\n📊 Deduplication Stats:\\n$STATS\\n\\n🔧 To adjust the time window, edit \`.specweave/config.json\`:\\n\`\`\`json\\n{\\n \\\"deduplication\\\": {\\n \\\"windowMs\\\": 2000 // Increase to 2 seconds\\n }\\n}\\n\`\`\`"
|
|
69
|
-
}
|
|
70
|
-
EOF
|
|
71
|
-
exit 0
|
|
72
|
-
fi
|
|
73
|
-
fi
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
# ==============================================================================
|
|
77
|
-
# PASS THROUGH: No duplicate detected, proceed with command
|
|
78
|
-
# ==============================================================================
|
|
79
|
-
|
|
80
|
-
cat <<EOF
|
|
81
|
-
{
|
|
82
|
-
"decision": "approve"
|
|
83
|
-
}
|
|
84
|
-
EOF
|
|
85
|
-
|
|
86
|
-
exit 0
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
|
|
2
|
-
import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
|
|
3
|
-
import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
import * as fs from "fs/promises";
|
|
6
|
-
async function syncSpecToJiraWithEnhancedContent(options) {
|
|
7
|
-
const { specPath, domain, project, dryRun = false, verbose = false } = options;
|
|
8
|
-
try {
|
|
9
|
-
const baseSpec = await parseSpecContent(specPath);
|
|
10
|
-
if (!baseSpec) {
|
|
11
|
-
return {
|
|
12
|
-
success: false,
|
|
13
|
-
action: "error",
|
|
14
|
-
error: "Failed to parse spec content"
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
if (verbose) {
|
|
18
|
-
console.log(`\u{1F4C4} Parsed spec: ${baseSpec.identifier.compact}`);
|
|
19
|
-
}
|
|
20
|
-
const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
|
|
21
|
-
const rootDir = await findSpecWeaveRoot(specPath);
|
|
22
|
-
const mapper = new SpecIncrementMapper(rootDir);
|
|
23
|
-
const mapping = await mapper.mapSpecToIncrements(specId);
|
|
24
|
-
if (verbose) {
|
|
25
|
-
console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
|
|
26
|
-
}
|
|
27
|
-
const taskMapping = buildTaskMapping(mapping.increments);
|
|
28
|
-
const architectureDocs = await findArchitectureDocs(rootDir, specId);
|
|
29
|
-
const enhancedSpec = {
|
|
30
|
-
...baseSpec,
|
|
31
|
-
summary: baseSpec.description,
|
|
32
|
-
taskMapping,
|
|
33
|
-
architectureDocs
|
|
34
|
-
};
|
|
35
|
-
const builder = new EnhancedContentBuilder();
|
|
36
|
-
const description = builder.buildExternalDescription(enhancedSpec);
|
|
37
|
-
if (verbose) {
|
|
38
|
-
console.log(`\u{1F4DD} Generated description: ${description.length} characters`);
|
|
39
|
-
}
|
|
40
|
-
if (dryRun) {
|
|
41
|
-
console.log("\u{1F50D} DRY RUN - Would create/update epic with:");
|
|
42
|
-
console.log(` Summary: ${baseSpec.title}`);
|
|
43
|
-
console.log(` Description length: ${description.length}`);
|
|
44
|
-
return {
|
|
45
|
-
success: true,
|
|
46
|
-
action: "no-change",
|
|
47
|
-
tasksLinked: taskMapping?.tasks.length || 0
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
if (!dryRun && (!domain || !project)) {
|
|
51
|
-
return {
|
|
52
|
-
success: false,
|
|
53
|
-
action: "error",
|
|
54
|
-
error: "JIRA domain/project not specified (required for actual sync)"
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const result = {
|
|
58
|
-
success: true,
|
|
59
|
-
action: dryRun ? "no-change" : "created",
|
|
60
|
-
// Assume create if not dry run
|
|
61
|
-
tasksLinked: taskMapping?.tasks.length || 0
|
|
62
|
-
};
|
|
63
|
-
if (domain && project && !dryRun) {
|
|
64
|
-
result.epicKey = `SPEC-001`;
|
|
65
|
-
result.epicUrl = `https://${domain}/browse/SPEC-001`;
|
|
66
|
-
if (verbose) {
|
|
67
|
-
console.log(`\u26A0\uFE0F JIRA API integration not implemented in this file`);
|
|
68
|
-
console.log(` Use jira-spec-sync.ts for actual JIRA synchronization`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return result;
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return {
|
|
74
|
-
success: false,
|
|
75
|
-
action: "error",
|
|
76
|
-
error: error.message
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function findSpecWeaveRoot(specPath) {
|
|
81
|
-
let currentDir = path.dirname(specPath);
|
|
82
|
-
while (true) {
|
|
83
|
-
const specweaveDir = path.join(currentDir, ".specweave");
|
|
84
|
-
try {
|
|
85
|
-
await fs.access(specweaveDir);
|
|
86
|
-
return currentDir;
|
|
87
|
-
} catch {
|
|
88
|
-
const parentDir = path.dirname(currentDir);
|
|
89
|
-
if (parentDir === currentDir) {
|
|
90
|
-
throw new Error(".specweave directory not found");
|
|
91
|
-
}
|
|
92
|
-
currentDir = parentDir;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
function buildTaskMapping(increments) {
|
|
97
|
-
if (increments.length === 0) return void 0;
|
|
98
|
-
const firstIncrement = increments[0];
|
|
99
|
-
const tasks = firstIncrement.tasks.map((task) => ({
|
|
100
|
-
id: task.id,
|
|
101
|
-
title: task.title,
|
|
102
|
-
userStories: task.userStories
|
|
103
|
-
}));
|
|
104
|
-
return {
|
|
105
|
-
incrementId: firstIncrement.id,
|
|
106
|
-
tasks,
|
|
107
|
-
tasksUrl: `tasks.md`
|
|
108
|
-
// JIRA doesn't support external links in same way
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
async function findArchitectureDocs(rootDir, specId) {
|
|
112
|
-
const docs = [];
|
|
113
|
-
const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
|
|
114
|
-
try {
|
|
115
|
-
const adrDir = path.join(archDir, "adr");
|
|
116
|
-
try {
|
|
117
|
-
const adrs = await fs.readdir(adrDir);
|
|
118
|
-
const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
|
|
119
|
-
for (const adr of relatedAdrs) {
|
|
120
|
-
docs.push({
|
|
121
|
-
type: "adr",
|
|
122
|
-
path: path.join(adrDir, adr),
|
|
123
|
-
title: adr.replace(".md", "").replace(/-/g, " ")
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
} catch {
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
130
|
-
return docs;
|
|
131
|
-
}
|
|
132
|
-
export {
|
|
133
|
-
syncSpecToJiraWithEnhancedContent
|
|
134
|
-
};
|