@harness-fe/mcp-server 4.0.0-next.2 → 4.0.0-next.4
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/dist/bin.d.ts +2 -0
- package/dist/bin.js +15 -0
- package/dist/daemon.d.ts +3 -3
- package/dist/daemon.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +65 -19
- package/dist/mcpHttp.d.ts +2 -2
- package/dist/mcpHttp.js +88 -18
- package/package.json +5 -7
- package/src/bin.ts +19 -0
- package/src/daemon.ts +3 -3
- package/src/experimental.test.ts +2 -2
- package/src/index.ts +4 -4
- package/src/mcp.ts +67 -23
- package/src/mcpHttp.test.ts +52 -3
- package/src/mcpHttp.ts +102 -23
- package/src/mcpLayer.e2e.test.ts +2 -2
- package/src/newCapabilities.e2e.test.ts +3 -3
- package/dist/auth.d.ts +0 -53
- package/dist/auth.js +0 -212
- package/dist/bridge.d.ts +0 -323
- package/dist/bridge.js +0 -1618
- package/dist/cli.d.ts +0 -18
- package/dist/cli.js +0 -293
- package/dist/dashboardApi.d.ts +0 -40
- package/dist/dashboardApi.js +0 -142
- package/dist/dashboardSpa.d.ts +0 -18
- package/dist/dashboardSpa.js +0 -180
- package/dist/dashboardUrl.d.ts +0 -13
- package/dist/dashboardUrl.js +0 -18
- package/dist/eventsHandler.d.ts +0 -24
- package/dist/eventsHandler.js +0 -114
- package/dist/identity.d.ts +0 -90
- package/dist/identity.js +0 -123
- package/dist/openBrowser.d.ts +0 -33
- package/dist/openBrowser.js +0 -63
- package/dist/remoteBridge.d.ts +0 -61
- package/dist/remoteBridge.js +0 -307
- package/dist/replayCreate.d.ts +0 -36
- package/dist/replayCreate.js +0 -156
- package/dist/replayViewer.d.ts +0 -20
- package/dist/replayViewer.js +0 -168
- package/dist/sessionRouter.d.ts +0 -45
- package/dist/sessionRouter.js +0 -88
- package/dist/store/JsonMemoryStore.d.ts +0 -52
- package/dist/store/JsonMemoryStore.js +0 -119
- package/dist/store/JsonTaskStore.d.ts +0 -21
- package/dist/store/JsonTaskStore.js +0 -53
- package/dist/store/JsonlStore.d.ts +0 -128
- package/dist/store/JsonlStore.js +0 -1172
- package/dist/store/MemoryEventStore.d.ts +0 -47
- package/dist/store/MemoryEventStore.js +0 -111
- package/dist/store/WriteQueue.d.ts +0 -51
- package/dist/store/WriteQueue.js +0 -142
- package/dist/store/index.d.ts +0 -6
- package/dist/store/index.js +0 -5
- package/dist/store/types.d.ts +0 -427
- package/dist/store/types.js +0 -19
- package/dist/visitorTimeline.d.ts +0 -24
- package/dist/visitorTimeline.js +0 -68
- package/src/auth.test.ts +0 -90
- package/src/auth.ts +0 -248
- package/src/bridge-auth.test.ts +0 -196
- package/src/bridge.test.ts +0 -1708
- package/src/bridge.ts +0 -1854
- package/src/cli.ts +0 -338
- package/src/dashboardApi.test.ts +0 -235
- package/src/dashboardApi.ts +0 -184
- package/src/dashboardSpa.test.ts +0 -239
- package/src/dashboardSpa.ts +0 -195
- package/src/dashboardUrl.test.ts +0 -46
- package/src/dashboardUrl.ts +0 -28
- package/src/eventsHandler.test.ts +0 -247
- package/src/eventsHandler.ts +0 -136
- package/src/identity.test.ts +0 -109
- package/src/identity.ts +0 -137
- package/src/openBrowser.test.ts +0 -103
- package/src/openBrowser.ts +0 -81
- package/src/remoteBridge.test.ts +0 -119
- package/src/remoteBridge.ts +0 -404
- package/src/replay.test.ts +0 -271
- package/src/replayCreate.ts +0 -194
- package/src/replayViewer.ts +0 -173
- package/src/sessionRouter.ts +0 -119
- package/src/store/JsonMemoryStore.test.ts +0 -175
- package/src/store/JsonMemoryStore.ts +0 -128
- package/src/store/JsonTaskStore.test.ts +0 -212
- package/src/store/JsonTaskStore.ts +0 -59
- package/src/store/JsonlStore.test.ts +0 -1538
- package/src/store/JsonlStore.ts +0 -1325
- package/src/store/MemoryEventStore.test.ts +0 -119
- package/src/store/MemoryEventStore.ts +0 -151
- package/src/store/WriteQueue.ts +0 -165
- package/src/store/identityTagging.test.ts +0 -67
- package/src/store/index.ts +0 -29
- package/src/store/types.ts +0 -532
- package/src/visitorTimeline.test.ts +0 -197
- package/src/visitorTimeline.ts +0 -89
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JsonMemoryStore — JSON-based persistence for agent memory (key-value store per project).
|
|
3
|
-
*
|
|
4
|
-
* File layout:
|
|
5
|
-
* {dataDir}/{projectId}/memory.json
|
|
6
|
-
*
|
|
7
|
-
* File format:
|
|
8
|
-
* {
|
|
9
|
-
* "key1": { "key": "key1", "value": "...", "updatedAt": 1700000000000 },
|
|
10
|
-
* "key2": { "key": "key2", "value": "...", "updatedAt": 1700000001000 }
|
|
11
|
-
* }
|
|
12
|
-
*
|
|
13
|
-
* All mutations use atomic write-then-rename for durability.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
import type { IMemoryStore, MemoryEntry } from './types.js';
|
|
19
|
-
import { sanitizeId } from './JsonlStore.js';
|
|
20
|
-
|
|
21
|
-
export class JsonMemoryStore implements IMemoryStore {
|
|
22
|
-
constructor(private readonly dataDir: string) {}
|
|
23
|
-
|
|
24
|
-
// ── Path helpers ──────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
private memoryPath(projectId: string): string {
|
|
27
|
-
return join(this.dataDir, sanitizeId(projectId), 'memory.json');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ── Private I/O ───────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Read and parse memory.json for a project.
|
|
34
|
-
* Returns a null-prototype object on missing or corrupt file (never throws).
|
|
35
|
-
* Using Object.create(null) prevents prototype pollution from keys like __proto__.
|
|
36
|
-
*/
|
|
37
|
-
private load(projectId: string): Record<string, MemoryEntry> {
|
|
38
|
-
const path = this.memoryPath(projectId);
|
|
39
|
-
try {
|
|
40
|
-
const raw = readFileSync(path, 'utf-8');
|
|
41
|
-
const parsed = JSON.parse(raw) as Record<string, MemoryEntry>;
|
|
42
|
-
// Copy into a null-prototype object to prevent prototype pollution
|
|
43
|
-
const safe: Record<string, MemoryEntry> = Object.create(null);
|
|
44
|
-
for (const key of Object.keys(parsed)) {
|
|
45
|
-
safe[key] = parsed[key];
|
|
46
|
-
}
|
|
47
|
-
return safe;
|
|
48
|
-
} catch {
|
|
49
|
-
return Object.create(null);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Atomically write memory data to disk using tmp + rename strategy.
|
|
55
|
-
* Logs error on failure without throwing.
|
|
56
|
-
*/
|
|
57
|
-
private save(projectId: string, data: Record<string, MemoryEntry>): void {
|
|
58
|
-
const path = this.memoryPath(projectId);
|
|
59
|
-
const tmpPath = `${path}.tmp`;
|
|
60
|
-
|
|
61
|
-
// Ensure the project directory exists
|
|
62
|
-
const dir = join(this.dataDir, sanitizeId(projectId));
|
|
63
|
-
try {
|
|
64
|
-
mkdirSync(dir, { recursive: true });
|
|
65
|
-
} catch (err) {
|
|
66
|
-
console.error(`[JsonMemoryStore] failed to create directory ${dir}:`, err);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
72
|
-
renameSync(tmpPath, path);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
console.error(`[JsonMemoryStore] failed to write ${path}:`, err);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── IMemoryStore implementation ───────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Get a memory entry by key.
|
|
82
|
-
* Returns undefined if the key does not exist or memory.json is missing.
|
|
83
|
-
*/
|
|
84
|
-
get(projectId: string, key: string): MemoryEntry | undefined {
|
|
85
|
-
const data = this.load(projectId);
|
|
86
|
-
return data[key];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Write or update a memory entry.
|
|
91
|
-
* Sets updatedAt to the current Unix ms timestamp.
|
|
92
|
-
* Returns the new/updated MemoryEntry.
|
|
93
|
-
*/
|
|
94
|
-
set(projectId: string, key: string, value: string): MemoryEntry {
|
|
95
|
-
const data = this.load(projectId);
|
|
96
|
-
const entry: MemoryEntry = {
|
|
97
|
-
key,
|
|
98
|
-
value,
|
|
99
|
-
updatedAt: Date.now(),
|
|
100
|
-
};
|
|
101
|
-
data[key] = entry;
|
|
102
|
-
this.save(projectId, data);
|
|
103
|
-
return entry;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Delete a memory entry by key.
|
|
108
|
-
* Returns true if the key existed and was removed, false otherwise.
|
|
109
|
-
*/
|
|
110
|
-
delete(projectId: string, key: string): boolean {
|
|
111
|
-
const data = this.load(projectId);
|
|
112
|
-
if (!(key in data)) {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
delete data[key];
|
|
116
|
-
this.save(projectId, data);
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* List all memory entries for a project, sorted by updatedAt descending.
|
|
122
|
-
* Returns an empty array if memory.json does not exist.
|
|
123
|
-
*/
|
|
124
|
-
list(projectId: string): MemoryEntry[] {
|
|
125
|
-
const data = this.load(projectId);
|
|
126
|
-
return Object.values(data).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { JsonTaskStore } from './JsonTaskStore.js';
|
|
6
|
-
import type { Task } from '@harness-fe/protocol';
|
|
7
|
-
|
|
8
|
-
function makeTempDir() {
|
|
9
|
-
return mkdtempSync(join(tmpdir(), 'json-task-store-test-'));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function cleanup(dir: string) {
|
|
13
|
-
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function makeTask(overrides: Partial<Task> = {}): Task {
|
|
17
|
-
return {
|
|
18
|
-
id: 'task-1',
|
|
19
|
-
tabId: 'tab-abc',
|
|
20
|
-
projectId: 'proj',
|
|
21
|
-
url: 'http://localhost:5173',
|
|
22
|
-
status: 'pending',
|
|
23
|
-
question: 'What does this button do?',
|
|
24
|
-
selector: { css: '#submit-btn' },
|
|
25
|
-
element: { outerHTML: '<button id="submit-btn">Submit</button>', rect: { x: 0, y: 0, width: 100, height: 40 } },
|
|
26
|
-
createdAt: Date.now(),
|
|
27
|
-
...overrides,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('JsonTaskStore', () => {
|
|
32
|
-
let dir: string;
|
|
33
|
-
let store: JsonTaskStore;
|
|
34
|
-
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
dir = makeTempDir();
|
|
37
|
-
store = new JsonTaskStore(dir);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
cleanup(dir);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// ── Load from missing file → empty array ─────────────────────────────
|
|
45
|
-
|
|
46
|
-
it('returns empty array when tasks.json does not exist', () => {
|
|
47
|
-
const tasks = store.loadTasks('proj');
|
|
48
|
-
expect(tasks).toEqual([]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns empty array for a project that has never been saved', () => {
|
|
52
|
-
const tasks = store.loadTasks('brand-new-project');
|
|
53
|
-
expect(tasks).toEqual([]);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// ── Load from corrupt JSON → empty array ─────────────────────────────
|
|
57
|
-
|
|
58
|
-
it('returns empty array when tasks.json contains invalid JSON', () => {
|
|
59
|
-
// Manually create the project directory and write corrupt JSON
|
|
60
|
-
const { mkdirSync } = require('node:fs');
|
|
61
|
-
const projDir = join(dir, 'proj');
|
|
62
|
-
mkdirSync(projDir, { recursive: true });
|
|
63
|
-
writeFileSync(join(projDir, 'tasks.json'), '{ this is not valid json !!!', 'utf-8');
|
|
64
|
-
|
|
65
|
-
const tasks = store.loadTasks('proj');
|
|
66
|
-
expect(tasks).toEqual([]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('returns empty array when tasks.json has valid JSON but wrong shape (no tasks array)', () => {
|
|
70
|
-
const { mkdirSync } = require('node:fs');
|
|
71
|
-
const projDir = join(dir, 'proj');
|
|
72
|
-
mkdirSync(projDir, { recursive: true });
|
|
73
|
-
writeFileSync(join(projDir, 'tasks.json'), JSON.stringify({ version: 1, data: [] }), 'utf-8');
|
|
74
|
-
|
|
75
|
-
const tasks = store.loadTasks('proj');
|
|
76
|
-
expect(tasks).toEqual([]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('returns empty array when tasks.json is empty', () => {
|
|
80
|
-
const { mkdirSync } = require('node:fs');
|
|
81
|
-
const projDir = join(dir, 'proj');
|
|
82
|
-
mkdirSync(projDir, { recursive: true });
|
|
83
|
-
writeFileSync(join(projDir, 'tasks.json'), '', 'utf-8');
|
|
84
|
-
|
|
85
|
-
const tasks = store.loadTasks('proj');
|
|
86
|
-
expect(tasks).toEqual([]);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// ── Save tasks → atomic write (tmp + rename) ─────────────────────────
|
|
90
|
-
|
|
91
|
-
it('saves tasks and the file exists afterwards', () => {
|
|
92
|
-
const tasks = [makeTask()];
|
|
93
|
-
store.saveTasks('proj', tasks);
|
|
94
|
-
|
|
95
|
-
const tasksPath = join(dir, 'proj', 'tasks.json');
|
|
96
|
-
expect(existsSync(tasksPath)).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('saves tasks and no .tmp file remains after save', () => {
|
|
100
|
-
const tasks = [makeTask()];
|
|
101
|
-
store.saveTasks('proj', tasks);
|
|
102
|
-
|
|
103
|
-
const tmpPath = join(dir, 'proj', 'tasks.json.tmp');
|
|
104
|
-
expect(existsSync(tmpPath)).toBe(false);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('saved file contains the correct JSON structure', () => {
|
|
108
|
-
const { readFileSync } = require('node:fs');
|
|
109
|
-
const tasks = [makeTask({ id: 'task-abc' })];
|
|
110
|
-
store.saveTasks('proj', tasks);
|
|
111
|
-
|
|
112
|
-
const raw = readFileSync(join(dir, 'proj', 'tasks.json'), 'utf-8');
|
|
113
|
-
const parsed = JSON.parse(raw);
|
|
114
|
-
expect(parsed.version).toBe(1);
|
|
115
|
-
expect(Array.isArray(parsed.tasks)).toBe(true);
|
|
116
|
-
expect(parsed.tasks).toHaveLength(1);
|
|
117
|
-
expect(parsed.tasks[0].id).toBe('task-abc');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('creates the project directory if it does not exist', () => {
|
|
121
|
-
const tasks = [makeTask()];
|
|
122
|
-
store.saveTasks('new-project', tasks);
|
|
123
|
-
|
|
124
|
-
const projDir = join(dir, 'new-project');
|
|
125
|
-
expect(existsSync(projDir)).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ── Round-trip: save then load ────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
it('round-trip: saved tasks can be loaded back and match the original', () => {
|
|
131
|
-
const tasks: Task[] = [
|
|
132
|
-
makeTask({ id: 'task-1', status: 'pending' }),
|
|
133
|
-
makeTask({ id: 'task-2', status: 'claimed', claimedAt: Date.now() }),
|
|
134
|
-
makeTask({ id: 'task-3', status: 'resolved', resolvedAt: Date.now(), note: 'Done!' }),
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
store.saveTasks('proj', tasks);
|
|
138
|
-
const loaded = store.loadTasks('proj');
|
|
139
|
-
|
|
140
|
-
expect(loaded).toHaveLength(3);
|
|
141
|
-
expect(loaded[0].id).toBe('task-1');
|
|
142
|
-
expect(loaded[1].id).toBe('task-2');
|
|
143
|
-
expect(loaded[2].id).toBe('task-3');
|
|
144
|
-
expect(loaded[2].note).toBe('Done!');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('round-trip: saving an empty array and loading returns empty array', () => {
|
|
148
|
-
store.saveTasks('proj', []);
|
|
149
|
-
const loaded = store.loadTasks('proj');
|
|
150
|
-
expect(loaded).toEqual([]);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('round-trip: overwriting tasks replaces the previous data', () => {
|
|
154
|
-
const initial = [makeTask({ id: 'task-1' }), makeTask({ id: 'task-2' })];
|
|
155
|
-
store.saveTasks('proj', initial);
|
|
156
|
-
|
|
157
|
-
const updated = [makeTask({ id: 'task-3' })];
|
|
158
|
-
store.saveTasks('proj', updated);
|
|
159
|
-
|
|
160
|
-
const loaded = store.loadTasks('proj');
|
|
161
|
-
expect(loaded).toHaveLength(1);
|
|
162
|
-
expect(loaded[0].id).toBe('task-3');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('round-trip: multiple saves preserve the last written state', () => {
|
|
166
|
-
for (let i = 0; i < 5; i++) {
|
|
167
|
-
store.saveTasks('proj', [makeTask({ id: `task-${i}` })]);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const loaded = store.loadTasks('proj');
|
|
171
|
-
expect(loaded).toHaveLength(1);
|
|
172
|
-
expect(loaded[0].id).toBe('task-4');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// ── Purge does not delete tasks.json ─────────────────────────────────
|
|
176
|
-
// JsonTaskStore has no purge method — verify that saveTasks/loadTasks
|
|
177
|
-
// continue to work correctly after multiple saves (simulating what purge
|
|
178
|
-
// would need to preserve).
|
|
179
|
-
|
|
180
|
-
it('tasks.json is preserved across multiple save operations', () => {
|
|
181
|
-
const tasks = [makeTask({ id: 'task-keep' })];
|
|
182
|
-
store.saveTasks('proj', tasks);
|
|
183
|
-
|
|
184
|
-
// Simulate additional saves (as would happen during normal operation)
|
|
185
|
-
store.saveTasks('proj', [...tasks, makeTask({ id: 'task-new' })]);
|
|
186
|
-
|
|
187
|
-
const tasksPath = join(dir, 'proj', 'tasks.json');
|
|
188
|
-
expect(existsSync(tasksPath)).toBe(true);
|
|
189
|
-
|
|
190
|
-
const loaded = store.loadTasks('proj');
|
|
191
|
-
expect(loaded).toHaveLength(2);
|
|
192
|
-
expect(loaded.map((t) => t.id)).toContain('task-keep');
|
|
193
|
-
expect(loaded.map((t) => t.id)).toContain('task-new');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('tasks.json is not affected by operations on other projects', () => {
|
|
197
|
-
const projATasks = [makeTask({ id: 'task-a', projectId: 'proj-a' })];
|
|
198
|
-
const projBTasks = [makeTask({ id: 'task-b', projectId: 'proj-b' })];
|
|
199
|
-
|
|
200
|
-
store.saveTasks('proj-a', projATasks);
|
|
201
|
-
store.saveTasks('proj-b', projBTasks);
|
|
202
|
-
|
|
203
|
-
// Loading proj-a should not be affected by proj-b
|
|
204
|
-
const loadedA = store.loadTasks('proj-a');
|
|
205
|
-
expect(loadedA).toHaveLength(1);
|
|
206
|
-
expect(loadedA[0].id).toBe('task-a');
|
|
207
|
-
|
|
208
|
-
const loadedB = store.loadTasks('proj-b');
|
|
209
|
-
expect(loadedB).toHaveLength(1);
|
|
210
|
-
expect(loadedB[0].id).toBe('task-b');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JsonTaskStore — JSON-based persistence for annotation tasks.
|
|
3
|
-
*
|
|
4
|
-
* File format: {dataDir}/{sanitizeId(projectId)}/tasks.json
|
|
5
|
-
* ```json
|
|
6
|
-
* { "version": 1, "tasks": Task[] }
|
|
7
|
-
* ```
|
|
8
|
-
*
|
|
9
|
-
* Writes are atomic: write to a .tmp file then rename to the final path.
|
|
10
|
-
* On read failure (missing or corrupt file), returns an empty array.
|
|
11
|
-
* On write failure, logs the error without throwing.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
15
|
-
import { join } from 'node:path';
|
|
16
|
-
import type { Task } from '@harness-fe/protocol';
|
|
17
|
-
import type { ITaskStore } from './types.js';
|
|
18
|
-
import { sanitizeId } from './JsonlStore.js';
|
|
19
|
-
|
|
20
|
-
interface TasksFile {
|
|
21
|
-
version: number;
|
|
22
|
-
tasks: Task[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class JsonTaskStore implements ITaskStore {
|
|
26
|
-
constructor(private readonly dataDir: string) {}
|
|
27
|
-
|
|
28
|
-
private tasksPath(projectId: string): string {
|
|
29
|
-
return join(this.dataDir, sanitizeId(projectId), 'tasks.json');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
loadTasks(projectId: string): Task[] {
|
|
33
|
-
const path = this.tasksPath(projectId);
|
|
34
|
-
if (!existsSync(path)) return [];
|
|
35
|
-
try {
|
|
36
|
-
const raw = readFileSync(path, 'utf-8');
|
|
37
|
-
const parsed = JSON.parse(raw) as TasksFile;
|
|
38
|
-
if (!Array.isArray(parsed?.tasks)) return [];
|
|
39
|
-
return parsed.tasks;
|
|
40
|
-
} catch {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
saveTasks(projectId: string, tasks: Task[]): void {
|
|
46
|
-
const path = this.tasksPath(projectId);
|
|
47
|
-
const dir = join(this.dataDir, sanitizeId(projectId));
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
mkdirSync(dir, { recursive: true });
|
|
51
|
-
const tmp = `${path}.tmp`;
|
|
52
|
-
const data: TasksFile = { version: 1, tasks };
|
|
53
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
54
|
-
renameSync(tmp, path);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
console.error(`[JsonTaskStore] saveTasks failed for project "${projectId}":`, err);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|