@clogg/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.mjs +407 -0
- package/package.json +35 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/login.ts
|
|
7
|
+
import open from "open";
|
|
8
|
+
|
|
9
|
+
// src/auth.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import os from "os";
|
|
13
|
+
import http from "http";
|
|
14
|
+
function getConfigDir() {
|
|
15
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
16
|
+
const base = xdg || path.join(os.homedir(), ".config");
|
|
17
|
+
return path.join(base, "clogg");
|
|
18
|
+
}
|
|
19
|
+
function getCredentialsPath() {
|
|
20
|
+
return path.join(getConfigDir(), "credentials.json");
|
|
21
|
+
}
|
|
22
|
+
function loadCredentials() {
|
|
23
|
+
try {
|
|
24
|
+
const data = fs.readFileSync(getCredentialsPath(), "utf-8");
|
|
25
|
+
const creds = JSON.parse(data);
|
|
26
|
+
if (new Date(creds.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
27
|
+
console.error("Token expired. Run `clogg login` to re-authenticate.");
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return creds;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function saveCredentials(creds) {
|
|
36
|
+
const dir = getConfigDir();
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
38
|
+
fs.writeFileSync(getCredentialsPath(), JSON.stringify(creds, null, 2));
|
|
39
|
+
fs.chmodSync(getCredentialsPath(), 384);
|
|
40
|
+
}
|
|
41
|
+
function clearCredentials() {
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(getCredentialsPath());
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getToken() {
|
|
50
|
+
const creds = loadCredentials();
|
|
51
|
+
if (!creds) {
|
|
52
|
+
console.error("Not logged in. Run `clogg login` first.");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
return creds.token;
|
|
56
|
+
}
|
|
57
|
+
function startCallbackServer() {
|
|
58
|
+
let resolveResult;
|
|
59
|
+
let rejectResult;
|
|
60
|
+
const result = new Promise((resolve, reject) => {
|
|
61
|
+
resolveResult = resolve;
|
|
62
|
+
rejectResult = reject;
|
|
63
|
+
});
|
|
64
|
+
const server = http.createServer((req, res) => {
|
|
65
|
+
const url = new URL(req.url || "/", `http://localhost`);
|
|
66
|
+
if (url.pathname === "/callback") {
|
|
67
|
+
const token = url.searchParams.get("token");
|
|
68
|
+
const username = url.searchParams.get("username");
|
|
69
|
+
const expiresAt = url.searchParams.get("expires_at");
|
|
70
|
+
if (token && username && expiresAt) {
|
|
71
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
72
|
+
res.end(`
|
|
73
|
+
<html>
|
|
74
|
+
<body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff;">
|
|
75
|
+
<div style="text-align: center;">
|
|
76
|
+
<h2 style="font-weight: 400;">Logged in as @${username}</h2>
|
|
77
|
+
<p style="opacity: 0.5;">You can close this tab.</p>
|
|
78
|
+
</div>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
81
|
+
`);
|
|
82
|
+
server.close();
|
|
83
|
+
resolveResult({ token, username, expiresAt });
|
|
84
|
+
} else {
|
|
85
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
86
|
+
res.end("Missing parameters");
|
|
87
|
+
server.close();
|
|
88
|
+
rejectResult(new Error("OAuth callback missing parameters"));
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
res.writeHead(404);
|
|
92
|
+
res.end();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
server.listen(0, "127.0.0.1");
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
server.close();
|
|
98
|
+
rejectResult(new Error("Login timed out \u2014 try again"));
|
|
99
|
+
}, 12e4);
|
|
100
|
+
return {
|
|
101
|
+
get port() {
|
|
102
|
+
return server.address()?.port || 0;
|
|
103
|
+
},
|
|
104
|
+
server,
|
|
105
|
+
result
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/commands/login.ts
|
|
110
|
+
var AUTH_URL = "https://www.clogg.one/api/cli/auth";
|
|
111
|
+
async function login() {
|
|
112
|
+
const { server, result } = startCallbackServer();
|
|
113
|
+
await new Promise((resolve) => {
|
|
114
|
+
if (server.listening) return resolve();
|
|
115
|
+
server.once("listening", resolve);
|
|
116
|
+
});
|
|
117
|
+
const port = server.address().port;
|
|
118
|
+
const url = `${AUTH_URL}?port=${port}`;
|
|
119
|
+
console.log("Opening browser for authentication...");
|
|
120
|
+
console.log(`If it doesn't open, visit: ${url}`);
|
|
121
|
+
await open(url);
|
|
122
|
+
try {
|
|
123
|
+
const { token, username, expiresAt } = await result;
|
|
124
|
+
saveCredentials({ token, username, expiresAt });
|
|
125
|
+
console.log(`
|
|
126
|
+
Logged in as @${username}`);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error(
|
|
129
|
+
`
|
|
130
|
+
Login failed: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
131
|
+
);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/commands/logout.ts
|
|
137
|
+
function logout() {
|
|
138
|
+
if (clearCredentials()) {
|
|
139
|
+
console.log("Logged out.");
|
|
140
|
+
} else {
|
|
141
|
+
console.log("Already logged out.");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/commands/whoami.ts
|
|
146
|
+
function whoami() {
|
|
147
|
+
const creds = loadCredentials();
|
|
148
|
+
if (!creds) {
|
|
149
|
+
console.log("Not logged in. Run `clogg login` to authenticate.");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const expires = new Date(creds.expiresAt);
|
|
153
|
+
const days = Math.ceil(
|
|
154
|
+
(expires.getTime() - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
155
|
+
);
|
|
156
|
+
console.log(`Logged in as @${creds.username}`);
|
|
157
|
+
console.log(`Token expires: ${expires.toLocaleDateString()} (${days} days)`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/api.ts
|
|
161
|
+
var API_BASE = "https://www.clogg.one";
|
|
162
|
+
async function request(path2, options = {}) {
|
|
163
|
+
const token = getToken();
|
|
164
|
+
const res = await fetch(`${API_BASE}${path2}`, {
|
|
165
|
+
...options,
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
Authorization: `Bearer ${token}`,
|
|
169
|
+
...options.headers
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const body = await res.json().catch(() => ({}));
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
error: body.error || `HTTP ${res.status}`
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, data: body };
|
|
180
|
+
}
|
|
181
|
+
async function get(path2) {
|
|
182
|
+
return request(path2);
|
|
183
|
+
}
|
|
184
|
+
async function post(path2, body) {
|
|
185
|
+
return request(path2, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: JSON.stringify(body)
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async function patch(path2, body) {
|
|
191
|
+
return request(path2, {
|
|
192
|
+
method: "PATCH",
|
|
193
|
+
body: JSON.stringify(body)
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function resolveProjectId(slug) {
|
|
197
|
+
const res = await get(
|
|
198
|
+
`/api/projects?slug=${encodeURIComponent(slug)}`
|
|
199
|
+
);
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
console.error(`Project "${slug}" not found.`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
return res.data.project.id;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/utils.ts
|
|
208
|
+
function table(headers, rows) {
|
|
209
|
+
const widths = headers.map(
|
|
210
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length))
|
|
211
|
+
);
|
|
212
|
+
const sep = widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
|
|
213
|
+
const formatRow = (row) => row.map((cell, i) => ` ${(cell || "").padEnd(widths[i])} `).join("\u2502");
|
|
214
|
+
console.log(formatRow(headers));
|
|
215
|
+
console.log(sep);
|
|
216
|
+
rows.forEach((row) => console.log(formatRow(row)));
|
|
217
|
+
}
|
|
218
|
+
function fatal(message) {
|
|
219
|
+
console.error(`Error: ${message}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/commands/project.ts
|
|
224
|
+
async function listProjects() {
|
|
225
|
+
const res = await get("/api/projects");
|
|
226
|
+
if (!res.ok) fatal(res.error);
|
|
227
|
+
const projects = res.data.projects;
|
|
228
|
+
if (projects.length === 0) {
|
|
229
|
+
console.log("No projects yet. Create one with `clogg project create`.");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
table(
|
|
233
|
+
["Slug", "Name", "Source", "Created"],
|
|
234
|
+
projects.map((p) => [
|
|
235
|
+
p.slug,
|
|
236
|
+
p.name,
|
|
237
|
+
p.githubOwner ? `${p.githubOwner}/${p.githubRepo}` : "manual",
|
|
238
|
+
new Date(p.createdAt).toLocaleDateString()
|
|
239
|
+
])
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
async function createProject(opts) {
|
|
243
|
+
let githubOwner;
|
|
244
|
+
let githubRepo;
|
|
245
|
+
if (opts.github) {
|
|
246
|
+
const parts = opts.github.split("/");
|
|
247
|
+
if (parts.length !== 2) {
|
|
248
|
+
fatal("--github must be in owner/repo format");
|
|
249
|
+
}
|
|
250
|
+
[githubOwner, githubRepo] = parts;
|
|
251
|
+
}
|
|
252
|
+
const res = await post("/api/projects", {
|
|
253
|
+
name: opts.name,
|
|
254
|
+
slug: opts.slug,
|
|
255
|
+
description: opts.description,
|
|
256
|
+
githubOwner,
|
|
257
|
+
githubRepo
|
|
258
|
+
});
|
|
259
|
+
if (!res.ok) fatal(res.error);
|
|
260
|
+
const p = res.data.project;
|
|
261
|
+
console.log(`Project created: ${p.name} (${p.slug})`);
|
|
262
|
+
if (githubOwner) {
|
|
263
|
+
console.log(`GitHub: ${githubOwner}/${githubRepo}`);
|
|
264
|
+
}
|
|
265
|
+
console.log(`URL: https://clogg.one/${p.slug}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/commands/add.ts
|
|
269
|
+
import fs2 from "fs";
|
|
270
|
+
async function add(opts) {
|
|
271
|
+
if (!opts.content && !opts.file) {
|
|
272
|
+
fatal("Provide --content or --file");
|
|
273
|
+
}
|
|
274
|
+
let content = opts.content || "";
|
|
275
|
+
if (opts.file) {
|
|
276
|
+
try {
|
|
277
|
+
content = fs2.readFileSync(opts.file, "utf-8");
|
|
278
|
+
} catch {
|
|
279
|
+
fatal(`Could not read file: ${opts.file}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const projectId = await resolveProjectId(opts.project);
|
|
283
|
+
const res = await post(
|
|
284
|
+
`/api/projects/${projectId}/changelogs`,
|
|
285
|
+
{
|
|
286
|
+
version: opts.version,
|
|
287
|
+
content,
|
|
288
|
+
title: opts.title
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
if (!res.ok) fatal(res.error);
|
|
292
|
+
console.log(`Changelog v${res.data.changelog.version} created.`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/commands/list.ts
|
|
296
|
+
async function listChangelogs(opts) {
|
|
297
|
+
const projectId = await resolveProjectId(opts.project);
|
|
298
|
+
const res = await get(
|
|
299
|
+
`/api/projects/${projectId}/changelogs`
|
|
300
|
+
);
|
|
301
|
+
if (!res.ok) fatal(res.error);
|
|
302
|
+
const changelogs = res.data.changelogs;
|
|
303
|
+
if (changelogs.length === 0) {
|
|
304
|
+
console.log("No changelogs yet.");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
table(
|
|
308
|
+
["Version", "Title", "Status", "Date"],
|
|
309
|
+
changelogs.map((c) => [
|
|
310
|
+
c.version,
|
|
311
|
+
c.title || "",
|
|
312
|
+
c.isPublished ? "published" : "draft",
|
|
313
|
+
new Date(c.releaseDate).toLocaleDateString()
|
|
314
|
+
])
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/commands/publish.ts
|
|
319
|
+
async function findChangelog(projectId, version) {
|
|
320
|
+
const res = await get(
|
|
321
|
+
`/api/projects/${projectId}/changelogs`
|
|
322
|
+
);
|
|
323
|
+
if (!res.ok) fatal(res.error);
|
|
324
|
+
const match = res.data.changelogs.find((c) => c.version === version);
|
|
325
|
+
if (!match) fatal(`Changelog v${version} not found`);
|
|
326
|
+
return match;
|
|
327
|
+
}
|
|
328
|
+
async function publish(opts) {
|
|
329
|
+
const projectId = await resolveProjectId(opts.project);
|
|
330
|
+
const changelog2 = await findChangelog(projectId, opts.version);
|
|
331
|
+
if (changelog2.isPublished) {
|
|
332
|
+
console.log(`v${opts.version} is already published.`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const res = await patch(
|
|
336
|
+
`/api/projects/${projectId}/changelogs/${changelog2.id}`,
|
|
337
|
+
{ isPublished: true }
|
|
338
|
+
);
|
|
339
|
+
if (!res.ok) fatal(res.error);
|
|
340
|
+
console.log(`Published v${opts.version}.`);
|
|
341
|
+
}
|
|
342
|
+
async function unpublish(opts) {
|
|
343
|
+
const projectId = await resolveProjectId(opts.project);
|
|
344
|
+
const changelog2 = await findChangelog(projectId, opts.version);
|
|
345
|
+
if (!changelog2.isPublished) {
|
|
346
|
+
console.log(`v${opts.version} is already a draft.`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const res = await patch(
|
|
350
|
+
`/api/projects/${projectId}/changelogs/${changelog2.id}`,
|
|
351
|
+
{ isPublished: false, publishedAt: null }
|
|
352
|
+
);
|
|
353
|
+
if (!res.ok) fatal(res.error);
|
|
354
|
+
console.log(`Unpublished v${opts.version}.`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/commands/sync.ts
|
|
358
|
+
async function sync(opts) {
|
|
359
|
+
const projectId = await resolveProjectId(opts.project);
|
|
360
|
+
console.log("Syncing GitHub releases...");
|
|
361
|
+
const res = await post(
|
|
362
|
+
`/api/projects/${projectId}/sync`,
|
|
363
|
+
{}
|
|
364
|
+
);
|
|
365
|
+
if (!res.ok) fatal(res.error);
|
|
366
|
+
console.log(`Sync started (job: ${res.data.jobId}).`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/commands/generate.ts
|
|
370
|
+
async function generate(opts) {
|
|
371
|
+
const projectId = await resolveProjectId(opts.project);
|
|
372
|
+
const listRes = await get(
|
|
373
|
+
`/api/projects/${projectId}/changelogs`
|
|
374
|
+
);
|
|
375
|
+
if (!listRes.ok) fatal(listRes.error);
|
|
376
|
+
const changelog2 = listRes.data.changelogs.find(
|
|
377
|
+
(c) => c.version === opts.version
|
|
378
|
+
);
|
|
379
|
+
if (!changelog2) fatal(`Changelog v${opts.version} not found`);
|
|
380
|
+
console.log("Regenerating changelog with AI...");
|
|
381
|
+
const res = await post(
|
|
382
|
+
`/api/projects/${projectId}/changelogs/generate`,
|
|
383
|
+
{ changelogId: changelog2.id }
|
|
384
|
+
);
|
|
385
|
+
if (!res.ok) fatal(res.error);
|
|
386
|
+
console.log(
|
|
387
|
+
`Regenerated v${opts.version}: ${res.data.changelog.title || "done"}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/index.ts
|
|
392
|
+
var program = new Command();
|
|
393
|
+
program.name("clogg").description("CLI for clogg.one \u2014 manage changelogs from the terminal").version("0.1.0");
|
|
394
|
+
program.command("login").description("Authenticate with clogg.one").action(login);
|
|
395
|
+
program.command("logout").description("Clear stored credentials").action(logout);
|
|
396
|
+
program.command("whoami").description("Show current user and token info").action(whoami);
|
|
397
|
+
var project = program.command("project").description("Manage projects");
|
|
398
|
+
project.command("list").description("List all projects").action(listProjects);
|
|
399
|
+
project.command("create").description("Create a new project").requiredOption("--name <name>", "Project name").requiredOption("--slug <slug>", "URL slug").option("--description <desc>", "Project description").option("--github <owner/repo>", "GitHub repository (owner/repo)").action(createProject);
|
|
400
|
+
var changelog = program.command("changelog").description("Manage changelogs");
|
|
401
|
+
changelog.command("list").description("List changelogs for a project").requiredOption("--project <slug>", "Project slug").action(listChangelogs);
|
|
402
|
+
program.command("add").description("Add a new changelog entry").requiredOption("--project <slug>", "Project slug").requiredOption("--version <version>", "Version string (e.g. 1.2.3)").option("--content <markdown>", "Markdown content").option("--file <path>", "Read content from file").option("--title <title>", "Release title").action(add);
|
|
403
|
+
program.command("publish").description("Publish a changelog entry").requiredOption("--project <slug>", "Project slug").requiredOption("--version <version>", "Version to publish").action(publish);
|
|
404
|
+
program.command("unpublish").description("Unpublish a changelog entry").requiredOption("--project <slug>", "Project slug").requiredOption("--version <version>", "Version to unpublish").action(unpublish);
|
|
405
|
+
program.command("sync").description("Sync GitHub releases for a project").requiredOption("--project <slug>", "Project slug").action(sync);
|
|
406
|
+
program.command("generate").description("Regenerate AI content for a changelog").requiredOption("--project <slug>", "Project slug").requiredOption("--version <version>", "Version to regenerate").action(generate);
|
|
407
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clogg/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for clogg.one — manage changelogs from the terminal",
|
|
5
|
+
"bin": {
|
|
6
|
+
"clogg": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^13.0.0",
|
|
19
|
+
"open": "^10.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"homepage": "https://clogg.one",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"changelog",
|
|
30
|
+
"cli",
|
|
31
|
+
"clogg",
|
|
32
|
+
"github-releases",
|
|
33
|
+
"ai-changelog"
|
|
34
|
+
]
|
|
35
|
+
}
|