cto-ai-cli 3.1.0 → 4.0.0
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/DOCS.md +352 -0
- package/README.md +192 -15
- package/dist/action/index.js +629 -83
- package/dist/api/dashboard.js +107 -23
- package/dist/api/dashboard.js.map +1 -1
- package/dist/api/server.js +108 -24
- package/dist/api/server.js.map +1 -1
- package/dist/cli/gateway.js +2925 -0
- package/dist/cli/score.js +3015 -237
- package/dist/cli/v2/index.js +133 -49
- package/dist/cli/v2/index.js.map +1 -1
- package/dist/engine/index.d.ts +85 -1
- package/dist/engine/index.js +665 -42
- package/dist/engine/index.js.map +1 -1
- package/dist/gateway/index.d.ts +281 -0
- package/dist/gateway/index.js +2803 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/govern/index.d.ts +67 -3
- package/dist/govern/index.js +462 -23
- package/dist/govern/index.js.map +1 -1
- package/dist/interact/index.js +108 -24
- package/dist/interact/index.js.map +1 -1
- package/dist/mcp/v2.js +130 -46
- package/dist/mcp/v2.js.map +1 -1
- package/package.json +3 -2
package/dist/cli/v2/index.js
CHANGED
|
@@ -935,8 +935,8 @@ var analyzeCommand = new Command2("analyze").description("Analyze project struct
|
|
|
935
935
|
// src/cli/v2/interact.ts
|
|
936
936
|
import { Command as Command3 } from "commander";
|
|
937
937
|
import chalk3 from "chalk";
|
|
938
|
-
import { resolve as resolve8, join as
|
|
939
|
-
import { writeFile as
|
|
938
|
+
import { resolve as resolve8, join as join9 } from "path";
|
|
939
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
940
940
|
|
|
941
941
|
// src/engine/cache.ts
|
|
942
942
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -1307,11 +1307,13 @@ function emptyResult(baseBranch) {
|
|
|
1307
1307
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1308
1308
|
|
|
1309
1309
|
// src/engine/selector.ts
|
|
1310
|
-
import { createHash as
|
|
1310
|
+
import { createHash as createHash4 } from "crypto";
|
|
1311
1311
|
|
|
1312
1312
|
// src/govern/secrets.ts
|
|
1313
1313
|
import { readFile as readFile4 } from "fs/promises";
|
|
1314
|
-
import {
|
|
1314
|
+
import { readFileSync, existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
|
|
1315
|
+
import { resolve as resolve7, relative as relative4, join as join6, dirname as dirname2 } from "path";
|
|
1316
|
+
import { createHash as createHash3 } from "crypto";
|
|
1315
1317
|
var BUILTIN_PATTERNS = [
|
|
1316
1318
|
// API Keys
|
|
1317
1319
|
{ type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
|
|
@@ -1336,15 +1338,66 @@ var BUILTIN_PATTERNS = [
|
|
|
1336
1338
|
{ type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
|
|
1337
1339
|
{ type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
|
|
1338
1340
|
// Environment variables with secrets
|
|
1339
|
-
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
|
|
1341
|
+
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
|
|
1342
|
+
// Stripe
|
|
1343
|
+
{ type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
|
|
1344
|
+
{ type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
|
|
1345
|
+
{ type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
|
|
1346
|
+
// Slack
|
|
1347
|
+
{ type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
|
|
1348
|
+
{ type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
|
|
1349
|
+
{ type: "api-key", source: "https://hooks\\.slack\\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+", flags: "g", severity: "high", description: "Slack Webhook URL" },
|
|
1350
|
+
// Google
|
|
1351
|
+
{ type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
|
|
1352
|
+
{ type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
|
|
1353
|
+
// Azure
|
|
1354
|
+
{ type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
|
|
1355
|
+
// Twilio
|
|
1356
|
+
{ type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
|
|
1357
|
+
// SendGrid
|
|
1358
|
+
{ type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
|
|
1359
|
+
// JWT
|
|
1360
|
+
{ type: "token", source: "eyJ[a-zA-Z0-9_-]{10,}\\.eyJ[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}", flags: "g", severity: "high", description: "JSON Web Token" },
|
|
1361
|
+
// Datadog
|
|
1362
|
+
{ type: "api-key", source: `(?:DD_API_KEY|DATADOG_API_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{32})['"]?`, flags: "gi", severity: "critical", description: "Datadog API Key" },
|
|
1363
|
+
{ type: "api-key", source: `(?:DD_APP_KEY|DATADOG_APP_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{40})['"]?`, flags: "gi", severity: "critical", description: "Datadog App Key" },
|
|
1364
|
+
// Sentry
|
|
1365
|
+
{ type: "connection-string", source: "https://[a-f0-9]{32}@[a-z0-9]+\\.ingest\\.sentry\\.io/[0-9]+", flags: "g", severity: "high", description: "Sentry DSN" },
|
|
1366
|
+
// Firebase
|
|
1367
|
+
{ type: "api-key", source: `(?:FIREBASE_API_KEY|FIREBASE_KEY)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{30,})['"]?`, flags: "gi", severity: "high", description: "Firebase API Key" },
|
|
1368
|
+
{ type: "connection-string", source: `firebase[a-z]*:\\/\\/[^\\s'"]+`, flags: "gi", severity: "high", description: "Firebase URL" },
|
|
1369
|
+
// Supabase
|
|
1370
|
+
{ type: "api-key", source: "sbp_[a-f0-9]{40}", flags: "g", severity: "critical", description: "Supabase Service Key" },
|
|
1371
|
+
{ type: "token", source: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}", flags: "g", severity: "high", description: "Supabase Anon/Service JWT" },
|
|
1372
|
+
// Vercel
|
|
1373
|
+
{ type: "token", source: `(?:VERCEL_TOKEN|VERCEL_API_TOKEN)\\s*[:=]\\s*['"]?([a-zA-Z0-9]{24,})['"]?`, flags: "gi", severity: "critical", description: "Vercel Token" },
|
|
1374
|
+
// Heroku
|
|
1375
|
+
{ type: "api-key", source: `(?:HEROKU_API_KEY|HEROKU_TOKEN)\\s*[:=]\\s*['"]?([a-f0-9\\-]{36,})['"]?`, flags: "gi", severity: "critical", description: "Heroku API Key" },
|
|
1376
|
+
// DigitalOcean
|
|
1377
|
+
{ type: "token", source: "dop_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean Personal Access Token" },
|
|
1378
|
+
{ type: "token", source: "doo_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean OAuth Token" },
|
|
1379
|
+
// Mailgun
|
|
1380
|
+
{ type: "api-key", source: "key-[a-zA-Z0-9]{32}", flags: "g", severity: "high", description: "Mailgun API Key" },
|
|
1381
|
+
// PII
|
|
1382
|
+
{ type: "pii", source: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", flags: "g", severity: "medium", description: "Email Address (PII)" },
|
|
1383
|
+
{ type: "pii", source: "\\b(?!000|666|9\\d{2})(\\d{3})[-.]?(?!00)(\\d{2})[-.]?(?!0000)(\\d{4})\\b", flags: "g", severity: "high", description: "Possible SSN (PII)" }
|
|
1340
1384
|
];
|
|
1385
|
+
var _cachedBuiltinPatterns = null;
|
|
1386
|
+
function getBuiltinPatterns() {
|
|
1387
|
+
if (!_cachedBuiltinPatterns) {
|
|
1388
|
+
_cachedBuiltinPatterns = BUILTIN_PATTERNS.map((def) => ({
|
|
1389
|
+
type: def.type,
|
|
1390
|
+
pattern: new RegExp(def.source, def.flags),
|
|
1391
|
+
severity: def.severity,
|
|
1392
|
+
description: def.description
|
|
1393
|
+
}));
|
|
1394
|
+
}
|
|
1395
|
+
return _cachedBuiltinPatterns;
|
|
1396
|
+
}
|
|
1341
1397
|
function buildPatterns(customPatterns = []) {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
severity: def.severity,
|
|
1346
|
-
description: def.description
|
|
1347
|
-
}));
|
|
1398
|
+
const builtins = getBuiltinPatterns();
|
|
1399
|
+
if (customPatterns.length === 0) return builtins;
|
|
1400
|
+
const patterns = [...builtins];
|
|
1348
1401
|
for (const custom of customPatterns) {
|
|
1349
1402
|
try {
|
|
1350
1403
|
patterns.push({
|
|
@@ -1358,7 +1411,7 @@ function buildPatterns(customPatterns = []) {
|
|
|
1358
1411
|
}
|
|
1359
1412
|
return patterns;
|
|
1360
1413
|
}
|
|
1361
|
-
function scanContentForSecrets(content, filePath, customPatterns = []) {
|
|
1414
|
+
function scanContentForSecrets(content, filePath, customPatterns = [], extraPiiSafeDomains) {
|
|
1362
1415
|
const findings = [];
|
|
1363
1416
|
const lines = content.split("\n");
|
|
1364
1417
|
const allPatterns = buildPatterns(customPatterns);
|
|
@@ -1370,6 +1423,7 @@ function scanContentForSecrets(content, filePath, customPatterns = []) {
|
|
|
1370
1423
|
while ((match = secretPattern.pattern.exec(line)) !== null) {
|
|
1371
1424
|
const matchText = match[0];
|
|
1372
1425
|
if (isTemplateOrPlaceholder(matchText)) continue;
|
|
1426
|
+
if (secretPattern.type === "pii" && isSafeEmail(matchText, extraPiiSafeDomains)) continue;
|
|
1373
1427
|
findings.push({
|
|
1374
1428
|
type: secretPattern.type,
|
|
1375
1429
|
file: filePath,
|
|
@@ -1417,6 +1471,36 @@ function isTemplateOrPlaceholder(value) {
|
|
|
1417
1471
|
];
|
|
1418
1472
|
return placeholders.some((p) => p.test(value));
|
|
1419
1473
|
}
|
|
1474
|
+
var PII_SAFE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
1475
|
+
"example.com",
|
|
1476
|
+
"example.org",
|
|
1477
|
+
"example.net",
|
|
1478
|
+
"test.com",
|
|
1479
|
+
"test.org",
|
|
1480
|
+
"test.net",
|
|
1481
|
+
"localhost",
|
|
1482
|
+
"localhost.localdomain",
|
|
1483
|
+
"email.com",
|
|
1484
|
+
"mail.com",
|
|
1485
|
+
"foo.com",
|
|
1486
|
+
"bar.com",
|
|
1487
|
+
"baz.com",
|
|
1488
|
+
"acme.com",
|
|
1489
|
+
"company.com",
|
|
1490
|
+
"corp.com",
|
|
1491
|
+
"noreply.com",
|
|
1492
|
+
"no-reply.com",
|
|
1493
|
+
"users.noreply.github.com",
|
|
1494
|
+
"placeholder.com"
|
|
1495
|
+
]);
|
|
1496
|
+
function isSafeEmail(value, extraDomains) {
|
|
1497
|
+
const match = value.match(/@([a-zA-Z0-9.-]+)$/);
|
|
1498
|
+
if (!match) return false;
|
|
1499
|
+
const domain = match[1].toLowerCase();
|
|
1500
|
+
if (PII_SAFE_EMAIL_DOMAINS.has(domain)) return true;
|
|
1501
|
+
if (extraDomains && extraDomains.has(domain)) return true;
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1420
1504
|
function deduplicateFindings(findings) {
|
|
1421
1505
|
const seen = /* @__PURE__ */ new Set();
|
|
1422
1506
|
return findings.filter((f) => {
|
|
@@ -1430,8 +1514,8 @@ function deduplicateFindings(findings) {
|
|
|
1430
1514
|
// src/engine/pruner.ts
|
|
1431
1515
|
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1432
1516
|
import { readFile as readFile5 } from "fs/promises";
|
|
1433
|
-
import { existsSync as
|
|
1434
|
-
import { join as
|
|
1517
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1518
|
+
import { join as join7 } from "path";
|
|
1435
1519
|
var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
|
|
1436
1520
|
async function pruneFile(file, level) {
|
|
1437
1521
|
if (level === "excluded") {
|
|
@@ -1677,9 +1761,9 @@ function addJSDoc(node, parts) {
|
|
|
1677
1761
|
function findTsConfig(filePath) {
|
|
1678
1762
|
let dir = filePath;
|
|
1679
1763
|
for (let i = 0; i < 10; i++) {
|
|
1680
|
-
dir =
|
|
1681
|
-
const candidate =
|
|
1682
|
-
if (
|
|
1764
|
+
dir = join7(dir, "..");
|
|
1765
|
+
const candidate = join7(dir, "tsconfig.json");
|
|
1766
|
+
if (existsSync4(candidate)) return candidate;
|
|
1683
1767
|
}
|
|
1684
1768
|
return void 0;
|
|
1685
1769
|
}
|
|
@@ -1893,7 +1977,7 @@ async function selectContext(input) {
|
|
|
1893
1977
|
);
|
|
1894
1978
|
const excludedRisk = excludedFiles.length > 0 ? Math.round(excludedFiles.reduce((s, f) => s + f.riskScore, 0) / excludedFiles.length) : 0;
|
|
1895
1979
|
const hashInput = selectedFiles.map((f) => `${f.relativePath}:${f.pruneLevel}`).sort().join("|") + `|budget:${budget}`;
|
|
1896
|
-
const hash =
|
|
1980
|
+
const hash = createHash4("sha256").update(hashInput).digest("hex").substring(0, 16);
|
|
1897
1981
|
return {
|
|
1898
1982
|
files: selectedFiles,
|
|
1899
1983
|
totalTokens: usedTokens,
|
|
@@ -2386,20 +2470,20 @@ function makeSection(id, role, content) {
|
|
|
2386
2470
|
}
|
|
2387
2471
|
|
|
2388
2472
|
// src/govern/audit.ts
|
|
2389
|
-
import { randomUUID, createHash as
|
|
2473
|
+
import { randomUUID, createHash as createHash5 } from "crypto";
|
|
2390
2474
|
import { readdir as readdir3, chmod } from "fs/promises";
|
|
2391
|
-
import { join as
|
|
2475
|
+
import { join as join8 } from "path";
|
|
2392
2476
|
import { userInfo } from "os";
|
|
2393
2477
|
import { homedir } from "os";
|
|
2394
2478
|
var CTO_DIR = ".cto-ai";
|
|
2395
2479
|
var AUDIT_DIR = "audit";
|
|
2396
2480
|
var MAX_ENTRIES_PER_FILE = 500;
|
|
2397
2481
|
function getAuditDir() {
|
|
2398
|
-
return
|
|
2482
|
+
return join8(homedir(), CTO_DIR, AUDIT_DIR);
|
|
2399
2483
|
}
|
|
2400
2484
|
function getCurrentAuditFile() {
|
|
2401
2485
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
|
|
2402
|
-
return
|
|
2486
|
+
return join8(getAuditDir(), `audit_${date}.json`);
|
|
2403
2487
|
}
|
|
2404
2488
|
function computeIntegrityHash(entry) {
|
|
2405
2489
|
const payload = JSON.stringify({
|
|
@@ -2410,7 +2494,7 @@ function computeIntegrityHash(entry) {
|
|
|
2410
2494
|
projectPath: entry.projectPath,
|
|
2411
2495
|
details: entry.details
|
|
2412
2496
|
});
|
|
2413
|
-
return
|
|
2497
|
+
return createHash5("sha256").update(payload).digest("hex");
|
|
2414
2498
|
}
|
|
2415
2499
|
async function ensureDir(dirPath) {
|
|
2416
2500
|
const { mkdir: mkdir5 } = await import("fs/promises");
|
|
@@ -2426,9 +2510,9 @@ async function readJSON(filePath) {
|
|
|
2426
2510
|
}
|
|
2427
2511
|
}
|
|
2428
2512
|
async function writeJSON(filePath, data) {
|
|
2429
|
-
const { writeFile:
|
|
2430
|
-
await ensureDir(
|
|
2431
|
-
await
|
|
2513
|
+
const { writeFile: writeFile7 } = await import("fs/promises");
|
|
2514
|
+
await ensureDir(join8(filePath, ".."));
|
|
2515
|
+
await writeFile7(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
2432
2516
|
}
|
|
2433
2517
|
async function logAudit(action, projectPath, details = {}) {
|
|
2434
2518
|
const auditDir = getAuditDir();
|
|
@@ -2477,7 +2561,7 @@ async function getAuditEntries(options = {}) {
|
|
|
2477
2561
|
const limit = options.limit ?? 100;
|
|
2478
2562
|
for (const file of auditFiles) {
|
|
2479
2563
|
if (allEntries.length >= limit) break;
|
|
2480
|
-
const entries = await readJSON(
|
|
2564
|
+
const entries = await readJSON(join8(auditDir, file));
|
|
2481
2565
|
if (!entries) continue;
|
|
2482
2566
|
for (const entry of entries.reverse()) {
|
|
2483
2567
|
if (allEntries.length >= limit) break;
|
|
@@ -2526,7 +2610,7 @@ async function purgeOldAuditEntries(retentionDays) {
|
|
|
2526
2610
|
const dateStr = file.replace("audit_", "").replace(".json", "");
|
|
2527
2611
|
if (dateStr < cutoffStr) {
|
|
2528
2612
|
try {
|
|
2529
|
-
await unlink(
|
|
2613
|
+
await unlink(join8(auditDir, file));
|
|
2530
2614
|
purged++;
|
|
2531
2615
|
} catch {
|
|
2532
2616
|
}
|
|
@@ -2753,8 +2837,8 @@ var interactCommand = new Command3("interact").description("Build optimized AI c
|
|
|
2753
2837
|
console.log("");
|
|
2754
2838
|
}
|
|
2755
2839
|
if (opts.output === "file") {
|
|
2756
|
-
const outPath =
|
|
2757
|
-
await
|
|
2840
|
+
const outPath = join9(resolve8(opts.path ?? "."), ".cto", `prompt-${plan.id}.md`);
|
|
2841
|
+
await writeFile4(outPath, plan.prompt.rendered, "utf-8");
|
|
2758
2842
|
console.log(chalk3.green(` \u{1F4BE} Prompt saved to ${outPath}`));
|
|
2759
2843
|
console.log("");
|
|
2760
2844
|
} else if (opts.output === "clipboard") {
|
|
@@ -2783,11 +2867,11 @@ var interactCommand = new Command3("interact").description("Build optimized AI c
|
|
|
2783
2867
|
// src/cli/v2/snapshot.ts
|
|
2784
2868
|
import { Command as Command4 } from "commander";
|
|
2785
2869
|
import chalk4 from "chalk";
|
|
2786
|
-
import { resolve as resolve9, join as
|
|
2787
|
-
import { readFile as readFile7, writeFile as
|
|
2870
|
+
import { resolve as resolve9, join as join10 } from "path";
|
|
2871
|
+
import { readFile as readFile7, writeFile as writeFile5, readdir as readdir4, mkdir as mkdir3 } from "fs/promises";
|
|
2788
2872
|
|
|
2789
2873
|
// src/govern/snapshot.ts
|
|
2790
|
-
import { randomUUID as randomUUID3, createHash as
|
|
2874
|
+
import { randomUUID as randomUUID3, createHash as createHash6 } from "crypto";
|
|
2791
2875
|
import "fs/promises";
|
|
2792
2876
|
function createSnapshot(name, analysis, selection, metadata = {}) {
|
|
2793
2877
|
const files = selection.files.map((f) => ({
|
|
@@ -2880,13 +2964,13 @@ function compareSnapshots(older, newer) {
|
|
|
2880
2964
|
};
|
|
2881
2965
|
}
|
|
2882
2966
|
function hashString(input) {
|
|
2883
|
-
return
|
|
2967
|
+
return createHash6("sha256").update(input).digest("hex").substring(0, 16);
|
|
2884
2968
|
}
|
|
2885
2969
|
|
|
2886
2970
|
// src/cli/v2/snapshot.ts
|
|
2887
2971
|
var SNAPSHOTS_DIR = ".cto/snapshots";
|
|
2888
2972
|
async function ensureSnapshotsDir(projectPath) {
|
|
2889
|
-
const dir =
|
|
2973
|
+
const dir = join10(projectPath, SNAPSHOTS_DIR);
|
|
2890
2974
|
await mkdir3(dir, { recursive: true });
|
|
2891
2975
|
return dir;
|
|
2892
2976
|
}
|
|
@@ -2914,8 +2998,8 @@ var snapshotCommand = new Command4("snapshot").description("Create and manage re
|
|
|
2914
2998
|
createdBy: process.env.USER ?? "unknown"
|
|
2915
2999
|
});
|
|
2916
3000
|
const dir = await ensureSnapshotsDir(projectPath);
|
|
2917
|
-
const filePath =
|
|
2918
|
-
await
|
|
3001
|
+
const filePath = join10(dir, `${name}.json`);
|
|
3002
|
+
await writeFile5(filePath, JSON.stringify(snap, null, 2), "utf-8");
|
|
2919
3003
|
console.log("");
|
|
2920
3004
|
console.log(chalk4.bold.cyan(` \u{1F4F8} Snapshot "${name}" created`));
|
|
2921
3005
|
console.log(` Files: ${snap.files.length}`);
|
|
@@ -2937,7 +3021,7 @@ var snapshotCommand = new Command4("snapshot").description("Create and manage re
|
|
|
2937
3021
|
try {
|
|
2938
3022
|
const projectPath = resolve9(opts.path ?? ".");
|
|
2939
3023
|
const budget = parseInt(opts.budget ?? "50000", 10);
|
|
2940
|
-
const snapPath =
|
|
3024
|
+
const snapPath = join10(projectPath, SNAPSHOTS_DIR, `${name}.json`);
|
|
2941
3025
|
const snap = await loadSnapshot(snapPath);
|
|
2942
3026
|
console.log(chalk4.dim(`
|
|
2943
3027
|
Re-analyzing project...`));
|
|
@@ -2974,9 +3058,9 @@ var snapshotCommand = new Command4("snapshot").description("Create and manage re
|
|
|
2974
3058
|
new Command4("compare").description("Compare two snapshots").argument("<older>", "Older snapshot name").argument("<newer>", "Newer snapshot name").option("-p, --path <path>", "Project path", ".").action(async (older, newer, opts) => {
|
|
2975
3059
|
try {
|
|
2976
3060
|
const projectPath = resolve9(opts.path ?? ".");
|
|
2977
|
-
const dir =
|
|
2978
|
-
const snap1 = await loadSnapshot(
|
|
2979
|
-
const snap2 = await loadSnapshot(
|
|
3061
|
+
const dir = join10(projectPath, SNAPSHOTS_DIR);
|
|
3062
|
+
const snap1 = await loadSnapshot(join10(dir, `${older}.json`));
|
|
3063
|
+
const snap2 = await loadSnapshot(join10(dir, `${newer}.json`));
|
|
2980
3064
|
const diff = compareSnapshots(snap1, snap2);
|
|
2981
3065
|
console.log("");
|
|
2982
3066
|
console.log(chalk4.bold.cyan(` \u{1F4CA} Snapshot Comparison: ${older} \u2192 ${newer}`));
|
|
@@ -3009,7 +3093,7 @@ var snapshotCommand = new Command4("snapshot").description("Create and manage re
|
|
|
3009
3093
|
new Command4("list").description("List saved snapshots").option("-p, --path <path>", "Project path", ".").action(async (opts) => {
|
|
3010
3094
|
try {
|
|
3011
3095
|
const projectPath = resolve9(opts.path ?? ".");
|
|
3012
|
-
const dir =
|
|
3096
|
+
const dir = join10(projectPath, SNAPSHOTS_DIR);
|
|
3013
3097
|
let files;
|
|
3014
3098
|
try {
|
|
3015
3099
|
files = await readdir4(dir);
|
|
@@ -3027,7 +3111,7 @@ var snapshotCommand = new Command4("snapshot").description("Create and manage re
|
|
|
3027
3111
|
console.log("");
|
|
3028
3112
|
for (const file of snapFiles) {
|
|
3029
3113
|
try {
|
|
3030
|
-
const snap = await loadSnapshot(
|
|
3114
|
+
const snap = await loadSnapshot(join10(dir, file));
|
|
3031
3115
|
const date = new Date(snap.createdAt).toLocaleString();
|
|
3032
3116
|
console.log(` ${chalk4.bold(snap.name)} \u2014 ${snap.files.length} files, ~${Math.round(snap.totalTokens / 1e3)}K tokens, coverage ${snap.coverageScore}%`);
|
|
3033
3117
|
console.log(` ${chalk4.dim(`Created: ${date} | Hash: ${snap.hash}`)}`);
|
|
@@ -3138,8 +3222,8 @@ var auditCommand = new Command5("audit").description("View and manage the audit
|
|
|
3138
3222
|
// src/cli/v2/policy.ts
|
|
3139
3223
|
import { Command as Command6 } from "commander";
|
|
3140
3224
|
import chalk6 from "chalk";
|
|
3141
|
-
import { resolve as resolve10, join as
|
|
3142
|
-
import { readFile as readFile8, writeFile as
|
|
3225
|
+
import { resolve as resolve10, join as join11 } from "path";
|
|
3226
|
+
import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir4 } from "fs/promises";
|
|
3143
3227
|
|
|
3144
3228
|
// src/govern/policy.ts
|
|
3145
3229
|
var DEFAULT_POLICY = {
|
|
@@ -3293,16 +3377,16 @@ function fileMatchesCategory(path, category) {
|
|
|
3293
3377
|
var POLICY_FILE2 = ".cto/policy.json";
|
|
3294
3378
|
async function loadPolicy(projectPath) {
|
|
3295
3379
|
try {
|
|
3296
|
-
const content = await readFile8(
|
|
3380
|
+
const content = await readFile8(join11(projectPath, POLICY_FILE2), "utf-8");
|
|
3297
3381
|
return JSON.parse(content);
|
|
3298
3382
|
} catch {
|
|
3299
3383
|
return DEFAULT_POLICY;
|
|
3300
3384
|
}
|
|
3301
3385
|
}
|
|
3302
3386
|
async function savePolicy(projectPath, policy) {
|
|
3303
|
-
const dir =
|
|
3387
|
+
const dir = join11(projectPath, ".cto");
|
|
3304
3388
|
await mkdir4(dir, { recursive: true });
|
|
3305
|
-
await
|
|
3389
|
+
await writeFile6(join11(projectPath, POLICY_FILE2), JSON.stringify(policy, null, 2), "utf-8");
|
|
3306
3390
|
}
|
|
3307
3391
|
var policyCommand = new Command6("policy").description("Manage context selection policies").addCommand(
|
|
3308
3392
|
new Command6("show").description("Show current policy rules").option("-p, --path <path>", "Project path", ".").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -3446,7 +3530,7 @@ var policyCommand = new Command6("policy").description("Manage context selection
|
|
|
3446
3530
|
const projectPath = resolve10(opts.path ?? ".");
|
|
3447
3531
|
await savePolicy(projectPath, DEFAULT_POLICY);
|
|
3448
3532
|
console.log(chalk6.green(`
|
|
3449
|
-
\u2705 Default policy initialized at ${
|
|
3533
|
+
\u2705 Default policy initialized at ${join11(projectPath, POLICY_FILE2)}
|
|
3450
3534
|
`));
|
|
3451
3535
|
} catch (err) {
|
|
3452
3536
|
console.error(chalk6.red(`
|