opencode-swarm-plugin 0.27.4 → 0.28.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.
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +22 -0
- package/bin/swarm.ts +19 -1
- package/dist/hive.d.ts +34 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.js +106 -0
- package/package.json +4 -3
- package/src/hive.integration.test.ts +506 -0
- package/src/hive.ts +177 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
$ bun build ./src/index.ts --outdir ./dist --target node --external @electric-sql/pglite --external swarm-mail && bun build ./src/plugin.ts --outfile ./dist/plugin.js --target node --external @electric-sql/pglite --external swarm-mail && tsc
|
|
2
|
-
Bundled 200 modules in
|
|
2
|
+
Bundled 200 modules in 34ms
|
|
3
3
|
|
|
4
|
-
index.js 1.
|
|
4
|
+
index.js 1.20 MB (entry point)
|
|
5
5
|
|
|
6
|
-
Bundled 201 modules in
|
|
6
|
+
Bundled 201 modules in 34ms
|
|
7
7
|
|
|
8
8
|
plugin.js 1.16 MB (entry point)
|
|
9
9
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# opencode-swarm-plugin
|
|
2
2
|
|
|
3
|
+
## 0.28.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`0ee4f65`](https://github.com/joelhooks/swarm-tools/commit/0ee4f656c2fb2cf62d3ef06d329d9e093d124c33) Thanks [@joelhooks](https://github.com/joelhooks)! - Add postinstall hint and update repo URL
|
|
8
|
+
|
|
9
|
+
- Show "Run swarm setup" hint after npm install
|
|
10
|
+
- Update repo URL to github.com/joelhooks/swarm-tools
|
|
11
|
+
- Add "Get started" commands to version output
|
|
12
|
+
|
|
13
|
+
## 0.28.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- [`de2fa62`](https://github.com/joelhooks/swarm-tools/commit/de2fa628524b88511e06164104ff7b5fb93d39e5) Thanks [@joelhooks](https://github.com/joelhooks)! - Add full beads→hive migration pipeline with JSONL import to PGLite
|
|
18
|
+
|
|
19
|
+
- Add `mergeHistoricBeads()` to merge beads.base.jsonl into issues.jsonl
|
|
20
|
+
- Add `importJsonlToPGLite()` to import JSONL records into PGLite database
|
|
21
|
+
- Wire both functions into `swarm setup` migration flow
|
|
22
|
+
- Fix closed_at constraint issue when importing closed cells
|
|
23
|
+
- TDD: 12 new integration tests for migration functions
|
|
24
|
+
|
|
3
25
|
## 0.27.4
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/bin/swarm.ts
CHANGED
|
@@ -30,6 +30,8 @@ import { fileURLToPath } from "url";
|
|
|
30
30
|
import {
|
|
31
31
|
checkBeadsMigrationNeeded,
|
|
32
32
|
migrateBeadsToHive,
|
|
33
|
+
mergeHistoricBeads,
|
|
34
|
+
importJsonlToPGLite,
|
|
33
35
|
} from "../src/hive";
|
|
34
36
|
|
|
35
37
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1581,6 +1583,18 @@ async function setup() {
|
|
|
1581
1583
|
if (result.migrated) {
|
|
1582
1584
|
migrateSpinner.stop("Migration complete");
|
|
1583
1585
|
p.log.success("Renamed .beads/ → .hive/");
|
|
1586
|
+
|
|
1587
|
+
// Merge historic beads into issues.jsonl
|
|
1588
|
+
const mergeResult = await mergeHistoricBeads(cwd);
|
|
1589
|
+
if (mergeResult.merged > 0) {
|
|
1590
|
+
p.log.success(`Merged ${mergeResult.merged} historic beads (${mergeResult.skipped} already present)`);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Import JSONL into PGLite database
|
|
1594
|
+
const importResult = await importJsonlToPGLite(cwd);
|
|
1595
|
+
if (importResult.imported > 0 || importResult.updated > 0) {
|
|
1596
|
+
p.log.success(`Database: ${importResult.imported} imported, ${importResult.updated} updated`);
|
|
1597
|
+
}
|
|
1584
1598
|
} else {
|
|
1585
1599
|
migrateSpinner.stop("Migration skipped");
|
|
1586
1600
|
p.log.warn(result.reason || "Unknown reason");
|
|
@@ -1952,7 +1966,11 @@ async function version() {
|
|
|
1952
1966
|
console.log(dim(" " + TAGLINE));
|
|
1953
1967
|
console.log();
|
|
1954
1968
|
console.log(" Version: " + VERSION);
|
|
1955
|
-
console.log(" Docs: https://github.com/joelhooks/
|
|
1969
|
+
console.log(" Docs: https://github.com/joelhooks/swarm-tools");
|
|
1970
|
+
console.log();
|
|
1971
|
+
console.log(cyan(" Get started:"));
|
|
1972
|
+
console.log(" swarm setup " + dim("Configure OpenCode integration"));
|
|
1973
|
+
console.log(" swarm doctor " + dim("Check dependencies"));
|
|
1956
1974
|
console.log();
|
|
1957
1975
|
|
|
1958
1976
|
// Check for updates (non-blocking)
|
package/dist/hive.d.ts
CHANGED
|
@@ -80,6 +80,40 @@ export declare function migrateBeadsToHive(projectPath: string): Promise<Migrati
|
|
|
80
80
|
* @param projectPath - Absolute path to the project root
|
|
81
81
|
*/
|
|
82
82
|
export declare function ensureHiveDirectory(projectPath: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* Merge historic beads from beads.base.jsonl into issues.jsonl
|
|
85
|
+
*
|
|
86
|
+
* This function reads beads.base.jsonl (historic data) and issues.jsonl (current data),
|
|
87
|
+
* merges them by ID (issues.jsonl version wins for duplicates), and writes the result
|
|
88
|
+
* back to issues.jsonl.
|
|
89
|
+
*
|
|
90
|
+
* Use case: After migrating from .beads to .hive, you may have a beads.base.jsonl file
|
|
91
|
+
* containing old beads that should be merged into the current issues.jsonl.
|
|
92
|
+
*
|
|
93
|
+
* @param projectPath - Absolute path to the project root
|
|
94
|
+
* @returns Object with merged and skipped counts
|
|
95
|
+
*/
|
|
96
|
+
export declare function mergeHistoricBeads(projectPath: string): Promise<{
|
|
97
|
+
merged: number;
|
|
98
|
+
skipped: number;
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* Import cells from .hive/issues.jsonl into PGLite database
|
|
102
|
+
*
|
|
103
|
+
* Reads the JSONL file and upserts each record into the cells table
|
|
104
|
+
* using the HiveAdapter. Provides granular error reporting for invalid lines.
|
|
105
|
+
*
|
|
106
|
+
* This function manually parses JSONL line-by-line to gracefully handle
|
|
107
|
+
* invalid JSON without throwing. Each valid line is imported via the adapter.
|
|
108
|
+
*
|
|
109
|
+
* @param projectPath - Absolute path to the project root
|
|
110
|
+
* @returns Object with imported, updated, and error counts
|
|
111
|
+
*/
|
|
112
|
+
export declare function importJsonlToPGLite(projectPath: string): Promise<{
|
|
113
|
+
imported: number;
|
|
114
|
+
updated: number;
|
|
115
|
+
errors: number;
|
|
116
|
+
}>;
|
|
83
117
|
/**
|
|
84
118
|
* Get or create a HiveAdapter instance for a project
|
|
85
119
|
* Exported for testing - allows tests to verify state directly
|
package/dist/hive.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hive.d.ts","sourceRoot":"","sources":["../src/hive.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hive.d.ts","sourceRoot":"","sources":["../src/hive.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -27778,6 +27778,110 @@ function ensureHiveDirectory(projectPath) {
|
|
|
27778
27778
|
mkdirSync(hiveDir, { recursive: true });
|
|
27779
27779
|
}
|
|
27780
27780
|
}
|
|
27781
|
+
async function mergeHistoricBeads(projectPath) {
|
|
27782
|
+
const { readFileSync: readFileSync2, writeFileSync, existsSync: existsSync2 } = await import("node:fs");
|
|
27783
|
+
const hiveDir = join(projectPath, ".hive");
|
|
27784
|
+
const basePath = join(hiveDir, "beads.base.jsonl");
|
|
27785
|
+
const issuesPath = join(hiveDir, "issues.jsonl");
|
|
27786
|
+
if (!existsSync2(basePath)) {
|
|
27787
|
+
return { merged: 0, skipped: 0 };
|
|
27788
|
+
}
|
|
27789
|
+
const baseContent = readFileSync2(basePath, "utf-8");
|
|
27790
|
+
const baseLines = baseContent.trim().split(`
|
|
27791
|
+
`).filter((l) => l);
|
|
27792
|
+
const baseBeads = baseLines.map((line) => JSON.parse(line));
|
|
27793
|
+
let issuesBeads = [];
|
|
27794
|
+
if (existsSync2(issuesPath)) {
|
|
27795
|
+
const issuesContent = readFileSync2(issuesPath, "utf-8");
|
|
27796
|
+
const issuesLines = issuesContent.trim().split(`
|
|
27797
|
+
`).filter((l) => l);
|
|
27798
|
+
issuesBeads = issuesLines.map((line) => JSON.parse(line));
|
|
27799
|
+
}
|
|
27800
|
+
const existingIds = new Set(issuesBeads.map((b) => b.id));
|
|
27801
|
+
let merged = 0;
|
|
27802
|
+
let skipped = 0;
|
|
27803
|
+
for (const baseBead of baseBeads) {
|
|
27804
|
+
if (existingIds.has(baseBead.id)) {
|
|
27805
|
+
skipped++;
|
|
27806
|
+
} else {
|
|
27807
|
+
issuesBeads.push(baseBead);
|
|
27808
|
+
merged++;
|
|
27809
|
+
}
|
|
27810
|
+
}
|
|
27811
|
+
const mergedContent = issuesBeads.map((b) => JSON.stringify(b)).join(`
|
|
27812
|
+
`) + `
|
|
27813
|
+
`;
|
|
27814
|
+
writeFileSync(issuesPath, mergedContent, "utf-8");
|
|
27815
|
+
return { merged, skipped };
|
|
27816
|
+
}
|
|
27817
|
+
async function importJsonlToPGLite(projectPath) {
|
|
27818
|
+
const jsonlPath = join(projectPath, ".hive", "issues.jsonl");
|
|
27819
|
+
if (!existsSync(jsonlPath)) {
|
|
27820
|
+
return { imported: 0, updated: 0, errors: 0 };
|
|
27821
|
+
}
|
|
27822
|
+
const jsonlContent = readFileSync(jsonlPath, "utf-8");
|
|
27823
|
+
if (!jsonlContent || jsonlContent.trim() === "") {
|
|
27824
|
+
return { imported: 0, updated: 0, errors: 0 };
|
|
27825
|
+
}
|
|
27826
|
+
const adapter = await getHiveAdapter(projectPath);
|
|
27827
|
+
const lines = jsonlContent.split(`
|
|
27828
|
+
`).filter((l) => l.trim());
|
|
27829
|
+
let imported = 0;
|
|
27830
|
+
let updated = 0;
|
|
27831
|
+
let errors3 = 0;
|
|
27832
|
+
for (const line of lines) {
|
|
27833
|
+
try {
|
|
27834
|
+
const cellData = JSON.parse(line);
|
|
27835
|
+
const existing = await adapter.getCell(projectPath, cellData.id);
|
|
27836
|
+
if (existing) {
|
|
27837
|
+
try {
|
|
27838
|
+
await adapter.updateCell(projectPath, cellData.id, {
|
|
27839
|
+
title: cellData.title,
|
|
27840
|
+
description: cellData.description,
|
|
27841
|
+
priority: cellData.priority,
|
|
27842
|
+
assignee: cellData.assignee
|
|
27843
|
+
});
|
|
27844
|
+
if (existing.status !== cellData.status) {
|
|
27845
|
+
if (cellData.status === "closed") {
|
|
27846
|
+
await adapter.closeCell(projectPath, cellData.id, "Imported from JSONL");
|
|
27847
|
+
} else {
|
|
27848
|
+
await adapter.changeCellStatus(projectPath, cellData.id, cellData.status);
|
|
27849
|
+
}
|
|
27850
|
+
}
|
|
27851
|
+
updated++;
|
|
27852
|
+
} catch (updateError) {
|
|
27853
|
+
errors3++;
|
|
27854
|
+
}
|
|
27855
|
+
} else {
|
|
27856
|
+
const db = await adapter.getDatabase();
|
|
27857
|
+
const status = cellData.status === "tombstone" ? "closed" : cellData.status;
|
|
27858
|
+
const isClosed = status === "closed";
|
|
27859
|
+
const closedAt = isClosed ? cellData.closed_at ? new Date(cellData.closed_at).getTime() : new Date(cellData.updated_at).getTime() : null;
|
|
27860
|
+
await db.query(`INSERT INTO cells (
|
|
27861
|
+
id, project_key, type, status, title, description, priority,
|
|
27862
|
+
parent_id, assignee, created_at, updated_at, closed_at
|
|
27863
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, [
|
|
27864
|
+
cellData.id,
|
|
27865
|
+
projectPath,
|
|
27866
|
+
cellData.issue_type,
|
|
27867
|
+
status,
|
|
27868
|
+
cellData.title,
|
|
27869
|
+
cellData.description || null,
|
|
27870
|
+
cellData.priority,
|
|
27871
|
+
cellData.parent_id || null,
|
|
27872
|
+
cellData.assignee || null,
|
|
27873
|
+
new Date(cellData.created_at).getTime(),
|
|
27874
|
+
new Date(cellData.updated_at).getTime(),
|
|
27875
|
+
closedAt
|
|
27876
|
+
]);
|
|
27877
|
+
imported++;
|
|
27878
|
+
}
|
|
27879
|
+
} catch (error45) {
|
|
27880
|
+
errors3++;
|
|
27881
|
+
}
|
|
27882
|
+
}
|
|
27883
|
+
return { imported, updated, errors: errors3 };
|
|
27884
|
+
}
|
|
27781
27885
|
var adapterCache = new Map;
|
|
27782
27886
|
async function getHiveAdapter(projectKey) {
|
|
27783
27887
|
if (adapterCache.has(projectKey)) {
|
|
@@ -35998,6 +36102,7 @@ export {
|
|
|
35998
36102
|
repoCrawlTools,
|
|
35999
36103
|
parseFrontmatter,
|
|
36000
36104
|
migrateBeadsToHive,
|
|
36105
|
+
mergeHistoricBeads,
|
|
36001
36106
|
mcpCallWithAutoInit,
|
|
36002
36107
|
mandateTools,
|
|
36003
36108
|
mandateSchemas,
|
|
@@ -36012,6 +36117,7 @@ export {
|
|
|
36012
36117
|
isAgentNotFoundError,
|
|
36013
36118
|
isAgentEvent,
|
|
36014
36119
|
invalidateSkillsCache,
|
|
36120
|
+
importJsonlToPGLite,
|
|
36015
36121
|
ifToolAvailable,
|
|
36016
36122
|
hive_update,
|
|
36017
36123
|
hive_sync,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"test:all": "bun test --timeout 60000 src/",
|
|
27
27
|
"test:watch": "bun test --watch src/",
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"publish:pkg": "npm publish --access public --provenance"
|
|
29
|
+
"publish:pkg": "npm publish --access public --provenance",
|
|
30
|
+
"postinstall": "node -e \"console.log('\\n\\x1b[33m Run \\x1b[36mswarm setup\\x1b[33m to configure OpenCode integration\\x1b[0m\\n')\""
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"@clack/prompts": "^0.11.0",
|
|
@@ -65,6 +66,6 @@
|
|
|
65
66
|
"license": "MIT",
|
|
66
67
|
"repository": {
|
|
67
68
|
"type": "git",
|
|
68
|
-
"url": "https://github.com/joelhooks/
|
|
69
|
+
"url": "https://github.com/joelhooks/swarm-tools"
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -830,4 +830,510 @@ describe("beads integration", () => {
|
|
|
830
830
|
rmSync(tempProject, { recursive: true, force: true });
|
|
831
831
|
});
|
|
832
832
|
});
|
|
833
|
+
|
|
834
|
+
describe("importJsonlToPGLite", () => {
|
|
835
|
+
it("imports empty JSONL - no-op", async () => {
|
|
836
|
+
const { importJsonlToPGLite } = await import("./hive");
|
|
837
|
+
const { mkdirSync, rmSync, writeFileSync } = await import("node:fs");
|
|
838
|
+
const { join } = await import("node:path");
|
|
839
|
+
const { tmpdir } = await import("node:os");
|
|
840
|
+
|
|
841
|
+
// Create temp project with empty JSONL
|
|
842
|
+
const tempProject = join(tmpdir(), `hive-import-test-${Date.now()}`);
|
|
843
|
+
const hiveDir = join(tempProject, ".hive");
|
|
844
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
845
|
+
writeFileSync(join(hiveDir, "issues.jsonl"), "");
|
|
846
|
+
|
|
847
|
+
const result = await importJsonlToPGLite(tempProject);
|
|
848
|
+
|
|
849
|
+
expect(result.imported).toBe(0);
|
|
850
|
+
expect(result.updated).toBe(0);
|
|
851
|
+
expect(result.errors).toBe(0);
|
|
852
|
+
|
|
853
|
+
// Cleanup
|
|
854
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("imports new records - all inserted", async () => {
|
|
858
|
+
const { importJsonlToPGLite, getHiveAdapter } = await import("./hive");
|
|
859
|
+
const { mkdirSync, rmSync, writeFileSync, unlinkSync } = await import("node:fs");
|
|
860
|
+
const { join } = await import("node:path");
|
|
861
|
+
const { tmpdir } = await import("node:os");
|
|
862
|
+
|
|
863
|
+
// Create temp project with new cells
|
|
864
|
+
const tempProject = join(tmpdir(), `hive-import-test-${Date.now()}`);
|
|
865
|
+
const hiveDir = join(tempProject, ".hive");
|
|
866
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
867
|
+
|
|
868
|
+
const cell1 = {
|
|
869
|
+
id: "bd-import-1",
|
|
870
|
+
title: "Import test 1",
|
|
871
|
+
status: "open" as const,
|
|
872
|
+
priority: 2,
|
|
873
|
+
issue_type: "task" as const,
|
|
874
|
+
created_at: new Date().toISOString(),
|
|
875
|
+
updated_at: new Date().toISOString(),
|
|
876
|
+
dependencies: [],
|
|
877
|
+
labels: [],
|
|
878
|
+
comments: [],
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const cell2 = {
|
|
882
|
+
id: "bd-import-2",
|
|
883
|
+
title: "Import test 2",
|
|
884
|
+
status: "in_progress" as const,
|
|
885
|
+
priority: 1,
|
|
886
|
+
issue_type: "bug" as const,
|
|
887
|
+
created_at: new Date().toISOString(),
|
|
888
|
+
updated_at: new Date().toISOString(),
|
|
889
|
+
dependencies: [],
|
|
890
|
+
labels: [],
|
|
891
|
+
comments: [],
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
writeFileSync(
|
|
895
|
+
join(hiveDir, "issues.jsonl"),
|
|
896
|
+
JSON.stringify(cell1) + "\n" + JSON.stringify(cell2) + "\n"
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
// CRITICAL: Call importJsonlToPGLite() which will call getHiveAdapter()
|
|
900
|
+
// The auto-migration will import cells, so we expect 0 imported here
|
|
901
|
+
// because auto-migration already did it
|
|
902
|
+
const result = await importJsonlToPGLite(tempProject);
|
|
903
|
+
|
|
904
|
+
// Auto-migration runs on first getHiveAdapter() call and imports cells
|
|
905
|
+
// So when importJsonlToPGLite() runs, cells are already there
|
|
906
|
+
// This is expected behavior - the function is idempotent
|
|
907
|
+
expect(result.imported + result.updated).toBe(2);
|
|
908
|
+
expect(result.errors).toBe(0);
|
|
909
|
+
|
|
910
|
+
// Verify cells exist in database
|
|
911
|
+
const adapter = await getHiveAdapter(tempProject);
|
|
912
|
+
const importedCell1 = await adapter.getCell(tempProject, "bd-import-1");
|
|
913
|
+
const importedCell2 = await adapter.getCell(tempProject, "bd-import-2");
|
|
914
|
+
|
|
915
|
+
expect(importedCell1).toBeDefined();
|
|
916
|
+
expect(importedCell1!.title).toBe("Import test 1");
|
|
917
|
+
expect(importedCell2).toBeDefined();
|
|
918
|
+
expect(importedCell2!.title).toBe("Import test 2");
|
|
919
|
+
|
|
920
|
+
// Cleanup
|
|
921
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it("updates existing records", async () => {
|
|
925
|
+
const { importJsonlToPGLite, getHiveAdapter } = await import("./hive");
|
|
926
|
+
const { mkdirSync, rmSync, writeFileSync, unlinkSync } = await import("node:fs");
|
|
927
|
+
const { join } = await import("node:path");
|
|
928
|
+
const { tmpdir } = await import("node:os");
|
|
929
|
+
|
|
930
|
+
// Create temp project
|
|
931
|
+
const tempProject = join(tmpdir(), `hive-import-test-${Date.now()}`);
|
|
932
|
+
const hiveDir = join(tempProject, ".hive");
|
|
933
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
934
|
+
|
|
935
|
+
// Write JSONL FIRST (before getHiveAdapter to avoid auto-migration)
|
|
936
|
+
const originalCell = {
|
|
937
|
+
id: "bd-update-1",
|
|
938
|
+
title: "Original title",
|
|
939
|
+
status: "open",
|
|
940
|
+
priority: 2,
|
|
941
|
+
issue_type: "task",
|
|
942
|
+
created_at: new Date().toISOString(),
|
|
943
|
+
updated_at: new Date().toISOString(),
|
|
944
|
+
dependencies: [],
|
|
945
|
+
labels: [],
|
|
946
|
+
comments: [],
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
writeFileSync(
|
|
950
|
+
join(hiveDir, "issues.jsonl"),
|
|
951
|
+
JSON.stringify(originalCell) + "\n"
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
// Get adapter - this will auto-migrate the original cell
|
|
955
|
+
const adapter = await getHiveAdapter(tempProject);
|
|
956
|
+
|
|
957
|
+
// Now update the JSONL with new data
|
|
958
|
+
const updatedCell = {
|
|
959
|
+
...originalCell,
|
|
960
|
+
title: "Updated title",
|
|
961
|
+
description: "New description",
|
|
962
|
+
status: "in_progress" as const,
|
|
963
|
+
priority: 0,
|
|
964
|
+
updated_at: new Date().toISOString(),
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
writeFileSync(
|
|
968
|
+
join(hiveDir, "issues.jsonl"),
|
|
969
|
+
JSON.stringify(updatedCell) + "\n"
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const result = await importJsonlToPGLite(tempProject);
|
|
973
|
+
|
|
974
|
+
expect(result.imported).toBe(0);
|
|
975
|
+
expect(result.updated).toBe(1);
|
|
976
|
+
expect(result.errors).toBe(0);
|
|
977
|
+
|
|
978
|
+
// Verify update
|
|
979
|
+
const cell = await adapter.getCell(tempProject, "bd-update-1");
|
|
980
|
+
expect(cell).toBeDefined();
|
|
981
|
+
expect(cell!.title).toBe("Updated title");
|
|
982
|
+
expect(cell!.description).toContain("New description");
|
|
983
|
+
expect(cell!.status).toBe("in_progress");
|
|
984
|
+
|
|
985
|
+
// Cleanup
|
|
986
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it("handles mixed new and existing records", async () => {
|
|
990
|
+
const { importJsonlToPGLite, getHiveAdapter } = await import("./hive");
|
|
991
|
+
const { mkdirSync, rmSync, writeFileSync } = await import("node:fs");
|
|
992
|
+
const { join } = await import("node:path");
|
|
993
|
+
const { tmpdir } = await import("node:os");
|
|
994
|
+
|
|
995
|
+
// Create temp project with NO initial JSONL (avoid auto-migration)
|
|
996
|
+
const tempProject = join(tmpdir(), `hive-import-test-${Date.now()}`);
|
|
997
|
+
const hiveDir = join(tempProject, ".hive");
|
|
998
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
999
|
+
|
|
1000
|
+
// Get adapter first (no auto-migration since no JSONL exists)
|
|
1001
|
+
const adapter = await getHiveAdapter(tempProject);
|
|
1002
|
+
|
|
1003
|
+
// Create existing cell directly via adapter
|
|
1004
|
+
await adapter.createCell(tempProject, {
|
|
1005
|
+
title: "Existing",
|
|
1006
|
+
type: "task",
|
|
1007
|
+
priority: 2,
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// Get the created cell to find its ID
|
|
1011
|
+
const cells = await adapter.queryCells(tempProject, { limit: 1 });
|
|
1012
|
+
const existingId = cells[0].id;
|
|
1013
|
+
|
|
1014
|
+
// Now write JSONL with updated existing + new cell
|
|
1015
|
+
const existingUpdated = {
|
|
1016
|
+
id: existingId,
|
|
1017
|
+
title: "Existing updated",
|
|
1018
|
+
status: "closed" as const,
|
|
1019
|
+
priority: 2,
|
|
1020
|
+
issue_type: "task" as const,
|
|
1021
|
+
created_at: new Date().toISOString(),
|
|
1022
|
+
updated_at: new Date().toISOString(),
|
|
1023
|
+
closed_at: new Date().toISOString(),
|
|
1024
|
+
dependencies: [],
|
|
1025
|
+
labels: [],
|
|
1026
|
+
comments: [],
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const newCell = {
|
|
1030
|
+
id: "bd-new",
|
|
1031
|
+
title: "Brand new",
|
|
1032
|
+
status: "open" as const,
|
|
1033
|
+
priority: 1,
|
|
1034
|
+
issue_type: "feature" as const,
|
|
1035
|
+
created_at: new Date().toISOString(),
|
|
1036
|
+
updated_at: new Date().toISOString(),
|
|
1037
|
+
dependencies: [],
|
|
1038
|
+
labels: [],
|
|
1039
|
+
comments: [],
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
writeFileSync(
|
|
1043
|
+
join(hiveDir, "issues.jsonl"),
|
|
1044
|
+
JSON.stringify(existingUpdated) + "\n" + JSON.stringify(newCell) + "\n"
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
const result = await importJsonlToPGLite(tempProject);
|
|
1048
|
+
|
|
1049
|
+
// importJsonlToPGLite() finds:
|
|
1050
|
+
// - existingId already exists (updated)
|
|
1051
|
+
// - bd-new is new (imported)
|
|
1052
|
+
expect(result.imported).toBe(1); // bd-new
|
|
1053
|
+
expect(result.updated).toBe(1); // existing cell
|
|
1054
|
+
expect(result.errors).toBe(0);
|
|
1055
|
+
|
|
1056
|
+
// Cleanup
|
|
1057
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it("skips invalid JSON lines and counts errors", async () => {
|
|
1061
|
+
const { importJsonlToPGLite } = await import("./hive");
|
|
1062
|
+
const { mkdirSync, rmSync, writeFileSync } = await import("node:fs");
|
|
1063
|
+
const { join } = await import("node:path");
|
|
1064
|
+
const { tmpdir } = await import("node:os");
|
|
1065
|
+
|
|
1066
|
+
// Create temp project
|
|
1067
|
+
const tempProject = join(tmpdir(), `hive-import-test-${Date.now()}`);
|
|
1068
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1069
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1070
|
+
|
|
1071
|
+
const validCell = {
|
|
1072
|
+
id: "bd-valid",
|
|
1073
|
+
title: "Valid",
|
|
1074
|
+
status: "open",
|
|
1075
|
+
priority: 2,
|
|
1076
|
+
issue_type: "task",
|
|
1077
|
+
created_at: new Date().toISOString(),
|
|
1078
|
+
updated_at: new Date().toISOString(),
|
|
1079
|
+
dependencies: [],
|
|
1080
|
+
labels: [],
|
|
1081
|
+
comments: [],
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
// Mix valid and invalid JSON
|
|
1085
|
+
writeFileSync(
|
|
1086
|
+
join(hiveDir, "issues.jsonl"),
|
|
1087
|
+
JSON.stringify(validCell) + "\n" +
|
|
1088
|
+
"{ invalid json \n" +
|
|
1089
|
+
'{"id":"incomplete"\n'
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
const result = await importJsonlToPGLite(tempProject);
|
|
1093
|
+
|
|
1094
|
+
expect(result.imported).toBe(1); // Only the valid one
|
|
1095
|
+
expect(result.errors).toBe(2); // Two invalid lines
|
|
1096
|
+
|
|
1097
|
+
// Cleanup
|
|
1098
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it("handles missing JSONL file gracefully", async () => {
|
|
1102
|
+
const { importJsonlToPGLite } = await import("./hive");
|
|
1103
|
+
const { mkdirSync, rmSync } = await import("node:fs");
|
|
1104
|
+
const { join } = await import("node:path");
|
|
1105
|
+
const { tmpdir } = await import("node:os");
|
|
1106
|
+
|
|
1107
|
+
// Create temp project without issues.jsonl
|
|
1108
|
+
const tempProject = join(tmpdir(), `hive-import-test-${Date.now()}`);
|
|
1109
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1110
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1111
|
+
|
|
1112
|
+
const result = await importJsonlToPGLite(tempProject);
|
|
1113
|
+
|
|
1114
|
+
expect(result.imported).toBe(0);
|
|
1115
|
+
expect(result.updated).toBe(0);
|
|
1116
|
+
expect(result.errors).toBe(0);
|
|
1117
|
+
|
|
1118
|
+
// Cleanup
|
|
1119
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
describe("mergeHistoricBeads", () => {
|
|
1124
|
+
it("merges empty base file - no changes", async () => {
|
|
1125
|
+
const { mergeHistoricBeads } = await import("./hive");
|
|
1126
|
+
const { mkdirSync, rmSync, writeFileSync } = await import("node:fs");
|
|
1127
|
+
const { join } = await import("node:path");
|
|
1128
|
+
const { tmpdir } = await import("node:os");
|
|
1129
|
+
|
|
1130
|
+
// Create temp project with .hive directory
|
|
1131
|
+
const tempProject = join(tmpdir(), `hive-merge-test-${Date.now()}`);
|
|
1132
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1133
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1134
|
+
|
|
1135
|
+
// Create empty base file
|
|
1136
|
+
writeFileSync(join(hiveDir, "beads.base.jsonl"), "");
|
|
1137
|
+
|
|
1138
|
+
// Create issues.jsonl with one bead
|
|
1139
|
+
const existingBead = { id: "bd-existing", title: "Existing bead" };
|
|
1140
|
+
writeFileSync(join(hiveDir, "issues.jsonl"), JSON.stringify(existingBead) + "\n");
|
|
1141
|
+
|
|
1142
|
+
const result = await mergeHistoricBeads(tempProject);
|
|
1143
|
+
|
|
1144
|
+
expect(result.merged).toBe(0);
|
|
1145
|
+
expect(result.skipped).toBe(0);
|
|
1146
|
+
|
|
1147
|
+
// Cleanup
|
|
1148
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it("merges empty issues file - all base records imported", async () => {
|
|
1152
|
+
const { mergeHistoricBeads } = await import("./hive");
|
|
1153
|
+
const { mkdirSync, rmSync, writeFileSync, readFileSync } = await import("node:fs");
|
|
1154
|
+
const { join } = await import("node:path");
|
|
1155
|
+
const { tmpdir } = await import("node:os");
|
|
1156
|
+
|
|
1157
|
+
// Create temp project
|
|
1158
|
+
const tempProject = join(tmpdir(), `hive-merge-test-${Date.now()}`);
|
|
1159
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1160
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1161
|
+
|
|
1162
|
+
// Create base file with 2 beads
|
|
1163
|
+
const baseBead1 = { id: "bd-base-1", title: "Historic bead 1" };
|
|
1164
|
+
const baseBead2 = { id: "bd-base-2", title: "Historic bead 2" };
|
|
1165
|
+
writeFileSync(
|
|
1166
|
+
join(hiveDir, "beads.base.jsonl"),
|
|
1167
|
+
JSON.stringify(baseBead1) + "\n" + JSON.stringify(baseBead2) + "\n"
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
// Empty issues file
|
|
1171
|
+
writeFileSync(join(hiveDir, "issues.jsonl"), "");
|
|
1172
|
+
|
|
1173
|
+
const result = await mergeHistoricBeads(tempProject);
|
|
1174
|
+
|
|
1175
|
+
expect(result.merged).toBe(2);
|
|
1176
|
+
expect(result.skipped).toBe(0);
|
|
1177
|
+
|
|
1178
|
+
// Verify issues.jsonl now has both beads
|
|
1179
|
+
const issuesContent = readFileSync(join(hiveDir, "issues.jsonl"), "utf-8");
|
|
1180
|
+
const lines = issuesContent.trim().split("\n").filter(l => l);
|
|
1181
|
+
expect(lines).toHaveLength(2);
|
|
1182
|
+
|
|
1183
|
+
const beads = lines.map(line => JSON.parse(line));
|
|
1184
|
+
expect(beads.find(b => b.id === "bd-base-1")).toBeDefined();
|
|
1185
|
+
expect(beads.find(b => b.id === "bd-base-2")).toBeDefined();
|
|
1186
|
+
|
|
1187
|
+
// Cleanup
|
|
1188
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
it("overlapping IDs - issues.jsonl wins (more recent)", async () => {
|
|
1192
|
+
const { mergeHistoricBeads } = await import("./hive");
|
|
1193
|
+
const { mkdirSync, rmSync, writeFileSync, readFileSync } = await import("node:fs");
|
|
1194
|
+
const { join } = await import("node:path");
|
|
1195
|
+
const { tmpdir } = await import("node:os");
|
|
1196
|
+
|
|
1197
|
+
// Create temp project
|
|
1198
|
+
const tempProject = join(tmpdir(), `hive-merge-test-${Date.now()}`);
|
|
1199
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1200
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1201
|
+
|
|
1202
|
+
// Base has old version of bd-overlap
|
|
1203
|
+
const baseOldVersion = { id: "bd-overlap", title: "Old title", status: "open" };
|
|
1204
|
+
writeFileSync(
|
|
1205
|
+
join(hiveDir, "beads.base.jsonl"),
|
|
1206
|
+
JSON.stringify(baseOldVersion) + "\n"
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
// Issues has new version (updated)
|
|
1210
|
+
const issuesNewVersion = { id: "bd-overlap", title: "New title", status: "closed" };
|
|
1211
|
+
writeFileSync(
|
|
1212
|
+
join(hiveDir, "issues.jsonl"),
|
|
1213
|
+
JSON.stringify(issuesNewVersion) + "\n"
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
const result = await mergeHistoricBeads(tempProject);
|
|
1217
|
+
|
|
1218
|
+
expect(result.merged).toBe(0); // Nothing new to merge
|
|
1219
|
+
expect(result.skipped).toBe(1); // Skipped the old version
|
|
1220
|
+
|
|
1221
|
+
// Verify issues.jsonl still has new version (unchanged)
|
|
1222
|
+
const issuesContent = readFileSync(join(hiveDir, "issues.jsonl"), "utf-8");
|
|
1223
|
+
const bead = JSON.parse(issuesContent.trim());
|
|
1224
|
+
expect(bead.title).toBe("New title");
|
|
1225
|
+
expect(bead.status).toBe("closed");
|
|
1226
|
+
|
|
1227
|
+
// Cleanup
|
|
1228
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it("no overlap - all records combined", async () => {
|
|
1232
|
+
const { mergeHistoricBeads } = await import("./hive");
|
|
1233
|
+
const { mkdirSync, rmSync, writeFileSync, readFileSync } = await import("node:fs");
|
|
1234
|
+
const { join } = await import("node:path");
|
|
1235
|
+
const { tmpdir } = await import("node:os");
|
|
1236
|
+
|
|
1237
|
+
// Create temp project
|
|
1238
|
+
const tempProject = join(tmpdir(), `hive-merge-test-${Date.now()}`);
|
|
1239
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1240
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1241
|
+
|
|
1242
|
+
// Base has 2 beads
|
|
1243
|
+
const baseBead1 = { id: "bd-base-1", title: "Historic 1" };
|
|
1244
|
+
const baseBead2 = { id: "bd-base-2", title: "Historic 2" };
|
|
1245
|
+
writeFileSync(
|
|
1246
|
+
join(hiveDir, "beads.base.jsonl"),
|
|
1247
|
+
JSON.stringify(baseBead1) + "\n" + JSON.stringify(baseBead2) + "\n"
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
// Issues has 2 different beads
|
|
1251
|
+
const issuesBead1 = { id: "bd-current-1", title: "Current 1" };
|
|
1252
|
+
const issuesBead2 = { id: "bd-current-2", title: "Current 2" };
|
|
1253
|
+
writeFileSync(
|
|
1254
|
+
join(hiveDir, "issues.jsonl"),
|
|
1255
|
+
JSON.stringify(issuesBead1) + "\n" + JSON.stringify(issuesBead2) + "\n"
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
const result = await mergeHistoricBeads(tempProject);
|
|
1259
|
+
|
|
1260
|
+
expect(result.merged).toBe(2); // Added 2 from base
|
|
1261
|
+
expect(result.skipped).toBe(0);
|
|
1262
|
+
|
|
1263
|
+
// Verify issues.jsonl now has all 4 beads
|
|
1264
|
+
const issuesContent = readFileSync(join(hiveDir, "issues.jsonl"), "utf-8");
|
|
1265
|
+
const lines = issuesContent.trim().split("\n").filter(l => l);
|
|
1266
|
+
expect(lines).toHaveLength(4);
|
|
1267
|
+
|
|
1268
|
+
const beads = lines.map(line => JSON.parse(line));
|
|
1269
|
+
expect(beads.find(b => b.id === "bd-base-1")).toBeDefined();
|
|
1270
|
+
expect(beads.find(b => b.id === "bd-base-2")).toBeDefined();
|
|
1271
|
+
expect(beads.find(b => b.id === "bd-current-1")).toBeDefined();
|
|
1272
|
+
expect(beads.find(b => b.id === "bd-current-2")).toBeDefined();
|
|
1273
|
+
|
|
1274
|
+
// Cleanup
|
|
1275
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
it("missing base file - graceful handling", async () => {
|
|
1279
|
+
const { mergeHistoricBeads } = await import("./hive");
|
|
1280
|
+
const { mkdirSync, rmSync, writeFileSync } = await import("node:fs");
|
|
1281
|
+
const { join } = await import("node:path");
|
|
1282
|
+
const { tmpdir } = await import("node:os");
|
|
1283
|
+
|
|
1284
|
+
// Create temp project with .hive but NO base file
|
|
1285
|
+
const tempProject = join(tmpdir(), `hive-merge-test-${Date.now()}`);
|
|
1286
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1287
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1288
|
+
|
|
1289
|
+
// Issues exists, base doesn't
|
|
1290
|
+
const issuesBead = { id: "bd-current", title: "Current" };
|
|
1291
|
+
writeFileSync(join(hiveDir, "issues.jsonl"), JSON.stringify(issuesBead) + "\n");
|
|
1292
|
+
|
|
1293
|
+
const result = await mergeHistoricBeads(tempProject);
|
|
1294
|
+
|
|
1295
|
+
// Should return zeros, not throw
|
|
1296
|
+
expect(result.merged).toBe(0);
|
|
1297
|
+
expect(result.skipped).toBe(0);
|
|
1298
|
+
|
|
1299
|
+
// Cleanup
|
|
1300
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
it("missing issues file - creates it from base", async () => {
|
|
1304
|
+
const { mergeHistoricBeads } = await import("./hive");
|
|
1305
|
+
const { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } = await import("node:fs");
|
|
1306
|
+
const { join } = await import("node:path");
|
|
1307
|
+
const { tmpdir } = await import("node:os");
|
|
1308
|
+
|
|
1309
|
+
// Create temp project with base but NO issues file
|
|
1310
|
+
const tempProject = join(tmpdir(), `hive-merge-test-${Date.now()}`);
|
|
1311
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1312
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1313
|
+
|
|
1314
|
+
// Base exists, issues doesn't
|
|
1315
|
+
const baseBead = { id: "bd-base", title: "Historic" };
|
|
1316
|
+
writeFileSync(
|
|
1317
|
+
join(hiveDir, "beads.base.jsonl"),
|
|
1318
|
+
JSON.stringify(baseBead) + "\n"
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
const issuesPath = join(hiveDir, "issues.jsonl");
|
|
1322
|
+
expect(existsSync(issuesPath)).toBe(false);
|
|
1323
|
+
|
|
1324
|
+
const result = await mergeHistoricBeads(tempProject);
|
|
1325
|
+
|
|
1326
|
+
expect(result.merged).toBe(1);
|
|
1327
|
+
expect(result.skipped).toBe(0);
|
|
1328
|
+
|
|
1329
|
+
// Verify issues.jsonl was created
|
|
1330
|
+
expect(existsSync(issuesPath)).toBe(true);
|
|
1331
|
+
const content = readFileSync(issuesPath, "utf-8");
|
|
1332
|
+
const bead = JSON.parse(content.trim());
|
|
1333
|
+
expect(bead.id).toBe("bd-base");
|
|
1334
|
+
|
|
1335
|
+
// Cleanup
|
|
1336
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
833
1339
|
});
|
package/src/hive.ts
CHANGED
|
@@ -235,6 +235,183 @@ export function ensureHiveDirectory(projectPath: string): void {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Merge historic beads from beads.base.jsonl into issues.jsonl
|
|
240
|
+
*
|
|
241
|
+
* This function reads beads.base.jsonl (historic data) and issues.jsonl (current data),
|
|
242
|
+
* merges them by ID (issues.jsonl version wins for duplicates), and writes the result
|
|
243
|
+
* back to issues.jsonl.
|
|
244
|
+
*
|
|
245
|
+
* Use case: After migrating from .beads to .hive, you may have a beads.base.jsonl file
|
|
246
|
+
* containing old beads that should be merged into the current issues.jsonl.
|
|
247
|
+
*
|
|
248
|
+
* @param projectPath - Absolute path to the project root
|
|
249
|
+
* @returns Object with merged and skipped counts
|
|
250
|
+
*/
|
|
251
|
+
export async function mergeHistoricBeads(projectPath: string): Promise<{merged: number, skipped: number}> {
|
|
252
|
+
const { readFileSync, writeFileSync, existsSync } = await import("node:fs");
|
|
253
|
+
const hiveDir = join(projectPath, ".hive");
|
|
254
|
+
const basePath = join(hiveDir, "beads.base.jsonl");
|
|
255
|
+
const issuesPath = join(hiveDir, "issues.jsonl");
|
|
256
|
+
|
|
257
|
+
// If base file doesn't exist, nothing to merge
|
|
258
|
+
if (!existsSync(basePath)) {
|
|
259
|
+
return { merged: 0, skipped: 0 };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Read base file
|
|
263
|
+
const baseContent = readFileSync(basePath, "utf-8");
|
|
264
|
+
const baseLines = baseContent.trim().split("\n").filter(l => l);
|
|
265
|
+
const baseBeads = baseLines.map(line => JSON.parse(line));
|
|
266
|
+
|
|
267
|
+
// Read issues file (or create empty if missing)
|
|
268
|
+
let issuesBeads: any[] = [];
|
|
269
|
+
if (existsSync(issuesPath)) {
|
|
270
|
+
const issuesContent = readFileSync(issuesPath, "utf-8");
|
|
271
|
+
const issuesLines = issuesContent.trim().split("\n").filter(l => l);
|
|
272
|
+
issuesBeads = issuesLines.map(line => JSON.parse(line));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Build set of existing IDs in issues.jsonl
|
|
276
|
+
const existingIds = new Set(issuesBeads.map(b => b.id));
|
|
277
|
+
|
|
278
|
+
// Merge: add beads from base that aren't in issues
|
|
279
|
+
let merged = 0;
|
|
280
|
+
let skipped = 0;
|
|
281
|
+
|
|
282
|
+
for (const baseBead of baseBeads) {
|
|
283
|
+
if (existingIds.has(baseBead.id)) {
|
|
284
|
+
skipped++;
|
|
285
|
+
} else {
|
|
286
|
+
issuesBeads.push(baseBead);
|
|
287
|
+
merged++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Write merged result back to issues.jsonl
|
|
292
|
+
const mergedContent = issuesBeads.map(b => JSON.stringify(b)).join("\n") + "\n";
|
|
293
|
+
writeFileSync(issuesPath, mergedContent, "utf-8");
|
|
294
|
+
|
|
295
|
+
return { merged, skipped };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Import cells from .hive/issues.jsonl into PGLite database
|
|
300
|
+
*
|
|
301
|
+
* Reads the JSONL file and upserts each record into the cells table
|
|
302
|
+
* using the HiveAdapter. Provides granular error reporting for invalid lines.
|
|
303
|
+
*
|
|
304
|
+
* This function manually parses JSONL line-by-line to gracefully handle
|
|
305
|
+
* invalid JSON without throwing. Each valid line is imported via the adapter.
|
|
306
|
+
*
|
|
307
|
+
* @param projectPath - Absolute path to the project root
|
|
308
|
+
* @returns Object with imported, updated, and error counts
|
|
309
|
+
*/
|
|
310
|
+
export async function importJsonlToPGLite(projectPath: string): Promise<{
|
|
311
|
+
imported: number;
|
|
312
|
+
updated: number;
|
|
313
|
+
errors: number;
|
|
314
|
+
}> {
|
|
315
|
+
const jsonlPath = join(projectPath, ".hive", "issues.jsonl");
|
|
316
|
+
|
|
317
|
+
// Handle missing file gracefully
|
|
318
|
+
if (!existsSync(jsonlPath)) {
|
|
319
|
+
return { imported: 0, updated: 0, errors: 0 };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Read JSONL content
|
|
323
|
+
const jsonlContent = readFileSync(jsonlPath, "utf-8");
|
|
324
|
+
|
|
325
|
+
// Handle empty file
|
|
326
|
+
if (!jsonlContent || jsonlContent.trim() === "") {
|
|
327
|
+
return { imported: 0, updated: 0, errors: 0 };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Get adapter - but we need to prevent auto-migration from running
|
|
331
|
+
// Auto-migration only runs if DB is empty, so we check first
|
|
332
|
+
const adapter = await getHiveAdapter(projectPath);
|
|
333
|
+
|
|
334
|
+
// Parse JSONL line-by-line, tolerating invalid JSON
|
|
335
|
+
const lines = jsonlContent.split("\n").filter(l => l.trim());
|
|
336
|
+
let imported = 0;
|
|
337
|
+
let updated = 0;
|
|
338
|
+
let errors = 0;
|
|
339
|
+
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
try {
|
|
342
|
+
const cellData = JSON.parse(line);
|
|
343
|
+
|
|
344
|
+
// Check if cell exists
|
|
345
|
+
const existing = await adapter.getCell(projectPath, cellData.id);
|
|
346
|
+
|
|
347
|
+
if (existing) {
|
|
348
|
+
// Update existing cell
|
|
349
|
+
try {
|
|
350
|
+
await adapter.updateCell(projectPath, cellData.id, {
|
|
351
|
+
title: cellData.title,
|
|
352
|
+
description: cellData.description,
|
|
353
|
+
priority: cellData.priority,
|
|
354
|
+
assignee: cellData.assignee,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Update status if needed - use closeCell for 'closed' status
|
|
358
|
+
if (existing.status !== cellData.status) {
|
|
359
|
+
if (cellData.status === "closed") {
|
|
360
|
+
await adapter.closeCell(projectPath, cellData.id, "Imported from JSONL");
|
|
361
|
+
} else {
|
|
362
|
+
await adapter.changeCellStatus(projectPath, cellData.id, cellData.status);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
updated++;
|
|
367
|
+
} catch (updateError) {
|
|
368
|
+
// Update failed - count as error
|
|
369
|
+
errors++;
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
// Create new cell - use direct DB insert to preserve ID
|
|
373
|
+
const db = await adapter.getDatabase();
|
|
374
|
+
|
|
375
|
+
const status = cellData.status === "tombstone" ? "closed" : cellData.status;
|
|
376
|
+
const isClosed = status === "closed";
|
|
377
|
+
const closedAt = isClosed
|
|
378
|
+
? (cellData.closed_at
|
|
379
|
+
? new Date(cellData.closed_at).getTime()
|
|
380
|
+
: new Date(cellData.updated_at).getTime())
|
|
381
|
+
: null;
|
|
382
|
+
|
|
383
|
+
await db.query(
|
|
384
|
+
`INSERT INTO cells (
|
|
385
|
+
id, project_key, type, status, title, description, priority,
|
|
386
|
+
parent_id, assignee, created_at, updated_at, closed_at
|
|
387
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
|
388
|
+
[
|
|
389
|
+
cellData.id,
|
|
390
|
+
projectPath,
|
|
391
|
+
cellData.issue_type,
|
|
392
|
+
status,
|
|
393
|
+
cellData.title,
|
|
394
|
+
cellData.description || null,
|
|
395
|
+
cellData.priority,
|
|
396
|
+
cellData.parent_id || null,
|
|
397
|
+
cellData.assignee || null,
|
|
398
|
+
new Date(cellData.created_at).getTime(),
|
|
399
|
+
new Date(cellData.updated_at).getTime(),
|
|
400
|
+
closedAt,
|
|
401
|
+
]
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
imported++;
|
|
405
|
+
}
|
|
406
|
+
} catch (error) {
|
|
407
|
+
// Invalid JSON or import error - count and continue
|
|
408
|
+
errors++;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { imported, updated, errors };
|
|
413
|
+
}
|
|
414
|
+
|
|
238
415
|
// ============================================================================
|
|
239
416
|
// Adapter Singleton
|
|
240
417
|
// ============================================================================
|