@syntesseraai/opencode-feature-factory 0.2.21 → 0.2.23
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/bin/ff-deploy.js +35 -80
- package/dist/agent-context.d.ts +54 -0
- package/dist/agent-context.js +273 -0
- package/dist/discovery.d.ts +18 -0
- package/dist/discovery.js +189 -0
- package/dist/discovery.test.d.ts +10 -0
- package/dist/discovery.test.js +97 -0
- package/dist/feature-factory-setup.d.ts +9 -0
- package/dist/feature-factory-setup.js +151 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +18 -0
- package/dist/output.d.ts +17 -0
- package/dist/output.js +48 -0
- package/dist/output.test.d.ts +8 -0
- package/dist/output.test.js +205 -0
- package/dist/plugins/ff-agents-clear-plugin.d.ts +2 -0
- package/dist/plugins/ff-agents-clear-plugin.js +55 -0
- package/dist/plugins/ff-agents-current-plugin.d.ts +2 -0
- package/dist/plugins/ff-agents-current-plugin.js +49 -0
- package/dist/plugins/ff-agents-show-plugin.d.ts +2 -0
- package/dist/plugins/ff-agents-show-plugin.js +26 -0
- package/dist/quality-gate-config.d.ts +37 -0
- package/dist/quality-gate-config.js +84 -0
- package/dist/quality-gate-config.test.d.ts +9 -0
- package/dist/quality-gate-config.test.js +164 -0
- package/dist/stop-quality-gate.d.ts +16 -0
- package/dist/stop-quality-gate.js +396 -0
- package/dist/stop-quality-gate.test.d.ts +8 -0
- package/dist/stop-quality-gate.test.js +549 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +1 -0
- package/dist/uuid.d.ts +13 -0
- package/dist/uuid.js +22 -0
- package/package.json +1 -1
package/bin/ff-deploy.js
CHANGED
|
@@ -27,9 +27,27 @@ const SOURCE_AGENTS_DIR = join(PACKAGE_ROOT, 'agents');
|
|
|
27
27
|
// Check if running in interactive mode (has TTY)
|
|
28
28
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
const
|
|
32
|
-
|
|
30
|
+
// Default MCP configuration
|
|
31
|
+
const DEFAULT_MCP_SERVERS = {
|
|
32
|
+
'jina-ai': {
|
|
33
|
+
type: 'remote',
|
|
34
|
+
url: 'https://mcp.jina.ai/v1',
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: 'Bearer {env:JINAAI_API_KEY}',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
gh_grep: {
|
|
40
|
+
type: 'remote',
|
|
41
|
+
url: 'https://mcp.grep.app',
|
|
42
|
+
},
|
|
43
|
+
context7: {
|
|
44
|
+
type: 'remote',
|
|
45
|
+
url: 'https://mcp.context7.com/mcp',
|
|
46
|
+
headers: {
|
|
47
|
+
CONTEXT7_API_KEY: '{env:CONTEXT7_API_KEY}',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
33
51
|
|
|
34
52
|
async function ensureDir(dir) {
|
|
35
53
|
try {
|
|
@@ -94,92 +112,32 @@ async function updateMCPConfig() {
|
|
|
94
112
|
// No existing config, will create new
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
//
|
|
115
|
+
// Check which MCP servers need to be added
|
|
116
|
+
const existingMcp = existingConfig.mcp || {};
|
|
98
117
|
const serversToAdd = {};
|
|
99
118
|
let serversAdded = 0;
|
|
100
119
|
let serversSkipped = 0;
|
|
101
|
-
let serversMissingKey = 0;
|
|
102
|
-
|
|
103
|
-
// jina-ai: requires JINAAI_API_KEY
|
|
104
|
-
if (!existingConfig.mcp?.['jina-ai']) {
|
|
105
|
-
if (JINAAI_API_KEY) {
|
|
106
|
-
serversToAdd['jina-ai'] = {
|
|
107
|
-
type: 'remote',
|
|
108
|
-
url: 'https://mcp.jina.ai/v1',
|
|
109
|
-
headers: {
|
|
110
|
-
Authorization: 'Bearer {env:JINAAI_API_KEY}',
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
serversAdded++;
|
|
114
|
-
if (isInteractive) {
|
|
115
|
-
console.log(' ✅ jina-ai: added (JINAAI_API_KEY found)');
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
serversMissingKey++;
|
|
119
|
-
if (isInteractive) {
|
|
120
|
-
console.log(' ⏭️ jina-ai: skipped (JINAAI_API_KEY not set)');
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
} else {
|
|
124
|
-
serversSkipped++;
|
|
125
|
-
if (isInteractive) {
|
|
126
|
-
console.log(' ⏭️ jina-ai: already exists');
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
120
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
url: 'https://mcp.grep.app',
|
|
135
|
-
};
|
|
136
|
-
serversAdded++;
|
|
137
|
-
if (isInteractive) {
|
|
138
|
-
console.log(' ✅ gh_grep: added (no API key required)');
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
serversSkipped++;
|
|
142
|
-
if (isInteractive) {
|
|
143
|
-
console.log(' ⏭️ gh_grep: already exists');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// context7: requires CONTEXT7_API_KEY
|
|
148
|
-
if (!existingConfig.mcp?.['context7']) {
|
|
149
|
-
if (CONTEXT7_API_KEY) {
|
|
150
|
-
serversToAdd['context7'] = {
|
|
151
|
-
type: 'remote',
|
|
152
|
-
url: 'https://mcp.context7.com/mcp',
|
|
153
|
-
headers: {
|
|
154
|
-
CONTEXT7_API_KEY: '{env:CONTEXT7_API_KEY}',
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
serversAdded++;
|
|
121
|
+
for (const [serverName, serverConfig] of Object.entries(DEFAULT_MCP_SERVERS)) {
|
|
122
|
+
if (existingMcp[serverName]) {
|
|
123
|
+
// Server already exists, skip
|
|
124
|
+
serversSkipped++;
|
|
158
125
|
if (isInteractive) {
|
|
159
|
-
console.log(
|
|
126
|
+
console.log(` ⏭️ ${serverName}: already exists, skipping`);
|
|
160
127
|
}
|
|
161
128
|
} else {
|
|
162
|
-
|
|
129
|
+
// Server doesn't exist, add it
|
|
130
|
+
serversToAdd[serverName] = serverConfig;
|
|
131
|
+
serversAdded++;
|
|
163
132
|
if (isInteractive) {
|
|
164
|
-
console.log(
|
|
133
|
+
console.log(` ✅ ${serverName}: will be added`);
|
|
165
134
|
}
|
|
166
135
|
}
|
|
167
|
-
} else {
|
|
168
|
-
serversSkipped++;
|
|
169
|
-
if (isInteractive) {
|
|
170
|
-
console.log(' ⏭️ context7: already exists');
|
|
171
|
-
}
|
|
172
136
|
}
|
|
173
137
|
|
|
174
138
|
if (serversAdded === 0) {
|
|
175
139
|
if (isInteractive) {
|
|
176
|
-
|
|
177
|
-
console.log('\n⚠️ No new MCP servers added. Set API keys to enable:');
|
|
178
|
-
console.log(' - JINAAI_API_KEY for jina-ai');
|
|
179
|
-
console.log(' - CONTEXT7_API_KEY for context7');
|
|
180
|
-
} else {
|
|
181
|
-
console.log('\n✅ All MCP servers already configured.');
|
|
182
|
-
}
|
|
140
|
+
console.log('\n✅ All MCP servers already configured. No changes needed.');
|
|
183
141
|
}
|
|
184
142
|
return;
|
|
185
143
|
}
|
|
@@ -198,7 +156,7 @@ async function updateMCPConfig() {
|
|
|
198
156
|
const updatedConfig = {
|
|
199
157
|
...existingConfig,
|
|
200
158
|
mcp: {
|
|
201
|
-
...
|
|
159
|
+
...existingMcp,
|
|
202
160
|
...serversToAdd,
|
|
203
161
|
},
|
|
204
162
|
};
|
|
@@ -212,9 +170,6 @@ async function updateMCPConfig() {
|
|
|
212
170
|
if (serversSkipped > 0) {
|
|
213
171
|
console.log(` Skipped ${serversSkipped} existing server(s)`);
|
|
214
172
|
}
|
|
215
|
-
if (serversMissingKey > 0) {
|
|
216
|
-
console.log(` Skipped ${serversMissingKey} server(s) due to missing API keys`);
|
|
217
|
-
}
|
|
218
173
|
console.log('\n📝 Note: Restart OpenCode to load new MCP configuration');
|
|
219
174
|
}
|
|
220
175
|
} catch (error) {
|
|
@@ -293,7 +248,7 @@ async function deploy() {
|
|
|
293
248
|
console.log(' - Use @ff-research to investigate external topics');
|
|
294
249
|
}
|
|
295
250
|
|
|
296
|
-
// Always update MCP config (no flag needed)
|
|
251
|
+
// Always update MCP config (no flag needed, no prompts)
|
|
297
252
|
await updateMCPConfig();
|
|
298
253
|
} catch (error) {
|
|
299
254
|
if (isInteractive) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
+
export interface AgentContext {
|
|
3
|
+
/** Unique UUID for this agent instance */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Agent type (planning, building, reviewing, etc.) */
|
|
6
|
+
agent: string;
|
|
7
|
+
/** Task title */
|
|
8
|
+
title: string;
|
|
9
|
+
/** Task description */
|
|
10
|
+
description: string;
|
|
11
|
+
/** Working directory/folder */
|
|
12
|
+
folder: string;
|
|
13
|
+
/** Current status */
|
|
14
|
+
status: 'in-progress' | 'completed' | 'delegated' | 'failed';
|
|
15
|
+
/** ISO timestamp when agent started */
|
|
16
|
+
started: string;
|
|
17
|
+
/** OpenCode session ID */
|
|
18
|
+
session: string;
|
|
19
|
+
/** Parent agent UUID (if delegated) */
|
|
20
|
+
parent?: string;
|
|
21
|
+
/** List of child agent UUIDs (if this agent delegated work) */
|
|
22
|
+
delegated_to?: string[];
|
|
23
|
+
/** Additional notes */
|
|
24
|
+
notes?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Write an agent context file
|
|
28
|
+
* File naming: {agent}-{uuid}.md
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeAgentContext(input: PluginInput, context: AgentContext): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Read an agent context file by UUID
|
|
33
|
+
*/
|
|
34
|
+
export declare function readAgentContextById(input: PluginInput, id: string): Promise<AgentContext | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Update agent status in context file
|
|
37
|
+
*/
|
|
38
|
+
export declare function updateAgentStatus(input: PluginInput, id: string, status: AgentContext['status']): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* List all active agents
|
|
41
|
+
*/
|
|
42
|
+
export declare function listActiveAgents(input: PluginInput, sessionId?: string, agentType?: string): Promise<AgentContext[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Find agent files by various criteria
|
|
45
|
+
*/
|
|
46
|
+
export declare function findAgentFiles(input: PluginInput, agentType?: string, sessionId?: string): Promise<string[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Find agent file by UUID
|
|
49
|
+
*/
|
|
50
|
+
export declare function findAgentFilesById(input: PluginInput, id: string): Promise<string[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Find all agent files
|
|
53
|
+
*/
|
|
54
|
+
export declare function findAllAgentFiles(input: PluginInput): Promise<string[]>;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { isValidUUID } from './uuid.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate the content for an agent context file
|
|
4
|
+
*/
|
|
5
|
+
function generateContextFileContent(context) {
|
|
6
|
+
const frontmatter = `---
|
|
7
|
+
id: "${context.id}"
|
|
8
|
+
agent: ${context.agent}
|
|
9
|
+
title: "${context.title}"
|
|
10
|
+
description: "${context.description}"
|
|
11
|
+
folder: "${context.folder}"
|
|
12
|
+
status: ${context.status}
|
|
13
|
+
started: "${context.started}"
|
|
14
|
+
session: "${context.session}"
|
|
15
|
+
${context.parent ? `parent: "${context.parent}"` : 'parent: null'}
|
|
16
|
+
${context.delegated_to && context.delegated_to.length > 0 ? `delegated_to:\n${context.delegated_to.map((id) => ` - "${id}"`).join('\n')}` : 'delegated_to: []'}
|
|
17
|
+
---`;
|
|
18
|
+
const body = `
|
|
19
|
+
|
|
20
|
+
## Task Context
|
|
21
|
+
|
|
22
|
+
${context.notes || 'No additional notes.'}
|
|
23
|
+
|
|
24
|
+
## Progress
|
|
25
|
+
|
|
26
|
+
- [ ] Task started
|
|
27
|
+
|
|
28
|
+
## Delegated Work
|
|
29
|
+
|
|
30
|
+
${context.delegated_to && context.delegated_to.length > 0 ? context.delegated_to.map((id) => `- Agent ${id} (pending)`).join('\n') : 'No delegated work.'}
|
|
31
|
+
`;
|
|
32
|
+
return frontmatter + body;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Write an agent context file
|
|
36
|
+
* File naming: {agent}-{uuid}.md
|
|
37
|
+
*/
|
|
38
|
+
export async function writeAgentContext(input, context) {
|
|
39
|
+
const { directory, $ } = input;
|
|
40
|
+
const fileName = `${context.agent}-${context.id}.md`;
|
|
41
|
+
const filePath = `${directory}/.feature-factory/agents/${fileName}`;
|
|
42
|
+
const content = generateContextFileContent(context);
|
|
43
|
+
try {
|
|
44
|
+
// Use echo to write file (Bun shell)
|
|
45
|
+
await $ `echo ${content} > ${filePath}`.quiet();
|
|
46
|
+
return filePath;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new Error(`Failed to write agent context file: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read an agent context file by UUID
|
|
54
|
+
*/
|
|
55
|
+
export async function readAgentContextById(input, id) {
|
|
56
|
+
const { directory, $ } = input;
|
|
57
|
+
if (!isValidUUID(id)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
// Find file with this UUID
|
|
62
|
+
const result = await $ `ls ${directory}/.feature-factory/agents/ | grep "-${id}.md"`.quiet();
|
|
63
|
+
const fileName = result.text().trim();
|
|
64
|
+
if (!fileName) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const filePath = `${directory}/.feature-factory/agents/${fileName}`;
|
|
68
|
+
const content = await $ `cat ${filePath}`.quiet();
|
|
69
|
+
return parseAgentContext(content.text());
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse agent context from markdown content
|
|
77
|
+
*/
|
|
78
|
+
function parseAgentContext(content) {
|
|
79
|
+
try {
|
|
80
|
+
// Extract frontmatter
|
|
81
|
+
const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---/);
|
|
82
|
+
if (!frontmatterMatch) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const frontmatter = frontmatterMatch[1];
|
|
86
|
+
const lines = frontmatter.split('\n');
|
|
87
|
+
const context = {};
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const match = line.match(/^([a-z_]+):\s*(.*)$/);
|
|
90
|
+
if (match) {
|
|
91
|
+
const [, key, value] = match;
|
|
92
|
+
const cleanValue = value.replace(/^["']|["']$/g, ''); // Remove quotes
|
|
93
|
+
if (key === 'delegated_to') {
|
|
94
|
+
// Handle array - this is simplified, real YAML parsing would be better
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
else if (key === 'parent' && cleanValue === 'null') {
|
|
98
|
+
context[key] = undefined;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
context[key] = cleanValue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Parse delegated_to array manually from content
|
|
106
|
+
const delegatedMatch = content.match(/delegated_to:\n((?: {2}- ".*"\n?)*)/);
|
|
107
|
+
if (delegatedMatch) {
|
|
108
|
+
const delegatedLines = delegatedMatch[1].trim().split('\n');
|
|
109
|
+
context.delegated_to = delegatedLines
|
|
110
|
+
.map((line) => line.match(/- "([^"]+)"/)?.[1])
|
|
111
|
+
.filter((id) => !!id);
|
|
112
|
+
}
|
|
113
|
+
return context;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Update agent status in context file
|
|
121
|
+
*/
|
|
122
|
+
export async function updateAgentStatus(input, id, status) {
|
|
123
|
+
const { directory, $ } = input;
|
|
124
|
+
try {
|
|
125
|
+
const result = await $ `ls ${directory}/.feature-factory/agents/ | grep "-${id}.md"`.quiet();
|
|
126
|
+
const fileName = result.text().trim();
|
|
127
|
+
if (!fileName) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const filePath = `${directory}/.feature-factory/agents/${fileName}`;
|
|
131
|
+
// Read current content
|
|
132
|
+
const content = await $ `cat ${filePath}`.quiet();
|
|
133
|
+
let text = content.text();
|
|
134
|
+
// Replace status line
|
|
135
|
+
text = text.replace(/status: \w+/, `status: ${status}`);
|
|
136
|
+
// Write back
|
|
137
|
+
await $ `echo ${text} > ${filePath}`.quiet();
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* List all active agents
|
|
146
|
+
*/
|
|
147
|
+
export async function listActiveAgents(input, sessionId, agentType) {
|
|
148
|
+
const { directory, $ } = input;
|
|
149
|
+
const agentsDir = `${directory}/.feature-factory/agents`;
|
|
150
|
+
try {
|
|
151
|
+
// Check if directory exists
|
|
152
|
+
await $ `test -d ${agentsDir}`.quiet();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const result = await $ `ls ${agentsDir}/*.md 2>/dev/null || echo ""`.quiet();
|
|
159
|
+
const files = result
|
|
160
|
+
.text()
|
|
161
|
+
.trim()
|
|
162
|
+
.split('\n')
|
|
163
|
+
.filter((f) => f.endsWith('.md'));
|
|
164
|
+
const agents = [];
|
|
165
|
+
for (const filePath of files) {
|
|
166
|
+
try {
|
|
167
|
+
const content = await $ `cat ${filePath}`.quiet();
|
|
168
|
+
const context = parseAgentContext(content.text());
|
|
169
|
+
if (context) {
|
|
170
|
+
// Apply filters
|
|
171
|
+
if (sessionId && context.session !== sessionId) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (agentType && context.agent !== agentType) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
agents.push(context);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Skip files that can't be read
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return agents;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Find agent files by various criteria
|
|
193
|
+
*/
|
|
194
|
+
export async function findAgentFiles(input, agentType, sessionId) {
|
|
195
|
+
const { directory, $ } = input;
|
|
196
|
+
const agentsDir = `${directory}/.feature-factory/agents`;
|
|
197
|
+
try {
|
|
198
|
+
await $ `test -d ${agentsDir}`.quiet();
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
let pattern = '*.md';
|
|
205
|
+
if (agentType) {
|
|
206
|
+
pattern = `${agentType}-*.md`;
|
|
207
|
+
}
|
|
208
|
+
const result = await $ `ls ${agentsDir}/${pattern} 2>/dev/null || echo ""`.quiet();
|
|
209
|
+
const files = result
|
|
210
|
+
.text()
|
|
211
|
+
.trim()
|
|
212
|
+
.split('\n')
|
|
213
|
+
.filter((f) => f && f.endsWith('.md'));
|
|
214
|
+
if (sessionId) {
|
|
215
|
+
// Filter by session ID (need to read files)
|
|
216
|
+
const filteredFiles = [];
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
try {
|
|
219
|
+
const content = await $ `cat ${file}`.quiet();
|
|
220
|
+
if (content.text().includes(`session: "${sessionId}"`)) {
|
|
221
|
+
filteredFiles.push(file);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return filteredFiles;
|
|
229
|
+
}
|
|
230
|
+
return files;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Find agent file by UUID
|
|
238
|
+
*/
|
|
239
|
+
export async function findAgentFilesById(input, id) {
|
|
240
|
+
const { directory, $ } = input;
|
|
241
|
+
if (!isValidUUID(id)) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const result = await $ `ls ${directory}/.feature-factory/agents/*-${id}.md 2>/dev/null || echo ""`.quiet();
|
|
246
|
+
return result
|
|
247
|
+
.text()
|
|
248
|
+
.trim()
|
|
249
|
+
.split('\n')
|
|
250
|
+
.filter((f) => f && f.endsWith('.md'));
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Find all agent files
|
|
258
|
+
*/
|
|
259
|
+
export async function findAllAgentFiles(input) {
|
|
260
|
+
const { directory, $ } = input;
|
|
261
|
+
const agentsDir = `${directory}/.feature-factory/agents`;
|
|
262
|
+
try {
|
|
263
|
+
const result = await $ `ls ${agentsDir}/*.md 2>/dev/null || echo ""`.quiet();
|
|
264
|
+
return result
|
|
265
|
+
.text()
|
|
266
|
+
.trim()
|
|
267
|
+
.split('\n')
|
|
268
|
+
.filter((f) => f && f.endsWith('.md'));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CommandStep, QualityGateConfig } from './types.js';
|
|
2
|
+
type BunShell = any;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the command plan based on configuration and discovery.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order:
|
|
7
|
+
* 1. Configured commands (qualityGate.lint/build/test) - highest priority
|
|
8
|
+
* 2. management/ci.sh (Feature Factory convention) - only if no configured commands
|
|
9
|
+
* 3. Conventional discovery (Node/Rust/Go/Python) - only if no ci.sh
|
|
10
|
+
*
|
|
11
|
+
* @returns Array of command steps to execute, or empty if nothing found
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveCommands(args: {
|
|
14
|
+
$: BunShell;
|
|
15
|
+
directory: string;
|
|
16
|
+
config: QualityGateConfig;
|
|
17
|
+
}): Promise<CommandStep[]>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { DEFAULT_QUALITY_GATE, fileExists, hasConfiguredCommands, readJsonFile, } from './quality-gate-config.js';
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// Package Manager Detection
|
|
4
|
+
// ============================================================================
|
|
5
|
+
/**
|
|
6
|
+
* Detect the package manager based on lockfile presence.
|
|
7
|
+
* Priority: pnpm > bun > yarn > npm
|
|
8
|
+
*/
|
|
9
|
+
async function detectPackageManager($, directory, override) {
|
|
10
|
+
if (override && override !== 'auto') {
|
|
11
|
+
return override;
|
|
12
|
+
}
|
|
13
|
+
// Priority order: pnpm > bun > yarn > npm
|
|
14
|
+
if (await fileExists($, `${directory}/pnpm-lock.yaml`))
|
|
15
|
+
return 'pnpm';
|
|
16
|
+
if (await fileExists($, `${directory}/bun.lockb`))
|
|
17
|
+
return 'bun';
|
|
18
|
+
if (await fileExists($, `${directory}/bun.lock`))
|
|
19
|
+
return 'bun';
|
|
20
|
+
if (await fileExists($, `${directory}/yarn.lock`))
|
|
21
|
+
return 'yarn';
|
|
22
|
+
if (await fileExists($, `${directory}/package-lock.json`))
|
|
23
|
+
return 'npm';
|
|
24
|
+
return 'npm'; // fallback
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the run command for a given package manager and script name
|
|
28
|
+
*/
|
|
29
|
+
function buildRunCommand(pm, script) {
|
|
30
|
+
switch (pm) {
|
|
31
|
+
case 'pnpm':
|
|
32
|
+
return `pnpm -s run ${script}`;
|
|
33
|
+
case 'bun':
|
|
34
|
+
return `bun run ${script}`;
|
|
35
|
+
case 'yarn':
|
|
36
|
+
return `yarn -s ${script}`;
|
|
37
|
+
case 'npm':
|
|
38
|
+
return `npm run -s ${script}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Node.js Discovery
|
|
43
|
+
// ============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Discover Node.js lint/build/test commands from package.json scripts
|
|
46
|
+
*/
|
|
47
|
+
async function discoverNodeCommands($, directory, pm) {
|
|
48
|
+
const pkgJson = await readJsonFile($, `${directory}/package.json`);
|
|
49
|
+
if (!pkgJson?.scripts)
|
|
50
|
+
return [];
|
|
51
|
+
const scripts = pkgJson.scripts;
|
|
52
|
+
const steps = [];
|
|
53
|
+
// Lint: prefer lint:ci, then lint
|
|
54
|
+
const lintScript = scripts['lint:ci'] ? 'lint:ci' : scripts['lint'] ? 'lint' : null;
|
|
55
|
+
if (lintScript) {
|
|
56
|
+
steps.push({ step: 'lint', cmd: buildRunCommand(pm, lintScript) });
|
|
57
|
+
}
|
|
58
|
+
// Build: prefer build:ci, then build
|
|
59
|
+
const buildScript = scripts['build:ci'] ? 'build:ci' : scripts['build'] ? 'build' : null;
|
|
60
|
+
if (buildScript) {
|
|
61
|
+
steps.push({ step: 'build', cmd: buildRunCommand(pm, buildScript) });
|
|
62
|
+
}
|
|
63
|
+
// Test: prefer test:ci, then test
|
|
64
|
+
const testScript = scripts['test:ci'] ? 'test:ci' : scripts['test'] ? 'test' : null;
|
|
65
|
+
if (testScript) {
|
|
66
|
+
steps.push({ step: 'test', cmd: buildRunCommand(pm, testScript) });
|
|
67
|
+
}
|
|
68
|
+
return steps;
|
|
69
|
+
}
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Rust Discovery
|
|
72
|
+
// ============================================================================
|
|
73
|
+
/**
|
|
74
|
+
* Discover Rust commands from Cargo.toml presence
|
|
75
|
+
*/
|
|
76
|
+
async function discoverRustCommands($, directory, includeClippy) {
|
|
77
|
+
if (!(await fileExists($, `${directory}/Cargo.toml`)))
|
|
78
|
+
return [];
|
|
79
|
+
const steps = [{ step: 'lint (fmt)', cmd: 'cargo fmt --check' }];
|
|
80
|
+
if (includeClippy) {
|
|
81
|
+
steps.push({
|
|
82
|
+
step: 'lint (clippy)',
|
|
83
|
+
cmd: 'cargo clippy --all-targets --all-features',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
steps.push({ step: 'build', cmd: 'cargo build' });
|
|
87
|
+
steps.push({ step: 'test', cmd: 'cargo test' });
|
|
88
|
+
return steps;
|
|
89
|
+
}
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Go Discovery
|
|
92
|
+
// ============================================================================
|
|
93
|
+
/**
|
|
94
|
+
* Discover Go commands from go.mod presence
|
|
95
|
+
*/
|
|
96
|
+
async function discoverGoCommands($, directory) {
|
|
97
|
+
if (!(await fileExists($, `${directory}/go.mod`)))
|
|
98
|
+
return [];
|
|
99
|
+
return [{ step: 'test', cmd: 'go test ./...' }];
|
|
100
|
+
}
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Python Discovery
|
|
103
|
+
// ============================================================================
|
|
104
|
+
/**
|
|
105
|
+
* Discover Python test commands with strong signal detection
|
|
106
|
+
*/
|
|
107
|
+
async function discoverPythonCommands($, directory) {
|
|
108
|
+
// Only add pytest if we have strong signal
|
|
109
|
+
const hasPytestIni = await fileExists($, `${directory}/pytest.ini`);
|
|
110
|
+
const hasPyproject = await fileExists($, `${directory}/pyproject.toml`);
|
|
111
|
+
if (!hasPytestIni && !hasPyproject)
|
|
112
|
+
return [];
|
|
113
|
+
// pytest.ini is strong signal
|
|
114
|
+
if (hasPytestIni) {
|
|
115
|
+
return [{ step: 'test', cmd: 'pytest' }];
|
|
116
|
+
}
|
|
117
|
+
// Check if pyproject.toml mentions pytest
|
|
118
|
+
if (hasPyproject) {
|
|
119
|
+
try {
|
|
120
|
+
const result = await $ `cat ${directory}/pyproject.toml`.quiet();
|
|
121
|
+
const content = result.text();
|
|
122
|
+
if (content.includes('pytest')) {
|
|
123
|
+
return [{ step: 'test', cmd: 'pytest' }];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore read errors - return empty array if file can't be read
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Main Resolution Logic
|
|
134
|
+
// ============================================================================
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the command plan based on configuration and discovery.
|
|
137
|
+
*
|
|
138
|
+
* Resolution order:
|
|
139
|
+
* 1. Configured commands (qualityGate.lint/build/test) - highest priority
|
|
140
|
+
* 2. management/ci.sh (Feature Factory convention) - only if no configured commands
|
|
141
|
+
* 3. Conventional discovery (Node/Rust/Go/Python) - only if no ci.sh
|
|
142
|
+
*
|
|
143
|
+
* @returns Array of command steps to execute, or empty if nothing found
|
|
144
|
+
*/
|
|
145
|
+
export async function resolveCommands(args) {
|
|
146
|
+
const { $, directory, config } = args;
|
|
147
|
+
const mergedConfig = { ...DEFAULT_QUALITY_GATE, ...config };
|
|
148
|
+
// 1. Configured commands take priority (do NOT run ci.sh if these exist)
|
|
149
|
+
if (hasConfiguredCommands(config)) {
|
|
150
|
+
const steps = [];
|
|
151
|
+
const order = mergedConfig.steps;
|
|
152
|
+
for (const stepName of order) {
|
|
153
|
+
const cmd = config[stepName];
|
|
154
|
+
if (cmd) {
|
|
155
|
+
steps.push({ step: stepName, cmd });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return steps;
|
|
159
|
+
}
|
|
160
|
+
// 2. Feature Factory CI script (only if no configured commands)
|
|
161
|
+
if (mergedConfig.useCiSh !== 'never') {
|
|
162
|
+
const ciShPath = `${directory}/management/ci.sh`;
|
|
163
|
+
if (await fileExists($, ciShPath)) {
|
|
164
|
+
return [{ step: 'ci', cmd: `bash ${ciShPath}` }];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// 3. Conventional discovery (only if no ci.sh)
|
|
168
|
+
const pm = await detectPackageManager($, directory, mergedConfig.packageManager);
|
|
169
|
+
// Try Node first (most common)
|
|
170
|
+
if (await fileExists($, `${directory}/package.json`)) {
|
|
171
|
+
const nodeSteps = await discoverNodeCommands($, directory, pm);
|
|
172
|
+
if (nodeSteps.length > 0)
|
|
173
|
+
return nodeSteps;
|
|
174
|
+
}
|
|
175
|
+
// Rust
|
|
176
|
+
const rustSteps = await discoverRustCommands($, directory, mergedConfig.include?.rustClippy ?? true);
|
|
177
|
+
if (rustSteps.length > 0)
|
|
178
|
+
return rustSteps;
|
|
179
|
+
// Go
|
|
180
|
+
const goSteps = await discoverGoCommands($, directory);
|
|
181
|
+
if (goSteps.length > 0)
|
|
182
|
+
return goSteps;
|
|
183
|
+
// Python
|
|
184
|
+
const pythonSteps = await discoverPythonCommands($, directory);
|
|
185
|
+
if (pythonSteps.length > 0)
|
|
186
|
+
return pythonSteps;
|
|
187
|
+
// No commands discovered
|
|
188
|
+
return [];
|
|
189
|
+
}
|