@bamptee/aia-code 2.0.12 → 2.0.13
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 +408 -34
- package/package.json +11 -2
- package/src/constants.js +24 -0
- package/src/providers/anthropic.js +2 -21
- package/src/providers/cli-runner.js +5 -3
- package/src/services/agent-sessions.js +110 -0
- package/src/services/apps.js +132 -0
- package/src/services/config.js +41 -0
- package/src/services/feature.js +28 -7
- package/src/services/model-call.js +2 -2
- package/src/services/runner.js +23 -1
- package/src/services/status.js +69 -1
- package/src/services/test-quick.js +229 -0
- package/src/services/worktrunk.js +135 -21
- package/src/types/test-quick.js +88 -0
- package/src/ui/api/config.js +28 -1
- package/src/ui/api/features.js +160 -50
- package/src/ui/api/index.js +2 -0
- package/src/ui/api/test-quick.js +207 -0
- package/src/ui/api/worktrunk.js +63 -25
- package/src/ui/public/components/config-view.js +95 -0
- package/src/ui/public/components/dashboard.js +823 -163
- package/src/ui/public/components/feature-detail.js +517 -124
- package/src/ui/public/components/test-quick.js +276 -0
- package/src/ui/public/components/worktrunk-panel.js +187 -25
- package/src/ui/public/index.html +13 -0
- package/src/ui/public/main.js +5 -1
- package/src/ui/server.js +97 -67
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { APP_ICONS } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
const MANIFEST_FILES = {
|
|
6
|
+
'package.json': 'node',
|
|
7
|
+
'pom.xml': 'java',
|
|
8
|
+
'go.mod': 'go',
|
|
9
|
+
'Cargo.toml': 'rust',
|
|
10
|
+
'requirements.txt': 'python',
|
|
11
|
+
'pyproject.toml': 'python',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const IGNORE_DIRS = new Set([
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.git',
|
|
17
|
+
'dist',
|
|
18
|
+
'build',
|
|
19
|
+
'vendor',
|
|
20
|
+
'.aia',
|
|
21
|
+
'coverage',
|
|
22
|
+
'__pycache__',
|
|
23
|
+
'.next',
|
|
24
|
+
'.nuxt',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect the icon for an app based on its manifest file
|
|
29
|
+
*/
|
|
30
|
+
async function detectIcon(appPath, manifestType) {
|
|
31
|
+
// For node projects, check for framework-specific indicators
|
|
32
|
+
if (manifestType === 'node') {
|
|
33
|
+
const pkgPath = path.join(appPath, 'package.json');
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
36
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
37
|
+
|
|
38
|
+
if (deps.react || deps['react-dom']) return 'react';
|
|
39
|
+
if (deps.vue) return 'vue';
|
|
40
|
+
if (deps['@angular/core']) return 'angular';
|
|
41
|
+
return 'node';
|
|
42
|
+
} catch {
|
|
43
|
+
return 'node';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return manifestType;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a directory is a git submodule
|
|
52
|
+
*/
|
|
53
|
+
async function isGitSubmodule(dirPath) {
|
|
54
|
+
const gitPath = path.join(dirPath, '.git');
|
|
55
|
+
try {
|
|
56
|
+
const stat = await fs.stat(gitPath);
|
|
57
|
+
// Submodules have a .git file (not directory) pointing to the main repo
|
|
58
|
+
return stat.isFile();
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Scan a directory for apps/submodules
|
|
66
|
+
*/
|
|
67
|
+
export async function scanApps(root = process.cwd()) {
|
|
68
|
+
const apps = [];
|
|
69
|
+
const visited = new Set();
|
|
70
|
+
|
|
71
|
+
async function scan(dir, depth = 0) {
|
|
72
|
+
// Limit depth to avoid deep recursion
|
|
73
|
+
if (depth > 3) return;
|
|
74
|
+
|
|
75
|
+
const relativePath = path.relative(root, dir);
|
|
76
|
+
if (visited.has(relativePath)) return;
|
|
77
|
+
visited.add(relativePath);
|
|
78
|
+
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for manifest files in current directory
|
|
87
|
+
let foundManifest = null;
|
|
88
|
+
for (const [manifest, type] of Object.entries(MANIFEST_FILES)) {
|
|
89
|
+
if (entries.some(e => e.isFile() && e.name === manifest)) {
|
|
90
|
+
foundManifest = { manifest, type };
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if this is a git submodule
|
|
96
|
+
const isSubmodule = await isGitSubmodule(dir);
|
|
97
|
+
|
|
98
|
+
// If we found a manifest or submodule (not at root), add as app
|
|
99
|
+
if ((foundManifest || isSubmodule) && relativePath) {
|
|
100
|
+
const appName = path.basename(dir);
|
|
101
|
+
const iconType = foundManifest
|
|
102
|
+
? await detectIcon(dir, foundManifest.type)
|
|
103
|
+
: 'generic';
|
|
104
|
+
|
|
105
|
+
apps.push({
|
|
106
|
+
name: appName,
|
|
107
|
+
path: relativePath,
|
|
108
|
+
icon: APP_ICONS[iconType] || APP_ICONS.generic,
|
|
109
|
+
enabled: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Don't scan deeper if we found an app
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Recurse into subdirectories
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (!entry.isDirectory()) continue;
|
|
119
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
120
|
+
if (entry.name.startsWith('.')) continue;
|
|
121
|
+
|
|
122
|
+
await scan(path.join(dir, entry.name), depth + 1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await scan(root);
|
|
127
|
+
|
|
128
|
+
// Sort by name
|
|
129
|
+
apps.sort((a, b) => a.name.localeCompare(b.name));
|
|
130
|
+
|
|
131
|
+
return apps;
|
|
132
|
+
}
|
package/src/services/config.js
CHANGED
|
@@ -4,6 +4,11 @@ import fs from 'fs-extra';
|
|
|
4
4
|
import yaml from 'yaml';
|
|
5
5
|
import { AIA_DIR } from '../constants.js';
|
|
6
6
|
|
|
7
|
+
// Project config path helper
|
|
8
|
+
function projectConfigPath(root) {
|
|
9
|
+
return path.join(root, AIA_DIR, 'config.yaml');
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
// Global user config directory
|
|
8
13
|
const GLOBAL_AIA_DIR = path.join(os.homedir(), '.aia');
|
|
9
14
|
const GLOBAL_CONFIG_PATH = path.join(GLOBAL_AIA_DIR, 'config.yaml');
|
|
@@ -103,3 +108,39 @@ export async function updateGlobalConfig(updates) {
|
|
|
103
108
|
export function getGlobalConfigPath() {
|
|
104
109
|
return GLOBAL_CONFIG_PATH;
|
|
105
110
|
}
|
|
111
|
+
|
|
112
|
+
// Project config helpers for apps
|
|
113
|
+
export async function loadProjectConfig(root = process.cwd()) {
|
|
114
|
+
const configPath = projectConfigPath(root);
|
|
115
|
+
if (!(await fs.pathExists(configPath))) {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
119
|
+
return yaml.parse(raw) || {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function getApps(root = process.cwd()) {
|
|
123
|
+
const config = await loadProjectConfig(root);
|
|
124
|
+
return config.apps || [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function setApps(apps, root = process.cwd()) {
|
|
128
|
+
const configPath = projectConfigPath(root);
|
|
129
|
+
let config = {};
|
|
130
|
+
if (await fs.pathExists(configPath)) {
|
|
131
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
132
|
+
config = yaml.parse(raw) || {};
|
|
133
|
+
}
|
|
134
|
+
config.apps = apps;
|
|
135
|
+
await fs.writeFile(configPath, yaml.stringify(config), 'utf-8');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function toggleApp(appName, enabled, root = process.cwd()) {
|
|
139
|
+
const apps = await getApps(root);
|
|
140
|
+
const app = apps.find(a => a.name === appName);
|
|
141
|
+
if (!app) {
|
|
142
|
+
throw new Error(`App "${appName}" not found`);
|
|
143
|
+
}
|
|
144
|
+
app.enabled = Boolean(enabled);
|
|
145
|
+
await setApps(apps, root);
|
|
146
|
+
}
|
package/src/services/feature.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
|
-
import { AIA_DIR, FEATURE_STEPS } from '../constants.js';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS, DEFAULT_FEATURE_TYPE, FEATURE_TYPES } from '../constants.js';
|
|
5
5
|
|
|
6
6
|
const FEATURE_FILES = [
|
|
7
7
|
'status.yaml',
|
|
@@ -36,21 +36,42 @@ function buildInitMd(name) {
|
|
|
36
36
|
`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function buildStatusYaml(name) {
|
|
39
|
+
function buildStatusYaml(name, { type = DEFAULT_FEATURE_TYPE, apps = [] } = {}) {
|
|
40
|
+
// Validate type
|
|
41
|
+
const validType = FEATURE_TYPES.includes(type) ? type : DEFAULT_FEATURE_TYPE;
|
|
42
|
+
|
|
43
|
+
// Validate apps - ensure it's an array of strings
|
|
44
|
+
const validApps = Array.isArray(apps)
|
|
45
|
+
? apps.filter(a => typeof a === 'string' && a.trim()).map(a => a.trim())
|
|
46
|
+
: [];
|
|
47
|
+
|
|
40
48
|
const steps = {};
|
|
41
49
|
for (const step of FEATURE_STEPS) {
|
|
42
50
|
steps[step] = 'pending';
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
// Bug type defaults to quick flow, feature type defaults to full flow
|
|
54
|
+
const flow = validType === 'bug' ? 'quick' : 'full';
|
|
55
|
+
const currentStep = flow === 'quick' ? 'dev-plan' : 'brief';
|
|
56
|
+
|
|
57
|
+
const status = {
|
|
46
58
|
feature: name,
|
|
47
|
-
|
|
59
|
+
type: validType,
|
|
60
|
+
flow,
|
|
61
|
+
current_step: currentStep,
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
48
63
|
steps,
|
|
49
64
|
knowledge: ['backend'],
|
|
50
|
-
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (validApps.length > 0) {
|
|
68
|
+
status.apps = validApps;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return yaml.stringify(status);
|
|
51
72
|
}
|
|
52
73
|
|
|
53
|
-
export async function createFeature(name, root = process.cwd()) {
|
|
74
|
+
export async function createFeature(name, root = process.cwd(), options = {}) {
|
|
54
75
|
validateFeatureName(name);
|
|
55
76
|
|
|
56
77
|
const featureDir = path.join(root, AIA_DIR, 'features', name);
|
|
@@ -64,7 +85,7 @@ export async function createFeature(name, root = process.cwd()) {
|
|
|
64
85
|
for (const file of FEATURE_FILES) {
|
|
65
86
|
const filePath = path.join(featureDir, file);
|
|
66
87
|
let content = '';
|
|
67
|
-
if (file === 'status.yaml') content = buildStatusYaml(name);
|
|
88
|
+
if (file === 'status.yaml') content = buildStatusYaml(name, options);
|
|
68
89
|
else if (file === 'init.md') content = buildInitMd(name);
|
|
69
90
|
await fs.writeFile(filePath, content, 'utf-8');
|
|
70
91
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { resolveModelAlias } from '../providers/registry.js';
|
|
3
3
|
|
|
4
|
-
export async function callModel(model, prompt, { verbose = false, apply = false, onData } = {}) {
|
|
4
|
+
export async function callModel(model, prompt, { verbose = false, apply = false, onData, cwd } = {}) {
|
|
5
5
|
const resolved = resolveModelAlias(model);
|
|
6
6
|
const displayName = resolved.model ?? `${model} (CLI default)`;
|
|
7
7
|
const mode = apply ? 'agent' : 'print';
|
|
8
8
|
|
|
9
9
|
console.log(chalk.yellow(`[AI] Calling ${displayName} (${mode} mode)...`));
|
|
10
10
|
|
|
11
|
-
return resolved.provider.generate(prompt, resolved.model, { verbose, apply, onData });
|
|
11
|
+
return resolved.provider.generate(prompt, resolved.model, { verbose, apply, onData, cwd });
|
|
12
12
|
}
|
package/src/services/runner.js
CHANGED
|
@@ -7,6 +7,8 @@ import { buildPrompt } from '../prompt-builder.js';
|
|
|
7
7
|
import { callModel } from './model-call.js';
|
|
8
8
|
import { loadStatus, updateStepStatus } from './status.js';
|
|
9
9
|
import { logExecution } from '../logger.js';
|
|
10
|
+
import { startSession, endSession, appendLog } from './agent-sessions.js';
|
|
11
|
+
import { hasWorktree, getWorktreePath, getFeatureBranch } from './worktrunk.js';
|
|
10
12
|
|
|
11
13
|
export async function runStep(step, feature, { description, instructions, history, attachments, model: modelOverride, verbose = false, apply = false, root = process.cwd(), onData } = {}) {
|
|
12
14
|
if (!FEATURE_STEPS.includes(step)) {
|
|
@@ -25,12 +27,29 @@ export async function runStep(step, feature, { description, instructions, histor
|
|
|
25
27
|
|
|
26
28
|
const shouldApply = apply || APPLY_STEPS.has(step);
|
|
27
29
|
|
|
30
|
+
// Start agent session for tracking
|
|
31
|
+
startSession(feature, step);
|
|
32
|
+
|
|
33
|
+
// Note: Claude CLI has a bug where it hangs in git worktrees
|
|
34
|
+
// So we always use the main repo root as CWD for now
|
|
35
|
+
// TODO: Re-enable worktree CWD when Claude CLI fixes this
|
|
36
|
+
// const branch = getFeatureBranch(feature);
|
|
37
|
+
// const wtPath = hasWorktree(branch, root) ? getWorktreePath(branch, root) : null;
|
|
38
|
+
// const cwd = wtPath || root;
|
|
39
|
+
const cwd = root;
|
|
40
|
+
|
|
41
|
+
// Wrapper onData to buffer logs in session
|
|
42
|
+
const wrappedOnData = (data) => {
|
|
43
|
+
appendLog(feature, data.text, data.type);
|
|
44
|
+
if (onData) onData(data);
|
|
45
|
+
};
|
|
46
|
+
|
|
28
47
|
try {
|
|
29
48
|
const model = modelOverride || await resolveModel(step, root);
|
|
30
49
|
const prompt = await buildPrompt(feature, step, { description, instructions, history, attachments, root });
|
|
31
50
|
|
|
32
51
|
const start = performance.now();
|
|
33
|
-
const output = await callModel(model, prompt, { verbose, apply: shouldApply, onData });
|
|
52
|
+
const output = await callModel(model, prompt, { verbose, apply: shouldApply, onData: wrappedOnData, cwd });
|
|
34
53
|
const duration = performance.now() - start;
|
|
35
54
|
|
|
36
55
|
const outputPath = path.join(root, AIA_DIR, 'features', feature, `${step}.md`);
|
|
@@ -44,5 +63,8 @@ export async function runStep(step, feature, { description, instructions, histor
|
|
|
44
63
|
} catch (err) {
|
|
45
64
|
await updateStepStatus(feature, step, STEP_STATUS.ERROR, root);
|
|
46
65
|
throw err;
|
|
66
|
+
} finally {
|
|
67
|
+
// End session regardless of success or failure
|
|
68
|
+
endSession(feature);
|
|
47
69
|
}
|
|
48
70
|
}
|
package/src/services/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
|
-
import { AIA_DIR, FEATURE_STEPS, STEP_STATUS } from '../constants.js';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS, STEP_STATUS, FEATURE_TYPES } from '../constants.js';
|
|
5
5
|
|
|
6
6
|
function statusPath(feature, root) {
|
|
7
7
|
return path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
|
|
@@ -79,3 +79,71 @@ export async function resetStep(feature, step, root = process.cwd()) {
|
|
|
79
79
|
|
|
80
80
|
// Keep the existing output — it will be fed back as context on re-run
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
export async function updateType(feature, type, root = process.cwd()) {
|
|
84
|
+
// Validate type
|
|
85
|
+
if (!FEATURE_TYPES.includes(type)) {
|
|
86
|
+
throw new Error(`Invalid type "${type}". Valid types: ${FEATURE_TYPES.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const status = await loadStatus(feature, root);
|
|
90
|
+
status.type = type;
|
|
91
|
+
|
|
92
|
+
// Bug type forces quick flow, but only if no steps have been completed
|
|
93
|
+
if (type === 'bug') {
|
|
94
|
+
const allPending = Object.values(status.steps).every(s => s === STEP_STATUS.PENDING);
|
|
95
|
+
if (allPending) {
|
|
96
|
+
status.flow = 'quick';
|
|
97
|
+
status.current_step = 'dev-plan';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const content = yaml.stringify(status);
|
|
102
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function updateApps(feature, apps, root = process.cwd()) {
|
|
106
|
+
// Validate apps - must be an array of strings
|
|
107
|
+
if (!Array.isArray(apps)) {
|
|
108
|
+
throw new Error('apps must be an array');
|
|
109
|
+
}
|
|
110
|
+
const validApps = apps.filter(a => typeof a === 'string' && a.trim()).map(a => a.trim());
|
|
111
|
+
|
|
112
|
+
const status = await loadStatus(feature, root);
|
|
113
|
+
status.apps = validApps;
|
|
114
|
+
const content = yaml.stringify(status);
|
|
115
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Soft delete a feature by setting deletedAt timestamp
|
|
120
|
+
* @param {string} feature - Feature name
|
|
121
|
+
* @param {string} root - Project root directory
|
|
122
|
+
*/
|
|
123
|
+
export async function softDeleteFeature(feature, root = process.cwd()) {
|
|
124
|
+
const status = await loadStatus(feature, root);
|
|
125
|
+
status.deletedAt = new Date().toISOString();
|
|
126
|
+
const content = yaml.stringify(status);
|
|
127
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Restore a deleted feature by clearing deletedAt
|
|
132
|
+
* @param {string} feature - Feature name
|
|
133
|
+
* @param {string} root - Project root directory
|
|
134
|
+
*/
|
|
135
|
+
export async function restoreFeature(feature, root = process.cwd()) {
|
|
136
|
+
const status = await loadStatus(feature, root);
|
|
137
|
+
delete status.deletedAt;
|
|
138
|
+
const content = yaml.stringify(status);
|
|
139
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a feature is deleted
|
|
144
|
+
* @param {object} status - Feature status object
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
export function isFeatureDeleted(status) {
|
|
148
|
+
return !!(status && status.deletedAt != null);
|
|
149
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Service layer for test-quick feature
|
|
3
|
+
* Provides business logic for managing test-quick items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import yaml from 'yaml';
|
|
9
|
+
import { AIA_DIR } from '../constants.js';
|
|
10
|
+
import {
|
|
11
|
+
TEST_QUICK_STATUS,
|
|
12
|
+
isValidTestQuickName,
|
|
13
|
+
createTestQuickItem,
|
|
14
|
+
} from '../types/test-quick.js';
|
|
15
|
+
|
|
16
|
+
const TEST_QUICK_DIR = 'test-quick';
|
|
17
|
+
const DATA_FILE = 'items.yaml';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets the test-quick directory path
|
|
21
|
+
* @param {string} root - Project root directory
|
|
22
|
+
* @returns {string} Path to test-quick directory
|
|
23
|
+
*/
|
|
24
|
+
function getTestQuickDir(root) {
|
|
25
|
+
return path.join(root, AIA_DIR, TEST_QUICK_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gets the data file path
|
|
30
|
+
* @param {string} root - Project root directory
|
|
31
|
+
* @returns {string} Path to data file
|
|
32
|
+
*/
|
|
33
|
+
function getDataFilePath(root) {
|
|
34
|
+
return path.join(getTestQuickDir(root), DATA_FILE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Ensures the test-quick directory exists
|
|
39
|
+
* @param {string} root - Project root directory
|
|
40
|
+
*/
|
|
41
|
+
async function ensureTestQuickDir(root) {
|
|
42
|
+
await fs.ensureDir(getTestQuickDir(root));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Loads all test-quick items from storage
|
|
47
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
48
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickItem[]>} List of items
|
|
49
|
+
*/
|
|
50
|
+
export async function loadItems(root = process.cwd()) {
|
|
51
|
+
const dataPath = getDataFilePath(root);
|
|
52
|
+
|
|
53
|
+
if (!(await fs.pathExists(dataPath))) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const content = await fs.readFile(dataPath, 'utf-8');
|
|
58
|
+
const data = yaml.parse(content);
|
|
59
|
+
|
|
60
|
+
return Array.isArray(data?.items) ? data.items : [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Saves all test-quick items to storage
|
|
65
|
+
* @param {import('../types/test-quick.js').TestQuickItem[]} items - Items to save
|
|
66
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
67
|
+
*/
|
|
68
|
+
async function saveItems(items, root = process.cwd()) {
|
|
69
|
+
await ensureTestQuickDir(root);
|
|
70
|
+
const dataPath = getDataFilePath(root);
|
|
71
|
+
const content = yaml.stringify({ items, updatedAt: new Date().toISOString() });
|
|
72
|
+
await fs.writeFile(dataPath, content, 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates a new test-quick item
|
|
77
|
+
* @param {Object} data - Item data
|
|
78
|
+
* @param {string} data.name - Item name
|
|
79
|
+
* @param {string} [data.description] - Item description
|
|
80
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
81
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Creation result
|
|
82
|
+
*/
|
|
83
|
+
export async function createItem(data, root = process.cwd()) {
|
|
84
|
+
const { name, description } = data;
|
|
85
|
+
|
|
86
|
+
if (!name) {
|
|
87
|
+
return { success: false, error: 'Name is required' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!isValidTestQuickName(name)) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: 'Invalid name. Use lowercase alphanumeric with hyphens (e.g., my-item)',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const items = await loadItems(root);
|
|
98
|
+
|
|
99
|
+
if (items.some(item => item.name === name)) {
|
|
100
|
+
return { success: false, error: `Item "${name}" already exists` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const newItem = createTestQuickItem({ name, description });
|
|
104
|
+
items.push(newItem);
|
|
105
|
+
|
|
106
|
+
await saveItems(items, root);
|
|
107
|
+
|
|
108
|
+
return { success: true, message: `Item "${name}" created`, data: newItem };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gets a single test-quick item by name
|
|
113
|
+
* @param {string} name - Item name
|
|
114
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
115
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Item result
|
|
116
|
+
*/
|
|
117
|
+
export async function getItem(name, root = process.cwd()) {
|
|
118
|
+
const items = await loadItems(root);
|
|
119
|
+
const item = items.find(i => i.name === name);
|
|
120
|
+
|
|
121
|
+
if (!item) {
|
|
122
|
+
return { success: false, error: `Item "${name}" not found` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { success: true, data: item };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Lists all test-quick items with optional filtering
|
|
130
|
+
* @param {Object} [options={}] - Filter options
|
|
131
|
+
* @param {string} [options.status] - Filter by status
|
|
132
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
133
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickListResult>} List result
|
|
134
|
+
*/
|
|
135
|
+
export async function listItems(options = {}, root = process.cwd()) {
|
|
136
|
+
let items = await loadItems(root);
|
|
137
|
+
|
|
138
|
+
if (options.status) {
|
|
139
|
+
items = items.filter(item => item.status === options.status);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { success: true, items, total: items.length };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Updates an existing test-quick item
|
|
147
|
+
* @param {string} name - Item name to update
|
|
148
|
+
* @param {Object} updates - Fields to update
|
|
149
|
+
* @param {string} [updates.description] - New description
|
|
150
|
+
* @param {string} [updates.status] - New status
|
|
151
|
+
* @param {Object} [updates.metadata] - New metadata
|
|
152
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
153
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Update result
|
|
154
|
+
*/
|
|
155
|
+
export async function updateItem(name, updates, root = process.cwd()) {
|
|
156
|
+
const items = await loadItems(root);
|
|
157
|
+
const index = items.findIndex(i => i.name === name);
|
|
158
|
+
|
|
159
|
+
if (index === -1) {
|
|
160
|
+
return { success: false, error: `Item "${name}" not found` };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const validStatuses = Object.values(TEST_QUICK_STATUS);
|
|
164
|
+
if (updates.status && !validStatuses.includes(updates.status)) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: `Invalid status. Valid values: ${validStatuses.join(', ')}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const updatedItem = {
|
|
172
|
+
...items[index],
|
|
173
|
+
...updates,
|
|
174
|
+
updatedAt: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
items[index] = updatedItem;
|
|
178
|
+
await saveItems(items, root);
|
|
179
|
+
|
|
180
|
+
return { success: true, message: `Item "${name}" updated`, data: updatedItem };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Deletes a test-quick item
|
|
185
|
+
* @param {string} name - Item name to delete
|
|
186
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
187
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Delete result
|
|
188
|
+
*/
|
|
189
|
+
export async function deleteItem(name, root = process.cwd()) {
|
|
190
|
+
const items = await loadItems(root);
|
|
191
|
+
const index = items.findIndex(i => i.name === name);
|
|
192
|
+
|
|
193
|
+
if (index === -1) {
|
|
194
|
+
return { success: false, error: `Item "${name}" not found` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const deletedItem = items[index];
|
|
198
|
+
items.splice(index, 1);
|
|
199
|
+
|
|
200
|
+
await saveItems(items, root);
|
|
201
|
+
|
|
202
|
+
return { success: true, message: `Item "${name}" deleted`, data: deletedItem };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Updates the status of a test-quick item
|
|
207
|
+
* @param {string} name - Item name
|
|
208
|
+
* @param {string} status - New status
|
|
209
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
210
|
+
* @returns {Promise<import('../types/test-quick.js').TestQuickResult>} Update result
|
|
211
|
+
*/
|
|
212
|
+
export async function updateStatus(name, status, root = process.cwd()) {
|
|
213
|
+
return updateItem(name, { status }, root);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Clears all completed items
|
|
218
|
+
* @param {string} [root=process.cwd()] - Project root directory
|
|
219
|
+
* @returns {Promise<{success: boolean, cleared: number}>} Clear result
|
|
220
|
+
*/
|
|
221
|
+
export async function clearCompleted(root = process.cwd()) {
|
|
222
|
+
const items = await loadItems(root);
|
|
223
|
+
const remaining = items.filter(item => item.status !== TEST_QUICK_STATUS.COMPLETED);
|
|
224
|
+
const clearedCount = items.length - remaining.length;
|
|
225
|
+
|
|
226
|
+
await saveItems(remaining, root);
|
|
227
|
+
|
|
228
|
+
return { success: true, cleared: clearedCount };
|
|
229
|
+
}
|