opencode-hive 0.8.0 → 0.8.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.
Files changed (48) hide show
  1. package/dist/index.js +19251 -587
  2. package/package.json +4 -3
  3. package/dist/e2e/opencode-runtime-smoke.test.d.ts +0 -1
  4. package/dist/e2e/opencode-runtime-smoke.test.js +0 -243
  5. package/dist/e2e/plugin-smoke.test.d.ts +0 -1
  6. package/dist/e2e/plugin-smoke.test.js +0 -127
  7. package/dist/services/contextService.d.ts +0 -15
  8. package/dist/services/contextService.js +0 -59
  9. package/dist/services/featureService.d.ts +0 -14
  10. package/dist/services/featureService.js +0 -107
  11. package/dist/services/featureService.test.d.ts +0 -1
  12. package/dist/services/featureService.test.js +0 -127
  13. package/dist/services/index.d.ts +0 -5
  14. package/dist/services/index.js +0 -4
  15. package/dist/services/planService.d.ts +0 -11
  16. package/dist/services/planService.js +0 -59
  17. package/dist/services/planService.test.d.ts +0 -1
  18. package/dist/services/planService.test.js +0 -115
  19. package/dist/services/sessionService.d.ts +0 -31
  20. package/dist/services/sessionService.js +0 -125
  21. package/dist/services/taskService.d.ts +0 -29
  22. package/dist/services/taskService.js +0 -382
  23. package/dist/services/taskService.test.d.ts +0 -1
  24. package/dist/services/taskService.test.js +0 -290
  25. package/dist/services/worktreeService.d.ts +0 -66
  26. package/dist/services/worktreeService.js +0 -498
  27. package/dist/services/worktreeService.test.d.ts +0 -1
  28. package/dist/services/worktreeService.test.js +0 -185
  29. package/dist/tools/contextTools.d.ts +0 -93
  30. package/dist/tools/contextTools.js +0 -83
  31. package/dist/tools/execTools.d.ts +0 -66
  32. package/dist/tools/execTools.js +0 -125
  33. package/dist/tools/featureTools.d.ts +0 -60
  34. package/dist/tools/featureTools.js +0 -73
  35. package/dist/tools/planTools.d.ts +0 -47
  36. package/dist/tools/planTools.js +0 -65
  37. package/dist/tools/sessionTools.d.ts +0 -35
  38. package/dist/tools/sessionTools.js +0 -95
  39. package/dist/tools/taskTools.d.ts +0 -79
  40. package/dist/tools/taskTools.js +0 -86
  41. package/dist/types.d.ts +0 -106
  42. package/dist/types.js +0 -1
  43. package/dist/utils/detection.d.ts +0 -12
  44. package/dist/utils/detection.js +0 -73
  45. package/dist/utils/paths.d.ts +0 -23
  46. package/dist/utils/paths.js +0 -92
  47. package/dist/utils/paths.test.d.ts +0 -1
  48. package/dist/utils/paths.test.js +0 -100
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-hive",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent Hive - from vibe coding to hive coding",
6
6
  "license": "MIT WITH Commons-Clause",
@@ -27,7 +27,7 @@
27
27
  "types": "./dist/index.d.ts",
28
28
  "scripts": {
29
29
  "clean": "rm -rf dist",
30
- "build": "npm run clean && tsc",
30
+ "build": "npm run clean && bun build src/index.ts --outdir dist --target node --format esm && tsc --emitDeclarationOnly",
31
31
  "dev": "opencode plugin dev",
32
32
  "test": "bun test"
33
33
  },
@@ -35,10 +35,11 @@
35
35
  "@opencode-ai/plugin": ">=0.13.7"
36
36
  },
37
37
  "dependencies": {
38
- "simple-git": "^3.27.0"
38
+ "hive-core": "workspace:*"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@opencode-ai/plugin": "^1.0.143",
42
+ "@opencode-ai/sdk": "^0.13.0",
42
43
  "@types/bun": "^1.2.0",
43
44
  "@types/node": "^20.0.0",
44
45
  "typescript": "^5.0.0"
@@ -1 +0,0 @@
1
- export {};
@@ -1,243 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import { createServer } from "net";
5
- import { createOpencodeClient, createOpencodeServer, } from "@opencode-ai/sdk";
6
- const EXPECTED_TOOLS = [
7
- "hive_feature_create",
8
- "hive_plan_write",
9
- "hive_plan_read",
10
- "hive_tasks_sync",
11
- "hive_exec_start",
12
- ];
13
- function isRecord(value) {
14
- return !!value && typeof value === "object";
15
- }
16
- function firstKey(record) {
17
- const keys = Object.keys(record);
18
- return keys.length > 0 ? keys[0] : null;
19
- }
20
- async function getDefaultModel(client) {
21
- const providers = (await client.provider.list());
22
- if (!isRecord(providers))
23
- return null;
24
- const connected = providers.connected;
25
- if (!Array.isArray(connected) || connected.length === 0)
26
- return null;
27
- const all = providers.all;
28
- if (!Array.isArray(all))
29
- return null;
30
- const defaultMap = providers.default;
31
- const providerID = typeof connected[0] === "string" ? connected[0] : null;
32
- if (!providerID)
33
- return null;
34
- const providerEntry = all.find((p) => isRecord(p) && p.id === providerID);
35
- if (!isRecord(providerEntry))
36
- return null;
37
- const models = providerEntry.models;
38
- if (!isRecord(models))
39
- return null;
40
- const supportsToolCall = (modelInfo) => {
41
- if (!isRecord(modelInfo))
42
- return false;
43
- const v = modelInfo.tool_call ?? modelInfo.toolcall;
44
- return v === true;
45
- };
46
- const defaultModelID = isRecord(defaultMap) && typeof defaultMap[providerID] === "string"
47
- ? defaultMap[providerID]
48
- : null;
49
- if (defaultModelID && supportsToolCall(models[defaultModelID])) {
50
- return { providerID, modelID: defaultModelID };
51
- }
52
- for (const modelID of Object.keys(models)) {
53
- if (supportsToolCall(models[modelID])) {
54
- return { providerID, modelID };
55
- }
56
- }
57
- const fallback = defaultModelID ?? firstKey(models);
58
- if (!fallback)
59
- return null;
60
- return { providerID, modelID: fallback };
61
- }
62
- async function getFreePort() {
63
- return await new Promise((resolve, reject) => {
64
- const server = createServer();
65
- server.unref();
66
- server.on("error", reject);
67
- server.listen(0, "127.0.0.1", () => {
68
- const address = server.address();
69
- if (typeof address !== "object" || !address) {
70
- server.close();
71
- reject(new Error("Failed to get free port"));
72
- return;
73
- }
74
- const port = address.port;
75
- server.close(() => resolve(port));
76
- });
77
- });
78
- }
79
- function safeRm(dir) {
80
- fs.rmSync(dir, { recursive: true, force: true });
81
- }
82
- function pickHivePluginEntry() {
83
- const distEntry = path.resolve(import.meta.dir, "..", "..", "dist", "index.js");
84
- if (fs.existsSync(distEntry))
85
- return distEntry;
86
- const tsEntry = path.resolve(import.meta.dir, "..", "index.ts");
87
- return tsEntry;
88
- }
89
- function extractStringArray(raw) {
90
- if (Array.isArray(raw) && raw.every((v) => typeof v === "string"))
91
- return raw;
92
- if (isRecord(raw) && Array.isArray(raw.ids) && raw.ids.every((v) => typeof v === "string")) {
93
- return raw.ids;
94
- }
95
- return [];
96
- }
97
- async function waitForTools(idsProvider, expected, timeoutMs) {
98
- const deadline = Date.now() + timeoutMs;
99
- while (Date.now() < deadline) {
100
- const ids = await idsProvider();
101
- const ok = expected.every((t) => ids.includes(t));
102
- if (ok)
103
- return ids;
104
- await new Promise((r) => setTimeout(r, 200));
105
- }
106
- return await idsProvider();
107
- }
108
- describe("e2e: OpenCode runtime loads opencode-hive", () => {
109
- it("exposes hive tools via /experimental/tool/ids", async () => {
110
- const tmpBase = "/tmp/hive-e2e-runtime";
111
- safeRm(tmpBase);
112
- fs.mkdirSync(tmpBase, { recursive: true });
113
- const projectDir = fs.mkdtempSync(path.join(tmpBase, "project-"));
114
- fs.mkdirSync(path.join(projectDir, ".opencode", "plugin"), { recursive: true });
115
- const hivePluginEntry = pickHivePluginEntry();
116
- const pluginFile = path.join(projectDir, ".opencode", "plugin", "hive.ts");
117
- const pluginSource = `import hive from ${JSON.stringify(hivePluginEntry)}\nexport const HivePlugin = hive\n`;
118
- fs.writeFileSync(pluginFile, pluginSource);
119
- const previousCwd = process.cwd();
120
- const previousConfigDir = process.env.OPENCODE_CONFIG_DIR;
121
- const previousDisableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS;
122
- process.chdir(projectDir);
123
- process.env.OPENCODE_CONFIG_DIR = path.join(projectDir, ".opencode");
124
- process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "true";
125
- const port = await getFreePort();
126
- const config = {
127
- plugin: [],
128
- logLevel: "ERROR",
129
- };
130
- const server = await createOpencodeServer({
131
- hostname: "127.0.0.1",
132
- port,
133
- timeout: 20000,
134
- config,
135
- });
136
- const client = createOpencodeClient({
137
- baseUrl: server.url,
138
- responseStyle: "data",
139
- throwOnError: true,
140
- directory: projectDir,
141
- });
142
- const abortController = new AbortController();
143
- async function approvePermissions(sessionID) {
144
- const sse = await client.event.subscribe({
145
- query: { directory: projectDir },
146
- signal: abortController.signal,
147
- });
148
- for await (const evt of sse.stream) {
149
- if (!evt || typeof evt !== "object")
150
- continue;
151
- const maybeType = evt.type;
152
- if (maybeType !== "permission.updated")
153
- continue;
154
- const properties = evt.properties;
155
- if (!isRecord(properties))
156
- continue;
157
- if (properties.sessionID !== sessionID)
158
- continue;
159
- const permissionID = typeof properties.id === "string" ? properties.id : null;
160
- if (!permissionID)
161
- continue;
162
- await client.postSessionIdPermissionsPermissionId({
163
- path: { id: sessionID, permissionID },
164
- body: { response: "once" },
165
- query: { directory: projectDir },
166
- });
167
- }
168
- }
169
- try {
170
- const ids = await waitForTools(async () => {
171
- const raw = (await client.tool.ids({ query: { directory: projectDir } }));
172
- return extractStringArray(raw);
173
- }, EXPECTED_TOOLS, 10000);
174
- for (const toolName of EXPECTED_TOOLS) {
175
- expect(ids).toContain(toolName);
176
- }
177
- const defaultModel = await getDefaultModel(client);
178
- if (!defaultModel) {
179
- return;
180
- }
181
- const session = (await client.session.create({
182
- body: { title: "hive runtime e2e" },
183
- query: { directory: projectDir },
184
- }));
185
- const sessionID = isRecord(session) && typeof session.id === "string" ? session.id : null;
186
- expect(sessionID).not.toBeNull();
187
- if (!sessionID)
188
- return;
189
- const permissionTask = approvePermissions(sessionID);
190
- const promptResult = await client.session.prompt({
191
- path: { id: sessionID },
192
- query: { directory: projectDir },
193
- body: {
194
- model: defaultModel,
195
- system: "Call the tool hive_feature_create exactly once with {\"name\":\"rt-feature\"}.",
196
- tools: {
197
- hive_feature_create: true,
198
- },
199
- parts: [
200
- {
201
- type: "text",
202
- text: "Create a Hive feature named rt-feature.",
203
- },
204
- ],
205
- },
206
- });
207
- const hasToolPart = Array.isArray(promptResult?.parts)
208
- ? promptResult.parts.some((p) => isRecord(p) && p.type === "tool" && p.tool === "hive_feature_create")
209
- : false;
210
- if (!hasToolPart) {
211
- abortController.abort();
212
- await permissionTask.catch(() => undefined);
213
- return;
214
- }
215
- const featureDir = path.join(projectDir, ".hive", "features", "rt-feature");
216
- const deadline = Date.now() + 20000;
217
- while (Date.now() < deadline && !fs.existsSync(featureDir)) {
218
- await new Promise((r) => setTimeout(r, 200));
219
- }
220
- abortController.abort();
221
- await permissionTask.catch(() => undefined);
222
- expect(fs.existsSync(featureDir)).toBe(true);
223
- }
224
- finally {
225
- abortController.abort();
226
- await server.close();
227
- process.chdir(previousCwd);
228
- if (previousConfigDir === undefined) {
229
- delete process.env.OPENCODE_CONFIG_DIR;
230
- }
231
- else {
232
- process.env.OPENCODE_CONFIG_DIR = previousConfigDir;
233
- }
234
- if (previousDisableDefault === undefined) {
235
- delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS;
236
- }
237
- else {
238
- process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = previousDisableDefault;
239
- }
240
- safeRm(tmpBase);
241
- }
242
- }, 60000);
243
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,127 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import { createOpencodeClient } from "@opencode-ai/sdk";
5
- import plugin from "../index";
6
- const OPENCODE_CLIENT = createOpencodeClient({ baseUrl: "http://localhost:1" });
7
- const EXPECTED_TOOLS = [
8
- "hive_feature_create",
9
- "hive_feature_list",
10
- "hive_feature_complete",
11
- "hive_plan_write",
12
- "hive_plan_read",
13
- "hive_plan_approve",
14
- "hive_tasks_sync",
15
- "hive_task_create",
16
- "hive_task_update",
17
- "hive_exec_start",
18
- "hive_exec_complete",
19
- "hive_exec_abort",
20
- ];
21
- const TEST_ROOT_BASE = "/tmp/hive-e2e-plugin";
22
- function createStubShell() {
23
- let shell;
24
- const fn = ((..._args) => {
25
- throw new Error("shell not available in this test");
26
- });
27
- shell = Object.assign(fn, {
28
- braces(pattern) {
29
- return [pattern];
30
- },
31
- escape(input) {
32
- return input;
33
- },
34
- env() {
35
- return shell;
36
- },
37
- cwd() {
38
- return shell;
39
- },
40
- nothrow() {
41
- return shell;
42
- },
43
- throws() {
44
- return shell;
45
- },
46
- });
47
- return shell;
48
- }
49
- function createToolContext(sessionID) {
50
- return {
51
- sessionID,
52
- messageID: "msg_test",
53
- agent: "test",
54
- abort: new AbortController().signal,
55
- };
56
- }
57
- function createProject(worktree) {
58
- return {
59
- id: "test",
60
- worktree,
61
- time: { created: Date.now() },
62
- };
63
- }
64
- describe("e2e: opencode-hive plugin (in-process)", () => {
65
- let testRoot;
66
- beforeEach(() => {
67
- fs.rmSync(TEST_ROOT_BASE, { recursive: true, force: true });
68
- fs.mkdirSync(TEST_ROOT_BASE, { recursive: true });
69
- testRoot = fs.mkdtempSync(path.join(TEST_ROOT_BASE, "project-"));
70
- });
71
- afterEach(() => {
72
- fs.rmSync(TEST_ROOT_BASE, { recursive: true, force: true });
73
- });
74
- it("registers expected tools and basic workflow works", async () => {
75
- const ctx = {
76
- directory: testRoot,
77
- worktree: testRoot,
78
- serverUrl: new URL("http://localhost:1"),
79
- project: createProject(testRoot),
80
- client: OPENCODE_CLIENT,
81
- $: createStubShell(),
82
- };
83
- const hooks = await plugin(ctx);
84
- expect(hooks.tool).toBeDefined();
85
- for (const toolName of EXPECTED_TOOLS) {
86
- expect(hooks.tool?.[toolName]).toBeDefined();
87
- expect(typeof hooks.tool?.[toolName].execute).toBe("function");
88
- }
89
- const sessionID = "sess_plugin_smoke";
90
- const toolContext = createToolContext(sessionID);
91
- const createOutput = await hooks.tool.hive_feature_create.execute({ name: "smoke-feature" }, toolContext);
92
- expect(createOutput).toContain('Feature "smoke-feature" created');
93
- const plan = `# Smoke Feature\n\n## Overview\n\nTest\n\n## Tasks\n\n### 1. First Task\nDo it\n`;
94
- const planOutput = await hooks.tool.hive_plan_write.execute({ content: plan, feature: "smoke-feature" }, toolContext);
95
- expect(planOutput).toContain("Plan written");
96
- const approveOutput = await hooks.tool.hive_plan_approve.execute({ feature: "smoke-feature" }, toolContext);
97
- expect(approveOutput).toContain("Plan approved");
98
- const syncOutput = await hooks.tool.hive_tasks_sync.execute({ feature: "smoke-feature" }, toolContext);
99
- expect(syncOutput).toContain("Tasks synced");
100
- const taskFolder = path.join(testRoot, ".hive", "features", "smoke-feature", "tasks", "01-first-task");
101
- expect(fs.existsSync(taskFolder)).toBe(true);
102
- // Open a session to test session tracking
103
- const sessionOutput = await hooks.tool.hive_session_open.execute({ feature: "smoke-feature" }, toolContext);
104
- expect(sessionOutput).toContain("smoke-feature");
105
- // Session is now stored in sessions.json via SessionService
106
- const sessionsPath = path.join(testRoot, ".hive", "features", "smoke-feature", "sessions.json");
107
- const sessions = JSON.parse(fs.readFileSync(sessionsPath, "utf-8"));
108
- expect(sessions.master).toBe(sessionID);
109
- });
110
- it("system prompt hook injects Hive instructions", async () => {
111
- const ctx = {
112
- directory: testRoot,
113
- worktree: testRoot,
114
- serverUrl: new URL("http://localhost:1"),
115
- project: createProject(testRoot),
116
- client: OPENCODE_CLIENT,
117
- $: createStubShell(),
118
- };
119
- const hooks = await plugin(ctx);
120
- await hooks.tool.hive_feature_create.execute({ name: "active" }, createToolContext("sess"));
121
- const output = { system: [] };
122
- await hooks["experimental.chat.system.transform"]?.({}, output);
123
- const joined = output.system.join("\n");
124
- expect(joined).toContain("## Hive - Feature Development System");
125
- expect(joined).toContain("hive_feature_create");
126
- });
127
- });
@@ -1,15 +0,0 @@
1
- export interface ContextFile {
2
- name: string;
3
- content: string;
4
- updatedAt: string;
5
- }
6
- export declare class ContextService {
7
- private projectRoot;
8
- constructor(projectRoot: string);
9
- write(featureName: string, fileName: string, content: string): string;
10
- read(featureName: string, fileName: string): string | null;
11
- list(featureName: string): ContextFile[];
12
- delete(featureName: string, fileName: string): boolean;
13
- compile(featureName: string): string;
14
- private normalizeFileName;
15
- }
@@ -1,59 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { getContextPath, ensureDir, fileExists, readText, writeText } from '../utils/paths.js';
4
- export class ContextService {
5
- projectRoot;
6
- constructor(projectRoot) {
7
- this.projectRoot = projectRoot;
8
- }
9
- write(featureName, fileName, content) {
10
- const contextPath = getContextPath(this.projectRoot, featureName);
11
- ensureDir(contextPath);
12
- const filePath = path.join(contextPath, this.normalizeFileName(fileName));
13
- writeText(filePath, content);
14
- return filePath;
15
- }
16
- read(featureName, fileName) {
17
- const contextPath = getContextPath(this.projectRoot, featureName);
18
- const filePath = path.join(contextPath, this.normalizeFileName(fileName));
19
- return readText(filePath);
20
- }
21
- list(featureName) {
22
- const contextPath = getContextPath(this.projectRoot, featureName);
23
- if (!fileExists(contextPath))
24
- return [];
25
- const files = fs.readdirSync(contextPath, { withFileTypes: true })
26
- .filter(f => f.isFile() && f.name.endsWith('.md'))
27
- .map(f => f.name);
28
- return files.map(name => {
29
- const filePath = path.join(contextPath, name);
30
- const stat = fs.statSync(filePath);
31
- const content = readText(filePath) || '';
32
- return {
33
- name: name.replace(/\.md$/, ''),
34
- content,
35
- updatedAt: stat.mtime.toISOString(),
36
- };
37
- });
38
- }
39
- delete(featureName, fileName) {
40
- const contextPath = getContextPath(this.projectRoot, featureName);
41
- const filePath = path.join(contextPath, this.normalizeFileName(fileName));
42
- if (fileExists(filePath)) {
43
- fs.unlinkSync(filePath);
44
- return true;
45
- }
46
- return false;
47
- }
48
- compile(featureName) {
49
- const files = this.list(featureName);
50
- if (files.length === 0)
51
- return '';
52
- const sections = files.map(f => `## ${f.name}\n\n${f.content}`);
53
- return sections.join('\n\n---\n\n');
54
- }
55
- normalizeFileName(name) {
56
- const normalized = name.replace(/\.md$/, '');
57
- return `${normalized}.md`;
58
- }
59
- }
@@ -1,14 +0,0 @@
1
- import { FeatureJson, FeatureStatusType, FeatureInfo } from '../types.js';
2
- export declare class FeatureService {
3
- private projectRoot;
4
- constructor(projectRoot: string);
5
- create(name: string, ticket?: string): FeatureJson;
6
- get(name: string): FeatureJson | null;
7
- list(): string[];
8
- updateStatus(name: string, status: FeatureStatusType): FeatureJson;
9
- getInfo(name: string): FeatureInfo | null;
10
- private getTasks;
11
- complete(name: string): FeatureJson;
12
- setSession(name: string, sessionId: string): void;
13
- getSession(name: string): string | undefined;
14
- }
@@ -1,107 +0,0 @@
1
- import * as fs from 'fs';
2
- import { getFeaturePath, getFeaturesPath, getFeatureJsonPath, getContextPath, getTasksPath, getPlanPath, getCommentsPath, ensureDir, readJson, writeJson, fileExists, } from '../utils/paths.js';
3
- export class FeatureService {
4
- projectRoot;
5
- constructor(projectRoot) {
6
- this.projectRoot = projectRoot;
7
- }
8
- create(name, ticket) {
9
- const featurePath = getFeaturePath(this.projectRoot, name);
10
- if (fileExists(featurePath)) {
11
- throw new Error(`Feature '${name}' already exists`);
12
- }
13
- ensureDir(featurePath);
14
- ensureDir(getContextPath(this.projectRoot, name));
15
- ensureDir(getTasksPath(this.projectRoot, name));
16
- const feature = {
17
- name,
18
- status: 'planning',
19
- ticket,
20
- createdAt: new Date().toISOString(),
21
- };
22
- writeJson(getFeatureJsonPath(this.projectRoot, name), feature);
23
- return feature;
24
- }
25
- get(name) {
26
- return readJson(getFeatureJsonPath(this.projectRoot, name));
27
- }
28
- list() {
29
- const featuresPath = getFeaturesPath(this.projectRoot);
30
- if (!fileExists(featuresPath))
31
- return [];
32
- return fs.readdirSync(featuresPath, { withFileTypes: true })
33
- .filter(d => d.isDirectory())
34
- .map(d => d.name);
35
- }
36
- updateStatus(name, status) {
37
- const feature = this.get(name);
38
- if (!feature)
39
- throw new Error(`Feature '${name}' not found`);
40
- feature.status = status;
41
- if (status === 'approved' && !feature.approvedAt) {
42
- feature.approvedAt = new Date().toISOString();
43
- }
44
- if (status === 'completed' && !feature.completedAt) {
45
- feature.completedAt = new Date().toISOString();
46
- }
47
- writeJson(getFeatureJsonPath(this.projectRoot, name), feature);
48
- return feature;
49
- }
50
- getInfo(name) {
51
- const feature = this.get(name);
52
- if (!feature)
53
- return null;
54
- const tasks = this.getTasks(name);
55
- const hasPlan = fileExists(getPlanPath(this.projectRoot, name));
56
- const comments = readJson(getCommentsPath(this.projectRoot, name));
57
- const commentCount = comments?.threads?.length || 0;
58
- return {
59
- name: feature.name,
60
- status: feature.status,
61
- tasks,
62
- hasPlan,
63
- commentCount,
64
- };
65
- }
66
- getTasks(featureName) {
67
- const tasksPath = getTasksPath(this.projectRoot, featureName);
68
- if (!fileExists(tasksPath))
69
- return [];
70
- const folders = fs.readdirSync(tasksPath, { withFileTypes: true })
71
- .filter(d => d.isDirectory())
72
- .map(d => d.name)
73
- .sort();
74
- return folders.map(folder => {
75
- const statusPath = `${tasksPath}/${folder}/status.json`;
76
- const status = readJson(statusPath);
77
- const name = folder.replace(/^\d+-/, '');
78
- return {
79
- folder,
80
- name,
81
- status: status?.status || 'pending',
82
- origin: status?.origin || 'plan',
83
- summary: status?.summary,
84
- };
85
- });
86
- }
87
- complete(name) {
88
- const feature = this.get(name);
89
- if (!feature)
90
- throw new Error(`Feature '${name}' not found`);
91
- if (feature.status === 'completed') {
92
- throw new Error(`Feature '${name}' is already completed`);
93
- }
94
- return this.updateStatus(name, 'completed');
95
- }
96
- setSession(name, sessionId) {
97
- const feature = this.get(name);
98
- if (!feature)
99
- throw new Error(`Feature '${name}' not found`);
100
- feature.sessionId = sessionId;
101
- writeJson(getFeatureJsonPath(this.projectRoot, name), feature);
102
- }
103
- getSession(name) {
104
- const feature = this.get(name);
105
- return feature?.sessionId;
106
- }
107
- }
@@ -1 +0,0 @@
1
- export {};