@vibeversion/cli 0.2.0 → 0.2.1
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/dist/index.js +555 -52
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,8 +14,8 @@ var ApiClient = class {
|
|
|
14
14
|
this.baseUrl = baseUrl;
|
|
15
15
|
this.token = token;
|
|
16
16
|
}
|
|
17
|
-
async request(method,
|
|
18
|
-
const url = `${this.baseUrl}${
|
|
17
|
+
async request(method, path5, body) {
|
|
18
|
+
const url = `${this.baseUrl}${path5}`;
|
|
19
19
|
const headers = {
|
|
20
20
|
"Content-Type": "application/json",
|
|
21
21
|
Accept: "application/json"
|
|
@@ -57,6 +57,9 @@ var ApiClient = class {
|
|
|
57
57
|
async createVersion(projectUlid, data) {
|
|
58
58
|
return this.request("POST", `/api/projects/${projectUlid}/versions`, data);
|
|
59
59
|
}
|
|
60
|
+
async triggerDeploy(projectUlid, data) {
|
|
61
|
+
return this.request("POST", `/api/projects/${projectUlid}/deploy`, data);
|
|
62
|
+
}
|
|
60
63
|
};
|
|
61
64
|
var ApiError = class extends Error {
|
|
62
65
|
constructor(status, message) {
|
|
@@ -70,21 +73,60 @@ var ApiError = class extends Error {
|
|
|
70
73
|
import fs from "fs";
|
|
71
74
|
import path from "path";
|
|
72
75
|
import os from "os";
|
|
73
|
-
var CONFIG_DIR = path.join(os.homedir(), ".
|
|
76
|
+
var CONFIG_DIR = path.join(os.homedir(), ".vv");
|
|
74
77
|
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
75
|
-
|
|
78
|
+
var LEGACY_CONFIG_DIR = path.join(os.homedir(), ".vibeversion");
|
|
79
|
+
var LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIG_DIR, "config.json");
|
|
80
|
+
function readAuthConfig(filePath) {
|
|
76
81
|
try {
|
|
77
|
-
const raw = fs.readFileSync(
|
|
82
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
78
83
|
return JSON.parse(raw);
|
|
79
84
|
} catch {
|
|
80
85
|
return null;
|
|
81
86
|
}
|
|
82
87
|
}
|
|
88
|
+
function migrateLegacyAuthConfig() {
|
|
89
|
+
if (!fs.existsSync(LEGACY_CONFIG_FILE)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const legacyConfig = readAuthConfig(LEGACY_CONFIG_FILE);
|
|
93
|
+
if (!legacyConfig) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
97
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(legacyConfig, null, 2) + "\n", {
|
|
98
|
+
mode: 384
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
fs.unlinkSync(LEGACY_CONFIG_FILE);
|
|
102
|
+
fs.rmdirSync(LEGACY_CONFIG_DIR);
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
return legacyConfig;
|
|
106
|
+
}
|
|
107
|
+
function cleanupLegacyAuthConfig() {
|
|
108
|
+
try {
|
|
109
|
+
fs.unlinkSync(LEGACY_CONFIG_FILE);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
fs.rmdirSync(LEGACY_CONFIG_DIR);
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function getAuthConfig() {
|
|
118
|
+
const currentConfig = readAuthConfig(CONFIG_FILE);
|
|
119
|
+
if (currentConfig) {
|
|
120
|
+
return currentConfig;
|
|
121
|
+
}
|
|
122
|
+
return migrateLegacyAuthConfig();
|
|
123
|
+
}
|
|
83
124
|
function saveAuthConfig(config) {
|
|
84
125
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
85
126
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
86
127
|
mode: 384
|
|
87
128
|
});
|
|
129
|
+
cleanupLegacyAuthConfig();
|
|
88
130
|
}
|
|
89
131
|
|
|
90
132
|
// src/commands/login.ts
|
|
@@ -122,38 +164,102 @@ async function loginCommand() {
|
|
|
122
164
|
}
|
|
123
165
|
|
|
124
166
|
// src/commands/init.ts
|
|
125
|
-
import { select } from "@inquirer/prompts";
|
|
126
|
-
import
|
|
167
|
+
import { confirm, select } from "@inquirer/prompts";
|
|
168
|
+
import chalk3 from "chalk";
|
|
127
169
|
import ora2 from "ora";
|
|
128
170
|
|
|
129
171
|
// src/lib/project.ts
|
|
130
172
|
import fs2 from "fs";
|
|
131
173
|
import path2 from "path";
|
|
132
|
-
var PROJECT_DIR = ".
|
|
174
|
+
var PROJECT_DIR = ".vv";
|
|
175
|
+
var LEGACY_PROJECT_DIR = ".vibeversion";
|
|
133
176
|
var PROJECT_FILE = "config.json";
|
|
134
177
|
function getProjectConfigPath(dir) {
|
|
135
178
|
return path2.join(dir, PROJECT_DIR, PROJECT_FILE);
|
|
136
179
|
}
|
|
137
|
-
function
|
|
180
|
+
function getLegacyProjectConfigPath(dir) {
|
|
181
|
+
return path2.join(dir, LEGACY_PROJECT_DIR, PROJECT_FILE);
|
|
182
|
+
}
|
|
183
|
+
function readProjectConfig(filePath) {
|
|
138
184
|
try {
|
|
139
|
-
const raw = fs2.readFileSync(
|
|
185
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
140
186
|
return JSON.parse(raw);
|
|
141
187
|
} catch {
|
|
142
188
|
return null;
|
|
143
189
|
}
|
|
144
190
|
}
|
|
191
|
+
function migrateProjectConfig(dir) {
|
|
192
|
+
const currentPath = getProjectConfigPath(dir);
|
|
193
|
+
if (fs2.existsSync(currentPath)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const legacyDir = path2.join(dir, LEGACY_PROJECT_DIR);
|
|
197
|
+
const legacyPath = getLegacyProjectConfigPath(dir);
|
|
198
|
+
if (!fs2.existsSync(legacyPath)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const projectDir = path2.join(dir, PROJECT_DIR);
|
|
202
|
+
try {
|
|
203
|
+
fs2.renameSync(legacyDir, projectDir);
|
|
204
|
+
return;
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
const legacyConfig = readProjectConfig(legacyPath);
|
|
208
|
+
if (!legacyConfig) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
fs2.mkdirSync(projectDir, { recursive: true });
|
|
212
|
+
fs2.writeFileSync(currentPath, JSON.stringify(legacyConfig, null, 2) + "\n");
|
|
213
|
+
try {
|
|
214
|
+
fs2.rmSync(legacyDir, { recursive: true, force: true });
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function cleanupLegacyProjectDir(dir) {
|
|
219
|
+
const legacyDir = path2.join(dir, LEGACY_PROJECT_DIR);
|
|
220
|
+
if (!fs2.existsSync(legacyDir)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
fs2.rmSync(legacyDir, { recursive: true, force: true });
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function getProjectConfig(dir = process.cwd()) {
|
|
229
|
+
migrateProjectConfig(dir);
|
|
230
|
+
const currentConfig = readProjectConfig(getProjectConfigPath(dir));
|
|
231
|
+
if (currentConfig) {
|
|
232
|
+
return currentConfig;
|
|
233
|
+
}
|
|
234
|
+
const legacyConfig = readProjectConfig(getLegacyProjectConfigPath(dir));
|
|
235
|
+
if (!legacyConfig) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
saveProjectConfig(legacyConfig, dir);
|
|
239
|
+
return legacyConfig;
|
|
240
|
+
}
|
|
145
241
|
function saveProjectConfig(config, dir = process.cwd()) {
|
|
242
|
+
migrateProjectConfig(dir);
|
|
146
243
|
const projectDir = path2.join(dir, PROJECT_DIR);
|
|
147
244
|
fs2.mkdirSync(projectDir, { recursive: true });
|
|
148
245
|
fs2.writeFileSync(path2.join(projectDir, PROJECT_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
246
|
+
cleanupLegacyProjectDir(dir);
|
|
149
247
|
}
|
|
150
248
|
function ensureGitignore(dir = process.cwd()) {
|
|
151
249
|
const gitignorePath = path2.join(dir, ".gitignore");
|
|
152
|
-
const entry = ".
|
|
250
|
+
const entry = ".vv/";
|
|
251
|
+
const legacyEntry = ".vibeversion/";
|
|
153
252
|
try {
|
|
154
253
|
const content = fs2.readFileSync(gitignorePath, "utf-8");
|
|
155
|
-
if (content.includes(entry))
|
|
156
|
-
|
|
254
|
+
if (content.includes(entry)) {
|
|
255
|
+
if (content.includes(legacyEntry)) {
|
|
256
|
+
const updated2 = content.replaceAll(legacyEntry, entry);
|
|
257
|
+
fs2.writeFileSync(gitignorePath, updated2.trimEnd() + "\n");
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const updated = content.includes(legacyEntry) ? content.replaceAll(legacyEntry, entry) : content;
|
|
262
|
+
fs2.writeFileSync(gitignorePath, updated.trimEnd() + "\n" + entry + "\n");
|
|
157
263
|
} catch {
|
|
158
264
|
fs2.writeFileSync(gitignorePath, entry + "\n");
|
|
159
265
|
}
|
|
@@ -189,6 +295,10 @@ async function stageAll(dir) {
|
|
|
189
295
|
}
|
|
190
296
|
}
|
|
191
297
|
}
|
|
298
|
+
async function isWorktreeClean(dir) {
|
|
299
|
+
const statusMatrix = await git.statusMatrix({ fs: fs3, dir });
|
|
300
|
+
return statusMatrix.every(([, head, workdir, stage]) => head === workdir && workdir === stage);
|
|
301
|
+
}
|
|
192
302
|
async function commit(dir, message) {
|
|
193
303
|
return git.commit({
|
|
194
304
|
fs: fs3,
|
|
@@ -247,6 +357,104 @@ async function diffStats(dir) {
|
|
|
247
357
|
}
|
|
248
358
|
return { filesChanged, insertions, deletions };
|
|
249
359
|
}
|
|
360
|
+
var MAX_DIFF_BYTES = 50 * 1024;
|
|
361
|
+
async function perFileStats(dir) {
|
|
362
|
+
const first = await isFirstCommit(dir);
|
|
363
|
+
if (first) return [];
|
|
364
|
+
const headOid = await git.resolveRef({ fs: fs3, dir, ref: "HEAD" });
|
|
365
|
+
const headCommit = await git.readCommit({ fs: fs3, dir, oid: headOid });
|
|
366
|
+
const currentTree = headCommit.commit.tree;
|
|
367
|
+
let parentTree = null;
|
|
368
|
+
if (headCommit.commit.parent.length > 0) {
|
|
369
|
+
const parentCommit = await git.readCommit({ fs: fs3, dir, oid: headCommit.commit.parent[0] });
|
|
370
|
+
parentTree = parentCommit.commit.tree;
|
|
371
|
+
}
|
|
372
|
+
const currentFiles = await walkTree(dir, currentTree);
|
|
373
|
+
const parentFiles = parentTree ? await walkTree(dir, parentTree) : /* @__PURE__ */ new Map();
|
|
374
|
+
const stats = [];
|
|
375
|
+
for (const [filepath, currentOid] of currentFiles) {
|
|
376
|
+
const parentOid = parentFiles.get(filepath);
|
|
377
|
+
if (!parentOid) {
|
|
378
|
+
const content = await readBlob(dir, currentOid);
|
|
379
|
+
stats.push({ path: filepath, insertions: countLines(content), deletions: 0 });
|
|
380
|
+
} else if (parentOid !== currentOid) {
|
|
381
|
+
const oldContent = await readBlob(dir, parentOid);
|
|
382
|
+
const newContent = await readBlob(dir, currentOid);
|
|
383
|
+
const oldLines = countLines(oldContent);
|
|
384
|
+
const newLines = countLines(newContent);
|
|
385
|
+
stats.push({
|
|
386
|
+
path: filepath,
|
|
387
|
+
insertions: Math.max(0, newLines - oldLines),
|
|
388
|
+
deletions: Math.max(0, oldLines - newLines)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const [filepath] of parentFiles) {
|
|
393
|
+
if (!currentFiles.has(filepath)) {
|
|
394
|
+
const content = await readBlob(dir, parentFiles.get(filepath));
|
|
395
|
+
stats.push({ path: filepath, insertions: 0, deletions: countLines(content) });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return stats;
|
|
399
|
+
}
|
|
400
|
+
async function generateDiffText(dir) {
|
|
401
|
+
const first = await isFirstCommit(dir);
|
|
402
|
+
if (first) return null;
|
|
403
|
+
const headOid = await git.resolveRef({ fs: fs3, dir, ref: "HEAD" });
|
|
404
|
+
const headCommit = await git.readCommit({ fs: fs3, dir, oid: headOid });
|
|
405
|
+
const currentTree = headCommit.commit.tree;
|
|
406
|
+
let parentTree = null;
|
|
407
|
+
if (headCommit.commit.parent.length > 0) {
|
|
408
|
+
const parentCommit = await git.readCommit({ fs: fs3, dir, oid: headCommit.commit.parent[0] });
|
|
409
|
+
parentTree = parentCommit.commit.tree;
|
|
410
|
+
}
|
|
411
|
+
const currentFiles = await walkTree(dir, currentTree);
|
|
412
|
+
const parentFiles = parentTree ? await walkTree(dir, parentTree) : /* @__PURE__ */ new Map();
|
|
413
|
+
const chunks = [];
|
|
414
|
+
let totalBytes = 0;
|
|
415
|
+
for (const [filepath, currentOid] of currentFiles) {
|
|
416
|
+
const parentOid = parentFiles.get(filepath);
|
|
417
|
+
if (!parentOid) {
|
|
418
|
+
const content = await readBlob(dir, currentOid);
|
|
419
|
+
const lines = content.split("\n").map((l) => `+${l}`);
|
|
420
|
+
const chunk = `--- /dev/null
|
|
421
|
+
+++ b/${filepath}
|
|
422
|
+
${lines.join("\n")}
|
|
423
|
+
`;
|
|
424
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
425
|
+
if (totalBytes > MAX_DIFF_BYTES) break;
|
|
426
|
+
chunks.push(chunk);
|
|
427
|
+
} else if (parentOid !== currentOid) {
|
|
428
|
+
const oldContent = await readBlob(dir, parentOid);
|
|
429
|
+
const newContent = await readBlob(dir, currentOid);
|
|
430
|
+
const oldLines = oldContent.split("\n");
|
|
431
|
+
const newLines = newContent.split("\n");
|
|
432
|
+
const removed = oldLines.filter((l) => !newLines.includes(l)).map((l) => `-${l}`);
|
|
433
|
+
const added = newLines.filter((l) => !oldLines.includes(l)).map((l) => `+${l}`);
|
|
434
|
+
const chunk = `--- a/${filepath}
|
|
435
|
+
+++ b/${filepath}
|
|
436
|
+
${[...removed, ...added].join("\n")}
|
|
437
|
+
`;
|
|
438
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
439
|
+
if (totalBytes > MAX_DIFF_BYTES) break;
|
|
440
|
+
chunks.push(chunk);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const [filepath] of parentFiles) {
|
|
444
|
+
if (!currentFiles.has(filepath)) {
|
|
445
|
+
const content = await readBlob(dir, parentFiles.get(filepath));
|
|
446
|
+
const lines = content.split("\n").map((l) => `-${l}`);
|
|
447
|
+
const chunk = `--- a/${filepath}
|
|
448
|
+
+++ /dev/null
|
|
449
|
+
${lines.join("\n")}
|
|
450
|
+
`;
|
|
451
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
452
|
+
if (totalBytes > MAX_DIFF_BYTES) break;
|
|
453
|
+
chunks.push(chunk);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return chunks.length > 0 ? chunks.join("\n") : null;
|
|
457
|
+
}
|
|
250
458
|
async function restoreCommit(dir, commitHash) {
|
|
251
459
|
const commitObj = await git.readCommit({ fs: fs3, dir, oid: commitHash });
|
|
252
460
|
const targetTree = commitObj.commit.tree;
|
|
@@ -298,7 +506,7 @@ async function walkTreeWithSize(dir, treeOid, prefix = "") {
|
|
|
298
506
|
function cleanEmptyDirs(dir, rootDir) {
|
|
299
507
|
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
300
508
|
for (const entry of entries) {
|
|
301
|
-
if (entry.isDirectory() && entry.name !== ".git" && entry.name !== ".
|
|
509
|
+
if (entry.isDirectory() && entry.name !== ".git" && entry.name !== ".vv") {
|
|
302
510
|
const fullPath = path3.join(dir, entry.name);
|
|
303
511
|
cleanEmptyDirs(fullPath, rootDir);
|
|
304
512
|
try {
|
|
@@ -336,11 +544,170 @@ function countLines(content) {
|
|
|
336
544
|
return content.split("\n").length;
|
|
337
545
|
}
|
|
338
546
|
|
|
547
|
+
// src/lib/hooks.ts
|
|
548
|
+
import fs4 from "fs";
|
|
549
|
+
import path4 from "path";
|
|
550
|
+
import chalk2 from "chalk";
|
|
551
|
+
function setupClaudeCodeHook(dir) {
|
|
552
|
+
const claudeDir = path4.join(dir, ".claude");
|
|
553
|
+
const settingsPath = path4.join(claudeDir, "settings.json");
|
|
554
|
+
if (!fs4.existsSync(claudeDir)) {
|
|
555
|
+
fs4.mkdirSync(claudeDir, { recursive: true });
|
|
556
|
+
}
|
|
557
|
+
let settings = {};
|
|
558
|
+
if (fs4.existsSync(settingsPath)) {
|
|
559
|
+
try {
|
|
560
|
+
settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!settings.hooks) {
|
|
565
|
+
settings.hooks = {};
|
|
566
|
+
}
|
|
567
|
+
const existing = settings.hooks.PostToolUse ?? [];
|
|
568
|
+
const alreadyHasHook = existing.some((h) => h.hooks?.some((hook) => hook.command?.includes("vv save")));
|
|
569
|
+
if (alreadyHasHook) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
settings.hooks.PostToolUse = [
|
|
573
|
+
...existing,
|
|
574
|
+
{
|
|
575
|
+
matcher: "Write|Edit",
|
|
576
|
+
hooks: [
|
|
577
|
+
{
|
|
578
|
+
type: "command",
|
|
579
|
+
command: "vv save --auto",
|
|
580
|
+
timeout: 30
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
}
|
|
584
|
+
];
|
|
585
|
+
fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
function setupCursorRule(dir) {
|
|
589
|
+
const cursorDir = path4.join(dir, ".cursor", "rules");
|
|
590
|
+
const rulePath = path4.join(cursorDir, "vibeversion.mdc");
|
|
591
|
+
if (fs4.existsSync(rulePath)) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
fs4.mkdirSync(cursorDir, { recursive: true });
|
|
595
|
+
const content = `---
|
|
596
|
+
alwaysApply: true
|
|
597
|
+
---
|
|
598
|
+
This project uses Vibeversion for version tracking.
|
|
599
|
+
After completing a task or making significant changes, run \`vv save\` to snapshot the project.
|
|
600
|
+
Available commands: \`vv save\`, \`vv deploy\`, \`vv restore\`, \`vv files\`.
|
|
601
|
+
`;
|
|
602
|
+
fs4.writeFileSync(rulePath, content);
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
function setupOpenCodePlugin(dir) {
|
|
606
|
+
const pluginDir = path4.join(dir, ".opencode", "plugins");
|
|
607
|
+
const pluginPath = path4.join(pluginDir, "vibeversion.js");
|
|
608
|
+
if (fs4.existsSync(pluginPath)) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
fs4.mkdirSync(pluginDir, { recursive: true });
|
|
612
|
+
const content = `export const VibeversionPlugin = async ({ $ }) => {
|
|
613
|
+
const debounceMs = 12000
|
|
614
|
+
let timer = null
|
|
615
|
+
let inFlight = false
|
|
616
|
+
|
|
617
|
+
const shouldIgnore = (filePath) => {
|
|
618
|
+
if (!filePath) return false
|
|
619
|
+
const normalized = String(filePath).replaceAll('\\\\', '/')
|
|
620
|
+
|
|
621
|
+
if (normalized.includes('/.git/')) return true
|
|
622
|
+
if (normalized.includes('/.vv/')) return true
|
|
623
|
+
if (normalized.includes('/.opencode/')) return true
|
|
624
|
+
if (normalized.endsWith('/.opencode/plugins/vibeversion.js')) return true
|
|
625
|
+
|
|
626
|
+
return false
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const runSave = async () => {
|
|
630
|
+
if (inFlight) {
|
|
631
|
+
timer = setTimeout(runSave, debounceMs)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
inFlight = true
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
await $\`vv save --auto\`
|
|
639
|
+
} catch {
|
|
640
|
+
// Best-effort auto-save
|
|
641
|
+
} finally {
|
|
642
|
+
inFlight = false
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
'file.edited': async (input) => {
|
|
648
|
+
const filePath =
|
|
649
|
+
input?.path ??
|
|
650
|
+
input?.file?.path ??
|
|
651
|
+
input?.filePath ??
|
|
652
|
+
input?.file?.name ??
|
|
653
|
+
input?.file
|
|
654
|
+
|
|
655
|
+
if (shouldIgnore(filePath)) return
|
|
656
|
+
|
|
657
|
+
if (timer) clearTimeout(timer)
|
|
658
|
+
timer = setTimeout(runSave, debounceMs)
|
|
659
|
+
},
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
`;
|
|
663
|
+
fs4.writeFileSync(pluginPath, content);
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
function setupClaudeMd(dir) {
|
|
667
|
+
const claudeMdPath = path4.join(dir, "CLAUDE.md");
|
|
668
|
+
const section = `
|
|
669
|
+
## Vibeversion
|
|
670
|
+
|
|
671
|
+
This project uses Vibeversion for version tracking. After completing a task, run \`vv save\` to snapshot the project.
|
|
672
|
+
|
|
673
|
+
Available commands:
|
|
674
|
+
- \`vv save\` - Save the current state
|
|
675
|
+
- \`vv deploy\` - Save and deploy
|
|
676
|
+
- \`vv restore\` - Restore to a previous version
|
|
677
|
+
- \`vv files\` - List files in a version
|
|
678
|
+
`;
|
|
679
|
+
if (fs4.existsSync(claudeMdPath)) {
|
|
680
|
+
const existing = fs4.readFileSync(claudeMdPath, "utf-8");
|
|
681
|
+
if (existing.includes("Vibeversion")) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
fs4.appendFileSync(claudeMdPath, section);
|
|
685
|
+
} else {
|
|
686
|
+
fs4.writeFileSync(claudeMdPath, `# CLAUDE.md
|
|
687
|
+
${section}`);
|
|
688
|
+
}
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
function setupAllHooks(dir) {
|
|
692
|
+
const claudeResult = setupClaudeCodeHook(dir);
|
|
693
|
+
console.log(claudeResult ? chalk2.green(" \u2713 Claude Code hook added (.claude/settings.json)") : chalk2.dim(" - Claude Code hook already exists"));
|
|
694
|
+
const cursorResult = setupCursorRule(dir);
|
|
695
|
+
console.log(cursorResult ? chalk2.green(" \u2713 Cursor rule added (.cursor/rules/vibeversion.mdc)") : chalk2.dim(" - Cursor rule already exists"));
|
|
696
|
+
const openCodeResult = setupOpenCodePlugin(dir);
|
|
697
|
+
console.log(
|
|
698
|
+
openCodeResult ? chalk2.green(" \u2713 OpenCode plugin added (.opencode/plugins/vibeversion.js)") : chalk2.dim(" - OpenCode plugin already exists")
|
|
699
|
+
);
|
|
700
|
+
const claudeMdResult = setupClaudeMd(dir);
|
|
701
|
+
console.log(
|
|
702
|
+
claudeMdResult ? chalk2.green(" \u2713 CLAUDE.md updated with Vibeversion commands") : chalk2.dim(" - CLAUDE.md already has Vibeversion section")
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
339
706
|
// src/commands/init.ts
|
|
340
707
|
async function initCommand() {
|
|
341
708
|
const auth = getAuthConfig();
|
|
342
709
|
if (!auth) {
|
|
343
|
-
console.log(
|
|
710
|
+
console.log(chalk3.red("Not logged in. Run `vv login` first."));
|
|
344
711
|
process.exit(1);
|
|
345
712
|
}
|
|
346
713
|
const spinner = ora2("Fetching your projects...").start();
|
|
@@ -355,7 +722,7 @@ async function initCommand() {
|
|
|
355
722
|
}
|
|
356
723
|
spinner.stop();
|
|
357
724
|
if (projects.length === 0) {
|
|
358
|
-
console.log(
|
|
725
|
+
console.log(chalk3.yellow("No projects found. Create one on your Vibeversion dashboard first."));
|
|
359
726
|
process.exit(1);
|
|
360
727
|
}
|
|
361
728
|
const projectUlid = await select({
|
|
@@ -371,50 +738,78 @@ async function initCommand() {
|
|
|
371
738
|
saveProjectConfig({ projectUlid, apiUrl: auth.apiUrl, projectName: project.name }, dir);
|
|
372
739
|
ensureGitignore(dir);
|
|
373
740
|
await ensureRepo(dir);
|
|
374
|
-
setupSpinner.succeed(`Linked to ${
|
|
741
|
+
setupSpinner.succeed(`Linked to ${chalk3.cyan(project.name)}.`);
|
|
742
|
+
const setupHooks = await confirm({
|
|
743
|
+
message: "Set up auto-save hooks for AI tools (Claude Code, Cursor, OpenCode)?",
|
|
744
|
+
default: true
|
|
745
|
+
});
|
|
746
|
+
if (setupHooks) {
|
|
747
|
+
console.log("");
|
|
748
|
+
setupAllHooks(dir);
|
|
749
|
+
}
|
|
750
|
+
console.log(chalk3.green(`
|
|
751
|
+
Run ${chalk3.bold("vv save")} to save your first version.
|
|
752
|
+
`));
|
|
375
753
|
}
|
|
376
754
|
|
|
377
755
|
// src/commands/save.ts
|
|
378
756
|
import { input as input2 } from "@inquirer/prompts";
|
|
379
|
-
import
|
|
757
|
+
import chalk4 from "chalk";
|
|
380
758
|
import ora3 from "ora";
|
|
381
|
-
async function saveCommand() {
|
|
759
|
+
async function saveCommand(options = {}) {
|
|
382
760
|
const auth = getAuthConfig();
|
|
383
761
|
if (!auth) {
|
|
384
|
-
console.log(
|
|
762
|
+
console.log(chalk4.red("Not logged in. Run `vv login` first."));
|
|
385
763
|
process.exit(1);
|
|
386
764
|
}
|
|
387
765
|
const dir = process.cwd();
|
|
388
766
|
const project = getProjectConfig(dir);
|
|
389
767
|
if (!project) {
|
|
390
|
-
console.log(
|
|
768
|
+
console.log(chalk4.red("Project not initialized. Run `vv init` first."));
|
|
391
769
|
process.exit(1);
|
|
392
770
|
}
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
771
|
+
const worktreeClean = await isWorktreeClean(dir);
|
|
772
|
+
if (worktreeClean) {
|
|
773
|
+
console.log(chalk4.yellow("No changes to save."));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
let title;
|
|
777
|
+
let synopsis;
|
|
778
|
+
if (options.auto) {
|
|
779
|
+
const stats = await diffStats(dir);
|
|
780
|
+
title = `Auto-save: ${stats.filesChanged} ${stats.filesChanged === 1 ? "file" : "files"} changed`;
|
|
781
|
+
synopsis = void 0;
|
|
782
|
+
} else {
|
|
783
|
+
title = await input2({
|
|
784
|
+
message: "What did you change?",
|
|
785
|
+
validate: (v) => v.trim().length > 0 ? true : "A title is required"
|
|
786
|
+
});
|
|
787
|
+
synopsis = await input2({
|
|
788
|
+
message: "Any extra details? (optional)"
|
|
789
|
+
});
|
|
790
|
+
}
|
|
400
791
|
const spinner = ora3("Saving...").start();
|
|
401
792
|
try {
|
|
402
793
|
await stageAll(dir);
|
|
403
794
|
const commitHash = await commit(dir, title);
|
|
404
795
|
const stats = await diffStats(dir);
|
|
796
|
+
const fileStats = await perFileStats(dir);
|
|
797
|
+
const diffText = await generateDiffText(dir);
|
|
405
798
|
const client = new ApiClient(project.apiUrl, auth.token);
|
|
406
799
|
await client.createVersion(project.projectUlid, {
|
|
407
800
|
commit_hash: commitHash,
|
|
408
801
|
title,
|
|
409
802
|
synopsis: synopsis || void 0,
|
|
410
803
|
risk_level: "low",
|
|
411
|
-
type: "named_save",
|
|
804
|
+
type: options.auto ? "auto_save" : "named_save",
|
|
412
805
|
files_changed: stats.filesChanged,
|
|
413
806
|
insertions: stats.insertions,
|
|
414
|
-
deletions: stats.deletions
|
|
807
|
+
deletions: stats.deletions,
|
|
808
|
+
diff_text: diffText,
|
|
809
|
+
per_file_stats: fileStats.length > 0 ? fileStats : void 0
|
|
415
810
|
});
|
|
416
811
|
spinner.succeed(
|
|
417
|
-
`Saved! ${
|
|
812
|
+
`Saved! ${chalk4.bold(`"${title}"`)} ${chalk4.dim(`- ${stats.filesChanged} files changed, +${stats.insertions} -${stats.deletions}`)}`
|
|
418
813
|
);
|
|
419
814
|
} catch (error) {
|
|
420
815
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -423,24 +818,93 @@ async function saveCommand() {
|
|
|
423
818
|
}
|
|
424
819
|
}
|
|
425
820
|
|
|
821
|
+
// src/commands/deploy.ts
|
|
822
|
+
import { input as input3 } from "@inquirer/prompts";
|
|
823
|
+
import chalk5 from "chalk";
|
|
824
|
+
import ora4 from "ora";
|
|
825
|
+
async function deployCommand(options = {}) {
|
|
826
|
+
const auth = getAuthConfig();
|
|
827
|
+
if (!auth) {
|
|
828
|
+
console.log(chalk5.red("Not logged in. Run `vv login` first."));
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
const dir = process.cwd();
|
|
832
|
+
const project = getProjectConfig(dir);
|
|
833
|
+
if (!project) {
|
|
834
|
+
console.log(chalk5.red("Project not initialized. Run `vv init` first."));
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
const worktreeClean = await isWorktreeClean(dir);
|
|
838
|
+
if (worktreeClean) {
|
|
839
|
+
console.log(chalk5.yellow("No changes to deploy."));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
let title;
|
|
843
|
+
let synopsis;
|
|
844
|
+
if (options.auto) {
|
|
845
|
+
const stats = await diffStats(dir);
|
|
846
|
+
title = `Auto-deploy: ${stats.filesChanged} ${stats.filesChanged === 1 ? "file" : "files"} changed`;
|
|
847
|
+
synopsis = void 0;
|
|
848
|
+
} else {
|
|
849
|
+
title = await input3({
|
|
850
|
+
message: "What are you deploying?",
|
|
851
|
+
validate: (v) => v.trim().length > 0 ? true : "A title is required"
|
|
852
|
+
});
|
|
853
|
+
synopsis = await input3({
|
|
854
|
+
message: "Any extra details? (optional)"
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const spinner = ora4("Saving and deploying...").start();
|
|
858
|
+
try {
|
|
859
|
+
await stageAll(dir);
|
|
860
|
+
const commitHash = await commit(dir, title);
|
|
861
|
+
const stats = await diffStats(dir);
|
|
862
|
+
const fileStats = await perFileStats(dir);
|
|
863
|
+
const diffText = await generateDiffText(dir);
|
|
864
|
+
const client = new ApiClient(project.apiUrl, auth.token);
|
|
865
|
+
await client.createVersion(project.projectUlid, {
|
|
866
|
+
commit_hash: commitHash,
|
|
867
|
+
title,
|
|
868
|
+
synopsis: synopsis || void 0,
|
|
869
|
+
risk_level: "low",
|
|
870
|
+
type: "deploy_save",
|
|
871
|
+
files_changed: stats.filesChanged,
|
|
872
|
+
insertions: stats.insertions,
|
|
873
|
+
deletions: stats.deletions,
|
|
874
|
+
diff_text: diffText,
|
|
875
|
+
per_file_stats: fileStats.length > 0 ? fileStats : void 0
|
|
876
|
+
});
|
|
877
|
+
await client.triggerDeploy(project.projectUlid, {
|
|
878
|
+
commit_hash: commitHash
|
|
879
|
+
});
|
|
880
|
+
spinner.succeed(
|
|
881
|
+
`Deployed! ${chalk5.bold(`"${title}"`)} ${chalk5.dim(`- ${stats.filesChanged} files changed, +${stats.insertions} -${stats.deletions}`)}`
|
|
882
|
+
);
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
885
|
+
spinner.fail(`Deploy failed: ${message}`);
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
426
890
|
// src/commands/restore.ts
|
|
427
891
|
import { select as select2 } from "@inquirer/prompts";
|
|
428
|
-
import
|
|
429
|
-
import
|
|
892
|
+
import chalk6 from "chalk";
|
|
893
|
+
import ora5 from "ora";
|
|
430
894
|
async function restoreCommand() {
|
|
431
895
|
const auth = getAuthConfig();
|
|
432
896
|
if (!auth) {
|
|
433
|
-
console.log(
|
|
897
|
+
console.log(chalk6.red("Not logged in. Run `vv login` first."));
|
|
434
898
|
process.exit(1);
|
|
435
899
|
}
|
|
436
900
|
const dir = process.cwd();
|
|
437
901
|
const project = getProjectConfig(dir);
|
|
438
902
|
if (!project) {
|
|
439
|
-
console.log(
|
|
903
|
+
console.log(chalk6.red("Project not initialized. Run `vv init` first."));
|
|
440
904
|
process.exit(1);
|
|
441
905
|
}
|
|
442
906
|
const client = new ApiClient(project.apiUrl, auth.token);
|
|
443
|
-
const fetchSpinner =
|
|
907
|
+
const fetchSpinner = ora5("Fetching versions...").start();
|
|
444
908
|
let versions;
|
|
445
909
|
try {
|
|
446
910
|
const response = await client.listVersions(project.projectUlid);
|
|
@@ -451,19 +915,19 @@ async function restoreCommand() {
|
|
|
451
915
|
}
|
|
452
916
|
fetchSpinner.stop();
|
|
453
917
|
if (versions.length === 0) {
|
|
454
|
-
console.log(
|
|
918
|
+
console.log(chalk6.yellow("No versions found. Save your project first with `vv save`."));
|
|
455
919
|
process.exit(1);
|
|
456
920
|
}
|
|
457
921
|
const selectedHash = await select2({
|
|
458
922
|
message: "Which version do you want to restore?",
|
|
459
923
|
choices: versions.map((v) => ({
|
|
460
|
-
name: `${v.title} ${
|
|
924
|
+
name: `${v.title} ${chalk6.dim(`(${new Date(v.created_at).toLocaleDateString()})`)}`,
|
|
461
925
|
value: v.commit_hash,
|
|
462
926
|
description: v.synopsis || void 0
|
|
463
927
|
}))
|
|
464
928
|
});
|
|
465
929
|
const selectedVersion = versions.find((v) => v.commit_hash === selectedHash);
|
|
466
|
-
const restoreSpinner =
|
|
930
|
+
const restoreSpinner = ora5("Restoring...").start();
|
|
467
931
|
try {
|
|
468
932
|
await stageAll(dir);
|
|
469
933
|
const autoHash = await commit(dir, `Auto-save before restore to: ${selectedVersion.title}`);
|
|
@@ -492,7 +956,7 @@ async function restoreCommand() {
|
|
|
492
956
|
deletions: restoreStats.deletions
|
|
493
957
|
});
|
|
494
958
|
restoreSpinner.succeed(
|
|
495
|
-
`Restored to ${
|
|
959
|
+
`Restored to ${chalk6.bold(`"${selectedVersion.title}"`)} ${chalk6.dim(`- ${restoreStats.filesChanged} files changed`)}`
|
|
496
960
|
);
|
|
497
961
|
} catch (error) {
|
|
498
962
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -503,8 +967,8 @@ async function restoreCommand() {
|
|
|
503
967
|
|
|
504
968
|
// src/commands/files.ts
|
|
505
969
|
import { select as select3 } from "@inquirer/prompts";
|
|
506
|
-
import
|
|
507
|
-
import
|
|
970
|
+
import chalk7 from "chalk";
|
|
971
|
+
import ora6 from "ora";
|
|
508
972
|
function formatSize(bytes) {
|
|
509
973
|
if (bytes < 1024) return `${bytes} B`;
|
|
510
974
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -520,11 +984,11 @@ function printTree(files) {
|
|
|
520
984
|
}
|
|
521
985
|
for (const [dir, dirFiles] of [...tree.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
522
986
|
if (dir !== ".") {
|
|
523
|
-
console.log(
|
|
987
|
+
console.log(chalk7.blue(` ${dir}/`));
|
|
524
988
|
}
|
|
525
989
|
for (const file of dirFiles) {
|
|
526
990
|
const name = file.path.split("/").pop();
|
|
527
|
-
const size =
|
|
991
|
+
const size = chalk7.dim(formatSize(file.size));
|
|
528
992
|
const indent = dir === "." ? " " : " ";
|
|
529
993
|
console.log(`${indent}${name} ${size}`);
|
|
530
994
|
}
|
|
@@ -534,30 +998,30 @@ async function filesCommand(options) {
|
|
|
534
998
|
const dir = process.cwd();
|
|
535
999
|
const project = getProjectConfig(dir);
|
|
536
1000
|
if (!project) {
|
|
537
|
-
console.log(
|
|
1001
|
+
console.log(chalk7.red("Project not initialized. Run `vv init` first."));
|
|
538
1002
|
process.exit(1);
|
|
539
1003
|
}
|
|
540
1004
|
let commitHash;
|
|
541
1005
|
if (options.version) {
|
|
542
1006
|
const auth = getAuthConfig();
|
|
543
1007
|
if (!auth) {
|
|
544
|
-
console.log(
|
|
1008
|
+
console.log(chalk7.red("Not logged in. Run `vv login` first."));
|
|
545
1009
|
process.exit(1);
|
|
546
1010
|
}
|
|
547
1011
|
const client = new ApiClient(project.apiUrl, auth.token);
|
|
548
|
-
const fetchSpinner =
|
|
1012
|
+
const fetchSpinner = ora6("Fetching versions...").start();
|
|
549
1013
|
try {
|
|
550
1014
|
const response = await client.listVersions(project.projectUlid);
|
|
551
1015
|
const versions = response.data;
|
|
552
1016
|
fetchSpinner.stop();
|
|
553
1017
|
if (versions.length === 0) {
|
|
554
|
-
console.log(
|
|
1018
|
+
console.log(chalk7.yellow("No versions found."));
|
|
555
1019
|
process.exit(1);
|
|
556
1020
|
}
|
|
557
1021
|
commitHash = await select3({
|
|
558
1022
|
message: "Which version?",
|
|
559
1023
|
choices: versions.map((v) => ({
|
|
560
|
-
name: `${v.title} ${
|
|
1024
|
+
name: `${v.title} ${chalk7.dim(`(${new Date(v.created_at).toLocaleDateString()})`)}`,
|
|
561
1025
|
value: v.commit_hash
|
|
562
1026
|
}))
|
|
563
1027
|
});
|
|
@@ -566,11 +1030,11 @@ async function filesCommand(options) {
|
|
|
566
1030
|
process.exit(1);
|
|
567
1031
|
}
|
|
568
1032
|
}
|
|
569
|
-
const spinner =
|
|
1033
|
+
const spinner = ora6("Reading files...").start();
|
|
570
1034
|
try {
|
|
571
1035
|
const files = await listFiles(dir, commitHash);
|
|
572
1036
|
spinner.stop();
|
|
573
|
-
console.log(
|
|
1037
|
+
console.log(chalk7.bold(`
|
|
574
1038
|
${files.length} files
|
|
575
1039
|
`));
|
|
576
1040
|
printTree(files);
|
|
@@ -582,11 +1046,50 @@ async function filesCommand(options) {
|
|
|
582
1046
|
}
|
|
583
1047
|
}
|
|
584
1048
|
|
|
1049
|
+
// src/commands/hooks.ts
|
|
1050
|
+
import chalk8 from "chalk";
|
|
1051
|
+
async function hooksCommand(options) {
|
|
1052
|
+
const dir = process.cwd();
|
|
1053
|
+
const project = getProjectConfig(dir);
|
|
1054
|
+
if (!project) {
|
|
1055
|
+
console.log(chalk8.red("Project not initialized. Run `vv init` first."));
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
console.log(chalk8.bold("\nSetting up AI tool hooks...\n"));
|
|
1059
|
+
if (options.all || !options.claude && !options.cursor && !options.opencode) {
|
|
1060
|
+
setupAllHooks(dir);
|
|
1061
|
+
} else {
|
|
1062
|
+
if (options.claude) {
|
|
1063
|
+
const result = setupClaudeCodeHook(dir);
|
|
1064
|
+
console.log(
|
|
1065
|
+
result ? chalk8.green(" \u2713 Claude Code hook added (.claude/settings.json)") : chalk8.dim(" - Claude Code hook already exists")
|
|
1066
|
+
);
|
|
1067
|
+
const mdResult = setupClaudeMd(dir);
|
|
1068
|
+
console.log(
|
|
1069
|
+
mdResult ? chalk8.green(" \u2713 CLAUDE.md updated with Vibeversion commands") : chalk8.dim(" - CLAUDE.md already has Vibeversion section")
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
if (options.cursor) {
|
|
1073
|
+
const result = setupCursorRule(dir);
|
|
1074
|
+
console.log(result ? chalk8.green(" \u2713 Cursor rule added (.cursor/rules/vibeversion.mdc)") : chalk8.dim(" - Cursor rule already exists"));
|
|
1075
|
+
}
|
|
1076
|
+
if (options.opencode) {
|
|
1077
|
+
const result = setupOpenCodePlugin(dir);
|
|
1078
|
+
console.log(
|
|
1079
|
+
result ? chalk8.green(" \u2713 OpenCode plugin added (.opencode/plugins/vibeversion.js)") : chalk8.dim(" - OpenCode plugin already exists")
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
console.log(chalk8.green("\nDone! Auto-save hooks are ready.\n"));
|
|
1084
|
+
}
|
|
1085
|
+
|
|
585
1086
|
// src/index.ts
|
|
586
1087
|
program.name("vv").description("Save and version your projects with Vibeversion").version("0.2.0");
|
|
587
1088
|
program.command("login").description("Log in to your Vibeversion account").action(loginCommand);
|
|
588
1089
|
program.command("init").description("Link this folder to a Vibeversion project").action(initCommand);
|
|
589
|
-
program.command("save").description("Save the current state of your project").action(saveCommand);
|
|
1090
|
+
program.command("save").description("Save the current state of your project").option("--auto", "Non-interactive mode with auto-generated title").action(saveCommand);
|
|
1091
|
+
program.command("deploy").description("Save and deploy your project").option("--auto", "Non-interactive mode with auto-generated title").action(deployCommand);
|
|
590
1092
|
program.command("restore").description("Restore your project to a previous version").action(restoreCommand);
|
|
591
1093
|
program.command("files").description("List files in the current or a specific version").option("--version", "Choose a specific version to view").action(filesCommand);
|
|
1094
|
+
program.command("hooks").description("Set up auto-save hooks for AI tools (Claude Code, Cursor, OpenCode)").option("--claude", "Only set up Claude Code hook").option("--cursor", "Only set up Cursor rule").option("--opencode", "Only set up OpenCode plugin").option("--all", "Set up all hooks").action(hooksCommand);
|
|
592
1095
|
program.parse();
|