idea-manager 1.0.1 → 1.1.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/next.config.mjs +1 -1
- package/package.json +2 -3
- package/src/app/api/projects/[id]/brainstorm/route.ts +3 -0
- package/src/app/api/projects/[id]/git-sync/route.ts +2 -0
- package/src/app/api/projects/[id]/route.ts +4 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +4 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +3 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +3 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +4 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +3 -0
- package/src/app/api/projects/[id]/sub-projects/route.ts +4 -0
- package/src/app/api/projects/route.ts +3 -0
- package/src/cli.ts +29 -8
- package/src/lib/db/index.ts +184 -10
- package/src/lib/db/schema.ts +2 -3
- package/src/lib/sync/exporter.ts +3 -2
- package/src/lib/sync/importer.ts +5 -3
- package/src/lib/sync/index.ts +4 -4
- package/src/lib/watcher.ts +2 -0
package/next.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idea-manager",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Turn free-form brainstorming into structured task trees with AI-generated prompts. Built-in MCP Server for autonomous AI agent execution. Local-first with SQLite, cross-PC sync via Git.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brainstorm",
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
"@dnd-kit/sortable": "^10.0.0",
|
|
49
49
|
"@dnd-kit/utilities": "^3.2.2",
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
51
|
-
"better-sqlite3": "^12.6.2",
|
|
52
51
|
"commander": "^14.0.3",
|
|
53
52
|
"nanoid": "^5.1.6",
|
|
54
53
|
"next": "16.1.6",
|
|
@@ -57,11 +56,11 @@
|
|
|
57
56
|
"react-dom": "19.2.3",
|
|
58
57
|
"react-markdown": "^10.1.0",
|
|
59
58
|
"remark-gfm": "^4.0.1",
|
|
59
|
+
"sql.js": "^1.14.1",
|
|
60
60
|
"tsx": "^4.21.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@tailwindcss/postcss": "^4",
|
|
64
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
65
64
|
"@types/node": "^20",
|
|
66
65
|
"@types/react": "^19",
|
|
67
66
|
"@types/react-dom": "^19",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { getBrainstorm, updateBrainstorm } from '@/lib/db/queries/brainstorms';
|
|
3
3
|
import { getProject } from '@/lib/db/queries/projects';
|
|
4
|
+
import { ensureDb } from '@/lib/db';
|
|
4
5
|
|
|
5
6
|
export async function GET(
|
|
6
7
|
_request: NextRequest,
|
|
7
8
|
{ params }: { params: Promise<{ id: string }> },
|
|
8
9
|
) {
|
|
10
|
+
await ensureDb();
|
|
9
11
|
const { id } = await params;
|
|
10
12
|
const project = getProject(id);
|
|
11
13
|
if (!project) {
|
|
@@ -20,6 +22,7 @@ export async function PUT(
|
|
|
20
22
|
request: NextRequest,
|
|
21
23
|
{ params }: { params: Promise<{ id: string }> },
|
|
22
24
|
) {
|
|
25
|
+
await ensureDb();
|
|
23
26
|
const { id } = await params;
|
|
24
27
|
const body = await request.json();
|
|
25
28
|
const { content } = body;
|
|
@@ -4,6 +4,7 @@ import { execFile } from 'child_process';
|
|
|
4
4
|
import { existsSync, readdirSync, statSync } from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import type { IGitSyncResult } from '@/types';
|
|
7
|
+
import { ensureDb } from '@/lib/db';
|
|
7
8
|
|
|
8
9
|
function gitPull(cwd: string): Promise<{ stdout: string; stderr: string }> {
|
|
9
10
|
return new Promise((resolve, reject) => {
|
|
@@ -68,6 +69,7 @@ export async function POST(
|
|
|
68
69
|
_request: NextRequest,
|
|
69
70
|
{ params }: { params: Promise<{ id: string }> },
|
|
70
71
|
) {
|
|
72
|
+
await ensureDb();
|
|
71
73
|
const { id } = await params;
|
|
72
74
|
const project = getProject(id);
|
|
73
75
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { getProject, updateProject, deleteProject } from '@/lib/db/queries/projects';
|
|
3
|
+
import { ensureDb } from '@/lib/db';
|
|
3
4
|
|
|
4
5
|
export async function GET(
|
|
5
6
|
_request: NextRequest,
|
|
6
7
|
{ params }: { params: Promise<{ id: string }> },
|
|
7
8
|
) {
|
|
9
|
+
await ensureDb();
|
|
8
10
|
const { id } = await params;
|
|
9
11
|
const project = getProject(id);
|
|
10
12
|
if (!project) {
|
|
@@ -17,6 +19,7 @@ export async function PUT(
|
|
|
17
19
|
request: NextRequest,
|
|
18
20
|
{ params }: { params: Promise<{ id: string }> },
|
|
19
21
|
) {
|
|
22
|
+
await ensureDb();
|
|
20
23
|
const { id } = await params;
|
|
21
24
|
const body = await request.json();
|
|
22
25
|
const project = updateProject(id, body);
|
|
@@ -30,6 +33,7 @@ export async function DELETE(
|
|
|
30
33
|
_request: NextRequest,
|
|
31
34
|
{ params }: { params: Promise<{ id: string }> },
|
|
32
35
|
) {
|
|
36
|
+
await ensureDb();
|
|
33
37
|
const { id } = await params;
|
|
34
38
|
const deleted = deleteProject(id);
|
|
35
39
|
if (!deleted) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { getSubProject, updateSubProject, deleteSubProject } from '@/lib/db/queries/sub-projects';
|
|
3
|
+
import { ensureDb } from '@/lib/db';
|
|
3
4
|
|
|
4
5
|
export async function GET(
|
|
5
6
|
_request: NextRequest,
|
|
6
7
|
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
7
8
|
) {
|
|
9
|
+
await ensureDb();
|
|
8
10
|
const { subId } = await params;
|
|
9
11
|
const sp = getSubProject(subId);
|
|
10
12
|
if (!sp) {
|
|
@@ -17,6 +19,7 @@ export async function PUT(
|
|
|
17
19
|
request: NextRequest,
|
|
18
20
|
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
19
21
|
) {
|
|
22
|
+
await ensureDb();
|
|
20
23
|
const { subId } = await params;
|
|
21
24
|
const body = await request.json();
|
|
22
25
|
const sp = updateSubProject(subId, body);
|
|
@@ -30,6 +33,7 @@ export async function DELETE(
|
|
|
30
33
|
_request: NextRequest,
|
|
31
34
|
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
32
35
|
) {
|
|
36
|
+
await ensureDb();
|
|
33
37
|
const { subId } = await params;
|
|
34
38
|
const deleted = deleteSubProject(subId);
|
|
35
39
|
if (!deleted) {
|
|
@@ -5,11 +5,13 @@ import { getTaskPrompt } from '@/lib/db/queries/task-prompts';
|
|
|
5
5
|
import { getBrainstorm } from '@/lib/db/queries/brainstorms';
|
|
6
6
|
import { getProject } from '@/lib/db/queries/projects';
|
|
7
7
|
import { runAgent } from '@/lib/ai/client';
|
|
8
|
+
import { ensureDb } from '@/lib/db';
|
|
8
9
|
|
|
9
10
|
export async function GET(
|
|
10
11
|
_request: NextRequest,
|
|
11
12
|
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
12
13
|
) {
|
|
14
|
+
await ensureDb();
|
|
13
15
|
const { taskId } = await params;
|
|
14
16
|
const conversations = getTaskConversations(taskId);
|
|
15
17
|
return NextResponse.json(conversations);
|
|
@@ -19,6 +21,7 @@ export async function POST(
|
|
|
19
21
|
request: NextRequest,
|
|
20
22
|
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
21
23
|
) {
|
|
24
|
+
await ensureDb();
|
|
22
25
|
const { id: projectId, taskId } = await params;
|
|
23
26
|
const body = await request.json();
|
|
24
27
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { getTaskPrompt, upsertTaskPrompt } from '@/lib/db/queries/task-prompts';
|
|
3
|
+
import { ensureDb } from '@/lib/db';
|
|
3
4
|
|
|
4
5
|
export async function GET(
|
|
5
6
|
_request: NextRequest,
|
|
6
7
|
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
7
8
|
) {
|
|
9
|
+
await ensureDb();
|
|
8
10
|
const { taskId } = await params;
|
|
9
11
|
const prompt = getTaskPrompt(taskId);
|
|
10
12
|
return NextResponse.json(prompt ?? { content: '', prompt_type: 'manual' });
|
|
@@ -14,6 +16,7 @@ export async function PUT(
|
|
|
14
16
|
request: NextRequest,
|
|
15
17
|
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
16
18
|
) {
|
|
19
|
+
await ensureDb();
|
|
17
20
|
const { taskId } = await params;
|
|
18
21
|
const body = await request.json();
|
|
19
22
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { getTask, updateTask, deleteTask } from '@/lib/db/queries/tasks';
|
|
3
|
+
import { ensureDb } from '@/lib/db';
|
|
3
4
|
|
|
4
5
|
export async function GET(
|
|
5
6
|
_request: NextRequest,
|
|
6
7
|
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
7
8
|
) {
|
|
9
|
+
await ensureDb();
|
|
8
10
|
const { taskId } = await params;
|
|
9
11
|
const task = getTask(taskId);
|
|
10
12
|
if (!task) {
|
|
@@ -17,6 +19,7 @@ export async function PUT(
|
|
|
17
19
|
request: NextRequest,
|
|
18
20
|
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
19
21
|
) {
|
|
22
|
+
await ensureDb();
|
|
20
23
|
const { taskId } = await params;
|
|
21
24
|
const body = await request.json();
|
|
22
25
|
const task = updateTask(taskId, body);
|
|
@@ -30,6 +33,7 @@ export async function DELETE(
|
|
|
30
33
|
_request: NextRequest,
|
|
31
34
|
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
32
35
|
) {
|
|
36
|
+
await ensureDb();
|
|
33
37
|
const { taskId } = await params;
|
|
34
38
|
const deleted = deleteTask(taskId);
|
|
35
39
|
if (!deleted) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { getTasks, createTask } from '@/lib/db/queries/tasks';
|
|
3
|
+
import { ensureDb } from '@/lib/db';
|
|
3
4
|
|
|
4
5
|
export async function GET(
|
|
5
6
|
_request: NextRequest,
|
|
6
7
|
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
7
8
|
) {
|
|
9
|
+
await ensureDb();
|
|
8
10
|
const { subId } = await params;
|
|
9
11
|
const tasks = getTasks(subId);
|
|
10
12
|
return NextResponse.json(tasks);
|
|
@@ -14,6 +16,7 @@ export async function POST(
|
|
|
14
16
|
request: NextRequest,
|
|
15
17
|
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
16
18
|
) {
|
|
19
|
+
await ensureDb();
|
|
17
20
|
const { id, subId } = await params;
|
|
18
21
|
const body = await request.json();
|
|
19
22
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { getSubProjectsWithStats, createSubProject, reorderSubProjects } from '@/lib/db/queries/sub-projects';
|
|
3
|
+
import { ensureDb } from '@/lib/db';
|
|
3
4
|
|
|
4
5
|
export async function GET(
|
|
5
6
|
_request: NextRequest,
|
|
6
7
|
{ params }: { params: Promise<{ id: string }> },
|
|
7
8
|
) {
|
|
9
|
+
await ensureDb();
|
|
8
10
|
const { id } = await params;
|
|
9
11
|
const subProjects = getSubProjectsWithStats(id);
|
|
10
12
|
return NextResponse.json(subProjects);
|
|
@@ -14,6 +16,7 @@ export async function POST(
|
|
|
14
16
|
request: NextRequest,
|
|
15
17
|
{ params }: { params: Promise<{ id: string }> },
|
|
16
18
|
) {
|
|
19
|
+
await ensureDb();
|
|
17
20
|
const { id } = await params;
|
|
18
21
|
const body = await request.json();
|
|
19
22
|
|
|
@@ -34,6 +37,7 @@ export async function PUT(
|
|
|
34
37
|
request: NextRequest,
|
|
35
38
|
{ params }: { params: Promise<{ id: string }> },
|
|
36
39
|
) {
|
|
40
|
+
await ensureDb();
|
|
37
41
|
const { id } = await params;
|
|
38
42
|
const body = await request.json();
|
|
39
43
|
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { listProjects, createProject } from '@/lib/db/queries/projects';
|
|
3
|
+
import { ensureDb } from '@/lib/db';
|
|
3
4
|
|
|
4
5
|
export async function GET() {
|
|
6
|
+
await ensureDb();
|
|
5
7
|
const projects = listProjects();
|
|
6
8
|
return NextResponse.json(projects);
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export async function POST(request: NextRequest) {
|
|
12
|
+
await ensureDb();
|
|
10
13
|
const body = await request.json();
|
|
11
14
|
const { name, description } = body;
|
|
12
15
|
|
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
+
import { ensureDb } from './lib/db';
|
|
4
5
|
import { startMcpServer } from './lib/mcp/server';
|
|
5
6
|
import { listProjects, getProject } from './lib/db/queries/projects';
|
|
6
7
|
import { getSubProjects } from './lib/db/queries/sub-projects';
|
|
@@ -78,6 +79,7 @@ program
|
|
|
78
79
|
.command('mcp')
|
|
79
80
|
.description('Start MCP server (stdio mode)')
|
|
80
81
|
.action(async () => {
|
|
82
|
+
await ensureDb();
|
|
81
83
|
const ctx: McpToolContext = {
|
|
82
84
|
listProjects,
|
|
83
85
|
getProject,
|
|
@@ -109,27 +111,46 @@ program
|
|
|
109
111
|
|
|
110
112
|
program
|
|
111
113
|
.command('start')
|
|
112
|
-
.description('Start the web UI
|
|
114
|
+
.description('Start the web UI on port 3456')
|
|
113
115
|
.option('-p, --port <port>', 'Port number', '3456')
|
|
114
116
|
.action(async (opts) => {
|
|
115
117
|
const port = opts.port;
|
|
116
|
-
|
|
117
|
-
console.log(` Starting on http://localhost:${port}\n`);
|
|
118
|
+
const fs = await import('fs');
|
|
118
119
|
|
|
119
|
-
// Resolve next CLI
|
|
120
|
-
// and npm global install hoisting issues
|
|
120
|
+
// Resolve next CLI
|
|
121
121
|
let nextCli: string;
|
|
122
122
|
try {
|
|
123
123
|
nextCli = require.resolve('next/dist/bin/next', { paths: [PKG_ROOT] });
|
|
124
124
|
} catch {
|
|
125
|
-
// Fallback: try to find next package manually
|
|
126
125
|
nextCli = path.join(PKG_ROOT, 'node_modules', 'next', 'dist', 'bin', 'next');
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
|
|
128
|
+
// Build if not already built
|
|
129
|
+
const buildDir = path.join(PKG_ROOT, '.next');
|
|
130
|
+
if (!fs.existsSync(buildDir)) {
|
|
131
|
+
console.log('\n IM - First run: building... (this may take a minute)\n');
|
|
132
|
+
const buildResult = spawn(process.execPath, [nextCli, 'build'], {
|
|
133
|
+
cwd: PKG_ROOT,
|
|
134
|
+
stdio: 'inherit',
|
|
135
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
136
|
+
});
|
|
137
|
+
await new Promise<void>((resolve, reject) => {
|
|
138
|
+
buildResult.on('exit', (code) => {
|
|
139
|
+
if (code !== 0) reject(new Error(`Build failed with code ${code}`));
|
|
140
|
+
else resolve();
|
|
141
|
+
});
|
|
142
|
+
buildResult.on('error', reject);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`\n IM - Idea Manager`);
|
|
147
|
+
console.log(` Starting on http://localhost:${port}\n`);
|
|
148
|
+
|
|
149
|
+
// Run production server (next start)
|
|
150
|
+
const child = spawn(process.execPath, [nextCli, 'start', '-p', port], {
|
|
130
151
|
cwd: PKG_ROOT,
|
|
131
152
|
stdio: 'inherit',
|
|
132
|
-
env: { ...process.env, NODE_ENV: '
|
|
153
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
133
154
|
});
|
|
134
155
|
|
|
135
156
|
child.on('error', (err) => {
|
package/src/lib/db/index.ts
CHANGED
|
@@ -1,16 +1,190 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs';
|
|
2
2
|
import { getDbPath } from '../utils/paths';
|
|
3
3
|
import { initSchema } from './schema';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Compatibility wrapper: mimics better-sqlite3 API on top of sql.js
|
|
6
|
+
class DatabaseWrapper {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
private db: any;
|
|
9
|
+
private dbPath: string;
|
|
10
|
+
private dirty = false;
|
|
11
|
+
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
db.pragma('journal_mode = WAL');
|
|
12
|
-
db.pragma('foreign_keys = ON');
|
|
13
|
-
initSchema(db);
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
constructor(db: any, dbPath: string) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
this.dbPath = dbPath;
|
|
14
17
|
}
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
private save() {
|
|
20
|
+
if (!this.dirty) return;
|
|
21
|
+
const data = this.db.export();
|
|
22
|
+
fs.writeFileSync(this.dbPath, Buffer.from(data));
|
|
23
|
+
this.dirty = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private scheduleSave() {
|
|
27
|
+
this.dirty = true;
|
|
28
|
+
if (this.saveTimer) return;
|
|
29
|
+
this.saveTimer = setTimeout(() => {
|
|
30
|
+
this.save();
|
|
31
|
+
this.saveTimer = null;
|
|
32
|
+
}, 100);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private immediatelySave() {
|
|
36
|
+
this.dirty = true;
|
|
37
|
+
if (this.saveTimer) {
|
|
38
|
+
clearTimeout(this.saveTimer);
|
|
39
|
+
this.saveTimer = null;
|
|
40
|
+
}
|
|
41
|
+
this.save();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private rowsToObjects(columns: string[], values: unknown[][]): Record<string, unknown>[] {
|
|
45
|
+
return values.map(row => {
|
|
46
|
+
const obj: Record<string, unknown> = {};
|
|
47
|
+
columns.forEach((col: string, i: number) => { obj[col] = row[i]; });
|
|
48
|
+
return obj;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
prepare(sql: string) {
|
|
53
|
+
const self = this;
|
|
54
|
+
const isWrite = /^\s*(INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)/i.test(sql);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
all(...params: unknown[]): any[] {
|
|
59
|
+
const stmt = self.db.prepare(sql);
|
|
60
|
+
if (params.length > 0) stmt.bind(params);
|
|
61
|
+
const columns: string[] = stmt.getColumnNames();
|
|
62
|
+
const rows: unknown[][] = [];
|
|
63
|
+
while (stmt.step()) {
|
|
64
|
+
rows.push(stmt.get());
|
|
65
|
+
}
|
|
66
|
+
stmt.free();
|
|
67
|
+
return self.rowsToObjects(columns, rows);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
get(...params: unknown[]): any {
|
|
72
|
+
const stmt = self.db.prepare(sql);
|
|
73
|
+
if (params.length > 0) stmt.bind(params);
|
|
74
|
+
let result: Record<string, unknown> | undefined;
|
|
75
|
+
if (stmt.step()) {
|
|
76
|
+
const columns = stmt.getColumnNames();
|
|
77
|
+
const row = stmt.get();
|
|
78
|
+
const obj: Record<string, unknown> = {};
|
|
79
|
+
columns.forEach((col: string, i: number) => { obj[col] = row[i]; });
|
|
80
|
+
result = obj;
|
|
81
|
+
}
|
|
82
|
+
stmt.free();
|
|
83
|
+
return result;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
run(...params: unknown[]) {
|
|
87
|
+
self.db.run(sql, params);
|
|
88
|
+
if (isWrite) self.scheduleSave();
|
|
89
|
+
const changes = self.db.getRowsModified();
|
|
90
|
+
return { changes };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
exec(sql: string) {
|
|
96
|
+
this.db.exec(sql);
|
|
97
|
+
if (/^\s*(INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)/im.test(sql)) {
|
|
98
|
+
this.scheduleSave();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pragma(str: string) {
|
|
103
|
+
if (str.startsWith('table_info(')) {
|
|
104
|
+
const table = str.match(/table_info\((\w+)\)/)?.[1];
|
|
105
|
+
if (!table) return [];
|
|
106
|
+
const result = this.db.exec(`PRAGMA table_info(${table})`);
|
|
107
|
+
if (!result.length) return [];
|
|
108
|
+
return this.rowsToObjects(result[0].columns, result[0].values);
|
|
109
|
+
}
|
|
110
|
+
if (str.includes('journal_mode') || str.includes('wal_checkpoint')) {
|
|
111
|
+
this.immediatelySave();
|
|
112
|
+
return 'memory';
|
|
113
|
+
}
|
|
114
|
+
if (str.includes('foreign_keys')) {
|
|
115
|
+
try { this.db.run(`PRAGMA ${str}`); } catch { /* ignore */ }
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const result = this.db.exec(`PRAGMA ${str}`);
|
|
120
|
+
if (result.length > 0 && result[0].values.length > 0) {
|
|
121
|
+
return result[0].values[0][0];
|
|
122
|
+
}
|
|
123
|
+
} catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
transaction<T>(fn: () => T): () => T {
|
|
127
|
+
const self = this;
|
|
128
|
+
return () => {
|
|
129
|
+
self.db.run('BEGIN');
|
|
130
|
+
try {
|
|
131
|
+
const result = fn();
|
|
132
|
+
self.db.run('COMMIT');
|
|
133
|
+
self.immediatelySave();
|
|
134
|
+
return result;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
self.db.run('ROLLBACK');
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
close() {
|
|
143
|
+
this.immediatelySave();
|
|
144
|
+
this.db.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let wrapper: DatabaseWrapper | null = null;
|
|
149
|
+
let initPromise: Promise<DatabaseWrapper> | null = null;
|
|
150
|
+
|
|
151
|
+
async function initAsync(): Promise<DatabaseWrapper> {
|
|
152
|
+
if (wrapper) return wrapper;
|
|
153
|
+
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
155
|
+
const initSqlJs = require('sql.js/dist/sql-asm.js');
|
|
156
|
+
const SQL = await initSqlJs();
|
|
157
|
+
|
|
158
|
+
const dbPath = getDbPath();
|
|
159
|
+
let db;
|
|
160
|
+
if (fs.existsSync(dbPath)) {
|
|
161
|
+
const fileBuffer = fs.readFileSync(dbPath);
|
|
162
|
+
db = new SQL.Database(fileBuffer);
|
|
163
|
+
} else {
|
|
164
|
+
db = new SQL.Database();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
wrapper = new DatabaseWrapper(db, dbPath);
|
|
168
|
+
initSchema(wrapper as unknown as Parameters<typeof initSchema>[0]);
|
|
169
|
+
|
|
170
|
+
process.on('exit', () => wrapper?.close());
|
|
171
|
+
|
|
172
|
+
return wrapper;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Call once before using getDb(). Safe to call multiple times. */
|
|
176
|
+
export async function ensureDb(): Promise<DatabaseWrapper> {
|
|
177
|
+
if (wrapper) return wrapper;
|
|
178
|
+
if (!initPromise) {
|
|
179
|
+
initPromise = initAsync();
|
|
180
|
+
}
|
|
181
|
+
return initPromise;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Sync getter — only works after ensureDb() has resolved. */
|
|
185
|
+
export function getDb(): DatabaseWrapper {
|
|
186
|
+
if (!wrapper) {
|
|
187
|
+
throw new Error('Database not initialized. Call await ensureDb() first.');
|
|
188
|
+
}
|
|
189
|
+
return wrapper;
|
|
16
190
|
}
|
package/src/lib/db/schema.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export function initSchema(db: Database.Database): void {
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
export function initSchema(db: any): void {
|
|
4
3
|
db.exec(`
|
|
5
4
|
CREATE TABLE IF NOT EXISTS projects (
|
|
6
5
|
id TEXT PRIMARY KEY,
|
package/src/lib/sync/exporter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import { getDb } from '../db';
|
|
2
|
+
import { ensureDb, getDb } from '../db';
|
|
3
3
|
|
|
4
4
|
// v2 active tables
|
|
5
5
|
const V2_TABLES = ['projects', 'brainstorms', 'sub_projects', 'tasks', 'task_prompts', 'task_conversations'];
|
|
@@ -9,7 +9,8 @@ function getExistingTables(db: ReturnType<typeof getDb>): string[] {
|
|
|
9
9
|
return rows.map(r => r.name);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function exportToFile(filePath: string): { tables: Record<string, number> } {
|
|
12
|
+
export async function exportToFile(filePath: string): Promise<{ tables: Record<string, number> }> {
|
|
13
|
+
await ensureDb();
|
|
13
14
|
const db = getDb();
|
|
14
15
|
const existingTables = getExistingTables(db);
|
|
15
16
|
|
package/src/lib/sync/importer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import { getDb } from '../db';
|
|
2
|
+
import { ensureDb, getDb } from '../db';
|
|
3
3
|
import { getDbPath } from '../utils/paths';
|
|
4
4
|
|
|
5
5
|
// Import order: parents first, then children
|
|
@@ -7,7 +7,8 @@ const IMPORT_ORDER = ['projects', 'brainstorms', 'sub_projects', 'tasks', 'task_
|
|
|
7
7
|
// Delete order: children first, then parents
|
|
8
8
|
const DELETE_ORDER = [...IMPORT_ORDER].reverse();
|
|
9
9
|
|
|
10
|
-
export function backupDb(): string {
|
|
10
|
+
export async function backupDb(): Promise<string> {
|
|
11
|
+
await ensureDb();
|
|
11
12
|
const dbPath = getDbPath();
|
|
12
13
|
const db = getDb();
|
|
13
14
|
// Flush WAL before backup
|
|
@@ -18,7 +19,8 @@ export function backupDb(): string {
|
|
|
18
19
|
return backupPath;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export function importFromFile(filePath: string): { tables: Record<string, number> } {
|
|
22
|
+
export async function importFromFile(filePath: string): Promise<{ tables: Record<string, number> }> {
|
|
23
|
+
await ensureDb();
|
|
22
24
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
23
25
|
const data = JSON.parse(raw);
|
|
24
26
|
|
package/src/lib/sync/index.ts
CHANGED
|
@@ -115,7 +115,7 @@ export async function syncInit() {
|
|
|
115
115
|
// Initial export + push if repo is empty
|
|
116
116
|
if (!fs.existsSync(path.join(syncDir, SYNC_FILE))) {
|
|
117
117
|
console.log(' Performing initial export...');
|
|
118
|
-
const { tables } = exportToFile(path.join(syncDir, SYNC_FILE));
|
|
118
|
+
const { tables } = await exportToFile(path.join(syncDir, SYNC_FILE));
|
|
119
119
|
printCounts('Exported', tables);
|
|
120
120
|
|
|
121
121
|
try {
|
|
@@ -142,7 +142,7 @@ export async function syncPush(message?: string) {
|
|
|
142
142
|
|
|
143
143
|
// Export
|
|
144
144
|
const filePath = path.join(syncDir, SYNC_FILE);
|
|
145
|
-
const { tables } = exportToFile(filePath);
|
|
145
|
+
const { tables } = await exportToFile(filePath);
|
|
146
146
|
printCounts('Exported', tables);
|
|
147
147
|
|
|
148
148
|
// Commit + push
|
|
@@ -194,12 +194,12 @@ export async function syncPull(opts: { backup?: boolean } = {}) {
|
|
|
194
194
|
|
|
195
195
|
// Backup
|
|
196
196
|
if (opts.backup !== false) {
|
|
197
|
-
const backupPath = backupDb();
|
|
197
|
+
const backupPath = await backupDb();
|
|
198
198
|
console.log(` Backup: ${backupPath}`);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
// Import
|
|
202
|
-
const { tables } = importFromFile(filePath);
|
|
202
|
+
const { tables } = await importFromFile(filePath);
|
|
203
203
|
printCounts('Imported', tables);
|
|
204
204
|
console.log(' Done. Refresh the browser to see updated data.\n');
|
|
205
205
|
}
|
package/src/lib/watcher.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import { ensureDb } from './db';
|
|
2
3
|
import { runAgent } from './ai/client';
|
|
3
4
|
import { listProjects, getProject } from './db/queries/projects';
|
|
4
5
|
import { getSubProject } from './db/queries/sub-projects';
|
|
@@ -163,6 +164,7 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
export async function startWatcher(options: WatcherOptions): Promise<void> {
|
|
167
|
+
await ensureDb();
|
|
166
168
|
if (options.projectId) {
|
|
167
169
|
const project = getProject(options.projectId);
|
|
168
170
|
if (!project) {
|