@changesmith/cli 1.4.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/README.md +206 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1258 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command8 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
|
|
11
|
+
// src/config.ts
|
|
12
|
+
import Conf from "conf";
|
|
13
|
+
import { readFile, writeFile, access } from "fs/promises";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
var CONFIG_FILE = ".changesmith.json";
|
|
16
|
+
var userConfig = new Conf({
|
|
17
|
+
projectName: "changesmith",
|
|
18
|
+
schema: {
|
|
19
|
+
token: { type: "string" },
|
|
20
|
+
userId: { type: "string" },
|
|
21
|
+
apiUrl: { type: "string" }
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
function getApiUrl() {
|
|
25
|
+
return getEnvApiUrl() ?? userConfig.get("apiUrl") ?? "https://api.changesmith.dev";
|
|
26
|
+
}
|
|
27
|
+
function setApiUrl(url) {
|
|
28
|
+
userConfig.set("apiUrl", url);
|
|
29
|
+
}
|
|
30
|
+
var MIN_TOKEN_LENGTH = 50;
|
|
31
|
+
var MAX_TOKEN_LENGTH = 4096;
|
|
32
|
+
function getEnvToken() {
|
|
33
|
+
const raw = process.env.CHANGESMITH_TOKEN?.trim();
|
|
34
|
+
return raw || void 0;
|
|
35
|
+
}
|
|
36
|
+
function getEnvApiUrl() {
|
|
37
|
+
const raw = process.env.CHANGESMITH_API_URL?.trim();
|
|
38
|
+
if (!raw) return void 0;
|
|
39
|
+
if (!envApiUrlWarned) {
|
|
40
|
+
try {
|
|
41
|
+
new URL(raw);
|
|
42
|
+
} catch {
|
|
43
|
+
envApiUrlWarned = true;
|
|
44
|
+
console.error(
|
|
45
|
+
`Warning: CHANGESMITH_API_URL is not a valid URL ("${raw}") \u2014 API calls may fail.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return raw;
|
|
50
|
+
}
|
|
51
|
+
var envTokenWarned = false;
|
|
52
|
+
var envApiUrlWarned = false;
|
|
53
|
+
function warnIfEnvTokenSuspicious() {
|
|
54
|
+
const envToken = getEnvToken();
|
|
55
|
+
if (!envToken || envTokenWarned) return;
|
|
56
|
+
if (envToken.length < MIN_TOKEN_LENGTH) {
|
|
57
|
+
envTokenWarned = true;
|
|
58
|
+
console.error("Warning: CHANGESMITH_TOKEN looks too short \u2014 API calls may fail.");
|
|
59
|
+
} else if (envToken.length > MAX_TOKEN_LENGTH) {
|
|
60
|
+
envTokenWarned = true;
|
|
61
|
+
console.error("Warning: CHANGESMITH_TOKEN is unusually long \u2014 this may not be a valid token.");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function getAuthToken() {
|
|
65
|
+
return getEnvToken() ?? userConfig.get("token");
|
|
66
|
+
}
|
|
67
|
+
function setAuthToken(token) {
|
|
68
|
+
userConfig.set("token", token);
|
|
69
|
+
}
|
|
70
|
+
function getUserId() {
|
|
71
|
+
return userConfig.get("userId");
|
|
72
|
+
}
|
|
73
|
+
function setUserId(id) {
|
|
74
|
+
userConfig.set("userId", id);
|
|
75
|
+
}
|
|
76
|
+
function clearAuth() {
|
|
77
|
+
userConfig.delete("token");
|
|
78
|
+
userConfig.delete("userId");
|
|
79
|
+
}
|
|
80
|
+
function isLoggedIn() {
|
|
81
|
+
return !!getAuthToken();
|
|
82
|
+
}
|
|
83
|
+
function isEnvAuth() {
|
|
84
|
+
return !!getEnvToken();
|
|
85
|
+
}
|
|
86
|
+
function isEnvApiUrl() {
|
|
87
|
+
return !!getEnvApiUrl();
|
|
88
|
+
}
|
|
89
|
+
function hasStoredToken() {
|
|
90
|
+
return !!userConfig.get("token");
|
|
91
|
+
}
|
|
92
|
+
function getProjectConfigPath(cwd = process.cwd()) {
|
|
93
|
+
return join(cwd, CONFIG_FILE);
|
|
94
|
+
}
|
|
95
|
+
async function projectConfigExists(cwd = process.cwd()) {
|
|
96
|
+
try {
|
|
97
|
+
await access(getProjectConfigPath(cwd));
|
|
98
|
+
return true;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function readProjectConfig(cwd = process.cwd()) {
|
|
104
|
+
try {
|
|
105
|
+
const content = await readFile(getProjectConfigPath(cwd), "utf-8");
|
|
106
|
+
return JSON.parse(content);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function writeProjectConfig(config, cwd = process.cwd()) {
|
|
112
|
+
await writeFile(getProjectConfigPath(cwd), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
113
|
+
}
|
|
114
|
+
function getDefaultProjectConfig() {
|
|
115
|
+
return {
|
|
116
|
+
version: 1,
|
|
117
|
+
excludeTypes: ["chore", "style", "ci"]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/git.ts
|
|
122
|
+
import { exec } from "child_process";
|
|
123
|
+
import { promisify } from "util";
|
|
124
|
+
import { access as access2 } from "fs/promises";
|
|
125
|
+
import { join as join2 } from "path";
|
|
126
|
+
var execAsync = promisify(exec);
|
|
127
|
+
var SAFE_GIT_REF = /^[\w./@^~{}\-]+$/;
|
|
128
|
+
function assertSafeRef(ref) {
|
|
129
|
+
if (!SAFE_GIT_REF.test(ref)) {
|
|
130
|
+
throw new Error(`Invalid git ref: "${ref}"`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function git(args, cwd = process.cwd()) {
|
|
134
|
+
const { stdout } = await execAsync(`git ${args}`, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
135
|
+
return stdout.trim();
|
|
136
|
+
}
|
|
137
|
+
async function isGitRepo(cwd = process.cwd()) {
|
|
138
|
+
try {
|
|
139
|
+
await access2(join2(cwd, ".git"));
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
try {
|
|
143
|
+
await git("rev-parse --git-dir", cwd);
|
|
144
|
+
return true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function getDefaultBranch(cwd = process.cwd()) {
|
|
151
|
+
try {
|
|
152
|
+
const remoteBranch = await git("symbolic-ref refs/remotes/origin/HEAD --short", cwd);
|
|
153
|
+
return remoteBranch.replace("origin/", "");
|
|
154
|
+
} catch {
|
|
155
|
+
try {
|
|
156
|
+
await git("show-ref --verify refs/heads/main", cwd);
|
|
157
|
+
return "main";
|
|
158
|
+
} catch {
|
|
159
|
+
return "master";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function getRemoteUrl(cwd = process.cwd()) {
|
|
164
|
+
try {
|
|
165
|
+
const url = await git("remote get-url origin", cwd);
|
|
166
|
+
return url;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function parseRepoFromUrl(url) {
|
|
172
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
173
|
+
if (sshMatch) {
|
|
174
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
175
|
+
}
|
|
176
|
+
const httpsMatch = url.match(/https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
177
|
+
if (httpsMatch) {
|
|
178
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
async function getTags(cwd = process.cwd()) {
|
|
183
|
+
try {
|
|
184
|
+
const output = await git("tag --sort=-version:refname", cwd);
|
|
185
|
+
if (!output) return [];
|
|
186
|
+
return output.split("\n").filter(Boolean);
|
|
187
|
+
} catch {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function getLatestTag(cwd = process.cwd()) {
|
|
192
|
+
try {
|
|
193
|
+
return await git("describe --tags --abbrev=0", cwd);
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function getPreviousTag(tag, cwd = process.cwd()) {
|
|
199
|
+
assertSafeRef(tag);
|
|
200
|
+
try {
|
|
201
|
+
return await git(`describe --tags --abbrev=0 ${tag}^`, cwd);
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function getGitRoot(cwd = process.cwd()) {
|
|
207
|
+
return git("rev-parse --show-toplevel", cwd);
|
|
208
|
+
}
|
|
209
|
+
async function isShallowClone(cwd = process.cwd()) {
|
|
210
|
+
try {
|
|
211
|
+
const result = await git("rev-parse --is-shallow-repository", cwd);
|
|
212
|
+
return result === "true";
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function getCommitsBetween(from, to, cwd = process.cwd()) {
|
|
218
|
+
assertSafeRef(from);
|
|
219
|
+
assertSafeRef(to);
|
|
220
|
+
const format = "%x00%x01%H%x00%an%x00%aI%x00%B";
|
|
221
|
+
const output = await git(`log ${from}..${to} --format="${format}" --no-merges`, cwd);
|
|
222
|
+
if (!output) return [];
|
|
223
|
+
const commits = [];
|
|
224
|
+
const rawCommits = output.split("\0").filter(Boolean);
|
|
225
|
+
for (const raw of rawCommits) {
|
|
226
|
+
const parts = raw.split("\0");
|
|
227
|
+
if (parts.length < 4) continue;
|
|
228
|
+
const sha = parts[0].trim();
|
|
229
|
+
const author = parts[1].trim();
|
|
230
|
+
const date = parts[2].trim();
|
|
231
|
+
const message = parts.slice(3).join("\0").trim();
|
|
232
|
+
if (sha) {
|
|
233
|
+
commits.push({ sha, message, author, date });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return commits;
|
|
237
|
+
}
|
|
238
|
+
async function getCommitDiff(sha, cwd = process.cwd()) {
|
|
239
|
+
assertSafeRef(sha);
|
|
240
|
+
const output = await git(`show ${sha} --numstat -p --format=""`, cwd);
|
|
241
|
+
const patchStart = output.search(/^diff --git /m);
|
|
242
|
+
const numstatSection = patchStart !== -1 ? output.slice(0, patchStart) : output;
|
|
243
|
+
const patchSection = patchStart !== -1 ? output.slice(patchStart) : "";
|
|
244
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
245
|
+
for (const line of numstatSection.split("\n").filter(Boolean)) {
|
|
246
|
+
const match2 = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
|
|
247
|
+
if (match2) {
|
|
248
|
+
const additions = match2[1] === "-" ? 0 : parseInt(match2[1], 10);
|
|
249
|
+
const deletions = match2[2] === "-" ? 0 : parseInt(match2[2], 10);
|
|
250
|
+
const filename = match2[3];
|
|
251
|
+
if (match2[1] !== "-" || match2[2] !== "-") {
|
|
252
|
+
fileStats.set(filename, { additions, deletions });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const files = [];
|
|
257
|
+
const filePatchRegex = /^diff --git a\/(.+?) b\/(.+?)$/gm;
|
|
258
|
+
const patchParts = [];
|
|
259
|
+
let match;
|
|
260
|
+
while ((match = filePatchRegex.exec(patchSection)) !== null) {
|
|
261
|
+
patchParts.push({ filename: match[2], startIndex: match.index });
|
|
262
|
+
}
|
|
263
|
+
for (let i = 0; i < patchParts.length; i++) {
|
|
264
|
+
const part = patchParts[i];
|
|
265
|
+
const endIndex = i + 1 < patchParts.length ? patchParts[i + 1].startIndex : patchSection.length;
|
|
266
|
+
const filePatch = patchSection.substring(part.startIndex, endIndex).trim();
|
|
267
|
+
const stats = fileStats.get(part.filename);
|
|
268
|
+
if (!stats) continue;
|
|
269
|
+
let status = "modified";
|
|
270
|
+
if (filePatch.includes("new file mode")) {
|
|
271
|
+
status = "added";
|
|
272
|
+
} else if (filePatch.includes("deleted file mode")) {
|
|
273
|
+
status = "deleted";
|
|
274
|
+
} else if (filePatch.includes("rename from")) {
|
|
275
|
+
status = "renamed";
|
|
276
|
+
}
|
|
277
|
+
files.push({
|
|
278
|
+
filename: part.filename,
|
|
279
|
+
status,
|
|
280
|
+
additions: stats.additions,
|
|
281
|
+
deletions: stats.deletions,
|
|
282
|
+
patch: filePatch
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return { sha, files };
|
|
286
|
+
}
|
|
287
|
+
async function deriveRepoName(cwd = process.cwd()) {
|
|
288
|
+
const remoteUrl = await getRemoteUrl(cwd);
|
|
289
|
+
if (remoteUrl) {
|
|
290
|
+
const ghInfo = parseRepoFromUrl(remoteUrl);
|
|
291
|
+
if (ghInfo) {
|
|
292
|
+
return `${ghInfo.owner}/${ghInfo.repo}`;
|
|
293
|
+
}
|
|
294
|
+
const genericMatch = remoteUrl.match(/[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
295
|
+
if (genericMatch) {
|
|
296
|
+
return genericMatch[1];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const { basename } = await import("path");
|
|
300
|
+
return basename(cwd);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/commands/init.ts
|
|
304
|
+
var initCommand = new Command("init").description("Initialize Changesmith in the current repository").option("-f, --force", "Overwrite existing config").action(async (options) => {
|
|
305
|
+
const spinner = ora("Detecting git repository...").start();
|
|
306
|
+
if (!await isGitRepo()) {
|
|
307
|
+
spinner.fail("Not a git repository");
|
|
308
|
+
console.log(chalk.gray("Run this command from the root of a git repository."));
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
spinner.succeed("Git repository detected");
|
|
312
|
+
if (await projectConfigExists()) {
|
|
313
|
+
if (!options.force) {
|
|
314
|
+
console.log(chalk.yellow("Changesmith is already initialized in this repository."));
|
|
315
|
+
console.log(chalk.gray(`Config file: ${getProjectConfigPath()}`));
|
|
316
|
+
console.log(chalk.gray("Use --force to overwrite the existing config."));
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const defaultBranch = await getDefaultBranch();
|
|
321
|
+
const remoteUrl = await getRemoteUrl();
|
|
322
|
+
const repoInfo = remoteUrl ? parseRepoFromUrl(remoteUrl) : null;
|
|
323
|
+
const config = {
|
|
324
|
+
...getDefaultProjectConfig(),
|
|
325
|
+
defaultBranch
|
|
326
|
+
};
|
|
327
|
+
const writeSpinner = ora("Creating config file...").start();
|
|
328
|
+
await writeProjectConfig(config);
|
|
329
|
+
writeSpinner.succeed("Created .changesmith.json");
|
|
330
|
+
console.log("");
|
|
331
|
+
console.log(chalk.green("Changesmith initialized successfully!"));
|
|
332
|
+
console.log("");
|
|
333
|
+
if (repoInfo) {
|
|
334
|
+
console.log(chalk.gray(`Repository: ${repoInfo.owner}/${repoInfo.repo}`));
|
|
335
|
+
}
|
|
336
|
+
console.log(chalk.gray(`Default branch: ${defaultBranch}`));
|
|
337
|
+
console.log("");
|
|
338
|
+
console.log("Next steps:");
|
|
339
|
+
console.log(chalk.cyan(" 1. ") + "Log in to Changesmith:");
|
|
340
|
+
console.log(chalk.gray(" changesmith login"));
|
|
341
|
+
console.log("");
|
|
342
|
+
console.log(chalk.cyan(" 2. ") + "Generate a changelog:");
|
|
343
|
+
console.log(chalk.gray(" changesmith generate v1.0.0"));
|
|
344
|
+
console.log("");
|
|
345
|
+
console.log(chalk.gray(`Edit ${getProjectConfigPath()} to customize changelog generation.`));
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// src/commands/generate.ts
|
|
349
|
+
import { Command as Command2 } from "commander";
|
|
350
|
+
import chalk2 from "chalk";
|
|
351
|
+
import ora2 from "ora";
|
|
352
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
353
|
+
|
|
354
|
+
// src/api.ts
|
|
355
|
+
var ApiError = class extends Error {
|
|
356
|
+
constructor(status, message, code) {
|
|
357
|
+
super(message);
|
|
358
|
+
this.status = status;
|
|
359
|
+
this.name = "ApiError";
|
|
360
|
+
this.code = code;
|
|
361
|
+
}
|
|
362
|
+
/** Structured error code from the API response JSON (e.g. 'expired_token', 'authorization_pending') */
|
|
363
|
+
code;
|
|
364
|
+
};
|
|
365
|
+
async function request(path, options = {}, tokenOverride) {
|
|
366
|
+
const apiUrl = getApiUrl();
|
|
367
|
+
warnIfEnvTokenSuspicious();
|
|
368
|
+
const token = tokenOverride ?? getAuthToken();
|
|
369
|
+
const headers = {
|
|
370
|
+
...options.headers || {}
|
|
371
|
+
};
|
|
372
|
+
if (token) {
|
|
373
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
374
|
+
}
|
|
375
|
+
if (options.body && !headers["Content-Type"]) {
|
|
376
|
+
headers["Content-Type"] = "application/json";
|
|
377
|
+
}
|
|
378
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
379
|
+
...options,
|
|
380
|
+
headers
|
|
381
|
+
});
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
switch (response.status) {
|
|
384
|
+
case 401: {
|
|
385
|
+
let message;
|
|
386
|
+
if (tokenOverride) {
|
|
387
|
+
message = "Not authenticated. The provided token may be invalid or expired.";
|
|
388
|
+
} else if (isEnvAuth()) {
|
|
389
|
+
message = "Not authenticated. CHANGESMITH_TOKEN may be invalid or expired.";
|
|
390
|
+
} else {
|
|
391
|
+
message = "Not authenticated. Run `changesmith login` to log in.";
|
|
392
|
+
}
|
|
393
|
+
throw new ApiError(401, message);
|
|
394
|
+
}
|
|
395
|
+
case 402:
|
|
396
|
+
throw new ApiError(402, "Usage limit reached. Upgrade your plan at changesmith.dev.");
|
|
397
|
+
case 403: {
|
|
398
|
+
let msg = "CLI access requires a Business plan. Upgrade at changesmith.dev/dashboard/settings";
|
|
399
|
+
try {
|
|
400
|
+
const json = await response.json();
|
|
401
|
+
if (typeof json.message === "string" && json.message.length > 0) {
|
|
402
|
+
msg = json.message;
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
throw new ApiError(403, msg);
|
|
407
|
+
}
|
|
408
|
+
case 404:
|
|
409
|
+
throw new ApiError(404, "Not found.");
|
|
410
|
+
default: {
|
|
411
|
+
const text = await response.text().catch(() => response.statusText);
|
|
412
|
+
let code;
|
|
413
|
+
let displayText = text;
|
|
414
|
+
try {
|
|
415
|
+
const json = JSON.parse(text);
|
|
416
|
+
if (typeof json === "object" && json !== null) {
|
|
417
|
+
if (typeof json.error === "string" && json.error.length > 0) {
|
|
418
|
+
code = json.error;
|
|
419
|
+
} else if (typeof json.code === "string" && json.code.length > 0) {
|
|
420
|
+
code = json.code;
|
|
421
|
+
}
|
|
422
|
+
displayText = typeof json.message === "string" && json.message || typeof json.error === "string" && json.error || text;
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
if (displayText.length > 200) {
|
|
427
|
+
displayText = displayText.substring(0, 200) + "...";
|
|
428
|
+
}
|
|
429
|
+
displayText = displayText.replace(/at [^\s]+\.(ts|js|mjs|cjs):\d+/g, "[internal]");
|
|
430
|
+
throw new ApiError(response.status, `API error (${response.status}): ${displayText}`, code);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return response.json();
|
|
435
|
+
}
|
|
436
|
+
function getMe(token) {
|
|
437
|
+
return request("/auth/me", {}, token);
|
|
438
|
+
}
|
|
439
|
+
function getRepoByName(owner, repo) {
|
|
440
|
+
return request(
|
|
441
|
+
`/api/repos/by-name/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
function startGeneration(repoId, body) {
|
|
445
|
+
return request(`/api/repos/${repoId}/generate`, {
|
|
446
|
+
method: "POST",
|
|
447
|
+
body: JSON.stringify(body)
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function getChangelog(changelogId) {
|
|
451
|
+
return request(`/api/changelogs/${changelogId}`);
|
|
452
|
+
}
|
|
453
|
+
function deviceAuthStart() {
|
|
454
|
+
return request("/auth/device/start", {
|
|
455
|
+
method: "POST"
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function deviceAuthPoll(deviceCode) {
|
|
459
|
+
return request("/auth/device/poll", {
|
|
460
|
+
method: "POST",
|
|
461
|
+
body: JSON.stringify({ deviceCode })
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function startLocalGeneration(body) {
|
|
465
|
+
return request("/api/generate/local", {
|
|
466
|
+
method: "POST",
|
|
467
|
+
body: JSON.stringify(body)
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
function getLocalGenerationResult(jobId) {
|
|
471
|
+
return request(
|
|
472
|
+
`/api/generate/local/${encodeURIComponent(jobId)}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/changelog.ts
|
|
477
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
478
|
+
import { resolve } from "path";
|
|
479
|
+
import { prependReleaseSection } from "@changesmith/core";
|
|
480
|
+
async function prependToChangelog(content, version, rootDir) {
|
|
481
|
+
const changelogPath = resolve(rootDir || process.cwd(), "CHANGELOG.md");
|
|
482
|
+
let existing;
|
|
483
|
+
try {
|
|
484
|
+
existing = await readFile2(changelogPath, "utf-8");
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
if (existing) {
|
|
488
|
+
const updated = prependReleaseSection(existing, content, version);
|
|
489
|
+
await writeFile2(changelogPath, updated, "utf-8");
|
|
490
|
+
} else {
|
|
491
|
+
await writeFile2(changelogPath, `# Changelog
|
|
492
|
+
|
|
493
|
+
${content}
|
|
494
|
+
`, "utf-8");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/commands/generate.ts
|
|
499
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
500
|
+
var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
501
|
+
var githubCommand = new Command2("github").description("Generate a changelog via GitHub App (requires installation)").argument("[version]", "Version tag to generate changelog for (e.g., v1.0.0)").option("--from <tag>", "Starting tag/commit (defaults to previous tag)").option("--to <ref>", "Ending ref (defaults to the version tag)").option("-o, --output <file>", "Output file (defaults to stdout)").option("-w, --write", "Prepend to CHANGELOG.md").action(async (version, options) => {
|
|
502
|
+
if (!isLoggedIn()) {
|
|
503
|
+
console.error(chalk2.red("Error: Not logged in"));
|
|
504
|
+
console.log(chalk2.gray("Run `changesmith login` to authenticate."));
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
if (!await isGitRepo()) {
|
|
508
|
+
console.error(chalk2.red("Error: Not a git repository"));
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
let targetVersion = version;
|
|
512
|
+
if (!targetVersion) {
|
|
513
|
+
const latestTag = await getLatestTag();
|
|
514
|
+
if (latestTag) {
|
|
515
|
+
targetVersion = latestTag;
|
|
516
|
+
console.log(chalk2.gray(`Using latest tag: ${targetVersion}`));
|
|
517
|
+
} else {
|
|
518
|
+
console.error(chalk2.red("Error: No version specified and no tags found"));
|
|
519
|
+
console.log(chalk2.gray("Usage: changesmith github v1.0.0 [-w] [-o file]"));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const remoteUrl = await getRemoteUrl();
|
|
524
|
+
const repoInfo = remoteUrl ? parseRepoFromUrl(remoteUrl) : null;
|
|
525
|
+
if (!repoInfo) {
|
|
526
|
+
console.error(chalk2.red("Error: Could not determine repository from git remote"));
|
|
527
|
+
console.log(chalk2.gray("Make sure your repository has a GitHub remote configured."));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
const repoSpinner = ora2("Looking up repository...").start();
|
|
531
|
+
let repoId;
|
|
532
|
+
try {
|
|
533
|
+
const repo = await getRepoByName(repoInfo.owner, repoInfo.repo);
|
|
534
|
+
repoId = repo.id;
|
|
535
|
+
repoSpinner.succeed(`Repository: ${chalk2.cyan(repo.fullName)}`);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
538
|
+
repoSpinner.fail("Repository not connected to Changesmith");
|
|
539
|
+
const appUrl = deriveWebUrl(getApiUrl());
|
|
540
|
+
console.log("");
|
|
541
|
+
console.log("To connect this repository:");
|
|
542
|
+
console.log(chalk2.cyan(" 1. ") + `Visit ${chalk2.underline(`${appUrl}/dashboard/repos`)}`);
|
|
543
|
+
console.log(chalk2.cyan(" 2. ") + 'Click "Add Repository" and install the GitHub App');
|
|
544
|
+
console.log(chalk2.cyan(" 3. ") + "Select this repository for access");
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
repoSpinner.fail("Failed to look up repository");
|
|
548
|
+
console.error(chalk2.red(error instanceof Error ? error.message : String(error)));
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
const genSpinner = ora2("Starting changelog generation...").start();
|
|
552
|
+
let changelogId;
|
|
553
|
+
try {
|
|
554
|
+
const result = await startGeneration(repoId, {
|
|
555
|
+
version: targetVersion,
|
|
556
|
+
fromTag: options.from,
|
|
557
|
+
toTag: options.to
|
|
558
|
+
});
|
|
559
|
+
changelogId = result.changelogId;
|
|
560
|
+
genSpinner.succeed(`Generation queued (${result.fromTag} \u2192 ${targetVersion})`);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
genSpinner.fail("Failed to start generation");
|
|
563
|
+
console.error(chalk2.red(error instanceof Error ? error.message : String(error)));
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
const gitRoot = await getGitRoot();
|
|
567
|
+
const pollSpinner = ora2("Generating changelog...").start();
|
|
568
|
+
const startTime = Date.now();
|
|
569
|
+
try {
|
|
570
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
571
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1e3);
|
|
572
|
+
pollSpinner.text = `Generating changelog... (${elapsed}s)`;
|
|
573
|
+
await sleep(POLL_INTERVAL_MS);
|
|
574
|
+
const changelog = await getChangelog(changelogId);
|
|
575
|
+
if (changelog.status === "draft" || changelog.status === "approved") {
|
|
576
|
+
pollSpinner.succeed("Changelog generated");
|
|
577
|
+
if (options.write) {
|
|
578
|
+
await prependToChangelog(changelog.content, targetVersion, gitRoot);
|
|
579
|
+
console.log(chalk2.green("CHANGELOG.md updated"));
|
|
580
|
+
} else if (options.output) {
|
|
581
|
+
await writeFile3(options.output, changelog.content + "\n", "utf-8");
|
|
582
|
+
console.log(chalk2.green(`Changelog written to ${options.output}`));
|
|
583
|
+
} else {
|
|
584
|
+
console.log("");
|
|
585
|
+
console.log(chalk2.cyan("\u2500".repeat(60)));
|
|
586
|
+
console.log(changelog.content);
|
|
587
|
+
console.log(chalk2.cyan("\u2500".repeat(60)));
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (changelog.status === "failed") {
|
|
592
|
+
const errorMsg = changelog.metadata?.errorMessage || "Unknown error";
|
|
593
|
+
pollSpinner.fail("Changelog generation failed");
|
|
594
|
+
console.error(chalk2.red(String(errorMsg)));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
if (changelog.status === "published") {
|
|
598
|
+
pollSpinner.succeed("Changelog already published");
|
|
599
|
+
console.log(changelog.content);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
pollSpinner.fail("Generation timed out");
|
|
604
|
+
console.error(chalk2.red("Changelog generation did not complete within 10 minutes."));
|
|
605
|
+
console.log(chalk2.gray("Check the dashboard for status."));
|
|
606
|
+
process.exit(1);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
pollSpinner.fail("Error while waiting for generation");
|
|
609
|
+
console.error(chalk2.red(error instanceof Error ? error.message : String(error)));
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
function sleep(ms) {
|
|
614
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
615
|
+
}
|
|
616
|
+
function deriveWebUrl(apiUrlString) {
|
|
617
|
+
try {
|
|
618
|
+
const apiUrl = new URL(apiUrlString);
|
|
619
|
+
if (apiUrl.hostname.startsWith("api.")) {
|
|
620
|
+
return `${apiUrl.protocol}//${apiUrl.hostname.slice(4)}`;
|
|
621
|
+
}
|
|
622
|
+
return apiUrl.origin;
|
|
623
|
+
} catch {
|
|
624
|
+
return apiUrlString;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/commands/local.ts
|
|
629
|
+
import { Command as Command3 } from "commander";
|
|
630
|
+
import chalk3 from "chalk";
|
|
631
|
+
import ora3 from "ora";
|
|
632
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
633
|
+
import { resolve as resolve2 } from "path";
|
|
634
|
+
var POLL_INTERVAL_MS2 = 3e3;
|
|
635
|
+
var POLL_TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
636
|
+
var MAX_TOTAL_DIFF_BYTES = 3 * 1024 * 1024;
|
|
637
|
+
var MAX_COMMITS = 500;
|
|
638
|
+
var localCommand = new Command3("local").description("Generate a changelog from local git commits (no GitHub App required)").argument("[version]", "Version tag to generate changelog for (e.g., v1.0.0)").option("--from <ref>", "Starting ref (defaults to the tag before version)").option("--to <ref>", "Ending ref (defaults to HEAD or the version tag)").option("-o, --output <file>", "Write output to a specific file").option("-w, --write", "Prepend to CHANGELOG.md").action(async (version, options) => {
|
|
639
|
+
if (!isLoggedIn()) {
|
|
640
|
+
console.error(chalk3.red("Error: Not logged in"));
|
|
641
|
+
console.log(chalk3.gray("Run `changesmith login` to authenticate."));
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
if (!await isGitRepo()) {
|
|
645
|
+
console.error(chalk3.red("Error: Not a git repository"));
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
if (await isShallowClone()) {
|
|
649
|
+
console.error(chalk3.yellow("Warning: This is a shallow clone. Commit history may be incomplete."));
|
|
650
|
+
console.log(chalk3.gray("For full history, run: git fetch --unshallow"));
|
|
651
|
+
}
|
|
652
|
+
let targetVersion = version;
|
|
653
|
+
if (!targetVersion) {
|
|
654
|
+
const latestTag = await getLatestTag();
|
|
655
|
+
if (latestTag) {
|
|
656
|
+
targetVersion = latestTag;
|
|
657
|
+
console.log(chalk3.gray(`Using latest tag: ${targetVersion}`));
|
|
658
|
+
} else {
|
|
659
|
+
console.error(chalk3.red("Error: No version specified and no tags found"));
|
|
660
|
+
console.log(chalk3.gray("Usage: changesmith local v1.0.0"));
|
|
661
|
+
console.log(chalk3.gray(" or: changesmith local v1.0.0 --from HEAD~50"));
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const toRef = options.to || targetVersion;
|
|
666
|
+
let fromRef = options.from;
|
|
667
|
+
if (!fromRef) {
|
|
668
|
+
const prevTag = await getPreviousTag(targetVersion);
|
|
669
|
+
if (prevTag) {
|
|
670
|
+
fromRef = prevTag;
|
|
671
|
+
console.log(chalk3.gray(`Using previous tag as start: ${fromRef}`));
|
|
672
|
+
} else {
|
|
673
|
+
console.error(chalk3.red("Error: No previous tag found."));
|
|
674
|
+
console.log(
|
|
675
|
+
chalk3.gray("Specify a starting ref with --from:")
|
|
676
|
+
);
|
|
677
|
+
console.log(chalk3.gray(` changesmith local ${targetVersion} --from HEAD~50`));
|
|
678
|
+
console.log(chalk3.gray(` changesmith local ${targetVersion} --from abc1234`));
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const commitSpinner = ora3("Collecting commits...").start();
|
|
683
|
+
let commits;
|
|
684
|
+
try {
|
|
685
|
+
commits = await getCommitsBetween(fromRef, toRef);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
commitSpinner.fail("Failed to collect commits");
|
|
688
|
+
console.error(chalk3.red(error instanceof Error ? error.message : String(error)));
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
if (commits.length === 0) {
|
|
692
|
+
commitSpinner.fail("No commits found in the specified range");
|
|
693
|
+
console.log(chalk3.gray(`Range: ${fromRef}..${toRef}`));
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
if (commits.length > MAX_COMMITS) {
|
|
697
|
+
commitSpinner.text = `Found ${commits.length} commits, using first ${MAX_COMMITS}`;
|
|
698
|
+
commits = commits.slice(0, MAX_COMMITS);
|
|
699
|
+
}
|
|
700
|
+
commitSpinner.succeed(`Found ${commits.length} commits (${fromRef}..${toRef})`);
|
|
701
|
+
const diffSpinner = ora3("Collecting diffs...").start();
|
|
702
|
+
const diffs = [];
|
|
703
|
+
let totalDiffBytes = 0;
|
|
704
|
+
let skippedDiffs = 0;
|
|
705
|
+
for (let i = 0; i < commits.length; i++) {
|
|
706
|
+
const commit = commits[i];
|
|
707
|
+
diffSpinner.text = `Collecting diffs... (${i + 1}/${commits.length})`;
|
|
708
|
+
if (totalDiffBytes >= MAX_TOTAL_DIFF_BYTES) {
|
|
709
|
+
skippedDiffs = commits.length - i;
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const diff = await getCommitDiff(commit.sha);
|
|
714
|
+
const diffSize = JSON.stringify(diff).length;
|
|
715
|
+
if (totalDiffBytes + diffSize > MAX_TOTAL_DIFF_BYTES) {
|
|
716
|
+
skippedDiffs = commits.length - i;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
diffs.push(diff);
|
|
720
|
+
totalDiffBytes += diffSize;
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (skippedDiffs > 0) {
|
|
725
|
+
diffSpinner.succeed(
|
|
726
|
+
`Collected diffs for ${diffs.length} commits (${skippedDiffs} skipped \u2014 3MB limit)`
|
|
727
|
+
);
|
|
728
|
+
} else {
|
|
729
|
+
diffSpinner.succeed(`Collected diffs for ${diffs.length} commits`);
|
|
730
|
+
}
|
|
731
|
+
const gitRoot = await getGitRoot();
|
|
732
|
+
let existingChangelog;
|
|
733
|
+
try {
|
|
734
|
+
existingChangelog = await readFile3(resolve2(gitRoot, "CHANGELOG.md"), "utf-8");
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
const projectConfig = await readProjectConfig();
|
|
738
|
+
const customInstructions = projectConfig?.customPrompt;
|
|
739
|
+
const repoName = await deriveRepoName();
|
|
740
|
+
const genSpinner = ora3("Starting changelog generation...").start();
|
|
741
|
+
let jobId;
|
|
742
|
+
try {
|
|
743
|
+
const result = await startLocalGeneration({
|
|
744
|
+
version: targetVersion,
|
|
745
|
+
repoName,
|
|
746
|
+
commits,
|
|
747
|
+
diffs,
|
|
748
|
+
existingChangelog,
|
|
749
|
+
customInstructions
|
|
750
|
+
});
|
|
751
|
+
jobId = result.jobId;
|
|
752
|
+
genSpinner.succeed("Generation queued");
|
|
753
|
+
} catch (error) {
|
|
754
|
+
genSpinner.fail("Failed to start generation");
|
|
755
|
+
if (error instanceof ApiError && error.status === 413) {
|
|
756
|
+
console.error(chalk3.red("Payload too large. Try a smaller commit range."));
|
|
757
|
+
} else {
|
|
758
|
+
console.error(chalk3.red(error instanceof Error ? error.message : String(error)));
|
|
759
|
+
}
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
const pollSpinner = ora3("Generating changelog...").start();
|
|
763
|
+
const startTime = Date.now();
|
|
764
|
+
try {
|
|
765
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS2) {
|
|
766
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1e3);
|
|
767
|
+
pollSpinner.text = `Generating changelog... (${elapsed}s)`;
|
|
768
|
+
await sleep2(POLL_INTERVAL_MS2);
|
|
769
|
+
const result = await getLocalGenerationResult(jobId);
|
|
770
|
+
if (result.status === "completed" && result.content) {
|
|
771
|
+
pollSpinner.succeed("Changelog generated");
|
|
772
|
+
if (options.write) {
|
|
773
|
+
await prependToChangelog(result.content, targetVersion, gitRoot);
|
|
774
|
+
console.log(chalk3.green("CHANGELOG.md updated"));
|
|
775
|
+
} else if (options.output) {
|
|
776
|
+
await writeFile4(options.output, result.content + "\n", "utf-8");
|
|
777
|
+
console.log(chalk3.green(`Changelog written to ${options.output}`));
|
|
778
|
+
} else {
|
|
779
|
+
console.log("");
|
|
780
|
+
console.log(chalk3.cyan("\u2500".repeat(60)));
|
|
781
|
+
console.log(result.content);
|
|
782
|
+
console.log(chalk3.cyan("\u2500".repeat(60)));
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (result.status === "error") {
|
|
787
|
+
pollSpinner.fail("Changelog generation failed");
|
|
788
|
+
console.error(chalk3.red(result.error || "Unknown error"));
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
pollSpinner.fail("Generation timed out");
|
|
793
|
+
console.error(chalk3.red("Changelog generation did not complete within 10 minutes."));
|
|
794
|
+
process.exit(1);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
pollSpinner.fail("Error while waiting for generation");
|
|
797
|
+
console.error(chalk3.red(error instanceof Error ? error.message : String(error)));
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
function sleep2(ms) {
|
|
802
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/commands/login.ts
|
|
806
|
+
import { Command as Command4 } from "commander";
|
|
807
|
+
import chalk4 from "chalk";
|
|
808
|
+
import ora4 from "ora";
|
|
809
|
+
var MIN_POLL_INTERVAL_MS = 5e3;
|
|
810
|
+
var MAX_POLL_INTERVAL_MS = 6e4;
|
|
811
|
+
var loginCommand = new Command4("login").description("Log in to Changesmith").option("--token <token>", "Use a pre-generated auth token (for CI/automation)").action(async (options) => {
|
|
812
|
+
if (options.token) {
|
|
813
|
+
await validateAndSaveToken(options.token, { showEnvWarning: isEnvAuth() });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (isEnvAuth()) {
|
|
817
|
+
console.log(chalk4.yellow("Already authenticated via CHANGESMITH_TOKEN environment variable."));
|
|
818
|
+
console.log(chalk4.gray("Unset CHANGESMITH_TOKEN to use stored credentials instead."));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (isLoggedIn()) {
|
|
822
|
+
console.log(chalk4.yellow("You are already logged in."));
|
|
823
|
+
console.log(chalk4.gray("Run `changesmith logout` to log out first."));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
await deviceAuthFlow();
|
|
827
|
+
});
|
|
828
|
+
async function validateAndSaveToken(token, opts = {}) {
|
|
829
|
+
const spinner = ora4("Validating token...").start();
|
|
830
|
+
try {
|
|
831
|
+
if (token.length < MIN_TOKEN_LENGTH || token.length > MAX_TOKEN_LENGTH) {
|
|
832
|
+
throw new Error("Invalid token format.");
|
|
833
|
+
}
|
|
834
|
+
const user = await getMe(token);
|
|
835
|
+
setAuthToken(token);
|
|
836
|
+
setUserId(user.id);
|
|
837
|
+
if (opts.showEnvWarning) {
|
|
838
|
+
spinner.succeed(
|
|
839
|
+
`Token saved for ${chalk4.cyan(user.githubLogin)}` + chalk4.yellow(" (CHANGESMITH_TOKEN currently takes precedence)")
|
|
840
|
+
);
|
|
841
|
+
console.log(chalk4.gray("Unset CHANGESMITH_TOKEN to use stored credentials."));
|
|
842
|
+
} else {
|
|
843
|
+
spinner.succeed(`Logged in as ${chalk4.cyan(user.githubLogin)}`);
|
|
844
|
+
}
|
|
845
|
+
} catch (error) {
|
|
846
|
+
spinner.fail("Token validation failed");
|
|
847
|
+
if (error instanceof ApiError) {
|
|
848
|
+
console.error(chalk4.red(error.message));
|
|
849
|
+
} else if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
850
|
+
console.error(chalk4.red("Unable to connect to API server. Check your network connection."));
|
|
851
|
+
} else if (error instanceof Error && error.name === "AbortError") {
|
|
852
|
+
console.error(chalk4.red("Request timed out. The API server is not responding."));
|
|
853
|
+
} else if (error instanceof Error && error.message === "Invalid token format.") {
|
|
854
|
+
console.error(chalk4.red(error.message));
|
|
855
|
+
} else {
|
|
856
|
+
console.error(chalk4.red("Token validation failed. Please try again."));
|
|
857
|
+
}
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function deviceAuthFlow() {
|
|
862
|
+
const apiUrl = getApiUrl();
|
|
863
|
+
console.log(chalk4.cyan("Changesmith Login"));
|
|
864
|
+
console.log("");
|
|
865
|
+
const spinner = ora4("Starting authentication...").start();
|
|
866
|
+
try {
|
|
867
|
+
const data = await deviceAuthStart();
|
|
868
|
+
spinner.stop();
|
|
869
|
+
console.log("To authenticate, visit:");
|
|
870
|
+
console.log("");
|
|
871
|
+
console.log(chalk4.cyan(` ${data.verificationUri}`));
|
|
872
|
+
console.log("");
|
|
873
|
+
console.log("And enter this code:");
|
|
874
|
+
console.log("");
|
|
875
|
+
console.log(chalk4.bold.green(` ${data.userCode}`));
|
|
876
|
+
console.log("");
|
|
877
|
+
const pollSpinner = ora4("Waiting for authentication...").start();
|
|
878
|
+
const token = await pollForToken(data.deviceCode, data.interval, data.expiresIn);
|
|
879
|
+
if (!token) {
|
|
880
|
+
pollSpinner.fail("Authentication timed out");
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
const user = await getMe(token);
|
|
885
|
+
setAuthToken(token);
|
|
886
|
+
setUserId(user.id);
|
|
887
|
+
pollSpinner.succeed(`Logged in as ${chalk4.cyan(user.githubLogin)}`);
|
|
888
|
+
} catch {
|
|
889
|
+
setAuthToken(token);
|
|
890
|
+
pollSpinner.succeed("Logged in");
|
|
891
|
+
console.log(
|
|
892
|
+
chalk4.yellow(
|
|
893
|
+
"Warning: Could not verify token. If commands fail, try `changesmith logout` and log in again."
|
|
894
|
+
)
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
console.log("");
|
|
898
|
+
console.log("You can now generate changelogs:");
|
|
899
|
+
console.log(chalk4.gray(" changesmith generate v1.0.0"));
|
|
900
|
+
} catch (error) {
|
|
901
|
+
spinner.fail("Authentication failed");
|
|
902
|
+
const appUrl = apiUrl.replace("/api", "").replace("api.", "");
|
|
903
|
+
console.log("");
|
|
904
|
+
console.log("To log in manually:");
|
|
905
|
+
console.log(chalk4.cyan(" 1. ") + `Visit ${appUrl}/dashboard/settings`);
|
|
906
|
+
console.log(chalk4.cyan(" 2. ") + "Generate an API token");
|
|
907
|
+
console.log(chalk4.cyan(" 3. ") + "Run: changesmith login --token YOUR_TOKEN");
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async function pollForToken(deviceCode, interval, expiresIn) {
|
|
911
|
+
const pollInterval = Math.max(
|
|
912
|
+
Math.min(interval * 1e3, MAX_POLL_INTERVAL_MS),
|
|
913
|
+
MIN_POLL_INTERVAL_MS
|
|
914
|
+
);
|
|
915
|
+
const safeExpiresIn = Math.max(60, Math.min(expiresIn, 3600));
|
|
916
|
+
const maxAttempts = Math.floor(safeExpiresIn * 1e3 / pollInterval);
|
|
917
|
+
const startTime = Date.now();
|
|
918
|
+
const absoluteTimeoutMs = safeExpiresIn * 1e3;
|
|
919
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
920
|
+
if (Date.now() - startTime >= absoluteTimeoutMs) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
|
|
924
|
+
try {
|
|
925
|
+
const data = await deviceAuthPoll(deviceCode);
|
|
926
|
+
return data.token;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
if (error instanceof ApiError) {
|
|
929
|
+
if (error.code === "ERR_DEVICE_002") {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/commands/logout.ts
|
|
941
|
+
import { Command as Command5 } from "commander";
|
|
942
|
+
import chalk5 from "chalk";
|
|
943
|
+
var logoutCommand = new Command5("logout").description("Log out from Changesmith cloud").option("--clear-stored", "Remove stored credentials (useful when CHANGESMITH_TOKEN is set)").action(async (options) => {
|
|
944
|
+
if (options.clearStored) {
|
|
945
|
+
if (hasStoredToken()) {
|
|
946
|
+
clearAuth();
|
|
947
|
+
console.log(chalk5.green("Stored credentials cleared."));
|
|
948
|
+
} else {
|
|
949
|
+
console.log(chalk5.gray("No stored credentials to clear."));
|
|
950
|
+
}
|
|
951
|
+
if (isEnvAuth()) {
|
|
952
|
+
console.log(chalk5.gray("Still authenticated via CHANGESMITH_TOKEN."));
|
|
953
|
+
}
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (isEnvAuth()) {
|
|
957
|
+
console.log(chalk5.yellow("Using CHANGESMITH_TOKEN environment variable."));
|
|
958
|
+
console.log(chalk5.gray("Unset the environment variable to log out:"));
|
|
959
|
+
console.log(chalk5.gray(" unset CHANGESMITH_TOKEN"));
|
|
960
|
+
if (hasStoredToken()) {
|
|
961
|
+
console.log("");
|
|
962
|
+
console.log(
|
|
963
|
+
chalk5.gray("Note: Stored credentials are preserved. Use --clear-stored to remove them.")
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (!isLoggedIn()) {
|
|
969
|
+
console.log(chalk5.gray("You are not logged in."));
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
clearAuth();
|
|
973
|
+
console.log(chalk5.green("Logged out successfully."));
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// src/commands/status.ts
|
|
977
|
+
import { Command as Command6 } from "commander";
|
|
978
|
+
import chalk6 from "chalk";
|
|
979
|
+
var statusCommand = new Command6("status").description("Show Changesmith status for the current repository").action(async () => {
|
|
980
|
+
console.log(chalk6.cyan("Changesmith Status"));
|
|
981
|
+
console.log("");
|
|
982
|
+
const isRepo = await isGitRepo();
|
|
983
|
+
console.log(`Git repository: ${isRepo ? chalk6.green("Yes") : chalk6.red("No")}`);
|
|
984
|
+
if (isRepo) {
|
|
985
|
+
const remoteUrl = await getRemoteUrl();
|
|
986
|
+
const repoInfo = remoteUrl ? parseRepoFromUrl(remoteUrl) : null;
|
|
987
|
+
if (repoInfo) {
|
|
988
|
+
console.log(`Repository: ${chalk6.white(`${repoInfo.owner}/${repoInfo.repo}`)}`);
|
|
989
|
+
}
|
|
990
|
+
const latestTag = await getLatestTag();
|
|
991
|
+
const tags = await getTags();
|
|
992
|
+
console.log(`Latest tag: ${latestTag ? chalk6.white(latestTag) : chalk6.gray("none")}`);
|
|
993
|
+
console.log(`Total tags: ${chalk6.white(tags.length.toString())}`);
|
|
994
|
+
}
|
|
995
|
+
console.log("");
|
|
996
|
+
const hasConfig = await projectConfigExists();
|
|
997
|
+
console.log(
|
|
998
|
+
`Project config: ${hasConfig ? chalk6.green(".changesmith.json") : chalk6.gray("not initialized")}`
|
|
999
|
+
);
|
|
1000
|
+
if (hasConfig) {
|
|
1001
|
+
const config = await readProjectConfig();
|
|
1002
|
+
if (config) {
|
|
1003
|
+
if (config.excludeTypes?.length) {
|
|
1004
|
+
console.log(`Excluded types: ${chalk6.gray(config.excludeTypes.join(", "))}`);
|
|
1005
|
+
}
|
|
1006
|
+
if (config.customPrompt) {
|
|
1007
|
+
console.log(`Custom prompt: ${chalk6.green("configured")}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
console.log("");
|
|
1012
|
+
const loggedIn = isLoggedIn();
|
|
1013
|
+
const envAuth = isEnvAuth();
|
|
1014
|
+
if (envAuth) {
|
|
1015
|
+
console.log(`Logged in: ${chalk6.green("yes")} ${chalk6.gray("(via CHANGESMITH_TOKEN)")}`);
|
|
1016
|
+
} else {
|
|
1017
|
+
console.log(`Logged in: ${loggedIn ? chalk6.green("yes") : chalk6.gray("no")}`);
|
|
1018
|
+
}
|
|
1019
|
+
if (loggedIn) {
|
|
1020
|
+
try {
|
|
1021
|
+
const user = await getMe();
|
|
1022
|
+
console.log(`User: ${chalk6.white(user.githubLogin)}`);
|
|
1023
|
+
console.log(`Plan: ${chalk6.white(user.plan)}`);
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
console.log("");
|
|
1028
|
+
if (!hasConfig) {
|
|
1029
|
+
console.log(chalk6.gray("Run `changesmith init` to initialize this repository."));
|
|
1030
|
+
} else if (!loggedIn) {
|
|
1031
|
+
console.log(chalk6.gray("Run `changesmith login` to authenticate."));
|
|
1032
|
+
} else {
|
|
1033
|
+
console.log(chalk6.gray("Run `changesmith generate` to create a changelog."));
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// src/commands/config.ts
|
|
1038
|
+
import { Command as Command7 } from "commander";
|
|
1039
|
+
import chalk7 from "chalk";
|
|
1040
|
+
var configCommand = new Command7("config").description("View or modify configuration").addCommand(
|
|
1041
|
+
new Command7("show").description("Show current configuration").option("--project", "Show project config only").option("--user", "Show user config only").action(async (options) => {
|
|
1042
|
+
if (!options.user) {
|
|
1043
|
+
await showProjectConfig();
|
|
1044
|
+
}
|
|
1045
|
+
if (!options.project) {
|
|
1046
|
+
showUserConfig();
|
|
1047
|
+
}
|
|
1048
|
+
})
|
|
1049
|
+
).addCommand(
|
|
1050
|
+
new Command7("set").description("Set a configuration value").argument("<key>", "Config key to set").argument("<value>", "Value to set").option("--global", "Set in user config (default is project config)").action(async (key, value, options) => {
|
|
1051
|
+
if (options.global) {
|
|
1052
|
+
setUserConfigValue(key, value);
|
|
1053
|
+
} else {
|
|
1054
|
+
await setProjectConfigValue(key, value);
|
|
1055
|
+
}
|
|
1056
|
+
})
|
|
1057
|
+
).addCommand(
|
|
1058
|
+
new Command7("get").description("Get a configuration value").argument("<key>", "Config key to get").option("--global", "Get from user config (default is project config)").action(async (key, options) => {
|
|
1059
|
+
if (options.global) {
|
|
1060
|
+
getUserConfigValue(key);
|
|
1061
|
+
} else {
|
|
1062
|
+
await getProjectConfigValue(key);
|
|
1063
|
+
}
|
|
1064
|
+
})
|
|
1065
|
+
).addCommand(
|
|
1066
|
+
new Command7("reset").description("Reset project config to defaults").option("-f, --force", "Skip confirmation").action(async (options) => {
|
|
1067
|
+
if (!await projectConfigExists()) {
|
|
1068
|
+
console.log(chalk7.yellow("No project config found."));
|
|
1069
|
+
console.log(chalk7.gray("Run `changesmith init` first."));
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (!options.force) {
|
|
1073
|
+
console.log(chalk7.yellow("This will reset .changesmith.json to defaults."));
|
|
1074
|
+
console.log(chalk7.gray("Use --force to confirm."));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
await writeProjectConfig(getDefaultProjectConfig());
|
|
1078
|
+
console.log(chalk7.green("Project config reset to defaults."));
|
|
1079
|
+
})
|
|
1080
|
+
);
|
|
1081
|
+
async function showProjectConfig() {
|
|
1082
|
+
console.log(chalk7.cyan("Project Configuration"));
|
|
1083
|
+
console.log(chalk7.gray(` File: ${getProjectConfigPath()}`));
|
|
1084
|
+
console.log("");
|
|
1085
|
+
if (!await projectConfigExists()) {
|
|
1086
|
+
console.log(chalk7.gray(" No project config found."));
|
|
1087
|
+
console.log(chalk7.gray(" Run `changesmith init` to create one."));
|
|
1088
|
+
console.log("");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const config = await readProjectConfig();
|
|
1092
|
+
if (!config) {
|
|
1093
|
+
console.log(chalk7.red(" Error reading config file."));
|
|
1094
|
+
console.log("");
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
console.log(chalk7.white(" version: ") + config.version);
|
|
1098
|
+
if (config.defaultBranch) {
|
|
1099
|
+
console.log(chalk7.white(" defaultBranch: ") + config.defaultBranch);
|
|
1100
|
+
}
|
|
1101
|
+
if (config.excludeTypes?.length) {
|
|
1102
|
+
console.log(chalk7.white(" excludeTypes: ") + config.excludeTypes.join(", "));
|
|
1103
|
+
}
|
|
1104
|
+
if (config.includeScopes?.length) {
|
|
1105
|
+
console.log(chalk7.white(" includeScopes: ") + config.includeScopes.join(", "));
|
|
1106
|
+
}
|
|
1107
|
+
if (config.excludeScopes?.length) {
|
|
1108
|
+
console.log(chalk7.white(" excludeScopes: ") + config.excludeScopes.join(", "));
|
|
1109
|
+
}
|
|
1110
|
+
if (config.styleGuide) {
|
|
1111
|
+
console.log(chalk7.white(" styleGuide: ") + config.styleGuide);
|
|
1112
|
+
}
|
|
1113
|
+
if (config.customPrompt) {
|
|
1114
|
+
console.log(chalk7.white(" customPrompt: ") + chalk7.green("(configured)"));
|
|
1115
|
+
}
|
|
1116
|
+
console.log("");
|
|
1117
|
+
}
|
|
1118
|
+
function showUserConfig() {
|
|
1119
|
+
console.log(chalk7.cyan("User Configuration"));
|
|
1120
|
+
console.log("");
|
|
1121
|
+
if (isEnvApiUrl()) {
|
|
1122
|
+
console.log(chalk7.white(" apiUrl: ") + getApiUrl() + chalk7.gray(" (via env)"));
|
|
1123
|
+
} else {
|
|
1124
|
+
console.log(chalk7.white(" apiUrl: ") + getApiUrl());
|
|
1125
|
+
}
|
|
1126
|
+
const loggedIn = isLoggedIn();
|
|
1127
|
+
const envAuth = isEnvAuth();
|
|
1128
|
+
if (envAuth) {
|
|
1129
|
+
console.log(chalk7.white(" loggedIn: ") + chalk7.green("yes") + chalk7.gray(" (via env)"));
|
|
1130
|
+
} else {
|
|
1131
|
+
console.log(
|
|
1132
|
+
chalk7.white(" loggedIn: ") + (loggedIn ? chalk7.green("yes") : chalk7.gray("no"))
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
if (!envAuth && isLoggedIn()) {
|
|
1136
|
+
const userId = getUserId();
|
|
1137
|
+
if (userId) {
|
|
1138
|
+
console.log(chalk7.white(" userId: ") + userId);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
console.log("");
|
|
1142
|
+
}
|
|
1143
|
+
function setUserConfigValue(key, value) {
|
|
1144
|
+
switch (key) {
|
|
1145
|
+
case "apiUrl":
|
|
1146
|
+
try {
|
|
1147
|
+
new URL(value);
|
|
1148
|
+
} catch {
|
|
1149
|
+
console.log(chalk7.red(`Invalid URL: ${value}`));
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
setApiUrl(value);
|
|
1153
|
+
console.log(chalk7.green(`Set apiUrl to: ${value}`));
|
|
1154
|
+
break;
|
|
1155
|
+
default:
|
|
1156
|
+
console.log(chalk7.red(`Unknown user config key: ${key}`));
|
|
1157
|
+
console.log(chalk7.gray("Available keys: apiUrl"));
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function getUserConfigValue(key) {
|
|
1162
|
+
switch (key) {
|
|
1163
|
+
case "apiUrl":
|
|
1164
|
+
console.log(getApiUrl());
|
|
1165
|
+
break;
|
|
1166
|
+
default:
|
|
1167
|
+
console.log(chalk7.red(`Unknown user config key: ${key}`));
|
|
1168
|
+
console.log(chalk7.gray("Available keys: apiUrl"));
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function setProjectConfigValue(key, value) {
|
|
1173
|
+
if (!await projectConfigExists()) {
|
|
1174
|
+
console.log(chalk7.red("No project config found."));
|
|
1175
|
+
console.log(chalk7.gray("Run `changesmith init` first."));
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
const config = await readProjectConfig();
|
|
1179
|
+
if (!config) {
|
|
1180
|
+
console.log(chalk7.red("Error reading config file."));
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
const validKeys = [
|
|
1184
|
+
"defaultBranch",
|
|
1185
|
+
"excludeTypes",
|
|
1186
|
+
"includeScopes",
|
|
1187
|
+
"excludeScopes",
|
|
1188
|
+
"styleGuide",
|
|
1189
|
+
"customPrompt"
|
|
1190
|
+
];
|
|
1191
|
+
if (!validKeys.includes(key)) {
|
|
1192
|
+
console.log(chalk7.red(`Unknown project config key: ${key}`));
|
|
1193
|
+
console.log(chalk7.gray(`Available keys: ${validKeys.join(", ")}`));
|
|
1194
|
+
process.exit(1);
|
|
1195
|
+
}
|
|
1196
|
+
const arrayKeys = ["excludeTypes", "includeScopes", "excludeScopes"];
|
|
1197
|
+
if (arrayKeys.includes(key)) {
|
|
1198
|
+
const arrayValue = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1199
|
+
const updatedConfig = { ...config, [key]: arrayValue };
|
|
1200
|
+
await writeProjectConfig(updatedConfig);
|
|
1201
|
+
console.log(chalk7.green(`Set ${key} to: [${arrayValue.join(", ")}]`));
|
|
1202
|
+
} else {
|
|
1203
|
+
const updatedConfig = { ...config, [key]: value };
|
|
1204
|
+
await writeProjectConfig(updatedConfig);
|
|
1205
|
+
console.log(chalk7.green(`Set ${key} to: ${value}`));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function getProjectConfigValue(key) {
|
|
1209
|
+
const validKeys = [
|
|
1210
|
+
"version",
|
|
1211
|
+
"defaultBranch",
|
|
1212
|
+
"excludeTypes",
|
|
1213
|
+
"includeScopes",
|
|
1214
|
+
"excludeScopes",
|
|
1215
|
+
"styleGuide",
|
|
1216
|
+
"customPrompt"
|
|
1217
|
+
];
|
|
1218
|
+
if (!validKeys.includes(key)) {
|
|
1219
|
+
console.log(chalk7.red(`Unknown project config key: ${key}`));
|
|
1220
|
+
console.log(chalk7.gray(`Available keys: ${validKeys.join(", ")}`));
|
|
1221
|
+
process.exit(1);
|
|
1222
|
+
}
|
|
1223
|
+
if (!await projectConfigExists()) {
|
|
1224
|
+
console.log(chalk7.red("No project config found."));
|
|
1225
|
+
process.exit(1);
|
|
1226
|
+
}
|
|
1227
|
+
const config = await readProjectConfig();
|
|
1228
|
+
if (!config) {
|
|
1229
|
+
console.log(chalk7.red("Error reading config file."));
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
const configRecord = config;
|
|
1233
|
+
const value = configRecord[key];
|
|
1234
|
+
if (value === void 0) {
|
|
1235
|
+
console.log(chalk7.gray("(not set)"));
|
|
1236
|
+
} else if (Array.isArray(value)) {
|
|
1237
|
+
console.log(value.join(", "));
|
|
1238
|
+
} else {
|
|
1239
|
+
console.log(String(value));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/index.ts
|
|
1244
|
+
var program = new Command8();
|
|
1245
|
+
program.name("changesmith").description("Generate beautiful changelogs from your git history").version("1.4.0");
|
|
1246
|
+
program.addCommand(initCommand);
|
|
1247
|
+
program.addCommand(localCommand);
|
|
1248
|
+
program.addCommand(githubCommand);
|
|
1249
|
+
program.addCommand(loginCommand);
|
|
1250
|
+
program.addCommand(logoutCommand);
|
|
1251
|
+
program.addCommand(statusCommand);
|
|
1252
|
+
program.addCommand(configCommand);
|
|
1253
|
+
var generateAlias = new Command8("generate").description('Alias for "github" command').allowUnknownOption(true).allowExcessArguments(true).helpOption(false).action(async function() {
|
|
1254
|
+
await githubCommand.parseAsync(["node", "github", ...generateAlias.args]);
|
|
1255
|
+
});
|
|
1256
|
+
program.addCommand(generateAlias, { hidden: true });
|
|
1257
|
+
program.parse();
|
|
1258
|
+
//# sourceMappingURL=index.js.map
|