@vibeversion/cli 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/dist/index.js +363 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/login.ts
|
|
7
|
+
import { input, password } from "@inquirer/prompts";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
|
|
11
|
+
// src/lib/api.ts
|
|
12
|
+
var ApiClient = class {
|
|
13
|
+
constructor(baseUrl, token) {
|
|
14
|
+
this.baseUrl = baseUrl;
|
|
15
|
+
this.token = token;
|
|
16
|
+
}
|
|
17
|
+
async request(method, path3, body) {
|
|
18
|
+
const url = `${this.baseUrl}${path3}`;
|
|
19
|
+
const headers = {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
Accept: "application/json"
|
|
22
|
+
};
|
|
23
|
+
if (this.token) {
|
|
24
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
25
|
+
}
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method,
|
|
28
|
+
headers,
|
|
29
|
+
body: body ? JSON.stringify(body) : void 0
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const text = await response.text();
|
|
33
|
+
let message;
|
|
34
|
+
try {
|
|
35
|
+
const json = JSON.parse(text);
|
|
36
|
+
message = json.message || json.error || text;
|
|
37
|
+
} catch {
|
|
38
|
+
message = text;
|
|
39
|
+
}
|
|
40
|
+
throw new ApiError(response.status, message);
|
|
41
|
+
}
|
|
42
|
+
return await response.json();
|
|
43
|
+
}
|
|
44
|
+
async createToken(email, password2, deviceName = "vv-cli") {
|
|
45
|
+
return this.request("POST", "/api/auth/token", {
|
|
46
|
+
email,
|
|
47
|
+
password: password2,
|
|
48
|
+
device_name: deviceName
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async listProjects() {
|
|
52
|
+
return this.request("GET", "/api/projects");
|
|
53
|
+
}
|
|
54
|
+
async createVersion(projectUlid, data) {
|
|
55
|
+
return this.request("POST", `/api/projects/${projectUlid}/versions`, data);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var ApiError = class extends Error {
|
|
59
|
+
constructor(status, message) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.status = status;
|
|
62
|
+
this.name = "ApiError";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/lib/config.ts
|
|
67
|
+
import fs from "fs";
|
|
68
|
+
import path from "path";
|
|
69
|
+
import os from "os";
|
|
70
|
+
var CONFIG_DIR = path.join(os.homedir(), ".vibeversion");
|
|
71
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
72
|
+
function getAuthConfig() {
|
|
73
|
+
try {
|
|
74
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
75
|
+
return JSON.parse(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function saveAuthConfig(config) {
|
|
81
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
82
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
83
|
+
mode: 384
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/commands/login.ts
|
|
88
|
+
var DEFAULT_API_URL = "https://vibeversion.com";
|
|
89
|
+
async function loginCommand() {
|
|
90
|
+
console.log(chalk.bold("\nLog in to Vibeversion\n"));
|
|
91
|
+
const apiUrl = process.env.VV_API_URL || await input({
|
|
92
|
+
message: "API URL",
|
|
93
|
+
default: DEFAULT_API_URL
|
|
94
|
+
});
|
|
95
|
+
const email = await input({
|
|
96
|
+
message: "Email",
|
|
97
|
+
validate: (v) => v.includes("@") ? true : "Enter a valid email"
|
|
98
|
+
});
|
|
99
|
+
const pw = await password({
|
|
100
|
+
message: "Password",
|
|
101
|
+
mask: "*"
|
|
102
|
+
});
|
|
103
|
+
const spinner = ora("Logging in...").start();
|
|
104
|
+
try {
|
|
105
|
+
const client = new ApiClient(apiUrl);
|
|
106
|
+
const { token } = await client.createToken(email, pw);
|
|
107
|
+
saveAuthConfig({ token, apiUrl, email });
|
|
108
|
+
spinner.succeed(`Logged in as ${chalk.cyan(email)}`);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error instanceof ApiError && error.status === 422) {
|
|
111
|
+
spinner.fail("Invalid email or password.");
|
|
112
|
+
} else if (error instanceof ApiError) {
|
|
113
|
+
spinner.fail(`Login failed: ${error.message}`);
|
|
114
|
+
} else {
|
|
115
|
+
spinner.fail("Could not connect to server. Check the API URL and try again.");
|
|
116
|
+
}
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/commands/init.ts
|
|
122
|
+
import { select } from "@inquirer/prompts";
|
|
123
|
+
import chalk2 from "chalk";
|
|
124
|
+
import ora2 from "ora";
|
|
125
|
+
|
|
126
|
+
// src/lib/project.ts
|
|
127
|
+
import fs2 from "fs";
|
|
128
|
+
import path2 from "path";
|
|
129
|
+
var PROJECT_DIR = ".vibeversion";
|
|
130
|
+
var PROJECT_FILE = "config.json";
|
|
131
|
+
function getProjectConfigPath(dir) {
|
|
132
|
+
return path2.join(dir, PROJECT_DIR, PROJECT_FILE);
|
|
133
|
+
}
|
|
134
|
+
function getProjectConfig(dir = process.cwd()) {
|
|
135
|
+
try {
|
|
136
|
+
const raw = fs2.readFileSync(getProjectConfigPath(dir), "utf-8");
|
|
137
|
+
return JSON.parse(raw);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function saveProjectConfig(config, dir = process.cwd()) {
|
|
143
|
+
const projectDir = path2.join(dir, PROJECT_DIR);
|
|
144
|
+
fs2.mkdirSync(projectDir, { recursive: true });
|
|
145
|
+
fs2.writeFileSync(path2.join(projectDir, PROJECT_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
146
|
+
}
|
|
147
|
+
function ensureGitignore(dir = process.cwd()) {
|
|
148
|
+
const gitignorePath = path2.join(dir, ".gitignore");
|
|
149
|
+
const entry = ".vibeversion/";
|
|
150
|
+
try {
|
|
151
|
+
const content = fs2.readFileSync(gitignorePath, "utf-8");
|
|
152
|
+
if (content.includes(entry)) return;
|
|
153
|
+
fs2.writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
154
|
+
} catch {
|
|
155
|
+
fs2.writeFileSync(gitignorePath, entry + "\n");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/lib/git.ts
|
|
160
|
+
import fs3 from "fs";
|
|
161
|
+
import git from "isomorphic-git";
|
|
162
|
+
var AUTHOR = { name: "Vibeversion", email: "cli@vibeversion.com" };
|
|
163
|
+
async function ensureRepo(dir) {
|
|
164
|
+
try {
|
|
165
|
+
await git.findRoot({ fs: fs3, filepath: dir });
|
|
166
|
+
} catch {
|
|
167
|
+
await git.init({ fs: fs3, dir });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function isFirstCommit(dir) {
|
|
171
|
+
try {
|
|
172
|
+
await git.resolveRef({ fs: fs3, dir, ref: "HEAD" });
|
|
173
|
+
return false;
|
|
174
|
+
} catch {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function stageAll(dir) {
|
|
179
|
+
const statusMatrix = await git.statusMatrix({ fs: fs3, dir });
|
|
180
|
+
for (const [filepath, head, workdir, stage] of statusMatrix) {
|
|
181
|
+
if (workdir === 0) {
|
|
182
|
+
await git.remove({ fs: fs3, dir, filepath });
|
|
183
|
+
} else if (head !== workdir || head !== stage) {
|
|
184
|
+
await git.add({ fs: fs3, dir, filepath });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function commit(dir, message) {
|
|
189
|
+
return git.commit({
|
|
190
|
+
fs: fs3,
|
|
191
|
+
dir,
|
|
192
|
+
message,
|
|
193
|
+
author: AUTHOR
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function diffStats(dir) {
|
|
197
|
+
const first = await isFirstCommit(dir);
|
|
198
|
+
if (first) {
|
|
199
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
200
|
+
}
|
|
201
|
+
const headOid = await git.resolveRef({ fs: fs3, dir, ref: "HEAD" });
|
|
202
|
+
const headCommit = await git.readCommit({ fs: fs3, dir, oid: headOid });
|
|
203
|
+
const currentTree = headCommit.commit.tree;
|
|
204
|
+
let parentTree = null;
|
|
205
|
+
if (headCommit.commit.parent.length > 0) {
|
|
206
|
+
const parentCommit = await git.readCommit({
|
|
207
|
+
fs: fs3,
|
|
208
|
+
dir,
|
|
209
|
+
oid: headCommit.commit.parent[0]
|
|
210
|
+
});
|
|
211
|
+
parentTree = parentCommit.commit.tree;
|
|
212
|
+
}
|
|
213
|
+
const currentFiles = await walkTree(dir, currentTree);
|
|
214
|
+
const parentFiles = parentTree ? await walkTree(dir, parentTree) : /* @__PURE__ */ new Map();
|
|
215
|
+
let filesChanged = 0;
|
|
216
|
+
let insertions = 0;
|
|
217
|
+
let deletions = 0;
|
|
218
|
+
for (const [filepath, currentOid] of currentFiles) {
|
|
219
|
+
const parentOid = parentFiles.get(filepath);
|
|
220
|
+
if (!parentOid) {
|
|
221
|
+
filesChanged++;
|
|
222
|
+
const content = await readBlob(dir, currentOid);
|
|
223
|
+
insertions += countLines(content);
|
|
224
|
+
} else if (parentOid !== currentOid) {
|
|
225
|
+
filesChanged++;
|
|
226
|
+
const oldContent = await readBlob(dir, parentOid);
|
|
227
|
+
const newContent = await readBlob(dir, currentOid);
|
|
228
|
+
const oldLines = countLines(oldContent);
|
|
229
|
+
const newLines = countLines(newContent);
|
|
230
|
+
if (newLines > oldLines) {
|
|
231
|
+
insertions += newLines - oldLines;
|
|
232
|
+
} else {
|
|
233
|
+
deletions += oldLines - newLines;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const [filepath] of parentFiles) {
|
|
238
|
+
if (!currentFiles.has(filepath)) {
|
|
239
|
+
filesChanged++;
|
|
240
|
+
const content = await readBlob(dir, parentFiles.get(filepath));
|
|
241
|
+
deletions += countLines(content);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { filesChanged, insertions, deletions };
|
|
245
|
+
}
|
|
246
|
+
async function walkTree(dir, treeOid, prefix = "") {
|
|
247
|
+
const files = /* @__PURE__ */ new Map();
|
|
248
|
+
const { tree } = await git.readTree({ fs: fs3, dir, oid: treeOid });
|
|
249
|
+
for (const entry of tree) {
|
|
250
|
+
const filepath = prefix ? `${prefix}/${entry.path}` : entry.path;
|
|
251
|
+
if (entry.type === "blob") {
|
|
252
|
+
files.set(filepath, entry.oid);
|
|
253
|
+
} else if (entry.type === "tree") {
|
|
254
|
+
const subtree = await walkTree(dir, entry.oid, filepath);
|
|
255
|
+
for (const [k, v] of subtree) {
|
|
256
|
+
files.set(k, v);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return files;
|
|
261
|
+
}
|
|
262
|
+
async function readBlob(dir, oid) {
|
|
263
|
+
const { blob } = await git.readBlob({ fs: fs3, dir, oid });
|
|
264
|
+
return new TextDecoder().decode(blob);
|
|
265
|
+
}
|
|
266
|
+
function countLines(content) {
|
|
267
|
+
if (content.length === 0) return 0;
|
|
268
|
+
return content.split("\n").length;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/commands/init.ts
|
|
272
|
+
async function initCommand() {
|
|
273
|
+
const auth = getAuthConfig();
|
|
274
|
+
if (!auth) {
|
|
275
|
+
console.log(chalk2.red("Not logged in. Run `vv login` first."));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
const spinner = ora2("Fetching your projects...").start();
|
|
279
|
+
const client = new ApiClient(auth.apiUrl, auth.token);
|
|
280
|
+
let projects;
|
|
281
|
+
try {
|
|
282
|
+
const response = await client.listProjects();
|
|
283
|
+
projects = response.data;
|
|
284
|
+
} catch {
|
|
285
|
+
spinner.fail("Could not fetch projects. Try `vv login` again.");
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
spinner.stop();
|
|
289
|
+
if (projects.length === 0) {
|
|
290
|
+
console.log(chalk2.yellow("No projects found. Create one on your Vibeversion dashboard first."));
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
const projectUlid = await select({
|
|
294
|
+
message: "Which project is this folder for?",
|
|
295
|
+
choices: projects.map((p) => ({
|
|
296
|
+
name: `${p.name} (${p.versions_count} saves)`,
|
|
297
|
+
value: p.ulid
|
|
298
|
+
}))
|
|
299
|
+
});
|
|
300
|
+
const project = projects.find((p) => p.ulid === projectUlid);
|
|
301
|
+
const dir = process.cwd();
|
|
302
|
+
const setupSpinner = ora2("Setting up...").start();
|
|
303
|
+
saveProjectConfig({ projectUlid, apiUrl: auth.apiUrl, projectName: project.name }, dir);
|
|
304
|
+
ensureGitignore(dir);
|
|
305
|
+
await ensureRepo(dir);
|
|
306
|
+
setupSpinner.succeed(`Linked to ${chalk2.cyan(project.name)}. Run ${chalk2.bold("vv save")} to save your first version.`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/commands/save.ts
|
|
310
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
311
|
+
import chalk3 from "chalk";
|
|
312
|
+
import ora3 from "ora";
|
|
313
|
+
async function saveCommand() {
|
|
314
|
+
const auth = getAuthConfig();
|
|
315
|
+
if (!auth) {
|
|
316
|
+
console.log(chalk3.red("Not logged in. Run `vv login` first."));
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
const dir = process.cwd();
|
|
320
|
+
const project = getProjectConfig(dir);
|
|
321
|
+
if (!project) {
|
|
322
|
+
console.log(chalk3.red("Project not initialized. Run `vv init` first."));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const title = await input2({
|
|
326
|
+
message: "What did you change?",
|
|
327
|
+
validate: (v) => v.trim().length > 0 ? true : "A title is required"
|
|
328
|
+
});
|
|
329
|
+
const synopsis = await input2({
|
|
330
|
+
message: "Any extra details? (optional)"
|
|
331
|
+
});
|
|
332
|
+
const spinner = ora3("Saving...").start();
|
|
333
|
+
try {
|
|
334
|
+
await stageAll(dir);
|
|
335
|
+
const commitHash = await commit(dir, title);
|
|
336
|
+
const stats = await diffStats(dir);
|
|
337
|
+
const client = new ApiClient(project.apiUrl, auth.token);
|
|
338
|
+
await client.createVersion(project.projectUlid, {
|
|
339
|
+
commit_hash: commitHash,
|
|
340
|
+
title,
|
|
341
|
+
synopsis: synopsis || void 0,
|
|
342
|
+
risk_level: "low",
|
|
343
|
+
type: "named_save",
|
|
344
|
+
files_changed: stats.filesChanged,
|
|
345
|
+
insertions: stats.insertions,
|
|
346
|
+
deletions: stats.deletions
|
|
347
|
+
});
|
|
348
|
+
spinner.succeed(
|
|
349
|
+
`Saved! ${chalk3.bold(`"${title}"`)} ${chalk3.dim(`- ${stats.filesChanged} files changed, +${stats.insertions} -${stats.deletions}`)}`
|
|
350
|
+
);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
353
|
+
spinner.fail(`Save failed: ${message}`);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/index.ts
|
|
359
|
+
program.name("vv").description("Save and version your projects with Vibeversion").version("0.1.0");
|
|
360
|
+
program.command("login").description("Log in to your Vibeversion account").action(loginCommand);
|
|
361
|
+
program.command("init").description("Link this folder to a Vibeversion project").action(initCommand);
|
|
362
|
+
program.command("save").description("Save the current state of your project").action(saveCommand);
|
|
363
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibeversion/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Save and version your projects with Vibeversion",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vv": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"vibeversion",
|
|
18
|
+
"version-control",
|
|
19
|
+
"save",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@inquirer/prompts": "^7.0.0",
|
|
25
|
+
"chalk": "^5.4.0",
|
|
26
|
+
"commander": "^13.0.0",
|
|
27
|
+
"isomorphic-git": "^1.27.0",
|
|
28
|
+
"ora": "^8.2.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"tsup": "^8.0.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
}
|
|
35
|
+
}
|