opencode-hive 0.4.4 → 0.5.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 (40) hide show
  1. package/README.md +0 -2
  2. package/dist/e2e/opencode-runtime-smoke.test.d.ts +1 -0
  3. package/dist/e2e/opencode-runtime-smoke.test.js +243 -0
  4. package/dist/e2e/plugin-smoke.test.d.ts +1 -0
  5. package/dist/e2e/plugin-smoke.test.js +127 -0
  6. package/dist/index.js +273 -62
  7. package/dist/services/contextService.d.ts +15 -0
  8. package/dist/services/contextService.js +59 -0
  9. package/dist/services/featureService.d.ts +0 -2
  10. package/dist/services/featureService.js +1 -11
  11. package/dist/services/featureService.test.d.ts +1 -0
  12. package/dist/services/featureService.test.js +127 -0
  13. package/dist/services/planService.test.d.ts +1 -0
  14. package/dist/services/planService.test.js +115 -0
  15. package/dist/services/sessionService.d.ts +31 -0
  16. package/dist/services/sessionService.js +125 -0
  17. package/dist/services/taskService.d.ts +2 -1
  18. package/dist/services/taskService.js +87 -12
  19. package/dist/services/taskService.test.d.ts +1 -0
  20. package/dist/services/taskService.test.js +159 -0
  21. package/dist/services/worktreeService.js +42 -17
  22. package/dist/services/worktreeService.test.d.ts +1 -0
  23. package/dist/services/worktreeService.test.js +117 -0
  24. package/dist/tools/contextTools.d.ts +93 -0
  25. package/dist/tools/contextTools.js +83 -0
  26. package/dist/tools/execTools.js +8 -3
  27. package/dist/tools/featureTools.d.ts +0 -14
  28. package/dist/tools/featureTools.js +7 -18
  29. package/dist/tools/planTools.js +8 -3
  30. package/dist/tools/sessionTools.d.ts +35 -0
  31. package/dist/tools/sessionTools.js +95 -0
  32. package/dist/tools/taskTools.js +9 -4
  33. package/dist/types.d.ts +35 -0
  34. package/dist/utils/detection.d.ts +12 -0
  35. package/dist/utils/detection.js +73 -0
  36. package/dist/utils/paths.d.ts +1 -1
  37. package/dist/utils/paths.js +2 -3
  38. package/dist/utils/paths.test.d.ts +1 -0
  39. package/dist/utils/paths.test.js +100 -0
  40. package/package.json +1 -1
@@ -1,7 +1,12 @@
1
1
  import { z } from 'zod';
2
2
  import { FeatureService } from '../services/featureService.js';
3
+ import { detectContext } from '../utils/detection.js';
3
4
  export function createFeatureTools(projectRoot) {
4
5
  const featureService = new FeatureService(projectRoot);
6
+ const getActiveFeature = () => {
7
+ const ctx = detectContext(projectRoot);
8
+ return ctx?.feature || null;
9
+ };
5
10
  return {
6
11
  hive_feature_create: {
7
12
  description: 'Create a new feature and set it as active.',
@@ -24,7 +29,7 @@ export function createFeatureTools(projectRoot) {
24
29
  parameters: z.object({}),
25
30
  execute: async () => {
26
31
  const features = featureService.list();
27
- const active = featureService.getActive();
32
+ const active = getActiveFeature();
28
33
  const details = features.map(name => {
29
34
  const info = featureService.getInfo(name);
30
35
  return {
@@ -37,22 +42,6 @@ export function createFeatureTools(projectRoot) {
37
42
  return { features: details, active };
38
43
  },
39
44
  },
40
- hive_feature_switch: {
41
- description: 'Switch to a different feature.',
42
- parameters: z.object({
43
- name: z.string().describe('Feature name to switch to'),
44
- }),
45
- execute: async ({ name }) => {
46
- featureService.setActive(name);
47
- const info = featureService.getInfo(name);
48
- return {
49
- switched: true,
50
- name,
51
- status: info?.status,
52
- message: `Switched to feature '${name}'`,
53
- };
54
- },
55
- },
56
45
  hive_feature_complete: {
57
46
  description: 'Mark feature as completed (irreversible)',
58
47
  parameters: z.object({
@@ -60,7 +49,7 @@ export function createFeatureTools(projectRoot) {
60
49
  name: z.string().optional(),
61
50
  }),
62
51
  execute: async ({ name }) => {
63
- const feature = name || featureService.getActive();
52
+ const feature = name || getActiveFeature();
64
53
  if (!feature) {
65
54
  return { error: 'No active feature.' };
66
55
  }
@@ -1,9 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  import { PlanService } from '../services/planService.js';
3
3
  import { FeatureService } from '../services/featureService.js';
4
+ import { detectContext } from '../utils/detection.js';
4
5
  export function createPlanTools(projectRoot) {
5
6
  const planService = new PlanService(projectRoot);
6
7
  const featureService = new FeatureService(projectRoot);
8
+ const getActiveFeature = () => {
9
+ const ctx = detectContext(projectRoot);
10
+ return ctx?.feature || null;
11
+ };
7
12
  return {
8
13
  hive_plan_write: {
9
14
  description: 'Write plan.md (clears existing comments)',
@@ -11,7 +16,7 @@ export function createPlanTools(projectRoot) {
11
16
  content: z.string().describe('The markdown content for the plan'),
12
17
  }),
13
18
  execute: async ({ content }) => {
14
- const feature = featureService.getActive();
19
+ const feature = getActiveFeature();
15
20
  if (!feature) {
16
21
  return { error: 'No active feature. Create one with hive_feature_create first.' };
17
22
  }
@@ -23,7 +28,7 @@ export function createPlanTools(projectRoot) {
23
28
  description: 'Read plan.md and user comments',
24
29
  parameters: z.object({}),
25
30
  execute: async () => {
26
- const feature = featureService.getActive();
31
+ const feature = getActiveFeature();
27
32
  if (!feature) {
28
33
  return { error: 'No active feature.' };
29
34
  }
@@ -38,7 +43,7 @@ export function createPlanTools(projectRoot) {
38
43
  description: 'Approve plan for execution',
39
44
  parameters: z.object({}),
40
45
  execute: async () => {
41
- const feature = featureService.getActive();
46
+ const feature = getActiveFeature();
42
47
  if (!feature) {
43
48
  return { error: 'No active feature.' };
44
49
  }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ export declare function createSessionTools(projectRoot: string): {
3
+ hive_session_open: {
4
+ description: string;
5
+ parameters: z.ZodObject<{
6
+ feature: z.ZodOptional<z.ZodString>;
7
+ task: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strip>;
9
+ execute: ({ feature, task }: {
10
+ feature?: string;
11
+ task?: string;
12
+ }, toolContext: unknown) => Promise<Record<string, unknown>>;
13
+ };
14
+ hive_session_list: {
15
+ description: string;
16
+ parameters: z.ZodObject<{}, z.core.$strip>;
17
+ execute: () => Promise<{
18
+ error: string;
19
+ feature?: undefined;
20
+ master?: undefined;
21
+ sessions?: undefined;
22
+ } | {
23
+ feature: string;
24
+ master: string | undefined;
25
+ sessions: {
26
+ sessionId: string;
27
+ taskFolder: string | undefined;
28
+ startedAt: string;
29
+ lastActiveAt: string;
30
+ isMaster: boolean;
31
+ }[];
32
+ error?: undefined;
33
+ }>;
34
+ };
35
+ };
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+ import { SessionService } from '../services/sessionService.js';
3
+ import { FeatureService } from '../services/featureService.js';
4
+ import { TaskService } from '../services/taskService.js';
5
+ import { PlanService } from '../services/planService.js';
6
+ import { ContextService } from '../services/contextService.js';
7
+ import { detectContext } from '../utils/detection.js';
8
+ export function createSessionTools(projectRoot) {
9
+ const sessionService = new SessionService(projectRoot);
10
+ const featureService = new FeatureService(projectRoot);
11
+ const taskService = new TaskService(projectRoot);
12
+ const planService = new PlanService(projectRoot);
13
+ const contextService = new ContextService(projectRoot);
14
+ const getActiveFeature = () => {
15
+ const ctx = detectContext(projectRoot);
16
+ return ctx?.feature || null;
17
+ };
18
+ return {
19
+ hive_session_open: {
20
+ description: 'Open a Hive session for a feature or task. Returns full context needed to resume work.',
21
+ parameters: z.object({
22
+ feature: z.string().optional().describe('Feature name (defaults to active)'),
23
+ task: z.string().optional().describe('Task folder to focus on'),
24
+ }),
25
+ execute: async ({ feature, task }, toolContext) => {
26
+ const featureName = feature || getActiveFeature();
27
+ if (!featureName)
28
+ return { error: 'No feature specified and no active feature' };
29
+ const featureData = featureService.get(featureName);
30
+ if (!featureData)
31
+ return { error: `Feature '${featureName}' not found` };
32
+ const ctx = toolContext;
33
+ if (ctx?.sessionID) {
34
+ sessionService.track(featureName, ctx.sessionID, task);
35
+ }
36
+ const planResult = planService.read(featureName);
37
+ const tasks = taskService.list(featureName);
38
+ const contextCompiled = contextService.compile(featureName);
39
+ const sessions = sessionService.list(featureName);
40
+ const response = {
41
+ feature: {
42
+ name: featureData.name,
43
+ status: featureData.status,
44
+ ticket: featureData.ticket,
45
+ },
46
+ plan: planResult ? {
47
+ content: planResult.content,
48
+ commentCount: planResult.comments.length,
49
+ } : null,
50
+ tasks: tasks.map(t => ({
51
+ folder: t.folder,
52
+ name: t.name,
53
+ status: t.status,
54
+ origin: t.origin,
55
+ })),
56
+ context: contextCompiled || null,
57
+ sessions: sessions.map(s => ({
58
+ sessionId: s.sessionId,
59
+ taskFolder: s.taskFolder,
60
+ isMaster: s.sessionId === sessionService.getMaster(featureName),
61
+ })),
62
+ };
63
+ if (task) {
64
+ const taskInfo = taskService.get(featureName, task);
65
+ if (taskInfo) {
66
+ response.focusedTask = taskInfo;
67
+ }
68
+ }
69
+ return response;
70
+ },
71
+ },
72
+ hive_session_list: {
73
+ description: 'List all sessions for the active feature',
74
+ parameters: z.object({}),
75
+ execute: async () => {
76
+ const feature = getActiveFeature();
77
+ if (!feature)
78
+ return { error: 'No active feature' };
79
+ const sessions = sessionService.list(feature);
80
+ const master = sessionService.getMaster(feature);
81
+ return {
82
+ feature,
83
+ master,
84
+ sessions: sessions.map(s => ({
85
+ sessionId: s.sessionId,
86
+ taskFolder: s.taskFolder,
87
+ startedAt: s.startedAt,
88
+ lastActiveAt: s.lastActiveAt,
89
+ isMaster: s.sessionId === master,
90
+ })),
91
+ };
92
+ },
93
+ },
94
+ };
95
+ }
@@ -1,15 +1,20 @@
1
1
  import { z } from 'zod';
2
2
  import { TaskService } from '../services/taskService.js';
3
3
  import { FeatureService } from '../services/featureService.js';
4
+ import { detectContext } from '../utils/detection.js';
4
5
  export function createTaskTools(projectRoot) {
5
6
  const taskService = new TaskService(projectRoot);
6
7
  const featureService = new FeatureService(projectRoot);
8
+ const getActiveFeature = () => {
9
+ const ctx = detectContext(projectRoot);
10
+ return ctx?.feature || null;
11
+ };
7
12
  return {
8
13
  hive_tasks_sync: {
9
14
  description: 'Generate tasks from approved plan',
10
15
  parameters: z.object({}),
11
16
  execute: async () => {
12
- const feature = featureService.getActive();
17
+ const feature = getActiveFeature();
13
18
  if (!feature) {
14
19
  return { error: 'No active feature.' };
15
20
  }
@@ -37,7 +42,7 @@ export function createTaskTools(projectRoot) {
37
42
  order: z.number().optional().describe('Task order number (defaults to next available)'),
38
43
  }),
39
44
  execute: async ({ name, order }) => {
40
- const feature = featureService.getActive();
45
+ const feature = getActiveFeature();
41
46
  if (!feature) {
42
47
  return { error: 'No active feature.' };
43
48
  }
@@ -57,7 +62,7 @@ export function createTaskTools(projectRoot) {
57
62
  summary: z.string().optional().describe('Summary of work done'),
58
63
  }),
59
64
  execute: async ({ task, status, summary }) => {
60
- const feature = featureService.getActive();
65
+ const feature = getActiveFeature();
61
66
  if (!feature) {
62
67
  return { error: 'No active feature.' };
63
68
  }
@@ -69,7 +74,7 @@ export function createTaskTools(projectRoot) {
69
74
  description: 'List all tasks for the active feature.',
70
75
  parameters: z.object({}),
71
76
  execute: async () => {
72
- const feature = featureService.getActive();
77
+ const feature = getActiveFeature();
73
78
  if (!feature) {
74
79
  return { error: 'No active feature.' };
75
80
  }
package/dist/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface TaskStatus {
16
16
  summary?: string;
17
17
  startedAt?: string;
18
18
  completedAt?: string;
19
+ baseCommit?: string;
19
20
  }
20
21
  export interface PlanComment {
21
22
  id: string;
@@ -52,3 +53,37 @@ export interface FeatureInfo {
52
53
  hasPlan: boolean;
53
54
  commentCount: number;
54
55
  }
56
+ export interface ContextFile {
57
+ name: string;
58
+ content: string;
59
+ updatedAt: string;
60
+ }
61
+ export interface SessionInfo {
62
+ sessionId: string;
63
+ taskFolder?: string;
64
+ startedAt: string;
65
+ lastActiveAt: string;
66
+ messageCount?: number;
67
+ }
68
+ export interface SessionsJson {
69
+ master?: string;
70
+ sessions: SessionInfo[];
71
+ }
72
+ export interface TaskSpec {
73
+ taskFolder: string;
74
+ featureName: string;
75
+ planSection: string;
76
+ context: string;
77
+ priorTasks: Array<{
78
+ folder: string;
79
+ summary?: string;
80
+ }>;
81
+ }
82
+ export interface HiveConfig {
83
+ enableToolsFor: string[];
84
+ agents: {
85
+ worker: {
86
+ visible: boolean;
87
+ };
88
+ };
89
+ }
@@ -0,0 +1,12 @@
1
+ import { FeatureJson } from '../types.js';
2
+ export interface DetectionResult {
3
+ projectRoot: string;
4
+ feature: string | null;
5
+ task: string | null;
6
+ isWorktree: boolean;
7
+ mainProjectRoot: string | null;
8
+ }
9
+ export declare function detectContext(cwd: string): DetectionResult;
10
+ export declare function listFeatures(projectRoot: string): string[];
11
+ export declare function getFeatureData(projectRoot: string, featureName: string): FeatureJson | null;
12
+ export declare function findProjectRoot(startDir: string): string | null;
@@ -0,0 +1,73 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { getFeaturesPath, getFeaturePath, readJson } from './paths.js';
4
+ export function detectContext(cwd) {
5
+ const result = {
6
+ projectRoot: cwd,
7
+ feature: null,
8
+ task: null,
9
+ isWorktree: false,
10
+ mainProjectRoot: null,
11
+ };
12
+ const worktreeMatch = cwd.match(/(.+)\/\.hive\/\.worktrees\/([^/]+)\/([^/]+)/);
13
+ if (worktreeMatch) {
14
+ result.mainProjectRoot = worktreeMatch[1];
15
+ result.feature = worktreeMatch[2];
16
+ result.task = worktreeMatch[3];
17
+ result.isWorktree = true;
18
+ result.projectRoot = worktreeMatch[1];
19
+ return result;
20
+ }
21
+ const gitPath = path.join(cwd, '.git');
22
+ if (fs.existsSync(gitPath)) {
23
+ const stat = fs.statSync(gitPath);
24
+ if (stat.isFile()) {
25
+ const gitContent = fs.readFileSync(gitPath, 'utf-8').trim();
26
+ const gitdirMatch = gitContent.match(/gitdir:\s*(.+)/);
27
+ if (gitdirMatch) {
28
+ const gitdir = gitdirMatch[1];
29
+ const worktreePathMatch = gitdir.match(/(.+)\/\.git\/worktrees\/(.+)/);
30
+ if (worktreePathMatch) {
31
+ const mainRepo = worktreePathMatch[1];
32
+ const cwdWorktreeMatch = cwd.match(/\.hive\/\.worktrees\/([^/]+)\/([^/]+)/);
33
+ if (cwdWorktreeMatch) {
34
+ result.mainProjectRoot = mainRepo;
35
+ result.feature = cwdWorktreeMatch[1];
36
+ result.task = cwdWorktreeMatch[2];
37
+ result.isWorktree = true;
38
+ result.projectRoot = mainRepo;
39
+ return result;
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ export function listFeatures(projectRoot) {
48
+ const featuresPath = getFeaturesPath(projectRoot);
49
+ if (!fs.existsSync(featuresPath))
50
+ return [];
51
+ return fs.readdirSync(featuresPath, { withFileTypes: true })
52
+ .filter(d => d.isDirectory())
53
+ .map(d => d.name);
54
+ }
55
+ export function getFeatureData(projectRoot, featureName) {
56
+ const featurePath = getFeaturePath(projectRoot, featureName);
57
+ const featureJsonPath = path.join(featurePath, 'feature.json');
58
+ return readJson(featureJsonPath);
59
+ }
60
+ export function findProjectRoot(startDir) {
61
+ let current = startDir;
62
+ const root = path.parse(current).root;
63
+ while (current !== root) {
64
+ if (fs.existsSync(path.join(current, '.hive'))) {
65
+ return current;
66
+ }
67
+ if (fs.existsSync(path.join(current, '.git'))) {
68
+ return current;
69
+ }
70
+ current = path.dirname(current);
71
+ }
72
+ return null;
73
+ }
@@ -9,7 +9,7 @@ export declare function getTasksPath(projectRoot: string, featureName: string):
9
9
  export declare function getTaskPath(projectRoot: string, featureName: string, taskFolder: string): string;
10
10
  export declare function getTaskStatusPath(projectRoot: string, featureName: string, taskFolder: string): string;
11
11
  export declare function getTaskReportPath(projectRoot: string, featureName: string, taskFolder: string): string;
12
- export declare function getActiveFeaturePath(projectRoot: string): string;
12
+ export declare function getTaskSpecPath(projectRoot: string, featureName: string, taskFolder: string): string;
13
13
  export declare function ensureDir(dirPath: string): void;
14
14
  export declare function fileExists(filePath: string): boolean;
15
15
  export declare function readJson<T>(filePath: string): T | null;
@@ -9,7 +9,6 @@ const COMMENTS_FILE = 'comments.json';
9
9
  const FEATURE_FILE = 'feature.json';
10
10
  const STATUS_FILE = 'status.json';
11
11
  const REPORT_FILE = 'report.md';
12
- const ACTIVE_FILE = 'active-feature';
13
12
  export function getHivePath(projectRoot) {
14
13
  return path.join(projectRoot, HIVE_DIR);
15
14
  }
@@ -43,8 +42,8 @@ export function getTaskStatusPath(projectRoot, featureName, taskFolder) {
43
42
  export function getTaskReportPath(projectRoot, featureName, taskFolder) {
44
43
  return path.join(getTaskPath(projectRoot, featureName, taskFolder), REPORT_FILE);
45
44
  }
46
- export function getActiveFeaturePath(projectRoot) {
47
- return path.join(getHivePath(projectRoot), ACTIVE_FILE);
45
+ export function getTaskSpecPath(projectRoot, featureName, taskFolder) {
46
+ return path.join(getTaskPath(projectRoot, featureName, taskFolder), 'spec.md');
48
47
  }
49
48
  export function ensureDir(dirPath) {
50
49
  if (!fs.existsSync(dirPath)) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { getHivePath, getFeaturesPath, getFeaturePath, getPlanPath, getCommentsPath, getFeatureJsonPath, getContextPath, getTasksPath, getTaskPath, getTaskStatusPath, getTaskReportPath, ensureDir, fileExists, readJson, writeJson, readText, writeText, } from "./paths";
5
+ const TEST_ROOT = "/tmp/hive-test-paths";
6
+ describe("paths.ts", () => {
7
+ beforeEach(() => {
8
+ fs.rmSync(TEST_ROOT, { recursive: true, force: true });
9
+ fs.mkdirSync(TEST_ROOT, { recursive: true });
10
+ });
11
+ afterEach(() => {
12
+ fs.rmSync(TEST_ROOT, { recursive: true, force: true });
13
+ });
14
+ describe("path generators", () => {
15
+ it("getHivePath returns correct path", () => {
16
+ expect(getHivePath(TEST_ROOT)).toBe(path.join(TEST_ROOT, ".hive"));
17
+ });
18
+ it("getFeaturesPath returns correct path", () => {
19
+ expect(getFeaturesPath(TEST_ROOT)).toBe(path.join(TEST_ROOT, ".hive", "features"));
20
+ });
21
+ it("getFeaturePath returns correct path", () => {
22
+ expect(getFeaturePath(TEST_ROOT, "my-feature")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature"));
23
+ });
24
+ it("getPlanPath returns correct path", () => {
25
+ expect(getPlanPath(TEST_ROOT, "my-feature")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature", "plan.md"));
26
+ });
27
+ it("getCommentsPath returns correct path", () => {
28
+ expect(getCommentsPath(TEST_ROOT, "my-feature")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature", "comments.json"));
29
+ });
30
+ it("getFeatureJsonPath returns correct path", () => {
31
+ expect(getFeatureJsonPath(TEST_ROOT, "my-feature")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature", "feature.json"));
32
+ });
33
+ it("getContextPath returns correct path", () => {
34
+ expect(getContextPath(TEST_ROOT, "my-feature")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature", "context"));
35
+ });
36
+ it("getTasksPath returns correct path", () => {
37
+ expect(getTasksPath(TEST_ROOT, "my-feature")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature", "tasks"));
38
+ });
39
+ it("getTaskPath returns correct path", () => {
40
+ expect(getTaskPath(TEST_ROOT, "my-feature", "01-setup")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature", "tasks", "01-setup"));
41
+ });
42
+ it("getTaskStatusPath returns correct path", () => {
43
+ expect(getTaskStatusPath(TEST_ROOT, "my-feature", "01-setup")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature", "tasks", "01-setup", "status.json"));
44
+ });
45
+ it("getTaskReportPath returns correct path", () => {
46
+ expect(getTaskReportPath(TEST_ROOT, "my-feature", "01-setup")).toBe(path.join(TEST_ROOT, ".hive", "features", "my-feature", "tasks", "01-setup", "report.md"));
47
+ });
48
+ });
49
+ describe("file utilities", () => {
50
+ it("ensureDir creates directory recursively", () => {
51
+ const deepPath = path.join(TEST_ROOT, "a", "b", "c");
52
+ ensureDir(deepPath);
53
+ expect(fs.existsSync(deepPath)).toBe(true);
54
+ });
55
+ it("ensureDir is idempotent", () => {
56
+ const dirPath = path.join(TEST_ROOT, "existing");
57
+ fs.mkdirSync(dirPath, { recursive: true });
58
+ expect(() => ensureDir(dirPath)).not.toThrow();
59
+ });
60
+ it("fileExists returns true for existing file", () => {
61
+ const filePath = path.join(TEST_ROOT, "exists.txt");
62
+ fs.writeFileSync(filePath, "content");
63
+ expect(fileExists(filePath)).toBe(true);
64
+ });
65
+ it("fileExists returns false for non-existing file", () => {
66
+ expect(fileExists(path.join(TEST_ROOT, "nope.txt"))).toBe(false);
67
+ });
68
+ it("writeJson and readJson roundtrip", () => {
69
+ const filePath = path.join(TEST_ROOT, "data.json");
70
+ const data = { name: "test", count: 42, nested: { value: true } };
71
+ writeJson(filePath, data);
72
+ const result = readJson(filePath);
73
+ expect(result).toEqual(data);
74
+ });
75
+ it("readJson returns null for non-existing file", () => {
76
+ expect(readJson(path.join(TEST_ROOT, "nope.json"))).toBeNull();
77
+ });
78
+ it("writeText and readText roundtrip", () => {
79
+ const filePath = path.join(TEST_ROOT, "text.md");
80
+ const content = "# Hello\n\nThis is content.";
81
+ writeText(filePath, content);
82
+ const result = readText(filePath);
83
+ expect(result).toBe(content);
84
+ });
85
+ it("readText returns null for non-existing file", () => {
86
+ expect(readText(path.join(TEST_ROOT, "nope.md"))).toBeNull();
87
+ });
88
+ it("writeJson creates parent directories", () => {
89
+ const filePath = path.join(TEST_ROOT, "deep", "nested", "data.json");
90
+ const expected = { test: true };
91
+ writeJson(filePath, expected);
92
+ expect(readJson(filePath)).toEqual(expected);
93
+ });
94
+ it("writeText creates parent directories", () => {
95
+ const filePath = path.join(TEST_ROOT, "deep", "nested", "file.txt");
96
+ writeText(filePath, "content");
97
+ expect(readText(filePath)).toBe("content");
98
+ });
99
+ });
100
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-hive",
3
- "version": "0.4.4",
3
+ "version": "0.5.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",