@evcraddock/slug-cli 0.4.0 → 0.6.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 +24 -5
- package/dist/commands.d.ts +1 -0
- package/dist/commands.js +207 -47
- package/dist/config.d.ts +15 -6
- package/dist/config.js +94 -39
- package/dist/index.js +20 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,18 +29,37 @@ Run `slug init --help` for template options. In repository-local development, `s
|
|
|
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`.
|
|
37
51
|
|
|
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.
|
|
52
|
+
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
53
|
|
|
40
54
|
Use `slug tag list` to list tags and post usage counts through the Slugkit tags API.
|
|
41
55
|
|
|
42
56
|
Example config:
|
|
43
57
|
|
|
44
58
|
```yaml
|
|
45
|
-
|
|
59
|
+
sites:
|
|
60
|
+
slugkit:
|
|
61
|
+
apiBaseUrl: https://slugkit.com/api/v1
|
|
62
|
+
apiKey: slug_...
|
|
63
|
+
testapp:
|
|
64
|
+
apiBaseUrl: http://localhost:3000/api/v1
|
|
46
65
|
```
|
package/dist/commands.d.ts
CHANGED
package/dist/commands.js
CHANGED
|
@@ -4,20 +4,20 @@ 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 { readConfig,
|
|
7
|
+
import { isValidSiteName, readConfig, removeConfigSite, setConfigSiteApiBaseUrl, setConfigSiteApiKey, toDisplayConfig, writeConfig, } from "./config.js";
|
|
8
8
|
import { CliError, createInvalidUsageError, ExitCode } from "./errors.js";
|
|
9
9
|
import { SlugHttpClient } from "./http.js";
|
|
10
10
|
import { writeJson } from "./output.js";
|
|
11
11
|
const HELP_TEXT = `slug - manage Slugkit sites
|
|
12
12
|
|
|
13
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
|
|
14
|
+
slug [--config <file>] [--site <name>] --help
|
|
15
|
+
slug [--config <file>] [--site <name>] --version
|
|
16
|
+
slug [--config <file>] [--site <name>] version [--json]
|
|
17
|
+
slug [--config <file>] --site <name> doctor [--json]
|
|
18
|
+
slug [--config <file>] --site <name> login [api-base-url]
|
|
19
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]
|
|
20
|
+
slug [--config <file>] --site <name> post list [--type article|link|note] [--status draft|published|all] [--tag <slug>] [--json]
|
|
21
21
|
slug [--config <file>] post show <slug> [--json]
|
|
22
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>] [--credit-contact-id <id>]... [--json]
|
|
23
23
|
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 +53,19 @@ Usage:
|
|
|
53
53
|
slug [--config <file>] contact create --name <name> [--url <url>] [--json]
|
|
54
54
|
slug [--config <file>] contact edit <id> [--name <name>] [--url <url>] [--json]
|
|
55
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]
|
|
56
|
+
slug [--config <file>] --site <name> site config show [--json]
|
|
57
|
+
slug [--config <file>] --site <name> site config set <field> <value> [--json]
|
|
58
58
|
slug [--config <file>] config show [--json]
|
|
59
|
-
slug [--config <file>] config
|
|
60
|
-
slug [--config <file>] config set
|
|
59
|
+
slug [--config <file>] config site add <name> --api-base-url <url> [--api-key <key>]
|
|
60
|
+
slug [--config <file>] config site set-api-base-url <name> <url>
|
|
61
|
+
slug [--config <file>] config site set-api-key <name> <key>
|
|
62
|
+
slug [--config <file>] config site list [--json]
|
|
63
|
+
slug [--config <file>] config site show <name> [--json]
|
|
64
|
+
slug [--config <file>] config site remove <name>
|
|
61
65
|
|
|
62
66
|
Options:
|
|
63
67
|
--config <file> Use a specific YAML config file.
|
|
68
|
+
--site <name> Target a named configured Slug API website.
|
|
64
69
|
--help Show this help.
|
|
65
70
|
--version Show the CLI version.
|
|
66
71
|
--json Print command output as JSON when supported.`;
|
|
@@ -478,12 +483,13 @@ function isNodeErrorWithCode(error, code) {
|
|
|
478
483
|
}
|
|
479
484
|
async function runDoctorCommand(context, args) {
|
|
480
485
|
const json = readJsonFlag(args);
|
|
481
|
-
const
|
|
486
|
+
const siteResult = await readDoctorSiteConfig(context);
|
|
482
487
|
const checks = [];
|
|
483
|
-
if (
|
|
484
|
-
checks.push({ name: "config", status: "fail", message:
|
|
488
|
+
if (!siteResult.ok) {
|
|
489
|
+
checks.push({ name: "config", status: "fail", message: siteResult.message });
|
|
485
490
|
return finishDoctor(context, { ok: false, checks }, json, ExitCode.InvalidUsage);
|
|
486
491
|
}
|
|
492
|
+
const config = siteResult.site;
|
|
487
493
|
const apiBaseUrl = normalizeUrl(config.apiBaseUrl);
|
|
488
494
|
checks.push({ name: "config", status: "pass", message: `apiBaseUrl: ${apiBaseUrl}` });
|
|
489
495
|
const health = await requestDoctorJson(context, apiBaseUrl, "/health");
|
|
@@ -502,6 +508,17 @@ async function runDoctorCommand(context, args) {
|
|
|
502
508
|
const result = { ok: checks.every((check) => check.status !== "fail"), checks };
|
|
503
509
|
return finishDoctor(context, result, json, getDoctorExitCode(checks));
|
|
504
510
|
}
|
|
511
|
+
async function readDoctorSiteConfig(context) {
|
|
512
|
+
try {
|
|
513
|
+
return { ok: true, site: await readSelectedSiteConfig(context) };
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
if (error instanceof CliError) {
|
|
517
|
+
return { ok: false, message: error.message };
|
|
518
|
+
}
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
505
522
|
function finishDoctor(context, result, json, exitCode) {
|
|
506
523
|
if (json) {
|
|
507
524
|
writeJson(context.writer, result);
|
|
@@ -658,10 +675,14 @@ function readApiErrorMessage(value) {
|
|
|
658
675
|
}
|
|
659
676
|
async function runLoginCommand(context, args) {
|
|
660
677
|
if (args.length > 1) {
|
|
661
|
-
throw createInvalidUsageError("Usage: slug login [api-base-url]");
|
|
678
|
+
throw createInvalidUsageError("Usage: slug --site <name> login [api-base-url]");
|
|
679
|
+
}
|
|
680
|
+
if (context.siteName === undefined || context.siteName.trim() === "") {
|
|
681
|
+
throw createInvalidUsageError("--site <name> is required for login");
|
|
662
682
|
}
|
|
663
683
|
const config = await readConfig(context.configPath);
|
|
664
|
-
const
|
|
684
|
+
const configuredSite = config.sites[context.siteName];
|
|
685
|
+
const apiBaseUrl = normalizeUrl(args[0] ?? configuredSite?.apiBaseUrl ?? (await promptForValue(context, "API base URL: ")));
|
|
665
686
|
const authUrl = createBrowserAuthUrl(apiBaseUrl);
|
|
666
687
|
context.writer.stdout("Open this URL to log in and create an API key:");
|
|
667
688
|
context.writer.stdout(authUrl);
|
|
@@ -685,8 +706,9 @@ async function runLoginCommand(context, args) {
|
|
|
685
706
|
fetchImpl: context.fetchImpl,
|
|
686
707
|
});
|
|
687
708
|
await client.requestJson({ path: "/auth/check" });
|
|
688
|
-
|
|
689
|
-
context.
|
|
709
|
+
const configWithSite = setConfigSiteApiBaseUrl(config, context.siteName, apiBaseUrl);
|
|
710
|
+
await writeConfig(context.configPath, setConfigSiteApiKey(configWithSite, context.siteName, apiKey));
|
|
711
|
+
context.writer.stdout(`Login successful. API key saved for site ${context.siteName}.`);
|
|
690
712
|
return { exitCode: ExitCode.Ok };
|
|
691
713
|
}
|
|
692
714
|
async function runPostsCommand(context, args) {
|
|
@@ -2086,31 +2108,44 @@ async function createConfiguredClient(context) {
|
|
|
2086
2108
|
return (await createConfiguredApiContext(context)).client;
|
|
2087
2109
|
}
|
|
2088
2110
|
async function createConfiguredApiContext(context) {
|
|
2089
|
-
const
|
|
2090
|
-
if (
|
|
2091
|
-
throw createInvalidUsageError(
|
|
2111
|
+
const site = await readSelectedSiteConfig(context);
|
|
2112
|
+
if (site.apiKey === undefined || site.apiKey.trim() === "") {
|
|
2113
|
+
throw createInvalidUsageError(`apiKey is not configured for site ${context.siteName ?? ""}. Run slug --site <name> login first.`);
|
|
2092
2114
|
}
|
|
2093
|
-
|
|
2094
|
-
throw createInvalidUsageError("apiKey is not configured. Run slug login first.");
|
|
2095
|
-
}
|
|
2096
|
-
const apiBaseUrl = normalizeUrl(config.apiBaseUrl);
|
|
2115
|
+
const apiBaseUrl = normalizeUrl(site.apiBaseUrl);
|
|
2097
2116
|
return {
|
|
2098
2117
|
apiBaseUrl,
|
|
2099
2118
|
client: new SlugHttpClient({
|
|
2100
2119
|
apiBaseUrl,
|
|
2101
|
-
apiKey:
|
|
2120
|
+
apiKey: site.apiKey,
|
|
2102
2121
|
fetchImpl: context.fetchImpl,
|
|
2103
2122
|
}),
|
|
2104
2123
|
};
|
|
2105
2124
|
}
|
|
2125
|
+
async function readSelectedSiteConfig(context) {
|
|
2126
|
+
if (context.siteName === undefined || context.siteName.trim() === "") {
|
|
2127
|
+
throw createInvalidUsageError("--site <name> is required for this command");
|
|
2128
|
+
}
|
|
2129
|
+
const config = await readConfig(context.configPath);
|
|
2130
|
+
const site = config.sites[context.siteName];
|
|
2131
|
+
if (site === undefined) {
|
|
2132
|
+
const names = Object.keys(config.sites).sort();
|
|
2133
|
+
const suffix = names.length === 0 ? "No sites are configured." : `Configured sites: ${names.join(", ")}`;
|
|
2134
|
+
throw createInvalidUsageError(`Unknown site: ${context.siteName}. ${suffix}`);
|
|
2135
|
+
}
|
|
2136
|
+
if (site.apiBaseUrl.trim() === "") {
|
|
2137
|
+
throw createInvalidUsageError(`apiBaseUrl is not configured for site ${context.siteName}`);
|
|
2138
|
+
}
|
|
2139
|
+
return site;
|
|
2140
|
+
}
|
|
2106
2141
|
async function runConfigCommand(context, args) {
|
|
2107
2142
|
switch (args[0]) {
|
|
2108
2143
|
case "show":
|
|
2109
2144
|
return runConfigShowCommand(context, args.slice(1));
|
|
2110
|
-
case "
|
|
2111
|
-
return
|
|
2145
|
+
case "site":
|
|
2146
|
+
return runConfigSiteCommand(context, args.slice(1));
|
|
2112
2147
|
default:
|
|
2113
|
-
throw createInvalidUsageError("Usage: slug config <show|
|
|
2148
|
+
throw createInvalidUsageError("Usage: slug config <show|site>");
|
|
2114
2149
|
}
|
|
2115
2150
|
}
|
|
2116
2151
|
async function runConfigShowCommand(context, args) {
|
|
@@ -2120,33 +2155,158 @@ async function runConfigShowCommand(context, args) {
|
|
|
2120
2155
|
if (json) {
|
|
2121
2156
|
writeJson(context.writer, displayConfig);
|
|
2122
2157
|
}
|
|
2158
|
+
else if (displayConfig.sites.length === 0) {
|
|
2159
|
+
context.writer.stdout("No sites configured.");
|
|
2160
|
+
}
|
|
2123
2161
|
else {
|
|
2124
|
-
|
|
2125
|
-
|
|
2162
|
+
for (const site of displayConfig.sites) {
|
|
2163
|
+
context.writer.stdout(`${site.name}: ${site.apiBaseUrl} (apiKeyConfigured: ${site.apiKeyConfigured ? "true" : "false"})`);
|
|
2164
|
+
}
|
|
2126
2165
|
}
|
|
2127
2166
|
return { exitCode: ExitCode.Ok };
|
|
2128
2167
|
}
|
|
2129
|
-
async function
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2168
|
+
async function runConfigSiteCommand(context, args) {
|
|
2169
|
+
switch (args[0]) {
|
|
2170
|
+
case "add":
|
|
2171
|
+
return runConfigSiteAddCommand(context, args.slice(1));
|
|
2172
|
+
case "set-api-base-url":
|
|
2173
|
+
return runConfigSiteSetApiBaseUrlCommand(context, args.slice(1));
|
|
2174
|
+
case "set-api-key":
|
|
2175
|
+
return runConfigSiteSetApiKeyCommand(context, args.slice(1));
|
|
2176
|
+
case "list":
|
|
2177
|
+
return runConfigSiteListCommand(context, args.slice(1));
|
|
2178
|
+
case "show":
|
|
2179
|
+
return runConfigSiteShowCommand(context, args.slice(1));
|
|
2180
|
+
case "remove":
|
|
2181
|
+
return runConfigSiteRemoveCommand(context, args.slice(1));
|
|
2182
|
+
default:
|
|
2183
|
+
throw createInvalidUsageError("Usage: slug config site <add|set-api-base-url|set-api-key|list|show|remove>");
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
async function runConfigSiteAddCommand(context, args) {
|
|
2187
|
+
const name = args[0];
|
|
2188
|
+
if (name === undefined) {
|
|
2189
|
+
throw createInvalidUsageError("Usage: slug config site add <name> --api-base-url <url> [--api-key <key>]");
|
|
2134
2190
|
}
|
|
2191
|
+
validateSiteName(name);
|
|
2192
|
+
const parsed = parseConfigSiteAddArgs(args.slice(1));
|
|
2135
2193
|
const config = await readConfig(context.configPath);
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
await writeConfig(context.configPath, setConfigApiBaseUrl(config, value));
|
|
2140
|
-
break;
|
|
2141
|
-
case "api-key":
|
|
2142
|
-
await writeConfig(context.configPath, setConfigApiKey(config, value));
|
|
2143
|
-
break;
|
|
2144
|
-
default:
|
|
2145
|
-
throw createInvalidUsageError(`Unknown config key: ${key}`);
|
|
2194
|
+
let nextConfig = setConfigSiteApiBaseUrl(config, name, parsed.apiBaseUrl);
|
|
2195
|
+
if (parsed.apiKey !== undefined) {
|
|
2196
|
+
nextConfig = setConfigSiteApiKey(nextConfig, name, parsed.apiKey);
|
|
2146
2197
|
}
|
|
2147
|
-
context.
|
|
2198
|
+
await writeConfig(context.configPath, nextConfig);
|
|
2199
|
+
context.writer.stdout(`Configured site ${name}.`);
|
|
2148
2200
|
return { exitCode: ExitCode.Ok };
|
|
2149
2201
|
}
|
|
2202
|
+
async function runConfigSiteSetApiBaseUrlCommand(context, args) {
|
|
2203
|
+
const [name, apiBaseUrl] = args;
|
|
2204
|
+
if (name === undefined || apiBaseUrl === undefined || args.length !== 2) {
|
|
2205
|
+
throw createInvalidUsageError("Usage: slug config site set-api-base-url <name> <url>");
|
|
2206
|
+
}
|
|
2207
|
+
validateSiteName(name);
|
|
2208
|
+
validateUrl(apiBaseUrl);
|
|
2209
|
+
const config = await readConfig(context.configPath);
|
|
2210
|
+
await writeConfig(context.configPath, setConfigSiteApiBaseUrl(config, name, apiBaseUrl));
|
|
2211
|
+
context.writer.stdout(`Updated site ${name} api-base-url.`);
|
|
2212
|
+
return { exitCode: ExitCode.Ok };
|
|
2213
|
+
}
|
|
2214
|
+
async function runConfigSiteSetApiKeyCommand(context, args) {
|
|
2215
|
+
const [name, apiKey] = args;
|
|
2216
|
+
if (name === undefined || apiKey === undefined || args.length !== 2) {
|
|
2217
|
+
throw createInvalidUsageError("Usage: slug config site set-api-key <name> <key>");
|
|
2218
|
+
}
|
|
2219
|
+
validateSiteName(name);
|
|
2220
|
+
const config = await readConfig(context.configPath);
|
|
2221
|
+
if (config.sites[name] === undefined) {
|
|
2222
|
+
throw createInvalidUsageError(`Unknown site: ${name}`);
|
|
2223
|
+
}
|
|
2224
|
+
await writeConfig(context.configPath, setConfigSiteApiKey(config, name, apiKey));
|
|
2225
|
+
context.writer.stdout(`Updated site ${name} api-key.`);
|
|
2226
|
+
return { exitCode: ExitCode.Ok };
|
|
2227
|
+
}
|
|
2228
|
+
async function runConfigSiteListCommand(context, args) {
|
|
2229
|
+
const json = readJsonFlag(args);
|
|
2230
|
+
const config = await readConfig(context.configPath);
|
|
2231
|
+
const sites = toDisplayConfig(config).sites;
|
|
2232
|
+
if (json) {
|
|
2233
|
+
writeJson(context.writer, { sites });
|
|
2234
|
+
}
|
|
2235
|
+
else if (sites.length === 0) {
|
|
2236
|
+
context.writer.stdout("No sites configured.");
|
|
2237
|
+
}
|
|
2238
|
+
else {
|
|
2239
|
+
for (const site of sites) {
|
|
2240
|
+
context.writer.stdout(`${site.name}: ${site.apiBaseUrl} (apiKeyConfigured: ${site.apiKeyConfigured ? "true" : "false"})`);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
return { exitCode: ExitCode.Ok };
|
|
2244
|
+
}
|
|
2245
|
+
async function runConfigSiteShowCommand(context, args) {
|
|
2246
|
+
const json = args.includes("--json");
|
|
2247
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
2248
|
+
const name = positional[0];
|
|
2249
|
+
if (name === undefined || positional.length !== 1) {
|
|
2250
|
+
throw createInvalidUsageError("Usage: slug config site show <name> [--json]");
|
|
2251
|
+
}
|
|
2252
|
+
const site = toDisplayConfig(await readConfig(context.configPath)).sites.find((candidate) => candidate.name === name);
|
|
2253
|
+
if (site === undefined) {
|
|
2254
|
+
throw createInvalidUsageError(`Unknown site: ${name}`);
|
|
2255
|
+
}
|
|
2256
|
+
if (json) {
|
|
2257
|
+
writeJson(context.writer, site);
|
|
2258
|
+
}
|
|
2259
|
+
else {
|
|
2260
|
+
context.writer.stdout(`name: ${site.name}`);
|
|
2261
|
+
context.writer.stdout(`apiBaseUrl: ${site.apiBaseUrl}`);
|
|
2262
|
+
context.writer.stdout(`apiKeyConfigured: ${site.apiKeyConfigured ? "true" : "false"}`);
|
|
2263
|
+
}
|
|
2264
|
+
return { exitCode: ExitCode.Ok };
|
|
2265
|
+
}
|
|
2266
|
+
async function runConfigSiteRemoveCommand(context, args) {
|
|
2267
|
+
const name = args[0];
|
|
2268
|
+
if (name === undefined || args.length !== 1) {
|
|
2269
|
+
throw createInvalidUsageError("Usage: slug config site remove <name>");
|
|
2270
|
+
}
|
|
2271
|
+
const config = await readConfig(context.configPath);
|
|
2272
|
+
if (config.sites[name] === undefined) {
|
|
2273
|
+
throw createInvalidUsageError(`Unknown site: ${name}`);
|
|
2274
|
+
}
|
|
2275
|
+
await writeConfig(context.configPath, removeConfigSite(config, name));
|
|
2276
|
+
context.writer.stdout(`Removed site ${name}.`);
|
|
2277
|
+
return { exitCode: ExitCode.Ok };
|
|
2278
|
+
}
|
|
2279
|
+
function parseConfigSiteAddArgs(args) {
|
|
2280
|
+
let apiBaseUrl;
|
|
2281
|
+
let apiKey;
|
|
2282
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2283
|
+
const arg = args[index];
|
|
2284
|
+
const value = args[index + 1];
|
|
2285
|
+
if ((arg === "--api-base-url" || arg === "--api-key") &&
|
|
2286
|
+
value !== undefined &&
|
|
2287
|
+
!value.startsWith("--")) {
|
|
2288
|
+
if (arg === "--api-base-url") {
|
|
2289
|
+
apiBaseUrl = value;
|
|
2290
|
+
}
|
|
2291
|
+
else {
|
|
2292
|
+
apiKey = value;
|
|
2293
|
+
}
|
|
2294
|
+
index += 1;
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
2297
|
+
throw createInvalidUsageError("Usage: slug config site add <name> --api-base-url <url> [--api-key <key>]");
|
|
2298
|
+
}
|
|
2299
|
+
if (apiBaseUrl === undefined) {
|
|
2300
|
+
throw createInvalidUsageError("Missing required option: --api-base-url");
|
|
2301
|
+
}
|
|
2302
|
+
validateUrl(apiBaseUrl);
|
|
2303
|
+
return { apiBaseUrl, apiKey };
|
|
2304
|
+
}
|
|
2305
|
+
function validateSiteName(name) {
|
|
2306
|
+
if (!isValidSiteName(name)) {
|
|
2307
|
+
throw createInvalidUsageError("Site names may contain only letters, numbers, dots, underscores, and hyphens");
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2150
2310
|
function readJsonFlag(args) {
|
|
2151
2311
|
if (args.length === 0) {
|
|
2152
2312
|
return false;
|
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/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") {
|