@compilr-dev/sdk 0.2.3 → 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 CHANGED
@@ -54,6 +54,8 @@ export type { CapabilityPack, CapabilityTier, LoadedCapability, CapabilityCatalo
54
54
  export { SystemPromptBuilder, buildSystemPrompt, detectGitRepository, getModuleStats, ALL_MODULES, IDENTITY_MODULE, STYLE_MODULE, TASK_EXECUTION_MODULE, TODO_MANAGEMENT_MODULE, TOOL_USAGE_DIRECT_MODULE, TOOL_USAGE_HINTS_MODULE, PLATFORM_TOOL_HINTS_MODULE, FACTORY_TOOL_HINTS_MODULE, TOOL_USAGE_META_MODULE, DELEGATION_MODULE, GIT_SAFETY_MODULE, SUGGEST_MODULE, IMPORTANT_RULES_MODULE, ENVIRONMENT_MODULE, shouldIncludeModule, getEstimatedTokensForConditions, getTotalEstimatedTokens, } from './system-prompt/index.js';
55
55
  export type { SystemPromptContext, BuildResult, SystemPromptModule, ModuleConditions, } from './system-prompt/index.js';
56
56
  export type { ProjectType, ProjectStatus, RepoPattern, WorkflowMode, LifecycleState, WorkItemType, WorkItemStatus, WorkItemPriority, GuidedStep, DocumentType, PlanStatus, Project, WorkItem, ProjectDocument, Plan, PlanSummary, PlanWithWorkItem, HistoryEntry, CreateProjectInput, UpdateProjectInput, ProjectListOptions, CreateWorkItemInput, UpdateWorkItemInput, QueryWorkItemsInput, CreateDocumentInput, UpdateDocumentInput, CreatePlanInput, UpdatePlanInput, ListPlansOptions, WorkItemQueryResult, ProjectListResult, BulkCreateItem, IProjectRepository, IWorkItemRepository, IDocumentRepository, IPlanRepository, IAnchorService, IArtifactService, IEpisodeService, AnchorData, ArtifactType, ArtifactData, ArtifactSummaryData, WorkEpisode, ProjectWorkSummary, PlatformContext, PlatformToolsConfig, PlatformHooks, StepCriteria, } from './platform/index.js';
57
+ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemRepository, SQLiteDocumentRepository, SQLitePlanRepository, getDatabase, closeDatabase, closeAllDatabases, databaseExists, SCHEMA_VERSION, SCHEMA_SQL, } from './platform/index.js';
58
+ export type { SQLiteRepositories, CreateSQLiteRepositoriesOptions, ProjectDeleteHooks, ProjectRecord, WorkItemRecord, ProjectDocumentRecord, } from './platform/index.js';
57
59
  export { createPlatformTools, createProjectTools, createWorkItemTools, createDocumentTools, createPlanTools, createBacklogTools, createAnchorTools, createArtifactTools, createEpisodeTools, } from './platform/index.js';
58
60
  export { STEP_ORDER, GUIDED_STEP_CRITERIA, getNextStep, isValidTransition, getStepCriteria, formatStepDisplay, getStepNumber, } from './platform/index.js';
59
61
  export { platformSkills, designSkill, sketchSkill, prdSkill, refineSkill, refineItemSkill, architectureSkill, sessionNotesSkill, buildSkill, scaffoldSkill, } from './skills/index.js';
package/dist/index.js CHANGED
@@ -113,6 +113,10 @@ ALL_MODULES, IDENTITY_MODULE, STYLE_MODULE, TASK_EXECUTION_MODULE, TODO_MANAGEME
113
113
  // Module utilities
114
114
  shouldIncludeModule, getEstimatedTokensForConditions, getTotalEstimatedTokens, } from './system-prompt/index.js';
115
115
  // =============================================================================
116
+ // Platform SQLite Repositories (concrete implementations)
117
+ // =============================================================================
118
+ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemRepository, SQLiteDocumentRepository, SQLitePlanRepository, getDatabase, closeDatabase, closeAllDatabases, databaseExists, SCHEMA_VERSION, SCHEMA_SQL, } from './platform/index.js';
119
+ // =============================================================================
116
120
  // Platform Tools (runtime — createPlatformTools factory + individual factories)
117
121
  // =============================================================================
118
122
  export { createPlatformTools, createProjectTools, createWorkItemTools, createDocumentTools, createPlanTools, createBacklogTools, createAnchorTools, createArtifactTools, createEpisodeTools, } from './platform/index.js';
@@ -6,5 +6,7 @@ export type { IProjectRepository, IWorkItemRepository, IDocumentRepository, IPla
6
6
  export type { IAnchorService, IArtifactService, IEpisodeService, AnchorData, ArtifactType, ArtifactData, ArtifactSummaryData, WorkEpisode, ProjectWorkSummary, } from './services.js';
7
7
  export type { PlatformContext, PlatformToolsConfig, PlatformHooks } from './context.js';
8
8
  export { createPlatformTools, createProjectTools, createWorkItemTools, createDocumentTools, createPlanTools, createBacklogTools, createAnchorTools, createArtifactTools, createEpisodeTools, } from './tools/index.js';
9
+ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemRepository, SQLiteDocumentRepository, SQLitePlanRepository, getDatabase, closeDatabase, closeAllDatabases, databaseExists, SCHEMA_VERSION, SCHEMA_SQL, } from './sqlite/index.js';
10
+ export type { SQLiteRepositories, CreateSQLiteRepositoriesOptions, ProjectDeleteHooks, ProjectRecord, WorkItemRecord, ProjectDocumentRecord, } from './sqlite/index.js';
9
11
  export { STEP_ORDER, GUIDED_STEP_CRITERIA, getNextStep, isValidTransition, getStepCriteria, formatStepDisplay, getStepNumber, } from './workflow.js';
10
12
  export type { StepCriteria } from './workflow.js';
@@ -3,5 +3,7 @@
3
3
  */
4
4
  // Platform tools (runtime)
5
5
  export { createPlatformTools, createProjectTools, createWorkItemTools, createDocumentTools, createPlanTools, createBacklogTools, createAnchorTools, createArtifactTools, createEpisodeTools, } from './tools/index.js';
6
+ // SQLite implementations (concrete repositories)
7
+ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemRepository, SQLiteDocumentRepository, SQLitePlanRepository, getDatabase, closeDatabase, closeAllDatabases, databaseExists, SCHEMA_VERSION, SCHEMA_SQL, } from './sqlite/index.js';
6
8
  // Workflow (pure step-criteria logic)
7
9
  export { STEP_ORDER, GUIDED_STEP_CRITERIA, getNextStep, isValidTransition, getStepCriteria, formatStepDisplay, getStepNumber, } from './workflow.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Database Connection — Shared SQLite singleton for ~/.compilr-dev/projects.db
3
+ *
4
+ * Uses better-sqlite3 (optional peer dependency).
5
+ * Consumers must install better-sqlite3 to use the SQLite repositories.
6
+ */
7
+ import type Database from 'better-sqlite3';
8
+ /**
9
+ * Get or create a database connection for the given path.
10
+ * Uses singleton pattern — same path always returns the same instance.
11
+ */
12
+ export declare function getDatabase(dbPath: string): Database.Database;
13
+ /**
14
+ * Close a specific database connection.
15
+ */
16
+ export declare function closeDatabase(dbPath: string): void;
17
+ /**
18
+ * Close all database connections.
19
+ */
20
+ export declare function closeAllDatabases(): void;
21
+ /**
22
+ * Check if a database file exists at the given path.
23
+ */
24
+ export declare function databaseExists(dbPath: string): boolean;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Database Connection — Shared SQLite singleton for ~/.compilr-dev/projects.db
3
+ *
4
+ * Uses better-sqlite3 (optional peer dependency).
5
+ * Consumers must install better-sqlite3 to use the SQLite repositories.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { SCHEMA_SQL, SCHEMA_VERSION } from './schema.js';
10
+ // Database instance (singleton per path)
11
+ const instances = new Map();
12
+ /**
13
+ * Get or create a database connection for the given path.
14
+ * Uses singleton pattern — same path always returns the same instance.
15
+ */
16
+ export function getDatabase(dbPath) {
17
+ const existing = instances.get(dbPath);
18
+ if (existing)
19
+ return existing;
20
+ // Dynamic require of better-sqlite3 — it's an optional peer dep
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ const BetterSqlite3 = require('better-sqlite3');
23
+ // Ensure directory exists
24
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
25
+ const db = new BetterSqlite3(dbPath);
26
+ // Enable foreign keys
27
+ db.pragma('foreign_keys = ON');
28
+ // WAL mode for better concurrency
29
+ db.pragma('journal_mode = WAL');
30
+ // Prevent SQLITE_BUSY under concurrent access
31
+ db.pragma('busy_timeout = 5000');
32
+ // Initialize schema
33
+ initializeSchema(db);
34
+ instances.set(dbPath, db);
35
+ return db;
36
+ }
37
+ /**
38
+ * Close a specific database connection.
39
+ */
40
+ export function closeDatabase(dbPath) {
41
+ const db = instances.get(dbPath);
42
+ if (db) {
43
+ db.close();
44
+ instances.delete(dbPath);
45
+ }
46
+ }
47
+ /**
48
+ * Close all database connections.
49
+ */
50
+ export function closeAllDatabases() {
51
+ for (const [, db] of instances) {
52
+ db.close();
53
+ }
54
+ instances.clear();
55
+ }
56
+ /**
57
+ * Check if a database file exists at the given path.
58
+ */
59
+ export function databaseExists(dbPath) {
60
+ return fs.existsSync(dbPath);
61
+ }
62
+ // =============================================================================
63
+ // Schema initialization & migrations
64
+ // =============================================================================
65
+ function initializeSchema(db) {
66
+ const tableExists = db
67
+ .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'`)
68
+ .get();
69
+ if (!tableExists) {
70
+ db.exec(SCHEMA_SQL);
71
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
72
+ return;
73
+ }
74
+ const currentVersion = db.prepare('SELECT MAX(version) as version FROM schema_version').get();
75
+ if (!currentVersion || currentVersion.version < SCHEMA_VERSION) {
76
+ runMigrations(db, currentVersion?.version ?? 0, SCHEMA_VERSION);
77
+ }
78
+ }
79
+ function runMigrations(db, fromVersion, toVersion) {
80
+ if (fromVersion < 2 && toVersion >= 2) {
81
+ db.exec(`ALTER TABLE project_documents ADD COLUMN status TEXT;`);
82
+ db.exec(`ALTER TABLE project_documents ADD COLUMN work_item_id INTEGER REFERENCES work_items(id) ON DELETE SET NULL;`);
83
+ db.exec(`
84
+ CREATE INDEX IF NOT EXISTS idx_project_documents_status ON project_documents(status);
85
+ CREATE INDEX IF NOT EXISTS idx_project_documents_work_item ON project_documents(work_item_id);
86
+ `);
87
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(2);
88
+ }
89
+ if (fromVersion < 3 && toVersion >= 3) {
90
+ db.exec(`ALTER TABLE work_items ADD COLUMN owner TEXT;`);
91
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_work_items_owner ON work_items(owner);`);
92
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(3);
93
+ }
94
+ if (fromVersion < 4 && toVersion >= 4) {
95
+ db.exec(`
96
+ CREATE TABLE IF NOT EXISTS terminal_sessions (
97
+ id TEXT PRIMARY KEY, project_id INTEGER, pid INTEGER NOT NULL,
98
+ tty_path TEXT, label TEXT, started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
99
+ last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,
100
+ active_agent TEXT DEFAULT 'default', agents_json TEXT DEFAULT '[]',
101
+ status TEXT DEFAULT 'active',
102
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL
103
+ );
104
+ CREATE INDEX IF NOT EXISTS idx_terminal_sessions_project ON terminal_sessions(project_id);
105
+ CREATE INDEX IF NOT EXISTS idx_terminal_sessions_status ON terminal_sessions(status);
106
+ `);
107
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(4);
108
+ }
109
+ if (fromVersion < 5 && toVersion >= 5) {
110
+ db.exec(`
111
+ CREATE TABLE IF NOT EXISTS file_locks (
112
+ path TEXT NOT NULL, project_id INTEGER NOT NULL,
113
+ session_id TEXT NOT NULL, agent_id TEXT NOT NULL,
114
+ locked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
115
+ PRIMARY KEY (path, project_id),
116
+ FOREIGN KEY (session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE,
117
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
118
+ );
119
+ CREATE INDEX IF NOT EXISTS idx_file_locks_session ON file_locks(session_id);
120
+ `);
121
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(5);
122
+ }
123
+ if (fromVersion < 6 && toVersion >= 6) {
124
+ db.exec(`
125
+ CREATE TABLE IF NOT EXISTS session_notifications (
126
+ id TEXT PRIMARY KEY, project_id INTEGER NOT NULL,
127
+ from_session_id TEXT NOT NULL, to_session_id TEXT,
128
+ type TEXT NOT NULL, title TEXT NOT NULL,
129
+ message TEXT, payload_json TEXT,
130
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP, read_at DATETIME,
131
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
132
+ FOREIGN KEY (from_session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE
133
+ );
134
+ CREATE INDEX IF NOT EXISTS idx_session_notifications_project ON session_notifications(project_id);
135
+ CREATE INDEX IF NOT EXISTS idx_session_notifications_to_session ON session_notifications(to_session_id);
136
+ CREATE INDEX IF NOT EXISTS idx_session_notifications_unread ON session_notifications(read_at);
137
+ `);
138
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(6);
139
+ }
140
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SQLite Document Repository — Concrete implementation of IDocumentRepository.
3
+ */
4
+ import type Database from 'better-sqlite3';
5
+ import type { IDocumentRepository } from '../repositories.js';
6
+ import type { ProjectDocument, DocumentType, CreateDocumentInput, UpdateDocumentInput } from '../types.js';
7
+ export declare class SQLiteDocumentRepository implements IDocumentRepository {
8
+ private readonly db;
9
+ constructor(db: Database.Database);
10
+ upsert(input: CreateDocumentInput): Promise<ProjectDocument>;
11
+ create(input: CreateDocumentInput): Promise<ProjectDocument>;
12
+ getById(id: number): Promise<ProjectDocument | null>;
13
+ getByType(projectId: number, docType: DocumentType): Promise<ProjectDocument | null>;
14
+ listByProject(projectId: number): Promise<ProjectDocument[]>;
15
+ update(id: number, input: UpdateDocumentInput): Promise<ProjectDocument | null>;
16
+ delete(id: number): Promise<boolean>;
17
+ deleteByType(projectId: number, docType: DocumentType): Promise<boolean>;
18
+ getTypeCounts(projectId: number): Promise<Record<DocumentType, number>>;
19
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * SQLite Document Repository — Concrete implementation of IDocumentRepository.
3
+ */
4
+ function recordToDocument(record) {
5
+ return {
6
+ id: record.id,
7
+ projectId: record.project_id,
8
+ docType: record.doc_type,
9
+ title: record.title,
10
+ content: record.content,
11
+ createdAt: new Date(record.created_at),
12
+ updatedAt: new Date(record.updated_at),
13
+ status: record.status,
14
+ workItemId: record.work_item_id,
15
+ };
16
+ }
17
+ export class SQLiteDocumentRepository {
18
+ db;
19
+ constructor(db) {
20
+ this.db = db;
21
+ }
22
+ upsert(input) {
23
+ const now = new Date().toISOString();
24
+ const existing = this.db
25
+ .prepare('SELECT * FROM project_documents WHERE project_id = ? AND doc_type = ?')
26
+ .get(input.project_id, input.doc_type);
27
+ if (existing) {
28
+ this.db
29
+ .prepare('UPDATE project_documents SET title = ?, content = ?, updated_at = ? WHERE id = ?')
30
+ .run(input.title, input.content, now, existing.id);
31
+ const record = this.db
32
+ .prepare('SELECT * FROM project_documents WHERE id = ?')
33
+ .get(existing.id);
34
+ return Promise.resolve(recordToDocument(record));
35
+ }
36
+ const result = this.db
37
+ .prepare(`INSERT INTO project_documents (project_id, doc_type, title, content, created_at, updated_at)
38
+ VALUES (?, ?, ?, ?, ?, ?)`)
39
+ .run(input.project_id, input.doc_type, input.title, input.content, now, now);
40
+ const record = this.db
41
+ .prepare('SELECT * FROM project_documents WHERE id = ?')
42
+ .get(Number(result.lastInsertRowid));
43
+ return Promise.resolve(recordToDocument(record));
44
+ }
45
+ create(input) {
46
+ const now = new Date().toISOString();
47
+ const result = this.db
48
+ .prepare(`INSERT INTO project_documents (project_id, doc_type, title, content, created_at, updated_at)
49
+ VALUES (?, ?, ?, ?, ?, ?)`)
50
+ .run(input.project_id, input.doc_type, input.title, input.content, now, now);
51
+ const record = this.db
52
+ .prepare('SELECT * FROM project_documents WHERE id = ?')
53
+ .get(Number(result.lastInsertRowid));
54
+ return Promise.resolve(recordToDocument(record));
55
+ }
56
+ getById(id) {
57
+ const record = this.db.prepare('SELECT * FROM project_documents WHERE id = ?').get(id);
58
+ return Promise.resolve(record ? recordToDocument(record) : null);
59
+ }
60
+ getByType(projectId, docType) {
61
+ const record = this.db
62
+ .prepare('SELECT * FROM project_documents WHERE project_id = ? AND doc_type = ?')
63
+ .get(projectId, docType);
64
+ return Promise.resolve(record ? recordToDocument(record) : null);
65
+ }
66
+ listByProject(projectId) {
67
+ const records = this.db
68
+ .prepare(`SELECT * FROM project_documents WHERE project_id = ?
69
+ ORDER BY
70
+ CASE doc_type
71
+ WHEN 'prd' THEN 1 WHEN 'architecture' THEN 2
72
+ WHEN 'design' THEN 3 WHEN 'notes' THEN 4
73
+ WHEN 'plan' THEN 5 ELSE 6
74
+ END, created_at ASC`)
75
+ .all(projectId);
76
+ return Promise.resolve(records.map(recordToDocument));
77
+ }
78
+ update(id, input) {
79
+ const updates = [];
80
+ const params = [];
81
+ if (input.title !== undefined) {
82
+ updates.push('title = ?');
83
+ params.push(input.title);
84
+ }
85
+ if (input.content !== undefined) {
86
+ updates.push('content = ?');
87
+ params.push(input.content);
88
+ }
89
+ if (updates.length === 0)
90
+ return this.getById(id);
91
+ updates.push('updated_at = ?');
92
+ params.push(new Date().toISOString());
93
+ params.push(id);
94
+ this.db
95
+ .prepare(`UPDATE project_documents SET ${updates.join(', ')} WHERE id = ?`)
96
+ .run(...params);
97
+ return this.getById(id);
98
+ }
99
+ delete(id) {
100
+ const result = this.db.prepare('DELETE FROM project_documents WHERE id = ?').run(id);
101
+ return Promise.resolve(result.changes > 0);
102
+ }
103
+ deleteByType(projectId, docType) {
104
+ const result = this.db
105
+ .prepare('DELETE FROM project_documents WHERE project_id = ? AND doc_type = ?')
106
+ .run(projectId, docType);
107
+ return Promise.resolve(result.changes > 0);
108
+ }
109
+ getTypeCounts(projectId) {
110
+ const results = this.db
111
+ .prepare('SELECT doc_type, COUNT(*) as count FROM project_documents WHERE project_id = ? GROUP BY doc_type')
112
+ .all(projectId);
113
+ const counts = {
114
+ prd: 0,
115
+ architecture: 0,
116
+ design: 0,
117
+ notes: 0,
118
+ plan: 0,
119
+ 'app-model': 0,
120
+ };
121
+ for (const row of results) {
122
+ counts[row.doc_type] = row.count;
123
+ }
124
+ return Promise.resolve(counts);
125
+ }
126
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SQLite Platform Repositories — Concrete implementations of platform interfaces.
3
+ *
4
+ * Requires better-sqlite3 as a peer dependency.
5
+ * Both CLI and Desktop import from this module to avoid code duplication.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createSQLiteRepositories, getDatabase } from '@compilr-dev/sdk/sqlite';
10
+ * // or from the main entry:
11
+ * import { createSQLiteRepositories, getDatabase } from '@compilr-dev/sdk';
12
+ *
13
+ * const db = getDatabase('~/.compilr-dev/projects.db');
14
+ * const repos = createSQLiteRepositories(db);
15
+ * // repos.projects, repos.workItems, repos.documents, repos.plans
16
+ * ```
17
+ */
18
+ import type Database from 'better-sqlite3';
19
+ import { type ProjectDeleteHooks } from './project-repository.js';
20
+ import type { IProjectRepository, IWorkItemRepository, IDocumentRepository, IPlanRepository } from '../repositories.js';
21
+ export interface SQLiteRepositories {
22
+ projects: IProjectRepository;
23
+ workItems: IWorkItemRepository;
24
+ documents: IDocumentRepository;
25
+ plans: IPlanRepository;
26
+ }
27
+ export interface CreateSQLiteRepositoriesOptions {
28
+ projectDeleteHooks?: ProjectDeleteHooks;
29
+ }
30
+ /**
31
+ * Create all 4 platform repositories backed by a single SQLite database.
32
+ */
33
+ export declare function createSQLiteRepositories(db: Database.Database, options?: CreateSQLiteRepositoriesOptions): SQLiteRepositories;
34
+ export { SQLiteProjectRepository, type ProjectDeleteHooks } from './project-repository.js';
35
+ export { SQLiteWorkItemRepository } from './work-item-repository.js';
36
+ export { SQLiteDocumentRepository } from './document-repository.js';
37
+ export { SQLitePlanRepository } from './plan-repository.js';
38
+ export { getDatabase, closeDatabase, closeAllDatabases, databaseExists } from './db.js';
39
+ export { SCHEMA_VERSION, SCHEMA_SQL } from './schema.js';
40
+ export type { ProjectRecord, WorkItemRecord, ProjectDocumentRecord } from './schema.js';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * SQLite Platform Repositories — Concrete implementations of platform interfaces.
3
+ *
4
+ * Requires better-sqlite3 as a peer dependency.
5
+ * Both CLI and Desktop import from this module to avoid code duplication.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createSQLiteRepositories, getDatabase } from '@compilr-dev/sdk/sqlite';
10
+ * // or from the main entry:
11
+ * import { createSQLiteRepositories, getDatabase } from '@compilr-dev/sdk';
12
+ *
13
+ * const db = getDatabase('~/.compilr-dev/projects.db');
14
+ * const repos = createSQLiteRepositories(db);
15
+ * // repos.projects, repos.workItems, repos.documents, repos.plans
16
+ * ```
17
+ */
18
+ import { SQLiteProjectRepository } from './project-repository.js';
19
+ import { SQLiteWorkItemRepository } from './work-item-repository.js';
20
+ import { SQLiteDocumentRepository } from './document-repository.js';
21
+ import { SQLitePlanRepository } from './plan-repository.js';
22
+ /**
23
+ * Create all 4 platform repositories backed by a single SQLite database.
24
+ */
25
+ export function createSQLiteRepositories(db, options) {
26
+ return {
27
+ projects: new SQLiteProjectRepository(db, options?.projectDeleteHooks),
28
+ workItems: new SQLiteWorkItemRepository(db),
29
+ documents: new SQLiteDocumentRepository(db),
30
+ plans: new SQLitePlanRepository(db),
31
+ };
32
+ }
33
+ // Re-export classes for advanced usage
34
+ export { SQLiteProjectRepository } from './project-repository.js';
35
+ export { SQLiteWorkItemRepository } from './work-item-repository.js';
36
+ export { SQLiteDocumentRepository } from './document-repository.js';
37
+ export { SQLitePlanRepository } from './plan-repository.js';
38
+ // Re-export database utilities
39
+ export { getDatabase, closeDatabase, closeAllDatabases, databaseExists } from './db.js';
40
+ // Re-export schema (for consumers that need direct access)
41
+ export { SCHEMA_VERSION, SCHEMA_SQL } from './schema.js';
@@ -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
+ }