runshift 0.0.2 → 0.0.3
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/.claude/settings.local.json +11 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +129 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/context/collector.d.ts +4 -0
- package/dist/context/collector.d.ts.map +1 -0
- package/dist/context/collector.js +191 -0
- package/dist/context/collector.js.map +1 -0
- package/dist/context/git.d.ts +7 -0
- package/dist/context/git.d.ts.map +1 -0
- package/dist/context/git.js +30 -0
- package/dist/context/git.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/display.d.ts +14 -0
- package/dist/ui/display.d.ts.map +1 -0
- package/dist/ui/display.js +148 -0
- package/dist/ui/display.js.map +1 -0
- package/dist/ui/prompt.d.ts +4 -0
- package/dist/ui/prompt.d.ts.map +1 -0
- package/dist/ui/prompt.js +31 -0
- package/dist/ui/prompt.js.map +1 -0
- package/dist/writer.d.ts +4 -0
- package/dist/writer.d.ts.map +1 -0
- package/dist/writer.js +29 -0
- package/dist/writer.js.map +1 -0
- package/package.json +22 -4
- package/src/commands/init.ts +163 -0
- package/src/context/collector.ts +205 -0
- package/src/context/git.ts +36 -0
- package/src/index.ts +35 -0
- package/src/types.ts +44 -0
- package/src/ui/display.ts +164 -0
- package/src/ui/prompt.ts +33 -0
- package/src/writer.ts +37 -0
- package/tsconfig.json +19 -0
- package/index.js +0 -2
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import figlet from "figlet";
|
|
3
|
+
const amber = chalk.hex("#f5a623");
|
|
4
|
+
const muted = chalk.hex("#6b6b7b");
|
|
5
|
+
const dim = chalk.dim;
|
|
6
|
+
const divider = muted(" " + "─".repeat(45));
|
|
7
|
+
export function showBanner() {
|
|
8
|
+
const banner = figlet.textSync("runshift", {
|
|
9
|
+
font: "Standard",
|
|
10
|
+
horizontalLayout: "default",
|
|
11
|
+
});
|
|
12
|
+
console.log(amber(banner));
|
|
13
|
+
console.log(muted(" v0.0.2"));
|
|
14
|
+
console.log(muted(" the control plane for agents, wherever they run.\n"));
|
|
15
|
+
console.log(divider + "\n");
|
|
16
|
+
}
|
|
17
|
+
export function showNotGitRepo() {
|
|
18
|
+
console.log(amber(" this directory is not a git repository."));
|
|
19
|
+
console.log(dim(" run runshift init from a project root with git initialized.\n"));
|
|
20
|
+
}
|
|
21
|
+
export function showDirtyWarning() {
|
|
22
|
+
console.log(muted(" ⚠ uncommitted changes detected\n"));
|
|
23
|
+
}
|
|
24
|
+
export function showBranchInfo(branch) {
|
|
25
|
+
console.log(dim(` on branch ${amber(branch)}\n`));
|
|
26
|
+
}
|
|
27
|
+
export function showScanResults(context) {
|
|
28
|
+
console.log(amber(" relay scanned your repository:\n"));
|
|
29
|
+
const deps = {
|
|
30
|
+
...context.packageJson.dependencies,
|
|
31
|
+
...context.packageJson.devDependencies,
|
|
32
|
+
};
|
|
33
|
+
const detections = [];
|
|
34
|
+
if (context.packageJson.name) {
|
|
35
|
+
const stack = [];
|
|
36
|
+
if (deps["next"])
|
|
37
|
+
stack.push("Next.js");
|
|
38
|
+
if (deps["@supabase/supabase-js"] || deps["@supabase/ssr"])
|
|
39
|
+
stack.push("Supabase");
|
|
40
|
+
if (deps["tailwindcss"])
|
|
41
|
+
stack.push("Tailwind");
|
|
42
|
+
if (deps["prisma"] || deps["@prisma/client"])
|
|
43
|
+
stack.push("Prisma");
|
|
44
|
+
if (deps["drizzle-orm"])
|
|
45
|
+
stack.push("Drizzle");
|
|
46
|
+
if (deps["stripe"])
|
|
47
|
+
stack.push("Stripe");
|
|
48
|
+
const label = stack.length > 0 ? stack.join(", ") + " detected" : "detected";
|
|
49
|
+
detections.push(`package.json — ${label}`);
|
|
50
|
+
}
|
|
51
|
+
if (context.envKeys.length > 0) {
|
|
52
|
+
detections.push(`.env.example — ${context.envKeys.length} environment variable${context.envKeys.length === 1 ? "" : "s"} found`);
|
|
53
|
+
}
|
|
54
|
+
if (context.migrationCount > 0) {
|
|
55
|
+
detections.push(`supabase/migrations/ — ${context.migrationCount} migration file${context.migrationCount === 1 ? "" : "s"} found`);
|
|
56
|
+
}
|
|
57
|
+
const existingRuleKeys = Object.keys(context.existingRules);
|
|
58
|
+
const cursorRules = existingRuleKeys.filter((k) => k.startsWith(".cursor/rules/"));
|
|
59
|
+
if (cursorRules.length > 0) {
|
|
60
|
+
detections.push(`.cursor/rules/ — ${cursorRules.length} existing file${cursorRules.length === 1 ? "" : "s"} detected`);
|
|
61
|
+
}
|
|
62
|
+
if (existingRuleKeys.includes("CLAUDE.md")) {
|
|
63
|
+
detections.push("existing CLAUDE.md detected");
|
|
64
|
+
}
|
|
65
|
+
if (context.tsconfig) {
|
|
66
|
+
detections.push("tsconfig.json detected");
|
|
67
|
+
}
|
|
68
|
+
const configCount = Object.keys(context.configFiles).length;
|
|
69
|
+
if (configCount > 0) {
|
|
70
|
+
detections.push(`${configCount} config file${configCount === 1 ? "" : "s"} found`);
|
|
71
|
+
}
|
|
72
|
+
for (const d of detections) {
|
|
73
|
+
console.log(dim(" ✓ ") + d);
|
|
74
|
+
}
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
export function showFindings(findings) {
|
|
78
|
+
const sections = [
|
|
79
|
+
["blast radius", findings.blastRadius],
|
|
80
|
+
["security gaps", findings.securityGaps],
|
|
81
|
+
["agent failure patterns", findings.agentFailurePatterns],
|
|
82
|
+
["parallelization boundaries", findings.parallelizationBoundaries],
|
|
83
|
+
["deprecated patterns", findings.deprecatedPatterns],
|
|
84
|
+
];
|
|
85
|
+
const hasFindings = sections.some(([, items]) => items.length > 0);
|
|
86
|
+
if (!hasFindings)
|
|
87
|
+
return;
|
|
88
|
+
console.log(amber(" relay found issues in your codebase:\n"));
|
|
89
|
+
for (const [title, items] of sections) {
|
|
90
|
+
if (items.length === 0)
|
|
91
|
+
continue;
|
|
92
|
+
console.log(amber(` ${title}`));
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
console.log(dim(` → ${item}`));
|
|
95
|
+
}
|
|
96
|
+
console.log();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function showFileList(files) {
|
|
100
|
+
console.log(amber(` relay will write ${files.length} file${files.length === 1 ? "" : "s"}:\n`));
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const prefix = file.action === "create" ? amber("+") : amber("~");
|
|
103
|
+
const action = file.action === "create" ? dim("(create)") : dim("(update — existing file)");
|
|
104
|
+
console.log(` ${prefix} ${file.path} ${action}`);
|
|
105
|
+
}
|
|
106
|
+
console.log(dim("\n + = create, ~ = update existing file\n"));
|
|
107
|
+
}
|
|
108
|
+
export function showWriting(filePath) {
|
|
109
|
+
console.log(dim(" ✓ ") + filePath);
|
|
110
|
+
}
|
|
111
|
+
export function showCommit() {
|
|
112
|
+
console.log(dim(" ✓ ") + "committed");
|
|
113
|
+
}
|
|
114
|
+
export function showSummary(summary) {
|
|
115
|
+
console.log(muted(` ${summary.replace(/\n/g, "\n ")}\n`));
|
|
116
|
+
}
|
|
117
|
+
export function showSuccess() {
|
|
118
|
+
console.log("\n" + divider + "\n");
|
|
119
|
+
console.log(amber(" ✓ relay is installed in your development workflow\n"));
|
|
120
|
+
console.log(muted(" next steps:"));
|
|
121
|
+
console.log(dim(" → open Claude Code and type /validate to run your first check"));
|
|
122
|
+
console.log(dim(" → type /runshift-update to refresh rules as your stack evolves\n"));
|
|
123
|
+
console.log(muted(" connect to the runshift control plane: ") + amber("runshift.ai"));
|
|
124
|
+
console.log("\n" + divider + "\n");
|
|
125
|
+
}
|
|
126
|
+
export function showError(type, message) {
|
|
127
|
+
console.log();
|
|
128
|
+
switch (type) {
|
|
129
|
+
case "network":
|
|
130
|
+
console.log(amber(" could not reach relay."));
|
|
131
|
+
console.log(dim(" check your connection and try again.\n"));
|
|
132
|
+
break;
|
|
133
|
+
case "rate-limit":
|
|
134
|
+
console.log(amber(" relay rate limit reached — try again in 1 hour.\n"));
|
|
135
|
+
break;
|
|
136
|
+
case "validation":
|
|
137
|
+
console.log(amber(" relay could not read this repository."));
|
|
138
|
+
if (message)
|
|
139
|
+
console.log(dim(` ${message}\n`));
|
|
140
|
+
break;
|
|
141
|
+
case "server":
|
|
142
|
+
console.log(amber(" relay encountered an error — try again or visit runshift.ai."));
|
|
143
|
+
if (message)
|
|
144
|
+
console.log(dim(` ${message}\n`));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=display.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"display.js","sourceRoot":"","sources":["../../src/ui/display.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/ui/prompt.ts"],"names":[],"mappings":"AAgBA,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAIhE;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAM7E;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
function ask(question) {
|
|
3
|
+
const rl = readline.createInterface({
|
|
4
|
+
input: process.stdin,
|
|
5
|
+
output: process.stdout,
|
|
6
|
+
});
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
rl.question(question, (answer) => {
|
|
9
|
+
rl.close();
|
|
10
|
+
resolve(answer.trim());
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function confirm(question) {
|
|
15
|
+
const answer = await ask(question);
|
|
16
|
+
const normalized = answer.toLowerCase();
|
|
17
|
+
return normalized === "y" || normalized === "yes";
|
|
18
|
+
}
|
|
19
|
+
export async function promptChoice(question) {
|
|
20
|
+
const answer = await ask(question);
|
|
21
|
+
const normalized = answer.toLowerCase();
|
|
22
|
+
if (normalized === "a" || normalized === "add")
|
|
23
|
+
return "a";
|
|
24
|
+
if (normalized === "y" || normalized === "yes")
|
|
25
|
+
return "y";
|
|
26
|
+
return "n";
|
|
27
|
+
}
|
|
28
|
+
export async function promptFilePath(question) {
|
|
29
|
+
return ask(question);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/ui/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAE1C,SAAS,GAAG,CAAC,QAAgB;IAC3B,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IAEH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE;YAC/B,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,QAAgB;IAC5C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACxC,OAAO,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK;QAAE,OAAO,GAAG,CAAC;IAC3D,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK;QAAE,OAAO,GAAG,CAAC;IAC3D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAgB;IACnD,OAAO,GAAG,CAAC,QAAQ,CAAC,CAAC;AACvB,CAAC"}
|
package/dist/writer.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,IAAI,CASrE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,OAAO,CAmBzE"}
|
package/dist/writer.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { showWriting, showCommit } from "./ui/display.js";
|
|
5
|
+
export function writeFiles(root, files) {
|
|
6
|
+
for (const file of files) {
|
|
7
|
+
const fullPath = path.join(root, file.path);
|
|
8
|
+
const dir = path.dirname(fullPath);
|
|
9
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
10
|
+
fs.writeFileSync(fullPath, file.content, "utf-8");
|
|
11
|
+
showWriting(file.path);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function commitFiles(root, files) {
|
|
15
|
+
try {
|
|
16
|
+
const filePaths = files.map((f) => `"${f.path}"`).join(" ");
|
|
17
|
+
execSync(`git add ${filePaths}`, {
|
|
18
|
+
cwd: root,
|
|
19
|
+
stdio: "pipe",
|
|
20
|
+
});
|
|
21
|
+
execSync('git commit -m "chore: install runshift agent governance rules"', { cwd: root, stdio: "pipe" });
|
|
22
|
+
showCommit();
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAsB;IAC7D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAEnC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAClD,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,KAAsB;IAC9D,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAE5D,QAAQ,CAAC,WAAW,SAAS,EAAE,EAAE;YAC/B,GAAG,EAAE,IAAI;YACT,KAAK,EAAE,MAAM;SACd,CAAC,CAAC;QAEH,QAAQ,CACN,gEAAgE,EAChE,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAC7B,CAAC;QAEF,UAAU,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runshift",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "The control plane for agents, wherever they run.",
|
|
5
|
-
"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
6
7
|
"bin": {
|
|
7
|
-
"runshift": "index.js"
|
|
8
|
+
"runshift": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
8
13
|
},
|
|
9
14
|
"keywords": [
|
|
10
15
|
"ai",
|
|
@@ -14,5 +19,18 @@
|
|
|
14
19
|
"governance"
|
|
15
20
|
],
|
|
16
21
|
"author": "Devin Crane",
|
|
17
|
-
"license": "MIT"
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"chalk": "^5.4.1",
|
|
25
|
+
"figlet": "^1.8.0",
|
|
26
|
+
"ora": "^8.2.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/figlet": "^1.7.0",
|
|
30
|
+
"@types/node": "^22.15.0",
|
|
31
|
+
"typescript": "^5.8.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
18
36
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import { collectRepoContext, addFileToContext } from "../context/collector.js";
|
|
3
|
+
import { getGitState } from "../context/git.js";
|
|
4
|
+
import {
|
|
5
|
+
showBanner,
|
|
6
|
+
showNotGitRepo,
|
|
7
|
+
showDirtyWarning,
|
|
8
|
+
showBranchInfo,
|
|
9
|
+
showScanResults,
|
|
10
|
+
showFindings,
|
|
11
|
+
showFileList,
|
|
12
|
+
showSummary,
|
|
13
|
+
showSuccess,
|
|
14
|
+
showError,
|
|
15
|
+
} from "../ui/display.js";
|
|
16
|
+
import { confirm, promptChoice, promptFilePath } from "../ui/prompt.js";
|
|
17
|
+
import { writeFiles, commitFiles } from "../writer.js";
|
|
18
|
+
import type { InitResponse } from "../types.js";
|
|
19
|
+
|
|
20
|
+
const API_URL =
|
|
21
|
+
process.env.RUNSHIFT_DEV === "true"
|
|
22
|
+
? "http://localhost:3000/api/cli/init"
|
|
23
|
+
: "https://runshift.ai/api/cli/init";
|
|
24
|
+
|
|
25
|
+
const TIMEOUT_MS = 240_000;
|
|
26
|
+
|
|
27
|
+
export async function init(): Promise<void> {
|
|
28
|
+
showBanner();
|
|
29
|
+
|
|
30
|
+
// ── 1. Git safety ─────────────────────────────────────────────────
|
|
31
|
+
const git = getGitState();
|
|
32
|
+
|
|
33
|
+
if (!git.isGitRepo) {
|
|
34
|
+
showNotGitRepo();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
showBranchInfo(git.branch);
|
|
39
|
+
|
|
40
|
+
if (git.isDirty) {
|
|
41
|
+
showDirtyWarning();
|
|
42
|
+
const proceed = await confirm(" continue with uncommitted changes? (y/n) ");
|
|
43
|
+
if (!proceed) {
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── 2. Collect context ────────────────────────────────────────────
|
|
49
|
+
const root = process.cwd();
|
|
50
|
+
const context = collectRepoContext(root);
|
|
51
|
+
|
|
52
|
+
// ── 3. Show scan results + prompt ─────────────────────────────────
|
|
53
|
+
showScanResults(context);
|
|
54
|
+
|
|
55
|
+
let choice = await promptChoice(" proceed? [y] add more files? [a] cancel? [n] ");
|
|
56
|
+
|
|
57
|
+
while (choice === "a") {
|
|
58
|
+
const filePath = await promptFilePath(" file path: ");
|
|
59
|
+
if (filePath) {
|
|
60
|
+
const added = addFileToContext(root, filePath, context);
|
|
61
|
+
if (!added) {
|
|
62
|
+
console.log(` could not read ${filePath}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
showScanResults(context);
|
|
66
|
+
choice = await promptChoice(" proceed? [y] add more files? [a] cancel? [n] ");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (choice === "n") {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── 4. Call API ───────────────────────────────────────────────────
|
|
74
|
+
const spinner = ora({
|
|
75
|
+
text: "relay is reading your repository...",
|
|
76
|
+
color: "yellow",
|
|
77
|
+
}).start();
|
|
78
|
+
|
|
79
|
+
console.log("calling:", API_URL);
|
|
80
|
+
|
|
81
|
+
let response: Response;
|
|
82
|
+
try {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
85
|
+
|
|
86
|
+
response = await fetch(API_URL, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify(context),
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
spinner.stop();
|
|
96
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
97
|
+
showError("network", "request timed out after 120s");
|
|
98
|
+
} else {
|
|
99
|
+
showError("network");
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
spinner.stop();
|
|
106
|
+
const body = (await response.json().catch(() => ({}))) as Record<string, unknown>;
|
|
107
|
+
const msg = (body.message ?? body.error) as string | undefined;
|
|
108
|
+
|
|
109
|
+
if (response.status === 429) {
|
|
110
|
+
showError("rate-limit");
|
|
111
|
+
} else if (response.status === 400) {
|
|
112
|
+
showError("validation", msg);
|
|
113
|
+
} else {
|
|
114
|
+
showError("server", msg);
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let data: InitResponse;
|
|
120
|
+
try {
|
|
121
|
+
data = (await response.json()) as InitResponse;
|
|
122
|
+
} catch {
|
|
123
|
+
spinner.stop();
|
|
124
|
+
showError("server", "invalid response from relay");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
spinner.stop();
|
|
129
|
+
console.log();
|
|
130
|
+
|
|
131
|
+
// ── 5. Show findings + file list ──────────────────────────────────
|
|
132
|
+
showSummary(data.summary);
|
|
133
|
+
showFindings(data.findings);
|
|
134
|
+
showFileList(data.files);
|
|
135
|
+
|
|
136
|
+
// ── 6. Confirm write ──────────────────────────────────────────────
|
|
137
|
+
const writeConfirm = await confirm(" write these files? (y/n) ");
|
|
138
|
+
if (!writeConfirm) {
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── 7. Re-check git before writing ────────────────────────────────
|
|
143
|
+
const gitNow = getGitState();
|
|
144
|
+
if (gitNow.isDirty && !git.isDirty) {
|
|
145
|
+
const proceed = await confirm(" working tree changed since scan — continue? (y/n) ");
|
|
146
|
+
if (!proceed) {
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── 8. Write + commit ─────────────────────────────────────────────
|
|
152
|
+
console.log();
|
|
153
|
+
writeFiles(root, data.files);
|
|
154
|
+
console.log();
|
|
155
|
+
|
|
156
|
+
const committed = commitFiles(root, data.files);
|
|
157
|
+
if (!committed) {
|
|
158
|
+
console.log(" ⚠ files written but git commit failed\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── 9. Success ────────────────────────────────────────────────────
|
|
162
|
+
showSuccess();
|
|
163
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { RepoContext } from "../types.js";
|
|
4
|
+
import { getGitState } from "./git.js";
|
|
5
|
+
|
|
6
|
+
const IGNORE_DIRS = new Set([
|
|
7
|
+
"node_modules", ".git", "dist", ".next", ".vercel", ".turbo",
|
|
8
|
+
"__pycache__", ".cache", "coverage", ".nyc_output", "build",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const CONFIG_PATTERNS = [
|
|
12
|
+
"next.config.ts", "next.config.js", "next.config.mjs",
|
|
13
|
+
"tailwind.config.ts", "tailwind.config.js",
|
|
14
|
+
"supabase/config.toml",
|
|
15
|
+
"vercel.json",
|
|
16
|
+
"prisma/schema.prisma",
|
|
17
|
+
"drizzle.config.ts",
|
|
18
|
+
".eslintrc.json", ".eslintrc.js", "eslint.config.js", "eslint.config.mjs",
|
|
19
|
+
".prettierrc", ".prettierrc.json",
|
|
20
|
+
"jest.config.ts", "jest.config.js",
|
|
21
|
+
"vitest.config.ts", "vitest.config.js",
|
|
22
|
+
"playwright.config.ts",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function readJsonSafe(filePath: string): Record<string, unknown> | null {
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readFileSafe(filePath: string): string | null {
|
|
35
|
+
try {
|
|
36
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getDirectoryTree(root: string, maxDepth: number = 2): string[] {
|
|
43
|
+
const entries: string[] = [];
|
|
44
|
+
|
|
45
|
+
function walk(dir: string, depth: number, prefix: string) {
|
|
46
|
+
if (depth > maxDepth) return;
|
|
47
|
+
|
|
48
|
+
let items: fs.Dirent[];
|
|
49
|
+
try {
|
|
50
|
+
items = fs.readdirSync(dir, { withFileTypes: true });
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
if (IGNORE_DIRS.has(item.name)) continue;
|
|
57
|
+
if (item.name.startsWith(".") && depth === 0 && item.isDirectory()) continue;
|
|
58
|
+
|
|
59
|
+
const relative = prefix ? `${prefix}/${item.name}` : item.name;
|
|
60
|
+
|
|
61
|
+
if (item.isDirectory()) {
|
|
62
|
+
entries.push(`${relative}/`);
|
|
63
|
+
walk(path.join(dir, item.name), depth + 1, relative);
|
|
64
|
+
} else if (depth <= 1) {
|
|
65
|
+
entries.push(relative);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
walk(root, 0, "");
|
|
71
|
+
return entries;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getRootConfigs(root: string): string[] {
|
|
75
|
+
const configPatterns = [
|
|
76
|
+
/^\..*rc$/,
|
|
77
|
+
/^\..*rc\.json$/,
|
|
78
|
+
/^\..*rc\.js$/,
|
|
79
|
+
/^\..*rc\.yml$/,
|
|
80
|
+
/^\..*rc\.yaml$/,
|
|
81
|
+
/\.config\.(ts|js|mjs|cjs)$/,
|
|
82
|
+
/^tsconfig.*\.json$/,
|
|
83
|
+
/^docker-compose.*\.ya?ml$/,
|
|
84
|
+
/^Dockerfile/,
|
|
85
|
+
/^Makefile$/,
|
|
86
|
+
/^\.env\.example$/,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const items = fs.readdirSync(root);
|
|
91
|
+
return items.filter((item) => {
|
|
92
|
+
return configPatterns.some((p) => p.test(item));
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function collectRepoContext(root: string): RepoContext {
|
|
100
|
+
// ── package.json ──
|
|
101
|
+
const pkgJson = readJsonSafe(path.join(root, "package.json"));
|
|
102
|
+
const packageJson = pkgJson
|
|
103
|
+
? {
|
|
104
|
+
name: pkgJson.name as string | undefined,
|
|
105
|
+
description: pkgJson.description as string | undefined,
|
|
106
|
+
dependencies: (pkgJson.dependencies ?? {}) as Record<string, string>,
|
|
107
|
+
devDependencies: (pkgJson.devDependencies ?? {}) as Record<string, string>,
|
|
108
|
+
scripts: (pkgJson.scripts ?? {}) as Record<string, string>,
|
|
109
|
+
workspaces: pkgJson.workspaces as string[] | { packages: string[] } | undefined,
|
|
110
|
+
}
|
|
111
|
+
: { dependencies: {}, devDependencies: {}, scripts: {} };
|
|
112
|
+
|
|
113
|
+
// ── tsconfig.json ──
|
|
114
|
+
const tsconfig = readJsonSafe(path.join(root, "tsconfig.json"));
|
|
115
|
+
|
|
116
|
+
// ── Directory tree ──
|
|
117
|
+
const directoryTree = getDirectoryTree(root);
|
|
118
|
+
|
|
119
|
+
// ── .env.example — key names only ──
|
|
120
|
+
const envKeys: string[] = [];
|
|
121
|
+
const envContent = readFileSafe(path.join(root, ".env.example"));
|
|
122
|
+
if (envContent) {
|
|
123
|
+
for (const line of envContent.split("\n")) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
126
|
+
const key = trimmed.split("=")[0].trim();
|
|
127
|
+
if (key) envKeys.push(key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Config files ──
|
|
133
|
+
const configFiles: Record<string, string> = {};
|
|
134
|
+
for (const pattern of CONFIG_PATTERNS) {
|
|
135
|
+
const fullPath = path.join(root, pattern);
|
|
136
|
+
const content = readFileSafe(fullPath);
|
|
137
|
+
if (content) {
|
|
138
|
+
// Cap config file content at 5000 chars
|
|
139
|
+
configFiles[pattern] = content.slice(0, 5000);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Existing rules ──
|
|
144
|
+
const existingRules: Record<string, string> = {};
|
|
145
|
+
|
|
146
|
+
// .cursor/rules/
|
|
147
|
+
const cursorRulesDir = path.join(root, ".cursor", "rules");
|
|
148
|
+
try {
|
|
149
|
+
const ruleFiles = fs.readdirSync(cursorRulesDir);
|
|
150
|
+
for (const file of ruleFiles) {
|
|
151
|
+
const content = readFileSafe(path.join(cursorRulesDir, file));
|
|
152
|
+
if (content) {
|
|
153
|
+
existingRules[`.cursor/rules/${file}`] = content;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// no existing rules
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// CLAUDE.md
|
|
161
|
+
const claudeMd = readFileSafe(path.join(root, "CLAUDE.md"));
|
|
162
|
+
if (claudeMd) {
|
|
163
|
+
existingRules["CLAUDE.md"] = claudeMd;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Migrations ──
|
|
167
|
+
let migrationCount = 0;
|
|
168
|
+
let migrationNames: string[] = [];
|
|
169
|
+
const migrationsDir = path.join(root, "supabase", "migrations");
|
|
170
|
+
try {
|
|
171
|
+
const migFiles = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
|
|
172
|
+
migrationCount = migFiles.length;
|
|
173
|
+
migrationNames = migFiles;
|
|
174
|
+
} catch {
|
|
175
|
+
// no migrations
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Root configs ──
|
|
179
|
+
const rootConfigs = getRootConfigs(root);
|
|
180
|
+
|
|
181
|
+
// ── Git state ──
|
|
182
|
+
const git = getGitState();
|
|
183
|
+
const gitState = git.isGitRepo ? { branch: git.branch, isDirty: git.isDirty } : null;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
packageJson,
|
|
187
|
+
tsconfig,
|
|
188
|
+
directoryTree,
|
|
189
|
+
envKeys,
|
|
190
|
+
configFiles,
|
|
191
|
+
existingRules,
|
|
192
|
+
migrationCount,
|
|
193
|
+
migrationNames,
|
|
194
|
+
rootConfigs,
|
|
195
|
+
gitState,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function addFileToContext(root: string, filePath: string, context: RepoContext): boolean {
|
|
200
|
+
const fullPath = path.resolve(root, filePath);
|
|
201
|
+
const content = readFileSafe(fullPath);
|
|
202
|
+
if (!content) return false;
|
|
203
|
+
context.configFiles[filePath] = content.slice(0, 5000);
|
|
204
|
+
return true;
|
|
205
|
+
}
|