@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,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("node:child_process");
|
|
4
|
+
const { requireLoginFresh, readRC } = require("../lib/config");
|
|
5
|
+
const api = require("../lib/api");
|
|
6
|
+
|
|
7
|
+
function fmtContext(ctx) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
|
|
10
|
+
if (ctx.stack && Object.keys(ctx.stack).length > 0) {
|
|
11
|
+
lines.push("Stack:");
|
|
12
|
+
for (const [k, v] of Object.entries(ctx.stack)) lines.push(` ${k}: ${v}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (ctx.hard_limits?.length) {
|
|
16
|
+
lines.push("\nHard limits:");
|
|
17
|
+
ctx.hard_limits.forEach(l => lines.push(` • ${l}`));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (ctx.active_constraints?.length) {
|
|
21
|
+
lines.push("\nActive constraints:");
|
|
22
|
+
ctx.active_constraints.forEach(l => lines.push(` • ${l}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (ctx.conventions && Object.keys(ctx.conventions).length > 0) {
|
|
26
|
+
lines.push("\nConventions:");
|
|
27
|
+
Object.values(ctx.conventions).forEach(v => lines.push(` • ${v}`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (ctx.recent_decisions?.length) {
|
|
31
|
+
lines.push("\nRecent decisions:");
|
|
32
|
+
ctx.recent_decisions.slice(0, 5).forEach(d => {
|
|
33
|
+
lines.push(` • ${d.what}${d.why ? ` — ${d.why}` : ""}`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (ctx.open_questions?.length) {
|
|
38
|
+
lines.push("\nOpen questions:");
|
|
39
|
+
ctx.open_questions.forEach(q => lines.push(` ? ${q}`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fmtPreview(ctx) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
|
|
48
|
+
if (ctx.stack && Object.keys(ctx.stack).length > 0) {
|
|
49
|
+
parts.push("Stack: " + Object.entries(ctx.stack).map(([k, v]) => `${k}=${v}`).join(", "));
|
|
50
|
+
}
|
|
51
|
+
if (ctx.hard_limits?.length) {
|
|
52
|
+
parts.push("Hard limits: " + ctx.hard_limits.join("; "));
|
|
53
|
+
}
|
|
54
|
+
if (ctx.active_constraints?.length) {
|
|
55
|
+
parts.push("Active constraints: " + ctx.active_constraints.join("; "));
|
|
56
|
+
}
|
|
57
|
+
if (ctx.conventions && Object.keys(ctx.conventions).length > 0) {
|
|
58
|
+
parts.push("Conventions: " + Object.values(ctx.conventions).join("; "));
|
|
59
|
+
}
|
|
60
|
+
if (ctx.recent_decisions?.length) {
|
|
61
|
+
const recent = ctx.recent_decisions.slice(0, 3).map(d => d.what).join("; ");
|
|
62
|
+
parts.push("Recent decisions: " + recent);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return parts.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function run(opts) {
|
|
69
|
+
const rc = readRC();
|
|
70
|
+
|
|
71
|
+
// ── Local fallback (not logged in / no project_id) ────────────────────────
|
|
72
|
+
if (!rc?.project_id) {
|
|
73
|
+
if (!rc?.context) {
|
|
74
|
+
console.error("No context found. Run: prepcli init");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const ctx = rc.context;
|
|
78
|
+
|
|
79
|
+
if (opts.preview) {
|
|
80
|
+
console.log(fmtPreview(ctx));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log("\nProject context (local):\n");
|
|
85
|
+
console.log(fmtContext(ctx));
|
|
86
|
+
console.log("\nNote: context is local only. Log in with `prepcli auth login` to sync to cloud.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Cloud ─────────────────────────────────────────────────────────────────
|
|
91
|
+
const cfg = await requireLoginFresh();
|
|
92
|
+
|
|
93
|
+
let ctx;
|
|
94
|
+
try {
|
|
95
|
+
const result = await api.get(`/projects/${rc.project_id}/context`, cfg.access_token);
|
|
96
|
+
ctx = result.context;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`Failed to fetch context: ${err.message}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!ctx) {
|
|
103
|
+
console.log("No context stored yet. Run: prepcli init");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (opts.preview) {
|
|
108
|
+
console.log(fmtPreview(ctx));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (opts.edit) {
|
|
113
|
+
const os = require("node:os");
|
|
114
|
+
const fs = require("node:fs");
|
|
115
|
+
const path = require("node:path");
|
|
116
|
+
|
|
117
|
+
const tmp = path.join(os.tmpdir(), `prepcli-ctx-${Date.now()}.json`);
|
|
118
|
+
fs.writeFileSync(tmp, JSON.stringify(ctx, null, 2));
|
|
119
|
+
|
|
120
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "nano";
|
|
121
|
+
try {
|
|
122
|
+
execSync(`${editor} "${tmp}"`, { stdio: "inherit" });
|
|
123
|
+
} catch {
|
|
124
|
+
console.error("Editor closed with error. No changes saved.");
|
|
125
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let updated;
|
|
130
|
+
try {
|
|
131
|
+
updated = JSON.parse(fs.readFileSync(tmp, "utf8"));
|
|
132
|
+
} catch {
|
|
133
|
+
console.error("File contains invalid JSON. No changes saved.");
|
|
134
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
138
|
+
|
|
139
|
+
await api.put(`/projects/${rc.project_id}/context`, updated, cfg.access_token);
|
|
140
|
+
console.log("✓ Context updated.");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`\nProject context (${rc.project_id.slice(0, 8)}…):\n`);
|
|
145
|
+
console.log(fmtContext(ctx));
|
|
146
|
+
if (ctx.last_updated) {
|
|
147
|
+
console.log(`\nLast updated: ${new Date(ctx.last_updated).toLocaleString()}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { run };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Phase 6 — Hardening
|
|
3
|
+
|
|
4
|
+
function run() {
|
|
5
|
+
console.log("[Phase 6] doctor — not implemented yet.");
|
|
6
|
+
console.log("Coming in Phase 6: check auth token, .prepclirc, git hooks,");
|
|
7
|
+
console.log("push refspec, and cloud connectivity. Report all issues.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = { run };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const readline = require("node:readline");
|
|
5
|
+
|
|
6
|
+
const { readSession, clearSession, isSessionStale } = require("../lib/session-file");
|
|
7
|
+
const { generateId, getCurrentCommit, getChangedFiles, buildRecord, recordFilename } = require("../lib/decision");
|
|
8
|
+
const {
|
|
9
|
+
shadowBranchExists, shadowBranchExistsOnRemote,
|
|
10
|
+
initShadowBranch, fetchShadowBranch,
|
|
11
|
+
writeDecisionRecord,
|
|
12
|
+
} = require("../lib/git");
|
|
13
|
+
const { isLoggedIn, requireLoginFresh, readRC } = require("../lib/config");
|
|
14
|
+
const api = require("../lib/api");
|
|
15
|
+
|
|
16
|
+
async function askSummary(prompt) {
|
|
17
|
+
// Git pipes push info through process.stdin — open /dev/tty directly for user input
|
|
18
|
+
let input = process.stdin;
|
|
19
|
+
|
|
20
|
+
if (!process.stdin.isTTY) {
|
|
21
|
+
try {
|
|
22
|
+
input = fs.createReadStream("/dev/tty");
|
|
23
|
+
} catch {
|
|
24
|
+
return ""; // No terminal available (CI) — skip silently
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return new Promise(resolve => {
|
|
29
|
+
const rl = readline.createInterface({ input, output: process.stderr });
|
|
30
|
+
rl.question(prompt, answer => {
|
|
31
|
+
resolve(answer.trim());
|
|
32
|
+
rl.close();
|
|
33
|
+
if (input !== process.stdin) input.destroy();
|
|
34
|
+
});
|
|
35
|
+
rl.on("error", () => resolve(""));
|
|
36
|
+
rl.on("close", () => resolve(""));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function handlePrePush() {
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
const session = readSession(cwd);
|
|
43
|
+
|
|
44
|
+
// No session turns — nothing to record, push proceeds normally
|
|
45
|
+
if (!session?.turns?.length) return;
|
|
46
|
+
|
|
47
|
+
const turns = session.turns;
|
|
48
|
+
|
|
49
|
+
if (isSessionStale(cwd)) {
|
|
50
|
+
process.stderr.write("\n[prepcli] Warning: this session started >24 hours ago.\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Show accumulated turns
|
|
54
|
+
process.stderr.write(`\n[prepcli] ${turns.length} AI turn${turns.length === 1 ? "" : "s"} this session:\n`);
|
|
55
|
+
for (const t of turns) {
|
|
56
|
+
process.stderr.write(` [${t.workflow}] ${t.what}\n`);
|
|
57
|
+
}
|
|
58
|
+
process.stderr.write("\n");
|
|
59
|
+
|
|
60
|
+
const summary = await askSummary(" Final summary? (Enter to skip): ");
|
|
61
|
+
|
|
62
|
+
if (!summary) {
|
|
63
|
+
clearSession(cwd);
|
|
64
|
+
process.stderr.write("\n");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const id = generateId();
|
|
69
|
+
const commitHash = getCurrentCommit(cwd);
|
|
70
|
+
const filesChanged = getChangedFiles(cwd);
|
|
71
|
+
const content = buildRecord({ id, session, commitHash, filesChanged, summary });
|
|
72
|
+
const filename = recordFilename(id);
|
|
73
|
+
|
|
74
|
+
// Ensure shadow branch exists
|
|
75
|
+
if (!shadowBranchExists(cwd)) {
|
|
76
|
+
if (shadowBranchExistsOnRemote(cwd)) {
|
|
77
|
+
fetchShadowBranch(cwd);
|
|
78
|
+
} else {
|
|
79
|
+
initShadowBranch(cwd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
process.stderr.write(" Writing to shadow branch...");
|
|
84
|
+
try {
|
|
85
|
+
writeDecisionRecord(filename, content, cwd);
|
|
86
|
+
} catch(e) {
|
|
87
|
+
process.stderr.write(` failed (${e.message})\n`);
|
|
88
|
+
clearSession(cwd);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
process.stderr.write(" done.\n");
|
|
93
|
+
|
|
94
|
+
// Write lean summary to cloud if logged in
|
|
95
|
+
if (isLoggedIn()) {
|
|
96
|
+
try {
|
|
97
|
+
const cfg = await requireLoginFresh();
|
|
98
|
+
const rc = readRC();
|
|
99
|
+
if (rc?.project_id) {
|
|
100
|
+
const lean = {
|
|
101
|
+
summary,
|
|
102
|
+
why: turns.filter(t => t.why).pop()?.why || "",
|
|
103
|
+
alternatives_rejected: turns.filter(t => t.why).map(t => t.why),
|
|
104
|
+
key_files: filesChanged.slice(0, 10),
|
|
105
|
+
workflow: [...new Set(turns.map(t => t.workflow))].join(","),
|
|
106
|
+
ai_turn_count: turns.length,
|
|
107
|
+
commit_hash: commitHash,
|
|
108
|
+
session_start: session.started_at,
|
|
109
|
+
session_end: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
await api.post(`/projects/${rc.project_id}/sessions`, lean, cfg.access_token);
|
|
112
|
+
}
|
|
113
|
+
} catch { /* cloud write must never block push */ }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
clearSession(cwd);
|
|
117
|
+
process.stderr.write(` ✓ Decision recorded (${id})\n`);
|
|
118
|
+
process.stderr.write(` View: prepcli log\n\n`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function run(hookName) {
|
|
122
|
+
if (hookName === "pre-push") {
|
|
123
|
+
handlePrePush()
|
|
124
|
+
.catch(() => {}) // errors must never block push
|
|
125
|
+
.then(() => process.exit(0));
|
|
126
|
+
} else {
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { run };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const readline = require("node:readline/promises");
|
|
4
|
+
const { detectStack } = require("../lib/detect");
|
|
5
|
+
const { isLoggedIn, requireLoginFresh, readRC, writeRC } = require("../lib/config");
|
|
6
|
+
const api = require("../lib/api");
|
|
7
|
+
|
|
8
|
+
function displayStack(stack, gitRemote) {
|
|
9
|
+
const lines = Object.entries(stack).map(([k, v]) => ` ${k}: ${v}`);
|
|
10
|
+
if (gitRemote) lines.push(` git_remote: ${gitRemote}`);
|
|
11
|
+
return lines.join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function prompt(rl, question) {
|
|
15
|
+
return (await rl.question(question)).trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function collectList(rl, header) {
|
|
19
|
+
console.log(`\n${header}`);
|
|
20
|
+
console.log(" Enter one per line. Press Enter on an empty line to finish.");
|
|
21
|
+
const items = [];
|
|
22
|
+
while (true) {
|
|
23
|
+
const line = (await rl.question(" > ")).trim();
|
|
24
|
+
if (!line) break;
|
|
25
|
+
items.push(line);
|
|
26
|
+
}
|
|
27
|
+
return items;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function run() {
|
|
31
|
+
const existing = readRC();
|
|
32
|
+
|
|
33
|
+
if (existing?.project_id || existing?.context) {
|
|
34
|
+
const rl0 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
try {
|
|
36
|
+
const ans = await prompt(rl0, "\n.prepclirc already exists. Re-initialize? [y/N]: ");
|
|
37
|
+
if (!ans.toLowerCase().startsWith("y")) {
|
|
38
|
+
console.log("Nothing changed.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
rl0.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log("\nScanning project...\n");
|
|
47
|
+
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const { stack, name, git_remote } = detectStack(cwd);
|
|
50
|
+
|
|
51
|
+
if (Object.keys(stack).length > 0) {
|
|
52
|
+
console.log("Detected stack:\n" + displayStack(stack, git_remote));
|
|
53
|
+
} else {
|
|
54
|
+
console.log("Could not auto-detect stack.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// ── Confirm or edit stack ─────────────────────────────────────────────────
|
|
61
|
+
const confirm = await prompt(rl, "\nDoes this look right? [Y/n]: ");
|
|
62
|
+
|
|
63
|
+
let finalStack = { ...stack };
|
|
64
|
+
|
|
65
|
+
if (confirm.toLowerCase() === "n") {
|
|
66
|
+
console.log("\nEdit stack (press Enter to keep current value, type to override):");
|
|
67
|
+
const fields = ["language", "runtime", "framework", "db", "ci", "hosting", "package_manager"];
|
|
68
|
+
for (const field of fields) {
|
|
69
|
+
const current = finalStack[field] || "";
|
|
70
|
+
const val = await prompt(rl, ` ${field}${current ? ` [${current}]` : ""}: `);
|
|
71
|
+
if (val) finalStack[field] = val;
|
|
72
|
+
else if (!current) delete finalStack[field];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Strip empty values
|
|
77
|
+
finalStack = Object.fromEntries(Object.entries(finalStack).filter(([, v]) => v));
|
|
78
|
+
|
|
79
|
+
// ── 3 questions ───────────────────────────────────────────────────────────
|
|
80
|
+
const hardLimits = await collectList(rl,
|
|
81
|
+
'Hard limits — things AI must NEVER do.\n e.g. "never add console.log to production", "all DB migrations need a rollback"'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const activeConstraints = await collectList(rl,
|
|
85
|
+
'Active constraints — things that are true RIGHT NOW.\n e.g. "auth module is frozen", "deployment freeze until Dec 15"'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const conventionLines = await collectList(rl,
|
|
89
|
+
'Conventions — patterns AI would get wrong.\n e.g. "use AppError class, never throw raw strings", "snake_case for DB columns"'
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const conventions = conventionLines.length > 0
|
|
93
|
+
? Object.fromEntries(conventionLines.map((v, i) => [String(i), v]))
|
|
94
|
+
: {};
|
|
95
|
+
|
|
96
|
+
const contextPayload = {
|
|
97
|
+
stack: finalStack,
|
|
98
|
+
hard_limits: hardLimits,
|
|
99
|
+
active_constraints: activeConstraints,
|
|
100
|
+
conventions,
|
|
101
|
+
recent_decisions: [],
|
|
102
|
+
open_questions: [],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ── Push to cloud if logged in ────────────────────────────────────────────
|
|
106
|
+
if (isLoggedIn()) {
|
|
107
|
+
try {
|
|
108
|
+
const cfg = await requireLoginFresh();
|
|
109
|
+
const projectName = name || cwd.split("/").pop();
|
|
110
|
+
|
|
111
|
+
process.stdout.write("\nPushing to cloud...");
|
|
112
|
+
|
|
113
|
+
const { project_id, already_existed } = await api.post("/projects", {
|
|
114
|
+
name: projectName,
|
|
115
|
+
git_remote: git_remote || null,
|
|
116
|
+
}, cfg.access_token);
|
|
117
|
+
|
|
118
|
+
await api.put(`/projects/${project_id}/context`, contextPayload, cfg.access_token);
|
|
119
|
+
|
|
120
|
+
writeRC({ project_id, git_remote: git_remote || null });
|
|
121
|
+
|
|
122
|
+
console.log(" done.");
|
|
123
|
+
if (already_existed) {
|
|
124
|
+
console.log(`✓ Updated existing project (${project_id.slice(0, 8)}…)`);
|
|
125
|
+
} else {
|
|
126
|
+
console.log(`✓ Project created (${project_id.slice(0, 8)}…)`);
|
|
127
|
+
}
|
|
128
|
+
console.log("✓ Context pushed to cloud.");
|
|
129
|
+
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`\nCloud sync failed: ${err.message}`);
|
|
132
|
+
console.log("Saving context locally only.");
|
|
133
|
+
writeRC({ project_id: null, git_remote: git_remote || null, context: contextPayload });
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
writeRC({ project_id: null, git_remote: git_remote || null, context: contextPayload });
|
|
137
|
+
console.log("\n✓ Context saved to .prepclirc (local only).");
|
|
138
|
+
console.log(" Log in with `prepcli auth login` to sync to cloud.");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Shadow branch + git hooks ─────────────────────────────────────────────
|
|
142
|
+
const gitLib = require("../lib/git");
|
|
143
|
+
|
|
144
|
+
process.stdout.write("\nSetting up shadow branch...");
|
|
145
|
+
try {
|
|
146
|
+
if (gitLib.shadowBranchExists(cwd)) {
|
|
147
|
+
console.log(" already exists.");
|
|
148
|
+
} else if (gitLib.shadowBranchExistsOnRemote(cwd)) {
|
|
149
|
+
gitLib.fetchShadowBranch(cwd);
|
|
150
|
+
console.log(" fetched from remote.");
|
|
151
|
+
} else {
|
|
152
|
+
gitLib.initShadowBranch(cwd);
|
|
153
|
+
console.log(" created.");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
gitLib.installPrePushHook(cwd);
|
|
157
|
+
console.log("✓ Pre-push hook installed.");
|
|
158
|
+
|
|
159
|
+
gitLib.configurePushRefspec(cwd);
|
|
160
|
+
console.log("✓ Push refspec configured.");
|
|
161
|
+
|
|
162
|
+
gitLib.ensureGitignoreEntry(cwd);
|
|
163
|
+
console.log("✓ .prepcli-session added to .gitignore.");
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.log(` skipped (${err.message})`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log("\nDone. Run `prepcli context` to review.");
|
|
169
|
+
|
|
170
|
+
} finally {
|
|
171
|
+
rl.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { run };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const readline = require("node:readline/promises");
|
|
7
|
+
|
|
8
|
+
const getClaudeTargets = require("../lib/targets/claude");
|
|
9
|
+
const getCursorTargets = require("../lib/targets/cursor");
|
|
10
|
+
const getWindsurfTargets = require("../lib/targets/windsurf");
|
|
11
|
+
const getAntigravityTargets = require("../lib/targets/antigravity");
|
|
12
|
+
|
|
13
|
+
const WORKFLOW_DIR = path.resolve(__dirname, "../../workflows");
|
|
14
|
+
|
|
15
|
+
function getTargets() {
|
|
16
|
+
const ctx = { cwd: process.cwd(), home: os.homedir() };
|
|
17
|
+
const all = [
|
|
18
|
+
...getClaudeTargets(ctx),
|
|
19
|
+
...getCursorTargets(ctx),
|
|
20
|
+
...getWindsurfTargets(ctx),
|
|
21
|
+
...getAntigravityTargets(ctx)
|
|
22
|
+
];
|
|
23
|
+
return all.map((t) => {
|
|
24
|
+
const cmdFound = t.commandNames.some(commandExists);
|
|
25
|
+
const pathFound = t.hintPaths.some(pathExists);
|
|
26
|
+
return { ...t, detected: cmdFound || pathFound };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function commandExists(cmd) {
|
|
31
|
+
return (process.env.PATH || "").split(path.delimiter).some((dir) => {
|
|
32
|
+
const exts = process.platform === "win32" ? ["", ".cmd", ".exe"] : [""];
|
|
33
|
+
return exts.some((ext) => {
|
|
34
|
+
try { fs.accessSync(path.join(dir, cmd + ext), fs.constants.X_OK); return true; }
|
|
35
|
+
catch { return false; }
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pathExists(p) {
|
|
41
|
+
try { fs.accessSync(p, fs.constants.F_OK); return true; } catch { return false; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function listWorkflows() {
|
|
45
|
+
return fs.readdirSync(WORKFLOW_DIR).filter((f) => f.endsWith(".md")).sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fmtDest(dest) {
|
|
49
|
+
const home = os.homedir();
|
|
50
|
+
if (dest.startsWith(home)) return `~${dest.slice(home.length)}`;
|
|
51
|
+
return path.relative(process.cwd(), dest) || ".";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function chooseTargets(targets, opts) {
|
|
55
|
+
if (opts.all || opts.yes) return targets;
|
|
56
|
+
|
|
57
|
+
if (opts.tool) {
|
|
58
|
+
const ids = opts.tool.split(",").map((s) => s.trim());
|
|
59
|
+
const found = ids.map((id) => {
|
|
60
|
+
const t = targets.find((t) => t.id === id);
|
|
61
|
+
if (!t) { console.error(`Unknown tool id: ${id}`); process.exit(1); }
|
|
62
|
+
return t;
|
|
63
|
+
});
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const recommended = targets.filter((t) => t.detected || t.defaultWhenUndetected);
|
|
68
|
+
|
|
69
|
+
if (!process.stdin.isTTY) return recommended;
|
|
70
|
+
|
|
71
|
+
console.log("\nWhere should prepcli install workflow files?\n");
|
|
72
|
+
targets.forEach((t, i) => {
|
|
73
|
+
const tag = t.detected ? "detected" : t.defaultWhenUndetected ? "default" : "not detected";
|
|
74
|
+
console.log(` ${i + 1}. ${t.label} — ${tag}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const defaults = recommended.map((t) => targets.indexOf(t) + 1).join(",");
|
|
78
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
79
|
+
try {
|
|
80
|
+
const answer = await rl.question(`\nChoose [${defaults || "none"}] or type all: `);
|
|
81
|
+
return parseSelection(answer.trim(), targets, recommended);
|
|
82
|
+
} finally {
|
|
83
|
+
rl.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseSelection(input, targets, recommended) {
|
|
88
|
+
if (!input || input === "") return recommended;
|
|
89
|
+
if (input === "all" || input === "a") return targets;
|
|
90
|
+
if (input === "none" || input === "n") return [];
|
|
91
|
+
const nums = input.split(/[,\s]+/).map(Number).filter((n) => Number.isInteger(n));
|
|
92
|
+
if (nums.length === 0) throw new Error("Enter numbers like 1,3 or 'all'.");
|
|
93
|
+
const invalid = nums.filter((n) => n < 1 || n > targets.length);
|
|
94
|
+
if (invalid.length > 0) throw new Error(`Out of range: ${invalid.join(", ")}`);
|
|
95
|
+
return [...new Set(nums)].map((n) => targets[n - 1]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function run(opts) {
|
|
99
|
+
const workflows = listWorkflows();
|
|
100
|
+
const targets = getTargets();
|
|
101
|
+
const selected = await chooseTargets(targets, opts);
|
|
102
|
+
|
|
103
|
+
if (selected.length === 0) { console.log("Nothing selected."); return; }
|
|
104
|
+
|
|
105
|
+
console.log(`\nInstalling ${workflows.length} workflow(s): ${workflows.join(", ")}\n`);
|
|
106
|
+
|
|
107
|
+
let failures = 0;
|
|
108
|
+
for (const t of selected) {
|
|
109
|
+
try {
|
|
110
|
+
fs.mkdirSync(t.destination, { recursive: true });
|
|
111
|
+
for (const file of workflows) {
|
|
112
|
+
fs.copyFileSync(path.join(WORKFLOW_DIR, file), path.join(t.destination, file));
|
|
113
|
+
}
|
|
114
|
+
console.log(` ✓ ${t.label} → ${fmtDest(t.destination)}`);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
failures++;
|
|
117
|
+
console.error(` ✗ ${t.label}: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (failures > 0) {
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
console.error(`\n${failures} error(s). Check folder permissions.`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log("\nDone. Use /prep /debug /review /plan /refactor /write in your AI tool.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { run };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("node:child_process");
|
|
4
|
+
const { shadowBranchExists, SHADOW_BRANCH, gitRoot } = require("../lib/git");
|
|
5
|
+
|
|
6
|
+
function execSafe(cmd, cwd) {
|
|
7
|
+
try { return execSync(cmd, { cwd, stdio: ["pipe", "pipe", "pipe"] }).toString().trim(); }
|
|
8
|
+
catch { return null; }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseRecord(content) {
|
|
12
|
+
const get = re => { const m = content.match(re); return m ? m[1].trim() : null; };
|
|
13
|
+
return {
|
|
14
|
+
id: get(/^id: (.+)$/m),
|
|
15
|
+
date: get(/^date: (.+)$/m),
|
|
16
|
+
workflow: get(/^workflow: (.+)$/m),
|
|
17
|
+
commit: get(/^commit: (.+)$/m),
|
|
18
|
+
turns: parseInt(get(/^ai_turn_count: (\d+)$/m) || "0"),
|
|
19
|
+
files: (get(/^files_changed: \[(.+)\]$/m) || "").split(",").map(f => f.trim()).filter(Boolean),
|
|
20
|
+
summary: get(/## Summary\n(.+)/),
|
|
21
|
+
why: get(/## Why This Approach\n(.+)/),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function run(opts = {}) {
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const root = gitRoot(cwd);
|
|
28
|
+
|
|
29
|
+
if (!shadowBranchExists(cwd)) {
|
|
30
|
+
console.log("No decision records found. Run `prepcli init` to set up the shadow branch.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const filesOut = execSafe(`git ls-tree --name-only ${SHADOW_BRANCH}`, root);
|
|
35
|
+
if (!filesOut) { console.log("No decisions recorded yet."); return; }
|
|
36
|
+
|
|
37
|
+
const filenames = filesOut.split("\n").filter(f => f.endsWith(".md")).reverse();
|
|
38
|
+
|
|
39
|
+
const records = filenames.map(filename => {
|
|
40
|
+
const content = execSafe(`git show ${SHADOW_BRANCH}:${filename}`, root);
|
|
41
|
+
if (!content) return null;
|
|
42
|
+
return { filename, ...parseRecord(content) };
|
|
43
|
+
}).filter(Boolean);
|
|
44
|
+
|
|
45
|
+
// Apply filters
|
|
46
|
+
let filtered = records;
|
|
47
|
+
if (opts.workflow) filtered = filtered.filter(r => r.workflow?.includes(opts.workflow));
|
|
48
|
+
if (opts.file) filtered = filtered.filter(r => r.files.some(f => f.includes(opts.file)));
|
|
49
|
+
if (opts.commit) filtered = filtered.filter(r => r.commit?.startsWith(opts.commit));
|
|
50
|
+
|
|
51
|
+
if (filtered.length === 0) { console.log("No matching decisions found."); return; }
|
|
52
|
+
|
|
53
|
+
for (const r of filtered) {
|
|
54
|
+
const date = r.date ? new Date(r.date).toLocaleDateString("en-US", { month: "short", day: "numeric" }) : "?";
|
|
55
|
+
const commit = r.commit && r.commit !== "none" ? r.commit.slice(0, 7) : "none";
|
|
56
|
+
|
|
57
|
+
console.log(`\n${date} • ${r.workflow || "?"} • ${r.turns} AI turn${r.turns === 1 ? "" : "s"} • commit ${commit}`);
|
|
58
|
+
console.log(` ${r.summary || r.filename}`);
|
|
59
|
+
if (r.why && r.why !== "Not recorded.") console.log(` Why: ${r.why}`);
|
|
60
|
+
if (r.files.length) console.log(` Files: ${r.files.filter(f => f).join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
console.log("");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { run };
|