@evcraddock/slug-cli 0.6.3 → 0.6.5
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 +1 -1
- package/dist/commands.d.ts +0 -1
- package/dist/commands.js +99 -59
- 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 latest template zip artifact and falls back to the template artifact published with the installed CLI version if the latest artifact is missing. `--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. Explicit `--template-url` and `SLUGKIT_TEMPLATE_URL` overrides do not fall back.
|
|
29
29
|
|
|
30
30
|
## Login and configuration
|
|
31
31
|
|
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";
|
|
@@ -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 latest template zip artifact and falls back to the CLI-versioned artifact if latest is missing. Set SLUGKIT_TEMPLATE_URL or pass --template-url to use a different zip artifact without fallback. 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,40 @@ 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
|
-
|
|
429
|
-
|
|
441
|
+
return downloadDefaultTemplateSite(context, getDefaultTemplateResolution(context));
|
|
442
|
+
}
|
|
443
|
+
function getDefaultTemplateResolution(context) {
|
|
444
|
+
const environmentUrl = process.env.SLUGKIT_TEMPLATE_URL?.trim();
|
|
445
|
+
if (environmentUrl !== undefined && environmentUrl !== "") {
|
|
446
|
+
return { primaryUrl: environmentUrl };
|
|
430
447
|
}
|
|
431
|
-
|
|
448
|
+
const primaryUrl = context.templateAssetBaseUrl !== undefined
|
|
449
|
+
? `${context.templateAssetBaseUrl.replace(/\/+$/u, "")}/slugkit-site-template.zip`
|
|
450
|
+
: DEFAULT_TEMPLATE_URL;
|
|
451
|
+
return {
|
|
452
|
+
primaryUrl,
|
|
453
|
+
fallbackUrl: getVersionedTemplateUrl(context.packageVersion),
|
|
454
|
+
};
|
|
432
455
|
}
|
|
433
|
-
function
|
|
434
|
-
|
|
435
|
-
|
|
456
|
+
function getVersionedTemplateUrl(packageVersion) {
|
|
457
|
+
const version = packageVersion.trim().replace(/^v/u, "");
|
|
458
|
+
return `https://forge.caradoc.com/erik/slugkit/releases/download/v${version}/slugkit-site-template-v${version}.zip`;
|
|
459
|
+
}
|
|
460
|
+
async function downloadDefaultTemplateSite(context, resolution) {
|
|
461
|
+
try {
|
|
462
|
+
return await downloadTemplateSite(context, resolution.primaryUrl);
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (resolution.fallbackUrl === undefined ||
|
|
466
|
+
!(error instanceof TemplateDownloadError) ||
|
|
467
|
+
error.status !== 404) {
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
return downloadTemplateSite(context, resolution.fallbackUrl);
|
|
436
471
|
}
|
|
437
|
-
throw createInvalidUsageError(`Unsupported template: ${template}. This CLI currently includes ${DEFAULT_TEMPLATE_NAME}; future releases may resolve external template packages.`);
|
|
438
472
|
}
|
|
439
473
|
async function downloadTemplateSite(context, templateUrl) {
|
|
440
474
|
let parsedUrl;
|
|
@@ -450,20 +484,52 @@ async function downloadTemplateSite(context, templateUrl) {
|
|
|
450
484
|
const fetchImpl = context.fetchImpl ?? fetch;
|
|
451
485
|
const response = await fetchImpl(parsedUrl);
|
|
452
486
|
if (!response.ok) {
|
|
453
|
-
throw new
|
|
487
|
+
throw new TemplateDownloadError(parsedUrl.toString(), response.status);
|
|
454
488
|
}
|
|
455
489
|
const tempDirectory = await mkdtemp(join(tmpdir(), "slug-template-"));
|
|
456
|
-
const archivePath = join(tempDirectory, "template.
|
|
490
|
+
const archivePath = join(tempDirectory, "template.zip");
|
|
457
491
|
const extractDirectory = join(tempDirectory, "template");
|
|
458
492
|
await mkdir(extractDirectory, { recursive: true });
|
|
459
493
|
await writeFile(archivePath, Buffer.from(await response.arrayBuffer()));
|
|
460
|
-
|
|
461
|
-
await
|
|
494
|
+
new AdmZip(archivePath).extractAllTo(extractDirectory, true);
|
|
495
|
+
const templateDirectory = await findExtractedTemplateDirectory(extractDirectory);
|
|
496
|
+
await assertTemplateSiteDirectory(templateDirectory);
|
|
462
497
|
return {
|
|
463
|
-
directory:
|
|
498
|
+
directory: templateDirectory,
|
|
464
499
|
cleanup: () => rm(tempDirectory, { force: true, recursive: true }),
|
|
465
500
|
};
|
|
466
501
|
}
|
|
502
|
+
class TemplateDownloadError extends CliError {
|
|
503
|
+
status;
|
|
504
|
+
constructor(templateUrl, status) {
|
|
505
|
+
super(`Failed to download Slugkit template from ${templateUrl}: HTTP ${status}`, ExitCode.NetworkError);
|
|
506
|
+
this.status = status;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function findExtractedTemplateDirectory(extractDirectory) {
|
|
510
|
+
if (await isTemplateSiteDirectory(extractDirectory)) {
|
|
511
|
+
return extractDirectory;
|
|
512
|
+
}
|
|
513
|
+
const entries = await readdir(extractDirectory, { withFileTypes: true });
|
|
514
|
+
for (const entry of entries) {
|
|
515
|
+
if (!entry.isDirectory())
|
|
516
|
+
continue;
|
|
517
|
+
const candidate = join(extractDirectory, entry.name);
|
|
518
|
+
if (await isTemplateSiteDirectory(candidate)) {
|
|
519
|
+
return candidate;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return extractDirectory;
|
|
523
|
+
}
|
|
524
|
+
async function isTemplateSiteDirectory(directory) {
|
|
525
|
+
try {
|
|
526
|
+
const packageJson = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
527
|
+
return packageJson.name === SITE_TEMPLATE_PACKAGE_NAME;
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
467
533
|
async function assertTemplateSiteDirectory(directory) {
|
|
468
534
|
try {
|
|
469
535
|
const packageJson = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
@@ -478,32 +544,6 @@ async function assertTemplateSiteDirectory(directory) {
|
|
|
478
544
|
throw createInvalidUsageError(`Template directory is not readable: ${directory}`);
|
|
479
545
|
}
|
|
480
546
|
}
|
|
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
547
|
function isNodeErrorWithCode(error, code) {
|
|
508
548
|
return isRecord(error) && error.code === code;
|
|
509
549
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evcraddock/slug-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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
|
}
|