@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.
Files changed (2) hide show
  1. package/dist/index.js +555 -52
  2. 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, path4, body) {
18
- const url = `${this.baseUrl}${path4}`;
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(), ".vibeversion");
76
+ var CONFIG_DIR = path.join(os.homedir(), ".vv");
74
77
  var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
75
- function getAuthConfig() {
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(CONFIG_FILE, "utf-8");
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 chalk2 from "chalk";
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 = ".vibeversion";
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 getProjectConfig(dir = process.cwd()) {
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(getProjectConfigPath(dir), "utf-8");
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 = ".vibeversion/";
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)) return;
156
- fs2.writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
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 !== ".vibeversion") {
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(chalk2.red("Not logged in. Run `vv login` first."));
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(chalk2.yellow("No projects found. Create one on your Vibeversion dashboard first."));
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 ${chalk2.cyan(project.name)}. Run ${chalk2.bold("vv save")} to save your first version.`);
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 chalk3 from "chalk";
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(chalk3.red("Not logged in. Run `vv login` first."));
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(chalk3.red("Project not initialized. Run `vv init` first."));
768
+ console.log(chalk4.red("Project not initialized. Run `vv init` first."));
391
769
  process.exit(1);
392
770
  }
393
- const title = await input2({
394
- message: "What did you change?",
395
- validate: (v) => v.trim().length > 0 ? true : "A title is required"
396
- });
397
- const synopsis = await input2({
398
- message: "Any extra details? (optional)"
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! ${chalk3.bold(`"${title}"`)} ${chalk3.dim(`- ${stats.filesChanged} files changed, +${stats.insertions} -${stats.deletions}`)}`
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 chalk4 from "chalk";
429
- import ora4 from "ora";
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(chalk4.red("Not logged in. Run `vv login` first."));
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(chalk4.red("Project not initialized. Run `vv init` first."));
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 = ora4("Fetching versions...").start();
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(chalk4.yellow("No versions found. Save your project first with `vv save`."));
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} ${chalk4.dim(`(${new Date(v.created_at).toLocaleDateString()})`)}`,
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 = ora4("Restoring...").start();
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 ${chalk4.bold(`"${selectedVersion.title}"`)} ${chalk4.dim(`- ${restoreStats.filesChanged} files changed`)}`
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 chalk5 from "chalk";
507
- import ora5 from "ora";
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(chalk5.blue(` ${dir}/`));
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 = chalk5.dim(formatSize(file.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(chalk5.red("Project not initialized. Run `vv init` first."));
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(chalk5.red("Not logged in. Run `vv login` first."));
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 = ora5("Fetching versions...").start();
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(chalk5.yellow("No versions found."));
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} ${chalk5.dim(`(${new Date(v.created_at).toLocaleDateString()})`)}`,
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 = ora5("Reading files...").start();
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(chalk5.bold(`
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeversion/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Save and version your projects with Vibeversion",
5
5
  "type": "module",
6
6
  "bin": {