@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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for discovery module
|
|
3
|
+
*
|
|
4
|
+
* Tests focus on the pure function:
|
|
5
|
+
* - buildRunCommand: builds package manager-specific run commands
|
|
6
|
+
*
|
|
7
|
+
* Note: Most functions in discovery.ts require shell access ($) so they're
|
|
8
|
+
* integration-tested elsewhere. This file tests the pure utility functions.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for discovery module
|
|
3
|
+
*
|
|
4
|
+
* Tests focus on the pure function:
|
|
5
|
+
* - buildRunCommand: builds package manager-specific run commands
|
|
6
|
+
*
|
|
7
|
+
* Note: Most functions in discovery.ts require shell access ($) so they're
|
|
8
|
+
* integration-tested elsewhere. This file tests the pure utility functions.
|
|
9
|
+
*/
|
|
10
|
+
// Re-implement buildRunCommand for testing since it's not exported
|
|
11
|
+
function buildRunCommand(pm, script) {
|
|
12
|
+
switch (pm) {
|
|
13
|
+
case 'pnpm':
|
|
14
|
+
return `pnpm -s run ${script}`;
|
|
15
|
+
case 'bun':
|
|
16
|
+
return `bun run ${script}`;
|
|
17
|
+
case 'yarn':
|
|
18
|
+
return `yarn -s ${script}`;
|
|
19
|
+
case 'npm':
|
|
20
|
+
return `npm run -s ${script}`;
|
|
21
|
+
default:
|
|
22
|
+
return `npm run -s ${script}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
describe('buildRunCommand', () => {
|
|
26
|
+
describe('pnpm', () => {
|
|
27
|
+
it('should build correct pnpm command', () => {
|
|
28
|
+
expect(buildRunCommand('pnpm', 'lint')).toBe('pnpm -s run lint');
|
|
29
|
+
});
|
|
30
|
+
it('should build correct pnpm command for build script', () => {
|
|
31
|
+
expect(buildRunCommand('pnpm', 'build')).toBe('pnpm -s run build');
|
|
32
|
+
});
|
|
33
|
+
it('should build correct pnpm command for test script', () => {
|
|
34
|
+
expect(buildRunCommand('pnpm', 'test')).toBe('pnpm -s run test');
|
|
35
|
+
});
|
|
36
|
+
it('should handle scripts with colons', () => {
|
|
37
|
+
expect(buildRunCommand('pnpm', 'lint:ci')).toBe('pnpm -s run lint:ci');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('bun', () => {
|
|
41
|
+
it('should build correct bun command', () => {
|
|
42
|
+
expect(buildRunCommand('bun', 'lint')).toBe('bun run lint');
|
|
43
|
+
});
|
|
44
|
+
it('should build correct bun command for build script', () => {
|
|
45
|
+
expect(buildRunCommand('bun', 'build')).toBe('bun run build');
|
|
46
|
+
});
|
|
47
|
+
it('should build correct bun command for test script', () => {
|
|
48
|
+
expect(buildRunCommand('bun', 'test')).toBe('bun run test');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('yarn', () => {
|
|
52
|
+
it('should build correct yarn command', () => {
|
|
53
|
+
expect(buildRunCommand('yarn', 'lint')).toBe('yarn -s lint');
|
|
54
|
+
});
|
|
55
|
+
it('should build correct yarn command for build script', () => {
|
|
56
|
+
expect(buildRunCommand('yarn', 'build')).toBe('yarn -s build');
|
|
57
|
+
});
|
|
58
|
+
it('should build correct yarn command for test script', () => {
|
|
59
|
+
expect(buildRunCommand('yarn', 'test')).toBe('yarn -s test');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('npm', () => {
|
|
63
|
+
it('should build correct npm command', () => {
|
|
64
|
+
expect(buildRunCommand('npm', 'lint')).toBe('npm run -s lint');
|
|
65
|
+
});
|
|
66
|
+
it('should build correct npm command for build script', () => {
|
|
67
|
+
expect(buildRunCommand('npm', 'build')).toBe('npm run -s build');
|
|
68
|
+
});
|
|
69
|
+
it('should build correct npm command for test script', () => {
|
|
70
|
+
expect(buildRunCommand('npm', 'test')).toBe('npm run -s test');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('edge cases', () => {
|
|
74
|
+
it('should handle script names with hyphens', () => {
|
|
75
|
+
expect(buildRunCommand('npm', 'type-check')).toBe('npm run -s type-check');
|
|
76
|
+
});
|
|
77
|
+
it('should handle script names with underscores', () => {
|
|
78
|
+
expect(buildRunCommand('pnpm', 'lint_fix')).toBe('pnpm -s run lint_fix');
|
|
79
|
+
});
|
|
80
|
+
it('should handle long script names', () => {
|
|
81
|
+
expect(buildRunCommand('yarn', 'test:unit:coverage')).toBe('yarn -s test:unit:coverage');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* PackageManager type validation tests
|
|
87
|
+
* These ensure the type constraints are working correctly
|
|
88
|
+
*/
|
|
89
|
+
describe('PackageManager type', () => {
|
|
90
|
+
it('should accept valid package manager values', () => {
|
|
91
|
+
const validManagers = ['pnpm', 'bun', 'yarn', 'npm'];
|
|
92
|
+
validManagers.forEach((pm) => {
|
|
93
|
+
expect(['pnpm', 'bun', 'yarn', 'npm']).toContain(pm);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
+
type BunShell = any;
|
|
3
|
+
/**
|
|
4
|
+
* Initialize the Feature Factory directory structure.
|
|
5
|
+
* Creates .feature-factory/ and .feature-factory/agents/ directories.
|
|
6
|
+
* Migrates management/ci.sh to .feature-factory/ci.sh if it exists.
|
|
7
|
+
*/
|
|
8
|
+
export declare function initializeFeatureFactory(input: PluginInput, $: BunShell): Promise<void>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const SERVICE_NAME = 'feature-factory';
|
|
2
|
+
/**
|
|
3
|
+
* Log a message using the OpenCode client's structured logging.
|
|
4
|
+
* Silently fails if logging is unavailable.
|
|
5
|
+
*/
|
|
6
|
+
async function log(client, level, message, extra) {
|
|
7
|
+
try {
|
|
8
|
+
await client.app.log({
|
|
9
|
+
body: {
|
|
10
|
+
service: SERVICE_NAME,
|
|
11
|
+
level,
|
|
12
|
+
message,
|
|
13
|
+
extra,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Logging failure should not affect plugin operation
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Execute a shell command with a timeout to prevent hangs
|
|
23
|
+
*/
|
|
24
|
+
async function executeWithTimeout(operation, timeoutMs = 5000, operationName = 'shell command') {
|
|
25
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
26
|
+
setTimeout(() => reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
27
|
+
});
|
|
28
|
+
try {
|
|
29
|
+
return await Promise.race([operation(), timeoutPromise]);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
// Return null on timeout or error
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate that a directory path is safe to use
|
|
38
|
+
*/
|
|
39
|
+
function isValidDirectory(directory) {
|
|
40
|
+
if (!directory || typeof directory !== 'string')
|
|
41
|
+
return false;
|
|
42
|
+
if (directory === '' || directory === '/')
|
|
43
|
+
return false;
|
|
44
|
+
// Check for common invalid patterns
|
|
45
|
+
if (directory.includes('\0') || directory.includes('..'))
|
|
46
|
+
return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Initialize the Feature Factory directory structure.
|
|
51
|
+
* Creates .feature-factory/ and .feature-factory/agents/ directories.
|
|
52
|
+
* Migrates management/ci.sh to .feature-factory/ci.sh if it exists.
|
|
53
|
+
*/
|
|
54
|
+
export async function initializeFeatureFactory(input, $) {
|
|
55
|
+
const { directory, client } = input;
|
|
56
|
+
// Validate directory before attempting any operations
|
|
57
|
+
if (!isValidDirectory(directory)) {
|
|
58
|
+
await log(client, 'debug', 'feature-factory-init-skipped-invalid-directory', {
|
|
59
|
+
directory: String(directory),
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const featureFactoryDir = `${directory}/.feature-factory`;
|
|
64
|
+
const agentsDir = `${featureFactoryDir}/agents`;
|
|
65
|
+
// Create directories if they don't exist (with timeout protection)
|
|
66
|
+
try {
|
|
67
|
+
const result = await executeWithTimeout(async () => await $ `mkdir -p ${agentsDir}`.quiet(), 3000, 'mkdir -p');
|
|
68
|
+
if (result === null) {
|
|
69
|
+
await log(client, 'warn', 'feature-factory-directory-creation-timeout', {
|
|
70
|
+
agentsDir,
|
|
71
|
+
});
|
|
72
|
+
return; // Exit early if we can't create directories
|
|
73
|
+
}
|
|
74
|
+
await log(client, 'debug', 'feature-factory-directories-created', {
|
|
75
|
+
featureFactoryDir,
|
|
76
|
+
agentsDir,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await log(client, 'warn', 'feature-factory-directory-creation-failed', {
|
|
81
|
+
error: String(error),
|
|
82
|
+
});
|
|
83
|
+
// Continue even if directory creation fails - it might already exist
|
|
84
|
+
}
|
|
85
|
+
// Check for CI script migration (with timeout protection)
|
|
86
|
+
const oldCiPath = `${directory}/management/ci.sh`;
|
|
87
|
+
const newCiPath = `${featureFactoryDir}/ci.sh`;
|
|
88
|
+
try {
|
|
89
|
+
// Check if old CI exists (with timeout)
|
|
90
|
+
const oldExists = await executeWithTimeout(async () => {
|
|
91
|
+
await $ `test -f ${oldCiPath}`.quiet();
|
|
92
|
+
return true;
|
|
93
|
+
}, 2000, 'test -f old-ci');
|
|
94
|
+
if (oldExists !== true) {
|
|
95
|
+
// Old doesn't exist, nothing to migrate
|
|
96
|
+
await log(client, 'debug', 'ci-sh-no-migration-needed', {
|
|
97
|
+
oldPath: oldCiPath,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Old exists, check if new already exists (with timeout)
|
|
102
|
+
const newExists = await executeWithTimeout(async () => {
|
|
103
|
+
await $ `test -f ${newCiPath}`.quiet();
|
|
104
|
+
return true;
|
|
105
|
+
}, 2000, 'test -f new-ci');
|
|
106
|
+
if (newExists === true) {
|
|
107
|
+
// Both exist - migration already done or user has both
|
|
108
|
+
await log(client, 'debug', 'ci-sh-both-locations-exist', {
|
|
109
|
+
oldPath: oldCiPath,
|
|
110
|
+
newPath: newCiPath,
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Old exists, new doesn't - migrate (with timeout)
|
|
115
|
+
const copied = await executeWithTimeout(async () => {
|
|
116
|
+
await $ `cp ${oldCiPath} ${newCiPath}`.quiet();
|
|
117
|
+
return true;
|
|
118
|
+
}, 3000, 'cp ci.sh');
|
|
119
|
+
if (copied === true) {
|
|
120
|
+
const removed = await executeWithTimeout(async () => {
|
|
121
|
+
await $ `rm ${oldCiPath}`.quiet();
|
|
122
|
+
return true;
|
|
123
|
+
}, 2000, 'rm old-ci');
|
|
124
|
+
if (removed === true) {
|
|
125
|
+
await log(client, 'info', 'ci-sh-migrated', {
|
|
126
|
+
from: oldCiPath,
|
|
127
|
+
to: newCiPath,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
await log(client, 'warn', 'ci-sh-migration-partial', {
|
|
132
|
+
from: oldCiPath,
|
|
133
|
+
to: newCiPath,
|
|
134
|
+
reason: 'copied but failed to remove old file',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
await log(client, 'error', 'ci-sh-migration-failed', {
|
|
140
|
+
from: oldCiPath,
|
|
141
|
+
to: newCiPath,
|
|
142
|
+
reason: 'copy operation timed out or failed',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
await log(client, 'error', 'ci-sh-migration-error', {
|
|
148
|
+
error: String(error),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
/**
|
|
3
|
+
* Stop Quality Gate Plugin
|
|
4
|
+
*
|
|
5
|
+
* Runs quality gate checks when the agent becomes idle after editing files.
|
|
6
|
+
*/
|
|
7
|
+
export declare const StopQualityGatePlugin: Plugin;
|
|
8
|
+
export default StopQualityGatePlugin;
|
|
9
|
+
export type { QualityGateConfig, CommandStep, StepResult, SessionState, PackageManager, } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StopQualityGateHooksPlugin } from './stop-quality-gate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stop Quality Gate Plugin
|
|
4
|
+
*
|
|
5
|
+
* Runs quality gate checks when the agent becomes idle after editing files.
|
|
6
|
+
*/
|
|
7
|
+
export const StopQualityGatePlugin = async (input) => {
|
|
8
|
+
const { directory } = input;
|
|
9
|
+
// Skip if no valid directory
|
|
10
|
+
if (!directory || directory === '' || directory === '/') {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
// Load quality gate plugin only (agents disabled for testing)
|
|
14
|
+
const qualityGateHooks = await StopQualityGateHooksPlugin(input).catch(() => ({}));
|
|
15
|
+
return qualityGateHooks;
|
|
16
|
+
};
|
|
17
|
+
// Default export for OpenCode plugin discovery
|
|
18
|
+
export default StopQualityGatePlugin;
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract lines that likely contain error information from command output.
|
|
3
|
+
* Includes 1-2 lines of context after each error line.
|
|
4
|
+
*
|
|
5
|
+
* @param output - The command output to search
|
|
6
|
+
* @param maxLines - Maximum number of error lines to extract
|
|
7
|
+
* @returns Array of error-like lines (up to maxLines)
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractErrorLines(output: string, maxLines: number): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Return the last N lines of output, with a truncation notice if needed.
|
|
12
|
+
*
|
|
13
|
+
* @param output - The full command output
|
|
14
|
+
* @param maxLines - Maximum number of lines to return
|
|
15
|
+
* @returns Truncated output with notice, or full output if short enough
|
|
16
|
+
*/
|
|
17
|
+
export declare function tailLines(output: string, maxLines: number): string;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regex pattern for detecting error-like lines in command output
|
|
3
|
+
*/
|
|
4
|
+
const ERROR_PATTERNS = /(error|failed|failure|panic|assert|exception|traceback|TypeError|ReferenceError|SyntaxError|FAILED|FAIL|ERR!)/i;
|
|
5
|
+
/**
|
|
6
|
+
* Extract lines that likely contain error information from command output.
|
|
7
|
+
* Includes 1-2 lines of context after each error line.
|
|
8
|
+
*
|
|
9
|
+
* @param output - The command output to search
|
|
10
|
+
* @param maxLines - Maximum number of error lines to extract
|
|
11
|
+
* @returns Array of error-like lines (up to maxLines)
|
|
12
|
+
*/
|
|
13
|
+
export function extractErrorLines(output, maxLines) {
|
|
14
|
+
const lines = output.split('\n');
|
|
15
|
+
const errorLines = [];
|
|
16
|
+
for (let i = 0; i < lines.length && errorLines.length < maxLines; i++) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
if (line && ERROR_PATTERNS.test(line)) {
|
|
19
|
+
errorLines.push(line);
|
|
20
|
+
// Include 1-2 lines of context after the error
|
|
21
|
+
const nextLine = lines[i + 1];
|
|
22
|
+
const nextNextLine = lines[i + 2];
|
|
23
|
+
if (nextLine !== undefined) {
|
|
24
|
+
errorLines.push(nextLine);
|
|
25
|
+
}
|
|
26
|
+
if (nextNextLine !== undefined) {
|
|
27
|
+
errorLines.push(nextNextLine);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return errorLines.slice(0, maxLines);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Return the last N lines of output, with a truncation notice if needed.
|
|
35
|
+
*
|
|
36
|
+
* @param output - The full command output
|
|
37
|
+
* @param maxLines - Maximum number of lines to return
|
|
38
|
+
* @returns Truncated output with notice, or full output if short enough
|
|
39
|
+
*/
|
|
40
|
+
export function tailLines(output, maxLines) {
|
|
41
|
+
const lines = output.split('\n');
|
|
42
|
+
if (lines.length <= maxLines) {
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
const truncatedCount = lines.length - maxLines;
|
|
46
|
+
const tail = lines.slice(-maxLines).join('\n');
|
|
47
|
+
return `... (${truncatedCount} lines truncated)\n${tail}`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for output module
|
|
3
|
+
*
|
|
4
|
+
* Tests focus on pure functions:
|
|
5
|
+
* - extractErrorLines: extracts error-like lines from command output
|
|
6
|
+
* - tailLines: returns last N lines with truncation notice
|
|
7
|
+
*/
|
|
8
|
+
import { extractErrorLines, tailLines } from './output.js';
|
|
9
|
+
describe('extractErrorLines', () => {
|
|
10
|
+
it('should return empty array for empty input', () => {
|
|
11
|
+
expect(extractErrorLines('', 10)).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
it('should return empty array when no errors found', () => {
|
|
14
|
+
const output = `
|
|
15
|
+
Build started
|
|
16
|
+
Compiling modules...
|
|
17
|
+
Build completed successfully
|
|
18
|
+
`;
|
|
19
|
+
expect(extractErrorLines(output, 10)).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
it('should extract lines containing "error"', () => {
|
|
22
|
+
const output = `
|
|
23
|
+
Starting build
|
|
24
|
+
error: Cannot find module 'missing'
|
|
25
|
+
Build failed
|
|
26
|
+
`;
|
|
27
|
+
const result = extractErrorLines(output, 10);
|
|
28
|
+
expect(result.some((line) => line.includes('error:'))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('should extract lines containing "Error" (case insensitive)', () => {
|
|
31
|
+
const output = `
|
|
32
|
+
Running tests
|
|
33
|
+
TypeError: undefined is not a function
|
|
34
|
+
at test.js:10:5
|
|
35
|
+
`;
|
|
36
|
+
const result = extractErrorLines(output, 10);
|
|
37
|
+
expect(result.some((line) => line.includes('TypeError'))).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('should extract lines containing "failed"', () => {
|
|
40
|
+
const output = `
|
|
41
|
+
Test suite: MyTests
|
|
42
|
+
Test failed: should work correctly
|
|
43
|
+
1 test failed
|
|
44
|
+
`;
|
|
45
|
+
const result = extractErrorLines(output, 10);
|
|
46
|
+
expect(result.some((line) => line.toLowerCase().includes('failed'))).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it('should extract lines containing "FAILED"', () => {
|
|
49
|
+
const output = `
|
|
50
|
+
Running: test_function
|
|
51
|
+
FAILED test_function - assertion error
|
|
52
|
+
`;
|
|
53
|
+
const result = extractErrorLines(output, 10);
|
|
54
|
+
expect(result.some((line) => line.includes('FAILED'))).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('should extract lines containing "panic"', () => {
|
|
57
|
+
const output = `
|
|
58
|
+
thread 'main' panicked at 'assertion failed'
|
|
59
|
+
note: run with RUST_BACKTRACE=1
|
|
60
|
+
`;
|
|
61
|
+
const result = extractErrorLines(output, 10);
|
|
62
|
+
expect(result.some((line) => line.includes('panic'))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('should extract lines containing "exception"', () => {
|
|
65
|
+
const output = `
|
|
66
|
+
Exception in thread "main" java.lang.NullPointerException
|
|
67
|
+
at Main.main(Main.java:5)
|
|
68
|
+
`;
|
|
69
|
+
const result = extractErrorLines(output, 10);
|
|
70
|
+
expect(result.some((line) => line.includes('Exception'))).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it('should extract lines containing "traceback"', () => {
|
|
73
|
+
const output = `
|
|
74
|
+
Traceback (most recent call last):
|
|
75
|
+
File "test.py", line 10
|
|
76
|
+
NameError: name 'x' is not defined
|
|
77
|
+
`;
|
|
78
|
+
const result = extractErrorLines(output, 10);
|
|
79
|
+
expect(result.some((line) => line.includes('Traceback'))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('should include 2 lines of context after error', () => {
|
|
82
|
+
const output = `line1
|
|
83
|
+
error: something went wrong
|
|
84
|
+
context line 1
|
|
85
|
+
context line 2
|
|
86
|
+
line5`;
|
|
87
|
+
const result = extractErrorLines(output, 10);
|
|
88
|
+
expect(result).toContain('error: something went wrong');
|
|
89
|
+
expect(result).toContain('context line 1');
|
|
90
|
+
expect(result).toContain('context line 2');
|
|
91
|
+
});
|
|
92
|
+
it('should respect maxLines limit', () => {
|
|
93
|
+
const output = `
|
|
94
|
+
error: first error
|
|
95
|
+
context1
|
|
96
|
+
context2
|
|
97
|
+
error: second error
|
|
98
|
+
context3
|
|
99
|
+
context4
|
|
100
|
+
error: third error
|
|
101
|
+
context5
|
|
102
|
+
context6
|
|
103
|
+
`;
|
|
104
|
+
const result = extractErrorLines(output, 5);
|
|
105
|
+
expect(result.length).toBeLessThanOrEqual(5);
|
|
106
|
+
});
|
|
107
|
+
it('should handle error at end of output without enough context lines', () => {
|
|
108
|
+
const output = `line1
|
|
109
|
+
line2
|
|
110
|
+
error: final error`;
|
|
111
|
+
const result = extractErrorLines(output, 10);
|
|
112
|
+
expect(result).toContain('error: final error');
|
|
113
|
+
});
|
|
114
|
+
it('should extract ReferenceError', () => {
|
|
115
|
+
const output = `ReferenceError: x is not defined`;
|
|
116
|
+
const result = extractErrorLines(output, 10);
|
|
117
|
+
expect(result).toContain('ReferenceError: x is not defined');
|
|
118
|
+
});
|
|
119
|
+
it('should extract SyntaxError', () => {
|
|
120
|
+
const output = `SyntaxError: Unexpected token '}'`;
|
|
121
|
+
const result = extractErrorLines(output, 10);
|
|
122
|
+
expect(result).toContain("SyntaxError: Unexpected token '}'");
|
|
123
|
+
});
|
|
124
|
+
it('should extract ERR! (npm style errors)', () => {
|
|
125
|
+
const output = `npm ERR! code ENOENT
|
|
126
|
+
npm ERR! path /app/package.json`;
|
|
127
|
+
const result = extractErrorLines(output, 10);
|
|
128
|
+
expect(result.some((line) => line.includes('ERR!'))).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it('should extract FAIL (test runner style)', () => {
|
|
131
|
+
const output = `FAIL src/test.ts
|
|
132
|
+
Test suite failed to run`;
|
|
133
|
+
const result = extractErrorLines(output, 10);
|
|
134
|
+
expect(result.some((line) => line.includes('FAIL'))).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
it('should extract multiple error types in same output', () => {
|
|
137
|
+
const output = `
|
|
138
|
+
Starting build...
|
|
139
|
+
error: Build failed
|
|
140
|
+
TypeError: Cannot read property 'x'
|
|
141
|
+
FAILED: test_something
|
|
142
|
+
Build process ended
|
|
143
|
+
`;
|
|
144
|
+
const result = extractErrorLines(output, 20);
|
|
145
|
+
expect(result.some((line) => line.includes('error:'))).toBe(true);
|
|
146
|
+
expect(result.some((line) => line.includes('TypeError'))).toBe(true);
|
|
147
|
+
expect(result.some((line) => line.includes('FAILED'))).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('tailLines', () => {
|
|
151
|
+
it('should return full output when lines count is less than maxLines', () => {
|
|
152
|
+
const output = 'line1\nline2\nline3';
|
|
153
|
+
expect(tailLines(output, 10)).toBe(output);
|
|
154
|
+
});
|
|
155
|
+
it('should return full output when lines count equals maxLines', () => {
|
|
156
|
+
const output = 'line1\nline2\nline3';
|
|
157
|
+
expect(tailLines(output, 3)).toBe(output);
|
|
158
|
+
});
|
|
159
|
+
it('should truncate and add notice when output exceeds maxLines', () => {
|
|
160
|
+
const output = 'line1\nline2\nline3\nline4\nline5';
|
|
161
|
+
const result = tailLines(output, 3);
|
|
162
|
+
expect(result).toContain('... (2 lines truncated)');
|
|
163
|
+
expect(result).toContain('line3');
|
|
164
|
+
expect(result).toContain('line4');
|
|
165
|
+
expect(result).toContain('line5');
|
|
166
|
+
expect(result).not.toContain('line1');
|
|
167
|
+
expect(result).not.toContain('line2');
|
|
168
|
+
});
|
|
169
|
+
it('should handle single line output', () => {
|
|
170
|
+
const output = 'single line';
|
|
171
|
+
expect(tailLines(output, 5)).toBe('single line');
|
|
172
|
+
});
|
|
173
|
+
it('should handle empty output', () => {
|
|
174
|
+
expect(tailLines('', 5)).toBe('');
|
|
175
|
+
});
|
|
176
|
+
it('should handle maxLines of 1', () => {
|
|
177
|
+
const output = 'line1\nline2\nline3';
|
|
178
|
+
const result = tailLines(output, 1);
|
|
179
|
+
expect(result).toContain('... (2 lines truncated)');
|
|
180
|
+
expect(result).toContain('line3');
|
|
181
|
+
});
|
|
182
|
+
it('should correctly count truncated lines', () => {
|
|
183
|
+
const lines = Array.from({ length: 100 }, (_, i) => `line${i + 1}`);
|
|
184
|
+
const output = lines.join('\n');
|
|
185
|
+
const result = tailLines(output, 10);
|
|
186
|
+
expect(result).toContain('... (90 lines truncated)');
|
|
187
|
+
});
|
|
188
|
+
it('should preserve line content in tail', () => {
|
|
189
|
+
const output = 'first\nsecond\nthird\nfourth\nfifth';
|
|
190
|
+
const result = tailLines(output, 2);
|
|
191
|
+
const resultLines = result.split('\n');
|
|
192
|
+
expect(resultLines[resultLines.length - 1]).toBe('fifth');
|
|
193
|
+
expect(resultLines[resultLines.length - 2]).toBe('fourth');
|
|
194
|
+
});
|
|
195
|
+
it('should handle output with empty lines', () => {
|
|
196
|
+
const output = 'line1\n\nline3\n\nline5';
|
|
197
|
+
const result = tailLines(output, 3);
|
|
198
|
+
expect(result).toContain('... (2 lines truncated)');
|
|
199
|
+
});
|
|
200
|
+
it('should handle output with only newlines', () => {
|
|
201
|
+
const output = '\n\n\n\n';
|
|
202
|
+
const result = tailLines(output, 2);
|
|
203
|
+
expect(result).toContain('... (3 lines truncated)');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import { findAgentFiles, findAgentFilesById, findAllAgentFiles } from '../agent-context.js';
|
|
3
|
+
export const FFAgentsClearPlugin = async (input) => {
|
|
4
|
+
const { $ } = input;
|
|
5
|
+
return {
|
|
6
|
+
tool: {
|
|
7
|
+
'ff-agents-clear': tool({
|
|
8
|
+
description: 'Clear agent context files. Can clear all, or filter by session, agent type, or specific UUID',
|
|
9
|
+
args: {
|
|
10
|
+
sessionID: tool.schema
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('Clear only agents for specific session'),
|
|
14
|
+
agent: tool.schema.string().optional().describe('Clear only specific agent type'),
|
|
15
|
+
id: tool.schema.string().optional().describe('Clear specific agent by UUID'),
|
|
16
|
+
},
|
|
17
|
+
async execute(args) {
|
|
18
|
+
try {
|
|
19
|
+
let files = [];
|
|
20
|
+
if (args.id) {
|
|
21
|
+
files = await findAgentFilesById(input, args.id);
|
|
22
|
+
}
|
|
23
|
+
else if (args.agent && args.sessionID) {
|
|
24
|
+
files = await findAgentFiles(input, args.agent, args.sessionID);
|
|
25
|
+
}
|
|
26
|
+
else if (args.sessionID) {
|
|
27
|
+
files = await findAgentFiles(input, undefined, args.sessionID);
|
|
28
|
+
}
|
|
29
|
+
else if (args.agent) {
|
|
30
|
+
files = await findAgentFiles(input, args.agent);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
files = await findAllAgentFiles(input);
|
|
34
|
+
}
|
|
35
|
+
if (files.length === 0) {
|
|
36
|
+
return 'No agent context files found to clear.';
|
|
37
|
+
}
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
try {
|
|
40
|
+
await $ `rm ${file}`.quiet();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Continue even if one file fails
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return `Cleared ${files.length} agent context file(s)`;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return `Error clearing agents: ${error}`;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
};
|