@vladimir-ks/aigile 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/aigile.js +1253 -17
- package/dist/bin/aigile.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -1
package/dist/bin/aigile.js
CHANGED
|
@@ -407,16 +407,26 @@ var init_config = __esm({
|
|
|
407
407
|
// src/db/connection.ts
|
|
408
408
|
var connection_exports = {};
|
|
409
409
|
__export(connection_exports, {
|
|
410
|
+
assignFilesToChunk: () => assignFilesToChunk,
|
|
410
411
|
closeDatabase: () => closeDatabase,
|
|
412
|
+
createChunk: () => createChunk,
|
|
413
|
+
flagFileQualityIssue: () => flagFileQualityIssue,
|
|
411
414
|
generateId: () => generateId,
|
|
415
|
+
getChunk: () => getChunk,
|
|
416
|
+
getCoverageStats: () => getCoverageStats,
|
|
412
417
|
getDatabase: () => getDatabase,
|
|
418
|
+
getFilesWithQualityIssues: () => getFilesWithQualityIssues,
|
|
413
419
|
getNextKey: () => getNextKey,
|
|
420
|
+
getSessionChunks: () => getSessionChunks,
|
|
421
|
+
getSessionFiles: () => getSessionFiles,
|
|
422
|
+
getUntaggedFiles: () => getUntaggedFiles,
|
|
414
423
|
initDatabase: () => initDatabase,
|
|
415
424
|
queryAll: () => queryAll,
|
|
416
425
|
queryOne: () => queryOne,
|
|
417
426
|
run: () => run,
|
|
418
427
|
runMigrations: () => runMigrations,
|
|
419
|
-
saveDatabase: () => saveDatabase
|
|
428
|
+
saveDatabase: () => saveDatabase,
|
|
429
|
+
tagFileReviewed: () => tagFileReviewed
|
|
420
430
|
});
|
|
421
431
|
import initSqlJs from "sql.js";
|
|
422
432
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -748,6 +758,7 @@ function initializeSchema(database) {
|
|
|
748
758
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
749
759
|
id TEXT PRIMARY KEY,
|
|
750
760
|
project_id TEXT REFERENCES projects(id),
|
|
761
|
+
name TEXT,
|
|
751
762
|
started_at TEXT DEFAULT (datetime('now')),
|
|
752
763
|
ended_at TEXT,
|
|
753
764
|
summary TEXT,
|
|
@@ -789,6 +800,39 @@ function initializeSchema(database) {
|
|
|
789
800
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
790
801
|
)
|
|
791
802
|
`);
|
|
803
|
+
database.run(`
|
|
804
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
805
|
+
id TEXT PRIMARY KEY,
|
|
806
|
+
session_id TEXT REFERENCES sessions(id),
|
|
807
|
+
name TEXT NOT NULL,
|
|
808
|
+
patterns TEXT,
|
|
809
|
+
assigned_files TEXT,
|
|
810
|
+
review_mode TEXT DEFAULT 'standard',
|
|
811
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
812
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
813
|
+
)
|
|
814
|
+
`);
|
|
815
|
+
database.run(`
|
|
816
|
+
CREATE TABLE IF NOT EXISTS session_files (
|
|
817
|
+
id TEXT PRIMARY KEY,
|
|
818
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
819
|
+
document_id TEXT NOT NULL REFERENCES documents(id),
|
|
820
|
+
chunk_id TEXT REFERENCES chunks(id),
|
|
821
|
+
agent_id TEXT,
|
|
822
|
+
report_path TEXT,
|
|
823
|
+
reviewed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
824
|
+
review_type TEXT DEFAULT 'assigned',
|
|
825
|
+
is_foundational INTEGER DEFAULT 0,
|
|
826
|
+
quality_issues TEXT,
|
|
827
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
828
|
+
UNIQUE(session_id, document_id)
|
|
829
|
+
)
|
|
830
|
+
`);
|
|
831
|
+
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)`);
|
|
833
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_chunk ON session_files(chunk_id)`);
|
|
834
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_report ON session_files(report_path)`);
|
|
835
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(session_id)`);
|
|
792
836
|
}
|
|
793
837
|
function generateId() {
|
|
794
838
|
return randomUUID();
|
|
@@ -812,6 +856,194 @@ function getNextKey(projectKey) {
|
|
|
812
856
|
}
|
|
813
857
|
return `${projectKey}-${nextValue}`;
|
|
814
858
|
}
|
|
859
|
+
function createChunk(sessionId, chunkId, name, patterns, assignedFiles, reviewMode = "standard") {
|
|
860
|
+
run(
|
|
861
|
+
`INSERT INTO chunks (id, session_id, name, patterns, assigned_files, review_mode)
|
|
862
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
863
|
+
[
|
|
864
|
+
chunkId,
|
|
865
|
+
sessionId,
|
|
866
|
+
name,
|
|
867
|
+
patterns ? JSON.stringify(patterns) : null,
|
|
868
|
+
assignedFiles ? JSON.stringify(assignedFiles) : null,
|
|
869
|
+
reviewMode
|
|
870
|
+
]
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
function getChunk(chunkId) {
|
|
874
|
+
return queryOne("SELECT * FROM chunks WHERE id = ?", [chunkId]);
|
|
875
|
+
}
|
|
876
|
+
function getSessionChunks(sessionId) {
|
|
877
|
+
return queryAll(
|
|
878
|
+
"SELECT * FROM chunks WHERE session_id = ? ORDER BY created_at",
|
|
879
|
+
[sessionId]
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
function assignFilesToChunk(chunkId, files) {
|
|
883
|
+
const chunk = getChunk(chunkId);
|
|
884
|
+
if (!chunk) {
|
|
885
|
+
throw new Error(`Chunk "${chunkId}" not found`);
|
|
886
|
+
}
|
|
887
|
+
const existing = chunk.assigned_files ? JSON.parse(chunk.assigned_files) : [];
|
|
888
|
+
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
|
+
);
|
|
893
|
+
}
|
|
894
|
+
function tagFileReviewed(sessionId, documentId, options = {}) {
|
|
895
|
+
const existing = queryOne(
|
|
896
|
+
"SELECT id FROM session_files WHERE session_id = ? AND document_id = ?",
|
|
897
|
+
[sessionId, documentId]
|
|
898
|
+
);
|
|
899
|
+
if (existing) {
|
|
900
|
+
run(
|
|
901
|
+
`UPDATE session_files SET
|
|
902
|
+
chunk_id = COALESCE(?, chunk_id),
|
|
903
|
+
agent_id = COALESCE(?, agent_id),
|
|
904
|
+
report_path = COALESCE(?, report_path),
|
|
905
|
+
review_type = ?,
|
|
906
|
+
is_foundational = CASE WHEN ? = 1 THEN 1 ELSE is_foundational END,
|
|
907
|
+
reviewed_at = datetime('now')
|
|
908
|
+
WHERE id = ?`,
|
|
909
|
+
[
|
|
910
|
+
options.chunkId ?? null,
|
|
911
|
+
options.agentId ?? null,
|
|
912
|
+
options.reportPath ?? null,
|
|
913
|
+
options.reviewType ?? "assigned",
|
|
914
|
+
options.isFoundational ? 1 : 0,
|
|
915
|
+
existing.id
|
|
916
|
+
]
|
|
917
|
+
);
|
|
918
|
+
return existing.id;
|
|
919
|
+
}
|
|
920
|
+
const id = generateId();
|
|
921
|
+
run(
|
|
922
|
+
`INSERT INTO session_files (id, session_id, document_id, chunk_id, agent_id, report_path, review_type, is_foundational)
|
|
923
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
924
|
+
[
|
|
925
|
+
id,
|
|
926
|
+
sessionId,
|
|
927
|
+
documentId,
|
|
928
|
+
options.chunkId ?? null,
|
|
929
|
+
options.agentId ?? null,
|
|
930
|
+
options.reportPath ?? null,
|
|
931
|
+
options.reviewType ?? "assigned",
|
|
932
|
+
options.isFoundational ? 1 : 0
|
|
933
|
+
]
|
|
934
|
+
);
|
|
935
|
+
return id;
|
|
936
|
+
}
|
|
937
|
+
function flagFileQualityIssue(sessionFileId, issues) {
|
|
938
|
+
const existing = queryOne(
|
|
939
|
+
"SELECT quality_issues FROM session_files WHERE id = ?",
|
|
940
|
+
[sessionFileId]
|
|
941
|
+
);
|
|
942
|
+
const current = existing?.quality_issues ? JSON.parse(existing.quality_issues) : [];
|
|
943
|
+
const merged = [.../* @__PURE__ */ new Set([...current, ...issues])];
|
|
944
|
+
run(
|
|
945
|
+
`UPDATE session_files SET quality_issues = ? WHERE id = ?`,
|
|
946
|
+
[JSON.stringify(merged), sessionFileId]
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
function getSessionFiles(sessionId, options = {}) {
|
|
950
|
+
let sql = "SELECT * FROM session_files WHERE session_id = ?";
|
|
951
|
+
const params = [sessionId];
|
|
952
|
+
if (options.chunkId) {
|
|
953
|
+
sql += " AND chunk_id = ?";
|
|
954
|
+
params.push(options.chunkId);
|
|
955
|
+
}
|
|
956
|
+
if (options.reviewType) {
|
|
957
|
+
sql += " AND review_type = ?";
|
|
958
|
+
params.push(options.reviewType);
|
|
959
|
+
}
|
|
960
|
+
if (options.foundationalOnly) {
|
|
961
|
+
sql += " AND is_foundational = 1";
|
|
962
|
+
}
|
|
963
|
+
return queryAll(sql, params);
|
|
964
|
+
}
|
|
965
|
+
function getFilesWithQualityIssues(sessionId) {
|
|
966
|
+
return queryAll(
|
|
967
|
+
`SELECT * FROM session_files
|
|
968
|
+
WHERE session_id = ? AND quality_issues IS NOT NULL AND quality_issues != '[]'`,
|
|
969
|
+
[sessionId]
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
function getUntaggedFiles(projectId, sessionId, options = {}) {
|
|
973
|
+
if (options.assignedOnly && options.chunkId) {
|
|
974
|
+
const chunk = getChunk(options.chunkId);
|
|
975
|
+
if (!chunk || !chunk.assigned_files) {
|
|
976
|
+
return [];
|
|
977
|
+
}
|
|
978
|
+
const assignedFiles = JSON.parse(chunk.assigned_files);
|
|
979
|
+
return queryAll(
|
|
980
|
+
`SELECT d.path, d.id as document_id
|
|
981
|
+
FROM documents d
|
|
982
|
+
WHERE d.project_id = ?
|
|
983
|
+
AND d.path IN (${assignedFiles.map(() => "?").join(",")})
|
|
984
|
+
AND d.id NOT IN (
|
|
985
|
+
SELECT sf.document_id FROM session_files sf WHERE sf.session_id = ?
|
|
986
|
+
)`,
|
|
987
|
+
[projectId, ...assignedFiles, sessionId]
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
return queryAll(
|
|
991
|
+
`SELECT d.path, d.id as document_id
|
|
992
|
+
FROM documents d
|
|
993
|
+
WHERE d.project_id = ?
|
|
994
|
+
AND d.id NOT IN (
|
|
995
|
+
SELECT sf.document_id FROM session_files sf WHERE sf.session_id = ?
|
|
996
|
+
)`,
|
|
997
|
+
[projectId, sessionId]
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
function getCoverageStats(sessionId, chunkId) {
|
|
1001
|
+
const baseWhere = chunkId ? "WHERE session_id = ? AND chunk_id = ?" : "WHERE session_id = ?";
|
|
1002
|
+
const params = chunkId ? [sessionId, chunkId] : [sessionId];
|
|
1003
|
+
const assigned = queryOne(
|
|
1004
|
+
`SELECT COUNT(*) as count FROM session_files ${baseWhere} AND review_type = 'assigned'`,
|
|
1005
|
+
params
|
|
1006
|
+
);
|
|
1007
|
+
const explored = queryOne(
|
|
1008
|
+
`SELECT COUNT(*) as count FROM session_files ${baseWhere} AND review_type = 'explored'`,
|
|
1009
|
+
params
|
|
1010
|
+
);
|
|
1011
|
+
const foundational = queryOne(
|
|
1012
|
+
`SELECT COUNT(*) as count FROM session_files ${baseWhere} AND is_foundational = 1`,
|
|
1013
|
+
params
|
|
1014
|
+
);
|
|
1015
|
+
const skipped = queryOne(
|
|
1016
|
+
`SELECT COUNT(*) as count FROM session_files ${baseWhere} AND review_type = 'skipped'`,
|
|
1017
|
+
params
|
|
1018
|
+
);
|
|
1019
|
+
let assignedTotal = 0;
|
|
1020
|
+
if (chunkId) {
|
|
1021
|
+
const chunk = getChunk(chunkId);
|
|
1022
|
+
if (chunk?.assigned_files) {
|
|
1023
|
+
try {
|
|
1024
|
+
assignedTotal = JSON.parse(chunk.assigned_files).length;
|
|
1025
|
+
} catch {
|
|
1026
|
+
assignedTotal = 0;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
const chunks = getSessionChunks(sessionId);
|
|
1031
|
+
for (const chunk of chunks) {
|
|
1032
|
+
if (chunk.assigned_files) {
|
|
1033
|
+
try {
|
|
1034
|
+
assignedTotal += JSON.parse(chunk.assigned_files).length;
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
assigned: { total: assignedTotal, reviewed: assigned?.count ?? 0 },
|
|
1042
|
+
explored: explored?.count ?? 0,
|
|
1043
|
+
foundational: foundational?.count ?? 0,
|
|
1044
|
+
skipped: skipped?.count ?? 0
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
815
1047
|
function runMigrations() {
|
|
816
1048
|
const database = getDatabase();
|
|
817
1049
|
const columns = queryAll(
|
|
@@ -856,6 +1088,7 @@ function runMigrations() {
|
|
|
856
1088
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
857
1089
|
id TEXT PRIMARY KEY,
|
|
858
1090
|
project_id TEXT REFERENCES projects(id),
|
|
1091
|
+
name TEXT,
|
|
859
1092
|
started_at TEXT DEFAULT (datetime('now')),
|
|
860
1093
|
ended_at TEXT,
|
|
861
1094
|
summary TEXT,
|
|
@@ -864,6 +1097,10 @@ function runMigrations() {
|
|
|
864
1097
|
status TEXT DEFAULT 'active'
|
|
865
1098
|
)
|
|
866
1099
|
`);
|
|
1100
|
+
try {
|
|
1101
|
+
database.run(`ALTER TABLE sessions ADD COLUMN name TEXT`);
|
|
1102
|
+
} catch {
|
|
1103
|
+
}
|
|
867
1104
|
database.run(`
|
|
868
1105
|
CREATE TABLE IF NOT EXISTS activity_log (
|
|
869
1106
|
id TEXT PRIMARY KEY,
|
|
@@ -877,6 +1114,42 @@ function runMigrations() {
|
|
|
877
1114
|
timestamp TEXT DEFAULT (datetime('now'))
|
|
878
1115
|
)
|
|
879
1116
|
`);
|
|
1117
|
+
database.run(`
|
|
1118
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
1119
|
+
id TEXT PRIMARY KEY,
|
|
1120
|
+
session_id TEXT REFERENCES sessions(id),
|
|
1121
|
+
name TEXT NOT NULL,
|
|
1122
|
+
patterns TEXT,
|
|
1123
|
+
assigned_files TEXT,
|
|
1124
|
+
review_mode TEXT DEFAULT 'standard',
|
|
1125
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1126
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
1127
|
+
)
|
|
1128
|
+
`);
|
|
1129
|
+
database.run(`
|
|
1130
|
+
CREATE TABLE IF NOT EXISTS session_files (
|
|
1131
|
+
id TEXT PRIMARY KEY,
|
|
1132
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
1133
|
+
document_id TEXT NOT NULL REFERENCES documents(id),
|
|
1134
|
+
chunk_id TEXT REFERENCES chunks(id),
|
|
1135
|
+
agent_id TEXT,
|
|
1136
|
+
report_path TEXT,
|
|
1137
|
+
reviewed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1138
|
+
review_type TEXT DEFAULT 'assigned',
|
|
1139
|
+
is_foundational INTEGER DEFAULT 0,
|
|
1140
|
+
quality_issues TEXT,
|
|
1141
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1142
|
+
UNIQUE(session_id, document_id)
|
|
1143
|
+
)
|
|
1144
|
+
`);
|
|
1145
|
+
try {
|
|
1146
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_session ON session_files(session_id)`);
|
|
1147
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_document ON session_files(document_id)`);
|
|
1148
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_chunk ON session_files(chunk_id)`);
|
|
1149
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_report ON session_files(report_path)`);
|
|
1150
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(session_id)`);
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
880
1153
|
saveDatabase();
|
|
881
1154
|
}
|
|
882
1155
|
var db, dbPath;
|
|
@@ -890,10 +1163,10 @@ var init_connection = __esm({
|
|
|
890
1163
|
});
|
|
891
1164
|
|
|
892
1165
|
// src/bin/aigile.ts
|
|
893
|
-
import { Command as
|
|
1166
|
+
import { Command as Command23 } from "commander";
|
|
894
1167
|
|
|
895
1168
|
// src/index.ts
|
|
896
|
-
var VERSION = true ? "0.2.
|
|
1169
|
+
var VERSION = true ? "0.2.4" : "0.0.0-dev";
|
|
897
1170
|
|
|
898
1171
|
// src/bin/aigile.ts
|
|
899
1172
|
init_connection();
|
|
@@ -997,7 +1270,7 @@ function error(message, opts = {}) {
|
|
|
997
1270
|
console.error(`${prefix} ${message}`);
|
|
998
1271
|
}
|
|
999
1272
|
}
|
|
1000
|
-
function
|
|
1273
|
+
function warning2(message, opts = {}) {
|
|
1001
1274
|
if (opts.json) {
|
|
1002
1275
|
console.log(JSON.stringify({ warning: message }));
|
|
1003
1276
|
} else {
|
|
@@ -1825,8 +2098,14 @@ function detectGitContext(targetPath) {
|
|
|
1825
2098
|
relativePath
|
|
1826
2099
|
};
|
|
1827
2100
|
}
|
|
2101
|
+
var VALID_PROFILES = ["full-repo", "subrepo", "module"];
|
|
1828
2102
|
function determineProfile(context, options, opts) {
|
|
1829
2103
|
if (options.profile) {
|
|
2104
|
+
if (!VALID_PROFILES.includes(options.profile)) {
|
|
2105
|
+
throw new Error(
|
|
2106
|
+
`Invalid profile "${options.profile}". Valid profiles: ${VALID_PROFILES.join(", ")}`
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
1830
2109
|
return options.profile;
|
|
1831
2110
|
}
|
|
1832
2111
|
if (context.isSubmodule) {
|
|
@@ -1859,6 +2138,8 @@ function determineDbMode(profile, context, options) {
|
|
|
1859
2138
|
case "module":
|
|
1860
2139
|
const parentDbPath = findParentDb(context);
|
|
1861
2140
|
return { mode: "shared", path: parentDbPath };
|
|
2141
|
+
default:
|
|
2142
|
+
return { mode: "local", path: ".aigile/aigile.db" };
|
|
1862
2143
|
}
|
|
1863
2144
|
}
|
|
1864
2145
|
function findParentDb(context) {
|
|
@@ -2098,7 +2379,7 @@ projectCommand.command("list").alias("ls").description("List all registered proj
|
|
|
2098
2379
|
console.log(" \u2713 = valid path, \u2717 = missing/invalid path");
|
|
2099
2380
|
if (invalidCount > 0) {
|
|
2100
2381
|
blank();
|
|
2101
|
-
|
|
2382
|
+
warning2(`${invalidCount} project(s) have invalid paths. Run "aigile project cleanup" to remove.`, opts);
|
|
2102
2383
|
}
|
|
2103
2384
|
});
|
|
2104
2385
|
projectCommand.command("show").argument("[key]", "Project key (uses default if not specified)").description("Show project details").action((key) => {
|
|
@@ -2482,13 +2763,45 @@ epicCommand.command("transition").argument("<key>", "Epic key").argument("<statu
|
|
|
2482
2763
|
run(`UPDATE epics SET status = ?, updated_at = datetime('now') WHERE key = ?`, [status, key]);
|
|
2483
2764
|
success(`Epic "${key}" transitioned from "${epic.status}" to "${status}".`, opts);
|
|
2484
2765
|
});
|
|
2485
|
-
epicCommand.command("delete").alias("rm").argument("<key>", "Epic key").option("--force", "Delete
|
|
2766
|
+
epicCommand.command("delete").alias("rm").argument("<key>", "Epic key").option("--force", "Delete and orphan child stories").option("--cascade", "Delete with all child stories and their tasks").description("Delete epic").action((key, options) => {
|
|
2486
2767
|
const opts = getOutputOptions(epicCommand);
|
|
2487
2768
|
const epic = queryOne("SELECT id FROM epics WHERE key = ?", [key]);
|
|
2488
2769
|
if (!epic) {
|
|
2489
2770
|
error(`Epic "${key}" not found.`, opts);
|
|
2490
2771
|
process.exit(1);
|
|
2491
2772
|
}
|
|
2773
|
+
const childCount = queryOne(
|
|
2774
|
+
"SELECT COUNT(*) as count FROM user_stories WHERE epic_id = ?",
|
|
2775
|
+
[epic.id]
|
|
2776
|
+
);
|
|
2777
|
+
if (childCount && childCount.count > 0) {
|
|
2778
|
+
if (!options.force && !options.cascade) {
|
|
2779
|
+
error(
|
|
2780
|
+
`Cannot delete epic "${key}": has ${childCount.count} child story(s). Use --force to orphan children, or --cascade to delete them.`,
|
|
2781
|
+
opts
|
|
2782
|
+
);
|
|
2783
|
+
process.exit(1);
|
|
2784
|
+
}
|
|
2785
|
+
if (options.cascade) {
|
|
2786
|
+
const taskCount = queryOne(
|
|
2787
|
+
`SELECT COUNT(*) as count FROM tasks WHERE story_id IN
|
|
2788
|
+
(SELECT id FROM user_stories WHERE epic_id = ?)`,
|
|
2789
|
+
[epic.id]
|
|
2790
|
+
);
|
|
2791
|
+
warning2(
|
|
2792
|
+
`Deleting ${childCount.count} child story(s) and ${taskCount?.count || 0} task(s)`,
|
|
2793
|
+
opts
|
|
2794
|
+
);
|
|
2795
|
+
run(
|
|
2796
|
+
`DELETE FROM tasks WHERE story_id IN (SELECT id FROM user_stories WHERE epic_id = ?)`,
|
|
2797
|
+
[epic.id]
|
|
2798
|
+
);
|
|
2799
|
+
run("DELETE FROM user_stories WHERE epic_id = ?", [epic.id]);
|
|
2800
|
+
} else {
|
|
2801
|
+
warning2(`Orphaning ${childCount.count} child story(s)`, opts);
|
|
2802
|
+
run("UPDATE user_stories SET epic_id = NULL WHERE epic_id = ?", [epic.id]);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2492
2805
|
run("DELETE FROM epics WHERE key = ?", [key]);
|
|
2493
2806
|
success(`Epic "${key}" deleted.`, opts);
|
|
2494
2807
|
});
|
|
@@ -2664,13 +2977,33 @@ storyCommand.command("update").argument("<key>", "Story key").option("-s, --summ
|
|
|
2664
2977
|
run(`UPDATE user_stories SET ${updates.join(", ")} WHERE key = ?`, params);
|
|
2665
2978
|
success(`Story "${key}" updated.`, opts);
|
|
2666
2979
|
});
|
|
2667
|
-
storyCommand.command("delete").alias("rm").argument("<key>", "Story key").option("--force", "Delete
|
|
2980
|
+
storyCommand.command("delete").alias("rm").argument("<key>", "Story key").option("--force", "Delete and orphan child tasks").option("--cascade", "Delete with all child tasks").description("Delete story").action((key, options) => {
|
|
2668
2981
|
const opts = getOutputOptions(storyCommand);
|
|
2669
2982
|
const story = queryOne("SELECT id FROM user_stories WHERE key = ?", [key]);
|
|
2670
2983
|
if (!story) {
|
|
2671
2984
|
error(`Story "${key}" not found.`, opts);
|
|
2672
2985
|
process.exit(1);
|
|
2673
2986
|
}
|
|
2987
|
+
const childCount = queryOne(
|
|
2988
|
+
"SELECT COUNT(*) as count FROM tasks WHERE story_id = ?",
|
|
2989
|
+
[story.id]
|
|
2990
|
+
);
|
|
2991
|
+
if (childCount && childCount.count > 0) {
|
|
2992
|
+
if (!options.force && !options.cascade) {
|
|
2993
|
+
error(
|
|
2994
|
+
`Cannot delete story "${key}": has ${childCount.count} child task(s). Use --force to orphan children, or --cascade to delete them.`,
|
|
2995
|
+
opts
|
|
2996
|
+
);
|
|
2997
|
+
process.exit(1);
|
|
2998
|
+
}
|
|
2999
|
+
if (options.cascade) {
|
|
3000
|
+
warning2(`Deleting ${childCount.count} child task(s)`, opts);
|
|
3001
|
+
run("DELETE FROM tasks WHERE story_id = ?", [story.id]);
|
|
3002
|
+
} else {
|
|
3003
|
+
warning2(`Orphaning ${childCount.count} child task(s)`, opts);
|
|
3004
|
+
run("UPDATE tasks SET story_id = NULL WHERE story_id = ?", [story.id]);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
2674
3007
|
run("DELETE FROM user_stories WHERE key = ?", [key]);
|
|
2675
3008
|
success(`Story "${key}" deleted.`, opts);
|
|
2676
3009
|
});
|
|
@@ -4679,8 +5012,8 @@ syncCommand.command("comments").option("-t, --type <type>", "Filter by type (use
|
|
|
4679
5012
|
params.push(options.type);
|
|
4680
5013
|
}
|
|
4681
5014
|
query += " ORDER BY d.path, dc.line_number";
|
|
4682
|
-
const { queryAll:
|
|
4683
|
-
const comments =
|
|
5015
|
+
const { queryAll: queryAll3 } = (init_connection(), __toCommonJS(connection_exports));
|
|
5016
|
+
const comments = queryAll3(query, params);
|
|
4684
5017
|
data(
|
|
4685
5018
|
comments,
|
|
4686
5019
|
[
|
|
@@ -4700,7 +5033,7 @@ init_config();
|
|
|
4700
5033
|
|
|
4701
5034
|
// src/services/session-service.ts
|
|
4702
5035
|
init_connection();
|
|
4703
|
-
function startSession(projectId) {
|
|
5036
|
+
function startSession(projectId, name) {
|
|
4704
5037
|
const existing = queryOne(
|
|
4705
5038
|
"SELECT id FROM sessions WHERE project_id = ? AND status = ?",
|
|
4706
5039
|
[projectId, "active"]
|
|
@@ -4713,14 +5046,16 @@ function startSession(projectId) {
|
|
|
4713
5046
|
}
|
|
4714
5047
|
const sessionId = generateId();
|
|
4715
5048
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5049
|
+
const sessionName = name || null;
|
|
4716
5050
|
run(
|
|
4717
|
-
`INSERT INTO sessions (id, project_id, started_at, status, entities_modified, files_modified)
|
|
4718
|
-
VALUES (?, ?, ?, 'active', 0, 0)`,
|
|
4719
|
-
[sessionId, projectId, now]
|
|
5051
|
+
`INSERT INTO sessions (id, project_id, name, started_at, status, entities_modified, files_modified)
|
|
5052
|
+
VALUES (?, ?, ?, ?, 'active', 0, 0)`,
|
|
5053
|
+
[sessionId, projectId, sessionName, now]
|
|
4720
5054
|
);
|
|
4721
5055
|
return {
|
|
4722
5056
|
id: sessionId,
|
|
4723
5057
|
projectId,
|
|
5058
|
+
name: sessionName,
|
|
4724
5059
|
startedAt: now,
|
|
4725
5060
|
endedAt: null,
|
|
4726
5061
|
summary: null,
|
|
@@ -4772,6 +5107,86 @@ function getActiveSession(projectId) {
|
|
|
4772
5107
|
status: session.status
|
|
4773
5108
|
};
|
|
4774
5109
|
}
|
|
5110
|
+
function getSessionByName(projectId, name) {
|
|
5111
|
+
const session = queryOne(
|
|
5112
|
+
"SELECT * FROM sessions WHERE project_id = ? AND name = ?",
|
|
5113
|
+
[projectId, name]
|
|
5114
|
+
);
|
|
5115
|
+
if (!session) {
|
|
5116
|
+
return null;
|
|
5117
|
+
}
|
|
5118
|
+
return {
|
|
5119
|
+
id: session.id,
|
|
5120
|
+
projectId: session.project_id,
|
|
5121
|
+
name: session.name,
|
|
5122
|
+
startedAt: session.started_at,
|
|
5123
|
+
endedAt: session.ended_at,
|
|
5124
|
+
summary: session.summary,
|
|
5125
|
+
entitiesModified: session.entities_modified,
|
|
5126
|
+
filesModified: session.files_modified,
|
|
5127
|
+
status: session.status
|
|
5128
|
+
};
|
|
5129
|
+
}
|
|
5130
|
+
function resumeSession(projectId, sessionId) {
|
|
5131
|
+
const session = queryOne(
|
|
5132
|
+
"SELECT * FROM sessions WHERE id = ? AND project_id = ?",
|
|
5133
|
+
[sessionId, projectId]
|
|
5134
|
+
);
|
|
5135
|
+
if (!session) {
|
|
5136
|
+
return null;
|
|
5137
|
+
}
|
|
5138
|
+
if (session.status === "completed") {
|
|
5139
|
+
return null;
|
|
5140
|
+
}
|
|
5141
|
+
const existing = queryOne(
|
|
5142
|
+
"SELECT id FROM sessions WHERE project_id = ? AND status = ? AND id != ?",
|
|
5143
|
+
[projectId, "active", sessionId]
|
|
5144
|
+
);
|
|
5145
|
+
if (existing) {
|
|
5146
|
+
run(
|
|
5147
|
+
`UPDATE sessions SET status = 'abandoned', ended_at = datetime('now') WHERE id = ?`,
|
|
5148
|
+
[existing.id]
|
|
5149
|
+
);
|
|
5150
|
+
}
|
|
5151
|
+
run(
|
|
5152
|
+
`UPDATE sessions SET status = 'active', ended_at = NULL WHERE id = ?`,
|
|
5153
|
+
[sessionId]
|
|
5154
|
+
);
|
|
5155
|
+
return {
|
|
5156
|
+
id: session.id,
|
|
5157
|
+
projectId: session.project_id,
|
|
5158
|
+
name: session.name,
|
|
5159
|
+
startedAt: session.started_at,
|
|
5160
|
+
endedAt: null,
|
|
5161
|
+
summary: session.summary,
|
|
5162
|
+
entitiesModified: session.entities_modified,
|
|
5163
|
+
filesModified: session.files_modified,
|
|
5164
|
+
status: "active"
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5167
|
+
function getSessionResumeInfo(sessionId) {
|
|
5168
|
+
const chunks = getSessionChunks(sessionId);
|
|
5169
|
+
let totalAssigned = 0;
|
|
5170
|
+
for (const chunk of chunks) {
|
|
5171
|
+
if (chunk.assigned_files) {
|
|
5172
|
+
try {
|
|
5173
|
+
totalAssigned += JSON.parse(chunk.assigned_files).length;
|
|
5174
|
+
} catch {
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
const reviewed = queryOne(
|
|
5179
|
+
`SELECT COUNT(*) as count FROM session_files WHERE session_id = ? AND review_type = 'assigned'`,
|
|
5180
|
+
[sessionId]
|
|
5181
|
+
);
|
|
5182
|
+
return {
|
|
5183
|
+
chunks,
|
|
5184
|
+
coverage: {
|
|
5185
|
+
total: totalAssigned,
|
|
5186
|
+
reviewed: reviewed?.count ?? 0
|
|
5187
|
+
}
|
|
5188
|
+
};
|
|
5189
|
+
}
|
|
4775
5190
|
function incrementSessionEntities(projectId, count = 1) {
|
|
4776
5191
|
run(
|
|
4777
5192
|
`UPDATE sessions SET entities_modified = entities_modified + ? WHERE project_id = ? AND status = 'active'`,
|
|
@@ -4956,7 +5371,7 @@ function getActivitySummary(projectId, since) {
|
|
|
4956
5371
|
|
|
4957
5372
|
// src/commands/session.ts
|
|
4958
5373
|
var sessionCommand = new Command11("session").description("Manage AI work sessions");
|
|
4959
|
-
sessionCommand.command("start").description("Start a new AI work session").action(() => {
|
|
5374
|
+
sessionCommand.command("start").argument("[name]", "Optional session name (e.g., init-241215-1030)").description("Start a new AI work session").action((name) => {
|
|
4960
5375
|
const opts = getOutputOptions(sessionCommand);
|
|
4961
5376
|
const projectRoot = findProjectRoot();
|
|
4962
5377
|
if (!projectRoot) {
|
|
@@ -4973,17 +5388,21 @@ sessionCommand.command("start").description("Start a new AI work session").actio
|
|
|
4973
5388
|
error(`Project "${config.project.key}" not found in database.`, opts);
|
|
4974
5389
|
process.exit(1);
|
|
4975
5390
|
}
|
|
4976
|
-
const session = startSession(project.id);
|
|
5391
|
+
const session = startSession(project.id, name);
|
|
4977
5392
|
if (opts.json) {
|
|
4978
5393
|
console.log(JSON.stringify({
|
|
4979
5394
|
success: true,
|
|
4980
5395
|
data: {
|
|
4981
5396
|
sessionId: session.id,
|
|
5397
|
+
name: session.name,
|
|
4982
5398
|
startedAt: session.startedAt
|
|
4983
5399
|
}
|
|
4984
5400
|
}));
|
|
4985
5401
|
} else {
|
|
4986
5402
|
success(`Session started: ${session.id.slice(0, 8)}...`, opts);
|
|
5403
|
+
if (session.name) {
|
|
5404
|
+
info(`Name: ${session.name}`, opts);
|
|
5405
|
+
}
|
|
4987
5406
|
info(`Started at: ${session.startedAt}`, opts);
|
|
4988
5407
|
}
|
|
4989
5408
|
});
|
|
@@ -5258,6 +5677,109 @@ sessionCommand.command("activity").option("-t, --type <type>", "Filter by entity
|
|
|
5258
5677
|
opts
|
|
5259
5678
|
);
|
|
5260
5679
|
});
|
|
5680
|
+
sessionCommand.command("find").argument("<name>", "Session name to search for").description("Find a session by name").action((name) => {
|
|
5681
|
+
const opts = getOutputOptions(sessionCommand);
|
|
5682
|
+
const projectRoot = findProjectRoot();
|
|
5683
|
+
if (!projectRoot) {
|
|
5684
|
+
error('Not in an AIGILE project. Run "aigile init" first.', opts);
|
|
5685
|
+
process.exit(1);
|
|
5686
|
+
}
|
|
5687
|
+
const config = loadProjectConfig(projectRoot);
|
|
5688
|
+
if (!config) {
|
|
5689
|
+
error("Could not load project config.", opts);
|
|
5690
|
+
process.exit(1);
|
|
5691
|
+
}
|
|
5692
|
+
const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
|
|
5693
|
+
if (!project) {
|
|
5694
|
+
error(`Project "${config.project.key}" not found in database.`, opts);
|
|
5695
|
+
process.exit(1);
|
|
5696
|
+
}
|
|
5697
|
+
const session = getSessionByName(project.id, name);
|
|
5698
|
+
if (!session) {
|
|
5699
|
+
error(`Session "${name}" not found.`, opts);
|
|
5700
|
+
process.exit(1);
|
|
5701
|
+
}
|
|
5702
|
+
if (opts.json) {
|
|
5703
|
+
console.log(JSON.stringify({
|
|
5704
|
+
success: true,
|
|
5705
|
+
data: {
|
|
5706
|
+
id: session.id,
|
|
5707
|
+
name: session.name,
|
|
5708
|
+
status: session.status,
|
|
5709
|
+
startedAt: session.startedAt,
|
|
5710
|
+
endedAt: session.endedAt
|
|
5711
|
+
}
|
|
5712
|
+
}));
|
|
5713
|
+
} else {
|
|
5714
|
+
success(`Found session: ${session.id.slice(0, 8)}...`, opts);
|
|
5715
|
+
console.log(` Name: ${session.name}`);
|
|
5716
|
+
console.log(` Status: ${session.status}`);
|
|
5717
|
+
console.log(` Started: ${session.startedAt}`);
|
|
5718
|
+
if (session.endedAt) {
|
|
5719
|
+
console.log(` Ended: ${session.endedAt}`);
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
});
|
|
5723
|
+
sessionCommand.command("resume").argument("<name-or-id>", "Session name or ID to resume").description("Resume an incomplete session").action((nameOrId) => {
|
|
5724
|
+
const opts = getOutputOptions(sessionCommand);
|
|
5725
|
+
const projectRoot = findProjectRoot();
|
|
5726
|
+
if (!projectRoot) {
|
|
5727
|
+
error('Not in an AIGILE project. Run "aigile init" first.', opts);
|
|
5728
|
+
process.exit(1);
|
|
5729
|
+
}
|
|
5730
|
+
const config = loadProjectConfig(projectRoot);
|
|
5731
|
+
if (!config) {
|
|
5732
|
+
error("Could not load project config.", opts);
|
|
5733
|
+
process.exit(1);
|
|
5734
|
+
}
|
|
5735
|
+
const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
|
|
5736
|
+
if (!project) {
|
|
5737
|
+
error(`Project "${config.project.key}" not found in database.`, opts);
|
|
5738
|
+
process.exit(1);
|
|
5739
|
+
}
|
|
5740
|
+
let session = getSessionByName(project.id, nameOrId);
|
|
5741
|
+
if (!session) {
|
|
5742
|
+
session = getSession(nameOrId);
|
|
5743
|
+
}
|
|
5744
|
+
if (!session) {
|
|
5745
|
+
error(`Session "${nameOrId}" not found.`, opts);
|
|
5746
|
+
process.exit(1);
|
|
5747
|
+
}
|
|
5748
|
+
if (session.status === "completed") {
|
|
5749
|
+
error(`Session "${nameOrId}" is already completed. Cannot resume.`, opts);
|
|
5750
|
+
process.exit(1);
|
|
5751
|
+
}
|
|
5752
|
+
const resumed = resumeSession(project.id, session.id);
|
|
5753
|
+
if (!resumed) {
|
|
5754
|
+
error(`Failed to resume session "${nameOrId}".`, opts);
|
|
5755
|
+
process.exit(1);
|
|
5756
|
+
}
|
|
5757
|
+
const resumeInfo = getSessionResumeInfo(session.id);
|
|
5758
|
+
if (opts.json) {
|
|
5759
|
+
console.log(JSON.stringify({
|
|
5760
|
+
success: true,
|
|
5761
|
+
data: {
|
|
5762
|
+
id: resumed.id,
|
|
5763
|
+
name: resumed.name,
|
|
5764
|
+
status: resumed.status,
|
|
5765
|
+
chunks: resumeInfo.chunks.length,
|
|
5766
|
+
coverage: resumeInfo.coverage
|
|
5767
|
+
}
|
|
5768
|
+
}));
|
|
5769
|
+
} else {
|
|
5770
|
+
success(`Session resumed: ${resumed.id.slice(0, 8)}...`, opts);
|
|
5771
|
+
if (resumed.name) {
|
|
5772
|
+
console.log(` Name: ${resumed.name}`);
|
|
5773
|
+
}
|
|
5774
|
+
console.log(` Status: ${resumed.status}`);
|
|
5775
|
+
console.log(` Chunks: ${resumeInfo.chunks.length}`);
|
|
5776
|
+
console.log(` Coverage: ${resumeInfo.coverage.reviewed}/${resumeInfo.coverage.total} assigned files reviewed`);
|
|
5777
|
+
if (resumeInfo.coverage.total > 0) {
|
|
5778
|
+
const pct = Math.round(resumeInfo.coverage.reviewed / resumeInfo.coverage.total * 100);
|
|
5779
|
+
console.log(` Progress: ${pct}%`);
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
});
|
|
5261
5783
|
function calculateDuration2(start, end) {
|
|
5262
5784
|
const startDate = new Date(start);
|
|
5263
5785
|
const endDate = end ? new Date(end) : /* @__PURE__ */ new Date();
|
|
@@ -8964,7 +9486,7 @@ daemonCommand.command("install").description("Install daemon to start automatica
|
|
|
8964
9486
|
info("Daemon will watch ALL registered projects", opts);
|
|
8965
9487
|
info('Run "aigile daemon start" to start the watcher', opts);
|
|
8966
9488
|
} catch (err) {
|
|
8967
|
-
|
|
9489
|
+
warning2("Service file created but could not enable. You may need to run:", opts);
|
|
8968
9490
|
console.log(` systemctl --user daemon-reload`);
|
|
8969
9491
|
console.log(` systemctl --user enable ${DAEMON_NAME}`);
|
|
8970
9492
|
}
|
|
@@ -9456,9 +9978,11 @@ function formatBytes(bytes) {
|
|
|
9456
9978
|
|
|
9457
9979
|
// src/commands/file.ts
|
|
9458
9980
|
init_connection();
|
|
9981
|
+
init_connection();
|
|
9459
9982
|
import { Command as Command21 } from "commander";
|
|
9460
9983
|
import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
|
|
9461
9984
|
import { join as join12, relative as relative4 } from "path";
|
|
9985
|
+
import { glob } from "glob";
|
|
9462
9986
|
init_config();
|
|
9463
9987
|
var fileCommand = new Command21("file").description("Shadow mode file analysis and management for brownfield projects");
|
|
9464
9988
|
function getProjectContext(opts) {
|
|
@@ -9848,10 +10372,721 @@ fileCommand.command("show <path>").description("Show detailed file analysis").ac
|
|
|
9848
10372
|
);
|
|
9849
10373
|
}
|
|
9850
10374
|
});
|
|
10375
|
+
fileCommand.command("tag").argument("<path>", "File path to tag").option("--chunk <id>", "Chunk ID this file belongs to").option("--report <path>", "Report this file contributes to").option("--type <type>", "Review type: assigned|explored|skipped", "assigned").option("--foundational", "Mark as foundational file").option("--agent <id>", "Agent ID that reviewed this file").description("Tag a file as reviewed in the current session").action((filePath, options) => {
|
|
10376
|
+
const opts = getOutputOptions(fileCommand);
|
|
10377
|
+
const ctx = getProjectContext(opts);
|
|
10378
|
+
if (!ctx) {
|
|
10379
|
+
process.exit(1);
|
|
10380
|
+
}
|
|
10381
|
+
const session = getActiveSession(ctx.projectId);
|
|
10382
|
+
if (!session) {
|
|
10383
|
+
error('No active session. Start one with "aigile session start".', opts);
|
|
10384
|
+
process.exit(1);
|
|
10385
|
+
}
|
|
10386
|
+
const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
|
|
10387
|
+
const doc = queryOne(
|
|
10388
|
+
"SELECT id FROM documents WHERE project_id = ? AND path = ?",
|
|
10389
|
+
[ctx.projectId, normalizedPath]
|
|
10390
|
+
);
|
|
10391
|
+
if (!doc) {
|
|
10392
|
+
const tracked = trackShadowFile(ctx.projectId, ctx.projectRoot, normalizedPath);
|
|
10393
|
+
if (!tracked) {
|
|
10394
|
+
error(`File not tracked: ${normalizedPath}. Run "aigile sync scan" first.`, opts);
|
|
10395
|
+
process.exit(1);
|
|
10396
|
+
}
|
|
10397
|
+
const newDoc = queryOne(
|
|
10398
|
+
"SELECT id FROM documents WHERE project_id = ? AND path = ?",
|
|
10399
|
+
[ctx.projectId, normalizedPath]
|
|
10400
|
+
);
|
|
10401
|
+
if (!newDoc) {
|
|
10402
|
+
error(`Could not track file: ${normalizedPath}`, opts);
|
|
10403
|
+
process.exit(1);
|
|
10404
|
+
}
|
|
10405
|
+
doc.id = newDoc.id;
|
|
10406
|
+
}
|
|
10407
|
+
const validTypes = ["assigned", "explored", "skipped"];
|
|
10408
|
+
if (!validTypes.includes(options.type)) {
|
|
10409
|
+
error(`Invalid review type "${options.type}". Must be: ${validTypes.join(", ")}`, opts);
|
|
10410
|
+
process.exit(1);
|
|
10411
|
+
}
|
|
10412
|
+
const sessionFileId = tagFileReviewed(session.id, doc.id, {
|
|
10413
|
+
chunkId: options.chunk,
|
|
10414
|
+
agentId: options.agent,
|
|
10415
|
+
reportPath: options.report,
|
|
10416
|
+
reviewType: options.type,
|
|
10417
|
+
isFoundational: options.foundational ?? false
|
|
10418
|
+
});
|
|
10419
|
+
if (opts.json) {
|
|
10420
|
+
console.log(JSON.stringify({
|
|
10421
|
+
success: true,
|
|
10422
|
+
data: {
|
|
10423
|
+
session_file_id: sessionFileId,
|
|
10424
|
+
path: normalizedPath,
|
|
10425
|
+
session_id: session.id,
|
|
10426
|
+
chunk_id: options.chunk ?? null,
|
|
10427
|
+
review_type: options.type,
|
|
10428
|
+
is_foundational: options.foundational ?? false
|
|
10429
|
+
}
|
|
10430
|
+
}));
|
|
10431
|
+
} else {
|
|
10432
|
+
success(`Tagged: ${normalizedPath}`, opts);
|
|
10433
|
+
if (options.chunk) {
|
|
10434
|
+
info(` Chunk: ${options.chunk}`, opts);
|
|
10435
|
+
}
|
|
10436
|
+
info(` Type: ${options.type}`, opts);
|
|
10437
|
+
}
|
|
10438
|
+
});
|
|
10439
|
+
fileCommand.command("tag-batch").option("--chunk <id>", "Chunk ID for all files").option("--glob <pattern>", "Glob pattern for files").option("--type <type>", "Review type: assigned|explored|skipped", "assigned").option("--foundational", "Mark all as foundational").option("--agent <id>", "Agent ID").description("Tag multiple files as reviewed (from glob pattern or stdin)").action(async (options) => {
|
|
10440
|
+
const opts = getOutputOptions(fileCommand);
|
|
10441
|
+
const ctx = getProjectContext(opts);
|
|
10442
|
+
if (!ctx) {
|
|
10443
|
+
process.exit(1);
|
|
10444
|
+
}
|
|
10445
|
+
const session = getActiveSession(ctx.projectId);
|
|
10446
|
+
if (!session) {
|
|
10447
|
+
error('No active session. Start one with "aigile session start".', opts);
|
|
10448
|
+
process.exit(1);
|
|
10449
|
+
}
|
|
10450
|
+
let filesToTag = [];
|
|
10451
|
+
if (options.glob) {
|
|
10452
|
+
const matches = await glob(options.glob, { cwd: ctx.projectRoot, nodir: true });
|
|
10453
|
+
filesToTag = matches.map((f) => relative4(ctx.projectRoot, join12(ctx.projectRoot, f)));
|
|
10454
|
+
} else {
|
|
10455
|
+
error("Please provide --glob pattern. Stdin not supported yet.", opts);
|
|
10456
|
+
process.exit(1);
|
|
10457
|
+
}
|
|
10458
|
+
if (filesToTag.length === 0) {
|
|
10459
|
+
error("No files matched the pattern.", opts);
|
|
10460
|
+
process.exit(1);
|
|
10461
|
+
}
|
|
10462
|
+
let tagged = 0;
|
|
10463
|
+
let skipped = 0;
|
|
10464
|
+
for (const filePath of filesToTag) {
|
|
10465
|
+
const doc = queryOne(
|
|
10466
|
+
"SELECT id FROM documents WHERE project_id = ? AND path = ?",
|
|
10467
|
+
[ctx.projectId, filePath]
|
|
10468
|
+
);
|
|
10469
|
+
if (!doc) {
|
|
10470
|
+
skipped++;
|
|
10471
|
+
continue;
|
|
10472
|
+
}
|
|
10473
|
+
tagFileReviewed(session.id, doc.id, {
|
|
10474
|
+
chunkId: options.chunk,
|
|
10475
|
+
agentId: options.agent,
|
|
10476
|
+
reviewType: options.type,
|
|
10477
|
+
isFoundational: options.foundational ?? false
|
|
10478
|
+
});
|
|
10479
|
+
tagged++;
|
|
10480
|
+
}
|
|
10481
|
+
if (opts.json) {
|
|
10482
|
+
console.log(JSON.stringify({
|
|
10483
|
+
success: true,
|
|
10484
|
+
data: { tagged, skipped, total: filesToTag.length }
|
|
10485
|
+
}));
|
|
10486
|
+
} else {
|
|
10487
|
+
success(`Tagged ${tagged} files (${skipped} skipped - not tracked)`, opts);
|
|
10488
|
+
}
|
|
10489
|
+
});
|
|
10490
|
+
fileCommand.command("untag").argument("<path>", "File path to untag").option("--session <id>", "Session ID (default: current)").description("Remove review tag from a file").action((filePath, options) => {
|
|
10491
|
+
const opts = getOutputOptions(fileCommand);
|
|
10492
|
+
const ctx = getProjectContext(opts);
|
|
10493
|
+
if (!ctx) {
|
|
10494
|
+
process.exit(1);
|
|
10495
|
+
}
|
|
10496
|
+
let sessionId = options.session;
|
|
10497
|
+
if (!sessionId) {
|
|
10498
|
+
const session = getActiveSession(ctx.projectId);
|
|
10499
|
+
if (!session) {
|
|
10500
|
+
error("No active session. Specify --session or start one.", opts);
|
|
10501
|
+
process.exit(1);
|
|
10502
|
+
}
|
|
10503
|
+
sessionId = session.id;
|
|
10504
|
+
}
|
|
10505
|
+
const doc = queryOne(
|
|
10506
|
+
"SELECT id FROM documents WHERE project_id = ? AND path = ?",
|
|
10507
|
+
[ctx.projectId, filePath]
|
|
10508
|
+
);
|
|
10509
|
+
if (!doc) {
|
|
10510
|
+
error(`File "${filePath}" not found in project.`, opts);
|
|
10511
|
+
process.exit(1);
|
|
10512
|
+
}
|
|
10513
|
+
const existing = queryOne(
|
|
10514
|
+
"SELECT id FROM session_files WHERE session_id = ? AND document_id = ?",
|
|
10515
|
+
[sessionId, doc.id]
|
|
10516
|
+
);
|
|
10517
|
+
if (!existing) {
|
|
10518
|
+
warning(`File "${filePath}" is not tagged in this session.`, opts);
|
|
10519
|
+
return;
|
|
10520
|
+
}
|
|
10521
|
+
queryOne("DELETE FROM session_files WHERE id = ?", [existing.id]);
|
|
10522
|
+
if (opts.json) {
|
|
10523
|
+
console.log(JSON.stringify({
|
|
10524
|
+
success: true,
|
|
10525
|
+
data: { path: filePath, untagged: true }
|
|
10526
|
+
}));
|
|
10527
|
+
} else {
|
|
10528
|
+
success(`Untagged: ${filePath}`, opts);
|
|
10529
|
+
}
|
|
10530
|
+
});
|
|
10531
|
+
fileCommand.command("clear-tags").option("--session <id>", "Session ID (default: current)").option("--chunk <id>", "Only clear tags for specific chunk").option("--confirm", "Skip confirmation prompt").description("Remove all file tags from a session (for re-review)").action((options) => {
|
|
10532
|
+
const opts = getOutputOptions(fileCommand);
|
|
10533
|
+
const ctx = getProjectContext(opts);
|
|
10534
|
+
if (!ctx) {
|
|
10535
|
+
process.exit(1);
|
|
10536
|
+
}
|
|
10537
|
+
let sessionId = options.session;
|
|
10538
|
+
if (!sessionId) {
|
|
10539
|
+
const session = getActiveSession(ctx.projectId);
|
|
10540
|
+
if (!session) {
|
|
10541
|
+
error("No active session. Specify --session or start one.", opts);
|
|
10542
|
+
process.exit(1);
|
|
10543
|
+
}
|
|
10544
|
+
sessionId = session.id;
|
|
10545
|
+
}
|
|
10546
|
+
let countQuery = "SELECT COUNT(*) as count FROM session_files WHERE session_id = ?";
|
|
10547
|
+
const params = [sessionId];
|
|
10548
|
+
if (options.chunk) {
|
|
10549
|
+
countQuery += " AND chunk_id = ?";
|
|
10550
|
+
params.push(options.chunk);
|
|
10551
|
+
}
|
|
10552
|
+
const result = queryOne(countQuery, params);
|
|
10553
|
+
const count = result?.count ?? 0;
|
|
10554
|
+
if (count === 0) {
|
|
10555
|
+
warning("No tags to clear.", opts);
|
|
10556
|
+
return;
|
|
10557
|
+
}
|
|
10558
|
+
if (!options.confirm && !opts.json) {
|
|
10559
|
+
console.log(`This will remove ${count} tag(s).`);
|
|
10560
|
+
console.log("Use --confirm to proceed.");
|
|
10561
|
+
return;
|
|
10562
|
+
}
|
|
10563
|
+
let deleteQuery = "DELETE FROM session_files WHERE session_id = ?";
|
|
10564
|
+
const deleteParams = [sessionId];
|
|
10565
|
+
if (options.chunk) {
|
|
10566
|
+
deleteQuery += " AND chunk_id = ?";
|
|
10567
|
+
deleteParams.push(options.chunk);
|
|
10568
|
+
}
|
|
10569
|
+
queryOne(deleteQuery, deleteParams);
|
|
10570
|
+
if (opts.json) {
|
|
10571
|
+
console.log(JSON.stringify({
|
|
10572
|
+
success: true,
|
|
10573
|
+
data: { session_id: sessionId, cleared: count }
|
|
10574
|
+
}));
|
|
10575
|
+
} else {
|
|
10576
|
+
success(`Cleared ${count} tag(s)`, opts);
|
|
10577
|
+
}
|
|
10578
|
+
});
|
|
10579
|
+
fileCommand.command("untagged").option("--session <id>", "Session ID (default: current)").option("--chunk <id>", "Filter by chunk").option("--assigned-only", "Only show untagged assigned files").description("List files not yet tagged/reviewed in session").action((options) => {
|
|
10580
|
+
const opts = getOutputOptions(fileCommand);
|
|
10581
|
+
const ctx = getProjectContext(opts);
|
|
10582
|
+
if (!ctx) {
|
|
10583
|
+
process.exit(1);
|
|
10584
|
+
}
|
|
10585
|
+
let sessionId = options.session;
|
|
10586
|
+
if (!sessionId) {
|
|
10587
|
+
const session = getActiveSession(ctx.projectId);
|
|
10588
|
+
if (!session) {
|
|
10589
|
+
error("No active session. Specify --session or start one.", opts);
|
|
10590
|
+
process.exit(1);
|
|
10591
|
+
}
|
|
10592
|
+
sessionId = session.id;
|
|
10593
|
+
}
|
|
10594
|
+
const untagged = getUntaggedFiles(ctx.projectId, sessionId, {
|
|
10595
|
+
chunkId: options.chunk,
|
|
10596
|
+
assignedOnly: options.assignedOnly
|
|
10597
|
+
});
|
|
10598
|
+
if (opts.json) {
|
|
10599
|
+
console.log(JSON.stringify({
|
|
10600
|
+
success: true,
|
|
10601
|
+
data: {
|
|
10602
|
+
session_id: sessionId,
|
|
10603
|
+
chunk_id: options.chunk ?? null,
|
|
10604
|
+
count: untagged.length,
|
|
10605
|
+
files: untagged.map((f) => f.path)
|
|
10606
|
+
}
|
|
10607
|
+
}));
|
|
10608
|
+
} else {
|
|
10609
|
+
if (untagged.length === 0) {
|
|
10610
|
+
success("All files have been tagged!", opts);
|
|
10611
|
+
return;
|
|
10612
|
+
}
|
|
10613
|
+
console.log(`Untagged files (${untagged.length}):`);
|
|
10614
|
+
for (const file of untagged) {
|
|
10615
|
+
console.log(` ${file.path}`);
|
|
10616
|
+
}
|
|
10617
|
+
}
|
|
10618
|
+
});
|
|
10619
|
+
fileCommand.command("coverage").option("--session <id>", "Session ID (default: current)").option("--by-chunk", "Group statistics by chunk").description("Show file review coverage statistics").action((options) => {
|
|
10620
|
+
const opts = getOutputOptions(fileCommand);
|
|
10621
|
+
const ctx = getProjectContext(opts);
|
|
10622
|
+
if (!ctx) {
|
|
10623
|
+
process.exit(1);
|
|
10624
|
+
}
|
|
10625
|
+
let sessionId = options.session;
|
|
10626
|
+
if (!sessionId) {
|
|
10627
|
+
const session = getActiveSession(ctx.projectId);
|
|
10628
|
+
if (!session) {
|
|
10629
|
+
error("No active session. Specify --session or start one.", opts);
|
|
10630
|
+
process.exit(1);
|
|
10631
|
+
}
|
|
10632
|
+
sessionId = session.id;
|
|
10633
|
+
}
|
|
10634
|
+
if (options.byChunk) {
|
|
10635
|
+
const chunks = getSessionChunks(sessionId);
|
|
10636
|
+
if (chunks.length === 0) {
|
|
10637
|
+
info("No chunks defined in this session.", opts);
|
|
10638
|
+
return;
|
|
10639
|
+
}
|
|
10640
|
+
const chunkStats = chunks.map((chunk) => {
|
|
10641
|
+
const stats = getCoverageStats(sessionId, chunk.id);
|
|
10642
|
+
const pct = stats.assigned.total > 0 ? Math.round(stats.assigned.reviewed / stats.assigned.total * 100) : 100;
|
|
10643
|
+
return {
|
|
10644
|
+
id: chunk.id,
|
|
10645
|
+
name: chunk.name,
|
|
10646
|
+
assigned: `${stats.assigned.reviewed}/${stats.assigned.total} (${pct}%)`,
|
|
10647
|
+
explored: stats.explored,
|
|
10648
|
+
foundational: stats.foundational,
|
|
10649
|
+
skipped: stats.skipped
|
|
10650
|
+
};
|
|
10651
|
+
});
|
|
10652
|
+
if (opts.json) {
|
|
10653
|
+
console.log(JSON.stringify({ success: true, data: chunkStats }));
|
|
10654
|
+
} else {
|
|
10655
|
+
data(
|
|
10656
|
+
chunkStats,
|
|
10657
|
+
[
|
|
10658
|
+
{ header: "ID", key: "id", width: 15 },
|
|
10659
|
+
{ header: "Name", key: "name", width: 20 },
|
|
10660
|
+
{ header: "Assigned", key: "assigned", width: 15 },
|
|
10661
|
+
{ header: "Explored", key: "explored", width: 10 },
|
|
10662
|
+
{ header: "Found.", key: "foundational", width: 8 },
|
|
10663
|
+
{ header: "Skipped", key: "skipped", width: 8 }
|
|
10664
|
+
],
|
|
10665
|
+
opts
|
|
10666
|
+
);
|
|
10667
|
+
}
|
|
10668
|
+
} else {
|
|
10669
|
+
const stats = getCoverageStats(sessionId);
|
|
10670
|
+
const totalTagged = getSessionFiles(sessionId).length;
|
|
10671
|
+
const untagged = getUntaggedFiles(ctx.projectId, sessionId);
|
|
10672
|
+
const total = totalTagged + untagged.length;
|
|
10673
|
+
const pct = total > 0 ? Math.round(totalTagged / total * 100) : 100;
|
|
10674
|
+
if (opts.json) {
|
|
10675
|
+
console.log(JSON.stringify({
|
|
10676
|
+
success: true,
|
|
10677
|
+
data: {
|
|
10678
|
+
session_id: sessionId,
|
|
10679
|
+
total_files: total,
|
|
10680
|
+
tagged: totalTagged,
|
|
10681
|
+
untagged: untagged.length,
|
|
10682
|
+
coverage_percent: pct,
|
|
10683
|
+
by_type: {
|
|
10684
|
+
assigned: stats.assigned.reviewed,
|
|
10685
|
+
explored: stats.explored,
|
|
10686
|
+
foundational: stats.foundational,
|
|
10687
|
+
skipped: stats.skipped
|
|
10688
|
+
}
|
|
10689
|
+
}
|
|
10690
|
+
}));
|
|
10691
|
+
} else {
|
|
10692
|
+
console.log(`
|
|
10693
|
+
Coverage for session ${sessionId.slice(0, 8)}...`);
|
|
10694
|
+
console.log(` Total files: ${total}`);
|
|
10695
|
+
console.log(` Tagged: ${totalTagged} (${pct}%)`);
|
|
10696
|
+
console.log(` Untagged: ${untagged.length}`);
|
|
10697
|
+
console.log(`
|
|
10698
|
+
By type:`);
|
|
10699
|
+
console.log(` Assigned: ${stats.assigned.reviewed}`);
|
|
10700
|
+
console.log(` Explored: ${stats.explored}`);
|
|
10701
|
+
console.log(` Foundational: ${stats.foundational}`);
|
|
10702
|
+
console.log(` Skipped: ${stats.skipped}`);
|
|
10703
|
+
}
|
|
10704
|
+
}
|
|
10705
|
+
});
|
|
10706
|
+
fileCommand.command("flag").argument("<path>", "File path to flag").option("--duplicate <path>", "Similar/duplicate file").option("--unclear <lines>", 'Unclear code at lines (e.g., "45-60")').option("--note <text>", "Description of the issue").description("Flag a file with quality issues").action((filePath, options) => {
|
|
10707
|
+
const opts = getOutputOptions(fileCommand);
|
|
10708
|
+
const ctx = getProjectContext(opts);
|
|
10709
|
+
if (!ctx) {
|
|
10710
|
+
process.exit(1);
|
|
10711
|
+
}
|
|
10712
|
+
const session = getActiveSession(ctx.projectId);
|
|
10713
|
+
if (!session) {
|
|
10714
|
+
error('No active session. Start one with "aigile session start".', opts);
|
|
10715
|
+
process.exit(1);
|
|
10716
|
+
}
|
|
10717
|
+
const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
|
|
10718
|
+
const sessionFile = queryOne(
|
|
10719
|
+
`SELECT sf.id FROM session_files sf
|
|
10720
|
+
JOIN documents d ON sf.document_id = d.id
|
|
10721
|
+
WHERE sf.session_id = ? AND d.path = ?`,
|
|
10722
|
+
[session.id, normalizedPath]
|
|
10723
|
+
);
|
|
10724
|
+
if (!sessionFile) {
|
|
10725
|
+
error(`File not tagged in this session: ${normalizedPath}. Tag it first with "aigile file tag".`, opts);
|
|
10726
|
+
process.exit(1);
|
|
10727
|
+
}
|
|
10728
|
+
const issues = [];
|
|
10729
|
+
if (options.duplicate) {
|
|
10730
|
+
issues.push(`duplicate:${options.duplicate}`);
|
|
10731
|
+
}
|
|
10732
|
+
if (options.unclear) {
|
|
10733
|
+
issues.push(`unclear:${options.unclear}`);
|
|
10734
|
+
}
|
|
10735
|
+
if (options.note) {
|
|
10736
|
+
issues.push(`note:${options.note}`);
|
|
10737
|
+
}
|
|
10738
|
+
if (issues.length === 0) {
|
|
10739
|
+
error("No issues specified. Use --duplicate, --unclear, or --note.", opts);
|
|
10740
|
+
process.exit(1);
|
|
10741
|
+
}
|
|
10742
|
+
flagFileQualityIssue(sessionFile.id, issues);
|
|
10743
|
+
if (opts.json) {
|
|
10744
|
+
console.log(JSON.stringify({
|
|
10745
|
+
success: true,
|
|
10746
|
+
data: {
|
|
10747
|
+
path: normalizedPath,
|
|
10748
|
+
issues
|
|
10749
|
+
}
|
|
10750
|
+
}));
|
|
10751
|
+
} else {
|
|
10752
|
+
success(`Flagged: ${normalizedPath}`, opts);
|
|
10753
|
+
for (const issue of issues) {
|
|
10754
|
+
info(` ${issue}`, opts);
|
|
10755
|
+
}
|
|
10756
|
+
}
|
|
10757
|
+
});
|
|
10758
|
+
fileCommand.command("duplicates").option("--session <id>", "Session ID (default: current)").description("List files flagged as duplicates").action((options) => {
|
|
10759
|
+
const opts = getOutputOptions(fileCommand);
|
|
10760
|
+
const ctx = getProjectContext(opts);
|
|
10761
|
+
if (!ctx) {
|
|
10762
|
+
process.exit(1);
|
|
10763
|
+
}
|
|
10764
|
+
let sessionId = options.session;
|
|
10765
|
+
if (!sessionId) {
|
|
10766
|
+
const session = getActiveSession(ctx.projectId);
|
|
10767
|
+
if (!session) {
|
|
10768
|
+
error("No active session. Specify --session or start one.", opts);
|
|
10769
|
+
process.exit(1);
|
|
10770
|
+
}
|
|
10771
|
+
sessionId = session.id;
|
|
10772
|
+
}
|
|
10773
|
+
const filesWithIssues = getFilesWithQualityIssues(sessionId);
|
|
10774
|
+
const duplicates = [];
|
|
10775
|
+
for (const sf of filesWithIssues) {
|
|
10776
|
+
if (!sf.quality_issues) continue;
|
|
10777
|
+
const issues = JSON.parse(sf.quality_issues);
|
|
10778
|
+
const doc = queryOne(
|
|
10779
|
+
"SELECT path FROM documents WHERE id = ?",
|
|
10780
|
+
[sf.document_id]
|
|
10781
|
+
);
|
|
10782
|
+
if (!doc) continue;
|
|
10783
|
+
for (const issue of issues) {
|
|
10784
|
+
if (issue.startsWith("duplicate:")) {
|
|
10785
|
+
const duplicateOf = issue.replace("duplicate:", "");
|
|
10786
|
+
const noteIssue = issues.find((i) => i.startsWith("note:"));
|
|
10787
|
+
duplicates.push({
|
|
10788
|
+
path: doc.path,
|
|
10789
|
+
duplicate_of: duplicateOf,
|
|
10790
|
+
note: noteIssue ? noteIssue.replace("note:", "") : void 0
|
|
10791
|
+
});
|
|
10792
|
+
}
|
|
10793
|
+
}
|
|
10794
|
+
}
|
|
10795
|
+
if (opts.json) {
|
|
10796
|
+
console.log(JSON.stringify({
|
|
10797
|
+
success: true,
|
|
10798
|
+
data: { duplicates }
|
|
10799
|
+
}));
|
|
10800
|
+
} else {
|
|
10801
|
+
if (duplicates.length === 0) {
|
|
10802
|
+
info("No duplicates flagged.", opts);
|
|
10803
|
+
return;
|
|
10804
|
+
}
|
|
10805
|
+
console.log(`Flagged duplicates (${duplicates.length}):`);
|
|
10806
|
+
for (const dup of duplicates) {
|
|
10807
|
+
console.log(` ${dup.path} <-> ${dup.duplicate_of}`);
|
|
10808
|
+
if (dup.note) {
|
|
10809
|
+
console.log(` Note: ${dup.note}`);
|
|
10810
|
+
}
|
|
10811
|
+
}
|
|
10812
|
+
}
|
|
10813
|
+
});
|
|
10814
|
+
fileCommand.command("issues").option("--session <id>", "Session ID (default: current)").description("List all files with quality issues").action((options) => {
|
|
10815
|
+
const opts = getOutputOptions(fileCommand);
|
|
10816
|
+
const ctx = getProjectContext(opts);
|
|
10817
|
+
if (!ctx) {
|
|
10818
|
+
process.exit(1);
|
|
10819
|
+
}
|
|
10820
|
+
let sessionId = options.session;
|
|
10821
|
+
if (!sessionId) {
|
|
10822
|
+
const session = getActiveSession(ctx.projectId);
|
|
10823
|
+
if (!session) {
|
|
10824
|
+
error("No active session. Specify --session or start one.", opts);
|
|
10825
|
+
process.exit(1);
|
|
10826
|
+
}
|
|
10827
|
+
sessionId = session.id;
|
|
10828
|
+
}
|
|
10829
|
+
const filesWithIssues = getFilesWithQualityIssues(sessionId);
|
|
10830
|
+
if (filesWithIssues.length === 0) {
|
|
10831
|
+
info("No quality issues flagged.", opts);
|
|
10832
|
+
return;
|
|
10833
|
+
}
|
|
10834
|
+
const issueList = [];
|
|
10835
|
+
for (const sf of filesWithIssues) {
|
|
10836
|
+
const doc = queryOne(
|
|
10837
|
+
"SELECT path FROM documents WHERE id = ?",
|
|
10838
|
+
[sf.document_id]
|
|
10839
|
+
);
|
|
10840
|
+
if (!doc) continue;
|
|
10841
|
+
issueList.push({
|
|
10842
|
+
path: doc.path,
|
|
10843
|
+
issues: sf.quality_issues ? JSON.parse(sf.quality_issues) : []
|
|
10844
|
+
});
|
|
10845
|
+
}
|
|
10846
|
+
if (opts.json) {
|
|
10847
|
+
console.log(JSON.stringify({
|
|
10848
|
+
success: true,
|
|
10849
|
+
data: { files_with_issues: issueList }
|
|
10850
|
+
}));
|
|
10851
|
+
} else {
|
|
10852
|
+
console.log(`Files with quality issues (${issueList.length}):`);
|
|
10853
|
+
for (const file of issueList) {
|
|
10854
|
+
console.log(`
|
|
10855
|
+
${file.path}:`);
|
|
10856
|
+
for (const issue of file.issues) {
|
|
10857
|
+
console.log(` - ${issue}`);
|
|
10858
|
+
}
|
|
10859
|
+
}
|
|
10860
|
+
}
|
|
10861
|
+
});
|
|
10862
|
+
|
|
10863
|
+
// src/commands/chunk.ts
|
|
10864
|
+
init_connection();
|
|
10865
|
+
init_connection();
|
|
10866
|
+
import { Command as Command22 } from "commander";
|
|
10867
|
+
import { glob as glob2 } from "glob";
|
|
10868
|
+
import { relative as relative5, resolve as resolve2 } from "path";
|
|
10869
|
+
init_config();
|
|
10870
|
+
function safeParseArray(json) {
|
|
10871
|
+
if (!json) return [];
|
|
10872
|
+
try {
|
|
10873
|
+
return JSON.parse(json);
|
|
10874
|
+
} catch {
|
|
10875
|
+
return [];
|
|
10876
|
+
}
|
|
10877
|
+
}
|
|
10878
|
+
var chunkCommand = new Command22("chunk").description("Manage file review chunks for verified coverage");
|
|
10879
|
+
chunkCommand.command("create").argument("<id>", "Chunk ID (e.g., chunk-001)").option("-n, --name <name>", "Human-readable name").option("-p, --pattern <patterns...>", "Glob patterns for files").option("-a, --assign <files...>", "Explicit file assignments").option("-m, --mode <mode>", "Review mode: quick|standard|audit", "standard").description("Create a new chunk with file assignments").action(async (id, options) => {
|
|
10880
|
+
const opts = getOutputOptions(chunkCommand);
|
|
10881
|
+
const projectRoot = findProjectRoot();
|
|
10882
|
+
if (!projectRoot) {
|
|
10883
|
+
error('Not in an AIGILE project. Run "aigile init" first.', opts);
|
|
10884
|
+
process.exit(1);
|
|
10885
|
+
}
|
|
10886
|
+
const config = loadProjectConfig(projectRoot);
|
|
10887
|
+
if (!config) {
|
|
10888
|
+
error("Could not load project config.", opts);
|
|
10889
|
+
process.exit(1);
|
|
10890
|
+
}
|
|
10891
|
+
const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
|
|
10892
|
+
if (!project) {
|
|
10893
|
+
error(`Project "${config.project.key}" not found in database.`, opts);
|
|
10894
|
+
process.exit(1);
|
|
10895
|
+
}
|
|
10896
|
+
const session = getActiveSession(project.id);
|
|
10897
|
+
if (!session) {
|
|
10898
|
+
error('No active session. Start one with "aigile session start".', opts);
|
|
10899
|
+
process.exit(1);
|
|
10900
|
+
}
|
|
10901
|
+
const existing = getChunk(id);
|
|
10902
|
+
if (existing) {
|
|
10903
|
+
error(`Chunk "${id}" already exists.`, opts);
|
|
10904
|
+
process.exit(1);
|
|
10905
|
+
}
|
|
10906
|
+
const validModes = ["quick", "standard", "audit"];
|
|
10907
|
+
if (!validModes.includes(options.mode)) {
|
|
10908
|
+
error(`Invalid mode "${options.mode}". Must be: ${validModes.join(", ")}`, opts);
|
|
10909
|
+
process.exit(1);
|
|
10910
|
+
}
|
|
10911
|
+
let assignedFiles = [];
|
|
10912
|
+
if (options.pattern) {
|
|
10913
|
+
for (const pattern of options.pattern) {
|
|
10914
|
+
const matches = await glob2(pattern, { cwd: projectRoot, nodir: true });
|
|
10915
|
+
assignedFiles.push(...matches.map((f) => relative5(projectRoot, resolve2(projectRoot, f))));
|
|
10916
|
+
}
|
|
10917
|
+
}
|
|
10918
|
+
if (options.assign) {
|
|
10919
|
+
assignedFiles.push(...options.assign);
|
|
10920
|
+
}
|
|
10921
|
+
assignedFiles = [...new Set(assignedFiles)];
|
|
10922
|
+
const name = options.name ?? id;
|
|
10923
|
+
createChunk(
|
|
10924
|
+
session.id,
|
|
10925
|
+
id,
|
|
10926
|
+
name,
|
|
10927
|
+
options.pattern ?? null,
|
|
10928
|
+
assignedFiles.length > 0 ? assignedFiles : null,
|
|
10929
|
+
options.mode
|
|
10930
|
+
);
|
|
10931
|
+
if (opts.json) {
|
|
10932
|
+
console.log(JSON.stringify({
|
|
10933
|
+
success: true,
|
|
10934
|
+
data: {
|
|
10935
|
+
id,
|
|
10936
|
+
name,
|
|
10937
|
+
patterns: options.pattern ?? [],
|
|
10938
|
+
assigned_files: assignedFiles,
|
|
10939
|
+
review_mode: options.mode,
|
|
10940
|
+
session_id: session.id
|
|
10941
|
+
}
|
|
10942
|
+
}));
|
|
10943
|
+
} else {
|
|
10944
|
+
success(`Created chunk "${id}" (${name})`, opts);
|
|
10945
|
+
if (assignedFiles.length > 0) {
|
|
10946
|
+
console.log(` Assigned files: ${assignedFiles.length}`);
|
|
10947
|
+
}
|
|
10948
|
+
if (options.pattern) {
|
|
10949
|
+
console.log(` Patterns: ${options.pattern.join(", ")}`);
|
|
10950
|
+
}
|
|
10951
|
+
console.log(` Review mode: ${options.mode}`);
|
|
10952
|
+
}
|
|
10953
|
+
});
|
|
10954
|
+
chunkCommand.command("files").argument("<id>", "Chunk ID").option("--json", "Output as JSON").description("List files assigned to a chunk").action((id) => {
|
|
10955
|
+
const opts = getOutputOptions(chunkCommand);
|
|
10956
|
+
const chunk = getChunk(id);
|
|
10957
|
+
if (!chunk) {
|
|
10958
|
+
error(`Chunk "${id}" not found.`, opts);
|
|
10959
|
+
process.exit(1);
|
|
10960
|
+
}
|
|
10961
|
+
const assignedFiles = safeParseArray(chunk.assigned_files);
|
|
10962
|
+
if (opts.json) {
|
|
10963
|
+
console.log(JSON.stringify({
|
|
10964
|
+
success: true,
|
|
10965
|
+
data: {
|
|
10966
|
+
chunk_id: id,
|
|
10967
|
+
name: chunk.name,
|
|
10968
|
+
patterns: safeParseArray(chunk.patterns),
|
|
10969
|
+
files: assignedFiles,
|
|
10970
|
+
review_mode: chunk.review_mode
|
|
10971
|
+
}
|
|
10972
|
+
}));
|
|
10973
|
+
} else {
|
|
10974
|
+
console.log(`Chunk: ${chunk.name} (${id})`);
|
|
10975
|
+
console.log(`Review mode: ${chunk.review_mode}`);
|
|
10976
|
+
console.log(`
|
|
10977
|
+
Assigned files (${assignedFiles.length}):`);
|
|
10978
|
+
for (const file of assignedFiles) {
|
|
10979
|
+
console.log(` ${file}`);
|
|
10980
|
+
}
|
|
10981
|
+
}
|
|
10982
|
+
});
|
|
10983
|
+
chunkCommand.command("assign").argument("<id>", "Chunk ID").argument("<files...>", "Files to assign").description("Assign additional files to a chunk").action((id, files) => {
|
|
10984
|
+
const opts = getOutputOptions(chunkCommand);
|
|
10985
|
+
const chunk = getChunk(id);
|
|
10986
|
+
if (!chunk) {
|
|
10987
|
+
error(`Chunk "${id}" not found.`, opts);
|
|
10988
|
+
process.exit(1);
|
|
10989
|
+
}
|
|
10990
|
+
try {
|
|
10991
|
+
assignFilesToChunk(id, files);
|
|
10992
|
+
if (opts.json) {
|
|
10993
|
+
const updated = getChunk(id);
|
|
10994
|
+
console.log(JSON.stringify({
|
|
10995
|
+
success: true,
|
|
10996
|
+
data: {
|
|
10997
|
+
chunk_id: id,
|
|
10998
|
+
added: files.length,
|
|
10999
|
+
total: safeParseArray(updated.assigned_files).length
|
|
11000
|
+
}
|
|
11001
|
+
}));
|
|
11002
|
+
} else {
|
|
11003
|
+
success(`Assigned ${files.length} file(s) to chunk "${id}"`, opts);
|
|
11004
|
+
}
|
|
11005
|
+
} catch (err) {
|
|
11006
|
+
error(err instanceof Error ? err.message : "Failed to assign files", opts);
|
|
11007
|
+
process.exit(1);
|
|
11008
|
+
}
|
|
11009
|
+
});
|
|
11010
|
+
chunkCommand.command("list").alias("ls").description("List all chunks in current session").action(() => {
|
|
11011
|
+
const opts = getOutputOptions(chunkCommand);
|
|
11012
|
+
const projectRoot = findProjectRoot();
|
|
11013
|
+
if (!projectRoot) {
|
|
11014
|
+
error('Not in an AIGILE project. Run "aigile init" first.', opts);
|
|
11015
|
+
process.exit(1);
|
|
11016
|
+
}
|
|
11017
|
+
const config = loadProjectConfig(projectRoot);
|
|
11018
|
+
if (!config) {
|
|
11019
|
+
error("Could not load project config.", opts);
|
|
11020
|
+
process.exit(1);
|
|
11021
|
+
}
|
|
11022
|
+
const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
|
|
11023
|
+
if (!project) {
|
|
11024
|
+
error(`Project "${config.project.key}" not found in database.`, opts);
|
|
11025
|
+
process.exit(1);
|
|
11026
|
+
}
|
|
11027
|
+
const session = getActiveSession(project.id);
|
|
11028
|
+
if (!session) {
|
|
11029
|
+
warning2("No active session.", opts);
|
|
11030
|
+
return;
|
|
11031
|
+
}
|
|
11032
|
+
const chunks = getSessionChunks(session.id);
|
|
11033
|
+
if (chunks.length === 0) {
|
|
11034
|
+
warning2("No chunks defined in current session.", opts);
|
|
11035
|
+
return;
|
|
11036
|
+
}
|
|
11037
|
+
data(
|
|
11038
|
+
chunks.map((c) => ({
|
|
11039
|
+
id: c.id,
|
|
11040
|
+
name: c.name,
|
|
11041
|
+
files: safeParseArray(c.assigned_files).length,
|
|
11042
|
+
mode: c.review_mode,
|
|
11043
|
+
created: c.created_at.split("T")[0]
|
|
11044
|
+
})),
|
|
11045
|
+
[
|
|
11046
|
+
{ header: "ID", key: "id", width: 15 },
|
|
11047
|
+
{ header: "Name", key: "name", width: 25 },
|
|
11048
|
+
{ header: "Files", key: "files", width: 8 },
|
|
11049
|
+
{ header: "Mode", key: "mode", width: 10 },
|
|
11050
|
+
{ header: "Created", key: "created", width: 12 }
|
|
11051
|
+
],
|
|
11052
|
+
opts
|
|
11053
|
+
);
|
|
11054
|
+
});
|
|
11055
|
+
chunkCommand.command("show").argument("<id>", "Chunk ID").description("Show chunk details").action((id) => {
|
|
11056
|
+
const opts = getOutputOptions(chunkCommand);
|
|
11057
|
+
const chunk = getChunk(id);
|
|
11058
|
+
if (!chunk) {
|
|
11059
|
+
error(`Chunk "${id}" not found.`, opts);
|
|
11060
|
+
process.exit(1);
|
|
11061
|
+
}
|
|
11062
|
+
const assignedFiles = safeParseArray(chunk.assigned_files);
|
|
11063
|
+
const patterns = safeParseArray(chunk.patterns);
|
|
11064
|
+
details(
|
|
11065
|
+
{
|
|
11066
|
+
id: chunk.id,
|
|
11067
|
+
name: chunk.name,
|
|
11068
|
+
review_mode: chunk.review_mode,
|
|
11069
|
+
patterns: patterns.length > 0 ? patterns.join(", ") : "-",
|
|
11070
|
+
assigned_files: assignedFiles.length,
|
|
11071
|
+
session_id: chunk.session_id.slice(0, 8) + "...",
|
|
11072
|
+
created_at: chunk.created_at
|
|
11073
|
+
},
|
|
11074
|
+
[
|
|
11075
|
+
{ label: "ID", key: "id" },
|
|
11076
|
+
{ label: "Name", key: "name" },
|
|
11077
|
+
{ label: "Review Mode", key: "review_mode" },
|
|
11078
|
+
{ label: "Patterns", key: "patterns" },
|
|
11079
|
+
{ label: "Assigned Files", key: "assigned_files" },
|
|
11080
|
+
{ label: "Session", key: "session_id" },
|
|
11081
|
+
{ label: "Created", key: "created_at" }
|
|
11082
|
+
],
|
|
11083
|
+
opts
|
|
11084
|
+
);
|
|
11085
|
+
});
|
|
9851
11086
|
|
|
9852
11087
|
// src/bin/aigile.ts
|
|
9853
11088
|
async function main() {
|
|
9854
|
-
const program = new
|
|
11089
|
+
const program = new Command23();
|
|
9855
11090
|
program.name("aigile").description("JIRA-compatible Agile project management CLI for AI-assisted development").version(VERSION, "-v, --version", "Display version number").option("--json", "Output in JSON format for machine parsing").option("--no-color", "Disable colored output");
|
|
9856
11091
|
program.hook("preAction", async () => {
|
|
9857
11092
|
await initDatabase();
|
|
@@ -9884,6 +11119,7 @@ async function main() {
|
|
|
9884
11119
|
program.addCommand(docCommand);
|
|
9885
11120
|
program.addCommand(daemonCommand);
|
|
9886
11121
|
program.addCommand(fileCommand);
|
|
11122
|
+
program.addCommand(chunkCommand);
|
|
9887
11123
|
await program.parseAsync(process.argv);
|
|
9888
11124
|
}
|
|
9889
11125
|
main().catch((err) => {
|