@vladimir-ks/aigile 0.1.1 → 0.2.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 +11 -0
- package/dist/bin/aigile.js +2356 -341
- package/dist/bin/aigile.js.map +1 -1
- package/dist/index.cjs +3 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/bin/aigile.js
CHANGED
|
@@ -20,6 +20,284 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
20
|
};
|
|
21
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
22
|
|
|
23
|
+
// src/config/monitoring-patterns.ts
|
|
24
|
+
function isBinaryExtension(extension) {
|
|
25
|
+
const ext = extension.startsWith(".") ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
|
|
26
|
+
return BINARY_EXTENSIONS.includes(ext);
|
|
27
|
+
}
|
|
28
|
+
function getHardIgnorePatterns() {
|
|
29
|
+
return [...HARD_IGNORE];
|
|
30
|
+
}
|
|
31
|
+
function getDefaultAllowPatterns() {
|
|
32
|
+
return [...DEFAULT_ALLOW];
|
|
33
|
+
}
|
|
34
|
+
function getDefaultDenyPatterns() {
|
|
35
|
+
return [...DEFAULT_DENY];
|
|
36
|
+
}
|
|
37
|
+
var HARD_IGNORE, DEFAULT_ALLOW, DEFAULT_DENY, BINARY_EXTENSIONS;
|
|
38
|
+
var init_monitoring_patterns = __esm({
|
|
39
|
+
"src/config/monitoring-patterns.ts"() {
|
|
40
|
+
"use strict";
|
|
41
|
+
HARD_IGNORE = [
|
|
42
|
+
// Git directory - internal git data
|
|
43
|
+
"**/.git/**",
|
|
44
|
+
// Database files - CRITICAL: prevents infinite sync loops
|
|
45
|
+
"**/*.db",
|
|
46
|
+
"**/*.db-journal",
|
|
47
|
+
"**/*.db-wal",
|
|
48
|
+
"**/*.db-shm",
|
|
49
|
+
"**/*.sqlite",
|
|
50
|
+
"**/*.sqlite3",
|
|
51
|
+
// Lock and PID files
|
|
52
|
+
"**/*.pid",
|
|
53
|
+
"**/*.lock",
|
|
54
|
+
// OS metadata files
|
|
55
|
+
"**/.DS_Store",
|
|
56
|
+
"**/Thumbs.db",
|
|
57
|
+
"**/.Spotlight-V100/**",
|
|
58
|
+
"**/.Trashes/**",
|
|
59
|
+
"**/ehthumbs.db",
|
|
60
|
+
"**/Desktop.ini"
|
|
61
|
+
];
|
|
62
|
+
DEFAULT_ALLOW = [
|
|
63
|
+
// Documentation
|
|
64
|
+
"**/*.md",
|
|
65
|
+
"**/*.markdown",
|
|
66
|
+
"**/*.txt",
|
|
67
|
+
"**/*.rst",
|
|
68
|
+
"**/*.adoc",
|
|
69
|
+
// BDD Features
|
|
70
|
+
"**/*.feature",
|
|
71
|
+
"**/*.gherkin",
|
|
72
|
+
// Code - TypeScript/JavaScript
|
|
73
|
+
"**/*.ts",
|
|
74
|
+
"**/*.tsx",
|
|
75
|
+
"**/*.js",
|
|
76
|
+
"**/*.jsx",
|
|
77
|
+
"**/*.mjs",
|
|
78
|
+
"**/*.cjs",
|
|
79
|
+
// Code - Python
|
|
80
|
+
"**/*.py",
|
|
81
|
+
"**/*.pyi",
|
|
82
|
+
"**/*.pyx",
|
|
83
|
+
// Code - Go
|
|
84
|
+
"**/*.go",
|
|
85
|
+
"**/*.mod",
|
|
86
|
+
"**/*.sum",
|
|
87
|
+
// Code - Rust
|
|
88
|
+
"**/*.rs",
|
|
89
|
+
// Code - Java/JVM
|
|
90
|
+
"**/*.java",
|
|
91
|
+
"**/*.kt",
|
|
92
|
+
"**/*.kts",
|
|
93
|
+
"**/*.scala",
|
|
94
|
+
"**/*.groovy",
|
|
95
|
+
// Code - C/C++
|
|
96
|
+
"**/*.c",
|
|
97
|
+
"**/*.cpp",
|
|
98
|
+
"**/*.cc",
|
|
99
|
+
"**/*.cxx",
|
|
100
|
+
"**/*.h",
|
|
101
|
+
"**/*.hpp",
|
|
102
|
+
"**/*.hxx",
|
|
103
|
+
// Code - Ruby
|
|
104
|
+
"**/*.rb",
|
|
105
|
+
"**/*.rake",
|
|
106
|
+
"**/Rakefile",
|
|
107
|
+
"**/Gemfile",
|
|
108
|
+
// Code - PHP
|
|
109
|
+
"**/*.php",
|
|
110
|
+
// Code - Shell
|
|
111
|
+
"**/*.sh",
|
|
112
|
+
"**/*.bash",
|
|
113
|
+
"**/*.zsh",
|
|
114
|
+
"**/*.fish",
|
|
115
|
+
// Config - YAML
|
|
116
|
+
"**/*.yaml",
|
|
117
|
+
"**/*.yml",
|
|
118
|
+
// Config - JSON
|
|
119
|
+
"**/*.json",
|
|
120
|
+
"**/*.jsonc",
|
|
121
|
+
"**/*.json5",
|
|
122
|
+
// Config - TOML
|
|
123
|
+
"**/*.toml",
|
|
124
|
+
// Config - XML
|
|
125
|
+
"**/*.xml",
|
|
126
|
+
"**/*.xsd",
|
|
127
|
+
"**/*.xsl",
|
|
128
|
+
"**/*.xslt",
|
|
129
|
+
"**/*.plist",
|
|
130
|
+
// Config - INI/ENV
|
|
131
|
+
"**/*.ini",
|
|
132
|
+
"**/*.env",
|
|
133
|
+
"**/*.env.*",
|
|
134
|
+
"**/*.properties",
|
|
135
|
+
"**/*.cfg",
|
|
136
|
+
"**/*.conf",
|
|
137
|
+
// Data formats
|
|
138
|
+
"**/*.csv",
|
|
139
|
+
"**/*.tsv",
|
|
140
|
+
"**/*.sql",
|
|
141
|
+
"**/*.graphql",
|
|
142
|
+
"**/*.gql",
|
|
143
|
+
// Web templates
|
|
144
|
+
"**/*.html",
|
|
145
|
+
"**/*.htm",
|
|
146
|
+
"**/*.css",
|
|
147
|
+
"**/*.scss",
|
|
148
|
+
"**/*.sass",
|
|
149
|
+
"**/*.less",
|
|
150
|
+
"**/*.vue",
|
|
151
|
+
"**/*.svelte",
|
|
152
|
+
// Build files
|
|
153
|
+
"**/Dockerfile",
|
|
154
|
+
"**/Dockerfile.*",
|
|
155
|
+
"**/docker-compose.yml",
|
|
156
|
+
"**/docker-compose.yaml",
|
|
157
|
+
"**/docker-compose.*.yml",
|
|
158
|
+
"**/docker-compose.*.yaml",
|
|
159
|
+
"**/Makefile",
|
|
160
|
+
"**/CMakeLists.txt",
|
|
161
|
+
// Package manifests
|
|
162
|
+
"**/package.json",
|
|
163
|
+
"**/tsconfig.json",
|
|
164
|
+
"**/tsconfig.*.json",
|
|
165
|
+
"**/jsconfig.json",
|
|
166
|
+
"**/pyproject.toml",
|
|
167
|
+
"**/setup.py",
|
|
168
|
+
"**/setup.cfg",
|
|
169
|
+
"**/requirements.txt",
|
|
170
|
+
"**/requirements-*.txt",
|
|
171
|
+
"**/Cargo.toml",
|
|
172
|
+
"**/Cargo.lock",
|
|
173
|
+
"**/go.mod",
|
|
174
|
+
"**/go.sum",
|
|
175
|
+
"**/pom.xml",
|
|
176
|
+
"**/build.gradle",
|
|
177
|
+
"**/settings.gradle"
|
|
178
|
+
];
|
|
179
|
+
DEFAULT_DENY = [
|
|
180
|
+
// Package managers
|
|
181
|
+
"**/node_modules/**",
|
|
182
|
+
"**/bower_components/**",
|
|
183
|
+
"**/vendor/**",
|
|
184
|
+
"**/.pnpm-store/**",
|
|
185
|
+
"**/pnpm-lock.yaml",
|
|
186
|
+
"**/package-lock.json",
|
|
187
|
+
"**/yarn.lock",
|
|
188
|
+
// Build output
|
|
189
|
+
"**/dist/**",
|
|
190
|
+
"**/build/**",
|
|
191
|
+
"**/out/**",
|
|
192
|
+
"**/target/**",
|
|
193
|
+
"**/__pycache__/**",
|
|
194
|
+
"**/*.pyc",
|
|
195
|
+
"**/*.pyo",
|
|
196
|
+
"**/*.class",
|
|
197
|
+
"**/*.o",
|
|
198
|
+
"**/*.obj",
|
|
199
|
+
"**/*.so",
|
|
200
|
+
"**/*.dll",
|
|
201
|
+
"**/*.dylib",
|
|
202
|
+
"**/*.exe",
|
|
203
|
+
// Test coverage
|
|
204
|
+
"**/coverage/**",
|
|
205
|
+
"**/.nyc_output/**",
|
|
206
|
+
"**/.coverage/**",
|
|
207
|
+
"**/htmlcov/**",
|
|
208
|
+
// IDE/Editor
|
|
209
|
+
"**/.idea/**",
|
|
210
|
+
"**/.vscode/**",
|
|
211
|
+
"**/.vs/**",
|
|
212
|
+
"**/*.swp",
|
|
213
|
+
"**/*.swo",
|
|
214
|
+
"**/*~",
|
|
215
|
+
"**/.*.swp",
|
|
216
|
+
// Temp files
|
|
217
|
+
"**/*.tmp",
|
|
218
|
+
"**/*.temp",
|
|
219
|
+
"**/*.bak",
|
|
220
|
+
"**/*.backup",
|
|
221
|
+
"**/*.old",
|
|
222
|
+
// Log files
|
|
223
|
+
"**/*.log",
|
|
224
|
+
"**/logs/**",
|
|
225
|
+
"**/npm-debug.log*",
|
|
226
|
+
"**/yarn-debug.log*",
|
|
227
|
+
"**/yarn-error.log*",
|
|
228
|
+
// Cache
|
|
229
|
+
"**/.cache/**",
|
|
230
|
+
"**/.parcel-cache/**",
|
|
231
|
+
"**/.next/**",
|
|
232
|
+
"**/.nuxt/**",
|
|
233
|
+
"**/.turbo/**",
|
|
234
|
+
// Environment
|
|
235
|
+
"**/.env.local",
|
|
236
|
+
"**/.env.*.local",
|
|
237
|
+
// Misc
|
|
238
|
+
"**/.terraform/**",
|
|
239
|
+
"**/terraform.tfstate*"
|
|
240
|
+
];
|
|
241
|
+
BINARY_EXTENSIONS = [
|
|
242
|
+
// Images
|
|
243
|
+
".png",
|
|
244
|
+
".jpg",
|
|
245
|
+
".jpeg",
|
|
246
|
+
".gif",
|
|
247
|
+
".bmp",
|
|
248
|
+
".ico",
|
|
249
|
+
".svg",
|
|
250
|
+
".webp",
|
|
251
|
+
".tiff",
|
|
252
|
+
".tif",
|
|
253
|
+
".psd",
|
|
254
|
+
".ai",
|
|
255
|
+
// Documents
|
|
256
|
+
".pdf",
|
|
257
|
+
".doc",
|
|
258
|
+
".docx",
|
|
259
|
+
".xls",
|
|
260
|
+
".xlsx",
|
|
261
|
+
".ppt",
|
|
262
|
+
".pptx",
|
|
263
|
+
".odt",
|
|
264
|
+
".ods",
|
|
265
|
+
".odp",
|
|
266
|
+
// Archives
|
|
267
|
+
".zip",
|
|
268
|
+
".tar",
|
|
269
|
+
".gz",
|
|
270
|
+
".bz2",
|
|
271
|
+
".xz",
|
|
272
|
+
".7z",
|
|
273
|
+
".rar",
|
|
274
|
+
// Audio
|
|
275
|
+
".mp3",
|
|
276
|
+
".wav",
|
|
277
|
+
".ogg",
|
|
278
|
+
".flac",
|
|
279
|
+
".aac",
|
|
280
|
+
".m4a",
|
|
281
|
+
// Video
|
|
282
|
+
".mp4",
|
|
283
|
+
".webm",
|
|
284
|
+
".mkv",
|
|
285
|
+
".avi",
|
|
286
|
+
".mov",
|
|
287
|
+
".wmv",
|
|
288
|
+
// Fonts
|
|
289
|
+
".ttf",
|
|
290
|
+
".otf",
|
|
291
|
+
".woff",
|
|
292
|
+
".woff2",
|
|
293
|
+
".eot",
|
|
294
|
+
// Compiled
|
|
295
|
+
".wasm",
|
|
296
|
+
".node"
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
23
301
|
// src/utils/config.ts
|
|
24
302
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
25
303
|
import { homedir } from "os";
|
|
@@ -68,9 +346,61 @@ function findProjectRoot(startPath = process.cwd()) {
|
|
|
68
346
|
}
|
|
69
347
|
return null;
|
|
70
348
|
}
|
|
349
|
+
function parseIgnoreFile(filePath) {
|
|
350
|
+
try {
|
|
351
|
+
const content = readFileSync(filePath, "utf-8");
|
|
352
|
+
const patterns = [];
|
|
353
|
+
for (let line of content.split("\n")) {
|
|
354
|
+
line = line.trim();
|
|
355
|
+
if (!line || line.startsWith("#")) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (line.startsWith("!")) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
let pattern = line;
|
|
362
|
+
if (pattern.startsWith("/")) {
|
|
363
|
+
pattern = pattern.slice(1);
|
|
364
|
+
}
|
|
365
|
+
if (!pattern.startsWith("**/") && !pattern.includes("/")) {
|
|
366
|
+
pattern = `**/${pattern}`;
|
|
367
|
+
}
|
|
368
|
+
if (pattern.endsWith("/")) {
|
|
369
|
+
pattern = pattern.slice(0, -1) + "/**";
|
|
370
|
+
}
|
|
371
|
+
patterns.push(pattern);
|
|
372
|
+
}
|
|
373
|
+
return patterns;
|
|
374
|
+
} catch {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function loadIgnorePatterns(projectPath) {
|
|
379
|
+
const ignorePath = join(projectPath, ".aigile", "ignore");
|
|
380
|
+
if (!existsSync(ignorePath)) {
|
|
381
|
+
return getDefaultDenyPatterns();
|
|
382
|
+
}
|
|
383
|
+
const patterns = parseIgnoreFile(ignorePath);
|
|
384
|
+
return patterns.length > 0 ? patterns : getDefaultDenyPatterns();
|
|
385
|
+
}
|
|
386
|
+
function loadAllowPatterns(projectPath) {
|
|
387
|
+
const config = loadProjectConfig(projectPath);
|
|
388
|
+
if (!config) {
|
|
389
|
+
return getDefaultAllowPatterns();
|
|
390
|
+
}
|
|
391
|
+
const extendedConfig = config;
|
|
392
|
+
if (extendedConfig.sync?.allow_patterns && extendedConfig.sync.allow_patterns.length > 0) {
|
|
393
|
+
return extendedConfig.sync.allow_patterns;
|
|
394
|
+
}
|
|
395
|
+
if (config.sync?.patterns && config.sync.patterns.length > 0) {
|
|
396
|
+
return config.sync.patterns;
|
|
397
|
+
}
|
|
398
|
+
return getDefaultAllowPatterns();
|
|
399
|
+
}
|
|
71
400
|
var init_config = __esm({
|
|
72
401
|
"src/utils/config.ts"() {
|
|
73
402
|
"use strict";
|
|
403
|
+
init_monitoring_patterns();
|
|
74
404
|
}
|
|
75
405
|
});
|
|
76
406
|
|
|
@@ -91,6 +421,7 @@ __export(connection_exports, {
|
|
|
91
421
|
import initSqlJs from "sql.js";
|
|
92
422
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
93
423
|
import { dirname } from "path";
|
|
424
|
+
import { randomUUID } from "crypto";
|
|
94
425
|
async function initDatabase() {
|
|
95
426
|
if (db) {
|
|
96
427
|
return db;
|
|
@@ -121,9 +452,13 @@ function getDatabase() {
|
|
|
121
452
|
}
|
|
122
453
|
function saveDatabase() {
|
|
123
454
|
if (db && dbPath) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
455
|
+
try {
|
|
456
|
+
const data2 = db.export();
|
|
457
|
+
const buffer = Buffer.from(data2);
|
|
458
|
+
writeFileSync2(dbPath, buffer);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Database save error: ${err}`);
|
|
461
|
+
}
|
|
127
462
|
}
|
|
128
463
|
}
|
|
129
464
|
function closeDatabase() {
|
|
@@ -383,6 +718,16 @@ function initializeSchema(database) {
|
|
|
383
718
|
meta_authors TEXT,
|
|
384
719
|
has_frontmatter INTEGER DEFAULT 0,
|
|
385
720
|
frontmatter_raw TEXT,
|
|
721
|
+
-- Shadow mode analysis fields
|
|
722
|
+
shadow_mode INTEGER DEFAULT 0,
|
|
723
|
+
analyzed_at TEXT,
|
|
724
|
+
analysis_confidence INTEGER,
|
|
725
|
+
file_type TEXT,
|
|
726
|
+
complexity_score INTEGER,
|
|
727
|
+
exports TEXT,
|
|
728
|
+
inferred_module TEXT,
|
|
729
|
+
inferred_component TEXT,
|
|
730
|
+
analysis_notes TEXT,
|
|
386
731
|
created_at TEXT DEFAULT (datetime('now')),
|
|
387
732
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
388
733
|
UNIQUE(project_id, path)
|
|
@@ -446,7 +791,7 @@ function initializeSchema(database) {
|
|
|
446
791
|
`);
|
|
447
792
|
}
|
|
448
793
|
function generateId() {
|
|
449
|
-
return
|
|
794
|
+
return randomUUID();
|
|
450
795
|
}
|
|
451
796
|
function getNextKey(projectKey) {
|
|
452
797
|
const row = queryOne(
|
|
@@ -483,7 +828,21 @@ function runMigrations() {
|
|
|
483
828
|
{ name: "meta_code_refs", type: "TEXT" },
|
|
484
829
|
{ name: "meta_authors", type: "TEXT" },
|
|
485
830
|
{ name: "has_frontmatter", type: "INTEGER DEFAULT 0" },
|
|
486
|
-
{ name: "frontmatter_raw", type: "TEXT" }
|
|
831
|
+
{ name: "frontmatter_raw", type: "TEXT" },
|
|
832
|
+
// Shadow mode analysis columns
|
|
833
|
+
{ name: "shadow_mode", type: "INTEGER DEFAULT 0" },
|
|
834
|
+
{ name: "analyzed_at", type: "TEXT" },
|
|
835
|
+
{ name: "analysis_confidence", type: "INTEGER" },
|
|
836
|
+
{ name: "file_type", type: "TEXT" },
|
|
837
|
+
{ name: "complexity_score", type: "INTEGER" },
|
|
838
|
+
{ name: "exports", type: "TEXT" },
|
|
839
|
+
{ name: "inferred_module", type: "TEXT" },
|
|
840
|
+
{ name: "inferred_component", type: "TEXT" },
|
|
841
|
+
{ name: "analysis_notes", type: "TEXT" },
|
|
842
|
+
// Tri-state monitoring columns
|
|
843
|
+
{ name: "monitoring_category", type: 'TEXT DEFAULT "unknown"' },
|
|
844
|
+
{ name: "needs_review", type: "INTEGER DEFAULT 0" },
|
|
845
|
+
{ name: "reviewed_at", type: "TEXT" }
|
|
487
846
|
];
|
|
488
847
|
for (const col of newColumns) {
|
|
489
848
|
if (!columnNames.has(col.name)) {
|
|
@@ -493,6 +852,31 @@ function runMigrations() {
|
|
|
493
852
|
}
|
|
494
853
|
}
|
|
495
854
|
}
|
|
855
|
+
database.run(`
|
|
856
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
857
|
+
id TEXT PRIMARY KEY,
|
|
858
|
+
project_id TEXT REFERENCES projects(id),
|
|
859
|
+
started_at TEXT DEFAULT (datetime('now')),
|
|
860
|
+
ended_at TEXT,
|
|
861
|
+
summary TEXT,
|
|
862
|
+
entities_modified INTEGER DEFAULT 0,
|
|
863
|
+
files_modified INTEGER DEFAULT 0,
|
|
864
|
+
status TEXT DEFAULT 'active'
|
|
865
|
+
)
|
|
866
|
+
`);
|
|
867
|
+
database.run(`
|
|
868
|
+
CREATE TABLE IF NOT EXISTS activity_log (
|
|
869
|
+
id TEXT PRIMARY KEY,
|
|
870
|
+
session_id TEXT REFERENCES sessions(id),
|
|
871
|
+
project_id TEXT REFERENCES projects(id),
|
|
872
|
+
entity_type TEXT NOT NULL,
|
|
873
|
+
entity_key TEXT NOT NULL,
|
|
874
|
+
action TEXT NOT NULL,
|
|
875
|
+
old_value TEXT,
|
|
876
|
+
new_value TEXT,
|
|
877
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
878
|
+
)
|
|
879
|
+
`);
|
|
496
880
|
saveDatabase();
|
|
497
881
|
}
|
|
498
882
|
var db, dbPath;
|
|
@@ -506,10 +890,10 @@ var init_connection = __esm({
|
|
|
506
890
|
});
|
|
507
891
|
|
|
508
892
|
// src/bin/aigile.ts
|
|
509
|
-
import { Command as
|
|
893
|
+
import { Command as Command22 } from "commander";
|
|
510
894
|
|
|
511
895
|
// src/index.ts
|
|
512
|
-
var VERSION = "0.1
|
|
896
|
+
var VERSION = "0.2.1";
|
|
513
897
|
|
|
514
898
|
// src/bin/aigile.ts
|
|
515
899
|
init_connection();
|
|
@@ -1651,8 +2035,13 @@ function updateGitignore(repoPath, opts) {
|
|
|
1651
2035
|
// src/commands/project.ts
|
|
1652
2036
|
init_connection();
|
|
1653
2037
|
import { Command as Command2 } from "commander";
|
|
2038
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2039
|
+
import { join as join5 } from "path";
|
|
2040
|
+
function isValidProject(path) {
|
|
2041
|
+
return existsSync5(path) && existsSync5(join5(path, ".aigile"));
|
|
2042
|
+
}
|
|
1654
2043
|
var projectCommand = new Command2("project").description("Manage registered projects");
|
|
1655
|
-
projectCommand.command("list").alias("ls").description("List all registered projects").action(() => {
|
|
2044
|
+
projectCommand.command("list").alias("ls").description("List all registered projects with validity status").action(() => {
|
|
1656
2045
|
const opts = getOutputOptions(projectCommand);
|
|
1657
2046
|
const projects = queryAll(`
|
|
1658
2047
|
SELECT key, name, path, is_default,
|
|
@@ -1668,23 +2057,48 @@ projectCommand.command("list").alias("ls").description("List all registered proj
|
|
|
1668
2057
|
}
|
|
1669
2058
|
return;
|
|
1670
2059
|
}
|
|
1671
|
-
const formattedProjects = projects.map((p) =>
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
2060
|
+
const formattedProjects = projects.map((p) => {
|
|
2061
|
+
const valid = isValidProject(p.path);
|
|
2062
|
+
return {
|
|
2063
|
+
status: valid ? "\u2713" : "\u2717",
|
|
2064
|
+
key: p.is_default ? `${p.key} *` : p.key,
|
|
2065
|
+
name: p.name,
|
|
2066
|
+
path: p.path,
|
|
2067
|
+
valid
|
|
2068
|
+
// For JSON output
|
|
2069
|
+
};
|
|
2070
|
+
});
|
|
2071
|
+
const invalidCount = formattedProjects.filter((p) => !p.valid).length;
|
|
2072
|
+
if (opts.json) {
|
|
2073
|
+
console.log(JSON.stringify({
|
|
2074
|
+
success: true,
|
|
2075
|
+
data: formattedProjects.map((p) => ({
|
|
2076
|
+
key: p.key.replace(" *", ""),
|
|
2077
|
+
name: p.name,
|
|
2078
|
+
path: p.path,
|
|
2079
|
+
valid: p.valid,
|
|
2080
|
+
is_default: p.key.includes("*")
|
|
2081
|
+
})),
|
|
2082
|
+
invalidCount
|
|
2083
|
+
}));
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
1676
2086
|
data(
|
|
1677
2087
|
formattedProjects,
|
|
1678
2088
|
[
|
|
2089
|
+
{ header: "", key: "status", width: 3 },
|
|
1679
2090
|
{ header: "Key", key: "key", width: 12 },
|
|
1680
2091
|
{ header: "Name", key: "name", width: 30 },
|
|
1681
2092
|
{ header: "Path", key: "path", width: 50 }
|
|
1682
2093
|
],
|
|
1683
2094
|
opts
|
|
1684
2095
|
);
|
|
1685
|
-
|
|
2096
|
+
blank();
|
|
2097
|
+
console.log(" * = default project");
|
|
2098
|
+
console.log(" \u2713 = valid path, \u2717 = missing/invalid path");
|
|
2099
|
+
if (invalidCount > 0) {
|
|
1686
2100
|
blank();
|
|
1687
|
-
|
|
2101
|
+
warning(`${invalidCount} project(s) have invalid paths. Run "aigile project cleanup" to remove.`, opts);
|
|
1688
2102
|
}
|
|
1689
2103
|
});
|
|
1690
2104
|
projectCommand.command("show").argument("[key]", "Project key (uses default if not specified)").description("Show project details").action((key) => {
|
|
@@ -1722,16 +2136,105 @@ projectCommand.command("set-default").argument("<key>", "Project key to set as d
|
|
|
1722
2136
|
run("UPDATE projects SET is_default = 1 WHERE key = ?", [key]);
|
|
1723
2137
|
success(`Default project set to "${key}".`, opts);
|
|
1724
2138
|
});
|
|
1725
|
-
projectCommand.command("remove").alias("rm").argument("<key>", "Project key to remove").option("--force", "Remove without confirmation").description("Remove project from registry (does not delete files)").action((key, options) => {
|
|
2139
|
+
projectCommand.command("remove").alias("rm").argument("<key>", "Project key to remove").option("--cascade", "Also delete all entities (epics, stories, tasks, etc.)").option("--force", "Remove without confirmation").description("Remove project from registry (does not delete files)").action((key, options) => {
|
|
1726
2140
|
const opts = getOutputOptions(projectCommand);
|
|
1727
|
-
const project = queryOne(
|
|
2141
|
+
const project = queryOne(
|
|
2142
|
+
"SELECT id, name, path FROM projects WHERE key = ?",
|
|
2143
|
+
[key]
|
|
2144
|
+
);
|
|
1728
2145
|
if (!project) {
|
|
1729
2146
|
error(`Project "${key}" not found.`, opts);
|
|
1730
2147
|
process.exit(1);
|
|
1731
2148
|
}
|
|
2149
|
+
if (options.cascade) {
|
|
2150
|
+
const tables = [
|
|
2151
|
+
"documents",
|
|
2152
|
+
"doc_comments",
|
|
2153
|
+
"tasks",
|
|
2154
|
+
"bugs",
|
|
2155
|
+
"user_stories",
|
|
2156
|
+
"epics",
|
|
2157
|
+
"initiatives",
|
|
2158
|
+
"sprints",
|
|
2159
|
+
"components",
|
|
2160
|
+
"versions",
|
|
2161
|
+
"personas",
|
|
2162
|
+
"ux_journeys",
|
|
2163
|
+
"sessions",
|
|
2164
|
+
"activity_log",
|
|
2165
|
+
"key_sequences"
|
|
2166
|
+
];
|
|
2167
|
+
for (const table of tables) {
|
|
2168
|
+
try {
|
|
2169
|
+
run(`DELETE FROM ${table} WHERE project_id = ?`, [project.id]);
|
|
2170
|
+
} catch {
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
info(`Deleted all entities for project "${key}".`, opts);
|
|
2174
|
+
}
|
|
1732
2175
|
run("DELETE FROM projects WHERE key = ?", [key]);
|
|
1733
2176
|
success(`Project "${key}" removed from registry.`, opts);
|
|
1734
2177
|
});
|
|
2178
|
+
projectCommand.command("cleanup").description("Remove all projects with invalid/missing paths").option("--dry-run", "Show what would be removed without removing").option("--cascade", "Also delete all entities for removed projects").action((options) => {
|
|
2179
|
+
const opts = getOutputOptions(projectCommand);
|
|
2180
|
+
const projects = queryAll("SELECT id, key, name, path FROM projects");
|
|
2181
|
+
const invalidProjects = projects.filter((p) => !isValidProject(p.path));
|
|
2182
|
+
if (invalidProjects.length === 0) {
|
|
2183
|
+
success("All projects have valid paths. Nothing to clean up.", opts);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (opts.json) {
|
|
2187
|
+
console.log(JSON.stringify({
|
|
2188
|
+
success: true,
|
|
2189
|
+
dryRun: options.dryRun ?? false,
|
|
2190
|
+
invalidProjects: invalidProjects.map((p) => ({
|
|
2191
|
+
key: p.key,
|
|
2192
|
+
name: p.name,
|
|
2193
|
+
path: p.path
|
|
2194
|
+
}))
|
|
2195
|
+
}));
|
|
2196
|
+
if (options.dryRun) {
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (options.dryRun) {
|
|
2201
|
+
info(`Would remove ${invalidProjects.length} invalid project(s):`, opts);
|
|
2202
|
+
for (const p of invalidProjects) {
|
|
2203
|
+
console.log(` - ${p.key}: ${p.path}`);
|
|
2204
|
+
}
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
for (const project of invalidProjects) {
|
|
2208
|
+
if (options.cascade) {
|
|
2209
|
+
const tables = [
|
|
2210
|
+
"documents",
|
|
2211
|
+
"doc_comments",
|
|
2212
|
+
"tasks",
|
|
2213
|
+
"bugs",
|
|
2214
|
+
"user_stories",
|
|
2215
|
+
"epics",
|
|
2216
|
+
"initiatives",
|
|
2217
|
+
"sprints",
|
|
2218
|
+
"components",
|
|
2219
|
+
"versions",
|
|
2220
|
+
"personas",
|
|
2221
|
+
"ux_journeys",
|
|
2222
|
+
"sessions",
|
|
2223
|
+
"activity_log",
|
|
2224
|
+
"key_sequences"
|
|
2225
|
+
];
|
|
2226
|
+
for (const table of tables) {
|
|
2227
|
+
try {
|
|
2228
|
+
run(`DELETE FROM ${table} WHERE project_id = ?`, [project.id]);
|
|
2229
|
+
} catch {
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
run("DELETE FROM projects WHERE id = ?", [project.id]);
|
|
2234
|
+
info(`Removed: ${project.key} (${project.path})`, opts);
|
|
2235
|
+
}
|
|
2236
|
+
success(`Cleaned up ${invalidProjects.length} invalid project(s).`, opts);
|
|
2237
|
+
});
|
|
1735
2238
|
|
|
1736
2239
|
// src/commands/epic.ts
|
|
1737
2240
|
init_connection();
|
|
@@ -2551,8 +3054,90 @@ bugCommand.command("delete").alias("rm").argument("<key>", "Bug key").option("--
|
|
|
2551
3054
|
init_connection();
|
|
2552
3055
|
import { Command as Command7 } from "commander";
|
|
2553
3056
|
init_config();
|
|
3057
|
+
|
|
3058
|
+
// src/utils/date.ts
|
|
3059
|
+
var DATE_FORMAT = "YYYY-MM-DD";
|
|
3060
|
+
function isValidDateFormat(dateStr) {
|
|
3061
|
+
if (!dateStr) return false;
|
|
3062
|
+
const regex = /^\d{4}-\d{2}-\d{2}$/;
|
|
3063
|
+
if (!regex.test(dateStr)) {
|
|
3064
|
+
return false;
|
|
3065
|
+
}
|
|
3066
|
+
const [year, month, day] = dateStr.split("-").map(Number);
|
|
3067
|
+
if (month < 1 || month > 12) {
|
|
3068
|
+
return false;
|
|
3069
|
+
}
|
|
3070
|
+
const daysInMonth = new Date(year, month, 0).getDate();
|
|
3071
|
+
if (day < 1 || day > daysInMonth) {
|
|
3072
|
+
return false;
|
|
3073
|
+
}
|
|
3074
|
+
return true;
|
|
3075
|
+
}
|
|
3076
|
+
function parseDate(dateStr) {
|
|
3077
|
+
if (!dateStr || typeof dateStr !== "string") {
|
|
3078
|
+
return null;
|
|
3079
|
+
}
|
|
3080
|
+
const trimmed = dateStr.trim();
|
|
3081
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
3082
|
+
return isValidDateFormat(trimmed) ? trimmed : null;
|
|
3083
|
+
}
|
|
3084
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
|
|
3085
|
+
const datePart = trimmed.split("T")[0];
|
|
3086
|
+
return isValidDateFormat(datePart) ? datePart : null;
|
|
3087
|
+
}
|
|
3088
|
+
const slashYMD = trimmed.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
|
|
3089
|
+
if (slashYMD) {
|
|
3090
|
+
const result = `${slashYMD[1]}-${slashYMD[2]}-${slashYMD[3]}`;
|
|
3091
|
+
return isValidDateFormat(result) ? result : null;
|
|
3092
|
+
}
|
|
3093
|
+
const slashMDY = trimmed.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
3094
|
+
if (slashMDY) {
|
|
3095
|
+
let [, first, second, year] = slashMDY;
|
|
3096
|
+
const firstNum = Number(first);
|
|
3097
|
+
const secondNum = Number(second);
|
|
3098
|
+
let month, day;
|
|
3099
|
+
if (firstNum > 12) {
|
|
3100
|
+
day = first.padStart(2, "0");
|
|
3101
|
+
month = second.padStart(2, "0");
|
|
3102
|
+
} else {
|
|
3103
|
+
month = first.padStart(2, "0");
|
|
3104
|
+
day = second.padStart(2, "0");
|
|
3105
|
+
}
|
|
3106
|
+
const result = `${year}-${month}-${day}`;
|
|
3107
|
+
return isValidDateFormat(result) ? result : null;
|
|
3108
|
+
}
|
|
3109
|
+
const dotDMY = trimmed.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
3110
|
+
if (dotDMY) {
|
|
3111
|
+
const [, day, month, year] = dotDMY;
|
|
3112
|
+
const result = `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
|
3113
|
+
return isValidDateFormat(result) ? result : null;
|
|
3114
|
+
}
|
|
3115
|
+
const parsed = new Date(trimmed);
|
|
3116
|
+
if (!isNaN(parsed.getTime())) {
|
|
3117
|
+
const year = parsed.getFullYear();
|
|
3118
|
+
const month = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
3119
|
+
const day = String(parsed.getDate()).padStart(2, "0");
|
|
3120
|
+
const result = `${year}-${month}-${day}`;
|
|
3121
|
+
return isValidDateFormat(result) ? result : null;
|
|
3122
|
+
}
|
|
3123
|
+
return null;
|
|
3124
|
+
}
|
|
3125
|
+
function validateAndStandardizeDate(dateStr, fieldName) {
|
|
3126
|
+
const standardized = parseDate(dateStr);
|
|
3127
|
+
if (!standardized) {
|
|
3128
|
+
throw new Error(
|
|
3129
|
+
`Invalid date format for ${fieldName}: "${dateStr}". Expected format: ${DATE_FORMAT} (e.g., 2025-12-14)`
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
return standardized;
|
|
3133
|
+
}
|
|
3134
|
+
function isEndDateValid(startDate, endDate) {
|
|
3135
|
+
return endDate >= startDate;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
// src/commands/sprint.ts
|
|
2554
3139
|
var sprintCommand = new Command7("sprint").description("Manage sprints");
|
|
2555
|
-
sprintCommand.command("create").argument("<name>", "Sprint name").requiredOption("--start <date>",
|
|
3140
|
+
sprintCommand.command("create").argument("<name>", "Sprint name").requiredOption("--start <date>", `Start date (${DATE_FORMAT})`).requiredOption("--end <date>", `End date (${DATE_FORMAT})`).option("-g, --goal <goal>", "Sprint goal").description("Create a new sprint").action((name, options) => {
|
|
2556
3141
|
const opts = getOutputOptions(sprintCommand);
|
|
2557
3142
|
const projectRoot = findProjectRoot();
|
|
2558
3143
|
if (!projectRoot) {
|
|
@@ -2569,16 +3154,29 @@ sprintCommand.command("create").argument("<name>", "Sprint name").requiredOption
|
|
|
2569
3154
|
error(`Project "${config.project.key}" not found.`, opts);
|
|
2570
3155
|
process.exit(1);
|
|
2571
3156
|
}
|
|
3157
|
+
let startDate;
|
|
3158
|
+
let endDate;
|
|
3159
|
+
try {
|
|
3160
|
+
startDate = validateAndStandardizeDate(options.start, "start date");
|
|
3161
|
+
endDate = validateAndStandardizeDate(options.end, "end date");
|
|
3162
|
+
} catch (err) {
|
|
3163
|
+
error(err instanceof Error ? err.message : String(err), opts);
|
|
3164
|
+
process.exit(1);
|
|
3165
|
+
}
|
|
3166
|
+
if (!isEndDateValid(startDate, endDate)) {
|
|
3167
|
+
error(`End date (${endDate}) must be after start date (${startDate}).`, opts);
|
|
3168
|
+
process.exit(1);
|
|
3169
|
+
}
|
|
2572
3170
|
const sprintId = generateId();
|
|
2573
3171
|
run(
|
|
2574
3172
|
`INSERT INTO sprints (id, project_id, name, goal, start_date, end_date, status)
|
|
2575
3173
|
VALUES (?, ?, ?, ?, ?, ?, 'future')`,
|
|
2576
|
-
[sprintId, project.id, name, options.goal ?? null,
|
|
3174
|
+
[sprintId, project.id, name, options.goal ?? null, startDate, endDate]
|
|
2577
3175
|
);
|
|
2578
3176
|
if (opts.json) {
|
|
2579
|
-
console.log(JSON.stringify({ success: true, data: { name, start:
|
|
3177
|
+
console.log(JSON.stringify({ success: true, data: { name, start: startDate, end: endDate } }));
|
|
2580
3178
|
} else {
|
|
2581
|
-
success(`Created sprint "${name}" (${
|
|
3179
|
+
success(`Created sprint "${name}" (${startDate} - ${endDate})`, opts);
|
|
2582
3180
|
}
|
|
2583
3181
|
});
|
|
2584
3182
|
sprintCommand.command("list").alias("ls").option("-s, --status <status>", "Filter by status (future/active/closed)").description("List sprints").action((options) => {
|
|
@@ -2813,7 +3411,7 @@ init_connection();
|
|
|
2813
3411
|
import { Command as Command9 } from "commander";
|
|
2814
3412
|
init_config();
|
|
2815
3413
|
var initiativeCommand = new Command9("initiative").description("Manage initiatives (portfolio-level objectives)");
|
|
2816
|
-
initiativeCommand.command("create").argument("<summary>", "Initiative summary").option("-d, --description <description>", "Initiative description").option("-p, --priority <priority>", "Priority (Highest/High/Medium/Low/Lowest)", "Medium").option("--owner <owner>", "Owner").option("--start <date>",
|
|
3414
|
+
initiativeCommand.command("create").argument("<summary>", "Initiative summary").option("-d, --description <description>", "Initiative description").option("-p, --priority <priority>", "Priority (Highest/High/Medium/Low/Lowest)", "Medium").option("--owner <owner>", "Owner").option("--start <date>", `Start date (${DATE_FORMAT})`).option("--target <date>", `Target date (${DATE_FORMAT})`).description("Create a new initiative").action((summary, options) => {
|
|
2817
3415
|
const opts = getOutputOptions(initiativeCommand);
|
|
2818
3416
|
const projectRoot = findProjectRoot();
|
|
2819
3417
|
if (!projectRoot) {
|
|
@@ -2830,6 +3428,23 @@ initiativeCommand.command("create").argument("<summary>", "Initiative summary").
|
|
|
2830
3428
|
error(`Project "${config.project.key}" not found in database.`, opts);
|
|
2831
3429
|
process.exit(1);
|
|
2832
3430
|
}
|
|
3431
|
+
let startDate = null;
|
|
3432
|
+
let targetDate = null;
|
|
3433
|
+
try {
|
|
3434
|
+
if (options.start) {
|
|
3435
|
+
startDate = validateAndStandardizeDate(options.start, "start date");
|
|
3436
|
+
}
|
|
3437
|
+
if (options.target) {
|
|
3438
|
+
targetDate = validateAndStandardizeDate(options.target, "target date");
|
|
3439
|
+
}
|
|
3440
|
+
} catch (err) {
|
|
3441
|
+
error(err instanceof Error ? err.message : String(err), opts);
|
|
3442
|
+
process.exit(1);
|
|
3443
|
+
}
|
|
3444
|
+
if (startDate && targetDate && !isEndDateValid(startDate, targetDate)) {
|
|
3445
|
+
error(`Target date (${targetDate}) must be after start date (${startDate}).`, opts);
|
|
3446
|
+
process.exit(1);
|
|
3447
|
+
}
|
|
2833
3448
|
const initiativeId = generateId();
|
|
2834
3449
|
const initiativeKey = getNextKey(config.project.key);
|
|
2835
3450
|
run(
|
|
@@ -2843,8 +3458,8 @@ initiativeCommand.command("create").argument("<summary>", "Initiative summary").
|
|
|
2843
3458
|
options.description ?? null,
|
|
2844
3459
|
options.priority,
|
|
2845
3460
|
options.owner ?? null,
|
|
2846
|
-
|
|
2847
|
-
|
|
3461
|
+
startDate,
|
|
3462
|
+
targetDate
|
|
2848
3463
|
]
|
|
2849
3464
|
);
|
|
2850
3465
|
if (opts.json) {
|
|
@@ -2919,9 +3534,12 @@ initiativeCommand.command("show").argument("<key>", "Initiative key").descriptio
|
|
|
2919
3534
|
opts
|
|
2920
3535
|
);
|
|
2921
3536
|
});
|
|
2922
|
-
initiativeCommand.command("update").argument("<key>", "Initiative key").option("-s, --summary <summary>", "New summary").option("-d, --description <description>", "New description").option("-p, --priority <priority>", "New priority").option("--owner <owner>", "New owner").option("--start <date>",
|
|
3537
|
+
initiativeCommand.command("update").argument("<key>", "Initiative key").option("-s, --summary <summary>", "New summary").option("-d, --description <description>", "New description").option("-p, --priority <priority>", "New priority").option("--owner <owner>", "New owner").option("--start <date>", `New start date (${DATE_FORMAT})`).option("--target <date>", `New target date (${DATE_FORMAT})`).description("Update initiative").action((key, options) => {
|
|
2923
3538
|
const opts = getOutputOptions(initiativeCommand);
|
|
2924
|
-
const initiative = queryOne(
|
|
3539
|
+
const initiative = queryOne(
|
|
3540
|
+
"SELECT id, start_date, target_date FROM initiatives WHERE key = ?",
|
|
3541
|
+
[key]
|
|
3542
|
+
);
|
|
2925
3543
|
if (!initiative) {
|
|
2926
3544
|
error(`Initiative "${key}" not found.`, opts);
|
|
2927
3545
|
process.exit(1);
|
|
@@ -2944,13 +3562,28 @@ initiativeCommand.command("update").argument("<key>", "Initiative key").option("
|
|
|
2944
3562
|
updates.push("owner = ?");
|
|
2945
3563
|
params.push(options.owner);
|
|
2946
3564
|
}
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3565
|
+
let startDate = null;
|
|
3566
|
+
let targetDate = null;
|
|
3567
|
+
try {
|
|
3568
|
+
if (options.start) {
|
|
3569
|
+
startDate = validateAndStandardizeDate(options.start, "start date");
|
|
3570
|
+
updates.push("start_date = ?");
|
|
3571
|
+
params.push(startDate);
|
|
3572
|
+
}
|
|
3573
|
+
if (options.target) {
|
|
3574
|
+
targetDate = validateAndStandardizeDate(options.target, "target date");
|
|
3575
|
+
updates.push("target_date = ?");
|
|
3576
|
+
params.push(targetDate);
|
|
3577
|
+
}
|
|
3578
|
+
} catch (err) {
|
|
3579
|
+
error(err instanceof Error ? err.message : String(err), opts);
|
|
3580
|
+
process.exit(1);
|
|
2950
3581
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3582
|
+
const effectiveStart = startDate ?? initiative.start_date;
|
|
3583
|
+
const effectiveTarget = targetDate ?? initiative.target_date;
|
|
3584
|
+
if (effectiveStart && effectiveTarget && !isEndDateValid(effectiveStart, effectiveTarget)) {
|
|
3585
|
+
error(`Target date (${effectiveTarget}) must be after start date (${effectiveStart}).`, opts);
|
|
3586
|
+
process.exit(1);
|
|
2954
3587
|
}
|
|
2955
3588
|
if (updates.length === 0) {
|
|
2956
3589
|
error("No updates specified.", opts);
|
|
@@ -2991,14 +3624,14 @@ initiativeCommand.command("delete").alias("rm").argument("<key>", "Initiative ke
|
|
|
2991
3624
|
// src/commands/sync.ts
|
|
2992
3625
|
init_connection();
|
|
2993
3626
|
import { Command as Command10 } from "commander";
|
|
2994
|
-
import { join as
|
|
3627
|
+
import { join as join7 } from "path";
|
|
2995
3628
|
init_config();
|
|
2996
3629
|
|
|
2997
3630
|
// src/services/file-scanner.ts
|
|
2998
3631
|
init_connection();
|
|
2999
3632
|
import { createHash } from "crypto";
|
|
3000
|
-
import { readFileSync as readFileSync5, statSync, readdirSync, existsSync as
|
|
3001
|
-
import { join as
|
|
3633
|
+
import { readFileSync as readFileSync5, statSync, readdirSync, existsSync as existsSync6 } from "fs";
|
|
3634
|
+
import { join as join6, relative as relative2, extname } from "path";
|
|
3002
3635
|
|
|
3003
3636
|
// src/services/frontmatter-parser.ts
|
|
3004
3637
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -3128,57 +3761,79 @@ function updateFrontmatterContent(content, updates) {
|
|
|
3128
3761
|
}
|
|
3129
3762
|
|
|
3130
3763
|
// src/services/file-scanner.ts
|
|
3131
|
-
|
|
3132
|
-
|
|
3764
|
+
init_monitoring_patterns();
|
|
3765
|
+
init_config();
|
|
3766
|
+
import picomatch from "picomatch";
|
|
3133
3767
|
function computeFileHash(filePath) {
|
|
3134
3768
|
const content = readFileSync5(filePath);
|
|
3135
3769
|
return createHash("sha256").update(content).digest("hex");
|
|
3136
3770
|
}
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3771
|
+
var FileClassifier = class {
|
|
3772
|
+
hardIgnoreMatcher;
|
|
3773
|
+
allowMatcher;
|
|
3774
|
+
denyMatcher;
|
|
3775
|
+
constructor(allowPatterns, denyPatterns) {
|
|
3776
|
+
this.hardIgnoreMatcher = picomatch(getHardIgnorePatterns());
|
|
3777
|
+
this.allowMatcher = picomatch(allowPatterns);
|
|
3778
|
+
this.denyMatcher = picomatch(denyPatterns);
|
|
3142
3779
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
const patternExt = "." + pattern.split("*.").pop();
|
|
3150
|
-
if (ext === patternExt) {
|
|
3151
|
-
return true;
|
|
3152
|
-
}
|
|
3780
|
+
/**
|
|
3781
|
+
* Classify a file into allow/deny/unknown category
|
|
3782
|
+
*/
|
|
3783
|
+
classify(relativePath) {
|
|
3784
|
+
if (this.hardIgnoreMatcher(relativePath)) {
|
|
3785
|
+
return "deny";
|
|
3153
3786
|
}
|
|
3154
|
-
if (
|
|
3155
|
-
return
|
|
3787
|
+
if (this.allowMatcher(relativePath)) {
|
|
3788
|
+
return "allow";
|
|
3156
3789
|
}
|
|
3790
|
+
if (this.denyMatcher(relativePath)) {
|
|
3791
|
+
return "deny";
|
|
3792
|
+
}
|
|
3793
|
+
return "unknown";
|
|
3157
3794
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3795
|
+
/**
|
|
3796
|
+
* Check if file should be completely skipped (hard ignore)
|
|
3797
|
+
*/
|
|
3798
|
+
isHardIgnored(relativePath) {
|
|
3799
|
+
return this.hardIgnoreMatcher(relativePath);
|
|
3800
|
+
}
|
|
3801
|
+
};
|
|
3802
|
+
function collectFiles(dir, rootDir, classifier, trackUnknown, files = []) {
|
|
3803
|
+
if (!existsSync6(dir)) {
|
|
3162
3804
|
return files;
|
|
3163
3805
|
}
|
|
3164
3806
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
3165
3807
|
for (const entry of entries) {
|
|
3166
|
-
const fullPath =
|
|
3808
|
+
const fullPath = join6(dir, entry.name);
|
|
3167
3809
|
const relativePath = relative2(rootDir, fullPath);
|
|
3168
|
-
if (
|
|
3810
|
+
if (classifier.isHardIgnored(relativePath)) {
|
|
3169
3811
|
continue;
|
|
3170
3812
|
}
|
|
3171
3813
|
if (entry.isDirectory()) {
|
|
3172
|
-
|
|
3173
|
-
|
|
3814
|
+
const dirCategory = classifier.classify(relativePath + "/");
|
|
3815
|
+
if (dirCategory === "deny") {
|
|
3816
|
+
continue;
|
|
3817
|
+
}
|
|
3818
|
+
collectFiles(fullPath, rootDir, classifier, trackUnknown, files);
|
|
3819
|
+
} else if (entry.isFile()) {
|
|
3820
|
+
const category = classifier.classify(relativePath);
|
|
3821
|
+
if (category === "deny") {
|
|
3822
|
+
continue;
|
|
3823
|
+
}
|
|
3824
|
+
if (category === "unknown" && !trackUnknown) {
|
|
3825
|
+
continue;
|
|
3826
|
+
}
|
|
3174
3827
|
try {
|
|
3175
3828
|
const stats = statSync(fullPath);
|
|
3176
|
-
const
|
|
3829
|
+
const ext = extname(entry.name).toLowerCase().slice(1);
|
|
3830
|
+
const isBinary = isBinaryExtension(ext);
|
|
3831
|
+
const shouldComputeHash = category === "allow" || !isBinary && stats.size < 10 * 1024 * 1024;
|
|
3832
|
+
const hash = shouldComputeHash ? computeFileHash(fullPath) : null;
|
|
3177
3833
|
let hasFrontmatter = false;
|
|
3178
3834
|
let frontmatterRaw;
|
|
3179
3835
|
let metadata;
|
|
3180
|
-
|
|
3181
|
-
if (ext === ".md" || ext === ".markdown") {
|
|
3836
|
+
if (category === "allow" && (ext === "md" || ext === "markdown") && !isBinary) {
|
|
3182
3837
|
const parsed = parseFrontmatterFromFile(fullPath);
|
|
3183
3838
|
if (parsed) {
|
|
3184
3839
|
hasFrontmatter = true;
|
|
@@ -3189,12 +3844,14 @@ function collectFiles(dir, rootDir, patterns, ignore, files = []) {
|
|
|
3189
3844
|
files.push({
|
|
3190
3845
|
path: relativePath,
|
|
3191
3846
|
filename: entry.name,
|
|
3192
|
-
extension: ext
|
|
3847
|
+
extension: ext,
|
|
3193
3848
|
size: stats.size,
|
|
3194
3849
|
hash,
|
|
3195
3850
|
hasFrontmatter,
|
|
3196
3851
|
frontmatterRaw,
|
|
3197
|
-
metadata
|
|
3852
|
+
metadata,
|
|
3853
|
+
category,
|
|
3854
|
+
isBinary
|
|
3198
3855
|
});
|
|
3199
3856
|
} catch {
|
|
3200
3857
|
}
|
|
@@ -3203,9 +3860,13 @@ function collectFiles(dir, rootDir, patterns, ignore, files = []) {
|
|
|
3203
3860
|
return files;
|
|
3204
3861
|
}
|
|
3205
3862
|
function scanDirectory(projectPath, options = {}) {
|
|
3206
|
-
const
|
|
3207
|
-
const
|
|
3208
|
-
|
|
3863
|
+
const allowPatterns = options.patterns ?? loadAllowPatterns(projectPath);
|
|
3864
|
+
const denyPatterns = options.ignore ?? loadIgnorePatterns(projectPath);
|
|
3865
|
+
const trackUnknown = options.trackUnknown ?? true;
|
|
3866
|
+
const finalAllowPatterns = allowPatterns.length > 0 ? allowPatterns : getDefaultAllowPatterns();
|
|
3867
|
+
const finalDenyPatterns = denyPatterns.length > 0 ? denyPatterns : getDefaultDenyPatterns();
|
|
3868
|
+
const classifier = new FileClassifier(finalAllowPatterns, finalDenyPatterns);
|
|
3869
|
+
return collectFiles(projectPath, projectPath, classifier, trackUnknown);
|
|
3209
3870
|
}
|
|
3210
3871
|
function syncFilesToDatabase(projectId, projectPath, files) {
|
|
3211
3872
|
const result = {
|
|
@@ -3228,13 +3889,15 @@ function syncFilesToDatabase(projectId, projectPath, files) {
|
|
|
3228
3889
|
const metaDependencies = file.metadata?.dependencies ? JSON.stringify(file.metadata.dependencies) : null;
|
|
3229
3890
|
const metaCodeRefs = file.metadata?.code_refs ? JSON.stringify(file.metadata.code_refs) : null;
|
|
3230
3891
|
const metaAuthors = file.metadata?.authors ? JSON.stringify(file.metadata.authors) : null;
|
|
3892
|
+
const needsReview = file.category === "unknown" ? 1 : 0;
|
|
3231
3893
|
if (!existing) {
|
|
3232
3894
|
run(
|
|
3233
3895
|
`INSERT INTO documents (
|
|
3234
3896
|
id, project_id, path, filename, extension, content_hash, size_bytes, status, last_scanned_at,
|
|
3235
3897
|
has_frontmatter, frontmatter_raw, meta_status, meta_version, meta_tldr, meta_title,
|
|
3236
|
-
meta_modules, meta_dependencies, meta_code_refs, meta_authors
|
|
3237
|
-
|
|
3898
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
3899
|
+
monitoring_category, needs_review
|
|
3900
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'tracked', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3238
3901
|
[
|
|
3239
3902
|
generateId(),
|
|
3240
3903
|
projectId,
|
|
@@ -3253,7 +3916,9 @@ function syncFilesToDatabase(projectId, projectPath, files) {
|
|
|
3253
3916
|
metaModules,
|
|
3254
3917
|
metaDependencies,
|
|
3255
3918
|
metaCodeRefs,
|
|
3256
|
-
metaAuthors
|
|
3919
|
+
metaAuthors,
|
|
3920
|
+
file.category,
|
|
3921
|
+
needsReview
|
|
3257
3922
|
]
|
|
3258
3923
|
);
|
|
3259
3924
|
result.new++;
|
|
@@ -3262,7 +3927,8 @@ function syncFilesToDatabase(projectId, projectPath, files) {
|
|
|
3262
3927
|
`UPDATE documents SET
|
|
3263
3928
|
content_hash = ?, size_bytes = ?, status = 'modified', last_scanned_at = ?, updated_at = datetime('now'),
|
|
3264
3929
|
has_frontmatter = ?, frontmatter_raw = ?, meta_status = ?, meta_version = ?, meta_tldr = ?, meta_title = ?,
|
|
3265
|
-
meta_modules = ?, meta_dependencies = ?, meta_code_refs = ?, meta_authors =
|
|
3930
|
+
meta_modules = ?, meta_dependencies = ?, meta_code_refs = ?, meta_authors = ?,
|
|
3931
|
+
monitoring_category = ?
|
|
3266
3932
|
WHERE id = ?`,
|
|
3267
3933
|
[
|
|
3268
3934
|
file.hash,
|
|
@@ -3278,14 +3944,15 @@ function syncFilesToDatabase(projectId, projectPath, files) {
|
|
|
3278
3944
|
metaDependencies,
|
|
3279
3945
|
metaCodeRefs,
|
|
3280
3946
|
metaAuthors,
|
|
3947
|
+
file.category,
|
|
3281
3948
|
existing.id
|
|
3282
3949
|
]
|
|
3283
3950
|
);
|
|
3284
3951
|
result.modified++;
|
|
3285
3952
|
} else {
|
|
3286
3953
|
run(
|
|
3287
|
-
`UPDATE documents SET last_scanned_at = ?, status = 'tracked' WHERE id = ?`,
|
|
3288
|
-
[now, existing.id]
|
|
3954
|
+
`UPDATE documents SET last_scanned_at = ?, status = 'tracked', monitoring_category = ? WHERE id = ?`,
|
|
3955
|
+
[now, file.category, existing.id]
|
|
3289
3956
|
);
|
|
3290
3957
|
result.unchanged++;
|
|
3291
3958
|
}
|
|
@@ -3315,6 +3982,14 @@ function getSyncStatus(projectId) {
|
|
|
3315
3982
|
FROM documents
|
|
3316
3983
|
WHERE project_id = ?
|
|
3317
3984
|
`, [projectId]);
|
|
3985
|
+
const categoryStats = queryOne(`
|
|
3986
|
+
SELECT
|
|
3987
|
+
SUM(CASE WHEN monitoring_category = 'allow' THEN 1 ELSE 0 END) as allow,
|
|
3988
|
+
SUM(CASE WHEN monitoring_category = 'deny' THEN 1 ELSE 0 END) as deny,
|
|
3989
|
+
SUM(CASE WHEN monitoring_category = 'unknown' OR monitoring_category IS NULL THEN 1 ELSE 0 END) as unknown
|
|
3990
|
+
FROM documents
|
|
3991
|
+
WHERE project_id = ?
|
|
3992
|
+
`, [projectId]);
|
|
3318
3993
|
const lastScan = queryOne(
|
|
3319
3994
|
"SELECT MAX(last_scanned_at) as last_scanned_at FROM documents WHERE project_id = ?",
|
|
3320
3995
|
[projectId]
|
|
@@ -3324,7 +3999,12 @@ function getSyncStatus(projectId) {
|
|
|
3324
3999
|
tracked: stats?.tracked ?? 0,
|
|
3325
4000
|
modified: stats?.modified ?? 0,
|
|
3326
4001
|
deleted: stats?.deleted ?? 0,
|
|
3327
|
-
lastScan: lastScan?.last_scanned_at ?? null
|
|
4002
|
+
lastScan: lastScan?.last_scanned_at ?? null,
|
|
4003
|
+
byCategory: {
|
|
4004
|
+
allow: categoryStats?.allow ?? 0,
|
|
4005
|
+
deny: categoryStats?.deny ?? 0,
|
|
4006
|
+
unknown: categoryStats?.unknown ?? 0
|
|
4007
|
+
}
|
|
3328
4008
|
};
|
|
3329
4009
|
}
|
|
3330
4010
|
function getDocuments(projectId, status) {
|
|
@@ -3417,65 +4097,306 @@ function searchDocumentsByTldr(projectId, searchTerm) {
|
|
|
3417
4097
|
[projectId, `%${searchTerm}%`]
|
|
3418
4098
|
);
|
|
3419
4099
|
}
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
positionToLine[pos++] = lineNum + 1;
|
|
3436
|
-
}
|
|
4100
|
+
function getUnanalyzedDocuments(projectId, limit, offset) {
|
|
4101
|
+
let query = `
|
|
4102
|
+
SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
|
|
4103
|
+
has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
|
|
4104
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
4105
|
+
shadow_mode, analyzed_at, analysis_confidence, file_type,
|
|
4106
|
+
complexity_score, exports, inferred_module, inferred_component, analysis_notes
|
|
4107
|
+
FROM documents
|
|
4108
|
+
WHERE project_id = ? AND analyzed_at IS NULL AND status != 'deleted'
|
|
4109
|
+
ORDER BY path
|
|
4110
|
+
`;
|
|
4111
|
+
const params = [projectId];
|
|
4112
|
+
if (limit !== void 0) {
|
|
4113
|
+
query += " LIMIT ?";
|
|
4114
|
+
params.push(limit);
|
|
3437
4115
|
}
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
comments.push({
|
|
3442
|
-
type: "user",
|
|
3443
|
-
content: match[1].trim(),
|
|
3444
|
-
lineNumber: positionToLine[match.index] ?? 1,
|
|
3445
|
-
raw: match[0]
|
|
3446
|
-
});
|
|
4116
|
+
if (offset !== void 0) {
|
|
4117
|
+
query += " OFFSET ?";
|
|
4118
|
+
params.push(offset);
|
|
3447
4119
|
}
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
4120
|
+
return queryAll(query, params);
|
|
4121
|
+
}
|
|
4122
|
+
function getUnanalyzedCount(projectId) {
|
|
4123
|
+
const result = queryOne(
|
|
4124
|
+
`SELECT COUNT(*) as count FROM documents
|
|
4125
|
+
WHERE project_id = ? AND analyzed_at IS NULL AND status != 'deleted'`,
|
|
4126
|
+
[projectId]
|
|
4127
|
+
);
|
|
4128
|
+
return result?.count ?? 0;
|
|
4129
|
+
}
|
|
4130
|
+
function getAnalyzedDocuments(projectId, limit) {
|
|
4131
|
+
let query = `
|
|
4132
|
+
SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
|
|
4133
|
+
has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
|
|
4134
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
4135
|
+
shadow_mode, analyzed_at, analysis_confidence, file_type,
|
|
4136
|
+
complexity_score, exports, inferred_module, inferred_component, analysis_notes
|
|
4137
|
+
FROM documents
|
|
4138
|
+
WHERE project_id = ? AND analyzed_at IS NOT NULL AND status != 'deleted'
|
|
4139
|
+
ORDER BY analyzed_at DESC
|
|
4140
|
+
`;
|
|
4141
|
+
const params = [projectId];
|
|
4142
|
+
if (limit !== void 0) {
|
|
4143
|
+
query += " LIMIT ?";
|
|
4144
|
+
params.push(limit);
|
|
3456
4145
|
}
|
|
3457
|
-
|
|
3458
|
-
return comments;
|
|
4146
|
+
return queryAll(query, params);
|
|
3459
4147
|
}
|
|
3460
|
-
function
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
4148
|
+
function getLowConfidenceDocuments(projectId, threshold = 70) {
|
|
4149
|
+
return queryAll(
|
|
4150
|
+
`SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
|
|
4151
|
+
has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
|
|
4152
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
4153
|
+
shadow_mode, analyzed_at, analysis_confidence, file_type,
|
|
4154
|
+
complexity_score, exports, inferred_module, inferred_component, analysis_notes
|
|
4155
|
+
FROM documents
|
|
4156
|
+
WHERE project_id = ? AND analyzed_at IS NOT NULL
|
|
4157
|
+
AND analysis_confidence IS NOT NULL AND analysis_confidence < ?
|
|
4158
|
+
AND status != 'deleted'
|
|
4159
|
+
ORDER BY analysis_confidence ASC`,
|
|
4160
|
+
[projectId, threshold]
|
|
3469
4161
|
);
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
4162
|
+
}
|
|
4163
|
+
function getDocumentsByInferredModule(projectId, module) {
|
|
4164
|
+
return queryAll(
|
|
4165
|
+
`SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
|
|
4166
|
+
has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
|
|
4167
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
4168
|
+
shadow_mode, analyzed_at, analysis_confidence, file_type,
|
|
4169
|
+
complexity_score, exports, inferred_module, inferred_component, analysis_notes
|
|
4170
|
+
FROM documents
|
|
4171
|
+
WHERE project_id = ? AND inferred_module = ? AND status != 'deleted'
|
|
4172
|
+
ORDER BY path`,
|
|
4173
|
+
[projectId, module]
|
|
4174
|
+
);
|
|
4175
|
+
}
|
|
4176
|
+
function getDocumentsByFileType(projectId, fileType) {
|
|
4177
|
+
return queryAll(
|
|
4178
|
+
`SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
|
|
4179
|
+
has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
|
|
4180
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
4181
|
+
shadow_mode, analyzed_at, analysis_confidence, file_type,
|
|
4182
|
+
complexity_score, exports, inferred_module, inferred_component, analysis_notes
|
|
4183
|
+
FROM documents
|
|
4184
|
+
WHERE project_id = ? AND file_type = ? AND status != 'deleted'
|
|
4185
|
+
ORDER BY path`,
|
|
4186
|
+
[projectId, fileType]
|
|
4187
|
+
);
|
|
4188
|
+
}
|
|
4189
|
+
function getDocumentWithAnalysis(projectId, filePath) {
|
|
4190
|
+
return queryOne(
|
|
4191
|
+
`SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
|
|
4192
|
+
has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
|
|
4193
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
4194
|
+
shadow_mode, analyzed_at, analysis_confidence, file_type,
|
|
4195
|
+
complexity_score, exports, inferred_module, inferred_component, analysis_notes
|
|
4196
|
+
FROM documents
|
|
4197
|
+
WHERE project_id = ? AND path = ?`,
|
|
4198
|
+
[projectId, filePath]
|
|
4199
|
+
);
|
|
4200
|
+
}
|
|
4201
|
+
function updateDocumentAnalysis(projectId, filePath, analysis) {
|
|
4202
|
+
const doc = getDocumentByPath(projectId, filePath);
|
|
4203
|
+
if (!doc) {
|
|
4204
|
+
return false;
|
|
4205
|
+
}
|
|
4206
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4207
|
+
const deps = analysis.dependencies ? JSON.stringify(analysis.dependencies) : null;
|
|
4208
|
+
const exps = analysis.exports ? JSON.stringify(analysis.exports) : null;
|
|
4209
|
+
run(
|
|
4210
|
+
`UPDATE documents SET
|
|
4211
|
+
meta_tldr = COALESCE(?, meta_tldr),
|
|
4212
|
+
meta_dependencies = COALESCE(?, meta_dependencies),
|
|
4213
|
+
inferred_module = COALESCE(?, inferred_module),
|
|
4214
|
+
inferred_component = COALESCE(?, inferred_component),
|
|
4215
|
+
file_type = COALESCE(?, file_type),
|
|
4216
|
+
exports = COALESCE(?, exports),
|
|
4217
|
+
complexity_score = COALESCE(?, complexity_score),
|
|
4218
|
+
analysis_confidence = COALESCE(?, analysis_confidence),
|
|
4219
|
+
analysis_notes = COALESCE(?, analysis_notes),
|
|
4220
|
+
analyzed_at = ?,
|
|
4221
|
+
updated_at = datetime('now')
|
|
4222
|
+
WHERE id = ?`,
|
|
4223
|
+
[
|
|
4224
|
+
analysis.tldr ?? null,
|
|
4225
|
+
deps,
|
|
4226
|
+
analysis.module ?? null,
|
|
4227
|
+
analysis.component ?? null,
|
|
4228
|
+
analysis.fileType ?? null,
|
|
4229
|
+
exps,
|
|
4230
|
+
analysis.complexity ?? null,
|
|
4231
|
+
analysis.confidence ?? null,
|
|
4232
|
+
analysis.notes ?? null,
|
|
4233
|
+
now,
|
|
4234
|
+
doc.id
|
|
4235
|
+
]
|
|
4236
|
+
);
|
|
4237
|
+
return true;
|
|
4238
|
+
}
|
|
4239
|
+
function getAnalysisProgress(projectId) {
|
|
4240
|
+
const counts = queryOne(`
|
|
4241
|
+
SELECT
|
|
4242
|
+
COUNT(*) as total,
|
|
4243
|
+
SUM(CASE WHEN analyzed_at IS NOT NULL THEN 1 ELSE 0 END) as analyzed,
|
|
4244
|
+
SUM(CASE WHEN analyzed_at IS NULL THEN 1 ELSE 0 END) as unanalyzed,
|
|
4245
|
+
SUM(CASE WHEN analysis_confidence IS NOT NULL AND analysis_confidence < 70 THEN 1 ELSE 0 END) as low_confidence
|
|
4246
|
+
FROM documents
|
|
4247
|
+
WHERE project_id = ? AND status != 'deleted'
|
|
4248
|
+
`, [projectId]);
|
|
4249
|
+
const moduleStats = queryAll(`
|
|
4250
|
+
SELECT
|
|
4251
|
+
COALESCE(inferred_module, 'unknown') as module,
|
|
4252
|
+
SUM(CASE WHEN analyzed_at IS NOT NULL THEN 1 ELSE 0 END) as analyzed,
|
|
4253
|
+
COUNT(*) as total
|
|
4254
|
+
FROM documents
|
|
4255
|
+
WHERE project_id = ? AND status != 'deleted'
|
|
4256
|
+
GROUP BY COALESCE(inferred_module, 'unknown')
|
|
4257
|
+
ORDER BY total DESC
|
|
4258
|
+
`, [projectId]);
|
|
4259
|
+
const typeStats = queryAll(`
|
|
4260
|
+
SELECT
|
|
4261
|
+
COALESCE(file_type, extension, 'unknown') as file_type,
|
|
4262
|
+
COUNT(*) as count
|
|
4263
|
+
FROM documents
|
|
4264
|
+
WHERE project_id = ? AND status != 'deleted'
|
|
4265
|
+
GROUP BY COALESCE(file_type, extension, 'unknown')
|
|
4266
|
+
ORDER BY count DESC
|
|
4267
|
+
`, [projectId]);
|
|
4268
|
+
const byModule = {};
|
|
4269
|
+
for (const stat of moduleStats) {
|
|
4270
|
+
byModule[stat.module ?? "unknown"] = {
|
|
4271
|
+
analyzed: stat.analyzed,
|
|
4272
|
+
total: stat.total
|
|
4273
|
+
};
|
|
4274
|
+
}
|
|
4275
|
+
const byFileType = {};
|
|
4276
|
+
for (const stat of typeStats) {
|
|
4277
|
+
byFileType[stat.file_type ?? "unknown"] = stat.count;
|
|
4278
|
+
}
|
|
4279
|
+
return {
|
|
4280
|
+
total: counts?.total ?? 0,
|
|
4281
|
+
analyzed: counts?.analyzed ?? 0,
|
|
4282
|
+
unanalyzed: counts?.unanalyzed ?? 0,
|
|
4283
|
+
lowConfidence: counts?.low_confidence ?? 0,
|
|
4284
|
+
byModule,
|
|
4285
|
+
byFileType
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
function getShadowDocuments(projectId) {
|
|
4289
|
+
return queryAll(
|
|
4290
|
+
`SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
|
|
4291
|
+
has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
|
|
4292
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
4293
|
+
shadow_mode, analyzed_at, analysis_confidence, file_type,
|
|
4294
|
+
complexity_score, exports, inferred_module, inferred_component, analysis_notes
|
|
4295
|
+
FROM documents
|
|
4296
|
+
WHERE project_id = ? AND shadow_mode = 1 AND status != 'deleted'
|
|
4297
|
+
ORDER BY path`,
|
|
4298
|
+
[projectId]
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
4301
|
+
function markAsShadowMode(projectId, filePath) {
|
|
4302
|
+
const doc = getDocumentByPath(projectId, filePath);
|
|
4303
|
+
if (!doc) {
|
|
4304
|
+
return false;
|
|
4305
|
+
}
|
|
4306
|
+
run(
|
|
4307
|
+
`UPDATE documents SET shadow_mode = 1, updated_at = datetime('now') WHERE id = ?`,
|
|
4308
|
+
[doc.id]
|
|
4309
|
+
);
|
|
4310
|
+
return true;
|
|
4311
|
+
}
|
|
4312
|
+
function trackShadowFile(projectId, projectPath, filePath) {
|
|
4313
|
+
const fullPath = join6(projectPath, filePath);
|
|
4314
|
+
if (!existsSync6(fullPath)) {
|
|
4315
|
+
return null;
|
|
4316
|
+
}
|
|
4317
|
+
const existing = getDocumentByPath(projectId, filePath);
|
|
4318
|
+
if (existing) {
|
|
4319
|
+
markAsShadowMode(projectId, filePath);
|
|
4320
|
+
return getDocumentWithAnalysis(projectId, filePath) ?? null;
|
|
4321
|
+
}
|
|
4322
|
+
try {
|
|
4323
|
+
const stats = statSync(fullPath);
|
|
4324
|
+
const hash = computeFileHash(fullPath);
|
|
4325
|
+
const filename = filePath.split("/").pop() ?? filePath;
|
|
4326
|
+
const ext = extname(filename).slice(1);
|
|
4327
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4328
|
+
const id = generateId();
|
|
4329
|
+
run(
|
|
4330
|
+
`INSERT INTO documents (
|
|
4331
|
+
id, project_id, path, filename, extension, content_hash, size_bytes,
|
|
4332
|
+
status, last_scanned_at, shadow_mode, has_frontmatter
|
|
4333
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'tracked', ?, 1, 0)`,
|
|
4334
|
+
[id, projectId, filePath, filename, ext, hash, stats.size, now]
|
|
4335
|
+
);
|
|
4336
|
+
return getDocumentWithAnalysis(projectId, filePath) ?? null;
|
|
4337
|
+
} catch {
|
|
4338
|
+
return null;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
// src/services/comment-parser.ts
|
|
4343
|
+
init_connection();
|
|
4344
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
4345
|
+
var USER_COMMENT_PATTERN = /\[\[!\s*([\s\S]*?)\s*\]\]/g;
|
|
4346
|
+
var AI_COMMENT_PATTERN = /\[\{!\s*([\s\S]*?)\s*\}]/g;
|
|
4347
|
+
function parseComments(filePath) {
|
|
4348
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
4349
|
+
const lines = content.split("\n");
|
|
4350
|
+
const comments = [];
|
|
4351
|
+
const positionToLine = [];
|
|
4352
|
+
let pos = 0;
|
|
4353
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
4354
|
+
const lineLength = lines[lineNum].length + 1;
|
|
4355
|
+
for (let i = 0; i < lineLength; i++) {
|
|
4356
|
+
positionToLine[pos++] = lineNum + 1;
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
let match;
|
|
4360
|
+
USER_COMMENT_PATTERN.lastIndex = 0;
|
|
4361
|
+
while ((match = USER_COMMENT_PATTERN.exec(content)) !== null) {
|
|
4362
|
+
comments.push({
|
|
4363
|
+
type: "user",
|
|
4364
|
+
content: match[1].trim(),
|
|
4365
|
+
lineNumber: positionToLine[match.index] ?? 1,
|
|
4366
|
+
raw: match[0]
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
AI_COMMENT_PATTERN.lastIndex = 0;
|
|
4370
|
+
while ((match = AI_COMMENT_PATTERN.exec(content)) !== null) {
|
|
4371
|
+
comments.push({
|
|
4372
|
+
type: "ai",
|
|
4373
|
+
content: match[1].trim(),
|
|
4374
|
+
lineNumber: positionToLine[match.index] ?? 1,
|
|
4375
|
+
raw: match[0]
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
comments.sort((a, b) => a.lineNumber - b.lineNumber);
|
|
4379
|
+
return comments;
|
|
4380
|
+
}
|
|
4381
|
+
function syncCommentsToDatabase(documentId, comments) {
|
|
4382
|
+
const result = {
|
|
4383
|
+
total: comments.length,
|
|
4384
|
+
new: 0,
|
|
4385
|
+
resolved: 0
|
|
4386
|
+
};
|
|
4387
|
+
const existingComments = queryAll(
|
|
4388
|
+
"SELECT id, marker_type, line_number, content FROM doc_comments WHERE document_id = ? AND resolved = 0",
|
|
4389
|
+
[documentId]
|
|
4390
|
+
);
|
|
4391
|
+
const processedIds = /* @__PURE__ */ new Set();
|
|
4392
|
+
for (const comment of comments) {
|
|
4393
|
+
const markerType = comment.type === "user" ? "user" : "ai";
|
|
4394
|
+
const existing = existingComments.find(
|
|
4395
|
+
(e) => e.marker_type === markerType && e.content === comment.content
|
|
4396
|
+
);
|
|
4397
|
+
if (existing) {
|
|
4398
|
+
processedIds.add(existing.id);
|
|
4399
|
+
if (existing.line_number !== comment.lineNumber) {
|
|
3479
4400
|
run(
|
|
3480
4401
|
"UPDATE doc_comments SET line_number = ? WHERE id = ?",
|
|
3481
4402
|
[comment.lineNumber, existing.id]
|
|
@@ -3519,7 +4440,7 @@ function getCommentStats(projectId) {
|
|
|
3519
4440
|
|
|
3520
4441
|
// src/commands/sync.ts
|
|
3521
4442
|
var syncCommand = new Command10("sync").description("File synchronization and document tracking");
|
|
3522
|
-
syncCommand.command("scan").option("--patterns <patterns>", 'Comma-separated glob patterns (e.g., "**/*.md,**/*.feature")').option("--ignore <dirs>", "Comma-separated directories to ignore").option("--comments", "Also parse and sync comments from files").description("Scan repository files and sync to database").action((options) => {
|
|
4443
|
+
syncCommand.command("scan").option("--patterns <patterns>", 'Comma-separated glob patterns (e.g., "**/*.md,**/*.feature")').option("--ignore <dirs>", "Comma-separated directories to ignore").option("--comments", "Also parse and sync comments from files").option("--shadow", "Shadow mode: track files without modifying them (for brownfield projects)").option("--track-all", "Track all source files, not just docs (use with --shadow)").option("--include <patterns>", 'Additional patterns to include (e.g., "**/*.ts,**/*.js")').option("--exclude <patterns>", "Additional patterns to exclude").description("Scan repository files and sync to database").action((options) => {
|
|
3523
4444
|
const opts = getOutputOptions(syncCommand);
|
|
3524
4445
|
const projectRoot = findProjectRoot();
|
|
3525
4446
|
if (!projectRoot) {
|
|
@@ -3537,12 +4458,47 @@ syncCommand.command("scan").option("--patterns <patterns>", 'Comma-separated glo
|
|
|
3537
4458
|
process.exit(1);
|
|
3538
4459
|
}
|
|
3539
4460
|
const scanOptions = {};
|
|
4461
|
+
if (options.trackAll) {
|
|
4462
|
+
scanOptions.patterns = [
|
|
4463
|
+
"**/*.md",
|
|
4464
|
+
"**/*.feature",
|
|
4465
|
+
"**/*.yaml",
|
|
4466
|
+
"**/*.yml",
|
|
4467
|
+
"**/*.ts",
|
|
4468
|
+
"**/*.tsx",
|
|
4469
|
+
"**/*.js",
|
|
4470
|
+
"**/*.jsx",
|
|
4471
|
+
"**/*.py",
|
|
4472
|
+
"**/*.go",
|
|
4473
|
+
"**/*.rs",
|
|
4474
|
+
"**/*.java",
|
|
4475
|
+
"**/*.css",
|
|
4476
|
+
"**/*.scss",
|
|
4477
|
+
"**/*.less",
|
|
4478
|
+
"**/*.json",
|
|
4479
|
+
"**/*.toml",
|
|
4480
|
+
"**/*.sql",
|
|
4481
|
+
"**/*.graphql"
|
|
4482
|
+
];
|
|
4483
|
+
}
|
|
3540
4484
|
if (options.patterns) {
|
|
3541
4485
|
scanOptions.patterns = options.patterns.split(",").map((p) => p.trim());
|
|
3542
4486
|
}
|
|
4487
|
+
if (options.include) {
|
|
4488
|
+
const includePatterns = options.include.split(",").map((p) => p.trim());
|
|
4489
|
+
scanOptions.patterns = [...scanOptions.patterns ?? [], ...includePatterns];
|
|
4490
|
+
}
|
|
3543
4491
|
if (options.ignore) {
|
|
3544
4492
|
scanOptions.ignore = options.ignore.split(",").map((d) => d.trim());
|
|
3545
4493
|
}
|
|
4494
|
+
if (options.exclude) {
|
|
4495
|
+
const excludePatterns = options.exclude.split(",").map((d) => d.trim());
|
|
4496
|
+
scanOptions.ignore = [...scanOptions.ignore ?? [], ...excludePatterns];
|
|
4497
|
+
}
|
|
4498
|
+
const isShadowMode = options.shadow || options.trackAll;
|
|
4499
|
+
if (isShadowMode) {
|
|
4500
|
+
info("Shadow mode: tracking files without modification", opts);
|
|
4501
|
+
}
|
|
3546
4502
|
info("Scanning files...", opts);
|
|
3547
4503
|
const files = scanDirectory(projectRoot, scanOptions);
|
|
3548
4504
|
const result = syncFilesToDatabase(project.id, projectRoot, files);
|
|
@@ -3552,7 +4508,7 @@ syncCommand.command("scan").option("--patterns <patterns>", 'Comma-separated glo
|
|
|
3552
4508
|
for (const file of files) {
|
|
3553
4509
|
if (["md", "feature", "yaml", "yml"].includes(file.extension)) {
|
|
3554
4510
|
try {
|
|
3555
|
-
const fullPath =
|
|
4511
|
+
const fullPath = join7(projectRoot, file.path);
|
|
3556
4512
|
const comments = parseComments(fullPath);
|
|
3557
4513
|
const doc = queryOne(
|
|
3558
4514
|
"SELECT id FROM documents WHERE project_id = ? AND path = ?",
|
|
@@ -3568,10 +4524,16 @@ syncCommand.command("scan").option("--patterns <patterns>", 'Comma-separated glo
|
|
|
3568
4524
|
}
|
|
3569
4525
|
}
|
|
3570
4526
|
}
|
|
4527
|
+
const fileTypeBreakdown = {};
|
|
4528
|
+
for (const file of files) {
|
|
4529
|
+
const ext = file.extension || "other";
|
|
4530
|
+
fileTypeBreakdown[ext] = (fileTypeBreakdown[ext] || 0) + 1;
|
|
4531
|
+
}
|
|
3571
4532
|
if (opts.json) {
|
|
3572
4533
|
console.log(JSON.stringify({
|
|
3573
4534
|
success: true,
|
|
3574
4535
|
data: {
|
|
4536
|
+
shadowMode: isShadowMode,
|
|
3575
4537
|
files: {
|
|
3576
4538
|
total: result.total,
|
|
3577
4539
|
new: result.new,
|
|
@@ -3579,6 +4541,7 @@ syncCommand.command("scan").option("--patterns <patterns>", 'Comma-separated glo
|
|
|
3579
4541
|
deleted: result.deleted,
|
|
3580
4542
|
unchanged: result.unchanged
|
|
3581
4543
|
},
|
|
4544
|
+
breakdown: isShadowMode ? fileTypeBreakdown : void 0,
|
|
3582
4545
|
comments: options.comments ? commentStats : void 0
|
|
3583
4546
|
}
|
|
3584
4547
|
}));
|
|
@@ -3591,6 +4554,12 @@ syncCommand.command("scan").option("--patterns <patterns>", 'Comma-separated glo
|
|
|
3591
4554
|
if (options.comments) {
|
|
3592
4555
|
console.log(` Comments: ${commentStats.new} new, ${commentStats.resolved} resolved`);
|
|
3593
4556
|
}
|
|
4557
|
+
if (isShadowMode && Object.keys(fileTypeBreakdown).length > 0) {
|
|
4558
|
+
console.log("\n File types:");
|
|
4559
|
+
for (const [ext, count] of Object.entries(fileTypeBreakdown).sort((a, b) => b[1] - a[1]).slice(0, 10)) {
|
|
4560
|
+
console.log(` .${ext}: ${count}`);
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
3594
4563
|
}
|
|
3595
4564
|
});
|
|
3596
4565
|
syncCommand.command("status").description("Show file sync status").action(() => {
|
|
@@ -6162,7 +7131,7 @@ init_connection();
|
|
|
6162
7131
|
import { Command as Command16 } from "commander";
|
|
6163
7132
|
init_config();
|
|
6164
7133
|
var versionCommand = new Command16("version").description("Manage project versions/releases");
|
|
6165
|
-
versionCommand.command("create").argument("<name>", "Version name (e.g., v1.0.0)").option("-d, --description <description>", "Version description").option("--start <date>",
|
|
7134
|
+
versionCommand.command("create").argument("<name>", "Version name (e.g., v1.0.0)").option("-d, --description <description>", "Version description").option("--start <date>", `Start date (${DATE_FORMAT})`).option("--release <date>", `Release date (${DATE_FORMAT})`).description("Create a new version").action((name, options) => {
|
|
6166
7135
|
const opts = getOutputOptions(versionCommand);
|
|
6167
7136
|
const projectRoot = findProjectRoot();
|
|
6168
7137
|
if (!projectRoot) {
|
|
@@ -6184,11 +7153,28 @@ versionCommand.command("create").argument("<name>", "Version name (e.g., v1.0.0)
|
|
|
6184
7153
|
error(`Version "${name}" already exists.`, opts);
|
|
6185
7154
|
process.exit(1);
|
|
6186
7155
|
}
|
|
7156
|
+
let startDate = null;
|
|
7157
|
+
let releaseDate = null;
|
|
7158
|
+
try {
|
|
7159
|
+
if (options.start) {
|
|
7160
|
+
startDate = validateAndStandardizeDate(options.start, "start date");
|
|
7161
|
+
}
|
|
7162
|
+
if (options.release) {
|
|
7163
|
+
releaseDate = validateAndStandardizeDate(options.release, "release date");
|
|
7164
|
+
}
|
|
7165
|
+
} catch (err) {
|
|
7166
|
+
error(err instanceof Error ? err.message : String(err), opts);
|
|
7167
|
+
process.exit(1);
|
|
7168
|
+
}
|
|
7169
|
+
if (startDate && releaseDate && !isEndDateValid(startDate, releaseDate)) {
|
|
7170
|
+
error(`Release date (${releaseDate}) must be after start date (${startDate}).`, opts);
|
|
7171
|
+
process.exit(1);
|
|
7172
|
+
}
|
|
6187
7173
|
const versionId = generateId();
|
|
6188
7174
|
run(
|
|
6189
7175
|
`INSERT INTO versions (id, project_id, name, description, status, start_date, release_date)
|
|
6190
7176
|
VALUES (?, ?, ?, ?, 'unreleased', ?, ?)`,
|
|
6191
|
-
[versionId, project.id, name, options.description ?? null,
|
|
7177
|
+
[versionId, project.id, name, options.description ?? null, startDate, releaseDate]
|
|
6192
7178
|
);
|
|
6193
7179
|
logCreate(project.id, "version", versionId, { name, description: options.description });
|
|
6194
7180
|
if (opts.json) {
|
|
@@ -6276,7 +7262,7 @@ versionCommand.command("show").argument("<name>", "Version name").description("S
|
|
|
6276
7262
|
opts
|
|
6277
7263
|
);
|
|
6278
7264
|
});
|
|
6279
|
-
versionCommand.command("update").argument("<name>", "Version name").option("-d, --description <description>", "Version description").option("--start <date>",
|
|
7265
|
+
versionCommand.command("update").argument("<name>", "Version name").option("-d, --description <description>", "Version description").option("--start <date>", `Start date (${DATE_FORMAT})`).option("--release <date>", `Release date (${DATE_FORMAT})`).option("--rename <newName>", "Rename version").description("Update a version").action((name, options) => {
|
|
6280
7266
|
const opts = getOutputOptions(versionCommand);
|
|
6281
7267
|
const projectRoot = findProjectRoot();
|
|
6282
7268
|
if (!projectRoot) {
|
|
@@ -6293,7 +7279,10 @@ versionCommand.command("update").argument("<name>", "Version name").option("-d,
|
|
|
6293
7279
|
error(`Project "${config.project.key}" not found.`, opts);
|
|
6294
7280
|
process.exit(1);
|
|
6295
7281
|
}
|
|
6296
|
-
const version = queryOne(
|
|
7282
|
+
const version = queryOne(
|
|
7283
|
+
"SELECT id, start_date, release_date FROM versions WHERE project_id = ? AND name = ?",
|
|
7284
|
+
[project.id, name]
|
|
7285
|
+
);
|
|
6297
7286
|
if (!version) {
|
|
6298
7287
|
error(`Version "${name}" not found.`, opts);
|
|
6299
7288
|
process.exit(1);
|
|
@@ -6306,15 +7295,30 @@ versionCommand.command("update").argument("<name>", "Version name").option("-d,
|
|
|
6306
7295
|
params.push(options.description);
|
|
6307
7296
|
changes.description = options.description;
|
|
6308
7297
|
}
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
7298
|
+
let startDate = null;
|
|
7299
|
+
let releaseDate = null;
|
|
7300
|
+
try {
|
|
7301
|
+
if (options.start !== void 0) {
|
|
7302
|
+
startDate = validateAndStandardizeDate(options.start, "start date");
|
|
7303
|
+
updates.push("start_date = ?");
|
|
7304
|
+
params.push(startDate);
|
|
7305
|
+
changes.start_date = startDate;
|
|
7306
|
+
}
|
|
7307
|
+
if (options.release !== void 0) {
|
|
7308
|
+
releaseDate = validateAndStandardizeDate(options.release, "release date");
|
|
7309
|
+
updates.push("release_date = ?");
|
|
7310
|
+
params.push(releaseDate);
|
|
7311
|
+
changes.release_date = releaseDate;
|
|
7312
|
+
}
|
|
7313
|
+
} catch (err) {
|
|
7314
|
+
error(err instanceof Error ? err.message : String(err), opts);
|
|
7315
|
+
process.exit(1);
|
|
6313
7316
|
}
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
7317
|
+
const effectiveStart = startDate ?? version.start_date;
|
|
7318
|
+
const effectiveRelease = releaseDate ?? version.release_date;
|
|
7319
|
+
if (effectiveStart && effectiveRelease && !isEndDateValid(effectiveStart, effectiveRelease)) {
|
|
7320
|
+
error(`Release date (${effectiveRelease}) must be after start date (${effectiveStart}).`, opts);
|
|
7321
|
+
process.exit(1);
|
|
6318
7322
|
}
|
|
6319
7323
|
if (options.rename) {
|
|
6320
7324
|
const existing = queryOne("SELECT id FROM versions WHERE project_id = ? AND name = ?", [project.id, options.rename]);
|
|
@@ -6874,7 +7878,7 @@ uxJourneyCommand.command("delete").alias("rm").argument("<key>", "Journey key").
|
|
|
6874
7878
|
init_connection();
|
|
6875
7879
|
import { Command as Command19 } from "commander";
|
|
6876
7880
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
6877
|
-
import { join as
|
|
7881
|
+
import { join as join8 } from "path";
|
|
6878
7882
|
init_config();
|
|
6879
7883
|
var docCommand = new Command19("doc").description("Document management and frontmatter operations");
|
|
6880
7884
|
function getProjectId(opts) {
|
|
@@ -6954,7 +7958,7 @@ docCommand.command("show <path>").description("Show document details including f
|
|
|
6954
7958
|
error(`Document not found: ${filePath}`, opts);
|
|
6955
7959
|
process.exit(1);
|
|
6956
7960
|
}
|
|
6957
|
-
const fullPath =
|
|
7961
|
+
const fullPath = join8(ctx.projectRoot, filePath);
|
|
6958
7962
|
const parsed = parseFrontmatterFromFile(fullPath);
|
|
6959
7963
|
if (opts.json) {
|
|
6960
7964
|
console.log(JSON.stringify({
|
|
@@ -7016,7 +8020,7 @@ docCommand.command("update <path>").option("--status <status>", "Set metadata st
|
|
|
7016
8020
|
if (!ctx) {
|
|
7017
8021
|
process.exit(1);
|
|
7018
8022
|
}
|
|
7019
|
-
const fullPath =
|
|
8023
|
+
const fullPath = join8(ctx.projectRoot, filePath);
|
|
7020
8024
|
let content;
|
|
7021
8025
|
try {
|
|
7022
8026
|
content = readFileSync7(fullPath, "utf-8");
|
|
@@ -7089,7 +8093,7 @@ docCommand.command("init-frontmatter <path>").option("--status <status>", "Initi
|
|
|
7089
8093
|
if (!ctx) {
|
|
7090
8094
|
process.exit(1);
|
|
7091
8095
|
}
|
|
7092
|
-
const fullPath =
|
|
8096
|
+
const fullPath = join8(ctx.projectRoot, filePath);
|
|
7093
8097
|
let content;
|
|
7094
8098
|
try {
|
|
7095
8099
|
content = readFileSync7(fullPath, "utf-8");
|
|
@@ -7163,38 +8167,46 @@ docCommand.command("stats").description("Show frontmatter statistics for the pro
|
|
|
7163
8167
|
// src/commands/daemon.ts
|
|
7164
8168
|
init_connection();
|
|
7165
8169
|
import { Command as Command20 } from "commander";
|
|
7166
|
-
import { existsSync as
|
|
7167
|
-
import { join as
|
|
8170
|
+
import { existsSync as existsSync9, writeFileSync as writeFileSync6, unlinkSync, readFileSync as readFileSync9, mkdirSync as mkdirSync5, statSync as statSync3, renameSync, readdirSync as readdirSync2 } from "fs";
|
|
8171
|
+
import { join as join11, dirname as dirname3 } from "path";
|
|
7168
8172
|
import { homedir as homedir2, platform } from "os";
|
|
7169
8173
|
import { spawn, execSync as execSync2 } from "child_process";
|
|
7170
8174
|
init_config();
|
|
7171
8175
|
|
|
8176
|
+
// src/services/daemon-manager.ts
|
|
8177
|
+
import { existsSync as existsSync8 } from "fs";
|
|
8178
|
+
import { join as join10 } from "path";
|
|
8179
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
8180
|
+
|
|
7172
8181
|
// src/services/file-watcher.ts
|
|
7173
8182
|
init_connection();
|
|
7174
8183
|
import { watch } from "chokidar";
|
|
7175
|
-
import {
|
|
7176
|
-
import {
|
|
8184
|
+
import { relative as relative3, extname as extname2, basename as basename4 } from "path";
|
|
8185
|
+
import { readFileSync as readFileSync8, statSync as statSync2 } from "fs";
|
|
7177
8186
|
import { EventEmitter } from "events";
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
"**/.git/**",
|
|
7182
|
-
"**/dist/**",
|
|
7183
|
-
"**/coverage/**",
|
|
7184
|
-
"**/.aigile/**"
|
|
7185
|
-
];
|
|
8187
|
+
init_monitoring_patterns();
|
|
8188
|
+
init_config();
|
|
8189
|
+
import picomatch2 from "picomatch";
|
|
7186
8190
|
var FileWatcher = class extends EventEmitter {
|
|
7187
8191
|
watcher = null;
|
|
7188
8192
|
config;
|
|
7189
8193
|
stats;
|
|
7190
8194
|
debounceTimers = /* @__PURE__ */ new Map();
|
|
8195
|
+
// Tri-state pattern matchers
|
|
8196
|
+
allowMatcher = null;
|
|
8197
|
+
denyMatcher = null;
|
|
8198
|
+
hardIgnoreMatcher = null;
|
|
7191
8199
|
constructor(config) {
|
|
7192
8200
|
super();
|
|
8201
|
+
const projectAllowPatterns = config.patterns ?? loadAllowPatterns(config.projectPath);
|
|
8202
|
+
const projectDenyPatterns = config.ignore ?? loadIgnorePatterns(config.projectPath);
|
|
7193
8203
|
this.config = {
|
|
7194
|
-
patterns:
|
|
7195
|
-
ignore:
|
|
7196
|
-
useGitignore:
|
|
8204
|
+
patterns: projectAllowPatterns.length > 0 ? projectAllowPatterns : getDefaultAllowPatterns(),
|
|
8205
|
+
ignore: projectDenyPatterns.length > 0 ? projectDenyPatterns : getDefaultDenyPatterns(),
|
|
8206
|
+
useGitignore: false,
|
|
8207
|
+
// Changed: don't use gitignore by default
|
|
7197
8208
|
debounceMs: 300,
|
|
8209
|
+
trackUnknown: true,
|
|
7198
8210
|
...config
|
|
7199
8211
|
};
|
|
7200
8212
|
this.stats = {
|
|
@@ -7202,8 +8214,37 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7202
8214
|
startedAt: null,
|
|
7203
8215
|
filesWatched: 0,
|
|
7204
8216
|
eventsProcessed: 0,
|
|
7205
|
-
lastEvent: null
|
|
8217
|
+
lastEvent: null,
|
|
8218
|
+
categoryCounts: {
|
|
8219
|
+
allow: 0,
|
|
8220
|
+
deny: 0,
|
|
8221
|
+
unknown: 0
|
|
8222
|
+
}
|
|
7206
8223
|
};
|
|
8224
|
+
this.initializeMatchers();
|
|
8225
|
+
}
|
|
8226
|
+
/**
|
|
8227
|
+
* Initialize picomatch matchers for tri-state classification
|
|
8228
|
+
*/
|
|
8229
|
+
initializeMatchers() {
|
|
8230
|
+
this.hardIgnoreMatcher = picomatch2(getHardIgnorePatterns());
|
|
8231
|
+
this.allowMatcher = picomatch2(this.config.patterns);
|
|
8232
|
+
this.denyMatcher = picomatch2(this.config.ignore);
|
|
8233
|
+
}
|
|
8234
|
+
/**
|
|
8235
|
+
* Classify a file into allow/deny/unknown category
|
|
8236
|
+
*/
|
|
8237
|
+
classifyFile(relativePath) {
|
|
8238
|
+
if (this.hardIgnoreMatcher && this.hardIgnoreMatcher(relativePath)) {
|
|
8239
|
+
return "deny";
|
|
8240
|
+
}
|
|
8241
|
+
if (this.allowMatcher && this.allowMatcher(relativePath)) {
|
|
8242
|
+
return "allow";
|
|
8243
|
+
}
|
|
8244
|
+
if (this.denyMatcher && this.denyMatcher(relativePath)) {
|
|
8245
|
+
return "deny";
|
|
8246
|
+
}
|
|
8247
|
+
return "unknown";
|
|
7207
8248
|
}
|
|
7208
8249
|
/**
|
|
7209
8250
|
* Start watching for file changes
|
|
@@ -7212,16 +8253,16 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7212
8253
|
if (this.watcher) {
|
|
7213
8254
|
return;
|
|
7214
8255
|
}
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
this.watcher = watch(watchPaths, {
|
|
7218
|
-
ignored: ignorePatterns,
|
|
8256
|
+
this.watcher = watch(this.config.projectPath, {
|
|
8257
|
+
ignored: getHardIgnorePatterns(),
|
|
7219
8258
|
persistent: true,
|
|
7220
8259
|
ignoreInitial: true,
|
|
7221
8260
|
awaitWriteFinish: {
|
|
7222
8261
|
stabilityThreshold: 200,
|
|
7223
8262
|
pollInterval: 100
|
|
7224
|
-
}
|
|
8263
|
+
},
|
|
8264
|
+
// Watch all directories but we'll filter files during processing
|
|
8265
|
+
depth: Infinity
|
|
7225
8266
|
});
|
|
7226
8267
|
this.watcher.on("add", (path) => this.handleFileEvent("add", path));
|
|
7227
8268
|
this.watcher.on("change", (path) => this.handleFileEvent("change", path));
|
|
@@ -7231,9 +8272,32 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7231
8272
|
this.stats.isRunning = true;
|
|
7232
8273
|
this.stats.startedAt = /* @__PURE__ */ new Date();
|
|
7233
8274
|
this.stats.filesWatched = this.getWatchedFilesCount();
|
|
8275
|
+
this.updateCategoryCounts();
|
|
7234
8276
|
this.emit("ready", this.stats);
|
|
7235
8277
|
});
|
|
7236
8278
|
}
|
|
8279
|
+
/**
|
|
8280
|
+
* Update category counts from database
|
|
8281
|
+
* This is best-effort - if DB isn't ready, we'll get counts later
|
|
8282
|
+
*/
|
|
8283
|
+
updateCategoryCounts() {
|
|
8284
|
+
try {
|
|
8285
|
+
const counts = queryAll(`
|
|
8286
|
+
SELECT monitoring_category, COUNT(*) as count
|
|
8287
|
+
FROM documents
|
|
8288
|
+
WHERE project_id = ? AND status != 'deleted'
|
|
8289
|
+
GROUP BY monitoring_category
|
|
8290
|
+
`, [this.config.projectId]);
|
|
8291
|
+
this.stats.categoryCounts = { allow: 0, deny: 0, unknown: 0 };
|
|
8292
|
+
for (const row of counts) {
|
|
8293
|
+
const cat = row.monitoring_category;
|
|
8294
|
+
if (cat in this.stats.categoryCounts) {
|
|
8295
|
+
this.stats.categoryCounts[cat] = row.count;
|
|
8296
|
+
}
|
|
8297
|
+
}
|
|
8298
|
+
} catch {
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
7237
8301
|
/**
|
|
7238
8302
|
* Stop watching for file changes
|
|
7239
8303
|
*/
|
|
@@ -7257,18 +8321,16 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7257
8321
|
return { ...this.stats };
|
|
7258
8322
|
}
|
|
7259
8323
|
/**
|
|
7260
|
-
*
|
|
8324
|
+
* Get the project path being watched
|
|
7261
8325
|
*/
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
}
|
|
7271
|
-
return patterns;
|
|
8326
|
+
getProjectPath() {
|
|
8327
|
+
return this.config.projectPath;
|
|
8328
|
+
}
|
|
8329
|
+
/**
|
|
8330
|
+
* Get the project ID
|
|
8331
|
+
*/
|
|
8332
|
+
getProjectId() {
|
|
8333
|
+
return this.config.projectId;
|
|
7272
8334
|
}
|
|
7273
8335
|
/**
|
|
7274
8336
|
* Handle a file event with debouncing
|
|
@@ -7289,20 +8351,28 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7289
8351
|
*/
|
|
7290
8352
|
processFileEvent(type, absolutePath) {
|
|
7291
8353
|
const relativePath = relative3(this.config.projectPath, absolutePath);
|
|
8354
|
+
const category = this.classifyFile(relativePath);
|
|
8355
|
+
if (category === "deny" && type !== "unlink") {
|
|
8356
|
+
return;
|
|
8357
|
+
}
|
|
8358
|
+
if (category === "unknown" && !this.config.trackUnknown && type !== "unlink") {
|
|
8359
|
+
return;
|
|
8360
|
+
}
|
|
7292
8361
|
const event = {
|
|
7293
8362
|
type,
|
|
7294
8363
|
path: relativePath,
|
|
7295
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
8364
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
8365
|
+
category
|
|
7296
8366
|
};
|
|
7297
8367
|
this.stats.lastEvent = event;
|
|
7298
8368
|
this.stats.eventsProcessed++;
|
|
7299
8369
|
try {
|
|
7300
8370
|
switch (type) {
|
|
7301
8371
|
case "add":
|
|
7302
|
-
this.syncFileAdd(absolutePath, relativePath);
|
|
8372
|
+
this.syncFileAdd(absolutePath, relativePath, category);
|
|
7303
8373
|
break;
|
|
7304
8374
|
case "change":
|
|
7305
|
-
this.syncFileChange(absolutePath, relativePath);
|
|
8375
|
+
this.syncFileChange(absolutePath, relativePath, category);
|
|
7306
8376
|
break;
|
|
7307
8377
|
case "unlink":
|
|
7308
8378
|
this.syncFileDelete(relativePath);
|
|
@@ -7316,16 +8386,18 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7316
8386
|
/**
|
|
7317
8387
|
* Sync a new file to the database
|
|
7318
8388
|
*/
|
|
7319
|
-
syncFileAdd(absolutePath, relativePath) {
|
|
8389
|
+
syncFileAdd(absolutePath, relativePath, category) {
|
|
7320
8390
|
const ext = extname2(relativePath).slice(1);
|
|
7321
|
-
const filename = relativePath
|
|
8391
|
+
const filename = basename4(relativePath);
|
|
8392
|
+
const isBinary = isBinaryExtension(ext);
|
|
7322
8393
|
try {
|
|
7323
8394
|
const stats = statSync2(absolutePath);
|
|
7324
|
-
const
|
|
8395
|
+
const shouldComputeHash = category === "allow" || !isBinary && stats.size < 10 * 1024 * 1024;
|
|
8396
|
+
const hash = shouldComputeHash ? computeFileHash(absolutePath) : null;
|
|
7325
8397
|
let hasFrontmatter = false;
|
|
7326
8398
|
let frontmatterRaw = null;
|
|
7327
8399
|
let metadata = null;
|
|
7328
|
-
if (ext === "md" || ext === "markdown") {
|
|
8400
|
+
if (category === "allow" && (ext === "md" || ext === "markdown") && !isBinary) {
|
|
7329
8401
|
const parsed = parseFrontmatterFromFile(absolutePath);
|
|
7330
8402
|
if (parsed) {
|
|
7331
8403
|
hasFrontmatter = true;
|
|
@@ -7338,18 +8410,23 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7338
8410
|
[this.config.projectId, relativePath]
|
|
7339
8411
|
);
|
|
7340
8412
|
if (existing) {
|
|
7341
|
-
this.updateDocument(existing.id, hash, stats.size, hasFrontmatter, frontmatterRaw, metadata);
|
|
8413
|
+
this.updateDocument(existing.id, hash, stats.size, hasFrontmatter, frontmatterRaw, metadata, category);
|
|
7342
8414
|
} else {
|
|
7343
|
-
this.insertDocument(relativePath, filename, ext, hash, stats.size, hasFrontmatter, frontmatterRaw, metadata);
|
|
8415
|
+
this.insertDocument(relativePath, filename, ext, hash, stats.size, hasFrontmatter, frontmatterRaw, metadata, category);
|
|
7344
8416
|
}
|
|
7345
|
-
} catch {
|
|
8417
|
+
} catch (err) {
|
|
8418
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
8419
|
+
if (errMsg.includes("Database") || errMsg.includes("database")) {
|
|
8420
|
+
throw err;
|
|
8421
|
+
}
|
|
8422
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] syncFileAdd error for ${relativePath}: ${errMsg}`);
|
|
7346
8423
|
}
|
|
7347
8424
|
}
|
|
7348
8425
|
/**
|
|
7349
8426
|
* Sync a changed file to the database
|
|
7350
8427
|
*/
|
|
7351
|
-
syncFileChange(absolutePath, relativePath) {
|
|
7352
|
-
this.syncFileAdd(absolutePath, relativePath);
|
|
8428
|
+
syncFileChange(absolutePath, relativePath, category) {
|
|
8429
|
+
this.syncFileAdd(absolutePath, relativePath, category);
|
|
7353
8430
|
}
|
|
7354
8431
|
/**
|
|
7355
8432
|
* Mark a file as deleted in the database
|
|
@@ -7369,14 +8446,16 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7369
8446
|
/**
|
|
7370
8447
|
* Insert a new document into the database
|
|
7371
8448
|
*/
|
|
7372
|
-
insertDocument(path, filename, extension, hash, size, hasFrontmatter, frontmatterRaw, metadata) {
|
|
8449
|
+
insertDocument(path, filename, extension, hash, size, hasFrontmatter, frontmatterRaw, metadata, category) {
|
|
7373
8450
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8451
|
+
const needsReview = category === "unknown" ? 1 : 0;
|
|
7374
8452
|
run(
|
|
7375
8453
|
`INSERT INTO documents (
|
|
7376
8454
|
id, project_id, path, filename, extension, content_hash, size_bytes, status, last_scanned_at,
|
|
7377
8455
|
has_frontmatter, frontmatter_raw, meta_status, meta_version, meta_tldr, meta_title,
|
|
7378
|
-
meta_modules, meta_dependencies, meta_code_refs, meta_authors
|
|
7379
|
-
|
|
8456
|
+
meta_modules, meta_dependencies, meta_code_refs, meta_authors,
|
|
8457
|
+
monitoring_category, needs_review
|
|
8458
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'tracked', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
7380
8459
|
[
|
|
7381
8460
|
generateId(),
|
|
7382
8461
|
this.config.projectId,
|
|
@@ -7395,20 +8474,23 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7395
8474
|
metadata?.modules ? JSON.stringify(metadata.modules) : null,
|
|
7396
8475
|
metadata?.dependencies ? JSON.stringify(metadata.dependencies) : null,
|
|
7397
8476
|
metadata?.code_refs ? JSON.stringify(metadata.code_refs) : null,
|
|
7398
|
-
metadata?.authors ? JSON.stringify(metadata.authors) : null
|
|
8477
|
+
metadata?.authors ? JSON.stringify(metadata.authors) : null,
|
|
8478
|
+
category,
|
|
8479
|
+
needsReview
|
|
7399
8480
|
]
|
|
7400
8481
|
);
|
|
7401
8482
|
}
|
|
7402
8483
|
/**
|
|
7403
8484
|
* Update an existing document in the database
|
|
7404
8485
|
*/
|
|
7405
|
-
updateDocument(id, hash, size, hasFrontmatter, frontmatterRaw, metadata) {
|
|
8486
|
+
updateDocument(id, hash, size, hasFrontmatter, frontmatterRaw, metadata, category) {
|
|
7406
8487
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7407
8488
|
run(
|
|
7408
8489
|
`UPDATE documents SET
|
|
7409
|
-
content_hash = ?, size_bytes = ?, status = 'tracked', last_scanned_at = ?, updated_at = datetime('now'),
|
|
8490
|
+
content_hash = COALESCE(?, content_hash), size_bytes = ?, status = 'tracked', last_scanned_at = ?, updated_at = datetime('now'),
|
|
7410
8491
|
has_frontmatter = ?, frontmatter_raw = ?, meta_status = ?, meta_version = ?, meta_tldr = ?, meta_title = ?,
|
|
7411
|
-
meta_modules = ?, meta_dependencies = ?, meta_code_refs = ?, meta_authors =
|
|
8492
|
+
meta_modules = ?, meta_dependencies = ?, meta_code_refs = ?, meta_authors = ?,
|
|
8493
|
+
monitoring_category = ?
|
|
7412
8494
|
WHERE id = ?`,
|
|
7413
8495
|
[
|
|
7414
8496
|
hash,
|
|
@@ -7424,6 +8506,7 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7424
8506
|
metadata?.dependencies ? JSON.stringify(metadata.dependencies) : null,
|
|
7425
8507
|
metadata?.code_refs ? JSON.stringify(metadata.code_refs) : null,
|
|
7426
8508
|
metadata?.authors ? JSON.stringify(metadata.authors) : null,
|
|
8509
|
+
category,
|
|
7427
8510
|
id
|
|
7428
8511
|
]
|
|
7429
8512
|
);
|
|
@@ -7443,65 +8526,275 @@ var FileWatcher = class extends EventEmitter {
|
|
|
7443
8526
|
return count;
|
|
7444
8527
|
}
|
|
7445
8528
|
};
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
8529
|
+
|
|
8530
|
+
// src/services/daemon-manager.ts
|
|
8531
|
+
init_connection();
|
|
8532
|
+
var MAX_WATCHER_RETRIES = 3;
|
|
8533
|
+
var INITIAL_RETRY_DELAY_MS = 5e3;
|
|
8534
|
+
var DaemonManager = class extends EventEmitter2 {
|
|
8535
|
+
watchers = /* @__PURE__ */ new Map();
|
|
8536
|
+
watcherRetries = /* @__PURE__ */ new Map();
|
|
8537
|
+
running = false;
|
|
8538
|
+
startedAt = null;
|
|
8539
|
+
/**
|
|
8540
|
+
* Start watching all registered projects
|
|
8541
|
+
*/
|
|
8542
|
+
async start() {
|
|
8543
|
+
if (this.running) {
|
|
8544
|
+
throw new Error("Daemon is already running");
|
|
8545
|
+
}
|
|
8546
|
+
const projects = await this.getActiveProjects();
|
|
8547
|
+
if (projects.length === 0) {
|
|
8548
|
+
console.log('No valid projects to watch. Register projects with "aigile init".');
|
|
8549
|
+
this.running = true;
|
|
8550
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
8551
|
+
return this.getStatus();
|
|
8552
|
+
}
|
|
8553
|
+
console.log(`Starting watchers for ${projects.length} project(s)...`);
|
|
8554
|
+
for (const project of projects) {
|
|
8555
|
+
await this.startWatcherWithRetry(project);
|
|
8556
|
+
}
|
|
8557
|
+
this.running = true;
|
|
8558
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
8559
|
+
this.emit("started", { projectCount: this.watchers.size });
|
|
8560
|
+
return this.getStatus();
|
|
8561
|
+
}
|
|
8562
|
+
/**
|
|
8563
|
+
* Start a watcher for a project with automatic retry on failure
|
|
8564
|
+
*/
|
|
8565
|
+
async startWatcherWithRetry(project) {
|
|
8566
|
+
const retryCount = this.watcherRetries.get(project.key) ?? 0;
|
|
8567
|
+
try {
|
|
8568
|
+
const config = {
|
|
8569
|
+
projectId: project.id,
|
|
8570
|
+
projectPath: project.path,
|
|
8571
|
+
trackUnknown: true
|
|
8572
|
+
};
|
|
8573
|
+
const watcher = new FileWatcher(config);
|
|
8574
|
+
watcher.on("sync", (event) => {
|
|
8575
|
+
this.emit("sync", { project: project.key, ...event });
|
|
8576
|
+
});
|
|
8577
|
+
watcher.on("syncError", (data2) => {
|
|
8578
|
+
this.emit("syncError", { project: project.key, ...data2 });
|
|
8579
|
+
});
|
|
8580
|
+
watcher.on("error", async (err) => {
|
|
8581
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Watcher error: ${err}`);
|
|
8582
|
+
this.emit("watcherError", { project: project.key, error: err });
|
|
8583
|
+
try {
|
|
8584
|
+
await watcher.stop();
|
|
8585
|
+
} catch {
|
|
8586
|
+
}
|
|
8587
|
+
this.watchers.delete(project.key);
|
|
8588
|
+
const currentRetries = this.watcherRetries.get(project.key) ?? 0;
|
|
8589
|
+
if (currentRetries < MAX_WATCHER_RETRIES) {
|
|
8590
|
+
this.watcherRetries.set(project.key, currentRetries + 1);
|
|
8591
|
+
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, currentRetries);
|
|
8592
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Will retry in ${delay}ms (attempt ${currentRetries + 1}/${MAX_WATCHER_RETRIES})`);
|
|
8593
|
+
setTimeout(async () => {
|
|
8594
|
+
if (this.running) {
|
|
8595
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Attempting restart...`);
|
|
8596
|
+
await this.startWatcherWithRetry(project);
|
|
8597
|
+
}
|
|
8598
|
+
}, delay);
|
|
8599
|
+
} else {
|
|
8600
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Max retries (${MAX_WATCHER_RETRIES}) exceeded - watcher disabled`);
|
|
8601
|
+
this.emit("watcherDisabled", { project: project.key });
|
|
8602
|
+
}
|
|
8603
|
+
});
|
|
8604
|
+
watcher.start();
|
|
8605
|
+
this.watchers.set(project.key, watcher);
|
|
8606
|
+
this.watcherRetries.set(project.key, 0);
|
|
8607
|
+
console.log(` \u2713 ${project.key}: ${project.path}`);
|
|
8608
|
+
} catch (error2) {
|
|
8609
|
+
console.error(` \u2717 ${project.key}: Failed to start watcher - ${error2}`);
|
|
8610
|
+
this.emit("watcherError", { project: project.key, error: error2 });
|
|
8611
|
+
if (retryCount < MAX_WATCHER_RETRIES) {
|
|
8612
|
+
this.watcherRetries.set(project.key, retryCount + 1);
|
|
8613
|
+
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
8614
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Will retry in ${delay}ms (attempt ${retryCount + 1}/${MAX_WATCHER_RETRIES})`);
|
|
8615
|
+
setTimeout(async () => {
|
|
8616
|
+
if (this.running) {
|
|
8617
|
+
await this.startWatcherWithRetry(project);
|
|
8618
|
+
}
|
|
8619
|
+
}, delay);
|
|
7461
8620
|
}
|
|
7462
|
-
|
|
7463
|
-
|
|
8621
|
+
}
|
|
8622
|
+
}
|
|
8623
|
+
/**
|
|
8624
|
+
* Stop all watchers
|
|
8625
|
+
*/
|
|
8626
|
+
async stop() {
|
|
8627
|
+
if (!this.running) {
|
|
8628
|
+
return;
|
|
8629
|
+
}
|
|
8630
|
+
console.log("Stopping all watchers...");
|
|
8631
|
+
for (const [key, watcher] of this.watchers) {
|
|
8632
|
+
try {
|
|
8633
|
+
await watcher.stop();
|
|
8634
|
+
console.log(` \u2713 Stopped: ${key}`);
|
|
8635
|
+
} catch (error2) {
|
|
8636
|
+
console.error(` \u2717 Error stopping ${key}: ${error2}`);
|
|
7464
8637
|
}
|
|
7465
|
-
|
|
7466
|
-
|
|
8638
|
+
}
|
|
8639
|
+
this.watchers.clear();
|
|
8640
|
+
this.running = false;
|
|
8641
|
+
this.startedAt = null;
|
|
8642
|
+
this.emit("stopped");
|
|
8643
|
+
}
|
|
8644
|
+
/**
|
|
8645
|
+
* Resync all projects
|
|
8646
|
+
*/
|
|
8647
|
+
async resyncAll() {
|
|
8648
|
+
const results = {};
|
|
8649
|
+
const projects = await this.getActiveProjects();
|
|
8650
|
+
console.log(`Resyncing ${projects.length} project(s)...`);
|
|
8651
|
+
for (const project of projects) {
|
|
8652
|
+
try {
|
|
8653
|
+
const files = scanDirectory(project.path);
|
|
8654
|
+
const syncResult = syncFilesToDatabase(project.id, project.path, files);
|
|
8655
|
+
const status = getSyncStatus(project.id);
|
|
8656
|
+
const result = {
|
|
8657
|
+
total: syncResult.total,
|
|
8658
|
+
new: syncResult.new,
|
|
8659
|
+
modified: syncResult.modified,
|
|
8660
|
+
deleted: syncResult.deleted,
|
|
8661
|
+
unchanged: syncResult.unchanged,
|
|
8662
|
+
allow: status.byCategory.allow,
|
|
8663
|
+
deny: status.byCategory.deny,
|
|
8664
|
+
unknown: status.byCategory.unknown
|
|
8665
|
+
};
|
|
8666
|
+
results[project.key] = result;
|
|
8667
|
+
console.log(` \u2713 ${project.key}: ${result.allow} allow, ${result.unknown} unknown`);
|
|
8668
|
+
} catch (error2) {
|
|
8669
|
+
console.error(` \u2717 ${project.key}: Resync failed - ${error2}`);
|
|
7467
8670
|
}
|
|
7468
|
-
patterns.push(pattern);
|
|
7469
8671
|
}
|
|
7470
|
-
return
|
|
7471
|
-
} catch {
|
|
7472
|
-
return [];
|
|
8672
|
+
return results;
|
|
7473
8673
|
}
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
8674
|
+
/**
|
|
8675
|
+
* Resync a specific project
|
|
8676
|
+
*/
|
|
8677
|
+
async resyncProject(key) {
|
|
8678
|
+
const projects = await this.getActiveProjects();
|
|
8679
|
+
const project = projects.find((p) => p.key === key);
|
|
8680
|
+
if (!project) {
|
|
8681
|
+
throw new Error(`Project "${key}" not found or invalid`);
|
|
8682
|
+
}
|
|
8683
|
+
const files = scanDirectory(project.path);
|
|
8684
|
+
const syncResult = syncFilesToDatabase(project.id, project.path, files);
|
|
8685
|
+
const status = getSyncStatus(project.id);
|
|
8686
|
+
return {
|
|
8687
|
+
total: syncResult.total,
|
|
8688
|
+
new: syncResult.new,
|
|
8689
|
+
modified: syncResult.modified,
|
|
8690
|
+
deleted: syncResult.deleted,
|
|
8691
|
+
unchanged: syncResult.unchanged,
|
|
8692
|
+
allow: status.byCategory.allow,
|
|
8693
|
+
deny: status.byCategory.deny,
|
|
8694
|
+
unknown: status.byCategory.unknown
|
|
8695
|
+
};
|
|
8696
|
+
}
|
|
8697
|
+
/**
|
|
8698
|
+
* Get daemon status
|
|
8699
|
+
*/
|
|
8700
|
+
getStatus() {
|
|
8701
|
+
const projectStatuses = [];
|
|
8702
|
+
const totalFiles = { allow: 0, deny: 0, unknown: 0 };
|
|
8703
|
+
const allProjects = queryAll("SELECT * FROM projects ORDER BY key");
|
|
8704
|
+
for (const project of allProjects) {
|
|
8705
|
+
const valid = this.isValidProject(project.path);
|
|
8706
|
+
const watcher = this.watchers.get(project.key);
|
|
8707
|
+
const stats = watcher?.getStats() ?? null;
|
|
8708
|
+
if (stats) {
|
|
8709
|
+
totalFiles.allow += stats.categoryCounts.allow;
|
|
8710
|
+
totalFiles.deny += stats.categoryCounts.deny;
|
|
8711
|
+
totalFiles.unknown += stats.categoryCounts.unknown;
|
|
8712
|
+
}
|
|
8713
|
+
projectStatuses.push({
|
|
8714
|
+
key: project.key,
|
|
8715
|
+
name: project.name,
|
|
8716
|
+
path: project.path,
|
|
8717
|
+
valid,
|
|
8718
|
+
watching: watcher !== void 0,
|
|
8719
|
+
stats
|
|
8720
|
+
});
|
|
8721
|
+
}
|
|
8722
|
+
return {
|
|
8723
|
+
running: this.running,
|
|
8724
|
+
projectCount: allProjects.length,
|
|
8725
|
+
watchingCount: this.watchers.size,
|
|
8726
|
+
projects: projectStatuses,
|
|
8727
|
+
totalFiles
|
|
8728
|
+
};
|
|
8729
|
+
}
|
|
8730
|
+
/**
|
|
8731
|
+
* Check if daemon is running
|
|
8732
|
+
*/
|
|
8733
|
+
isRunning() {
|
|
8734
|
+
return this.running;
|
|
8735
|
+
}
|
|
8736
|
+
/**
|
|
8737
|
+
* Get uptime in milliseconds
|
|
8738
|
+
*/
|
|
8739
|
+
getUptime() {
|
|
8740
|
+
if (!this.startedAt) {
|
|
8741
|
+
return 0;
|
|
8742
|
+
}
|
|
8743
|
+
return Date.now() - this.startedAt.getTime();
|
|
8744
|
+
}
|
|
8745
|
+
/**
|
|
8746
|
+
* Get all valid registered projects
|
|
8747
|
+
*/
|
|
8748
|
+
async getActiveProjects() {
|
|
8749
|
+
const projects = queryAll("SELECT * FROM projects ORDER BY key");
|
|
8750
|
+
return projects.filter((p) => this.isValidProject(p.path));
|
|
8751
|
+
}
|
|
8752
|
+
/**
|
|
8753
|
+
* Check if a project path is valid
|
|
8754
|
+
*/
|
|
8755
|
+
isValidProject(path) {
|
|
8756
|
+
return existsSync8(path) && existsSync8(join10(path, ".aigile"));
|
|
8757
|
+
}
|
|
8758
|
+
};
|
|
8759
|
+
var daemonManagerInstance = null;
|
|
8760
|
+
function getDaemonManager() {
|
|
8761
|
+
if (!daemonManagerInstance) {
|
|
8762
|
+
daemonManagerInstance = new DaemonManager();
|
|
8763
|
+
}
|
|
8764
|
+
return daemonManagerInstance;
|
|
8765
|
+
}
|
|
8766
|
+
|
|
7479
8767
|
// src/commands/daemon.ts
|
|
8768
|
+
var CRASH_DIR_NAME = "crashes";
|
|
8769
|
+
var MAX_CRASH_REPORTS = 10;
|
|
8770
|
+
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8771
|
+
var MAX_LOG_FILES = 5;
|
|
8772
|
+
var SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
7480
8773
|
var daemonCommand = new Command20("daemon").description("Manage the file watcher daemon");
|
|
7481
8774
|
var PLATFORM = platform();
|
|
7482
8775
|
var DAEMON_NAME = "com.aigile.watcher";
|
|
7483
8776
|
function getDaemonPaths() {
|
|
7484
8777
|
const aigileHome = getAigileHome();
|
|
7485
8778
|
const basePaths = {
|
|
7486
|
-
pidFile:
|
|
7487
|
-
logFile:
|
|
8779
|
+
pidFile: join11(aigileHome, "daemon.pid"),
|
|
8780
|
+
logFile: join11(aigileHome, "daemon.log")
|
|
7488
8781
|
};
|
|
7489
8782
|
if (PLATFORM === "darwin") {
|
|
7490
8783
|
return {
|
|
7491
8784
|
...basePaths,
|
|
7492
|
-
plist:
|
|
8785
|
+
plist: join11(homedir2(), "Library", "LaunchAgents", `${DAEMON_NAME}.plist`)
|
|
7493
8786
|
};
|
|
7494
8787
|
} else if (PLATFORM === "linux") {
|
|
7495
8788
|
return {
|
|
7496
8789
|
...basePaths,
|
|
7497
|
-
service:
|
|
8790
|
+
service: join11(homedir2(), ".config", "systemd", "user", `${DAEMON_NAME}.service`)
|
|
7498
8791
|
};
|
|
7499
8792
|
}
|
|
7500
8793
|
return basePaths;
|
|
7501
8794
|
}
|
|
7502
8795
|
function isDaemonRunning() {
|
|
7503
8796
|
const paths = getDaemonPaths();
|
|
7504
|
-
if (!
|
|
8797
|
+
if (!existsSync9(paths.pidFile)) {
|
|
7505
8798
|
return { running: false };
|
|
7506
8799
|
}
|
|
7507
8800
|
try {
|
|
@@ -7517,10 +8810,78 @@ function isDaemonRunning() {
|
|
|
7517
8810
|
return { running: false };
|
|
7518
8811
|
}
|
|
7519
8812
|
}
|
|
7520
|
-
function
|
|
8813
|
+
function writeCrashReport(error2) {
|
|
8814
|
+
try {
|
|
8815
|
+
const crashDir = join11(getAigileHome(), CRASH_DIR_NAME);
|
|
8816
|
+
if (!existsSync9(crashDir)) {
|
|
8817
|
+
mkdirSync5(crashDir, { recursive: true });
|
|
8818
|
+
}
|
|
8819
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
8820
|
+
const crashFile = join11(crashDir, `crash-${timestamp}.log`);
|
|
8821
|
+
const report = [
|
|
8822
|
+
`AIGILE Daemon Crash Report`,
|
|
8823
|
+
`==========================`,
|
|
8824
|
+
``,
|
|
8825
|
+
`Time: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
8826
|
+
`Node: ${process.version}`,
|
|
8827
|
+
`Platform: ${platform()}`,
|
|
8828
|
+
`PID: ${process.pid}`,
|
|
8829
|
+
``,
|
|
8830
|
+
`Error:`,
|
|
8831
|
+
error2 instanceof Error ? error2.stack || error2.message : String(error2)
|
|
8832
|
+
].join("\n");
|
|
8833
|
+
writeFileSync6(crashFile, report);
|
|
8834
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Crash report saved: ${crashFile}`);
|
|
8835
|
+
cleanupOldCrashReports(crashDir);
|
|
8836
|
+
} catch (writeErr) {
|
|
8837
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Failed to write crash report: ${writeErr}`);
|
|
8838
|
+
}
|
|
8839
|
+
}
|
|
8840
|
+
function cleanupOldCrashReports(crashDir) {
|
|
8841
|
+
try {
|
|
8842
|
+
const files = readdirSync2(crashDir).filter((f) => f.startsWith("crash-") && f.endsWith(".log")).map((f) => ({ name: f, path: join11(crashDir, f) })).sort((a, b) => b.name.localeCompare(a.name));
|
|
8843
|
+
for (let i = MAX_CRASH_REPORTS; i < files.length; i++) {
|
|
8844
|
+
try {
|
|
8845
|
+
unlinkSync(files[i].path);
|
|
8846
|
+
} catch {
|
|
8847
|
+
}
|
|
8848
|
+
}
|
|
8849
|
+
} catch {
|
|
8850
|
+
}
|
|
8851
|
+
}
|
|
8852
|
+
function rotateLogIfNeeded() {
|
|
8853
|
+
const paths = getDaemonPaths();
|
|
8854
|
+
const logPath = paths.logFile;
|
|
8855
|
+
try {
|
|
8856
|
+
if (!existsSync9(logPath)) return;
|
|
8857
|
+
const stats = statSync3(logPath);
|
|
8858
|
+
if (stats.size < MAX_LOG_SIZE) return;
|
|
8859
|
+
const timestamp = Date.now();
|
|
8860
|
+
const rotatedPath = `${logPath}.${timestamp}`;
|
|
8861
|
+
renameSync(logPath, rotatedPath);
|
|
8862
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Log rotated: ${rotatedPath}`);
|
|
8863
|
+
cleanupOldLogs(dirname3(logPath));
|
|
8864
|
+
} catch (err) {
|
|
8865
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Log rotation error: ${err}`);
|
|
8866
|
+
}
|
|
8867
|
+
}
|
|
8868
|
+
function cleanupOldLogs(logDir) {
|
|
8869
|
+
try {
|
|
8870
|
+
const files = readdirSync2(logDir).filter((f) => f.startsWith("daemon.log.")).map((f) => ({ name: f, path: join11(logDir, f) })).sort((a, b) => b.name.localeCompare(a.name));
|
|
8871
|
+
for (let i = MAX_LOG_FILES; i < files.length; i++) {
|
|
8872
|
+
try {
|
|
8873
|
+
unlinkSync(files[i].path);
|
|
8874
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Removed old log: ${files[i].name}`);
|
|
8875
|
+
} catch {
|
|
8876
|
+
}
|
|
8877
|
+
}
|
|
8878
|
+
} catch {
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8881
|
+
function generateLaunchAgentPlist() {
|
|
7521
8882
|
const paths = getDaemonPaths();
|
|
7522
8883
|
const nodePath = process.execPath;
|
|
7523
|
-
const aigilePath =
|
|
8884
|
+
const aigilePath = join11(dirname3(dirname3(import.meta.url.replace("file://", ""))), "bin", "aigile.js");
|
|
7524
8885
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
7525
8886
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7526
8887
|
<plist version="1.0">
|
|
@@ -7533,15 +8894,18 @@ function generateLaunchAgentPlist(projectPath) {
|
|
|
7533
8894
|
<string>${aigilePath}</string>
|
|
7534
8895
|
<string>daemon</string>
|
|
7535
8896
|
<string>run</string>
|
|
7536
|
-
<string>--project</string>
|
|
7537
|
-
<string>${projectPath}</string>
|
|
7538
8897
|
</array>
|
|
7539
8898
|
<key>WorkingDirectory</key>
|
|
7540
|
-
<string>${
|
|
8899
|
+
<string>${homedir2()}</string>
|
|
7541
8900
|
<key>RunAtLoad</key>
|
|
7542
8901
|
<true/>
|
|
7543
8902
|
<key>KeepAlive</key>
|
|
7544
|
-
<
|
|
8903
|
+
<dict>
|
|
8904
|
+
<key>SuccessfulExit</key>
|
|
8905
|
+
<false/>
|
|
8906
|
+
</dict>
|
|
8907
|
+
<key>ThrottleInterval</key>
|
|
8908
|
+
<integer>10</integer>
|
|
7545
8909
|
<key>StandardOutPath</key>
|
|
7546
8910
|
<string>${paths.logFile}</string>
|
|
7547
8911
|
<key>StandardErrorPath</key>
|
|
@@ -7549,56 +8913,55 @@ function generateLaunchAgentPlist(projectPath) {
|
|
|
7549
8913
|
</dict>
|
|
7550
8914
|
</plist>`;
|
|
7551
8915
|
}
|
|
7552
|
-
function generateSystemdService(
|
|
8916
|
+
function generateSystemdService() {
|
|
7553
8917
|
const paths = getDaemonPaths();
|
|
7554
8918
|
const nodePath = process.execPath;
|
|
7555
|
-
const aigilePath =
|
|
8919
|
+
const aigilePath = join11(dirname3(dirname3(import.meta.url.replace("file://", ""))), "bin", "aigile.js");
|
|
7556
8920
|
return `[Unit]
|
|
7557
8921
|
Description=AIGILE File Watcher Daemon
|
|
7558
8922
|
After=network.target
|
|
7559
8923
|
|
|
7560
8924
|
[Service]
|
|
7561
8925
|
Type=simple
|
|
7562
|
-
ExecStart=${nodePath} ${aigilePath} daemon run
|
|
7563
|
-
WorkingDirectory=${
|
|
7564
|
-
Restart=
|
|
7565
|
-
RestartSec=
|
|
8926
|
+
ExecStart=${nodePath} ${aigilePath} daemon run
|
|
8927
|
+
WorkingDirectory=${homedir2()}
|
|
8928
|
+
Restart=on-failure
|
|
8929
|
+
RestartSec=10
|
|
8930
|
+
StartLimitIntervalSec=300
|
|
8931
|
+
StartLimitBurst=5
|
|
7566
8932
|
StandardOutput=append:${paths.logFile}
|
|
7567
8933
|
StandardError=append:${paths.logFile}
|
|
7568
8934
|
|
|
7569
8935
|
[Install]
|
|
7570
8936
|
WantedBy=default.target`;
|
|
7571
8937
|
}
|
|
7572
|
-
daemonCommand.command("install").description("Install daemon to start automatically on system boot").action(() => {
|
|
8938
|
+
daemonCommand.command("install").description("Install daemon to start automatically on system boot (watches ALL registered projects)").action(() => {
|
|
7573
8939
|
const opts = getOutputOptions(daemonCommand);
|
|
7574
|
-
const projectRoot = findProjectRoot();
|
|
7575
|
-
if (!projectRoot) {
|
|
7576
|
-
error('Not in an AIGILE project. Run "aigile init" first.', opts);
|
|
7577
|
-
process.exit(1);
|
|
7578
|
-
}
|
|
7579
8940
|
const paths = getDaemonPaths();
|
|
7580
8941
|
if (PLATFORM === "darwin") {
|
|
7581
|
-
const plistDir =
|
|
7582
|
-
if (!
|
|
8942
|
+
const plistDir = dirname3(paths.plist);
|
|
8943
|
+
if (!existsSync9(plistDir)) {
|
|
7583
8944
|
mkdirSync5(plistDir, { recursive: true });
|
|
7584
8945
|
}
|
|
7585
|
-
const plistContent = generateLaunchAgentPlist(
|
|
8946
|
+
const plistContent = generateLaunchAgentPlist();
|
|
7586
8947
|
writeFileSync6(paths.plist, plistContent);
|
|
7587
8948
|
success("Installed macOS LaunchAgent", opts);
|
|
7588
8949
|
info(`Plist location: ${paths.plist}`, opts);
|
|
8950
|
+
info("Daemon will watch ALL registered projects", opts);
|
|
7589
8951
|
info('Run "aigile daemon start" to start the watcher', opts);
|
|
7590
8952
|
} else if (PLATFORM === "linux") {
|
|
7591
|
-
const serviceDir =
|
|
7592
|
-
if (!
|
|
8953
|
+
const serviceDir = dirname3(paths.service);
|
|
8954
|
+
if (!existsSync9(serviceDir)) {
|
|
7593
8955
|
mkdirSync5(serviceDir, { recursive: true });
|
|
7594
8956
|
}
|
|
7595
|
-
const serviceContent = generateSystemdService(
|
|
8957
|
+
const serviceContent = generateSystemdService();
|
|
7596
8958
|
writeFileSync6(paths.service, serviceContent);
|
|
7597
8959
|
try {
|
|
7598
8960
|
execSync2("systemctl --user daemon-reload");
|
|
7599
8961
|
execSync2(`systemctl --user enable ${DAEMON_NAME}`);
|
|
7600
8962
|
success("Installed and enabled systemd user service", opts);
|
|
7601
8963
|
info(`Service location: ${paths.service}`, opts);
|
|
8964
|
+
info("Daemon will watch ALL registered projects", opts);
|
|
7602
8965
|
info('Run "aigile daemon start" to start the watcher', opts);
|
|
7603
8966
|
} catch (err) {
|
|
7604
8967
|
warning("Service file created but could not enable. You may need to run:", opts);
|
|
@@ -7622,14 +8985,14 @@ daemonCommand.command("uninstall").description("Remove daemon from auto-start").
|
|
|
7622
8985
|
} catch {
|
|
7623
8986
|
}
|
|
7624
8987
|
}
|
|
7625
|
-
if (PLATFORM === "darwin" && paths.plist &&
|
|
8988
|
+
if (PLATFORM === "darwin" && paths.plist && existsSync9(paths.plist)) {
|
|
7626
8989
|
try {
|
|
7627
8990
|
execSync2(`launchctl unload ${paths.plist}`);
|
|
7628
8991
|
} catch {
|
|
7629
8992
|
}
|
|
7630
8993
|
unlinkSync(paths.plist);
|
|
7631
8994
|
success("Removed macOS LaunchAgent", opts);
|
|
7632
|
-
} else if (PLATFORM === "linux" && paths.service &&
|
|
8995
|
+
} else if (PLATFORM === "linux" && paths.service && existsSync9(paths.service)) {
|
|
7633
8996
|
try {
|
|
7634
8997
|
execSync2(`systemctl --user stop ${DAEMON_NAME}`);
|
|
7635
8998
|
execSync2(`systemctl --user disable ${DAEMON_NAME}`);
|
|
@@ -7644,11 +9007,11 @@ daemonCommand.command("uninstall").description("Remove daemon from auto-start").
|
|
|
7644
9007
|
} else {
|
|
7645
9008
|
info("No daemon installation found", opts);
|
|
7646
9009
|
}
|
|
7647
|
-
if (
|
|
9010
|
+
if (existsSync9(paths.pidFile)) {
|
|
7648
9011
|
unlinkSync(paths.pidFile);
|
|
7649
9012
|
}
|
|
7650
9013
|
});
|
|
7651
|
-
daemonCommand.command("start").description("Start the file watcher daemon").action(() => {
|
|
9014
|
+
daemonCommand.command("start").description("Start the file watcher daemon (watches ALL registered projects)").action(() => {
|
|
7652
9015
|
const opts = getOutputOptions(daemonCommand);
|
|
7653
9016
|
const paths = getDaemonPaths();
|
|
7654
9017
|
const status = isDaemonRunning();
|
|
@@ -7656,36 +9019,37 @@ daemonCommand.command("start").description("Start the file watcher daemon").acti
|
|
|
7656
9019
|
info(`Daemon already running (PID: ${status.pid})`, opts);
|
|
7657
9020
|
return;
|
|
7658
9021
|
}
|
|
7659
|
-
if (PLATFORM === "darwin" && paths.plist &&
|
|
9022
|
+
if (PLATFORM === "darwin" && paths.plist && existsSync9(paths.plist)) {
|
|
7660
9023
|
try {
|
|
7661
9024
|
execSync2(`launchctl load ${paths.plist}`);
|
|
7662
|
-
success("Started daemon via launchctl", opts);
|
|
9025
|
+
success("Started daemon via launchctl (watching all projects)", opts);
|
|
7663
9026
|
} catch (err) {
|
|
7664
9027
|
error("Failed to start daemon via launchctl", opts);
|
|
7665
9028
|
process.exit(1);
|
|
7666
9029
|
}
|
|
7667
|
-
} else if (PLATFORM === "linux" && paths.service &&
|
|
9030
|
+
} else if (PLATFORM === "linux" && paths.service && existsSync9(paths.service)) {
|
|
7668
9031
|
try {
|
|
7669
9032
|
execSync2(`systemctl --user start ${DAEMON_NAME}`);
|
|
7670
|
-
success("Started daemon via systemctl", opts);
|
|
9033
|
+
success("Started daemon via systemctl (watching all projects)", opts);
|
|
7671
9034
|
} catch (err) {
|
|
7672
9035
|
error("Failed to start daemon via systemctl", opts);
|
|
7673
9036
|
process.exit(1);
|
|
7674
9037
|
}
|
|
7675
9038
|
} else {
|
|
7676
|
-
const
|
|
7677
|
-
if (!projectRoot) {
|
|
7678
|
-
error('Not in an AIGILE project. Run "aigile init" first.', opts);
|
|
7679
|
-
process.exit(1);
|
|
7680
|
-
}
|
|
7681
|
-
const child = spawn(process.execPath, [process.argv[1], "daemon", "run", "--project", projectRoot], {
|
|
9039
|
+
const child = spawn(process.execPath, [process.argv[1], "daemon", "run"], {
|
|
7682
9040
|
detached: true,
|
|
7683
9041
|
stdio: "ignore"
|
|
7684
9042
|
});
|
|
7685
9043
|
child.unref();
|
|
7686
9044
|
if (child.pid) {
|
|
7687
|
-
|
|
7688
|
-
|
|
9045
|
+
try {
|
|
9046
|
+
process.kill(child.pid, 0);
|
|
9047
|
+
writeFileSync6(paths.pidFile, String(child.pid));
|
|
9048
|
+
success(`Started daemon (PID: ${child.pid}) - watching all projects`, opts);
|
|
9049
|
+
} catch {
|
|
9050
|
+
error("Failed to start daemon - process died immediately", opts);
|
|
9051
|
+
process.exit(1);
|
|
9052
|
+
}
|
|
7689
9053
|
} else {
|
|
7690
9054
|
error("Failed to start daemon", opts);
|
|
7691
9055
|
process.exit(1);
|
|
@@ -7695,14 +9059,14 @@ daemonCommand.command("start").description("Start the file watcher daemon").acti
|
|
|
7695
9059
|
daemonCommand.command("stop").description("Stop the file watcher daemon").action(() => {
|
|
7696
9060
|
const opts = getOutputOptions(daemonCommand);
|
|
7697
9061
|
const paths = getDaemonPaths();
|
|
7698
|
-
if (PLATFORM === "darwin" && paths.plist &&
|
|
9062
|
+
if (PLATFORM === "darwin" && paths.plist && existsSync9(paths.plist)) {
|
|
7699
9063
|
try {
|
|
7700
9064
|
execSync2(`launchctl unload ${paths.plist}`);
|
|
7701
9065
|
success("Stopped daemon via launchctl", opts);
|
|
7702
9066
|
} catch {
|
|
7703
9067
|
info("Daemon was not running", opts);
|
|
7704
9068
|
}
|
|
7705
|
-
} else if (PLATFORM === "linux" && paths.service &&
|
|
9069
|
+
} else if (PLATFORM === "linux" && paths.service && existsSync9(paths.service)) {
|
|
7706
9070
|
try {
|
|
7707
9071
|
execSync2(`systemctl --user stop ${DAEMON_NAME}`);
|
|
7708
9072
|
success("Stopped daemon via systemctl", opts);
|
|
@@ -7714,7 +9078,7 @@ daemonCommand.command("stop").description("Stop the file watcher daemon").action
|
|
|
7714
9078
|
if (status.running && status.pid) {
|
|
7715
9079
|
try {
|
|
7716
9080
|
process.kill(status.pid, "SIGTERM");
|
|
7717
|
-
if (
|
|
9081
|
+
if (existsSync9(paths.pidFile)) {
|
|
7718
9082
|
unlinkSync(paths.pidFile);
|
|
7719
9083
|
}
|
|
7720
9084
|
success(`Stopped daemon (PID: ${status.pid})`, opts);
|
|
@@ -7727,97 +9091,215 @@ daemonCommand.command("stop").description("Stop the file watcher daemon").action
|
|
|
7727
9091
|
}
|
|
7728
9092
|
}
|
|
7729
9093
|
});
|
|
7730
|
-
daemonCommand.command("status").description("Show daemon status").action(() => {
|
|
9094
|
+
daemonCommand.command("status").description("Show daemon status for ALL registered projects").action(() => {
|
|
7731
9095
|
const opts = getOutputOptions(daemonCommand);
|
|
7732
9096
|
const paths = getDaemonPaths();
|
|
7733
9097
|
const status = isDaemonRunning();
|
|
7734
|
-
const
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
9098
|
+
const projects = queryAll("SELECT id, key, name, path, is_default FROM projects ORDER BY is_default DESC, key");
|
|
9099
|
+
const projectStats = [];
|
|
9100
|
+
let totalFiles = { allow: 0, unknown: 0, total: 0 };
|
|
9101
|
+
for (const project of projects) {
|
|
9102
|
+
const valid = existsSync9(project.path) && existsSync9(join11(project.path, ".aigile"));
|
|
9103
|
+
const counts = queryAll(`
|
|
9104
|
+
SELECT COALESCE(monitoring_category, 'unknown') as monitoring_category, COUNT(*) as count
|
|
9105
|
+
FROM documents
|
|
9106
|
+
WHERE project_id = ? AND status != 'deleted'
|
|
9107
|
+
GROUP BY monitoring_category
|
|
9108
|
+
`, [project.id]);
|
|
9109
|
+
let allow = 0;
|
|
9110
|
+
let unknown = 0;
|
|
9111
|
+
for (const row of counts) {
|
|
9112
|
+
if (row.monitoring_category === "allow") allow = row.count;
|
|
9113
|
+
else if (row.monitoring_category === "unknown") unknown = row.count;
|
|
9114
|
+
}
|
|
9115
|
+
projectStats.push({
|
|
9116
|
+
key: project.key,
|
|
9117
|
+
name: project.name,
|
|
9118
|
+
path: project.path,
|
|
9119
|
+
valid,
|
|
9120
|
+
allow,
|
|
9121
|
+
unknown,
|
|
9122
|
+
total: allow + unknown
|
|
9123
|
+
});
|
|
9124
|
+
if (valid) {
|
|
9125
|
+
totalFiles.allow += allow;
|
|
9126
|
+
totalFiles.unknown += unknown;
|
|
9127
|
+
totalFiles.total += allow + unknown;
|
|
9128
|
+
}
|
|
7747
9129
|
}
|
|
9130
|
+
const validCount = projectStats.filter((p) => p.valid).length;
|
|
7748
9131
|
if (opts.json) {
|
|
7749
|
-
console.log(JSON.stringify({
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
9132
|
+
console.log(JSON.stringify({
|
|
9133
|
+
success: true,
|
|
9134
|
+
data: {
|
|
9135
|
+
running: status.running,
|
|
9136
|
+
pid: status.pid ?? null,
|
|
9137
|
+
platform: PLATFORM,
|
|
9138
|
+
installed: PLATFORM === "darwin" ? paths.plist && existsSync9(paths.plist) : PLATFORM === "linux" ? paths.service && existsSync9(paths.service) : false,
|
|
9139
|
+
projectCount: projects.length,
|
|
9140
|
+
validProjectCount: validCount,
|
|
9141
|
+
projects: projectStats,
|
|
9142
|
+
totalFiles,
|
|
9143
|
+
paths: {
|
|
9144
|
+
database: getDbPath(),
|
|
9145
|
+
pidFile: paths.pidFile,
|
|
9146
|
+
logFile: paths.logFile
|
|
9147
|
+
}
|
|
9148
|
+
}
|
|
9149
|
+
}));
|
|
9150
|
+
return;
|
|
7763
9151
|
}
|
|
7764
|
-
});
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
const
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
9152
|
+
console.log("\n\u{1F4CA} Daemon Status\n");
|
|
9153
|
+
console.log(`\u251C\u2500\u2500 Running: ${status.running ? "\u2705 Yes" : "\u274C No"}${status.pid ? ` (PID: ${status.pid})` : ""}`);
|
|
9154
|
+
console.log(`\u251C\u2500\u2500 Platform: ${PLATFORM}`);
|
|
9155
|
+
const installed = PLATFORM === "darwin" ? paths.plist && existsSync9(paths.plist) : PLATFORM === "linux" ? paths.service && existsSync9(paths.service) : false;
|
|
9156
|
+
console.log(`\u251C\u2500\u2500 Installed: ${installed ? "\u2705 Yes" : "\u274C No"}`);
|
|
9157
|
+
console.log(`\u251C\u2500\u2500 Projects: ${validCount}/${projects.length} valid`);
|
|
9158
|
+
if (projectStats.length > 0) {
|
|
9159
|
+
for (let i = 0; i < projectStats.length; i++) {
|
|
9160
|
+
const p = projectStats[i];
|
|
9161
|
+
const isLast = i === projectStats.length - 1;
|
|
9162
|
+
const prefix = isLast ? "\u2502 \u2514\u2500\u2500" : "\u2502 \u251C\u2500\u2500";
|
|
9163
|
+
const validStr = p.valid ? "\u2713" : "\u2717";
|
|
9164
|
+
console.log(`${prefix} ${validStr} ${p.key}: ${p.allow} allow, ${p.unknown} unknown`);
|
|
9165
|
+
}
|
|
7771
9166
|
}
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
9167
|
+
console.log("\u251C\u2500\u2500 Total Files:");
|
|
9168
|
+
console.log(`\u2502 \u251C\u2500\u2500 Allow (focus): ${totalFiles.allow}`);
|
|
9169
|
+
console.log(`\u2502 \u2514\u2500\u2500 Unknown (review): ${totalFiles.unknown}`);
|
|
9170
|
+
console.log("\u2514\u2500\u2500 System:");
|
|
9171
|
+
console.log(` \u251C\u2500\u2500 Database: ${getDbPath()}`);
|
|
9172
|
+
console.log(` \u251C\u2500\u2500 PID File: ${paths.pidFile}`);
|
|
9173
|
+
console.log(` \u2514\u2500\u2500 Log File: ${paths.logFile}`);
|
|
9174
|
+
console.log("");
|
|
9175
|
+
if (projectStats.some((p) => !p.valid)) {
|
|
9176
|
+
console.log('\u26A0\uFE0F Some projects have invalid paths. Run "aigile project cleanup" to remove them.\n');
|
|
7775
9177
|
}
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
9178
|
+
});
|
|
9179
|
+
function getRelativeTime(isoDate) {
|
|
9180
|
+
try {
|
|
9181
|
+
const date = new Date(isoDate);
|
|
9182
|
+
const now = /* @__PURE__ */ new Date();
|
|
9183
|
+
const diffMs = now.getTime() - date.getTime();
|
|
9184
|
+
const diffSec = Math.floor(diffMs / 1e3);
|
|
9185
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
9186
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
9187
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
9188
|
+
if (diffSec < 60) return "just now";
|
|
9189
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
9190
|
+
if (diffHour < 24) return `${diffHour}h ago`;
|
|
9191
|
+
return `${diffDay}d ago`;
|
|
9192
|
+
} catch {
|
|
9193
|
+
return isoDate;
|
|
7780
9194
|
}
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
9195
|
+
}
|
|
9196
|
+
daemonCommand.command("run").option("--skip-resync", "Skip initial resync on startup").description("Run the file watcher in foreground for ALL registered projects").action(async (options) => {
|
|
9197
|
+
const opts = getOutputOptions(daemonCommand);
|
|
9198
|
+
const paths = getDaemonPaths();
|
|
9199
|
+
process.on("uncaughtException", (err) => {
|
|
9200
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] FATAL: Uncaught exception:`);
|
|
9201
|
+
console.error(err.stack || err.message);
|
|
9202
|
+
writeCrashReport(err);
|
|
9203
|
+
if (existsSync9(paths.pidFile)) {
|
|
9204
|
+
try {
|
|
9205
|
+
unlinkSync(paths.pidFile);
|
|
9206
|
+
} catch {
|
|
9207
|
+
}
|
|
9208
|
+
}
|
|
7784
9209
|
process.exit(1);
|
|
7785
|
-
}
|
|
7786
|
-
writeFileSync6(paths.pidFile, String(process.pid));
|
|
7787
|
-
const watcher = createFileWatcher({
|
|
7788
|
-
projectId: project.id,
|
|
7789
|
-
projectPath: projectRoot,
|
|
7790
|
-
useGitignore: true
|
|
7791
9210
|
});
|
|
7792
|
-
|
|
7793
|
-
console.
|
|
9211
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
9212
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] FATAL: Unhandled promise rejection:`);
|
|
9213
|
+
console.error(reason);
|
|
9214
|
+
writeCrashReport(reason);
|
|
9215
|
+
if (existsSync9(paths.pidFile)) {
|
|
9216
|
+
try {
|
|
9217
|
+
unlinkSync(paths.pidFile);
|
|
9218
|
+
} catch {
|
|
9219
|
+
}
|
|
9220
|
+
}
|
|
9221
|
+
process.exit(1);
|
|
7794
9222
|
});
|
|
7795
|
-
|
|
7796
|
-
|
|
9223
|
+
rotateLogIfNeeded();
|
|
9224
|
+
writeFileSync6(paths.pidFile, String(process.pid));
|
|
9225
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting AIGILE daemon for all registered projects...`);
|
|
9226
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] PID: ${process.pid}, Node: ${process.version}, Platform: ${platform()}`);
|
|
9227
|
+
const manager = getDaemonManager();
|
|
9228
|
+
if (!options.skipResync) {
|
|
9229
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Performing initial resync for all projects...`);
|
|
9230
|
+
try {
|
|
9231
|
+
const results = await manager.resyncAll();
|
|
9232
|
+
const projectCount = Object.keys(results).length;
|
|
9233
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Resync complete: ${projectCount} projects synced`);
|
|
9234
|
+
} catch (err) {
|
|
9235
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Resync warning: ${err}`);
|
|
9236
|
+
}
|
|
9237
|
+
}
|
|
9238
|
+
manager.on("sync", (event) => {
|
|
9239
|
+
const categoryStr = event.category ? ` [${event.category}]` : "";
|
|
9240
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${event.project}] Synced: ${event.type} ${event.path}${categoryStr}`);
|
|
7797
9241
|
});
|
|
7798
|
-
|
|
7799
|
-
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Sync error: ${event.type} ${event.path} - ${err}`);
|
|
9242
|
+
manager.on("syncError", ({ project, event, error: err }) => {
|
|
9243
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project}] Sync error: ${event.type} ${event.path} - ${err}`);
|
|
7800
9244
|
});
|
|
7801
|
-
|
|
7802
|
-
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Watcher error: ${err}`);
|
|
9245
|
+
manager.on("watcherError", ({ project, error: err }) => {
|
|
9246
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project}] Watcher error: ${err}`);
|
|
7803
9247
|
});
|
|
9248
|
+
let isShuttingDown = false;
|
|
7804
9249
|
const shutdown = async () => {
|
|
9250
|
+
if (isShuttingDown) return;
|
|
9251
|
+
isShuttingDown = true;
|
|
7805
9252
|
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Shutting down...`);
|
|
7806
|
-
|
|
7807
|
-
|
|
9253
|
+
const forceExitTimeout = setTimeout(() => {
|
|
9254
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Shutdown timeout (${SHUTDOWN_TIMEOUT_MS}ms) - forcing exit`);
|
|
9255
|
+
if (existsSync9(paths.pidFile)) {
|
|
9256
|
+
try {
|
|
9257
|
+
unlinkSync(paths.pidFile);
|
|
9258
|
+
} catch {
|
|
9259
|
+
}
|
|
9260
|
+
}
|
|
9261
|
+
process.exit(1);
|
|
9262
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
9263
|
+
try {
|
|
9264
|
+
await manager.stop();
|
|
9265
|
+
} catch (err) {
|
|
9266
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Error during shutdown: ${err}`);
|
|
9267
|
+
}
|
|
9268
|
+
clearTimeout(forceExitTimeout);
|
|
9269
|
+
if (existsSync9(paths.pidFile)) {
|
|
7808
9270
|
unlinkSync(paths.pidFile);
|
|
7809
9271
|
}
|
|
9272
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Daemon stopped gracefully`);
|
|
7810
9273
|
process.exit(0);
|
|
7811
9274
|
};
|
|
7812
9275
|
process.on("SIGTERM", shutdown);
|
|
7813
9276
|
process.on("SIGINT", shutdown);
|
|
7814
|
-
|
|
7815
|
-
|
|
9277
|
+
const logRotationInterval = setInterval(() => {
|
|
9278
|
+
rotateLogIfNeeded();
|
|
9279
|
+
}, 60 * 60 * 1e3);
|
|
9280
|
+
process.on("exit", () => {
|
|
9281
|
+
clearInterval(logRotationInterval);
|
|
9282
|
+
});
|
|
9283
|
+
try {
|
|
9284
|
+
const status = await manager.start();
|
|
9285
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Daemon started - watching ${status.watchingCount} projects`);
|
|
9286
|
+
for (const p of status.projects) {
|
|
9287
|
+
if (p.watching) {
|
|
9288
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] \u2713 ${p.key}: ${p.path}`);
|
|
9289
|
+
}
|
|
9290
|
+
}
|
|
9291
|
+
} catch (err) {
|
|
9292
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Failed to start daemon: ${err}`);
|
|
9293
|
+
if (existsSync9(paths.pidFile)) {
|
|
9294
|
+
unlinkSync(paths.pidFile);
|
|
9295
|
+
}
|
|
9296
|
+
process.exit(1);
|
|
9297
|
+
}
|
|
7816
9298
|
});
|
|
7817
9299
|
daemonCommand.command("logs").option("-n, --lines <number>", "Number of lines to show", "50").option("-f, --follow", "Follow log output").description("Show daemon logs").action((options) => {
|
|
7818
9300
|
const opts = getOutputOptions(daemonCommand);
|
|
7819
9301
|
const paths = getDaemonPaths();
|
|
7820
|
-
if (!
|
|
9302
|
+
if (!existsSync9(paths.logFile)) {
|
|
7821
9303
|
info("No log file found. Daemon may not have run yet.", opts);
|
|
7822
9304
|
return;
|
|
7823
9305
|
}
|
|
@@ -7838,15 +9320,547 @@ daemonCommand.command("logs").option("-n, --lines <number>", "Number of lines to
|
|
|
7838
9320
|
}
|
|
7839
9321
|
}
|
|
7840
9322
|
});
|
|
9323
|
+
daemonCommand.command("resync").option("--project <key>", "Resync only a specific project").description("Perform a full resync of all files for all registered projects").action(async (options) => {
|
|
9324
|
+
const opts = getOutputOptions(daemonCommand);
|
|
9325
|
+
const manager = getDaemonManager();
|
|
9326
|
+
if (options.project) {
|
|
9327
|
+
info(`Resyncing project ${options.project}...`, opts);
|
|
9328
|
+
try {
|
|
9329
|
+
const result = await manager.resyncProject(options.project);
|
|
9330
|
+
if (!result) {
|
|
9331
|
+
error(`Project "${options.project}" not found or invalid.`, opts);
|
|
9332
|
+
process.exit(1);
|
|
9333
|
+
}
|
|
9334
|
+
if (opts.json) {
|
|
9335
|
+
console.log(JSON.stringify({
|
|
9336
|
+
success: true,
|
|
9337
|
+
data: {
|
|
9338
|
+
project: options.project,
|
|
9339
|
+
...result
|
|
9340
|
+
}
|
|
9341
|
+
}));
|
|
9342
|
+
} else {
|
|
9343
|
+
success(`Resync complete for ${options.project}:`, opts);
|
|
9344
|
+
console.log(` Allow: ${result.allow}`);
|
|
9345
|
+
console.log(` Deny: ${result.deny}`);
|
|
9346
|
+
console.log(` Unknown: ${result.unknown}`);
|
|
9347
|
+
}
|
|
9348
|
+
} catch (err) {
|
|
9349
|
+
error(`Resync failed: ${err}`, opts);
|
|
9350
|
+
process.exit(1);
|
|
9351
|
+
}
|
|
9352
|
+
return;
|
|
9353
|
+
}
|
|
9354
|
+
info("Resyncing all registered projects...", opts);
|
|
9355
|
+
info("This may take a while for large repositories.", opts);
|
|
9356
|
+
try {
|
|
9357
|
+
const results = await manager.resyncAll();
|
|
9358
|
+
const projectKeys = Object.keys(results);
|
|
9359
|
+
if (opts.json) {
|
|
9360
|
+
console.log(JSON.stringify({
|
|
9361
|
+
success: true,
|
|
9362
|
+
data: {
|
|
9363
|
+
projectCount: projectKeys.length,
|
|
9364
|
+
projects: results
|
|
9365
|
+
}
|
|
9366
|
+
}));
|
|
9367
|
+
} else {
|
|
9368
|
+
success(`Resync complete: ${projectKeys.length} projects`, opts);
|
|
9369
|
+
for (const [key, result] of Object.entries(results)) {
|
|
9370
|
+
console.log(` ${key}: ${result.allow} allow, ${result.unknown} unknown`);
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
} catch (err) {
|
|
9374
|
+
error(`Resync failed: ${err}`, opts);
|
|
9375
|
+
process.exit(1);
|
|
9376
|
+
}
|
|
9377
|
+
});
|
|
9378
|
+
daemonCommand.command("review").option("--list", "Just list unknown files without interactive review").option("--auto", "Auto-suggest category based on extension").description("Review and classify unknown files").action((options) => {
|
|
9379
|
+
const opts = getOutputOptions(daemonCommand);
|
|
9380
|
+
const projectRoot = findProjectRoot();
|
|
9381
|
+
if (!projectRoot) {
|
|
9382
|
+
error('Not in an AIGILE project. Run "aigile init" first.', opts);
|
|
9383
|
+
process.exit(1);
|
|
9384
|
+
}
|
|
9385
|
+
const config = loadProjectConfig(projectRoot);
|
|
9386
|
+
if (!config) {
|
|
9387
|
+
error("Could not load project config.", opts);
|
|
9388
|
+
process.exit(1);
|
|
9389
|
+
}
|
|
9390
|
+
const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
|
|
9391
|
+
if (!project) {
|
|
9392
|
+
error(`Project "${config.project.key}" not found in database.`, opts);
|
|
9393
|
+
process.exit(1);
|
|
9394
|
+
}
|
|
9395
|
+
const unknownFiles = queryAll(`
|
|
9396
|
+
SELECT id, path, extension, size_bytes, updated_at
|
|
9397
|
+
FROM documents
|
|
9398
|
+
WHERE project_id = ? AND monitoring_category = 'unknown' AND status != 'deleted'
|
|
9399
|
+
ORDER BY path
|
|
9400
|
+
`, [project.id]);
|
|
9401
|
+
if (unknownFiles.length === 0) {
|
|
9402
|
+
success("No unknown files to review! All files are classified.", opts);
|
|
9403
|
+
return;
|
|
9404
|
+
}
|
|
9405
|
+
if (opts.json) {
|
|
9406
|
+
console.log(JSON.stringify({
|
|
9407
|
+
success: true,
|
|
9408
|
+
data: {
|
|
9409
|
+
count: unknownFiles.length,
|
|
9410
|
+
files: unknownFiles
|
|
9411
|
+
}
|
|
9412
|
+
}));
|
|
9413
|
+
return;
|
|
9414
|
+
}
|
|
9415
|
+
if (options.list) {
|
|
9416
|
+
console.log(`
|
|
9417
|
+
\u{1F4CB} Unknown Files (${unknownFiles.length} total)
|
|
9418
|
+
`);
|
|
9419
|
+
data(
|
|
9420
|
+
unknownFiles.map((f) => ({
|
|
9421
|
+
path: f.path,
|
|
9422
|
+
extension: f.extension || "-",
|
|
9423
|
+
size: formatBytes(f.size_bytes),
|
|
9424
|
+
updated: getRelativeTime(f.updated_at)
|
|
9425
|
+
})),
|
|
9426
|
+
[
|
|
9427
|
+
{ header: "Path", key: "path", width: 60 },
|
|
9428
|
+
{ header: "Ext", key: "extension", width: 8 },
|
|
9429
|
+
{ header: "Size", key: "size", width: 10 },
|
|
9430
|
+
{ header: "Updated", key: "updated", width: 12 }
|
|
9431
|
+
],
|
|
9432
|
+
opts
|
|
9433
|
+
);
|
|
9434
|
+
console.log('\nUse "aigile daemon allow <pattern>" or "aigile daemon deny <pattern>" to classify files.');
|
|
9435
|
+
return;
|
|
9436
|
+
}
|
|
9437
|
+
console.log(`
|
|
9438
|
+
\u{1F4CB} Unknown Files (${unknownFiles.length} total)
|
|
9439
|
+
`);
|
|
9440
|
+
console.log('Run "aigile daemon review --list" to see all files.');
|
|
9441
|
+
console.log('Use "aigile daemon allow <pattern>" or "aigile daemon deny <pattern>" to classify.\n');
|
|
9442
|
+
const sample = unknownFiles.slice(0, 10);
|
|
9443
|
+
for (const file of sample) {
|
|
9444
|
+
console.log(` ${file.path} (.${file.extension || "no ext"})`);
|
|
9445
|
+
}
|
|
9446
|
+
if (unknownFiles.length > 10) {
|
|
9447
|
+
console.log(` ... and ${unknownFiles.length - 10} more files`);
|
|
9448
|
+
}
|
|
9449
|
+
});
|
|
9450
|
+
function formatBytes(bytes) {
|
|
9451
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
9452
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
9453
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
9454
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
|
9455
|
+
}
|
|
9456
|
+
|
|
9457
|
+
// src/commands/file.ts
|
|
9458
|
+
init_connection();
|
|
9459
|
+
import { Command as Command21 } from "commander";
|
|
9460
|
+
import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
|
|
9461
|
+
import { join as join12, relative as relative4 } from "path";
|
|
9462
|
+
init_config();
|
|
9463
|
+
var fileCommand = new Command21("file").description("Shadow mode file analysis and management for brownfield projects");
|
|
9464
|
+
function getProjectContext(opts) {
|
|
9465
|
+
const projectRoot = findProjectRoot();
|
|
9466
|
+
if (!projectRoot) {
|
|
9467
|
+
error('Not in an AIGILE project. Run "aigile init" first.', opts);
|
|
9468
|
+
return null;
|
|
9469
|
+
}
|
|
9470
|
+
const config = loadProjectConfig(projectRoot);
|
|
9471
|
+
if (!config) {
|
|
9472
|
+
error("Could not load project config.", opts);
|
|
9473
|
+
return null;
|
|
9474
|
+
}
|
|
9475
|
+
const project = queryOne(
|
|
9476
|
+
"SELECT id FROM projects WHERE key = ?",
|
|
9477
|
+
[config.project.key]
|
|
9478
|
+
);
|
|
9479
|
+
if (!project) {
|
|
9480
|
+
error(`Project "${config.project.key}" not found in database.`, opts);
|
|
9481
|
+
return null;
|
|
9482
|
+
}
|
|
9483
|
+
return { projectId: project.id, projectRoot, projectKey: config.project.key };
|
|
9484
|
+
}
|
|
9485
|
+
function formatDocForTable(doc) {
|
|
9486
|
+
return {
|
|
9487
|
+
path: doc.path,
|
|
9488
|
+
module: doc.inferred_module ?? "-",
|
|
9489
|
+
type: doc.file_type ?? doc.extension ?? "-",
|
|
9490
|
+
analyzed: doc.analyzed_at ? "Yes" : "No",
|
|
9491
|
+
confidence: doc.analysis_confidence ?? "-",
|
|
9492
|
+
tldr: doc.meta_tldr ? doc.meta_tldr.length > 40 ? doc.meta_tldr.slice(0, 37) + "..." : doc.meta_tldr : "-"
|
|
9493
|
+
};
|
|
9494
|
+
}
|
|
9495
|
+
fileCommand.command("analyze <path>").option("--tldr <tldr>", "One-line summary of file purpose").option("--module <module>", "Module this file belongs to").option("--component <component>", "Component within module").option("--type <type>", "File type: component|service|util|config|test|doc|style|data").option("--deps <deps>", "Dependencies (comma-separated)").option("--exports <exports>", "Exported functions/classes (comma-separated)").option("--complexity <n>", "Complexity score (1-10)", parseInt).option("--confidence <n>", "AI confidence in analysis (0-100)", parseInt).option("--notes <notes>", "Additional analysis notes").description("Add analysis metadata to a tracked file (shadow mode)").action((filePath, options) => {
|
|
9496
|
+
const opts = getOutputOptions(fileCommand);
|
|
9497
|
+
const ctx = getProjectContext(opts);
|
|
9498
|
+
if (!ctx) {
|
|
9499
|
+
process.exit(1);
|
|
9500
|
+
}
|
|
9501
|
+
const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
|
|
9502
|
+
const doc = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
|
|
9503
|
+
if (!doc) {
|
|
9504
|
+
const tracked = trackShadowFile(ctx.projectId, ctx.projectRoot, normalizedPath);
|
|
9505
|
+
if (!tracked) {
|
|
9506
|
+
error(`File not tracked. Run "aigile sync scan" or track with "aigile file track ${normalizedPath}"`, opts);
|
|
9507
|
+
process.exit(1);
|
|
9508
|
+
}
|
|
9509
|
+
}
|
|
9510
|
+
const analysis = {};
|
|
9511
|
+
if (options.tldr) analysis.tldr = options.tldr;
|
|
9512
|
+
if (options.module) analysis.module = options.module;
|
|
9513
|
+
if (options.component) analysis.component = options.component;
|
|
9514
|
+
if (options.type) analysis.fileType = options.type;
|
|
9515
|
+
if (options.deps) analysis.dependencies = options.deps.split(",").map((s) => s.trim());
|
|
9516
|
+
if (options.exports) analysis.exports = options.exports.split(",").map((s) => s.trim());
|
|
9517
|
+
if (options.complexity) analysis.complexity = options.complexity;
|
|
9518
|
+
if (options.confidence) analysis.confidence = options.confidence;
|
|
9519
|
+
if (options.notes) analysis.notes = options.notes;
|
|
9520
|
+
if (Object.keys(analysis).length === 0) {
|
|
9521
|
+
error("No analysis options provided. Use --tldr, --module, --type, etc.", opts);
|
|
9522
|
+
process.exit(1);
|
|
9523
|
+
}
|
|
9524
|
+
const updated = updateDocumentAnalysis(ctx.projectId, normalizedPath, analysis);
|
|
9525
|
+
if (!updated) {
|
|
9526
|
+
error(`Failed to update analysis for: ${normalizedPath}`, opts);
|
|
9527
|
+
process.exit(1);
|
|
9528
|
+
}
|
|
9529
|
+
const updatedDoc = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
|
|
9530
|
+
if (opts.json) {
|
|
9531
|
+
console.log(JSON.stringify({
|
|
9532
|
+
success: true,
|
|
9533
|
+
data: {
|
|
9534
|
+
path: normalizedPath,
|
|
9535
|
+
analyzed: true,
|
|
9536
|
+
analyzedAt: updatedDoc?.analyzed_at,
|
|
9537
|
+
metadata: {
|
|
9538
|
+
tldr: updatedDoc?.meta_tldr,
|
|
9539
|
+
module: updatedDoc?.inferred_module,
|
|
9540
|
+
component: updatedDoc?.inferred_component,
|
|
9541
|
+
type: updatedDoc?.file_type,
|
|
9542
|
+
dependencies: updatedDoc?.meta_dependencies ? JSON.parse(updatedDoc.meta_dependencies) : null,
|
|
9543
|
+
exports: updatedDoc?.exports ? JSON.parse(updatedDoc.exports) : null,
|
|
9544
|
+
complexity: updatedDoc?.complexity_score,
|
|
9545
|
+
confidence: updatedDoc?.analysis_confidence
|
|
9546
|
+
}
|
|
9547
|
+
}
|
|
9548
|
+
}));
|
|
9549
|
+
} else {
|
|
9550
|
+
success(`Analysis added for: ${normalizedPath}`, opts);
|
|
9551
|
+
if (updatedDoc?.meta_tldr) {
|
|
9552
|
+
info(`TLDR: ${updatedDoc.meta_tldr}`, opts);
|
|
9553
|
+
}
|
|
9554
|
+
}
|
|
9555
|
+
});
|
|
9556
|
+
fileCommand.command("list").alias("ls").option("--unanalyzed", "Only files without analysis").option("--analyzed", "Only files with analysis").option("--low-confidence [threshold]", "Files with low confidence (default: <70)", "70").option("--module <module>", "Filter by inferred module").option("--type <type>", "Filter by file type").option("--shadow", "Only shadow mode files").option("--count", "Return count only").option("--limit <n>", "Limit results", "100").option("--offset <n>", "Pagination offset", "0").option("--format <format>", "Output format: table|paths|json", "table").description("List tracked files with analysis filters").action((options) => {
|
|
9557
|
+
const opts = getOutputOptions(fileCommand);
|
|
9558
|
+
const ctx = getProjectContext(opts);
|
|
9559
|
+
if (!ctx) {
|
|
9560
|
+
process.exit(1);
|
|
9561
|
+
}
|
|
9562
|
+
const limit = parseInt(options.limit, 10);
|
|
9563
|
+
const offset = parseInt(options.offset, 10);
|
|
9564
|
+
if (options.count) {
|
|
9565
|
+
let count = 0;
|
|
9566
|
+
if (options.unanalyzed) {
|
|
9567
|
+
count = getUnanalyzedCount(ctx.projectId);
|
|
9568
|
+
} else {
|
|
9569
|
+
const docs = options.unanalyzed ? getUnanalyzedDocuments(ctx.projectId) : getAnalyzedDocuments(ctx.projectId);
|
|
9570
|
+
count = docs.length;
|
|
9571
|
+
}
|
|
9572
|
+
if (opts.json) {
|
|
9573
|
+
console.log(JSON.stringify({ success: true, data: { count } }));
|
|
9574
|
+
} else {
|
|
9575
|
+
console.log(count);
|
|
9576
|
+
}
|
|
9577
|
+
return;
|
|
9578
|
+
}
|
|
9579
|
+
let documents = [];
|
|
9580
|
+
if (options.unanalyzed) {
|
|
9581
|
+
documents = getUnanalyzedDocuments(ctx.projectId, limit, offset);
|
|
9582
|
+
} else if (options.analyzed) {
|
|
9583
|
+
documents = getAnalyzedDocuments(ctx.projectId, limit);
|
|
9584
|
+
} else if (options.lowConfidence) {
|
|
9585
|
+
const threshold = parseInt(options.lowConfidence, 10) || 70;
|
|
9586
|
+
documents = getLowConfidenceDocuments(ctx.projectId, threshold);
|
|
9587
|
+
} else if (options.module) {
|
|
9588
|
+
documents = getDocumentsByInferredModule(ctx.projectId, options.module);
|
|
9589
|
+
} else if (options.type) {
|
|
9590
|
+
documents = getDocumentsByFileType(ctx.projectId, options.type);
|
|
9591
|
+
} else if (options.shadow) {
|
|
9592
|
+
documents = getShadowDocuments(ctx.projectId);
|
|
9593
|
+
} else {
|
|
9594
|
+
documents = getUnanalyzedDocuments(ctx.projectId, limit, offset);
|
|
9595
|
+
}
|
|
9596
|
+
if (opts.json || options.format === "json") {
|
|
9597
|
+
console.log(JSON.stringify({
|
|
9598
|
+
success: true,
|
|
9599
|
+
data: documents.map((d) => ({
|
|
9600
|
+
path: d.path,
|
|
9601
|
+
filename: d.filename,
|
|
9602
|
+
extension: d.extension,
|
|
9603
|
+
module: d.inferred_module,
|
|
9604
|
+
component: d.inferred_component,
|
|
9605
|
+
fileType: d.file_type,
|
|
9606
|
+
analyzed: !!d.analyzed_at,
|
|
9607
|
+
analyzedAt: d.analyzed_at,
|
|
9608
|
+
confidence: d.analysis_confidence,
|
|
9609
|
+
tldr: d.meta_tldr,
|
|
9610
|
+
dependencies: d.meta_dependencies ? JSON.parse(d.meta_dependencies) : null,
|
|
9611
|
+
exports: d.exports ? JSON.parse(d.exports) : null,
|
|
9612
|
+
complexity: d.complexity_score
|
|
9613
|
+
}))
|
|
9614
|
+
}));
|
|
9615
|
+
} else if (options.format === "paths") {
|
|
9616
|
+
documents.forEach((d) => console.log(d.path));
|
|
9617
|
+
} else {
|
|
9618
|
+
const displayDocs = documents.map(formatDocForTable);
|
|
9619
|
+
data(
|
|
9620
|
+
displayDocs,
|
|
9621
|
+
[
|
|
9622
|
+
{ header: "Path", key: "path", width: 50 },
|
|
9623
|
+
{ header: "Module", key: "module", width: 15 },
|
|
9624
|
+
{ header: "Type", key: "type", width: 10 },
|
|
9625
|
+
{ header: "Analyzed", key: "analyzed", width: 8 },
|
|
9626
|
+
{ header: "Conf", key: "confidence", width: 5 }
|
|
9627
|
+
],
|
|
9628
|
+
opts
|
|
9629
|
+
);
|
|
9630
|
+
info(`Showing ${documents.length} files`, opts);
|
|
9631
|
+
}
|
|
9632
|
+
});
|
|
9633
|
+
fileCommand.command("progress").description("Show analysis progress statistics").action(() => {
|
|
9634
|
+
const opts = getOutputOptions(fileCommand);
|
|
9635
|
+
const ctx = getProjectContext(opts);
|
|
9636
|
+
if (!ctx) {
|
|
9637
|
+
process.exit(1);
|
|
9638
|
+
}
|
|
9639
|
+
const progress = getAnalysisProgress(ctx.projectId);
|
|
9640
|
+
if (opts.json) {
|
|
9641
|
+
console.log(JSON.stringify({ success: true, data: progress }));
|
|
9642
|
+
} else {
|
|
9643
|
+
const analyzedPct = progress.total > 0 ? Math.round(progress.analyzed / progress.total * 100) : 0;
|
|
9644
|
+
console.log("\n\u{1F4CA} Analysis Progress\n");
|
|
9645
|
+
console.log(`Total files: ${progress.total}`);
|
|
9646
|
+
console.log(`Analyzed: ${progress.analyzed} (${analyzedPct}%)`);
|
|
9647
|
+
console.log(`Unanalyzed: ${progress.unanalyzed} (${100 - analyzedPct}%)`);
|
|
9648
|
+
console.log(`Low confidence: ${progress.lowConfidence}`);
|
|
9649
|
+
const moduleEntries = Object.entries(progress.byModule);
|
|
9650
|
+
if (moduleEntries.length > 0) {
|
|
9651
|
+
console.log("\nBy module:");
|
|
9652
|
+
for (const [module, stats] of moduleEntries) {
|
|
9653
|
+
const pct = stats.total > 0 ? Math.round(stats.analyzed / stats.total * 100) : 0;
|
|
9654
|
+
console.log(` ${module}: ${stats.analyzed}/${stats.total} (${pct}%)`);
|
|
9655
|
+
}
|
|
9656
|
+
}
|
|
9657
|
+
const typeEntries = Object.entries(progress.byFileType);
|
|
9658
|
+
if (typeEntries.length > 0) {
|
|
9659
|
+
console.log("\nBy file type:");
|
|
9660
|
+
for (const [type, count] of typeEntries.slice(0, 10)) {
|
|
9661
|
+
console.log(` ${type}: ${count}`);
|
|
9662
|
+
}
|
|
9663
|
+
}
|
|
9664
|
+
console.log("");
|
|
9665
|
+
}
|
|
9666
|
+
});
|
|
9667
|
+
fileCommand.command("read <path>").option("--with-metadata", "Include existing DB metadata").option("--line-numbers", "Include line numbers").option("--limit <n>", "Limit number of lines", parseInt).description("Read file content (for AI agent analysis)").action((filePath, options) => {
|
|
9668
|
+
const opts = getOutputOptions(fileCommand);
|
|
9669
|
+
const ctx = getProjectContext(opts);
|
|
9670
|
+
if (!ctx) {
|
|
9671
|
+
process.exit(1);
|
|
9672
|
+
}
|
|
9673
|
+
const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
|
|
9674
|
+
const fullPath = join12(ctx.projectRoot, normalizedPath);
|
|
9675
|
+
if (!existsSync10(fullPath)) {
|
|
9676
|
+
error(`File not found: ${normalizedPath}`, opts);
|
|
9677
|
+
process.exit(1);
|
|
9678
|
+
}
|
|
9679
|
+
let content;
|
|
9680
|
+
try {
|
|
9681
|
+
content = readFileSync10(fullPath, "utf-8");
|
|
9682
|
+
} catch {
|
|
9683
|
+
error(`Could not read file: ${normalizedPath}`, opts);
|
|
9684
|
+
process.exit(1);
|
|
9685
|
+
}
|
|
9686
|
+
let metadata;
|
|
9687
|
+
if (options.withMetadata) {
|
|
9688
|
+
metadata = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
|
|
9689
|
+
}
|
|
9690
|
+
let lines = content.split("\n");
|
|
9691
|
+
if (options.limit && options.limit > 0) {
|
|
9692
|
+
lines = lines.slice(0, options.limit);
|
|
9693
|
+
}
|
|
9694
|
+
if (opts.json) {
|
|
9695
|
+
console.log(JSON.stringify({
|
|
9696
|
+
success: true,
|
|
9697
|
+
data: {
|
|
9698
|
+
path: normalizedPath,
|
|
9699
|
+
content: lines.join("\n"),
|
|
9700
|
+
lineCount: lines.length,
|
|
9701
|
+
metadata: metadata ? {
|
|
9702
|
+
tldr: metadata.meta_tldr,
|
|
9703
|
+
module: metadata.inferred_module,
|
|
9704
|
+
component: metadata.inferred_component,
|
|
9705
|
+
fileType: metadata.file_type,
|
|
9706
|
+
analyzed: !!metadata.analyzed_at,
|
|
9707
|
+
confidence: metadata.analysis_confidence
|
|
9708
|
+
} : null
|
|
9709
|
+
}
|
|
9710
|
+
}));
|
|
9711
|
+
} else {
|
|
9712
|
+
if (options.withMetadata && metadata) {
|
|
9713
|
+
console.log(`# File: ${normalizedPath}`);
|
|
9714
|
+
if (metadata.meta_tldr) console.log(`# TLDR: ${metadata.meta_tldr}`);
|
|
9715
|
+
if (metadata.inferred_module) console.log(`# Module: ${metadata.inferred_module}`);
|
|
9716
|
+
if (metadata.file_type) console.log(`# Type: ${metadata.file_type}`);
|
|
9717
|
+
console.log("");
|
|
9718
|
+
}
|
|
9719
|
+
if (options.lineNumbers) {
|
|
9720
|
+
lines.forEach((line, i) => {
|
|
9721
|
+
console.log(`${String(i + 1).padStart(4)} | ${line}`);
|
|
9722
|
+
});
|
|
9723
|
+
} else {
|
|
9724
|
+
console.log(lines.join("\n"));
|
|
9725
|
+
}
|
|
9726
|
+
}
|
|
9727
|
+
});
|
|
9728
|
+
fileCommand.command("track <path>").option("--type <type>", "File type classification").option("--module <module>", "Module assignment").option("--notes <notes>", "Initial notes").description("Track a file in shadow mode (brownfield)").action((filePath, options) => {
|
|
9729
|
+
const opts = getOutputOptions(fileCommand);
|
|
9730
|
+
const ctx = getProjectContext(opts);
|
|
9731
|
+
if (!ctx) {
|
|
9732
|
+
process.exit(1);
|
|
9733
|
+
}
|
|
9734
|
+
const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
|
|
9735
|
+
const tracked = trackShadowFile(ctx.projectId, ctx.projectRoot, normalizedPath);
|
|
9736
|
+
if (!tracked) {
|
|
9737
|
+
error(`Could not track file: ${normalizedPath}`, opts);
|
|
9738
|
+
process.exit(1);
|
|
9739
|
+
}
|
|
9740
|
+
if (options.type || options.module || options.notes) {
|
|
9741
|
+
const analysis = {};
|
|
9742
|
+
if (options.type) analysis.fileType = options.type;
|
|
9743
|
+
if (options.module) analysis.module = options.module;
|
|
9744
|
+
if (options.notes) analysis.notes = options.notes;
|
|
9745
|
+
updateDocumentAnalysis(ctx.projectId, normalizedPath, analysis);
|
|
9746
|
+
}
|
|
9747
|
+
const doc = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
|
|
9748
|
+
if (opts.json) {
|
|
9749
|
+
console.log(JSON.stringify({
|
|
9750
|
+
success: true,
|
|
9751
|
+
data: {
|
|
9752
|
+
path: normalizedPath,
|
|
9753
|
+
tracked: true,
|
|
9754
|
+
shadowMode: true,
|
|
9755
|
+
metadata: doc ? {
|
|
9756
|
+
fileType: doc.file_type,
|
|
9757
|
+
module: doc.inferred_module
|
|
9758
|
+
} : null
|
|
9759
|
+
}
|
|
9760
|
+
}));
|
|
9761
|
+
} else {
|
|
9762
|
+
success(`Tracked in shadow mode: ${normalizedPath}`, opts);
|
|
9763
|
+
}
|
|
9764
|
+
});
|
|
9765
|
+
fileCommand.command("show <path>").description("Show detailed file analysis").action((filePath) => {
|
|
9766
|
+
const opts = getOutputOptions(fileCommand);
|
|
9767
|
+
const ctx = getProjectContext(opts);
|
|
9768
|
+
if (!ctx) {
|
|
9769
|
+
process.exit(1);
|
|
9770
|
+
}
|
|
9771
|
+
const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
|
|
9772
|
+
const doc = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
|
|
9773
|
+
if (!doc) {
|
|
9774
|
+
error(`File not tracked: ${normalizedPath}`, opts);
|
|
9775
|
+
process.exit(1);
|
|
9776
|
+
}
|
|
9777
|
+
if (opts.json) {
|
|
9778
|
+
console.log(JSON.stringify({
|
|
9779
|
+
success: true,
|
|
9780
|
+
data: {
|
|
9781
|
+
path: doc.path,
|
|
9782
|
+
filename: doc.filename,
|
|
9783
|
+
extension: doc.extension,
|
|
9784
|
+
status: doc.status,
|
|
9785
|
+
sizeBytes: doc.size_bytes,
|
|
9786
|
+
lastScanned: doc.last_scanned_at,
|
|
9787
|
+
hasFrontmatter: !!doc.has_frontmatter,
|
|
9788
|
+
shadowMode: !!doc.shadow_mode,
|
|
9789
|
+
analysis: {
|
|
9790
|
+
analyzed: !!doc.analyzed_at,
|
|
9791
|
+
analyzedAt: doc.analyzed_at,
|
|
9792
|
+
confidence: doc.analysis_confidence,
|
|
9793
|
+
tldr: doc.meta_tldr,
|
|
9794
|
+
module: doc.inferred_module,
|
|
9795
|
+
component: doc.inferred_component,
|
|
9796
|
+
fileType: doc.file_type,
|
|
9797
|
+
complexity: doc.complexity_score,
|
|
9798
|
+
dependencies: doc.meta_dependencies ? JSON.parse(doc.meta_dependencies) : null,
|
|
9799
|
+
exports: doc.exports ? JSON.parse(doc.exports) : null,
|
|
9800
|
+
notes: doc.analysis_notes
|
|
9801
|
+
}
|
|
9802
|
+
}
|
|
9803
|
+
}));
|
|
9804
|
+
} else {
|
|
9805
|
+
const displayData = {
|
|
9806
|
+
path: doc.path,
|
|
9807
|
+
filename: doc.filename,
|
|
9808
|
+
extension: doc.extension,
|
|
9809
|
+
status: doc.status,
|
|
9810
|
+
size_bytes: doc.size_bytes,
|
|
9811
|
+
last_scanned: doc.last_scanned_at,
|
|
9812
|
+
shadow_mode: doc.shadow_mode ? "Yes" : "No",
|
|
9813
|
+
analyzed: doc.analyzed_at ? "Yes" : "No",
|
|
9814
|
+
analyzed_at: doc.analyzed_at ?? "-",
|
|
9815
|
+
confidence: doc.analysis_confidence ?? "-",
|
|
9816
|
+
tldr: doc.meta_tldr ?? "-",
|
|
9817
|
+
module: doc.inferred_module ?? "-",
|
|
9818
|
+
component: doc.inferred_component ?? "-",
|
|
9819
|
+
file_type: doc.file_type ?? "-",
|
|
9820
|
+
complexity: doc.complexity_score ?? "-",
|
|
9821
|
+
dependencies: doc.meta_dependencies ? JSON.parse(doc.meta_dependencies).join(", ") : "-",
|
|
9822
|
+
exports: doc.exports ? JSON.parse(doc.exports).join(", ") : "-",
|
|
9823
|
+
notes: doc.analysis_notes ?? "-"
|
|
9824
|
+
};
|
|
9825
|
+
details(
|
|
9826
|
+
displayData,
|
|
9827
|
+
[
|
|
9828
|
+
{ label: "Path", key: "path" },
|
|
9829
|
+
{ label: "Filename", key: "filename" },
|
|
9830
|
+
{ label: "Extension", key: "extension" },
|
|
9831
|
+
{ label: "Status", key: "status" },
|
|
9832
|
+
{ label: "Size (bytes)", key: "size_bytes" },
|
|
9833
|
+
{ label: "Last Scanned", key: "last_scanned" },
|
|
9834
|
+
{ label: "Shadow Mode", key: "shadow_mode" },
|
|
9835
|
+
{ label: "Analyzed", key: "analyzed" },
|
|
9836
|
+
{ label: "Analyzed At", key: "analyzed_at" },
|
|
9837
|
+
{ label: "Confidence", key: "confidence" },
|
|
9838
|
+
{ label: "TLDR", key: "tldr" },
|
|
9839
|
+
{ label: "Module", key: "module" },
|
|
9840
|
+
{ label: "Component", key: "component" },
|
|
9841
|
+
{ label: "File Type", key: "file_type" },
|
|
9842
|
+
{ label: "Complexity", key: "complexity" },
|
|
9843
|
+
{ label: "Dependencies", key: "dependencies" },
|
|
9844
|
+
{ label: "Exports", key: "exports" },
|
|
9845
|
+
{ label: "Notes", key: "notes" }
|
|
9846
|
+
],
|
|
9847
|
+
opts
|
|
9848
|
+
);
|
|
9849
|
+
}
|
|
9850
|
+
});
|
|
7841
9851
|
|
|
7842
9852
|
// src/bin/aigile.ts
|
|
7843
9853
|
async function main() {
|
|
7844
|
-
const program = new
|
|
9854
|
+
const program = new Command22();
|
|
7845
9855
|
program.name("aigile").description("JIRA-compatible Agile project management CLI for AI-assisted development").version(VERSION, "-v, --version", "Display version number").option("--json", "Output in JSON format for machine parsing").option("--no-color", "Disable colored output");
|
|
7846
9856
|
program.hook("preAction", async () => {
|
|
7847
9857
|
await initDatabase();
|
|
7848
9858
|
});
|
|
7849
9859
|
program.hook("postAction", () => {
|
|
9860
|
+
const args = process.argv.slice(2);
|
|
9861
|
+
if (args[0] === "daemon" && args[1] === "run") {
|
|
9862
|
+
return;
|
|
9863
|
+
}
|
|
7850
9864
|
closeDatabase();
|
|
7851
9865
|
});
|
|
7852
9866
|
program.addCommand(initCommand);
|
|
@@ -7869,6 +9883,7 @@ async function main() {
|
|
|
7869
9883
|
program.addCommand(uxJourneyCommand);
|
|
7870
9884
|
program.addCommand(docCommand);
|
|
7871
9885
|
program.addCommand(daemonCommand);
|
|
9886
|
+
program.addCommand(fileCommand);
|
|
7872
9887
|
await program.parseAsync(process.argv);
|
|
7873
9888
|
}
|
|
7874
9889
|
main().catch((err) => {
|