@dadado/agent-kit-cli 4.4.5 → 4.5.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/dist/index.js +1828 -1002
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { defineCommand as
|
|
4
|
+
import { defineCommand as defineCommand12, runMain } from "citty";
|
|
5
5
|
|
|
6
6
|
// src/commands/add.ts
|
|
7
7
|
import { defineCommand } from "citty";
|
|
@@ -18,8 +18,11 @@ var DEFAULT_PROTECTED_PATHS = [
|
|
|
18
18
|
".cursor/HANDOFF.md",
|
|
19
19
|
".cursor/plans/**",
|
|
20
20
|
".cursor/memory/**",
|
|
21
|
-
".cursor/context
|
|
21
|
+
".cursor/context/config.json",
|
|
22
|
+
".cursor/context/current/**",
|
|
23
|
+
".cursor/context/backups/**"
|
|
22
24
|
];
|
|
25
|
+
var LEGACY_CONTEXT_PROTECTED_GLOB = ".cursor/context/**";
|
|
23
26
|
var DOMAIN_PACK_IDS = [
|
|
24
27
|
"cybersec",
|
|
25
28
|
"devops",
|
|
@@ -63,12 +66,12 @@ async function listDirectory(rootDir) {
|
|
|
63
66
|
import path2 from "path";
|
|
64
67
|
function resolveContained(root, rel) {
|
|
65
68
|
const rootAbs = path2.resolve(root);
|
|
66
|
-
const
|
|
67
|
-
const relToRoot = path2.relative(rootAbs,
|
|
69
|
+
const candidate2 = path2.resolve(rootAbs, rel);
|
|
70
|
+
const relToRoot = path2.relative(rootAbs, candidate2);
|
|
68
71
|
if (relToRoot.startsWith("..") || path2.isAbsolute(relToRoot)) {
|
|
69
72
|
throw new Error(`Path escapes registry/project root: ${rel}`);
|
|
70
73
|
}
|
|
71
|
-
return
|
|
74
|
+
return candidate2;
|
|
72
75
|
}
|
|
73
76
|
function toPosixRel(root, absPath) {
|
|
74
77
|
return path2.relative(root, absPath).split(path2.sep).join("/");
|
|
@@ -116,11 +119,32 @@ function matchesAnyGlob(relPath, globs) {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
// src/lifecycle/protected.ts
|
|
122
|
+
var CONTEXT_SESSION_GLOBS = [
|
|
123
|
+
".cursor/context/config.json",
|
|
124
|
+
".cursor/context/current/**",
|
|
125
|
+
".cursor/context/backups/**"
|
|
126
|
+
];
|
|
127
|
+
function normalizeProtectedGlobs(globs) {
|
|
128
|
+
const out = [];
|
|
129
|
+
let sawLegacyContext = false;
|
|
130
|
+
for (const raw of globs) {
|
|
131
|
+
const g = normalizeRelPath(raw);
|
|
132
|
+
if (g === LEGACY_CONTEXT_PROTECTED_GLOB) {
|
|
133
|
+
sawLegacyContext = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
out.push(g);
|
|
137
|
+
}
|
|
138
|
+
if (sawLegacyContext) {
|
|
139
|
+
out.push(...CONTEXT_SESSION_GLOBS);
|
|
140
|
+
}
|
|
141
|
+
return [...new Set(out)];
|
|
142
|
+
}
|
|
119
143
|
function resolveProtectedGlobs(manifest) {
|
|
120
144
|
const fromManifest = manifest?.protected ?? [];
|
|
121
145
|
const overrides = (manifest?.overrides ?? []).map((o) => o.path);
|
|
122
146
|
const merged = [...DEFAULT_PROTECTED_PATHS, ...fromManifest, ...overrides];
|
|
123
|
-
return
|
|
147
|
+
return normalizeProtectedGlobs(merged);
|
|
124
148
|
}
|
|
125
149
|
function isProtectedPath(relPath, globs) {
|
|
126
150
|
return matchesAnyGlob(relPath, globs);
|
|
@@ -128,10 +152,19 @@ function isProtectedPath(relPath, globs) {
|
|
|
128
152
|
|
|
129
153
|
// src/lifecycle/apply.ts
|
|
130
154
|
function emptyStats() {
|
|
131
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
written: [],
|
|
157
|
+
removed: [],
|
|
158
|
+
collisions: [],
|
|
159
|
+
skippedProtected: [],
|
|
160
|
+
missing: [],
|
|
161
|
+
unchanged: []
|
|
162
|
+
};
|
|
132
163
|
}
|
|
133
164
|
function mergeStats(into, from) {
|
|
134
165
|
into.written.push(...from.written);
|
|
166
|
+
into.removed.push(...from.removed);
|
|
167
|
+
into.collisions.push(...from.collisions);
|
|
135
168
|
into.skippedProtected.push(...from.skippedProtected);
|
|
136
169
|
into.missing.push(...from.missing);
|
|
137
170
|
into.unchanged.push(...from.unchanged);
|
|
@@ -192,11 +225,12 @@ function buildManifest(input) {
|
|
|
192
225
|
const manifest = {
|
|
193
226
|
schemaVersion: 1,
|
|
194
227
|
version: input.version,
|
|
195
|
-
protected: input.protected ?? [...DEFAULT_PROTECTED_PATHS]
|
|
228
|
+
protected: normalizeProtectedGlobs(input.protected ?? [...DEFAULT_PROTECTED_PATHS])
|
|
196
229
|
};
|
|
197
230
|
if (input.profile) manifest.profile = input.profile;
|
|
198
231
|
if (input.packs?.length) manifest.packs = [...new Set(input.packs)].sort();
|
|
199
232
|
if (input.skills?.length) manifest.skills = [...new Set(input.skills)].sort();
|
|
233
|
+
if (input.personalization) manifest.personalization = input.personalization;
|
|
200
234
|
if (input.registryUrl || input.registryRef) {
|
|
201
235
|
manifest.registry = {};
|
|
202
236
|
if (input.registryUrl) manifest.registry.url = input.registryUrl;
|
|
@@ -234,6 +268,14 @@ function logApplyStats(stats) {
|
|
|
234
268
|
if (stats.unchanged.length > 0) {
|
|
235
269
|
logger.info(`Unchanged: ${stats.unchanged.length}`);
|
|
236
270
|
}
|
|
271
|
+
if (stats.removed.length > 0) {
|
|
272
|
+
logger.info(`Removed managed legacy files: ${stats.removed.length}`);
|
|
273
|
+
for (const p of stats.removed) logger.info(` - ${p}`);
|
|
274
|
+
}
|
|
275
|
+
if (stats.collisions.length > 0) {
|
|
276
|
+
logger.warn("Slash collision preserved because the legacy command is customized:");
|
|
277
|
+
for (const p of stats.collisions) logger.info(` ! ${p}`);
|
|
278
|
+
}
|
|
237
279
|
if (stats.skippedProtected.length > 0) {
|
|
238
280
|
logger.warn(`Skipped protected (L3): ${stats.skippedProtected.length}`);
|
|
239
281
|
for (const p of stats.skippedProtected) logger.info(` ~ ${p}`);
|
|
@@ -370,7 +412,21 @@ var REGISTRY_CLI_ARGS = {
|
|
|
370
412
|
};
|
|
371
413
|
|
|
372
414
|
// src/lifecycle/version.ts
|
|
373
|
-
|
|
415
|
+
import { createRequire } from "module";
|
|
416
|
+
var require2 = createRequire(import.meta.url);
|
|
417
|
+
function loadPackageVersion() {
|
|
418
|
+
for (const packagePath of ["../package.json", "../../package.json"]) {
|
|
419
|
+
try {
|
|
420
|
+
const metadata = require2(packagePath);
|
|
421
|
+
if (typeof metadata.version === "string" && metadata.version.length > 0) {
|
|
422
|
+
return metadata.version;
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
throw new Error("Unable to resolve the Agent Kit CLI package version");
|
|
428
|
+
}
|
|
429
|
+
var KIT_VERSION = loadPackageVersion();
|
|
374
430
|
|
|
375
431
|
// src/manifest/index.ts
|
|
376
432
|
import path5 from "path";
|
|
@@ -494,6 +550,21 @@ function parseAgentKitManifest(raw) {
|
|
|
494
550
|
}
|
|
495
551
|
}
|
|
496
552
|
}
|
|
553
|
+
let personalization;
|
|
554
|
+
if (rest.personalization !== void 0) {
|
|
555
|
+
if (!isPlainObject(rest.personalization)) {
|
|
556
|
+
issues.push("personalization must be an object");
|
|
557
|
+
} else if (typeof rest.personalization.contractVersion !== "number" || typeof rest.personalization.generatorVersion !== "string" || rest.personalization.origin !== "repository-profile" || typeof rest.personalization.resultPath !== "string") {
|
|
558
|
+
issues.push("personalization must contain a valid origin and version contract");
|
|
559
|
+
} else {
|
|
560
|
+
personalization = {
|
|
561
|
+
contractVersion: rest.personalization.contractVersion,
|
|
562
|
+
generatorVersion: rest.personalization.generatorVersion,
|
|
563
|
+
origin: "repository-profile",
|
|
564
|
+
resultPath: rest.personalization.resultPath
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
497
568
|
if (rest.installedAt !== void 0 && typeof rest.installedAt !== "string") {
|
|
498
569
|
issues.push("installedAt must be a string");
|
|
499
570
|
}
|
|
@@ -506,6 +577,7 @@ function parseAgentKitManifest(raw) {
|
|
|
506
577
|
"protected",
|
|
507
578
|
"overrides",
|
|
508
579
|
"registry",
|
|
580
|
+
"personalization",
|
|
509
581
|
"installedAt"
|
|
510
582
|
]);
|
|
511
583
|
for (const key of Object.keys(rest)) {
|
|
@@ -526,6 +598,7 @@ function parseAgentKitManifest(raw) {
|
|
|
526
598
|
if (protectedPaths) manifest.protected = protectedPaths;
|
|
527
599
|
if (overrides) manifest.overrides = overrides;
|
|
528
600
|
if (registry) manifest.registry = registry;
|
|
601
|
+
if (personalization) manifest.personalization = personalization;
|
|
529
602
|
if (typeof rest.installedAt === "string") manifest.installedAt = rest.installedAt;
|
|
530
603
|
return manifest;
|
|
531
604
|
}
|
|
@@ -888,6 +961,10 @@ var L0_ARTIFACTS = [
|
|
|
888
961
|
target: ".cursor/rules/docs-professional-standard.mdc"
|
|
889
962
|
},
|
|
890
963
|
{ source: ".cursor/rules/memory-loop.mdc", target: ".cursor/rules/memory-loop.mdc" },
|
|
964
|
+
{
|
|
965
|
+
source: ".cursor/rules/hitl-ask-questions.mdc",
|
|
966
|
+
target: ".cursor/rules/hitl-ask-questions.mdc"
|
|
967
|
+
},
|
|
891
968
|
{
|
|
892
969
|
source: "registry/rules/git-secrets-safety.mdc",
|
|
893
970
|
target: ".cursor/rules/git-secrets-safety.mdc"
|
|
@@ -898,8 +975,8 @@ var L0_ARTIFACTS = [
|
|
|
898
975
|
target: ".cursor/commands/start-project.md"
|
|
899
976
|
},
|
|
900
977
|
{
|
|
901
|
-
source: ".cursor/commands/onboard.md",
|
|
902
|
-
target: ".cursor/commands/onboard.md"
|
|
978
|
+
source: ".cursor/commands/agent-kit-onboard.md",
|
|
979
|
+
target: ".cursor/commands/agent-kit-onboard.md"
|
|
903
980
|
},
|
|
904
981
|
{
|
|
905
982
|
source: ".cursor/commands/continue-plan.md",
|
|
@@ -925,6 +1002,52 @@ var L0_ARTIFACTS = [
|
|
|
925
1002
|
target: ".cursor/commands/git-staging.md"
|
|
926
1003
|
},
|
|
927
1004
|
{ source: ".cursor/commands/git-prod.md", target: ".cursor/commands/git-prod.md" },
|
|
1005
|
+
{
|
|
1006
|
+
source: ".cursor/commands/plan-external-review.md",
|
|
1007
|
+
target: ".cursor/commands/plan-external-review.md"
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
source: ".cursor/commands/plan-review-triage.md",
|
|
1011
|
+
target: ".cursor/commands/plan-review-triage.md"
|
|
1012
|
+
},
|
|
1013
|
+
// Context (templates + example config; private config.json is not L0)
|
|
1014
|
+
{
|
|
1015
|
+
source: ".cursor/context/templates/plan-external-review-prompt.md",
|
|
1016
|
+
target: ".cursor/context/templates/plan-external-review-prompt.md"
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
source: ".cursor/context/templates/plan.md",
|
|
1020
|
+
target: ".cursor/context/templates/plan.md"
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
source: ".cursor/context/templates/context-pack.md",
|
|
1024
|
+
target: ".cursor/context/templates/context-pack.md"
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
source: ".cursor/context/templates/task-brief.md",
|
|
1028
|
+
target: ".cursor/context/templates/task-brief.md"
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
source: ".cursor/context/templates/handoff.md",
|
|
1032
|
+
target: ".cursor/context/templates/handoff.md"
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
source: ".cursor/context/templates/adr.md",
|
|
1036
|
+
target: ".cursor/context/templates/adr.md"
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
source: ".cursor/context/templates/plan-monitor.md",
|
|
1040
|
+
target: ".cursor/context/templates/plan-monitor.md"
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
source: ".cursor/context/config.example.json",
|
|
1044
|
+
target: ".cursor/context/config.example.json"
|
|
1045
|
+
},
|
|
1046
|
+
// Scripts (canonical launcher; consumers never receive repo-root scripts/)
|
|
1047
|
+
{
|
|
1048
|
+
source: ".cursor/scripts/plan-external-review.sh",
|
|
1049
|
+
target: ".cursor/scripts/plan-external-review.sh"
|
|
1050
|
+
},
|
|
928
1051
|
// Secrets gate (structural)
|
|
929
1052
|
{
|
|
930
1053
|
source: ".cursor/hooks/pre-commit/check-secrets.sh",
|
|
@@ -1366,40 +1489,592 @@ var diffCommand = defineCommand3({
|
|
|
1366
1489
|
}
|
|
1367
1490
|
});
|
|
1368
1491
|
|
|
1369
|
-
// src/commands/
|
|
1370
|
-
import
|
|
1371
|
-
import { readFile as readFile6, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1372
|
-
import path10 from "path";
|
|
1492
|
+
// src/commands/doctor.ts
|
|
1493
|
+
import path19 from "path";
|
|
1373
1494
|
import { defineCommand as defineCommand4 } from "citty";
|
|
1374
1495
|
|
|
1496
|
+
// src/scanner/readiness.ts
|
|
1497
|
+
import { createHash as createHash2 } from "crypto";
|
|
1498
|
+
function action(id, status, recommendation, owner) {
|
|
1499
|
+
return { id, status, recommendation, owner };
|
|
1500
|
+
}
|
|
1501
|
+
function check(id, title, status, essential, evidence, actions = []) {
|
|
1502
|
+
return { id, title, status, essential, evidence, actions };
|
|
1503
|
+
}
|
|
1504
|
+
function pillar(pillarName, checks) {
|
|
1505
|
+
return { pillar: pillarName, checks };
|
|
1506
|
+
}
|
|
1507
|
+
function repositoryFingerprint(scan) {
|
|
1508
|
+
const fingerprintInput = {
|
|
1509
|
+
purpose: scan.purpose,
|
|
1510
|
+
stack: {
|
|
1511
|
+
language: scan.stack.language,
|
|
1512
|
+
framework: scan.stack.framework,
|
|
1513
|
+
packageManager: scan.stack.packageManager,
|
|
1514
|
+
workspaces: scan.stack.workspaces
|
|
1515
|
+
},
|
|
1516
|
+
git: {
|
|
1517
|
+
mode: scan.git.mode,
|
|
1518
|
+
remoteUrl: scan.git.remoteUrl,
|
|
1519
|
+
currentBranch: scan.git.currentBranch,
|
|
1520
|
+
defaultBranch: scan.git.defaultBranch
|
|
1521
|
+
},
|
|
1522
|
+
context: scan.context.sources,
|
|
1523
|
+
agentKitVersion: scan.agentKit.version
|
|
1524
|
+
};
|
|
1525
|
+
return createHash2("sha256").update(JSON.stringify(fingerprintInput)).digest("hex");
|
|
1526
|
+
}
|
|
1527
|
+
function buildPillars(scan) {
|
|
1528
|
+
const agentKitStatus = scan.agentKit.installed ? "ready" : "auto_fix";
|
|
1529
|
+
const purposeStatus = scan.purpose.value === "unknown" ? "needs_choice" : "ready";
|
|
1530
|
+
const gitStatus = scan.git.mode === "none" ? "needs_choice" : "ready";
|
|
1531
|
+
const secretsStatus = scan.safety.trackedSensitiveFiles.length > 0 ? "blocked" : scan.safety.missingSecretPatterns.length > 0 ? "auto_fix" : "ready";
|
|
1532
|
+
const contextStatus = scan.context.sources.length > 0 ? "ready" : "needs_choice";
|
|
1533
|
+
const providerStatus = scan.git.mode === "none" ? "needs_choice" : scan.git.mode === "local-only" ? "ready" : scan.git.providerKind === "unknown" || scan.git.providerKind === "custom" && scan.git.providerConfidence === "low" ? "needs_choice" : "ready";
|
|
1534
|
+
return [
|
|
1535
|
+
pillar("workspace", [
|
|
1536
|
+
check(
|
|
1537
|
+
"workspace.agent-kit",
|
|
1538
|
+
"Agent Kit installation",
|
|
1539
|
+
agentKitStatus,
|
|
1540
|
+
true,
|
|
1541
|
+
scan.agentKit.manifestPath ? [{ source: "file", value: scan.agentKit.manifestPath }] : [],
|
|
1542
|
+
agentKitStatus === "ready" ? [] : [action("install-agent-kit", "auto_fix", "Install the managed L0 inventory", "system")]
|
|
1543
|
+
)
|
|
1544
|
+
]),
|
|
1545
|
+
pillar("purpose-context", [
|
|
1546
|
+
check(
|
|
1547
|
+
"purpose.classification",
|
|
1548
|
+
"Repository purpose",
|
|
1549
|
+
purposeStatus,
|
|
1550
|
+
true,
|
|
1551
|
+
scan.purpose.evidence,
|
|
1552
|
+
purposeStatus === "ready" ? [] : [
|
|
1553
|
+
action(
|
|
1554
|
+
"confirm-repository-purpose",
|
|
1555
|
+
"needs_choice",
|
|
1556
|
+
"Confirm the stable repository purpose",
|
|
1557
|
+
"user"
|
|
1558
|
+
)
|
|
1559
|
+
]
|
|
1560
|
+
),
|
|
1561
|
+
check(
|
|
1562
|
+
"context.sources",
|
|
1563
|
+
"Project context sources",
|
|
1564
|
+
contextStatus,
|
|
1565
|
+
true,
|
|
1566
|
+
scan.context.sources,
|
|
1567
|
+
contextStatus === "ready" ? [] : [
|
|
1568
|
+
action(
|
|
1569
|
+
"identify-context-source",
|
|
1570
|
+
"needs_choice",
|
|
1571
|
+
"Identify the repository source of truth",
|
|
1572
|
+
"user"
|
|
1573
|
+
)
|
|
1574
|
+
]
|
|
1575
|
+
)
|
|
1576
|
+
]),
|
|
1577
|
+
pillar("source-control", [
|
|
1578
|
+
check(
|
|
1579
|
+
"git.repository",
|
|
1580
|
+
"Git repository",
|
|
1581
|
+
gitStatus,
|
|
1582
|
+
true,
|
|
1583
|
+
scan.git.currentBranch ? [{ source: "git", value: `branch:${scan.git.currentBranch}` }] : [],
|
|
1584
|
+
gitStatus === "ready" ? [] : [
|
|
1585
|
+
action(
|
|
1586
|
+
"choose-source-control",
|
|
1587
|
+
"needs_choice",
|
|
1588
|
+
"Choose whether to initialize Git",
|
|
1589
|
+
"user"
|
|
1590
|
+
)
|
|
1591
|
+
]
|
|
1592
|
+
),
|
|
1593
|
+
check(
|
|
1594
|
+
"git.staging-branch",
|
|
1595
|
+
"Staging branch",
|
|
1596
|
+
scan.git.hasLocalStaging || scan.git.hasRemoteStaging ? "ready" : "needs_choice",
|
|
1597
|
+
false,
|
|
1598
|
+
[
|
|
1599
|
+
...scan.git.hasLocalStaging ? [{ source: "git", value: "local branch:staging" }] : [],
|
|
1600
|
+
...scan.git.hasRemoteStaging ? [{ source: "git", value: "remote branch:staging" }] : []
|
|
1601
|
+
],
|
|
1602
|
+
scan.git.hasLocalStaging || scan.git.hasRemoteStaging ? [] : [
|
|
1603
|
+
action(
|
|
1604
|
+
"choose-branch-strategy",
|
|
1605
|
+
"needs_choice",
|
|
1606
|
+
"Confirm the repository promotion strategy",
|
|
1607
|
+
"user"
|
|
1608
|
+
)
|
|
1609
|
+
]
|
|
1610
|
+
)
|
|
1611
|
+
]),
|
|
1612
|
+
pillar("safety", [
|
|
1613
|
+
check(
|
|
1614
|
+
"safety.secrets",
|
|
1615
|
+
"Secrets hygiene",
|
|
1616
|
+
secretsStatus,
|
|
1617
|
+
true,
|
|
1618
|
+
[
|
|
1619
|
+
...scan.safety.ignoredSecretPatterns.map((value) => ({
|
|
1620
|
+
source: "file",
|
|
1621
|
+
value: `.gitignore:${value}`
|
|
1622
|
+
})),
|
|
1623
|
+
...scan.safety.trackedSensitiveFiles.map((value) => ({
|
|
1624
|
+
source: "git",
|
|
1625
|
+
value: `tracked:${value}`
|
|
1626
|
+
}))
|
|
1627
|
+
],
|
|
1628
|
+
secretsStatus === "ready" ? [] : secretsStatus === "blocked" ? [
|
|
1629
|
+
action(
|
|
1630
|
+
"remove-tracked-secrets",
|
|
1631
|
+
"blocked",
|
|
1632
|
+
"Remove tracked sensitive files and rotate exposed values",
|
|
1633
|
+
"user"
|
|
1634
|
+
)
|
|
1635
|
+
] : [
|
|
1636
|
+
action(
|
|
1637
|
+
"merge-secret-ignores",
|
|
1638
|
+
"auto_fix",
|
|
1639
|
+
"Merge required secret patterns into .gitignore",
|
|
1640
|
+
"system"
|
|
1641
|
+
)
|
|
1642
|
+
]
|
|
1643
|
+
)
|
|
1644
|
+
]),
|
|
1645
|
+
pillar("stack-tooling", [
|
|
1646
|
+
check(
|
|
1647
|
+
"stack.detected",
|
|
1648
|
+
"Stack and package manager",
|
|
1649
|
+
scan.stack.language !== "unknown" || scan.purpose.value !== "unknown" ? "ready" : "needs_choice",
|
|
1650
|
+
true,
|
|
1651
|
+
scan.stack.packageManagerEvidence ?? scan.purpose.evidence
|
|
1652
|
+
)
|
|
1653
|
+
]),
|
|
1654
|
+
pillar("quality-ci", [
|
|
1655
|
+
check(
|
|
1656
|
+
"quality.validation",
|
|
1657
|
+
"Tests and validation",
|
|
1658
|
+
scan.quality.hasTests || scan.quality.validationCommands.length > 0 ? "ready" : "manual",
|
|
1659
|
+
false,
|
|
1660
|
+
scan.infra.ciFiles.map((value) => ({ source: "file", value })),
|
|
1661
|
+
scan.quality.hasTests || scan.quality.validationCommands.length > 0 ? [] : [
|
|
1662
|
+
action(
|
|
1663
|
+
"document-validation",
|
|
1664
|
+
"manual",
|
|
1665
|
+
"Document a repeatable repository validation command",
|
|
1666
|
+
"user"
|
|
1667
|
+
)
|
|
1668
|
+
]
|
|
1669
|
+
)
|
|
1670
|
+
]),
|
|
1671
|
+
pillar("deploy-infrastructure", [
|
|
1672
|
+
check(
|
|
1673
|
+
"infrastructure.detected",
|
|
1674
|
+
"Deployment and infrastructure evidence",
|
|
1675
|
+
"ready",
|
|
1676
|
+
false,
|
|
1677
|
+
[...scan.infra.infrastructureFiles, ...scan.infra.deploymentFiles].map((value) => ({
|
|
1678
|
+
source: "file",
|
|
1679
|
+
value
|
|
1680
|
+
}))
|
|
1681
|
+
)
|
|
1682
|
+
]),
|
|
1683
|
+
pillar("collaboration", [
|
|
1684
|
+
check(
|
|
1685
|
+
"collaboration.provider",
|
|
1686
|
+
"Repository provider",
|
|
1687
|
+
providerStatus,
|
|
1688
|
+
false,
|
|
1689
|
+
scan.git.providerEvidence,
|
|
1690
|
+
providerStatus === "ready" ? [] : [
|
|
1691
|
+
action(
|
|
1692
|
+
"confirm-provider",
|
|
1693
|
+
"needs_choice",
|
|
1694
|
+
"Confirm the remote provider or local-only model",
|
|
1695
|
+
"user"
|
|
1696
|
+
)
|
|
1697
|
+
]
|
|
1698
|
+
)
|
|
1699
|
+
]),
|
|
1700
|
+
pillar("agent-kit-personalization", [
|
|
1701
|
+
check(
|
|
1702
|
+
"agent-kit.context",
|
|
1703
|
+
"Agent guidance",
|
|
1704
|
+
scan.context.hasAgentGuidance ? "ready" : "auto_fix",
|
|
1705
|
+
false,
|
|
1706
|
+
scan.context.sources.filter((item) => item.value.includes("guidance")),
|
|
1707
|
+
scan.context.hasAgentGuidance ? [] : [
|
|
1708
|
+
action(
|
|
1709
|
+
"prepare-agent-context",
|
|
1710
|
+
"auto_fix",
|
|
1711
|
+
"Prepare Agent Kit-owned context from detected evidence",
|
|
1712
|
+
"system"
|
|
1713
|
+
)
|
|
1714
|
+
]
|
|
1715
|
+
)
|
|
1716
|
+
])
|
|
1717
|
+
];
|
|
1718
|
+
}
|
|
1719
|
+
function createReadinessReport(scan, options) {
|
|
1720
|
+
const pillars = buildPillars(scan);
|
|
1721
|
+
const checks = pillars.flatMap((item) => item.checks);
|
|
1722
|
+
const statuses = ["ready", "auto_fix", "needs_choice", "manual", "blocked"];
|
|
1723
|
+
const summary = Object.fromEntries(
|
|
1724
|
+
statuses.map((status) => [status, checks.filter((item) => item.status === status).length])
|
|
1725
|
+
);
|
|
1726
|
+
const pendingActions = checks.flatMap((item) => item.actions).filter((item) => item.status !== "ready");
|
|
1727
|
+
const { rootDir: _rootDir, ...portableScan } = scan;
|
|
1728
|
+
return {
|
|
1729
|
+
schemaVersion: 1,
|
|
1730
|
+
generatorVersion: options.generatorVersion,
|
|
1731
|
+
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1732
|
+
repositoryFingerprint: repositoryFingerprint(scan),
|
|
1733
|
+
summary,
|
|
1734
|
+
scan: portableScan,
|
|
1735
|
+
pillars,
|
|
1736
|
+
appliedSafeFixes: [],
|
|
1737
|
+
pendingActions,
|
|
1738
|
+
deferredChecks: []
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/scanner/safe-fixes.ts
|
|
1743
|
+
import { readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
|
|
1744
|
+
import path17 from "path";
|
|
1745
|
+
|
|
1746
|
+
// src/scanner/detect-repository.ts
|
|
1747
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1748
|
+
import path10 from "path";
|
|
1749
|
+
var CONTEXT_PATHS = [
|
|
1750
|
+
["README.md", "README"],
|
|
1751
|
+
["README", "README"],
|
|
1752
|
+
["AGENTS.md", "agent guidance"],
|
|
1753
|
+
["CLAUDE.md", "agent guidance"],
|
|
1754
|
+
[".cursor/rules", "Cursor rules"],
|
|
1755
|
+
["docs/architecture.md", "architecture"],
|
|
1756
|
+
["docs/adr", "architecture decisions"],
|
|
1757
|
+
["docs/runbooks", "runbooks"],
|
|
1758
|
+
["runbooks", "runbooks"],
|
|
1759
|
+
["prompts", "prompts"],
|
|
1760
|
+
["schemas", "schemas"],
|
|
1761
|
+
["sql", "SQL"],
|
|
1762
|
+
["knowledge", "knowledge base"]
|
|
1763
|
+
];
|
|
1764
|
+
async function existingEvidence(rootDir, candidates) {
|
|
1765
|
+
const evidence = await Promise.all(
|
|
1766
|
+
candidates.map(
|
|
1767
|
+
async ([relativePath, label]) => await fileExists(path10.join(rootDir, relativePath)) ? { source: "file", value: `${relativePath}:${label}` } : void 0
|
|
1768
|
+
)
|
|
1769
|
+
);
|
|
1770
|
+
return evidence.flatMap((item) => item ? [item] : []);
|
|
1771
|
+
}
|
|
1772
|
+
async function detectContext(rootDir) {
|
|
1773
|
+
const sources = await existingEvidence(rootDir, CONTEXT_PATHS);
|
|
1774
|
+
const paths = sources.map((item) => item.value.split(":")[0]);
|
|
1775
|
+
return {
|
|
1776
|
+
sources,
|
|
1777
|
+
hasReadme: paths.some((item) => item === "README" || item === "README.md"),
|
|
1778
|
+
hasArchitecture: sources.some((item) => item.value.includes("architecture")),
|
|
1779
|
+
hasRunbooks: sources.some((item) => item.value.includes("runbooks")),
|
|
1780
|
+
hasAgentGuidance: sources.some(
|
|
1781
|
+
(item) => item.value.includes("agent guidance") || item.value.includes("Cursor rules")
|
|
1782
|
+
)
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
async function detectPurpose(rootDir, stack) {
|
|
1786
|
+
const entries = await listDirectory(rootDir);
|
|
1787
|
+
const lowerEntries = entries.map((entry) => entry.toLowerCase());
|
|
1788
|
+
const packageJson = await readJson(path10.join(rootDir, "package.json"));
|
|
1789
|
+
const categories = [];
|
|
1790
|
+
const evidence = [];
|
|
1791
|
+
const add = (category, value2) => {
|
|
1792
|
+
if (!categories.includes(category)) categories.push(category);
|
|
1793
|
+
evidence.push({ source: "file", value: value2 });
|
|
1794
|
+
};
|
|
1795
|
+
if (stack.workspaces) add("monorepo", "workspace configuration");
|
|
1796
|
+
else if (stack.language === "node" && packageJson?.private !== true && (packageJson?.exports !== void 0 || packageJson?.main || packageJson?.types)) {
|
|
1797
|
+
add("library", "package export configuration");
|
|
1798
|
+
} else if (stack.hasProjectFiles) add("application", "application package marker");
|
|
1799
|
+
if (lowerEntries.some((entry) => ["docs", "documentation", "mkdocs.yml"].includes(entry))) {
|
|
1800
|
+
add("documentation", "documentation structure");
|
|
1801
|
+
}
|
|
1802
|
+
if (lowerEntries.some(
|
|
1803
|
+
(entry) => ["knowledge", "content", "wiki", "playbooks", "handbook"].includes(entry)
|
|
1804
|
+
)) {
|
|
1805
|
+
add("knowledge", "knowledge structure");
|
|
1806
|
+
}
|
|
1807
|
+
if (lowerEntries.some(
|
|
1808
|
+
(entry) => ["runbooks", "infra", "terraform", "ansible", "k8s", "kubernetes"].includes(entry)
|
|
1809
|
+
)) {
|
|
1810
|
+
add("operations", "operations structure");
|
|
1811
|
+
}
|
|
1812
|
+
if (lowerEntries.some(
|
|
1813
|
+
(entry) => ["n8n", "workflows", "automations", "prompts", "sql", "schemas"].includes(entry)
|
|
1814
|
+
) || lowerEntries.some((entry) => entry.endsWith(".sql"))) {
|
|
1815
|
+
add("automation", "automation or data artifact");
|
|
1816
|
+
}
|
|
1817
|
+
const meaningfulCategories = categories.filter((category) => category !== "unknown");
|
|
1818
|
+
const value = meaningfulCategories.length === 0 ? "unknown" : meaningfulCategories.length === 1 ? meaningfulCategories[0] ?? "unknown" : "mixed";
|
|
1819
|
+
return {
|
|
1820
|
+
value,
|
|
1821
|
+
categories: meaningfulCategories.length > 0 ? meaningfulCategories : ["unknown"],
|
|
1822
|
+
confidence: evidence.length > 0 ? "high" : "low",
|
|
1823
|
+
evidence
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
async function detectAgentKit(rootDir) {
|
|
1827
|
+
const manifestRelativePath = ".cursor/agent-kit.json";
|
|
1828
|
+
const manifestPath = path10.join(rootDir, manifestRelativePath);
|
|
1829
|
+
const installed = await fileExists(manifestPath);
|
|
1830
|
+
const manifest = installed ? await readJson(manifestPath) : null;
|
|
1831
|
+
return {
|
|
1832
|
+
installed,
|
|
1833
|
+
manifestPath: installed ? manifestRelativePath : void 0,
|
|
1834
|
+
version: manifest?.version,
|
|
1835
|
+
hasPlans: await fileExists(path10.join(rootDir, ".cursor/plans")),
|
|
1836
|
+
hasHandoff: await fileExists(path10.join(rootDir, ".cursor/HANDOFF.md")),
|
|
1837
|
+
hasMemory: await fileExists(path10.join(rootDir, ".cursor/memory"))
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
var REQUIRED_SECRET_PATTERNS = [
|
|
1841
|
+
".env",
|
|
1842
|
+
".env.*",
|
|
1843
|
+
"*.key",
|
|
1844
|
+
"*.pem",
|
|
1845
|
+
"*.p12",
|
|
1846
|
+
"*.pfx",
|
|
1847
|
+
"*credentials*.json",
|
|
1848
|
+
"*service-account*.json"
|
|
1849
|
+
];
|
|
1850
|
+
async function detectSafety(rootDir, trackedFiles) {
|
|
1851
|
+
const gitignorePath = path10.join(rootDir, ".gitignore");
|
|
1852
|
+
const hasGitignore = await fileExists(gitignorePath);
|
|
1853
|
+
const gitignore = hasGitignore ? await readFile6(gitignorePath, "utf8") : "";
|
|
1854
|
+
const lines = gitignore.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1855
|
+
const ignoredSecretPatterns = REQUIRED_SECRET_PATTERNS.filter(
|
|
1856
|
+
(pattern) => lines.includes(pattern)
|
|
1857
|
+
);
|
|
1858
|
+
const trackedSensitiveFiles = trackedFiles.filter(
|
|
1859
|
+
(file) => /(^|\/)(\.env(\..+)?|.*\.(key|pem|p12|pfx)|.*credentials.*\.json)$/i.test(file)
|
|
1860
|
+
);
|
|
1861
|
+
const hookPaths = [".husky", ".git/hooks/pre-commit", "git-hooks/pre-commit"];
|
|
1862
|
+
const hasHooks = (await Promise.all(hookPaths.map((item) => fileExists(path10.join(rootDir, item))))).some(Boolean);
|
|
1863
|
+
const guardCandidates = [
|
|
1864
|
+
".husky/pre-commit",
|
|
1865
|
+
".husky/pre-push",
|
|
1866
|
+
"git-hooks/pre-commit",
|
|
1867
|
+
"git-hooks/pre-push"
|
|
1868
|
+
];
|
|
1869
|
+
const guardContents = await Promise.all(
|
|
1870
|
+
guardCandidates.map(
|
|
1871
|
+
async (item) => await fileExists(path10.join(rootDir, item)) ? readFile6(path10.join(rootDir, item), "utf8") : ""
|
|
1872
|
+
)
|
|
1873
|
+
);
|
|
1874
|
+
return {
|
|
1875
|
+
hasGitignore,
|
|
1876
|
+
ignoredSecretPatterns,
|
|
1877
|
+
missingSecretPatterns: REQUIRED_SECRET_PATTERNS.filter(
|
|
1878
|
+
(pattern) => !ignoredSecretPatterns.includes(pattern)
|
|
1879
|
+
),
|
|
1880
|
+
trackedSensitiveFiles,
|
|
1881
|
+
hasHooks,
|
|
1882
|
+
hasMainBranchGuard: guardContents.some(
|
|
1883
|
+
(content) => content.includes("main") || content.includes("master")
|
|
1884
|
+
)
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// src/scanner/scan.ts
|
|
1889
|
+
import path16 from "path";
|
|
1890
|
+
|
|
1891
|
+
// src/scanner/detect-git.ts
|
|
1892
|
+
import { execFile as execFile2 } from "child_process";
|
|
1893
|
+
import path11 from "path";
|
|
1894
|
+
import { promisify as promisify2 } from "util";
|
|
1895
|
+
var exec = promisify2(execFile2);
|
|
1896
|
+
function remoteHostname(remoteUrl) {
|
|
1897
|
+
const scpMatch = remoteUrl.match(/^[^@]+@([^:]+):/);
|
|
1898
|
+
if (scpMatch?.[1]) return scpMatch[1].toLowerCase();
|
|
1899
|
+
try {
|
|
1900
|
+
return new URL(remoteUrl).hostname.toLowerCase();
|
|
1901
|
+
} catch {
|
|
1902
|
+
return void 0;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function sanitizeRemoteUrl(remoteUrl) {
|
|
1906
|
+
try {
|
|
1907
|
+
const parsed = new URL(remoteUrl);
|
|
1908
|
+
parsed.username = "";
|
|
1909
|
+
parsed.password = "";
|
|
1910
|
+
return parsed.toString();
|
|
1911
|
+
} catch {
|
|
1912
|
+
return remoteUrl;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
async function detectProvider(rootDir, remoteUrl) {
|
|
1916
|
+
const configuration = await readJson(
|
|
1917
|
+
path11.join(rootDir, ".cursor", "agent-kit.config.json")
|
|
1918
|
+
);
|
|
1919
|
+
const configuredProvider = configuration?.git?.provider;
|
|
1920
|
+
if (configuredProvider) {
|
|
1921
|
+
const configuredKind = configuration.git?.providerKind ?? (configuredProvider === "github" ? "github" : configuredProvider === "gitlab" ? remoteUrl && remoteHostname(remoteUrl) === "gitlab.com" ? "gitlab-saas" : "gitlab-self-hosted" : configuredProvider === "other" ? "custom" : "known-other");
|
|
1922
|
+
return {
|
|
1923
|
+
provider: configuredProvider,
|
|
1924
|
+
providerKind: configuredKind,
|
|
1925
|
+
confidence: "high",
|
|
1926
|
+
evidence: [{ source: "configuration", value: ".cursor/agent-kit.config.json#git.provider" }]
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
if (!remoteUrl) {
|
|
1930
|
+
return { providerKind: "unknown", confidence: "low", evidence: [] };
|
|
1931
|
+
}
|
|
1932
|
+
const hostname = remoteHostname(remoteUrl);
|
|
1933
|
+
const remoteEvidence = [{ source: "git", value: `remote:${remoteUrl}` }];
|
|
1934
|
+
if (hostname === "github.com") {
|
|
1935
|
+
return {
|
|
1936
|
+
provider: "github",
|
|
1937
|
+
providerKind: "github",
|
|
1938
|
+
confidence: "high",
|
|
1939
|
+
evidence: remoteEvidence
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
if (hostname === "gitlab.com") {
|
|
1943
|
+
return {
|
|
1944
|
+
provider: "gitlab",
|
|
1945
|
+
providerKind: "gitlab-saas",
|
|
1946
|
+
confidence: "high",
|
|
1947
|
+
evidence: remoteEvidence
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
if (hostname === "bitbucket.org") {
|
|
1951
|
+
return {
|
|
1952
|
+
provider: "bitbucket",
|
|
1953
|
+
providerKind: "known-other",
|
|
1954
|
+
confidence: "high",
|
|
1955
|
+
evidence: remoteEvidence
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
if (hostname === "dev.azure.com" || hostname?.endsWith(".visualstudio.com")) {
|
|
1959
|
+
return {
|
|
1960
|
+
provider: "azure-devops",
|
|
1961
|
+
providerKind: "known-other",
|
|
1962
|
+
confidence: "high",
|
|
1963
|
+
evidence: remoteEvidence
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
if (hostname === "codeberg.org") {
|
|
1967
|
+
return {
|
|
1968
|
+
provider: "gitea",
|
|
1969
|
+
providerKind: "known-other",
|
|
1970
|
+
confidence: "high",
|
|
1971
|
+
evidence: remoteEvidence
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
if (await fileExists(path11.join(rootDir, ".gitlab-ci.yml"))) {
|
|
1975
|
+
return {
|
|
1976
|
+
provider: "gitlab",
|
|
1977
|
+
providerKind: "gitlab-self-hosted",
|
|
1978
|
+
confidence: "medium",
|
|
1979
|
+
evidence: [...remoteEvidence, { source: "file", value: ".gitlab-ci.yml" }]
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
return {
|
|
1983
|
+
provider: "other",
|
|
1984
|
+
providerKind: hostname ? "custom" : "unknown",
|
|
1985
|
+
confidence: "low",
|
|
1986
|
+
evidence: remoteEvidence
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
function inferWorkflow(currentBranch) {
|
|
1990
|
+
if (!currentBranch) return "unknown";
|
|
1991
|
+
if (currentBranch === "main" || currentBranch === "master") return "feature-pr";
|
|
1992
|
+
if (currentBranch.includes("develop") || currentBranch.includes("release")) return "gitflow";
|
|
1993
|
+
if (currentBranch.includes("staging") || currentBranch.includes("homolog")) return "homolog-prod";
|
|
1994
|
+
return "feature-pr";
|
|
1995
|
+
}
|
|
1996
|
+
async function runGit(args, rootDir) {
|
|
1997
|
+
try {
|
|
1998
|
+
const { stdout } = await exec("git", args, { cwd: rootDir });
|
|
1999
|
+
return stdout.trim();
|
|
2000
|
+
} catch {
|
|
2001
|
+
return void 0;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
async function listTrackedFiles(rootDir) {
|
|
2005
|
+
return (await runGit(["ls-files"], rootDir))?.split("\n").filter(Boolean) ?? [];
|
|
2006
|
+
}
|
|
2007
|
+
async function detectGit(rootDir) {
|
|
2008
|
+
const isGit = await runGit(["rev-parse", "--is-inside-work-tree"], rootDir) === "true";
|
|
2009
|
+
if (!isGit) {
|
|
2010
|
+
return {
|
|
2011
|
+
providerKind: "unknown",
|
|
2012
|
+
providerConfidence: "low",
|
|
2013
|
+
providerEvidence: [],
|
|
2014
|
+
remotes: [],
|
|
2015
|
+
mode: "none",
|
|
2016
|
+
workflow: "unknown",
|
|
2017
|
+
isDirty: false,
|
|
2018
|
+
hasLocalStaging: false,
|
|
2019
|
+
hasRemoteStaging: false
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
const remoteNames = (await runGit(["remote"], rootDir))?.split("\n").filter(Boolean) ?? [];
|
|
2023
|
+
const remotes = (await Promise.all(
|
|
2024
|
+
remoteNames.map(async (name) => {
|
|
2025
|
+
const url = await runGit(["remote", "get-url", name], rootDir);
|
|
2026
|
+
return url ? { name, url: sanitizeRemoteUrl(url) } : void 0;
|
|
2027
|
+
})
|
|
2028
|
+
)).filter((remote) => remote !== void 0);
|
|
2029
|
+
const primary = remotes.find((remote) => remote.name === "origin") ?? remotes[0];
|
|
2030
|
+
const remoteUrl = primary?.url;
|
|
2031
|
+
const [currentBranch, remoteHead, localBranchOutput, remoteBranchOutput, status, provider] = await Promise.all([
|
|
2032
|
+
runGit(["branch", "--show-current"], rootDir),
|
|
2033
|
+
primary ? runGit(["symbolic-ref", "--short", `refs/remotes/${primary.name}/HEAD`], rootDir) : Promise.resolve(void 0),
|
|
2034
|
+
runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], rootDir),
|
|
2035
|
+
runGit(["for-each-ref", "--format=%(refname:short)", "refs/remotes"], rootDir),
|
|
2036
|
+
runGit(["status", "--porcelain"], rootDir),
|
|
2037
|
+
detectProvider(rootDir, remoteUrl)
|
|
2038
|
+
]);
|
|
2039
|
+
const localBranches = localBranchOutput?.split("\n").filter(Boolean);
|
|
2040
|
+
const remoteBranches = remoteBranchOutput?.split("\n").filter(Boolean);
|
|
2041
|
+
const defaultBranch = remoteHead?.split("/").slice(1).join("/") || (localBranches?.includes("main") ? "main" : localBranches?.includes("master") ? "master" : void 0);
|
|
2042
|
+
return {
|
|
2043
|
+
provider: provider.provider,
|
|
2044
|
+
providerKind: provider.providerKind,
|
|
2045
|
+
providerConfidence: provider.confidence,
|
|
2046
|
+
providerEvidence: provider.evidence,
|
|
2047
|
+
remoteUrl,
|
|
2048
|
+
remoteName: primary?.name,
|
|
2049
|
+
remotes,
|
|
2050
|
+
mode: remotes.length > 0 ? "remote-hosted" : "local-only",
|
|
2051
|
+
currentBranch,
|
|
2052
|
+
defaultBranch,
|
|
2053
|
+
isDirty: Boolean(status),
|
|
2054
|
+
hasLocalStaging: localBranches?.includes("staging") ?? false,
|
|
2055
|
+
hasRemoteStaging: remoteBranches?.some(
|
|
2056
|
+
(branch) => branch === "origin/staging" || branch.endsWith("/staging")
|
|
2057
|
+
) ?? false,
|
|
2058
|
+
workflow: inferWorkflow(currentBranch)
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/scanner/detect-ide.ts
|
|
2063
|
+
import path12 from "path";
|
|
2064
|
+
async function detectIde(rootDir) {
|
|
2065
|
+
const hasCursor = await fileExists(path12.join(rootDir, ".cursor"));
|
|
2066
|
+
const hasVSCode = await fileExists(path12.join(rootDir, ".vscode"));
|
|
2067
|
+
const hasWindsurf = await fileExists(path12.join(rootDir, ".windsurfrules"));
|
|
2068
|
+
if (hasCursor) return { ide: "cursor", plan: "cursor-pro" };
|
|
2069
|
+
if (hasVSCode) return { ide: "vscode", plan: "vscode-pro" };
|
|
2070
|
+
if (hasWindsurf) return { ide: "windsurf", plan: "windsurf" };
|
|
2071
|
+
return { ide: "unknown", plan: "default" };
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/scanner/detect-infra.ts
|
|
2075
|
+
import path13 from "path";
|
|
2076
|
+
|
|
1375
2077
|
// src/types.ts
|
|
1376
|
-
var GIT_PLATFORM_META = {
|
|
1377
|
-
github: {
|
|
1378
|
-
cli: "gh",
|
|
1379
|
-
prTerm: "Pull Request",
|
|
1380
|
-
prCommand: "gh pr create",
|
|
1381
|
-
ciDefault: "github-actions"
|
|
1382
|
-
},
|
|
1383
|
-
gitlab: {
|
|
1384
|
-
cli: "glab",
|
|
1385
|
-
prTerm: "Merge Request",
|
|
1386
|
-
prCommand: "glab mr create",
|
|
1387
|
-
ciDefault: "gitlab-ci"
|
|
1388
|
-
},
|
|
1389
|
-
bitbucket: {
|
|
1390
|
-
cli: "bb",
|
|
1391
|
-
prTerm: "Pull Request",
|
|
1392
|
-
prCommand: "bb pr create",
|
|
1393
|
-
ciDefault: "bitbucket-pipelines"
|
|
1394
|
-
},
|
|
1395
|
-
"azure-devops": {
|
|
1396
|
-
cli: "az repos",
|
|
1397
|
-
prTerm: "Pull Request",
|
|
1398
|
-
prCommand: "az repos pr create",
|
|
1399
|
-
ciDefault: "azure-pipelines"
|
|
1400
|
-
},
|
|
1401
|
-
gitea: { cli: "tea", prTerm: "Pull Request", prCommand: "tea pr create", ciDefault: "none" }
|
|
1402
|
-
};
|
|
1403
2078
|
var CI_PLATFORM_FILES = {
|
|
1404
2079
|
"github-actions": ".github/workflows",
|
|
1405
2080
|
"gitlab-ci": ".gitlab-ci.yml",
|
|
@@ -1424,48 +2099,625 @@ var PM_TOOL_LABELS = {
|
|
|
1424
2099
|
none: "None"
|
|
1425
2100
|
};
|
|
1426
2101
|
|
|
1427
|
-
// src/
|
|
1428
|
-
function
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
todos.push({ id: m[1], content: m[2].replace(/^["']|["']$/g, ""), status: m[3] });
|
|
2102
|
+
// src/scanner/detect-infra.ts
|
|
2103
|
+
async function detectInfra(rootDir) {
|
|
2104
|
+
const docker = await fileExists(path13.join(rootDir, "Dockerfile")) || await fileExists(path13.join(rootDir, "docker-compose.yml")) || await fileExists(path13.join(rootDir, "docker-compose.yaml"));
|
|
2105
|
+
const kubernetes = await fileExists(path13.join(rootDir, "k8s")) || await fileExists(path13.join(rootDir, "kubernetes"));
|
|
2106
|
+
let ci = "none";
|
|
2107
|
+
const ciFiles = [];
|
|
2108
|
+
for (const [platform, filePath] of Object.entries(CI_PLATFORM_FILES)) {
|
|
2109
|
+
if (await fileExists(path13.join(rootDir, filePath))) {
|
|
2110
|
+
if (ci === "none") ci = platform;
|
|
2111
|
+
ciFiles.push(filePath);
|
|
1438
2112
|
}
|
|
1439
2113
|
}
|
|
1440
|
-
|
|
2114
|
+
const infrastructureCandidates = [
|
|
2115
|
+
"Dockerfile",
|
|
2116
|
+
"docker-compose.yml",
|
|
2117
|
+
"docker-compose.yaml",
|
|
2118
|
+
"k8s",
|
|
2119
|
+
"kubernetes",
|
|
2120
|
+
"terraform",
|
|
2121
|
+
"infra"
|
|
2122
|
+
];
|
|
2123
|
+
const deploymentCandidates = [
|
|
2124
|
+
"vercel.json",
|
|
2125
|
+
"netlify.toml",
|
|
2126
|
+
"fly.toml",
|
|
2127
|
+
"render.yaml",
|
|
2128
|
+
"Procfile",
|
|
2129
|
+
"deploy",
|
|
2130
|
+
"scripts/deploy.sh"
|
|
2131
|
+
];
|
|
2132
|
+
const infrastructureFiles = (await Promise.all(
|
|
2133
|
+
infrastructureCandidates.map(
|
|
2134
|
+
async (file) => await fileExists(path13.join(rootDir, file)) ? file : void 0
|
|
2135
|
+
)
|
|
2136
|
+
)).filter((file) => file !== void 0);
|
|
2137
|
+
const deploymentFiles = (await Promise.all(
|
|
2138
|
+
deploymentCandidates.map(
|
|
2139
|
+
async (file) => await fileExists(path13.join(rootDir, file)) ? file : void 0
|
|
2140
|
+
)
|
|
2141
|
+
)).filter((file) => file !== void 0);
|
|
2142
|
+
return { docker, kubernetes, ci, ciFiles, infrastructureFiles, deploymentFiles };
|
|
1441
2143
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
2144
|
+
|
|
2145
|
+
// src/scanner/detect-services.ts
|
|
2146
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2147
|
+
import path14 from "path";
|
|
2148
|
+
async function detectProjectManagement(rootDir) {
|
|
2149
|
+
const tools = [];
|
|
2150
|
+
const mcpConfigPaths = [
|
|
2151
|
+
path14.join(rootDir, ".cursor", "mcp.json"),
|
|
2152
|
+
path14.join(rootDir, "mcp.json")
|
|
2153
|
+
];
|
|
2154
|
+
for (const configPath of mcpConfigPaths) {
|
|
2155
|
+
if (!await fileExists(configPath)) continue;
|
|
2156
|
+
try {
|
|
2157
|
+
const raw = await readFile7(configPath, "utf8");
|
|
2158
|
+
const lower = raw.toLowerCase();
|
|
2159
|
+
if (lower.includes("clickup")) tools.push("clickup");
|
|
2160
|
+
if (lower.includes("jira") || lower.includes("atlassian")) tools.push("jira");
|
|
2161
|
+
if (lower.includes("linear")) tools.push("linear");
|
|
2162
|
+
if (lower.includes("asana")) tools.push("asana");
|
|
2163
|
+
if (lower.includes("youtrack")) tools.push("youtrack");
|
|
2164
|
+
if (lower.includes("shortcut")) tools.push("shortcut");
|
|
2165
|
+
} catch {
|
|
1450
2166
|
}
|
|
1451
2167
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
async function loadProfile(rootDir) {
|
|
1458
|
-
const configPath = path10.join(rootDir, ".cursor", "agent-kit.config.json");
|
|
1459
|
-
try {
|
|
1460
|
-
return await readJson(configPath);
|
|
1461
|
-
} catch {
|
|
1462
|
-
return null;
|
|
2168
|
+
if (await fileExists(path14.join(rootDir, ".github", "ISSUE_TEMPLATE"))) {
|
|
2169
|
+
tools.push("github-issues");
|
|
2170
|
+
}
|
|
2171
|
+
if (await fileExists(path14.join(rootDir, ".github", "projects"))) {
|
|
2172
|
+
tools.push("github-projects");
|
|
1463
2173
|
}
|
|
2174
|
+
return [...new Set(tools)];
|
|
1464
2175
|
}
|
|
1465
|
-
function
|
|
1466
|
-
const
|
|
1467
|
-
const
|
|
1468
|
-
|
|
2176
|
+
async function detectServices(rootDir) {
|
|
2177
|
+
const hasPrisma = await fileExists(path14.join(rootDir, "prisma/schema.prisma"));
|
|
2178
|
+
const hasSequelize = await fileExists(path14.join(rootDir, "sequelize"));
|
|
2179
|
+
const hasDrizzle = await fileExists(path14.join(rootDir, "drizzle.config.ts"));
|
|
2180
|
+
const hasKnex = await fileExists(path14.join(rootDir, "knexfile.ts"));
|
|
2181
|
+
const hasTypeorm = await fileExists(path14.join(rootDir, "ormconfig.json"));
|
|
2182
|
+
const database = hasPrisma || hasSequelize || hasDrizzle || hasKnex || hasTypeorm ? "postgresql" : void 0;
|
|
2183
|
+
const orm = hasPrisma ? "prisma" : hasDrizzle ? "drizzle" : hasSequelize ? "sequelize" : hasKnex ? "knex" : hasTypeorm ? "typeorm" : void 0;
|
|
2184
|
+
const projectManagement = await detectProjectManagement(rootDir);
|
|
2185
|
+
return {
|
|
2186
|
+
database,
|
|
2187
|
+
orm,
|
|
2188
|
+
projectManagement: projectManagement.length > 0 ? projectManagement : void 0
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// src/scanner/detect-stack.ts
|
|
2193
|
+
import path15 from "path";
|
|
2194
|
+
var PROJECT_MARKERS = [
|
|
2195
|
+
"package.json",
|
|
2196
|
+
"requirements.txt",
|
|
2197
|
+
"pyproject.toml",
|
|
2198
|
+
"go.mod",
|
|
2199
|
+
"Gemfile",
|
|
2200
|
+
"pom.xml",
|
|
2201
|
+
"build.gradle",
|
|
2202
|
+
"composer.json",
|
|
2203
|
+
"Cargo.toml",
|
|
2204
|
+
"pnpm-workspace.yaml"
|
|
2205
|
+
];
|
|
2206
|
+
var LOCKFILES = [
|
|
2207
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
2208
|
+
["yarn.lock", "yarn"],
|
|
2209
|
+
["bun.lockb", "bun"],
|
|
2210
|
+
["bun.lock", "bun"],
|
|
2211
|
+
["package-lock.json", "npm"]
|
|
2212
|
+
];
|
|
2213
|
+
async function detectPackageManager(rootDir, packageJson) {
|
|
2214
|
+
const configured = packageJson.packageManager?.split("@")[0];
|
|
2215
|
+
if (configured && ["npm", "pnpm", "yarn", "bun"].includes(configured)) {
|
|
2216
|
+
return {
|
|
2217
|
+
packageManager: configured,
|
|
2218
|
+
evidence: [{ source: "configuration", value: `package.json#packageManager=${configured}` }]
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
for (const [lockfile, packageManager] of LOCKFILES) {
|
|
2222
|
+
if (await fileExists(path15.join(rootDir, lockfile))) {
|
|
2223
|
+
return {
|
|
2224
|
+
packageManager,
|
|
2225
|
+
evidence: [{ source: "file", value: lockfile }]
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return { evidence: [] };
|
|
2230
|
+
}
|
|
2231
|
+
function commandsForScripts(scripts, packageManager) {
|
|
2232
|
+
const runner = packageManager ?? "npm";
|
|
2233
|
+
const command = (name) => runner === "npm" ? `npm run ${name}` : `${runner} ${name}`;
|
|
2234
|
+
const testCommands = Object.keys(scripts).filter((name) => name === "test" || name.startsWith("test:")).map(command);
|
|
2235
|
+
const validationCommands = ["lint", "typecheck", "check", "build"].filter((name) => scripts[name]).map(command);
|
|
2236
|
+
return { testCommands, validationCommands };
|
|
2237
|
+
}
|
|
2238
|
+
async function detectStack(rootDir) {
|
|
2239
|
+
const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path15.join(rootDir, item))))).some(Boolean);
|
|
2240
|
+
const hasPackageJson = await fileExists(path15.join(rootDir, "package.json"));
|
|
2241
|
+
if (hasPackageJson) {
|
|
2242
|
+
const packageJson = await readJson(path15.join(rootDir, "package.json")) ?? {};
|
|
2243
|
+
const scripts = packageJson.scripts ?? {};
|
|
2244
|
+
const packageManager = await detectPackageManager(rootDir, packageJson);
|
|
2245
|
+
const commands = commandsForScripts(scripts, packageManager.packageManager);
|
|
2246
|
+
const hasNextConfig = await fileExists(path15.join(rootDir, "next.config.js")) || await fileExists(path15.join(rootDir, "next.config.mjs")) || await fileExists(path15.join(rootDir, "next.config.ts"));
|
|
2247
|
+
const hasNestConfig = await fileExists(path15.join(rootDir, "nest-cli.json"));
|
|
2248
|
+
return {
|
|
2249
|
+
language: "node",
|
|
2250
|
+
framework: hasNextConfig ? "nextjs" : hasNestConfig ? "nestjs" : "node",
|
|
2251
|
+
packageManager: packageManager.packageManager,
|
|
2252
|
+
packageManagerEvidence: packageManager.evidence,
|
|
2253
|
+
scripts,
|
|
2254
|
+
workspaces: packageJson.workspaces !== void 0 || await fileExists(path15.join(rootDir, "pnpm-workspace.yaml")),
|
|
2255
|
+
...commands,
|
|
2256
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
if (await fileExists(path15.join(rootDir, "pyproject.toml"))) {
|
|
2260
|
+
return {
|
|
2261
|
+
language: "python",
|
|
2262
|
+
framework: "python",
|
|
2263
|
+
workspaces: false,
|
|
2264
|
+
testCommands: [],
|
|
2265
|
+
validationCommands: [],
|
|
2266
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
if (await fileExists(path15.join(rootDir, "go.mod"))) {
|
|
2270
|
+
return {
|
|
2271
|
+
language: "go",
|
|
2272
|
+
framework: "go",
|
|
2273
|
+
workspaces: false,
|
|
2274
|
+
testCommands: ["go test ./..."],
|
|
2275
|
+
validationCommands: [],
|
|
2276
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
if (await fileExists(path15.join(rootDir, "Cargo.toml"))) {
|
|
2280
|
+
return {
|
|
2281
|
+
language: "rust",
|
|
2282
|
+
framework: "rust",
|
|
2283
|
+
workspaces: false,
|
|
2284
|
+
testCommands: ["cargo test"],
|
|
2285
|
+
validationCommands: ["cargo check"],
|
|
2286
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
if (await fileExists(path15.join(rootDir, "composer.json"))) {
|
|
2290
|
+
return {
|
|
2291
|
+
language: "php",
|
|
2292
|
+
framework: "php",
|
|
2293
|
+
workspaces: false,
|
|
2294
|
+
testCommands: [],
|
|
2295
|
+
validationCommands: [],
|
|
2296
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
return {
|
|
2300
|
+
language: "unknown",
|
|
2301
|
+
workspaces: false,
|
|
2302
|
+
testCommands: [],
|
|
2303
|
+
validationCommands: [],
|
|
2304
|
+
hasProjectFiles: hasAnyProjectMarker
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/scanner/scan.ts
|
|
2309
|
+
var GREENFIELD_SAFE_FILES = /* @__PURE__ */ new Set([
|
|
2310
|
+
".git",
|
|
2311
|
+
".gitignore",
|
|
2312
|
+
"LICENSE",
|
|
2313
|
+
"README.md",
|
|
2314
|
+
".cursor",
|
|
2315
|
+
".vscode"
|
|
2316
|
+
]);
|
|
2317
|
+
function isGreenfieldByEntries(entries) {
|
|
2318
|
+
const meaningful = entries.filter((entry) => !GREENFIELD_SAFE_FILES.has(entry));
|
|
2319
|
+
return meaningful.length === 0;
|
|
2320
|
+
}
|
|
2321
|
+
async function runScanner(rootDir) {
|
|
2322
|
+
const normalizedRoot = path16.resolve(rootDir);
|
|
2323
|
+
const entries = await listDirectory(normalizedRoot);
|
|
2324
|
+
const stack = await detectStack(normalizedRoot);
|
|
2325
|
+
const purpose = await detectPurpose(normalizedRoot, stack);
|
|
2326
|
+
const [git, ide, infra, services, context, agentKit, trackedFiles] = await Promise.all([
|
|
2327
|
+
detectGit(normalizedRoot),
|
|
2328
|
+
detectIde(normalizedRoot),
|
|
2329
|
+
detectInfra(normalizedRoot),
|
|
2330
|
+
detectServices(normalizedRoot),
|
|
2331
|
+
detectContext(normalizedRoot),
|
|
2332
|
+
detectAgentKit(normalizedRoot),
|
|
2333
|
+
listTrackedFiles(normalizedRoot)
|
|
2334
|
+
]);
|
|
2335
|
+
const safety = await detectSafety(normalizedRoot, trackedFiles);
|
|
2336
|
+
const isGreenfield = isGreenfieldByEntries(entries) && purpose.value === "unknown";
|
|
2337
|
+
return {
|
|
2338
|
+
rootDir: normalizedRoot,
|
|
2339
|
+
isGreenfield,
|
|
2340
|
+
purpose,
|
|
2341
|
+
stack,
|
|
2342
|
+
git,
|
|
2343
|
+
ide,
|
|
2344
|
+
infra,
|
|
2345
|
+
services,
|
|
2346
|
+
context,
|
|
2347
|
+
agentKit,
|
|
2348
|
+
safety,
|
|
2349
|
+
quality: {
|
|
2350
|
+
testCommands: stack.testCommands,
|
|
2351
|
+
validationCommands: stack.validationCommands,
|
|
2352
|
+
ci: infra.ci,
|
|
2353
|
+
hasTests: stack.testCommands.length > 0
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/scanner/safe-fixes.ts
|
|
2359
|
+
var PROFILE_RELATIVE_PATH = ".cursor/agent-kit.config.json";
|
|
2360
|
+
var CONTEXT_CONFIG_RELATIVE_PATH = ".cursor/context/config.json";
|
|
2361
|
+
var ESSENTIAL_DIRECTORIES = [
|
|
2362
|
+
".cursor",
|
|
2363
|
+
".cursor/context",
|
|
2364
|
+
".cursor/context/current",
|
|
2365
|
+
".cursor/context/backups",
|
|
2366
|
+
".cursor/plans",
|
|
2367
|
+
".cursor/memory"
|
|
2368
|
+
];
|
|
2369
|
+
function isObject(value) {
|
|
2370
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2371
|
+
}
|
|
2372
|
+
function mergeMissing(existing, defaults) {
|
|
2373
|
+
if (!isObject(existing) || !isObject(defaults)) return existing ?? defaults;
|
|
2374
|
+
const merged = { ...existing };
|
|
2375
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
2376
|
+
merged[key] = key in existing ? mergeMissing(existing[key], value) : value;
|
|
2377
|
+
}
|
|
2378
|
+
return merged;
|
|
2379
|
+
}
|
|
2380
|
+
function jsonEqual(left, right) {
|
|
2381
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2382
|
+
}
|
|
2383
|
+
function relativeEvidence(relativePath, detail) {
|
|
2384
|
+
return [`${relativePath}: ${detail}`];
|
|
2385
|
+
}
|
|
2386
|
+
function createProfile(scan, report, generatedAt) {
|
|
2387
|
+
const git = {
|
|
2388
|
+
mode: scan.git.mode,
|
|
2389
|
+
workflow: scan.git.workflow,
|
|
2390
|
+
remotes: scan.git.remotes,
|
|
2391
|
+
remoteUrl: scan.git.remoteUrl,
|
|
2392
|
+
remoteName: scan.git.remoteName,
|
|
2393
|
+
currentBranch: scan.git.currentBranch,
|
|
2394
|
+
defaultBranch: scan.git.defaultBranch,
|
|
2395
|
+
isDirty: scan.git.isDirty,
|
|
2396
|
+
hasLocalStaging: scan.git.hasLocalStaging,
|
|
2397
|
+
hasRemoteStaging: scan.git.hasRemoteStaging
|
|
2398
|
+
};
|
|
2399
|
+
if (scan.git.providerConfidence === "high") {
|
|
2400
|
+
git.provider = scan.git.provider;
|
|
2401
|
+
git.providerKind = scan.git.providerKind;
|
|
2402
|
+
git.providerConfidence = scan.git.providerConfidence;
|
|
2403
|
+
git.providerEvidence = scan.git.providerEvidence;
|
|
2404
|
+
}
|
|
2405
|
+
return {
|
|
2406
|
+
schemaVersion: 1,
|
|
2407
|
+
contractVersion: 1,
|
|
2408
|
+
purpose: scan.purpose,
|
|
2409
|
+
stack: scan.stack,
|
|
2410
|
+
git,
|
|
2411
|
+
infra: scan.infra,
|
|
2412
|
+
services: scan.services,
|
|
2413
|
+
context: scan.context,
|
|
2414
|
+
detection: {
|
|
2415
|
+
generatedAt,
|
|
2416
|
+
repositoryFingerprint: report.repositoryFingerprint,
|
|
2417
|
+
providerConfidence: scan.git.providerConfidence,
|
|
2418
|
+
providerEvidence: scan.git.providerEvidence
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
function createOnboardingState(report, generatedAt, deferredItems = report.deferredChecks) {
|
|
2423
|
+
const validDeferredCheckIds = new Set(
|
|
2424
|
+
deferredItems.filter((item) => item.reason.trim().length > 0).map((item) => item.checkId)
|
|
2425
|
+
);
|
|
2426
|
+
const unresolvedEssential = report.pillars.flatMap((pillar2) => pillar2.checks).some(
|
|
2427
|
+
(check2) => check2.essential && check2.status !== "ready" && (check2.status === "blocked" || !validDeferredCheckIds.has(check2.id))
|
|
2428
|
+
);
|
|
2429
|
+
return {
|
|
2430
|
+
contractVersion: 1,
|
|
2431
|
+
status: unresolvedEssential ? "in_progress" : "completed",
|
|
2432
|
+
updatedAt: generatedAt,
|
|
2433
|
+
checks: Object.fromEntries(
|
|
2434
|
+
report.pillars.flatMap((pillar2) => pillar2.checks).map((check2) => [
|
|
2435
|
+
check2.id,
|
|
2436
|
+
{
|
|
2437
|
+
status: check2.status,
|
|
2438
|
+
essential: check2.essential,
|
|
2439
|
+
evidence: check2.evidence
|
|
2440
|
+
}
|
|
2441
|
+
])
|
|
2442
|
+
),
|
|
2443
|
+
deferredItems
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
function validDeferredItems(value, fallback) {
|
|
2447
|
+
if (!Array.isArray(value)) return fallback;
|
|
2448
|
+
return value.filter(
|
|
2449
|
+
(item) => isObject(item) && typeof item.checkId === "string" && typeof item.reason === "string" && (item.recoveryCommand === void 0 || typeof item.recoveryCommand === "string")
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
function reconcileOnboardingState(report, existingConfig, generatedAt) {
|
|
2453
|
+
const existing = isObject(existingConfig.onboarding) ? existingConfig.onboarding : {};
|
|
2454
|
+
const deferredItems = validDeferredItems(existing.deferredItems, report.deferredChecks);
|
|
2455
|
+
const derived = createOnboardingState(report, generatedAt, deferredItems);
|
|
2456
|
+
const candidate2 = {
|
|
2457
|
+
...existing,
|
|
2458
|
+
...derived
|
|
2459
|
+
};
|
|
2460
|
+
const { updatedAt: _candidateUpdatedAt, ...candidateState } = candidate2;
|
|
2461
|
+
const { updatedAt: existingUpdatedAt, ...existingState } = existing;
|
|
2462
|
+
candidate2.updatedAt = jsonEqual(candidateState, existingState) && typeof existingUpdatedAt === "string" ? existingUpdatedAt : generatedAt;
|
|
2463
|
+
return candidate2;
|
|
2464
|
+
}
|
|
2465
|
+
function preferenceDefaults(onboarding, onboarded) {
|
|
2466
|
+
return {
|
|
2467
|
+
onboarded: onboarded === true,
|
|
2468
|
+
onboarding,
|
|
2469
|
+
externalPlanReview: {
|
|
2470
|
+
enabled: false,
|
|
2471
|
+
backend: "claude",
|
|
2472
|
+
autoRemediate: false,
|
|
2473
|
+
offerOnExhausted: true
|
|
2474
|
+
},
|
|
2475
|
+
autoHandoff: false,
|
|
2476
|
+
workspaceSkin: {
|
|
2477
|
+
default: "autopilot",
|
|
2478
|
+
modes: {
|
|
2479
|
+
"continue-plan": "autopilot",
|
|
2480
|
+
"run-plan": "night-shift",
|
|
2481
|
+
"cli-run-plan": "ghost-runner"
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
function mergeSecretIgnores(existing) {
|
|
2487
|
+
const activeLines = new Set(
|
|
2488
|
+
existing.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"))
|
|
2489
|
+
);
|
|
2490
|
+
const missing = REQUIRED_SECRET_PATTERNS.filter((pattern) => !activeLines.has(pattern));
|
|
2491
|
+
if (missing.length === 0) return existing;
|
|
2492
|
+
const prefix = existing.length === 0 ? "" : existing.endsWith("\n") ? existing : `${existing}
|
|
2493
|
+
`;
|
|
2494
|
+
return `${prefix}${missing.join("\n")}
|
|
2495
|
+
`;
|
|
2496
|
+
}
|
|
2497
|
+
function recordChange(changes, id, relativePath, changed, dryRun, evidence) {
|
|
2498
|
+
changes.push({
|
|
2499
|
+
id,
|
|
2500
|
+
path: relativePath,
|
|
2501
|
+
status: changed ? dryRun ? "planned" : "applied" : "skipped",
|
|
2502
|
+
evidence
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
function appliedActions(changes) {
|
|
2506
|
+
return changes.filter((change) => change.status === "applied").map((change) => ({
|
|
2507
|
+
id: change.id,
|
|
2508
|
+
status: "ready",
|
|
2509
|
+
recommendation: `Applied safe local change to ${change.path}`,
|
|
2510
|
+
owner: "system"
|
|
2511
|
+
}));
|
|
2512
|
+
}
|
|
2513
|
+
async function executeSafeReadinessFixes(rootDir, options) {
|
|
2514
|
+
const dryRun = options.dryRun ?? false;
|
|
2515
|
+
const generatedAt = options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2516
|
+
const beforeScan = await runScanner(rootDir);
|
|
2517
|
+
const before = createReadinessReport(beforeScan, {
|
|
2518
|
+
generatorVersion: options.generatorVersion,
|
|
2519
|
+
generatedAt
|
|
2520
|
+
});
|
|
2521
|
+
const changes = [];
|
|
2522
|
+
for (const relativePath of ESSENTIAL_DIRECTORIES) {
|
|
2523
|
+
const absolutePath = path17.join(beforeScan.rootDir, relativePath);
|
|
2524
|
+
const exists = await fileExists(absolutePath);
|
|
2525
|
+
if (!exists && !dryRun) await ensureDir(absolutePath);
|
|
2526
|
+
recordChange(
|
|
2527
|
+
changes,
|
|
2528
|
+
"ensure-agent-kit-directory",
|
|
2529
|
+
relativePath,
|
|
2530
|
+
!exists,
|
|
2531
|
+
dryRun,
|
|
2532
|
+
relativeEvidence(relativePath, exists ? "already exists" : "missing directory")
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const gitignoreRelativePath = ".gitignore";
|
|
2536
|
+
const gitignorePath = path17.join(beforeScan.rootDir, gitignoreRelativePath);
|
|
2537
|
+
const existingGitignore = await fileExists(gitignorePath) ? await readFile8(gitignorePath, "utf8") : "";
|
|
2538
|
+
const mergedGitignore = mergeSecretIgnores(existingGitignore);
|
|
2539
|
+
const gitignoreChanged = mergedGitignore !== existingGitignore;
|
|
2540
|
+
if (gitignoreChanged && !dryRun) await writeFile3(gitignorePath, mergedGitignore, "utf8");
|
|
2541
|
+
recordChange(
|
|
2542
|
+
changes,
|
|
2543
|
+
"merge-secret-ignores",
|
|
2544
|
+
gitignoreRelativePath,
|
|
2545
|
+
gitignoreChanged,
|
|
2546
|
+
dryRun,
|
|
2547
|
+
relativeEvidence(
|
|
2548
|
+
gitignoreRelativePath,
|
|
2549
|
+
gitignoreChanged ? "required secret patterns are missing" : "required patterns are present"
|
|
2550
|
+
)
|
|
2551
|
+
);
|
|
2552
|
+
const profilePath = path17.join(beforeScan.rootDir, PROFILE_RELATIVE_PATH);
|
|
2553
|
+
const existingProfile = await readJson(profilePath) ?? {};
|
|
2554
|
+
const desiredProfile = createProfile(beforeScan, before, generatedAt);
|
|
2555
|
+
const mergedProfile = mergeMissing(existingProfile, desiredProfile);
|
|
2556
|
+
const profileChanged = !jsonEqual(existingProfile, mergedProfile);
|
|
2557
|
+
if (profileChanged && !dryRun) await writeJson(profilePath, mergedProfile);
|
|
2558
|
+
recordChange(
|
|
2559
|
+
changes,
|
|
2560
|
+
"merge-repository-profile",
|
|
2561
|
+
PROFILE_RELATIVE_PATH,
|
|
2562
|
+
profileChanged,
|
|
2563
|
+
dryRun,
|
|
2564
|
+
relativeEvidence(
|
|
2565
|
+
PROFILE_RELATIVE_PATH,
|
|
2566
|
+
profileChanged ? "missing scanner-derived facts" : "existing facts preserved"
|
|
2567
|
+
)
|
|
2568
|
+
);
|
|
2569
|
+
const evidenceScan = dryRun ? beforeScan : await runScanner(beforeScan.rootDir);
|
|
2570
|
+
const evidenceReport = createReadinessReport(evidenceScan, {
|
|
2571
|
+
generatorVersion: options.generatorVersion,
|
|
2572
|
+
generatedAt
|
|
2573
|
+
});
|
|
2574
|
+
const contextConfigPath = path17.join(beforeScan.rootDir, CONTEXT_CONFIG_RELATIVE_PATH);
|
|
2575
|
+
const existingContextConfig = await readJson(contextConfigPath) ?? {};
|
|
2576
|
+
const onboarding = reconcileOnboardingState(evidenceReport, existingContextConfig, generatedAt);
|
|
2577
|
+
const defaults = preferenceDefaults(onboarding, existingContextConfig.onboarded);
|
|
2578
|
+
const mergedContextConfig = mergeMissing(existingContextConfig, defaults);
|
|
2579
|
+
mergedContextConfig.onboarding = onboarding;
|
|
2580
|
+
const contextConfigChanged = !jsonEqual(existingContextConfig, mergedContextConfig);
|
|
2581
|
+
if (contextConfigChanged && !dryRun) await writeJson(contextConfigPath, mergedContextConfig);
|
|
2582
|
+
recordChange(
|
|
2583
|
+
changes,
|
|
2584
|
+
"merge-onboarding-state",
|
|
2585
|
+
CONTEXT_CONFIG_RELATIVE_PATH,
|
|
2586
|
+
contextConfigChanged,
|
|
2587
|
+
dryRun,
|
|
2588
|
+
relativeEvidence(
|
|
2589
|
+
CONTEXT_CONFIG_RELATIVE_PATH,
|
|
2590
|
+
contextConfigChanged ? "missing preferences or onboarding state" : "state already merged"
|
|
2591
|
+
)
|
|
2592
|
+
);
|
|
2593
|
+
const afterScan = dryRun ? beforeScan : await runScanner(beforeScan.rootDir);
|
|
2594
|
+
const after = createReadinessReport(afterScan, {
|
|
2595
|
+
generatorVersion: options.generatorVersion,
|
|
2596
|
+
generatedAt
|
|
2597
|
+
});
|
|
2598
|
+
after.appliedSafeFixes = appliedActions(changes);
|
|
2599
|
+
return { dryRun, changes, before, after };
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// src/scanner/snapshot.ts
|
|
2603
|
+
import path18 from "path";
|
|
2604
|
+
var READINESS_SNAPSHOT_RELATIVE_PATH = ".cursor/context/readiness.json";
|
|
2605
|
+
async function writeReadinessSnapshot(rootDir, report) {
|
|
2606
|
+
const snapshotPath = path18.join(rootDir, READINESS_SNAPSHOT_RELATIVE_PATH);
|
|
2607
|
+
await writeJson(snapshotPath, report);
|
|
2608
|
+
return snapshotPath;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// src/commands/doctor.ts
|
|
2612
|
+
async function runDoctor(cwd, options = {}) {
|
|
2613
|
+
const rootDir = path19.resolve(cwd);
|
|
2614
|
+
if (options.fixSafe) {
|
|
2615
|
+
const execution = await executeSafeReadinessFixes(rootDir, {
|
|
2616
|
+
generatorVersion: KIT_VERSION,
|
|
2617
|
+
generatedAt: options.generatedAt
|
|
2618
|
+
});
|
|
2619
|
+
await writeReadinessSnapshot(rootDir, execution.after);
|
|
2620
|
+
return { report: execution.after, safeChanges: execution.changes };
|
|
2621
|
+
}
|
|
2622
|
+
const scan = await runScanner(rootDir);
|
|
2623
|
+
const report = createReadinessReport(scan, {
|
|
2624
|
+
generatorVersion: KIT_VERSION,
|
|
2625
|
+
generatedAt: options.generatedAt
|
|
2626
|
+
});
|
|
2627
|
+
await writeReadinessSnapshot(rootDir, report);
|
|
2628
|
+
return { report, safeChanges: [] };
|
|
2629
|
+
}
|
|
2630
|
+
function printDoctorSummary(result) {
|
|
2631
|
+
const { summary, pendingActions } = result.report;
|
|
2632
|
+
const fixed = result.safeChanges.filter((change) => change.status === "applied").length;
|
|
2633
|
+
const nextAction = pendingActions[0];
|
|
2634
|
+
console.log("Repository readiness");
|
|
2635
|
+
console.log(
|
|
2636
|
+
` ready: ${summary.ready}, choices: ${summary.needs_choice}, manual: ${summary.manual}, blocked: ${summary.blocked}`
|
|
2637
|
+
);
|
|
2638
|
+
console.log(` safe fixes applied: ${fixed}`);
|
|
2639
|
+
console.log(` pending actions: ${pendingActions.length}`);
|
|
2640
|
+
console.log(
|
|
2641
|
+
nextAction ? `Next: ${nextAction.recommendation}` : "Next: repository readiness checks are complete"
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
var doctorCommand = defineCommand4({
|
|
2645
|
+
meta: {
|
|
2646
|
+
name: "doctor",
|
|
2647
|
+
description: "Diagnose repository readiness and optionally apply safe local fixes."
|
|
2648
|
+
},
|
|
2649
|
+
args: {
|
|
2650
|
+
cwd: {
|
|
2651
|
+
type: "string",
|
|
2652
|
+
default: process.cwd()
|
|
2653
|
+
},
|
|
2654
|
+
json: {
|
|
2655
|
+
type: "boolean",
|
|
2656
|
+
description: "Print deterministic machine-readable JSON without status messages",
|
|
2657
|
+
default: false
|
|
2658
|
+
},
|
|
2659
|
+
"fix-safe": {
|
|
2660
|
+
type: "boolean",
|
|
2661
|
+
description: "Apply only local, reversible, merge-safe readiness fixes",
|
|
2662
|
+
default: false
|
|
2663
|
+
}
|
|
2664
|
+
},
|
|
2665
|
+
async run({ args }) {
|
|
2666
|
+
const result = await runDoctor(args.cwd, { fixSafe: args["fix-safe"] });
|
|
2667
|
+
if (args.json) {
|
|
2668
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
printDoctorSummary(result);
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
|
|
2675
|
+
// src/commands/handoff.ts
|
|
2676
|
+
import { spawn } from "child_process";
|
|
2677
|
+
import { readFile as readFile9, readdir as readdir2, writeFile as writeFile4 } from "fs/promises";
|
|
2678
|
+
import path20 from "path";
|
|
2679
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
2680
|
+
function parsePlanFrontmatter(raw) {
|
|
2681
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
2682
|
+
if (!match?.[1]) return null;
|
|
2683
|
+
const block = match[1];
|
|
2684
|
+
const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim();
|
|
2685
|
+
const todos = [];
|
|
2686
|
+
const todoBlocks = block.matchAll(/- id:\s*(\S+)\s*\n\s*content:\s*(.+)\n\s*status:\s*(\S+)/g);
|
|
2687
|
+
for (const m of todoBlocks) {
|
|
2688
|
+
if (m[1] && m[2] && m[3]) {
|
|
2689
|
+
todos.push({ id: m[1], content: m[2].replace(/^["']|["']$/g, ""), status: m[3] });
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
return { name, todos };
|
|
2693
|
+
}
|
|
2694
|
+
async function findActivePlan(plansDir) {
|
|
2695
|
+
if (!await fileExists(plansDir)) return null;
|
|
2696
|
+
const files = (await readdir2(plansDir)).filter((f) => f.endsWith(".plan.md")).sort().reverse();
|
|
2697
|
+
for (const file of files) {
|
|
2698
|
+
const raw = await readFile9(path20.join(plansDir, file), "utf8");
|
|
2699
|
+
const fm = parsePlanFrontmatter(raw);
|
|
2700
|
+
if (fm?.todos?.some((t) => t.status !== "completed" && t.status !== "cancelled")) {
|
|
2701
|
+
return { file, raw };
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return files[0] ? { file: files[0], raw: await readFile9(path20.join(plansDir, files[0]), "utf8") } : null;
|
|
2705
|
+
}
|
|
2706
|
+
function now() {
|
|
2707
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
|
|
2708
|
+
}
|
|
2709
|
+
async function loadProfile(rootDir) {
|
|
2710
|
+
const configPath = path20.join(rootDir, ".cursor", "agent-kit.config.json");
|
|
2711
|
+
try {
|
|
2712
|
+
return await readJson(configPath);
|
|
2713
|
+
} catch {
|
|
2714
|
+
return null;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function buildRoutines(profile) {
|
|
2718
|
+
const lines = [];
|
|
2719
|
+
const workflow = profile?.git.workflow;
|
|
2720
|
+
if (workflow === "homolog-prod") {
|
|
1469
2721
|
lines.push("- [ ] `git staging` - move changes to staging");
|
|
1470
2722
|
lines.push("- [ ] `git prod` - promote to production (after approval)");
|
|
1471
2723
|
} else {
|
|
@@ -1541,7 +2793,7 @@ function runCursorHandoff(scriptPath, cwd) {
|
|
|
1541
2793
|
child.on("close", (code) => resolve(code ?? 1));
|
|
1542
2794
|
});
|
|
1543
2795
|
}
|
|
1544
|
-
var handoffCommand =
|
|
2796
|
+
var handoffCommand = defineCommand5({
|
|
1545
2797
|
meta: {
|
|
1546
2798
|
name: "handoff",
|
|
1547
2799
|
description: "Write .cursor/HANDOFF.md from the active Cursor plan, or run ./cursor-handoff handoff when no plan exists."
|
|
@@ -1555,15 +2807,15 @@ var handoffCommand = defineCommand4({
|
|
|
1555
2807
|
},
|
|
1556
2808
|
async run({ args }) {
|
|
1557
2809
|
const profile = await loadProfile(args.cwd);
|
|
1558
|
-
const plansDir =
|
|
1559
|
-
const handoffPath =
|
|
2810
|
+
const plansDir = path20.join(args.cwd, ".cursor", "plans");
|
|
2811
|
+
const handoffPath = path20.join(args.cwd, ".cursor", "HANDOFF.md");
|
|
1560
2812
|
const plan = await findActivePlan(plansDir);
|
|
1561
2813
|
if (plan) {
|
|
1562
2814
|
const fm = parsePlanFrontmatter(plan.raw);
|
|
1563
2815
|
if (fm) {
|
|
1564
|
-
await ensureDir(
|
|
2816
|
+
await ensureDir(path20.join(args.cwd, ".cursor"));
|
|
1565
2817
|
const content = buildHandoff(plan.file, fm, profile);
|
|
1566
|
-
await
|
|
2818
|
+
await writeFile4(handoffPath, content, "utf8");
|
|
1567
2819
|
logger.success("HANDOFF.md updated: .cursor/HANDOFF.md");
|
|
1568
2820
|
logger.info(
|
|
1569
2821
|
`Plan: ${plan.file} (${fm.todos?.filter((t) => t.status === "completed").length}/${fm.todos?.length} completed)`
|
|
@@ -1581,7 +2833,7 @@ var handoffCommand = defineCommand4({
|
|
|
1581
2833
|
}
|
|
1582
2834
|
logger.warn(`Plan ${plan.file} without valid frontmatter; trying legacy flow.`);
|
|
1583
2835
|
}
|
|
1584
|
-
const scriptPath =
|
|
2836
|
+
const scriptPath = path20.join(args.cwd, "cursor-handoff");
|
|
1585
2837
|
if (!await fileExists(scriptPath)) {
|
|
1586
2838
|
printV3Guidance();
|
|
1587
2839
|
return;
|
|
@@ -1606,868 +2858,320 @@ var handoffCommand = defineCommand4({
|
|
|
1606
2858
|
});
|
|
1607
2859
|
|
|
1608
2860
|
// src/commands/init.ts
|
|
1609
|
-
import path23 from "path";
|
|
1610
2861
|
import { intro, outro } from "@clack/prompts";
|
|
1611
|
-
import { defineCommand as
|
|
1612
|
-
|
|
1613
|
-
// src/generator/index.ts
|
|
1614
|
-
import { writeFile as writeFile9 } from "fs/promises";
|
|
1615
|
-
import path16 from "path";
|
|
2862
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
1616
2863
|
|
|
1617
|
-
// src/
|
|
1618
|
-
import
|
|
1619
|
-
import
|
|
2864
|
+
// src/commands/install.ts
|
|
2865
|
+
import path23 from "path";
|
|
2866
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1620
2867
|
|
|
1621
|
-
// src/generator/
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
if (!p || p === "other") return "git";
|
|
1637
|
-
return GIT_PLATFORM_META[p]?.cli ?? "git";
|
|
1638
|
-
}
|
|
1639
|
-
function prTerminology(profile) {
|
|
1640
|
-
const p = profile.git.provider;
|
|
1641
|
-
if (!p || p === "other") return "PR/MR";
|
|
1642
|
-
return GIT_PLATFORM_META[p]?.prTerm ?? "PR/MR";
|
|
1643
|
-
}
|
|
1644
|
-
function pmToolsList(profile) {
|
|
1645
|
-
const tools = profile.services.projectManagement;
|
|
1646
|
-
if (!tools || tools.length === 0) return "";
|
|
1647
|
-
return tools.map((t) => PM_TOOL_LABELS[t]).join(", ");
|
|
1648
|
-
}
|
|
1649
|
-
function pmRoutineLine(profile) {
|
|
1650
|
-
const label = pmToolsList(profile);
|
|
1651
|
-
if (!label) return "";
|
|
1652
|
-
return `- Update tasks in ${label} (if applicable)`;
|
|
1653
|
-
}
|
|
1654
|
-
function ciLabel(profile) {
|
|
1655
|
-
const labels = {
|
|
1656
|
-
"github-actions": "GitHub Actions",
|
|
1657
|
-
"gitlab-ci": "GitLab CI",
|
|
1658
|
-
"azure-pipelines": "Azure Pipelines",
|
|
1659
|
-
"bitbucket-pipelines": "Bitbucket Pipelines",
|
|
1660
|
-
jenkins: "Jenkins",
|
|
1661
|
-
circleci: "CircleCI",
|
|
1662
|
-
travis: "Travis CI"
|
|
1663
|
-
};
|
|
1664
|
-
return labels[profile.infra.ci] ?? "";
|
|
1665
|
-
}
|
|
1666
|
-
function devopsFlowSummary(profile) {
|
|
1667
|
-
const parts = [];
|
|
1668
|
-
const provider = gitProviderLabel(profile);
|
|
1669
|
-
parts.push(`Git: ${provider}`);
|
|
1670
|
-
if (profile.git.workflow !== "unknown") {
|
|
1671
|
-
const workflowLabels = {
|
|
1672
|
-
"trunk-based": "trunk-based",
|
|
1673
|
-
"feature-pr": `feature branch \u2192 ${prTerminology(profile)} \u2192 main`,
|
|
1674
|
-
gitflow: "gitflow (develop/release/main)",
|
|
1675
|
-
"homolog-prod": "staging \u2192 production"
|
|
1676
|
-
};
|
|
1677
|
-
parts.push(`Workflow: ${workflowLabels[profile.git.workflow] ?? profile.git.workflow}`);
|
|
1678
|
-
}
|
|
1679
|
-
const ci = ciLabel(profile);
|
|
1680
|
-
if (ci) parts.push(`CI/CD: ${ci}`);
|
|
1681
|
-
const pm = pmToolsList(profile);
|
|
1682
|
-
if (pm) parts.push(`Project mgmt: ${pm}`);
|
|
1683
|
-
return parts.join(" | ");
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// src/generator/agents-md.ts
|
|
1687
|
-
async function generateAgentsMd(profile) {
|
|
1688
|
-
const target = path11.join(profile.rootDir, "AGENTS.md");
|
|
1689
|
-
const flow = devopsFlowSummary(profile);
|
|
1690
|
-
const prTerm = prTerminology(profile);
|
|
1691
|
-
const content = `# AGENTS.md
|
|
1692
|
-
|
|
1693
|
-
Project configured with Agent Kit v3.
|
|
1694
|
-
|
|
1695
|
-
## Detected context
|
|
1696
|
-
- Stack: ${profile.stack.language}${profile.stack.framework ? ` (${profile.stack.framework})` : ""}
|
|
1697
|
-
- IDE: ${profile.ide.ide} (${profile.ide.plan})
|
|
1698
|
-
- ${flow}
|
|
1699
|
-
|
|
1700
|
-
## Guidelines
|
|
1701
|
-
1. Prefer small, verifiable changes.
|
|
1702
|
-
2. Adapt response depth to user's IDE plan/model.
|
|
1703
|
-
3. Use /worktree for experiments, /best-of-n for critical decisions (Cursor 3.0).
|
|
1704
|
-
4. Security review before merge.
|
|
1705
|
-
5. Always create a ${prTerm} \u2014 never push directly to main.
|
|
1706
|
-
`;
|
|
1707
|
-
await writeFile4(target, content, "utf8");
|
|
2868
|
+
// src/generator/personalization.ts
|
|
2869
|
+
import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
2870
|
+
import path21 from "path";
|
|
2871
|
+
var PERSONALIZATION_CONTRACT_VERSION = 1;
|
|
2872
|
+
var CONTEXT_PATH = ".cursor/project-context.md";
|
|
2873
|
+
var AGENTS_PATH = "AGENTS.md";
|
|
2874
|
+
var RESULT_PATH = ".cursor/context/personalization.json";
|
|
2875
|
+
function uniqueEvidence(evidence) {
|
|
2876
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2877
|
+
return evidence.filter((item) => {
|
|
2878
|
+
const key = `${item.source}:${item.value}`;
|
|
2879
|
+
if (seen.has(key)) return false;
|
|
2880
|
+
seen.add(key);
|
|
2881
|
+
return true;
|
|
2882
|
+
});
|
|
1708
2883
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
import path12 from "path";
|
|
1713
|
-
function buildGitWorkflowRule(profile) {
|
|
1714
|
-
const prTerm = prTerminology(profile);
|
|
1715
|
-
const cli = gitCliTool(profile);
|
|
1716
|
-
const provider = gitProviderLabel(profile);
|
|
1717
|
-
const pm = pmToolsList(profile);
|
|
1718
|
-
const lines = [
|
|
1719
|
-
"# Git Workflow",
|
|
1720
|
-
`- Platform: ${provider} | CLI: \`${cli}\``,
|
|
1721
|
-
"- Use Conventional Commits (feat:, fix:, docs:, refactor:, chore:, perf:, test:).",
|
|
1722
|
-
"- For risky changes, prefer /worktree (isolated git worktree).",
|
|
1723
|
-
"- For comparing approaches, use /best-of-n."
|
|
1724
|
-
];
|
|
1725
|
-
if (profile.git.workflow === "homolog-prod") {
|
|
1726
|
-
lines.push(
|
|
1727
|
-
"- Flow: development \u2192 staging \u2192 production.",
|
|
1728
|
-
`- Create ${prTerm} targeting the staging branch; merge to main only via promotion.`,
|
|
1729
|
-
"- NEVER commit directly to main."
|
|
1730
|
-
);
|
|
1731
|
-
} else if (profile.git.workflow === "feature-pr") {
|
|
1732
|
-
lines.push(
|
|
1733
|
-
`- Flow: feature branch \u2192 ${prTerm} \u2192 main.`,
|
|
1734
|
-
`- Always create a ${prTerm} for review before merging.`
|
|
1735
|
-
);
|
|
1736
|
-
} else if (profile.git.workflow === "gitflow") {
|
|
1737
|
-
lines.push(
|
|
1738
|
-
"- Flow: feature \u2192 develop \u2192 release \u2192 main.",
|
|
1739
|
-
`- Create ${prTerm} targeting develop for features, main for releases.`
|
|
1740
|
-
);
|
|
1741
|
-
}
|
|
1742
|
-
if (pm) {
|
|
1743
|
-
lines.push(`- After merge: update task status in ${pm} if integration is available.`);
|
|
1744
|
-
}
|
|
1745
|
-
return `${lines.join("\n")}
|
|
1746
|
-
`;
|
|
2884
|
+
function purposeEvidence(profile) {
|
|
2885
|
+
if (profile.purpose.confidence === "low") return [];
|
|
2886
|
+
return profile.purpose.evidence;
|
|
1747
2887
|
}
|
|
1748
|
-
function
|
|
1749
|
-
|
|
1750
|
-
const routines = ["- Suggest routines: git commit/push, review CHANGELOG."];
|
|
1751
|
-
if (pm) routines.push(`${pm}.`);
|
|
1752
|
-
return `# Handoff \u2014 State in File
|
|
1753
|
-
- After completing each task: save .cursor/HANDOFF.md with progress and next steps.
|
|
1754
|
-
- Update to-dos in the plan (.cursor/plans/) when completing or starting a task.
|
|
1755
|
-
- One HANDOFF per project \u2014 source of truth for continuity.
|
|
1756
|
-
${routines.join("\n")}
|
|
1757
|
-
- To resume: /continue-plan in a new conversation.
|
|
1758
|
-
- Native features (summaries, /resume, transcripts, Agents Window) complement \u2014 not replace.
|
|
1759
|
-
- Context at ~60%: save handoff and suggest new conversation.
|
|
1760
|
-
`;
|
|
2888
|
+
function contextEvidence(profile, pattern) {
|
|
2889
|
+
return profile.context.sources.filter((item) => pattern.test(item.value));
|
|
1761
2890
|
}
|
|
1762
|
-
function
|
|
1763
|
-
|
|
1764
|
-
return [
|
|
1765
|
-
{
|
|
1766
|
-
filename: "01-core.mdc",
|
|
1767
|
-
content: `# Core Rule
|
|
1768
|
-
- Respond directly and concisely.
|
|
1769
|
-
- Focus on small changes and local validation.
|
|
1770
|
-
- Size each task for ~50% of context window.
|
|
1771
|
-
`
|
|
1772
|
-
},
|
|
1773
|
-
{
|
|
1774
|
-
filename: "02-handoff.mdc",
|
|
1775
|
-
content: buildHandoffRule(profile)
|
|
1776
|
-
}
|
|
1777
|
-
];
|
|
1778
|
-
}
|
|
1779
|
-
return [
|
|
1780
|
-
{
|
|
1781
|
-
filename: "01-core.mdc",
|
|
1782
|
-
content: `# Core Rule
|
|
1783
|
-
- Execute tasks end-to-end whenever possible.
|
|
1784
|
-
- Prioritize security, tests, and architectural consistency.
|
|
1785
|
-
- Size each task for ~50% of context window.
|
|
1786
|
-
`
|
|
1787
|
-
},
|
|
1788
|
-
{
|
|
1789
|
-
filename: "02-git-workflow.mdc",
|
|
1790
|
-
content: buildGitWorkflowRule(profile)
|
|
1791
|
-
},
|
|
1792
|
-
{
|
|
1793
|
-
filename: "03-handoff.mdc",
|
|
1794
|
-
content: buildHandoffRule(profile)
|
|
1795
|
-
},
|
|
1796
|
-
{
|
|
1797
|
-
filename: "04-ide-guide.mdc",
|
|
1798
|
-
content: `# Cursor Guide
|
|
1799
|
-
- Use Agents Window for parallelism \u2014 each agent reads HANDOFF before acting.
|
|
1800
|
-
- Use Await for long-running processes.
|
|
1801
|
-
- Transcripts and @mentions for cross-reference.
|
|
1802
|
-
- /worktree for risky changes.
|
|
1803
|
-
- /best-of-n to compare approaches.
|
|
1804
|
-
`
|
|
1805
|
-
}
|
|
1806
|
-
];
|
|
2891
|
+
function readinessEvidence(report, checkId) {
|
|
2892
|
+
return report.pillars.flatMap((pillar2) => pillar2.checks).filter((check2) => check2.id === checkId).flatMap((check2) => check2.evidence);
|
|
1807
2893
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
const agentsDir = path12.join(profile.rootDir, ".cursor", "agents");
|
|
1812
|
-
const commandsDir = path12.join(profile.rootDir, ".cursor", "commands");
|
|
1813
|
-
await Promise.all([
|
|
1814
|
-
ensureDir(rulesDir),
|
|
1815
|
-
ensureDir(skillsDir),
|
|
1816
|
-
ensureDir(agentsDir),
|
|
1817
|
-
ensureDir(commandsDir)
|
|
1818
|
-
]);
|
|
1819
|
-
const rules = cursorRulesByPlan(profile);
|
|
1820
|
-
await Promise.all(
|
|
1821
|
-
rules.map((rule) => writeFile5(path12.join(rulesDir, rule.filename), rule.content, "utf8"))
|
|
1822
|
-
);
|
|
1823
|
-
const includeAgents = profile.ide.plan !== "cursor-free";
|
|
1824
|
-
if (includeAgents) {
|
|
1825
|
-
await writeFile5(
|
|
1826
|
-
path12.join(agentsDir, "security-reviewer.md"),
|
|
1827
|
-
"# Security Reviewer\n\nFocus on auth, PII, secrets, injection, and logging.\n",
|
|
1828
|
-
"utf8"
|
|
1829
|
-
);
|
|
1830
|
-
}
|
|
1831
|
-
const ci = ciLabel(profile);
|
|
1832
|
-
const provider = gitProviderLabel(profile);
|
|
1833
|
-
const statusLines = [
|
|
1834
|
-
"# /agent-kit-status",
|
|
1835
|
-
"",
|
|
1836
|
-
"Show current profile and active components.",
|
|
1837
|
-
"",
|
|
1838
|
-
"## DevOps Flow",
|
|
1839
|
-
`- Git: ${provider} (${profile.git.workflow})`
|
|
1840
|
-
];
|
|
1841
|
-
if (ci) statusLines.push(`- CI/CD: ${ci}`);
|
|
1842
|
-
const pm = pmToolsList(profile);
|
|
1843
|
-
if (pm) statusLines.push(`- Project management: ${pm}`);
|
|
1844
|
-
statusLines.push("");
|
|
1845
|
-
await writeFile5(
|
|
1846
|
-
path12.join(commandsDir, "agent-kit-status.md"),
|
|
1847
|
-
`${statusLines.join("\n")}
|
|
1848
|
-
`,
|
|
1849
|
-
"utf8"
|
|
2894
|
+
function hasPurpose(profile, purposes) {
|
|
2895
|
+
return purposes.some(
|
|
2896
|
+
(purpose) => profile.purpose.value === purpose || profile.purpose.categories.includes(purpose)
|
|
1850
2897
|
);
|
|
1851
2898
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
import path13 from "path";
|
|
1856
|
-
async function generateGitHooks(profile) {
|
|
1857
|
-
if (!profile.installHooks) return;
|
|
1858
|
-
const hooksDir = path13.join(profile.rootDir, ".git", "hooks");
|
|
1859
|
-
await ensureDir(hooksDir);
|
|
1860
|
-
const preCommitPath = path13.join(hooksDir, "pre-commit");
|
|
1861
|
-
const preCommit = `#!/usr/bin/env bash
|
|
1862
|
-
set -euo pipefail
|
|
1863
|
-
|
|
1864
|
-
if command -v rg >/dev/null 2>&1; then
|
|
1865
|
-
rg -n --hidden --glob '!node_modules/**' '(AKIA|BEGIN PRIVATE KEY|xoxb-)' . && {
|
|
1866
|
-
echo "Potential secret detected. Commit blocked."
|
|
1867
|
-
exit 1
|
|
1868
|
-
} || true
|
|
1869
|
-
fi
|
|
1870
|
-
`;
|
|
1871
|
-
await writeFile6(preCommitPath, preCommit, "utf8");
|
|
1872
|
-
await chmod(preCommitPath, 493);
|
|
2899
|
+
function candidate(kind, id, evidence, status) {
|
|
2900
|
+
const verified = uniqueEvidence(evidence);
|
|
2901
|
+
return verified.length > 0 ? { kind, id, status, evidence: verified } : null;
|
|
1873
2902
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
const githubDir = path14.join(profile.rootDir, ".github");
|
|
1881
|
-
await Promise.all([ensureDir(vscodeDir), ensureDir(githubDir)]);
|
|
1882
|
-
await writeFile7(
|
|
1883
|
-
path14.join(vscodeDir, "settings.json"),
|
|
1884
|
-
`${JSON.stringify(
|
|
1885
|
-
{
|
|
1886
|
-
"editor.formatOnSave": true,
|
|
1887
|
-
"editor.codeActionsOnSave": {
|
|
1888
|
-
"source.fixAll": "explicit"
|
|
1889
|
-
},
|
|
1890
|
-
"files.autoSave": "afterDelay"
|
|
1891
|
-
},
|
|
1892
|
-
null,
|
|
1893
|
-
2
|
|
1894
|
-
)}
|
|
1895
|
-
`,
|
|
1896
|
-
"utf8"
|
|
1897
|
-
);
|
|
1898
|
-
const provider = gitProviderLabel(profile);
|
|
1899
|
-
const prTerm = prTerminology(profile);
|
|
1900
|
-
await writeFile7(
|
|
1901
|
-
path14.join(githubDir, "copilot-instructions.md"),
|
|
1902
|
-
`# Copilot Instructions
|
|
1903
|
-
|
|
1904
|
-
- Keep code changes small and testable.
|
|
1905
|
-
- Use Conventional Commits (feat:, fix:, docs:, etc.).
|
|
1906
|
-
- Prefer security-safe defaults.
|
|
1907
|
-
- Git platform: ${provider}. Always create a ${prTerm} for review.
|
|
1908
|
-
`,
|
|
1909
|
-
"utf8"
|
|
1910
|
-
);
|
|
1911
|
-
if (profile.ide.plan === "vscode-pro") {
|
|
1912
|
-
await writeFile7(
|
|
1913
|
-
path14.join(vscodeDir, "security-review.agent.md"),
|
|
1914
|
-
"# Security Review Agent\n\nSpecialized mode for security review.\n",
|
|
1915
|
-
"utf8"
|
|
2903
|
+
function componentAvailable(index, item) {
|
|
2904
|
+
if (item.kind === "skill") return allSkills(index).some((entry) => entry.id === item.id);
|
|
2905
|
+
if (item.kind === "pack") return allPacks(index).some((entry) => entry.id === item.id);
|
|
2906
|
+
if (item.kind === "agent" || item.kind === "command") {
|
|
2907
|
+
return (index.artifacts ?? []).some(
|
|
2908
|
+
(entry) => entry.kind === item.kind && entry.id === item.id
|
|
1916
2909
|
);
|
|
1917
2910
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
- Keep instructions concise and objective.
|
|
1928
|
-
- Prefer small diffs and explicit validation.
|
|
1929
|
-
- Apply security review before merge.
|
|
1930
|
-
- Use Conventional Commits (feat:, fix:, docs:, etc.).
|
|
1931
|
-
- Git: ${provider}. Create a ${prTerm} for every change.
|
|
1932
|
-
`;
|
|
1933
|
-
await writeFile8(path15.join(profile.rootDir, ".windsurfrules"), content, "utf8");
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
// src/generator/index.ts
|
|
1937
|
-
async function generateFromProfile(profile) {
|
|
1938
|
-
await generateAgentsMd(profile);
|
|
1939
|
-
await generateGitHooks(profile);
|
|
1940
|
-
if (profile.ide.ide === "cursor" || profile.ide.plan.startsWith("cursor")) {
|
|
1941
|
-
await generateCursorArtifacts(profile);
|
|
1942
|
-
}
|
|
1943
|
-
if (profile.ide.ide === "vscode" || profile.ide.plan.startsWith("vscode")) {
|
|
1944
|
-
await generateVSCodeArtifacts(profile);
|
|
1945
|
-
}
|
|
1946
|
-
if (profile.ide.ide === "windsurf" || profile.ide.plan === "windsurf") {
|
|
1947
|
-
await generateWindsurfArtifacts(profile);
|
|
1948
|
-
}
|
|
1949
|
-
if (profile.ide.plan === "default") {
|
|
1950
|
-
await Promise.all([
|
|
1951
|
-
generateCursorArtifacts(profile),
|
|
1952
|
-
generateVSCodeArtifacts(profile),
|
|
1953
|
-
generateWindsurfArtifacts(profile)
|
|
1954
|
-
]);
|
|
1955
|
-
}
|
|
1956
|
-
const pluginDir = path16.join(profile.rootDir, ".cursor-plugin");
|
|
1957
|
-
await ensureDir(pluginDir);
|
|
1958
|
-
await writeFile9(
|
|
1959
|
-
path16.join(pluginDir, "plugin.json"),
|
|
1960
|
-
`${JSON.stringify(
|
|
1961
|
-
{
|
|
1962
|
-
name: "agent-kit",
|
|
1963
|
-
displayName: "Agent Kit",
|
|
1964
|
-
author: "agent-kit-startup",
|
|
1965
|
-
description: "Bootstrap de ambiente dev com IA (Cursor, VS Code, Windsurf)",
|
|
1966
|
-
keywords: ["agents", "context", "automation", "multi-ide"],
|
|
1967
|
-
license: "MIT",
|
|
1968
|
-
version: "3.0.0"
|
|
1969
|
-
},
|
|
1970
|
-
null,
|
|
1971
|
-
2
|
|
1972
|
-
)}
|
|
1973
|
-
`,
|
|
1974
|
-
"utf8"
|
|
1975
|
-
);
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
// src/scanner/scan.ts
|
|
1979
|
-
import path22 from "path";
|
|
1980
|
-
|
|
1981
|
-
// src/scanner/detect-git.ts
|
|
1982
|
-
import { execFile as execFile2 } from "child_process";
|
|
1983
|
-
import path17 from "path";
|
|
1984
|
-
import { promisify as promisify2 } from "util";
|
|
1985
|
-
var exec = promisify2(execFile2);
|
|
1986
|
-
function detectProvider(remoteUrl) {
|
|
1987
|
-
if (!remoteUrl) return void 0;
|
|
1988
|
-
if (remoteUrl.includes("github")) return "github";
|
|
1989
|
-
if (remoteUrl.includes("gitlab")) return "gitlab";
|
|
1990
|
-
if (remoteUrl.includes("bitbucket")) return "bitbucket";
|
|
1991
|
-
if (remoteUrl.includes("dev.azure.com") || remoteUrl.includes("visualstudio.com"))
|
|
1992
|
-
return "azure-devops";
|
|
1993
|
-
if (remoteUrl.includes("gitea") || remoteUrl.includes("codeberg")) return "gitea";
|
|
1994
|
-
return "other";
|
|
1995
|
-
}
|
|
1996
|
-
function inferWorkflow(currentBranch) {
|
|
1997
|
-
if (!currentBranch) return "unknown";
|
|
1998
|
-
if (currentBranch === "main" || currentBranch === "master") return "feature-pr";
|
|
1999
|
-
if (currentBranch.includes("develop") || currentBranch.includes("release")) return "gitflow";
|
|
2000
|
-
if (currentBranch.includes("staging") || currentBranch.includes("homolog")) return "homolog-prod";
|
|
2001
|
-
return "feature-pr";
|
|
2002
|
-
}
|
|
2003
|
-
async function runGit(args, rootDir) {
|
|
2004
|
-
try {
|
|
2005
|
-
const { stdout } = await exec("git", args, { cwd: rootDir });
|
|
2006
|
-
return stdout.trim();
|
|
2007
|
-
} catch {
|
|
2008
|
-
return void 0;
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
async function detectGit(rootDir) {
|
|
2012
|
-
const hasGit = await fileExists(path17.join(rootDir, ".git"));
|
|
2013
|
-
if (!hasGit) return { workflow: "unknown" };
|
|
2014
|
-
const remoteUrl = await runGit(["remote", "get-url", "origin"], rootDir);
|
|
2015
|
-
const currentBranch = await runGit(["branch", "--show-current"], rootDir);
|
|
2016
|
-
return {
|
|
2017
|
-
provider: detectProvider(remoteUrl),
|
|
2018
|
-
remoteUrl,
|
|
2019
|
-
currentBranch,
|
|
2020
|
-
workflow: inferWorkflow(currentBranch)
|
|
2021
|
-
};
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
// src/scanner/detect-ide.ts
|
|
2025
|
-
import path18 from "path";
|
|
2026
|
-
async function detectIde(rootDir) {
|
|
2027
|
-
const hasCursor = await fileExists(path18.join(rootDir, ".cursor"));
|
|
2028
|
-
const hasVSCode = await fileExists(path18.join(rootDir, ".vscode"));
|
|
2029
|
-
const hasWindsurf = await fileExists(path18.join(rootDir, ".windsurfrules"));
|
|
2030
|
-
if (hasCursor) return { ide: "cursor", plan: "cursor-pro" };
|
|
2031
|
-
if (hasVSCode) return { ide: "vscode", plan: "vscode-pro" };
|
|
2032
|
-
if (hasWindsurf) return { ide: "windsurf", plan: "windsurf" };
|
|
2033
|
-
return { ide: "unknown", plan: "default" };
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// src/scanner/detect-infra.ts
|
|
2037
|
-
import path19 from "path";
|
|
2038
|
-
async function detectInfra(rootDir) {
|
|
2039
|
-
const docker = await fileExists(path19.join(rootDir, "Dockerfile")) || await fileExists(path19.join(rootDir, "docker-compose.yml")) || await fileExists(path19.join(rootDir, "docker-compose.yaml"));
|
|
2040
|
-
const kubernetes = await fileExists(path19.join(rootDir, "k8s")) || await fileExists(path19.join(rootDir, "kubernetes"));
|
|
2041
|
-
let ci = "none";
|
|
2042
|
-
for (const [platform, filePath] of Object.entries(CI_PLATFORM_FILES)) {
|
|
2043
|
-
if (await fileExists(path19.join(rootDir, filePath))) {
|
|
2044
|
-
ci = platform;
|
|
2045
|
-
break;
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
return { docker, kubernetes, ci };
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
// src/scanner/detect-services.ts
|
|
2052
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
2053
|
-
import path20 from "path";
|
|
2054
|
-
async function detectProjectManagement(rootDir) {
|
|
2055
|
-
const tools = [];
|
|
2056
|
-
const mcpConfigPaths = [
|
|
2057
|
-
path20.join(rootDir, ".cursor", "mcp.json"),
|
|
2058
|
-
path20.join(rootDir, "mcp.json")
|
|
2911
|
+
return true;
|
|
2912
|
+
}
|
|
2913
|
+
function buildPersonalizationPlan(profile, report, registry) {
|
|
2914
|
+
const items = [];
|
|
2915
|
+
const purpose = purposeEvidence(profile);
|
|
2916
|
+
const packageEvidence = profile.stack.packageManagerEvidence ?? [];
|
|
2917
|
+
const n8nEvidence = [
|
|
2918
|
+
...contextEvidence(profile, /(^|[/.-])n8n([/.-]|$)/i),
|
|
2919
|
+
...purpose.filter((item) => /n8n/i.test(item.value))
|
|
2059
2920
|
];
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
}
|
|
2074
|
-
if (await fileExists(path20.join(rootDir, ".github", "ISSUE_TEMPLATE"))) {
|
|
2075
|
-
tools.push("github-issues");
|
|
2921
|
+
const sqlEvidence = [
|
|
2922
|
+
...contextEvidence(profile, /(^|[/.-])(sql|schema|migration)([/.-]|$)/i),
|
|
2923
|
+
...purpose.filter((item) => /\bsql\b/i.test(item.value))
|
|
2924
|
+
];
|
|
2925
|
+
const promptEvidence = contextEvidence(profile, /(^|[/.-])prompts?([/.-]|$)/i);
|
|
2926
|
+
const ciEvidence = profile.infra.ciFiles.map((value) => ({ source: "file", value }));
|
|
2927
|
+
const infraEvidence = [
|
|
2928
|
+
...ciEvidence,
|
|
2929
|
+
...profile.infra.infrastructureFiles.map((value) => ({ source: "file", value })),
|
|
2930
|
+
...profile.infra.deploymentFiles.map((value) => ({ source: "file", value }))
|
|
2931
|
+
];
|
|
2932
|
+
if (hasPurpose(profile, ["documentation", "knowledge"])) {
|
|
2933
|
+
items.push(candidate("skill", "docs-repo", purpose, "applied"));
|
|
2076
2934
|
}
|
|
2077
|
-
if (
|
|
2078
|
-
|
|
2935
|
+
if (profile.stack.language.toLowerCase() === "node" && packageEvidence.length > 0) {
|
|
2936
|
+
items.push(candidate("skill", "cursor-skills-node", packageEvidence, "applied"));
|
|
2079
2937
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
const hasSequelize = await fileExists(path20.join(rootDir, "sequelize"));
|
|
2085
|
-
const hasDrizzle = await fileExists(path20.join(rootDir, "drizzle.config.ts"));
|
|
2086
|
-
const hasKnex = await fileExists(path20.join(rootDir, "knexfile.ts"));
|
|
2087
|
-
const hasTypeorm = await fileExists(path20.join(rootDir, "ormconfig.json"));
|
|
2088
|
-
const database = hasPrisma || hasSequelize || hasDrizzle || hasKnex || hasTypeorm ? "postgresql" : void 0;
|
|
2089
|
-
const orm = hasPrisma ? "prisma" : hasDrizzle ? "drizzle" : hasSequelize ? "sequelize" : hasKnex ? "knex" : hasTypeorm ? "typeorm" : void 0;
|
|
2090
|
-
const projectManagement = await detectProjectManagement(rootDir);
|
|
2091
|
-
return {
|
|
2092
|
-
database,
|
|
2093
|
-
orm,
|
|
2094
|
-
projectManagement: projectManagement.length > 0 ? projectManagement : void 0
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
// src/scanner/detect-stack.ts
|
|
2099
|
-
import path21 from "path";
|
|
2100
|
-
var PROJECT_MARKERS = [
|
|
2101
|
-
"package.json",
|
|
2102
|
-
"requirements.txt",
|
|
2103
|
-
"pyproject.toml",
|
|
2104
|
-
"go.mod",
|
|
2105
|
-
"Gemfile",
|
|
2106
|
-
"pom.xml",
|
|
2107
|
-
"build.gradle",
|
|
2108
|
-
"composer.json",
|
|
2109
|
-
"Cargo.toml"
|
|
2110
|
-
];
|
|
2111
|
-
async function detectStack(rootDir) {
|
|
2112
|
-
const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path21.join(rootDir, item))))).some(Boolean);
|
|
2113
|
-
const hasPackageJson = await fileExists(path21.join(rootDir, "package.json"));
|
|
2114
|
-
if (hasPackageJson) {
|
|
2115
|
-
const hasNextConfig = await fileExists(path21.join(rootDir, "next.config.js")) || await fileExists(path21.join(rootDir, "next.config.mjs")) || await fileExists(path21.join(rootDir, "next.config.ts"));
|
|
2116
|
-
const hasNestConfig = await fileExists(path21.join(rootDir, "nest-cli.json"));
|
|
2117
|
-
return {
|
|
2118
|
-
language: "node",
|
|
2119
|
-
framework: hasNextConfig ? "nextjs" : hasNestConfig ? "nestjs" : "node",
|
|
2120
|
-
packageManager: "pnpm",
|
|
2121
|
-
hasProjectFiles: hasAnyProjectMarker
|
|
2122
|
-
};
|
|
2938
|
+
if (n8nEvidence.length > 0) {
|
|
2939
|
+
items.push(candidate("skill", "n8n-workflows", n8nEvidence, "applied"));
|
|
2940
|
+
} else if (hasPurpose(profile, ["automation"])) {
|
|
2941
|
+
items.push(candidate("skill", "n8n-workflows", purpose, "recommended-confirmation"));
|
|
2123
2942
|
}
|
|
2124
|
-
if (
|
|
2125
|
-
|
|
2943
|
+
if (sqlEvidence.length > 0) {
|
|
2944
|
+
items.push(candidate("skill", "sql-postgres", sqlEvidence, "applied"));
|
|
2945
|
+
} else if (profile.services.database?.toLowerCase().includes("postgres")) {
|
|
2946
|
+
items.push(
|
|
2947
|
+
candidate(
|
|
2948
|
+
"skill",
|
|
2949
|
+
"sql-postgres",
|
|
2950
|
+
[{ source: "configuration", value: `database:${profile.services.database}` }],
|
|
2951
|
+
"recommended-confirmation"
|
|
2952
|
+
)
|
|
2953
|
+
);
|
|
2126
2954
|
}
|
|
2127
|
-
if (
|
|
2128
|
-
|
|
2955
|
+
if (promptEvidence.length > 0) {
|
|
2956
|
+
items.push(candidate("skill", "prompts-markdown", promptEvidence, "applied"));
|
|
2129
2957
|
}
|
|
2130
|
-
if (
|
|
2131
|
-
|
|
2958
|
+
if (infraEvidence.length > 0) {
|
|
2959
|
+
items.push(candidate("pack", "devops", infraEvidence, "applied"));
|
|
2132
2960
|
}
|
|
2133
|
-
if (
|
|
2134
|
-
|
|
2961
|
+
if (profile.stack.testCommands.length > 0 && packageEvidence.length > 0) {
|
|
2962
|
+
items.push(candidate("pack", "quality", packageEvidence, "applied"));
|
|
2963
|
+
items.push(candidate("agent", "test-suites", packageEvidence, "applied"));
|
|
2135
2964
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
".vscode"
|
|
2147
|
-
]);
|
|
2148
|
-
function isGreenfieldByEntries(entries) {
|
|
2149
|
-
const meaningful = entries.filter((entry) => !GREENFIELD_SAFE_FILES.has(entry));
|
|
2150
|
-
return meaningful.length === 0;
|
|
2151
|
-
}
|
|
2152
|
-
async function runScanner(rootDir) {
|
|
2153
|
-
const normalizedRoot = path22.resolve(rootDir);
|
|
2154
|
-
const entries = await listDirectory(normalizedRoot);
|
|
2155
|
-
const stack = await detectStack(normalizedRoot);
|
|
2156
|
-
const isGreenfield = isGreenfieldByEntries(entries) || !stack.hasProjectFiles;
|
|
2157
|
-
return {
|
|
2158
|
-
rootDir: normalizedRoot,
|
|
2159
|
-
isGreenfield,
|
|
2160
|
-
stack,
|
|
2161
|
-
git: await detectGit(normalizedRoot),
|
|
2162
|
-
ide: await detectIde(normalizedRoot),
|
|
2163
|
-
infra: await detectInfra(normalizedRoot),
|
|
2164
|
-
services: await detectServices(normalizedRoot)
|
|
2165
|
-
};
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
// src/utils/prompts.ts
|
|
2169
|
-
import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
|
|
2170
|
-
var WORKSPACE_SKIN_MODE_DEFAULTS = {
|
|
2171
|
-
default: "autopilot",
|
|
2172
|
-
modes: {
|
|
2173
|
-
"continue-plan": "autopilot",
|
|
2174
|
-
"run-plan": "night-shift",
|
|
2175
|
-
"cli-run-plan": "ghost-runner"
|
|
2965
|
+
const pmTools = (profile.services.projectManagement ?? []).filter((tool) => tool !== "none");
|
|
2966
|
+
if (pmTools.length > 0) {
|
|
2967
|
+
items.push(
|
|
2968
|
+
candidate(
|
|
2969
|
+
"pack",
|
|
2970
|
+
"project-management",
|
|
2971
|
+
pmTools.map((tool) => ({ source: "configuration", value: `projectManagement:${tool}` })),
|
|
2972
|
+
"recommended-confirmation"
|
|
2973
|
+
)
|
|
2974
|
+
);
|
|
2176
2975
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
if (choice.kind === "skip") return null;
|
|
2180
|
-
if (choice.kind === "mode-defaults")
|
|
2181
|
-
return { ...WORKSPACE_SKIN_MODE_DEFAULTS, modes: { ...WORKSPACE_SKIN_MODE_DEFAULTS.modes } };
|
|
2182
|
-
return {
|
|
2183
|
-
default: choice.id,
|
|
2184
|
-
modes: {
|
|
2185
|
-
"continue-plan": choice.id,
|
|
2186
|
-
"run-plan": choice.id,
|
|
2187
|
-
"cli-run-plan": choice.id
|
|
2188
|
-
}
|
|
2189
|
-
};
|
|
2190
|
-
}
|
|
2191
|
-
function ensureNotCancelled(value) {
|
|
2192
|
-
if (isCancel(value)) {
|
|
2193
|
-
cancel("Operation cancelled.");
|
|
2194
|
-
process.exit(0);
|
|
2195
|
-
}
|
|
2196
|
-
return value;
|
|
2197
|
-
}
|
|
2198
|
-
async function askIdeAndPlan(current) {
|
|
2199
|
-
const ide = ensureNotCancelled(
|
|
2200
|
-
await select({
|
|
2201
|
-
message: "Which is your main IDE?",
|
|
2202
|
-
initialValue: current.ide === "unknown" ? void 0 : current.ide,
|
|
2203
|
-
options: [
|
|
2204
|
-
{ label: "Cursor", value: "cursor" },
|
|
2205
|
-
{ label: "VS Code", value: "vscode" },
|
|
2206
|
-
{ label: "Windsurf", value: "windsurf" },
|
|
2207
|
-
{ label: "Other", value: "other" }
|
|
2208
|
-
]
|
|
2209
|
-
})
|
|
2210
|
-
);
|
|
2211
|
-
const plan = ensureNotCancelled(
|
|
2212
|
-
await select({
|
|
2213
|
-
message: "What's your IDE plan?",
|
|
2214
|
-
initialValue: current.plan === "default" ? void 0 : current.plan,
|
|
2215
|
-
options: [
|
|
2216
|
-
{ label: "Cursor Free", value: "cursor-free" },
|
|
2217
|
-
{ label: "Cursor Pro / Business", value: "cursor-pro" },
|
|
2218
|
-
{ label: "VS Code + Copilot Free", value: "vscode-free" },
|
|
2219
|
-
{ label: "VS Code + Copilot Pro / Business", value: "vscode-pro" },
|
|
2220
|
-
{ label: "Windsurf", value: "windsurf" },
|
|
2221
|
-
{ label: "Don't know / default", value: "default" }
|
|
2222
|
-
]
|
|
2223
|
-
})
|
|
2224
|
-
);
|
|
2225
|
-
return { ide, plan };
|
|
2226
|
-
}
|
|
2227
|
-
async function askGitWorkflow(current) {
|
|
2228
|
-
return ensureNotCancelled(
|
|
2229
|
-
await select({
|
|
2230
|
-
message: "What's your Git workflow?",
|
|
2231
|
-
initialValue: current === "unknown" ? void 0 : current,
|
|
2232
|
-
options: [
|
|
2233
|
-
{ label: "trunk-based (direct to main)", value: "trunk-based" },
|
|
2234
|
-
{ label: "feature branch -> PR/MR -> main", value: "feature-pr" },
|
|
2235
|
-
{ label: "gitflow (develop/release/main)", value: "gitflow" },
|
|
2236
|
-
{ label: "staging -> production (staging -> main)", value: "homolog-prod" }
|
|
2237
|
-
]
|
|
2238
|
-
})
|
|
2976
|
+
const safetyEvidence = readinessEvidence(report, "safety.secrets").filter(
|
|
2977
|
+
(item) => item.value.startsWith("tracked:")
|
|
2239
2978
|
);
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
{ label: "ClickUp", value: "clickup" },
|
|
2251
|
-
{ label: "Azure Boards", value: "azure-boards" },
|
|
2252
|
-
{ label: "Asana", value: "asana" },
|
|
2253
|
-
{ label: "Trello", value: "trello" },
|
|
2254
|
-
{ label: "Shortcut", value: "shortcut" },
|
|
2255
|
-
{ label: "Notion", value: "notion" },
|
|
2256
|
-
{ label: "YouTrack", value: "youtrack" }
|
|
2257
|
-
],
|
|
2258
|
-
required: false
|
|
2259
|
-
})
|
|
2979
|
+
if (safetyEvidence.length > 0) {
|
|
2980
|
+
items.push(candidate("pack", "cybersec", safetyEvidence, "recommended-confirmation"));
|
|
2981
|
+
}
|
|
2982
|
+
items.push(
|
|
2983
|
+
candidate(
|
|
2984
|
+
"command",
|
|
2985
|
+
"start-project",
|
|
2986
|
+
readinessEvidence(report, "agent-kit.context"),
|
|
2987
|
+
"applied"
|
|
2988
|
+
)
|
|
2260
2989
|
);
|
|
2990
|
+
const availableItems = items.filter((item) => item !== null);
|
|
2991
|
+
return availableItems.map(
|
|
2992
|
+
(item) => componentAvailable(registry, item) ? item : { ...item, status: "unavailable" }
|
|
2993
|
+
).sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`));
|
|
2994
|
+
}
|
|
2995
|
+
function renderProjectContext(profile) {
|
|
2996
|
+
const sections = ["# Project Context", "", "Verified repository facts:"];
|
|
2997
|
+
const purpose = purposeEvidence(profile);
|
|
2998
|
+
if (purpose.length > 0 && profile.purpose.value !== "unknown") {
|
|
2999
|
+
sections.push(`- Purpose: ${profile.purpose.value}.`);
|
|
3000
|
+
}
|
|
3001
|
+
const packageEvidence = profile.stack.packageManagerEvidence ?? [];
|
|
3002
|
+
if (packageEvidence.length > 0) {
|
|
3003
|
+
sections.push(`- Runtime family: ${profile.stack.language}.`);
|
|
3004
|
+
if (profile.stack.packageManager) {
|
|
3005
|
+
sections.push(`- Package manager: ${profile.stack.packageManager}.`);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if (profile.git.provider && (profile.git.providerEvidence?.length ?? 0) > 0) {
|
|
3009
|
+
sections.push(`- Git provider: ${profile.git.provider}.`);
|
|
3010
|
+
}
|
|
3011
|
+
if (profile.infra.ci !== "none" && profile.infra.ciFiles.length > 0) {
|
|
3012
|
+
sections.push(`- CI: ${profile.infra.ci}.`);
|
|
3013
|
+
}
|
|
3014
|
+
if (profile.context.sources.length > 0) {
|
|
3015
|
+
sections.push("", "## Sources", ...profile.context.sources.map((item) => `- ${item.value}`));
|
|
3016
|
+
}
|
|
3017
|
+
return `${sections.join("\n")}
|
|
3018
|
+
`;
|
|
2261
3019
|
}
|
|
2262
|
-
async function
|
|
2263
|
-
const
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
{ label: "Skip for now", value: "skip" }
|
|
2276
|
-
]
|
|
2277
|
-
})
|
|
2278
|
-
);
|
|
2279
|
-
if (value === "skip") return { kind: "skip" };
|
|
2280
|
-
if (value === "mode-defaults") return { kind: "mode-defaults" };
|
|
2281
|
-
return { kind: "skin", id: value };
|
|
2282
|
-
}
|
|
2283
|
-
async function runExistingProjectWizard(scan) {
|
|
2284
|
-
const ide = await askIdeAndPlan(scan.ide);
|
|
2285
|
-
const workflow = await askGitWorkflow(scan.git.workflow);
|
|
2286
|
-
const projectManagement = await askProjectManagement(scan.services.projectManagement ?? []);
|
|
2287
|
-
const installHooks = ensureNotCancelled(
|
|
2288
|
-
await confirm({
|
|
2289
|
-
message: "Install git hooks? (pre-commit: secrets + lint)",
|
|
2290
|
-
initialValue: true
|
|
2291
|
-
})
|
|
2292
|
-
);
|
|
2293
|
-
const workspaceSkinChoice = await askWorkspaceSkin();
|
|
3020
|
+
async function createOwnedFile(rootDir, relativePath, content, evidence) {
|
|
3021
|
+
const target = path21.join(rootDir, relativePath);
|
|
3022
|
+
if (await fileExists(target)) {
|
|
3023
|
+
return {
|
|
3024
|
+
kind: "file",
|
|
3025
|
+
id: relativePath,
|
|
3026
|
+
path: relativePath,
|
|
3027
|
+
status: "skipped-customized",
|
|
3028
|
+
evidence
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
await ensureDir(path21.dirname(target));
|
|
3032
|
+
await writeFile5(target, content, "utf8");
|
|
2294
3033
|
return {
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
services: {
|
|
2301
|
-
...scan.services,
|
|
2302
|
-
projectManagement: projectManagement.length > 0 ? projectManagement : void 0
|
|
2303
|
-
},
|
|
2304
|
-
installHooks,
|
|
2305
|
-
selectedCoreComponents: [
|
|
2306
|
-
"git-workflow",
|
|
2307
|
-
"security-review",
|
|
2308
|
-
"clean-code",
|
|
2309
|
-
"docs-repo",
|
|
2310
|
-
"ide-guide"
|
|
2311
|
-
],
|
|
2312
|
-
workspaceSkinChoice
|
|
3034
|
+
kind: "file",
|
|
3035
|
+
id: relativePath,
|
|
3036
|
+
path: relativePath,
|
|
3037
|
+
status: "applied",
|
|
3038
|
+
evidence
|
|
2313
3039
|
};
|
|
2314
3040
|
}
|
|
2315
|
-
async function
|
|
2316
|
-
const
|
|
2317
|
-
|
|
2318
|
-
message: "What will be the main stack?",
|
|
2319
|
-
options: [
|
|
2320
|
-
{ label: "Node.js (JavaScript/TypeScript)", value: "node" },
|
|
2321
|
-
{ label: "Python", value: "python" },
|
|
2322
|
-
{ label: "Go", value: "go" },
|
|
2323
|
-
{ label: "Ruby", value: "ruby" },
|
|
2324
|
-
{ label: "Java / Kotlin", value: "java" },
|
|
2325
|
-
{ label: "PHP", value: "php" },
|
|
2326
|
-
{ label: "Rust", value: "rust" },
|
|
2327
|
-
{ label: "C# / .NET", value: "dotnet" },
|
|
2328
|
-
{ label: "Other", value: "other" }
|
|
2329
|
-
]
|
|
2330
|
-
})
|
|
2331
|
-
);
|
|
2332
|
-
const framework = ensureNotCancelled(
|
|
2333
|
-
await text({
|
|
2334
|
-
message: "Main framework? (ex: nextjs, nestjs, django, rails, spring, none)",
|
|
2335
|
-
placeholder: "none",
|
|
2336
|
-
initialValue: "none",
|
|
2337
|
-
validate(value) {
|
|
2338
|
-
return value.trim().length === 0 ? "Enter a framework or 'none'" : void 0;
|
|
2339
|
-
}
|
|
2340
|
-
})
|
|
2341
|
-
);
|
|
2342
|
-
const database = ensureNotCancelled(
|
|
2343
|
-
await select({
|
|
2344
|
-
message: "Will you use a database?",
|
|
2345
|
-
options: [
|
|
2346
|
-
{ label: "PostgreSQL", value: "postgresql" },
|
|
2347
|
-
{ label: "MySQL", value: "mysql" },
|
|
2348
|
-
{ label: "MongoDB", value: "mongodb" },
|
|
2349
|
-
{ label: "SQLite", value: "sqlite" },
|
|
2350
|
-
{ label: "SQL Server", value: "sqlserver" },
|
|
2351
|
-
{ label: "None for now", value: "none" }
|
|
2352
|
-
]
|
|
2353
|
-
})
|
|
2354
|
-
);
|
|
2355
|
-
const orm = ensureNotCancelled(
|
|
2356
|
-
await text({
|
|
2357
|
-
message: "ORM / query builder?",
|
|
2358
|
-
placeholder: "prisma, drizzle, sequelize, typeorm, none",
|
|
2359
|
-
initialValue: "none"
|
|
2360
|
-
})
|
|
2361
|
-
);
|
|
2362
|
-
const ide = await askIdeAndPlan(scan.ide);
|
|
2363
|
-
const workflow = await askGitWorkflow(scan.git.workflow);
|
|
2364
|
-
const projectManagement = await askProjectManagement();
|
|
2365
|
-
const installHooks = ensureNotCancelled(
|
|
2366
|
-
await confirm({
|
|
2367
|
-
message: "Install git hooks? (pre-commit: secrets + lint)",
|
|
2368
|
-
initialValue: true
|
|
2369
|
-
})
|
|
2370
|
-
);
|
|
2371
|
-
const workspaceSkinChoice = await askWorkspaceSkin();
|
|
2372
|
-
return {
|
|
2373
|
-
rootDir: scan.rootDir,
|
|
2374
|
-
stack: {
|
|
2375
|
-
language,
|
|
2376
|
-
framework: framework === "none" ? void 0 : framework,
|
|
2377
|
-
hasProjectFiles: false
|
|
2378
|
-
},
|
|
2379
|
-
git: {
|
|
2380
|
-
...scan.git,
|
|
2381
|
-
workflow
|
|
2382
|
-
},
|
|
2383
|
-
ide,
|
|
2384
|
-
infra: scan.infra,
|
|
2385
|
-
services: {
|
|
2386
|
-
database: database === "none" ? void 0 : database,
|
|
2387
|
-
orm: orm === "none" ? void 0 : orm,
|
|
2388
|
-
projectManagement: projectManagement.length > 0 ? projectManagement : void 0
|
|
2389
|
-
},
|
|
2390
|
-
installHooks,
|
|
2391
|
-
selectedCoreComponents: [
|
|
2392
|
-
"git-workflow",
|
|
2393
|
-
"security-review",
|
|
2394
|
-
"clean-code",
|
|
2395
|
-
"docs-repo",
|
|
2396
|
-
"ide-guide"
|
|
2397
|
-
],
|
|
2398
|
-
workspaceSkinChoice
|
|
2399
|
-
};
|
|
3041
|
+
async function packTargets(registryRoot, packId) {
|
|
3042
|
+
const manifest = await loadPackManifest(registryRoot, packId);
|
|
3043
|
+
return manifest.members.map((member) => packMemberTargets(member).targetRel);
|
|
2400
3044
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
description: "Project root directory",
|
|
2420
|
-
default: process.cwd()
|
|
3045
|
+
async function existingTargets(projectRoot, targets) {
|
|
3046
|
+
const checks = await Promise.all(
|
|
3047
|
+
targets.map(
|
|
3048
|
+
async (target) => await fileExists(path21.join(projectRoot, target)) ? target : null
|
|
3049
|
+
)
|
|
3050
|
+
);
|
|
3051
|
+
return checks.filter((target) => target !== null);
|
|
3052
|
+
}
|
|
3053
|
+
async function applyPersonalization(input) {
|
|
3054
|
+
const planned = buildPersonalizationPlan(input.profile, input.report, input.registry);
|
|
3055
|
+
const componentResults = [];
|
|
3056
|
+
const protectedPaths = new Set(input.manifest.protected ?? []);
|
|
3057
|
+
const packs = new Set(input.manifest.packs ?? []);
|
|
3058
|
+
const skills = new Set(input.manifest.skills ?? []);
|
|
3059
|
+
for (const item of planned) {
|
|
3060
|
+
if (item.status !== "applied") {
|
|
3061
|
+
componentResults.push(item);
|
|
3062
|
+
continue;
|
|
2421
3063
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
logger.success(`Profile saved in ${configPath}`);
|
|
2435
|
-
const skinConfig = workspaceSkinChoice !== void 0 ? workspaceSkinConfigFromChoice(workspaceSkinChoice) : null;
|
|
2436
|
-
if (skinConfig) {
|
|
2437
|
-
const contextConfigPath = await mergeWorkspaceSkinConfig(scan.rootDir, skinConfig);
|
|
2438
|
-
logger.success(`Workspace skin saved in ${contextConfigPath}`);
|
|
2439
|
-
}
|
|
2440
|
-
await generateFromProfile(profile);
|
|
2441
|
-
try {
|
|
2442
|
-
const registry = await resolveRegistryRoot({ cwd: scan.rootDir });
|
|
2443
|
-
const stats = await installSkillsByIds(
|
|
2444
|
-
registry.root,
|
|
2445
|
-
scan.rootDir,
|
|
2446
|
-
profile.selectedCoreComponents
|
|
3064
|
+
if (item.kind === "skill") {
|
|
3065
|
+
const skill = allSkills(input.registry).find((entry) => entry.id === item.id);
|
|
3066
|
+
if (!skill) {
|
|
3067
|
+
componentResults.push({ ...item, status: "unavailable" });
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
const target = path21.posix.join(
|
|
3071
|
+
".cursor",
|
|
3072
|
+
"skills",
|
|
3073
|
+
skill.path.includes("/core/") ? "core" : "community",
|
|
3074
|
+
skill.id,
|
|
3075
|
+
"SKILL.md"
|
|
2447
3076
|
);
|
|
2448
|
-
if (
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
} else {
|
|
2453
|
-
logger.info("No new skills to copy (already installed or empty selection).");
|
|
3077
|
+
if (await fileExists(path21.join(input.rootDir, target))) {
|
|
3078
|
+
componentResults.push({ ...item, status: "skipped-customized", path: target });
|
|
3079
|
+
protectedPaths.add(target);
|
|
3080
|
+
continue;
|
|
2454
3081
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
3082
|
+
await installSkill(input.registryRoot, input.rootDir, skill);
|
|
3083
|
+
skills.add(item.id);
|
|
3084
|
+
protectedPaths.add(target);
|
|
3085
|
+
componentResults.push({ ...item, path: target });
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
if (item.kind === "pack") {
|
|
3089
|
+
const targets = await packTargets(input.registryRoot, item.id);
|
|
3090
|
+
const customizedTargets = await existingTargets(input.rootDir, targets);
|
|
3091
|
+
await installPack(input.registryRoot, input.rootDir, item.id, {
|
|
3092
|
+
protectedGlobs: customizedTargets
|
|
3093
|
+
});
|
|
3094
|
+
for (const target of targets) protectedPaths.add(target);
|
|
3095
|
+
packs.add(item.id);
|
|
3096
|
+
componentResults.push(
|
|
3097
|
+
customizedTargets.length > 0 ? { ...item, status: "skipped-customized" } : item
|
|
2458
3098
|
);
|
|
3099
|
+
continue;
|
|
2459
3100
|
}
|
|
2460
|
-
|
|
2461
|
-
console.log(` Open this folder in Cursor: ${scan.rootDir}`);
|
|
2462
|
-
console.log(" Run /onboard in chat; then /start-project when you have a goal");
|
|
2463
|
-
console.log(" Optional: agent-kit status");
|
|
2464
|
-
outro("Setup completed.");
|
|
3101
|
+
componentResults.push(item);
|
|
2465
3102
|
}
|
|
2466
|
-
|
|
3103
|
+
const profileEvidence = uniqueEvidence([
|
|
3104
|
+
...purposeEvidence(input.profile),
|
|
3105
|
+
...input.profile.stack.packageManagerEvidence ?? [],
|
|
3106
|
+
...input.profile.git.providerEvidence ?? [],
|
|
3107
|
+
...input.profile.context.sources
|
|
3108
|
+
]);
|
|
3109
|
+
const fileResults = await Promise.all([
|
|
3110
|
+
createOwnedFile(
|
|
3111
|
+
input.rootDir,
|
|
3112
|
+
CONTEXT_PATH,
|
|
3113
|
+
renderProjectContext(input.profile),
|
|
3114
|
+
profileEvidence
|
|
3115
|
+
),
|
|
3116
|
+
createOwnedFile(
|
|
3117
|
+
input.rootDir,
|
|
3118
|
+
AGENTS_PATH,
|
|
3119
|
+
"# Repository Agent Guidance\n\nUse `.cursor/project-context.md` for verified repository facts. Preserve existing project-owned guidance and request confirmation before adding optional integrations.\n",
|
|
3120
|
+
profileEvidence
|
|
3121
|
+
)
|
|
3122
|
+
]);
|
|
3123
|
+
protectedPaths.add(CONTEXT_PATH);
|
|
3124
|
+
protectedPaths.add(AGENTS_PATH);
|
|
3125
|
+
const result = {
|
|
3126
|
+
contractVersion: PERSONALIZATION_CONTRACT_VERSION,
|
|
3127
|
+
generatorVersion: input.generatorVersion,
|
|
3128
|
+
repositoryFingerprint: input.report.repositoryFingerprint,
|
|
3129
|
+
items: [...fileResults, ...componentResults],
|
|
3130
|
+
protectedPaths: [...protectedPaths].sort()
|
|
3131
|
+
};
|
|
3132
|
+
await writeJson(path21.join(input.rootDir, RESULT_PATH), result);
|
|
3133
|
+
return {
|
|
3134
|
+
result,
|
|
3135
|
+
manifest: {
|
|
3136
|
+
...input.manifest,
|
|
3137
|
+
packs: [...packs].sort(),
|
|
3138
|
+
skills: [...skills].sort(),
|
|
3139
|
+
protected: result.protectedPaths,
|
|
3140
|
+
personalization: {
|
|
3141
|
+
contractVersion: PERSONALIZATION_CONTRACT_VERSION,
|
|
3142
|
+
generatorVersion: input.generatorVersion,
|
|
3143
|
+
origin: "repository-profile",
|
|
3144
|
+
resultPath: RESULT_PATH
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
async function readRepositoryProfile(rootDir) {
|
|
3150
|
+
const target = path21.join(rootDir, ".cursor/agent-kit.config.json");
|
|
3151
|
+
if (!await fileExists(target)) return null;
|
|
3152
|
+
return JSON.parse(await readFile10(target, "utf8"));
|
|
3153
|
+
}
|
|
2467
3154
|
|
|
2468
|
-
// src/
|
|
2469
|
-
import
|
|
2470
|
-
import {
|
|
3155
|
+
// src/lifecycle/onboard-migration.ts
|
|
3156
|
+
import { createHash as createHash3 } from "crypto";
|
|
3157
|
+
import { readFile as readFile11, unlink } from "fs/promises";
|
|
3158
|
+
import path22 from "path";
|
|
3159
|
+
var LEGACY_ONBOARD_PATH = ".cursor/commands/onboard.md";
|
|
3160
|
+
var NAMESPACED_ONBOARD_PATH = ".cursor/commands/agent-kit-onboard.md";
|
|
3161
|
+
var MANAGED_LEGACY_HASHES = /* @__PURE__ */ new Set([
|
|
3162
|
+
"b274a68941813f19b185893cb7c5561dff027f53270890029992f208e24992fe"
|
|
3163
|
+
]);
|
|
3164
|
+
async function migrateLegacyOnboardCommand(projectRoot, managedHashes = MANAGED_LEGACY_HASHES) {
|
|
3165
|
+
const legacyPath = path22.join(projectRoot, LEGACY_ONBOARD_PATH);
|
|
3166
|
+
if (!await fileExists(legacyPath)) return "absent";
|
|
3167
|
+
const namespacedPath = path22.join(projectRoot, NAMESPACED_ONBOARD_PATH);
|
|
3168
|
+
if (!await fileExists(namespacedPath)) return "preserved-customized";
|
|
3169
|
+
const content = await readFile11(legacyPath);
|
|
3170
|
+
const hash = createHash3("sha256").update(content).digest("hex");
|
|
3171
|
+
if (!managedHashes.has(hash)) return "preserved-customized";
|
|
3172
|
+
await unlink(legacyPath);
|
|
3173
|
+
return "removed-managed";
|
|
3174
|
+
}
|
|
2471
3175
|
|
|
2472
3176
|
// src/lifecycle/sync.ts
|
|
2473
3177
|
async function installL0(registryRoot, projectRoot, protectedGlobs) {
|
|
@@ -2482,6 +3186,12 @@ async function installL0(registryRoot, projectRoot, protectedGlobs) {
|
|
|
2482
3186
|
);
|
|
2483
3187
|
recordOutcome(stats, artifact.target, outcome);
|
|
2484
3188
|
}
|
|
3189
|
+
const migration = await migrateLegacyOnboardCommand(projectRoot);
|
|
3190
|
+
if (migration === "removed-managed") {
|
|
3191
|
+
stats.removed.push(".cursor/commands/onboard.md");
|
|
3192
|
+
} else if (migration === "preserved-customized") {
|
|
3193
|
+
stats.collisions.push(".cursor/commands/onboard.md");
|
|
3194
|
+
}
|
|
2485
3195
|
return stats;
|
|
2486
3196
|
}
|
|
2487
3197
|
async function syncFromManifest(registryRoot, projectRoot, manifest) {
|
|
@@ -2511,11 +3221,73 @@ function parsePackList(raw) {
|
|
|
2511
3221
|
)
|
|
2512
3222
|
];
|
|
2513
3223
|
}
|
|
2514
|
-
function
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
console.log("
|
|
2518
|
-
console.log(
|
|
3224
|
+
function printReadinessNarrative(result) {
|
|
3225
|
+
const { summary, pendingActions } = result.readiness;
|
|
3226
|
+
const fixed = result.safeChanges.filter((change) => change.status === "applied").length;
|
|
3227
|
+
console.log("\nRepository readiness");
|
|
3228
|
+
console.log(
|
|
3229
|
+
` ready: ${summary.ready}, choices: ${summary.needs_choice}, manual: ${summary.manual}, blocked: ${summary.blocked}`
|
|
3230
|
+
);
|
|
3231
|
+
console.log(` safe fixes applied: ${fixed}`);
|
|
3232
|
+
console.log(` pending actions: ${pendingActions.length}`);
|
|
3233
|
+
console.log(
|
|
3234
|
+
pendingActions.length > 0 ? "Next: run /agent-kit-onboard in Cursor to resolve the first pending action" : "Next: run /start-project in Cursor when you have a deliverable"
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
async function performInstall(options) {
|
|
3238
|
+
const projectRoot = path23.resolve(options.cwd);
|
|
3239
|
+
const packs = parsePackList(options.pack);
|
|
3240
|
+
const existing = await loadAgentKitManifest(projectRoot);
|
|
3241
|
+
const registry = await resolveRegistryFromCli({
|
|
3242
|
+
cwd: projectRoot,
|
|
3243
|
+
registry: options.registry,
|
|
3244
|
+
url: options.url,
|
|
3245
|
+
ref: options.ref,
|
|
3246
|
+
refresh: options.refresh,
|
|
3247
|
+
manifest: existing
|
|
3248
|
+
});
|
|
3249
|
+
const draft = buildManifest({
|
|
3250
|
+
version: KIT_VERSION,
|
|
3251
|
+
profile: options.profile ?? existing?.profile ?? "default",
|
|
3252
|
+
packs: packs.length > 0 ? packs : existing?.packs,
|
|
3253
|
+
skills: existing?.skills,
|
|
3254
|
+
protected: existing?.protected,
|
|
3255
|
+
personalization: existing?.personalization,
|
|
3256
|
+
registryUrl: registry.url ?? existing?.registry?.url,
|
|
3257
|
+
registryRef: registry.ref ?? existing?.registry?.ref
|
|
3258
|
+
});
|
|
3259
|
+
const stats = (draft.packs?.length ?? 0) > 0 || (draft.skills?.length ?? 0) > 0 ? await syncFromManifest(registry.root, projectRoot, draft) : await installL0(registry.root, projectRoot, resolveProtectedGlobs(draft));
|
|
3260
|
+
const manifestPath = await saveManifest(projectRoot, draft);
|
|
3261
|
+
const readinessExecution = await executeSafeReadinessFixes(projectRoot, {
|
|
3262
|
+
generatorVersion: KIT_VERSION
|
|
3263
|
+
});
|
|
3264
|
+
let readiness = readinessExecution.after;
|
|
3265
|
+
const profile = await readRepositoryProfile(projectRoot);
|
|
3266
|
+
if (profile) {
|
|
3267
|
+
const registryIndex = await loadRegistry(registry.root);
|
|
3268
|
+
const personalization = await applyPersonalization({
|
|
3269
|
+
rootDir: projectRoot,
|
|
3270
|
+
registryRoot: registry.root,
|
|
3271
|
+
profile,
|
|
3272
|
+
report: readinessExecution.after,
|
|
3273
|
+
registry: registryIndex,
|
|
3274
|
+
manifest: draft,
|
|
3275
|
+
generatorVersion: KIT_VERSION
|
|
3276
|
+
});
|
|
3277
|
+
await saveManifest(projectRoot, personalization.manifest);
|
|
3278
|
+
readiness = createReadinessReport(await runScanner(projectRoot), {
|
|
3279
|
+
generatorVersion: KIT_VERSION
|
|
3280
|
+
});
|
|
3281
|
+
readiness.appliedSafeFixes = readinessExecution.after.appliedSafeFixes;
|
|
3282
|
+
}
|
|
3283
|
+
await writeReadinessSnapshot(projectRoot, readiness);
|
|
3284
|
+
return {
|
|
3285
|
+
projectRoot,
|
|
3286
|
+
manifestPath,
|
|
3287
|
+
stats,
|
|
3288
|
+
readiness,
|
|
3289
|
+
safeChanges: readinessExecution.changes
|
|
3290
|
+
};
|
|
2519
3291
|
}
|
|
2520
3292
|
var installCommand = defineCommand6({
|
|
2521
3293
|
meta: {
|
|
@@ -2539,50 +3311,61 @@ var installCommand = defineCommand6({
|
|
|
2539
3311
|
...REGISTRY_CLI_ARGS
|
|
2540
3312
|
},
|
|
2541
3313
|
async run({ args }) {
|
|
2542
|
-
const projectRoot =
|
|
3314
|
+
const projectRoot = path23.resolve(args.cwd);
|
|
2543
3315
|
logger.info(`Installing into: ${projectRoot}`);
|
|
2544
3316
|
const packs = parsePackList(args.pack);
|
|
2545
3317
|
for (const id of packs) {
|
|
2546
3318
|
if (!DOMAIN_PACK_IDS.includes(id)) {
|
|
2547
|
-
logger.warn(`Pack '${id}' is not in the known L1 list
|
|
3319
|
+
logger.warn(`Pack '${id}' is not in the known L1 list; will still try registry.`);
|
|
2548
3320
|
}
|
|
2549
3321
|
}
|
|
2550
|
-
const
|
|
2551
|
-
const registry = await resolveRegistryFromCli({
|
|
3322
|
+
const result = await performInstall({
|
|
2552
3323
|
cwd: projectRoot,
|
|
3324
|
+
profile: args.profile,
|
|
3325
|
+
pack: args.pack,
|
|
2553
3326
|
registry: args.registry,
|
|
2554
3327
|
url: args.url,
|
|
2555
3328
|
ref: args.ref,
|
|
2556
|
-
refresh: args.refresh
|
|
2557
|
-
manifest: existing
|
|
2558
|
-
});
|
|
2559
|
-
logger.info(`Registry: ${registry.root} (${registry.source})`);
|
|
2560
|
-
const draft = buildManifest({
|
|
2561
|
-
version: KIT_VERSION,
|
|
2562
|
-
profile: args.profile ?? existing?.profile ?? "default",
|
|
2563
|
-
packs: packs.length > 0 ? packs : existing?.packs,
|
|
2564
|
-
skills: existing?.skills,
|
|
2565
|
-
protected: existing?.protected,
|
|
2566
|
-
registryUrl: registry.url ?? existing?.registry?.url,
|
|
2567
|
-
registryRef: registry.ref ?? existing?.registry?.ref
|
|
3329
|
+
refresh: args.refresh
|
|
2568
3330
|
});
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
3331
|
+
logApplyStats(result.stats);
|
|
3332
|
+
logger.success(`Manifest written: ${result.manifestPath}`);
|
|
3333
|
+
logger.success("Readiness snapshot written: .cursor/context/readiness.json");
|
|
3334
|
+
printReadinessNarrative(result);
|
|
3335
|
+
}
|
|
3336
|
+
});
|
|
3337
|
+
|
|
3338
|
+
// src/commands/init.ts
|
|
3339
|
+
async function runInitCompatibility(cwd, installer = performInstall) {
|
|
3340
|
+
return installer({ cwd });
|
|
3341
|
+
}
|
|
3342
|
+
var initCommand = defineCommand7({
|
|
3343
|
+
meta: {
|
|
3344
|
+
name: "init",
|
|
3345
|
+
description: "Guided compatibility entry point for install and repository readiness."
|
|
3346
|
+
},
|
|
3347
|
+
args: {
|
|
3348
|
+
cwd: {
|
|
3349
|
+
type: "string",
|
|
3350
|
+
description: "Project root directory",
|
|
3351
|
+
default: process.cwd()
|
|
2575
3352
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
3353
|
+
},
|
|
3354
|
+
async run({ args }) {
|
|
3355
|
+
intro(`agent-kit v${KIT_VERSION}`);
|
|
3356
|
+
logger.info("init now uses the canonical install and readiness workflow.");
|
|
3357
|
+
const result = await runInitCompatibility(args.cwd);
|
|
3358
|
+
const pending = result.readiness.pendingActions.length;
|
|
3359
|
+
logger.success(`L0 and readiness prepared in ${result.projectRoot}`);
|
|
3360
|
+
outro(
|
|
3361
|
+
pending > 0 ? "Next: run /agent-kit-onboard in Cursor to resolve the first pending action." : "Next: run /start-project in Cursor when you have a deliverable."
|
|
3362
|
+
);
|
|
2580
3363
|
}
|
|
2581
3364
|
});
|
|
2582
3365
|
|
|
2583
3366
|
// src/commands/run-plan.ts
|
|
2584
|
-
import
|
|
2585
|
-
import { defineCommand as
|
|
3367
|
+
import path28 from "path";
|
|
3368
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
2586
3369
|
|
|
2587
3370
|
// src/plan-loop/backends.ts
|
|
2588
3371
|
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
@@ -2666,13 +3449,14 @@ function listBackendIds() {
|
|
|
2666
3449
|
}
|
|
2667
3450
|
|
|
2668
3451
|
// src/plan-loop/run-loop.ts
|
|
2669
|
-
import { mkdir as mkdir4, readFile as
|
|
2670
|
-
import
|
|
3452
|
+
import { mkdir as mkdir4, readFile as readFile14, rm, unlink as unlink2 } from "fs/promises";
|
|
3453
|
+
import path27 from "path";
|
|
2671
3454
|
|
|
2672
3455
|
// src/plan-loop/external-review.ts
|
|
2673
3456
|
import { spawn as spawn3 } from "child_process";
|
|
2674
|
-
import
|
|
2675
|
-
var
|
|
3457
|
+
import path24 from "path";
|
|
3458
|
+
var CANONICAL_REL = path24.join(".cursor", "scripts", "plan-external-review.sh");
|
|
3459
|
+
var FALLBACK_REL = path24.join("scripts", "plan-external-review.sh");
|
|
2676
3460
|
function isPlanExhaustedReason(reason) {
|
|
2677
3461
|
const r = reason.trim().toLowerCase();
|
|
2678
3462
|
if (!r) return false;
|
|
@@ -2689,20 +3473,32 @@ function shouldArmExternalPlanReview(input) {
|
|
|
2689
3473
|
if (input.stopReason && isPlanExhaustedReason(input.stopReason)) return true;
|
|
2690
3474
|
return false;
|
|
2691
3475
|
}
|
|
2692
|
-
async function armExternalPlanReview(root,
|
|
2693
|
-
const spawnFn =
|
|
2694
|
-
const existsFn =
|
|
2695
|
-
const log =
|
|
2696
|
-
const
|
|
2697
|
-
|
|
3476
|
+
async function armExternalPlanReview(root, options = {}) {
|
|
3477
|
+
const spawnFn = options.spawnFn ?? spawn3;
|
|
3478
|
+
const existsFn = options.existsFn ?? fileExists;
|
|
3479
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
3480
|
+
const force = options.force === true;
|
|
3481
|
+
const canonicalPath = path24.join(root, CANONICAL_REL);
|
|
3482
|
+
const fallbackPath = path24.join(root, FALLBACK_REL);
|
|
3483
|
+
let scriptPath = null;
|
|
3484
|
+
let scriptRel = CANONICAL_REL;
|
|
3485
|
+
if (await existsFn(canonicalPath)) {
|
|
3486
|
+
scriptPath = canonicalPath;
|
|
3487
|
+
scriptRel = CANONICAL_REL;
|
|
3488
|
+
} else if (await existsFn(fallbackPath)) {
|
|
3489
|
+
scriptPath = fallbackPath;
|
|
3490
|
+
scriptRel = FALLBACK_REL;
|
|
3491
|
+
}
|
|
3492
|
+
if (!scriptPath) {
|
|
2698
3493
|
log(
|
|
2699
|
-
`tip: ${
|
|
3494
|
+
`tip: ${CANONICAL_REL} missing (optional wrapper: ${FALLBACK_REL}). Manual: /plan-external-review (enable externalPlanReview in .cursor/context/config.json).`
|
|
2700
3495
|
);
|
|
2701
3496
|
return { invoked: false, exitCode: null, output: "" };
|
|
2702
3497
|
}
|
|
2703
3498
|
log("Plan exhausted: arming optional external plan review (opt-in handled by script)...");
|
|
3499
|
+
const args = force ? [scriptPath, "--force"] : [scriptPath];
|
|
2704
3500
|
return new Promise((resolve) => {
|
|
2705
|
-
const child = spawnFn("bash",
|
|
3501
|
+
const child = spawnFn("bash", args, {
|
|
2706
3502
|
cwd: root,
|
|
2707
3503
|
env: process.env
|
|
2708
3504
|
});
|
|
@@ -2720,14 +3516,14 @@ async function armExternalPlanReview(root, deps = {}) {
|
|
|
2720
3516
|
});
|
|
2721
3517
|
child.on("error", (err) => {
|
|
2722
3518
|
log(
|
|
2723
|
-
`tip: external plan review launcher failed (${err.message}). Manual: ${
|
|
3519
|
+
`tip: external plan review launcher failed (${err.message}). Manual: ${scriptRel} or /plan-external-review`
|
|
2724
3520
|
);
|
|
2725
3521
|
resolve({ invoked: true, exitCode: null, output });
|
|
2726
3522
|
});
|
|
2727
3523
|
child.on("close", (code) => {
|
|
2728
3524
|
if (code !== 0 && code !== null) {
|
|
2729
3525
|
log(
|
|
2730
|
-
`tip: external plan review exited ${code} (ignored; does not fail the loop). Manual: ${
|
|
3526
|
+
`tip: external plan review exited ${code} (ignored; does not fail the loop). Manual: ${scriptRel} or /plan-external-review`
|
|
2731
3527
|
);
|
|
2732
3528
|
}
|
|
2733
3529
|
resolve({ invoked: true, exitCode: code, output });
|
|
@@ -2736,8 +3532,8 @@ async function armExternalPlanReview(root, deps = {}) {
|
|
|
2736
3532
|
}
|
|
2737
3533
|
|
|
2738
3534
|
// src/plan-loop/plan-state.ts
|
|
2739
|
-
import { readFile as
|
|
2740
|
-
import
|
|
3535
|
+
import { readFile as readFile12, readdir as readdir3 } from "fs/promises";
|
|
3536
|
+
import path25 from "path";
|
|
2741
3537
|
function countPendingTodos(raw) {
|
|
2742
3538
|
const lines = raw.split(/\r?\n/);
|
|
2743
3539
|
let inFront = 0;
|
|
@@ -2765,18 +3561,18 @@ function countPendingTodos(raw) {
|
|
|
2765
3561
|
async function findActivePlanFile(plansDir) {
|
|
2766
3562
|
if (!await fileExists(plansDir)) return null;
|
|
2767
3563
|
const files = (await readdir3(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
|
|
2768
|
-
return files[0] ?
|
|
3564
|
+
return files[0] ? path25.join(plansDir, files[0]) : null;
|
|
2769
3565
|
}
|
|
2770
3566
|
async function readPlan(planPath) {
|
|
2771
|
-
return
|
|
3567
|
+
return readFile12(planPath, "utf8");
|
|
2772
3568
|
}
|
|
2773
3569
|
|
|
2774
3570
|
// src/plan-loop/sentinel.ts
|
|
2775
|
-
import { readFile as
|
|
3571
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2776
3572
|
var SENTINEL_RE = /LOOP_TICK_RESULT:\s*(continue|stop(?:\s*[—\-].*)?)/i;
|
|
2777
|
-
function takeFromText(
|
|
2778
|
-
if (!
|
|
2779
|
-
const m = SENTINEL_RE.exec(
|
|
3573
|
+
function takeFromText(text) {
|
|
3574
|
+
if (!text) return null;
|
|
3575
|
+
const m = SENTINEL_RE.exec(text);
|
|
2780
3576
|
if (!m?.[1]) return null;
|
|
2781
3577
|
const raw = m[1].trim();
|
|
2782
3578
|
if (raw.toLowerCase().startsWith("stop")) {
|
|
@@ -2819,7 +3615,7 @@ function parseSentinelFromLog(content) {
|
|
|
2819
3615
|
}
|
|
2820
3616
|
async function parseSentinelFromLogFile(logPath) {
|
|
2821
3617
|
try {
|
|
2822
|
-
const content = await
|
|
3618
|
+
const content = await readFile13(logPath, "utf8");
|
|
2823
3619
|
return parseSentinelFromLog(content);
|
|
2824
3620
|
} catch {
|
|
2825
3621
|
return { kind: "missing" };
|
|
@@ -2832,7 +3628,7 @@ function formatSentinelLine(sentinel) {
|
|
|
2832
3628
|
}
|
|
2833
3629
|
|
|
2834
3630
|
// src/plan-loop/skin-banners.ts
|
|
2835
|
-
import
|
|
3631
|
+
import path26 from "path";
|
|
2836
3632
|
import {
|
|
2837
3633
|
blue,
|
|
2838
3634
|
cyan as cyan2,
|
|
@@ -2868,7 +3664,7 @@ function resolveColor(name, fallback) {
|
|
|
2868
3664
|
async function resolveCliSkinId(root) {
|
|
2869
3665
|
try {
|
|
2870
3666
|
const cfg = await readJson(
|
|
2871
|
-
|
|
3667
|
+
path26.join(root, ".cursor", "context", "config.json")
|
|
2872
3668
|
);
|
|
2873
3669
|
const id = cfg?.workspaceSkin?.modes?.[CLI_RUN_PLAN_MODE];
|
|
2874
3670
|
if (typeof id === "string" && id.trim()) return id.trim();
|
|
@@ -2878,7 +3674,7 @@ async function resolveCliSkinId(root) {
|
|
|
2878
3674
|
}
|
|
2879
3675
|
async function loadSkinPack(root, skinId) {
|
|
2880
3676
|
try {
|
|
2881
|
-
const skinPath =
|
|
3677
|
+
const skinPath = path26.join(root, "registry", "skins", "core", skinId, "skin.json");
|
|
2882
3678
|
const pack = await readJson(skinPath);
|
|
2883
3679
|
if (!pack || typeof pack.id !== "string") return null;
|
|
2884
3680
|
return pack;
|
|
@@ -2937,9 +3733,9 @@ function sleep(ms) {
|
|
|
2937
3733
|
return new Promise((r) => setTimeout(r, ms));
|
|
2938
3734
|
}
|
|
2939
3735
|
async function runPlanLoop(opts) {
|
|
2940
|
-
const plansDir =
|
|
2941
|
-
const stopFile =
|
|
2942
|
-
const logDir =
|
|
3736
|
+
const plansDir = path27.join(opts.root, ".cursor", "plans");
|
|
3737
|
+
const stopFile = path27.join(opts.root, ".cursor", "loop.stop");
|
|
3738
|
+
const logDir = path27.join(opts.root, ".cursor", "loop-logs");
|
|
2943
3739
|
const planPath = await findActivePlanFile(plansDir);
|
|
2944
3740
|
if (!planPath) {
|
|
2945
3741
|
logger.error("No active plan in .cursor/plans/");
|
|
@@ -2948,7 +3744,7 @@ async function runPlanLoop(opts) {
|
|
|
2948
3744
|
const pending = async () => countPendingTodos(await readPlan(planPath));
|
|
2949
3745
|
await mkdir4(logDir, { recursive: true });
|
|
2950
3746
|
try {
|
|
2951
|
-
await
|
|
3747
|
+
await unlink2(stopFile);
|
|
2952
3748
|
} catch {
|
|
2953
3749
|
}
|
|
2954
3750
|
const onSigInt = () => {
|
|
@@ -2960,7 +3756,7 @@ async function runPlanLoop(opts) {
|
|
|
2960
3756
|
try {
|
|
2961
3757
|
const skin = await loadCliRunPlanSkin(opts.root);
|
|
2962
3758
|
const banners = createSkinBannerPrinter(skin);
|
|
2963
|
-
console.log(`Active plan: ${
|
|
3759
|
+
console.log(`Active plan: ${path27.basename(planPath)}`);
|
|
2964
3760
|
console.log(`Pending to-dos: ${await pending()} | max ticks: ${opts.maxTicks}`);
|
|
2965
3761
|
console.log(`Backend: ${opts.backend.id}`);
|
|
2966
3762
|
if (skin) {
|
|
@@ -3003,8 +3799,8 @@ async function runPlanLoop(opts) {
|
|
|
3003
3799
|
planExhausted = true;
|
|
3004
3800
|
break;
|
|
3005
3801
|
}
|
|
3006
|
-
const logPath =
|
|
3007
|
-
const relLog =
|
|
3802
|
+
const logPath = path27.join(logDir, `tick-${stamp()}.log`);
|
|
3803
|
+
const relLog = path27.relative(opts.root, logPath);
|
|
3008
3804
|
console.log("");
|
|
3009
3805
|
const tickLine = `=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`;
|
|
3010
3806
|
if (banners) banners.tickStart(tickLine);
|
|
@@ -3023,7 +3819,7 @@ async function runPlanLoop(opts) {
|
|
|
3023
3819
|
return 1;
|
|
3024
3820
|
}
|
|
3025
3821
|
try {
|
|
3026
|
-
const logText = await
|
|
3822
|
+
const logText = await readFile14(logPath, "utf8");
|
|
3027
3823
|
if (logText.includes("Too many MCP tools")) {
|
|
3028
3824
|
const msg = "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again.";
|
|
3029
3825
|
if (banners) banners.stop(msg);
|
|
@@ -3080,7 +3876,7 @@ async function runPlanLoop(opts) {
|
|
|
3080
3876
|
const finishDetail = `after ${tick} tick(s); pending: ${pendingNow}`;
|
|
3081
3877
|
if (banners) banners.phaseComplete(finishDetail);
|
|
3082
3878
|
console.log(
|
|
3083
|
-
`Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${
|
|
3879
|
+
`Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path27.relative(opts.root, logDir)}/`
|
|
3084
3880
|
);
|
|
3085
3881
|
if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
|
|
3086
3882
|
await armExternalPlanReview(opts.root);
|
|
@@ -3092,7 +3888,7 @@ async function runPlanLoop(opts) {
|
|
|
3092
3888
|
}
|
|
3093
3889
|
|
|
3094
3890
|
// src/commands/run-plan.ts
|
|
3095
|
-
var runPlanCommand =
|
|
3891
|
+
var runPlanCommand = defineCommand8({
|
|
3096
3892
|
meta: {
|
|
3097
3893
|
name: "run-plan",
|
|
3098
3894
|
description: "Headless continuous plan runner: one fresh agent per tick (LOOP_TICK_RESULT contract). Never git-prod."
|
|
@@ -3151,7 +3947,7 @@ var runPlanCommand = defineCommand7({
|
|
|
3151
3947
|
return;
|
|
3152
3948
|
}
|
|
3153
3949
|
const code = await runPlanLoop({
|
|
3154
|
-
root:
|
|
3950
|
+
root: path28.resolve(args.cwd),
|
|
3155
3951
|
maxTicks,
|
|
3156
3952
|
sleepSeconds,
|
|
3157
3953
|
model: args.model ? String(args.model) : void 0,
|
|
@@ -3163,8 +3959,8 @@ var runPlanCommand = defineCommand7({
|
|
|
3163
3959
|
});
|
|
3164
3960
|
|
|
3165
3961
|
// src/commands/scan.ts
|
|
3166
|
-
import { defineCommand as
|
|
3167
|
-
var scanCommand =
|
|
3962
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
3963
|
+
var scanCommand = defineCommand9({
|
|
3168
3964
|
meta: {
|
|
3169
3965
|
name: "scan",
|
|
3170
3966
|
description: "Scan the current repository and print detected profile."
|
|
@@ -3185,9 +3981,21 @@ var scanCommand = defineCommand8({
|
|
|
3185
3981
|
});
|
|
3186
3982
|
|
|
3187
3983
|
// src/commands/status.ts
|
|
3188
|
-
import
|
|
3189
|
-
import { defineCommand as
|
|
3190
|
-
|
|
3984
|
+
import path29 from "path";
|
|
3985
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
3986
|
+
function profileStatus(profile) {
|
|
3987
|
+
if (!profile) return { origin: "none", evidence: [], profile: null };
|
|
3988
|
+
if ("detection" in profile && profile.detection && typeof profile.detection === "object") {
|
|
3989
|
+
const detection = profile.detection;
|
|
3990
|
+
return {
|
|
3991
|
+
origin: "readiness-scanner",
|
|
3992
|
+
evidence: detection.providerEvidence ?? [],
|
|
3993
|
+
profile
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
return { origin: "legacy-wizard", evidence: [], profile };
|
|
3997
|
+
}
|
|
3998
|
+
var statusCommand = defineCommand10({
|
|
3191
3999
|
meta: {
|
|
3192
4000
|
name: "status",
|
|
3193
4001
|
description: "Show Agent Kit distribution status (manifest + optional wizard profile)."
|
|
@@ -3204,15 +4012,26 @@ var statusCommand = defineCommand9({
|
|
|
3204
4012
|
}
|
|
3205
4013
|
},
|
|
3206
4014
|
async run({ args }) {
|
|
3207
|
-
const
|
|
3208
|
-
const
|
|
3209
|
-
|
|
4015
|
+
const rootDir = path29.resolve(args.cwd);
|
|
4016
|
+
const [manifest, rawProfile, scan] = await Promise.all([
|
|
4017
|
+
loadAgentKitManifest(rootDir),
|
|
4018
|
+
readJson(
|
|
4019
|
+
path29.join(rootDir, ".cursor", "agent-kit.config.json")
|
|
4020
|
+
),
|
|
4021
|
+
runScanner(rootDir)
|
|
4022
|
+
]);
|
|
4023
|
+
const readiness = createReadinessReport(scan, { generatorVersion: KIT_VERSION });
|
|
4024
|
+
const profile = profileStatus(rawProfile);
|
|
4025
|
+
const nextAction = readiness.pendingActions[0];
|
|
3210
4026
|
if (args.json) {
|
|
3211
4027
|
console.log(
|
|
3212
4028
|
JSON.stringify(
|
|
3213
4029
|
{
|
|
4030
|
+
runtimeVersion: KIT_VERSION,
|
|
3214
4031
|
manifest: manifest ?? null,
|
|
3215
|
-
|
|
4032
|
+
readiness,
|
|
4033
|
+
pendingActions: readiness.pendingActions,
|
|
4034
|
+
profile
|
|
3216
4035
|
},
|
|
3217
4036
|
null,
|
|
3218
4037
|
2
|
|
@@ -3221,11 +4040,12 @@ var statusCommand = defineCommand9({
|
|
|
3221
4040
|
return;
|
|
3222
4041
|
}
|
|
3223
4042
|
if (!manifest) {
|
|
3224
|
-
logger.warn(`No ${MANIFEST_RELATIVE_PATH}
|
|
4043
|
+
logger.warn(`No ${MANIFEST_RELATIVE_PATH}: run agent-kit install.`);
|
|
3225
4044
|
} else {
|
|
3226
4045
|
const protectedGlobs = resolveProtectedGlobs(manifest);
|
|
3227
4046
|
console.log("Agent Kit status");
|
|
3228
|
-
console.log(`
|
|
4047
|
+
console.log(` runtime: ${KIT_VERSION}`);
|
|
4048
|
+
console.log(` installed: ${manifest.version}`);
|
|
3229
4049
|
console.log(` profile: ${manifest.profile ?? "(none)"}`);
|
|
3230
4050
|
console.log(` packs: ${(manifest.packs ?? []).join(", ") || "(none)"}`);
|
|
3231
4051
|
console.log(` skills: ${(manifest.skills ?? []).length} listed`);
|
|
@@ -3233,21 +4053,26 @@ var statusCommand = defineCommand9({
|
|
|
3233
4053
|
console.log(
|
|
3234
4054
|
` registry: ${manifest.registry?.url ?? "(default)"} @ ${manifest.registry?.ref ?? "(default)"}`
|
|
3235
4055
|
);
|
|
3236
|
-
if (manifest.installedAt) console.log(` installed:
|
|
3237
|
-
}
|
|
3238
|
-
if (profile) {
|
|
3239
|
-
console.log("Wizard profile (agent-kit.config.json): present");
|
|
3240
|
-
console.log(` IDE: ${profile.ide?.ide ?? "?"}`);
|
|
3241
|
-
console.log(` core picks: ${(profile.selectedCoreComponents ?? []).join(", ") || "(none)"}`);
|
|
3242
|
-
} else {
|
|
3243
|
-
logger.info("No wizard profile \u2014 optional; run agent-kit init for generators.");
|
|
4056
|
+
if (manifest.installedAt) console.log(` installed at: ${manifest.installedAt}`);
|
|
3244
4057
|
}
|
|
4058
|
+
console.log("Repository readiness");
|
|
4059
|
+
console.log(
|
|
4060
|
+
` ready: ${readiness.summary.ready}, choices: ${readiness.summary.needs_choice}, manual: ${readiness.summary.manual}, blocked: ${readiness.summary.blocked}`
|
|
4061
|
+
);
|
|
4062
|
+
console.log(` pending: ${readiness.pendingActions.length}`);
|
|
4063
|
+
console.log(` profile origin: ${profile.origin}`);
|
|
4064
|
+
console.log(
|
|
4065
|
+
` profile evidence: ${profile.evidence.map((item) => item.value).join(", ") || "(none)"}`
|
|
4066
|
+
);
|
|
4067
|
+
console.log(
|
|
4068
|
+
nextAction ? `Next: ${nextAction.recommendation}` : "Next: repository readiness checks are complete"
|
|
4069
|
+
);
|
|
3245
4070
|
}
|
|
3246
4071
|
});
|
|
3247
4072
|
|
|
3248
4073
|
// src/commands/update.ts
|
|
3249
|
-
import { defineCommand as
|
|
3250
|
-
var updateCommand =
|
|
4074
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
4075
|
+
var updateCommand = defineCommand11({
|
|
3251
4076
|
meta: {
|
|
3252
4077
|
name: "update",
|
|
3253
4078
|
description: "Re-apply L0/packs/skills from the registry; never overwrites L3 protected paths."
|
|
@@ -3292,7 +4117,7 @@ var updateCommand = defineCommand10({
|
|
|
3292
4117
|
});
|
|
3293
4118
|
|
|
3294
4119
|
// src/index.ts
|
|
3295
|
-
var main =
|
|
4120
|
+
var main = defineCommand12({
|
|
3296
4121
|
meta: {
|
|
3297
4122
|
name: "agent-kit",
|
|
3298
4123
|
description: "HITL framework for AI-assisted IDEs"
|
|
@@ -3302,6 +4127,7 @@ var main = defineCommand11({
|
|
|
3302
4127
|
install: installCommand,
|
|
3303
4128
|
scan: scanCommand,
|
|
3304
4129
|
add: addCommand,
|
|
4130
|
+
doctor: doctorCommand,
|
|
3305
4131
|
status: statusCommand,
|
|
3306
4132
|
update: updateCommand,
|
|
3307
4133
|
diff: diffCommand,
|