@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.
@@ -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
- const data2 = db.export();
125
- const buffer = Buffer.from(data2);
126
- writeFileSync2(dbPath, buffer);
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 crypto.randomUUID();
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 Command21 } from "commander";
893
+ import { Command as Command22 } from "commander";
510
894
 
511
895
  // src/index.ts
512
- var VERSION = "0.1.0";
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
- key: p.is_default ? `${p.key} *` : p.key,
1673
- name: p.name,
1674
- path: p.path
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
- if (!opts.json) {
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
- console.log(" * = default project");
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("SELECT id FROM projects WHERE key = ?", [key]);
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>", "Start date (YYYY-MM-DD)").requiredOption("--end <date>", "End date (YYYY-MM-DD)").option("-g, --goal <goal>", "Sprint goal").description("Create a new sprint").action((name, options) => {
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, options.start, options.end]
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: options.start, end: options.end } }));
3177
+ console.log(JSON.stringify({ success: true, data: { name, start: startDate, end: endDate } }));
2580
3178
  } else {
2581
- success(`Created sprint "${name}" (${options.start} - ${options.end})`, opts);
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>", "Start date (YYYY-MM-DD)").option("--target <date>", "Target date (YYYY-MM-DD)").description("Create a new initiative").action((summary, options) => {
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
- options.start ?? null,
2847
- options.target ?? null
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>", "New start date").option("--target <date>", "New target date").description("Update initiative").action((key, options) => {
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("SELECT id FROM initiatives WHERE key = ?", [key]);
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
- if (options.start) {
2948
- updates.push("start_date = ?");
2949
- params.push(options.start);
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
- if (options.target) {
2952
- updates.push("target_date = ?");
2953
- params.push(options.target);
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 join6 } from "path";
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 existsSync5 } from "fs";
3001
- import { join as join5, relative as relative2, extname } from "path";
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
- var DEFAULT_PATTERNS = ["**/*.md", "**/*.feature", "**/*.yaml", "**/*.yml"];
3132
- var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "coverage", ".aigile"];
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
- function shouldIgnore(relativePath, ignorePatterns) {
3138
- for (const pattern of ignorePatterns) {
3139
- if (relativePath.includes(pattern) || relativePath.startsWith(pattern)) {
3140
- return true;
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
- return false;
3144
- }
3145
- function matchesPattern(filename, patterns) {
3146
- const ext = extname(filename).toLowerCase();
3147
- for (const pattern of patterns) {
3148
- if (pattern.includes("*.")) {
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 (pattern === filename) {
3155
- return true;
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
- return false;
3159
- }
3160
- function collectFiles(dir, rootDir, patterns, ignore, files = []) {
3161
- if (!existsSync5(dir)) {
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 = join5(dir, entry.name);
3808
+ const fullPath = join6(dir, entry.name);
3167
3809
  const relativePath = relative2(rootDir, fullPath);
3168
- if (shouldIgnore(relativePath, ignore)) {
3810
+ if (classifier.isHardIgnored(relativePath)) {
3169
3811
  continue;
3170
3812
  }
3171
3813
  if (entry.isDirectory()) {
3172
- collectFiles(fullPath, rootDir, patterns, ignore, files);
3173
- } else if (entry.isFile() && matchesPattern(entry.name, patterns)) {
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 hash = computeFileHash(fullPath);
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
- const ext = extname(entry.name).toLowerCase();
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.slice(1),
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 patterns = options.patterns ?? DEFAULT_PATTERNS;
3207
- const ignore = options.ignore ?? DEFAULT_IGNORE;
3208
- return collectFiles(projectPath, projectPath, patterns, ignore);
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
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'tracked', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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
- // src/services/comment-parser.ts
3422
- init_connection();
3423
- import { readFileSync as readFileSync6 } from "fs";
3424
- var USER_COMMENT_PATTERN = /\[\[!\s*([\s\S]*?)\s*\]\]/g;
3425
- var AI_COMMENT_PATTERN = /\[\{!\s*([\s\S]*?)\s*\}]/g;
3426
- function parseComments(filePath) {
3427
- const content = readFileSync6(filePath, "utf-8");
3428
- const lines = content.split("\n");
3429
- const comments = [];
3430
- const positionToLine = [];
3431
- let pos = 0;
3432
- for (let lineNum = 0; lineNum < lines.length; lineNum++) {
3433
- const lineLength = lines[lineNum].length + 1;
3434
- for (let i = 0; i < lineLength; i++) {
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
- let match;
3439
- USER_COMMENT_PATTERN.lastIndex = 0;
3440
- while ((match = USER_COMMENT_PATTERN.exec(content)) !== null) {
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
- AI_COMMENT_PATTERN.lastIndex = 0;
3449
- while ((match = AI_COMMENT_PATTERN.exec(content)) !== null) {
3450
- comments.push({
3451
- type: "ai",
3452
- content: match[1].trim(),
3453
- lineNumber: positionToLine[match.index] ?? 1,
3454
- raw: match[0]
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
- comments.sort((a, b) => a.lineNumber - b.lineNumber);
3458
- return comments;
4146
+ return queryAll(query, params);
3459
4147
  }
3460
- function syncCommentsToDatabase(documentId, comments) {
3461
- const result = {
3462
- total: comments.length,
3463
- new: 0,
3464
- resolved: 0
3465
- };
3466
- const existingComments = queryAll(
3467
- "SELECT id, marker_type, line_number, content FROM doc_comments WHERE document_id = ? AND resolved = 0",
3468
- [documentId]
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
- const processedIds = /* @__PURE__ */ new Set();
3471
- for (const comment of comments) {
3472
- const markerType = comment.type === "user" ? "user" : "ai";
3473
- const existing = existingComments.find(
3474
- (e) => e.marker_type === markerType && e.content === comment.content
3475
- );
3476
- if (existing) {
3477
- processedIds.add(existing.id);
3478
- if (existing.line_number !== comment.lineNumber) {
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 = join6(projectRoot, file.path);
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>", "Start date (YYYY-MM-DD)").option("--release <date>", "Release date (YYYY-MM-DD)").description("Create a new version").action((name, options) => {
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, options.start ?? null, options.release ?? 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>", "Start date (YYYY-MM-DD)").option("--release <date>", "Release date (YYYY-MM-DD)").option("--rename <newName>", "Rename version").description("Update a version").action((name, options) => {
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("SELECT id FROM versions WHERE project_id = ? AND name = ?", [project.id, name]);
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
- if (options.start !== void 0) {
6310
- updates.push("start_date = ?");
6311
- params.push(options.start);
6312
- changes.start_date = options.start;
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
- if (options.release !== void 0) {
6315
- updates.push("release_date = ?");
6316
- params.push(options.release);
6317
- changes.release_date = options.release;
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 join7 } from "path";
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 = join7(ctx.projectRoot, filePath);
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 = join7(ctx.projectRoot, filePath);
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 = join7(ctx.projectRoot, filePath);
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 existsSync7, writeFileSync as writeFileSync6, unlinkSync, readFileSync as readFileSync9, mkdirSync as mkdirSync5 } from "fs";
7167
- import { join as join9, dirname as dirname4 } from "path";
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 { join as join8, relative as relative3, extname as extname2 } from "path";
7176
- import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
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
- var DEFAULT_PATTERNS2 = ["**/*.md", "**/*.feature", "**/*.yaml", "**/*.yml"];
7179
- var DEFAULT_IGNORE2 = [
7180
- "**/node_modules/**",
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: DEFAULT_PATTERNS2,
7195
- ignore: DEFAULT_IGNORE2,
7196
- useGitignore: true,
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
- const watchPaths = this.config.patterns.map((p) => join8(this.config.projectPath, p));
7216
- const ignorePatterns = this.buildIgnorePatterns();
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
- * Build ignore patterns including gitignore if enabled
8324
+ * Get the project path being watched
7261
8325
  */
7262
- buildIgnorePatterns() {
7263
- const patterns = [...this.config.ignore || []];
7264
- if (this.config.useGitignore) {
7265
- const gitignorePath = join8(this.config.projectPath, ".gitignore");
7266
- if (existsSync6(gitignorePath)) {
7267
- const gitignorePatterns = parseGitignore(gitignorePath);
7268
- patterns.push(...gitignorePatterns);
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.split("/").pop() || relativePath;
8391
+ const filename = basename4(relativePath);
8392
+ const isBinary = isBinaryExtension(ext);
7322
8393
  try {
7323
8394
  const stats = statSync2(absolutePath);
7324
- const hash = computeFileHash(absolutePath);
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
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'tracked', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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
- function parseGitignore(gitignorePath) {
7447
- try {
7448
- const content = readFileSync8(gitignorePath, "utf-8");
7449
- const patterns = [];
7450
- for (let line of content.split("\n")) {
7451
- line = line.trim();
7452
- if (!line || line.startsWith("#")) {
7453
- continue;
7454
- }
7455
- if (line.startsWith("!")) {
7456
- continue;
7457
- }
7458
- let pattern = line;
7459
- if (pattern.startsWith("/")) {
7460
- pattern = pattern.slice(1);
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
- if (!pattern.startsWith("**/") && !pattern.includes("/")) {
7463
- pattern = `**/${pattern}`;
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
- if (pattern.endsWith("/")) {
7466
- pattern = pattern.slice(0, -1) + "/**";
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 patterns;
7471
- } catch {
7472
- return [];
8672
+ return results;
7473
8673
  }
7474
- }
7475
- function createFileWatcher(config) {
7476
- return new FileWatcher(config);
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: join9(aigileHome, "daemon.pid"),
7487
- logFile: join9(aigileHome, "daemon.log")
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: join9(homedir2(), "Library", "LaunchAgents", `${DAEMON_NAME}.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: join9(homedir2(), ".config", "systemd", "user", `${DAEMON_NAME}.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 (!existsSync7(paths.pidFile)) {
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 generateLaunchAgentPlist(projectPath) {
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 = join9(dirname4(dirname4(import.meta.url.replace("file://", ""))), "bin", "aigile.js");
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>${projectPath}</string>
8899
+ <string>${homedir2()}</string>
7541
8900
  <key>RunAtLoad</key>
7542
8901
  <true/>
7543
8902
  <key>KeepAlive</key>
7544
- <true/>
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(projectPath) {
8916
+ function generateSystemdService() {
7553
8917
  const paths = getDaemonPaths();
7554
8918
  const nodePath = process.execPath;
7555
- const aigilePath = join9(dirname4(dirname4(import.meta.url.replace("file://", ""))), "bin", "aigile.js");
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 --project ${projectPath}
7563
- WorkingDirectory=${projectPath}
7564
- Restart=always
7565
- RestartSec=5
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 = dirname4(paths.plist);
7582
- if (!existsSync7(plistDir)) {
8942
+ const plistDir = dirname3(paths.plist);
8943
+ if (!existsSync9(plistDir)) {
7583
8944
  mkdirSync5(plistDir, { recursive: true });
7584
8945
  }
7585
- const plistContent = generateLaunchAgentPlist(projectRoot);
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 = dirname4(paths.service);
7592
- if (!existsSync7(serviceDir)) {
8953
+ const serviceDir = dirname3(paths.service);
8954
+ if (!existsSync9(serviceDir)) {
7593
8955
  mkdirSync5(serviceDir, { recursive: true });
7594
8956
  }
7595
- const serviceContent = generateSystemdService(projectRoot);
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 && existsSync7(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 && existsSync7(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 (existsSync7(paths.pidFile)) {
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 && existsSync7(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 && existsSync7(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 projectRoot = findProjectRoot();
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
- writeFileSync6(paths.pidFile, String(child.pid));
7688
- success(`Started daemon (PID: ${child.pid})`, opts);
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 && existsSync7(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 && existsSync7(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 (existsSync7(paths.pidFile)) {
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 statusInfo = {
7735
- running: status.running ? "Yes" : "No",
7736
- pid: status.pid ?? "-",
7737
- platform: PLATFORM,
7738
- pid_file: paths.pidFile,
7739
- log_file: paths.logFile
7740
- };
7741
- if (PLATFORM === "darwin") {
7742
- statusInfo.plist = paths.plist ?? "-";
7743
- statusInfo.installed = paths.plist && existsSync7(paths.plist) ? "Yes" : "No";
7744
- } else if (PLATFORM === "linux") {
7745
- statusInfo.service = paths.service ?? "-";
7746
- statusInfo.installed = paths.service && existsSync7(paths.service) ? "Yes" : "No";
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({ success: true, data: statusInfo }));
7750
- } else {
7751
- details(
7752
- statusInfo,
7753
- [
7754
- { label: "Running", key: "running" },
7755
- { label: "PID", key: "pid" },
7756
- { label: "Platform", key: "platform" },
7757
- { label: "Installed", key: "installed" },
7758
- { label: "PID File", key: "pid_file" },
7759
- { label: "Log File", key: "log_file" }
7760
- ],
7761
- opts
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
- daemonCommand.command("run").option("--project <path>", "Project path to watch").description("Run the file watcher in foreground (used by daemon)").action(async (options) => {
7766
- const opts = getOutputOptions(daemonCommand);
7767
- const paths = getDaemonPaths();
7768
- let projectRoot = options.project;
7769
- if (!projectRoot) {
7770
- projectRoot = findProjectRoot();
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
- if (!projectRoot) {
7773
- error('Not in an AIGILE project. Run "aigile init" first.', opts);
7774
- process.exit(1);
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
- const config = loadProjectConfig(projectRoot);
7777
- if (!config) {
7778
- error("Could not load project config.", opts);
7779
- process.exit(1);
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
- const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
7782
- if (!project) {
7783
- error(`Project "${config.project.key}" not found in database.`, opts);
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
- watcher.on("ready", (stats) => {
7793
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Watcher ready - watching ${stats.filesWatched} files`);
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
- watcher.on("sync", (event) => {
7796
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synced: ${event.type} ${event.path}`);
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
- watcher.on("syncError", ({ event, error: err }) => {
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
- watcher.on("error", (err) => {
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
- await watcher.stop();
7807
- if (existsSync7(paths.pidFile)) {
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
- watcher.start();
7815
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting file watcher for ${projectRoot}...`);
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 (!existsSync7(paths.logFile)) {
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 Command21();
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) => {