@dadado/agent-kit-cli 4.4.7 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1800 -1037
- 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";
|
|
@@ -66,12 +66,12 @@ async function listDirectory(rootDir) {
|
|
|
66
66
|
import path2 from "path";
|
|
67
67
|
function resolveContained(root, rel) {
|
|
68
68
|
const rootAbs = path2.resolve(root);
|
|
69
|
-
const
|
|
70
|
-
const relToRoot = path2.relative(rootAbs,
|
|
69
|
+
const candidate2 = path2.resolve(rootAbs, rel);
|
|
70
|
+
const relToRoot = path2.relative(rootAbs, candidate2);
|
|
71
71
|
if (relToRoot.startsWith("..") || path2.isAbsolute(relToRoot)) {
|
|
72
72
|
throw new Error(`Path escapes registry/project root: ${rel}`);
|
|
73
73
|
}
|
|
74
|
-
return
|
|
74
|
+
return candidate2;
|
|
75
75
|
}
|
|
76
76
|
function toPosixRel(root, absPath) {
|
|
77
77
|
return path2.relative(root, absPath).split(path2.sep).join("/");
|
|
@@ -152,10 +152,19 @@ function isProtectedPath(relPath, globs) {
|
|
|
152
152
|
|
|
153
153
|
// src/lifecycle/apply.ts
|
|
154
154
|
function emptyStats() {
|
|
155
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
written: [],
|
|
157
|
+
removed: [],
|
|
158
|
+
collisions: [],
|
|
159
|
+
skippedProtected: [],
|
|
160
|
+
missing: [],
|
|
161
|
+
unchanged: []
|
|
162
|
+
};
|
|
156
163
|
}
|
|
157
164
|
function mergeStats(into, from) {
|
|
158
165
|
into.written.push(...from.written);
|
|
166
|
+
into.removed.push(...from.removed);
|
|
167
|
+
into.collisions.push(...from.collisions);
|
|
159
168
|
into.skippedProtected.push(...from.skippedProtected);
|
|
160
169
|
into.missing.push(...from.missing);
|
|
161
170
|
into.unchanged.push(...from.unchanged);
|
|
@@ -221,6 +230,7 @@ function buildManifest(input) {
|
|
|
221
230
|
if (input.profile) manifest.profile = input.profile;
|
|
222
231
|
if (input.packs?.length) manifest.packs = [...new Set(input.packs)].sort();
|
|
223
232
|
if (input.skills?.length) manifest.skills = [...new Set(input.skills)].sort();
|
|
233
|
+
if (input.personalization) manifest.personalization = input.personalization;
|
|
224
234
|
if (input.registryUrl || input.registryRef) {
|
|
225
235
|
manifest.registry = {};
|
|
226
236
|
if (input.registryUrl) manifest.registry.url = input.registryUrl;
|
|
@@ -258,6 +268,14 @@ function logApplyStats(stats) {
|
|
|
258
268
|
if (stats.unchanged.length > 0) {
|
|
259
269
|
logger.info(`Unchanged: ${stats.unchanged.length}`);
|
|
260
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
|
+
}
|
|
261
279
|
if (stats.skippedProtected.length > 0) {
|
|
262
280
|
logger.warn(`Skipped protected (L3): ${stats.skippedProtected.length}`);
|
|
263
281
|
for (const p of stats.skippedProtected) logger.info(` ~ ${p}`);
|
|
@@ -394,7 +412,21 @@ var REGISTRY_CLI_ARGS = {
|
|
|
394
412
|
};
|
|
395
413
|
|
|
396
414
|
// src/lifecycle/version.ts
|
|
397
|
-
|
|
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();
|
|
398
430
|
|
|
399
431
|
// src/manifest/index.ts
|
|
400
432
|
import path5 from "path";
|
|
@@ -518,6 +550,21 @@ function parseAgentKitManifest(raw) {
|
|
|
518
550
|
}
|
|
519
551
|
}
|
|
520
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
|
+
}
|
|
521
568
|
if (rest.installedAt !== void 0 && typeof rest.installedAt !== "string") {
|
|
522
569
|
issues.push("installedAt must be a string");
|
|
523
570
|
}
|
|
@@ -530,6 +577,7 @@ function parseAgentKitManifest(raw) {
|
|
|
530
577
|
"protected",
|
|
531
578
|
"overrides",
|
|
532
579
|
"registry",
|
|
580
|
+
"personalization",
|
|
533
581
|
"installedAt"
|
|
534
582
|
]);
|
|
535
583
|
for (const key of Object.keys(rest)) {
|
|
@@ -550,6 +598,7 @@ function parseAgentKitManifest(raw) {
|
|
|
550
598
|
if (protectedPaths) manifest.protected = protectedPaths;
|
|
551
599
|
if (overrides) manifest.overrides = overrides;
|
|
552
600
|
if (registry) manifest.registry = registry;
|
|
601
|
+
if (personalization) manifest.personalization = personalization;
|
|
553
602
|
if (typeof rest.installedAt === "string") manifest.installedAt = rest.installedAt;
|
|
554
603
|
return manifest;
|
|
555
604
|
}
|
|
@@ -912,6 +961,10 @@ var L0_ARTIFACTS = [
|
|
|
912
961
|
target: ".cursor/rules/docs-professional-standard.mdc"
|
|
913
962
|
},
|
|
914
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
|
+
},
|
|
915
968
|
{
|
|
916
969
|
source: "registry/rules/git-secrets-safety.mdc",
|
|
917
970
|
target: ".cursor/rules/git-secrets-safety.mdc"
|
|
@@ -922,8 +975,8 @@ var L0_ARTIFACTS = [
|
|
|
922
975
|
target: ".cursor/commands/start-project.md"
|
|
923
976
|
},
|
|
924
977
|
{
|
|
925
|
-
source: ".cursor/commands/onboard.md",
|
|
926
|
-
target: ".cursor/commands/onboard.md"
|
|
978
|
+
source: ".cursor/commands/agent-kit-onboard.md",
|
|
979
|
+
target: ".cursor/commands/agent-kit-onboard.md"
|
|
927
980
|
},
|
|
928
981
|
{
|
|
929
982
|
source: ".cursor/commands/continue-plan.md",
|
|
@@ -962,6 +1015,26 @@ var L0_ARTIFACTS = [
|
|
|
962
1015
|
source: ".cursor/context/templates/plan-external-review-prompt.md",
|
|
963
1016
|
target: ".cursor/context/templates/plan-external-review-prompt.md"
|
|
964
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
|
+
},
|
|
965
1038
|
{
|
|
966
1039
|
source: ".cursor/context/templates/plan-monitor.md",
|
|
967
1040
|
target: ".cursor/context/templates/plan-monitor.md"
|
|
@@ -1416,40 +1489,592 @@ var diffCommand = defineCommand3({
|
|
|
1416
1489
|
}
|
|
1417
1490
|
});
|
|
1418
1491
|
|
|
1419
|
-
// src/commands/
|
|
1420
|
-
import
|
|
1421
|
-
import { readFile as readFile6, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1422
|
-
import path10 from "path";
|
|
1492
|
+
// src/commands/doctor.ts
|
|
1493
|
+
import path19 from "path";
|
|
1423
1494
|
import { defineCommand as defineCommand4 } from "citty";
|
|
1424
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
|
+
|
|
1425
2077
|
// src/types.ts
|
|
1426
|
-
var GIT_PLATFORM_META = {
|
|
1427
|
-
github: {
|
|
1428
|
-
cli: "gh",
|
|
1429
|
-
prTerm: "Pull Request",
|
|
1430
|
-
prCommand: "gh pr create",
|
|
1431
|
-
ciDefault: "github-actions"
|
|
1432
|
-
},
|
|
1433
|
-
gitlab: {
|
|
1434
|
-
cli: "glab",
|
|
1435
|
-
prTerm: "Merge Request",
|
|
1436
|
-
prCommand: "glab mr create",
|
|
1437
|
-
ciDefault: "gitlab-ci"
|
|
1438
|
-
},
|
|
1439
|
-
bitbucket: {
|
|
1440
|
-
cli: "bb",
|
|
1441
|
-
prTerm: "Pull Request",
|
|
1442
|
-
prCommand: "bb pr create",
|
|
1443
|
-
ciDefault: "bitbucket-pipelines"
|
|
1444
|
-
},
|
|
1445
|
-
"azure-devops": {
|
|
1446
|
-
cli: "az repos",
|
|
1447
|
-
prTerm: "Pull Request",
|
|
1448
|
-
prCommand: "az repos pr create",
|
|
1449
|
-
ciDefault: "azure-pipelines"
|
|
1450
|
-
},
|
|
1451
|
-
gitea: { cli: "tea", prTerm: "Pull Request", prCommand: "tea pr create", ciDefault: "none" }
|
|
1452
|
-
};
|
|
1453
2078
|
var CI_PLATFORM_FILES = {
|
|
1454
2079
|
"github-actions": ".github/workflows",
|
|
1455
2080
|
"gitlab-ci": ".gitlab-ci.yml",
|
|
@@ -1474,73 +2099,650 @@ var PM_TOOL_LABELS = {
|
|
|
1474
2099
|
none: "None"
|
|
1475
2100
|
};
|
|
1476
2101
|
|
|
1477
|
-
// src/
|
|
1478
|
-
function
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
const
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
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);
|
|
1488
2112
|
}
|
|
1489
2113
|
}
|
|
1490
|
-
|
|
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 };
|
|
1491
2143
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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 {
|
|
1500
2166
|
}
|
|
1501
2167
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
function now() {
|
|
1505
|
-
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
|
|
1506
|
-
}
|
|
1507
|
-
async function loadProfile(rootDir) {
|
|
1508
|
-
const configPath = path10.join(rootDir, ".cursor", "agent-kit.config.json");
|
|
1509
|
-
try {
|
|
1510
|
-
return await readJson(configPath);
|
|
1511
|
-
} catch {
|
|
1512
|
-
return null;
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
function buildRoutines(profile) {
|
|
1516
|
-
const lines = [];
|
|
1517
|
-
const workflow = profile?.git.workflow;
|
|
1518
|
-
if (workflow === "homolog-prod") {
|
|
1519
|
-
lines.push("- [ ] `git staging` - move changes to staging");
|
|
1520
|
-
lines.push("- [ ] `git prod` - promote to production (after approval)");
|
|
1521
|
-
} else {
|
|
1522
|
-
lines.push("- [ ] Commit, push and open PR/MR");
|
|
1523
|
-
}
|
|
1524
|
-
const pmTools = profile?.services.projectManagement;
|
|
1525
|
-
if (pmTools && pmTools.length > 0) {
|
|
1526
|
-
const labels = pmTools.map((t) => PM_TOOL_LABELS[t]).join(", ");
|
|
1527
|
-
lines.push(`- [ ] Update tasks in ${labels} (if applicable)`);
|
|
2168
|
+
if (await fileExists(path14.join(rootDir, ".github", "ISSUE_TEMPLATE"))) {
|
|
2169
|
+
tools.push("github-issues");
|
|
1528
2170
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
}
|
|
1532
|
-
function closingInstruction(profile) {
|
|
1533
|
-
const pmTools = profile?.services.projectManagement;
|
|
1534
|
-
if (pmTools && pmTools.length > 0) {
|
|
1535
|
-
const labels = pmTools.map((t) => PM_TOOL_LABELS[t]).join(", ");
|
|
1536
|
-
return `All todos completed. Check git staging/prod and update ${labels} if applicable.`;
|
|
2171
|
+
if (await fileExists(path14.join(rootDir, ".github", "projects"))) {
|
|
2172
|
+
tools.push("github-projects");
|
|
1537
2173
|
}
|
|
1538
|
-
return
|
|
2174
|
+
return [...new Set(tools)];
|
|
1539
2175
|
}
|
|
1540
|
-
function
|
|
1541
|
-
const
|
|
1542
|
-
const
|
|
1543
|
-
const
|
|
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") {
|
|
2721
|
+
lines.push("- [ ] `git staging` - move changes to staging");
|
|
2722
|
+
lines.push("- [ ] `git prod` - promote to production (after approval)");
|
|
2723
|
+
} else {
|
|
2724
|
+
lines.push("- [ ] Commit, push and open PR/MR");
|
|
2725
|
+
}
|
|
2726
|
+
const pmTools = profile?.services.projectManagement;
|
|
2727
|
+
if (pmTools && pmTools.length > 0) {
|
|
2728
|
+
const labels = pmTools.map((t) => PM_TOOL_LABELS[t]).join(", ");
|
|
2729
|
+
lines.push(`- [ ] Update tasks in ${labels} (if applicable)`);
|
|
2730
|
+
}
|
|
2731
|
+
lines.push("- [ ] Review CHANGELOG");
|
|
2732
|
+
return lines;
|
|
2733
|
+
}
|
|
2734
|
+
function closingInstruction(profile) {
|
|
2735
|
+
const pmTools = profile?.services.projectManagement;
|
|
2736
|
+
if (pmTools && pmTools.length > 0) {
|
|
2737
|
+
const labels = pmTools.map((t) => PM_TOOL_LABELS[t]).join(", ");
|
|
2738
|
+
return `All todos completed. Check git staging/prod and update ${labels} if applicable.`;
|
|
2739
|
+
}
|
|
2740
|
+
return "All todos completed. Check commit/push and PR/MR.";
|
|
2741
|
+
}
|
|
2742
|
+
function buildHandoff(planFile, fm, profile) {
|
|
2743
|
+
const completed = fm.todos?.filter((t) => t.status === "completed") ?? [];
|
|
2744
|
+
const pending = fm.todos?.filter((t) => t.status === "pending") ?? [];
|
|
2745
|
+
const inProgress = fm.todos?.filter((t) => t.status === "in_progress") ?? [];
|
|
1544
2746
|
const nextTodo = inProgress[0] ?? pending[0];
|
|
1545
2747
|
const completedPhase = completed.length;
|
|
1546
2748
|
const totalPhases = fm.todos?.length ?? 0;
|
|
@@ -1591,7 +2793,7 @@ function runCursorHandoff(scriptPath, cwd) {
|
|
|
1591
2793
|
child.on("close", (code) => resolve(code ?? 1));
|
|
1592
2794
|
});
|
|
1593
2795
|
}
|
|
1594
|
-
var handoffCommand =
|
|
2796
|
+
var handoffCommand = defineCommand5({
|
|
1595
2797
|
meta: {
|
|
1596
2798
|
name: "handoff",
|
|
1597
2799
|
description: "Write .cursor/HANDOFF.md from the active Cursor plan, or run ./cursor-handoff handoff when no plan exists."
|
|
@@ -1605,15 +2807,15 @@ var handoffCommand = defineCommand4({
|
|
|
1605
2807
|
},
|
|
1606
2808
|
async run({ args }) {
|
|
1607
2809
|
const profile = await loadProfile(args.cwd);
|
|
1608
|
-
const plansDir =
|
|
1609
|
-
const handoffPath =
|
|
2810
|
+
const plansDir = path20.join(args.cwd, ".cursor", "plans");
|
|
2811
|
+
const handoffPath = path20.join(args.cwd, ".cursor", "HANDOFF.md");
|
|
1610
2812
|
const plan = await findActivePlan(plansDir);
|
|
1611
2813
|
if (plan) {
|
|
1612
2814
|
const fm = parsePlanFrontmatter(plan.raw);
|
|
1613
2815
|
if (fm) {
|
|
1614
|
-
await ensureDir(
|
|
2816
|
+
await ensureDir(path20.join(args.cwd, ".cursor"));
|
|
1615
2817
|
const content = buildHandoff(plan.file, fm, profile);
|
|
1616
|
-
await
|
|
2818
|
+
await writeFile4(handoffPath, content, "utf8");
|
|
1617
2819
|
logger.success("HANDOFF.md updated: .cursor/HANDOFF.md");
|
|
1618
2820
|
logger.info(
|
|
1619
2821
|
`Plan: ${plan.file} (${fm.todos?.filter((t) => t.status === "completed").length}/${fm.todos?.length} completed)`
|
|
@@ -1631,7 +2833,7 @@ var handoffCommand = defineCommand4({
|
|
|
1631
2833
|
}
|
|
1632
2834
|
logger.warn(`Plan ${plan.file} without valid frontmatter; trying legacy flow.`);
|
|
1633
2835
|
}
|
|
1634
|
-
const scriptPath =
|
|
2836
|
+
const scriptPath = path20.join(args.cwd, "cursor-handoff");
|
|
1635
2837
|
if (!await fileExists(scriptPath)) {
|
|
1636
2838
|
printV3Guidance();
|
|
1637
2839
|
return;
|
|
@@ -1646,878 +2848,330 @@ var handoffCommand = defineCommand4({
|
|
|
1646
2848
|
try {
|
|
1647
2849
|
const code = await runCursorHandoff(scriptPath, args.cwd);
|
|
1648
2850
|
if (code !== 0) {
|
|
1649
|
-
process.exitCode = code;
|
|
1650
|
-
}
|
|
1651
|
-
} catch (err) {
|
|
1652
|
-
logger.warn(`Failed to execute cursor-handoff: ${String(err)}`);
|
|
1653
|
-
printV3Guidance();
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
});
|
|
1657
|
-
|
|
1658
|
-
// src/commands/init.ts
|
|
1659
|
-
import path23 from "path";
|
|
1660
|
-
import { intro, outro } from "@clack/prompts";
|
|
1661
|
-
import { defineCommand as defineCommand5 } from "citty";
|
|
1662
|
-
|
|
1663
|
-
// src/generator/index.ts
|
|
1664
|
-
import { writeFile as writeFile9 } from "fs/promises";
|
|
1665
|
-
import path16 from "path";
|
|
1666
|
-
|
|
1667
|
-
// src/generator/agents-md.ts
|
|
1668
|
-
import { writeFile as writeFile4 } from "fs/promises";
|
|
1669
|
-
import path11 from "path";
|
|
1670
|
-
|
|
1671
|
-
// src/generator/platform.ts
|
|
1672
|
-
function gitProviderLabel(profile) {
|
|
1673
|
-
const p = profile.git.provider;
|
|
1674
|
-
if (!p || p === "other") return "Git";
|
|
1675
|
-
const labels = {
|
|
1676
|
-
github: "GitHub",
|
|
1677
|
-
gitlab: "GitLab",
|
|
1678
|
-
bitbucket: "Bitbucket",
|
|
1679
|
-
"azure-devops": "Azure DevOps",
|
|
1680
|
-
gitea: "Gitea"
|
|
1681
|
-
};
|
|
1682
|
-
return labels[p] ?? "Git";
|
|
1683
|
-
}
|
|
1684
|
-
function gitCliTool(profile) {
|
|
1685
|
-
const p = profile.git.provider;
|
|
1686
|
-
if (!p || p === "other") return "git";
|
|
1687
|
-
return GIT_PLATFORM_META[p]?.cli ?? "git";
|
|
1688
|
-
}
|
|
1689
|
-
function prTerminology(profile) {
|
|
1690
|
-
const p = profile.git.provider;
|
|
1691
|
-
if (!p || p === "other") return "PR/MR";
|
|
1692
|
-
return GIT_PLATFORM_META[p]?.prTerm ?? "PR/MR";
|
|
1693
|
-
}
|
|
1694
|
-
function pmToolsList(profile) {
|
|
1695
|
-
const tools = profile.services.projectManagement;
|
|
1696
|
-
if (!tools || tools.length === 0) return "";
|
|
1697
|
-
return tools.map((t) => PM_TOOL_LABELS[t]).join(", ");
|
|
1698
|
-
}
|
|
1699
|
-
function pmRoutineLine(profile) {
|
|
1700
|
-
const label = pmToolsList(profile);
|
|
1701
|
-
if (!label) return "";
|
|
1702
|
-
return `- Update tasks in ${label} (if applicable)`;
|
|
1703
|
-
}
|
|
1704
|
-
function ciLabel(profile) {
|
|
1705
|
-
const labels = {
|
|
1706
|
-
"github-actions": "GitHub Actions",
|
|
1707
|
-
"gitlab-ci": "GitLab CI",
|
|
1708
|
-
"azure-pipelines": "Azure Pipelines",
|
|
1709
|
-
"bitbucket-pipelines": "Bitbucket Pipelines",
|
|
1710
|
-
jenkins: "Jenkins",
|
|
1711
|
-
circleci: "CircleCI",
|
|
1712
|
-
travis: "Travis CI"
|
|
1713
|
-
};
|
|
1714
|
-
return labels[profile.infra.ci] ?? "";
|
|
1715
|
-
}
|
|
1716
|
-
function devopsFlowSummary(profile) {
|
|
1717
|
-
const parts = [];
|
|
1718
|
-
const provider = gitProviderLabel(profile);
|
|
1719
|
-
parts.push(`Git: ${provider}`);
|
|
1720
|
-
if (profile.git.workflow !== "unknown") {
|
|
1721
|
-
const workflowLabels = {
|
|
1722
|
-
"trunk-based": "trunk-based",
|
|
1723
|
-
"feature-pr": `feature branch \u2192 ${prTerminology(profile)} \u2192 main`,
|
|
1724
|
-
gitflow: "gitflow (develop/release/main)",
|
|
1725
|
-
"homolog-prod": "staging \u2192 production"
|
|
1726
|
-
};
|
|
1727
|
-
parts.push(`Workflow: ${workflowLabels[profile.git.workflow] ?? profile.git.workflow}`);
|
|
1728
|
-
}
|
|
1729
|
-
const ci = ciLabel(profile);
|
|
1730
|
-
if (ci) parts.push(`CI/CD: ${ci}`);
|
|
1731
|
-
const pm = pmToolsList(profile);
|
|
1732
|
-
if (pm) parts.push(`Project mgmt: ${pm}`);
|
|
1733
|
-
return parts.join(" | ");
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// src/generator/agents-md.ts
|
|
1737
|
-
async function generateAgentsMd(profile) {
|
|
1738
|
-
const target = path11.join(profile.rootDir, "AGENTS.md");
|
|
1739
|
-
const flow = devopsFlowSummary(profile);
|
|
1740
|
-
const prTerm = prTerminology(profile);
|
|
1741
|
-
const content = `# AGENTS.md
|
|
1742
|
-
|
|
1743
|
-
Project configured with Agent Kit v3.
|
|
1744
|
-
|
|
1745
|
-
## Detected context
|
|
1746
|
-
- Stack: ${profile.stack.language}${profile.stack.framework ? ` (${profile.stack.framework})` : ""}
|
|
1747
|
-
- IDE: ${profile.ide.ide} (${profile.ide.plan})
|
|
1748
|
-
- ${flow}
|
|
1749
|
-
|
|
1750
|
-
## Guidelines
|
|
1751
|
-
1. Prefer small, verifiable changes.
|
|
1752
|
-
2. Adapt response depth to user's IDE plan/model.
|
|
1753
|
-
3. Use /worktree for experiments, /best-of-n for critical decisions (Cursor 3.0).
|
|
1754
|
-
4. Security review before merge.
|
|
1755
|
-
5. Always create a ${prTerm} \u2014 never push directly to main.
|
|
1756
|
-
`;
|
|
1757
|
-
await writeFile4(target, content, "utf8");
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
// src/generator/cursor.ts
|
|
1761
|
-
import { writeFile as writeFile5 } from "fs/promises";
|
|
1762
|
-
import path12 from "path";
|
|
1763
|
-
function buildGitWorkflowRule(profile) {
|
|
1764
|
-
const prTerm = prTerminology(profile);
|
|
1765
|
-
const cli = gitCliTool(profile);
|
|
1766
|
-
const provider = gitProviderLabel(profile);
|
|
1767
|
-
const pm = pmToolsList(profile);
|
|
1768
|
-
const lines = [
|
|
1769
|
-
"# Git Workflow",
|
|
1770
|
-
`- Platform: ${provider} | CLI: \`${cli}\``,
|
|
1771
|
-
"- Use Conventional Commits (feat:, fix:, docs:, refactor:, chore:, perf:, test:).",
|
|
1772
|
-
"- For risky changes, prefer /worktree (isolated git worktree).",
|
|
1773
|
-
"- For comparing approaches, use /best-of-n."
|
|
1774
|
-
];
|
|
1775
|
-
if (profile.git.workflow === "homolog-prod") {
|
|
1776
|
-
lines.push(
|
|
1777
|
-
"- Flow: development \u2192 staging \u2192 production.",
|
|
1778
|
-
`- Create ${prTerm} targeting the staging branch; merge to main only via promotion.`,
|
|
1779
|
-
"- NEVER commit directly to main."
|
|
1780
|
-
);
|
|
1781
|
-
} else if (profile.git.workflow === "feature-pr") {
|
|
1782
|
-
lines.push(
|
|
1783
|
-
`- Flow: feature branch \u2192 ${prTerm} \u2192 main.`,
|
|
1784
|
-
`- Always create a ${prTerm} for review before merging.`
|
|
1785
|
-
);
|
|
1786
|
-
} else if (profile.git.workflow === "gitflow") {
|
|
1787
|
-
lines.push(
|
|
1788
|
-
"- Flow: feature \u2192 develop \u2192 release \u2192 main.",
|
|
1789
|
-
`- Create ${prTerm} targeting develop for features, main for releases.`
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
|
-
if (pm) {
|
|
1793
|
-
lines.push(`- After merge: update task status in ${pm} if integration is available.`);
|
|
1794
|
-
}
|
|
1795
|
-
return `${lines.join("\n")}
|
|
1796
|
-
`;
|
|
1797
|
-
}
|
|
1798
|
-
function buildHandoffRule(profile) {
|
|
1799
|
-
const pm = pmRoutineLine(profile);
|
|
1800
|
-
const routines = ["- Suggest routines: git commit/push, review CHANGELOG."];
|
|
1801
|
-
if (pm) routines.push(`${pm}.`);
|
|
1802
|
-
return `# Handoff \u2014 State in File
|
|
1803
|
-
- After completing each task: save .cursor/HANDOFF.md with progress and next steps.
|
|
1804
|
-
- Update to-dos in the plan (.cursor/plans/) when completing or starting a task.
|
|
1805
|
-
- One HANDOFF per project \u2014 source of truth for continuity.
|
|
1806
|
-
${routines.join("\n")}
|
|
1807
|
-
- To resume: /continue-plan in a new conversation.
|
|
1808
|
-
- Native features (summaries, /resume, transcripts, Agents Window) complement \u2014 not replace.
|
|
1809
|
-
- Context at ~60%: save handoff and suggest new conversation.
|
|
1810
|
-
`;
|
|
1811
|
-
}
|
|
1812
|
-
function cursorRulesByPlan(profile) {
|
|
1813
|
-
if (profile.ide.plan === "cursor-free") {
|
|
1814
|
-
return [
|
|
1815
|
-
{
|
|
1816
|
-
filename: "01-core.mdc",
|
|
1817
|
-
content: `# Core Rule
|
|
1818
|
-
- Respond directly and concisely.
|
|
1819
|
-
- Focus on small changes and local validation.
|
|
1820
|
-
- Size each task for ~50% of context window.
|
|
1821
|
-
`
|
|
1822
|
-
},
|
|
1823
|
-
{
|
|
1824
|
-
filename: "02-handoff.mdc",
|
|
1825
|
-
content: buildHandoffRule(profile)
|
|
1826
|
-
}
|
|
1827
|
-
];
|
|
1828
|
-
}
|
|
1829
|
-
return [
|
|
1830
|
-
{
|
|
1831
|
-
filename: "01-core.mdc",
|
|
1832
|
-
content: `# Core Rule
|
|
1833
|
-
- Execute tasks end-to-end whenever possible.
|
|
1834
|
-
- Prioritize security, tests, and architectural consistency.
|
|
1835
|
-
- Size each task for ~50% of context window.
|
|
1836
|
-
`
|
|
1837
|
-
},
|
|
1838
|
-
{
|
|
1839
|
-
filename: "02-git-workflow.mdc",
|
|
1840
|
-
content: buildGitWorkflowRule(profile)
|
|
1841
|
-
},
|
|
1842
|
-
{
|
|
1843
|
-
filename: "03-handoff.mdc",
|
|
1844
|
-
content: buildHandoffRule(profile)
|
|
1845
|
-
},
|
|
1846
|
-
{
|
|
1847
|
-
filename: "04-ide-guide.mdc",
|
|
1848
|
-
content: `# Cursor Guide
|
|
1849
|
-
- Use Agents Window for parallelism \u2014 each agent reads HANDOFF before acting.
|
|
1850
|
-
- Use Await for long-running processes.
|
|
1851
|
-
- Transcripts and @mentions for cross-reference.
|
|
1852
|
-
- /worktree for risky changes.
|
|
1853
|
-
- /best-of-n to compare approaches.
|
|
1854
|
-
`
|
|
1855
|
-
}
|
|
1856
|
-
];
|
|
1857
|
-
}
|
|
1858
|
-
async function generateCursorArtifacts(profile) {
|
|
1859
|
-
const rulesDir = path12.join(profile.rootDir, ".cursor", "rules");
|
|
1860
|
-
const skillsDir = path12.join(profile.rootDir, ".cursor", "skills", "core");
|
|
1861
|
-
const agentsDir = path12.join(profile.rootDir, ".cursor", "agents");
|
|
1862
|
-
const commandsDir = path12.join(profile.rootDir, ".cursor", "commands");
|
|
1863
|
-
await Promise.all([
|
|
1864
|
-
ensureDir(rulesDir),
|
|
1865
|
-
ensureDir(skillsDir),
|
|
1866
|
-
ensureDir(agentsDir),
|
|
1867
|
-
ensureDir(commandsDir)
|
|
1868
|
-
]);
|
|
1869
|
-
const rules = cursorRulesByPlan(profile);
|
|
1870
|
-
await Promise.all(
|
|
1871
|
-
rules.map((rule) => writeFile5(path12.join(rulesDir, rule.filename), rule.content, "utf8"))
|
|
1872
|
-
);
|
|
1873
|
-
const includeAgents = profile.ide.plan !== "cursor-free";
|
|
1874
|
-
if (includeAgents) {
|
|
1875
|
-
await writeFile5(
|
|
1876
|
-
path12.join(agentsDir, "security-reviewer.md"),
|
|
1877
|
-
"# Security Reviewer\n\nFocus on auth, PII, secrets, injection, and logging.\n",
|
|
1878
|
-
"utf8"
|
|
1879
|
-
);
|
|
1880
|
-
}
|
|
1881
|
-
const ci = ciLabel(profile);
|
|
1882
|
-
const provider = gitProviderLabel(profile);
|
|
1883
|
-
const statusLines = [
|
|
1884
|
-
"# /agent-kit-status",
|
|
1885
|
-
"",
|
|
1886
|
-
"Show current profile and active components.",
|
|
1887
|
-
"",
|
|
1888
|
-
"## DevOps Flow",
|
|
1889
|
-
`- Git: ${provider} (${profile.git.workflow})`
|
|
1890
|
-
];
|
|
1891
|
-
if (ci) statusLines.push(`- CI/CD: ${ci}`);
|
|
1892
|
-
const pm = pmToolsList(profile);
|
|
1893
|
-
if (pm) statusLines.push(`- Project management: ${pm}`);
|
|
1894
|
-
statusLines.push("");
|
|
1895
|
-
await writeFile5(
|
|
1896
|
-
path12.join(commandsDir, "agent-kit-status.md"),
|
|
1897
|
-
`${statusLines.join("\n")}
|
|
1898
|
-
`,
|
|
1899
|
-
"utf8"
|
|
1900
|
-
);
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
// src/generator/git-hooks.ts
|
|
1904
|
-
import { chmod, writeFile as writeFile6 } from "fs/promises";
|
|
1905
|
-
import path13 from "path";
|
|
1906
|
-
async function generateGitHooks(profile) {
|
|
1907
|
-
if (!profile.installHooks) return;
|
|
1908
|
-
const hooksDir = path13.join(profile.rootDir, ".git", "hooks");
|
|
1909
|
-
await ensureDir(hooksDir);
|
|
1910
|
-
const preCommitPath = path13.join(hooksDir, "pre-commit");
|
|
1911
|
-
const preCommit = `#!/usr/bin/env bash
|
|
1912
|
-
set -euo pipefail
|
|
1913
|
-
|
|
1914
|
-
if command -v rg >/dev/null 2>&1; then
|
|
1915
|
-
rg -n --hidden --glob '!node_modules/**' '(AKIA|BEGIN PRIVATE KEY|xoxb-)' . && {
|
|
1916
|
-
echo "Potential secret detected. Commit blocked."
|
|
1917
|
-
exit 1
|
|
1918
|
-
} || true
|
|
1919
|
-
fi
|
|
1920
|
-
`;
|
|
1921
|
-
await writeFile6(preCommitPath, preCommit, "utf8");
|
|
1922
|
-
await chmod(preCommitPath, 493);
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
// src/generator/vscode.ts
|
|
1926
|
-
import { writeFile as writeFile7 } from "fs/promises";
|
|
1927
|
-
import path14 from "path";
|
|
1928
|
-
async function generateVSCodeArtifacts(profile) {
|
|
1929
|
-
const vscodeDir = path14.join(profile.rootDir, ".vscode");
|
|
1930
|
-
const githubDir = path14.join(profile.rootDir, ".github");
|
|
1931
|
-
await Promise.all([ensureDir(vscodeDir), ensureDir(githubDir)]);
|
|
1932
|
-
await writeFile7(
|
|
1933
|
-
path14.join(vscodeDir, "settings.json"),
|
|
1934
|
-
`${JSON.stringify(
|
|
1935
|
-
{
|
|
1936
|
-
"editor.formatOnSave": true,
|
|
1937
|
-
"editor.codeActionsOnSave": {
|
|
1938
|
-
"source.fixAll": "explicit"
|
|
1939
|
-
},
|
|
1940
|
-
"files.autoSave": "afterDelay"
|
|
1941
|
-
},
|
|
1942
|
-
null,
|
|
1943
|
-
2
|
|
1944
|
-
)}
|
|
1945
|
-
`,
|
|
1946
|
-
"utf8"
|
|
1947
|
-
);
|
|
1948
|
-
const provider = gitProviderLabel(profile);
|
|
1949
|
-
const prTerm = prTerminology(profile);
|
|
1950
|
-
await writeFile7(
|
|
1951
|
-
path14.join(githubDir, "copilot-instructions.md"),
|
|
1952
|
-
`# Copilot Instructions
|
|
1953
|
-
|
|
1954
|
-
- Keep code changes small and testable.
|
|
1955
|
-
- Use Conventional Commits (feat:, fix:, docs:, etc.).
|
|
1956
|
-
- Prefer security-safe defaults.
|
|
1957
|
-
- Git platform: ${provider}. Always create a ${prTerm} for review.
|
|
1958
|
-
`,
|
|
1959
|
-
"utf8"
|
|
1960
|
-
);
|
|
1961
|
-
if (profile.ide.plan === "vscode-pro") {
|
|
1962
|
-
await writeFile7(
|
|
1963
|
-
path14.join(vscodeDir, "security-review.agent.md"),
|
|
1964
|
-
"# Security Review Agent\n\nSpecialized mode for security review.\n",
|
|
1965
|
-
"utf8"
|
|
1966
|
-
);
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
// src/generator/windsurf.ts
|
|
1971
|
-
import { writeFile as writeFile8 } from "fs/promises";
|
|
1972
|
-
import path15 from "path";
|
|
1973
|
-
async function generateWindsurfArtifacts(profile) {
|
|
1974
|
-
const provider = gitProviderLabel(profile);
|
|
1975
|
-
const prTerm = prTerminology(profile);
|
|
1976
|
-
const content = `# Windsurf Rules
|
|
1977
|
-
- Keep instructions concise and objective.
|
|
1978
|
-
- Prefer small diffs and explicit validation.
|
|
1979
|
-
- Apply security review before merge.
|
|
1980
|
-
- Use Conventional Commits (feat:, fix:, docs:, etc.).
|
|
1981
|
-
- Git: ${provider}. Create a ${prTerm} for every change.
|
|
1982
|
-
`;
|
|
1983
|
-
await writeFile8(path15.join(profile.rootDir, ".windsurfrules"), content, "utf8");
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
// src/generator/index.ts
|
|
1987
|
-
async function generateFromProfile(profile) {
|
|
1988
|
-
await generateAgentsMd(profile);
|
|
1989
|
-
await generateGitHooks(profile);
|
|
1990
|
-
if (profile.ide.ide === "cursor" || profile.ide.plan.startsWith("cursor")) {
|
|
1991
|
-
await generateCursorArtifacts(profile);
|
|
1992
|
-
}
|
|
1993
|
-
if (profile.ide.ide === "vscode" || profile.ide.plan.startsWith("vscode")) {
|
|
1994
|
-
await generateVSCodeArtifacts(profile);
|
|
1995
|
-
}
|
|
1996
|
-
if (profile.ide.ide === "windsurf" || profile.ide.plan === "windsurf") {
|
|
1997
|
-
await generateWindsurfArtifacts(profile);
|
|
1998
|
-
}
|
|
1999
|
-
if (profile.ide.plan === "default") {
|
|
2000
|
-
await Promise.all([
|
|
2001
|
-
generateCursorArtifacts(profile),
|
|
2002
|
-
generateVSCodeArtifacts(profile),
|
|
2003
|
-
generateWindsurfArtifacts(profile)
|
|
2004
|
-
]);
|
|
2005
|
-
}
|
|
2006
|
-
const pluginDir = path16.join(profile.rootDir, ".cursor-plugin");
|
|
2007
|
-
await ensureDir(pluginDir);
|
|
2008
|
-
await writeFile9(
|
|
2009
|
-
path16.join(pluginDir, "plugin.json"),
|
|
2010
|
-
`${JSON.stringify(
|
|
2011
|
-
{
|
|
2012
|
-
name: "agent-kit",
|
|
2013
|
-
displayName: "Agent Kit",
|
|
2014
|
-
author: "agent-kit-startup",
|
|
2015
|
-
description: "Bootstrap de ambiente dev com IA (Cursor, VS Code, Windsurf)",
|
|
2016
|
-
keywords: ["agents", "context", "automation", "multi-ide"],
|
|
2017
|
-
license: "MIT",
|
|
2018
|
-
version: "3.0.0"
|
|
2019
|
-
},
|
|
2020
|
-
null,
|
|
2021
|
-
2
|
|
2022
|
-
)}
|
|
2023
|
-
`,
|
|
2024
|
-
"utf8"
|
|
2025
|
-
);
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
// src/scanner/scan.ts
|
|
2029
|
-
import path22 from "path";
|
|
2030
|
-
|
|
2031
|
-
// src/scanner/detect-git.ts
|
|
2032
|
-
import { execFile as execFile2 } from "child_process";
|
|
2033
|
-
import path17 from "path";
|
|
2034
|
-
import { promisify as promisify2 } from "util";
|
|
2035
|
-
var exec = promisify2(execFile2);
|
|
2036
|
-
function detectProvider(remoteUrl) {
|
|
2037
|
-
if (!remoteUrl) return void 0;
|
|
2038
|
-
if (remoteUrl.includes("github")) return "github";
|
|
2039
|
-
if (remoteUrl.includes("gitlab")) return "gitlab";
|
|
2040
|
-
if (remoteUrl.includes("bitbucket")) return "bitbucket";
|
|
2041
|
-
if (remoteUrl.includes("dev.azure.com") || remoteUrl.includes("visualstudio.com"))
|
|
2042
|
-
return "azure-devops";
|
|
2043
|
-
if (remoteUrl.includes("gitea") || remoteUrl.includes("codeberg")) return "gitea";
|
|
2044
|
-
return "other";
|
|
2045
|
-
}
|
|
2046
|
-
function inferWorkflow(currentBranch) {
|
|
2047
|
-
if (!currentBranch) return "unknown";
|
|
2048
|
-
if (currentBranch === "main" || currentBranch === "master") return "feature-pr";
|
|
2049
|
-
if (currentBranch.includes("develop") || currentBranch.includes("release")) return "gitflow";
|
|
2050
|
-
if (currentBranch.includes("staging") || currentBranch.includes("homolog")) return "homolog-prod";
|
|
2051
|
-
return "feature-pr";
|
|
2052
|
-
}
|
|
2053
|
-
async function runGit(args, rootDir) {
|
|
2054
|
-
try {
|
|
2055
|
-
const { stdout } = await exec("git", args, { cwd: rootDir });
|
|
2056
|
-
return stdout.trim();
|
|
2057
|
-
} catch {
|
|
2058
|
-
return void 0;
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
async function detectGit(rootDir) {
|
|
2062
|
-
const hasGit = await fileExists(path17.join(rootDir, ".git"));
|
|
2063
|
-
if (!hasGit) return { workflow: "unknown" };
|
|
2064
|
-
const remoteUrl = await runGit(["remote", "get-url", "origin"], rootDir);
|
|
2065
|
-
const currentBranch = await runGit(["branch", "--show-current"], rootDir);
|
|
2066
|
-
return {
|
|
2067
|
-
provider: detectProvider(remoteUrl),
|
|
2068
|
-
remoteUrl,
|
|
2069
|
-
currentBranch,
|
|
2070
|
-
workflow: inferWorkflow(currentBranch)
|
|
2071
|
-
};
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
// src/scanner/detect-ide.ts
|
|
2075
|
-
import path18 from "path";
|
|
2076
|
-
async function detectIde(rootDir) {
|
|
2077
|
-
const hasCursor = await fileExists(path18.join(rootDir, ".cursor"));
|
|
2078
|
-
const hasVSCode = await fileExists(path18.join(rootDir, ".vscode"));
|
|
2079
|
-
const hasWindsurf = await fileExists(path18.join(rootDir, ".windsurfrules"));
|
|
2080
|
-
if (hasCursor) return { ide: "cursor", plan: "cursor-pro" };
|
|
2081
|
-
if (hasVSCode) return { ide: "vscode", plan: "vscode-pro" };
|
|
2082
|
-
if (hasWindsurf) return { ide: "windsurf", plan: "windsurf" };
|
|
2083
|
-
return { ide: "unknown", plan: "default" };
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
// src/scanner/detect-infra.ts
|
|
2087
|
-
import path19 from "path";
|
|
2088
|
-
async function detectInfra(rootDir) {
|
|
2089
|
-
const docker = await fileExists(path19.join(rootDir, "Dockerfile")) || await fileExists(path19.join(rootDir, "docker-compose.yml")) || await fileExists(path19.join(rootDir, "docker-compose.yaml"));
|
|
2090
|
-
const kubernetes = await fileExists(path19.join(rootDir, "k8s")) || await fileExists(path19.join(rootDir, "kubernetes"));
|
|
2091
|
-
let ci = "none";
|
|
2092
|
-
for (const [platform, filePath] of Object.entries(CI_PLATFORM_FILES)) {
|
|
2093
|
-
if (await fileExists(path19.join(rootDir, filePath))) {
|
|
2094
|
-
ci = platform;
|
|
2095
|
-
break;
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
return { docker, kubernetes, ci };
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
// src/scanner/detect-services.ts
|
|
2102
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
2103
|
-
import path20 from "path";
|
|
2104
|
-
async function detectProjectManagement(rootDir) {
|
|
2105
|
-
const tools = [];
|
|
2106
|
-
const mcpConfigPaths = [
|
|
2107
|
-
path20.join(rootDir, ".cursor", "mcp.json"),
|
|
2108
|
-
path20.join(rootDir, "mcp.json")
|
|
2109
|
-
];
|
|
2110
|
-
for (const configPath of mcpConfigPaths) {
|
|
2111
|
-
if (!await fileExists(configPath)) continue;
|
|
2112
|
-
try {
|
|
2113
|
-
const raw = await readFile7(configPath, "utf8");
|
|
2114
|
-
const lower = raw.toLowerCase();
|
|
2115
|
-
if (lower.includes("clickup")) tools.push("clickup");
|
|
2116
|
-
if (lower.includes("jira") || lower.includes("atlassian")) tools.push("jira");
|
|
2117
|
-
if (lower.includes("linear")) tools.push("linear");
|
|
2118
|
-
if (lower.includes("asana")) tools.push("asana");
|
|
2119
|
-
if (lower.includes("youtrack")) tools.push("youtrack");
|
|
2120
|
-
if (lower.includes("shortcut")) tools.push("shortcut");
|
|
2121
|
-
} catch {
|
|
2851
|
+
process.exitCode = code;
|
|
2852
|
+
}
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
logger.warn(`Failed to execute cursor-handoff: ${String(err)}`);
|
|
2855
|
+
printV3Guidance();
|
|
2122
2856
|
}
|
|
2123
2857
|
}
|
|
2124
|
-
|
|
2125
|
-
tools.push("github-issues");
|
|
2126
|
-
}
|
|
2127
|
-
if (await fileExists(path20.join(rootDir, ".github", "projects"))) {
|
|
2128
|
-
tools.push("github-projects");
|
|
2129
|
-
}
|
|
2130
|
-
return [...new Set(tools)];
|
|
2131
|
-
}
|
|
2132
|
-
async function detectServices(rootDir) {
|
|
2133
|
-
const hasPrisma = await fileExists(path20.join(rootDir, "prisma/schema.prisma"));
|
|
2134
|
-
const hasSequelize = await fileExists(path20.join(rootDir, "sequelize"));
|
|
2135
|
-
const hasDrizzle = await fileExists(path20.join(rootDir, "drizzle.config.ts"));
|
|
2136
|
-
const hasKnex = await fileExists(path20.join(rootDir, "knexfile.ts"));
|
|
2137
|
-
const hasTypeorm = await fileExists(path20.join(rootDir, "ormconfig.json"));
|
|
2138
|
-
const database = hasPrisma || hasSequelize || hasDrizzle || hasKnex || hasTypeorm ? "postgresql" : void 0;
|
|
2139
|
-
const orm = hasPrisma ? "prisma" : hasDrizzle ? "drizzle" : hasSequelize ? "sequelize" : hasKnex ? "knex" : hasTypeorm ? "typeorm" : void 0;
|
|
2140
|
-
const projectManagement = await detectProjectManagement(rootDir);
|
|
2141
|
-
return {
|
|
2142
|
-
database,
|
|
2143
|
-
orm,
|
|
2144
|
-
projectManagement: projectManagement.length > 0 ? projectManagement : void 0
|
|
2145
|
-
};
|
|
2146
|
-
}
|
|
2858
|
+
});
|
|
2147
2859
|
|
|
2148
|
-
// src/
|
|
2860
|
+
// src/commands/init.ts
|
|
2861
|
+
import { intro, outro } from "@clack/prompts";
|
|
2862
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
2863
|
+
|
|
2864
|
+
// src/commands/install.ts
|
|
2865
|
+
import path23 from "path";
|
|
2866
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
2867
|
+
|
|
2868
|
+
// src/generator/personalization.ts
|
|
2869
|
+
import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
2149
2870
|
import path21 from "path";
|
|
2150
|
-
var
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path21.join(rootDir, item))))).some(Boolean);
|
|
2163
|
-
const hasPackageJson = await fileExists(path21.join(rootDir, "package.json"));
|
|
2164
|
-
if (hasPackageJson) {
|
|
2165
|
-
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"));
|
|
2166
|
-
const hasNestConfig = await fileExists(path21.join(rootDir, "nest-cli.json"));
|
|
2167
|
-
return {
|
|
2168
|
-
language: "node",
|
|
2169
|
-
framework: hasNextConfig ? "nextjs" : hasNestConfig ? "nestjs" : "node",
|
|
2170
|
-
packageManager: "pnpm",
|
|
2171
|
-
hasProjectFiles: hasAnyProjectMarker
|
|
2172
|
-
};
|
|
2173
|
-
}
|
|
2174
|
-
if (await fileExists(path21.join(rootDir, "pyproject.toml"))) {
|
|
2175
|
-
return { language: "python", framework: "python", hasProjectFiles: hasAnyProjectMarker };
|
|
2176
|
-
}
|
|
2177
|
-
if (await fileExists(path21.join(rootDir, "go.mod"))) {
|
|
2178
|
-
return { language: "go", framework: "go", hasProjectFiles: hasAnyProjectMarker };
|
|
2179
|
-
}
|
|
2180
|
-
if (await fileExists(path21.join(rootDir, "Cargo.toml"))) {
|
|
2181
|
-
return { language: "rust", framework: "rust", hasProjectFiles: hasAnyProjectMarker };
|
|
2182
|
-
}
|
|
2183
|
-
if (await fileExists(path21.join(rootDir, "composer.json"))) {
|
|
2184
|
-
return { language: "php", framework: "php", hasProjectFiles: hasAnyProjectMarker };
|
|
2185
|
-
}
|
|
2186
|
-
return { language: "unknown", hasProjectFiles: hasAnyProjectMarker };
|
|
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
|
+
});
|
|
2187
2883
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
".git",
|
|
2192
|
-
".gitignore",
|
|
2193
|
-
"LICENSE",
|
|
2194
|
-
"README.md",
|
|
2195
|
-
".cursor",
|
|
2196
|
-
".vscode"
|
|
2197
|
-
]);
|
|
2198
|
-
function isGreenfieldByEntries(entries) {
|
|
2199
|
-
const meaningful = entries.filter((entry) => !GREENFIELD_SAFE_FILES.has(entry));
|
|
2200
|
-
return meaningful.length === 0;
|
|
2884
|
+
function purposeEvidence(profile) {
|
|
2885
|
+
if (profile.purpose.confidence === "low") return [];
|
|
2886
|
+
return profile.purpose.evidence;
|
|
2201
2887
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
const entries = await listDirectory(normalizedRoot);
|
|
2205
|
-
const stack = await detectStack(normalizedRoot);
|
|
2206
|
-
const isGreenfield = isGreenfieldByEntries(entries) || !stack.hasProjectFiles;
|
|
2207
|
-
return {
|
|
2208
|
-
rootDir: normalizedRoot,
|
|
2209
|
-
isGreenfield,
|
|
2210
|
-
stack,
|
|
2211
|
-
git: await detectGit(normalizedRoot),
|
|
2212
|
-
ide: await detectIde(normalizedRoot),
|
|
2213
|
-
infra: await detectInfra(normalizedRoot),
|
|
2214
|
-
services: await detectServices(normalizedRoot)
|
|
2215
|
-
};
|
|
2888
|
+
function contextEvidence(profile, pattern) {
|
|
2889
|
+
return profile.context.sources.filter((item) => pattern.test(item.value));
|
|
2216
2890
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
|
|
2220
|
-
var WORKSPACE_SKIN_MODE_DEFAULTS = {
|
|
2221
|
-
default: "autopilot",
|
|
2222
|
-
modes: {
|
|
2223
|
-
"continue-plan": "autopilot",
|
|
2224
|
-
"run-plan": "night-shift",
|
|
2225
|
-
"cli-run-plan": "ghost-runner"
|
|
2226
|
-
}
|
|
2227
|
-
};
|
|
2228
|
-
function workspaceSkinConfigFromChoice(choice) {
|
|
2229
|
-
if (choice.kind === "skip") return null;
|
|
2230
|
-
if (choice.kind === "mode-defaults")
|
|
2231
|
-
return { ...WORKSPACE_SKIN_MODE_DEFAULTS, modes: { ...WORKSPACE_SKIN_MODE_DEFAULTS.modes } };
|
|
2232
|
-
return {
|
|
2233
|
-
default: choice.id,
|
|
2234
|
-
modes: {
|
|
2235
|
-
"continue-plan": choice.id,
|
|
2236
|
-
"run-plan": choice.id,
|
|
2237
|
-
"cli-run-plan": choice.id
|
|
2238
|
-
}
|
|
2239
|
-
};
|
|
2891
|
+
function readinessEvidence(report, checkId) {
|
|
2892
|
+
return report.pillars.flatMap((pillar2) => pillar2.checks).filter((check2) => check2.id === checkId).flatMap((check2) => check2.evidence);
|
|
2240
2893
|
}
|
|
2241
|
-
function
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
process.exit(0);
|
|
2245
|
-
}
|
|
2246
|
-
return value;
|
|
2247
|
-
}
|
|
2248
|
-
async function askIdeAndPlan(current) {
|
|
2249
|
-
const ide = ensureNotCancelled(
|
|
2250
|
-
await select({
|
|
2251
|
-
message: "Which is your main IDE?",
|
|
2252
|
-
initialValue: current.ide === "unknown" ? void 0 : current.ide,
|
|
2253
|
-
options: [
|
|
2254
|
-
{ label: "Cursor", value: "cursor" },
|
|
2255
|
-
{ label: "VS Code", value: "vscode" },
|
|
2256
|
-
{ label: "Windsurf", value: "windsurf" },
|
|
2257
|
-
{ label: "Other", value: "other" }
|
|
2258
|
-
]
|
|
2259
|
-
})
|
|
2260
|
-
);
|
|
2261
|
-
const plan = ensureNotCancelled(
|
|
2262
|
-
await select({
|
|
2263
|
-
message: "What's your IDE plan?",
|
|
2264
|
-
initialValue: current.plan === "default" ? void 0 : current.plan,
|
|
2265
|
-
options: [
|
|
2266
|
-
{ label: "Cursor Free", value: "cursor-free" },
|
|
2267
|
-
{ label: "Cursor Pro / Business", value: "cursor-pro" },
|
|
2268
|
-
{ label: "VS Code + Copilot Free", value: "vscode-free" },
|
|
2269
|
-
{ label: "VS Code + Copilot Pro / Business", value: "vscode-pro" },
|
|
2270
|
-
{ label: "Windsurf", value: "windsurf" },
|
|
2271
|
-
{ label: "Don't know / default", value: "default" }
|
|
2272
|
-
]
|
|
2273
|
-
})
|
|
2274
|
-
);
|
|
2275
|
-
return { ide, plan };
|
|
2276
|
-
}
|
|
2277
|
-
async function askGitWorkflow(current) {
|
|
2278
|
-
return ensureNotCancelled(
|
|
2279
|
-
await select({
|
|
2280
|
-
message: "What's your Git workflow?",
|
|
2281
|
-
initialValue: current === "unknown" ? void 0 : current,
|
|
2282
|
-
options: [
|
|
2283
|
-
{ label: "trunk-based (direct to main)", value: "trunk-based" },
|
|
2284
|
-
{ label: "feature branch -> PR/MR -> main", value: "feature-pr" },
|
|
2285
|
-
{ label: "gitflow (develop/release/main)", value: "gitflow" },
|
|
2286
|
-
{ label: "staging -> production (staging -> main)", value: "homolog-prod" }
|
|
2287
|
-
]
|
|
2288
|
-
})
|
|
2894
|
+
function hasPurpose(profile, purposes) {
|
|
2895
|
+
return purposes.some(
|
|
2896
|
+
(purpose) => profile.purpose.value === purpose || profile.purpose.categories.includes(purpose)
|
|
2289
2897
|
);
|
|
2290
2898
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
message: "Do you use any project management system?",
|
|
2295
|
-
initialValues: existing,
|
|
2296
|
-
options: [
|
|
2297
|
-
{ label: "GitHub Issues / Projects", value: "github-issues" },
|
|
2298
|
-
{ label: "Jira", value: "jira" },
|
|
2299
|
-
{ label: "Linear", value: "linear" },
|
|
2300
|
-
{ label: "ClickUp", value: "clickup" },
|
|
2301
|
-
{ label: "Azure Boards", value: "azure-boards" },
|
|
2302
|
-
{ label: "Asana", value: "asana" },
|
|
2303
|
-
{ label: "Trello", value: "trello" },
|
|
2304
|
-
{ label: "Shortcut", value: "shortcut" },
|
|
2305
|
-
{ label: "Notion", value: "notion" },
|
|
2306
|
-
{ label: "YouTrack", value: "youtrack" }
|
|
2307
|
-
],
|
|
2308
|
-
required: false
|
|
2309
|
-
})
|
|
2310
|
-
);
|
|
2899
|
+
function candidate(kind, id, evidence, status) {
|
|
2900
|
+
const verified = uniqueEvidence(evidence);
|
|
2901
|
+
return verified.length > 0 ? { kind, id, status, evidence: verified } : null;
|
|
2311
2902
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
},
|
|
2322
|
-
{ label: "Autopilot", value: "autopilot" },
|
|
2323
|
-
{ label: "Night Shift", value: "night-shift" },
|
|
2324
|
-
{ label: "Ghost Runner", value: "ghost-runner" },
|
|
2325
|
-
{ label: "Skip for now", value: "skip" }
|
|
2326
|
-
]
|
|
2327
|
-
})
|
|
2328
|
-
);
|
|
2329
|
-
if (value === "skip") return { kind: "skip" };
|
|
2330
|
-
if (value === "mode-defaults") return { kind: "mode-defaults" };
|
|
2331
|
-
return { kind: "skin", id: value };
|
|
2332
|
-
}
|
|
2333
|
-
async function runExistingProjectWizard(scan) {
|
|
2334
|
-
const ide = await askIdeAndPlan(scan.ide);
|
|
2335
|
-
const workflow = await askGitWorkflow(scan.git.workflow);
|
|
2336
|
-
const projectManagement = await askProjectManagement(scan.services.projectManagement ?? []);
|
|
2337
|
-
const installHooks = ensureNotCancelled(
|
|
2338
|
-
await confirm({
|
|
2339
|
-
message: "Install git hooks? (pre-commit: secrets + lint)",
|
|
2340
|
-
initialValue: true
|
|
2341
|
-
})
|
|
2342
|
-
);
|
|
2343
|
-
const workspaceSkinChoice = await askWorkspaceSkin();
|
|
2344
|
-
return {
|
|
2345
|
-
rootDir: scan.rootDir,
|
|
2346
|
-
stack: scan.stack,
|
|
2347
|
-
git: { ...scan.git, workflow },
|
|
2348
|
-
ide,
|
|
2349
|
-
infra: scan.infra,
|
|
2350
|
-
services: {
|
|
2351
|
-
...scan.services,
|
|
2352
|
-
projectManagement: projectManagement.length > 0 ? projectManagement : void 0
|
|
2353
|
-
},
|
|
2354
|
-
installHooks,
|
|
2355
|
-
selectedCoreComponents: [
|
|
2356
|
-
"git-workflow",
|
|
2357
|
-
"security-review",
|
|
2358
|
-
"clean-code",
|
|
2359
|
-
"docs-repo",
|
|
2360
|
-
"ide-guide"
|
|
2361
|
-
],
|
|
2362
|
-
workspaceSkinChoice
|
|
2363
|
-
};
|
|
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
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
return true;
|
|
2364
2912
|
}
|
|
2365
|
-
|
|
2366
|
-
const
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
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))
|
|
2920
|
+
];
|
|
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"));
|
|
2934
|
+
}
|
|
2935
|
+
if (profile.stack.language.toLowerCase() === "node" && packageEvidence.length > 0) {
|
|
2936
|
+
items.push(candidate("skill", "cursor-skills-node", packageEvidence, "applied"));
|
|
2937
|
+
}
|
|
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"));
|
|
2942
|
+
}
|
|
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
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
if (promptEvidence.length > 0) {
|
|
2956
|
+
items.push(candidate("skill", "prompts-markdown", promptEvidence, "applied"));
|
|
2957
|
+
}
|
|
2958
|
+
if (infraEvidence.length > 0) {
|
|
2959
|
+
items.push(candidate("pack", "devops", infraEvidence, "applied"));
|
|
2960
|
+
}
|
|
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"));
|
|
2964
|
+
}
|
|
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
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
const safetyEvidence = readinessEvidence(report, "safety.secrets").filter(
|
|
2977
|
+
(item) => item.value.startsWith("tracked:")
|
|
2411
2978
|
);
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
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
|
+
)
|
|
2420
2989
|
);
|
|
2421
|
-
const
|
|
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
|
+
`;
|
|
3019
|
+
}
|
|
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");
|
|
2422
3033
|
return {
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
},
|
|
2429
|
-
git: {
|
|
2430
|
-
...scan.git,
|
|
2431
|
-
workflow
|
|
2432
|
-
},
|
|
2433
|
-
ide,
|
|
2434
|
-
infra: scan.infra,
|
|
2435
|
-
services: {
|
|
2436
|
-
database: database === "none" ? void 0 : database,
|
|
2437
|
-
orm: orm === "none" ? void 0 : orm,
|
|
2438
|
-
projectManagement: projectManagement.length > 0 ? projectManagement : void 0
|
|
2439
|
-
},
|
|
2440
|
-
installHooks,
|
|
2441
|
-
selectedCoreComponents: [
|
|
2442
|
-
"git-workflow",
|
|
2443
|
-
"security-review",
|
|
2444
|
-
"clean-code",
|
|
2445
|
-
"docs-repo",
|
|
2446
|
-
"ide-guide"
|
|
2447
|
-
],
|
|
2448
|
-
workspaceSkinChoice
|
|
3034
|
+
kind: "file",
|
|
3035
|
+
id: relativePath,
|
|
3036
|
+
path: relativePath,
|
|
3037
|
+
status: "applied",
|
|
3038
|
+
evidence
|
|
2449
3039
|
};
|
|
2450
3040
|
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
await
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
3041
|
+
async function packTargets(registryRoot, packId) {
|
|
3042
|
+
const manifest = await loadPackManifest(registryRoot, packId);
|
|
3043
|
+
return manifest.members.map((member) => packMemberTargets(member).targetRel);
|
|
3044
|
+
}
|
|
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;
|
|
2471
3063
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
logger.success(`Profile saved in ${configPath}`);
|
|
2485
|
-
const skinConfig = workspaceSkinChoice !== void 0 ? workspaceSkinConfigFromChoice(workspaceSkinChoice) : null;
|
|
2486
|
-
if (skinConfig) {
|
|
2487
|
-
const contextConfigPath = await mergeWorkspaceSkinConfig(scan.rootDir, skinConfig);
|
|
2488
|
-
logger.success(`Workspace skin saved in ${contextConfigPath}`);
|
|
2489
|
-
}
|
|
2490
|
-
await generateFromProfile(profile);
|
|
2491
|
-
try {
|
|
2492
|
-
const registry = await resolveRegistryRoot({ cwd: scan.rootDir });
|
|
2493
|
-
const stats = await installSkillsByIds(
|
|
2494
|
-
registry.root,
|
|
2495
|
-
scan.rootDir,
|
|
2496
|
-
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"
|
|
2497
3076
|
);
|
|
2498
|
-
if (
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
} else {
|
|
2503
|
-
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;
|
|
2504
3081
|
}
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
|
2508
3098
|
);
|
|
3099
|
+
continue;
|
|
2509
3100
|
}
|
|
2510
|
-
|
|
2511
|
-
console.log(` Open this folder in Cursor: ${scan.rootDir}`);
|
|
2512
|
-
console.log(" Run /onboard in chat; then /start-project when you have a goal");
|
|
2513
|
-
console.log(" Optional: agent-kit status");
|
|
2514
|
-
outro("Setup completed.");
|
|
3101
|
+
componentResults.push(item);
|
|
2515
3102
|
}
|
|
2516
|
-
|
|
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
|
+
}
|
|
2517
3154
|
|
|
2518
|
-
// src/
|
|
2519
|
-
import
|
|
2520
|
-
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
|
+
}
|
|
2521
3175
|
|
|
2522
3176
|
// src/lifecycle/sync.ts
|
|
2523
3177
|
async function installL0(registryRoot, projectRoot, protectedGlobs) {
|
|
@@ -2532,6 +3186,12 @@ async function installL0(registryRoot, projectRoot, protectedGlobs) {
|
|
|
2532
3186
|
);
|
|
2533
3187
|
recordOutcome(stats, artifact.target, outcome);
|
|
2534
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
|
+
}
|
|
2535
3195
|
return stats;
|
|
2536
3196
|
}
|
|
2537
3197
|
async function syncFromManifest(registryRoot, projectRoot, manifest) {
|
|
@@ -2561,11 +3221,73 @@ function parsePackList(raw) {
|
|
|
2561
3221
|
)
|
|
2562
3222
|
];
|
|
2563
3223
|
}
|
|
2564
|
-
function
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
console.log("
|
|
2568
|
-
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
|
+
};
|
|
2569
3291
|
}
|
|
2570
3292
|
var installCommand = defineCommand6({
|
|
2571
3293
|
meta: {
|
|
@@ -2589,50 +3311,61 @@ var installCommand = defineCommand6({
|
|
|
2589
3311
|
...REGISTRY_CLI_ARGS
|
|
2590
3312
|
},
|
|
2591
3313
|
async run({ args }) {
|
|
2592
|
-
const projectRoot =
|
|
3314
|
+
const projectRoot = path23.resolve(args.cwd);
|
|
2593
3315
|
logger.info(`Installing into: ${projectRoot}`);
|
|
2594
3316
|
const packs = parsePackList(args.pack);
|
|
2595
3317
|
for (const id of packs) {
|
|
2596
3318
|
if (!DOMAIN_PACK_IDS.includes(id)) {
|
|
2597
|
-
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.`);
|
|
2598
3320
|
}
|
|
2599
3321
|
}
|
|
2600
|
-
const
|
|
2601
|
-
const registry = await resolveRegistryFromCli({
|
|
3322
|
+
const result = await performInstall({
|
|
2602
3323
|
cwd: projectRoot,
|
|
3324
|
+
profile: args.profile,
|
|
3325
|
+
pack: args.pack,
|
|
2603
3326
|
registry: args.registry,
|
|
2604
3327
|
url: args.url,
|
|
2605
3328
|
ref: args.ref,
|
|
2606
|
-
refresh: args.refresh
|
|
2607
|
-
manifest: existing
|
|
2608
|
-
});
|
|
2609
|
-
logger.info(`Registry: ${registry.root} (${registry.source})`);
|
|
2610
|
-
const draft = buildManifest({
|
|
2611
|
-
version: KIT_VERSION,
|
|
2612
|
-
profile: args.profile ?? existing?.profile ?? "default",
|
|
2613
|
-
packs: packs.length > 0 ? packs : existing?.packs,
|
|
2614
|
-
skills: existing?.skills,
|
|
2615
|
-
protected: existing?.protected,
|
|
2616
|
-
registryUrl: registry.url ?? existing?.registry?.url,
|
|
2617
|
-
registryRef: registry.ref ?? existing?.registry?.ref
|
|
3329
|
+
refresh: args.refresh
|
|
2618
3330
|
});
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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()
|
|
2625
3352
|
}
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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
|
+
);
|
|
2630
3363
|
}
|
|
2631
3364
|
});
|
|
2632
3365
|
|
|
2633
3366
|
// src/commands/run-plan.ts
|
|
2634
|
-
import
|
|
2635
|
-
import { defineCommand as
|
|
3367
|
+
import path28 from "path";
|
|
3368
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
2636
3369
|
|
|
2637
3370
|
// src/plan-loop/backends.ts
|
|
2638
3371
|
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
@@ -2716,14 +3449,14 @@ function listBackendIds() {
|
|
|
2716
3449
|
}
|
|
2717
3450
|
|
|
2718
3451
|
// src/plan-loop/run-loop.ts
|
|
2719
|
-
import { mkdir as mkdir4, readFile as
|
|
2720
|
-
import
|
|
3452
|
+
import { mkdir as mkdir4, readFile as readFile14, rm, unlink as unlink2 } from "fs/promises";
|
|
3453
|
+
import path27 from "path";
|
|
2721
3454
|
|
|
2722
3455
|
// src/plan-loop/external-review.ts
|
|
2723
3456
|
import { spawn as spawn3 } from "child_process";
|
|
2724
|
-
import
|
|
2725
|
-
var CANONICAL_REL =
|
|
2726
|
-
var FALLBACK_REL =
|
|
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");
|
|
2727
3460
|
function isPlanExhaustedReason(reason) {
|
|
2728
3461
|
const r = reason.trim().toLowerCase();
|
|
2729
3462
|
if (!r) return false;
|
|
@@ -2745,8 +3478,8 @@ async function armExternalPlanReview(root, options = {}) {
|
|
|
2745
3478
|
const existsFn = options.existsFn ?? fileExists;
|
|
2746
3479
|
const log = options.log ?? ((line) => console.log(line));
|
|
2747
3480
|
const force = options.force === true;
|
|
2748
|
-
const canonicalPath =
|
|
2749
|
-
const fallbackPath =
|
|
3481
|
+
const canonicalPath = path24.join(root, CANONICAL_REL);
|
|
3482
|
+
const fallbackPath = path24.join(root, FALLBACK_REL);
|
|
2750
3483
|
let scriptPath = null;
|
|
2751
3484
|
let scriptRel = CANONICAL_REL;
|
|
2752
3485
|
if (await existsFn(canonicalPath)) {
|
|
@@ -2799,8 +3532,8 @@ async function armExternalPlanReview(root, options = {}) {
|
|
|
2799
3532
|
}
|
|
2800
3533
|
|
|
2801
3534
|
// src/plan-loop/plan-state.ts
|
|
2802
|
-
import { readFile as
|
|
2803
|
-
import
|
|
3535
|
+
import { readFile as readFile12, readdir as readdir3 } from "fs/promises";
|
|
3536
|
+
import path25 from "path";
|
|
2804
3537
|
function countPendingTodos(raw) {
|
|
2805
3538
|
const lines = raw.split(/\r?\n/);
|
|
2806
3539
|
let inFront = 0;
|
|
@@ -2828,18 +3561,18 @@ function countPendingTodos(raw) {
|
|
|
2828
3561
|
async function findActivePlanFile(plansDir) {
|
|
2829
3562
|
if (!await fileExists(plansDir)) return null;
|
|
2830
3563
|
const files = (await readdir3(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
|
|
2831
|
-
return files[0] ?
|
|
3564
|
+
return files[0] ? path25.join(plansDir, files[0]) : null;
|
|
2832
3565
|
}
|
|
2833
3566
|
async function readPlan(planPath) {
|
|
2834
|
-
return
|
|
3567
|
+
return readFile12(planPath, "utf8");
|
|
2835
3568
|
}
|
|
2836
3569
|
|
|
2837
3570
|
// src/plan-loop/sentinel.ts
|
|
2838
|
-
import { readFile as
|
|
3571
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2839
3572
|
var SENTINEL_RE = /LOOP_TICK_RESULT:\s*(continue|stop(?:\s*[—\-].*)?)/i;
|
|
2840
|
-
function takeFromText(
|
|
2841
|
-
if (!
|
|
2842
|
-
const m = SENTINEL_RE.exec(
|
|
3573
|
+
function takeFromText(text) {
|
|
3574
|
+
if (!text) return null;
|
|
3575
|
+
const m = SENTINEL_RE.exec(text);
|
|
2843
3576
|
if (!m?.[1]) return null;
|
|
2844
3577
|
const raw = m[1].trim();
|
|
2845
3578
|
if (raw.toLowerCase().startsWith("stop")) {
|
|
@@ -2882,7 +3615,7 @@ function parseSentinelFromLog(content) {
|
|
|
2882
3615
|
}
|
|
2883
3616
|
async function parseSentinelFromLogFile(logPath) {
|
|
2884
3617
|
try {
|
|
2885
|
-
const content = await
|
|
3618
|
+
const content = await readFile13(logPath, "utf8");
|
|
2886
3619
|
return parseSentinelFromLog(content);
|
|
2887
3620
|
} catch {
|
|
2888
3621
|
return { kind: "missing" };
|
|
@@ -2895,7 +3628,7 @@ function formatSentinelLine(sentinel) {
|
|
|
2895
3628
|
}
|
|
2896
3629
|
|
|
2897
3630
|
// src/plan-loop/skin-banners.ts
|
|
2898
|
-
import
|
|
3631
|
+
import path26 from "path";
|
|
2899
3632
|
import {
|
|
2900
3633
|
blue,
|
|
2901
3634
|
cyan as cyan2,
|
|
@@ -2931,7 +3664,7 @@ function resolveColor(name, fallback) {
|
|
|
2931
3664
|
async function resolveCliSkinId(root) {
|
|
2932
3665
|
try {
|
|
2933
3666
|
const cfg = await readJson(
|
|
2934
|
-
|
|
3667
|
+
path26.join(root, ".cursor", "context", "config.json")
|
|
2935
3668
|
);
|
|
2936
3669
|
const id = cfg?.workspaceSkin?.modes?.[CLI_RUN_PLAN_MODE];
|
|
2937
3670
|
if (typeof id === "string" && id.trim()) return id.trim();
|
|
@@ -2941,7 +3674,7 @@ async function resolveCliSkinId(root) {
|
|
|
2941
3674
|
}
|
|
2942
3675
|
async function loadSkinPack(root, skinId) {
|
|
2943
3676
|
try {
|
|
2944
|
-
const skinPath =
|
|
3677
|
+
const skinPath = path26.join(root, "registry", "skins", "core", skinId, "skin.json");
|
|
2945
3678
|
const pack = await readJson(skinPath);
|
|
2946
3679
|
if (!pack || typeof pack.id !== "string") return null;
|
|
2947
3680
|
return pack;
|
|
@@ -3000,9 +3733,9 @@ function sleep(ms) {
|
|
|
3000
3733
|
return new Promise((r) => setTimeout(r, ms));
|
|
3001
3734
|
}
|
|
3002
3735
|
async function runPlanLoop(opts) {
|
|
3003
|
-
const plansDir =
|
|
3004
|
-
const stopFile =
|
|
3005
|
-
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");
|
|
3006
3739
|
const planPath = await findActivePlanFile(plansDir);
|
|
3007
3740
|
if (!planPath) {
|
|
3008
3741
|
logger.error("No active plan in .cursor/plans/");
|
|
@@ -3011,7 +3744,7 @@ async function runPlanLoop(opts) {
|
|
|
3011
3744
|
const pending = async () => countPendingTodos(await readPlan(planPath));
|
|
3012
3745
|
await mkdir4(logDir, { recursive: true });
|
|
3013
3746
|
try {
|
|
3014
|
-
await
|
|
3747
|
+
await unlink2(stopFile);
|
|
3015
3748
|
} catch {
|
|
3016
3749
|
}
|
|
3017
3750
|
const onSigInt = () => {
|
|
@@ -3023,7 +3756,7 @@ async function runPlanLoop(opts) {
|
|
|
3023
3756
|
try {
|
|
3024
3757
|
const skin = await loadCliRunPlanSkin(opts.root);
|
|
3025
3758
|
const banners = createSkinBannerPrinter(skin);
|
|
3026
|
-
console.log(`Active plan: ${
|
|
3759
|
+
console.log(`Active plan: ${path27.basename(planPath)}`);
|
|
3027
3760
|
console.log(`Pending to-dos: ${await pending()} | max ticks: ${opts.maxTicks}`);
|
|
3028
3761
|
console.log(`Backend: ${opts.backend.id}`);
|
|
3029
3762
|
if (skin) {
|
|
@@ -3066,8 +3799,8 @@ async function runPlanLoop(opts) {
|
|
|
3066
3799
|
planExhausted = true;
|
|
3067
3800
|
break;
|
|
3068
3801
|
}
|
|
3069
|
-
const logPath =
|
|
3070
|
-
const relLog =
|
|
3802
|
+
const logPath = path27.join(logDir, `tick-${stamp()}.log`);
|
|
3803
|
+
const relLog = path27.relative(opts.root, logPath);
|
|
3071
3804
|
console.log("");
|
|
3072
3805
|
const tickLine = `=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`;
|
|
3073
3806
|
if (banners) banners.tickStart(tickLine);
|
|
@@ -3086,7 +3819,7 @@ async function runPlanLoop(opts) {
|
|
|
3086
3819
|
return 1;
|
|
3087
3820
|
}
|
|
3088
3821
|
try {
|
|
3089
|
-
const logText = await
|
|
3822
|
+
const logText = await readFile14(logPath, "utf8");
|
|
3090
3823
|
if (logText.includes("Too many MCP tools")) {
|
|
3091
3824
|
const msg = "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again.";
|
|
3092
3825
|
if (banners) banners.stop(msg);
|
|
@@ -3143,7 +3876,7 @@ async function runPlanLoop(opts) {
|
|
|
3143
3876
|
const finishDetail = `after ${tick} tick(s); pending: ${pendingNow}`;
|
|
3144
3877
|
if (banners) banners.phaseComplete(finishDetail);
|
|
3145
3878
|
console.log(
|
|
3146
|
-
`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)}/`
|
|
3147
3880
|
);
|
|
3148
3881
|
if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
|
|
3149
3882
|
await armExternalPlanReview(opts.root);
|
|
@@ -3155,7 +3888,7 @@ async function runPlanLoop(opts) {
|
|
|
3155
3888
|
}
|
|
3156
3889
|
|
|
3157
3890
|
// src/commands/run-plan.ts
|
|
3158
|
-
var runPlanCommand =
|
|
3891
|
+
var runPlanCommand = defineCommand8({
|
|
3159
3892
|
meta: {
|
|
3160
3893
|
name: "run-plan",
|
|
3161
3894
|
description: "Headless continuous plan runner: one fresh agent per tick (LOOP_TICK_RESULT contract). Never git-prod."
|
|
@@ -3214,7 +3947,7 @@ var runPlanCommand = defineCommand7({
|
|
|
3214
3947
|
return;
|
|
3215
3948
|
}
|
|
3216
3949
|
const code = await runPlanLoop({
|
|
3217
|
-
root:
|
|
3950
|
+
root: path28.resolve(args.cwd),
|
|
3218
3951
|
maxTicks,
|
|
3219
3952
|
sleepSeconds,
|
|
3220
3953
|
model: args.model ? String(args.model) : void 0,
|
|
@@ -3226,8 +3959,8 @@ var runPlanCommand = defineCommand7({
|
|
|
3226
3959
|
});
|
|
3227
3960
|
|
|
3228
3961
|
// src/commands/scan.ts
|
|
3229
|
-
import { defineCommand as
|
|
3230
|
-
var scanCommand =
|
|
3962
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
3963
|
+
var scanCommand = defineCommand9({
|
|
3231
3964
|
meta: {
|
|
3232
3965
|
name: "scan",
|
|
3233
3966
|
description: "Scan the current repository and print detected profile."
|
|
@@ -3248,9 +3981,21 @@ var scanCommand = defineCommand8({
|
|
|
3248
3981
|
});
|
|
3249
3982
|
|
|
3250
3983
|
// src/commands/status.ts
|
|
3251
|
-
import
|
|
3252
|
-
import { defineCommand as
|
|
3253
|
-
|
|
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({
|
|
3254
3999
|
meta: {
|
|
3255
4000
|
name: "status",
|
|
3256
4001
|
description: "Show Agent Kit distribution status (manifest + optional wizard profile)."
|
|
@@ -3267,15 +4012,26 @@ var statusCommand = defineCommand9({
|
|
|
3267
4012
|
}
|
|
3268
4013
|
},
|
|
3269
4014
|
async run({ args }) {
|
|
3270
|
-
const
|
|
3271
|
-
const
|
|
3272
|
-
|
|
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];
|
|
3273
4026
|
if (args.json) {
|
|
3274
4027
|
console.log(
|
|
3275
4028
|
JSON.stringify(
|
|
3276
4029
|
{
|
|
4030
|
+
runtimeVersion: KIT_VERSION,
|
|
3277
4031
|
manifest: manifest ?? null,
|
|
3278
|
-
|
|
4032
|
+
readiness,
|
|
4033
|
+
pendingActions: readiness.pendingActions,
|
|
4034
|
+
profile
|
|
3279
4035
|
},
|
|
3280
4036
|
null,
|
|
3281
4037
|
2
|
|
@@ -3284,11 +4040,12 @@ var statusCommand = defineCommand9({
|
|
|
3284
4040
|
return;
|
|
3285
4041
|
}
|
|
3286
4042
|
if (!manifest) {
|
|
3287
|
-
logger.warn(`No ${MANIFEST_RELATIVE_PATH}
|
|
4043
|
+
logger.warn(`No ${MANIFEST_RELATIVE_PATH}: run agent-kit install.`);
|
|
3288
4044
|
} else {
|
|
3289
4045
|
const protectedGlobs = resolveProtectedGlobs(manifest);
|
|
3290
4046
|
console.log("Agent Kit status");
|
|
3291
|
-
console.log(`
|
|
4047
|
+
console.log(` runtime: ${KIT_VERSION}`);
|
|
4048
|
+
console.log(` installed: ${manifest.version}`);
|
|
3292
4049
|
console.log(` profile: ${manifest.profile ?? "(none)"}`);
|
|
3293
4050
|
console.log(` packs: ${(manifest.packs ?? []).join(", ") || "(none)"}`);
|
|
3294
4051
|
console.log(` skills: ${(manifest.skills ?? []).length} listed`);
|
|
@@ -3296,21 +4053,26 @@ var statusCommand = defineCommand9({
|
|
|
3296
4053
|
console.log(
|
|
3297
4054
|
` registry: ${manifest.registry?.url ?? "(default)"} @ ${manifest.registry?.ref ?? "(default)"}`
|
|
3298
4055
|
);
|
|
3299
|
-
if (manifest.installedAt) console.log(` installed:
|
|
3300
|
-
}
|
|
3301
|
-
if (profile) {
|
|
3302
|
-
console.log("Wizard profile (agent-kit.config.json): present");
|
|
3303
|
-
console.log(` IDE: ${profile.ide?.ide ?? "?"}`);
|
|
3304
|
-
console.log(` core picks: ${(profile.selectedCoreComponents ?? []).join(", ") || "(none)"}`);
|
|
3305
|
-
} else {
|
|
3306
|
-
logger.info("No wizard profile \u2014 optional; run agent-kit init for generators.");
|
|
4056
|
+
if (manifest.installedAt) console.log(` installed at: ${manifest.installedAt}`);
|
|
3307
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
|
+
);
|
|
3308
4070
|
}
|
|
3309
4071
|
});
|
|
3310
4072
|
|
|
3311
4073
|
// src/commands/update.ts
|
|
3312
|
-
import { defineCommand as
|
|
3313
|
-
var updateCommand =
|
|
4074
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
4075
|
+
var updateCommand = defineCommand11({
|
|
3314
4076
|
meta: {
|
|
3315
4077
|
name: "update",
|
|
3316
4078
|
description: "Re-apply L0/packs/skills from the registry; never overwrites L3 protected paths."
|
|
@@ -3355,7 +4117,7 @@ var updateCommand = defineCommand10({
|
|
|
3355
4117
|
});
|
|
3356
4118
|
|
|
3357
4119
|
// src/index.ts
|
|
3358
|
-
var main =
|
|
4120
|
+
var main = defineCommand12({
|
|
3359
4121
|
meta: {
|
|
3360
4122
|
name: "agent-kit",
|
|
3361
4123
|
description: "HITL framework for AI-assisted IDEs"
|
|
@@ -3365,6 +4127,7 @@ var main = defineCommand11({
|
|
|
3365
4127
|
install: installCommand,
|
|
3366
4128
|
scan: scanCommand,
|
|
3367
4129
|
add: addCommand,
|
|
4130
|
+
doctor: doctorCommand,
|
|
3368
4131
|
status: statusCommand,
|
|
3369
4132
|
update: updateCommand,
|
|
3370
4133
|
diff: diffCommand,
|