ownkey 0.2.1 → 0.5.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/README.md +108 -38
- package/dist/ai/factory.d.ts +23 -0
- package/dist/ai/factory.d.ts.map +1 -0
- package/dist/ai/factory.js +96 -0
- package/dist/ai/factory.js.map +1 -0
- package/dist/ai/gemini.d.ts +21 -0
- package/dist/ai/gemini.d.ts.map +1 -0
- package/dist/ai/gemini.js +87 -0
- package/dist/ai/gemini.js.map +1 -0
- package/dist/ai/provider.d.ts +39 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +99 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/commands/config.js +3 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/suggest.d.ts.map +1 -1
- package/dist/commands/suggest.js +102 -10
- package/dist/commands/suggest.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +5 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/keychain.d.ts.map +1 -1
- package/dist/config/keychain.js +12 -0
- package/dist/config/keychain.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +41 -15
- package/dist/db/client.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +11 -3
- package/.prettierrc +0 -9
- package/src/commands/apply.ts +0 -27
- package/src/commands/config.ts +0 -154
- package/src/commands/scan.ts +0 -114
- package/src/commands/suggest.ts +0 -37
- package/src/config/defaults.ts +0 -80
- package/src/config/keychain.ts +0 -125
- package/src/config/manager.ts +0 -166
- package/src/core/scanner.ts +0 -174
- package/src/db/client.ts +0 -151
- package/src/db/operations.ts +0 -217
- package/src/db/schema.ts +0 -118
- package/src/index.ts +0 -74
- package/src/types/index.ts +0 -198
- package/src/utils/encryption.ts +0 -54
- package/src/utils/logger.ts +0 -111
- package/tsconfig.json +0 -33
- /package/{src → dist}/db/migrations/001_init.sql +0 -0
package/src/core/scanner.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import ignore, { Ignore } from 'ignore';
|
|
2
|
-
import { readFile, stat, readdir } from 'fs/promises';
|
|
3
|
-
import { join, relative, extname } from 'path';
|
|
4
|
-
import { existsSync } from 'fs';
|
|
5
|
-
import { FileMetadata, FileContent, ScanResult } from '../types/index.js';
|
|
6
|
-
import { logger } from '../utils/logger.js';
|
|
7
|
-
import { DEFAULT_EXCLUDES, BINARY_EXTENSIONS, MAX_FILE_SIZE_BYTES } from '../config/defaults.js';
|
|
8
|
-
|
|
9
|
-
export class FileScanner {
|
|
10
|
-
private ig: Ignore;
|
|
11
|
-
private maxFileSize: number;
|
|
12
|
-
private rootPath: string;
|
|
13
|
-
private skippedFiles: string[] = [];
|
|
14
|
-
private warnings: string[] = [];
|
|
15
|
-
|
|
16
|
-
constructor(rootPath: string, maxFileSize: number = MAX_FILE_SIZE_BYTES) {
|
|
17
|
-
this.rootPath = rootPath;
|
|
18
|
-
this.maxFileSize = maxFileSize;
|
|
19
|
-
this.ig = ignore();
|
|
20
|
-
this.ig.add(DEFAULT_EXCLUDES);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async initialize(): Promise<void> {
|
|
24
|
-
// Load .gitignore
|
|
25
|
-
const gitignorePath = join(this.rootPath, '.gitignore');
|
|
26
|
-
if (existsSync(gitignorePath)) {
|
|
27
|
-
const content = await readFile(gitignorePath, 'utf-8');
|
|
28
|
-
this.ig.add(content);
|
|
29
|
-
logger.verbose('Loaded .gitignore patterns');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Load .ownkeyignore
|
|
33
|
-
const ownkeyignorePath = join(this.rootPath, '.ownkeyignore');
|
|
34
|
-
if (existsSync(ownkeyignorePath)) {
|
|
35
|
-
const content = await readFile(ownkeyignorePath, 'utf-8');
|
|
36
|
-
this.ig.add(content);
|
|
37
|
-
logger.verbose('Loaded .ownkeyignore patterns');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async scan(): Promise<ScanResult> {
|
|
42
|
-
await this.initialize();
|
|
43
|
-
|
|
44
|
-
const files: FileMetadata[] = [];
|
|
45
|
-
let totalSize = 0;
|
|
46
|
-
|
|
47
|
-
await this.scanDirectory(this.rootPath, files);
|
|
48
|
-
|
|
49
|
-
for (const file of files) {
|
|
50
|
-
totalSize += file.size;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
logger.verbose(`Scanned ${files.length} files (${this.formatBytes(totalSize)})`);
|
|
54
|
-
|
|
55
|
-
if (this.skippedFiles.length > 0) {
|
|
56
|
-
logger.verbose(`Skipped ${this.skippedFiles.length} files`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
files,
|
|
61
|
-
totalSize,
|
|
62
|
-
skippedFiles: this.skippedFiles,
|
|
63
|
-
warnings: this.warnings,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private async scanDirectory(dirPath: string, files: FileMetadata[]): Promise<void> {
|
|
68
|
-
let entries;
|
|
69
|
-
try {
|
|
70
|
-
entries = await readdir(dirPath, { withFileTypes: true });
|
|
71
|
-
} catch (error) {
|
|
72
|
-
this.warnings.push(`Cannot read directory: ${dirPath}`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
for (const entry of entries) {
|
|
77
|
-
const fullPath = join(dirPath, entry.name);
|
|
78
|
-
const relativePath = relative(this.rootPath, fullPath);
|
|
79
|
-
|
|
80
|
-
// Check if ignored
|
|
81
|
-
if (this.ig.ignores(relativePath)) {
|
|
82
|
-
logger.debug(`Ignored: ${relativePath}`);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (entry.isDirectory()) {
|
|
87
|
-
await this.scanDirectory(fullPath, files);
|
|
88
|
-
} else if (entry.isFile()) {
|
|
89
|
-
await this.processFile(fullPath, relativePath, files);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private async processFile(
|
|
95
|
-
fullPath: string,
|
|
96
|
-
relativePath: string,
|
|
97
|
-
files: FileMetadata[]
|
|
98
|
-
): Promise<void> {
|
|
99
|
-
try {
|
|
100
|
-
const stats = await stat(fullPath);
|
|
101
|
-
|
|
102
|
-
// Check file size
|
|
103
|
-
if (stats.size > this.maxFileSize) {
|
|
104
|
-
this.skippedFiles.push(`${relativePath} (too large: ${this.formatBytes(stats.size)})`);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Check if binary
|
|
109
|
-
const ext = extname(fullPath).toLowerCase();
|
|
110
|
-
if (BINARY_EXTENSIONS.includes(ext)) {
|
|
111
|
-
this.skippedFiles.push(`${relativePath} (binary file)`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Check if file is actually text (simple heuristic)
|
|
116
|
-
if (!(await this.isTextFile(fullPath))) {
|
|
117
|
-
this.skippedFiles.push(`${relativePath} (binary content)`);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
files.push({
|
|
122
|
-
path: fullPath,
|
|
123
|
-
relativePath,
|
|
124
|
-
size: stats.size,
|
|
125
|
-
lastModified: stats.mtime,
|
|
126
|
-
extension: ext,
|
|
127
|
-
});
|
|
128
|
-
} catch (error) {
|
|
129
|
-
this.warnings.push(`Cannot process file: ${relativePath}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private async isTextFile(filePath: string): Promise<boolean> {
|
|
134
|
-
try {
|
|
135
|
-
const buffer = await readFile(filePath);
|
|
136
|
-
const sample = buffer.subarray(0, Math.min(8000, buffer.length));
|
|
137
|
-
|
|
138
|
-
// Check for null bytes (common in binary files)
|
|
139
|
-
for (let i = 0; i < sample.length; i++) {
|
|
140
|
-
if (sample[i] === 0) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return true;
|
|
146
|
-
} catch {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async loadFileContents(files: FileMetadata[]): Promise<FileContent[]> {
|
|
152
|
-
const contents: FileContent[] = [];
|
|
153
|
-
|
|
154
|
-
for (const file of files) {
|
|
155
|
-
try {
|
|
156
|
-
const content = await readFile(file.path, 'utf-8');
|
|
157
|
-
contents.push({
|
|
158
|
-
...file,
|
|
159
|
-
content,
|
|
160
|
-
});
|
|
161
|
-
} catch (error) {
|
|
162
|
-
logger.warn(`Failed to read file: ${file.relativePath}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return contents;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private formatBytes(bytes: number): string {
|
|
170
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
171
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
172
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
173
|
-
}
|
|
174
|
-
}
|
package/src/db/client.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import postgres from 'postgres';
|
|
2
|
-
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
3
|
-
import { readFile } from 'fs/promises';
|
|
4
|
-
import { join, dirname } from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { DatabaseError } from '../types/index.js';
|
|
7
|
-
import { logger } from '../utils/logger.js';
|
|
8
|
-
import { configManager } from '../config/manager.js';
|
|
9
|
-
import * as schema from './schema.js';
|
|
10
|
-
|
|
11
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
-
const __dirname = dirname(__filename);
|
|
13
|
-
|
|
14
|
-
export class DatabaseClient {
|
|
15
|
-
private sql: postgres.Sql | null = null;
|
|
16
|
-
private db: ReturnType<typeof drizzle> | null = null;
|
|
17
|
-
private isLocalOnly: boolean = false;
|
|
18
|
-
|
|
19
|
-
async connect(localOnly: boolean = false): Promise<void> {
|
|
20
|
-
if (localOnly) {
|
|
21
|
-
this.isLocalOnly = true;
|
|
22
|
-
logger.info('Running in local-only mode (no database persistence)');
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const config = await configManager.load();
|
|
28
|
-
|
|
29
|
-
if (!config.supabaseUrl || !config.supabaseServiceRoleKey) {
|
|
30
|
-
logger.warn('Supabase not configured. Running in local-only mode.');
|
|
31
|
-
logger.info('Run "ownkey config" to set up database persistence.');
|
|
32
|
-
this.isLocalOnly = true;
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Create connection string
|
|
37
|
-
const connectionString = `${config.supabaseUrl}?sslmode=require`;
|
|
38
|
-
|
|
39
|
-
this.sql = postgres(connectionString, {
|
|
40
|
-
password: config.supabaseServiceRoleKey,
|
|
41
|
-
max: 10,
|
|
42
|
-
idle_timeout: 20,
|
|
43
|
-
connect_timeout: 10,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
this.db = drizzle(this.sql, { schema });
|
|
47
|
-
|
|
48
|
-
// Test connection
|
|
49
|
-
await this.sql`SELECT 1`;
|
|
50
|
-
logger.verbose('Database connection established');
|
|
51
|
-
|
|
52
|
-
// Run migrations
|
|
53
|
-
await this.runMigrations();
|
|
54
|
-
} catch (error) {
|
|
55
|
-
logger.warn('Failed to connect to database. Running in local-only mode.');
|
|
56
|
-
logger.debug('Database error:', error);
|
|
57
|
-
this.isLocalOnly = true;
|
|
58
|
-
this.sql = null;
|
|
59
|
-
this.db = null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async disconnect(): Promise<void> {
|
|
64
|
-
if (this.sql) {
|
|
65
|
-
await this.sql.end();
|
|
66
|
-
this.sql = null;
|
|
67
|
-
this.db = null;
|
|
68
|
-
logger.verbose('Database connection closed');
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
getDb(): ReturnType<typeof drizzle> {
|
|
73
|
-
if (this.isLocalOnly) {
|
|
74
|
-
throw new DatabaseError('Database not available in local-only mode');
|
|
75
|
-
}
|
|
76
|
-
if (!this.db) {
|
|
77
|
-
throw new DatabaseError('Database not connected');
|
|
78
|
-
}
|
|
79
|
-
return this.db;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
isConnected(): boolean {
|
|
83
|
-
return this.db !== null && !this.isLocalOnly;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
isLocalOnlyMode(): boolean {
|
|
87
|
-
return this.isLocalOnly;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private async runMigrations(): Promise<void> {
|
|
91
|
-
if (!this.sql) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
logger.verbose('Checking database migrations...');
|
|
97
|
-
|
|
98
|
-
// Check if migrations table exists
|
|
99
|
-
const tableExists = await this.sql`
|
|
100
|
-
SELECT EXISTS (
|
|
101
|
-
SELECT FROM information_schema.tables
|
|
102
|
-
WHERE table_name = 'schema_migrations'
|
|
103
|
-
);
|
|
104
|
-
`;
|
|
105
|
-
|
|
106
|
-
if (!tableExists[0]?.exists) {
|
|
107
|
-
logger.info('Initializing database schema...');
|
|
108
|
-
await this.runMigration(1);
|
|
109
|
-
logger.success('Database schema initialized');
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Check current version
|
|
114
|
-
const currentVersion = await this.sql`
|
|
115
|
-
SELECT MAX(version) as version FROM schema_migrations;
|
|
116
|
-
`;
|
|
117
|
-
|
|
118
|
-
const version = currentVersion[0]?.version || 0;
|
|
119
|
-
logger.verbose(`Current schema version: ${version}`);
|
|
120
|
-
|
|
121
|
-
// Run pending migrations
|
|
122
|
-
const latestVersion = 1; // Update this as we add more migrations
|
|
123
|
-
if (version < latestVersion) {
|
|
124
|
-
for (let v = version + 1; v <= latestVersion; v++) {
|
|
125
|
-
await this.runMigration(v);
|
|
126
|
-
logger.success(`Applied migration ${v}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
} catch (error) {
|
|
130
|
-
throw new DatabaseError('Failed to run migrations', error);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private async runMigration(version: number): Promise<void> {
|
|
135
|
-
if (!this.sql) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const migrationFile = join(__dirname, 'migrations', `${String(version).padStart(3, '0')}_init.sql`);
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
const migrationSql = await readFile(migrationFile, 'utf-8');
|
|
143
|
-
await this.sql.unsafe(migrationSql);
|
|
144
|
-
logger.verbose(`Migration ${version} applied successfully`);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
throw new DatabaseError(`Failed to apply migration ${version}`, error);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export const dbClient = new DatabaseClient();
|
package/src/db/operations.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { eq, desc } from 'drizzle-orm';
|
|
2
|
-
import { dbClient } from './client.js';
|
|
3
|
-
import * as schema from './schema.js';
|
|
4
|
-
import {
|
|
5
|
-
Project,
|
|
6
|
-
Run,
|
|
7
|
-
SuggestionRecord,
|
|
8
|
-
RunStatus,
|
|
9
|
-
SuggestionAction,
|
|
10
|
-
} from '../types/index.js';
|
|
11
|
-
import { logger } from '../utils/logger.js';
|
|
12
|
-
|
|
13
|
-
export class DatabaseOperations {
|
|
14
|
-
// Projects
|
|
15
|
-
async createProject(name: string, rootPath: string): Promise<Project | null> {
|
|
16
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
17
|
-
logger.verbose('Skipping project creation (local-only mode)');
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const db = dbClient.getDb();
|
|
22
|
-
const [project] = await db
|
|
23
|
-
.insert(schema.projects)
|
|
24
|
-
.values({ name, rootPath })
|
|
25
|
-
.returning();
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
id: project.id,
|
|
29
|
-
name: project.name,
|
|
30
|
-
rootPath: project.rootPath,
|
|
31
|
-
createdAt: project.createdAt,
|
|
32
|
-
updatedAt: project.updatedAt,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async getProjectByPath(rootPath: string): Promise<Project | null> {
|
|
37
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const db = dbClient.getDb();
|
|
42
|
-
const [project] = await db
|
|
43
|
-
.select()
|
|
44
|
-
.from(schema.projects)
|
|
45
|
-
.where(eq(schema.projects.rootPath, rootPath))
|
|
46
|
-
.limit(1);
|
|
47
|
-
|
|
48
|
-
if (!project) return null;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
id: project.id,
|
|
52
|
-
name: project.name,
|
|
53
|
-
rootPath: project.rootPath,
|
|
54
|
-
createdAt: project.createdAt,
|
|
55
|
-
updatedAt: project.updatedAt,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Runs
|
|
60
|
-
async createRun(projectId: string, command: string): Promise<Run | null> {
|
|
61
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
62
|
-
logger.verbose('Skipping run creation (local-only mode)');
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const db = dbClient.getDb();
|
|
67
|
-
const [run] = await db
|
|
68
|
-
.insert(schema.runs)
|
|
69
|
-
.values({ projectId, command, status: 'running' })
|
|
70
|
-
.returning();
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
id: run.id,
|
|
74
|
-
projectId: run.projectId,
|
|
75
|
-
command: run.command,
|
|
76
|
-
status: run.status as RunStatus,
|
|
77
|
-
summary: run.summary || undefined,
|
|
78
|
-
startedAt: run.startedAt,
|
|
79
|
-
finishedAt: run.finishedAt || undefined,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async updateRunStatus(
|
|
84
|
-
runId: string,
|
|
85
|
-
status: RunStatus,
|
|
86
|
-
summary?: string
|
|
87
|
-
): Promise<void> {
|
|
88
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const db = dbClient.getDb();
|
|
93
|
-
await db
|
|
94
|
-
.update(schema.runs)
|
|
95
|
-
.set({ status, summary, finishedAt: new Date() })
|
|
96
|
-
.where(eq(schema.runs.id, runId));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async getRecentRuns(limit: number = 10): Promise<Run[]> {
|
|
100
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const db = dbClient.getDb();
|
|
105
|
-
const runs = await db
|
|
106
|
-
.select()
|
|
107
|
-
.from(schema.runs)
|
|
108
|
-
.orderBy(desc(schema.runs.startedAt))
|
|
109
|
-
.limit(limit);
|
|
110
|
-
|
|
111
|
-
return runs.map((run) => ({
|
|
112
|
-
id: run.id,
|
|
113
|
-
projectId: run.projectId,
|
|
114
|
-
command: run.command,
|
|
115
|
-
status: run.status as RunStatus,
|
|
116
|
-
summary: run.summary || undefined,
|
|
117
|
-
startedAt: run.startedAt,
|
|
118
|
-
finishedAt: run.finishedAt || undefined,
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Suggestions
|
|
123
|
-
async createSuggestion(
|
|
124
|
-
projectId: string,
|
|
125
|
-
suggestion: Omit<SuggestionRecord, 'id' | 'projectId' | 'createdAt'>
|
|
126
|
-
): Promise<SuggestionRecord | null> {
|
|
127
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
128
|
-
logger.verbose('Skipping suggestion storage (local-only mode)');
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const db = dbClient.getDb();
|
|
133
|
-
const [created] = await db
|
|
134
|
-
.insert(schema.suggestions)
|
|
135
|
-
.values({
|
|
136
|
-
projectId,
|
|
137
|
-
type: suggestion.type,
|
|
138
|
-
severity: suggestion.severity,
|
|
139
|
-
content: suggestion.content,
|
|
140
|
-
diff: suggestion.diff,
|
|
141
|
-
filePath: suggestion.filePath,
|
|
142
|
-
rationale: suggestion.rationale,
|
|
143
|
-
tags: suggestion.tags,
|
|
144
|
-
})
|
|
145
|
-
.returning();
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
id: created.id,
|
|
149
|
-
projectId: created.projectId,
|
|
150
|
-
type: created.type as any,
|
|
151
|
-
severity: created.severity as any,
|
|
152
|
-
content: created.content,
|
|
153
|
-
diff: created.diff,
|
|
154
|
-
filePath: created.filePath,
|
|
155
|
-
rationale: created.rationale,
|
|
156
|
-
tags: created.tags as any,
|
|
157
|
-
createdAt: created.createdAt,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async getSuggestionById(id: string): Promise<SuggestionRecord | null> {
|
|
162
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const db = dbClient.getDb();
|
|
167
|
-
const [suggestion] = await db
|
|
168
|
-
.select()
|
|
169
|
-
.from(schema.suggestions)
|
|
170
|
-
.where(eq(schema.suggestions.id, id))
|
|
171
|
-
.limit(1);
|
|
172
|
-
|
|
173
|
-
if (!suggestion) return null;
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
id: suggestion.id,
|
|
177
|
-
projectId: suggestion.projectId,
|
|
178
|
-
type: suggestion.type as any,
|
|
179
|
-
severity: suggestion.severity as any,
|
|
180
|
-
content: suggestion.content,
|
|
181
|
-
diff: suggestion.diff,
|
|
182
|
-
filePath: suggestion.filePath,
|
|
183
|
-
rationale: suggestion.rationale,
|
|
184
|
-
tags: suggestion.tags as any,
|
|
185
|
-
createdAt: suggestion.createdAt,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async linkRunSuggestion(runId: string, suggestionId: string): Promise<void> {
|
|
190
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const db = dbClient.getDb();
|
|
195
|
-
await db.insert(schema.runSuggestions).values({ runId, suggestionId });
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Suggestion History
|
|
199
|
-
async recordSuggestionAction(
|
|
200
|
-
suggestionId: string,
|
|
201
|
-
action: SuggestionAction,
|
|
202
|
-
details?: string
|
|
203
|
-
): Promise<void> {
|
|
204
|
-
if (dbClient.isLocalOnlyMode()) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const db = dbClient.getDb();
|
|
209
|
-
await db.insert(schema.suggestionHistory).values({
|
|
210
|
-
suggestionId,
|
|
211
|
-
action,
|
|
212
|
-
details,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export const dbOps = new DatabaseOperations();
|
package/src/db/schema.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { pgTable, uuid, text, timestamp, jsonb, integer } from 'drizzle-orm/pg-core';
|
|
2
|
-
import { relations } from 'drizzle-orm';
|
|
3
|
-
|
|
4
|
-
// Projects table
|
|
5
|
-
export const projects = pgTable('projects', {
|
|
6
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
7
|
-
name: text('name').notNull(),
|
|
8
|
-
rootPath: text('root_path').notNull(),
|
|
9
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
10
|
-
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
// Runs table
|
|
14
|
-
export const runs = pgTable('runs', {
|
|
15
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
16
|
-
projectId: uuid('project_id')
|
|
17
|
-
.references(() => projects.id)
|
|
18
|
-
.notNull(),
|
|
19
|
-
command: text('command').notNull(),
|
|
20
|
-
status: text('status').notNull(), // 'running' | 'completed' | 'failed'
|
|
21
|
-
summary: text('summary'),
|
|
22
|
-
startedAt: timestamp('started_at').defaultNow().notNull(),
|
|
23
|
-
finishedAt: timestamp('finished_at'),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Suggestions table
|
|
27
|
-
export const suggestions = pgTable('suggestions', {
|
|
28
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
29
|
-
projectId: uuid('project_id')
|
|
30
|
-
.references(() => projects.id)
|
|
31
|
-
.notNull(),
|
|
32
|
-
type: text('type').notNull(), // 'bug' | 'security' | 'performance' | etc.
|
|
33
|
-
severity: text('severity').notNull(), // 'critical' | 'high' | 'medium' | 'low' | 'info'
|
|
34
|
-
content: text('content').notNull(),
|
|
35
|
-
diff: text('diff').notNull(),
|
|
36
|
-
filePath: text('file_path').notNull(),
|
|
37
|
-
rationale: text('rationale').notNull(),
|
|
38
|
-
tags: jsonb('tags'),
|
|
39
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Suggestion history table
|
|
43
|
-
export const suggestionHistory = pgTable('suggestion_history', {
|
|
44
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
45
|
-
suggestionId: uuid('suggestion_id')
|
|
46
|
-
.references(() => suggestions.id)
|
|
47
|
-
.notNull(),
|
|
48
|
-
action: text('action').notNull(), // 'applied' | 'rejected' | 'undone'
|
|
49
|
-
details: text('details'),
|
|
50
|
-
actedAt: timestamp('acted_at').defaultNow().notNull(),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Run suggestions junction table
|
|
54
|
-
export const runSuggestions = pgTable('run_suggestions', {
|
|
55
|
-
runId: uuid('run_id')
|
|
56
|
-
.references(() => runs.id)
|
|
57
|
-
.notNull(),
|
|
58
|
-
suggestionId: uuid('suggestion_id')
|
|
59
|
-
.references(() => suggestions.id)
|
|
60
|
-
.notNull(),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// User API keys table (for fingerprints only)
|
|
64
|
-
export const userApiKeys = pgTable('user_api_keys', {
|
|
65
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
66
|
-
projectId: uuid('project_id').references(() => projects.id),
|
|
67
|
-
provider: text('provider').notNull(),
|
|
68
|
-
keyFingerprint: text('key_fingerprint').notNull(),
|
|
69
|
-
addedAt: timestamp('added_at').defaultNow().notNull(),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Schema migrations table
|
|
73
|
-
export const schemaMigrations = pgTable('schema_migrations', {
|
|
74
|
-
version: integer('version').primaryKey(),
|
|
75
|
-
appliedAt: timestamp('applied_at').defaultNow().notNull(),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Relations
|
|
79
|
-
export const projectsRelations = relations(projects, ({ many }) => ({
|
|
80
|
-
runs: many(runs),
|
|
81
|
-
suggestions: many(suggestions),
|
|
82
|
-
apiKeys: many(userApiKeys),
|
|
83
|
-
}));
|
|
84
|
-
|
|
85
|
-
export const runsRelations = relations(runs, ({ one, many }) => ({
|
|
86
|
-
project: one(projects, {
|
|
87
|
-
fields: [runs.projectId],
|
|
88
|
-
references: [projects.id],
|
|
89
|
-
}),
|
|
90
|
-
runSuggestions: many(runSuggestions),
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
export const suggestionsRelations = relations(suggestions, ({ one, many }) => ({
|
|
94
|
-
project: one(projects, {
|
|
95
|
-
fields: [suggestions.projectId],
|
|
96
|
-
references: [projects.id],
|
|
97
|
-
}),
|
|
98
|
-
history: many(suggestionHistory),
|
|
99
|
-
runSuggestions: many(runSuggestions),
|
|
100
|
-
}));
|
|
101
|
-
|
|
102
|
-
export const suggestionHistoryRelations = relations(suggestionHistory, ({ one }) => ({
|
|
103
|
-
suggestion: one(suggestions, {
|
|
104
|
-
fields: [suggestionHistory.suggestionId],
|
|
105
|
-
references: [suggestions.id],
|
|
106
|
-
}),
|
|
107
|
-
}));
|
|
108
|
-
|
|
109
|
-
export const runSuggestionsRelations = relations(runSuggestions, ({ one }) => ({
|
|
110
|
-
run: one(runs, {
|
|
111
|
-
fields: [runSuggestions.runId],
|
|
112
|
-
references: [runs.id],
|
|
113
|
-
}),
|
|
114
|
-
suggestion: one(suggestions, {
|
|
115
|
-
fields: [runSuggestions.suggestionId],
|
|
116
|
-
references: [suggestions.id],
|
|
117
|
-
}),
|
|
118
|
-
}));
|