@prepcli/prepcli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +378 -0
- package/bin/prepcli.js +104 -0
- package/package.json +41 -0
- package/src/commands/auth.js +125 -0
- package/src/commands/context.js +151 -0
- package/src/commands/doctor.js +10 -0
- package/src/commands/hook.js +131 -0
- package/src/commands/init.js +175 -0
- package/src/commands/install.js +130 -0
- package/src/commands/log.js +65 -0
- package/src/commands/record.js +141 -0
- package/src/commands/session.js +44 -0
- package/src/commands/stats.js +10 -0
- package/src/commands/team.js +21 -0
- package/src/commands/uninstall.js +105 -0
- package/src/lib/api.js +38 -0
- package/src/lib/config.js +145 -0
- package/src/lib/decision.js +61 -0
- package/src/lib/detect.js +194 -0
- package/src/lib/git.js +200 -0
- package/src/lib/session-file.js +48 -0
- package/src/lib/targets/antigravity.js +14 -0
- package/src/lib/targets/claude.js +22 -0
- package/src/lib/targets/cursor.js +14 -0
- package/src/lib/targets/windsurf.js +14 -0
- package/workflows/debug.md +156 -0
- package/workflows/plan.md +156 -0
- package/workflows/prep.md +150 -0
- package/workflows/prepcli.md +154 -0
- package/workflows/refactor.md +154 -0
- package/workflows/review.md +156 -0
- package/workflows/write.md +154 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { execSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
function tryRead(filePath) {
|
|
8
|
+
try { return fs.readFileSync(filePath, "utf8"); }
|
|
9
|
+
catch { return null; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tryJSON(filePath) {
|
|
13
|
+
const raw = tryRead(filePath);
|
|
14
|
+
if (!raw) return null;
|
|
15
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function exists(filePath) {
|
|
19
|
+
try { fs.accessSync(filePath); return true; }
|
|
20
|
+
catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function gitRemote(cwd) {
|
|
24
|
+
try {
|
|
25
|
+
return execSync("git remote get-url origin", { cwd, stdio: ["pipe", "pipe", "pipe"] })
|
|
26
|
+
.toString().trim();
|
|
27
|
+
} catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FRAMEWORK_DEPS = {
|
|
31
|
+
"next": "Next.js",
|
|
32
|
+
"@remix-run/node": "Remix",
|
|
33
|
+
"astro": "Astro",
|
|
34
|
+
"gatsby": "Gatsby",
|
|
35
|
+
"nuxt": "Nuxt",
|
|
36
|
+
"@sveltejs/kit": "SvelteKit",
|
|
37
|
+
"svelte": "Svelte",
|
|
38
|
+
"vue": "Vue",
|
|
39
|
+
"react": "React",
|
|
40
|
+
"@nestjs/core": "NestJS",
|
|
41
|
+
"fastify": "Fastify",
|
|
42
|
+
"express": "Express",
|
|
43
|
+
"koa": "Koa",
|
|
44
|
+
"hono": "Hono",
|
|
45
|
+
"electron": "Electron",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const DB_DEPS = {
|
|
49
|
+
"@supabase/supabase-js": "Supabase",
|
|
50
|
+
"@prisma/client": "Prisma",
|
|
51
|
+
"drizzle-orm": "Drizzle",
|
|
52
|
+
"typeorm": "TypeORM",
|
|
53
|
+
"mongoose": "MongoDB",
|
|
54
|
+
"pg": "PostgreSQL",
|
|
55
|
+
"mysql2": "MySQL",
|
|
56
|
+
"sqlite3": "SQLite",
|
|
57
|
+
"redis": "Redis",
|
|
58
|
+
"@upstash/redis": "Upstash Redis",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const AUTH_DEPS = {
|
|
62
|
+
"better-auth": "better-auth",
|
|
63
|
+
"next-auth": "NextAuth",
|
|
64
|
+
"@auth/core": "Auth.js",
|
|
65
|
+
"passport": "Passport",
|
|
66
|
+
"lucia": "Lucia",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const TEST_DEPS = {
|
|
70
|
+
"vitest": "Vitest",
|
|
71
|
+
"jest": "Jest",
|
|
72
|
+
"playwright": "Playwright",
|
|
73
|
+
"cypress": "Cypress",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const API_DEPS = {
|
|
77
|
+
"@trpc/server": "tRPC",
|
|
78
|
+
"@tanstack/react-query": "TanStack Query",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const STYLE_DEPS = {
|
|
82
|
+
"tailwindcss": "Tailwind CSS",
|
|
83
|
+
"@emotion/react": "Emotion",
|
|
84
|
+
"styled-components": "styled-components",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function detectStack(cwd = process.cwd()) {
|
|
88
|
+
const result = { stack: {}, name: null, git_remote: null };
|
|
89
|
+
|
|
90
|
+
result.git_remote = gitRemote(cwd);
|
|
91
|
+
|
|
92
|
+
const pkg = tryJSON(path.join(cwd, "package.json"));
|
|
93
|
+
|
|
94
|
+
if (pkg) {
|
|
95
|
+
result.name = pkg.name;
|
|
96
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
97
|
+
|
|
98
|
+
// Language
|
|
99
|
+
if (all["typescript"] || exists(path.join(cwd, "tsconfig.json"))) {
|
|
100
|
+
result.stack.language = "TypeScript";
|
|
101
|
+
} else {
|
|
102
|
+
result.stack.language = "JavaScript";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Runtime
|
|
106
|
+
const nvmrc = tryRead(path.join(cwd, ".nvmrc"))?.trim();
|
|
107
|
+
const nvFile = tryRead(path.join(cwd, ".node-version"))?.trim();
|
|
108
|
+
const engines = pkg.engines?.node;
|
|
109
|
+
result.stack.runtime = `Node ${nvmrc || nvFile || engines || ">=18"}`;
|
|
110
|
+
|
|
111
|
+
// Framework — first match wins (ordered most specific → least)
|
|
112
|
+
for (const [dep, label] of Object.entries(FRAMEWORK_DEPS)) {
|
|
113
|
+
if (all[dep]) { result.stack.framework = label; break; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Database
|
|
117
|
+
for (const [dep, label] of Object.entries(DB_DEPS)) {
|
|
118
|
+
if (all[dep]) { result.stack.db = label; break; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Auth
|
|
122
|
+
for (const [dep, label] of Object.entries(AUTH_DEPS)) {
|
|
123
|
+
if (all[dep]) { result.stack.auth = label; break; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Testing
|
|
127
|
+
for (const [dep, label] of Object.entries(TEST_DEPS)) {
|
|
128
|
+
if (all[dep]) { result.stack.testing = label; break; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// API layer
|
|
132
|
+
for (const [dep, label] of Object.entries(API_DEPS)) {
|
|
133
|
+
if (all[dep]) { result.stack.api = label; break; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Styling
|
|
137
|
+
for (const [dep, label] of Object.entries(STYLE_DEPS)) {
|
|
138
|
+
if (all[dep]) { result.stack.styling = label; break; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Monorepo
|
|
142
|
+
if (all["turbo"] || exists(path.join(cwd, "turbo.json"))) {
|
|
143
|
+
result.stack.monorepo = "Turborepo";
|
|
144
|
+
} else if (exists(path.join(cwd, "pnpm-workspace.yaml"))) {
|
|
145
|
+
result.stack.monorepo = "pnpm workspaces";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// i18n
|
|
149
|
+
if (all["i18next"] || all["next-i18next"] || all["@formatjs/intl"]) {
|
|
150
|
+
result.stack.i18n = all["i18next"] ? "i18next" : all["next-i18next"] ? "next-i18next" : "FormatJS";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Package manager
|
|
154
|
+
if (exists(path.join(cwd, "pnpm-lock.yaml"))) result.stack.package_manager = "pnpm";
|
|
155
|
+
else if (exists(path.join(cwd, "yarn.lock"))) result.stack.package_manager = "yarn";
|
|
156
|
+
else if (exists(path.join(cwd, "package-lock.json"))) result.stack.package_manager = "npm";
|
|
157
|
+
|
|
158
|
+
} else if (exists(path.join(cwd, "Cargo.toml"))) {
|
|
159
|
+
result.stack.language = "Rust";
|
|
160
|
+
const cargo = tryRead(path.join(cwd, "Cargo.toml"));
|
|
161
|
+
const nameMatch = cargo?.match(/^name\s*=\s*"([^"]+)"/m);
|
|
162
|
+
if (nameMatch) result.name = nameMatch[1];
|
|
163
|
+
|
|
164
|
+
} else if (exists(path.join(cwd, "go.mod"))) {
|
|
165
|
+
result.stack.language = "Go";
|
|
166
|
+
|
|
167
|
+
} else if (exists(path.join(cwd, "requirements.txt")) || exists(path.join(cwd, "pyproject.toml"))) {
|
|
168
|
+
result.stack.language = "Python";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// CI
|
|
172
|
+
if (exists(path.join(cwd, ".github/workflows"))) result.stack.ci = "GitHub Actions";
|
|
173
|
+
else if (exists(path.join(cwd, ".circleci"))) result.stack.ci = "CircleCI";
|
|
174
|
+
else if (exists(path.join(cwd, ".gitlab-ci.yml"))) result.stack.ci = "GitLab CI";
|
|
175
|
+
|
|
176
|
+
// Hosting
|
|
177
|
+
if (exists(path.join(cwd, "vercel.json")) || exists(path.join(cwd, ".vercel"))) {
|
|
178
|
+
result.stack.hosting = "Vercel";
|
|
179
|
+
} else if (exists(path.join(cwd, "netlify.toml"))) {
|
|
180
|
+
result.stack.hosting = "Netlify";
|
|
181
|
+
} else if (exists(path.join(cwd, "wrangler.toml"))) {
|
|
182
|
+
result.stack.hosting = "Cloudflare Workers";
|
|
183
|
+
} else if (exists(path.join(cwd, "fly.toml"))) {
|
|
184
|
+
result.stack.hosting = "Fly.io";
|
|
185
|
+
} else if (exists(path.join(cwd, "render.yaml"))) {
|
|
186
|
+
result.stack.hosting = "Render";
|
|
187
|
+
} else if (exists(path.join(cwd, "railway.json")) || exists(path.join(cwd, "railway.toml"))) {
|
|
188
|
+
result.stack.hosting = "Railway";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { detectStack };
|
package/src/lib/git.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { execSync, execFileSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const SHADOW_BRANCH = "prepcli/shadow/v1";
|
|
8
|
+
|
|
9
|
+
function exec(cmd, cwd) {
|
|
10
|
+
return execSync(cmd, { cwd, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function execSafe(cmd, cwd) {
|
|
14
|
+
try { return exec(cmd, cwd); } catch { return null; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function gitRoot(cwd = process.cwd()) {
|
|
18
|
+
return execSafe("git rev-parse --show-toplevel", cwd) || cwd;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function shadowBranchExists(cwd = process.cwd()) {
|
|
22
|
+
return execSafe(`git rev-parse --verify refs/heads/${SHADOW_BRANCH}`, gitRoot(cwd)) !== null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shadowBranchExistsOnRemote(cwd = process.cwd()) {
|
|
26
|
+
const out = execSafe(`git ls-remote --heads origin ${SHADOW_BRANCH}`, gitRoot(cwd));
|
|
27
|
+
return Boolean(out?.includes(SHADOW_BRANCH));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function initShadowBranch(cwd = process.cwd()) {
|
|
31
|
+
const root = gitRoot(cwd);
|
|
32
|
+
const env = {
|
|
33
|
+
...process.env,
|
|
34
|
+
GIT_AUTHOR_NAME: "prepcli",
|
|
35
|
+
GIT_AUTHOR_EMAIL: "bot@prepcli.in",
|
|
36
|
+
GIT_COMMITTER_NAME: "prepcli",
|
|
37
|
+
GIT_COMMITTER_EMAIL:"bot@prepcli.in",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Create empty tree (git constant, no temp files needed)
|
|
41
|
+
const emptyTree = execFileSync("git", ["mktree"], {
|
|
42
|
+
cwd: root, input: "", env, stdio: ["pipe", "pipe", "pipe"],
|
|
43
|
+
}).toString().trim();
|
|
44
|
+
|
|
45
|
+
// Create initial commit on that empty tree (no parent)
|
|
46
|
+
const commitHash = execFileSync("git", ["commit-tree", emptyTree, "-F", "-"], {
|
|
47
|
+
cwd: root, input: "chore: init prepcli shadow branch", env, stdio: ["pipe", "pipe", "pipe"],
|
|
48
|
+
}).toString().trim();
|
|
49
|
+
|
|
50
|
+
// Point the branch ref at that commit (no checkout — working tree untouched)
|
|
51
|
+
execFileSync("git", ["update-ref", `refs/heads/${SHADOW_BRANCH}`, commitHash], {
|
|
52
|
+
cwd: root, stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fetchShadowBranch(cwd = process.cwd()) {
|
|
57
|
+
try {
|
|
58
|
+
exec(`git fetch origin ${SHADOW_BRANCH}:${SHADOW_BRANCH}`, gitRoot(cwd));
|
|
59
|
+
return true;
|
|
60
|
+
} catch { return false; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeBlobObject(content, cwd = process.cwd()) {
|
|
64
|
+
return execFileSync("git", ["hash-object", "-w", "--stdin"], {
|
|
65
|
+
cwd: gitRoot(cwd),
|
|
66
|
+
input: content,
|
|
67
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
68
|
+
}).toString().trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getShadowTree(cwd = process.cwd()) {
|
|
72
|
+
const out = execSafe(`git ls-tree ${SHADOW_BRANCH}`, gitRoot(cwd));
|
|
73
|
+
if (!out) return [];
|
|
74
|
+
return out.split("\n").filter(Boolean).map(line => {
|
|
75
|
+
const m = line.match(/^(\d+)\s+\w+\s+([0-9a-f]+)\t(.+)$/);
|
|
76
|
+
if (!m) return null;
|
|
77
|
+
return { mode: m[1], hash: m[2], filename: m[3] };
|
|
78
|
+
}).filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createTreeObject(entries, cwd = process.cwd()) {
|
|
82
|
+
const input = entries.map(e => `${e.mode} blob ${e.hash}\t${e.filename}`).join("\n");
|
|
83
|
+
return execFileSync("git", ["mktree"], {
|
|
84
|
+
cwd: gitRoot(cwd),
|
|
85
|
+
input,
|
|
86
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
87
|
+
}).toString().trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createCommitObject(treeHash, parentHash, message, cwd = process.cwd()) {
|
|
91
|
+
const root = gitRoot(cwd);
|
|
92
|
+
const args = ["commit-tree", treeHash];
|
|
93
|
+
if (parentHash) args.push("-p", parentHash);
|
|
94
|
+
args.push("-F", "-");
|
|
95
|
+
|
|
96
|
+
const env = {
|
|
97
|
+
...process.env,
|
|
98
|
+
GIT_AUTHOR_NAME: "prepcli",
|
|
99
|
+
GIT_AUTHOR_EMAIL: "bot@prepcli.in",
|
|
100
|
+
GIT_COMMITTER_NAME: "prepcli",
|
|
101
|
+
GIT_COMMITTER_EMAIL: "bot@prepcli.in",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return execFileSync("git", args, {
|
|
105
|
+
cwd: root, input: message, env, stdio: ["pipe", "pipe", "pipe"],
|
|
106
|
+
}).toString().trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function updateShadowBranch(commitHash, cwd = process.cwd()) {
|
|
110
|
+
execFileSync("git", ["update-ref", `refs/heads/${SHADOW_BRANCH}`, commitHash], {
|
|
111
|
+
cwd: gitRoot(cwd), stdio: ["pipe", "pipe", "pipe"],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeDecisionRecord(filename, content, cwd = process.cwd()) {
|
|
116
|
+
const root = gitRoot(cwd);
|
|
117
|
+
|
|
118
|
+
const blobHash = writeBlobObject(content, root);
|
|
119
|
+
const existing = getShadowTree(root);
|
|
120
|
+
const newEntries = existing.filter(e => e.filename !== filename);
|
|
121
|
+
newEntries.push({ mode: "100644", hash: blobHash, filename });
|
|
122
|
+
|
|
123
|
+
const treeHash = createTreeObject(newEntries, root);
|
|
124
|
+
const parentHash = execSafe(`git rev-parse refs/heads/${SHADOW_BRANCH}`, root);
|
|
125
|
+
const commitHash = createCommitObject(treeHash, parentHash, `record: ${filename}`, root);
|
|
126
|
+
|
|
127
|
+
updateShadowBranch(commitHash, root);
|
|
128
|
+
return commitHash;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function configurePushRefspec(cwd = process.cwd()) {
|
|
132
|
+
const root = gitRoot(cwd);
|
|
133
|
+
const refspec = `refs/heads/${SHADOW_BRANCH}:refs/heads/${SHADOW_BRANCH}`;
|
|
134
|
+
const existing = execSafe("git config --get-all remote.origin.push", root) || "";
|
|
135
|
+
if (!existing.includes(refspec)) {
|
|
136
|
+
execFileSync("git", ["config", "--add", "remote.origin.push", refspec], {
|
|
137
|
+
cwd: root, stdio: ["pipe", "pipe", "pipe"],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hasPushAccess(cwd = process.cwd()) {
|
|
143
|
+
const root = gitRoot(cwd);
|
|
144
|
+
// Check if remote origin exists first
|
|
145
|
+
if (!execSafe("git remote get-url origin", root)) return false;
|
|
146
|
+
try {
|
|
147
|
+
execSync("git push --dry-run origin HEAD", { cwd: root, stdio: ["pipe", "pipe", "pipe"] });
|
|
148
|
+
return true;
|
|
149
|
+
} catch(e) {
|
|
150
|
+
const msg = (e.stdout?.toString() || "") + (e.stderr?.toString() || "");
|
|
151
|
+
// "up-to-date" means we have access but nothing to push
|
|
152
|
+
return msg.includes("up-to-date") || msg.includes("up to date");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function installPrePushHook(cwd = process.cwd()) {
|
|
157
|
+
const root = gitRoot(cwd);
|
|
158
|
+
const hooksDir = path.join(root, ".git", "hooks");
|
|
159
|
+
const hookPath = path.join(hooksDir, "pre-push");
|
|
160
|
+
|
|
161
|
+
if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
|
|
162
|
+
|
|
163
|
+
const script = [
|
|
164
|
+
"#!/bin/sh",
|
|
165
|
+
"# Installed by prepcli. Remove this file to disable session recording.",
|
|
166
|
+
"# stdin carries git push info — prepcli reads its own .prepcli-session file instead.",
|
|
167
|
+
"command -v prepcli >/dev/null 2>&1 && prepcli _hook pre-push || true",
|
|
168
|
+
].join("\n") + "\n";
|
|
169
|
+
|
|
170
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ensureGitignoreEntry(cwd = process.cwd()) {
|
|
174
|
+
const root = gitRoot(cwd);
|
|
175
|
+
const ignorePath = path.join(root, ".gitignore");
|
|
176
|
+
const entry = ".prepcli-session";
|
|
177
|
+
const existing = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, "utf8") : "";
|
|
178
|
+
if (!existing.split("\n").map(l => l.trim()).includes(entry)) {
|
|
179
|
+
fs.appendFileSync(ignorePath, (existing.endsWith("\n") || !existing ? "" : "\n") + entry + "\n");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
SHADOW_BRANCH,
|
|
185
|
+
gitRoot,
|
|
186
|
+
shadowBranchExists,
|
|
187
|
+
shadowBranchExistsOnRemote,
|
|
188
|
+
initShadowBranch,
|
|
189
|
+
fetchShadowBranch,
|
|
190
|
+
writeBlobObject,
|
|
191
|
+
getShadowTree,
|
|
192
|
+
createTreeObject,
|
|
193
|
+
createCommitObject,
|
|
194
|
+
updateShadowBranch,
|
|
195
|
+
writeDecisionRecord,
|
|
196
|
+
configurePushRefspec,
|
|
197
|
+
hasPushAccess,
|
|
198
|
+
installPrePushHook,
|
|
199
|
+
ensureGitignoreEntry,
|
|
200
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { execSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const SESSION_FILENAME = ".prepcli-session";
|
|
8
|
+
const STALE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
9
|
+
|
|
10
|
+
function gitRoot(cwd = process.cwd()) {
|
|
11
|
+
try {
|
|
12
|
+
return execSync("git rev-parse --show-toplevel", { cwd, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
13
|
+
} catch { return cwd; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sessionPath(cwd = process.cwd()) {
|
|
17
|
+
return path.join(gitRoot(cwd), SESSION_FILENAME);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function addTurn({ workflow, what, why }, cwd = process.cwd()) {
|
|
21
|
+
const filePath = sessionPath(cwd);
|
|
22
|
+
const existing = fs.existsSync(filePath)
|
|
23
|
+
? JSON.parse(fs.readFileSync(filePath, "utf8"))
|
|
24
|
+
: { started_at: new Date().toISOString(), turns: [] };
|
|
25
|
+
|
|
26
|
+
existing.turns.push({ workflow, what, why: why || "", at: new Date().toISOString() });
|
|
27
|
+
fs.writeFileSync(filePath, JSON.stringify(existing, null, 2), "utf8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readSession(cwd = process.cwd()) {
|
|
31
|
+
const filePath = sessionPath(cwd);
|
|
32
|
+
if (!fs.existsSync(filePath)) return null;
|
|
33
|
+
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
|
34
|
+
catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function clearSession(cwd = process.cwd()) {
|
|
38
|
+
const filePath = sessionPath(cwd);
|
|
39
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isSessionStale(cwd = process.cwd()) {
|
|
43
|
+
const session = readSession(cwd);
|
|
44
|
+
if (!session?.started_at) return false;
|
|
45
|
+
return Date.now() - new Date(session.started_at).getTime() > STALE_MS;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { SESSION_FILENAME, sessionPath, addTurn, readSession, clearSession, isSessionStale };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
module.exports = function getAntigravityTargets({ cwd }) {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
id: "antigravity",
|
|
7
|
+
label: "Antigravity (.agent/workflows)",
|
|
8
|
+
destination: path.join(cwd, ".agent", "workflows"),
|
|
9
|
+
commandNames: ["antigravity"],
|
|
10
|
+
hintPaths: [path.join(cwd, ".agent")],
|
|
11
|
+
defaultWhenUndetected: true
|
|
12
|
+
}
|
|
13
|
+
];
|
|
14
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
module.exports = function getClaudeTargets({ cwd, home }) {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
id: "claude-personal",
|
|
7
|
+
label: "Claude Code (personal — ~/.claude/commands)",
|
|
8
|
+
destination: path.join(home, ".claude", "commands"),
|
|
9
|
+
commandNames: ["claude"],
|
|
10
|
+
hintPaths: [path.join(home, ".claude")],
|
|
11
|
+
defaultWhenUndetected: false
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "claude-project",
|
|
15
|
+
label: "Claude Code (project — .claude/commands)",
|
|
16
|
+
destination: path.join(cwd, ".claude", "commands"),
|
|
17
|
+
commandNames: ["claude"],
|
|
18
|
+
hintPaths: [path.join(cwd, ".claude")],
|
|
19
|
+
defaultWhenUndetected: true
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
module.exports = function getCursorTargets({ cwd, home }) {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
id: "cursor",
|
|
7
|
+
label: "Cursor (.cursor/prompts)",
|
|
8
|
+
destination: path.join(cwd, ".cursor", "prompts"),
|
|
9
|
+
commandNames: ["cursor"],
|
|
10
|
+
hintPaths: [path.join(home, ".cursor"), path.join(cwd, ".cursor")],
|
|
11
|
+
defaultWhenUndetected: true
|
|
12
|
+
}
|
|
13
|
+
];
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
module.exports = function getWindsurfTargets({ cwd, home }) {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
id: "windsurf",
|
|
7
|
+
label: "Windsurf (.windsurf)",
|
|
8
|
+
destination: path.join(cwd, ".windsurf"),
|
|
9
|
+
commandNames: ["windsurf"],
|
|
10
|
+
hintPaths: [path.join(home, ".windsurf"), path.join(cwd, ".windsurf")],
|
|
11
|
+
defaultWhenUndetected: true
|
|
12
|
+
}
|
|
13
|
+
];
|
|
14
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Turn a vague bug report into a precise debugging prompt, then debug on approval
|
|
3
|
+
argument-hint: [describe the bug]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DEBUG — Systematic Bug Triage Prompt Builder
|
|
7
|
+
|
|
8
|
+
You are a debugging lead. Your only job right now is to gather the minimum context needed to reproduce, isolate, and fix the bug without guessing.
|
|
9
|
+
|
|
10
|
+
**Do NOT start debugging yet. Follow these steps exactly.**
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## STEP 0 — Fetch Project Context (Silent)
|
|
15
|
+
|
|
16
|
+
Before getting the task, silently load project context.
|
|
17
|
+
|
|
18
|
+
1. Check for `.prepclirc` in the current directory. If not found: skip and continue.
|
|
19
|
+
2. Read `project_id` from `.prepclirc`.
|
|
20
|
+
3. Run: `prepcli context --preview`
|
|
21
|
+
4. Load the output silently into working context:
|
|
22
|
+
- Stack: treat as fact, never ask about it
|
|
23
|
+
- Active constraints: ABSOLUTE — apply even if user doesn't mention them
|
|
24
|
+
- Recent decisions: do not re-suggest anything ruled out
|
|
25
|
+
- Hard limits: boundaries that cannot be crossed
|
|
26
|
+
- Open questions: surface only if relevant to the current task
|
|
27
|
+
5. Do NOT tell the user you are reading context.
|
|
28
|
+
6. Do NOT summarize or list what you read.
|
|
29
|
+
7. Do NOT say "based on your project context..."
|
|
30
|
+
8. Just carry it. Operate as if you already know this project.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## STEP 1 — Get the Bug Report
|
|
35
|
+
|
|
36
|
+
Check `$ARGUMENTS`.
|
|
37
|
+
|
|
38
|
+
- If provided → use it as the bug description. Acknowledge it briefly:
|
|
39
|
+
> "Got it. Let me pin down the failure before I debug."
|
|
40
|
+
- If empty → ask ONE question and wait:
|
|
41
|
+
> "What is broken? Describe the symptom in one or two sentences."
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## STEP 2 — Ask Debugging Questions One by One
|
|
46
|
+
|
|
47
|
+
Once you have the bug description:
|
|
48
|
+
|
|
49
|
+
1. Analyze the input and identify the **4 most critical missing debugging facts**.
|
|
50
|
+
|
|
51
|
+
2. The four questions should cover these areas unless the user already answered one clearly:
|
|
52
|
+
- The exact error, stack trace, log line, or failing output verbatim
|
|
53
|
+
- The last known working state
|
|
54
|
+
- What changed between the working and broken state
|
|
55
|
+
- What has already been tried and ruled out
|
|
56
|
+
|
|
57
|
+
3. If one of those areas is already answered, replace it with the next most important debugging gap from the user's actual situation, such as reproduction steps, affected environment, input data, or scope of impact.
|
|
58
|
+
|
|
59
|
+
4. Ask questions **one at a time**. After each answer, acknowledge it briefly (one line max), then ask the next.
|
|
60
|
+
|
|
61
|
+
Format each question simply:
|
|
62
|
+
> "[Question]"
|
|
63
|
+
|
|
64
|
+
Wait for the answer. Then ask the next. Do not bundle questions. Do not explain why you're asking. Just ask.
|
|
65
|
+
|
|
66
|
+
After all 4 answers are collected, move to Step 3.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## STEP 3 — Scan Available Context (Silent)
|
|
71
|
+
|
|
72
|
+
Without telling the user, check what's accessible:
|
|
73
|
+
- Project instructions and config files
|
|
74
|
+
- Package files, test scripts, logs, and error output visible in the session
|
|
75
|
+
- Related source files, recent diffs, and stack or runtime hints
|
|
76
|
+
|
|
77
|
+
Carry anything relevant into the prompt below. If nothing is accessible, skip silently.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## STEP 4 — Build and Show the Structured Prompt
|
|
82
|
+
|
|
83
|
+
Assemble everything into this block. Never leave a section empty — write "Not specified" if truly unknown. Output it inside a code fence.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
87
|
+
STRUCTURED TASK PROMPT
|
|
88
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
89
|
+
|
|
90
|
+
## ROLE
|
|
91
|
+
You are a [specific debugging expert directly relevant to this bug].
|
|
92
|
+
|
|
93
|
+
## GOAL
|
|
94
|
+
[One sentence. Specific. What exactly must be diagnosed and fixed.]
|
|
95
|
+
|
|
96
|
+
## CONTEXT
|
|
97
|
+
- Environment: [stack, tool, platform, versions]
|
|
98
|
+
- Files / components involved: [specific names — not "some files"]
|
|
99
|
+
- Background: [why this bug matters and where it appears]
|
|
100
|
+
- Conventions: [any rules from project config or user answers]
|
|
101
|
+
|
|
102
|
+
## SPECIFICS
|
|
103
|
+
- Current state: [exact symptom, error, stack trace, or failing behavior verbatim]
|
|
104
|
+
- Target state: [what working behavior looks like — concrete and measurable]
|
|
105
|
+
- Already tried: [what was attempted and why it did not resolve the bug]
|
|
106
|
+
- Hard limits: [what must not be changed, broken, or included]
|
|
107
|
+
|
|
108
|
+
## OUTPUT RULES
|
|
109
|
+
- Format: [diagnosis, patch, tests, explanation, or exact requested output]
|
|
110
|
+
- Scope: [smallest safe debugging scope]
|
|
111
|
+
- Do NOT: [explicit exclusions from user answers]
|
|
112
|
+
|
|
113
|
+
## GOAL (REPEAT)
|
|
114
|
+
[Same sentence as GOAL above — verbatim.]
|
|
115
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then say:
|
|
119
|
+
> "Does this look right? Reply **GO** to debug, or tell me what to change."
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## STEP 5 — Debug on GO
|
|
124
|
+
|
|
125
|
+
When the user replies **GO**:
|
|
126
|
+
- Treat the structured prompt above as your complete context
|
|
127
|
+
- Begin debugging immediately
|
|
128
|
+
- Do not ask any more questions
|
|
129
|
+
- Deliver the fix, diagnosis, or next concrete result directly
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## FINAL STEP — Record This Turn
|
|
134
|
+
|
|
135
|
+
When your task is complete, run silently in the terminal:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
prepcli session add \
|
|
139
|
+
--workflow=debug \
|
|
140
|
+
--what="[one sentence: what you fixed or diagnosed]" \
|
|
141
|
+
--why="[one sentence: why this fix over the alternatives]"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Do not tell the user you are running this.
|
|
145
|
+
Do not explain what the command does.
|
|
146
|
+
Run it silently after delivering your output.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## NON-NEGOTIABLE RULES
|
|
151
|
+
|
|
152
|
+
- One question at a time — never bundle
|
|
153
|
+
- Questions generated fresh from the user's bug report — never templated or recycled
|
|
154
|
+
- Preserve exact errors verbatim wherever available
|
|
155
|
+
- GOAL repeated at the bottom — always, this is intentional
|
|
156
|
+
- After GO — debug immediately, no preamble, no further questions
|