@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.
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+
3
+ const readline = require("node:readline/promises");
4
+
5
+ const { generateId, getCurrentCommit, getChangedFiles, recordFilename } = require("../lib/decision");
6
+ const {
7
+ shadowBranchExists, shadowBranchExistsOnRemote,
8
+ initShadowBranch, fetchShadowBranch,
9
+ writeDecisionRecord,
10
+ } = require("../lib/git");
11
+ const { isLoggedIn, requireLoginFresh, readRC } = require("../lib/config");
12
+ const api = require("../lib/api");
13
+
14
+ async function prompt(rl, question, fallback = "") {
15
+ const answer = (await rl.question(question)).trim();
16
+ return answer || fallback;
17
+ }
18
+
19
+ function buildRecord({ id, what, why, ruledOut, workflow, commitHash, filesChanged }) {
20
+ const date = new Date().toISOString();
21
+ const filesList = (filesChanged || []).join(", ") || "none";
22
+ const ruledOutSection = ruledOut
23
+ ? ruledOut.split(",").map(r => `- ${r.trim()}`).join("\n")
24
+ : "None recorded.";
25
+
26
+ return `---
27
+ id: ${id}
28
+ commit: ${commitHash || "none"}
29
+ date: ${date}
30
+ workflow: ${workflow}
31
+ files_changed: [${filesList}]
32
+ ai_turn_count: 0
33
+ ---
34
+
35
+ ## Summary
36
+ ${what}
37
+
38
+ ## Why This Approach
39
+ ${why}
40
+
41
+ ## What Was Tried and Ruled Out
42
+ ${ruledOutSection}
43
+
44
+ ## AI Session Turns
45
+ Manual record — not from an AI session.
46
+ `;
47
+ }
48
+
49
+ async function run(opts = {}) {
50
+ const cwd = process.cwd();
51
+
52
+ // ── Collect fields ──────────────────────────────────────────────────────────
53
+ let { what, why, ruledOut, workflow } = opts;
54
+
55
+ if (!what || !why) {
56
+ // Interactive mode
57
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
58
+
59
+ try {
60
+ console.log("\n[prepcli] Recording decision\n");
61
+
62
+ if (!what) {
63
+ what = await prompt(rl, "What did you decide or discover?\n> ");
64
+ if (!what) { console.log("Cancelled — nothing recorded."); return; }
65
+ }
66
+
67
+ if (!why) {
68
+ why = await prompt(rl, "\nWhy?\n> ");
69
+ if (!why) { console.log("Cancelled — nothing recorded."); return; }
70
+ }
71
+
72
+ if (!ruledOut) {
73
+ ruledOut = await prompt(rl, "\nWhat was ruled out? (Enter to skip)\n> ");
74
+ }
75
+
76
+ if (!workflow) {
77
+ workflow = await prompt(rl, "\nWorkflow? [manual/debug/plan/discovery] (Enter for manual)\n> ", "manual");
78
+ }
79
+
80
+ } finally {
81
+ rl.close();
82
+ }
83
+ }
84
+
85
+ workflow = workflow || "manual";
86
+
87
+ // ── Ensure shadow branch exists ─────────────────────────────────────────────
88
+ if (!shadowBranchExists(cwd)) {
89
+ process.stdout.write("\nInitialising shadow branch...");
90
+ if (shadowBranchExistsOnRemote(cwd)) {
91
+ fetchShadowBranch(cwd);
92
+ console.log(" fetched from remote.");
93
+ } else {
94
+ initShadowBranch(cwd);
95
+ console.log(" created.");
96
+ }
97
+ }
98
+
99
+ // ── Build and write record ──────────────────────────────────────────────────
100
+ const id = generateId();
101
+ const commitHash = getCurrentCommit(cwd);
102
+ const filesChanged = getChangedFiles(cwd);
103
+ const content = buildRecord({ id, what, why, ruledOut, workflow, commitHash, filesChanged });
104
+ const filename = recordFilename(id);
105
+
106
+ process.stdout.write("\nWriting to shadow branch...");
107
+ try {
108
+ writeDecisionRecord(filename, content, cwd);
109
+ } catch(e) {
110
+ console.error(` failed: ${e.message}`);
111
+ process.exit(1);
112
+ }
113
+ console.log(" done.");
114
+
115
+ // ── Write lean summary to cloud if logged in ────────────────────────────────
116
+ if (isLoggedIn()) {
117
+ try {
118
+ const cfg = await requireLoginFresh();
119
+ const rc = readRC();
120
+ if (rc?.project_id) {
121
+ const lean = {
122
+ summary: what,
123
+ why,
124
+ alternatives_rejected: ruledOut ? ruledOut.split(",").map(r => r.trim()) : [],
125
+ key_files: filesChanged.slice(0, 10),
126
+ workflow,
127
+ ai_turn_count: 0,
128
+ commit_hash: commitHash,
129
+ session_start: new Date().toISOString(),
130
+ session_end: new Date().toISOString(),
131
+ };
132
+ await api.post(`/projects/${rc.project_id}/sessions`, lean, cfg.access_token);
133
+ }
134
+ } catch { /* cloud write is non-fatal */ }
135
+ }
136
+
137
+ console.log(`\n✓ Decision recorded (${id})`);
138
+ console.log(" View: prepcli log\n");
139
+ }
140
+
141
+ module.exports = { run };
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ const { addTurn, readSession, clearSession } = require("../lib/session-file");
4
+
5
+ function run(action, opts = {}) {
6
+ if (action === "add") {
7
+ const { workflow, what, why } = opts;
8
+
9
+ if (!workflow || !what) {
10
+ console.error("prepcli session add requires --workflow and --what");
11
+ process.exit(1);
12
+ }
13
+
14
+ addTurn({ workflow, what, why: why || "" });
15
+ // Silent — no output. AI calls this invisibly.
16
+ return;
17
+ }
18
+
19
+ if (action === "show") {
20
+ const session = readSession();
21
+ if (!session?.turns?.length) {
22
+ console.log("No active session.");
23
+ return;
24
+ }
25
+ console.log(`Session started: ${session.started_at}`);
26
+ console.log(`Turns: ${session.turns.length}`);
27
+ for (const t of session.turns) {
28
+ console.log(` [${t.workflow}] ${t.what}`);
29
+ if (t.why) console.log(` why: ${t.why}`);
30
+ }
31
+ return;
32
+ }
33
+
34
+ if (action === "clear") {
35
+ clearSession();
36
+ console.log("Session cleared.");
37
+ return;
38
+ }
39
+
40
+ console.error(`Unknown session action: ${action}. Use: add | show | clear`);
41
+ process.exit(1);
42
+ }
43
+
44
+ module.exports = { run };
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ // Phase 4 — Delta Tracker
3
+
4
+ function run(opts) {
5
+ console.log("[Phase 4] stats — not implemented yet.");
6
+ console.log("Coming in Phase 4: show prompt quality scores and delta trends");
7
+ console.log("from Supabase across all sessions in this project.");
8
+ }
9
+
10
+ module.exports = { run };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ // Phase 6 — Team Features
3
+
4
+ function run(action, email) {
5
+ switch (action) {
6
+ case "invite":
7
+ console.log(`[Phase 6] team invite ${email || "<email>"} — not implemented yet.`);
8
+ break;
9
+ case "list":
10
+ console.log("[Phase 6] team list — not implemented yet.");
11
+ break;
12
+ case "remove":
13
+ console.log(`[Phase 6] team remove ${email || "<email>"} — not implemented yet.`);
14
+ break;
15
+ default:
16
+ console.error(`Unknown team action: ${action}. Use invite | list | remove`);
17
+ process.exit(1);
18
+ }
19
+ }
20
+
21
+ module.exports = { run };
@@ -0,0 +1,105 @@
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
+ return [
18
+ ...getClaudeTargets(ctx),
19
+ ...getCursorTargets(ctx),
20
+ ...getWindsurfTargets(ctx),
21
+ ...getAntigravityTargets(ctx),
22
+ ];
23
+ }
24
+
25
+ function listWorkflows() {
26
+ return fs.readdirSync(WORKFLOW_DIR).filter((f) => f.endsWith(".md")).sort();
27
+ }
28
+
29
+ function fmtDest(dest) {
30
+ const home = os.homedir();
31
+ if (dest.startsWith(home)) return `~${dest.slice(home.length)}`;
32
+ return path.relative(process.cwd(), dest) || ".";
33
+ }
34
+
35
+ function isInstalled(destination, workflows) {
36
+ return workflows.some((f) => {
37
+ try { fs.accessSync(path.join(destination, f)); return true; }
38
+ catch { return false; }
39
+ });
40
+ }
41
+
42
+ async function run(opts) {
43
+ const workflows = listWorkflows();
44
+ const targets = getTargets();
45
+
46
+ const installed = targets.filter((t) => isInstalled(t.destination, workflows));
47
+
48
+ if (installed.length === 0) {
49
+ console.log("No prepcli workflows found on this machine.");
50
+ return;
51
+ }
52
+
53
+ if (opts.all || opts.yes) {
54
+ return removeFrom(installed, workflows);
55
+ }
56
+
57
+ console.log("\nprepcli workflows found in:\n");
58
+ installed.forEach((t, i) => {
59
+ console.log(` ${i + 1}. ${t.label} (${fmtDest(t.destination)})`);
60
+ });
61
+
62
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
63
+ try {
64
+ const answer = (await rl.question("\nRemove from which? [all / 1,2 / none]: ")).trim();
65
+ if (!answer || answer === "none" || answer === "n") {
66
+ console.log("Nothing removed.");
67
+ return;
68
+ }
69
+
70
+ const selected = answer === "all" || answer === "a"
71
+ ? installed
72
+ : answer.split(/[,\s]+/).map(Number).filter(Boolean).map((n) => installed[n - 1]).filter(Boolean);
73
+
74
+ await removeFrom(selected, workflows);
75
+ } finally {
76
+ rl.close();
77
+ }
78
+ }
79
+
80
+ function removeFrom(targets, workflows) {
81
+ let failures = 0;
82
+
83
+ for (const t of targets) {
84
+ try {
85
+ for (const file of workflows) {
86
+ const filePath = path.join(t.destination, file);
87
+ try { fs.unlinkSync(filePath); } catch {}
88
+ }
89
+ console.log(` ✓ Removed from ${t.label} (${fmtDest(t.destination)})`);
90
+ } catch (err) {
91
+ failures++;
92
+ console.error(` ✗ ${t.label}: ${err.message}`);
93
+ }
94
+ }
95
+
96
+ if (failures > 0) {
97
+ process.exitCode = 1;
98
+ console.error(`\n${failures} error(s). Check folder permissions.`);
99
+ return;
100
+ }
101
+
102
+ console.log("\nDone. Workflows removed.");
103
+ }
104
+
105
+ module.exports = { run };
package/src/lib/api.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+
3
+ const path = require("node:path");
4
+ require("dotenv").config({ path: path.join(__dirname, "../../.env") });
5
+
6
+ const WORKER_URL = process.env.PREPCLI_API_URL || "https://api.prepcli.in";
7
+
8
+ async function request(method, endpoint, body, token) {
9
+ const headers = { "Content-Type": "application/json" };
10
+ if (token) headers["Authorization"] = `Bearer ${token}`;
11
+
12
+ const options = { method, headers };
13
+ if (body) options.body = JSON.stringify(body);
14
+
15
+ let res;
16
+ try {
17
+ res = await fetch(`${WORKER_URL}${endpoint}`, options);
18
+ } catch {
19
+ throw new Error("Cannot reach prepcli server. Check your connection.");
20
+ }
21
+
22
+ let data;
23
+ try { data = await res.json(); } catch { data = {}; }
24
+
25
+ if (!res.ok) {
26
+ if (res.status === 401) throw new Error("session_expired");
27
+ throw new Error(data.error || "Server error");
28
+ }
29
+
30
+ return data;
31
+ }
32
+
33
+ module.exports = {
34
+ get: (endpoint, token) => request("GET", endpoint, null, token),
35
+ post: (endpoint, body, token) => request("POST", endpoint, body, token),
36
+ put: (endpoint, body, token) => request("PUT", endpoint, body, token),
37
+ del: (endpoint, token) => request("DELETE", endpoint, null, token),
38
+ };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), ".prepcli");
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
9
+ const RC_FILE = ".prepclirc";
10
+ const KNOWN_EMAILS_FILE = path.join(CONFIG_DIR, "known_emails.json");
11
+
12
+ function readConfig() {
13
+ try {
14
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function writeConfig(data) {
21
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
22
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
23
+ }
24
+
25
+ function deleteConfig() {
26
+ try { fs.unlinkSync(CONFIG_FILE); } catch {}
27
+ }
28
+
29
+ function isLoggedIn() {
30
+ const cfg = readConfig();
31
+ return !!(cfg && cfg.access_token);
32
+ }
33
+
34
+ // Silently refresh the access token if it expires within 5 minutes.
35
+ // Uses the stored refresh_token — user never needs to re-login unless
36
+ // the refresh token itself expires (30 days, configurable in Supabase).
37
+ async function refreshIfNeeded() {
38
+ const cfg = readConfig();
39
+ if (!cfg) return null;
40
+
41
+ const now = Math.floor(Date.now() / 1000);
42
+ const expiresIn = (cfg.expires_at || 0) - now;
43
+
44
+ // Still valid for more than 5 minutes — nothing to do
45
+ if (expiresIn > 300) return cfg;
46
+
47
+ if (!cfg.refresh_token) {
48
+ // No refresh token stored — force re-login
49
+ deleteConfig();
50
+ return null;
51
+ }
52
+
53
+ try {
54
+ const api = require("./api");
55
+ const data = await api.post("/auth/refresh", { refresh_token: cfg.refresh_token });
56
+
57
+ const updated = {
58
+ ...cfg,
59
+ access_token: data.access_token,
60
+ refresh_token: data.refresh_token,
61
+ expires_at: data.expires_at,
62
+ };
63
+ writeConfig(updated);
64
+ return updated;
65
+
66
+ } catch (err) {
67
+ if (err.message === "session_expired") {
68
+ deleteConfig();
69
+ return null;
70
+ }
71
+ // Network error — return existing config and let the next API call fail naturally
72
+ return cfg;
73
+ }
74
+ }
75
+
76
+ function requireLogin() {
77
+ if (!isLoggedIn()) {
78
+ console.error("Not logged in. Run: prepcli auth login");
79
+ process.exit(1);
80
+ }
81
+ return readConfig();
82
+ }
83
+
84
+ // Async version — use this in all commands that call Supabase.
85
+ // Silently refreshes the token if needed before returning.
86
+ async function requireLoginFresh() {
87
+ if (!isLoggedIn()) {
88
+ console.error("Not logged in. Run: prepcli auth login");
89
+ process.exit(1);
90
+ }
91
+ const cfg = await refreshIfNeeded();
92
+ if (!cfg) {
93
+ console.error("Session expired. Run: prepcli auth login");
94
+ process.exit(1);
95
+ }
96
+ return cfg;
97
+ }
98
+
99
+ function readRC() {
100
+ try {
101
+ return JSON.parse(fs.readFileSync(RC_FILE, "utf8"));
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ function writeRC(data) {
108
+ fs.writeFileSync(RC_FILE, JSON.stringify(data, null, 2));
109
+ }
110
+
111
+ function requireRC() {
112
+ const rc = readRC();
113
+ if (!rc || !rc.project_id) {
114
+ console.error("No .prepclirc found. Run: npx prepcli init");
115
+ process.exit(1);
116
+ }
117
+ return rc;
118
+ }
119
+
120
+
121
+ function readKnownEmails() {
122
+ try {
123
+ return JSON.parse(fs.readFileSync(KNOWN_EMAILS_FILE, "utf8"));
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+
129
+ function addKnownEmail(email) {
130
+ const emails = readKnownEmails();
131
+ const normalized = email.toLowerCase();
132
+ if (!emails.includes(normalized)) {
133
+ emails.push(normalized);
134
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
135
+ fs.writeFileSync(KNOWN_EMAILS_FILE, JSON.stringify(emails, null, 2));
136
+ }
137
+ }
138
+
139
+ module.exports = {
140
+ readConfig, writeConfig, deleteConfig,
141
+ isLoggedIn, requireLogin, requireLoginFresh, refreshIfNeeded,
142
+ readRC, writeRC, requireRC,
143
+ readKnownEmails, addKnownEmail,
144
+ CONFIG_FILE, RC_FILE, KNOWN_EMAILS_FILE
145
+ };
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const { execSync } = require("node:child_process");
5
+
6
+ function generateId() {
7
+ return "dec-" + crypto.randomBytes(4).toString("hex");
8
+ }
9
+
10
+ function getCurrentCommit(cwd = process.cwd()) {
11
+ try {
12
+ return execSync("git rev-parse HEAD", { cwd, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
13
+ } catch { return null; }
14
+ }
15
+
16
+ function getChangedFiles(cwd = process.cwd()) {
17
+ try {
18
+ const out = execSync("git diff --name-only HEAD~1 HEAD", { cwd, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
19
+ return out.split("\n").filter(Boolean);
20
+ } catch { return []; }
21
+ }
22
+
23
+ function buildRecord({ id, session, commitHash, filesChanged, summary }) {
24
+ const date = new Date().toISOString();
25
+ const turns = session.turns || [];
26
+
27
+ const workflows = [...new Set(turns.map(t => t.workflow))].join(", ") || "unknown";
28
+ const turnLines = turns.map((t, i) => `${i + 1}. [${t.workflow}] ${t.what}`).join("\n") || "None recorded.";
29
+ const altLines = turns.filter(t => t.why).map(t => `- ${t.why}`).join("\n") || "None recorded.";
30
+ const lastWhy = turns.filter(t => t.why).pop()?.why || "Not recorded.";
31
+ const filesList = (filesChanged || []).join(", ") || "unknown";
32
+
33
+ return `---
34
+ id: ${id}
35
+ commit: ${commitHash || "none"}
36
+ date: ${date}
37
+ workflow: ${workflows}
38
+ files_changed: [${filesList}]
39
+ ai_turn_count: ${turns.length}
40
+ ---
41
+
42
+ ## Summary
43
+ ${summary || turns.map(t => t.what).join("; ")}
44
+
45
+ ## Why This Approach
46
+ ${lastWhy}
47
+
48
+ ## What Was Tried and Ruled Out
49
+ ${altLines}
50
+
51
+ ## AI Session Turns
52
+ ${turnLines}
53
+ `;
54
+ }
55
+
56
+ function recordFilename(id, date = new Date()) {
57
+ const d = date.toISOString().slice(0, 10).replace(/-/g, "");
58
+ return `${d}-${id}.md`;
59
+ }
60
+
61
+ module.exports = { generateId, getCurrentCommit, getChangedFiles, buildRecord, recordFilename };