@vladimir-ks/aigile 0.2.6 → 0.2.8

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.
@@ -426,7 +426,8 @@ __export(connection_exports, {
426
426
  run: () => run,
427
427
  runMigrations: () => runMigrations,
428
428
  saveDatabase: () => saveDatabase,
429
- tagFileReviewed: () => tagFileReviewed
429
+ tagFileReviewed: () => tagFileReviewed,
430
+ transaction: () => transaction
430
431
  });
431
432
  import initSqlJs from "sql.js";
432
433
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
@@ -460,16 +461,33 @@ function getDatabase() {
460
461
  }
461
462
  return db;
462
463
  }
463
- function saveDatabase() {
464
- if (db && dbPath) {
464
+ function saveDatabase(retries = 3) {
465
+ if (!db || !dbPath) {
466
+ return;
467
+ }
468
+ let lastError = null;
469
+ for (let attempt = 0; attempt < retries; attempt++) {
465
470
  try {
466
471
  const data2 = db.export();
467
472
  const buffer = Buffer.from(data2);
468
473
  writeFileSync2(dbPath, buffer);
474
+ return;
469
475
  } catch (err) {
470
- console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Database save error: ${err}`);
476
+ lastError = err instanceof Error ? err : new Error(String(err));
477
+ console.error(
478
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Database save attempt ${attempt + 1}/${retries} failed: ${lastError.message}`
479
+ );
480
+ if (attempt < retries - 1) {
481
+ const delay = Math.min(100 * Math.pow(2, attempt), 1e3);
482
+ const start = Date.now();
483
+ while (Date.now() - start < delay) {
484
+ }
485
+ }
471
486
  }
472
487
  }
488
+ throw new Error(
489
+ `Failed to save database after ${retries} attempts: ${lastError?.message || "Unknown error"}`
490
+ );
473
491
  }
474
492
  function closeDatabase() {
475
493
  if (db) {
@@ -497,7 +515,36 @@ function queryOne(sql, params = []) {
497
515
  function run(sql, params = []) {
498
516
  const database = getDatabase();
499
517
  database.run(sql, params);
500
- saveDatabase();
518
+ if (transactionDepth === 0) {
519
+ saveDatabase();
520
+ }
521
+ }
522
+ async function transaction(callback) {
523
+ const database = getDatabase();
524
+ const isOutermost = transactionDepth === 0;
525
+ transactionDepth++;
526
+ try {
527
+ if (isOutermost) {
528
+ database.run("BEGIN TRANSACTION");
529
+ }
530
+ const result = await callback();
531
+ if (isOutermost) {
532
+ database.run("COMMIT");
533
+ saveDatabase();
534
+ }
535
+ return result;
536
+ } catch (err) {
537
+ if (isOutermost) {
538
+ try {
539
+ database.run("ROLLBACK");
540
+ } catch (rollbackErr) {
541
+ console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Rollback failed: ${rollbackErr}`);
542
+ }
543
+ }
544
+ throw err;
545
+ } finally {
546
+ transactionDepth--;
547
+ }
501
548
  }
502
549
  function initializeSchema(database) {
503
550
  database.run(`
@@ -829,7 +876,9 @@ function initializeSchema(database) {
829
876
  )
830
877
  `);
831
878
  database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_session ON session_files(session_id)`);
832
- database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_document ON session_files(document_id)`);
879
+ database.run(
880
+ `CREATE INDEX IF NOT EXISTS idx_session_files_document ON session_files(document_id)`
881
+ );
833
882
  database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_chunk ON session_files(chunk_id)`);
834
883
  database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_report ON session_files(report_path)`);
835
884
  database.run(`CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(session_id)`);
@@ -849,10 +898,11 @@ function getNextKey(projectKey) {
849
898
  [nextValue, projectKey]
850
899
  );
851
900
  } else {
852
- run(
853
- `INSERT INTO key_sequences (id, prefix, current_value) VALUES (?, ?, ?)`,
854
- [generateId(), projectKey, nextValue]
855
- );
901
+ run(`INSERT INTO key_sequences (id, prefix, current_value) VALUES (?, ?, ?)`, [
902
+ generateId(),
903
+ projectKey,
904
+ nextValue
905
+ ]);
856
906
  }
857
907
  return `${projectKey}-${nextValue}`;
858
908
  }
@@ -874,10 +924,9 @@ function getChunk(chunkId) {
874
924
  return queryOne("SELECT * FROM chunks WHERE id = ?", [chunkId]);
875
925
  }
876
926
  function getSessionChunks(sessionId) {
877
- return queryAll(
878
- "SELECT * FROM chunks WHERE session_id = ? ORDER BY created_at",
879
- [sessionId]
880
- );
927
+ return queryAll("SELECT * FROM chunks WHERE session_id = ? ORDER BY created_at", [
928
+ sessionId
929
+ ]);
881
930
  }
882
931
  function assignFilesToChunk(chunkId, files) {
883
932
  const chunk = getChunk(chunkId);
@@ -886,10 +935,10 @@ function assignFilesToChunk(chunkId, files) {
886
935
  }
887
936
  const existing = chunk.assigned_files ? JSON.parse(chunk.assigned_files) : [];
888
937
  const merged = [.../* @__PURE__ */ new Set([...existing, ...files])];
889
- run(
890
- `UPDATE chunks SET assigned_files = ?, updated_at = datetime('now') WHERE id = ?`,
891
- [JSON.stringify(merged), chunkId]
892
- );
938
+ run(`UPDATE chunks SET assigned_files = ?, updated_at = datetime('now') WHERE id = ?`, [
939
+ JSON.stringify(merged),
940
+ chunkId
941
+ ]);
893
942
  }
894
943
  function tagFileReviewed(sessionId, documentId, options = {}) {
895
944
  const existing = queryOne(
@@ -941,10 +990,10 @@ function flagFileQualityIssue(sessionFileId, issues) {
941
990
  );
942
991
  const current = existing?.quality_issues ? JSON.parse(existing.quality_issues) : [];
943
992
  const merged = [.../* @__PURE__ */ new Set([...current, ...issues])];
944
- run(
945
- `UPDATE session_files SET quality_issues = ? WHERE id = ?`,
946
- [JSON.stringify(merged), sessionFileId]
947
- );
993
+ run(`UPDATE session_files SET quality_issues = ? WHERE id = ?`, [
994
+ JSON.stringify(merged),
995
+ sessionFileId
996
+ ]);
948
997
  }
949
998
  function getSessionFiles(sessionId, options = {}) {
950
999
  let sql = "SELECT * FROM session_files WHERE session_id = ?";
@@ -1066,9 +1115,7 @@ function getCoverageStats(sessionId, chunkId) {
1066
1115
  }
1067
1116
  function runMigrations() {
1068
1117
  const database = getDatabase();
1069
- const columns = queryAll(
1070
- `PRAGMA table_info(documents)`
1071
- );
1118
+ const columns = queryAll(`PRAGMA table_info(documents)`);
1072
1119
  const columnNames = new Set(columns.map((c) => c.name));
1073
1120
  const newColumns = [
1074
1121
  { name: "meta_status", type: "TEXT" },
@@ -1094,7 +1141,13 @@ function runMigrations() {
1094
1141
  // Tri-state monitoring columns
1095
1142
  { name: "monitoring_category", type: 'TEXT DEFAULT "unknown"' },
1096
1143
  { name: "needs_review", type: "INTEGER DEFAULT 0" },
1097
- { name: "reviewed_at", type: "TEXT" }
1144
+ { name: "reviewed_at", type: "TEXT" },
1145
+ // Template system columns (v3)
1146
+ { name: "last_approved_at", type: "TEXT" },
1147
+ { name: "last_modified_at", type: "TEXT" },
1148
+ { name: "is_external", type: "INTEGER DEFAULT 0" },
1149
+ { name: "meta_related", type: "TEXT" },
1150
+ { name: "meta_exception", type: "TEXT" }
1098
1151
  ];
1099
1152
  for (const col of newColumns) {
1100
1153
  if (!columnNames.has(col.name)) {
@@ -1163,22 +1216,29 @@ function runMigrations() {
1163
1216
  )
1164
1217
  `);
1165
1218
  try {
1166
- database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_session ON session_files(session_id)`);
1167
- database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_document ON session_files(document_id)`);
1219
+ database.run(
1220
+ `CREATE INDEX IF NOT EXISTS idx_session_files_session ON session_files(session_id)`
1221
+ );
1222
+ database.run(
1223
+ `CREATE INDEX IF NOT EXISTS idx_session_files_document ON session_files(document_id)`
1224
+ );
1168
1225
  database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_chunk ON session_files(chunk_id)`);
1169
- database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_report ON session_files(report_path)`);
1226
+ database.run(
1227
+ `CREATE INDEX IF NOT EXISTS idx_session_files_report ON session_files(report_path)`
1228
+ );
1170
1229
  database.run(`CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(session_id)`);
1171
1230
  } catch {
1172
1231
  }
1173
1232
  saveDatabase();
1174
1233
  }
1175
- var db, dbPath;
1234
+ var db, dbPath, transactionDepth;
1176
1235
  var init_connection = __esm({
1177
1236
  "src/db/connection.ts"() {
1178
1237
  "use strict";
1179
1238
  init_config();
1180
1239
  db = null;
1181
1240
  dbPath = "";
1241
+ transactionDepth = 0;
1182
1242
  }
1183
1243
  });
1184
1244
 
@@ -1186,15 +1246,15 @@ var init_connection = __esm({
1186
1246
  import { Command as Command23 } from "commander";
1187
1247
 
1188
1248
  // src/index.ts
1189
- var VERSION = true ? "0.2.6" : "0.0.0-dev";
1249
+ var VERSION = true ? "0.2.8" : "0.0.0-dev";
1190
1250
 
1191
1251
  // src/bin/aigile.ts
1192
1252
  init_connection();
1193
1253
 
1194
1254
  // src/commands/init.ts
1195
1255
  import { Command } from "commander";
1196
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, appendFileSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
1197
- import { resolve, join as join4, relative, basename as basename2 } from "path";
1256
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, appendFileSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
1257
+ import { resolve as resolve2, join as join4, relative, basename as basename2 } from "path";
1198
1258
 
1199
1259
  // src/utils/git.ts
1200
1260
  import { execSync } from "child_process";
@@ -1290,7 +1350,7 @@ function error(message, opts = {}) {
1290
1350
  console.error(`${prefix} ${message}`);
1291
1351
  }
1292
1352
  }
1293
- function warning2(message, opts = {}) {
1353
+ function warning(message, opts = {}) {
1294
1354
  if (opts.json) {
1295
1355
  console.log(JSON.stringify({ warning: message }));
1296
1356
  } else {
@@ -1351,749 +1411,99 @@ function header(title, opts = {}) {
1351
1411
  console.log(formatted);
1352
1412
  }
1353
1413
 
1354
- // src/services/template-packs.ts
1355
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
1356
- import { join as join3, dirname as dirname2 } from "path";
1357
- function createFrontmatter(title, tldr, modules = []) {
1358
- return `---
1359
- metadata:
1360
- status: TEMPLATE
1361
- version: "1.0"
1362
- tldr: "${tldr}"
1363
- title: "${title}"
1364
- modules: [${modules.map((m) => `"${m}"`).join(", ")}]
1365
- authors: []
1366
- dependencies: []
1367
- ---
1368
-
1369
- `;
1370
- }
1371
- function getFullRepoTemplates() {
1372
- return [
1373
- // 00_DOCS hierarchy
1374
- {
1375
- path: "00_DOCS/00_vision/01_mission-vision.md",
1376
- content: createFrontmatter(
1377
- "Mission & Vision",
1378
- "Define company mission, vision, values, and elevator pitch",
1379
- ["vision"]
1380
- ) + `# Mission & Vision
1381
-
1382
- ## Mission Statement
1383
- <!-- Why does this company/product exist? -->
1384
-
1385
- ## Vision Statement
1386
- <!-- What future are we creating? -->
1387
-
1388
- ## Core Values
1389
- <!-- What principles guide our decisions? -->
1390
-
1391
- ## Elevator Pitch
1392
- <!-- One sentence: What do we do? -->
1393
- `
1394
- },
1395
- {
1396
- path: "00_DOCS/01_strategy/01_strategic-analysis.md",
1397
- content: createFrontmatter(
1398
- "Strategic Analysis",
1399
- "SWOT analysis and strategic pillars",
1400
- ["strategy"]
1401
- ) + `# Strategic Analysis
1402
-
1403
- ## SWOT Analysis
1404
-
1405
- ### Strengths
1406
- -
1407
-
1408
- ### Weaknesses
1409
- -
1410
-
1411
- ### Opportunities
1412
- -
1413
-
1414
- ### Threats
1415
- -
1416
-
1417
- ## Strategic Pillars
1418
- <!-- Key focus areas for the next 1-3 years -->
1419
-
1420
- `
1421
- },
1422
- {
1423
- path: "00_DOCS/02_target-audience/01_personas.md",
1424
- content: createFrontmatter(
1425
- "User Personas",
1426
- "Define ideal customer profile and user personas",
1427
- ["personas", "ux"]
1428
- ) + `# User Personas
1429
-
1430
- ## Ideal Customer Profile (ICP)
1431
- <!-- Who is our primary customer? -->
1432
-
1433
- ## Primary Personas
1434
-
1435
- ### Persona 1: [Name]
1436
- - **Role:**
1437
- - **Goals:**
1438
- - **Frustrations:**
1439
- - **Key Tasks:**
1440
-
1441
- `
1442
- },
1443
- {
1444
- path: "00_DOCS/03_finance-legal/01_model.md",
1445
- content: createFrontmatter(
1446
- "Business Model",
1447
- "Revenue model and financial overview",
1448
- ["finance"]
1449
- ) + `# Business Model
1450
-
1451
- ## Revenue Model
1452
- <!-- How do we make money? -->
1453
-
1454
- ## Pricing Strategy
1455
- <!-- How do we price our offerings? -->
1456
-
1457
- ## Key Metrics
1458
- <!-- What metrics matter most? -->
1459
-
1460
- `
1461
- },
1462
- {
1463
- path: "00_DOCS/04_raw-inputs/README.md",
1464
- content: createFrontmatter(
1465
- "Raw Inputs",
1466
- "Repository for raw user feedback, interviews, and research data",
1467
- ["research"]
1468
- ) + `# Raw Inputs
1469
-
1470
- This directory contains raw, unprocessed input from users, stakeholders, and research.
1471
-
1472
- ## Contents
1473
- - User interviews
1474
- - Survey responses
1475
- - Support tickets
1476
- - Feature requests
1477
- - Competitive intelligence
1478
-
1479
- `
1480
- },
1481
- {
1482
- path: "00_DOCS/05_research/README.md",
1483
- content: createFrontmatter(
1484
- "Research",
1485
- "Market research and analysis documents",
1486
- ["research"]
1487
- ) + `# Research
1488
-
1489
- This directory contains research and analysis documents.
1490
-
1491
- ## Contents
1492
- - Market analysis
1493
- - User research
1494
- - Technical research
1495
- - Competitive analysis
1496
-
1497
- `
1498
- },
1499
- {
1500
- path: "00_DOCS/06_product-specs/01_prd.md",
1501
- content: createFrontmatter(
1502
- "Product Requirements Document",
1503
- "Main PRD template for product features",
1504
- ["product", "specs"]
1505
- ) + `# Product Requirements Document (PRD)
1506
-
1507
- ## Overview
1508
- <!-- Brief description of the product/feature -->
1509
-
1510
- ## Problem Statement
1511
- <!-- What problem are we solving? -->
1512
-
1513
- ## Goals & Success Metrics
1514
- <!-- How will we measure success? -->
1515
-
1516
- ## User Stories
1517
- <!-- Link to user stories -->
1518
-
1519
- ## Requirements
1520
- ### Functional Requirements
1521
- -
1522
-
1523
- ### Non-Functional Requirements
1524
- -
1525
-
1526
- ## Out of Scope
1527
- -
1528
-
1529
- `
1530
- },
1531
- {
1532
- path: "00_DOCS/06_product-specs/epics/README.md",
1533
- content: createFrontmatter(
1534
- "Epics Directory",
1535
- "Contains epic-level product specifications",
1536
- ["product", "epics"]
1537
- ) + `# Epics
1538
-
1539
- This directory contains epic-level specifications.
1540
-
1541
- `
1542
- },
1543
- {
1544
- path: "00_DOCS/06_product-specs/stories/README.md",
1545
- content: createFrontmatter(
1546
- "User Stories Directory",
1547
- "Contains user story specifications",
1548
- ["product", "stories"]
1549
- ) + `# User Stories
1550
-
1551
- This directory contains user story specifications.
1552
-
1553
- `
1554
- },
1555
- {
1556
- path: "00_DOCS/07_ux-design/00_README.md",
1557
- content: createFrontmatter(
1558
- "UX Design",
1559
- "UX design documentation and journey maps",
1560
- ["ux"]
1561
- ) + `# UX Design
1562
-
1563
- ## Contents
1564
- - User journeys
1565
- - Wireframes
1566
- - Design system references
1567
- - Interaction patterns
1568
-
1569
- `
1570
- },
1571
- {
1572
- path: "00_DOCS/07_ux-design/journeys/README.md",
1573
- content: createFrontmatter(
1574
- "User Journeys",
1575
- "User journey maps and flow documentation",
1576
- ["ux", "journeys"]
1577
- ) + `# User Journeys
1578
-
1579
- This directory contains user journey maps.
1580
-
1581
- `
1582
- },
1583
- {
1584
- path: "00_DOCS/08_go-to-market/01_launch-plan.md",
1585
- content: createFrontmatter(
1586
- "Launch Plan",
1587
- "Go-to-market strategy and launch checklist",
1588
- ["gtm"]
1589
- ) + `# Launch Plan
1590
-
1591
- ## Launch Goals
1592
- -
1593
-
1594
- ## Target Audience
1595
- -
1596
-
1597
- ## Channels
1598
- -
1599
-
1600
- ## Timeline
1601
- -
1602
-
1603
- ## Checklist
1604
- - [ ] Marketing materials ready
1605
- - [ ] Documentation complete
1606
- - [ ] Support trained
1607
- - [ ] Analytics configured
1608
-
1609
- `
1610
- },
1611
- {
1612
- path: "00_DOCS/11_people-ops/ai-agents/README.md",
1613
- content: createFrontmatter(
1614
- "AI Agents",
1615
- "AI agent configurations and prompts",
1616
- ["ai", "agents"]
1617
- ) + `# AI Agents
1618
-
1619
- This directory contains AI agent configurations and prompts.
1620
-
1621
- `
1622
- },
1623
- {
1624
- path: "00_DOCS/11_people-ops/sops/README.md",
1625
- content: createFrontmatter(
1626
- "Standard Operating Procedures",
1627
- "Team SOPs and process documentation",
1628
- ["sops"]
1629
- ) + `# Standard Operating Procedures
1630
-
1631
- This directory contains team SOPs and process documentation.
1632
-
1633
- `
1634
- },
1635
- {
1636
- path: "00_DOCS/12_customer-success/installation.md",
1637
- content: createFrontmatter(
1638
- "Installation Guide",
1639
- "Installation and setup instructions",
1640
- ["docs", "installation"]
1641
- ) + `# Installation Guide
1642
-
1643
- ## Prerequisites
1644
- -
1645
-
1646
- ## Installation Steps
1647
- 1.
1648
- 2.
1649
- 3.
1650
-
1651
- ## Verification
1652
- -
1653
-
1654
- `
1655
- },
1656
- {
1657
- path: "00_DOCS/12_customer-success/quick-start.md",
1658
- content: createFrontmatter(
1659
- "Quick Start Guide",
1660
- "Getting started guide for new users",
1661
- ["docs", "quickstart"]
1662
- ) + `# Quick Start Guide
1663
-
1664
- ## Overview
1665
- <!-- Brief intro -->
1666
-
1667
- ## Step 1: Setup
1668
- <!-- First step -->
1669
-
1670
- ## Step 2: First Action
1671
- <!-- Second step -->
1672
-
1673
- ## Next Steps
1674
- <!-- What to do next -->
1675
-
1676
- `
1677
- },
1678
- {
1679
- path: "00_DOCS/12_customer-success/troubleshooting.md",
1680
- content: createFrontmatter(
1681
- "Troubleshooting Guide",
1682
- "Common issues and solutions",
1683
- ["docs", "troubleshooting"]
1684
- ) + `# Troubleshooting
1685
-
1686
- ## Common Issues
1687
-
1688
- ### Issue: [Description]
1689
- **Symptoms:**
1690
- **Solution:**
1691
-
1692
- `
1693
- },
1694
- {
1695
- path: "00_DOCS/99_archive/00_README.md",
1696
- content: createFrontmatter(
1697
- "Archive",
1698
- "Archived documents no longer in active use",
1699
- ["archive"]
1700
- ) + `# Archive
1701
-
1702
- This directory contains archived documents that are no longer in active use but are kept for reference.
1703
-
1704
- `
1705
- },
1706
- // 01_SPECS hierarchy
1707
- {
1708
- path: "01_SPECS/00_adr/README.md",
1709
- content: createFrontmatter(
1710
- "Architecture Decision Records",
1711
- "ADRs documenting key technical decisions",
1712
- ["specs", "adr"]
1713
- ) + `# Architecture Decision Records (ADRs)
1714
-
1715
- This directory contains ADRs for documenting key technical decisions.
1716
-
1717
- ## Template
1718
- Use the template in \`template.md\` for new ADRs.
1719
-
1720
- `
1721
- },
1722
- {
1723
- path: "01_SPECS/01_domain-models/README.md",
1724
- content: createFrontmatter(
1725
- "Domain Models",
1726
- "Domain model specifications",
1727
- ["specs", "domain"]
1728
- ) + `# Domain Models
1729
-
1730
- This directory contains domain model specifications.
1731
-
1732
- `
1733
- },
1734
- {
1735
- path: "01_SPECS/02_api-contracts/README.md",
1736
- content: createFrontmatter(
1737
- "API Contracts",
1738
- "API specifications and contracts",
1739
- ["specs", "api"]
1740
- ) + `# API Contracts
1741
-
1742
- This directory contains API specifications and contracts.
1743
-
1744
- `
1745
- },
1746
- {
1747
- path: "01_SPECS/03_c4-components/README.md",
1748
- content: createFrontmatter(
1749
- "C4 Architecture",
1750
- "C4 model component diagrams",
1751
- ["specs", "architecture"]
1752
- ) + `# C4 Architecture Components
1753
-
1754
- This directory contains C4 model architecture diagrams.
1755
-
1756
- `
1757
- },
1758
- {
1759
- path: "01_SPECS/04_ui-ux-specs/README.md",
1760
- content: createFrontmatter(
1761
- "UI/UX Specifications",
1762
- "UI/UX technical specifications",
1763
- ["specs", "ux"]
1764
- ) + `# UI/UX Specifications
1765
-
1766
- This directory contains UI/UX technical specifications.
1767
-
1768
- `
1769
- },
1770
- {
1771
- path: "01_SPECS/05_infrastructure/README.md",
1772
- content: createFrontmatter(
1773
- "Infrastructure",
1774
- "Infrastructure and deployment specifications",
1775
- ["specs", "infra"]
1776
- ) + `# Infrastructure
1777
-
1778
- This directory contains infrastructure and deployment specifications.
1779
-
1780
- `
1781
- },
1782
- // 02_FEATURES hierarchy
1783
- {
1784
- path: "02_FEATURES/00_smoke-tests/README.md",
1785
- content: createFrontmatter(
1786
- "Smoke Tests",
1787
- "Critical path smoke test scenarios",
1788
- ["features", "tests"]
1789
- ) + `# Smoke Tests
1790
-
1791
- This directory contains smoke test feature files for critical paths.
1792
-
1793
- `
1794
- },
1795
- {
1796
- path: "02_FEATURES/01_epics/README.md",
1797
- content: createFrontmatter(
1798
- "Epic Features",
1799
- "BDD feature files organized by epic",
1800
- ["features", "epics"]
1801
- ) + `# Epic Features
1802
-
1803
- This directory contains BDD feature files organized by epic.
1804
-
1805
- `
1806
- },
1807
- {
1808
- path: "02_FEATURES/02_end-to-end/README.md",
1809
- content: createFrontmatter(
1810
- "End-to-End Features",
1811
- "End-to-end scenario feature files",
1812
- ["features", "e2e"]
1813
- ) + `# End-to-End Features
1814
-
1815
- This directory contains end-to-end scenario feature files.
1816
-
1817
- `
1818
- },
1819
- {
1820
- path: "02_FEATURES/03_nfr/README.md",
1821
- content: createFrontmatter(
1822
- "Non-Functional Requirements",
1823
- "NFR feature files (performance, security, etc.)",
1824
- ["features", "nfr"]
1825
- ) + `# Non-Functional Requirements
1826
-
1827
- This directory contains NFR feature files:
1828
- - Performance
1829
- - Security
1830
- - Scalability
1831
- - Accessibility
1832
-
1833
- `
1834
- },
1835
- // 03_TESTING_INFRA hierarchy
1836
- {
1837
- path: "03_TESTING_INFRA/00_governance/README.md",
1838
- content: createFrontmatter(
1839
- "Test Governance",
1840
- "Testing standards and policies",
1841
- ["testing", "governance"]
1842
- ) + `# Test Governance
1843
-
1844
- This directory contains testing standards and policies.
1845
-
1846
- `
1847
- },
1848
- {
1849
- path: "03_TESTING_INFRA/01_unit-layer/README.md",
1850
- content: createFrontmatter(
1851
- "Unit Testing Layer",
1852
- "Unit test infrastructure and guides",
1853
- ["testing", "unit"]
1854
- ) + `# Unit Testing Layer
1855
-
1856
- This directory contains unit test infrastructure.
1857
-
1858
- `
1859
- },
1860
- {
1861
- path: "03_TESTING_INFRA/02_integration-layer/README.md",
1862
- content: createFrontmatter(
1863
- "Integration Testing Layer",
1864
- "Integration test infrastructure and guides",
1865
- ["testing", "integration"]
1866
- ) + `# Integration Testing Layer
1867
-
1868
- This directory contains integration test infrastructure.
1869
-
1870
- `
1871
- },
1872
- {
1873
- path: "03_TESTING_INFRA/03_e2e-layer/README.md",
1874
- content: createFrontmatter(
1875
- "E2E Testing Layer",
1876
- "End-to-end test infrastructure and guides",
1877
- ["testing", "e2e"]
1878
- ) + `# E2E Testing Layer
1879
-
1880
- This directory contains E2E test infrastructure.
1881
-
1882
- `
1883
- },
1884
- {
1885
- path: "03_TESTING_INFRA/04_manual-qa-layer/README.md",
1886
- content: createFrontmatter(
1887
- "Manual QA Layer",
1888
- "Manual QA procedures and checklists",
1889
- ["testing", "qa"]
1890
- ) + `# Manual QA Layer
1891
-
1892
- This directory contains manual QA procedures and checklists.
1893
-
1894
- `
1895
- },
1896
- {
1897
- path: "03_TESTING_INFRA/qa/README.md",
1898
- content: createFrontmatter(
1899
- "QA Documentation",
1900
- "QA procedures and documentation",
1901
- ["testing", "qa"]
1902
- ) + `# QA Documentation
1903
-
1904
- This directory contains QA procedures and documentation.
1905
-
1906
- `
1907
- },
1908
- {
1909
- path: "03_TESTING_INFRA/reports/README.md",
1910
- content: createFrontmatter(
1911
- "Test Reports",
1912
- "Test execution reports",
1913
- ["testing", "reports"]
1914
- ) + `# Test Reports
1915
-
1916
- This directory contains test execution reports.
1917
-
1918
- `
1919
- }
1920
- ];
1414
+ // src/services/template-loader.ts
1415
+ import { readdirSync, statSync, readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
1416
+ import { join as join3, dirname as dirname2, resolve, sep } from "path";
1417
+ import { fileURLToPath } from "url";
1418
+ var __filename = fileURLToPath(import.meta.url);
1419
+ var __dirname = dirname2(__filename);
1420
+ function getTemplateDir(profile) {
1421
+ const isDev = process.env.NODE_ENV === "development" || !process.pkg;
1422
+ let baseDir;
1423
+ if (isDev) {
1424
+ baseDir = join3(__dirname, "..", "..", "artifacts", "templates");
1425
+ } else {
1426
+ baseDir = join3(__dirname, "..", "artifacts", "templates");
1427
+ }
1428
+ return join3(baseDir, profile);
1921
1429
  }
1922
- function getSubrepoTemplates() {
1923
- return getFullRepoTemplates();
1430
+ function loadTemplates(profile) {
1431
+ const templateDir = getTemplateDir(profile);
1432
+ if (!existsSync3(templateDir)) {
1433
+ throw new Error(`Template directory not found: ${templateDir}`);
1434
+ }
1435
+ return recursiveReadTemplates(templateDir, "");
1924
1436
  }
1925
- function getModuleTemplates(moduleName) {
1926
- return [
1927
- {
1928
- path: "docs/01_module-overview.md",
1929
- content: createFrontmatter(
1930
- `${moduleName} Overview`,
1931
- `Overview documentation for ${moduleName} module`,
1932
- [moduleName]
1933
- ) + `# ${moduleName} Module Overview
1934
-
1935
- ## Purpose
1936
- <!-- What does this module do? -->
1937
-
1938
- ## Key Concepts
1939
- <!-- Core concepts and terminology -->
1940
-
1941
- ## Dependencies
1942
- <!-- What does this module depend on? -->
1943
-
1944
- ## API
1945
- <!-- Public API surface -->
1946
-
1947
- `
1948
- },
1949
- {
1950
- path: "docs/02_mini-prd.md",
1951
- content: createFrontmatter(
1952
- `${moduleName} Requirements`,
1953
- `Module-level requirements for ${moduleName}`,
1954
- [moduleName, "specs"]
1955
- ) + `# ${moduleName} Module Requirements
1956
-
1957
- ## Goals
1958
- <!-- What should this module achieve? -->
1959
-
1960
- ## Requirements
1961
- ### Functional
1962
- -
1963
-
1964
- ### Non-Functional
1965
- -
1966
-
1967
- ## Out of Scope
1968
- -
1969
-
1970
- `
1971
- },
1972
- {
1973
- path: "specs/01_spec-template.md",
1974
- content: createFrontmatter(
1975
- `${moduleName} Spec Template`,
1976
- `Specification template for ${moduleName}`,
1977
- [moduleName, "specs"]
1978
- ) + `# ${moduleName} Specification
1979
-
1980
- ## Overview
1981
- <!-- Brief description -->
1982
-
1983
- ## Design
1984
- <!-- Technical design -->
1985
-
1986
- ## Implementation Notes
1987
- <!-- Key implementation details -->
1988
-
1989
- `
1990
- },
1991
- {
1992
- path: "features/01_happy-path.feature",
1993
- content: `Feature: ${moduleName} Happy Path
1994
- As a user
1995
- I want to use ${moduleName}
1996
- So that I can achieve my goals
1997
-
1998
- Scenario: Basic usage
1999
- Given I have configured ${moduleName}
2000
- When I perform the main action
2001
- Then I should see the expected result
2002
- `
2003
- },
2004
- {
2005
- path: "features/02_edge-cases.feature",
2006
- content: `Feature: ${moduleName} Edge Cases
2007
- As a user
2008
- I want ${moduleName} to handle edge cases gracefully
2009
- So that I don't encounter errors
2010
-
2011
- Scenario: Empty input
2012
- Given I have configured ${moduleName}
2013
- When I provide empty input
2014
- Then I should see a helpful error message
2015
- `
1437
+ function recursiveReadTemplates(baseDir, relativePath) {
1438
+ const fullPath = join3(baseDir, relativePath);
1439
+ if (!existsSync3(fullPath)) {
1440
+ return [];
1441
+ }
1442
+ const entries = readdirSync(fullPath);
1443
+ const files = [];
1444
+ for (const entry of entries) {
1445
+ const entryPath = join3(fullPath, entry);
1446
+ const entryRelative = relativePath ? join3(relativePath, entry) : entry;
1447
+ try {
1448
+ const stat = statSync(entryPath);
1449
+ if (stat.isDirectory()) {
1450
+ files.push(...recursiveReadTemplates(baseDir, entryRelative));
1451
+ } else if (stat.isFile()) {
1452
+ const content = readFileSync3(entryPath, "utf-8");
1453
+ files.push({
1454
+ relativePath: entryRelative,
1455
+ content
1456
+ });
1457
+ }
1458
+ } catch (err) {
1459
+ console.warn(`Warning: Could not read ${entryPath}: ${err}`);
2016
1460
  }
2017
- ];
2018
- }
2019
- function getTemplatesForProfile(profile, moduleName) {
2020
- switch (profile) {
2021
- case "full-repo":
2022
- return getFullRepoTemplates();
2023
- case "subrepo":
2024
- return getSubrepoTemplates();
2025
- case "module":
2026
- return getModuleTemplates(moduleName ?? "module");
2027
- default:
2028
- return [];
2029
1461
  }
1462
+ return files;
2030
1463
  }
2031
1464
  function writeTemplates(targetDir, templates) {
2032
1465
  let written = 0;
2033
1466
  let skipped = 0;
1467
+ const errors = [];
1468
+ const resolvedTargetDir = resolve(targetDir);
2034
1469
  for (const template of templates) {
2035
- const fullPath = join3(targetDir, template.path);
2036
- const dir = dirname2(fullPath);
2037
- if (!existsSync3(dir)) {
2038
- mkdirSync3(dir, { recursive: true });
1470
+ const fullPath = join3(targetDir, template.relativePath);
1471
+ const resolvedFullPath = resolve(fullPath);
1472
+ if (!resolvedFullPath.startsWith(resolvedTargetDir + sep) && resolvedFullPath !== resolvedTargetDir) {
1473
+ errors.push({
1474
+ path: template.relativePath,
1475
+ error: "Path traversal detected - file path escapes target directory"
1476
+ });
1477
+ continue;
2039
1478
  }
2040
- if (!existsSync3(fullPath)) {
2041
- writeFileSync3(fullPath, template.content, "utf-8");
2042
- written++;
2043
- } else {
1479
+ if (existsSync3(fullPath)) {
2044
1480
  skipped++;
1481
+ continue;
1482
+ }
1483
+ try {
1484
+ const dir = dirname2(fullPath);
1485
+ if (!existsSync3(dir)) {
1486
+ mkdirSync3(dir, { recursive: true });
1487
+ }
1488
+ writeFileSync3(fullPath, template.content, "utf-8");
1489
+ written++;
1490
+ } catch (err) {
1491
+ errors.push({
1492
+ path: template.relativePath,
1493
+ error: err instanceof Error ? err.message : String(err)
1494
+ });
2045
1495
  }
2046
1496
  }
2047
- return { written, skipped };
2048
- }
2049
- function generateConfigYaml(config, projectKey, projectName) {
2050
- let yaml = `# AIGILE Project Configuration
2051
- # Auto-generated by aigile init
2052
-
2053
- db:
2054
- mode: ${config.db.mode}
2055
- path: ${config.db.path}
2056
-
2057
- profile: ${config.profile}
2058
- repo_root: ${config.repo_root}
2059
- `;
2060
- if (config.parent_repo_root) {
2061
- yaml += `parent_repo_root: ${config.parent_repo_root}
2062
- `;
2063
- }
2064
- if (config.module) {
2065
- yaml += `
2066
- module:
2067
- name: ${config.module.name}
2068
- kind: ${config.module.kind}
2069
- path: ${config.module.path}
2070
- `;
2071
- }
2072
- yaml += `
2073
- project:
2074
- key: ${projectKey}
2075
- name: ${projectName}
2076
-
2077
- sync:
2078
- enabled: true
2079
- patterns:
2080
- - "**/*.md"
2081
- - "**/*.feature"
2082
- - "**/*.yaml"
2083
- - "**/*.yml"
2084
- ignore:
2085
- - node_modules
2086
- - dist
2087
- - .git
2088
- - coverage
2089
- `;
2090
- return yaml;
1497
+ return { written, skipped, errors };
2091
1498
  }
2092
1499
 
2093
1500
  // src/commands/init.ts
2094
- var initCommand = new Command("init").description("Initialize AIGILE in current or specified directory").argument("[path]", "Directory path (defaults to current directory)").option("-k, --key <key>", "Project key (auto-generated if not specified)").option("-n, --name <name>", "Project name (auto-detected if not specified)").option("-p, --profile <profile>", "Init profile: full-repo, subrepo, module").option("--db-mode <mode>", "Database mode: local, shared (default based on profile)").option("--module-kind <kind>", "Module kind: library, service, ui, cli, other (for module profile)").option("--skip-templates", "Skip template file creation").option("-f, --force", "Reinitialize existing project").action(async (pathArg, options) => {
1501
+ var initCommand = new Command("init").description("Initialize AIGILE in current or specified directory").argument("[path]", "Directory path (defaults to current directory)").option("-k, --key <key>", "Project key (auto-generated if not specified)").option("-n, --name <name>", "Project name (auto-detected if not specified)").option("-p, --profile <profile>", "Init profile: full-repo, minor-project, module").option("--db-mode <mode>", "Database mode: local, shared (default based on profile)").option(
1502
+ "--module-kind <kind>",
1503
+ "Module kind: library, service, ui, cli, other (for module profile)"
1504
+ ).option("--skip-templates", "Skip template file creation").option("-f, --force", "Reinitialize existing project").action(async (pathArg, options) => {
2095
1505
  const opts = getOutputOptions(initCommand);
2096
- const targetPath = resolve(pathArg ?? process.cwd());
1506
+ const targetPath = resolve2(pathArg ?? process.cwd());
2097
1507
  try {
2098
1508
  await initProject(targetPath, options, opts);
2099
1509
  } catch (err) {
@@ -2118,7 +1528,7 @@ function detectGitContext(targetPath) {
2118
1528
  relativePath
2119
1529
  };
2120
1530
  }
2121
- var VALID_PROFILES = ["full-repo", "subrepo", "module"];
1531
+ var VALID_PROFILES = ["full-repo", "minor-project", "module"];
2122
1532
  function determineProfile(context, options, opts) {
2123
1533
  if (options.profile) {
2124
1534
  if (!VALID_PROFILES.includes(options.profile)) {
@@ -2129,8 +1539,8 @@ function determineProfile(context, options, opts) {
2129
1539
  return options.profile;
2130
1540
  }
2131
1541
  if (context.isSubmodule) {
2132
- info("Detected git submodule - using subrepo profile", opts);
2133
- return "subrepo";
1542
+ info("Detected git submodule - using minor-project profile", opts);
1543
+ return "minor-project";
2134
1544
  }
2135
1545
  if (context.isSubdirectory) {
2136
1546
  const parentAigile = join4(context.gitRoot, ".aigile");
@@ -2153,7 +1563,7 @@ function determineDbMode(profile, context, options) {
2153
1563
  switch (profile) {
2154
1564
  case "full-repo":
2155
1565
  return { mode: "local", path: ".aigile/aigile.db" };
2156
- case "subrepo":
1566
+ case "minor-project":
2157
1567
  return { mode: "local", path: ".aigile/aigile.db" };
2158
1568
  case "module":
2159
1569
  const parentDbPath = findParentDb(context);
@@ -2176,6 +1586,33 @@ function findParentDb(context) {
2176
1586
  }
2177
1587
  return ".aigile/aigile.db";
2178
1588
  }
1589
+ function generateConfigYaml(config, projectKey, projectName) {
1590
+ const lines = ["# AIGILE Project Configuration", ""];
1591
+ lines.push("project:");
1592
+ lines.push(` key: "${projectKey}"`);
1593
+ lines.push(` name: "${projectName}"`);
1594
+ lines.push("");
1595
+ lines.push("db:");
1596
+ lines.push(` mode: ${config.db.mode}`);
1597
+ lines.push(` path: ${config.db.path}`);
1598
+ lines.push("");
1599
+ lines.push(`profile: ${config.profile}`);
1600
+ lines.push("");
1601
+ lines.push(`repo_root: ${config.repo_root}`);
1602
+ if (config.module) {
1603
+ lines.push("");
1604
+ lines.push("module:");
1605
+ lines.push(` name: ${config.module.name}`);
1606
+ lines.push(` kind: ${config.module.kind}`);
1607
+ lines.push(` path: ${config.module.path}`);
1608
+ }
1609
+ if (config.parent_repo_root) {
1610
+ lines.push("");
1611
+ lines.push(`parent_repo_root: ${config.parent_repo_root}`);
1612
+ }
1613
+ lines.push("");
1614
+ return lines.join("\n");
1615
+ }
2179
1616
  async function initProject(targetPath, options, opts) {
2180
1617
  if (!isGitRepo(targetPath)) {
2181
1618
  throw new Error('AIGILE requires a git repository. Run "git init" first.');
@@ -2225,35 +1662,47 @@ async function initProject(targetPath, options, opts) {
2225
1662
  kind: options.moduleKind ?? "other",
2226
1663
  path: context.relativePath
2227
1664
  };
2228
- aigileConfig.parent_repo_root = "../".repeat(context.relativePath.split("/").filter((p) => p).length);
1665
+ aigileConfig.parent_repo_root = "../".repeat(
1666
+ context.relativePath.split("/").filter((p) => p).length
1667
+ );
2229
1668
  }
2230
- if (profile === "subrepo" && context.superprojectRoot) {
1669
+ if (profile === "minor-project" && context.superprojectRoot) {
2231
1670
  aigileConfig.parent_repo_root = "..";
2232
1671
  }
2233
1672
  const configYaml = generateConfigYaml(aigileConfig, projectKey, projectName);
2234
1673
  writeFileSync4(join4(aigileDir, "config.yaml"), configYaml, "utf-8");
2235
- let templatesResult = { written: 0, skipped: 0 };
1674
+ let templatesResult = { written: 0, skipped: 0, errors: [] };
2236
1675
  if (!options.skipTemplates) {
2237
- const templates = getTemplatesForProfile(profile, moduleName);
2238
- templatesResult = writeTemplates(aigileDir, templates);
1676
+ try {
1677
+ const templates = loadTemplates(profile);
1678
+ templatesResult = writeTemplates(targetPath, templates);
1679
+ } catch (err) {
1680
+ warning(
1681
+ `Could not load templates: ${err instanceof Error ? err.message : String(err)}`,
1682
+ opts
1683
+ );
1684
+ }
2239
1685
  }
2240
1686
  if (dbConfig.mode === "local") {
2241
1687
  registerProject(targetPath, (projectId) => {
2242
- const existingProject = queryOne(
2243
- "SELECT id FROM projects WHERE path = ?",
2244
- [targetPath]
2245
- );
1688
+ const existingProject = queryOne("SELECT id FROM projects WHERE path = ?", [
1689
+ targetPath
1690
+ ]);
2246
1691
  const id = existingProject?.id ?? generateId();
2247
1692
  if (existingProject) {
2248
- run(
2249
- `UPDATE projects SET key = ?, name = ?, updated_at = datetime('now') WHERE id = ?`,
2250
- [projectKey, projectName, id]
2251
- );
1693
+ run(`UPDATE projects SET key = ?, name = ?, updated_at = datetime('now') WHERE id = ?`, [
1694
+ projectKey,
1695
+ projectName,
1696
+ id
1697
+ ]);
2252
1698
  } else {
2253
- run(
2254
- `INSERT INTO projects (id, key, name, path, is_default) VALUES (?, ?, ?, ?, ?)`,
2255
- [id, projectKey, projectName, targetPath, 0]
2256
- );
1699
+ run(`INSERT INTO projects (id, key, name, path, is_default) VALUES (?, ?, ?, ?, ?)`, [
1700
+ id,
1701
+ projectKey,
1702
+ projectName,
1703
+ targetPath,
1704
+ 0
1705
+ ]);
2257
1706
  }
2258
1707
  const defaultProject = queryOne(
2259
1708
  "SELECT id FROM projects WHERE is_default = 1",
@@ -2267,17 +1716,19 @@ async function initProject(targetPath, options, opts) {
2267
1716
  }
2268
1717
  updateGitignore(targetPath, opts);
2269
1718
  if (opts.json) {
2270
- console.log(JSON.stringify({
2271
- success: true,
2272
- project: {
2273
- key: projectKey,
2274
- name: projectName,
2275
- path: targetPath,
2276
- profile,
2277
- dbMode: dbConfig.mode
2278
- },
2279
- templates: templatesResult
2280
- }));
1719
+ console.log(
1720
+ JSON.stringify({
1721
+ success: true,
1722
+ project: {
1723
+ key: projectKey,
1724
+ name: projectName,
1725
+ path: targetPath,
1726
+ profile,
1727
+ dbMode: dbConfig.mode
1728
+ },
1729
+ templates: templatesResult
1730
+ })
1731
+ );
2281
1732
  } else {
2282
1733
  blank();
2283
1734
  success(`AIGILE initialized with ${profile} profile`, opts);
@@ -2293,11 +1744,14 @@ async function initProject(targetPath, options, opts) {
2293
1744
  header("Templates:", opts);
2294
1745
  console.log(` Written: ${templatesResult.written} files`);
2295
1746
  console.log(` Skipped: ${templatesResult.skipped} files (already exist)`);
1747
+ if (templatesResult.errors.length > 0) {
1748
+ console.log(` Errors: ${templatesResult.errors.length} files`);
1749
+ }
2296
1750
  }
2297
1751
  blank();
2298
1752
  header("Next steps:", opts);
2299
- if (profile === "full-repo" || profile === "subrepo") {
2300
- console.log(" 1. Fill in 00_DOCS/00_vision/01_mission-vision.md");
1753
+ if (profile === "full-repo" || profile === "minor-project") {
1754
+ console.log(" 1. Fill in 00_DOCS/01_vision-foundations/01_mission-vision-values.md");
2301
1755
  console.log(' 2. Run "aigile sync scan" to index your files');
2302
1756
  console.log(' 3. Run "aigile daemon install && aigile daemon start" for auto-sync');
2303
1757
  } else {
@@ -2317,7 +1771,7 @@ function updateGitignore(repoPath, opts) {
2317
1771
  info('No .gitignore found. Consider adding ".aigile/" to ignore local config.', opts);
2318
1772
  return;
2319
1773
  }
2320
- const content = readFileSync3(gitignorePath, "utf-8");
1774
+ const content = readFileSync4(gitignorePath, "utf-8");
2321
1775
  const lines = content.split("\n");
2322
1776
  const hasPattern = lines.some((line) => {
2323
1777
  const trimmed = line.trim();
@@ -2399,7 +1853,7 @@ projectCommand.command("list").alias("ls").description("List all registered proj
2399
1853
  console.log(" \u2713 = valid path, \u2717 = missing/invalid path");
2400
1854
  if (invalidCount > 0) {
2401
1855
  blank();
2402
- warning2(`${invalidCount} project(s) have invalid paths. Run "aigile project cleanup" to remove.`, opts);
1856
+ warning(`${invalidCount} project(s) have invalid paths. Run "aigile project cleanup" to remove.`, opts);
2403
1857
  }
2404
1858
  });
2405
1859
  projectCommand.command("show").argument("[key]", "Project key (uses default if not specified)").description("Show project details").action((key) => {
@@ -2808,7 +2262,7 @@ epicCommand.command("delete").alias("rm").argument("<key>", "Epic key").option("
2808
2262
  (SELECT id FROM user_stories WHERE epic_id = ?)`,
2809
2263
  [epic.id]
2810
2264
  );
2811
- warning2(
2265
+ warning(
2812
2266
  `Deleting ${childCount.count} child story(s) and ${taskCount?.count || 0} task(s)`,
2813
2267
  opts
2814
2268
  );
@@ -2818,7 +2272,7 @@ epicCommand.command("delete").alias("rm").argument("<key>", "Epic key").option("
2818
2272
  );
2819
2273
  run("DELETE FROM user_stories WHERE epic_id = ?", [epic.id]);
2820
2274
  } else {
2821
- warning2(`Orphaning ${childCount.count} child story(s)`, opts);
2275
+ warning(`Orphaning ${childCount.count} child story(s)`, opts);
2822
2276
  run("UPDATE user_stories SET epic_id = NULL WHERE epic_id = ?", [epic.id]);
2823
2277
  }
2824
2278
  }
@@ -3017,10 +2471,10 @@ storyCommand.command("delete").alias("rm").argument("<key>", "Story key").option
3017
2471
  process.exit(1);
3018
2472
  }
3019
2473
  if (options.cascade) {
3020
- warning2(`Deleting ${childCount.count} child task(s)`, opts);
2474
+ warning(`Deleting ${childCount.count} child task(s)`, opts);
3021
2475
  run("DELETE FROM tasks WHERE story_id = ?", [story.id]);
3022
2476
  } else {
3023
- warning2(`Orphaning ${childCount.count} child task(s)`, opts);
2477
+ warning(`Orphaning ${childCount.count} child task(s)`, opts);
3024
2478
  run("UPDATE tasks SET story_id = NULL WHERE story_id = ?", [story.id]);
3025
2479
  }
3026
2480
  }
@@ -3983,11 +3437,11 @@ init_config();
3983
3437
  // src/services/file-scanner.ts
3984
3438
  init_connection();
3985
3439
  import { createHash } from "crypto";
3986
- import { readFileSync as readFileSync5, statSync, readdirSync, existsSync as existsSync6 } from "fs";
3440
+ import { readFileSync as readFileSync6, statSync as statSync2, readdirSync as readdirSync2, existsSync as existsSync6 } from "fs";
3987
3441
  import { join as join6, relative as relative2, extname } from "path";
3988
3442
 
3989
3443
  // src/services/frontmatter-parser.ts
3990
- import { readFileSync as readFileSync4 } from "fs";
3444
+ import { readFileSync as readFileSync5 } from "fs";
3991
3445
  import { parse as parseYaml2 } from "yaml";
3992
3446
  var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
3993
3447
  function extractFrontmatter(content) {
@@ -4009,9 +3463,11 @@ function extractFrontmatter(content) {
4009
3463
  tldr: data2?.tldr,
4010
3464
  modules: data2?.modules,
4011
3465
  dependencies: data2?.dependencies,
3466
+ related: data2?.related,
4012
3467
  code_refs: data2?.code_refs,
4013
3468
  authors: data2?.authors,
4014
- title: data2?.title
3469
+ title: data2?.title,
3470
+ exception: data2?.exception
4015
3471
  };
4016
3472
  }
4017
3473
  if (metadata.modules && !Array.isArray(metadata.modules)) {
@@ -4020,6 +3476,9 @@ function extractFrontmatter(content) {
4020
3476
  if (metadata.dependencies && !Array.isArray(metadata.dependencies)) {
4021
3477
  metadata.dependencies = [String(metadata.dependencies)];
4022
3478
  }
3479
+ if (metadata.related && !Array.isArray(metadata.related)) {
3480
+ metadata.related = [String(metadata.related)];
3481
+ }
4023
3482
  if (metadata.code_refs && !Array.isArray(metadata.code_refs)) {
4024
3483
  metadata.code_refs = [String(metadata.code_refs)];
4025
3484
  }
@@ -4039,7 +3498,7 @@ function extractFrontmatter(content) {
4039
3498
  }
4040
3499
  function parseFrontmatterFromFile(filePath) {
4041
3500
  try {
4042
- const content = readFileSync4(filePath, "utf-8");
3501
+ const content = readFileSync5(filePath, "utf-8");
4043
3502
  return extractFrontmatter(content);
4044
3503
  } catch {
4045
3504
  return null;
@@ -4068,9 +3527,15 @@ function serializeMetadata(metadata) {
4068
3527
  if (metadata.dependencies && metadata.dependencies.length > 0) {
4069
3528
  lines.push(` dependencies: [${metadata.dependencies.join(", ")}]`);
4070
3529
  }
3530
+ if (metadata.related && metadata.related.length > 0) {
3531
+ lines.push(` related: [${metadata.related.join(", ")}]`);
3532
+ }
4071
3533
  if (metadata.code_refs && metadata.code_refs.length > 0) {
4072
3534
  lines.push(` code_refs: [${metadata.code_refs.join(", ")}]`);
4073
3535
  }
3536
+ if (metadata.exception) {
3537
+ lines.push(` exception: "${metadata.exception.replace(/"/g, '\\"')}"`);
3538
+ }
4074
3539
  lines.push("---");
4075
3540
  return lines.join("\n");
4076
3541
  }
@@ -4092,22 +3557,18 @@ function updateFrontmatterContent(content, updates) {
4092
3557
  ...updates
4093
3558
  };
4094
3559
  if (updates.dependencies && existing.metadata.dependencies) {
4095
- merged.dependencies = [.../* @__PURE__ */ new Set([
4096
- ...existing.metadata.dependencies,
4097
- ...updates.dependencies
4098
- ])];
3560
+ merged.dependencies = [
3561
+ .../* @__PURE__ */ new Set([...existing.metadata.dependencies, ...updates.dependencies])
3562
+ ];
4099
3563
  }
4100
3564
  if (updates.modules && existing.metadata.modules) {
4101
- merged.modules = [.../* @__PURE__ */ new Set([
4102
- ...existing.metadata.modules,
4103
- ...updates.modules
4104
- ])];
3565
+ merged.modules = [.../* @__PURE__ */ new Set([...existing.metadata.modules, ...updates.modules])];
3566
+ }
3567
+ if (updates.related && existing.metadata.related) {
3568
+ merged.related = [.../* @__PURE__ */ new Set([...existing.metadata.related, ...updates.related])];
4105
3569
  }
4106
3570
  if (updates.code_refs && existing.metadata.code_refs) {
4107
- merged.code_refs = [.../* @__PURE__ */ new Set([
4108
- ...existing.metadata.code_refs,
4109
- ...updates.code_refs
4110
- ])];
3571
+ merged.code_refs = [.../* @__PURE__ */ new Set([...existing.metadata.code_refs, ...updates.code_refs])];
4111
3572
  }
4112
3573
  const newFrontmatter = serializeMetadata(merged);
4113
3574
  return content.replace(FRONTMATTER_REGEX, newFrontmatter + "\n\n");
@@ -4118,7 +3579,7 @@ init_monitoring_patterns();
4118
3579
  init_config();
4119
3580
  import picomatch from "picomatch";
4120
3581
  function computeFileHash(filePath) {
4121
- const content = readFileSync5(filePath);
3582
+ const content = readFileSync6(filePath);
4122
3583
  return createHash("sha256").update(content).digest("hex");
4123
3584
  }
4124
3585
  var FileClassifier = class {
@@ -4156,7 +3617,7 @@ function collectFiles(dir, rootDir, classifier, trackUnknown, files = []) {
4156
3617
  if (!existsSync6(dir)) {
4157
3618
  return files;
4158
3619
  }
4159
- const entries = readdirSync(dir, { withFileTypes: true });
3620
+ const entries = readdirSync2(dir, { withFileTypes: true });
4160
3621
  for (const entry of entries) {
4161
3622
  const fullPath = join6(dir, entry.name);
4162
3623
  const relativePath = relative2(rootDir, fullPath);
@@ -4178,7 +3639,7 @@ function collectFiles(dir, rootDir, classifier, trackUnknown, files = []) {
4178
3639
  continue;
4179
3640
  }
4180
3641
  try {
4181
- const stats = statSync(fullPath);
3642
+ const stats = statSync2(fullPath);
4182
3643
  const ext = extname(entry.name).toLowerCase().slice(1);
4183
3644
  const isBinary = isBinaryExtension(ext);
4184
3645
  const shouldComputeHash = category === "allow" || !isBinary && stats.size < 10 * 1024 * 1024;
@@ -4374,50 +3835,6 @@ function getDocuments(projectId, status) {
4374
3835
  query += " ORDER BY path";
4375
3836
  return queryAll(query, params);
4376
3837
  }
4377
- function getDocumentsByMetaStatus(projectId, metaStatus) {
4378
- return queryAll(
4379
- `SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
4380
- has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
4381
- meta_modules, meta_dependencies, meta_code_refs, meta_authors
4382
- FROM documents
4383
- WHERE project_id = ? AND meta_status = ? AND status != 'deleted'
4384
- ORDER BY path`,
4385
- [projectId, metaStatus]
4386
- );
4387
- }
4388
- function getDocumentsByModule(projectId, module) {
4389
- return queryAll(
4390
- `SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
4391
- has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
4392
- meta_modules, meta_dependencies, meta_code_refs, meta_authors
4393
- FROM documents
4394
- WHERE project_id = ? AND meta_modules LIKE ? AND status != 'deleted'
4395
- ORDER BY path`,
4396
- [projectId, `%"${module}"%`]
4397
- );
4398
- }
4399
- function getDocumentsWithFrontmatter(projectId) {
4400
- return queryAll(
4401
- `SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
4402
- has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
4403
- meta_modules, meta_dependencies, meta_code_refs, meta_authors
4404
- FROM documents
4405
- WHERE project_id = ? AND has_frontmatter = 1 AND status != 'deleted'
4406
- ORDER BY path`,
4407
- [projectId]
4408
- );
4409
- }
4410
- function getDocumentsWithoutFrontmatter(projectId) {
4411
- return queryAll(
4412
- `SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
4413
- has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
4414
- meta_modules, meta_dependencies, meta_code_refs, meta_authors
4415
- FROM documents
4416
- WHERE project_id = ? AND (has_frontmatter = 0 OR has_frontmatter IS NULL) AND status != 'deleted'
4417
- ORDER BY path`,
4418
- [projectId]
4419
- );
4420
- }
4421
3838
  function getDocumentByPath(projectId, filePath) {
4422
3839
  return queryOne(
4423
3840
  `SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
@@ -4428,28 +3845,6 @@ function getDocumentByPath(projectId, filePath) {
4428
3845
  [projectId, filePath]
4429
3846
  );
4430
3847
  }
4431
- function getTemplateDocuments(projectId) {
4432
- return queryAll(
4433
- `SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
4434
- has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
4435
- meta_modules, meta_dependencies, meta_code_refs, meta_authors
4436
- FROM documents
4437
- WHERE project_id = ? AND meta_status = 'TEMPLATE' AND status != 'deleted'
4438
- ORDER BY path`,
4439
- [projectId]
4440
- );
4441
- }
4442
- function searchDocumentsByTldr(projectId, searchTerm) {
4443
- return queryAll(
4444
- `SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
4445
- has_frontmatter, meta_status, meta_version, meta_tldr, meta_title,
4446
- meta_modules, meta_dependencies, meta_code_refs, meta_authors
4447
- FROM documents
4448
- WHERE project_id = ? AND meta_tldr LIKE ? AND status != 'deleted'
4449
- ORDER BY path`,
4450
- [projectId, `%${searchTerm}%`]
4451
- );
4452
- }
4453
3848
  function getUnanalyzedDocuments(projectId, limit, offset) {
4454
3849
  let query = `
4455
3850
  SELECT id, path, filename, extension, status, size_bytes, last_scanned_at,
@@ -4673,7 +4068,7 @@ function trackShadowFile(projectId, projectPath, filePath) {
4673
4068
  return getDocumentWithAnalysis(projectId, filePath) ?? null;
4674
4069
  }
4675
4070
  try {
4676
- const stats = statSync(fullPath);
4071
+ const stats = statSync2(fullPath);
4677
4072
  const hash = computeFileHash(fullPath);
4678
4073
  const filename = filePath.split("/").pop() ?? filePath;
4679
4074
  const ext = extname(filename).slice(1);
@@ -4694,11 +4089,11 @@ function trackShadowFile(projectId, projectPath, filePath) {
4694
4089
 
4695
4090
  // src/services/comment-parser.ts
4696
4091
  init_connection();
4697
- import { readFileSync as readFileSync6 } from "fs";
4092
+ import { readFileSync as readFileSync7 } from "fs";
4698
4093
  var USER_COMMENT_PATTERN = /\[\[!\s*([\s\S]*?)\s*\]\]/g;
4699
4094
  var AI_COMMENT_PATTERN = /\[\{!\s*([\s\S]*?)\s*\}]/g;
4700
4095
  function parseComments(filePath) {
4701
- const content = readFileSync6(filePath, "utf-8");
4096
+ const content = readFileSync7(filePath, "utf-8");
4702
4097
  const lines = content.split("\n");
4703
4098
  const comments = [];
4704
4099
  const positionToLine = [];
@@ -5059,10 +4454,9 @@ function startSession(projectId, name) {
5059
4454
  [projectId, "active"]
5060
4455
  );
5061
4456
  if (existing) {
5062
- run(
5063
- `UPDATE sessions SET status = 'abandoned', ended_at = datetime('now') WHERE id = ?`,
5064
- [existing.id]
5065
- );
4457
+ run(`UPDATE sessions SET status = 'abandoned', ended_at = datetime('now') WHERE id = ?`, [
4458
+ existing.id
4459
+ ]);
5066
4460
  }
5067
4461
  const sessionId = generateId();
5068
4462
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5086,20 +4480,22 @@ function startSession(projectId, name) {
5086
4480
  }
5087
4481
  function endSession(projectId, summary) {
5088
4482
  const session = queryOne(
5089
- "SELECT id, started_at, entities_modified, files_modified FROM sessions WHERE project_id = ? AND status = ?",
4483
+ "SELECT id, name, started_at, entities_modified, files_modified FROM sessions WHERE project_id = ? AND status = ?",
5090
4484
  [projectId, "active"]
5091
4485
  );
5092
4486
  if (!session) {
5093
4487
  return null;
5094
4488
  }
5095
4489
  const now = (/* @__PURE__ */ new Date()).toISOString();
5096
- run(
5097
- `UPDATE sessions SET status = 'completed', ended_at = ?, summary = ? WHERE id = ?`,
5098
- [now, summary ?? null, session.id]
5099
- );
4490
+ run(`UPDATE sessions SET status = 'completed', ended_at = ?, summary = ? WHERE id = ?`, [
4491
+ now,
4492
+ summary ?? null,
4493
+ session.id
4494
+ ]);
5100
4495
  return {
5101
4496
  id: session.id,
5102
4497
  projectId,
4498
+ name: session.name,
5103
4499
  startedAt: session.started_at,
5104
4500
  endedAt: now,
5105
4501
  summary: summary ?? null,
@@ -5109,16 +4505,14 @@ function endSession(projectId, summary) {
5109
4505
  };
5110
4506
  }
5111
4507
  function getActiveSession(projectId) {
5112
- const session = queryOne(
5113
- "SELECT * FROM sessions WHERE project_id = ? AND status = ?",
5114
- [projectId, "active"]
5115
- );
4508
+ const session = queryOne("SELECT * FROM sessions WHERE project_id = ? AND status = ?", [projectId, "active"]);
5116
4509
  if (!session) {
5117
4510
  return null;
5118
4511
  }
5119
4512
  return {
5120
4513
  id: session.id,
5121
4514
  projectId: session.project_id,
4515
+ name: session.name,
5122
4516
  startedAt: session.started_at,
5123
4517
  endedAt: session.ended_at,
5124
4518
  summary: session.summary,
@@ -5128,10 +4522,7 @@ function getActiveSession(projectId) {
5128
4522
  };
5129
4523
  }
5130
4524
  function getSessionByName(projectId, name) {
5131
- const session = queryOne(
5132
- "SELECT * FROM sessions WHERE project_id = ? AND name = ?",
5133
- [projectId, name]
5134
- );
4525
+ const session = queryOne("SELECT * FROM sessions WHERE project_id = ? AND name = ?", [projectId, name]);
5135
4526
  if (!session) {
5136
4527
  return null;
5137
4528
  }
@@ -5148,10 +4539,7 @@ function getSessionByName(projectId, name) {
5148
4539
  };
5149
4540
  }
5150
4541
  function resumeSession(projectId, sessionId) {
5151
- const session = queryOne(
5152
- "SELECT * FROM sessions WHERE id = ? AND project_id = ?",
5153
- [sessionId, projectId]
5154
- );
4542
+ const session = queryOne("SELECT * FROM sessions WHERE id = ? AND project_id = ?", [sessionId, projectId]);
5155
4543
  if (!session) {
5156
4544
  return null;
5157
4545
  }
@@ -5163,15 +4551,11 @@ function resumeSession(projectId, sessionId) {
5163
4551
  [projectId, "active", sessionId]
5164
4552
  );
5165
4553
  if (existing) {
5166
- run(
5167
- `UPDATE sessions SET status = 'abandoned', ended_at = datetime('now') WHERE id = ?`,
5168
- [existing.id]
5169
- );
4554
+ run(`UPDATE sessions SET status = 'abandoned', ended_at = datetime('now') WHERE id = ?`, [
4555
+ existing.id
4556
+ ]);
5170
4557
  }
5171
- run(
5172
- `UPDATE sessions SET status = 'active', ended_at = NULL WHERE id = ?`,
5173
- [sessionId]
5174
- );
4558
+ run(`UPDATE sessions SET status = 'active', ended_at = NULL WHERE id = ?`, [sessionId]);
5175
4559
  return {
5176
4560
  id: session.id,
5177
4561
  projectId: session.project_id,
@@ -5249,6 +4633,7 @@ function getSession(sessionId) {
5249
4633
  return {
5250
4634
  id: session.id,
5251
4635
  projectId: session.project_id,
4636
+ name: session.name,
5252
4637
  startedAt: session.started_at,
5253
4638
  endedAt: session.ended_at,
5254
4639
  summary: session.summary,
@@ -8419,321 +7804,872 @@ uxJourneyCommand.command("delete").alias("rm").argument("<key>", "Journey key").
8419
7804
  // src/commands/doc.ts
8420
7805
  init_connection();
8421
7806
  import { Command as Command19 } from "commander";
8422
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
8423
- import { join as join8 } from "path";
8424
- init_config();
8425
- var docCommand = new Command19("doc").description("Document management and frontmatter operations");
8426
- function getProjectId(opts) {
8427
- const projectRoot = findProjectRoot();
8428
- if (!projectRoot) {
8429
- error('Not in an AIGILE project. Run "aigile init" first.', opts);
8430
- return null;
7807
+ import { existsSync as existsSync7 } from "fs";
7808
+ import { resolve as resolve4, relative as relative4 } from "path";
7809
+
7810
+ // src/services/dependency-graph.ts
7811
+ init_connection();
7812
+ import { resolve as resolve3, relative as relative3, dirname as dirname3 } from "path";
7813
+ function buildDependencyGraph(projectId) {
7814
+ const docs = queryAll(
7815
+ `SELECT id, path, meta_status, last_approved_at, last_modified_at, meta_dependencies
7816
+ FROM documents
7817
+ WHERE project_id = ?`,
7818
+ [projectId]
7819
+ );
7820
+ const graph = /* @__PURE__ */ new Map();
7821
+ for (const doc of docs) {
7822
+ const dependencies = doc.meta_dependencies ? JSON.parse(doc.meta_dependencies) : [];
7823
+ graph.set(doc.path, {
7824
+ id: doc.id,
7825
+ path: doc.path,
7826
+ status: doc.meta_status || "UNKNOWN",
7827
+ lastApprovedAt: doc.last_approved_at || void 0,
7828
+ lastModifiedAt: doc.last_modified_at || void 0,
7829
+ dependencies,
7830
+ dependents: []
7831
+ });
8431
7832
  }
8432
- const config = loadProjectConfig(projectRoot);
8433
- if (!config) {
8434
- error("Could not load project config.", opts);
8435
- return null;
7833
+ for (const [path, node] of graph.entries()) {
7834
+ for (const depPath of node.dependencies) {
7835
+ const absoluteDep = resolveDocPath(path, depPath);
7836
+ const depNode = graph.get(absoluteDep);
7837
+ if (depNode) {
7838
+ depNode.dependents.push(path);
7839
+ }
7840
+ }
8436
7841
  }
8437
- const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
8438
- if (!project) {
8439
- error(`Project "${config.project.key}" not found in database.`, opts);
8440
- return null;
7842
+ return graph;
7843
+ }
7844
+ function resolveDocPath(fromPath, relativePath) {
7845
+ if (relativePath.startsWith("./") || relativePath.startsWith("../")) {
7846
+ const fromDir = dirname3(fromPath);
7847
+ const resolved = resolve3(fromDir, relativePath);
7848
+ return relative3(".", resolved);
8441
7849
  }
8442
- return { projectId: project.id, projectRoot };
7850
+ return relativePath;
8443
7851
  }
8444
- function formatDocForDisplay(doc) {
7852
+ function checkStaleness(docPath, graph) {
7853
+ const doc = graph.get(docPath);
7854
+ if (!doc) {
7855
+ return {
7856
+ isStale: false,
7857
+ staleDependencies: []
7858
+ };
7859
+ }
7860
+ if (!doc.lastApprovedAt) {
7861
+ return {
7862
+ isStale: false,
7863
+ staleDependencies: []
7864
+ };
7865
+ }
7866
+ const staleDeps = [];
7867
+ for (const depRelPath of doc.dependencies) {
7868
+ const depAbsPath = resolveDocPath(docPath, depRelPath);
7869
+ const depNode = graph.get(depAbsPath);
7870
+ if (!depNode || !depNode.lastModifiedAt) {
7871
+ continue;
7872
+ }
7873
+ if (depNode.lastModifiedAt > doc.lastApprovedAt) {
7874
+ staleDeps.push({
7875
+ path: depRelPath,
7876
+ lastModified: depNode.lastModifiedAt,
7877
+ parentApproved: doc.lastApprovedAt
7878
+ });
7879
+ }
7880
+ }
7881
+ const isStale = staleDeps.length > 0;
8445
7882
  return {
8446
- path: doc.path,
8447
- filename: doc.filename,
8448
- extension: doc.extension,
8449
- sync_status: doc.status,
8450
- has_frontmatter: doc.has_frontmatter ? "Yes" : "No",
8451
- meta_status: doc.meta_status ?? "-",
8452
- meta_version: doc.meta_version ?? "-",
8453
- meta_tldr: doc.meta_tldr ?? "-",
8454
- meta_modules: doc.meta_modules ? JSON.parse(doc.meta_modules).join(", ") : "-",
8455
- meta_dependencies: doc.meta_dependencies ? JSON.parse(doc.meta_dependencies).join(", ") : "-"
7883
+ isStale,
7884
+ reason: isStale ? `${staleDeps.length} dependencies modified since approval` : void 0,
7885
+ staleDependencies: staleDeps
8456
7886
  };
8457
7887
  }
8458
- docCommand.command("list").alias("ls").option("--meta-status <status>", "Filter by frontmatter status (DRAFT, IN-REVIEW, APPROVED, TEMPLATE)").option("--module <module>", "Filter by module").option("--with-frontmatter", "Only show documents with frontmatter").option("--without-frontmatter", "Only show documents without frontmatter (shadow mode)").option("--templates", "Only show TEMPLATE documents").option("--search <term>", "Search by tldr content").description("List documents with metadata filters").action((options) => {
8459
- const opts = getOutputOptions(docCommand);
8460
- const ctx = getProjectId(opts);
8461
- if (!ctx) {
7888
+ function checkCascadeApproval(docPath, graph) {
7889
+ const doc = graph.get(docPath);
7890
+ if (!doc) {
7891
+ return {
7892
+ canCascade: false,
7893
+ affectedDocs: [],
7894
+ blockedBy: []
7895
+ };
7896
+ }
7897
+ const canApprove = [];
7898
+ const blocked = [];
7899
+ for (const dependentPath of doc.dependents) {
7900
+ const dependent = graph.get(dependentPath);
7901
+ if (!dependent) {
7902
+ continue;
7903
+ }
7904
+ const allDepsApproved = dependent.dependencies.every((depRelPath) => {
7905
+ const depAbsPath = resolveDocPath(dependentPath, depRelPath);
7906
+ const depNode = graph.get(depAbsPath);
7907
+ if (depAbsPath === docPath) {
7908
+ return true;
7909
+ }
7910
+ return depNode && depNode.status === "APPROVED";
7911
+ });
7912
+ if (allDepsApproved) {
7913
+ canApprove.push(dependentPath);
7914
+ } else {
7915
+ blocked.push(dependentPath);
7916
+ }
7917
+ }
7918
+ return {
7919
+ canCascade: canApprove.length > 0,
7920
+ affectedDocs: canApprove,
7921
+ blockedBy: blocked
7922
+ };
7923
+ }
7924
+ function getTopologicalOrder(graph) {
7925
+ const visited = /* @__PURE__ */ new Set();
7926
+ const result = [];
7927
+ function visit(path) {
7928
+ if (visited.has(path)) {
7929
+ return;
7930
+ }
7931
+ visited.add(path);
7932
+ const node = graph.get(path);
7933
+ if (!node) {
7934
+ return;
7935
+ }
7936
+ for (const depRelPath of node.dependencies) {
7937
+ const depAbsPath = resolveDocPath(path, depRelPath);
7938
+ visit(depAbsPath);
7939
+ }
7940
+ result.push(path);
7941
+ }
7942
+ for (const path of graph.keys()) {
7943
+ visit(path);
7944
+ }
7945
+ return result;
7946
+ }
7947
+ function getFoundationalDocs(graph) {
7948
+ const foundational = [];
7949
+ for (const [path, node] of graph.entries()) {
7950
+ if (node.dependencies.length === 0) {
7951
+ foundational.push(path);
7952
+ }
7953
+ }
7954
+ return foundational;
7955
+ }
7956
+ function getDirectDependents(docPath, graph) {
7957
+ const doc = graph.get(docPath);
7958
+ return doc ? [...doc.dependents] : [];
7959
+ }
7960
+ function getTransitiveDependents(docPath, graph) {
7961
+ const visited = /* @__PURE__ */ new Set();
7962
+ const result = [];
7963
+ function visit(path) {
7964
+ if (visited.has(path)) {
7965
+ return;
7966
+ }
7967
+ visited.add(path);
7968
+ const node = graph.get(path);
7969
+ if (!node) {
7970
+ return;
7971
+ }
7972
+ for (const dependentPath of node.dependents) {
7973
+ result.push(dependentPath);
7974
+ visit(dependentPath);
7975
+ }
7976
+ }
7977
+ visit(docPath);
7978
+ return result;
7979
+ }
7980
+
7981
+ // src/commands/doc.ts
7982
+ init_config();
7983
+ init_config();
7984
+ import { writeFileSync as writeFileSync5, readFileSync as readFileSync8 } from "fs";
7985
+ async function ensureProjectContext() {
7986
+ const projectPath = findProjectRoot();
7987
+ if (!projectPath) {
7988
+ throw new Error('Not in an AIGILE project. Run "aigile init" first.');
7989
+ }
7990
+ const config = loadProjectConfig(projectPath);
7991
+ if (!config) {
7992
+ throw new Error("Could not load project config.");
7993
+ }
7994
+ const project = queryOne("SELECT id FROM projects WHERE key = ?", [
7995
+ config.project.key
7996
+ ]);
7997
+ if (!project) {
7998
+ throw new Error(`Project "${config.project.key}" not found in database.`);
7999
+ }
8000
+ return { projectId: project.id, projectPath, projectKey: config.project.key };
8001
+ }
8002
+ var integrityCommand = new Command19("integrity").description("Check document integrity and dependencies").option("--fix", "Auto-fix missing dependencies in frontmatter").option("--all", "Show all documents, not just problems").action(async (options) => {
8003
+ const opts = getOutputOptions(integrityCommand);
8004
+ const cwd = process.cwd();
8005
+ try {
8006
+ await runIntegrityCheck(cwd, options, opts);
8007
+ } catch (err) {
8008
+ error(err instanceof Error ? err.message : String(err), opts);
8462
8009
  process.exit(1);
8463
8010
  }
8464
- let documents = [];
8465
- if (options.metaStatus) {
8466
- documents = getDocumentsByMetaStatus(ctx.projectId, options.metaStatus);
8467
- } else if (options.module) {
8468
- documents = getDocumentsByModule(ctx.projectId, options.module);
8469
- } else if (options.withFrontmatter) {
8470
- documents = getDocumentsWithFrontmatter(ctx.projectId);
8471
- } else if (options.withoutFrontmatter) {
8472
- documents = getDocumentsWithoutFrontmatter(ctx.projectId);
8473
- } else if (options.templates) {
8474
- documents = getTemplateDocuments(ctx.projectId);
8475
- } else if (options.search) {
8476
- documents = searchDocumentsByTldr(ctx.projectId, options.search);
8477
- } else {
8478
- documents = getDocumentsWithFrontmatter(ctx.projectId);
8011
+ });
8012
+ var approveCommand = new Command19("approve").description("Approve a document").argument("<file>", "File path to approve").option("--cascade", "Auto-approve dependents if all their dependencies are approved").action(async (filePath, options) => {
8013
+ const opts = getOutputOptions(approveCommand);
8014
+ const cwd = process.cwd();
8015
+ try {
8016
+ await approveDocument(cwd, filePath, options, opts);
8017
+ } catch (err) {
8018
+ error(err instanceof Error ? err.message : String(err), opts);
8019
+ process.exit(1);
8479
8020
  }
8480
- const displayDocs = documents.map(formatDocForDisplay);
8481
- data(
8482
- displayDocs,
8483
- [
8484
- { header: "Path", key: "path", width: 45 },
8485
- { header: "Status", key: "meta_status", width: 12 },
8486
- { header: "Version", key: "meta_version", width: 8 },
8487
- { header: "Modules", key: "meta_modules", width: 20 }
8488
- ],
8489
- opts
8490
- );
8491
8021
  });
8492
- docCommand.command("show <path>").description("Show document details including frontmatter metadata").action((filePath) => {
8493
- const opts = getOutputOptions(docCommand);
8494
- const ctx = getProjectId(opts);
8495
- if (!ctx) {
8022
+ var ackCommand = new Command19("ack").description("Acknowledge document changes (clears STALE)").argument("<file>", "File path to acknowledge").action(async (filePath) => {
8023
+ const opts = getOutputOptions(ackCommand);
8024
+ const cwd = process.cwd();
8025
+ try {
8026
+ await acknowledgeDocument(cwd, filePath, opts);
8027
+ } catch (err) {
8028
+ error(err instanceof Error ? err.message : String(err), opts);
8496
8029
  process.exit(1);
8497
8030
  }
8498
- const doc = getDocumentByPath(ctx.projectId, filePath);
8499
- if (!doc) {
8500
- error(`Document not found: ${filePath}`, opts);
8031
+ });
8032
+ var readyCommand = new Command19("ready").description("Mark document as ready for review").argument("<file>", "File path to mark ready").action(async (filePath) => {
8033
+ const opts = getOutputOptions(readyCommand);
8034
+ const cwd = process.cwd();
8035
+ try {
8036
+ await markReady(cwd, filePath, opts);
8037
+ } catch (err) {
8038
+ error(err instanceof Error ? err.message : String(err), opts);
8501
8039
  process.exit(1);
8502
8040
  }
8503
- const fullPath = join8(ctx.projectRoot, filePath);
8504
- const parsed = parseFrontmatterFromFile(fullPath);
8505
- if (opts.json) {
8506
- console.log(JSON.stringify({
8507
- success: true,
8508
- data: {
8509
- database: doc,
8510
- frontmatter: parsed ? {
8511
- raw: parsed.raw,
8512
- metadata: parsed.metadata
8513
- } : null
8514
- }
8515
- }));
8516
- } else {
8517
- const displayData = {
8518
- path: doc.path,
8519
- filename: doc.filename,
8520
- extension: doc.extension,
8521
- sync_status: doc.status,
8522
- size_bytes: doc.size_bytes,
8523
- last_scanned: doc.last_scanned_at,
8524
- has_frontmatter: doc.has_frontmatter ? "Yes" : "No"
8525
- };
8526
- if (parsed) {
8527
- displayData.meta_status = parsed.metadata.status ?? "-";
8528
- displayData.meta_version = parsed.metadata.version ?? "-";
8529
- displayData.meta_tldr = parsed.metadata.tldr ?? "-";
8530
- displayData.meta_title = parsed.metadata.title ?? "-";
8531
- displayData.meta_modules = parsed.metadata.modules?.join(", ") ?? "-";
8532
- displayData.meta_dependencies = parsed.metadata.dependencies?.join(", ") ?? "-";
8533
- displayData.meta_code_refs = parsed.metadata.code_refs?.join(", ") ?? "-";
8534
- displayData.meta_authors = parsed.metadata.authors?.join(", ") ?? "-";
8535
- }
8536
- details(
8537
- displayData,
8538
- [
8539
- { label: "Path", key: "path" },
8540
- { label: "Filename", key: "filename" },
8541
- { label: "Extension", key: "extension" },
8542
- { label: "Sync Status", key: "sync_status" },
8543
- { label: "Size (bytes)", key: "size_bytes" },
8544
- { label: "Last Scanned", key: "last_scanned" },
8545
- { label: "Has Frontmatter", key: "has_frontmatter" },
8546
- { label: "Meta Status", key: "meta_status" },
8547
- { label: "Meta Version", key: "meta_version" },
8548
- { label: "Meta Title", key: "meta_title" },
8549
- { label: "Meta TLDR", key: "meta_tldr" },
8550
- { label: "Meta Modules", key: "meta_modules" },
8551
- { label: "Meta Dependencies", key: "meta_dependencies" },
8552
- { label: "Meta Code Refs", key: "meta_code_refs" },
8553
- { label: "Meta Authors", key: "meta_authors" }
8554
- ],
8555
- opts
8556
- );
8041
+ });
8042
+ var reviewCommand = new Command19("review").description("Interactive document review (TUI)").action(async () => {
8043
+ const opts = getOutputOptions(reviewCommand);
8044
+ warning("Interactive review TUI not yet implemented", opts);
8045
+ info("Use 'aigile doc integrity' to see document status", opts);
8046
+ });
8047
+ var exceptionCommand = new Command19("exception").description("Mark document with structural exception").argument("<file>", "File path").argument("<reason>", "Reason for exception").action(async (filePath, reason) => {
8048
+ const opts = getOutputOptions(exceptionCommand);
8049
+ const cwd = process.cwd();
8050
+ try {
8051
+ await markException(cwd, filePath, reason, opts);
8052
+ } catch (err) {
8053
+ error(err instanceof Error ? err.message : String(err), opts);
8054
+ process.exit(1);
8557
8055
  }
8558
8056
  });
8559
- docCommand.command("update <path>").option("--status <status>", "Set metadata status (DRAFT, IN-REVIEW, APPROVED, TEMPLATE, PRODUCTION)").option("--version <version>", "Set metadata version").option("--tldr <tldr>", "Set one-sentence summary").option("--title <title>", "Set document title").option("--add-module <module>", "Add a module to the modules list").option("--add-dependency <dep>", "Add a dependency path").option("--add-code-ref <ref>", "Add a code reference path").option("--add-author <author>", "Add an author").option("--dry-run", "Show what would be changed without modifying file").description("Update frontmatter metadata in a document").action((filePath, options) => {
8560
- const opts = getOutputOptions(docCommand);
8561
- const ctx = getProjectId(opts);
8562
- if (!ctx) {
8057
+ var refactorCommand = new Command19("refactor").description("Mark document for refactoring").argument("<file>", "File path").action(async (filePath) => {
8058
+ const opts = getOutputOptions(refactorCommand);
8059
+ const cwd = process.cwd();
8060
+ try {
8061
+ await markRefactoring(cwd, filePath, opts);
8062
+ } catch (err) {
8063
+ error(err instanceof Error ? err.message : String(err), opts);
8563
8064
  process.exit(1);
8564
8065
  }
8565
- const fullPath = join8(ctx.projectRoot, filePath);
8566
- let content;
8066
+ });
8067
+ var archiveCommand = new Command19("archive").description("Archive a document").argument("<file>", "File path to archive").action(async (filePath) => {
8068
+ const opts = getOutputOptions(archiveCommand);
8069
+ const cwd = process.cwd();
8567
8070
  try {
8568
- content = readFileSync7(fullPath, "utf-8");
8569
- } catch {
8570
- error(`Could not read file: ${filePath}`, opts);
8071
+ await archiveDocument(cwd, filePath, opts);
8072
+ } catch (err) {
8073
+ error(err instanceof Error ? err.message : String(err), opts);
8571
8074
  process.exit(1);
8572
8075
  }
8573
- const updates = {};
8574
- if (options.status) {
8575
- updates.status = options.status;
8076
+ });
8077
+ async function runIntegrityCheck(cwd, options, opts) {
8078
+ const config = loadProjectConfig(cwd);
8079
+ if (!config?.project?.key) {
8080
+ throw new Error("AIGILE not initialized in this directory");
8576
8081
  }
8577
- if (options.version) {
8578
- updates.version = options.version;
8082
+ const projectId = config.project.key;
8083
+ const graph = buildDependencyGraph(projectId);
8084
+ blank();
8085
+ header("Document Integrity Check", opts);
8086
+ blank();
8087
+ const orderedDocs = getTopologicalOrder(graph);
8088
+ let issueCount = 0;
8089
+ const stale = [];
8090
+ const needsReview = [];
8091
+ const templates = [];
8092
+ for (const docPath of orderedDocs) {
8093
+ const node = graph.get(docPath);
8094
+ if (!node) continue;
8095
+ const stalenessCheck = checkStaleness(docPath, graph);
8096
+ const hasIssues = stalenessCheck.isStale || node.status === "NEEDS_REVIEW" || node.status === "TEMPLATE";
8097
+ if (!hasIssues && !options.all) {
8098
+ continue;
8099
+ }
8100
+ console.log(`\u{1F4C4} ${docPath}`);
8101
+ console.log(` Status: ${node.status}`);
8102
+ if (stalenessCheck.isStale) {
8103
+ console.log(` \u26A0\uFE0F STALE: ${stalenessCheck.reason}`);
8104
+ stale.push(docPath);
8105
+ issueCount++;
8106
+ }
8107
+ if (node.status === "NEEDS_REVIEW") {
8108
+ console.log(` \u{1F4DD} Needs review`);
8109
+ needsReview.push(docPath);
8110
+ issueCount++;
8111
+ }
8112
+ if (node.status === "TEMPLATE") {
8113
+ console.log(` \u{1F4CB} Template (needs filling)`);
8114
+ templates.push(docPath);
8115
+ issueCount++;
8116
+ }
8117
+ if (node.dependencies.length > 0) {
8118
+ console.log(` Dependencies: ${node.dependencies.join(", ")}`);
8119
+ }
8120
+ if (node.dependents.length > 0) {
8121
+ console.log(` Dependents: ${node.dependents.length} files`);
8122
+ }
8123
+ blank();
8579
8124
  }
8580
- if (options.tldr) {
8581
- updates.tldr = options.tldr;
8125
+ blank();
8126
+ header("Summary", opts);
8127
+ console.log(` Total documents: ${orderedDocs.length}`);
8128
+ console.log(` Issues found: ${issueCount}`);
8129
+ if (stale.length > 0) {
8130
+ console.log(` - Stale: ${stale.length}`);
8582
8131
  }
8583
- if (options.title) {
8584
- updates.title = options.title;
8132
+ if (needsReview.length > 0) {
8133
+ console.log(` - Needs review: ${needsReview.length}`);
8585
8134
  }
8586
- if (options.addModule) {
8587
- updates.modules = [options.addModule];
8135
+ if (templates.length > 0) {
8136
+ console.log(` - Templates: ${templates.length}`);
8588
8137
  }
8589
- if (options.addDependency) {
8590
- updates.dependencies = [options.addDependency];
8138
+ blank();
8139
+ if (issueCount === 0) {
8140
+ success("\u2713 All documents are up to date", opts);
8141
+ } else {
8142
+ info("Run 'aigile doc approve <file>' to approve documents", opts);
8143
+ info("Run 'aigile doc ack <file>' to acknowledge changes", opts);
8144
+ }
8145
+ }
8146
+ async function approveDocument(cwd, filePath, options, opts) {
8147
+ const config = loadProjectConfig(cwd);
8148
+ if (!config?.project?.key) {
8149
+ throw new Error("AIGILE not initialized in this directory");
8591
8150
  }
8592
- if (options.addCodeRef) {
8593
- updates.code_refs = [options.addCodeRef];
8151
+ const absolutePath = resolve4(cwd, filePath);
8152
+ if (!existsSync7(absolutePath)) {
8153
+ throw new Error(`File not found: ${filePath}`);
8594
8154
  }
8595
- if (options.addAuthor) {
8596
- updates.authors = [options.addAuthor];
8155
+ const projectId = config.project.key;
8156
+ const relativePath = relative4(cwd, absolutePath);
8157
+ const doc = queryOne(
8158
+ `SELECT id, meta_status FROM documents WHERE project_id = ? AND path = ?`,
8159
+ [projectId, relativePath]
8160
+ );
8161
+ if (!doc) {
8162
+ throw new Error(`Document not found in database: ${filePath}`);
8163
+ }
8164
+ const content = readFileSync8(absolutePath, "utf-8");
8165
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
8166
+ const updatedContent = updateFrontmatterContent(content, {
8167
+ status: "APPROVED",
8168
+ reviewed: now
8169
+ });
8170
+ writeFileSync5(absolutePath, updatedContent, "utf-8");
8171
+ run(`UPDATE documents SET meta_status = ?, last_approved_at = ?, reviewed_at = ? WHERE id = ?`, [
8172
+ "APPROVED",
8173
+ now,
8174
+ now,
8175
+ doc.id
8176
+ ]);
8177
+ success(`\u2713 Approved: ${filePath}`, opts);
8178
+ if (options.cascade) {
8179
+ const graph = buildDependencyGraph(projectId);
8180
+ const cascadeResult = checkCascadeApproval(relativePath, graph);
8181
+ if (cascadeResult.canCascade && cascadeResult.affectedDocs.length > 0) {
8182
+ blank();
8183
+ info(`Cascade approving ${cascadeResult.affectedDocs.length} dependents...`, opts);
8184
+ for (const depPath of cascadeResult.affectedDocs) {
8185
+ const depAbsolute = resolve4(cwd, depPath);
8186
+ const depContent = readFileSync8(depAbsolute, "utf-8");
8187
+ const updatedDep = updateFrontmatterContent(depContent, {
8188
+ status: "APPROVED",
8189
+ reviewed: now
8190
+ });
8191
+ writeFileSync5(depAbsolute, updatedDep, "utf-8");
8192
+ const depDoc = queryOne(
8193
+ `SELECT id FROM documents WHERE project_id = ? AND path = ?`,
8194
+ [projectId, depPath]
8195
+ );
8196
+ if (depDoc) {
8197
+ run(
8198
+ `UPDATE documents SET meta_status = ?, last_approved_at = ?, reviewed_at = ? WHERE id = ?`,
8199
+ ["APPROVED", now, now, depDoc.id]
8200
+ );
8201
+ }
8202
+ success(` \u2713 ${depPath}`, opts);
8203
+ }
8204
+ }
8597
8205
  }
8598
- if (Object.keys(updates).length === 0) {
8599
- error("No updates specified. Use --status, --version, --tldr, etc.", opts);
8206
+ }
8207
+ async function acknowledgeDocument(cwd, filePath, opts) {
8208
+ const config = loadProjectConfig(cwd);
8209
+ if (!config?.project?.key) {
8210
+ throw new Error("AIGILE not initialized in this directory");
8211
+ }
8212
+ const absolutePath = resolve4(cwd, filePath);
8213
+ if (!existsSync7(absolutePath)) {
8214
+ throw new Error(`File not found: ${filePath}`);
8215
+ }
8216
+ const projectId = config.project.key;
8217
+ const relativePath = relative4(cwd, absolutePath);
8218
+ const doc = queryOne(
8219
+ `SELECT id FROM documents WHERE project_id = ? AND path = ?`,
8220
+ [projectId, relativePath]
8221
+ );
8222
+ if (!doc) {
8223
+ throw new Error(`Document not found in database: ${filePath}`);
8224
+ }
8225
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
8226
+ run(`UPDATE documents SET last_approved_at = ? WHERE id = ?`, [now, doc.id]);
8227
+ success(`\u2713 Acknowledged: ${filePath}`, opts);
8228
+ info("Document is no longer marked as stale", opts);
8229
+ }
8230
+ async function markReady(cwd, filePath, opts) {
8231
+ const config = loadProjectConfig(cwd);
8232
+ if (!config?.project?.key) {
8233
+ throw new Error("AIGILE not initialized in this directory");
8234
+ }
8235
+ const absolutePath = resolve4(cwd, filePath);
8236
+ if (!existsSync7(absolutePath)) {
8237
+ throw new Error(`File not found: ${filePath}`);
8238
+ }
8239
+ const projectId = config.project.key;
8240
+ const relativePath = relative4(cwd, absolutePath);
8241
+ const content = readFileSync8(absolutePath, "utf-8");
8242
+ const updatedContent = updateFrontmatterContent(content, {
8243
+ status: "NEEDS_REVIEW"
8244
+ });
8245
+ writeFileSync5(absolutePath, updatedContent, "utf-8");
8246
+ const doc = queryOne(
8247
+ `SELECT id FROM documents WHERE project_id = ? AND path = ?`,
8248
+ [projectId, relativePath]
8249
+ );
8250
+ if (doc) {
8251
+ run(`UPDATE documents SET meta_status = ?, needs_review = 1 WHERE id = ?`, [
8252
+ "NEEDS_REVIEW",
8253
+ doc.id
8254
+ ]);
8255
+ }
8256
+ success(`\u2713 Marked ready for review: ${filePath}`, opts);
8257
+ }
8258
+ async function markException(cwd, filePath, reason, opts) {
8259
+ const config = loadProjectConfig(cwd);
8260
+ if (!config?.project?.key) {
8261
+ throw new Error("AIGILE not initialized in this directory");
8262
+ }
8263
+ const absolutePath = resolve4(cwd, filePath);
8264
+ if (!existsSync7(absolutePath)) {
8265
+ throw new Error(`File not found: ${filePath}`);
8266
+ }
8267
+ const projectId = config.project.key;
8268
+ const relativePath = relative4(cwd, absolutePath);
8269
+ const content = readFileSync8(absolutePath, "utf-8");
8270
+ const updatedContent = updateFrontmatterContent(content, {
8271
+ exception: reason
8272
+ });
8273
+ writeFileSync5(absolutePath, updatedContent, "utf-8");
8274
+ const doc = queryOne(
8275
+ `SELECT id FROM documents WHERE project_id = ? AND path = ?`,
8276
+ [projectId, relativePath]
8277
+ );
8278
+ if (doc) {
8279
+ run(`UPDATE documents SET meta_exception = ? WHERE id = ?`, [reason, doc.id]);
8280
+ }
8281
+ success(`\u2713 Marked exception: ${filePath}`, opts);
8282
+ info(`Reason: ${reason}`, opts);
8283
+ }
8284
+ async function markRefactoring(cwd, filePath, opts) {
8285
+ const config = loadProjectConfig(cwd);
8286
+ if (!config?.project?.key) {
8287
+ throw new Error("AIGILE not initialized in this directory");
8288
+ }
8289
+ const absolutePath = resolve4(cwd, filePath);
8290
+ if (!existsSync7(absolutePath)) {
8291
+ throw new Error(`File not found: ${filePath}`);
8292
+ }
8293
+ const projectId = config.project.key;
8294
+ const relativePath = relative4(cwd, absolutePath);
8295
+ const doc = queryOne(
8296
+ `SELECT id FROM documents WHERE project_id = ? AND path = ?`,
8297
+ [projectId, relativePath]
8298
+ );
8299
+ if (!doc) {
8300
+ throw new Error(`Document not found in database: ${filePath}`);
8301
+ }
8302
+ run(`UPDATE documents SET monitoring_category = ? WHERE id = ?`, [
8303
+ "REFACTORING_REQUIRED",
8304
+ doc.id
8305
+ ]);
8306
+ success(`\u2713 Marked for refactoring: ${filePath}`, opts);
8307
+ info("This file needs to be moved or restructured", opts);
8308
+ }
8309
+ async function archiveDocument(cwd, filePath, opts) {
8310
+ const config = loadProjectConfig(cwd);
8311
+ if (!config?.project?.key) {
8312
+ throw new Error("AIGILE not initialized in this directory");
8313
+ }
8314
+ const absolutePath = resolve4(cwd, filePath);
8315
+ if (!existsSync7(absolutePath)) {
8316
+ throw new Error(`File not found: ${filePath}`);
8317
+ }
8318
+ const projectId = config.project.key;
8319
+ const relativePath = relative4(cwd, absolutePath);
8320
+ const content = readFileSync8(absolutePath, "utf-8");
8321
+ const updatedContent = updateFrontmatterContent(content, {
8322
+ status: "ARCHIVED"
8323
+ });
8324
+ writeFileSync5(absolutePath, updatedContent, "utf-8");
8325
+ const doc = queryOne(
8326
+ `SELECT id FROM documents WHERE project_id = ? AND path = ?`,
8327
+ [projectId, relativePath]
8328
+ );
8329
+ if (doc) {
8330
+ run(`UPDATE documents SET meta_status = ? WHERE id = ?`, ["ARCHIVED", doc.id]);
8331
+ }
8332
+ success(`\u2713 Archived: ${filePath}`, opts);
8333
+ info("Document moved to archived status", opts);
8334
+ }
8335
+ var dependentsCommand = new Command19("dependents").description("Show documents that depend on the specified file").argument("<file>", "Document path (relative to project root)").option("--depth <n>", "Maximum depth to traverse (default: unlimited)", parseInt).option("--json", "Output as JSON").action(async (file, opts) => {
8336
+ const { projectId, projectPath } = await ensureProjectContext();
8337
+ const filePath = file.startsWith(".aigile/") ? file : `.aigile/${file}`;
8338
+ const graph = buildDependencyGraph(projectId);
8339
+ const doc = graph.get(filePath);
8340
+ if (!doc) {
8341
+ error(`Document not found: ${filePath}`);
8600
8342
  process.exit(1);
8601
8343
  }
8602
- const newContent = updateFrontmatterContent(content, updates);
8603
- if (options.dryRun) {
8344
+ const directDependents = getDirectDependents(filePath, graph);
8345
+ const allDependents = opts.depth === 1 ? directDependents : getTransitiveDependents(filePath, graph);
8346
+ if (opts.json) {
8347
+ console.log(
8348
+ JSON.stringify(
8349
+ {
8350
+ document: filePath,
8351
+ directDependents,
8352
+ allDependents,
8353
+ count: allDependents.length
8354
+ },
8355
+ null,
8356
+ 2
8357
+ )
8358
+ );
8359
+ return;
8360
+ }
8361
+ success(`Dependents of: ${filePath}`, opts);
8362
+ console.log();
8363
+ if (allDependents.length === 0) {
8364
+ info("No dependents found (this is a leaf document)", opts);
8365
+ return;
8366
+ }
8367
+ console.log(`Total: ${allDependents.length} dependent(s)`);
8368
+ console.log();
8369
+ for (const depPath of allDependents) {
8370
+ const depDoc = graph.get(depPath);
8371
+ const tldr = depDoc?.status ? `[${depDoc.status}]` : "";
8372
+ console.log(` \u2022 ${depPath} ${tldr}`);
8373
+ }
8374
+ });
8375
+ var dependenciesCommand = new Command19("dependencies").description("Show documents that the specified file depends on").argument("<file>", "Document path (relative to project root)").option("--json", "Output as JSON").action(async (file, opts) => {
8376
+ const { projectId, projectPath } = await ensureProjectContext();
8377
+ const filePath = file.startsWith(".aigile/") ? file : `.aigile/${file}`;
8378
+ const graph = buildDependencyGraph(projectId);
8379
+ const doc = graph.get(filePath);
8380
+ if (!doc) {
8381
+ error(`Document not found: ${filePath}`);
8382
+ process.exit(1);
8383
+ }
8384
+ const dependencies = doc.dependencies || [];
8385
+ if (opts.json) {
8386
+ console.log(
8387
+ JSON.stringify(
8388
+ {
8389
+ document: filePath,
8390
+ dependencies,
8391
+ count: dependencies.length
8392
+ },
8393
+ null,
8394
+ 2
8395
+ )
8396
+ );
8397
+ return;
8398
+ }
8399
+ success(`Dependencies of: ${filePath}`, opts);
8400
+ console.log();
8401
+ if (dependencies.length === 0) {
8402
+ info("No dependencies (this is a foundational document)", opts);
8403
+ return;
8404
+ }
8405
+ console.log(
8406
+ `Total: ${dependencies.length} dependenc${dependencies.length === 1 ? "y" : "ies"}`
8407
+ );
8408
+ console.log();
8409
+ for (const depPath of dependencies) {
8410
+ const depDoc = graph.get(depPath);
8411
+ const tldr = depDoc?.status ? `[${depDoc.status}]` : "";
8412
+ console.log(` \u2022 ${depPath} ${tldr}`);
8413
+ }
8414
+ });
8415
+ var nextTemplateCommand = new Command19("next-template").description("Find the next template document to fill (foundation-first)").option("--json", "Output as JSON").action(async (opts) => {
8416
+ const { projectId } = await ensureProjectContext();
8417
+ const templates = queryAll(
8418
+ `SELECT path, meta_tldr, meta_dependencies
8419
+ FROM documents
8420
+ WHERE project_id = ? AND meta_status = 'TEMPLATE'
8421
+ ORDER BY path ASC`,
8422
+ [projectId]
8423
+ );
8424
+ if (templates.length === 0) {
8604
8425
  if (opts.json) {
8605
- console.log(JSON.stringify({
8606
- success: true,
8607
- dryRun: true,
8608
- updates,
8609
- preview: newContent.substring(0, 500) + (newContent.length > 500 ? "..." : "")
8610
- }));
8611
- } else {
8612
- info("Dry run - would update frontmatter with:", opts);
8613
- console.log(JSON.stringify(updates, null, 2));
8614
- console.log("\nNew frontmatter preview:");
8615
- const lines = newContent.split("\n");
8616
- const endIndex = lines.findIndex((l, i) => i > 0 && l === "---");
8617
- lines.slice(0, endIndex + 1).forEach((l) => console.log(l));
8426
+ console.log(JSON.stringify({ nextTemplate: null, remaining: 0 }, null, 2));
8427
+ return;
8618
8428
  }
8429
+ success("\u2713 No templates remaining - all documents filled!", opts);
8619
8430
  return;
8620
8431
  }
8621
- try {
8622
- writeFileSync5(fullPath, newContent, "utf-8");
8623
- success(`Updated frontmatter in ${filePath}`, opts);
8624
- if (!opts.json) {
8625
- info('Run "aigile sync scan" to update the database.', opts);
8432
+ const graph = buildDependencyGraph(projectId);
8433
+ const foundational = getFoundationalDocs(graph).filter((path) => {
8434
+ const doc = graph.get(path);
8435
+ return doc?.status === "TEMPLATE";
8436
+ });
8437
+ const nextTemplate = foundational[0] || templates[0].path;
8438
+ const nextDoc = graph.get(nextTemplate);
8439
+ if (opts.json) {
8440
+ console.log(
8441
+ JSON.stringify(
8442
+ {
8443
+ nextTemplate,
8444
+ tldr: nextDoc?.status || null,
8445
+ remaining: templates.length,
8446
+ foundational: foundational.length
8447
+ },
8448
+ null,
8449
+ 2
8450
+ )
8451
+ );
8452
+ return;
8453
+ }
8454
+ success(`Next template to fill: ${nextTemplate}`, opts);
8455
+ if (nextDoc?.status) {
8456
+ info(`TLDR: ${nextDoc.status}`, opts);
8457
+ }
8458
+ console.log();
8459
+ info(`Remaining templates: ${templates.length}`, opts);
8460
+ info(`Foundational templates: ${foundational.length}`, opts);
8461
+ });
8462
+ var statsCommand = new Command19("stats").description("Show document statistics and progress").option("--json", "Output as JSON").action(async (opts) => {
8463
+ const { projectId } = await ensureProjectContext();
8464
+ const statusCounts = queryAll(
8465
+ `SELECT meta_status, COUNT(*) as count
8466
+ FROM documents
8467
+ WHERE project_id = ? AND meta_status IS NOT NULL
8468
+ GROUP BY meta_status`,
8469
+ [projectId]
8470
+ );
8471
+ const stats = {};
8472
+ let total = 0;
8473
+ for (const row of statusCounts) {
8474
+ stats[row.meta_status || "UNKNOWN"] = row.count;
8475
+ total += row.count;
8476
+ }
8477
+ const graph = buildDependencyGraph(projectId);
8478
+ let staleCount = 0;
8479
+ for (const [path] of graph) {
8480
+ const staleness = checkStaleness(path, graph);
8481
+ if (staleness.isStale) {
8482
+ staleCount++;
8626
8483
  }
8627
- } catch (err) {
8628
- error(`Could not write file: ${filePath}`, opts);
8629
- process.exit(1);
8630
8484
  }
8485
+ if (opts.json) {
8486
+ console.log(
8487
+ JSON.stringify(
8488
+ {
8489
+ total,
8490
+ byStatus: stats,
8491
+ stale: staleCount,
8492
+ completion: total > 0 ? Math.round((stats.APPROVED || 0) / total * 100) : 0
8493
+ },
8494
+ null,
8495
+ 2
8496
+ )
8497
+ );
8498
+ return;
8499
+ }
8500
+ success("Document Statistics", opts);
8501
+ console.log();
8502
+ console.log(`Total Documents: ${total}`);
8503
+ console.log();
8504
+ if (Object.keys(stats).length > 0) {
8505
+ console.log("By Status:");
8506
+ for (const [status, count] of Object.entries(stats)) {
8507
+ const pct = total > 0 ? (count / total * 100).toFixed(1) : "0.0";
8508
+ console.log(` ${status.padEnd(15)} ${count.toString().padStart(4)} (${pct}%)`);
8509
+ }
8510
+ console.log();
8511
+ }
8512
+ if (staleCount > 0) {
8513
+ warning(`\u26A0 ${staleCount} document(s) are stale (dependencies changed)`, opts);
8514
+ }
8515
+ const approved = stats.APPROVED || 0;
8516
+ const completion = total > 0 ? (approved / total * 100).toFixed(1) : "0.0";
8517
+ info(`Completion: ${completion}% (${approved}/${total} approved)`, opts);
8631
8518
  });
8632
- docCommand.command("init-frontmatter <path>").option("--status <status>", "Initial status (default: DRAFT)").option("--version <version>", "Initial version (default: 0.1)").option("--tldr <tldr>", "One-sentence summary").option("--title <title>", "Document title").description("Add frontmatter to a file that does not have it").action((filePath, options) => {
8633
- const opts = getOutputOptions(docCommand);
8634
- const ctx = getProjectId(opts);
8635
- if (!ctx) {
8636
- process.exit(1);
8519
+ var docCommand = new Command19("doc").description("Document integrity and status management commands").addCommand(integrityCommand).addCommand(approveCommand).addCommand(ackCommand).addCommand(readyCommand).addCommand(reviewCommand).addCommand(exceptionCommand).addCommand(refactorCommand).addCommand(archiveCommand).addCommand(dependentsCommand).addCommand(dependenciesCommand).addCommand(nextTemplateCommand).addCommand(statsCommand);
8520
+
8521
+ // src/commands/daemon.ts
8522
+ init_connection();
8523
+ import { Command as Command20 } from "commander";
8524
+ import {
8525
+ existsSync as existsSync11,
8526
+ writeFileSync as writeFileSync7,
8527
+ unlinkSync,
8528
+ readFileSync as readFileSync11,
8529
+ mkdirSync as mkdirSync5,
8530
+ statSync as statSync5,
8531
+ renameSync,
8532
+ readdirSync as readdirSync3
8533
+ } from "fs";
8534
+ import { join as join12, dirname as dirname4 } from "path";
8535
+ import { homedir as homedir2, platform } from "os";
8536
+ import { spawn, spawnSync, execSync as execSync2 } from "child_process";
8537
+ init_config();
8538
+
8539
+ // src/services/daemon-manager.ts
8540
+ import { existsSync as existsSync10 } from "fs";
8541
+ import { join as join11 } from "path";
8542
+ import { EventEmitter as EventEmitter2 } from "events";
8543
+
8544
+ // src/services/file-watcher.ts
8545
+ init_connection();
8546
+ import { watch } from "chokidar";
8547
+ import { relative as relative5, extname as extname2, basename as basename4 } from "path";
8548
+ import { existsSync as existsSync9, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
8549
+ import { EventEmitter } from "events";
8550
+ init_monitoring_patterns();
8551
+ init_config();
8552
+
8553
+ // src/services/document-status-transitions.ts
8554
+ init_connection();
8555
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
8556
+ import { resolve as resolve5 } from "path";
8557
+ import { createHash as createHash2 } from "crypto";
8558
+ async function handleDocumentEdit(projectId, docPath, absolutePath) {
8559
+ if (!existsSync8(absolutePath)) {
8560
+ return false;
8637
8561
  }
8638
- const fullPath = join8(ctx.projectRoot, filePath);
8639
- let content;
8640
- try {
8641
- content = readFileSync7(fullPath, "utf-8");
8642
- } catch {
8643
- error(`Could not read file: ${filePath}`, opts);
8644
- process.exit(1);
8562
+ const doc = queryOne(
8563
+ `SELECT id, meta_status, content_hash FROM documents WHERE project_id = ? AND path = ?`,
8564
+ [projectId, docPath]
8565
+ );
8566
+ if (!doc) {
8567
+ return false;
8645
8568
  }
8646
- const existing = parseFrontmatterFromFile(fullPath);
8647
- if (existing) {
8648
- error('File already has frontmatter. Use "aigile doc update" instead.', opts);
8649
- process.exit(1);
8569
+ const currentStatus = doc.meta_status;
8570
+ if (!currentStatus || currentStatus === "ARCHIVED") {
8571
+ return false;
8650
8572
  }
8651
- const metadata = {
8652
- status: options.status ?? "DRAFT",
8653
- version: options.version ?? "0.1",
8654
- tldr: options.tldr ?? "",
8655
- title: options.title
8656
- };
8657
- const newContent = updateFrontmatterContent(content, metadata);
8658
8573
  try {
8659
- writeFileSync5(fullPath, newContent, "utf-8");
8660
- success(`Added frontmatter to ${filePath}`, opts);
8661
- if (!opts.json) {
8662
- info('Run "aigile sync scan" to update the database.', opts);
8663
- }
8664
- } catch {
8665
- error(`Could not write file: ${filePath}`, opts);
8666
- process.exit(1);
8574
+ const parsed = parseFrontmatterFromFile(absolutePath);
8575
+ const fileStatus = parsed?.metadata?.status;
8576
+ const content = readFileSync9(absolutePath, "utf-8");
8577
+ const contentHash = createHash2("sha256").update(content).digest("hex");
8578
+ if (contentHash === doc.content_hash) {
8579
+ return false;
8580
+ }
8581
+ let newStatus = null;
8582
+ switch (currentStatus) {
8583
+ case "TEMPLATE":
8584
+ newStatus = "DRAFT";
8585
+ break;
8586
+ case "APPROVED":
8587
+ newStatus = "NEEDS_REVIEW";
8588
+ break;
8589
+ case "NEEDS_REVIEW":
8590
+ case "DRAFT":
8591
+ break;
8592
+ }
8593
+ if (!newStatus) {
8594
+ return false;
8595
+ }
8596
+ const updatedContent = updateFrontmatterContent(content, {
8597
+ status: newStatus
8598
+ });
8599
+ writeFileSync6(absolutePath, updatedContent, "utf-8");
8600
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
8601
+ run(
8602
+ `UPDATE documents
8603
+ SET meta_status = ?, content_hash = ?, last_modified_at = ?, needs_review = ?
8604
+ WHERE id = ?`,
8605
+ [
8606
+ newStatus,
8607
+ contentHash,
8608
+ now,
8609
+ newStatus === "NEEDS_REVIEW" ? 1 : 0,
8610
+ doc.id
8611
+ ]
8612
+ );
8613
+ console.log(
8614
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Status transition: ${docPath} (${currentStatus} \u2192 ${newStatus})`
8615
+ );
8616
+ return true;
8617
+ } catch (err) {
8618
+ console.error(
8619
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Error in status transition for ${docPath}: ${err}`
8620
+ );
8621
+ return false;
8667
8622
  }
8668
- });
8669
- docCommand.command("stats").description("Show frontmatter statistics for the project").action(() => {
8670
- const opts = getOutputOptions(docCommand);
8671
- const ctx = getProjectId(opts);
8672
- if (!ctx) {
8673
- process.exit(1);
8623
+ }
8624
+ async function markDependentsStale(projectId, docPath) {
8625
+ const dependents = queryAll(
8626
+ `SELECT id, path, meta_dependencies FROM documents WHERE project_id = ?`,
8627
+ [projectId]
8628
+ );
8629
+ let markedCount = 0;
8630
+ for (const dep of dependents) {
8631
+ try {
8632
+ const dependencies = dep.meta_dependencies ? JSON.parse(dep.meta_dependencies) : [];
8633
+ if (dependencies.includes(docPath)) {
8634
+ run(`UPDATE documents SET needs_review = 1 WHERE id = ?`, [dep.id]);
8635
+ markedCount++;
8636
+ }
8637
+ } catch {
8638
+ }
8674
8639
  }
8675
- const withFrontmatter = getDocumentsWithFrontmatter(ctx.projectId);
8676
- const withoutFrontmatter = getDocumentsWithoutFrontmatter(ctx.projectId);
8677
- const templates = getTemplateDocuments(ctx.projectId);
8678
- const drafts = getDocumentsByMetaStatus(ctx.projectId, "DRAFT");
8679
- const inReview = getDocumentsByMetaStatus(ctx.projectId, "IN-REVIEW");
8680
- const approved = getDocumentsByMetaStatus(ctx.projectId, "APPROVED");
8681
- const stats = {
8682
- total_documents: withFrontmatter.length + withoutFrontmatter.length,
8683
- with_frontmatter: withFrontmatter.length,
8684
- without_frontmatter: withoutFrontmatter.length,
8685
- templates: templates.length,
8686
- drafts: drafts.length,
8687
- in_review: inReview.length,
8688
- approved: approved.length
8689
- };
8690
- if (opts.json) {
8691
- console.log(JSON.stringify({ success: true, data: stats }));
8692
- } else {
8693
- details(
8694
- stats,
8695
- [
8696
- { label: "Total Documents", key: "total_documents" },
8697
- { label: "With Frontmatter", key: "with_frontmatter" },
8698
- { label: "Without Frontmatter", key: "without_frontmatter" },
8699
- { label: "TEMPLATE", key: "templates" },
8700
- { label: "DRAFT", key: "drafts" },
8701
- { label: "IN-REVIEW", key: "in_review" },
8702
- { label: "APPROVED", key: "approved" }
8703
- ],
8704
- opts
8640
+ if (markedCount > 0) {
8641
+ console.log(
8642
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Marked ${markedCount} dependents as stale for ${docPath}`
8705
8643
  );
8706
8644
  }
8707
- });
8708
-
8709
- // src/commands/daemon.ts
8710
- init_connection();
8711
- import { Command as Command20 } from "commander";
8712
- import { existsSync as existsSync9, writeFileSync as writeFileSync6, unlinkSync, readFileSync as readFileSync9, mkdirSync as mkdirSync5, statSync as statSync3, renameSync, readdirSync as readdirSync2 } from "fs";
8713
- import { join as join11, dirname as dirname3 } from "path";
8714
- import { homedir as homedir2, platform } from "os";
8715
- import { spawn, execSync as execSync2 } from "child_process";
8716
- init_config();
8717
-
8718
- // src/services/daemon-manager.ts
8719
- import { existsSync as existsSync8 } from "fs";
8720
- import { join as join10 } from "path";
8721
- import { EventEmitter as EventEmitter2 } from "events";
8645
+ return markedCount;
8646
+ }
8647
+ function isAigileDoc(docPath) {
8648
+ return docPath.startsWith(".aigile/") && (docPath.endsWith(".md") || docPath.endsWith(".feature"));
8649
+ }
8650
+ async function onFileChanged(projectId, projectPath, relativeDocPath) {
8651
+ if (!isAigileDoc(relativeDocPath)) {
8652
+ return;
8653
+ }
8654
+ const absolutePath = resolve5(projectPath, relativeDocPath);
8655
+ const transitioned = await handleDocumentEdit(
8656
+ projectId,
8657
+ relativeDocPath,
8658
+ absolutePath
8659
+ );
8660
+ if (transitioned || existsSync8(absolutePath)) {
8661
+ await markDependentsStale(projectId, relativeDocPath);
8662
+ }
8663
+ }
8722
8664
 
8723
8665
  // src/services/file-watcher.ts
8724
- init_connection();
8725
- import { watch } from "chokidar";
8726
- import { relative as relative3, extname as extname2, basename as basename4 } from "path";
8727
- import { readFileSync as readFileSync8, statSync as statSync2 } from "fs";
8728
- import { EventEmitter } from "events";
8729
- init_monitoring_patterns();
8730
- init_config();
8731
8666
  import picomatch2 from "picomatch";
8732
8667
  var FileWatcher = class extends EventEmitter {
8733
8668
  watcher = null;
8734
8669
  config;
8735
8670
  stats;
8736
8671
  debounceTimers = /* @__PURE__ */ new Map();
8672
+ cleanupInterval = null;
8737
8673
  // Tri-state pattern matchers
8738
8674
  allowMatcher = null;
8739
8675
  denyMatcher = null;
@@ -8817,6 +8753,12 @@ var FileWatcher = class extends EventEmitter {
8817
8753
  this.updateCategoryCounts();
8818
8754
  this.emit("ready", this.stats);
8819
8755
  });
8756
+ this.cleanupInterval = setInterval(
8757
+ () => {
8758
+ this.cleanupStaleTimers();
8759
+ },
8760
+ 5 * 60 * 1e3
8761
+ );
8820
8762
  }
8821
8763
  /**
8822
8764
  * Update category counts from database
@@ -8824,12 +8766,15 @@ var FileWatcher = class extends EventEmitter {
8824
8766
  */
8825
8767
  updateCategoryCounts() {
8826
8768
  try {
8827
- const counts = queryAll(`
8769
+ const counts = queryAll(
8770
+ `
8828
8771
  SELECT monitoring_category, COUNT(*) as count
8829
8772
  FROM documents
8830
8773
  WHERE project_id = ? AND status != 'deleted'
8831
8774
  GROUP BY monitoring_category
8832
- `, [this.config.projectId]);
8775
+ `,
8776
+ [this.config.projectId]
8777
+ );
8833
8778
  this.stats.categoryCounts = { allow: 0, deny: 0, unknown: 0 };
8834
8779
  for (const row of counts) {
8835
8780
  const cat = row.monitoring_category;
@@ -8847,6 +8792,10 @@ var FileWatcher = class extends EventEmitter {
8847
8792
  if (!this.watcher) {
8848
8793
  return;
8849
8794
  }
8795
+ if (this.cleanupInterval) {
8796
+ clearInterval(this.cleanupInterval);
8797
+ this.cleanupInterval = null;
8798
+ }
8850
8799
  for (const timer of this.debounceTimers.values()) {
8851
8800
  clearTimeout(timer);
8852
8801
  }
@@ -8856,6 +8805,28 @@ var FileWatcher = class extends EventEmitter {
8856
8805
  this.stats.isRunning = false;
8857
8806
  this.emit("stopped");
8858
8807
  }
8808
+ /**
8809
+ * Periodic cleanup of stale debounce timers
8810
+ * Removes timers for files that no longer exist
8811
+ */
8812
+ cleanupStaleTimers() {
8813
+ const staleKeys = [];
8814
+ for (const [path] of this.debounceTimers) {
8815
+ if (!existsSync9(path)) {
8816
+ staleKeys.push(path);
8817
+ }
8818
+ }
8819
+ for (const key of staleKeys) {
8820
+ const timer = this.debounceTimers.get(key);
8821
+ if (timer) {
8822
+ clearTimeout(timer);
8823
+ this.debounceTimers.delete(key);
8824
+ }
8825
+ }
8826
+ if (staleKeys.length > 0) {
8827
+ console.log(`[FileWatcher] Cleaned up ${staleKeys.length} stale debounce timers`);
8828
+ }
8829
+ }
8859
8830
  /**
8860
8831
  * Get current watcher statistics
8861
8832
  */
@@ -8881,6 +8852,11 @@ var FileWatcher = class extends EventEmitter {
8881
8852
  const existingTimer = this.debounceTimers.get(absolutePath);
8882
8853
  if (existingTimer) {
8883
8854
  clearTimeout(existingTimer);
8855
+ this.debounceTimers.delete(absolutePath);
8856
+ }
8857
+ if (type === "unlink") {
8858
+ this.processFileEvent(type, absolutePath);
8859
+ return;
8884
8860
  }
8885
8861
  const timer = setTimeout(() => {
8886
8862
  this.debounceTimers.delete(absolutePath);
@@ -8892,7 +8868,7 @@ var FileWatcher = class extends EventEmitter {
8892
8868
  * Process a file event (after debounce)
8893
8869
  */
8894
8870
  processFileEvent(type, absolutePath) {
8895
- const relativePath = relative3(this.config.projectPath, absolutePath);
8871
+ const relativePath = relative5(this.config.projectPath, absolutePath);
8896
8872
  const category = this.classifyFile(relativePath);
8897
8873
  if (category === "deny" && type !== "unlink") {
8898
8874
  return;
@@ -8915,6 +8891,14 @@ var FileWatcher = class extends EventEmitter {
8915
8891
  break;
8916
8892
  case "change":
8917
8893
  this.syncFileChange(absolutePath, relativePath, category);
8894
+ onFileChanged(this.config.projectId, this.config.projectPath, relativePath).catch((err) => {
8895
+ this.emit("transitionError", {
8896
+ path: relativePath,
8897
+ error: err,
8898
+ timestamp: /* @__PURE__ */ new Date()
8899
+ });
8900
+ console.error(`[FileWatcher] Error in onFileChanged for ${relativePath}:`, err);
8901
+ });
8918
8902
  break;
8919
8903
  case "unlink":
8920
8904
  this.syncFileDelete(relativePath);
@@ -8933,7 +8917,7 @@ var FileWatcher = class extends EventEmitter {
8933
8917
  const filename = basename4(relativePath);
8934
8918
  const isBinary = isBinaryExtension(ext);
8935
8919
  try {
8936
- const stats = statSync2(absolutePath);
8920
+ const stats = statSync4(absolutePath);
8937
8921
  const shouldComputeHash = category === "allow" || !isBinary && stats.size < 10 * 1024 * 1024;
8938
8922
  const hash = shouldComputeHash ? computeFileHash(absolutePath) : null;
8939
8923
  let hasFrontmatter = false;
@@ -8952,16 +8936,36 @@ var FileWatcher = class extends EventEmitter {
8952
8936
  [this.config.projectId, relativePath]
8953
8937
  );
8954
8938
  if (existing) {
8955
- this.updateDocument(existing.id, hash, stats.size, hasFrontmatter, frontmatterRaw, metadata, category);
8939
+ this.updateDocument(
8940
+ existing.id,
8941
+ hash,
8942
+ stats.size,
8943
+ hasFrontmatter,
8944
+ frontmatterRaw,
8945
+ metadata,
8946
+ category
8947
+ );
8956
8948
  } else {
8957
- this.insertDocument(relativePath, filename, ext, hash, stats.size, hasFrontmatter, frontmatterRaw, metadata, category);
8949
+ this.insertDocument(
8950
+ relativePath,
8951
+ filename,
8952
+ ext,
8953
+ hash,
8954
+ stats.size,
8955
+ hasFrontmatter,
8956
+ frontmatterRaw,
8957
+ metadata,
8958
+ category
8959
+ );
8958
8960
  }
8959
8961
  } catch (err) {
8960
8962
  const errMsg = err instanceof Error ? err.message : String(err);
8961
8963
  if (errMsg.includes("Database") || errMsg.includes("database")) {
8962
8964
  throw err;
8963
8965
  }
8964
- console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] syncFileAdd error for ${relativePath}: ${errMsg}`);
8966
+ console.error(
8967
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] syncFileAdd error for ${relativePath}: ${errMsg}`
8968
+ );
8965
8969
  }
8966
8970
  }
8967
8971
  /**
@@ -8979,10 +8983,9 @@ var FileWatcher = class extends EventEmitter {
8979
8983
  [this.config.projectId, relativePath]
8980
8984
  );
8981
8985
  if (doc) {
8982
- run(
8983
- `UPDATE documents SET status = 'deleted', updated_at = datetime('now') WHERE id = ?`,
8984
- [doc.id]
8985
- );
8986
+ run(`UPDATE documents SET status = 'deleted', updated_at = datetime('now') WHERE id = ?`, [
8987
+ doc.id
8988
+ ]);
8986
8989
  }
8987
8990
  }
8988
8991
  /**
@@ -9076,6 +9079,7 @@ var INITIAL_RETRY_DELAY_MS = 5e3;
9076
9079
  var DaemonManager = class extends EventEmitter2 {
9077
9080
  watchers = /* @__PURE__ */ new Map();
9078
9081
  watcherRetries = /* @__PURE__ */ new Map();
9082
+ retryTimeouts = /* @__PURE__ */ new Map();
9079
9083
  running = false;
9080
9084
  startedAt = null;
9081
9085
  /**
@@ -9131,21 +9135,37 @@ var DaemonManager = class extends EventEmitter2 {
9131
9135
  if (currentRetries < MAX_WATCHER_RETRIES) {
9132
9136
  this.watcherRetries.set(project.key, currentRetries + 1);
9133
9137
  const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, currentRetries);
9134
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Will retry in ${delay}ms (attempt ${currentRetries + 1}/${MAX_WATCHER_RETRIES})`);
9135
- setTimeout(async () => {
9138
+ console.log(
9139
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Will retry in ${delay}ms (attempt ${currentRetries + 1}/${MAX_WATCHER_RETRIES})`
9140
+ );
9141
+ const existingTimeout2 = this.retryTimeouts.get(project.key);
9142
+ if (existingTimeout2) {
9143
+ clearTimeout(existingTimeout2);
9144
+ }
9145
+ const timeout = setTimeout(async () => {
9146
+ this.retryTimeouts.delete(project.key);
9136
9147
  if (this.running) {
9137
9148
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Attempting restart...`);
9138
9149
  await this.startWatcherWithRetry(project);
9139
9150
  }
9140
9151
  }, delay);
9152
+ this.retryTimeouts.set(project.key, timeout);
9141
9153
  } else {
9142
- console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Max retries (${MAX_WATCHER_RETRIES}) exceeded - watcher disabled`);
9154
+ console.error(
9155
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Max retries (${MAX_WATCHER_RETRIES}) exceeded - watcher disabled`
9156
+ );
9157
+ this.watcherRetries.delete(project.key);
9143
9158
  this.emit("watcherDisabled", { project: project.key });
9144
9159
  }
9145
9160
  });
9146
9161
  watcher.start();
9147
9162
  this.watchers.set(project.key, watcher);
9148
- this.watcherRetries.set(project.key, 0);
9163
+ this.watcherRetries.delete(project.key);
9164
+ const existingTimeout = this.retryTimeouts.get(project.key);
9165
+ if (existingTimeout) {
9166
+ clearTimeout(existingTimeout);
9167
+ this.retryTimeouts.delete(project.key);
9168
+ }
9149
9169
  console.log(` \u2713 ${project.key}: ${project.path}`);
9150
9170
  } catch (error2) {
9151
9171
  console.error(` \u2717 ${project.key}: Failed to start watcher - ${error2}`);
@@ -9153,12 +9173,22 @@ var DaemonManager = class extends EventEmitter2 {
9153
9173
  if (retryCount < MAX_WATCHER_RETRIES) {
9154
9174
  this.watcherRetries.set(project.key, retryCount + 1);
9155
9175
  const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
9156
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Will retry in ${delay}ms (attempt ${retryCount + 1}/${MAX_WATCHER_RETRIES})`);
9157
- setTimeout(async () => {
9176
+ console.log(
9177
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] [${project.key}] Will retry in ${delay}ms (attempt ${retryCount + 1}/${MAX_WATCHER_RETRIES})`
9178
+ );
9179
+ const existingTimeout = this.retryTimeouts.get(project.key);
9180
+ if (existingTimeout) {
9181
+ clearTimeout(existingTimeout);
9182
+ }
9183
+ const timeout = setTimeout(async () => {
9184
+ this.retryTimeouts.delete(project.key);
9158
9185
  if (this.running) {
9159
9186
  await this.startWatcherWithRetry(project);
9160
9187
  }
9161
9188
  }, delay);
9189
+ this.retryTimeouts.set(project.key, timeout);
9190
+ } else {
9191
+ this.watcherRetries.delete(project.key);
9162
9192
  }
9163
9193
  }
9164
9194
  }
@@ -9170,6 +9200,11 @@ var DaemonManager = class extends EventEmitter2 {
9170
9200
  return;
9171
9201
  }
9172
9202
  console.log("Stopping all watchers...");
9203
+ for (const [key, timeout] of this.retryTimeouts) {
9204
+ clearTimeout(timeout);
9205
+ console.log(` \u2713 Cleared retry timeout: ${key}`);
9206
+ }
9207
+ this.retryTimeouts.clear();
9173
9208
  for (const [key, watcher] of this.watchers) {
9174
9209
  try {
9175
9210
  await watcher.stop();
@@ -9179,6 +9214,7 @@ var DaemonManager = class extends EventEmitter2 {
9179
9214
  }
9180
9215
  }
9181
9216
  this.watchers.clear();
9217
+ this.watcherRetries.clear();
9182
9218
  this.running = false;
9183
9219
  this.startedAt = null;
9184
9220
  this.emit("stopped");
@@ -9295,7 +9331,7 @@ var DaemonManager = class extends EventEmitter2 {
9295
9331
  * Check if a project path is valid
9296
9332
  */
9297
9333
  isValidProject(path) {
9298
- return existsSync8(path) && existsSync8(join10(path, ".aigile"));
9334
+ return existsSync10(path) && existsSync10(join11(path, ".aigile"));
9299
9335
  }
9300
9336
  };
9301
9337
  var daemonManagerInstance = null;
@@ -9318,29 +9354,29 @@ var DAEMON_NAME = "com.aigile.watcher";
9318
9354
  function getDaemonPaths() {
9319
9355
  const aigileHome = getAigileHome();
9320
9356
  const basePaths = {
9321
- pidFile: join11(aigileHome, "daemon.pid"),
9322
- logFile: join11(aigileHome, "daemon.log")
9357
+ pidFile: join12(aigileHome, "daemon.pid"),
9358
+ logFile: join12(aigileHome, "daemon.log")
9323
9359
  };
9324
9360
  if (PLATFORM === "darwin") {
9325
9361
  return {
9326
9362
  ...basePaths,
9327
- plist: join11(homedir2(), "Library", "LaunchAgents", `${DAEMON_NAME}.plist`)
9363
+ plist: join12(homedir2(), "Library", "LaunchAgents", `${DAEMON_NAME}.plist`)
9328
9364
  };
9329
9365
  } else if (PLATFORM === "linux") {
9330
9366
  return {
9331
9367
  ...basePaths,
9332
- service: join11(homedir2(), ".config", "systemd", "user", `${DAEMON_NAME}.service`)
9368
+ service: join12(homedir2(), ".config", "systemd", "user", `${DAEMON_NAME}.service`)
9333
9369
  };
9334
9370
  }
9335
9371
  return basePaths;
9336
9372
  }
9337
9373
  function isDaemonRunning() {
9338
9374
  const paths = getDaemonPaths();
9339
- if (!existsSync9(paths.pidFile)) {
9375
+ if (!existsSync11(paths.pidFile)) {
9340
9376
  return { running: false };
9341
9377
  }
9342
9378
  try {
9343
- const pid = parseInt(readFileSync9(paths.pidFile, "utf-8").trim(), 10);
9379
+ const pid = parseInt(readFileSync11(paths.pidFile, "utf-8").trim(), 10);
9344
9380
  try {
9345
9381
  process.kill(pid, 0);
9346
9382
  return { running: true, pid };
@@ -9354,12 +9390,12 @@ function isDaemonRunning() {
9354
9390
  }
9355
9391
  function writeCrashReport(error2) {
9356
9392
  try {
9357
- const crashDir = join11(getAigileHome(), CRASH_DIR_NAME);
9358
- if (!existsSync9(crashDir)) {
9393
+ const crashDir = join12(getAigileHome(), CRASH_DIR_NAME);
9394
+ if (!existsSync11(crashDir)) {
9359
9395
  mkdirSync5(crashDir, { recursive: true });
9360
9396
  }
9361
9397
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9362
- const crashFile = join11(crashDir, `crash-${timestamp}.log`);
9398
+ const crashFile = join12(crashDir, `crash-${timestamp}.log`);
9363
9399
  const report = [
9364
9400
  `AIGILE Daemon Crash Report`,
9365
9401
  `==========================`,
@@ -9372,7 +9408,7 @@ function writeCrashReport(error2) {
9372
9408
  `Error:`,
9373
9409
  error2 instanceof Error ? error2.stack || error2.message : String(error2)
9374
9410
  ].join("\n");
9375
- writeFileSync6(crashFile, report);
9411
+ writeFileSync7(crashFile, report);
9376
9412
  console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Crash report saved: ${crashFile}`);
9377
9413
  cleanupOldCrashReports(crashDir);
9378
9414
  } catch (writeErr) {
@@ -9381,7 +9417,7 @@ function writeCrashReport(error2) {
9381
9417
  }
9382
9418
  function cleanupOldCrashReports(crashDir) {
9383
9419
  try {
9384
- 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));
9420
+ const files = readdirSync3(crashDir).filter((f) => f.startsWith("crash-") && f.endsWith(".log")).map((f) => ({ name: f, path: join12(crashDir, f) })).sort((a, b) => b.name.localeCompare(a.name));
9385
9421
  for (let i = MAX_CRASH_REPORTS; i < files.length; i++) {
9386
9422
  try {
9387
9423
  unlinkSync(files[i].path);
@@ -9395,21 +9431,21 @@ function rotateLogIfNeeded() {
9395
9431
  const paths = getDaemonPaths();
9396
9432
  const logPath = paths.logFile;
9397
9433
  try {
9398
- if (!existsSync9(logPath)) return;
9399
- const stats = statSync3(logPath);
9434
+ if (!existsSync11(logPath)) return;
9435
+ const stats = statSync5(logPath);
9400
9436
  if (stats.size < MAX_LOG_SIZE) return;
9401
9437
  const timestamp = Date.now();
9402
9438
  const rotatedPath = `${logPath}.${timestamp}`;
9403
9439
  renameSync(logPath, rotatedPath);
9404
9440
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Log rotated: ${rotatedPath}`);
9405
- cleanupOldLogs(dirname3(logPath));
9441
+ cleanupOldLogs(dirname4(logPath));
9406
9442
  } catch (err) {
9407
9443
  console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Log rotation error: ${err}`);
9408
9444
  }
9409
9445
  }
9410
9446
  function cleanupOldLogs(logDir) {
9411
9447
  try {
9412
- 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));
9448
+ const files = readdirSync3(logDir).filter((f) => f.startsWith("daemon.log.")).map((f) => ({ name: f, path: join12(logDir, f) })).sort((a, b) => b.name.localeCompare(a.name));
9413
9449
  for (let i = MAX_LOG_FILES; i < files.length; i++) {
9414
9450
  try {
9415
9451
  unlinkSync(files[i].path);
@@ -9423,7 +9459,11 @@ function cleanupOldLogs(logDir) {
9423
9459
  function generateLaunchAgentPlist() {
9424
9460
  const paths = getDaemonPaths();
9425
9461
  const nodePath = process.execPath;
9426
- const aigilePath = join11(dirname3(dirname3(import.meta.url.replace("file://", ""))), "bin", "aigile.js");
9462
+ const aigilePath = join12(
9463
+ dirname4(dirname4(import.meta.url.replace("file://", ""))),
9464
+ "bin",
9465
+ "aigile.js"
9466
+ );
9427
9467
  return `<?xml version="1.0" encoding="UTF-8"?>
9428
9468
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9429
9469
  <plist version="1.0">
@@ -9458,7 +9498,11 @@ function generateLaunchAgentPlist() {
9458
9498
  function generateSystemdService() {
9459
9499
  const paths = getDaemonPaths();
9460
9500
  const nodePath = process.execPath;
9461
- const aigilePath = join11(dirname3(dirname3(import.meta.url.replace("file://", ""))), "bin", "aigile.js");
9501
+ const aigilePath = join12(
9502
+ dirname4(dirname4(import.meta.url.replace("file://", ""))),
9503
+ "bin",
9504
+ "aigile.js"
9505
+ );
9462
9506
  return `[Unit]
9463
9507
  Description=AIGILE File Watcher Daemon
9464
9508
  After=network.target
@@ -9477,27 +9521,29 @@ StandardError=append:${paths.logFile}
9477
9521
  [Install]
9478
9522
  WantedBy=default.target`;
9479
9523
  }
9480
- daemonCommand.command("install").description("Install daemon to start automatically on system boot (watches ALL registered projects)").action(() => {
9524
+ daemonCommand.command("install").description(
9525
+ "Install daemon to start automatically on system boot (watches ALL registered projects)"
9526
+ ).action(() => {
9481
9527
  const opts = getOutputOptions(daemonCommand);
9482
9528
  const paths = getDaemonPaths();
9483
9529
  if (PLATFORM === "darwin") {
9484
- const plistDir = dirname3(paths.plist);
9485
- if (!existsSync9(plistDir)) {
9530
+ const plistDir = dirname4(paths.plist);
9531
+ if (!existsSync11(plistDir)) {
9486
9532
  mkdirSync5(plistDir, { recursive: true });
9487
9533
  }
9488
9534
  const plistContent = generateLaunchAgentPlist();
9489
- writeFileSync6(paths.plist, plistContent);
9535
+ writeFileSync7(paths.plist, plistContent);
9490
9536
  success("Installed macOS LaunchAgent", opts);
9491
9537
  info(`Plist location: ${paths.plist}`, opts);
9492
9538
  info("Daemon will watch ALL registered projects", opts);
9493
9539
  info('Run "aigile daemon start" to start the watcher', opts);
9494
9540
  } else if (PLATFORM === "linux") {
9495
- const serviceDir = dirname3(paths.service);
9496
- if (!existsSync9(serviceDir)) {
9541
+ const serviceDir = dirname4(paths.service);
9542
+ if (!existsSync11(serviceDir)) {
9497
9543
  mkdirSync5(serviceDir, { recursive: true });
9498
9544
  }
9499
9545
  const serviceContent = generateSystemdService();
9500
- writeFileSync6(paths.service, serviceContent);
9546
+ writeFileSync7(paths.service, serviceContent);
9501
9547
  try {
9502
9548
  execSync2("systemctl --user daemon-reload");
9503
9549
  execSync2(`systemctl --user enable ${DAEMON_NAME}`);
@@ -9506,7 +9552,7 @@ daemonCommand.command("install").description("Install daemon to start automatica
9506
9552
  info("Daemon will watch ALL registered projects", opts);
9507
9553
  info('Run "aigile daemon start" to start the watcher', opts);
9508
9554
  } catch (err) {
9509
- warning2("Service file created but could not enable. You may need to run:", opts);
9555
+ warning("Service file created but could not enable. You may need to run:", opts);
9510
9556
  console.log(` systemctl --user daemon-reload`);
9511
9557
  console.log(` systemctl --user enable ${DAEMON_NAME}`);
9512
9558
  }
@@ -9527,14 +9573,14 @@ daemonCommand.command("uninstall").description("Remove daemon from auto-start").
9527
9573
  } catch {
9528
9574
  }
9529
9575
  }
9530
- if (PLATFORM === "darwin" && paths.plist && existsSync9(paths.plist)) {
9576
+ if (PLATFORM === "darwin" && paths.plist && existsSync11(paths.plist)) {
9531
9577
  try {
9532
9578
  execSync2(`launchctl unload ${paths.plist}`);
9533
9579
  } catch {
9534
9580
  }
9535
9581
  unlinkSync(paths.plist);
9536
9582
  success("Removed macOS LaunchAgent", opts);
9537
- } else if (PLATFORM === "linux" && paths.service && existsSync9(paths.service)) {
9583
+ } else if (PLATFORM === "linux" && paths.service && existsSync11(paths.service)) {
9538
9584
  try {
9539
9585
  execSync2(`systemctl --user stop ${DAEMON_NAME}`);
9540
9586
  execSync2(`systemctl --user disable ${DAEMON_NAME}`);
@@ -9549,7 +9595,7 @@ daemonCommand.command("uninstall").description("Remove daemon from auto-start").
9549
9595
  } else {
9550
9596
  info("No daemon installation found", opts);
9551
9597
  }
9552
- if (existsSync9(paths.pidFile)) {
9598
+ if (existsSync11(paths.pidFile)) {
9553
9599
  unlinkSync(paths.pidFile);
9554
9600
  }
9555
9601
  });
@@ -9561,7 +9607,7 @@ daemonCommand.command("start").description("Start the file watcher daemon (watch
9561
9607
  info(`Daemon already running (PID: ${status.pid})`, opts);
9562
9608
  return;
9563
9609
  }
9564
- if (PLATFORM === "darwin" && paths.plist && existsSync9(paths.plist)) {
9610
+ if (PLATFORM === "darwin" && paths.plist && existsSync11(paths.plist)) {
9565
9611
  try {
9566
9612
  execSync2(`launchctl load ${paths.plist}`);
9567
9613
  success("Started daemon via launchctl (watching all projects)", opts);
@@ -9569,7 +9615,7 @@ daemonCommand.command("start").description("Start the file watcher daemon (watch
9569
9615
  error("Failed to start daemon via launchctl", opts);
9570
9616
  process.exit(1);
9571
9617
  }
9572
- } else if (PLATFORM === "linux" && paths.service && existsSync9(paths.service)) {
9618
+ } else if (PLATFORM === "linux" && paths.service && existsSync11(paths.service)) {
9573
9619
  try {
9574
9620
  execSync2(`systemctl --user start ${DAEMON_NAME}`);
9575
9621
  success("Started daemon via systemctl (watching all projects)", opts);
@@ -9586,7 +9632,7 @@ daemonCommand.command("start").description("Start the file watcher daemon (watch
9586
9632
  if (child.pid) {
9587
9633
  try {
9588
9634
  process.kill(child.pid, 0);
9589
- writeFileSync6(paths.pidFile, String(child.pid));
9635
+ writeFileSync7(paths.pidFile, String(child.pid));
9590
9636
  success(`Started daemon (PID: ${child.pid}) - watching all projects`, opts);
9591
9637
  } catch {
9592
9638
  error("Failed to start daemon - process died immediately", opts);
@@ -9601,14 +9647,14 @@ daemonCommand.command("start").description("Start the file watcher daemon (watch
9601
9647
  daemonCommand.command("stop").description("Stop the file watcher daemon").action(() => {
9602
9648
  const opts = getOutputOptions(daemonCommand);
9603
9649
  const paths = getDaemonPaths();
9604
- if (PLATFORM === "darwin" && paths.plist && existsSync9(paths.plist)) {
9650
+ if (PLATFORM === "darwin" && paths.plist && existsSync11(paths.plist)) {
9605
9651
  try {
9606
9652
  execSync2(`launchctl unload ${paths.plist}`);
9607
9653
  success("Stopped daemon via launchctl", opts);
9608
9654
  } catch {
9609
9655
  info("Daemon was not running", opts);
9610
9656
  }
9611
- } else if (PLATFORM === "linux" && paths.service && existsSync9(paths.service)) {
9657
+ } else if (PLATFORM === "linux" && paths.service && existsSync11(paths.service)) {
9612
9658
  try {
9613
9659
  execSync2(`systemctl --user stop ${DAEMON_NAME}`);
9614
9660
  success("Stopped daemon via systemctl", opts);
@@ -9620,7 +9666,7 @@ daemonCommand.command("stop").description("Stop the file watcher daemon").action
9620
9666
  if (status.running && status.pid) {
9621
9667
  try {
9622
9668
  process.kill(status.pid, "SIGTERM");
9623
- if (existsSync9(paths.pidFile)) {
9669
+ if (existsSync11(paths.pidFile)) {
9624
9670
  unlinkSync(paths.pidFile);
9625
9671
  }
9626
9672
  success(`Stopped daemon (PID: ${status.pid})`, opts);
@@ -9641,13 +9687,16 @@ daemonCommand.command("status").description("Show daemon status for ALL register
9641
9687
  const projectStats = [];
9642
9688
  let totalFiles = { allow: 0, unknown: 0, total: 0 };
9643
9689
  for (const project of projects) {
9644
- const valid = existsSync9(project.path) && existsSync9(join11(project.path, ".aigile"));
9645
- const counts = queryAll(`
9690
+ const valid = existsSync11(project.path) && existsSync11(join12(project.path, ".aigile"));
9691
+ const counts = queryAll(
9692
+ `
9646
9693
  SELECT COALESCE(monitoring_category, 'unknown') as monitoring_category, COUNT(*) as count
9647
9694
  FROM documents
9648
9695
  WHERE project_id = ? AND status != 'deleted'
9649
9696
  GROUP BY monitoring_category
9650
- `, [project.id]);
9697
+ `,
9698
+ [project.id]
9699
+ );
9651
9700
  let allow = 0;
9652
9701
  let unknown = 0;
9653
9702
  for (const row of counts) {
@@ -9671,30 +9720,34 @@ daemonCommand.command("status").description("Show daemon status for ALL register
9671
9720
  }
9672
9721
  const validCount = projectStats.filter((p) => p.valid).length;
9673
9722
  if (opts.json) {
9674
- console.log(JSON.stringify({
9675
- success: true,
9676
- data: {
9677
- running: status.running,
9678
- pid: status.pid ?? null,
9679
- platform: PLATFORM,
9680
- installed: PLATFORM === "darwin" ? paths.plist && existsSync9(paths.plist) : PLATFORM === "linux" ? paths.service && existsSync9(paths.service) : false,
9681
- projectCount: projects.length,
9682
- validProjectCount: validCount,
9683
- projects: projectStats,
9684
- totalFiles,
9685
- paths: {
9686
- database: getDbPath(),
9687
- pidFile: paths.pidFile,
9688
- logFile: paths.logFile
9723
+ console.log(
9724
+ JSON.stringify({
9725
+ success: true,
9726
+ data: {
9727
+ running: status.running,
9728
+ pid: status.pid ?? null,
9729
+ platform: PLATFORM,
9730
+ installed: PLATFORM === "darwin" ? paths.plist && existsSync11(paths.plist) : PLATFORM === "linux" ? paths.service && existsSync11(paths.service) : false,
9731
+ projectCount: projects.length,
9732
+ validProjectCount: validCount,
9733
+ projects: projectStats,
9734
+ totalFiles,
9735
+ paths: {
9736
+ database: getDbPath(),
9737
+ pidFile: paths.pidFile,
9738
+ logFile: paths.logFile
9739
+ }
9689
9740
  }
9690
- }
9691
- }));
9741
+ })
9742
+ );
9692
9743
  return;
9693
9744
  }
9694
9745
  console.log("\n\u{1F4CA} Daemon Status\n");
9695
- console.log(`\u251C\u2500\u2500 Running: ${status.running ? "\u2705 Yes" : "\u274C No"}${status.pid ? ` (PID: ${status.pid})` : ""}`);
9746
+ console.log(
9747
+ `\u251C\u2500\u2500 Running: ${status.running ? "\u2705 Yes" : "\u274C No"}${status.pid ? ` (PID: ${status.pid})` : ""}`
9748
+ );
9696
9749
  console.log(`\u251C\u2500\u2500 Platform: ${PLATFORM}`);
9697
- const installed = PLATFORM === "darwin" ? paths.plist && existsSync9(paths.plist) : PLATFORM === "linux" ? paths.service && existsSync9(paths.service) : false;
9750
+ const installed = PLATFORM === "darwin" ? paths.plist && existsSync11(paths.plist) : PLATFORM === "linux" ? paths.service && existsSync11(paths.service) : false;
9698
9751
  console.log(`\u251C\u2500\u2500 Installed: ${installed ? "\u2705 Yes" : "\u274C No"}`);
9699
9752
  console.log(`\u251C\u2500\u2500 Projects: ${validCount}/${projects.length} valid`);
9700
9753
  if (projectStats.length > 0) {
@@ -9715,7 +9768,9 @@ daemonCommand.command("status").description("Show daemon status for ALL register
9715
9768
  console.log(` \u2514\u2500\u2500 Log File: ${paths.logFile}`);
9716
9769
  console.log("");
9717
9770
  if (projectStats.some((p) => !p.valid)) {
9718
- console.log('\u26A0\uFE0F Some projects have invalid paths. Run "aigile project cleanup" to remove them.\n');
9771
+ console.log(
9772
+ '\u26A0\uFE0F Some projects have invalid paths. Run "aigile project cleanup" to remove them.\n'
9773
+ );
9719
9774
  }
9720
9775
  });
9721
9776
  function getRelativeTime(isoDate) {
@@ -9742,7 +9797,7 @@ daemonCommand.command("run").option("--skip-resync", "Skip initial resync on sta
9742
9797
  console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] FATAL: Uncaught exception:`);
9743
9798
  console.error(err.stack || err.message);
9744
9799
  writeCrashReport(err);
9745
- if (existsSync9(paths.pidFile)) {
9800
+ if (existsSync11(paths.pidFile)) {
9746
9801
  try {
9747
9802
  unlinkSync(paths.pidFile);
9748
9803
  } catch {
@@ -9754,7 +9809,7 @@ daemonCommand.command("run").option("--skip-resync", "Skip initial resync on sta
9754
9809
  console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] FATAL: Unhandled promise rejection:`);
9755
9810
  console.error(reason);
9756
9811
  writeCrashReport(reason);
9757
- if (existsSync9(paths.pidFile)) {
9812
+ if (existsSync11(paths.pidFile)) {
9758
9813
  try {
9759
9814
  unlinkSync(paths.pidFile);
9760
9815
  } catch {
@@ -9763,26 +9818,36 @@ daemonCommand.command("run").option("--skip-resync", "Skip initial resync on sta
9763
9818
  process.exit(1);
9764
9819
  });
9765
9820
  rotateLogIfNeeded();
9766
- writeFileSync6(paths.pidFile, String(process.pid));
9767
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting AIGILE daemon for all registered projects...`);
9768
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] PID: ${process.pid}, Node: ${process.version}, Platform: ${platform()}`);
9821
+ writeFileSync7(paths.pidFile, String(process.pid));
9822
+ console.log(
9823
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Starting AIGILE daemon for all registered projects...`
9824
+ );
9825
+ console.log(
9826
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] PID: ${process.pid}, Node: ${process.version}, Platform: ${platform()}`
9827
+ );
9769
9828
  const manager = getDaemonManager();
9770
9829
  if (!options.skipResync) {
9771
9830
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Performing initial resync for all projects...`);
9772
9831
  try {
9773
9832
  const results = await manager.resyncAll();
9774
9833
  const projectCount = Object.keys(results).length;
9775
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Resync complete: ${projectCount} projects synced`);
9834
+ console.log(
9835
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Resync complete: ${projectCount} projects synced`
9836
+ );
9776
9837
  } catch (err) {
9777
9838
  console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Resync warning: ${err}`);
9778
9839
  }
9779
9840
  }
9780
9841
  manager.on("sync", (event) => {
9781
9842
  const categoryStr = event.category ? ` [${event.category}]` : "";
9782
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${event.project}] Synced: ${event.type} ${event.path}${categoryStr}`);
9843
+ console.log(
9844
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] [${event.project}] Synced: ${event.type} ${event.path}${categoryStr}`
9845
+ );
9783
9846
  });
9784
9847
  manager.on("syncError", ({ project, event, error: err }) => {
9785
- console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project}] Sync error: ${event.type} ${event.path} - ${err}`);
9848
+ console.error(
9849
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] [${project}] Sync error: ${event.type} ${event.path} - ${err}`
9850
+ );
9786
9851
  });
9787
9852
  manager.on("watcherError", ({ project, error: err }) => {
9788
9853
  console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${project}] Watcher error: ${err}`);
@@ -9793,8 +9858,10 @@ daemonCommand.command("run").option("--skip-resync", "Skip initial resync on sta
9793
9858
  isShuttingDown = true;
9794
9859
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Shutting down...`);
9795
9860
  const forceExitTimeout = setTimeout(() => {
9796
- console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Shutdown timeout (${SHUTDOWN_TIMEOUT_MS}ms) - forcing exit`);
9797
- if (existsSync9(paths.pidFile)) {
9861
+ console.error(
9862
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Shutdown timeout (${SHUTDOWN_TIMEOUT_MS}ms) - forcing exit`
9863
+ );
9864
+ if (existsSync11(paths.pidFile)) {
9798
9865
  try {
9799
9866
  unlinkSync(paths.pidFile);
9800
9867
  } catch {
@@ -9808,7 +9875,7 @@ daemonCommand.command("run").option("--skip-resync", "Skip initial resync on sta
9808
9875
  console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Error during shutdown: ${err}`);
9809
9876
  }
9810
9877
  clearTimeout(forceExitTimeout);
9811
- if (existsSync9(paths.pidFile)) {
9878
+ if (existsSync11(paths.pidFile)) {
9812
9879
  unlinkSync(paths.pidFile);
9813
9880
  }
9814
9881
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Daemon stopped gracefully`);
@@ -9816,15 +9883,20 @@ daemonCommand.command("run").option("--skip-resync", "Skip initial resync on sta
9816
9883
  };
9817
9884
  process.on("SIGTERM", shutdown);
9818
9885
  process.on("SIGINT", shutdown);
9819
- const logRotationInterval = setInterval(() => {
9820
- rotateLogIfNeeded();
9821
- }, 60 * 60 * 1e3);
9886
+ const logRotationInterval = setInterval(
9887
+ () => {
9888
+ rotateLogIfNeeded();
9889
+ },
9890
+ 60 * 60 * 1e3
9891
+ );
9822
9892
  process.on("exit", () => {
9823
9893
  clearInterval(logRotationInterval);
9824
9894
  });
9825
9895
  try {
9826
9896
  const status = await manager.start();
9827
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Daemon started - watching ${status.watchingCount} projects`);
9897
+ console.log(
9898
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Daemon started - watching ${status.watchingCount} projects`
9899
+ );
9828
9900
  for (const p of status.projects) {
9829
9901
  if (p.watching) {
9830
9902
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] \u2713 ${p.key}: ${p.path}`);
@@ -9832,7 +9904,7 @@ daemonCommand.command("run").option("--skip-resync", "Skip initial resync on sta
9832
9904
  }
9833
9905
  } catch (err) {
9834
9906
  console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Failed to start daemon: ${err}`);
9835
- if (existsSync9(paths.pidFile)) {
9907
+ if (existsSync11(paths.pidFile)) {
9836
9908
  unlinkSync(paths.pidFile);
9837
9909
  }
9838
9910
  process.exit(1);
@@ -9841,10 +9913,15 @@ daemonCommand.command("run").option("--skip-resync", "Skip initial resync on sta
9841
9913
  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) => {
9842
9914
  const opts = getOutputOptions(daemonCommand);
9843
9915
  const paths = getDaemonPaths();
9844
- if (!existsSync9(paths.logFile)) {
9916
+ if (!existsSync11(paths.logFile)) {
9845
9917
  info("No log file found. Daemon may not have run yet.", opts);
9846
9918
  return;
9847
9919
  }
9920
+ const lines = parseInt(options.lines, 10);
9921
+ if (isNaN(lines) || lines < 1 || lines > 1e4) {
9922
+ error("Invalid lines parameter. Must be a number between 1 and 10000.", opts);
9923
+ return;
9924
+ }
9848
9925
  if (options.follow) {
9849
9926
  const tail = spawn("tail", ["-f", paths.logFile], {
9850
9927
  stdio: "inherit"
@@ -9855,8 +9932,14 @@ daemonCommand.command("logs").option("-n, --lines <number>", "Number of lines to
9855
9932
  });
9856
9933
  } else {
9857
9934
  try {
9858
- const output = execSync2(`tail -n ${options.lines} "${paths.logFile}"`, { encoding: "utf-8" });
9859
- console.log(output);
9935
+ const result = spawnSync("tail", ["-n", lines.toString(), paths.logFile], {
9936
+ encoding: "utf-8"
9937
+ });
9938
+ if (result.status === 0 && result.stdout) {
9939
+ console.log(result.stdout);
9940
+ } else {
9941
+ error("Failed to read log file", opts);
9942
+ }
9860
9943
  } catch {
9861
9944
  error("Failed to read log file", opts);
9862
9945
  }
@@ -9874,13 +9957,15 @@ daemonCommand.command("resync").option("--project <key>", "Resync only a specifi
9874
9957
  process.exit(1);
9875
9958
  }
9876
9959
  if (opts.json) {
9877
- console.log(JSON.stringify({
9878
- success: true,
9879
- data: {
9880
- project: options.project,
9881
- ...result
9882
- }
9883
- }));
9960
+ console.log(
9961
+ JSON.stringify({
9962
+ success: true,
9963
+ data: {
9964
+ project: options.project,
9965
+ ...result
9966
+ }
9967
+ })
9968
+ );
9884
9969
  } else {
9885
9970
  success(`Resync complete for ${options.project}:`, opts);
9886
9971
  console.log(` Allow: ${result.allow}`);
@@ -9899,13 +9984,15 @@ daemonCommand.command("resync").option("--project <key>", "Resync only a specifi
9899
9984
  const results = await manager.resyncAll();
9900
9985
  const projectKeys = Object.keys(results);
9901
9986
  if (opts.json) {
9902
- console.log(JSON.stringify({
9903
- success: true,
9904
- data: {
9905
- projectCount: projectKeys.length,
9906
- projects: results
9907
- }
9908
- }));
9987
+ console.log(
9988
+ JSON.stringify({
9989
+ success: true,
9990
+ data: {
9991
+ projectCount: projectKeys.length,
9992
+ projects: results
9993
+ }
9994
+ })
9995
+ );
9909
9996
  } else {
9910
9997
  success(`Resync complete: ${projectKeys.length} projects`, opts);
9911
9998
  for (const [key, result] of Object.entries(results)) {
@@ -9929,29 +10016,36 @@ daemonCommand.command("review").option("--list", "Just list unknown files withou
9929
10016
  error("Could not load project config.", opts);
9930
10017
  process.exit(1);
9931
10018
  }
9932
- const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
10019
+ const project = queryOne("SELECT id FROM projects WHERE key = ?", [
10020
+ config.project.key
10021
+ ]);
9933
10022
  if (!project) {
9934
10023
  error(`Project "${config.project.key}" not found in database.`, opts);
9935
10024
  process.exit(1);
9936
10025
  }
9937
- const unknownFiles = queryAll(`
10026
+ const unknownFiles = queryAll(
10027
+ `
9938
10028
  SELECT id, path, extension, size_bytes, updated_at
9939
10029
  FROM documents
9940
10030
  WHERE project_id = ? AND monitoring_category = 'unknown' AND status != 'deleted'
9941
10031
  ORDER BY path
9942
- `, [project.id]);
10032
+ `,
10033
+ [project.id]
10034
+ );
9943
10035
  if (unknownFiles.length === 0) {
9944
10036
  success("No unknown files to review! All files are classified.", opts);
9945
10037
  return;
9946
10038
  }
9947
10039
  if (opts.json) {
9948
- console.log(JSON.stringify({
9949
- success: true,
9950
- data: {
9951
- count: unknownFiles.length,
9952
- files: unknownFiles
9953
- }
9954
- }));
10040
+ console.log(
10041
+ JSON.stringify({
10042
+ success: true,
10043
+ data: {
10044
+ count: unknownFiles.length,
10045
+ files: unknownFiles
10046
+ }
10047
+ })
10048
+ );
9955
10049
  return;
9956
10050
  }
9957
10051
  if (options.list) {
@@ -9973,14 +10067,18 @@ daemonCommand.command("review").option("--list", "Just list unknown files withou
9973
10067
  ],
9974
10068
  opts
9975
10069
  );
9976
- console.log('\nUse "aigile daemon allow <pattern>" or "aigile daemon deny <pattern>" to classify files.');
10070
+ console.log(
10071
+ '\nUse "aigile daemon allow <pattern>" or "aigile daemon deny <pattern>" to classify files.'
10072
+ );
9977
10073
  return;
9978
10074
  }
9979
10075
  console.log(`
9980
10076
  \u{1F4CB} Unknown Files (${unknownFiles.length} total)
9981
10077
  `);
9982
10078
  console.log('Run "aigile daemon review --list" to see all files.');
9983
- console.log('Use "aigile daemon allow <pattern>" or "aigile daemon deny <pattern>" to classify.\n');
10079
+ console.log(
10080
+ 'Use "aigile daemon allow <pattern>" or "aigile daemon deny <pattern>" to classify.\n'
10081
+ );
9984
10082
  const sample = unknownFiles.slice(0, 10);
9985
10083
  for (const file of sample) {
9986
10084
  console.log(` ${file.path} (.${file.extension || "no ext"})`);
@@ -10000,11 +10098,13 @@ function formatBytes(bytes) {
10000
10098
  init_connection();
10001
10099
  init_connection();
10002
10100
  import { Command as Command21 } from "commander";
10003
- import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
10004
- import { join as join12, relative as relative4 } from "path";
10101
+ import { readFileSync as readFileSync12, existsSync as existsSync12 } from "fs";
10102
+ import { join as join13, relative as relative6 } from "path";
10005
10103
  import { glob } from "glob";
10006
10104
  init_config();
10007
- var fileCommand = new Command21("file").description("Shadow mode file analysis and management for brownfield projects");
10105
+ var fileCommand = new Command21("file").description(
10106
+ "Shadow mode file analysis and management for brownfield projects"
10107
+ );
10008
10108
  function getProjectContext(opts) {
10009
10109
  const projectRoot = findProjectRoot();
10010
10110
  if (!projectRoot) {
@@ -10016,10 +10116,9 @@ function getProjectContext(opts) {
10016
10116
  error("Could not load project config.", opts);
10017
10117
  return null;
10018
10118
  }
10019
- const project = queryOne(
10020
- "SELECT id FROM projects WHERE key = ?",
10021
- [config.project.key]
10022
- );
10119
+ const project = queryOne("SELECT id FROM projects WHERE key = ?", [
10120
+ config.project.key
10121
+ ]);
10023
10122
  if (!project) {
10024
10123
  error(`Project "${config.project.key}" not found in database.`, opts);
10025
10124
  return null;
@@ -10042,12 +10141,15 @@ fileCommand.command("analyze <path>").option("--tldr <tldr>", "One-line summary
10042
10141
  if (!ctx) {
10043
10142
  process.exit(1);
10044
10143
  }
10045
- const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
10144
+ const normalizedPath = filePath.startsWith("/") ? relative6(ctx.projectRoot, filePath) : filePath;
10046
10145
  const doc = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
10047
10146
  if (!doc) {
10048
10147
  const tracked = trackShadowFile(ctx.projectId, ctx.projectRoot, normalizedPath);
10049
10148
  if (!tracked) {
10050
- error(`File not tracked. Run "aigile sync scan" or track with "aigile file track ${normalizedPath}"`, opts);
10149
+ error(
10150
+ `File not tracked. Run "aigile sync scan" or track with "aigile file track ${normalizedPath}"`,
10151
+ opts
10152
+ );
10051
10153
  process.exit(1);
10052
10154
  }
10053
10155
  }
@@ -10072,24 +10174,26 @@ fileCommand.command("analyze <path>").option("--tldr <tldr>", "One-line summary
10072
10174
  }
10073
10175
  const updatedDoc = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
10074
10176
  if (opts.json) {
10075
- console.log(JSON.stringify({
10076
- success: true,
10077
- data: {
10078
- path: normalizedPath,
10079
- analyzed: true,
10080
- analyzedAt: updatedDoc?.analyzed_at,
10081
- metadata: {
10082
- tldr: updatedDoc?.meta_tldr,
10083
- module: updatedDoc?.inferred_module,
10084
- component: updatedDoc?.inferred_component,
10085
- type: updatedDoc?.file_type,
10086
- dependencies: updatedDoc?.meta_dependencies ? JSON.parse(updatedDoc.meta_dependencies) : null,
10087
- exports: updatedDoc?.exports ? JSON.parse(updatedDoc.exports) : null,
10088
- complexity: updatedDoc?.complexity_score,
10089
- confidence: updatedDoc?.analysis_confidence
10177
+ console.log(
10178
+ JSON.stringify({
10179
+ success: true,
10180
+ data: {
10181
+ path: normalizedPath,
10182
+ analyzed: true,
10183
+ analyzedAt: updatedDoc?.analyzed_at,
10184
+ metadata: {
10185
+ tldr: updatedDoc?.meta_tldr,
10186
+ module: updatedDoc?.inferred_module,
10187
+ component: updatedDoc?.inferred_component,
10188
+ type: updatedDoc?.file_type,
10189
+ dependencies: updatedDoc?.meta_dependencies ? JSON.parse(updatedDoc.meta_dependencies) : null,
10190
+ exports: updatedDoc?.exports ? JSON.parse(updatedDoc.exports) : null,
10191
+ complexity: updatedDoc?.complexity_score,
10192
+ confidence: updatedDoc?.analysis_confidence
10193
+ }
10090
10194
  }
10091
- }
10092
- }));
10195
+ })
10196
+ );
10093
10197
  } else {
10094
10198
  success(`Analysis added for: ${normalizedPath}`, opts);
10095
10199
  if (updatedDoc?.meta_tldr) {
@@ -10138,24 +10242,26 @@ fileCommand.command("list").alias("ls").option("--unanalyzed", "Only files witho
10138
10242
  documents = getUnanalyzedDocuments(ctx.projectId, limit, offset);
10139
10243
  }
10140
10244
  if (opts.json || options.format === "json") {
10141
- console.log(JSON.stringify({
10142
- success: true,
10143
- data: documents.map((d) => ({
10144
- path: d.path,
10145
- filename: d.filename,
10146
- extension: d.extension,
10147
- module: d.inferred_module,
10148
- component: d.inferred_component,
10149
- fileType: d.file_type,
10150
- analyzed: !!d.analyzed_at,
10151
- analyzedAt: d.analyzed_at,
10152
- confidence: d.analysis_confidence,
10153
- tldr: d.meta_tldr,
10154
- dependencies: d.meta_dependencies ? JSON.parse(d.meta_dependencies) : null,
10155
- exports: d.exports ? JSON.parse(d.exports) : null,
10156
- complexity: d.complexity_score
10157
- }))
10158
- }));
10245
+ console.log(
10246
+ JSON.stringify({
10247
+ success: true,
10248
+ data: documents.map((d) => ({
10249
+ path: d.path,
10250
+ filename: d.filename,
10251
+ extension: d.extension,
10252
+ module: d.inferred_module,
10253
+ component: d.inferred_component,
10254
+ fileType: d.file_type,
10255
+ analyzed: !!d.analyzed_at,
10256
+ analyzedAt: d.analyzed_at,
10257
+ confidence: d.analysis_confidence,
10258
+ tldr: d.meta_tldr,
10259
+ dependencies: d.meta_dependencies ? JSON.parse(d.meta_dependencies) : null,
10260
+ exports: d.exports ? JSON.parse(d.exports) : null,
10261
+ complexity: d.complexity_score
10262
+ }))
10263
+ })
10264
+ );
10159
10265
  } else if (options.format === "paths") {
10160
10266
  documents.forEach((d) => console.log(d.path));
10161
10267
  } else {
@@ -10214,15 +10320,15 @@ fileCommand.command("read <path>").option("--with-metadata", "Include existing D
10214
10320
  if (!ctx) {
10215
10321
  process.exit(1);
10216
10322
  }
10217
- const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
10218
- const fullPath = join12(ctx.projectRoot, normalizedPath);
10219
- if (!existsSync10(fullPath)) {
10323
+ const normalizedPath = filePath.startsWith("/") ? relative6(ctx.projectRoot, filePath) : filePath;
10324
+ const fullPath = join13(ctx.projectRoot, normalizedPath);
10325
+ if (!existsSync12(fullPath)) {
10220
10326
  error(`File not found: ${normalizedPath}`, opts);
10221
10327
  process.exit(1);
10222
10328
  }
10223
10329
  let content;
10224
10330
  try {
10225
- content = readFileSync10(fullPath, "utf-8");
10331
+ content = readFileSync12(fullPath, "utf-8");
10226
10332
  } catch {
10227
10333
  error(`Could not read file: ${normalizedPath}`, opts);
10228
10334
  process.exit(1);
@@ -10236,22 +10342,24 @@ fileCommand.command("read <path>").option("--with-metadata", "Include existing D
10236
10342
  lines = lines.slice(0, options.limit);
10237
10343
  }
10238
10344
  if (opts.json) {
10239
- console.log(JSON.stringify({
10240
- success: true,
10241
- data: {
10242
- path: normalizedPath,
10243
- content: lines.join("\n"),
10244
- lineCount: lines.length,
10245
- metadata: metadata ? {
10246
- tldr: metadata.meta_tldr,
10247
- module: metadata.inferred_module,
10248
- component: metadata.inferred_component,
10249
- fileType: metadata.file_type,
10250
- analyzed: !!metadata.analyzed_at,
10251
- confidence: metadata.analysis_confidence
10252
- } : null
10253
- }
10254
- }));
10345
+ console.log(
10346
+ JSON.stringify({
10347
+ success: true,
10348
+ data: {
10349
+ path: normalizedPath,
10350
+ content: lines.join("\n"),
10351
+ lineCount: lines.length,
10352
+ metadata: metadata ? {
10353
+ tldr: metadata.meta_tldr,
10354
+ module: metadata.inferred_module,
10355
+ component: metadata.inferred_component,
10356
+ fileType: metadata.file_type,
10357
+ analyzed: !!metadata.analyzed_at,
10358
+ confidence: metadata.analysis_confidence
10359
+ } : null
10360
+ }
10361
+ })
10362
+ );
10255
10363
  } else {
10256
10364
  if (options.withMetadata && metadata) {
10257
10365
  console.log(`# File: ${normalizedPath}`);
@@ -10275,7 +10383,7 @@ fileCommand.command("track <path>").option("--type <type>", "File type classific
10275
10383
  if (!ctx) {
10276
10384
  process.exit(1);
10277
10385
  }
10278
- const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
10386
+ const normalizedPath = filePath.startsWith("/") ? relative6(ctx.projectRoot, filePath) : filePath;
10279
10387
  const tracked = trackShadowFile(ctx.projectId, ctx.projectRoot, normalizedPath);
10280
10388
  if (!tracked) {
10281
10389
  error(`Could not track file: ${normalizedPath}`, opts);
@@ -10290,18 +10398,20 @@ fileCommand.command("track <path>").option("--type <type>", "File type classific
10290
10398
  }
10291
10399
  const doc = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
10292
10400
  if (opts.json) {
10293
- console.log(JSON.stringify({
10294
- success: true,
10295
- data: {
10296
- path: normalizedPath,
10297
- tracked: true,
10298
- shadowMode: true,
10299
- metadata: doc ? {
10300
- fileType: doc.file_type,
10301
- module: doc.inferred_module
10302
- } : null
10303
- }
10304
- }));
10401
+ console.log(
10402
+ JSON.stringify({
10403
+ success: true,
10404
+ data: {
10405
+ path: normalizedPath,
10406
+ tracked: true,
10407
+ shadowMode: true,
10408
+ metadata: doc ? {
10409
+ fileType: doc.file_type,
10410
+ module: doc.inferred_module
10411
+ } : null
10412
+ }
10413
+ })
10414
+ );
10305
10415
  } else {
10306
10416
  success(`Tracked in shadow mode: ${normalizedPath}`, opts);
10307
10417
  }
@@ -10312,39 +10422,41 @@ fileCommand.command("show <path>").description("Show detailed file analysis").ac
10312
10422
  if (!ctx) {
10313
10423
  process.exit(1);
10314
10424
  }
10315
- const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
10425
+ const normalizedPath = filePath.startsWith("/") ? relative6(ctx.projectRoot, filePath) : filePath;
10316
10426
  const doc = getDocumentWithAnalysis(ctx.projectId, normalizedPath);
10317
10427
  if (!doc) {
10318
10428
  error(`File not tracked: ${normalizedPath}`, opts);
10319
10429
  process.exit(1);
10320
10430
  }
10321
10431
  if (opts.json) {
10322
- console.log(JSON.stringify({
10323
- success: true,
10324
- data: {
10325
- path: doc.path,
10326
- filename: doc.filename,
10327
- extension: doc.extension,
10328
- status: doc.status,
10329
- sizeBytes: doc.size_bytes,
10330
- lastScanned: doc.last_scanned_at,
10331
- hasFrontmatter: !!doc.has_frontmatter,
10332
- shadowMode: !!doc.shadow_mode,
10333
- analysis: {
10334
- analyzed: !!doc.analyzed_at,
10335
- analyzedAt: doc.analyzed_at,
10336
- confidence: doc.analysis_confidence,
10337
- tldr: doc.meta_tldr,
10338
- module: doc.inferred_module,
10339
- component: doc.inferred_component,
10340
- fileType: doc.file_type,
10341
- complexity: doc.complexity_score,
10342
- dependencies: doc.meta_dependencies ? JSON.parse(doc.meta_dependencies) : null,
10343
- exports: doc.exports ? JSON.parse(doc.exports) : null,
10344
- notes: doc.analysis_notes
10432
+ console.log(
10433
+ JSON.stringify({
10434
+ success: true,
10435
+ data: {
10436
+ path: doc.path,
10437
+ filename: doc.filename,
10438
+ extension: doc.extension,
10439
+ status: doc.status,
10440
+ sizeBytes: doc.size_bytes,
10441
+ lastScanned: doc.last_scanned_at,
10442
+ hasFrontmatter: !!doc.has_frontmatter,
10443
+ shadowMode: !!doc.shadow_mode,
10444
+ analysis: {
10445
+ analyzed: !!doc.analyzed_at,
10446
+ analyzedAt: doc.analyzed_at,
10447
+ confidence: doc.analysis_confidence,
10448
+ tldr: doc.meta_tldr,
10449
+ module: doc.inferred_module,
10450
+ component: doc.inferred_component,
10451
+ fileType: doc.file_type,
10452
+ complexity: doc.complexity_score,
10453
+ dependencies: doc.meta_dependencies ? JSON.parse(doc.meta_dependencies) : null,
10454
+ exports: doc.exports ? JSON.parse(doc.exports) : null,
10455
+ notes: doc.analysis_notes
10456
+ }
10345
10457
  }
10346
- }
10347
- }));
10458
+ })
10459
+ );
10348
10460
  } else {
10349
10461
  const displayData = {
10350
10462
  path: doc.path,
@@ -10403,8 +10515,8 @@ fileCommand.command("tag").argument("<path>", "File path to tag").option("--chun
10403
10515
  error('No active session. Start one with "aigile session start".', opts);
10404
10516
  process.exit(1);
10405
10517
  }
10406
- const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
10407
- const doc = queryOne(
10518
+ const normalizedPath = filePath.startsWith("/") ? relative6(ctx.projectRoot, filePath) : filePath;
10519
+ let doc = queryOne(
10408
10520
  "SELECT id FROM documents WHERE project_id = ? AND path = ?",
10409
10521
  [ctx.projectId, normalizedPath]
10410
10522
  );
@@ -10422,7 +10534,7 @@ fileCommand.command("tag").argument("<path>", "File path to tag").option("--chun
10422
10534
  error(`Could not track file: ${normalizedPath}`, opts);
10423
10535
  process.exit(1);
10424
10536
  }
10425
- doc.id = newDoc.id;
10537
+ doc = newDoc;
10426
10538
  }
10427
10539
  const validTypes = ["assigned", "explored", "skipped"];
10428
10540
  if (!validTypes.includes(options.type)) {
@@ -10437,17 +10549,19 @@ fileCommand.command("tag").argument("<path>", "File path to tag").option("--chun
10437
10549
  isFoundational: options.foundational ?? false
10438
10550
  });
10439
10551
  if (opts.json) {
10440
- console.log(JSON.stringify({
10441
- success: true,
10442
- data: {
10443
- session_file_id: sessionFileId,
10444
- path: normalizedPath,
10445
- session_id: session.id,
10446
- chunk_id: options.chunk ?? null,
10447
- review_type: options.type,
10448
- is_foundational: options.foundational ?? false
10449
- }
10450
- }));
10552
+ console.log(
10553
+ JSON.stringify({
10554
+ success: true,
10555
+ data: {
10556
+ session_file_id: sessionFileId,
10557
+ path: normalizedPath,
10558
+ session_id: session.id,
10559
+ chunk_id: options.chunk ?? null,
10560
+ review_type: options.type,
10561
+ is_foundational: options.foundational ?? false
10562
+ }
10563
+ })
10564
+ );
10451
10565
  } else {
10452
10566
  success(`Tagged: ${normalizedPath}`, opts);
10453
10567
  if (options.chunk) {
@@ -10470,7 +10584,7 @@ fileCommand.command("tag-batch").option("--chunk <id>", "Chunk ID for all files"
10470
10584
  let filesToTag = [];
10471
10585
  if (options.glob) {
10472
10586
  const matches = await glob(options.glob, { cwd: ctx.projectRoot, nodir: true });
10473
- filesToTag = matches.map((f) => relative4(ctx.projectRoot, join12(ctx.projectRoot, f)));
10587
+ filesToTag = matches.map((f) => relative6(ctx.projectRoot, join13(ctx.projectRoot, f)));
10474
10588
  } else {
10475
10589
  error("Please provide --glob pattern. Stdin not supported yet.", opts);
10476
10590
  process.exit(1);
@@ -10499,10 +10613,12 @@ fileCommand.command("tag-batch").option("--chunk <id>", "Chunk ID for all files"
10499
10613
  tagged++;
10500
10614
  }
10501
10615
  if (opts.json) {
10502
- console.log(JSON.stringify({
10503
- success: true,
10504
- data: { tagged, skipped, total: filesToTag.length }
10505
- }));
10616
+ console.log(
10617
+ JSON.stringify({
10618
+ success: true,
10619
+ data: { tagged, skipped, total: filesToTag.length }
10620
+ })
10621
+ );
10506
10622
  } else {
10507
10623
  success(`Tagged ${tagged} files (${skipped} skipped - not tracked)`, opts);
10508
10624
  }
@@ -10538,12 +10654,14 @@ fileCommand.command("untag").argument("<path>", "File path to untag").option("--
10538
10654
  warning(`File "${filePath}" is not tagged in this session.`, opts);
10539
10655
  return;
10540
10656
  }
10541
- queryOne("DELETE FROM session_files WHERE id = ?", [existing.id]);
10657
+ run("DELETE FROM session_files WHERE id = ?", [existing.id]);
10542
10658
  if (opts.json) {
10543
- console.log(JSON.stringify({
10544
- success: true,
10545
- data: { path: filePath, untagged: true }
10546
- }));
10659
+ console.log(
10660
+ JSON.stringify({
10661
+ success: true,
10662
+ data: { path: filePath, untagged: true }
10663
+ })
10664
+ );
10547
10665
  } else {
10548
10666
  success(`Untagged: ${filePath}`, opts);
10549
10667
  }
@@ -10586,12 +10704,14 @@ fileCommand.command("clear-tags").option("--session <id>", "Session ID (default:
10586
10704
  deleteQuery += " AND chunk_id = ?";
10587
10705
  deleteParams.push(options.chunk);
10588
10706
  }
10589
- queryOne(deleteQuery, deleteParams);
10707
+ run(deleteQuery, deleteParams);
10590
10708
  if (opts.json) {
10591
- console.log(JSON.stringify({
10592
- success: true,
10593
- data: { session_id: sessionId, cleared: count }
10594
- }));
10709
+ console.log(
10710
+ JSON.stringify({
10711
+ success: true,
10712
+ data: { session_id: sessionId, cleared: count }
10713
+ })
10714
+ );
10595
10715
  } else {
10596
10716
  success(`Cleared ${count} tag(s)`, opts);
10597
10717
  }
@@ -10616,15 +10736,17 @@ fileCommand.command("untagged").option("--session <id>", "Session ID (default: c
10616
10736
  assignedOnly: options.assignedOnly
10617
10737
  });
10618
10738
  if (opts.json) {
10619
- console.log(JSON.stringify({
10620
- success: true,
10621
- data: {
10622
- session_id: sessionId,
10623
- chunk_id: options.chunk ?? null,
10624
- count: untagged.length,
10625
- files: untagged.map((f) => f.path)
10626
- }
10627
- }));
10739
+ console.log(
10740
+ JSON.stringify({
10741
+ success: true,
10742
+ data: {
10743
+ session_id: sessionId,
10744
+ chunk_id: options.chunk ?? null,
10745
+ count: untagged.length,
10746
+ files: untagged.map((f) => f.path)
10747
+ }
10748
+ })
10749
+ );
10628
10750
  } else {
10629
10751
  if (untagged.length === 0) {
10630
10752
  success("All files have been tagged!", opts);
@@ -10692,22 +10814,24 @@ fileCommand.command("coverage").option("--session <id>", "Session ID (default: c
10692
10814
  const total = totalTagged + untagged.length;
10693
10815
  const pct = total > 0 ? Math.round(totalTagged / total * 100) : 100;
10694
10816
  if (opts.json) {
10695
- console.log(JSON.stringify({
10696
- success: true,
10697
- data: {
10698
- session_id: sessionId,
10699
- total_files: total,
10700
- tagged: totalTagged,
10701
- untagged: untagged.length,
10702
- coverage_percent: pct,
10703
- by_type: {
10704
- assigned: stats.assigned.reviewed,
10705
- explored: stats.explored,
10706
- foundational: stats.foundational,
10707
- skipped: stats.skipped
10817
+ console.log(
10818
+ JSON.stringify({
10819
+ success: true,
10820
+ data: {
10821
+ session_id: sessionId,
10822
+ total_files: total,
10823
+ tagged: totalTagged,
10824
+ untagged: untagged.length,
10825
+ coverage_percent: pct,
10826
+ by_type: {
10827
+ assigned: stats.assigned.reviewed,
10828
+ explored: stats.explored,
10829
+ foundational: stats.foundational,
10830
+ skipped: stats.skipped
10831
+ }
10708
10832
  }
10709
- }
10710
- }));
10833
+ })
10834
+ );
10711
10835
  } else {
10712
10836
  console.log(`
10713
10837
  Coverage for session ${sessionId.slice(0, 8)}...`);
@@ -10734,7 +10858,7 @@ fileCommand.command("flag").argument("<path>", "File path to flag").option("--du
10734
10858
  error('No active session. Start one with "aigile session start".', opts);
10735
10859
  process.exit(1);
10736
10860
  }
10737
- const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
10861
+ const normalizedPath = filePath.startsWith("/") ? relative6(ctx.projectRoot, filePath) : filePath;
10738
10862
  const sessionFile = queryOne(
10739
10863
  `SELECT sf.id FROM session_files sf
10740
10864
  JOIN documents d ON sf.document_id = d.id
@@ -10742,7 +10866,10 @@ fileCommand.command("flag").argument("<path>", "File path to flag").option("--du
10742
10866
  [session.id, normalizedPath]
10743
10867
  );
10744
10868
  if (!sessionFile) {
10745
- error(`File not tagged in this session: ${normalizedPath}. Tag it first with "aigile file tag".`, opts);
10869
+ error(
10870
+ `File not tagged in this session: ${normalizedPath}. Tag it first with "aigile file tag".`,
10871
+ opts
10872
+ );
10746
10873
  process.exit(1);
10747
10874
  }
10748
10875
  const issues = [];
@@ -10761,13 +10888,15 @@ fileCommand.command("flag").argument("<path>", "File path to flag").option("--du
10761
10888
  }
10762
10889
  flagFileQualityIssue(sessionFile.id, issues);
10763
10890
  if (opts.json) {
10764
- console.log(JSON.stringify({
10765
- success: true,
10766
- data: {
10767
- path: normalizedPath,
10768
- issues
10769
- }
10770
- }));
10891
+ console.log(
10892
+ JSON.stringify({
10893
+ success: true,
10894
+ data: {
10895
+ path: normalizedPath,
10896
+ issues
10897
+ }
10898
+ })
10899
+ );
10771
10900
  } else {
10772
10901
  success(`Flagged: ${normalizedPath}`, opts);
10773
10902
  for (const issue of issues) {
@@ -10795,10 +10924,9 @@ fileCommand.command("duplicates").option("--session <id>", "Session ID (default:
10795
10924
  for (const sf of filesWithIssues) {
10796
10925
  if (!sf.quality_issues) continue;
10797
10926
  const issues = JSON.parse(sf.quality_issues);
10798
- const doc = queryOne(
10799
- "SELECT path FROM documents WHERE id = ?",
10800
- [sf.document_id]
10801
- );
10927
+ const doc = queryOne("SELECT path FROM documents WHERE id = ?", [
10928
+ sf.document_id
10929
+ ]);
10802
10930
  if (!doc) continue;
10803
10931
  for (const issue of issues) {
10804
10932
  if (issue.startsWith("duplicate:")) {
@@ -10813,10 +10941,12 @@ fileCommand.command("duplicates").option("--session <id>", "Session ID (default:
10813
10941
  }
10814
10942
  }
10815
10943
  if (opts.json) {
10816
- console.log(JSON.stringify({
10817
- success: true,
10818
- data: { duplicates }
10819
- }));
10944
+ console.log(
10945
+ JSON.stringify({
10946
+ success: true,
10947
+ data: { duplicates }
10948
+ })
10949
+ );
10820
10950
  } else {
10821
10951
  if (duplicates.length === 0) {
10822
10952
  info("No duplicates flagged.", opts);
@@ -10853,10 +10983,9 @@ fileCommand.command("issues").option("--session <id>", "Session ID (default: cur
10853
10983
  }
10854
10984
  const issueList = [];
10855
10985
  for (const sf of filesWithIssues) {
10856
- const doc = queryOne(
10857
- "SELECT path FROM documents WHERE id = ?",
10858
- [sf.document_id]
10859
- );
10986
+ const doc = queryOne("SELECT path FROM documents WHERE id = ?", [
10987
+ sf.document_id
10988
+ ]);
10860
10989
  if (!doc) continue;
10861
10990
  issueList.push({
10862
10991
  path: doc.path,
@@ -10864,10 +10993,12 @@ fileCommand.command("issues").option("--session <id>", "Session ID (default: cur
10864
10993
  });
10865
10994
  }
10866
10995
  if (opts.json) {
10867
- console.log(JSON.stringify({
10868
- success: true,
10869
- data: { files_with_issues: issueList }
10870
- }));
10996
+ console.log(
10997
+ JSON.stringify({
10998
+ success: true,
10999
+ data: { files_with_issues: issueList }
11000
+ })
11001
+ );
10871
11002
  } else {
10872
11003
  console.log(`Files with quality issues (${issueList.length}):`);
10873
11004
  for (const file of issueList) {
@@ -10885,7 +11016,7 @@ init_connection();
10885
11016
  init_connection();
10886
11017
  import { Command as Command22 } from "commander";
10887
11018
  import { glob as glob2 } from "glob";
10888
- import { relative as relative5, resolve as resolve2 } from "path";
11019
+ import { relative as relative7, resolve as resolve6 } from "path";
10889
11020
  init_config();
10890
11021
  function safeParseArray(json) {
10891
11022
  if (!json) return [];
@@ -10932,7 +11063,7 @@ chunkCommand.command("create").argument("<id>", "Chunk ID (e.g., chunk-001)").op
10932
11063
  if (options.pattern) {
10933
11064
  for (const pattern of options.pattern) {
10934
11065
  const matches = await glob2(pattern, { cwd: projectRoot, nodir: true });
10935
- assignedFiles.push(...matches.map((f) => relative5(projectRoot, resolve2(projectRoot, f))));
11066
+ assignedFiles.push(...matches.map((f) => relative7(projectRoot, resolve6(projectRoot, f))));
10936
11067
  }
10937
11068
  }
10938
11069
  if (options.assign) {
@@ -11046,12 +11177,12 @@ chunkCommand.command("list").alias("ls").description("List all chunks in current
11046
11177
  }
11047
11178
  const session = getActiveSession(project.id);
11048
11179
  if (!session) {
11049
- warning2("No active session.", opts);
11180
+ warning("No active session.", opts);
11050
11181
  return;
11051
11182
  }
11052
11183
  const chunks = getSessionChunks(session.id);
11053
11184
  if (chunks.length === 0) {
11054
- warning2("No chunks defined in current session.", opts);
11185
+ warning("No chunks defined in current session.", opts);
11055
11186
  return;
11056
11187
  }
11057
11188
  data(