@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 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` consumes the package-backed `@evcraddock/slug-template` default template. `--template-dir` and `--template-url` remain available for local or remote template testing, and `--template` reserves the selection path for future named or external template packages.
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
 
@@ -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 { fileURLToPath } from "node:url";
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 = getTemplateMetadata().templateSitePackageName;
138
- const DEFAULT_TEMPLATE_METADATA = getTemplateMetadata();
139
- const DEFAULT_TEMPLATE_NAME = DEFAULT_TEMPLATE_METADATA.name;
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 tarball from a custom URL.
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 uses ${SLUGKIT_TEMPLATE_PACKAGE_NAME}. Template packages are initialization sources only; generated sites do not depend on them at runtime.`;
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, _name) {
335
- return !isTemplateEntryIncluded(relativePath);
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: `${SLUGKIT_TEMPLATE_PACKAGE_NAME}/${DEFAULT_TEMPLATE_NAME}`,
411
+ template: DEFAULT_TEMPLATE_NAME,
398
412
  generatedAt: new Date().toISOString(),
399
- packageMetadata: createGeneratedSitePackageMetadata(DEFAULT_TEMPLATE_METADATA),
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(metadata) {
417
+ function createGeneratedSitePackageMetadata() {
404
418
  return {
405
419
  template: {
406
- packageName: metadata.packageName,
407
- name: metadata.name,
408
- version: metadata.version,
420
+ packageName: "zip-artifact",
421
+ name: DEFAULT_TEMPLATE_NAME,
422
+ version: "downloaded",
409
423
  },
410
- packages: { ...metadata.includedSlugkitPackages },
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
- return resolveNamedTemplateSource(options.template);
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
- const localTemplateDirectory = findTemplateSiteDirectory(context.templateSiteDirectory);
428
- if (localTemplateDirectory !== undefined) {
429
- return { directory: localTemplateDirectory };
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
- return { directory: getTemplateRoot() };
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 resolveNamedTemplateSource(template) {
434
- if (template === DEFAULT_TEMPLATE_NAME || template === SLUGKIT_TEMPLATE_PACKAGE_NAME) {
435
- return { directory: getTemplateRoot() };
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 CliError(`Failed to download Slugkit template from ${parsedUrl.toString()}: HTTP ${response.status}`, ExitCode.NetworkError);
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.tgz");
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
- await extractTarball({ file: archivePath, cwd: extractDirectory });
461
- await assertTemplateSiteDirectory(extractDirectory);
494
+ new AdmZip(archivePath).extractAllTo(extractDirectory, true);
495
+ const templateDirectory = await findExtractedTemplateDirectory(extractDirectory);
496
+ await assertTemplateSiteDirectory(templateDirectory);
462
497
  return {
463
- directory: extractDirectory,
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",
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
- "@evcraddock/slug-template": "0.1.3",
31
- "tar": "^7.5.16"
30
+ "adm-zip": "^0.5.18"
32
31
  }
33
32
  }