@evcraddock/slug-cli 0.2.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 +46 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +2184 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +125 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +18 -0
- package/dist/http.d.ts +27 -0
- package/dist/http.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +103 -0
- package/dist/output.d.ts +10 -0
- package/dist/output.js +15 -0
- package/package.json +31 -0
package/dist/commands.js
ADDED
|
@@ -0,0 +1,2184 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, basename, extname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { x as extractTarball } from "tar";
|
|
7
|
+
import { readConfig, setConfigApiBaseUrl, setConfigApiKey, toDisplayConfig, writeConfig, } from "./config.js";
|
|
8
|
+
import { CliError, createInvalidUsageError, ExitCode } from "./errors.js";
|
|
9
|
+
import { SlugHttpClient } from "./http.js";
|
|
10
|
+
import { writeJson } from "./output.js";
|
|
11
|
+
const HELP_TEXT = `slug - manage Slugkit sites
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
slug [--config <file>] --help
|
|
15
|
+
slug [--config <file>] --version
|
|
16
|
+
slug [--config <file>] version [--json]
|
|
17
|
+
slug [--config <file>] doctor [--json]
|
|
18
|
+
slug [--config <file>] login
|
|
19
|
+
slug [--config <file>] init <directory> --name <name> [--site-title <title>] [--template-url <url>] [--template-dir <dir>] [--json]
|
|
20
|
+
slug [--config <file>] post list [--type article|link|note] [--status draft|published|all] [--tag <slug>] [--json]
|
|
21
|
+
slug [--config <file>] post show <slug> [--json]
|
|
22
|
+
slug [--config <file>] post create --type article|link|note --slug <slug> --content <text> [--title <text>] [--url <url>] [--excerpt <text>] [--tag <slug>]... [--source-id <id>] [--author-id <id>]... [--json]
|
|
23
|
+
slug [--config <file>] post edit <slug> [--slug <new-slug>] [--title <text>] [--content <text>] [--url <url>] [--excerpt <text>] [--tag <slug>]... [--source-id <id>] [--author-id <id>]... [--json]
|
|
24
|
+
slug [--config <file>] post delete <slug> [--json]
|
|
25
|
+
slug [--config <file>] post publish <slug> [--json]
|
|
26
|
+
slug [--config <file>] post unpublish <slug> [--json]
|
|
27
|
+
slug [--config <file>] comment list [--post <slug>] [--status pending|approved|hidden|all] [--json]
|
|
28
|
+
slug [--config <file>] comment approve <id> [--json]
|
|
29
|
+
slug [--config <file>] comment hide <id> [--json]
|
|
30
|
+
slug [--config <file>] comments create <post-slug> --content <text> [--json]
|
|
31
|
+
slug [--config <file>] media upload <file> [--alt <text>] [--key <key>] [--json]
|
|
32
|
+
slug [--config <file>] media show <id> [--json]
|
|
33
|
+
slug [--config <file>] media delete <id> [--json]
|
|
34
|
+
slug [--config <file>] tag list [--json]
|
|
35
|
+
slug [--config <file>] sources list [--json]
|
|
36
|
+
slug [--config <file>] sources show <id> [--json]
|
|
37
|
+
slug [--config <file>] sources create --name <name> [--url <url>] [--description <text>] [--image-url <url>] [--favicon-url <url>] [--contact-id <id>]... [--json]
|
|
38
|
+
slug [--config <file>] sources edit <id> [--name <name>] [--url <url>] [--description <text>] [--image-url <url>] [--favicon-url <url>] [--contact-id <id>]... [--json]
|
|
39
|
+
slug [--config <file>] sources delete <id> [--json]
|
|
40
|
+
slug [--config <file>] accounts list [--owner-type contact|source|site] [--owner-id <id>] [--json]
|
|
41
|
+
slug [--config <file>] accounts show <id> [--json]
|
|
42
|
+
slug [--config <file>] accounts create --owner-type contact|source|site [--owner-id <id>] --label <label> --url <url> [--avatar-url <url>] [--kind <kind>] [--protocol <protocol>] [--default] [--sort-order <number>] [--json]
|
|
43
|
+
slug [--config <file>] accounts edit <id> [--label <label>] [--url <url>] [--avatar-url <url>] [--kind <kind>] [--protocol <protocol>] [--default] [--sort-order <number>] [--json]
|
|
44
|
+
slug [--config <file>] accounts delete <id> [--json]
|
|
45
|
+
slug [--config <file>] followers list [--json]
|
|
46
|
+
slug [--config <file>] engagement summary --post <slug> [--json]
|
|
47
|
+
slug [--config <file>] engagement list --post <slug> --type like|boost [--json]
|
|
48
|
+
slug [--config <file>] following list [--json]
|
|
49
|
+
slug [--config <file>] following follow <target> [--json]
|
|
50
|
+
slug [--config <file>] following unfollow <id> [--json]
|
|
51
|
+
slug [--config <file>] contact list [--json]
|
|
52
|
+
slug [--config <file>] contact show <id> [--json]
|
|
53
|
+
slug [--config <file>] contact create --name <name> [--url <url>] [--json]
|
|
54
|
+
slug [--config <file>] contact edit <id> [--name <name>] [--url <url>] [--json]
|
|
55
|
+
slug [--config <file>] contact delete <id> [--json]
|
|
56
|
+
slug [--config <file>] site config show [--json]
|
|
57
|
+
slug [--config <file>] site config set <field> <value> [--json]
|
|
58
|
+
slug [--config <file>] config show [--json]
|
|
59
|
+
slug [--config <file>] config set api-base-url <url>
|
|
60
|
+
slug [--config <file>] config set api-key <key>
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--config <file> Use a specific YAML config file.
|
|
64
|
+
--help Show this help.
|
|
65
|
+
--version Show the CLI version.
|
|
66
|
+
--json Print command output as JSON when supported.`;
|
|
67
|
+
export async function runCommand(context) {
|
|
68
|
+
const args = context.args;
|
|
69
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
70
|
+
context.writer.stdout(HELP_TEXT);
|
|
71
|
+
return { exitCode: ExitCode.Ok };
|
|
72
|
+
}
|
|
73
|
+
if (args[0] === "--version") {
|
|
74
|
+
context.writer.stdout(`slug ${context.packageVersion}`);
|
|
75
|
+
return { exitCode: ExitCode.Ok };
|
|
76
|
+
}
|
|
77
|
+
switch (args[0]) {
|
|
78
|
+
case "version":
|
|
79
|
+
return runVersionCommand(context, args.slice(1));
|
|
80
|
+
case "doctor":
|
|
81
|
+
return runDoctorCommand(context, args.slice(1));
|
|
82
|
+
case "login":
|
|
83
|
+
return runLoginCommand(context, args.slice(1));
|
|
84
|
+
case "init":
|
|
85
|
+
return runInitCommand(context, args.slice(1));
|
|
86
|
+
case "post":
|
|
87
|
+
return runPostsCommand(context, args.slice(1));
|
|
88
|
+
case "comment":
|
|
89
|
+
return runCommentCommand(context, args.slice(1));
|
|
90
|
+
case "comments":
|
|
91
|
+
return runCommentsCommand(context, args.slice(1));
|
|
92
|
+
case "media":
|
|
93
|
+
return runMediaCommand(context, args.slice(1));
|
|
94
|
+
case "tag":
|
|
95
|
+
return runTagsCommand(context, args.slice(1));
|
|
96
|
+
case "sources":
|
|
97
|
+
return runSourcesCommand(context, args.slice(1));
|
|
98
|
+
case "accounts":
|
|
99
|
+
return runAccountsCommand(context, args.slice(1));
|
|
100
|
+
case "followers":
|
|
101
|
+
return runFollowersCommand(context, args.slice(1));
|
|
102
|
+
case "engagement":
|
|
103
|
+
return runEngagementCommand(context, args.slice(1));
|
|
104
|
+
case "following":
|
|
105
|
+
return runFollowingCommand(context, args.slice(1));
|
|
106
|
+
case "contact":
|
|
107
|
+
return runContactCommand(context, args.slice(1));
|
|
108
|
+
case "site":
|
|
109
|
+
return runSiteCommand(context, args.slice(1));
|
|
110
|
+
case "config":
|
|
111
|
+
return runConfigCommand(context, args.slice(1));
|
|
112
|
+
default:
|
|
113
|
+
throw createInvalidUsageError(`Unknown command: ${args[0]}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function runVersionCommand(context, args) {
|
|
117
|
+
const json = readJsonFlag(args);
|
|
118
|
+
if (json) {
|
|
119
|
+
writeJson(context.writer, { version: context.packageVersion });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
context.writer.stdout(`slug ${context.packageVersion}`);
|
|
123
|
+
}
|
|
124
|
+
return { exitCode: ExitCode.Ok };
|
|
125
|
+
}
|
|
126
|
+
const SUPPORTED_API_MAJOR_VERSION = 1;
|
|
127
|
+
const SITE_TEMPLATE_PACKAGE_NAME = "@slugkit/template-site";
|
|
128
|
+
const SITE_TEMPLATE_RELEASE_BASE_URL = "https://forge.caradoc.com/erik/slugkit/releases/download";
|
|
129
|
+
const INIT_HELP_TEXT = `slug init - create a standalone Slugkit-compatible website
|
|
130
|
+
|
|
131
|
+
Usage:
|
|
132
|
+
slug init <directory> --name <name> [--site-title <title>] [--template-url <url>] [--template-dir <dir>] [--json]
|
|
133
|
+
|
|
134
|
+
Options:
|
|
135
|
+
--name <name> npm package name for the generated site.
|
|
136
|
+
--site-title <title> Human-readable site title. Defaults to a title derived from --name.
|
|
137
|
+
--template-url <url> Download a template tarball from a custom URL.
|
|
138
|
+
--template-dir <dir> Copy a template from a local directory.
|
|
139
|
+
--json Print command output as JSON.
|
|
140
|
+
--help, -h Show this help.
|
|
141
|
+
|
|
142
|
+
By default, slug init uses the repository-local template/site directory when available.
|
|
143
|
+
Installed CLI releases download the versioned template asset that matches the CLI version.`;
|
|
144
|
+
const SITE_INIT_NEXT_STEPS = [
|
|
145
|
+
"Install dependencies",
|
|
146
|
+
"Copy and edit .env",
|
|
147
|
+
"Run migrations",
|
|
148
|
+
"Start the dev server",
|
|
149
|
+
"Create an API key",
|
|
150
|
+
"Configure ActivityPub domain and actor settings",
|
|
151
|
+
"Configure slug and run slug doctor",
|
|
152
|
+
];
|
|
153
|
+
const TEMPLATE_COPY_EXCLUDES = new Set([
|
|
154
|
+
".env",
|
|
155
|
+
"node_modules",
|
|
156
|
+
"dist",
|
|
157
|
+
"build",
|
|
158
|
+
"data",
|
|
159
|
+
"coverage",
|
|
160
|
+
]);
|
|
161
|
+
async function runInitCommand(context, args) {
|
|
162
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
163
|
+
context.writer.stdout(INIT_HELP_TEXT);
|
|
164
|
+
return { exitCode: ExitCode.Ok };
|
|
165
|
+
}
|
|
166
|
+
const options = parseInitArgs(args);
|
|
167
|
+
const targetDirectory = resolve(options.directory);
|
|
168
|
+
const templateSource = await resolveInitTemplateSource(context, options);
|
|
169
|
+
try {
|
|
170
|
+
await assertDirectoryIsMissingOrEmpty(targetDirectory);
|
|
171
|
+
await mkdir(targetDirectory, { recursive: true });
|
|
172
|
+
await copyTemplateSite(templateSource.directory, targetDirectory);
|
|
173
|
+
await replaceGeneratedSitePlaceholders(targetDirectory, options);
|
|
174
|
+
await writeGeneratedSiteMarker(targetDirectory, options);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await templateSource.cleanup?.();
|
|
178
|
+
}
|
|
179
|
+
const data = {
|
|
180
|
+
directory: options.directory,
|
|
181
|
+
name: options.name,
|
|
182
|
+
siteTitle: options.siteTitle,
|
|
183
|
+
nextSteps: SITE_INIT_NEXT_STEPS,
|
|
184
|
+
};
|
|
185
|
+
if (options.json) {
|
|
186
|
+
writeJson(context.writer, { data });
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
context.writer.stdout(`Created Slugkit site: ${options.directory}`);
|
|
190
|
+
context.writer.stdout(`Package name: ${options.name}`);
|
|
191
|
+
context.writer.stdout(`Site title: ${options.siteTitle}`);
|
|
192
|
+
context.writer.stdout("Next steps:");
|
|
193
|
+
for (const step of SITE_INIT_NEXT_STEPS) {
|
|
194
|
+
context.writer.stdout(`- ${step}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { exitCode: ExitCode.Ok };
|
|
198
|
+
}
|
|
199
|
+
function parseInitArgs(args) {
|
|
200
|
+
let directory;
|
|
201
|
+
let name;
|
|
202
|
+
let siteTitle;
|
|
203
|
+
let templateUrl;
|
|
204
|
+
let templateDir;
|
|
205
|
+
let json = false;
|
|
206
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
207
|
+
const arg = args[index];
|
|
208
|
+
if (arg === "--json") {
|
|
209
|
+
json = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (arg === "--name") {
|
|
213
|
+
name = readRequiredInitOptionValue(args, index, "name");
|
|
214
|
+
index += 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (arg === "--site-title") {
|
|
218
|
+
siteTitle = readRequiredInitOptionValue(args, index, "site-title");
|
|
219
|
+
index += 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (arg === "--template-url") {
|
|
223
|
+
templateUrl = readRequiredInitOptionValue(args, index, "template-url");
|
|
224
|
+
index += 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (arg === "--template-dir") {
|
|
228
|
+
templateDir = readRequiredInitOptionValue(args, index, "template-dir");
|
|
229
|
+
index += 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (arg?.startsWith("--")) {
|
|
233
|
+
throw createInvalidUsageError(`Unsupported option: ${arg}`);
|
|
234
|
+
}
|
|
235
|
+
if (directory !== undefined || arg === undefined) {
|
|
236
|
+
throw createInvalidUsageError("Usage: slug init <directory> --name <name> [--site-title <title>] [--template-url <url>] [--template-dir <dir>] [--json]");
|
|
237
|
+
}
|
|
238
|
+
directory = arg;
|
|
239
|
+
}
|
|
240
|
+
if (directory === undefined || name === undefined) {
|
|
241
|
+
throw createInvalidUsageError("Usage: slug init <directory> --name <name> [--site-title <title>] [--template-url <url>] [--template-dir <dir>] [--json]");
|
|
242
|
+
}
|
|
243
|
+
const normalizedName = name.trim();
|
|
244
|
+
validatePackageName(normalizedName);
|
|
245
|
+
if (templateUrl !== undefined && templateDir !== undefined) {
|
|
246
|
+
throw createInvalidUsageError("Use only one of --template-url or --template-dir");
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
directory,
|
|
250
|
+
name: normalizedName,
|
|
251
|
+
siteTitle: siteTitle?.trim() || humanizePackageName(normalizedName),
|
|
252
|
+
json,
|
|
253
|
+
templateUrl: templateUrl?.trim() || undefined,
|
|
254
|
+
templateDir: templateDir?.trim() || undefined,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function readRequiredInitOptionValue(args, index, name) {
|
|
258
|
+
const value = args[index + 1];
|
|
259
|
+
if (value === undefined || value.startsWith("--")) {
|
|
260
|
+
throw createInvalidUsageError(`Option --${name} requires a value`);
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
function validatePackageName(name) {
|
|
265
|
+
if (name === "" || name.length > 214) {
|
|
266
|
+
throw createInvalidUsageError("--name must be a valid npm package name");
|
|
267
|
+
}
|
|
268
|
+
const packageNamePattern = /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/u;
|
|
269
|
+
if (!packageNamePattern.test(name) || name.includes("..")) {
|
|
270
|
+
throw createInvalidUsageError("--name must be a valid npm package name");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function humanizePackageName(name) {
|
|
274
|
+
const unscoped = name.includes("/") ? (name.split("/").at(-1) ?? name) : name;
|
|
275
|
+
return unscoped
|
|
276
|
+
.replace(/[._-]+/gu, " ")
|
|
277
|
+
.trim()
|
|
278
|
+
.replace(/\b\w/gu, (letter) => letter.toUpperCase());
|
|
279
|
+
}
|
|
280
|
+
async function assertDirectoryIsMissingOrEmpty(directory) {
|
|
281
|
+
try {
|
|
282
|
+
const stats = await stat(directory);
|
|
283
|
+
if (!stats.isDirectory()) {
|
|
284
|
+
throw createInvalidUsageError(`Target path is not a directory: ${directory}`);
|
|
285
|
+
}
|
|
286
|
+
const entries = await readdir(directory);
|
|
287
|
+
if (entries.length > 0) {
|
|
288
|
+
throw createInvalidUsageError(`Target directory is not empty: ${directory}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof CliError) {
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
if (isNodeErrorWithCode(error, "ENOENT")) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
throw createInvalidUsageError(`Cannot inspect target directory: ${directory}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function copyTemplateSite(source, destination) {
|
|
302
|
+
const entries = await readdir(source, { withFileTypes: true });
|
|
303
|
+
for (const entry of entries) {
|
|
304
|
+
if (shouldExcludeTemplateEntry(entry.name)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const sourcePath = join(source, entry.name);
|
|
308
|
+
const destinationPath = join(destination, entry.name);
|
|
309
|
+
if (entry.isDirectory()) {
|
|
310
|
+
await mkdir(destinationPath, { recursive: true });
|
|
311
|
+
await copyTemplateSite(sourcePath, destinationPath);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (entry.isFile()) {
|
|
315
|
+
await mkdir(dirname(destinationPath), { recursive: true });
|
|
316
|
+
await writeFile(destinationPath, await readFile(sourcePath));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function shouldExcludeTemplateEntry(name) {
|
|
321
|
+
return TEMPLATE_COPY_EXCLUDES.has(name) || name.endsWith(".log") || name.endsWith(".tsbuildinfo");
|
|
322
|
+
}
|
|
323
|
+
async function replaceGeneratedSitePlaceholders(directory, options) {
|
|
324
|
+
await updateJsonFile(join(directory, "package.json"), (value) => ({
|
|
325
|
+
...value,
|
|
326
|
+
name: options.name,
|
|
327
|
+
}));
|
|
328
|
+
await replaceTextFile(join(directory, "src", "config", "site.ts"), [
|
|
329
|
+
['name: "slugkit"', `name: ${JSON.stringify(options.siteTitle)}`],
|
|
330
|
+
[
|
|
331
|
+
'intro: "A personal website starter with feeds, posts, and federation in mind."',
|
|
332
|
+
`intro: ${JSON.stringify(`${options.siteTitle} is ready for posts, feeds, and federation.`)}`,
|
|
333
|
+
],
|
|
334
|
+
]);
|
|
335
|
+
await replaceTextFile(join(directory, "README.md"), [
|
|
336
|
+
["# Slugkit Template Site", `# ${options.siteTitle}`],
|
|
337
|
+
[
|
|
338
|
+
"This is the in-repo Slugkit template website. It is used for local CLI/API testing and is the source directory that future `slug init` work should copy into standalone site projects.\n\nGenerated sites should contain this website code, not the `slug` CLI source code.",
|
|
339
|
+
"This is a standalone Slugkit-compatible website generated from the Slugkit template. This site owns its code and can be customized freely while preserving the Slugkit API contract.",
|
|
340
|
+
],
|
|
341
|
+
[
|
|
342
|
+
"From the repository root:\n\n```bash\nmake dev\n```\n\nFrom this directory:\n\n```bash\nnpm run dev\n```",
|
|
343
|
+
"From this generated site directory:\n\n```bash\nnpm run dev\n```",
|
|
344
|
+
],
|
|
345
|
+
[
|
|
346
|
+
"Run migrations from the repository root:",
|
|
347
|
+
"Run migrations from this generated site directory:",
|
|
348
|
+
],
|
|
349
|
+
["npm run db:migrate --workspace @slugkit/template-site", "npm run db:migrate"],
|
|
350
|
+
["npm run db:status --workspace @slugkit/template-site", "npm run db:status"],
|
|
351
|
+
[
|
|
352
|
+
"In repository-local development, run `make garage-setup` once from the repository root to create the local Garage bucket/key and fill the secret values below before normal `make dev` use.",
|
|
353
|
+
"For local media development, configure these values for a local or remote S3-compatible service before uploading media.",
|
|
354
|
+
],
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
async function updateJsonFile(path, update) {
|
|
358
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
359
|
+
await writeFile(path, `${JSON.stringify(update(value), null, 2)}\n`, "utf8");
|
|
360
|
+
}
|
|
361
|
+
async function replaceTextFile(path, replacements) {
|
|
362
|
+
let content = await readFile(path, "utf8");
|
|
363
|
+
for (const [search, replacement] of replacements) {
|
|
364
|
+
content = content.replace(search, replacement);
|
|
365
|
+
}
|
|
366
|
+
await writeFile(path, content, "utf8");
|
|
367
|
+
}
|
|
368
|
+
async function writeGeneratedSiteMarker(directory, options) {
|
|
369
|
+
const marker = {
|
|
370
|
+
slugkitSite: true,
|
|
371
|
+
name: options.name,
|
|
372
|
+
siteTitle: options.siteTitle,
|
|
373
|
+
template: "slugkit/template-site",
|
|
374
|
+
generatedAt: new Date().toISOString(),
|
|
375
|
+
};
|
|
376
|
+
await writeFile(join(directory, ".slugkit-site.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
377
|
+
}
|
|
378
|
+
async function resolveInitTemplateSource(context, options) {
|
|
379
|
+
if (options.templateDir !== undefined) {
|
|
380
|
+
const directory = resolve(options.templateDir);
|
|
381
|
+
await assertTemplateSiteDirectory(directory);
|
|
382
|
+
return { directory };
|
|
383
|
+
}
|
|
384
|
+
if (options.templateUrl !== undefined) {
|
|
385
|
+
return downloadTemplateSite(context, options.templateUrl);
|
|
386
|
+
}
|
|
387
|
+
const localTemplateDirectory = findTemplateSiteDirectory(context.templateSiteDirectory);
|
|
388
|
+
if (localTemplateDirectory !== undefined) {
|
|
389
|
+
return { directory: localTemplateDirectory };
|
|
390
|
+
}
|
|
391
|
+
return downloadTemplateSite(context, getDefaultTemplateAssetUrl(context));
|
|
392
|
+
}
|
|
393
|
+
function getDefaultTemplateAssetUrl(context) {
|
|
394
|
+
const version = context.packageVersion.trim().replace(/^v/, "");
|
|
395
|
+
const baseUrl = context.templateAssetBaseUrl ?? SITE_TEMPLATE_RELEASE_BASE_URL;
|
|
396
|
+
const assetName = getTemplateAssetName(version);
|
|
397
|
+
return `${baseUrl.replace(/\/$/u, "")}/v${encodeURIComponent(version)}/${encodeURIComponent(assetName)}`;
|
|
398
|
+
}
|
|
399
|
+
function getTemplateAssetName(version) {
|
|
400
|
+
return `slugkit-site-template-v${version}.tgz`;
|
|
401
|
+
}
|
|
402
|
+
async function downloadTemplateSite(context, templateUrl) {
|
|
403
|
+
let parsedUrl;
|
|
404
|
+
try {
|
|
405
|
+
parsedUrl = new URL(templateUrl);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
throw createInvalidUsageError(`Invalid template URL: ${templateUrl}`);
|
|
409
|
+
}
|
|
410
|
+
if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") {
|
|
411
|
+
throw createInvalidUsageError("Template URL must use http or https");
|
|
412
|
+
}
|
|
413
|
+
const fetchImpl = context.fetchImpl ?? fetch;
|
|
414
|
+
const response = await fetchImpl(parsedUrl);
|
|
415
|
+
if (!response.ok) {
|
|
416
|
+
throw new CliError(`Failed to download Slugkit template from ${parsedUrl.toString()}: HTTP ${response.status}`, ExitCode.NetworkError);
|
|
417
|
+
}
|
|
418
|
+
const tempDirectory = await mkdtemp(join(tmpdir(), "slug-template-"));
|
|
419
|
+
const archivePath = join(tempDirectory, "template.tgz");
|
|
420
|
+
const extractDirectory = join(tempDirectory, "template");
|
|
421
|
+
await mkdir(extractDirectory, { recursive: true });
|
|
422
|
+
await writeFile(archivePath, Buffer.from(await response.arrayBuffer()));
|
|
423
|
+
await extractTarball({ file: archivePath, cwd: extractDirectory });
|
|
424
|
+
await assertTemplateSiteDirectory(extractDirectory);
|
|
425
|
+
return {
|
|
426
|
+
directory: extractDirectory,
|
|
427
|
+
cleanup: () => rm(tempDirectory, { force: true, recursive: true }),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function assertTemplateSiteDirectory(directory) {
|
|
431
|
+
try {
|
|
432
|
+
const packageJson = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
433
|
+
if (packageJson.name !== SITE_TEMPLATE_PACKAGE_NAME) {
|
|
434
|
+
throw createInvalidUsageError(`Template directory is not a Slugkit site template: ${directory}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
if (error instanceof CliError) {
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
throw createInvalidUsageError(`Template directory is not readable: ${directory}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function findTemplateSiteDirectory(templateSiteDirectory) {
|
|
445
|
+
if (templateSiteDirectory !== undefined) {
|
|
446
|
+
if (templateSiteDirectory === null) {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
return templateSiteDirectory;
|
|
450
|
+
}
|
|
451
|
+
const commandsDirectory = dirname(fileURLToPath(import.meta.url));
|
|
452
|
+
const candidates = [
|
|
453
|
+
resolve(commandsDirectory, "..", "..", "template", "site"),
|
|
454
|
+
resolve(process.cwd(), "template", "site"),
|
|
455
|
+
resolve(process.cwd(), "..", "template", "site"),
|
|
456
|
+
];
|
|
457
|
+
for (const candidate of candidates) {
|
|
458
|
+
try {
|
|
459
|
+
const packageJson = JSON.parse(readFileSync(join(candidate, "package.json"), "utf8"));
|
|
460
|
+
if (packageJson.name === SITE_TEMPLATE_PACKAGE_NAME) {
|
|
461
|
+
return candidate;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Try the next candidate.
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
function isNodeErrorWithCode(error, code) {
|
|
471
|
+
return isRecord(error) && error.code === code;
|
|
472
|
+
}
|
|
473
|
+
async function runDoctorCommand(context, args) {
|
|
474
|
+
const json = readJsonFlag(args);
|
|
475
|
+
const config = await readConfig(context.configPath);
|
|
476
|
+
const checks = [];
|
|
477
|
+
if (config.apiBaseUrl === undefined || config.apiBaseUrl.trim() === "") {
|
|
478
|
+
checks.push({ name: "config", status: "fail", message: "apiBaseUrl is not configured" });
|
|
479
|
+
return finishDoctor(context, { ok: false, checks }, json, ExitCode.InvalidUsage);
|
|
480
|
+
}
|
|
481
|
+
const apiBaseUrl = normalizeUrl(config.apiBaseUrl);
|
|
482
|
+
checks.push({ name: "config", status: "pass", message: `apiBaseUrl: ${apiBaseUrl}` });
|
|
483
|
+
const health = await requestDoctorJson(context, apiBaseUrl, "/health");
|
|
484
|
+
checks.push(createHealthCheck(health));
|
|
485
|
+
const meta = await requestDoctorJson(context, apiBaseUrl, "/meta");
|
|
486
|
+
checks.push(createMetaCheck(meta));
|
|
487
|
+
const openapi = await requestDoctorJson(context, apiBaseUrl, "/openapi.json");
|
|
488
|
+
checks.push(createOpenApiCheck(openapi));
|
|
489
|
+
if (config.apiKey === undefined || config.apiKey.trim() === "") {
|
|
490
|
+
checks.push({ name: "auth", status: "warn", message: "apiKey is not configured" });
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const auth = await requestDoctorJson(context, apiBaseUrl, "/auth/check", config.apiKey);
|
|
494
|
+
checks.push(createAuthCheck(auth));
|
|
495
|
+
}
|
|
496
|
+
const result = { ok: checks.every((check) => check.status !== "fail"), checks };
|
|
497
|
+
return finishDoctor(context, result, json, getDoctorExitCode(checks));
|
|
498
|
+
}
|
|
499
|
+
function finishDoctor(context, result, json, exitCode) {
|
|
500
|
+
if (json) {
|
|
501
|
+
writeJson(context.writer, result);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
context.writer.stdout(`Slug doctor: ${result.ok ? "ok" : "failed"}`);
|
|
505
|
+
for (const check of result.checks) {
|
|
506
|
+
const suffix = check.message === undefined ? "" : ` - ${check.message}`;
|
|
507
|
+
context.writer.stdout(`${check.status} ${check.name}${suffix}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return { exitCode };
|
|
511
|
+
}
|
|
512
|
+
async function requestDoctorJson(context, apiBaseUrl, path, apiKey) {
|
|
513
|
+
const fetchImpl = context.fetchImpl ?? fetch;
|
|
514
|
+
let response;
|
|
515
|
+
try {
|
|
516
|
+
response = await fetchImpl(`${apiBaseUrl}${path}`, {
|
|
517
|
+
headers: createDoctorHeaders(apiKey),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return { ok: false, kind: "network", message: "Network request failed" };
|
|
522
|
+
}
|
|
523
|
+
const data = await readResponseJson(response);
|
|
524
|
+
const apiMessage = readApiErrorMessage(data);
|
|
525
|
+
if (response.status === 501) {
|
|
526
|
+
return {
|
|
527
|
+
ok: false,
|
|
528
|
+
kind: "notImplemented",
|
|
529
|
+
message: apiMessage ?? "This site does not implement this operation.",
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (response.status === 401 || response.status === 403) {
|
|
533
|
+
return { ok: false, kind: "auth", message: apiMessage ?? "Authentication failed" };
|
|
534
|
+
}
|
|
535
|
+
if (!response.ok) {
|
|
536
|
+
return {
|
|
537
|
+
ok: false,
|
|
538
|
+
kind: "api",
|
|
539
|
+
message: apiMessage ?? `API request failed with status ${response.status}`,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return { ok: true, data };
|
|
543
|
+
}
|
|
544
|
+
function createDoctorHeaders(apiKey) {
|
|
545
|
+
const headers = { accept: "application/json" };
|
|
546
|
+
if (apiKey !== undefined) {
|
|
547
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
548
|
+
}
|
|
549
|
+
return headers;
|
|
550
|
+
}
|
|
551
|
+
async function readResponseJson(response) {
|
|
552
|
+
try {
|
|
553
|
+
return await response.json();
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function createHealthCheck(result) {
|
|
560
|
+
if (!result.ok) {
|
|
561
|
+
return { name: "health", status: "fail", message: result.message };
|
|
562
|
+
}
|
|
563
|
+
if (!isRecord(result.data) || result.data.status !== "ok") {
|
|
564
|
+
return { name: "health", status: "fail", message: "Health response is invalid" };
|
|
565
|
+
}
|
|
566
|
+
return { name: "health", status: "pass", message: "API is reachable" };
|
|
567
|
+
}
|
|
568
|
+
function createMetaCheck(result) {
|
|
569
|
+
if (!result.ok) {
|
|
570
|
+
return { name: "meta", status: "fail", message: result.message };
|
|
571
|
+
}
|
|
572
|
+
if (!isMetaDocument(result.data)) {
|
|
573
|
+
return { name: "meta", status: "fail", message: "Metadata response is invalid" };
|
|
574
|
+
}
|
|
575
|
+
if (result.data.api.name !== "slugkit") {
|
|
576
|
+
return { name: "meta", status: "fail", message: "API name is not slugkit" };
|
|
577
|
+
}
|
|
578
|
+
if (readMajorVersion(result.data.api.version) !== SUPPORTED_API_MAJOR_VERSION) {
|
|
579
|
+
return {
|
|
580
|
+
name: "meta",
|
|
581
|
+
status: "fail",
|
|
582
|
+
message: `API version ${result.data.api.version} is not compatible`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return { name: "meta", status: "pass", message: `API version ${result.data.api.version}` };
|
|
586
|
+
}
|
|
587
|
+
function createOpenApiCheck(result) {
|
|
588
|
+
if (!result.ok) {
|
|
589
|
+
return { name: "openapi", status: "fail", message: result.message };
|
|
590
|
+
}
|
|
591
|
+
if (!isOpenApiDocument(result.data)) {
|
|
592
|
+
return { name: "openapi", status: "fail", message: "OpenAPI document is invalid" };
|
|
593
|
+
}
|
|
594
|
+
const document = result.data;
|
|
595
|
+
const missingPath = ["/health", "/meta", "/openapi.json", "/auth/check"].find((path) => !(path in document.paths));
|
|
596
|
+
if (missingPath !== undefined) {
|
|
597
|
+
return { name: "openapi", status: "fail", message: `OpenAPI is missing ${missingPath}` };
|
|
598
|
+
}
|
|
599
|
+
return { name: "openapi", status: "pass", message: `OpenAPI ${document.openapi}` };
|
|
600
|
+
}
|
|
601
|
+
function createAuthCheck(result) {
|
|
602
|
+
if (!result.ok) {
|
|
603
|
+
return { name: "auth", status: "fail", message: result.message };
|
|
604
|
+
}
|
|
605
|
+
if (!isRecord(result.data) ||
|
|
606
|
+
!isRecord(result.data.data) ||
|
|
607
|
+
result.data.data.authenticated !== true) {
|
|
608
|
+
return { name: "auth", status: "fail", message: "Authentication check response is invalid" };
|
|
609
|
+
}
|
|
610
|
+
return { name: "auth", status: "pass", message: "API key is valid" };
|
|
611
|
+
}
|
|
612
|
+
function getDoctorExitCode(checks) {
|
|
613
|
+
const failedChecks = checks.filter((check) => check.status === "fail");
|
|
614
|
+
if (failedChecks.length === 0) {
|
|
615
|
+
return ExitCode.Ok;
|
|
616
|
+
}
|
|
617
|
+
if (failedChecks.some((check) => check.name === "auth")) {
|
|
618
|
+
return ExitCode.AuthenticationError;
|
|
619
|
+
}
|
|
620
|
+
if (failedChecks.some((check) => check.message === "Network request failed")) {
|
|
621
|
+
return ExitCode.NetworkError;
|
|
622
|
+
}
|
|
623
|
+
return ExitCode.ApiError;
|
|
624
|
+
}
|
|
625
|
+
function isMetaDocument(value) {
|
|
626
|
+
return (isRecord(value) &&
|
|
627
|
+
isRecord(value.api) &&
|
|
628
|
+
typeof value.api.name === "string" &&
|
|
629
|
+
typeof value.api.version === "string" &&
|
|
630
|
+
typeof value.api.baseUrl === "string" &&
|
|
631
|
+
typeof value.api.openapiUrl === "string");
|
|
632
|
+
}
|
|
633
|
+
function isOpenApiDocument(value) {
|
|
634
|
+
return (isRecord(value) &&
|
|
635
|
+
typeof value.openapi === "string" &&
|
|
636
|
+
isRecord(value.info) &&
|
|
637
|
+
typeof value.info.version === "string" &&
|
|
638
|
+
isRecord(value.paths));
|
|
639
|
+
}
|
|
640
|
+
function isRecord(value) {
|
|
641
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
642
|
+
}
|
|
643
|
+
function readMajorVersion(value) {
|
|
644
|
+
const major = Number.parseInt(value.split(".")[0] ?? "", 10);
|
|
645
|
+
return Number.isInteger(major) ? major : undefined;
|
|
646
|
+
}
|
|
647
|
+
function readApiErrorMessage(value) {
|
|
648
|
+
if (!isRecord(value) || !isRecord(value.error) || typeof value.error.message !== "string") {
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
return value.error.message;
|
|
652
|
+
}
|
|
653
|
+
async function runLoginCommand(context, args) {
|
|
654
|
+
if (args.length > 1) {
|
|
655
|
+
throw createInvalidUsageError("Usage: slug login [api-base-url]");
|
|
656
|
+
}
|
|
657
|
+
const config = await readConfig(context.configPath);
|
|
658
|
+
const apiBaseUrl = normalizeUrl(args[0] ?? config.apiBaseUrl ?? (await promptForValue(context, "API base URL: ")));
|
|
659
|
+
const authUrl = createBrowserAuthUrl(apiBaseUrl);
|
|
660
|
+
context.writer.stdout("Open this URL to create an API key:");
|
|
661
|
+
context.writer.stdout(authUrl);
|
|
662
|
+
if (context.openUrl !== undefined) {
|
|
663
|
+
try {
|
|
664
|
+
await context.openUrl(authUrl);
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
context.writer.stdout("Could not open a browser automatically.");
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const apiKey = (await promptForValue(context, "API key: ")).trim();
|
|
671
|
+
if (apiKey === "") {
|
|
672
|
+
throw createInvalidUsageError("API key is required");
|
|
673
|
+
}
|
|
674
|
+
const client = new SlugHttpClient({
|
|
675
|
+
apiBaseUrl,
|
|
676
|
+
apiKey,
|
|
677
|
+
fetchImpl: context.fetchImpl,
|
|
678
|
+
});
|
|
679
|
+
await client.requestJson({ path: "/auth/check" });
|
|
680
|
+
await writeConfig(context.configPath, setConfigApiKey(setConfigApiBaseUrl(config, apiBaseUrl), apiKey));
|
|
681
|
+
context.writer.stdout("Login successful. API key saved.");
|
|
682
|
+
return { exitCode: ExitCode.Ok };
|
|
683
|
+
}
|
|
684
|
+
async function runPostsCommand(context, args) {
|
|
685
|
+
switch (args[0]) {
|
|
686
|
+
case "list":
|
|
687
|
+
return runPostsListCommand(context, args.slice(1));
|
|
688
|
+
case "show":
|
|
689
|
+
return runPostsShowCommand(context, args.slice(1));
|
|
690
|
+
case "create":
|
|
691
|
+
return runPostsCreateCommand(context, args.slice(1));
|
|
692
|
+
case "edit":
|
|
693
|
+
return runPostsEditCommand(context, args.slice(1));
|
|
694
|
+
case "delete":
|
|
695
|
+
return runPostsDeleteCommand(context, args.slice(1));
|
|
696
|
+
case "publish":
|
|
697
|
+
return runPostsLifecycleCommand(context, args.slice(1), "publish");
|
|
698
|
+
case "unpublish":
|
|
699
|
+
return runPostsLifecycleCommand(context, args.slice(1), "unpublish");
|
|
700
|
+
default:
|
|
701
|
+
throw createInvalidUsageError("Usage: slug post <list|show|create|edit|delete|publish|unpublish>");
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async function runPostsListCommand(context, args) {
|
|
705
|
+
const parsed = parsePostFlags(args, ["type", "status", "tag"]);
|
|
706
|
+
const api = await createConfiguredApiContext(context);
|
|
707
|
+
const query = new URLSearchParams();
|
|
708
|
+
addOptionalQuery(query, "type", parsed.options.type);
|
|
709
|
+
addOptionalQuery(query, "status", parsed.options.status);
|
|
710
|
+
addOptionalQuery(query, "tag", parsed.options.tag);
|
|
711
|
+
const suffix = query.size === 0 ? "" : `?${query.toString()}`;
|
|
712
|
+
const response = await api.client.requestJson({ path: `/posts${suffix}` });
|
|
713
|
+
if (parsed.json) {
|
|
714
|
+
writeJson(context.writer, response);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
writePostList(context.writer, response.data);
|
|
718
|
+
}
|
|
719
|
+
return { exitCode: ExitCode.Ok };
|
|
720
|
+
}
|
|
721
|
+
async function runPostsShowCommand(context, args) {
|
|
722
|
+
const { slug, json } = readSlugCommandArgs(args, "Usage: slug post show <slug> [--json]");
|
|
723
|
+
const api = await createConfiguredApiContext(context);
|
|
724
|
+
const response = await api.client.requestJson({
|
|
725
|
+
path: `/posts/${encodeURIComponent(slug)}`,
|
|
726
|
+
});
|
|
727
|
+
if (json) {
|
|
728
|
+
writeJson(context.writer, response);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
writePost(context.writer, response.data);
|
|
732
|
+
}
|
|
733
|
+
return { exitCode: ExitCode.Ok };
|
|
734
|
+
}
|
|
735
|
+
async function runPostsCreateCommand(context, args) {
|
|
736
|
+
const parsed = parsePostFlags(args, [
|
|
737
|
+
"type",
|
|
738
|
+
"slug",
|
|
739
|
+
"content",
|
|
740
|
+
"title",
|
|
741
|
+
"url",
|
|
742
|
+
"excerpt",
|
|
743
|
+
"tag",
|
|
744
|
+
"source-id",
|
|
745
|
+
"author-id",
|
|
746
|
+
]);
|
|
747
|
+
const body = createPostMutationInput(parsed.options, true);
|
|
748
|
+
const api = await createConfiguredApiContext(context);
|
|
749
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
750
|
+
const response = await api.client.requestJson({
|
|
751
|
+
method: "POST",
|
|
752
|
+
path: "/posts",
|
|
753
|
+
body,
|
|
754
|
+
});
|
|
755
|
+
writePostMutationOutput(context.writer, response, parsed.json);
|
|
756
|
+
return { exitCode: ExitCode.Ok };
|
|
757
|
+
}
|
|
758
|
+
async function runPostsEditCommand(context, args) {
|
|
759
|
+
const slug = args[0];
|
|
760
|
+
if (slug === undefined || slug.startsWith("--")) {
|
|
761
|
+
throw createInvalidUsageError("Usage: slug post edit <slug> [options]");
|
|
762
|
+
}
|
|
763
|
+
const parsed = parsePostFlags(args.slice(1), [
|
|
764
|
+
"slug",
|
|
765
|
+
"content",
|
|
766
|
+
"title",
|
|
767
|
+
"url",
|
|
768
|
+
"excerpt",
|
|
769
|
+
"tag",
|
|
770
|
+
"source-id",
|
|
771
|
+
"author-id",
|
|
772
|
+
]);
|
|
773
|
+
const body = createPostMutationInput(parsed.options, false);
|
|
774
|
+
if (Object.keys(body).length === 0) {
|
|
775
|
+
throw createInvalidUsageError("At least one edit option is required");
|
|
776
|
+
}
|
|
777
|
+
const api = await createConfiguredApiContext(context);
|
|
778
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
779
|
+
const response = await api.client.requestJson({
|
|
780
|
+
method: "PUT",
|
|
781
|
+
path: `/posts/${encodeURIComponent(slug)}`,
|
|
782
|
+
body,
|
|
783
|
+
});
|
|
784
|
+
writePostMutationOutput(context.writer, response, parsed.json);
|
|
785
|
+
return { exitCode: ExitCode.Ok };
|
|
786
|
+
}
|
|
787
|
+
async function runPostsDeleteCommand(context, args) {
|
|
788
|
+
const { slug, json } = readSlugCommandArgs(args, "Usage: slug post delete <slug> [--json]");
|
|
789
|
+
const api = await createConfiguredApiContext(context);
|
|
790
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
791
|
+
await api.client.requestVoid({ method: "DELETE", path: `/posts/${encodeURIComponent(slug)}` });
|
|
792
|
+
if (json) {
|
|
793
|
+
writeJson(context.writer, { data: { slug, deleted: true } });
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
context.writer.stdout(`Deleted post ${slug}.`);
|
|
797
|
+
}
|
|
798
|
+
return { exitCode: ExitCode.Ok };
|
|
799
|
+
}
|
|
800
|
+
async function runPostsLifecycleCommand(context, args, action) {
|
|
801
|
+
const { slug, json } = readSlugCommandArgs(args, `Usage: slug post ${action} <slug> [--json]`);
|
|
802
|
+
const api = await createConfiguredApiContext(context);
|
|
803
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
804
|
+
const response = await api.client.requestJson({
|
|
805
|
+
method: "POST",
|
|
806
|
+
path: `/posts/${encodeURIComponent(slug)}/${action}`,
|
|
807
|
+
});
|
|
808
|
+
writePostMutationOutput(context.writer, response, json);
|
|
809
|
+
return { exitCode: ExitCode.Ok };
|
|
810
|
+
}
|
|
811
|
+
function parsePostFlags(args, allowed) {
|
|
812
|
+
const options = {};
|
|
813
|
+
let json = false;
|
|
814
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
815
|
+
const arg = args[index];
|
|
816
|
+
if (arg === "--json") {
|
|
817
|
+
json = true;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (arg === undefined || !arg.startsWith("--")) {
|
|
821
|
+
throw createInvalidUsageError(`Unsupported argument: ${arg ?? ""}`.trim());
|
|
822
|
+
}
|
|
823
|
+
const key = arg.slice(2);
|
|
824
|
+
const value = args[index + 1];
|
|
825
|
+
if (!allowed.includes(key)) {
|
|
826
|
+
throw createInvalidUsageError(`Unsupported option: --${key}`);
|
|
827
|
+
}
|
|
828
|
+
if (value === undefined || value.startsWith("--")) {
|
|
829
|
+
throw createInvalidUsageError(`Option --${key} requires a value`);
|
|
830
|
+
}
|
|
831
|
+
if (key === "tag" || key === "author-id") {
|
|
832
|
+
const current = options[key];
|
|
833
|
+
options[key] = [...(Array.isArray(current) ? current : []), value];
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
options[key] = value;
|
|
837
|
+
}
|
|
838
|
+
index += 1;
|
|
839
|
+
}
|
|
840
|
+
return { json, options };
|
|
841
|
+
}
|
|
842
|
+
function createPostMutationInput(options, requireCreateFields) {
|
|
843
|
+
const input = {};
|
|
844
|
+
copyStringOption(options, input, "type", "type");
|
|
845
|
+
copyStringOption(options, input, "slug", "slug");
|
|
846
|
+
copyStringOption(options, input, "title", "title");
|
|
847
|
+
copyStringOption(options, input, "content", "content");
|
|
848
|
+
copyStringOption(options, input, "url", "url");
|
|
849
|
+
copyStringOption(options, input, "excerpt", "excerpt");
|
|
850
|
+
copyNumberOption(options, input, "source-id", "sourceId");
|
|
851
|
+
if (Array.isArray(options.tag)) {
|
|
852
|
+
input.tagSlugs = options.tag;
|
|
853
|
+
}
|
|
854
|
+
if (Array.isArray(options["author-id"])) {
|
|
855
|
+
input.authorIds = options["author-id"].map((value) => readPositiveInteger(value, "author-id"));
|
|
856
|
+
}
|
|
857
|
+
if (requireCreateFields) {
|
|
858
|
+
for (const key of ["type", "slug", "content"]) {
|
|
859
|
+
if (typeof options[key] !== "string") {
|
|
860
|
+
throw createInvalidUsageError(`Missing required option: --${key}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return input;
|
|
865
|
+
}
|
|
866
|
+
function copyStringOption(options, input, optionKey, inputKey) {
|
|
867
|
+
if (typeof options[optionKey] === "string") {
|
|
868
|
+
input[inputKey] = options[optionKey];
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function copyNumberOption(options, input, optionKey, inputKey) {
|
|
872
|
+
if (typeof options[optionKey] === "string") {
|
|
873
|
+
input[inputKey] = readPositiveInteger(options[optionKey], optionKey);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function readPositiveInteger(value, optionName) {
|
|
877
|
+
const integer = Number.parseInt(value, 10);
|
|
878
|
+
if (!Number.isInteger(integer) || integer.toString() !== value || integer < 1) {
|
|
879
|
+
throw createInvalidUsageError(`--${optionName} must be a positive integer`);
|
|
880
|
+
}
|
|
881
|
+
return integer;
|
|
882
|
+
}
|
|
883
|
+
function readSlugCommandArgs(args, usage) {
|
|
884
|
+
const json = args.includes("--json");
|
|
885
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
886
|
+
if (positional.length !== 1 || positional[0] === undefined || positional[0].startsWith("--")) {
|
|
887
|
+
throw createInvalidUsageError(usage);
|
|
888
|
+
}
|
|
889
|
+
return { slug: positional[0], json };
|
|
890
|
+
}
|
|
891
|
+
function addOptionalQuery(query, key, value) {
|
|
892
|
+
if (typeof value === "string") {
|
|
893
|
+
query.set(key, value);
|
|
894
|
+
}
|
|
895
|
+
else if (Array.isArray(value) && value[0] !== undefined) {
|
|
896
|
+
query.set(key, value[0]);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function writePostList(writer, posts) {
|
|
900
|
+
if (posts.length === 0) {
|
|
901
|
+
writer.stdout("No posts found.");
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
for (const post of posts) {
|
|
905
|
+
writer.stdout(`${post.slug} • ${post.type} • ${post.title ?? "(untitled)"} • ${post.publishedAt ?? "draft"}`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function writePost(writer, post) {
|
|
909
|
+
writer.stdout(`slug: ${post.slug}`);
|
|
910
|
+
writer.stdout(`type: ${post.type}`);
|
|
911
|
+
writer.stdout(`title: ${post.title ?? ""}`);
|
|
912
|
+
writer.stdout(`publishedAt: ${post.publishedAt ?? "draft"}`);
|
|
913
|
+
}
|
|
914
|
+
function writePostMutationOutput(writer, response, json) {
|
|
915
|
+
if (json) {
|
|
916
|
+
writeJson(writer, response);
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
writer.stdout(`Post ${response.data.slug}: ${response.data.type} (${response.data.publishedAt ?? "draft"})`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function writeMutationTarget(writer, apiBaseUrl, json) {
|
|
923
|
+
if (!json) {
|
|
924
|
+
writer.stdout(`Target: ${apiBaseUrl}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function runCommentCommand(context, args) {
|
|
928
|
+
switch (args[0]) {
|
|
929
|
+
case "list":
|
|
930
|
+
return runCommentListCommand(context, args.slice(1));
|
|
931
|
+
case "approve":
|
|
932
|
+
return runCommentModerationCommand(context, args.slice(1), "approve");
|
|
933
|
+
case "hide":
|
|
934
|
+
return runCommentModerationCommand(context, args.slice(1), "hide");
|
|
935
|
+
default:
|
|
936
|
+
throw createInvalidUsageError("Usage: slug comment <list|approve|hide>");
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async function runCommentListCommand(context, args) {
|
|
940
|
+
const parsed = parseCommentListFlags(args);
|
|
941
|
+
const client = await createConfiguredClient(context);
|
|
942
|
+
const query = new URLSearchParams();
|
|
943
|
+
addOptionalQuery(query, "status", parsed.options.status);
|
|
944
|
+
const path = parsed.options.post === undefined
|
|
945
|
+
? `/comments?${createCommentStatusQuery(parsed.options.status)}`
|
|
946
|
+
: `/posts/${encodeURIComponent(parsed.options.post)}/comments${query.size === 0 ? "" : `?${query.toString()}`}`;
|
|
947
|
+
const response = await client.requestJson({ path });
|
|
948
|
+
if (parsed.json) {
|
|
949
|
+
writeJson(context.writer, response);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
writeCommentList(context.writer, response.data);
|
|
953
|
+
}
|
|
954
|
+
return { exitCode: ExitCode.Ok };
|
|
955
|
+
}
|
|
956
|
+
async function runCommentModerationCommand(context, args, action) {
|
|
957
|
+
const { id, json } = readIdCommandArgs(args, `Usage: slug comment ${action} <id> [--json]`);
|
|
958
|
+
const api = await createConfiguredApiContext(context);
|
|
959
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
960
|
+
const response = await api.client.requestJson({
|
|
961
|
+
method: "POST",
|
|
962
|
+
path: `/comments/${encodeURIComponent(id.toString())}/${action}`,
|
|
963
|
+
});
|
|
964
|
+
if (json) {
|
|
965
|
+
writeJson(context.writer, response);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
context.writer.stdout(`Comment ${response.data.id}: ${response.data.moderationStatus}`);
|
|
969
|
+
}
|
|
970
|
+
return { exitCode: ExitCode.Ok };
|
|
971
|
+
}
|
|
972
|
+
function parseCommentListFlags(args) {
|
|
973
|
+
const parsed = parsePostFlags(args, ["post", "status"]);
|
|
974
|
+
const post = readOptionalStringOption(parsed.options, "post");
|
|
975
|
+
const status = readOptionalCommentStatus(parsed.options.status);
|
|
976
|
+
return { json: parsed.json, options: { post, status } };
|
|
977
|
+
}
|
|
978
|
+
function readOptionalStringOption(options, key) {
|
|
979
|
+
const value = options[key];
|
|
980
|
+
return typeof value === "string" ? value : undefined;
|
|
981
|
+
}
|
|
982
|
+
function readOptionalCommentStatus(value) {
|
|
983
|
+
if (value === undefined) {
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
if (value === "pending" || value === "approved" || value === "hidden" || value === "all") {
|
|
987
|
+
return value;
|
|
988
|
+
}
|
|
989
|
+
throw createInvalidUsageError("--status must be pending, approved, hidden, or all");
|
|
990
|
+
}
|
|
991
|
+
function createCommentStatusQuery(status) {
|
|
992
|
+
const query = new URLSearchParams({ status: status ?? "pending" });
|
|
993
|
+
return query.toString();
|
|
994
|
+
}
|
|
995
|
+
function writeCommentList(writer, comments) {
|
|
996
|
+
if (comments.length === 0) {
|
|
997
|
+
writer.stdout("No comments found.");
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
for (const comment of comments) {
|
|
1001
|
+
writer.stdout(`${comment.id} • ${comment.postSlug} • ${comment.authorName} • ${comment.moderationStatus} • ${comment.contentText}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async function runCommentsCommand(context, args) {
|
|
1005
|
+
switch (args[0]) {
|
|
1006
|
+
case "create":
|
|
1007
|
+
return runCommentsCreateCommand(context, args.slice(1));
|
|
1008
|
+
default:
|
|
1009
|
+
throw createInvalidUsageError("Usage: slug comments create <post-slug> --content <text> [--json]");
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
async function runCommentsCreateCommand(context, args) {
|
|
1013
|
+
const slug = args[0];
|
|
1014
|
+
if (slug === undefined || slug.startsWith("--")) {
|
|
1015
|
+
throw createInvalidUsageError("Usage: slug comments create <post-slug> --content <text> [--json]");
|
|
1016
|
+
}
|
|
1017
|
+
const parsed = parseCommentCreateFlags(args.slice(1));
|
|
1018
|
+
const api = await createConfiguredApiContext(context);
|
|
1019
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
1020
|
+
const response = await api.client.requestJson({
|
|
1021
|
+
method: "POST",
|
|
1022
|
+
path: `/posts/${encodeURIComponent(slug)}/comments`,
|
|
1023
|
+
body: { content: parsed.content },
|
|
1024
|
+
});
|
|
1025
|
+
writeCommentMutationOutput(context.writer, response, parsed.json);
|
|
1026
|
+
return { exitCode: ExitCode.Ok };
|
|
1027
|
+
}
|
|
1028
|
+
function parseCommentCreateFlags(args) {
|
|
1029
|
+
const parsed = parsePostFlags(args, ["content"]);
|
|
1030
|
+
const content = parsed.options.content;
|
|
1031
|
+
if (typeof content !== "string") {
|
|
1032
|
+
throw createInvalidUsageError("Missing required option: --content");
|
|
1033
|
+
}
|
|
1034
|
+
return { content, json: parsed.json };
|
|
1035
|
+
}
|
|
1036
|
+
function writeCommentMutationOutput(writer, response, json) {
|
|
1037
|
+
if (json) {
|
|
1038
|
+
writeJson(writer, response);
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
writer.stdout(`Comment ${response.data.id} on ${response.data.postSlug}: ${response.data.moderationStatus}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function runMediaCommand(context, args) {
|
|
1045
|
+
switch (args[0]) {
|
|
1046
|
+
case "upload":
|
|
1047
|
+
return runMediaUploadCommand(context, args.slice(1));
|
|
1048
|
+
case "show":
|
|
1049
|
+
return runMediaShowCommand(context, args.slice(1));
|
|
1050
|
+
case "delete":
|
|
1051
|
+
return runMediaDeleteCommand(context, args.slice(1));
|
|
1052
|
+
default:
|
|
1053
|
+
throw createInvalidUsageError("Usage: slug media <upload|show|delete>");
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
async function runMediaUploadCommand(context, args) {
|
|
1057
|
+
const parsed = parseMediaUploadFlags(args);
|
|
1058
|
+
const file = await readMediaUploadFile(parsed.file);
|
|
1059
|
+
const fileBody = file.body.buffer.slice(file.body.byteOffset, file.body.byteOffset + file.body.byteLength);
|
|
1060
|
+
const body = new FormData();
|
|
1061
|
+
body.set("file", new File([fileBody], file.filename, { type: file.mimeType }));
|
|
1062
|
+
if (parsed.altText !== undefined)
|
|
1063
|
+
body.set("altText", parsed.altText);
|
|
1064
|
+
if (parsed.key !== undefined)
|
|
1065
|
+
body.set("key", parsed.key);
|
|
1066
|
+
const api = await createConfiguredApiContext(context);
|
|
1067
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
1068
|
+
const response = await api.client.requestJson({
|
|
1069
|
+
method: "POST",
|
|
1070
|
+
path: "/media",
|
|
1071
|
+
body,
|
|
1072
|
+
});
|
|
1073
|
+
writeMediaOutput(context.writer, response, parsed.json);
|
|
1074
|
+
return { exitCode: ExitCode.Ok };
|
|
1075
|
+
}
|
|
1076
|
+
async function runMediaShowCommand(context, args) {
|
|
1077
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug media show <id> [--json]");
|
|
1078
|
+
const client = await createConfiguredClient(context);
|
|
1079
|
+
const response = await client.requestJson({
|
|
1080
|
+
path: `/media/${encodeURIComponent(id.toString())}`,
|
|
1081
|
+
});
|
|
1082
|
+
writeMediaOutput(context.writer, response, json);
|
|
1083
|
+
return { exitCode: ExitCode.Ok };
|
|
1084
|
+
}
|
|
1085
|
+
async function runMediaDeleteCommand(context, args) {
|
|
1086
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug media delete <id> [--json]");
|
|
1087
|
+
const api = await createConfiguredApiContext(context);
|
|
1088
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
1089
|
+
await api.client.requestVoid({
|
|
1090
|
+
method: "DELETE",
|
|
1091
|
+
path: `/media/${encodeURIComponent(id.toString())}`,
|
|
1092
|
+
});
|
|
1093
|
+
if (json) {
|
|
1094
|
+
writeJson(context.writer, { data: { id, deleted: true } });
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
context.writer.stdout(`Deleted media ${id}.`);
|
|
1098
|
+
}
|
|
1099
|
+
return { exitCode: ExitCode.Ok };
|
|
1100
|
+
}
|
|
1101
|
+
function parseMediaUploadFlags(args) {
|
|
1102
|
+
let json = false;
|
|
1103
|
+
let altText;
|
|
1104
|
+
let key;
|
|
1105
|
+
const positional = [];
|
|
1106
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1107
|
+
const arg = args[index];
|
|
1108
|
+
if (arg === "--json") {
|
|
1109
|
+
json = true;
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (arg === "--alt" || arg === "--key") {
|
|
1113
|
+
const value = args[index + 1];
|
|
1114
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1115
|
+
throw createInvalidUsageError(`Option ${arg} requires a value`);
|
|
1116
|
+
}
|
|
1117
|
+
if (arg === "--alt") {
|
|
1118
|
+
altText = value;
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
key = value;
|
|
1122
|
+
}
|
|
1123
|
+
index += 1;
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
if (arg === undefined || arg.startsWith("--")) {
|
|
1127
|
+
throw createInvalidUsageError(`Unsupported option: ${arg ?? ""}`.trim());
|
|
1128
|
+
}
|
|
1129
|
+
positional.push(arg);
|
|
1130
|
+
}
|
|
1131
|
+
if (positional.length !== 1 || positional[0] === undefined) {
|
|
1132
|
+
throw createInvalidUsageError("Usage: slug media upload <file> [--alt <text>] [--key <key>] [--json]");
|
|
1133
|
+
}
|
|
1134
|
+
return { file: positional[0], altText, key, json };
|
|
1135
|
+
}
|
|
1136
|
+
async function readMediaUploadFile(filePath) {
|
|
1137
|
+
try {
|
|
1138
|
+
const stats = await stat(filePath);
|
|
1139
|
+
if (!stats.isFile()) {
|
|
1140
|
+
throw createInvalidUsageError(`Media upload path is not a file: ${filePath}`);
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
filename: basename(filePath),
|
|
1144
|
+
mimeType: inferMimeType(filePath),
|
|
1145
|
+
body: await readFile(filePath),
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
catch (error) {
|
|
1149
|
+
if (error instanceof CliError) {
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
1152
|
+
throw createInvalidUsageError(`Cannot read media file: ${filePath}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function inferMimeType(filePath) {
|
|
1156
|
+
switch (extname(filePath).toLowerCase()) {
|
|
1157
|
+
case ".avif":
|
|
1158
|
+
return "image/avif";
|
|
1159
|
+
case ".gif":
|
|
1160
|
+
return "image/gif";
|
|
1161
|
+
case ".jpg":
|
|
1162
|
+
case ".jpeg":
|
|
1163
|
+
return "image/jpeg";
|
|
1164
|
+
case ".png":
|
|
1165
|
+
return "image/png";
|
|
1166
|
+
case ".webp":
|
|
1167
|
+
return "image/webp";
|
|
1168
|
+
case ".mp4":
|
|
1169
|
+
return "video/mp4";
|
|
1170
|
+
case ".webm":
|
|
1171
|
+
return "video/webm";
|
|
1172
|
+
case ".mp3":
|
|
1173
|
+
return "audio/mpeg";
|
|
1174
|
+
case ".wav":
|
|
1175
|
+
return "audio/wav";
|
|
1176
|
+
default:
|
|
1177
|
+
return "application/octet-stream";
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function writeMediaOutput(writer, response, json) {
|
|
1181
|
+
if (json) {
|
|
1182
|
+
writeJson(writer, response);
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
writeMedia(writer, response.data);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
function writeMedia(writer, media) {
|
|
1189
|
+
writer.stdout(`id: ${media.id}`);
|
|
1190
|
+
writer.stdout(`filename: ${media.filename}`);
|
|
1191
|
+
writer.stdout(`mimeType: ${media.mimeType}`);
|
|
1192
|
+
writer.stdout(`key: ${media.key}`);
|
|
1193
|
+
writer.stdout(`altText: ${media.altText ?? ""}`);
|
|
1194
|
+
writer.stdout(`url: ${media.url}`);
|
|
1195
|
+
if (media.createdAt !== undefined)
|
|
1196
|
+
writer.stdout(`createdAt: ${media.createdAt}`);
|
|
1197
|
+
}
|
|
1198
|
+
async function runTagsCommand(context, args) {
|
|
1199
|
+
if (args[0] !== "list") {
|
|
1200
|
+
throw createInvalidUsageError("Usage: slug tag list [--json]");
|
|
1201
|
+
}
|
|
1202
|
+
const json = readJsonFlag(args.slice(1));
|
|
1203
|
+
const client = await createConfiguredClient(context);
|
|
1204
|
+
const response = await client.requestJson({ path: "/tags" });
|
|
1205
|
+
if (json) {
|
|
1206
|
+
writeJson(context.writer, response);
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
writeTagList(context.writer, response.data);
|
|
1210
|
+
}
|
|
1211
|
+
return { exitCode: ExitCode.Ok };
|
|
1212
|
+
}
|
|
1213
|
+
function writeTagList(writer, tags) {
|
|
1214
|
+
if (tags.length === 0) {
|
|
1215
|
+
writer.stdout("No tags found.");
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
for (const tag of tags) {
|
|
1219
|
+
writer.stdout(`${tag.slug} • ${tag.name} • ${tag.count}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function runSourcesCommand(context, args) {
|
|
1223
|
+
switch (args[0]) {
|
|
1224
|
+
case "list":
|
|
1225
|
+
return runSourcesListCommand(context, args.slice(1));
|
|
1226
|
+
case "show":
|
|
1227
|
+
return runSourcesShowCommand(context, args.slice(1));
|
|
1228
|
+
case "create":
|
|
1229
|
+
return runSourcesCreateCommand(context, args.slice(1));
|
|
1230
|
+
case "edit":
|
|
1231
|
+
return runSourcesEditCommand(context, args.slice(1));
|
|
1232
|
+
case "delete":
|
|
1233
|
+
return runSourcesDeleteCommand(context, args.slice(1));
|
|
1234
|
+
default:
|
|
1235
|
+
throw createInvalidUsageError("Usage: slug sources <list|show|create|edit|delete>");
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async function runSourcesListCommand(context, args) {
|
|
1239
|
+
const json = readJsonFlag(args);
|
|
1240
|
+
const client = await createConfiguredClient(context);
|
|
1241
|
+
const response = await client.requestJson({ path: "/sources" });
|
|
1242
|
+
if (json) {
|
|
1243
|
+
writeJson(context.writer, response);
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
writeSourceList(context.writer, response.data);
|
|
1247
|
+
}
|
|
1248
|
+
return { exitCode: ExitCode.Ok };
|
|
1249
|
+
}
|
|
1250
|
+
async function runSourcesShowCommand(context, args) {
|
|
1251
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug sources show <id> [--json]");
|
|
1252
|
+
const client = await createConfiguredClient(context);
|
|
1253
|
+
const response = await client.requestJson({
|
|
1254
|
+
path: `/sources/${encodeURIComponent(id.toString())}`,
|
|
1255
|
+
});
|
|
1256
|
+
if (json) {
|
|
1257
|
+
writeJson(context.writer, response);
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
writeSource(context.writer, response.data);
|
|
1261
|
+
}
|
|
1262
|
+
return { exitCode: ExitCode.Ok };
|
|
1263
|
+
}
|
|
1264
|
+
async function runSourcesCreateCommand(context, args) {
|
|
1265
|
+
const parsed = parseSourceFlags(args);
|
|
1266
|
+
const body = createSourceMutationInput(parsed.options, true);
|
|
1267
|
+
const api = await createConfiguredApiContext(context);
|
|
1268
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
1269
|
+
const response = await api.client.requestJson({
|
|
1270
|
+
method: "POST",
|
|
1271
|
+
path: "/sources",
|
|
1272
|
+
body,
|
|
1273
|
+
});
|
|
1274
|
+
writeSourceMutationOutput(context.writer, response, parsed.json);
|
|
1275
|
+
return { exitCode: ExitCode.Ok };
|
|
1276
|
+
}
|
|
1277
|
+
async function runSourcesEditCommand(context, args) {
|
|
1278
|
+
const idArg = args[0];
|
|
1279
|
+
if (idArg === undefined || idArg.startsWith("--")) {
|
|
1280
|
+
throw createInvalidUsageError("Usage: slug sources edit <id> [options]");
|
|
1281
|
+
}
|
|
1282
|
+
const id = readPositiveInteger(idArg, "id");
|
|
1283
|
+
const parsed = parseSourceFlags(args.slice(1));
|
|
1284
|
+
const body = createSourceMutationInput(parsed.options, false);
|
|
1285
|
+
if (Object.keys(body).length === 0) {
|
|
1286
|
+
throw createInvalidUsageError("At least one edit option is required");
|
|
1287
|
+
}
|
|
1288
|
+
const api = await createConfiguredApiContext(context);
|
|
1289
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
1290
|
+
const response = await api.client.requestJson({
|
|
1291
|
+
method: "PUT",
|
|
1292
|
+
path: `/sources/${encodeURIComponent(id.toString())}`,
|
|
1293
|
+
body,
|
|
1294
|
+
});
|
|
1295
|
+
writeSourceMutationOutput(context.writer, response, parsed.json);
|
|
1296
|
+
return { exitCode: ExitCode.Ok };
|
|
1297
|
+
}
|
|
1298
|
+
async function runSourcesDeleteCommand(context, args) {
|
|
1299
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug sources delete <id> [--json]");
|
|
1300
|
+
const api = await createConfiguredApiContext(context);
|
|
1301
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
1302
|
+
await api.client.requestVoid({
|
|
1303
|
+
method: "DELETE",
|
|
1304
|
+
path: `/sources/${encodeURIComponent(id.toString())}`,
|
|
1305
|
+
});
|
|
1306
|
+
if (json) {
|
|
1307
|
+
writeJson(context.writer, { data: { id, deleted: true } });
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
context.writer.stdout(`Deleted source ${id}.`);
|
|
1311
|
+
}
|
|
1312
|
+
return { exitCode: ExitCode.Ok };
|
|
1313
|
+
}
|
|
1314
|
+
function parseSourceFlags(args) {
|
|
1315
|
+
const options = {};
|
|
1316
|
+
let json = false;
|
|
1317
|
+
const allowed = ["name", "url", "description", "image-url", "favicon-url", "contact-id"];
|
|
1318
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1319
|
+
const arg = args[index];
|
|
1320
|
+
if (arg === "--json") {
|
|
1321
|
+
json = true;
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
if (arg === undefined || !arg.startsWith("--")) {
|
|
1325
|
+
throw createInvalidUsageError(`Unsupported argument: ${arg ?? ""}`.trim());
|
|
1326
|
+
}
|
|
1327
|
+
const key = arg.slice(2);
|
|
1328
|
+
const value = args[index + 1];
|
|
1329
|
+
if (!allowed.includes(key)) {
|
|
1330
|
+
throw createInvalidUsageError(`Unsupported option: --${key}`);
|
|
1331
|
+
}
|
|
1332
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1333
|
+
throw createInvalidUsageError(`Option --${key} requires a value`);
|
|
1334
|
+
}
|
|
1335
|
+
if (key === "contact-id") {
|
|
1336
|
+
const current = options[key];
|
|
1337
|
+
options[key] = [...(Array.isArray(current) ? current : []), value];
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
options[key] = value;
|
|
1341
|
+
}
|
|
1342
|
+
index += 1;
|
|
1343
|
+
}
|
|
1344
|
+
return { json, options };
|
|
1345
|
+
}
|
|
1346
|
+
function createSourceMutationInput(options, requireName) {
|
|
1347
|
+
const input = {};
|
|
1348
|
+
copySourceStringOption(options, input, "name", "name");
|
|
1349
|
+
copySourceStringOption(options, input, "url", "url");
|
|
1350
|
+
copySourceStringOption(options, input, "description", "description");
|
|
1351
|
+
copySourceStringOption(options, input, "image-url", "imageUrl");
|
|
1352
|
+
copySourceStringOption(options, input, "favicon-url", "faviconUrl");
|
|
1353
|
+
if (Array.isArray(options["contact-id"])) {
|
|
1354
|
+
input.contactIds = options["contact-id"].map((value) => readPositiveInteger(value, "contact-id"));
|
|
1355
|
+
}
|
|
1356
|
+
if (requireName && typeof options.name !== "string") {
|
|
1357
|
+
throw createInvalidUsageError("Missing required option: --name");
|
|
1358
|
+
}
|
|
1359
|
+
return input;
|
|
1360
|
+
}
|
|
1361
|
+
function copySourceStringOption(options, input, optionKey, inputKey) {
|
|
1362
|
+
if (typeof options[optionKey] === "string") {
|
|
1363
|
+
input[inputKey] = options[optionKey];
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function writeSourceList(writer, sources) {
|
|
1367
|
+
if (sources.length === 0) {
|
|
1368
|
+
writer.stdout("No sources found.");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
for (const source of sources) {
|
|
1372
|
+
writer.stdout(`${source.id} • ${source.name} • ${source.url ?? ""} • ${source.contacts?.length ?? 0} contacts • ${source.accounts?.length ?? 0} accounts`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function writeSource(writer, source) {
|
|
1376
|
+
writer.stdout(`id: ${source.id}`);
|
|
1377
|
+
writer.stdout(`name: ${source.name}`);
|
|
1378
|
+
writer.stdout(`url: ${source.url ?? ""}`);
|
|
1379
|
+
writer.stdout(`description: ${source.description ?? ""}`);
|
|
1380
|
+
writer.stdout(`imageUrl: ${source.imageUrl ?? ""}`);
|
|
1381
|
+
writer.stdout(`faviconUrl: ${source.faviconUrl ?? ""}`);
|
|
1382
|
+
if (source.contacts !== undefined && source.contacts.length > 0) {
|
|
1383
|
+
writer.stdout("contacts:");
|
|
1384
|
+
for (const contact of source.contacts) {
|
|
1385
|
+
writer.stdout(`- ${contact.id} • ${contact.name} • ${contact.url ?? ""}`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (source.accounts !== undefined && source.accounts.length > 0) {
|
|
1389
|
+
writer.stdout("accounts:");
|
|
1390
|
+
for (const account of source.accounts) {
|
|
1391
|
+
writer.stdout(`- ${formatAccountSummary(account)}`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function writeSourceMutationOutput(writer, response, json) {
|
|
1396
|
+
if (json) {
|
|
1397
|
+
writeJson(writer, response);
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
writer.stdout(`Source ${response.data.id}: ${response.data.name}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
async function runAccountsCommand(context, args) {
|
|
1404
|
+
switch (args[0]) {
|
|
1405
|
+
case "list":
|
|
1406
|
+
return runAccountsListCommand(context, args.slice(1));
|
|
1407
|
+
case "show":
|
|
1408
|
+
return runAccountsShowCommand(context, args.slice(1));
|
|
1409
|
+
case "create":
|
|
1410
|
+
return runAccountsCreateCommand(context, args.slice(1));
|
|
1411
|
+
case "edit":
|
|
1412
|
+
return runAccountsEditCommand(context, args.slice(1));
|
|
1413
|
+
case "delete":
|
|
1414
|
+
return runAccountsDeleteCommand(context, args.slice(1));
|
|
1415
|
+
default:
|
|
1416
|
+
throw createInvalidUsageError("Usage: slug accounts <list|show|create|edit|delete>");
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async function runAccountsListCommand(context, args) {
|
|
1420
|
+
const parsed = parseAccountFlags(args);
|
|
1421
|
+
const client = await createConfiguredClient(context);
|
|
1422
|
+
const query = new URLSearchParams();
|
|
1423
|
+
addOptionalAccountQuery(query, "ownerType", parsed.options["owner-type"]);
|
|
1424
|
+
addOptionalAccountQuery(query, "ownerId", parsed.options["owner-id"]);
|
|
1425
|
+
const response = await client.requestJson({
|
|
1426
|
+
path: `/accounts${query.size === 0 ? "" : `?${query.toString()}`}`,
|
|
1427
|
+
});
|
|
1428
|
+
if (parsed.json) {
|
|
1429
|
+
writeJson(context.writer, response);
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
writeAccountList(context.writer, response.data);
|
|
1433
|
+
}
|
|
1434
|
+
return { exitCode: ExitCode.Ok };
|
|
1435
|
+
}
|
|
1436
|
+
async function runAccountsShowCommand(context, args) {
|
|
1437
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug accounts show <id> [--json]");
|
|
1438
|
+
const client = await createConfiguredClient(context);
|
|
1439
|
+
const response = await client.requestJson({
|
|
1440
|
+
path: `/accounts/${encodeURIComponent(id.toString())}`,
|
|
1441
|
+
});
|
|
1442
|
+
if (json) {
|
|
1443
|
+
writeJson(context.writer, response);
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
writeAccount(context.writer, response.data);
|
|
1447
|
+
}
|
|
1448
|
+
return { exitCode: ExitCode.Ok };
|
|
1449
|
+
}
|
|
1450
|
+
async function runAccountsCreateCommand(context, args) {
|
|
1451
|
+
const parsed = parseAccountFlags(args);
|
|
1452
|
+
const body = createAccountMutationInput(parsed.options, true);
|
|
1453
|
+
const api = await createConfiguredApiContext(context);
|
|
1454
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
1455
|
+
const response = await api.client.requestJson({
|
|
1456
|
+
method: "POST",
|
|
1457
|
+
path: "/accounts",
|
|
1458
|
+
body,
|
|
1459
|
+
});
|
|
1460
|
+
writeAccountMutationOutput(context.writer, response, parsed.json);
|
|
1461
|
+
return { exitCode: ExitCode.Ok };
|
|
1462
|
+
}
|
|
1463
|
+
async function runAccountsEditCommand(context, args) {
|
|
1464
|
+
const idArg = args[0];
|
|
1465
|
+
if (idArg === undefined || idArg.startsWith("--")) {
|
|
1466
|
+
throw createInvalidUsageError("Usage: slug accounts edit <id> [options]");
|
|
1467
|
+
}
|
|
1468
|
+
const id = readPositiveInteger(idArg, "id");
|
|
1469
|
+
const parsed = parseAccountFlags(args.slice(1));
|
|
1470
|
+
const body = createAccountMutationInput(parsed.options, false);
|
|
1471
|
+
if (Object.keys(body).length === 0) {
|
|
1472
|
+
throw createInvalidUsageError("At least one edit option is required");
|
|
1473
|
+
}
|
|
1474
|
+
const api = await createConfiguredApiContext(context);
|
|
1475
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
1476
|
+
const response = await api.client.requestJson({
|
|
1477
|
+
method: "PUT",
|
|
1478
|
+
path: `/accounts/${encodeURIComponent(id.toString())}`,
|
|
1479
|
+
body,
|
|
1480
|
+
});
|
|
1481
|
+
writeAccountMutationOutput(context.writer, response, parsed.json);
|
|
1482
|
+
return { exitCode: ExitCode.Ok };
|
|
1483
|
+
}
|
|
1484
|
+
async function runAccountsDeleteCommand(context, args) {
|
|
1485
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug accounts delete <id> [--json]");
|
|
1486
|
+
const api = await createConfiguredApiContext(context);
|
|
1487
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
1488
|
+
await api.client.requestVoid({
|
|
1489
|
+
method: "DELETE",
|
|
1490
|
+
path: `/accounts/${encodeURIComponent(id.toString())}`,
|
|
1491
|
+
});
|
|
1492
|
+
if (json) {
|
|
1493
|
+
writeJson(context.writer, { data: { id, deleted: true } });
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
context.writer.stdout(`Deleted account ${id}.`);
|
|
1497
|
+
}
|
|
1498
|
+
return { exitCode: ExitCode.Ok };
|
|
1499
|
+
}
|
|
1500
|
+
function addOptionalAccountQuery(query, key, value) {
|
|
1501
|
+
if (typeof value === "string") {
|
|
1502
|
+
query.set(key, value);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
function parseAccountFlags(args) {
|
|
1506
|
+
const options = {};
|
|
1507
|
+
let json = false;
|
|
1508
|
+
const valueOptions = [
|
|
1509
|
+
"owner-type",
|
|
1510
|
+
"owner-id",
|
|
1511
|
+
"label",
|
|
1512
|
+
"url",
|
|
1513
|
+
"avatar-url",
|
|
1514
|
+
"kind",
|
|
1515
|
+
"protocol",
|
|
1516
|
+
"sort-order",
|
|
1517
|
+
];
|
|
1518
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1519
|
+
const arg = args[index];
|
|
1520
|
+
if (arg === "--json") {
|
|
1521
|
+
json = true;
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
if (arg === "--default") {
|
|
1525
|
+
options.default = true;
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
if (arg === undefined || !arg.startsWith("--")) {
|
|
1529
|
+
throw createInvalidUsageError(`Unsupported argument: ${arg ?? ""}`.trim());
|
|
1530
|
+
}
|
|
1531
|
+
const key = arg.slice(2);
|
|
1532
|
+
const value = args[index + 1];
|
|
1533
|
+
if (!valueOptions.includes(key)) {
|
|
1534
|
+
throw createInvalidUsageError(`Unsupported option: --${key}`);
|
|
1535
|
+
}
|
|
1536
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1537
|
+
throw createInvalidUsageError(`Option --${key} requires a value`);
|
|
1538
|
+
}
|
|
1539
|
+
options[key] = value;
|
|
1540
|
+
index += 1;
|
|
1541
|
+
}
|
|
1542
|
+
return { json, options };
|
|
1543
|
+
}
|
|
1544
|
+
function createAccountMutationInput(options, requireCreateFields) {
|
|
1545
|
+
const input = {};
|
|
1546
|
+
if (typeof options["owner-type"] === "string") {
|
|
1547
|
+
input.ownerType = readAccountOwnerType(options["owner-type"]);
|
|
1548
|
+
}
|
|
1549
|
+
copyAccountStringOption(options, input, "label", "label");
|
|
1550
|
+
copyAccountStringOption(options, input, "url", "url");
|
|
1551
|
+
copyAccountStringOption(options, input, "avatar-url", "avatarUrl");
|
|
1552
|
+
copyAccountStringOption(options, input, "kind", "kind");
|
|
1553
|
+
copyAccountStringOption(options, input, "protocol", "protocol");
|
|
1554
|
+
if (typeof options["owner-id"] === "string") {
|
|
1555
|
+
input.ownerId = readPositiveInteger(options["owner-id"], "owner-id");
|
|
1556
|
+
}
|
|
1557
|
+
if (typeof options["sort-order"] === "string") {
|
|
1558
|
+
input.sortOrder = readInteger(options["sort-order"], "sort-order");
|
|
1559
|
+
}
|
|
1560
|
+
if (options.default === true) {
|
|
1561
|
+
input.isDefault = true;
|
|
1562
|
+
}
|
|
1563
|
+
if (requireCreateFields) {
|
|
1564
|
+
for (const key of ["owner-type", "label", "url"]) {
|
|
1565
|
+
if (typeof options[key] !== "string") {
|
|
1566
|
+
throw createInvalidUsageError(`Missing required option: --${key}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return input;
|
|
1571
|
+
}
|
|
1572
|
+
function readAccountOwnerType(value) {
|
|
1573
|
+
if (value === "contact" || value === "source" || value === "site") {
|
|
1574
|
+
return value;
|
|
1575
|
+
}
|
|
1576
|
+
throw createInvalidUsageError("--owner-type must be contact, source, or site");
|
|
1577
|
+
}
|
|
1578
|
+
function copyAccountStringOption(options, input, optionKey, inputKey) {
|
|
1579
|
+
if (typeof options[optionKey] === "string") {
|
|
1580
|
+
input[inputKey] = options[optionKey];
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
function readInteger(value, optionName) {
|
|
1584
|
+
const integer = Number.parseInt(value, 10);
|
|
1585
|
+
if (!Number.isInteger(integer) || integer.toString() !== value) {
|
|
1586
|
+
throw createInvalidUsageError(`--${optionName} must be an integer`);
|
|
1587
|
+
}
|
|
1588
|
+
return integer;
|
|
1589
|
+
}
|
|
1590
|
+
function writeAccountList(writer, accounts) {
|
|
1591
|
+
if (accounts.length === 0) {
|
|
1592
|
+
writer.stdout("No accounts found.");
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
for (const account of accounts) {
|
|
1596
|
+
writer.stdout(`${account.id} • ${account.ownerType}${account.ownerId === null ? "" : `:${account.ownerId}`} • ${formatAccountSummary(account)}${account.isDefault ? " • default" : ""}`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
function writeAccount(writer, account) {
|
|
1600
|
+
writer.stdout(`id: ${account.id}`);
|
|
1601
|
+
writer.stdout(`ownerType: ${account.ownerType}`);
|
|
1602
|
+
writer.stdout(`ownerId: ${account.ownerId ?? ""}`);
|
|
1603
|
+
writer.stdout(`label: ${account.label}`);
|
|
1604
|
+
writer.stdout(`url: ${account.url}`);
|
|
1605
|
+
writer.stdout(`avatarUrl: ${account.avatarUrl ?? ""}`);
|
|
1606
|
+
writer.stdout(`kind: ${account.kind ?? ""}`);
|
|
1607
|
+
writer.stdout(`protocol: ${account.protocol ?? ""}`);
|
|
1608
|
+
writer.stdout(`isDefault: ${account.isDefault ? "true" : "false"}`);
|
|
1609
|
+
writer.stdout(`sortOrder: ${account.sortOrder}`);
|
|
1610
|
+
}
|
|
1611
|
+
function writeAccountMutationOutput(writer, response, json) {
|
|
1612
|
+
if (json) {
|
|
1613
|
+
writeJson(writer, response);
|
|
1614
|
+
}
|
|
1615
|
+
else {
|
|
1616
|
+
writer.stdout(`Account ${response.data.id}: ${response.data.label}`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
async function runFollowersCommand(context, args) {
|
|
1620
|
+
if (args[0] !== "list") {
|
|
1621
|
+
throw createInvalidUsageError("Usage: slug followers list [--json]");
|
|
1622
|
+
}
|
|
1623
|
+
const json = readJsonFlag(args.slice(1));
|
|
1624
|
+
const client = await createConfiguredClient(context);
|
|
1625
|
+
const response = await client.requestJson({ path: "/followers" });
|
|
1626
|
+
if (json) {
|
|
1627
|
+
writeJson(context.writer, response);
|
|
1628
|
+
}
|
|
1629
|
+
else {
|
|
1630
|
+
writeFollowerList(context.writer, response.data);
|
|
1631
|
+
}
|
|
1632
|
+
return { exitCode: ExitCode.Ok };
|
|
1633
|
+
}
|
|
1634
|
+
function writeFollowerList(writer, followers) {
|
|
1635
|
+
if (followers.length === 0) {
|
|
1636
|
+
writer.stdout("No followers found.");
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
for (const follower of followers) {
|
|
1640
|
+
const displayName = follower.displayName ?? "";
|
|
1641
|
+
const followedAt = follower.followedAt ?? "";
|
|
1642
|
+
writer.stdout(`${follower.id} • ${follower.handle} • ${displayName} • ${follower.profileUrl} • ${followedAt}`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
async function runEngagementCommand(context, args) {
|
|
1646
|
+
switch (args[0]) {
|
|
1647
|
+
case "summary":
|
|
1648
|
+
return runEngagementSummaryCommand(context, args.slice(1));
|
|
1649
|
+
case "list":
|
|
1650
|
+
return runEngagementListCommand(context, args.slice(1));
|
|
1651
|
+
default:
|
|
1652
|
+
throw createInvalidUsageError("Usage: slug engagement <summary|list>");
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
async function runEngagementSummaryCommand(context, args) {
|
|
1656
|
+
const parsed = parseEngagementFlags(args, false);
|
|
1657
|
+
const client = await createConfiguredClient(context);
|
|
1658
|
+
const response = await client.requestJson({
|
|
1659
|
+
path: `/posts/${encodeURIComponent(parsed.post)}/engagement`,
|
|
1660
|
+
});
|
|
1661
|
+
if (parsed.json) {
|
|
1662
|
+
writeJson(context.writer, response);
|
|
1663
|
+
}
|
|
1664
|
+
else {
|
|
1665
|
+
writeEngagementSummary(context.writer, response.data);
|
|
1666
|
+
}
|
|
1667
|
+
return { exitCode: ExitCode.Ok };
|
|
1668
|
+
}
|
|
1669
|
+
async function runEngagementListCommand(context, args) {
|
|
1670
|
+
const parsed = parseEngagementFlags(args, true);
|
|
1671
|
+
const client = await createConfiguredClient(context);
|
|
1672
|
+
const response = await client.requestJson({
|
|
1673
|
+
path: `/posts/${encodeURIComponent(parsed.post)}/${parsed.type === "like" ? "likes" : "boosts"}`,
|
|
1674
|
+
});
|
|
1675
|
+
if (parsed.json) {
|
|
1676
|
+
writeJson(context.writer, response);
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
writeEngagementActorList(context.writer, response.data, parsed.type);
|
|
1680
|
+
}
|
|
1681
|
+
return { exitCode: ExitCode.Ok };
|
|
1682
|
+
}
|
|
1683
|
+
function parseEngagementFlags(args, requireType) {
|
|
1684
|
+
let json = false;
|
|
1685
|
+
let post;
|
|
1686
|
+
let type;
|
|
1687
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1688
|
+
const arg = args[index];
|
|
1689
|
+
if (arg === "--json") {
|
|
1690
|
+
json = true;
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
if (arg !== "--post" && arg !== "--type") {
|
|
1694
|
+
throw createInvalidUsageError(`Unsupported option: ${arg ?? ""}`.trim());
|
|
1695
|
+
}
|
|
1696
|
+
const value = args[index + 1];
|
|
1697
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1698
|
+
throw createInvalidUsageError(`Option ${arg} requires a value`);
|
|
1699
|
+
}
|
|
1700
|
+
if (arg === "--post") {
|
|
1701
|
+
post = value;
|
|
1702
|
+
}
|
|
1703
|
+
else {
|
|
1704
|
+
if (value !== "like" && value !== "boost") {
|
|
1705
|
+
throw createInvalidUsageError("--type must be like or boost");
|
|
1706
|
+
}
|
|
1707
|
+
type = value;
|
|
1708
|
+
}
|
|
1709
|
+
index += 1;
|
|
1710
|
+
}
|
|
1711
|
+
if (post === undefined) {
|
|
1712
|
+
throw createInvalidUsageError("Missing required option: --post");
|
|
1713
|
+
}
|
|
1714
|
+
if (requireType && type === undefined) {
|
|
1715
|
+
throw createInvalidUsageError("Missing required option: --type");
|
|
1716
|
+
}
|
|
1717
|
+
return { json, post, type };
|
|
1718
|
+
}
|
|
1719
|
+
function writeEngagementSummary(writer, summary) {
|
|
1720
|
+
writer.stdout(`post: ${summary.postSlug}`);
|
|
1721
|
+
writer.stdout(`likes: ${summary.likes.count}`);
|
|
1722
|
+
writer.stdout(`boosts: ${summary.boosts.count}`);
|
|
1723
|
+
writer.stdout(`comments: ${summary.comments.count}`);
|
|
1724
|
+
writer.stdout(`pendingComments: ${summary.comments.pendingCount}`);
|
|
1725
|
+
}
|
|
1726
|
+
function writeEngagementActorList(writer, actors, type) {
|
|
1727
|
+
if (actors.length === 0) {
|
|
1728
|
+
writer.stdout(`No ${type === "boost" ? "boosts" : "likes"} found.`);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
for (const actor of actors) {
|
|
1732
|
+
writer.stdout(`${actor.handle} • ${actor.displayName} • ${actor.profileUrl} • ${actor.activityUrl} • ${actor.receivedAt}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async function runFollowingCommand(context, args) {
|
|
1736
|
+
switch (args[0]) {
|
|
1737
|
+
case "list":
|
|
1738
|
+
return runFollowingListCommand(context, args.slice(1));
|
|
1739
|
+
case "follow":
|
|
1740
|
+
return runFollowingFollowCommand(context, args.slice(1));
|
|
1741
|
+
case "unfollow":
|
|
1742
|
+
return runFollowingUnfollowCommand(context, args.slice(1));
|
|
1743
|
+
default:
|
|
1744
|
+
throw createInvalidUsageError("Usage: slug following <list|follow|unfollow>");
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
async function runFollowingListCommand(context, args) {
|
|
1748
|
+
const json = readJsonFlag(args);
|
|
1749
|
+
const client = await createConfiguredClient(context);
|
|
1750
|
+
const response = await client.requestJson({ path: "/following" });
|
|
1751
|
+
if (json) {
|
|
1752
|
+
writeJson(context.writer, response);
|
|
1753
|
+
}
|
|
1754
|
+
else {
|
|
1755
|
+
writeFollowingList(context.writer, response.data);
|
|
1756
|
+
}
|
|
1757
|
+
return { exitCode: ExitCode.Ok };
|
|
1758
|
+
}
|
|
1759
|
+
async function runFollowingFollowCommand(context, args) {
|
|
1760
|
+
const { target, json } = readFollowingTargetArgs(args);
|
|
1761
|
+
const api = await createConfiguredApiContext(context);
|
|
1762
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
1763
|
+
const response = await api.client.requestJson({
|
|
1764
|
+
method: "POST",
|
|
1765
|
+
path: "/following",
|
|
1766
|
+
body: { target },
|
|
1767
|
+
});
|
|
1768
|
+
if (json) {
|
|
1769
|
+
writeJson(context.writer, response);
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
writeFollowingMutationOutput(context.writer, response.data);
|
|
1773
|
+
}
|
|
1774
|
+
return { exitCode: ExitCode.Ok };
|
|
1775
|
+
}
|
|
1776
|
+
async function runFollowingUnfollowCommand(context, args) {
|
|
1777
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug following unfollow <id> [--json]");
|
|
1778
|
+
const api = await createConfiguredApiContext(context);
|
|
1779
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
1780
|
+
const response = await api.client.requestJson({
|
|
1781
|
+
method: "POST",
|
|
1782
|
+
path: `/following/${encodeURIComponent(id.toString())}/unfollow`,
|
|
1783
|
+
});
|
|
1784
|
+
if (json) {
|
|
1785
|
+
writeJson(context.writer, response);
|
|
1786
|
+
}
|
|
1787
|
+
else {
|
|
1788
|
+
context.writer.stdout(`Following ${response.data.id}: ${response.data.status}`);
|
|
1789
|
+
}
|
|
1790
|
+
return { exitCode: ExitCode.Ok };
|
|
1791
|
+
}
|
|
1792
|
+
function readFollowingTargetArgs(args) {
|
|
1793
|
+
const json = args.includes("--json");
|
|
1794
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
1795
|
+
if (positional.length !== 1 || positional[0] === undefined || positional[0].startsWith("--")) {
|
|
1796
|
+
throw createInvalidUsageError("Usage: slug following follow <target> [--json]");
|
|
1797
|
+
}
|
|
1798
|
+
return { target: positional[0], json };
|
|
1799
|
+
}
|
|
1800
|
+
function writeFollowingList(writer, following) {
|
|
1801
|
+
if (following.length === 0) {
|
|
1802
|
+
writer.stdout("No following records found.");
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
for (const record of following) {
|
|
1806
|
+
writer.stdout(formatFollowingRecord(record));
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function writeFollowingMutationOutput(writer, following) {
|
|
1810
|
+
writer.stdout(`Following ${following.id}: ${following.handle} • ${following.status}`);
|
|
1811
|
+
writer.stdout(`Profile: ${following.profileUrl}`);
|
|
1812
|
+
}
|
|
1813
|
+
function formatFollowingRecord(record) {
|
|
1814
|
+
const displayName = record.displayName ?? "";
|
|
1815
|
+
return `${record.id} • ${record.handle} • ${displayName} • ${record.status} • ${record.profileUrl}`;
|
|
1816
|
+
}
|
|
1817
|
+
async function runContactCommand(context, args) {
|
|
1818
|
+
switch (args[0]) {
|
|
1819
|
+
case "list":
|
|
1820
|
+
return runContactListCommand(context, args.slice(1));
|
|
1821
|
+
case "show":
|
|
1822
|
+
return runContactShowCommand(context, args.slice(1));
|
|
1823
|
+
case "create":
|
|
1824
|
+
return runContactCreateCommand(context, args.slice(1));
|
|
1825
|
+
case "edit":
|
|
1826
|
+
return runContactEditCommand(context, args.slice(1));
|
|
1827
|
+
case "delete":
|
|
1828
|
+
return runContactDeleteCommand(context, args.slice(1));
|
|
1829
|
+
default:
|
|
1830
|
+
throw createInvalidUsageError("Usage: slug contact <list|show|create|edit|delete>");
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
async function runContactListCommand(context, args) {
|
|
1834
|
+
const json = readJsonFlag(args);
|
|
1835
|
+
const client = await createConfiguredClient(context);
|
|
1836
|
+
const response = await client.requestJson({ path: "/contacts" });
|
|
1837
|
+
if (json) {
|
|
1838
|
+
writeJson(context.writer, response);
|
|
1839
|
+
}
|
|
1840
|
+
else {
|
|
1841
|
+
writeContactList(context.writer, response.data);
|
|
1842
|
+
}
|
|
1843
|
+
return { exitCode: ExitCode.Ok };
|
|
1844
|
+
}
|
|
1845
|
+
async function runContactShowCommand(context, args) {
|
|
1846
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug contact show <id> [--json]");
|
|
1847
|
+
const client = await createConfiguredClient(context);
|
|
1848
|
+
const response = await client.requestJson({
|
|
1849
|
+
path: `/contacts/${encodeURIComponent(id.toString())}`,
|
|
1850
|
+
});
|
|
1851
|
+
if (json) {
|
|
1852
|
+
writeJson(context.writer, response);
|
|
1853
|
+
}
|
|
1854
|
+
else {
|
|
1855
|
+
writeContact(context.writer, response.data);
|
|
1856
|
+
}
|
|
1857
|
+
return { exitCode: ExitCode.Ok };
|
|
1858
|
+
}
|
|
1859
|
+
async function runContactCreateCommand(context, args) {
|
|
1860
|
+
const parsed = parseContactFlags(args);
|
|
1861
|
+
const body = createContactMutationInput(parsed.options, true);
|
|
1862
|
+
const api = await createConfiguredApiContext(context);
|
|
1863
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
1864
|
+
const response = await api.client.requestJson({
|
|
1865
|
+
method: "POST",
|
|
1866
|
+
path: "/contacts",
|
|
1867
|
+
body,
|
|
1868
|
+
});
|
|
1869
|
+
writeContactMutationOutput(context.writer, response, parsed.json);
|
|
1870
|
+
return { exitCode: ExitCode.Ok };
|
|
1871
|
+
}
|
|
1872
|
+
async function runContactEditCommand(context, args) {
|
|
1873
|
+
const idArg = args[0];
|
|
1874
|
+
if (idArg === undefined || idArg.startsWith("--")) {
|
|
1875
|
+
throw createInvalidUsageError("Usage: slug contact edit <id> [options]");
|
|
1876
|
+
}
|
|
1877
|
+
const id = readPositiveInteger(idArg, "id");
|
|
1878
|
+
const parsed = parseContactFlags(args.slice(1));
|
|
1879
|
+
const body = createContactMutationInput(parsed.options, false);
|
|
1880
|
+
if (Object.keys(body).length === 0) {
|
|
1881
|
+
throw createInvalidUsageError("At least one edit option is required");
|
|
1882
|
+
}
|
|
1883
|
+
const api = await createConfiguredApiContext(context);
|
|
1884
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, parsed.json);
|
|
1885
|
+
const response = await api.client.requestJson({
|
|
1886
|
+
method: "PUT",
|
|
1887
|
+
path: `/contacts/${encodeURIComponent(id.toString())}`,
|
|
1888
|
+
body,
|
|
1889
|
+
});
|
|
1890
|
+
writeContactMutationOutput(context.writer, response, parsed.json);
|
|
1891
|
+
return { exitCode: ExitCode.Ok };
|
|
1892
|
+
}
|
|
1893
|
+
async function runContactDeleteCommand(context, args) {
|
|
1894
|
+
const { id, json } = readIdCommandArgs(args, "Usage: slug contact delete <id> [--json]");
|
|
1895
|
+
const api = await createConfiguredApiContext(context);
|
|
1896
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
1897
|
+
await api.client.requestVoid({
|
|
1898
|
+
method: "DELETE",
|
|
1899
|
+
path: `/contacts/${encodeURIComponent(id.toString())}`,
|
|
1900
|
+
});
|
|
1901
|
+
if (json) {
|
|
1902
|
+
writeJson(context.writer, { data: { id, deleted: true } });
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
context.writer.stdout(`Deleted contact ${id}.`);
|
|
1906
|
+
}
|
|
1907
|
+
return { exitCode: ExitCode.Ok };
|
|
1908
|
+
}
|
|
1909
|
+
function parseContactFlags(args) {
|
|
1910
|
+
const options = {};
|
|
1911
|
+
let json = false;
|
|
1912
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1913
|
+
const arg = args[index];
|
|
1914
|
+
if (arg === "--json") {
|
|
1915
|
+
json = true;
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
if (arg === undefined || !arg.startsWith("--")) {
|
|
1919
|
+
throw createInvalidUsageError(`Unsupported argument: ${arg ?? ""}`.trim());
|
|
1920
|
+
}
|
|
1921
|
+
const key = arg.slice(2);
|
|
1922
|
+
const value = args[index + 1];
|
|
1923
|
+
if (!["name", "url", "avatar-url", "description"].includes(key)) {
|
|
1924
|
+
throw createInvalidUsageError(`Unsupported option: --${key}`);
|
|
1925
|
+
}
|
|
1926
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1927
|
+
throw createInvalidUsageError(`Option --${key} requires a value`);
|
|
1928
|
+
}
|
|
1929
|
+
options[key] = value;
|
|
1930
|
+
index += 1;
|
|
1931
|
+
}
|
|
1932
|
+
return { json, options };
|
|
1933
|
+
}
|
|
1934
|
+
function createContactMutationInput(options, requireName) {
|
|
1935
|
+
const input = {};
|
|
1936
|
+
if (options.name !== undefined)
|
|
1937
|
+
input.name = options.name;
|
|
1938
|
+
if (options.url !== undefined)
|
|
1939
|
+
input.url = options.url;
|
|
1940
|
+
if (options["avatar-url"] !== undefined)
|
|
1941
|
+
input.avatarUrl = options["avatar-url"];
|
|
1942
|
+
if (options.description !== undefined)
|
|
1943
|
+
input.description = options.description;
|
|
1944
|
+
if (requireName && options.name === undefined) {
|
|
1945
|
+
throw createInvalidUsageError("Missing required option: --name");
|
|
1946
|
+
}
|
|
1947
|
+
return input;
|
|
1948
|
+
}
|
|
1949
|
+
function readIdCommandArgs(args, usage) {
|
|
1950
|
+
const json = args.includes("--json");
|
|
1951
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
1952
|
+
if (positional.length !== 1 || positional[0] === undefined || positional[0].startsWith("--")) {
|
|
1953
|
+
throw createInvalidUsageError(usage);
|
|
1954
|
+
}
|
|
1955
|
+
return { id: readPositiveInteger(positional[0], "id"), json };
|
|
1956
|
+
}
|
|
1957
|
+
function writeContactList(writer, contacts) {
|
|
1958
|
+
if (contacts.length === 0) {
|
|
1959
|
+
writer.stdout("No contacts found.");
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
for (const contact of contacts) {
|
|
1963
|
+
const url = contact.url ?? "";
|
|
1964
|
+
const accountSummary = contact.accounts === undefined ? "" : ` • ${contact.accounts.length} accounts`;
|
|
1965
|
+
writer.stdout(`${contact.id} • ${contact.name} • ${url}${accountSummary}`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function writeContact(writer, contact) {
|
|
1969
|
+
writer.stdout(`id: ${contact.id}`);
|
|
1970
|
+
writer.stdout(`name: ${contact.name}`);
|
|
1971
|
+
writer.stdout(`url: ${contact.url ?? ""}`);
|
|
1972
|
+
if (contact.avatarUrl !== undefined)
|
|
1973
|
+
writer.stdout(`avatarUrl: ${contact.avatarUrl ?? ""}`);
|
|
1974
|
+
if (contact.description !== undefined)
|
|
1975
|
+
writer.stdout(`description: ${contact.description ?? ""}`);
|
|
1976
|
+
if (contact.defaultAccount !== undefined && contact.defaultAccount !== null) {
|
|
1977
|
+
writer.stdout(`defaultAccount: ${formatAccountSummary(contact.defaultAccount)}`);
|
|
1978
|
+
}
|
|
1979
|
+
if (contact.accounts !== undefined && contact.accounts.length > 0) {
|
|
1980
|
+
writer.stdout("accounts:");
|
|
1981
|
+
for (const account of contact.accounts) {
|
|
1982
|
+
writer.stdout(`- ${formatAccountSummary(account)}`);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
function writeContactMutationOutput(writer, response, json) {
|
|
1987
|
+
if (json) {
|
|
1988
|
+
writeJson(writer, response);
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
writer.stdout(`Contact ${response.data.id}: ${response.data.name}`);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
function formatAccountSummary(account) {
|
|
1995
|
+
const suffix = [account.kind, account.protocol]
|
|
1996
|
+
.filter((value) => value !== undefined && value !== null)
|
|
1997
|
+
.join("/");
|
|
1998
|
+
return suffix === ""
|
|
1999
|
+
? `${account.label} <${account.url}>`
|
|
2000
|
+
: `${account.label} <${account.url}> (${suffix})`;
|
|
2001
|
+
}
|
|
2002
|
+
async function runSiteCommand(context, args) {
|
|
2003
|
+
if (args[0] !== "config") {
|
|
2004
|
+
throw createInvalidUsageError("Usage: slug site config <show|set>");
|
|
2005
|
+
}
|
|
2006
|
+
switch (args[1]) {
|
|
2007
|
+
case "show":
|
|
2008
|
+
return runSiteConfigShowCommand(context, args.slice(2));
|
|
2009
|
+
case "set":
|
|
2010
|
+
return runSiteConfigSetCommand(context, args.slice(2));
|
|
2011
|
+
default:
|
|
2012
|
+
throw createInvalidUsageError("Usage: slug site config <show|set>");
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
async function runSiteConfigShowCommand(context, args) {
|
|
2016
|
+
const json = readJsonFlag(args);
|
|
2017
|
+
const client = await createConfiguredClient(context);
|
|
2018
|
+
const response = await client.requestJson({ path: "/site-config" });
|
|
2019
|
+
if (json) {
|
|
2020
|
+
writeJson(context.writer, response.data);
|
|
2021
|
+
}
|
|
2022
|
+
else {
|
|
2023
|
+
writeSiteConfig(context.writer, response.data.core);
|
|
2024
|
+
}
|
|
2025
|
+
return { exitCode: ExitCode.Ok };
|
|
2026
|
+
}
|
|
2027
|
+
async function runSiteConfigSetCommand(context, args) {
|
|
2028
|
+
const json = args[2] === "--json";
|
|
2029
|
+
if (args.length !== (json ? 3 : 2)) {
|
|
2030
|
+
throw createInvalidUsageError("Usage: slug site config set <field> <value> [--json]");
|
|
2031
|
+
}
|
|
2032
|
+
const [field, value] = args;
|
|
2033
|
+
if (field === undefined || value === undefined) {
|
|
2034
|
+
throw createInvalidUsageError("Usage: slug site config set <field> <value> [--json]");
|
|
2035
|
+
}
|
|
2036
|
+
const client = await createConfiguredClient(context);
|
|
2037
|
+
const response = await client.requestJson({
|
|
2038
|
+
method: "PATCH",
|
|
2039
|
+
path: "/site-config",
|
|
2040
|
+
body: createSiteConfigUpdate(field, value),
|
|
2041
|
+
});
|
|
2042
|
+
if (json) {
|
|
2043
|
+
writeJson(context.writer, response.data);
|
|
2044
|
+
}
|
|
2045
|
+
else {
|
|
2046
|
+
context.writer.stdout(`Updated site config ${field}.`);
|
|
2047
|
+
writeSiteConfig(context.writer, response.data.core);
|
|
2048
|
+
}
|
|
2049
|
+
return { exitCode: ExitCode.Ok };
|
|
2050
|
+
}
|
|
2051
|
+
function createSiteConfigUpdate(field, value) {
|
|
2052
|
+
switch (field) {
|
|
2053
|
+
case "name":
|
|
2054
|
+
return { core: { name: value } };
|
|
2055
|
+
case "url":
|
|
2056
|
+
return { core: { url: value } };
|
|
2057
|
+
case "tagline":
|
|
2058
|
+
return { core: { tagline: value } };
|
|
2059
|
+
case "description":
|
|
2060
|
+
return { core: { description: value } };
|
|
2061
|
+
case "homepage.intro":
|
|
2062
|
+
return { core: { homepage: { intro: value } } };
|
|
2063
|
+
case "homepage.body":
|
|
2064
|
+
return { core: { homepage: { body: value } } };
|
|
2065
|
+
default:
|
|
2066
|
+
throw createInvalidUsageError(`Unknown site config field: ${field}`);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
function writeSiteConfig(writer, config) {
|
|
2070
|
+
writer.stdout(`name: ${config.name}`);
|
|
2071
|
+
writer.stdout(`url: ${config.url}`);
|
|
2072
|
+
writer.stdout(`tagline: ${config.tagline}`);
|
|
2073
|
+
writer.stdout(`description: ${config.description}`);
|
|
2074
|
+
writer.stdout(`homepage.intro: ${config.homepage.intro}`);
|
|
2075
|
+
writer.stdout(`homepage.body: ${config.homepage.body}`);
|
|
2076
|
+
}
|
|
2077
|
+
async function createConfiguredClient(context) {
|
|
2078
|
+
return (await createConfiguredApiContext(context)).client;
|
|
2079
|
+
}
|
|
2080
|
+
async function createConfiguredApiContext(context) {
|
|
2081
|
+
const config = await readConfig(context.configPath);
|
|
2082
|
+
if (config.apiBaseUrl === undefined || config.apiBaseUrl.trim() === "") {
|
|
2083
|
+
throw createInvalidUsageError("apiBaseUrl is not configured");
|
|
2084
|
+
}
|
|
2085
|
+
if (config.apiKey === undefined || config.apiKey.trim() === "") {
|
|
2086
|
+
throw createInvalidUsageError("apiKey is not configured. Run slug login first.");
|
|
2087
|
+
}
|
|
2088
|
+
const apiBaseUrl = normalizeUrl(config.apiBaseUrl);
|
|
2089
|
+
return {
|
|
2090
|
+
apiBaseUrl,
|
|
2091
|
+
client: new SlugHttpClient({
|
|
2092
|
+
apiBaseUrl,
|
|
2093
|
+
apiKey: config.apiKey,
|
|
2094
|
+
fetchImpl: context.fetchImpl,
|
|
2095
|
+
}),
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
async function runConfigCommand(context, args) {
|
|
2099
|
+
switch (args[0]) {
|
|
2100
|
+
case "show":
|
|
2101
|
+
return runConfigShowCommand(context, args.slice(1));
|
|
2102
|
+
case "set":
|
|
2103
|
+
return runConfigSetCommand(context, args.slice(1));
|
|
2104
|
+
default:
|
|
2105
|
+
throw createInvalidUsageError("Usage: slug config <show|set>");
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
async function runConfigShowCommand(context, args) {
|
|
2109
|
+
const json = readJsonFlag(args);
|
|
2110
|
+
const config = await readConfig(context.configPath);
|
|
2111
|
+
const displayConfig = toDisplayConfig(config);
|
|
2112
|
+
if (json) {
|
|
2113
|
+
writeJson(context.writer, displayConfig);
|
|
2114
|
+
}
|
|
2115
|
+
else {
|
|
2116
|
+
context.writer.stdout(`apiBaseUrl: ${displayConfig.apiBaseUrl ?? "not configured"}`);
|
|
2117
|
+
context.writer.stdout(`apiKeyConfigured: ${displayConfig.apiKeyConfigured ? "true" : "false"}`);
|
|
2118
|
+
}
|
|
2119
|
+
return { exitCode: ExitCode.Ok };
|
|
2120
|
+
}
|
|
2121
|
+
async function runConfigSetCommand(context, args) {
|
|
2122
|
+
const key = args[0];
|
|
2123
|
+
const value = args[1];
|
|
2124
|
+
if (key === undefined || value === undefined || args.length !== 2) {
|
|
2125
|
+
throw createInvalidUsageError("Usage: slug config set <api-base-url|api-key> <value>");
|
|
2126
|
+
}
|
|
2127
|
+
const config = await readConfig(context.configPath);
|
|
2128
|
+
switch (key) {
|
|
2129
|
+
case "api-base-url":
|
|
2130
|
+
validateUrl(value);
|
|
2131
|
+
await writeConfig(context.configPath, setConfigApiBaseUrl(config, value));
|
|
2132
|
+
break;
|
|
2133
|
+
case "api-key":
|
|
2134
|
+
await writeConfig(context.configPath, setConfigApiKey(config, value));
|
|
2135
|
+
break;
|
|
2136
|
+
default:
|
|
2137
|
+
throw createInvalidUsageError(`Unknown config key: ${key}`);
|
|
2138
|
+
}
|
|
2139
|
+
context.writer.stdout(`Updated ${key}.`);
|
|
2140
|
+
return { exitCode: ExitCode.Ok };
|
|
2141
|
+
}
|
|
2142
|
+
function readJsonFlag(args) {
|
|
2143
|
+
if (args.length === 0) {
|
|
2144
|
+
return false;
|
|
2145
|
+
}
|
|
2146
|
+
if (args.length === 1 && args[0] === "--json") {
|
|
2147
|
+
return true;
|
|
2148
|
+
}
|
|
2149
|
+
throw createInvalidUsageError(`Unsupported option: ${args.join(" ")}`);
|
|
2150
|
+
}
|
|
2151
|
+
async function promptForValue(context, message) {
|
|
2152
|
+
if (context.prompt === undefined) {
|
|
2153
|
+
throw createInvalidUsageError(`${message.replace(/: $/, "")} is required`);
|
|
2154
|
+
}
|
|
2155
|
+
return await context.prompt(message);
|
|
2156
|
+
}
|
|
2157
|
+
export function createBrowserAuthUrl(apiBaseUrl) {
|
|
2158
|
+
const url = new URL(apiBaseUrl);
|
|
2159
|
+
const normalizedPath = url.pathname.replace(/\/+$/, "");
|
|
2160
|
+
if (normalizedPath.endsWith("/api/v1")) {
|
|
2161
|
+
url.pathname = `${normalizedPath.slice(0, -"/api/v1".length)}/cli/auth`;
|
|
2162
|
+
}
|
|
2163
|
+
else {
|
|
2164
|
+
url.pathname = "/cli/auth";
|
|
2165
|
+
}
|
|
2166
|
+
url.search = "";
|
|
2167
|
+
url.hash = "";
|
|
2168
|
+
return url.toString();
|
|
2169
|
+
}
|
|
2170
|
+
function normalizeUrl(value) {
|
|
2171
|
+
validateUrl(value);
|
|
2172
|
+
return value.replace(/\/+$/, "");
|
|
2173
|
+
}
|
|
2174
|
+
function validateUrl(value) {
|
|
2175
|
+
try {
|
|
2176
|
+
const url = new URL(value);
|
|
2177
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2178
|
+
throw new Error("Unsupported protocol");
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
catch {
|
|
2182
|
+
throw createInvalidUsageError("api-base-url must be a valid HTTP or HTTPS URL");
|
|
2183
|
+
}
|
|
2184
|
+
}
|