runshift 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +13 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +184 -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 +37 -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 +16 -0
- package/dist/ui/display.d.ts.map +1 -0
- package/dist/ui/display.js +166 -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 +231 -0
- package/src/context/collector.ts +205 -0
- package/src/context/git.ts +36 -0
- package/src/index.ts +39 -0
- package/src/types.ts +44 -0
- package/src/ui/display.ts +184 -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,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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface GitState {
|
|
4
|
+
isGitRepo: boolean;
|
|
5
|
+
branch: string;
|
|
6
|
+
isDirty: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getGitState(): GitState {
|
|
10
|
+
try {
|
|
11
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
12
|
+
} catch {
|
|
13
|
+
return { isGitRepo: false, branch: "", isDirty: false };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let branch = "";
|
|
17
|
+
try {
|
|
18
|
+
branch = execSync("git branch --show-current", { stdio: "pipe" })
|
|
19
|
+
.toString()
|
|
20
|
+
.trim();
|
|
21
|
+
} catch {
|
|
22
|
+
branch = "unknown";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let isDirty = false;
|
|
26
|
+
try {
|
|
27
|
+
const status = execSync("git status --porcelain", { stdio: "pipe" })
|
|
28
|
+
.toString()
|
|
29
|
+
.trim();
|
|
30
|
+
isDirty = status.length > 0;
|
|
31
|
+
} catch {
|
|
32
|
+
isDirty = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { isGitRepo: true, branch, isDirty };
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { init } from "./commands/init.js";
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const command = args[0] ?? "init";
|
|
7
|
+
|
|
8
|
+
switch (command) {
|
|
9
|
+
case "init":
|
|
10
|
+
init(args.slice(1)).catch((err) => {
|
|
11
|
+
console.error(err.message ?? err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
14
|
+
break;
|
|
15
|
+
case "--version":
|
|
16
|
+
case "-v":
|
|
17
|
+
console.log("runshift 0.0.3");
|
|
18
|
+
break;
|
|
19
|
+
case "--help":
|
|
20
|
+
case "-h":
|
|
21
|
+
console.log(`
|
|
22
|
+
runshift — the control plane for agents, wherever they run.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
npx runshift init [options] Read your repo, generate governance rules
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--version, -v Show version
|
|
29
|
+
--help, -h Show this help
|
|
30
|
+
|
|
31
|
+
Init options:
|
|
32
|
+
--dry-run Preview changes without writing files
|
|
33
|
+
--branch <name> Run on a new branch (default: relay-init)
|
|
34
|
+
`);
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
console.error(`Unknown command: ${command}\nRun "runshift --help" for usage.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ── Shared types mirroring the API contract ──
|
|
2
|
+
|
|
3
|
+
export interface RepoContext {
|
|
4
|
+
packageJson: {
|
|
5
|
+
name?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
dependencies?: Record<string, string>;
|
|
8
|
+
devDependencies?: Record<string, string>;
|
|
9
|
+
scripts?: Record<string, string>;
|
|
10
|
+
workspaces?: string[] | { packages: string[] };
|
|
11
|
+
};
|
|
12
|
+
tsconfig: Record<string, unknown> | null;
|
|
13
|
+
directoryTree: string[];
|
|
14
|
+
envKeys: string[];
|
|
15
|
+
configFiles: Record<string, string>;
|
|
16
|
+
existingRules: Record<string, string>;
|
|
17
|
+
migrationCount: number;
|
|
18
|
+
migrationNames: string[];
|
|
19
|
+
rootConfigs: string[];
|
|
20
|
+
gitState: {
|
|
21
|
+
branch: string;
|
|
22
|
+
isDirty: boolean;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GeneratedFile {
|
|
27
|
+
path: string;
|
|
28
|
+
content: string;
|
|
29
|
+
action: "create" | "update";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Findings {
|
|
33
|
+
blastRadius: string[];
|
|
34
|
+
securityGaps: string[];
|
|
35
|
+
agentFailurePatterns: string[];
|
|
36
|
+
parallelizationBoundaries: string[];
|
|
37
|
+
deprecatedPatterns: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface InitResponse {
|
|
41
|
+
files: GeneratedFile[];
|
|
42
|
+
findings: Findings;
|
|
43
|
+
summary: string;
|
|
44
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import figlet from "figlet";
|
|
3
|
+
import type { Findings, GeneratedFile, RepoContext } from "../types.js";
|
|
4
|
+
|
|
5
|
+
const amber = chalk.hex("#f5a623");
|
|
6
|
+
const muted = chalk.hex("#6b6b7b");
|
|
7
|
+
const dim = chalk.dim;
|
|
8
|
+
const divider = muted(" " + "─".repeat(45));
|
|
9
|
+
|
|
10
|
+
export function showBanner(): void {
|
|
11
|
+
const banner = figlet.textSync("runshift", {
|
|
12
|
+
font: "Standard",
|
|
13
|
+
horizontalLayout: "default",
|
|
14
|
+
});
|
|
15
|
+
console.log(amber(banner));
|
|
16
|
+
console.log(muted(" v0.0.3"));
|
|
17
|
+
console.log(muted(" the control plane for agents, wherever they run."));
|
|
18
|
+
console.log(dim(" usage: npx runshift init [--dry-run] [--branch <name>]\n"));
|
|
19
|
+
console.log(divider + "\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function showNotGitRepo(): void {
|
|
23
|
+
console.log(amber(" this directory is not a git repository."));
|
|
24
|
+
console.log(dim(" run runshift init from a project root with git initialized.\n"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function showDirtyWarning(): void {
|
|
28
|
+
console.log(muted(" ⚠ uncommitted changes detected\n"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function showBranchInfo(branch: string): void {
|
|
32
|
+
console.log(dim(` on branch ${amber(branch)}\n`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function showScanResults(context: RepoContext): void {
|
|
36
|
+
console.log(amber(" relay scanned your repository:\n"));
|
|
37
|
+
|
|
38
|
+
const deps: Record<string, string> = {
|
|
39
|
+
...context.packageJson.dependencies,
|
|
40
|
+
...context.packageJson.devDependencies,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const detections: string[] = [];
|
|
44
|
+
|
|
45
|
+
if (context.packageJson.name) {
|
|
46
|
+
const stack: string[] = [];
|
|
47
|
+
if (deps["next"]) stack.push("Next.js");
|
|
48
|
+
if (deps["@supabase/supabase-js"] || deps["@supabase/ssr"]) stack.push("Supabase");
|
|
49
|
+
if (deps["tailwindcss"]) stack.push("Tailwind");
|
|
50
|
+
if (deps["prisma"] || deps["@prisma/client"]) stack.push("Prisma");
|
|
51
|
+
if (deps["drizzle-orm"]) stack.push("Drizzle");
|
|
52
|
+
if (deps["stripe"]) stack.push("Stripe");
|
|
53
|
+
const label = stack.length > 0 ? stack.join(", ") + " detected" : "detected";
|
|
54
|
+
detections.push(`package.json — ${label}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (context.envKeys.length > 0) {
|
|
58
|
+
detections.push(`.env.example — ${context.envKeys.length} environment variable${context.envKeys.length === 1 ? "" : "s"} found`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (context.migrationCount > 0) {
|
|
62
|
+
detections.push(`supabase/migrations/ — ${context.migrationCount} migration file${context.migrationCount === 1 ? "" : "s"} found`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const existingRuleKeys = Object.keys(context.existingRules);
|
|
66
|
+
const cursorRules = existingRuleKeys.filter((k) => k.startsWith(".cursor/rules/"));
|
|
67
|
+
if (cursorRules.length > 0) {
|
|
68
|
+
detections.push(`.cursor/rules/ — ${cursorRules.length} existing file${cursorRules.length === 1 ? "" : "s"} detected`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (existingRuleKeys.includes("CLAUDE.md")) {
|
|
72
|
+
detections.push("existing CLAUDE.md detected");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (context.tsconfig) {
|
|
76
|
+
detections.push("tsconfig.json detected");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const configCount = Object.keys(context.configFiles).length;
|
|
80
|
+
if (configCount > 0) {
|
|
81
|
+
detections.push(`${configCount} config file${configCount === 1 ? "" : "s"} found`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const d of detections) {
|
|
85
|
+
console.log(dim(" ✓ ") + d);
|
|
86
|
+
}
|
|
87
|
+
console.log();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function showFindings(findings: Findings): void {
|
|
91
|
+
const sections: [string, string[]][] = [
|
|
92
|
+
["blast radius", findings.blastRadius],
|
|
93
|
+
["security gaps", findings.securityGaps],
|
|
94
|
+
["agent failure patterns", findings.agentFailurePatterns],
|
|
95
|
+
["parallelization boundaries", findings.parallelizationBoundaries],
|
|
96
|
+
["deprecated patterns", findings.deprecatedPatterns],
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const hasFindings = sections.some(([, items]) => items.length > 0);
|
|
100
|
+
if (!hasFindings) return;
|
|
101
|
+
|
|
102
|
+
console.log(amber(" relay found issues in your codebase:\n"));
|
|
103
|
+
|
|
104
|
+
for (const [title, items] of sections) {
|
|
105
|
+
if (items.length === 0) continue;
|
|
106
|
+
console.log(amber(` ${title}`));
|
|
107
|
+
for (const item of items) {
|
|
108
|
+
console.log(dim(` → ${item}`));
|
|
109
|
+
}
|
|
110
|
+
console.log();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function showFileList(files: GeneratedFile[]): void {
|
|
115
|
+
console.log(amber(` relay will write ${files.length} file${files.length === 1 ? "" : "s"}:\n`));
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const prefix = file.action === "create" ? amber("+") : amber("~");
|
|
118
|
+
const action = file.action === "create" ? dim("(create)") : dim("(update — existing file)");
|
|
119
|
+
console.log(` ${prefix} ${file.path} ${action}`);
|
|
120
|
+
}
|
|
121
|
+
console.log(dim("\n + = create, ~ = update existing file\n"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function showWriting(filePath: string): void {
|
|
125
|
+
console.log(dim(" ✓ ") + filePath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function showCommit(): void {
|
|
129
|
+
console.log(dim(" ✓ ") + "committed");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function showSummary(summary: string): void {
|
|
133
|
+
console.log(muted(` ${summary.replace(/\n/g, "\n ")}\n`));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function showSuccess(): void {
|
|
137
|
+
console.log("\n" + divider + "\n");
|
|
138
|
+
console.log(amber(" ✓ relay is installed in your development workflow\n"));
|
|
139
|
+
console.log(muted(" next steps:"));
|
|
140
|
+
console.log(dim(" → open Claude Code and type /validate to run your first check"));
|
|
141
|
+
console.log(dim(" → type /runshift-update to refresh rules as your stack evolves\n"));
|
|
142
|
+
console.log(muted(" connect to the runshift control plane: ") + amber("runshift.ai"));
|
|
143
|
+
console.log("\n" + divider + "\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function showDataPolicy(): void {
|
|
147
|
+
console.log(amber(" relay will send to runshift.ai:\n"));
|
|
148
|
+
console.log(dim(" ✓ package.json (dependencies and scripts only)"));
|
|
149
|
+
console.log(dim(" ✓ directory structure (top 2 levels, folder names only)"));
|
|
150
|
+
console.log(dim(" ✓ .env.example (key names only — values are never read)"));
|
|
151
|
+
console.log(dim(" ✓ existing CLAUDE.md (if present)"));
|
|
152
|
+
console.log(dim(" ✓ existing .cursor/rules/ (if present)"));
|
|
153
|
+
console.log(dim(" ✓ migration file names (no file contents)"));
|
|
154
|
+
console.log(dim("\n no source code is sent."));
|
|
155
|
+
console.log(dim(" no secret values are ever read.\n"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function showDryRunComplete(): void {
|
|
159
|
+
console.log("\n" + divider + "\n");
|
|
160
|
+
console.log(amber(" dry run complete — no files written."));
|
|
161
|
+
console.log(dim(" run npx runshift init to install.\n"));
|
|
162
|
+
console.log(divider + "\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function showError(type: "network" | "rate-limit" | "validation" | "server", message?: string): void {
|
|
166
|
+
console.log();
|
|
167
|
+
switch (type) {
|
|
168
|
+
case "network":
|
|
169
|
+
console.log(amber(" could not reach relay."));
|
|
170
|
+
console.log(dim(" check your connection and try again.\n"));
|
|
171
|
+
break;
|
|
172
|
+
case "rate-limit":
|
|
173
|
+
console.log(amber(" relay rate limit reached — try again in 1 hour.\n"));
|
|
174
|
+
break;
|
|
175
|
+
case "validation":
|
|
176
|
+
console.log(amber(" relay could not read this repository."));
|
|
177
|
+
if (message) console.log(dim(` ${message}\n`));
|
|
178
|
+
break;
|
|
179
|
+
case "server":
|
|
180
|
+
console.log(amber(" relay encountered an error — try again or visit runshift.ai."));
|
|
181
|
+
if (message) console.log(dim(` ${message}\n`));
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/ui/prompt.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
|
|
3
|
+
function ask(question: string): Promise<string> {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
rl.question(question, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer.trim());
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function confirm(question: string): Promise<boolean> {
|
|
18
|
+
const answer = await ask(question);
|
|
19
|
+
const normalized = answer.toLowerCase();
|
|
20
|
+
return normalized === "y" || normalized === "yes";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function promptChoice(question: string): Promise<"y" | "a" | "n"> {
|
|
24
|
+
const answer = await ask(question);
|
|
25
|
+
const normalized = answer.toLowerCase();
|
|
26
|
+
if (normalized === "a" || normalized === "add") return "a";
|
|
27
|
+
if (normalized === "y" || normalized === "yes") return "y";
|
|
28
|
+
return "n";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function promptFilePath(question: string): Promise<string> {
|
|
32
|
+
return ask(question);
|
|
33
|
+
}
|
package/src/writer.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import type { GeneratedFile } from "./types.js";
|
|
5
|
+
import { showWriting, showCommit } from "./ui/display.js";
|
|
6
|
+
|
|
7
|
+
export function writeFiles(root: string, files: GeneratedFile[]): void {
|
|
8
|
+
for (const file of files) {
|
|
9
|
+
const fullPath = path.join(root, file.path);
|
|
10
|
+
const dir = path.dirname(fullPath);
|
|
11
|
+
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
fs.writeFileSync(fullPath, file.content, "utf-8");
|
|
14
|
+
showWriting(file.path);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function commitFiles(root: string, files: GeneratedFile[]): boolean {
|
|
19
|
+
try {
|
|
20
|
+
const filePaths = files.map((f) => `"${f.path}"`).join(" ");
|
|
21
|
+
|
|
22
|
+
execSync(`git add ${filePaths}`, {
|
|
23
|
+
cwd: root,
|
|
24
|
+
stdio: "pipe",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
execSync(
|
|
28
|
+
'git commit -m "chore: install runshift agent governance rules"',
|
|
29
|
+
{ cwd: root, stdio: "pipe" },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
showCommit();
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
package/index.js
DELETED