@compilr-dev/sdk 0.2.2 → 0.2.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/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/mcp-config.d.ts +74 -0
- package/dist/mcp-config.js +124 -0
- package/dist/platform/index.d.ts +2 -0
- package/dist/platform/index.js +2 -0
- package/dist/platform/sqlite/db.d.ts +24 -0
- package/dist/platform/sqlite/db.js +140 -0
- package/dist/platform/sqlite/document-repository.d.ts +19 -0
- package/dist/platform/sqlite/document-repository.js +126 -0
- package/dist/platform/sqlite/index.d.ts +40 -0
- package/dist/platform/sqlite/index.js +41 -0
- package/dist/platform/sqlite/plan-repository.d.ts +24 -0
- package/dist/platform/sqlite/plan-repository.js +205 -0
- package/dist/platform/sqlite/project-repository.d.ts +34 -0
- package/dist/platform/sqlite/project-repository.js +282 -0
- package/dist/platform/sqlite/schema.d.ts +65 -0
- package/dist/platform/sqlite/schema.js +159 -0
- package/dist/platform/sqlite/work-item-repository.d.ts +23 -0
- package/dist/platform/sqlite/work-item-repository.js +350 -0
- package/package.json +11 -4
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Plan Repository — Concrete implementation of IPlanRepository.
|
|
3
|
+
*
|
|
4
|
+
* Plans are stored in project_documents with doc_type='plan'.
|
|
5
|
+
*/
|
|
6
|
+
import type Database from 'better-sqlite3';
|
|
7
|
+
import type { IPlanRepository } from '../repositories.js';
|
|
8
|
+
import type { Plan, PlanSummary, PlanWithWorkItem, PlanStatus, CreatePlanInput, UpdatePlanInput, ListPlansOptions } from '../types.js';
|
|
9
|
+
export declare class SQLitePlanRepository implements IPlanRepository {
|
|
10
|
+
private readonly db;
|
|
11
|
+
constructor(db: Database.Database);
|
|
12
|
+
create(input: CreatePlanInput): Promise<Plan>;
|
|
13
|
+
getById(id: number): Promise<Plan | null>;
|
|
14
|
+
getByName(projectId: number, name: string): Promise<Plan | null>;
|
|
15
|
+
getWithWorkItem(id: number): Promise<PlanWithWorkItem | null>;
|
|
16
|
+
update(id: number, input: UpdatePlanInput): Promise<Plan | null>;
|
|
17
|
+
delete(id: number): Promise<boolean>;
|
|
18
|
+
list(projectId: number, options?: ListPlansOptions): Promise<PlanSummary[]>;
|
|
19
|
+
countByStatus(projectId: number): Promise<Record<PlanStatus, number>>;
|
|
20
|
+
getInProgress(projectId: number): Promise<PlanSummary[]>;
|
|
21
|
+
hasInProgress(projectId: number): Promise<boolean>;
|
|
22
|
+
linkWorkItem(planId: number, workItemId: number): Promise<Plan | null>;
|
|
23
|
+
unlinkWorkItem(planId: number): Promise<Plan | null>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Plan Repository — Concrete implementation of IPlanRepository.
|
|
3
|
+
*
|
|
4
|
+
* Plans are stored in project_documents with doc_type='plan'.
|
|
5
|
+
*/
|
|
6
|
+
const VALID_TRANSITIONS = {
|
|
7
|
+
draft: ['approved', 'abandoned'],
|
|
8
|
+
approved: ['in_progress', 'abandoned'],
|
|
9
|
+
in_progress: ['completed', 'abandoned'],
|
|
10
|
+
completed: [],
|
|
11
|
+
abandoned: ['draft'],
|
|
12
|
+
};
|
|
13
|
+
function recordToPlan(record) {
|
|
14
|
+
return {
|
|
15
|
+
id: record.id,
|
|
16
|
+
projectId: record.project_id,
|
|
17
|
+
name: record.title,
|
|
18
|
+
content: record.content,
|
|
19
|
+
status: record.status ?? 'draft',
|
|
20
|
+
workItemId: record.work_item_id,
|
|
21
|
+
createdAt: new Date(record.created_at),
|
|
22
|
+
updatedAt: new Date(record.updated_at),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function recordToPlanSummary(record) {
|
|
26
|
+
return {
|
|
27
|
+
id: record.id,
|
|
28
|
+
name: record.title,
|
|
29
|
+
status: record.status ?? 'draft',
|
|
30
|
+
workItemId: record.work_item_id,
|
|
31
|
+
workItemTitle: record.work_item_title,
|
|
32
|
+
workItemItemId: record.work_item_item_id,
|
|
33
|
+
updatedAt: new Date(record.updated_at),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export class SQLitePlanRepository {
|
|
37
|
+
db;
|
|
38
|
+
constructor(db) {
|
|
39
|
+
this.db = db;
|
|
40
|
+
}
|
|
41
|
+
create(input) {
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
const result = this.db
|
|
44
|
+
.prepare(`INSERT INTO project_documents (
|
|
45
|
+
project_id, doc_type, title, content, status, work_item_id, created_at, updated_at
|
|
46
|
+
) VALUES (?, 'plan', ?, ?, 'draft', ?, ?, ?)`)
|
|
47
|
+
.run(input.project_id, input.name, input.content, input.work_item_id ?? null, now, now);
|
|
48
|
+
const record = this.db
|
|
49
|
+
.prepare(`SELECT * FROM project_documents WHERE id = ? AND doc_type = 'plan'`)
|
|
50
|
+
.get(Number(result.lastInsertRowid));
|
|
51
|
+
return Promise.resolve(recordToPlan(record));
|
|
52
|
+
}
|
|
53
|
+
getById(id) {
|
|
54
|
+
const record = this.db
|
|
55
|
+
.prepare(`SELECT * FROM project_documents WHERE id = ? AND doc_type = 'plan'`)
|
|
56
|
+
.get(id);
|
|
57
|
+
return Promise.resolve(record ? recordToPlan(record) : null);
|
|
58
|
+
}
|
|
59
|
+
getByName(projectId, name) {
|
|
60
|
+
const record = this.db
|
|
61
|
+
.prepare(`SELECT * FROM project_documents WHERE project_id = ? AND doc_type = 'plan' AND title = ?`)
|
|
62
|
+
.get(projectId, name);
|
|
63
|
+
return Promise.resolve(record ? recordToPlan(record) : null);
|
|
64
|
+
}
|
|
65
|
+
getWithWorkItem(id) {
|
|
66
|
+
const record = this.db
|
|
67
|
+
.prepare(`SELECT pd.*, wi.item_id as wi_item_id, wi.title as wi_title, wi.status as wi_status
|
|
68
|
+
FROM project_documents pd
|
|
69
|
+
LEFT JOIN work_items wi ON pd.work_item_id = wi.id
|
|
70
|
+
WHERE pd.id = ? AND pd.doc_type = 'plan'`)
|
|
71
|
+
.get(id);
|
|
72
|
+
if (!record)
|
|
73
|
+
return Promise.resolve(null);
|
|
74
|
+
const plan = recordToPlan(record);
|
|
75
|
+
if (record.wi_item_id && record.work_item_id !== null) {
|
|
76
|
+
plan.workItem = {
|
|
77
|
+
id: record.work_item_id,
|
|
78
|
+
itemId: record.wi_item_id,
|
|
79
|
+
title: record.wi_title ?? '',
|
|
80
|
+
status: record.wi_status ?? '',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return Promise.resolve(plan);
|
|
84
|
+
}
|
|
85
|
+
update(id, input) {
|
|
86
|
+
const currentRecord = this.db
|
|
87
|
+
.prepare(`SELECT * FROM project_documents WHERE id = ? AND doc_type = 'plan'`)
|
|
88
|
+
.get(id);
|
|
89
|
+
if (!currentRecord)
|
|
90
|
+
return Promise.resolve(null);
|
|
91
|
+
const currentStatus = currentRecord.status ?? 'draft';
|
|
92
|
+
if (input.status && input.status !== currentStatus) {
|
|
93
|
+
if (!VALID_TRANSITIONS[currentStatus].includes(input.status)) {
|
|
94
|
+
throw new Error(`Invalid status transition: ${currentStatus} → ${input.status}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const updates = [];
|
|
98
|
+
const params = [];
|
|
99
|
+
if (input.content !== undefined) {
|
|
100
|
+
updates.push('content = ?');
|
|
101
|
+
params.push(input.content);
|
|
102
|
+
}
|
|
103
|
+
if (input.status !== undefined) {
|
|
104
|
+
updates.push('status = ?');
|
|
105
|
+
params.push(input.status);
|
|
106
|
+
}
|
|
107
|
+
if (input.work_item_id !== undefined) {
|
|
108
|
+
updates.push('work_item_id = ?');
|
|
109
|
+
params.push(input.work_item_id);
|
|
110
|
+
}
|
|
111
|
+
if (updates.length === 0)
|
|
112
|
+
return Promise.resolve(recordToPlan(currentRecord));
|
|
113
|
+
updates.push('updated_at = ?');
|
|
114
|
+
params.push(new Date().toISOString());
|
|
115
|
+
params.push(id);
|
|
116
|
+
this.db
|
|
117
|
+
.prepare(`UPDATE project_documents SET ${updates.join(', ')} WHERE id = ? AND doc_type = 'plan'`)
|
|
118
|
+
.run(...params);
|
|
119
|
+
return this.getById(id);
|
|
120
|
+
}
|
|
121
|
+
delete(id) {
|
|
122
|
+
const result = this.db
|
|
123
|
+
.prepare(`DELETE FROM project_documents WHERE id = ? AND doc_type = 'plan'`)
|
|
124
|
+
.run(id);
|
|
125
|
+
return Promise.resolve(result.changes > 0);
|
|
126
|
+
}
|
|
127
|
+
list(projectId, options = {}) {
|
|
128
|
+
const conditions = ['pd.project_id = ?', "pd.doc_type = 'plan'"];
|
|
129
|
+
const params = [projectId];
|
|
130
|
+
if (options.status) {
|
|
131
|
+
if (Array.isArray(options.status)) {
|
|
132
|
+
const placeholders = options.status.map(() => '?').join(', ');
|
|
133
|
+
conditions.push(`pd.status IN (${placeholders})`);
|
|
134
|
+
params.push(...options.status);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
conditions.push('pd.status = ?');
|
|
138
|
+
params.push(options.status);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (options.work_item_id !== undefined) {
|
|
142
|
+
conditions.push('pd.work_item_id = ?');
|
|
143
|
+
params.push(options.work_item_id);
|
|
144
|
+
}
|
|
145
|
+
let orderBy = 'pd.updated_at DESC';
|
|
146
|
+
switch (options.order_by) {
|
|
147
|
+
case 'updated_asc':
|
|
148
|
+
orderBy = 'pd.updated_at ASC';
|
|
149
|
+
break;
|
|
150
|
+
case 'created_desc':
|
|
151
|
+
orderBy = 'pd.created_at DESC';
|
|
152
|
+
break;
|
|
153
|
+
case 'name_asc':
|
|
154
|
+
orderBy = 'pd.title ASC';
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
const limit = options.limit ?? 20;
|
|
158
|
+
const query = `
|
|
159
|
+
SELECT pd.*, wi.title as work_item_title, wi.item_id as work_item_item_id
|
|
160
|
+
FROM project_documents pd
|
|
161
|
+
LEFT JOIN work_items wi ON pd.work_item_id = wi.id
|
|
162
|
+
WHERE ${conditions.join(' AND ')}
|
|
163
|
+
ORDER BY ${orderBy}
|
|
164
|
+
LIMIT ?
|
|
165
|
+
`;
|
|
166
|
+
params.push(limit);
|
|
167
|
+
const records = this.db.prepare(query).all(...params);
|
|
168
|
+
return Promise.resolve(records.map(recordToPlanSummary));
|
|
169
|
+
}
|
|
170
|
+
countByStatus(projectId) {
|
|
171
|
+
const results = this.db
|
|
172
|
+
.prepare(`SELECT status, COUNT(*) as count FROM project_documents
|
|
173
|
+
WHERE project_id = ? AND doc_type = 'plan' GROUP BY status`)
|
|
174
|
+
.all(projectId);
|
|
175
|
+
const counts = {
|
|
176
|
+
draft: 0,
|
|
177
|
+
approved: 0,
|
|
178
|
+
in_progress: 0,
|
|
179
|
+
completed: 0,
|
|
180
|
+
abandoned: 0,
|
|
181
|
+
};
|
|
182
|
+
for (const row of results) {
|
|
183
|
+
if (row.status && row.status in counts) {
|
|
184
|
+
counts[row.status] = row.count;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return Promise.resolve(counts);
|
|
188
|
+
}
|
|
189
|
+
getInProgress(projectId) {
|
|
190
|
+
return this.list(projectId, { status: 'in_progress' });
|
|
191
|
+
}
|
|
192
|
+
hasInProgress(projectId) {
|
|
193
|
+
const result = this.db
|
|
194
|
+
.prepare(`SELECT COUNT(*) as count FROM project_documents
|
|
195
|
+
WHERE project_id = ? AND doc_type = 'plan' AND status = 'in_progress'`)
|
|
196
|
+
.get(projectId);
|
|
197
|
+
return Promise.resolve(result.count > 0);
|
|
198
|
+
}
|
|
199
|
+
linkWorkItem(planId, workItemId) {
|
|
200
|
+
return this.update(planId, { work_item_id: workItemId });
|
|
201
|
+
}
|
|
202
|
+
unlinkWorkItem(planId) {
|
|
203
|
+
return this.update(planId, { work_item_id: null });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Project Repository — Concrete implementation of IProjectRepository.
|
|
3
|
+
*
|
|
4
|
+
* All methods return Promises (wrapping synchronous better-sqlite3 calls)
|
|
5
|
+
* to satisfy the async IProjectRepository interface.
|
|
6
|
+
*/
|
|
7
|
+
import type Database from 'better-sqlite3';
|
|
8
|
+
import type { IProjectRepository } from '../repositories.js';
|
|
9
|
+
import type { Project, ProjectStatus, CreateProjectInput, UpdateProjectInput, ProjectListOptions, ProjectListResult } from '../types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Options for customizing project deletion behavior.
|
|
12
|
+
* Consumers can provide a callback to clean up platform-specific files.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProjectDeleteHooks {
|
|
15
|
+
onDelete?: (projectId: number) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare class SQLiteProjectRepository implements IProjectRepository {
|
|
18
|
+
private readonly db;
|
|
19
|
+
private readonly deleteHooks?;
|
|
20
|
+
constructor(db: Database.Database, hooks?: ProjectDeleteHooks);
|
|
21
|
+
create(input: CreateProjectInput): Promise<Project>;
|
|
22
|
+
getById(id: number): Promise<Project | null>;
|
|
23
|
+
getByName(name: string): Promise<Project | null>;
|
|
24
|
+
getByPath(path: string): Promise<Project | null>;
|
|
25
|
+
getByDocsPath(docsPath: string): Promise<Project | null>;
|
|
26
|
+
findByPath(searchPath: string): Promise<Project | null>;
|
|
27
|
+
list(options?: ProjectListOptions): Promise<ProjectListResult>;
|
|
28
|
+
update(id: number, input: UpdateProjectInput): Promise<Project | null>;
|
|
29
|
+
touch(id: number): Promise<void>;
|
|
30
|
+
delete(id: number): Promise<boolean>;
|
|
31
|
+
archive(id: number): Promise<Project | null>;
|
|
32
|
+
isNameAvailable(name: string, excludeId?: number): Promise<boolean>;
|
|
33
|
+
getStatusCounts(): Promise<Record<ProjectStatus, number>>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Project Repository — Concrete implementation of IProjectRepository.
|
|
3
|
+
*
|
|
4
|
+
* All methods return Promises (wrapping synchronous better-sqlite3 calls)
|
|
5
|
+
* to satisfy the async IProjectRepository interface.
|
|
6
|
+
*/
|
|
7
|
+
function recordToProject(record) {
|
|
8
|
+
return {
|
|
9
|
+
id: record.id,
|
|
10
|
+
name: record.name,
|
|
11
|
+
displayName: record.display_name,
|
|
12
|
+
description: record.description,
|
|
13
|
+
type: record.type,
|
|
14
|
+
status: record.status,
|
|
15
|
+
path: record.path,
|
|
16
|
+
docsPath: record.docs_path,
|
|
17
|
+
repoPattern: record.repo_pattern,
|
|
18
|
+
language: record.language,
|
|
19
|
+
framework: record.framework,
|
|
20
|
+
packageManager: record.package_manager,
|
|
21
|
+
runtimeVersion: record.runtime_version,
|
|
22
|
+
commands: record.commands ? JSON.parse(record.commands) : null,
|
|
23
|
+
gitRemote: record.git_remote,
|
|
24
|
+
gitBranch: record.git_branch,
|
|
25
|
+
workflowMode: record.workflow_mode,
|
|
26
|
+
lifecycleState: record.lifecycle_state,
|
|
27
|
+
currentItemId: record.current_item_id,
|
|
28
|
+
lastContext: record.last_context
|
|
29
|
+
? JSON.parse(record.last_context)
|
|
30
|
+
: null,
|
|
31
|
+
metadata: record.metadata ? JSON.parse(record.metadata) : null,
|
|
32
|
+
createdAt: new Date(record.created_at),
|
|
33
|
+
updatedAt: new Date(record.updated_at),
|
|
34
|
+
lastActivityAt: record.last_activity_at ? new Date(record.last_activity_at) : null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export class SQLiteProjectRepository {
|
|
38
|
+
db;
|
|
39
|
+
deleteHooks;
|
|
40
|
+
constructor(db, hooks) {
|
|
41
|
+
this.db = db;
|
|
42
|
+
this.deleteHooks = hooks;
|
|
43
|
+
}
|
|
44
|
+
create(input) {
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
const result = this.db
|
|
47
|
+
.prepare(`INSERT INTO projects (
|
|
48
|
+
name, display_name, description, type, path, docs_path, repo_pattern,
|
|
49
|
+
language, framework, package_manager, runtime_version, commands,
|
|
50
|
+
git_remote, git_branch, workflow_mode, metadata,
|
|
51
|
+
created_at, updated_at, last_activity_at
|
|
52
|
+
) VALUES (
|
|
53
|
+
@name, @display_name, @description, @type, @path, @docs_path, @repo_pattern,
|
|
54
|
+
@language, @framework, @package_manager, @runtime_version, @commands,
|
|
55
|
+
@git_remote, @git_branch, @workflow_mode, @metadata,
|
|
56
|
+
@created_at, @updated_at, @last_activity_at
|
|
57
|
+
)`)
|
|
58
|
+
.run({
|
|
59
|
+
name: input.name,
|
|
60
|
+
display_name: input.display_name,
|
|
61
|
+
description: input.description ?? null,
|
|
62
|
+
type: input.type ?? 'general',
|
|
63
|
+
path: input.path,
|
|
64
|
+
docs_path: input.docs_path ?? null,
|
|
65
|
+
repo_pattern: input.repo_pattern ?? 'single',
|
|
66
|
+
language: input.language ?? null,
|
|
67
|
+
framework: input.framework ?? null,
|
|
68
|
+
package_manager: input.package_manager ?? null,
|
|
69
|
+
runtime_version: input.runtime_version ?? null,
|
|
70
|
+
commands: input.commands ? JSON.stringify(input.commands) : null,
|
|
71
|
+
git_remote: input.git_remote ?? null,
|
|
72
|
+
git_branch: input.git_branch ?? 'main',
|
|
73
|
+
workflow_mode: input.workflow_mode ?? 'flexible',
|
|
74
|
+
metadata: input.metadata ? JSON.stringify(input.metadata) : null,
|
|
75
|
+
created_at: now,
|
|
76
|
+
updated_at: now,
|
|
77
|
+
last_activity_at: now,
|
|
78
|
+
});
|
|
79
|
+
const record = this.db
|
|
80
|
+
.prepare('SELECT * FROM projects WHERE id = ?')
|
|
81
|
+
.get(Number(result.lastInsertRowid));
|
|
82
|
+
if (!record)
|
|
83
|
+
throw new Error('Failed to create project');
|
|
84
|
+
return Promise.resolve(recordToProject(record));
|
|
85
|
+
}
|
|
86
|
+
getById(id) {
|
|
87
|
+
const record = this.db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
|
88
|
+
return Promise.resolve(record ? recordToProject(record) : null);
|
|
89
|
+
}
|
|
90
|
+
getByName(name) {
|
|
91
|
+
const record = this.db.prepare('SELECT * FROM projects WHERE name = ?').get(name);
|
|
92
|
+
return Promise.resolve(record ? recordToProject(record) : null);
|
|
93
|
+
}
|
|
94
|
+
getByPath(path) {
|
|
95
|
+
const record = this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path);
|
|
96
|
+
return Promise.resolve(record ? recordToProject(record) : null);
|
|
97
|
+
}
|
|
98
|
+
getByDocsPath(docsPath) {
|
|
99
|
+
const record = this.db.prepare('SELECT * FROM projects WHERE docs_path = ?').get(docsPath);
|
|
100
|
+
return Promise.resolve(record ? recordToProject(record) : null);
|
|
101
|
+
}
|
|
102
|
+
async findByPath(searchPath) {
|
|
103
|
+
let project = await this.getByPath(searchPath);
|
|
104
|
+
if (project)
|
|
105
|
+
return project;
|
|
106
|
+
project = await this.getByDocsPath(searchPath);
|
|
107
|
+
if (project)
|
|
108
|
+
return project;
|
|
109
|
+
const parts = searchPath.split('/').filter(Boolean);
|
|
110
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
111
|
+
const parentPath = '/' + parts.slice(0, i).join('/');
|
|
112
|
+
if (!parentPath || parentPath === '/')
|
|
113
|
+
break;
|
|
114
|
+
project = await this.getByPath(parentPath);
|
|
115
|
+
if (project)
|
|
116
|
+
return project;
|
|
117
|
+
project = await this.getByDocsPath(parentPath);
|
|
118
|
+
if (project)
|
|
119
|
+
return project;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
list(options) {
|
|
124
|
+
const conditions = [];
|
|
125
|
+
const params = {};
|
|
126
|
+
if (options?.status && options.status !== 'all') {
|
|
127
|
+
conditions.push('status = @status');
|
|
128
|
+
params.status = options.status;
|
|
129
|
+
}
|
|
130
|
+
if (options?.type && options.type !== 'all') {
|
|
131
|
+
conditions.push('type = @type');
|
|
132
|
+
params.type = options.type;
|
|
133
|
+
}
|
|
134
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
135
|
+
const countResult = this.db
|
|
136
|
+
.prepare(`SELECT COUNT(*) as count FROM projects ${whereClause}`)
|
|
137
|
+
.get(params);
|
|
138
|
+
let query = `SELECT * FROM projects ${whereClause} ORDER BY last_activity_at DESC NULLS LAST, updated_at DESC`;
|
|
139
|
+
if (options?.limit) {
|
|
140
|
+
query += ` LIMIT @limit`;
|
|
141
|
+
params.limit = options.limit;
|
|
142
|
+
}
|
|
143
|
+
if (options?.offset) {
|
|
144
|
+
query += ` OFFSET @offset`;
|
|
145
|
+
params.offset = options.offset;
|
|
146
|
+
}
|
|
147
|
+
const records = this.db.prepare(query).all(params);
|
|
148
|
+
return Promise.resolve({
|
|
149
|
+
projects: records.map(recordToProject),
|
|
150
|
+
total: countResult.count,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
update(id, input) {
|
|
154
|
+
const updates = [];
|
|
155
|
+
const params = { id };
|
|
156
|
+
if (input.display_name !== undefined) {
|
|
157
|
+
updates.push('display_name = @display_name');
|
|
158
|
+
params.display_name = input.display_name;
|
|
159
|
+
}
|
|
160
|
+
if (input.description !== undefined) {
|
|
161
|
+
updates.push('description = @description');
|
|
162
|
+
params.description = input.description;
|
|
163
|
+
}
|
|
164
|
+
if (input.type !== undefined) {
|
|
165
|
+
updates.push('type = @type');
|
|
166
|
+
params.type = input.type;
|
|
167
|
+
}
|
|
168
|
+
if (input.status !== undefined) {
|
|
169
|
+
updates.push('status = @status');
|
|
170
|
+
params.status = input.status;
|
|
171
|
+
}
|
|
172
|
+
if (input.docs_path !== undefined) {
|
|
173
|
+
updates.push('docs_path = @docs_path');
|
|
174
|
+
params.docs_path = input.docs_path;
|
|
175
|
+
}
|
|
176
|
+
if (input.repo_pattern !== undefined) {
|
|
177
|
+
updates.push('repo_pattern = @repo_pattern');
|
|
178
|
+
params.repo_pattern = input.repo_pattern;
|
|
179
|
+
}
|
|
180
|
+
if (input.language !== undefined) {
|
|
181
|
+
updates.push('language = @language');
|
|
182
|
+
params.language = input.language;
|
|
183
|
+
}
|
|
184
|
+
if (input.framework !== undefined) {
|
|
185
|
+
updates.push('framework = @framework');
|
|
186
|
+
params.framework = input.framework;
|
|
187
|
+
}
|
|
188
|
+
if (input.package_manager !== undefined) {
|
|
189
|
+
updates.push('package_manager = @package_manager');
|
|
190
|
+
params.package_manager = input.package_manager;
|
|
191
|
+
}
|
|
192
|
+
if (input.runtime_version !== undefined) {
|
|
193
|
+
updates.push('runtime_version = @runtime_version');
|
|
194
|
+
params.runtime_version = input.runtime_version;
|
|
195
|
+
}
|
|
196
|
+
if (input.commands !== undefined) {
|
|
197
|
+
updates.push('commands = @commands');
|
|
198
|
+
params.commands = JSON.stringify(input.commands);
|
|
199
|
+
}
|
|
200
|
+
if (input.git_remote !== undefined) {
|
|
201
|
+
updates.push('git_remote = @git_remote');
|
|
202
|
+
params.git_remote = input.git_remote;
|
|
203
|
+
}
|
|
204
|
+
if (input.git_branch !== undefined) {
|
|
205
|
+
updates.push('git_branch = @git_branch');
|
|
206
|
+
params.git_branch = input.git_branch;
|
|
207
|
+
}
|
|
208
|
+
if (input.workflow_mode !== undefined) {
|
|
209
|
+
updates.push('workflow_mode = @workflow_mode');
|
|
210
|
+
params.workflow_mode = input.workflow_mode;
|
|
211
|
+
}
|
|
212
|
+
if (input.lifecycle_state !== undefined) {
|
|
213
|
+
updates.push('lifecycle_state = @lifecycle_state');
|
|
214
|
+
params.lifecycle_state = input.lifecycle_state;
|
|
215
|
+
}
|
|
216
|
+
if (input.current_item_id !== undefined) {
|
|
217
|
+
updates.push('current_item_id = @current_item_id');
|
|
218
|
+
params.current_item_id = input.current_item_id;
|
|
219
|
+
}
|
|
220
|
+
if (input.last_context !== undefined) {
|
|
221
|
+
updates.push('last_context = @last_context');
|
|
222
|
+
params.last_context = JSON.stringify(input.last_context);
|
|
223
|
+
}
|
|
224
|
+
if (input.metadata !== undefined) {
|
|
225
|
+
updates.push('metadata = @metadata');
|
|
226
|
+
params.metadata = JSON.stringify(input.metadata);
|
|
227
|
+
}
|
|
228
|
+
if (updates.length === 0) {
|
|
229
|
+
return this.getById(id);
|
|
230
|
+
}
|
|
231
|
+
updates.push('updated_at = @updated_at');
|
|
232
|
+
params.updated_at = new Date().toISOString();
|
|
233
|
+
this.db.prepare(`UPDATE projects SET ${updates.join(', ')} WHERE id = @id`).run(params);
|
|
234
|
+
return this.getById(id);
|
|
235
|
+
}
|
|
236
|
+
touch(id) {
|
|
237
|
+
const now = new Date().toISOString();
|
|
238
|
+
this.db
|
|
239
|
+
.prepare('UPDATE projects SET last_activity_at = ?, updated_at = ? WHERE id = ?')
|
|
240
|
+
.run(now, now, id);
|
|
241
|
+
return Promise.resolve();
|
|
242
|
+
}
|
|
243
|
+
delete(id) {
|
|
244
|
+
const project = this.db.prepare('SELECT status FROM projects WHERE id = ?').get(id);
|
|
245
|
+
if (!project)
|
|
246
|
+
return Promise.resolve(false);
|
|
247
|
+
if (project.status !== 'archived') {
|
|
248
|
+
throw new Error('Project must be archived before deletion. Use archive() first.');
|
|
249
|
+
}
|
|
250
|
+
const result = this.db.prepare('DELETE FROM projects WHERE id = ?').run(id);
|
|
251
|
+
if (result.changes > 0) {
|
|
252
|
+
this.deleteHooks?.onDelete?.(id);
|
|
253
|
+
}
|
|
254
|
+
return Promise.resolve(result.changes > 0);
|
|
255
|
+
}
|
|
256
|
+
archive(id) {
|
|
257
|
+
return this.update(id, { status: 'archived' });
|
|
258
|
+
}
|
|
259
|
+
isNameAvailable(name, excludeId) {
|
|
260
|
+
const query = excludeId
|
|
261
|
+
? 'SELECT id FROM projects WHERE name = ? AND id != ?'
|
|
262
|
+
: 'SELECT id FROM projects WHERE name = ?';
|
|
263
|
+
const params = excludeId ? [name, excludeId] : [name];
|
|
264
|
+
const result = this.db.prepare(query).get(...params);
|
|
265
|
+
return Promise.resolve(!result);
|
|
266
|
+
}
|
|
267
|
+
getStatusCounts() {
|
|
268
|
+
const results = this.db
|
|
269
|
+
.prepare('SELECT status, COUNT(*) as count FROM projects GROUP BY status')
|
|
270
|
+
.all();
|
|
271
|
+
const counts = {
|
|
272
|
+
active: 0,
|
|
273
|
+
paused: 0,
|
|
274
|
+
completed: 0,
|
|
275
|
+
archived: 0,
|
|
276
|
+
};
|
|
277
|
+
for (const row of results) {
|
|
278
|
+
counts[row.status] = row.count;
|
|
279
|
+
}
|
|
280
|
+
return Promise.resolve(counts);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Schema for compilr-dev workflow system
|
|
3
|
+
*
|
|
4
|
+
* Shared between CLI and Desktop — both access ~/.compilr-dev/projects.db
|
|
5
|
+
* Schema version must be kept in sync across all consumers.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SCHEMA_VERSION = 6;
|
|
8
|
+
export declare const SCHEMA_SQL = "\n-- Schema version tracking\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Projects table\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n display_name TEXT NOT NULL,\n description TEXT,\n type TEXT DEFAULT 'general',\n status TEXT DEFAULT 'active',\n path TEXT NOT NULL,\n docs_path TEXT,\n repo_pattern TEXT DEFAULT 'single',\n language TEXT,\n framework TEXT,\n package_manager TEXT,\n runtime_version TEXT,\n commands TEXT,\n git_remote TEXT,\n git_branch TEXT DEFAULT 'main',\n workflow_mode TEXT DEFAULT 'flexible',\n lifecycle_state TEXT DEFAULT 'setup',\n current_item_id TEXT,\n last_context TEXT,\n metadata TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n last_activity_at DATETIME\n);\n\n-- Work items (backlog items, tasks, bugs)\nCREATE TABLE IF NOT EXISTS work_items (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n item_number INTEGER NOT NULL,\n item_id TEXT NOT NULL,\n type TEXT NOT NULL,\n status TEXT DEFAULT 'backlog',\n priority TEXT DEFAULT 'medium',\n guided_step TEXT,\n owner TEXT,\n title TEXT NOT NULL,\n description TEXT,\n estimated_effort TEXT,\n actual_minutes INTEGER,\n completed_at DATETIME,\n completed_by TEXT,\n commit_hash TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n UNIQUE (project_id, item_id)\n);\n\n-- Project documents (PRD, architecture, plans, etc.)\nCREATE TABLE IF NOT EXISTS project_documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n doc_type TEXT NOT NULL,\n title TEXT NOT NULL,\n content TEXT NOT NULL,\n status TEXT,\n work_item_id INTEGER,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE SET NULL\n);\n\n-- Work item history (audit trail)\nCREATE TABLE IF NOT EXISTS work_item_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n work_item_id INTEGER NOT NULL,\n project_id INTEGER NOT NULL,\n action TEXT NOT NULL,\n old_value TEXT,\n new_value TEXT,\n notes TEXT,\n changed_by TEXT,\n changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE CASCADE,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_path ON projects(path);\nCREATE INDEX IF NOT EXISTS idx_projects_docs_path ON projects(docs_path);\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_work_items_project ON work_items(project_id);\nCREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status);\nCREATE INDEX IF NOT EXISTS idx_work_items_priority ON work_items(priority);\nCREATE INDEX IF NOT EXISTS idx_work_items_owner ON work_items(owner);\nCREATE INDEX IF NOT EXISTS idx_project_documents_project ON project_documents(project_id);\nCREATE INDEX IF NOT EXISTS idx_project_documents_type ON project_documents(doc_type);\nCREATE INDEX IF NOT EXISTS idx_project_documents_status ON project_documents(status);\nCREATE INDEX IF NOT EXISTS idx_project_documents_work_item ON project_documents(work_item_id);\nCREATE INDEX IF NOT EXISTS idx_work_item_history_item ON work_item_history(work_item_id);\n\n-- Terminal sessions (multi-terminal awareness)\nCREATE TABLE IF NOT EXISTS terminal_sessions (\n id TEXT PRIMARY KEY,\n project_id INTEGER,\n pid INTEGER NOT NULL,\n tty_path TEXT,\n label TEXT,\n started_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,\n active_agent TEXT DEFAULT 'default',\n agents_json TEXT DEFAULT '[]',\n status TEXT DEFAULT 'active',\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL\n);\nCREATE INDEX IF NOT EXISTS idx_terminal_sessions_project ON terminal_sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_terminal_sessions_status ON terminal_sessions(status);\n\n-- File locks (multi-terminal file lock awareness)\nCREATE TABLE IF NOT EXISTS file_locks (\n path TEXT NOT NULL,\n project_id INTEGER NOT NULL,\n session_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n locked_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (path, project_id),\n FOREIGN KEY (session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_file_locks_session ON file_locks(session_id);\n\n-- Session notifications (cross-session notifications)\nCREATE TABLE IF NOT EXISTS session_notifications (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n from_session_id TEXT NOT NULL,\n to_session_id TEXT,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n message TEXT,\n payload_json TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n read_at DATETIME,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n FOREIGN KEY (from_session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_project ON session_notifications(project_id);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_to_session ON session_notifications(to_session_id);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_unread ON session_notifications(read_at);\n";
|
|
9
|
+
export interface ProjectRecord {
|
|
10
|
+
id: number;
|
|
11
|
+
name: string;
|
|
12
|
+
display_name: string;
|
|
13
|
+
description: string | null;
|
|
14
|
+
type: string;
|
|
15
|
+
status: string;
|
|
16
|
+
path: string;
|
|
17
|
+
docs_path: string | null;
|
|
18
|
+
repo_pattern: string;
|
|
19
|
+
language: string | null;
|
|
20
|
+
framework: string | null;
|
|
21
|
+
package_manager: string | null;
|
|
22
|
+
runtime_version: string | null;
|
|
23
|
+
commands: string | null;
|
|
24
|
+
git_remote: string | null;
|
|
25
|
+
git_branch: string;
|
|
26
|
+
workflow_mode: string;
|
|
27
|
+
lifecycle_state: string;
|
|
28
|
+
current_item_id: string | null;
|
|
29
|
+
last_context: string | null;
|
|
30
|
+
metadata: string | null;
|
|
31
|
+
created_at: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
last_activity_at: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface WorkItemRecord {
|
|
36
|
+
id: number;
|
|
37
|
+
project_id: number;
|
|
38
|
+
item_number: number;
|
|
39
|
+
item_id: string;
|
|
40
|
+
type: string;
|
|
41
|
+
status: string;
|
|
42
|
+
priority: string;
|
|
43
|
+
guided_step: string | null;
|
|
44
|
+
owner: string | null;
|
|
45
|
+
title: string;
|
|
46
|
+
description: string | null;
|
|
47
|
+
estimated_effort: string | null;
|
|
48
|
+
actual_minutes: number | null;
|
|
49
|
+
completed_at: string | null;
|
|
50
|
+
completed_by: string | null;
|
|
51
|
+
commit_hash: string | null;
|
|
52
|
+
created_at: string;
|
|
53
|
+
updated_at: string;
|
|
54
|
+
}
|
|
55
|
+
export interface ProjectDocumentRecord {
|
|
56
|
+
id: number;
|
|
57
|
+
project_id: number;
|
|
58
|
+
doc_type: string;
|
|
59
|
+
title: string;
|
|
60
|
+
content: string;
|
|
61
|
+
status: string | null;
|
|
62
|
+
work_item_id: number | null;
|
|
63
|
+
created_at: string;
|
|
64
|
+
updated_at: string;
|
|
65
|
+
}
|