deploylog 0.3.0 → 0.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 +109 -30
- package/dist/api.d.ts +59 -1
- package/dist/api.js +43 -2
- package/dist/editor.d.ts +23 -0
- package/dist/editor.js +46 -0
- package/dist/entry-commands.d.ts +111 -0
- package/dist/entry-commands.js +157 -0
- package/dist/index.js +479 -108
- package/dist/init.d.ts +38 -0
- package/dist/init.js +86 -0
- package/dist/open.d.ts +33 -0
- package/dist/open.js +51 -0
- package/dist/project-config.d.ts +15 -5
- package/dist/project-config.js +43 -7
- package/dist/push.d.ts +71 -0
- package/dist/push.js +149 -0
- package/dist/resolve.d.ts +18 -0
- package/dist/resolve.js +16 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { setApiKey, setApiUrl, getConfigPath, clearConfig } from './config.js';
|
|
5
|
-
import { listProjects, listEntries,
|
|
4
|
+
import { setApiKey, setApiUrl, getApiUrl, getConfigPath, clearConfig } from './config.js';
|
|
5
|
+
import { listProjects, listEntries, createProject, importGithub, whoami, ApiError, } from './api.js';
|
|
6
6
|
import { readProjectConfig } from './project-config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { runPush, defaultPushDeps } from './push.js';
|
|
8
|
+
import { runSetPublished, runView, runEdit, runDelete, defaultEntryCommandDeps, } from './entry-commands.js';
|
|
9
|
+
import { runInit, defaultInitDeps } from './init.js';
|
|
10
|
+
import { runOpen } from './open.js';
|
|
8
11
|
const program = new Command();
|
|
9
12
|
program
|
|
10
13
|
.name('deploylog')
|
|
11
14
|
.description('Push changelog entries from the terminal')
|
|
12
|
-
|
|
15
|
+
// Without positional options, the global -V/--version greedily matches
|
|
16
|
+
// `push --version 1.4.0` and prints the CLI version instead of setting the
|
|
17
|
+
// entry's semver. Program options now must precede the subcommand.
|
|
18
|
+
.enablePositionalOptions()
|
|
19
|
+
.version('0.4.0');
|
|
13
20
|
// ─── login ──────────────────────────────────────────────────────────────────
|
|
14
21
|
program
|
|
15
22
|
.command('login')
|
|
@@ -69,12 +76,18 @@ program
|
|
|
69
76
|
console.log(chalk.green('Logged out. Credentials removed.'));
|
|
70
77
|
});
|
|
71
78
|
// ─── projects ───────────────────────────────────────────────────────────────
|
|
72
|
-
program
|
|
79
|
+
const projectsCmd = program
|
|
73
80
|
.command('projects')
|
|
81
|
+
.alias('proj')
|
|
74
82
|
.description('List projects in your organization')
|
|
75
|
-
.
|
|
83
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
84
|
+
.action(async (opts) => {
|
|
76
85
|
try {
|
|
77
86
|
const projects = await listProjects();
|
|
87
|
+
if (opts.json) {
|
|
88
|
+
printJson(projects);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
78
91
|
if (projects.length === 0) {
|
|
79
92
|
console.log(chalk.dim('No projects found.'));
|
|
80
93
|
return;
|
|
@@ -85,18 +98,92 @@ program
|
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
catch (err) {
|
|
88
|
-
handleError(err);
|
|
101
|
+
handleError(err, opts.json);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
projectsCmd
|
|
105
|
+
.command('create <name>')
|
|
106
|
+
.description('Create a new project (slug is generated from the name)')
|
|
107
|
+
.option('--url <url>', 'Project website URL')
|
|
108
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
109
|
+
.action(async (name, opts, command) => {
|
|
110
|
+
// The parent `projects` command also defines --json and commander hands
|
|
111
|
+
// the flag to whichever parsed it — honor both.
|
|
112
|
+
const json = Boolean(opts.json || command.optsWithGlobals().json);
|
|
113
|
+
try {
|
|
114
|
+
const project = await createProject(name, opts.url);
|
|
115
|
+
if (json) {
|
|
116
|
+
printJson(project);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log(`${chalk.green('✓')} Project created: ${chalk.bold(project.name)}`);
|
|
120
|
+
console.log(` Slug: ${chalk.cyan(project.slug)}`);
|
|
121
|
+
console.log(chalk.dim(` Public changelog: ${getApiUrl()}/p/${project.slug}/changelog`));
|
|
122
|
+
console.log(chalk.dim(' Run `deploylog init` in the repo to wire pushes to it.'));
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
handleError(err, json);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// ─── whoami ─────────────────────────────────────────────────────────────────
|
|
129
|
+
program
|
|
130
|
+
.command('whoami')
|
|
131
|
+
.description('Show the authenticated org, plan, API key, and AI usage')
|
|
132
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
133
|
+
.action(async (opts) => {
|
|
134
|
+
try {
|
|
135
|
+
const me = await whoami();
|
|
136
|
+
if (opts.json) {
|
|
137
|
+
printJson({ ...me, api_url: getApiUrl(), config_path: getConfigPath() });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const limitLabel = me.ai_usage.limit === null ? '∞' : String(me.ai_usage.limit);
|
|
141
|
+
console.log(`${chalk.bold(me.organization.name)} ${chalk.dim(me.organization.slug)}`);
|
|
142
|
+
console.log(` Plan: ${chalk.cyan(me.organization.plan ?? 'free')}`);
|
|
143
|
+
console.log(` API key: ${me.api_key.name} ${chalk.dim(`${me.api_key.prefix}… [${me.api_key.permissions.join(', ')}]`)}`);
|
|
144
|
+
if (me.api_key.last_used_at) {
|
|
145
|
+
console.log(chalk.dim(` Last used: ${me.api_key.last_used_at}`));
|
|
146
|
+
}
|
|
147
|
+
console.log(` AI usage: ${me.ai_usage.used}/${limitLabel} this month ${chalk.dim(`(${me.ai_usage.month_key})`)}`);
|
|
148
|
+
console.log(chalk.dim(` API URL: ${getApiUrl()}`));
|
|
149
|
+
console.log(chalk.dim(` Config: ${getConfigPath()}`));
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
handleError(err, opts.json);
|
|
89
153
|
}
|
|
90
154
|
});
|
|
91
155
|
// ─── list ───────────────────────────────────────────────────────────────────
|
|
92
156
|
program
|
|
93
157
|
.command('list')
|
|
158
|
+
.alias('ls')
|
|
94
159
|
.description('List recent entries for a project')
|
|
95
160
|
.option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
|
|
161
|
+
.option('--drafts', 'Only drafts')
|
|
162
|
+
.option('--published', 'Only published entries')
|
|
163
|
+
.option('-T, --type <type>', 'Filter by entry type')
|
|
164
|
+
.option('-n, --limit <n>', 'Max entries to show (1-50)', (v) => parseInt(v, 10))
|
|
165
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
96
166
|
.action(async (opts) => {
|
|
97
167
|
try {
|
|
168
|
+
if (opts.drafts && opts.published) {
|
|
169
|
+
console.error(chalk.red('Use at most one of --drafts / --published.'));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
98
172
|
const slug = resolveProject(opts.project);
|
|
99
|
-
const
|
|
173
|
+
const filters = {};
|
|
174
|
+
if (opts.drafts)
|
|
175
|
+
filters.status = 'draft';
|
|
176
|
+
if (opts.published)
|
|
177
|
+
filters.status = 'published';
|
|
178
|
+
if (opts.type)
|
|
179
|
+
filters.type = opts.type;
|
|
180
|
+
if (opts.limit !== undefined)
|
|
181
|
+
filters.limit = opts.limit;
|
|
182
|
+
const entries = await listEntries(slug, filters);
|
|
183
|
+
if (opts.json) {
|
|
184
|
+
printJson(entries);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
100
187
|
if (entries.length === 0) {
|
|
101
188
|
console.log(chalk.dim('No entries found.'));
|
|
102
189
|
return;
|
|
@@ -113,10 +200,57 @@ program
|
|
|
113
200
|
day: 'numeric',
|
|
114
201
|
});
|
|
115
202
|
console.log(` ${status} ${e.title} ${type} ${version} ${chalk.dim(date)}`);
|
|
203
|
+
console.log(` ${chalk.dim(`${e.slug} ${e.id}`)}`);
|
|
204
|
+
}
|
|
205
|
+
const effectiveLimit = filters.limit ?? 50;
|
|
206
|
+
if (entries.length >= effectiveLimit) {
|
|
207
|
+
console.log(chalk.dim(`\nShowing the ${entries.length} most recent. Older entries: narrow with --drafts / --published / --type, or reference by id.`));
|
|
116
208
|
}
|
|
117
209
|
}
|
|
118
210
|
catch (err) {
|
|
119
|
-
handleError(err);
|
|
211
|
+
handleError(err, opts.json);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// ─── view ───────────────────────────────────────────────────────────────────
|
|
215
|
+
program
|
|
216
|
+
.command('view <entry>')
|
|
217
|
+
.alias('show')
|
|
218
|
+
.description('Show a full entry (slug or id), including its markdown body')
|
|
219
|
+
.option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
|
|
220
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
221
|
+
.action(async (ref, opts) => {
|
|
222
|
+
try {
|
|
223
|
+
const result = await runView({ ref, project: opts.project });
|
|
224
|
+
if (result.kind !== 'found') {
|
|
225
|
+
if (opts.json) {
|
|
226
|
+
printJsonError('NOT_FOUND', result.message);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.error(chalk.red(result.message));
|
|
230
|
+
}
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
const e = result.entry;
|
|
234
|
+
if (opts.json) {
|
|
235
|
+
printJson(e);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const status = e.published ? chalk.green('published') : chalk.yellow('draft');
|
|
239
|
+
console.log(`${chalk.bold(e.title)} ${status}`);
|
|
240
|
+
console.log(chalk.dim(` id: ${e.id}`));
|
|
241
|
+
console.log(chalk.dim(` slug: ${e.slug}`));
|
|
242
|
+
if (e.entry_type)
|
|
243
|
+
console.log(chalk.dim(` type: ${e.entry_type}`));
|
|
244
|
+
if (e.version)
|
|
245
|
+
console.log(chalk.dim(` version: v${e.version}`));
|
|
246
|
+
console.log(chalk.dim(` created: ${e.created_at}`));
|
|
247
|
+
if (e.published_at)
|
|
248
|
+
console.log(chalk.dim(` published: ${e.published_at}`));
|
|
249
|
+
console.log();
|
|
250
|
+
console.log(e.body_markdown);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
handleError(err, opts.json);
|
|
120
254
|
}
|
|
121
255
|
});
|
|
122
256
|
// ─── push ───────────────────────────────────────────────────────────────────
|
|
@@ -135,121 +269,341 @@ program
|
|
|
135
269
|
.option('-a, --ai-summarize', 'Rewrite the entry with Claude Haiku (user-friendly release notes)')
|
|
136
270
|
.option('--ai', 'Alias of --ai-summarize')
|
|
137
271
|
.option('-y, --yes', 'Skip interactive confirmation for AI-generated content')
|
|
272
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
138
273
|
.action(async (opts) => {
|
|
139
274
|
try {
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
275
|
+
// JSON mode is a machine contract: no prompts (non-TTY path), progress
|
|
276
|
+
// and notices to stderr, stdout reserved for the data payload.
|
|
277
|
+
const deps = opts.json
|
|
278
|
+
? {
|
|
279
|
+
...defaultPushDeps,
|
|
280
|
+
isTTY: false,
|
|
281
|
+
out: {
|
|
282
|
+
progressStart: (msg) => process.stderr.write(`${msg} `),
|
|
283
|
+
progressDone: (msg) => process.stderr.write(`${msg}\n`),
|
|
284
|
+
notice: (msg) => console.error(msg),
|
|
285
|
+
aiPreview: () => { },
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
: defaultPushDeps;
|
|
289
|
+
const result = await runPush(opts, deps);
|
|
290
|
+
switch (result.kind) {
|
|
291
|
+
case 'created': {
|
|
292
|
+
const entry = result.entry;
|
|
293
|
+
if (opts.json) {
|
|
294
|
+
printJson(entry);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
const status = entry.published ? chalk.green('Published') : chalk.yellow('Draft');
|
|
298
|
+
console.log(`\n${chalk.green('✓')} Entry created: ${chalk.bold(entry.title)}`);
|
|
299
|
+
console.log(` Status: ${status}`);
|
|
300
|
+
console.log(` Slug: ${chalk.dim(entry.slug)}`);
|
|
301
|
+
if (entry.version)
|
|
302
|
+
console.log(` Version: ${chalk.dim(`v${entry.version}`)}`);
|
|
303
|
+
break;
|
|
154
304
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
305
|
+
case 'cancelled':
|
|
306
|
+
// User declined at the confirm prompt — a clean, non-error stop.
|
|
307
|
+
console.log(chalk.yellow(result.message));
|
|
308
|
+
break;
|
|
309
|
+
case 'no-commits':
|
|
310
|
+
// Valid command, nothing to do — warn (yellow) and exit non-zero.
|
|
311
|
+
if (opts.json)
|
|
312
|
+
printJsonError('NO_COMMITS', result.message);
|
|
313
|
+
else
|
|
314
|
+
console.error(chalk.yellow(result.message));
|
|
164
315
|
process.exit(1);
|
|
316
|
+
default:
|
|
317
|
+
// no-ai-source | missing-fields — a misuse refusal.
|
|
318
|
+
if (opts.json)
|
|
319
|
+
printJsonError(result.kind.toUpperCase().replace(/-/g, '_'), result.message);
|
|
320
|
+
else
|
|
321
|
+
console.error(chalk.red(result.message));
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
handleError(err, opts.json);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
// ─── edit ───────────────────────────────────────────────────────────────────
|
|
330
|
+
program
|
|
331
|
+
.command('edit <entry>')
|
|
332
|
+
.description('Edit an entry (slug or id) via flags, --body-file, or $EDITOR')
|
|
333
|
+
.option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
|
|
334
|
+
.option('-t, --title <title>', "New title (may change a draft's slug)")
|
|
335
|
+
.option('-T, --type <type>', 'Entry type: feature, fix, improvement, breaking, announcement')
|
|
336
|
+
.option('--version <version>', 'Semver version (pass "" to clear)')
|
|
337
|
+
.option('-b, --body <markdown>', 'New body (Markdown)')
|
|
338
|
+
.option('--body-file <path>', 'Read the new body from a file (- for stdin)')
|
|
339
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
340
|
+
.action(async (ref, opts) => {
|
|
341
|
+
try {
|
|
342
|
+
// JSON mode never opens $EDITOR — force the non-interactive path.
|
|
343
|
+
const deps = opts.json ? { ...defaultEntryCommandDeps, isTTY: false } : undefined;
|
|
344
|
+
const result = await runEdit({ ref, ...opts }, deps);
|
|
345
|
+
switch (result.kind) {
|
|
346
|
+
case 'updated': {
|
|
347
|
+
const e = result.entry;
|
|
348
|
+
if (opts.json) {
|
|
349
|
+
printJson({ ...e, previous_slug: result.previousSlug });
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
console.log(`${chalk.green('✓')} Updated: ${chalk.bold(e.title)}`);
|
|
353
|
+
if (e.slug !== result.previousSlug) {
|
|
354
|
+
console.log(chalk.yellow(` Slug changed from '${result.previousSlug}' to '${e.slug}'.`));
|
|
355
|
+
console.log(chalk.dim(' Scripts referencing the old slug should switch to the id.'));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
console.log(` Slug: ${chalk.dim(e.slug)}`);
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
165
361
|
}
|
|
362
|
+
case 'unchanged':
|
|
363
|
+
case 'cancelled':
|
|
364
|
+
if (opts.json)
|
|
365
|
+
printJsonError(result.kind.toUpperCase(), result.message);
|
|
366
|
+
else
|
|
367
|
+
console.log(chalk.yellow(result.message));
|
|
368
|
+
break;
|
|
369
|
+
case 'body-rejected':
|
|
370
|
+
if (opts.json)
|
|
371
|
+
printJsonError('VALIDATION_ERROR', result.message);
|
|
372
|
+
else
|
|
373
|
+
console.error(chalk.red(result.message));
|
|
374
|
+
process.exit(1);
|
|
375
|
+
default:
|
|
376
|
+
if (opts.json)
|
|
377
|
+
printJsonError(result.kind.toUpperCase().replace(/-/g, '_'), result.message);
|
|
378
|
+
else
|
|
379
|
+
console.error(chalk.red(result.message));
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
handleError(err, opts.json);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
// ─── import ─────────────────────────────────────────────────────────────────
|
|
388
|
+
const importCmd = program
|
|
389
|
+
.command('import')
|
|
390
|
+
.description('Import entries from an external source');
|
|
391
|
+
importCmd
|
|
392
|
+
.command('github <repo>')
|
|
393
|
+
.description('Import GitHub releases as draft entries (owner/repo or a github.com URL)')
|
|
394
|
+
.option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
|
|
395
|
+
.option('--token <token>', 'GitHub personal access token for private repos / rate limits (or set DEPLOYLOG_GITHUB_TOKEN); never stored')
|
|
396
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
397
|
+
.action(async (repo, opts) => {
|
|
398
|
+
try {
|
|
399
|
+
const slug = resolveProject(opts.project);
|
|
400
|
+
const token = opts.token ?? process.env.DEPLOYLOG_GITHUB_TOKEN;
|
|
401
|
+
const result = await importGithub(slug, repo, token);
|
|
402
|
+
if (opts.json) {
|
|
403
|
+
printJson(result);
|
|
404
|
+
return;
|
|
166
405
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
406
|
+
if (result.imported === 0 && result.skipped === 0) {
|
|
407
|
+
console.log(chalk.yellow('No releases found to import.'));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
console.log(`${chalk.green('✓')} Imported ${chalk.bold(String(result.imported))} release${result.imported === 1 ? '' : 's'} as drafts` +
|
|
411
|
+
(result.skipped > 0 ? chalk.dim(` (${result.skipped} already imported, skipped)`) : ''));
|
|
412
|
+
if (result.imported > 0) {
|
|
413
|
+
console.log(chalk.dim(' Review with `deploylog list --drafts`, then `deploylog publish <slug>`.'));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
handleError(err, opts.json);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
// ─── init ───────────────────────────────────────────────────────────────────
|
|
421
|
+
program
|
|
422
|
+
.command('init')
|
|
423
|
+
.description('Scaffold a .deploylog.yml in this directory (project + optional default type)')
|
|
424
|
+
.option('-p, --project <slug>', 'Project slug (interactive pick if omitted)')
|
|
425
|
+
.option('-T, --type <type>', 'Default entry type for pushes from this repo')
|
|
426
|
+
.option('--force', 'Overwrite an existing .deploylog.yml')
|
|
427
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
428
|
+
.action(async (opts) => {
|
|
429
|
+
try {
|
|
430
|
+
const deps = opts.json ? { ...defaultInitDeps, isTTY: false } : undefined;
|
|
431
|
+
const result = await runInit(opts, deps);
|
|
432
|
+
switch (result.kind) {
|
|
433
|
+
case 'written':
|
|
434
|
+
if (opts.json) {
|
|
435
|
+
printJson({ path: result.path, project: result.project, default_type: result.defaultType ?? null });
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
console.log(`${chalk.green('✓')} Wrote ${result.path}`);
|
|
439
|
+
console.log(` project: ${chalk.cyan(result.project)}`);
|
|
440
|
+
if (result.defaultType)
|
|
441
|
+
console.log(` default_type: ${chalk.cyan(result.defaultType)}`);
|
|
442
|
+
console.log(chalk.dim(' `deploylog push` in this directory now targets that project.'));
|
|
443
|
+
break;
|
|
444
|
+
case 'cancelled':
|
|
445
|
+
console.log(chalk.yellow(result.message));
|
|
446
|
+
break;
|
|
447
|
+
default:
|
|
448
|
+
if (opts.json)
|
|
449
|
+
printJsonError(result.kind.toUpperCase().replace(/-/g, '_'), result.message);
|
|
450
|
+
else
|
|
451
|
+
console.error(chalk.red(result.message));
|
|
176
452
|
process.exit(1);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
handleError(err, opts.json);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
// ─── delete ─────────────────────────────────────────────────────────────────
|
|
460
|
+
program
|
|
461
|
+
.command('delete <entry>')
|
|
462
|
+
.alias('rm')
|
|
463
|
+
.description('Delete an entry permanently (slug or id)')
|
|
464
|
+
.option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
|
|
465
|
+
.option('-y, --yes', 'Skip the confirmation prompt (required in non-interactive mode)')
|
|
466
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
467
|
+
.action(async (ref, opts) => {
|
|
468
|
+
try {
|
|
469
|
+
// JSON mode never prompts — deletion then requires an explicit --yes.
|
|
470
|
+
const deps = opts.json ? { ...defaultEntryCommandDeps, isTTY: false } : undefined;
|
|
471
|
+
const result = await runDelete({ ref, project: opts.project, yes: opts.yes }, deps);
|
|
472
|
+
switch (result.kind) {
|
|
473
|
+
case 'deleted':
|
|
474
|
+
if (opts.json) {
|
|
475
|
+
printJson({ id: result.id, title: result.title, deleted: true });
|
|
476
|
+
break;
|
|
195
477
|
}
|
|
478
|
+
console.log(`${chalk.green('✓')} Deleted: ${chalk.bold(result.title)}`);
|
|
479
|
+
break;
|
|
480
|
+
case 'cancelled':
|
|
481
|
+
console.log(chalk.yellow(result.message));
|
|
482
|
+
break;
|
|
483
|
+
default:
|
|
484
|
+
if (opts.json)
|
|
485
|
+
printJsonError(result.kind.toUpperCase().replace(/-/g, '_'), result.message);
|
|
486
|
+
else
|
|
487
|
+
console.error(chalk.red(result.message));
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
handleError(err, opts.json);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
// ─── publish / unpublish ────────────────────────────────────────────────────
|
|
496
|
+
program
|
|
497
|
+
.command('publish <entry>')
|
|
498
|
+
.alias('pub')
|
|
499
|
+
.description('Publish a draft entry (slug or id); sends the email digest on Pro')
|
|
500
|
+
.option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
|
|
501
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
502
|
+
.action(async (ref, opts) => {
|
|
503
|
+
try {
|
|
504
|
+
const result = await runSetPublished({ ref, project: opts.project, publish: true });
|
|
505
|
+
switch (result.kind) {
|
|
506
|
+
case 'updated': {
|
|
507
|
+
const e = result.entry;
|
|
508
|
+
if (opts.json) {
|
|
509
|
+
printJson(e);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
if (!e.changed) {
|
|
513
|
+
console.log(chalk.yellow(`Entry is already published — nothing to do.`));
|
|
514
|
+
console.log(` ${chalk.dim('Slug:')} ${e.slug}`);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
console.log(`${chalk.green('✓')} Published: ${chalk.bold(e.title)}`);
|
|
518
|
+
console.log(` Slug: ${chalk.dim(e.slug)}`);
|
|
519
|
+
console.log(chalk.dim(' Subscribers get the email digest (Pro plans, first publish only).'));
|
|
520
|
+
break;
|
|
196
521
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
522
|
+
default:
|
|
523
|
+
if (opts.json)
|
|
524
|
+
printJsonError(result.kind.toUpperCase().replace(/-/g, '_'), result.message);
|
|
525
|
+
else
|
|
526
|
+
console.error(chalk.red(result.message));
|
|
527
|
+
process.exit(1);
|
|
202
528
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
handleError(err, opts.json);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
program
|
|
535
|
+
.command('unpublish <entry>')
|
|
536
|
+
.alias('unpub')
|
|
537
|
+
.description('Revert a published entry to draft (its public URL and feeds drop it)')
|
|
538
|
+
.option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
|
|
539
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
540
|
+
.action(async (ref, opts) => {
|
|
541
|
+
try {
|
|
542
|
+
const result = await runSetPublished({ ref, project: opts.project, publish: false });
|
|
543
|
+
switch (result.kind) {
|
|
544
|
+
case 'updated': {
|
|
545
|
+
const e = result.entry;
|
|
546
|
+
if (opts.json) {
|
|
547
|
+
printJson(e);
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
if (!e.changed) {
|
|
551
|
+
console.log(chalk.yellow(`Entry is already a draft — nothing to do.`));
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
console.log(`${chalk.green('✓')} Unpublished: ${chalk.bold(e.title)}`);
|
|
555
|
+
console.log(chalk.dim(' The public page and feeds drop it (cached copies may lag a few minutes).\n' +
|
|
556
|
+
' Republishing sets a new publish date.'));
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
default:
|
|
560
|
+
if (opts.json)
|
|
561
|
+
printJsonError(result.kind.toUpperCase().replace(/-/g, '_'), result.message);
|
|
562
|
+
else
|
|
563
|
+
console.error(chalk.red(result.message));
|
|
564
|
+
process.exit(1);
|
|
206
565
|
}
|
|
207
|
-
|
|
208
|
-
|
|
566
|
+
}
|
|
567
|
+
catch (err) {
|
|
568
|
+
handleError(err, opts.json);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
// ─── open ───────────────────────────────────────────────────────────────────
|
|
572
|
+
program
|
|
573
|
+
.command('open [entry]')
|
|
574
|
+
.alias('o')
|
|
575
|
+
.description("Open the project's public changelog (or one entry's page) in the browser")
|
|
576
|
+
.option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
|
|
577
|
+
.option('--json', 'Output JSON (machine-readable, never prompts)')
|
|
578
|
+
.action(async (ref, opts) => {
|
|
579
|
+
try {
|
|
580
|
+
const result = await runOpen({ ref, project: opts.project });
|
|
581
|
+
switch (result.kind) {
|
|
582
|
+
case 'opened':
|
|
583
|
+
if (opts.json)
|
|
584
|
+
printJson({ url: result.url, opened: true });
|
|
585
|
+
else
|
|
586
|
+
console.log(chalk.dim(result.url));
|
|
587
|
+
break;
|
|
588
|
+
case 'printed':
|
|
589
|
+
if (opts.json)
|
|
590
|
+
printJson({ url: result.url, opened: false });
|
|
591
|
+
else
|
|
592
|
+
console.log(result.url);
|
|
593
|
+
break;
|
|
594
|
+
default:
|
|
595
|
+
if (opts.json)
|
|
596
|
+
printJsonError(result.kind.toUpperCase().replace(/-/g, '_'), result.message);
|
|
597
|
+
else
|
|
598
|
+
console.error(chalk.red(result.message));
|
|
599
|
+
process.exit(1);
|
|
209
600
|
}
|
|
210
|
-
const entry = await createEntry(slug, {
|
|
211
|
-
title,
|
|
212
|
-
body_markdown: body,
|
|
213
|
-
entry_type: entryType,
|
|
214
|
-
version,
|
|
215
|
-
publish: opts.publish && !opts.draft,
|
|
216
|
-
});
|
|
217
|
-
const status = entry.published
|
|
218
|
-
? chalk.green('Published')
|
|
219
|
-
: chalk.yellow('Draft');
|
|
220
|
-
console.log(`\n${chalk.green('✓')} Entry created: ${chalk.bold(entry.title)}`);
|
|
221
|
-
console.log(` Status: ${status}`);
|
|
222
|
-
console.log(` Slug: ${chalk.dim(entry.slug)}`);
|
|
223
|
-
if (entry.version)
|
|
224
|
-
console.log(` Version: ${chalk.dim(`v${entry.version}`)}`);
|
|
225
601
|
}
|
|
226
602
|
catch (err) {
|
|
227
|
-
handleError(err);
|
|
603
|
+
handleError(err, opts.json);
|
|
228
604
|
}
|
|
229
605
|
});
|
|
230
606
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
231
|
-
function printAiPreview(summary, usage) {
|
|
232
|
-
console.log();
|
|
233
|
-
console.log(chalk.bold('AI-generated entry:'));
|
|
234
|
-
console.log(` ${chalk.dim('Title:')} ${summary.title}`);
|
|
235
|
-
console.log(` ${chalk.dim('Type:')} ${summary.entry_type}`);
|
|
236
|
-
console.log(chalk.dim(' Body:'));
|
|
237
|
-
for (const line of summary.body_markdown.split('\n')) {
|
|
238
|
-
console.log(` ${line}`);
|
|
239
|
-
}
|
|
240
|
-
const limitLabel = usage.limit === null ? '∞' : String(usage.limit);
|
|
241
|
-
console.log(chalk.dim(` Usage: ${usage.used}/${limitLabel} this month (${usage.month_key})`));
|
|
242
|
-
console.log();
|
|
243
|
-
}
|
|
244
|
-
async function confirm(question) {
|
|
245
|
-
const { createInterface } = await import('node:readline');
|
|
246
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
247
|
-
const answer = await new Promise((resolve) => {
|
|
248
|
-
rl.question(`${question} [y/N] `, resolve);
|
|
249
|
-
});
|
|
250
|
-
rl.close();
|
|
251
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
252
|
-
}
|
|
253
607
|
function resolveProject(cliArg) {
|
|
254
608
|
if (cliArg)
|
|
255
609
|
return cliArg;
|
|
@@ -261,7 +615,24 @@ function resolveProject(cliArg) {
|
|
|
261
615
|
console.error(chalk.dim(' project: my-app'));
|
|
262
616
|
process.exit(1);
|
|
263
617
|
}
|
|
264
|
-
|
|
618
|
+
/** JSON mode: raw data on stdout, so output pipes into jq/scripts cleanly. */
|
|
619
|
+
function printJson(data) {
|
|
620
|
+
console.log(JSON.stringify(data, null, 2));
|
|
621
|
+
}
|
|
622
|
+
/** JSON mode errors go to stderr in the API's own error envelope shape. */
|
|
623
|
+
function printJsonError(code, message) {
|
|
624
|
+
console.error(JSON.stringify({ error: { code, message } }));
|
|
625
|
+
}
|
|
626
|
+
function handleError(err, json) {
|
|
627
|
+
if (json) {
|
|
628
|
+
if (err instanceof ApiError) {
|
|
629
|
+
printJsonError(err.code, err.message);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
printJsonError('ERROR', err instanceof Error ? err.message : 'An unknown error occurred');
|
|
633
|
+
}
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
265
636
|
if (err instanceof ApiError) {
|
|
266
637
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
267
638
|
if (err.status === 401) {
|