@evcraddock/slug-cli 0.5.0 → 0.6.1
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 +27 -6
- package/dist/commands.d.ts +1 -0
- package/dist/commands.js +401 -98
- package/dist/config.d.ts +15 -6
- package/dist/config.js +94 -39
- package/dist/http.d.ts +0 -7
- package/dist/http.js +2 -1
- package/dist/index.js +20 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -25,22 +25,43 @@ Create a standalone site with:
|
|
|
25
25
|
slug init ./my-site --name my-site --site-title "My Site"
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Run `slug init --help` for template options.
|
|
28
|
+
Run `slug init --help` for template options. By default, `slug init` consumes the package-backed `@evcraddock/slug-template` default template. `--template-dir` and `--template-url` remain available for local or remote template testing, and `--template` reserves the selection path for future named or external template packages.
|
|
29
29
|
|
|
30
30
|
## Login and configuration
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
The CLI stores named Slug API websites. There is no default or current site; API-backed commands require `--site <name>` so the target is explicit.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Add a site and API key directly:
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
```bash
|
|
37
|
+
slug config site add slugkit --api-base-url https://slugkit.com/api/v1 --api-key <key>
|
|
38
|
+
slug --site slugkit doctor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or use browser-assisted login for a named site:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
slug --site slugkit login https://slugkit.com/api/v1
|
|
45
|
+
slug --site slugkit doctor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The CLI stores YAML config at `${XDG_CONFIG_HOME:-~/.config}/slug/config.yaml` by default. Tests and isolated runs can set `SLUG_CONFIG_HOME` to override the config directory. `slug config show` and `slug config site list` report whether API keys are configured without printing key values.
|
|
49
|
+
|
|
50
|
+
Use `slug --site <name> site config show` to read supported runtime site configuration through the Slugkit API. Use `slug --site <name> site config set <field> <value>` to update supported fields such as `name`, `url`, `tagline`, `description`, `homepage.intro`, and `homepage.body`.
|
|
51
|
+
|
|
52
|
+
Use `slug --site <name> doctor` to check API compatibility and package metadata. Use `slug --site <name> upgrade` for advisory, non-destructive package update recommendations; it reports suggested dependency changes without rewriting site files.
|
|
37
53
|
|
|
38
|
-
Use `slug post list`, `slug post show <slug>`, `slug post create`, `slug post edit <slug>`, `slug post publish <slug>`, `slug post unpublish <slug>`, and `slug post delete <slug>` to manage posts through the Slugkit posts API.
|
|
54
|
+
Use `slug --site <name> post list`, `slug --site <name> post show <slug>`, `slug --site <name> post create`, `slug --site <name> post edit <slug>`, `slug --site <name> post publish <slug>`, `slug --site <name> post unpublish <slug>`, and `slug --site <name> post delete <slug>` to manage posts through the Slugkit posts API.
|
|
39
55
|
|
|
40
56
|
Use `slug tag list` to list tags and post usage counts through the Slugkit tags API.
|
|
41
57
|
|
|
42
58
|
Example config:
|
|
43
59
|
|
|
44
60
|
```yaml
|
|
45
|
-
|
|
61
|
+
sites:
|
|
62
|
+
slugkit:
|
|
63
|
+
apiBaseUrl: https://slugkit.com/api/v1
|
|
64
|
+
apiKey: slug_...
|
|
65
|
+
testapp:
|
|
66
|
+
apiBaseUrl: http://localhost:3000/api/v1
|
|
46
67
|
```
|
package/dist/commands.d.ts
CHANGED
package/dist/commands.js
CHANGED
|
@@ -4,20 +4,23 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, basename, extname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { x as extractTarball } from "tar";
|
|
7
|
-
import {
|
|
7
|
+
import { SLUGKIT_API_MAJOR_VERSION, SLUGKIT_API_NAME, compareSlugkitApiVersions, normalizeApiBaseUrl, readSlugkitApiMajorVersion, } from "@evcraddock/slug-core";
|
|
8
|
+
import { GENERATED_SITE_SLUGKIT_DEPENDENCIES, SLUGKIT_TEMPLATE_PACKAGE_NAME, getTemplateMetadata, getTemplateRoot, isTemplateEntryIncluded, } from "@evcraddock/slug-template";
|
|
9
|
+
import { isValidSiteName, readConfig, removeConfigSite, setConfigSiteApiBaseUrl, setConfigSiteApiKey, toDisplayConfig, writeConfig, } from "./config.js";
|
|
8
10
|
import { CliError, createInvalidUsageError, ExitCode } from "./errors.js";
|
|
9
11
|
import { SlugHttpClient } from "./http.js";
|
|
10
12
|
import { writeJson } from "./output.js";
|
|
11
13
|
const HELP_TEXT = `slug - manage Slugkit sites
|
|
12
14
|
|
|
13
15
|
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>]
|
|
19
|
-
slug [--config <file>]
|
|
20
|
-
slug [--config <file>]
|
|
16
|
+
slug [--config <file>] [--site <name>] --help
|
|
17
|
+
slug [--config <file>] [--site <name>] --version
|
|
18
|
+
slug [--config <file>] [--site <name>] version [--json]
|
|
19
|
+
slug [--config <file>] --site <name> doctor [--json]
|
|
20
|
+
slug [--config <file>] --site <name> upgrade [--json]
|
|
21
|
+
slug [--config <file>] --site <name> login [api-base-url]
|
|
22
|
+
slug [--config <file>] init <directory> --name <name> [--site-title <title>] [--template <name>] [--template-url <url>] [--template-dir <dir>] [--json]
|
|
23
|
+
slug [--config <file>] --site <name> post list [--type article|link|note] [--status draft|published|all] [--tag <slug>] [--json]
|
|
21
24
|
slug [--config <file>] post show <slug> [--json]
|
|
22
25
|
slug [--config <file>] post create --type article|link|note --slug <slug> --content <text> [--title <text>] [--url <url>] [--excerpt <text>] [--tag <slug>]... [--source-id <id>] [--credit-contact-id <id>]... [--json]
|
|
23
26
|
slug [--config <file>] post edit <slug> [--slug <new-slug>] [--title <text>] [--content <text>] [--url <url>] [--excerpt <text>] [--tag <slug>]... [--source-id <id>] [--credit-contact-id <id>]... [--json]
|
|
@@ -53,14 +56,19 @@ Usage:
|
|
|
53
56
|
slug [--config <file>] contact create --name <name> [--url <url>] [--json]
|
|
54
57
|
slug [--config <file>] contact edit <id> [--name <name>] [--url <url>] [--json]
|
|
55
58
|
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]
|
|
59
|
+
slug [--config <file>] --site <name> site config show [--json]
|
|
60
|
+
slug [--config <file>] --site <name> site config set <field> <value> [--json]
|
|
58
61
|
slug [--config <file>] config show [--json]
|
|
59
|
-
slug [--config <file>] config
|
|
60
|
-
slug [--config <file>] config set
|
|
62
|
+
slug [--config <file>] config site add <name> --api-base-url <url> [--api-key <key>]
|
|
63
|
+
slug [--config <file>] config site set-api-base-url <name> <url>
|
|
64
|
+
slug [--config <file>] config site set-api-key <name> <key>
|
|
65
|
+
slug [--config <file>] config site list [--json]
|
|
66
|
+
slug [--config <file>] config site show <name> [--json]
|
|
67
|
+
slug [--config <file>] config site remove <name>
|
|
61
68
|
|
|
62
69
|
Options:
|
|
63
70
|
--config <file> Use a specific YAML config file.
|
|
71
|
+
--site <name> Target a named configured Slug API website.
|
|
64
72
|
--help Show this help.
|
|
65
73
|
--version Show the CLI version.
|
|
66
74
|
--json Print command output as JSON when supported.`;
|
|
@@ -79,6 +87,8 @@ export async function runCommand(context) {
|
|
|
79
87
|
return runVersionCommand(context, args.slice(1));
|
|
80
88
|
case "doctor":
|
|
81
89
|
return runDoctorCommand(context, args.slice(1));
|
|
90
|
+
case "upgrade":
|
|
91
|
+
return runUpgradeCommand(context, args.slice(1));
|
|
82
92
|
case "login":
|
|
83
93
|
return runLoginCommand(context, args.slice(1));
|
|
84
94
|
case "init":
|
|
@@ -123,24 +133,25 @@ async function runVersionCommand(context, args) {
|
|
|
123
133
|
}
|
|
124
134
|
return { exitCode: ExitCode.Ok };
|
|
125
135
|
}
|
|
126
|
-
const SUPPORTED_API_MAJOR_VERSION =
|
|
127
|
-
const SITE_TEMPLATE_PACKAGE_NAME =
|
|
128
|
-
const
|
|
136
|
+
const SUPPORTED_API_MAJOR_VERSION = SLUGKIT_API_MAJOR_VERSION;
|
|
137
|
+
const SITE_TEMPLATE_PACKAGE_NAME = getTemplateMetadata().templateSitePackageName;
|
|
138
|
+
const DEFAULT_TEMPLATE_METADATA = getTemplateMetadata();
|
|
139
|
+
const DEFAULT_TEMPLATE_NAME = DEFAULT_TEMPLATE_METADATA.name;
|
|
129
140
|
const INIT_HELP_TEXT = `slug init - create a standalone Slugkit-compatible website
|
|
130
141
|
|
|
131
142
|
Usage:
|
|
132
|
-
slug init <directory> --name <name> [--site-title <title>] [--template-url <url>] [--template-dir <dir>] [--json]
|
|
143
|
+
slug init <directory> --name <name> [--site-title <title>] [--template <name>] [--template-url <url>] [--template-dir <dir>] [--json]
|
|
133
144
|
|
|
134
145
|
Options:
|
|
135
146
|
--name <name> npm package name for the generated site.
|
|
136
147
|
--site-title <title> Human-readable site title. Defaults to a title derived from --name.
|
|
148
|
+
--template <name> Template name or package. Defaults to ${DEFAULT_TEMPLATE_NAME}.
|
|
137
149
|
--template-url <url> Download a template tarball from a custom URL.
|
|
138
150
|
--template-dir <dir> Copy a template from a local directory.
|
|
139
151
|
--json Print command output as JSON.
|
|
140
152
|
--help, -h Show this help.
|
|
141
153
|
|
|
142
|
-
By default, slug init uses
|
|
143
|
-
Installed CLI releases download the versioned template asset that matches the CLI version.`;
|
|
154
|
+
By default, slug init uses ${SLUGKIT_TEMPLATE_PACKAGE_NAME}. Template packages are initialization sources only; generated sites do not depend on them at runtime.`;
|
|
144
155
|
const SITE_INIT_NEXT_STEPS = [
|
|
145
156
|
"Install dependencies",
|
|
146
157
|
"Copy and edit .env",
|
|
@@ -150,14 +161,6 @@ const SITE_INIT_NEXT_STEPS = [
|
|
|
150
161
|
"Configure ActivityPub domain and actor settings",
|
|
151
162
|
"Configure slug and run slug doctor",
|
|
152
163
|
];
|
|
153
|
-
const TEMPLATE_COPY_EXCLUDES = new Set([
|
|
154
|
-
".env",
|
|
155
|
-
"node_modules",
|
|
156
|
-
"dist",
|
|
157
|
-
"build",
|
|
158
|
-
"data",
|
|
159
|
-
"coverage",
|
|
160
|
-
]);
|
|
161
164
|
async function runInitCommand(context, args) {
|
|
162
165
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
163
166
|
context.writer.stdout(INIT_HELP_TEXT);
|
|
@@ -200,6 +203,7 @@ function parseInitArgs(args) {
|
|
|
200
203
|
let directory;
|
|
201
204
|
let name;
|
|
202
205
|
let siteTitle;
|
|
206
|
+
let template;
|
|
203
207
|
let templateUrl;
|
|
204
208
|
let templateDir;
|
|
205
209
|
let json = false;
|
|
@@ -219,6 +223,11 @@ function parseInitArgs(args) {
|
|
|
219
223
|
index += 1;
|
|
220
224
|
continue;
|
|
221
225
|
}
|
|
226
|
+
if (arg === "--template") {
|
|
227
|
+
template = readRequiredInitOptionValue(args, index, "template");
|
|
228
|
+
index += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
222
231
|
if (arg === "--template-url") {
|
|
223
232
|
templateUrl = readRequiredInitOptionValue(args, index, "template-url");
|
|
224
233
|
index += 1;
|
|
@@ -233,15 +242,18 @@ function parseInitArgs(args) {
|
|
|
233
242
|
throw createInvalidUsageError(`Unsupported option: ${arg}`);
|
|
234
243
|
}
|
|
235
244
|
if (directory !== undefined || arg === undefined) {
|
|
236
|
-
throw createInvalidUsageError("Usage: slug init <directory> --name <name> [--site-title <title>] [--template-url <url>] [--template-dir <dir>] [--json]");
|
|
245
|
+
throw createInvalidUsageError("Usage: slug init <directory> --name <name> [--site-title <title>] [--template <name>] [--template-url <url>] [--template-dir <dir>] [--json]");
|
|
237
246
|
}
|
|
238
247
|
directory = arg;
|
|
239
248
|
}
|
|
240
249
|
if (directory === undefined || name === undefined) {
|
|
241
|
-
throw createInvalidUsageError("Usage: slug init <directory> --name <name> [--site-title <title>] [--template-url <url>] [--template-dir <dir>] [--json]");
|
|
250
|
+
throw createInvalidUsageError("Usage: slug init <directory> --name <name> [--site-title <title>] [--template <name>] [--template-url <url>] [--template-dir <dir>] [--json]");
|
|
242
251
|
}
|
|
243
252
|
const normalizedName = name.trim();
|
|
244
253
|
validatePackageName(normalizedName);
|
|
254
|
+
if (template !== undefined && (templateUrl !== undefined || templateDir !== undefined)) {
|
|
255
|
+
throw createInvalidUsageError("Use --template, --template-url, or --template-dir, not more than one");
|
|
256
|
+
}
|
|
245
257
|
if (templateUrl !== undefined && templateDir !== undefined) {
|
|
246
258
|
throw createInvalidUsageError("Use only one of --template-url or --template-dir");
|
|
247
259
|
}
|
|
@@ -250,6 +262,7 @@ function parseInitArgs(args) {
|
|
|
250
262
|
name: normalizedName,
|
|
251
263
|
siteTitle: siteTitle?.trim() || humanizePackageName(normalizedName),
|
|
252
264
|
json,
|
|
265
|
+
template: template?.trim() || undefined,
|
|
253
266
|
templateUrl: templateUrl?.trim() || undefined,
|
|
254
267
|
templateDir: templateDir?.trim() || undefined,
|
|
255
268
|
};
|
|
@@ -318,20 +331,15 @@ async function copyTemplateSite(source, destination, relativeDirectory = "") {
|
|
|
318
331
|
}
|
|
319
332
|
}
|
|
320
333
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
join("src", "federation", "__tests__", "follow.test.ts"),
|
|
324
|
-
]);
|
|
325
|
-
function shouldExcludeTemplateEntry(relativePath, name) {
|
|
326
|
-
return (GENERATED_SITE_TEST_EXCLUDES.has(relativePath) ||
|
|
327
|
-
TEMPLATE_COPY_EXCLUDES.has(name) ||
|
|
328
|
-
name.endsWith(".log") ||
|
|
329
|
-
name.endsWith(".tsbuildinfo"));
|
|
334
|
+
function shouldExcludeTemplateEntry(relativePath, _name) {
|
|
335
|
+
return !isTemplateEntryIncluded(relativePath);
|
|
330
336
|
}
|
|
331
337
|
async function replaceGeneratedSitePlaceholders(directory, options) {
|
|
332
338
|
await updateJsonFile(join(directory, "package.json"), (value) => ({
|
|
333
339
|
...value,
|
|
334
340
|
name: options.name,
|
|
341
|
+
dependencies: replaceGeneratedSiteWorkspaceDependencies(value.dependencies),
|
|
342
|
+
devDependencies: replaceGeneratedSiteWorkspaceDependencies(value.devDependencies),
|
|
335
343
|
}));
|
|
336
344
|
await replaceTextFile(join(directory, "src", "config", "site.ts"), [
|
|
337
345
|
['name: "slugkit"', `name: ${JSON.stringify(options.siteTitle)}`],
|
|
@@ -360,6 +368,16 @@ async function replaceGeneratedSitePlaceholders(directory, options) {
|
|
|
360
368
|
],
|
|
361
369
|
]);
|
|
362
370
|
}
|
|
371
|
+
function replaceGeneratedSiteWorkspaceDependencies(value) {
|
|
372
|
+
if (!isRecord(value))
|
|
373
|
+
return value;
|
|
374
|
+
return Object.fromEntries(Object.entries(value).map(([name, version]) => [
|
|
375
|
+
name,
|
|
376
|
+
version === "workspace:*" && GENERATED_SITE_SLUGKIT_DEPENDENCIES[name] !== undefined
|
|
377
|
+
? GENERATED_SITE_SLUGKIT_DEPENDENCIES[name]
|
|
378
|
+
: version,
|
|
379
|
+
]));
|
|
380
|
+
}
|
|
363
381
|
async function updateJsonFile(path, update) {
|
|
364
382
|
const value = JSON.parse(await readFile(path, "utf8"));
|
|
365
383
|
await writeFile(path, `${JSON.stringify(update(value), null, 2)}\n`, "utf8");
|
|
@@ -376,11 +394,24 @@ async function writeGeneratedSiteMarker(directory, options) {
|
|
|
376
394
|
slugkitSite: true,
|
|
377
395
|
name: options.name,
|
|
378
396
|
siteTitle: options.siteTitle,
|
|
379
|
-
template:
|
|
397
|
+
template: `${SLUGKIT_TEMPLATE_PACKAGE_NAME}/${DEFAULT_TEMPLATE_NAME}`,
|
|
380
398
|
generatedAt: new Date().toISOString(),
|
|
399
|
+
packageMetadata: createGeneratedSitePackageMetadata(DEFAULT_TEMPLATE_METADATA),
|
|
381
400
|
};
|
|
382
401
|
await writeFile(join(directory, ".slugkit-site.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
383
402
|
}
|
|
403
|
+
function createGeneratedSitePackageMetadata(metadata) {
|
|
404
|
+
return {
|
|
405
|
+
template: {
|
|
406
|
+
packageName: metadata.packageName,
|
|
407
|
+
name: metadata.name,
|
|
408
|
+
version: metadata.version,
|
|
409
|
+
},
|
|
410
|
+
packages: { ...metadata.includedSlugkitPackages },
|
|
411
|
+
advisoryOnly: true,
|
|
412
|
+
migrationNotesUrl: "https://slugkit.com/docs/package-updates",
|
|
413
|
+
};
|
|
414
|
+
}
|
|
384
415
|
async function resolveInitTemplateSource(context, options) {
|
|
385
416
|
if (options.templateDir !== undefined) {
|
|
386
417
|
const directory = resolve(options.templateDir);
|
|
@@ -390,20 +421,20 @@ async function resolveInitTemplateSource(context, options) {
|
|
|
390
421
|
if (options.templateUrl !== undefined) {
|
|
391
422
|
return downloadTemplateSite(context, options.templateUrl);
|
|
392
423
|
}
|
|
424
|
+
if (options.template !== undefined) {
|
|
425
|
+
return resolveNamedTemplateSource(options.template);
|
|
426
|
+
}
|
|
393
427
|
const localTemplateDirectory = findTemplateSiteDirectory(context.templateSiteDirectory);
|
|
394
428
|
if (localTemplateDirectory !== undefined) {
|
|
395
429
|
return { directory: localTemplateDirectory };
|
|
396
430
|
}
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
function getDefaultTemplateAssetUrl(context) {
|
|
400
|
-
const version = context.packageVersion.trim().replace(/^v/, "");
|
|
401
|
-
const baseUrl = context.templateAssetBaseUrl ?? SITE_TEMPLATE_RELEASE_BASE_URL;
|
|
402
|
-
const assetName = getTemplateAssetName(version);
|
|
403
|
-
return `${baseUrl.replace(/\/$/u, "")}/v${encodeURIComponent(version)}/${encodeURIComponent(assetName)}`;
|
|
431
|
+
return { directory: getTemplateRoot() };
|
|
404
432
|
}
|
|
405
|
-
function
|
|
406
|
-
|
|
433
|
+
function resolveNamedTemplateSource(template) {
|
|
434
|
+
if (template === DEFAULT_TEMPLATE_NAME || template === SLUGKIT_TEMPLATE_PACKAGE_NAME) {
|
|
435
|
+
return { directory: getTemplateRoot() };
|
|
436
|
+
}
|
|
437
|
+
throw createInvalidUsageError(`Unsupported template: ${template}. This CLI currently includes ${DEFAULT_TEMPLATE_NAME}; future releases may resolve external template packages.`);
|
|
407
438
|
}
|
|
408
439
|
async function downloadTemplateSite(context, templateUrl) {
|
|
409
440
|
let parsedUrl;
|
|
@@ -478,18 +509,20 @@ function isNodeErrorWithCode(error, code) {
|
|
|
478
509
|
}
|
|
479
510
|
async function runDoctorCommand(context, args) {
|
|
480
511
|
const json = readJsonFlag(args);
|
|
481
|
-
const
|
|
512
|
+
const siteResult = await readDoctorSiteConfig(context);
|
|
482
513
|
const checks = [];
|
|
483
|
-
if (
|
|
484
|
-
checks.push({ name: "config", status: "fail", message:
|
|
514
|
+
if (!siteResult.ok) {
|
|
515
|
+
checks.push({ name: "config", status: "fail", message: siteResult.message });
|
|
485
516
|
return finishDoctor(context, { ok: false, checks }, json, ExitCode.InvalidUsage);
|
|
486
517
|
}
|
|
518
|
+
const config = siteResult.site;
|
|
487
519
|
const apiBaseUrl = normalizeUrl(config.apiBaseUrl);
|
|
488
520
|
checks.push({ name: "config", status: "pass", message: `apiBaseUrl: ${apiBaseUrl}` });
|
|
489
521
|
const health = await requestDoctorJson(context, apiBaseUrl, "/health");
|
|
490
522
|
checks.push(createHealthCheck(health));
|
|
491
523
|
const meta = await requestDoctorJson(context, apiBaseUrl, "/meta");
|
|
492
524
|
checks.push(createMetaCheck(meta));
|
|
525
|
+
checks.push(createPackageMetadataCheck(meta));
|
|
493
526
|
const openapi = await requestDoctorJson(context, apiBaseUrl, "/openapi.json");
|
|
494
527
|
checks.push(createOpenApiCheck(openapi));
|
|
495
528
|
if (config.apiKey === undefined || config.apiKey.trim() === "") {
|
|
@@ -502,6 +535,17 @@ async function runDoctorCommand(context, args) {
|
|
|
502
535
|
const result = { ok: checks.every((check) => check.status !== "fail"), checks };
|
|
503
536
|
return finishDoctor(context, result, json, getDoctorExitCode(checks));
|
|
504
537
|
}
|
|
538
|
+
async function readDoctorSiteConfig(context) {
|
|
539
|
+
try {
|
|
540
|
+
return { ok: true, site: await readSelectedSiteConfig(context) };
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
if (error instanceof CliError) {
|
|
544
|
+
return { ok: false, message: error.message };
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
505
549
|
function finishDoctor(context, result, json, exitCode) {
|
|
506
550
|
if (json) {
|
|
507
551
|
writeJson(context.writer, result);
|
|
@@ -578,10 +622,10 @@ function createMetaCheck(result) {
|
|
|
578
622
|
if (!isMetaDocument(result.data)) {
|
|
579
623
|
return { name: "meta", status: "fail", message: "Metadata response is invalid" };
|
|
580
624
|
}
|
|
581
|
-
if (result.data.api.name !==
|
|
625
|
+
if (result.data.api.name !== SLUGKIT_API_NAME) {
|
|
582
626
|
return { name: "meta", status: "fail", message: "API name is not slugkit" };
|
|
583
627
|
}
|
|
584
|
-
if (
|
|
628
|
+
if (readSlugkitApiMajorVersion(result.data.api.version) !== SUPPORTED_API_MAJOR_VERSION) {
|
|
585
629
|
return {
|
|
586
630
|
name: "meta",
|
|
587
631
|
status: "fail",
|
|
@@ -590,6 +634,35 @@ function createMetaCheck(result) {
|
|
|
590
634
|
}
|
|
591
635
|
return { name: "meta", status: "pass", message: `API version ${result.data.api.version}` };
|
|
592
636
|
}
|
|
637
|
+
function createPackageMetadataCheck(result) {
|
|
638
|
+
if (!result.ok) {
|
|
639
|
+
return { name: "packages", status: "warn", message: "Package metadata is unavailable" };
|
|
640
|
+
}
|
|
641
|
+
if (!isMetaDocument(result.data) || result.data.packageMetadata === undefined) {
|
|
642
|
+
return {
|
|
643
|
+
name: "packages",
|
|
644
|
+
status: "warn",
|
|
645
|
+
message: "Package metadata is not exposed by this site",
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const metadata = result.data.packageMetadata;
|
|
649
|
+
const stalePackages = findStaleSlugkitPackages(metadata.packages);
|
|
650
|
+
if (stalePackages.length > 0) {
|
|
651
|
+
return {
|
|
652
|
+
name: "packages",
|
|
653
|
+
status: "warn",
|
|
654
|
+
message: `Updates available: ${stalePackages
|
|
655
|
+
.map((entry) => `${entry.packageName} ${entry.currentVersion} → ${entry.recommendedVersion}`)
|
|
656
|
+
.join(", ")}`,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
const packageCount = Object.keys(metadata.packages).length;
|
|
660
|
+
return {
|
|
661
|
+
name: "packages",
|
|
662
|
+
status: "pass",
|
|
663
|
+
message: `${packageCount} Slugkit package versions reported; upgrades are advisory`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
593
666
|
function createOpenApiCheck(result) {
|
|
594
667
|
if (!result.ok) {
|
|
595
668
|
return { name: "openapi", status: "fail", message: result.message };
|
|
@@ -628,13 +701,108 @@ function getDoctorExitCode(checks) {
|
|
|
628
701
|
}
|
|
629
702
|
return ExitCode.ApiError;
|
|
630
703
|
}
|
|
704
|
+
async function runUpgradeCommand(context, args) {
|
|
705
|
+
const json = readJsonFlag(args);
|
|
706
|
+
const siteResult = await readDoctorSiteConfig(context);
|
|
707
|
+
if (!siteResult.ok) {
|
|
708
|
+
const result = {
|
|
709
|
+
ok: false,
|
|
710
|
+
advisoryOnly: true,
|
|
711
|
+
message: siteResult.message,
|
|
712
|
+
recommendations: [],
|
|
713
|
+
};
|
|
714
|
+
return finishUpgrade(context, result, json, ExitCode.InvalidUsage);
|
|
715
|
+
}
|
|
716
|
+
const apiBaseUrl = normalizeUrl(siteResult.site.apiBaseUrl);
|
|
717
|
+
const meta = await requestDoctorJson(context, apiBaseUrl, "/meta");
|
|
718
|
+
if (!meta.ok) {
|
|
719
|
+
const result = {
|
|
720
|
+
ok: false,
|
|
721
|
+
advisoryOnly: true,
|
|
722
|
+
message: meta.message,
|
|
723
|
+
recommendations: [],
|
|
724
|
+
};
|
|
725
|
+
return finishUpgrade(context, result, json, getDoctorExitCode([{ name: "meta", status: "fail", message: meta.message }]));
|
|
726
|
+
}
|
|
727
|
+
if (!isMetaDocument(meta.data) || meta.data.packageMetadata === undefined) {
|
|
728
|
+
const result = {
|
|
729
|
+
ok: true,
|
|
730
|
+
advisoryOnly: true,
|
|
731
|
+
message: "Package metadata is unavailable; no files were changed.",
|
|
732
|
+
recommendations: [],
|
|
733
|
+
};
|
|
734
|
+
return finishUpgrade(context, result, json, ExitCode.Ok);
|
|
735
|
+
}
|
|
736
|
+
const metadata = meta.data.packageMetadata;
|
|
737
|
+
const recommendations = findStaleSlugkitPackages(metadata.packages);
|
|
738
|
+
const result = {
|
|
739
|
+
ok: true,
|
|
740
|
+
advisoryOnly: true,
|
|
741
|
+
message: recommendations.length === 0
|
|
742
|
+
? "No Slugkit package updates are currently recommended. No files were changed."
|
|
743
|
+
: "Slugkit package updates are available. Review and apply them manually; no files were changed.",
|
|
744
|
+
recommendations,
|
|
745
|
+
migrationNotesUrl: metadata.migrationNotesUrl,
|
|
746
|
+
};
|
|
747
|
+
if (metadata.template !== undefined) {
|
|
748
|
+
result.template = `${metadata.template.packageName}/${metadata.template.name}@${metadata.template.version}`;
|
|
749
|
+
}
|
|
750
|
+
return finishUpgrade(context, result, json, ExitCode.Ok);
|
|
751
|
+
}
|
|
752
|
+
function finishUpgrade(context, result, json, exitCode) {
|
|
753
|
+
if (json) {
|
|
754
|
+
writeJson(context.writer, result);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
context.writer.stdout("Slug upgrade: advisory only");
|
|
758
|
+
context.writer.stdout(result.message);
|
|
759
|
+
if (result.template !== undefined) {
|
|
760
|
+
context.writer.stdout(`Template: ${result.template}`);
|
|
761
|
+
}
|
|
762
|
+
if (result.recommendations.length > 0) {
|
|
763
|
+
context.writer.stdout("Recommended package updates:");
|
|
764
|
+
for (const recommendation of result.recommendations) {
|
|
765
|
+
context.writer.stdout(`- ${recommendation.packageName}: ${recommendation.currentVersion} -> ${recommendation.recommendedVersion}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (result.migrationNotesUrl !== undefined) {
|
|
769
|
+
context.writer.stdout(`Migration notes: ${result.migrationNotesUrl}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return { exitCode };
|
|
773
|
+
}
|
|
631
774
|
function isMetaDocument(value) {
|
|
632
775
|
return (isRecord(value) &&
|
|
633
776
|
isRecord(value.api) &&
|
|
634
777
|
typeof value.api.name === "string" &&
|
|
635
778
|
typeof value.api.version === "string" &&
|
|
636
779
|
typeof value.api.baseUrl === "string" &&
|
|
637
|
-
typeof value.api.openapiUrl === "string"
|
|
780
|
+
typeof value.api.openapiUrl === "string" &&
|
|
781
|
+
(value.packageMetadata === undefined || isPackageMetadata(value.packageMetadata)));
|
|
782
|
+
}
|
|
783
|
+
function isPackageMetadata(value) {
|
|
784
|
+
return (isRecord(value) &&
|
|
785
|
+
isRecord(value.packages) &&
|
|
786
|
+
value.advisoryOnly === true &&
|
|
787
|
+
Object.values(value.packages).every((version) => typeof version === "string") &&
|
|
788
|
+
(value.template === undefined ||
|
|
789
|
+
(isRecord(value.template) &&
|
|
790
|
+
typeof value.template.packageName === "string" &&
|
|
791
|
+
typeof value.template.name === "string" &&
|
|
792
|
+
typeof value.template.version === "string")) &&
|
|
793
|
+
(value.migrationNotesUrl === undefined || typeof value.migrationNotesUrl === "string"));
|
|
794
|
+
}
|
|
795
|
+
function findStaleSlugkitPackages(packages) {
|
|
796
|
+
return Object.entries(GENERATED_SITE_SLUGKIT_DEPENDENCIES)
|
|
797
|
+
.map(([packageName, recommendedVersion]) => {
|
|
798
|
+
const currentVersion = packages[packageName];
|
|
799
|
+
if (currentVersion === undefined)
|
|
800
|
+
return undefined;
|
|
801
|
+
if (compareSlugkitApiVersions(currentVersion, recommendedVersion) >= 0)
|
|
802
|
+
return undefined;
|
|
803
|
+
return { packageName, currentVersion, recommendedVersion };
|
|
804
|
+
})
|
|
805
|
+
.filter((entry) => entry !== undefined);
|
|
638
806
|
}
|
|
639
807
|
function isOpenApiDocument(value) {
|
|
640
808
|
return (isRecord(value) &&
|
|
@@ -646,10 +814,6 @@ function isOpenApiDocument(value) {
|
|
|
646
814
|
function isRecord(value) {
|
|
647
815
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
648
816
|
}
|
|
649
|
-
function readMajorVersion(value) {
|
|
650
|
-
const major = Number.parseInt(value.split(".")[0] ?? "", 10);
|
|
651
|
-
return Number.isInteger(major) ? major : undefined;
|
|
652
|
-
}
|
|
653
817
|
function readApiErrorMessage(value) {
|
|
654
818
|
if (!isRecord(value) || !isRecord(value.error) || typeof value.error.message !== "string") {
|
|
655
819
|
return undefined;
|
|
@@ -658,10 +822,14 @@ function readApiErrorMessage(value) {
|
|
|
658
822
|
}
|
|
659
823
|
async function runLoginCommand(context, args) {
|
|
660
824
|
if (args.length > 1) {
|
|
661
|
-
throw createInvalidUsageError("Usage: slug login [api-base-url]");
|
|
825
|
+
throw createInvalidUsageError("Usage: slug --site <name> login [api-base-url]");
|
|
826
|
+
}
|
|
827
|
+
if (context.siteName === undefined || context.siteName.trim() === "") {
|
|
828
|
+
throw createInvalidUsageError("--site <name> is required for login");
|
|
662
829
|
}
|
|
663
830
|
const config = await readConfig(context.configPath);
|
|
664
|
-
const
|
|
831
|
+
const configuredSite = config.sites[context.siteName];
|
|
832
|
+
const apiBaseUrl = normalizeUrl(args[0] ?? configuredSite?.apiBaseUrl ?? (await promptForValue(context, "API base URL: ")));
|
|
665
833
|
const authUrl = createBrowserAuthUrl(apiBaseUrl);
|
|
666
834
|
context.writer.stdout("Open this URL to log in and create an API key:");
|
|
667
835
|
context.writer.stdout(authUrl);
|
|
@@ -685,8 +853,9 @@ async function runLoginCommand(context, args) {
|
|
|
685
853
|
fetchImpl: context.fetchImpl,
|
|
686
854
|
});
|
|
687
855
|
await client.requestJson({ path: "/auth/check" });
|
|
688
|
-
|
|
689
|
-
context.
|
|
856
|
+
const configWithSite = setConfigSiteApiBaseUrl(config, context.siteName, apiBaseUrl);
|
|
857
|
+
await writeConfig(context.configPath, setConfigSiteApiKey(configWithSite, context.siteName, apiKey));
|
|
858
|
+
context.writer.stdout(`Login successful. API key saved for site ${context.siteName}.`);
|
|
690
859
|
return { exitCode: ExitCode.Ok };
|
|
691
860
|
}
|
|
692
861
|
async function runPostsCommand(context, args) {
|
|
@@ -2086,31 +2255,44 @@ async function createConfiguredClient(context) {
|
|
|
2086
2255
|
return (await createConfiguredApiContext(context)).client;
|
|
2087
2256
|
}
|
|
2088
2257
|
async function createConfiguredApiContext(context) {
|
|
2089
|
-
const
|
|
2090
|
-
if (
|
|
2091
|
-
throw createInvalidUsageError(
|
|
2092
|
-
}
|
|
2093
|
-
if (config.apiKey === undefined || config.apiKey.trim() === "") {
|
|
2094
|
-
throw createInvalidUsageError("apiKey is not configured. Run slug login first.");
|
|
2258
|
+
const site = await readSelectedSiteConfig(context);
|
|
2259
|
+
if (site.apiKey === undefined || site.apiKey.trim() === "") {
|
|
2260
|
+
throw createInvalidUsageError(`apiKey is not configured for site ${context.siteName ?? ""}. Run slug --site <name> login first.`);
|
|
2095
2261
|
}
|
|
2096
|
-
const apiBaseUrl = normalizeUrl(
|
|
2262
|
+
const apiBaseUrl = normalizeUrl(site.apiBaseUrl);
|
|
2097
2263
|
return {
|
|
2098
2264
|
apiBaseUrl,
|
|
2099
2265
|
client: new SlugHttpClient({
|
|
2100
2266
|
apiBaseUrl,
|
|
2101
|
-
apiKey:
|
|
2267
|
+
apiKey: site.apiKey,
|
|
2102
2268
|
fetchImpl: context.fetchImpl,
|
|
2103
2269
|
}),
|
|
2104
2270
|
};
|
|
2105
2271
|
}
|
|
2272
|
+
async function readSelectedSiteConfig(context) {
|
|
2273
|
+
if (context.siteName === undefined || context.siteName.trim() === "") {
|
|
2274
|
+
throw createInvalidUsageError("--site <name> is required for this command");
|
|
2275
|
+
}
|
|
2276
|
+
const config = await readConfig(context.configPath);
|
|
2277
|
+
const site = config.sites[context.siteName];
|
|
2278
|
+
if (site === undefined) {
|
|
2279
|
+
const names = Object.keys(config.sites).sort();
|
|
2280
|
+
const suffix = names.length === 0 ? "No sites are configured." : `Configured sites: ${names.join(", ")}`;
|
|
2281
|
+
throw createInvalidUsageError(`Unknown site: ${context.siteName}. ${suffix}`);
|
|
2282
|
+
}
|
|
2283
|
+
if (site.apiBaseUrl.trim() === "") {
|
|
2284
|
+
throw createInvalidUsageError(`apiBaseUrl is not configured for site ${context.siteName}`);
|
|
2285
|
+
}
|
|
2286
|
+
return site;
|
|
2287
|
+
}
|
|
2106
2288
|
async function runConfigCommand(context, args) {
|
|
2107
2289
|
switch (args[0]) {
|
|
2108
2290
|
case "show":
|
|
2109
2291
|
return runConfigShowCommand(context, args.slice(1));
|
|
2110
|
-
case "
|
|
2111
|
-
return
|
|
2292
|
+
case "site":
|
|
2293
|
+
return runConfigSiteCommand(context, args.slice(1));
|
|
2112
2294
|
default:
|
|
2113
|
-
throw createInvalidUsageError("Usage: slug config <show|
|
|
2295
|
+
throw createInvalidUsageError("Usage: slug config <show|site>");
|
|
2114
2296
|
}
|
|
2115
2297
|
}
|
|
2116
2298
|
async function runConfigShowCommand(context, args) {
|
|
@@ -2120,33 +2302,158 @@ async function runConfigShowCommand(context, args) {
|
|
|
2120
2302
|
if (json) {
|
|
2121
2303
|
writeJson(context.writer, displayConfig);
|
|
2122
2304
|
}
|
|
2305
|
+
else if (displayConfig.sites.length === 0) {
|
|
2306
|
+
context.writer.stdout("No sites configured.");
|
|
2307
|
+
}
|
|
2123
2308
|
else {
|
|
2124
|
-
|
|
2125
|
-
|
|
2309
|
+
for (const site of displayConfig.sites) {
|
|
2310
|
+
context.writer.stdout(`${site.name}: ${site.apiBaseUrl} (apiKeyConfigured: ${site.apiKeyConfigured ? "true" : "false"})`);
|
|
2311
|
+
}
|
|
2126
2312
|
}
|
|
2127
2313
|
return { exitCode: ExitCode.Ok };
|
|
2128
2314
|
}
|
|
2129
|
-
async function
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2315
|
+
async function runConfigSiteCommand(context, args) {
|
|
2316
|
+
switch (args[0]) {
|
|
2317
|
+
case "add":
|
|
2318
|
+
return runConfigSiteAddCommand(context, args.slice(1));
|
|
2319
|
+
case "set-api-base-url":
|
|
2320
|
+
return runConfigSiteSetApiBaseUrlCommand(context, args.slice(1));
|
|
2321
|
+
case "set-api-key":
|
|
2322
|
+
return runConfigSiteSetApiKeyCommand(context, args.slice(1));
|
|
2323
|
+
case "list":
|
|
2324
|
+
return runConfigSiteListCommand(context, args.slice(1));
|
|
2325
|
+
case "show":
|
|
2326
|
+
return runConfigSiteShowCommand(context, args.slice(1));
|
|
2327
|
+
case "remove":
|
|
2328
|
+
return runConfigSiteRemoveCommand(context, args.slice(1));
|
|
2329
|
+
default:
|
|
2330
|
+
throw createInvalidUsageError("Usage: slug config site <add|set-api-base-url|set-api-key|list|show|remove>");
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
async function runConfigSiteAddCommand(context, args) {
|
|
2334
|
+
const name = args[0];
|
|
2335
|
+
if (name === undefined) {
|
|
2336
|
+
throw createInvalidUsageError("Usage: slug config site add <name> --api-base-url <url> [--api-key <key>]");
|
|
2134
2337
|
}
|
|
2338
|
+
validateSiteName(name);
|
|
2339
|
+
const parsed = parseConfigSiteAddArgs(args.slice(1));
|
|
2135
2340
|
const config = await readConfig(context.configPath);
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2341
|
+
let nextConfig = setConfigSiteApiBaseUrl(config, name, parsed.apiBaseUrl);
|
|
2342
|
+
if (parsed.apiKey !== undefined) {
|
|
2343
|
+
nextConfig = setConfigSiteApiKey(nextConfig, name, parsed.apiKey);
|
|
2344
|
+
}
|
|
2345
|
+
await writeConfig(context.configPath, nextConfig);
|
|
2346
|
+
context.writer.stdout(`Configured site ${name}.`);
|
|
2347
|
+
return { exitCode: ExitCode.Ok };
|
|
2348
|
+
}
|
|
2349
|
+
async function runConfigSiteSetApiBaseUrlCommand(context, args) {
|
|
2350
|
+
const [name, apiBaseUrl] = args;
|
|
2351
|
+
if (name === undefined || apiBaseUrl === undefined || args.length !== 2) {
|
|
2352
|
+
throw createInvalidUsageError("Usage: slug config site set-api-base-url <name> <url>");
|
|
2353
|
+
}
|
|
2354
|
+
validateSiteName(name);
|
|
2355
|
+
validateUrl(apiBaseUrl);
|
|
2356
|
+
const config = await readConfig(context.configPath);
|
|
2357
|
+
await writeConfig(context.configPath, setConfigSiteApiBaseUrl(config, name, apiBaseUrl));
|
|
2358
|
+
context.writer.stdout(`Updated site ${name} api-base-url.`);
|
|
2359
|
+
return { exitCode: ExitCode.Ok };
|
|
2360
|
+
}
|
|
2361
|
+
async function runConfigSiteSetApiKeyCommand(context, args) {
|
|
2362
|
+
const [name, apiKey] = args;
|
|
2363
|
+
if (name === undefined || apiKey === undefined || args.length !== 2) {
|
|
2364
|
+
throw createInvalidUsageError("Usage: slug config site set-api-key <name> <key>");
|
|
2365
|
+
}
|
|
2366
|
+
validateSiteName(name);
|
|
2367
|
+
const config = await readConfig(context.configPath);
|
|
2368
|
+
if (config.sites[name] === undefined) {
|
|
2369
|
+
throw createInvalidUsageError(`Unknown site: ${name}`);
|
|
2370
|
+
}
|
|
2371
|
+
await writeConfig(context.configPath, setConfigSiteApiKey(config, name, apiKey));
|
|
2372
|
+
context.writer.stdout(`Updated site ${name} api-key.`);
|
|
2373
|
+
return { exitCode: ExitCode.Ok };
|
|
2374
|
+
}
|
|
2375
|
+
async function runConfigSiteListCommand(context, args) {
|
|
2376
|
+
const json = readJsonFlag(args);
|
|
2377
|
+
const config = await readConfig(context.configPath);
|
|
2378
|
+
const sites = toDisplayConfig(config).sites;
|
|
2379
|
+
if (json) {
|
|
2380
|
+
writeJson(context.writer, { sites });
|
|
2381
|
+
}
|
|
2382
|
+
else if (sites.length === 0) {
|
|
2383
|
+
context.writer.stdout("No sites configured.");
|
|
2384
|
+
}
|
|
2385
|
+
else {
|
|
2386
|
+
for (const site of sites) {
|
|
2387
|
+
context.writer.stdout(`${site.name}: ${site.apiBaseUrl} (apiKeyConfigured: ${site.apiKeyConfigured ? "true" : "false"})`);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return { exitCode: ExitCode.Ok };
|
|
2391
|
+
}
|
|
2392
|
+
async function runConfigSiteShowCommand(context, args) {
|
|
2393
|
+
const json = args.includes("--json");
|
|
2394
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
2395
|
+
const name = positional[0];
|
|
2396
|
+
if (name === undefined || positional.length !== 1) {
|
|
2397
|
+
throw createInvalidUsageError("Usage: slug config site show <name> [--json]");
|
|
2398
|
+
}
|
|
2399
|
+
const site = toDisplayConfig(await readConfig(context.configPath)).sites.find((candidate) => candidate.name === name);
|
|
2400
|
+
if (site === undefined) {
|
|
2401
|
+
throw createInvalidUsageError(`Unknown site: ${name}`);
|
|
2402
|
+
}
|
|
2403
|
+
if (json) {
|
|
2404
|
+
writeJson(context.writer, site);
|
|
2405
|
+
}
|
|
2406
|
+
else {
|
|
2407
|
+
context.writer.stdout(`name: ${site.name}`);
|
|
2408
|
+
context.writer.stdout(`apiBaseUrl: ${site.apiBaseUrl}`);
|
|
2409
|
+
context.writer.stdout(`apiKeyConfigured: ${site.apiKeyConfigured ? "true" : "false"}`);
|
|
2146
2410
|
}
|
|
2147
|
-
context.writer.stdout(`Updated ${key}.`);
|
|
2148
2411
|
return { exitCode: ExitCode.Ok };
|
|
2149
2412
|
}
|
|
2413
|
+
async function runConfigSiteRemoveCommand(context, args) {
|
|
2414
|
+
const name = args[0];
|
|
2415
|
+
if (name === undefined || args.length !== 1) {
|
|
2416
|
+
throw createInvalidUsageError("Usage: slug config site remove <name>");
|
|
2417
|
+
}
|
|
2418
|
+
const config = await readConfig(context.configPath);
|
|
2419
|
+
if (config.sites[name] === undefined) {
|
|
2420
|
+
throw createInvalidUsageError(`Unknown site: ${name}`);
|
|
2421
|
+
}
|
|
2422
|
+
await writeConfig(context.configPath, removeConfigSite(config, name));
|
|
2423
|
+
context.writer.stdout(`Removed site ${name}.`);
|
|
2424
|
+
return { exitCode: ExitCode.Ok };
|
|
2425
|
+
}
|
|
2426
|
+
function parseConfigSiteAddArgs(args) {
|
|
2427
|
+
let apiBaseUrl;
|
|
2428
|
+
let apiKey;
|
|
2429
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2430
|
+
const arg = args[index];
|
|
2431
|
+
const value = args[index + 1];
|
|
2432
|
+
if ((arg === "--api-base-url" || arg === "--api-key") &&
|
|
2433
|
+
value !== undefined &&
|
|
2434
|
+
!value.startsWith("--")) {
|
|
2435
|
+
if (arg === "--api-base-url") {
|
|
2436
|
+
apiBaseUrl = value;
|
|
2437
|
+
}
|
|
2438
|
+
else {
|
|
2439
|
+
apiKey = value;
|
|
2440
|
+
}
|
|
2441
|
+
index += 1;
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
throw createInvalidUsageError("Usage: slug config site add <name> --api-base-url <url> [--api-key <key>]");
|
|
2445
|
+
}
|
|
2446
|
+
if (apiBaseUrl === undefined) {
|
|
2447
|
+
throw createInvalidUsageError("Missing required option: --api-base-url");
|
|
2448
|
+
}
|
|
2449
|
+
validateUrl(apiBaseUrl);
|
|
2450
|
+
return { apiBaseUrl, apiKey };
|
|
2451
|
+
}
|
|
2452
|
+
function validateSiteName(name) {
|
|
2453
|
+
if (!isValidSiteName(name)) {
|
|
2454
|
+
throw createInvalidUsageError("Site names may contain only letters, numbers, dots, underscores, and hyphens");
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2150
2457
|
function readJsonFlag(args) {
|
|
2151
2458
|
if (args.length === 0) {
|
|
2152
2459
|
return false;
|
|
@@ -2176,17 +2483,13 @@ export function createBrowserAuthUrl(apiBaseUrl) {
|
|
|
2176
2483
|
return url.toString();
|
|
2177
2484
|
}
|
|
2178
2485
|
function normalizeUrl(value) {
|
|
2179
|
-
validateUrl(value);
|
|
2180
|
-
return value.replace(/\/+$/, "");
|
|
2181
|
-
}
|
|
2182
|
-
function validateUrl(value) {
|
|
2183
2486
|
try {
|
|
2184
|
-
|
|
2185
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2186
|
-
throw new Error("Unsupported protocol");
|
|
2187
|
-
}
|
|
2487
|
+
return normalizeApiBaseUrl(value);
|
|
2188
2488
|
}
|
|
2189
2489
|
catch {
|
|
2190
2490
|
throw createInvalidUsageError("api-base-url must be a valid HTTP or HTTPS URL");
|
|
2191
2491
|
}
|
|
2192
2492
|
}
|
|
2493
|
+
function validateUrl(value) {
|
|
2494
|
+
normalizeUrl(value);
|
|
2495
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
apiBaseUrl
|
|
1
|
+
export interface SlugSiteConfig {
|
|
2
|
+
apiBaseUrl: string;
|
|
3
3
|
apiKey?: string;
|
|
4
4
|
apiKeyReference?: string;
|
|
5
5
|
}
|
|
6
|
-
export interface
|
|
7
|
-
|
|
6
|
+
export interface SlugConfig {
|
|
7
|
+
sites: Record<string, SlugSiteConfig>;
|
|
8
|
+
}
|
|
9
|
+
export interface DisplaySiteConfig {
|
|
10
|
+
name: string;
|
|
11
|
+
apiBaseUrl: string;
|
|
8
12
|
apiKeyConfigured: boolean;
|
|
9
13
|
apiKeyReference?: string;
|
|
10
14
|
}
|
|
15
|
+
export interface DisplayConfig {
|
|
16
|
+
sites: DisplaySiteConfig[];
|
|
17
|
+
}
|
|
11
18
|
export declare function createDefaultConfig(): SlugConfig;
|
|
12
19
|
export declare function getConfigPath(environment?: NodeJS.ProcessEnv): string;
|
|
13
20
|
export declare function readConfig(configPath: string): Promise<SlugConfig>;
|
|
14
21
|
export declare function writeConfig(configPath: string, config: SlugConfig): Promise<void>;
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function
|
|
22
|
+
export declare function setConfigSiteApiBaseUrl(config: SlugConfig, siteName: string, apiBaseUrl: string): SlugConfig;
|
|
23
|
+
export declare function setConfigSiteApiKey(config: SlugConfig, siteName: string, apiKey: string): SlugConfig;
|
|
24
|
+
export declare function removeConfigSite(config: SlugConfig, siteName: string): SlugConfig;
|
|
17
25
|
export declare function toDisplayConfig(config: SlugConfig): DisplayConfig;
|
|
26
|
+
export declare function isValidSiteName(name: string): boolean;
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
export function createDefaultConfig() {
|
|
5
|
-
return {};
|
|
5
|
+
return { sites: {} };
|
|
6
6
|
}
|
|
7
7
|
export function getConfigPath(environment = process.env) {
|
|
8
8
|
const configHome = environment.SLUG_CONFIG_HOME ?? environment.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
@@ -24,19 +24,46 @@ export async function writeConfig(configPath, config) {
|
|
|
24
24
|
await mkdir(dirname(configPath), { recursive: true });
|
|
25
25
|
await writeFile(configPath, serializeConfig(config), "utf8");
|
|
26
26
|
}
|
|
27
|
-
export function
|
|
28
|
-
|
|
27
|
+
export function setConfigSiteApiBaseUrl(config, siteName, apiBaseUrl) {
|
|
28
|
+
const site = config.sites[siteName] ?? { apiBaseUrl };
|
|
29
|
+
return {
|
|
30
|
+
...config,
|
|
31
|
+
sites: {
|
|
32
|
+
...config.sites,
|
|
33
|
+
[siteName]: { ...site, apiBaseUrl },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function setConfigSiteApiKey(config, siteName, apiKey) {
|
|
38
|
+
const site = config.sites[siteName];
|
|
39
|
+
if (site === undefined) {
|
|
40
|
+
throw new Error(`Site is not configured: ${siteName}`);
|
|
41
|
+
}
|
|
42
|
+
const nextSite = { ...site, apiKey };
|
|
43
|
+
delete nextSite.apiKeyReference;
|
|
44
|
+
return {
|
|
45
|
+
...config,
|
|
46
|
+
sites: {
|
|
47
|
+
...config.sites,
|
|
48
|
+
[siteName]: nextSite,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
29
51
|
}
|
|
30
|
-
export function
|
|
31
|
-
const
|
|
32
|
-
delete
|
|
33
|
-
return
|
|
52
|
+
export function removeConfigSite(config, siteName) {
|
|
53
|
+
const sites = { ...config.sites };
|
|
54
|
+
delete sites[siteName];
|
|
55
|
+
return { ...config, sites };
|
|
34
56
|
}
|
|
35
57
|
export function toDisplayConfig(config) {
|
|
36
58
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
sites: Object.entries(config.sites)
|
|
60
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
61
|
+
.map(([name, site]) => ({
|
|
62
|
+
name,
|
|
63
|
+
apiBaseUrl: site.apiBaseUrl,
|
|
64
|
+
apiKeyConfigured: site.apiKey !== undefined || site.apiKeyReference !== undefined,
|
|
65
|
+
apiKeyReference: site.apiKeyReference,
|
|
66
|
+
})),
|
|
40
67
|
};
|
|
41
68
|
}
|
|
42
69
|
function parseConfig(content) {
|
|
@@ -47,47 +74,75 @@ function parseConfig(content) {
|
|
|
47
74
|
if (trimmed.startsWith("{")) {
|
|
48
75
|
return normalizeParsedConfig(JSON.parse(trimmed));
|
|
49
76
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
const lines = content.split(/\r?\n/);
|
|
78
|
+
const sites = {};
|
|
79
|
+
let currentSite;
|
|
80
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
81
|
+
const uncommented = stripComment(rawLine);
|
|
82
|
+
const line = uncommented.trimEnd();
|
|
83
|
+
if (line.trim() === "" || line.trim() === "---") {
|
|
54
84
|
continue;
|
|
55
85
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
if (line === "sites:" || line === "sites: {}") {
|
|
87
|
+
currentSite = undefined;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const siteMatch = /^ {2}([A-Za-z0-9._-]+):\s*$/.exec(line);
|
|
91
|
+
if (siteMatch !== null) {
|
|
92
|
+
currentSite = siteMatch[1];
|
|
93
|
+
sites[currentSite] = sites[currentSite] ?? {};
|
|
94
|
+
continue;
|
|
59
95
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
96
|
+
const fieldMatch = /^ {4}(apiBaseUrl|apiKey|apiKeyReference):(?:\s*(.*))?$/.exec(line);
|
|
97
|
+
if (fieldMatch !== null && currentSite !== undefined) {
|
|
98
|
+
const [, key, rawValue = ""] = fieldMatch;
|
|
99
|
+
sites[currentSite][key] = parseScalar(rawValue.trim());
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`Invalid slug config YAML at line ${index + 1}`);
|
|
62
103
|
}
|
|
63
|
-
return normalizeParsedConfig(
|
|
104
|
+
return normalizeParsedConfig({ sites: sites });
|
|
64
105
|
}
|
|
65
106
|
function serializeConfig(config) {
|
|
66
|
-
const lines = [];
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
107
|
+
const lines = ["sites:"];
|
|
108
|
+
const entries = Object.entries(config.sites).sort(([left], [right]) => left.localeCompare(right));
|
|
109
|
+
for (const [name, site] of entries) {
|
|
110
|
+
lines.push(` ${name}:`);
|
|
111
|
+
lines.push(` apiBaseUrl: ${formatScalar(site.apiBaseUrl)}`);
|
|
112
|
+
if (site.apiKey !== undefined) {
|
|
113
|
+
lines.push(` apiKey: ${formatScalar(site.apiKey)}`);
|
|
114
|
+
}
|
|
115
|
+
if (site.apiKeyReference !== undefined) {
|
|
116
|
+
lines.push(` apiKeyReference: ${formatScalar(site.apiKeyReference)}`);
|
|
117
|
+
}
|
|
75
118
|
}
|
|
76
|
-
return
|
|
119
|
+
return entries.length === 0 ? "sites: {}\n" : `${lines.join("\n")}\n`;
|
|
77
120
|
}
|
|
78
121
|
function normalizeParsedConfig(config) {
|
|
79
|
-
const normalized =
|
|
80
|
-
if (typeof config.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
122
|
+
const normalized = createDefaultConfig();
|
|
123
|
+
if (config.sites !== undefined && typeof config.sites === "object" && config.sites !== null) {
|
|
124
|
+
for (const [name, site] of Object.entries(config.sites)) {
|
|
125
|
+
if (!isValidSiteName(name) || typeof site !== "object" || site === null) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const partial = site;
|
|
129
|
+
if (typeof partial.apiBaseUrl !== "string") {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
normalized.sites[name] = { apiBaseUrl: partial.apiBaseUrl };
|
|
133
|
+
if (typeof partial.apiKey === "string") {
|
|
134
|
+
normalized.sites[name].apiKey = partial.apiKey;
|
|
135
|
+
}
|
|
136
|
+
if (typeof partial.apiKeyReference === "string") {
|
|
137
|
+
normalized.sites[name].apiKeyReference = partial.apiKeyReference;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
88
140
|
}
|
|
89
141
|
return normalized;
|
|
90
142
|
}
|
|
143
|
+
export function isValidSiteName(name) {
|
|
144
|
+
return /^[A-Za-z0-9._-]+$/u.test(name);
|
|
145
|
+
}
|
|
91
146
|
function stripComment(line) {
|
|
92
147
|
let quote;
|
|
93
148
|
for (let index = 0; index < line.length; index += 1) {
|
package/dist/http.d.ts
CHANGED
|
@@ -8,13 +8,6 @@ export interface HttpRequestOptions {
|
|
|
8
8
|
path: string;
|
|
9
9
|
body?: unknown;
|
|
10
10
|
}
|
|
11
|
-
export interface ApiErrorBody {
|
|
12
|
-
error?: {
|
|
13
|
-
code?: string;
|
|
14
|
-
message?: string;
|
|
15
|
-
operation?: string;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
11
|
export declare class SlugHttpClient {
|
|
19
12
|
private readonly apiBaseUrl;
|
|
20
13
|
private readonly apiKey;
|
package/dist/http.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { normalizeApiBaseUrl } from "@evcraddock/slug-core";
|
|
1
2
|
import { CliError, ExitCode } from "./errors.js";
|
|
2
3
|
export class SlugHttpClient {
|
|
3
4
|
apiBaseUrl;
|
|
4
5
|
apiKey;
|
|
5
6
|
fetchImpl;
|
|
6
7
|
constructor(options) {
|
|
7
|
-
this.apiBaseUrl = options.apiBaseUrl
|
|
8
|
+
this.apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl);
|
|
8
9
|
this.apiKey = options.apiKey;
|
|
9
10
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
10
11
|
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
15
15
|
const result = await runCommand({
|
|
16
16
|
args: parsedArgs.commandArgs,
|
|
17
17
|
configPath: parsedArgs.configPath ?? getConfigPath(),
|
|
18
|
+
siteName: parsedArgs.siteName,
|
|
18
19
|
packageVersion: readPackageVersion(),
|
|
19
20
|
writer,
|
|
20
21
|
openUrl,
|
|
@@ -60,6 +61,7 @@ async function promptUser(message) {
|
|
|
60
61
|
function parseGlobalOptions(args) {
|
|
61
62
|
const commandArgs = [];
|
|
62
63
|
let configPath;
|
|
64
|
+
let siteName;
|
|
63
65
|
for (let index = 0; index < args.length; index += 1) {
|
|
64
66
|
const arg = args[index];
|
|
65
67
|
if (arg === "--config") {
|
|
@@ -79,9 +81,26 @@ function parseGlobalOptions(args) {
|
|
|
79
81
|
configPath = value;
|
|
80
82
|
continue;
|
|
81
83
|
}
|
|
84
|
+
if (arg === "--site") {
|
|
85
|
+
const value = args[index + 1];
|
|
86
|
+
if (value === undefined || value.startsWith("--")) {
|
|
87
|
+
throw createInvalidUsageError("Usage: slug --site <name> <command>");
|
|
88
|
+
}
|
|
89
|
+
siteName = value;
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (arg?.startsWith("--site=")) {
|
|
94
|
+
const value = arg.slice("--site=".length);
|
|
95
|
+
if (value === "") {
|
|
96
|
+
throw createInvalidUsageError("Usage: slug --site <name> <command>");
|
|
97
|
+
}
|
|
98
|
+
siteName = value;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
82
101
|
commandArgs.push(arg);
|
|
83
102
|
}
|
|
84
|
-
return { commandArgs, configPath };
|
|
103
|
+
return { commandArgs, configPath, siteName };
|
|
85
104
|
}
|
|
86
105
|
function getOpenCommand(url) {
|
|
87
106
|
if (process.platform === "darwin") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evcraddock/slug-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Command-line tool for Slugkit sites.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"test:watch": "vitest --config ../vitest.config.ts src"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@evcraddock/slug-core": "0.1.1",
|
|
30
|
+
"@evcraddock/slug-template": "0.1.1",
|
|
29
31
|
"tar": "^7.5.16"
|
|
30
32
|
}
|
|
31
33
|
}
|