@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 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
- Run `slug login` to open the site's browser auth flow, paste the generated API key, verify it, and save the config. Use `slug doctor` to check configured site reachability and compatibility. Use `slug --config ./dev.slug.yaml login` to run with a specific config file.
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
- 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` reports whether an API key is configured without printing the key.
34
+ Add a site and API key directly:
35
35
 
36
- Use `slug site config show` to read supported runtime site configuration through the Slugkit API. Use `slug site config set <field> <value>` to update supported fields such as `name`, `url`, `tagline`, `description`, `homepage.intro`, and `homepage.body`.
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
- apiBaseUrl: https://example.com/api/v1
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
  ```
@@ -3,6 +3,7 @@ import { type OutputWriter } from "./output.js";
3
3
  export interface CommandContext {
4
4
  args: string[];
5
5
  configPath: string;
6
+ siteName?: string;
6
7
  packageVersion: string;
7
8
  writer: OutputWriter;
8
9
  fetchImpl?: typeof fetch;
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, setConfigApiBaseUrl, setConfigApiKey, toDisplayConfig, writeConfig, } from "./config.js";
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 set api-base-url <url>
60
- slug [--config <file>] config set api-key <key>
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 config = await readConfig(context.configPath);
486
+ const siteResult = await readDoctorSiteConfig(context);
482
487
  const checks = [];
483
- if (config.apiBaseUrl === undefined || config.apiBaseUrl.trim() === "") {
484
- checks.push({ name: "config", status: "fail", message: "apiBaseUrl is not configured" });
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 apiBaseUrl = normalizeUrl(args[0] ?? config.apiBaseUrl ?? (await promptForValue(context, "API base URL: ")));
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
- await writeConfig(context.configPath, setConfigApiKey(setConfigApiBaseUrl(config, apiBaseUrl), apiKey));
689
- context.writer.stdout("Login successful. API key saved.");
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 config = await readConfig(context.configPath);
2090
- if (config.apiBaseUrl === undefined || config.apiBaseUrl.trim() === "") {
2091
- throw createInvalidUsageError("apiBaseUrl is not configured");
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
- if (config.apiKey === undefined || config.apiKey.trim() === "") {
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: config.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 "set":
2111
- return runConfigSetCommand(context, args.slice(1));
2145
+ case "site":
2146
+ return runConfigSiteCommand(context, args.slice(1));
2112
2147
  default:
2113
- throw createInvalidUsageError("Usage: slug config <show|set>");
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
- context.writer.stdout(`apiBaseUrl: ${displayConfig.apiBaseUrl ?? "not configured"}`);
2125
- context.writer.stdout(`apiKeyConfigured: ${displayConfig.apiKeyConfigured ? "true" : "false"}`);
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 runConfigSetCommand(context, args) {
2130
- const key = args[0];
2131
- const value = args[1];
2132
- if (key === undefined || value === undefined || args.length !== 2) {
2133
- throw createInvalidUsageError("Usage: slug config set <api-base-url|api-key> <value>");
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
- switch (key) {
2137
- case "api-base-url":
2138
- validateUrl(value);
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.writer.stdout(`Updated ${key}.`);
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 SlugConfig {
2
- apiBaseUrl?: string;
1
+ export interface SlugSiteConfig {
2
+ apiBaseUrl: string;
3
3
  apiKey?: string;
4
4
  apiKeyReference?: string;
5
5
  }
6
- export interface DisplayConfig {
7
- apiBaseUrl?: string;
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 setConfigApiBaseUrl(config: SlugConfig, apiBaseUrl: string): SlugConfig;
16
- export declare function setConfigApiKey(config: SlugConfig, apiKey: string): SlugConfig;
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 setConfigApiBaseUrl(config, apiBaseUrl) {
28
- return { ...config, apiBaseUrl };
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 setConfigApiKey(config, apiKey) {
31
- const nextConfig = { ...config, apiKey };
32
- delete nextConfig.apiKeyReference;
33
- return nextConfig;
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
- apiBaseUrl: config.apiBaseUrl,
38
- apiKeyConfigured: config.apiKey !== undefined || config.apiKeyReference !== undefined,
39
- apiKeyReference: config.apiKeyReference,
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 parsed = {};
51
- for (const [index, rawLine] of content.split(/\r?\n/).entries()) {
52
- const line = stripComment(rawLine).trim();
53
- if (line === "" || line === "---") {
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
- const match = /^(apiBaseUrl|apiKey|apiKeyReference):(?:\s*(.*))?$/.exec(line);
57
- if (match === null) {
58
- throw new Error(`Invalid slug config YAML at line ${index + 1}`);
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 [, key, rawValue = ""] = match;
61
- parsed[key] = parseScalar(rawValue.trim());
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(parsed);
104
+ return normalizeParsedConfig({ sites: sites });
64
105
  }
65
106
  function serializeConfig(config) {
66
- const lines = [];
67
- if (config.apiBaseUrl !== undefined) {
68
- lines.push(`apiBaseUrl: ${formatScalar(config.apiBaseUrl)}`);
69
- }
70
- if (config.apiKey !== undefined) {
71
- lines.push(`apiKey: ${formatScalar(config.apiKey)}`);
72
- }
73
- if (config.apiKeyReference !== undefined) {
74
- lines.push(`apiKeyReference: ${formatScalar(config.apiKeyReference)}`);
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 lines.length === 0 ? "{}\n" : `${lines.join("\n")}\n`;
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.apiBaseUrl === "string") {
81
- normalized.apiBaseUrl = config.apiBaseUrl;
82
- }
83
- if (typeof config.apiKey === "string") {
84
- normalized.apiKey = config.apiKey;
85
- }
86
- if (typeof config.apiKeyReference === "string") {
87
- normalized.apiKeyReference = config.apiKeyReference;
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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evcraddock/slug-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Command-line tool for Slugkit sites.",
5
5
  "private": false,
6
6
  "type": "module",