@evcraddock/slug-cli 0.6.2 → 0.6.4
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 +2 -2
- package/dist/commands.d.ts +0 -1
- package/dist/commands.js +116 -63
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ 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. By default, `slug init`
|
|
28
|
+
Run `slug init --help` for template options. By default, `slug init` downloads the default template zip artifact. `--template-dir` remains available for local template development, `--template-url` supports alternate remote zip artifacts, and `--template` reserves the selection path for future named templates.
|
|
29
29
|
|
|
30
30
|
## Login and configuration
|
|
31
31
|
|
|
@@ -51,7 +51,7 @@ Use `slug --site <name> site config show` to read supported runtime site configu
|
|
|
51
51
|
|
|
52
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.
|
|
53
53
|
|
|
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.
|
|
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. For migration imports, pass `--published-at <date-or-datetime>` to `post create` or `post publish` to preserve an original publish date without triggering follower delivery by default.
|
|
55
55
|
|
|
56
56
|
Use `slug tag list` to list tags and post usage counts through the Slugkit tags API.
|
|
57
57
|
|
package/dist/commands.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ export interface CommandContext {
|
|
|
9
9
|
fetchImpl?: typeof fetch;
|
|
10
10
|
openUrl?: (url: string) => Promise<void> | void;
|
|
11
11
|
prompt?: (message: string) => Promise<string> | string;
|
|
12
|
-
templateSiteDirectory?: string | null;
|
|
13
12
|
templateAssetBaseUrl?: string;
|
|
14
13
|
}
|
|
15
14
|
export interface CommandResult {
|
package/dist/commands.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
2
|
import { tmpdir } from "node:os";
|
|
4
3
|
import { dirname, basename, extname, join, resolve } from "node:path";
|
|
5
|
-
import
|
|
6
|
-
import { x as extractTarball } from "tar";
|
|
4
|
+
import AdmZip from "adm-zip";
|
|
7
5
|
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
6
|
import { isValidSiteName, readConfig, removeConfigSite, setConfigSiteApiBaseUrl, setConfigSiteApiKey, toDisplayConfig, writeConfig, } from "./config.js";
|
|
10
7
|
import { CliError, createInvalidUsageError, ExitCode } from "./errors.js";
|
|
11
8
|
import { SlugHttpClient } from "./http.js";
|
|
@@ -22,10 +19,10 @@ Usage:
|
|
|
22
19
|
slug [--config <file>] init <directory> --name <name> [--site-title <title>] [--template <name>] [--template-url <url>] [--template-dir <dir>] [--json]
|
|
23
20
|
slug [--config <file>] --site <name> post list [--type article|link|note] [--status draft|published|all] [--tag <slug>] [--json]
|
|
24
21
|
slug [--config <file>] post show <slug> [--json]
|
|
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]
|
|
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]
|
|
22
|
+
slug [--config <file>] post create --type article|link|note --slug <slug> --content <text> [--title <text>] [--url <url>] [--excerpt <text>] [--published-at <datetime>] [--tag <slug>]... [--source-id <id>] [--credit-contact-id <id>]... [--json]
|
|
23
|
+
slug [--config <file>] post edit <slug> [--slug <new-slug>] [--title <text>] [--content <text>] [--url <url>] [--excerpt <text>] [--published-at <datetime>] [--tag <slug>]... [--source-id <id>] [--credit-contact-id <id>]... [--json]
|
|
27
24
|
slug [--config <file>] post delete <slug> [--json]
|
|
28
|
-
slug [--config <file>] post publish <slug> [--json]
|
|
25
|
+
slug [--config <file>] post publish <slug> [--published-at <datetime>] [--json]
|
|
29
26
|
slug [--config <file>] post unpublish <slug> [--json]
|
|
30
27
|
slug [--config <file>] comment list [--post <slug>] [--status pending|approved|hidden|all] [--json]
|
|
31
28
|
slug [--config <file>] comment approve <id> [--json]
|
|
@@ -134,9 +131,16 @@ async function runVersionCommand(context, args) {
|
|
|
134
131
|
return { exitCode: ExitCode.Ok };
|
|
135
132
|
}
|
|
136
133
|
const SUPPORTED_API_MAJOR_VERSION = SLUGKIT_API_MAJOR_VERSION;
|
|
137
|
-
const SITE_TEMPLATE_PACKAGE_NAME =
|
|
138
|
-
const
|
|
139
|
-
const
|
|
134
|
+
const SITE_TEMPLATE_PACKAGE_NAME = "@slugkit/template-site";
|
|
135
|
+
const DEFAULT_TEMPLATE_NAME = "default";
|
|
136
|
+
const DEFAULT_TEMPLATE_URL = "https://forge.caradoc.com/erik/slugkit/releases/download/slugkit-template-latest/slugkit-site-template.zip";
|
|
137
|
+
const GENERATED_SITE_SLUGKIT_DEPENDENCIES = {
|
|
138
|
+
"@evcraddock/slug-api": "0.1.1",
|
|
139
|
+
"@evcraddock/slug-auth": "0.1.0",
|
|
140
|
+
"@evcraddock/slug-core": "0.1.1",
|
|
141
|
+
"@evcraddock/slug-federation": "0.1.0",
|
|
142
|
+
"@evcraddock/slug-media": "0.1.0",
|
|
143
|
+
};
|
|
140
144
|
const INIT_HELP_TEXT = `slug init - create a standalone Slugkit-compatible website
|
|
141
145
|
|
|
142
146
|
Usage:
|
|
@@ -146,12 +150,12 @@ Options:
|
|
|
146
150
|
--name <name> npm package name for the generated site.
|
|
147
151
|
--site-title <title> Human-readable site title. Defaults to a title derived from --name.
|
|
148
152
|
--template <name> Template name or package. Defaults to ${DEFAULT_TEMPLATE_NAME}.
|
|
149
|
-
--template-url <url> Download a template
|
|
153
|
+
--template-url <url> Download a template zip artifact from a custom URL.
|
|
150
154
|
--template-dir <dir> Copy a template from a local directory.
|
|
151
155
|
--json Print command output as JSON.
|
|
152
156
|
--help, -h Show this help.
|
|
153
157
|
|
|
154
|
-
By default, slug init
|
|
158
|
+
By default, slug init downloads the default template zip artifact. Set SLUGKIT_TEMPLATE_URL or pass --template-url to use a different zip artifact. Generated sites do not depend on the template artifact at runtime.`;
|
|
155
159
|
const SITE_INIT_NEXT_STEPS = [
|
|
156
160
|
"Install dependencies",
|
|
157
161
|
"Copy and edit .env",
|
|
@@ -331,8 +335,18 @@ async function copyTemplateSite(source, destination, relativeDirectory = "") {
|
|
|
331
335
|
}
|
|
332
336
|
}
|
|
333
337
|
}
|
|
334
|
-
function shouldExcludeTemplateEntry(relativePath,
|
|
335
|
-
|
|
338
|
+
function shouldExcludeTemplateEntry(relativePath, name) {
|
|
339
|
+
const parts = relativePath.split(/[\\/]+/u).filter(Boolean);
|
|
340
|
+
return (parts.includes("node_modules") ||
|
|
341
|
+
parts.includes("dist") ||
|
|
342
|
+
parts.includes("build") ||
|
|
343
|
+
parts.includes("data") ||
|
|
344
|
+
parts.includes("coverage") ||
|
|
345
|
+
relativePath === "src/__tests__/dev-environment.test.ts" ||
|
|
346
|
+
relativePath === "src/federation/__tests__/follow.test.ts" ||
|
|
347
|
+
relativePath === ".env" ||
|
|
348
|
+
name.endsWith(".log") ||
|
|
349
|
+
name.endsWith(".tsbuildinfo"));
|
|
336
350
|
}
|
|
337
351
|
async function replaceGeneratedSitePlaceholders(directory, options) {
|
|
338
352
|
await updateJsonFile(join(directory, "package.json"), (value) => ({
|
|
@@ -394,20 +408,20 @@ async function writeGeneratedSiteMarker(directory, options) {
|
|
|
394
408
|
slugkitSite: true,
|
|
395
409
|
name: options.name,
|
|
396
410
|
siteTitle: options.siteTitle,
|
|
397
|
-
template:
|
|
411
|
+
template: DEFAULT_TEMPLATE_NAME,
|
|
398
412
|
generatedAt: new Date().toISOString(),
|
|
399
|
-
packageMetadata: createGeneratedSitePackageMetadata(
|
|
413
|
+
packageMetadata: createGeneratedSitePackageMetadata(),
|
|
400
414
|
};
|
|
401
415
|
await writeFile(join(directory, ".slugkit-site.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
402
416
|
}
|
|
403
|
-
function createGeneratedSitePackageMetadata(
|
|
417
|
+
function createGeneratedSitePackageMetadata() {
|
|
404
418
|
return {
|
|
405
419
|
template: {
|
|
406
|
-
packageName:
|
|
407
|
-
name:
|
|
408
|
-
version:
|
|
420
|
+
packageName: "zip-artifact",
|
|
421
|
+
name: DEFAULT_TEMPLATE_NAME,
|
|
422
|
+
version: "downloaded",
|
|
409
423
|
},
|
|
410
|
-
packages: { ...
|
|
424
|
+
packages: { ...GENERATED_SITE_SLUGKIT_DEPENDENCIES },
|
|
411
425
|
advisoryOnly: true,
|
|
412
426
|
migrationNotesUrl: "https://slugkit.com/docs/package-updates",
|
|
413
427
|
};
|
|
@@ -421,20 +435,20 @@ async function resolveInitTemplateSource(context, options) {
|
|
|
421
435
|
if (options.templateUrl !== undefined) {
|
|
422
436
|
return downloadTemplateSite(context, options.templateUrl);
|
|
423
437
|
}
|
|
424
|
-
if (options.template !== undefined) {
|
|
425
|
-
|
|
438
|
+
if (options.template !== undefined && options.template !== DEFAULT_TEMPLATE_NAME) {
|
|
439
|
+
throw createInvalidUsageError(`Unsupported template: ${options.template}. Use --template-url for external template zip artifacts.`);
|
|
426
440
|
}
|
|
427
|
-
|
|
428
|
-
if (localTemplateDirectory !== undefined) {
|
|
429
|
-
return { directory: localTemplateDirectory };
|
|
430
|
-
}
|
|
431
|
-
return { directory: getTemplateRoot() };
|
|
441
|
+
return downloadTemplateSite(context, getDefaultTemplateUrl(context));
|
|
432
442
|
}
|
|
433
|
-
function
|
|
434
|
-
if (
|
|
435
|
-
return {
|
|
443
|
+
function getDefaultTemplateUrl(context) {
|
|
444
|
+
if (context.templateAssetBaseUrl !== undefined) {
|
|
445
|
+
return `${context.templateAssetBaseUrl.replace(/\/+$/u, "")}/slugkit-site-template.zip`;
|
|
446
|
+
}
|
|
447
|
+
const environmentUrl = process.env.SLUGKIT_TEMPLATE_URL?.trim();
|
|
448
|
+
if (environmentUrl !== undefined && environmentUrl !== "") {
|
|
449
|
+
return environmentUrl;
|
|
436
450
|
}
|
|
437
|
-
|
|
451
|
+
return DEFAULT_TEMPLATE_URL;
|
|
438
452
|
}
|
|
439
453
|
async function downloadTemplateSite(context, templateUrl) {
|
|
440
454
|
let parsedUrl;
|
|
@@ -453,17 +467,42 @@ async function downloadTemplateSite(context, templateUrl) {
|
|
|
453
467
|
throw new CliError(`Failed to download Slugkit template from ${parsedUrl.toString()}: HTTP ${response.status}`, ExitCode.NetworkError);
|
|
454
468
|
}
|
|
455
469
|
const tempDirectory = await mkdtemp(join(tmpdir(), "slug-template-"));
|
|
456
|
-
const archivePath = join(tempDirectory, "template.
|
|
470
|
+
const archivePath = join(tempDirectory, "template.zip");
|
|
457
471
|
const extractDirectory = join(tempDirectory, "template");
|
|
458
472
|
await mkdir(extractDirectory, { recursive: true });
|
|
459
473
|
await writeFile(archivePath, Buffer.from(await response.arrayBuffer()));
|
|
460
|
-
|
|
461
|
-
await
|
|
474
|
+
new AdmZip(archivePath).extractAllTo(extractDirectory, true);
|
|
475
|
+
const templateDirectory = await findExtractedTemplateDirectory(extractDirectory);
|
|
476
|
+
await assertTemplateSiteDirectory(templateDirectory);
|
|
462
477
|
return {
|
|
463
|
-
directory:
|
|
478
|
+
directory: templateDirectory,
|
|
464
479
|
cleanup: () => rm(tempDirectory, { force: true, recursive: true }),
|
|
465
480
|
};
|
|
466
481
|
}
|
|
482
|
+
async function findExtractedTemplateDirectory(extractDirectory) {
|
|
483
|
+
if (await isTemplateSiteDirectory(extractDirectory)) {
|
|
484
|
+
return extractDirectory;
|
|
485
|
+
}
|
|
486
|
+
const entries = await readdir(extractDirectory, { withFileTypes: true });
|
|
487
|
+
for (const entry of entries) {
|
|
488
|
+
if (!entry.isDirectory())
|
|
489
|
+
continue;
|
|
490
|
+
const candidate = join(extractDirectory, entry.name);
|
|
491
|
+
if (await isTemplateSiteDirectory(candidate)) {
|
|
492
|
+
return candidate;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return extractDirectory;
|
|
496
|
+
}
|
|
497
|
+
async function isTemplateSiteDirectory(directory) {
|
|
498
|
+
try {
|
|
499
|
+
const packageJson = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
500
|
+
return packageJson.name === SITE_TEMPLATE_PACKAGE_NAME;
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
467
506
|
async function assertTemplateSiteDirectory(directory) {
|
|
468
507
|
try {
|
|
469
508
|
const packageJson = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
@@ -478,32 +517,6 @@ async function assertTemplateSiteDirectory(directory) {
|
|
|
478
517
|
throw createInvalidUsageError(`Template directory is not readable: ${directory}`);
|
|
479
518
|
}
|
|
480
519
|
}
|
|
481
|
-
function findTemplateSiteDirectory(templateSiteDirectory) {
|
|
482
|
-
if (templateSiteDirectory !== undefined) {
|
|
483
|
-
if (templateSiteDirectory === null) {
|
|
484
|
-
return undefined;
|
|
485
|
-
}
|
|
486
|
-
return templateSiteDirectory;
|
|
487
|
-
}
|
|
488
|
-
const commandsDirectory = dirname(fileURLToPath(import.meta.url));
|
|
489
|
-
const candidates = [
|
|
490
|
-
resolve(commandsDirectory, "..", "..", "template", "site"),
|
|
491
|
-
resolve(process.cwd(), "template", "site"),
|
|
492
|
-
resolve(process.cwd(), "..", "template", "site"),
|
|
493
|
-
];
|
|
494
|
-
for (const candidate of candidates) {
|
|
495
|
-
try {
|
|
496
|
-
const packageJson = JSON.parse(readFileSync(join(candidate, "package.json"), "utf8"));
|
|
497
|
-
if (packageJson.name === SITE_TEMPLATE_PACKAGE_NAME) {
|
|
498
|
-
return candidate;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
// Try the next candidate.
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return undefined;
|
|
506
|
-
}
|
|
507
520
|
function isNodeErrorWithCode(error, code) {
|
|
508
521
|
return isRecord(error) && error.code === code;
|
|
509
522
|
}
|
|
@@ -920,6 +933,7 @@ async function runPostsCreateCommand(context, args) {
|
|
|
920
933
|
"tag",
|
|
921
934
|
"source-id",
|
|
922
935
|
"credit-contact-id",
|
|
936
|
+
"published-at",
|
|
923
937
|
]);
|
|
924
938
|
const body = createPostMutationInput(parsed.options, true);
|
|
925
939
|
const api = await createConfiguredApiContext(context);
|
|
@@ -946,6 +960,7 @@ async function runPostsEditCommand(context, args) {
|
|
|
946
960
|
"tag",
|
|
947
961
|
"source-id",
|
|
948
962
|
"credit-contact-id",
|
|
963
|
+
"published-at",
|
|
949
964
|
]);
|
|
950
965
|
const body = createPostMutationInput(parsed.options, false);
|
|
951
966
|
if (Object.keys(body).length === 0) {
|
|
@@ -975,12 +990,16 @@ async function runPostsDeleteCommand(context, args) {
|
|
|
975
990
|
return { exitCode: ExitCode.Ok };
|
|
976
991
|
}
|
|
977
992
|
async function runPostsLifecycleCommand(context, args, action) {
|
|
978
|
-
const
|
|
993
|
+
const usage = action === "publish"
|
|
994
|
+
? "Usage: slug post publish <slug> [--published-at <datetime>] [--json]"
|
|
995
|
+
: "Usage: slug post unpublish <slug> [--json]";
|
|
996
|
+
const { slug, json, publishedAt } = readPostLifecycleArgs(args, usage, action);
|
|
979
997
|
const api = await createConfiguredApiContext(context);
|
|
980
998
|
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
981
999
|
const response = await api.client.requestJson({
|
|
982
1000
|
method: "POST",
|
|
983
1001
|
path: `/posts/${encodeURIComponent(slug)}/${action}`,
|
|
1002
|
+
body: publishedAt === undefined ? undefined : { publishedAt },
|
|
984
1003
|
});
|
|
985
1004
|
writePostMutationOutput(context.writer, response, json);
|
|
986
1005
|
return { exitCode: ExitCode.Ok };
|
|
@@ -1025,6 +1044,9 @@ function createPostMutationInput(options, requireCreateFields) {
|
|
|
1025
1044
|
copyStringOption(options, input, "url", "url");
|
|
1026
1045
|
copyStringOption(options, input, "excerpt", "excerpt");
|
|
1027
1046
|
copyNumberOption(options, input, "source-id", "sourceId");
|
|
1047
|
+
if (typeof options["published-at"] === "string") {
|
|
1048
|
+
input.publishedAt = normalizeCliPublishedAt(options["published-at"]);
|
|
1049
|
+
}
|
|
1028
1050
|
if (Array.isArray(options.tag)) {
|
|
1029
1051
|
input.tagSlugs = options.tag;
|
|
1030
1052
|
}
|
|
@@ -1065,6 +1087,37 @@ function readSlugCommandArgs(args, usage) {
|
|
|
1065
1087
|
}
|
|
1066
1088
|
return { slug: positional[0], json };
|
|
1067
1089
|
}
|
|
1090
|
+
function readPostLifecycleArgs(args, usage, action) {
|
|
1091
|
+
if (action === "unpublish") {
|
|
1092
|
+
const { slug, json } = readSlugCommandArgs(args, usage);
|
|
1093
|
+
return { slug, json };
|
|
1094
|
+
}
|
|
1095
|
+
const slug = args[0];
|
|
1096
|
+
if (slug === undefined || slug.startsWith("--")) {
|
|
1097
|
+
throw createInvalidUsageError(usage);
|
|
1098
|
+
}
|
|
1099
|
+
const parsed = parsePostFlags(args.slice(1), ["published-at"]);
|
|
1100
|
+
return {
|
|
1101
|
+
slug,
|
|
1102
|
+
json: parsed.json,
|
|
1103
|
+
publishedAt: typeof parsed.options["published-at"] === "string"
|
|
1104
|
+
? normalizeCliPublishedAt(parsed.options["published-at"])
|
|
1105
|
+
: undefined,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function normalizeCliPublishedAt(value) {
|
|
1109
|
+
const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
1110
|
+
const isDateTime = /^\d{4}-\d{2}-\d{2}T/.test(value);
|
|
1111
|
+
if (!isDateOnly && !isDateTime) {
|
|
1112
|
+
throw createInvalidUsageError("--published-at must be a valid ISO date or datetime");
|
|
1113
|
+
}
|
|
1114
|
+
const normalizedInput = isDateOnly ? `${value}T00:00:00.000Z` : value;
|
|
1115
|
+
const date = new Date(normalizedInput);
|
|
1116
|
+
if (Number.isNaN(date.valueOf())) {
|
|
1117
|
+
throw createInvalidUsageError("--published-at must be a valid ISO date or datetime");
|
|
1118
|
+
}
|
|
1119
|
+
return date.toISOString();
|
|
1120
|
+
}
|
|
1068
1121
|
function addOptionalQuery(query, key, value) {
|
|
1069
1122
|
if (typeof value === "string") {
|
|
1070
1123
|
query.set(key, value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evcraddock/slug-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Command-line tool for Slugkit sites.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@evcraddock/slug-core": "0.1.1",
|
|
30
|
-
"
|
|
31
|
-
"tar": "^7.5.16"
|
|
30
|
+
"adm-zip": "^0.5.18"
|
|
32
31
|
}
|
|
33
32
|
}
|