404lab 1.0.1 → 2.0.1

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.
@@ -0,0 +1,71 @@
1
+ import { installMultipleTemplates } from "../core/installer.js";
2
+ import { templateExists, resolveTemplateName } from "../core/templates.js";
3
+ import { getProjectInfo } from "../core/project.js";
4
+ import { log, colors } from "../ui/colors.js";
5
+
6
+ export async function addCommand(templates, flags) {
7
+ if (!templates.length) {
8
+ log.error("No template specified.");
9
+ log.info("Usage: 404lab add <template> [templates...]");
10
+ process.exit(1);
11
+ }
12
+
13
+ const invalidTemplates = templates.filter((t) => !templateExists(t));
14
+
15
+ if (invalidTemplates.length) {
16
+ log.error(`Unknown template(s): ${invalidTemplates.join(", ")}`);
17
+ log.info("Run '404lab list' to see available templates.");
18
+ process.exit(1);
19
+ }
20
+
21
+ const projectInfo = getProjectInfo();
22
+
23
+ if (!projectInfo.valid) {
24
+ log.error(projectInfo.error);
25
+ process.exit(1);
26
+ }
27
+
28
+ log.success(`Next.js project detected`);
29
+ log.info(`Router: ${colors.cyan(projectInfo.router.type)}`);
30
+
31
+ if (projectInfo.alias.hasAlias) {
32
+ log.info(`Alias: ${colors.cyan(projectInfo.alias.alias)}`);
33
+ }
34
+
35
+ log.blank();
36
+
37
+ const isDry = flags.dry;
38
+ const isForce = flags.force;
39
+
40
+ if (isDry) {
41
+ log.warn("Dry run mode - no files will be written\n");
42
+ }
43
+
44
+ const results = await installMultipleTemplates(templates, {
45
+ force: isForce,
46
+ dry: isDry,
47
+ });
48
+
49
+ log.blank();
50
+
51
+ const successful = results.filter((r) => r.success);
52
+ const failed = results.filter((r) => !r.success);
53
+
54
+ if (isDry) {
55
+ log.info(
56
+ `Dry run complete. ${successful.length} template(s) would be installed.`,
57
+ );
58
+ return;
59
+ }
60
+
61
+ if (successful.length) {
62
+ log.success(`${successful.length} template(s) installed successfully.`);
63
+ }
64
+
65
+ if (failed.length) {
66
+ log.warn(`${failed.length} template(s) failed or skipped.`);
67
+ }
68
+
69
+ log.blank();
70
+ log.plain(colors.dim("Enjoy your new 404 page!"));
71
+ }
@@ -0,0 +1,30 @@
1
+ import { getTemplateKeys, TEMPLATES } from "../core/templates.js";
2
+ import { getInstalledTemplates } from "../utils/config.js";
3
+ import { colors, log } from "../ui/colors.js";
4
+
5
+ export function listCommand() {
6
+ const keys = getTemplateKeys();
7
+ const installed = getInstalledTemplates();
8
+
9
+ log.blank();
10
+ log.plain(colors.bold("Available Templates"));
11
+ log.plain(colors.dim("─".repeat(40)));
12
+ log.blank();
13
+
14
+ const maxKeyLen = Math.max(...keys.map((k) => k.length));
15
+
16
+ keys.forEach((key) => {
17
+ const componentName = TEMPLATES[key];
18
+ const isInstalled = installed.includes(componentName);
19
+ const status = isInstalled ? colors.green(" ✔ installed") : "";
20
+ const paddedKey = key.padEnd(maxKeyLen + 2);
21
+
22
+ log.plain(
23
+ ` ${colors.cyan(paddedKey)}${colors.dim(componentName)}${status}`,
24
+ );
25
+ });
26
+
27
+ log.blank();
28
+ log.plain(colors.dim(`Total: ${keys.length} templates`));
29
+ log.blank();
30
+ }
@@ -0,0 +1,31 @@
1
+ import { uninstallTemplate } from "../core/installer.js";
2
+ import { templateExists } from "../core/templates.js";
3
+ import { log } from "../ui/colors.js";
4
+
5
+ export async function removeCommand(template) {
6
+ if (!template) {
7
+ log.error("No template specified.");
8
+ log.info("Usage: 404lab remove <template>");
9
+ process.exit(1);
10
+ }
11
+
12
+ if (!templateExists(template)) {
13
+ log.error(`Unknown template: ${template}`);
14
+ log.info("Run '404lab list' to see available templates.");
15
+ process.exit(1);
16
+ }
17
+
18
+ const result = await uninstallTemplate(template);
19
+
20
+ if (result.cancelled) {
21
+ log.info("Removal cancelled.");
22
+ return;
23
+ }
24
+
25
+ if (!result.success) {
26
+ log.error(result.error);
27
+ process.exit(1);
28
+ }
29
+
30
+ log.success(`${result.componentName} removed.`);
31
+ }
@@ -0,0 +1,156 @@
1
+ import path from "path";
2
+ import { safeWrite, deleteFile, fileExists } from "../utils/fs.js";
3
+ import {
4
+ addInstalledTemplate,
5
+ removeInstalledTemplate,
6
+ } from "../utils/config.js";
7
+ import { getTemplateContent, resolveTemplateName } from "./templates.js";
8
+ import { getProjectInfo } from "./project.js";
9
+ import { log } from "../ui/colors.js";
10
+ import { createSpinner } from "../ui/spinner.js";
11
+ import { confirm } from "../ui/prompt.js";
12
+
13
+ function generateNotFoundContent(componentName, router, alias) {
14
+ const importPath = alias.hasAlias
15
+ ? `@/components/404/${componentName}`
16
+ : router.type === "app"
17
+ ? `../components/404/${componentName}`
18
+ : `../components/404/${componentName}`;
19
+
20
+ return `import ${componentName} from "${importPath}";
21
+
22
+ export default function NotFoundPage() {
23
+ return <${componentName} />;
24
+ }
25
+ `;
26
+ }
27
+
28
+ function getNotFoundPath(router) {
29
+ if (router.type === "app") {
30
+ return path.join(router.path, "not-found.tsx");
31
+ }
32
+ return path.join(router.path, "404.tsx");
33
+ }
34
+
35
+ function getComponentPath(componentsDir, componentName) {
36
+ return path.join(componentsDir, "404", `${componentName}.tsx`);
37
+ }
38
+
39
+ export async function installTemplate(templateKey, options = {}) {
40
+ const { force = false, dry = false, root = process.cwd() } = options;
41
+
42
+ const componentName = resolveTemplateName(templateKey);
43
+
44
+ if (!componentName) {
45
+ return { success: false, error: `Unknown template: ${templateKey}` };
46
+ }
47
+
48
+ const projectInfo = getProjectInfo(root);
49
+
50
+ if (!projectInfo.valid) {
51
+ return { success: false, error: projectInfo.error };
52
+ }
53
+
54
+ const { router, alias, componentsDir } = projectInfo;
55
+
56
+ const spinner = createSpinner(`Installing ${componentName}...`);
57
+
58
+ if (!dry) {
59
+ spinner.start();
60
+ }
61
+
62
+ try {
63
+ const templateContent = getTemplateContent(componentName);
64
+ const componentPath = getComponentPath(componentsDir, componentName);
65
+ const notFoundPath = getNotFoundPath(router);
66
+ const notFoundContent = generateNotFoundContent(
67
+ componentName,
68
+ router,
69
+ alias,
70
+ );
71
+
72
+ if (!dry) {
73
+ spinner.stop();
74
+ }
75
+
76
+ const componentResult = await safeWrite(componentPath, templateContent, {
77
+ force,
78
+ dry,
79
+ root,
80
+ });
81
+
82
+ const notFoundResult = await safeWrite(notFoundPath, notFoundContent, {
83
+ force,
84
+ dry,
85
+ root,
86
+ });
87
+
88
+ if (!dry && (componentResult.written || notFoundResult.written)) {
89
+ addInstalledTemplate(componentName, root);
90
+ }
91
+
92
+ return {
93
+ success: true,
94
+ componentName,
95
+ dry,
96
+ files: {
97
+ component: componentResult,
98
+ notFound: notFoundResult,
99
+ },
100
+ };
101
+ } catch (err) {
102
+ if (!dry) {
103
+ spinner.fail(`Failed to install ${componentName}`);
104
+ }
105
+ return { success: false, error: err.message };
106
+ }
107
+ }
108
+
109
+ export async function installMultipleTemplates(templateKeys, options = {}) {
110
+ const results = [];
111
+
112
+ for (const key of templateKeys) {
113
+ const result = await installTemplate(key, options);
114
+ results.push({ key, ...result });
115
+
116
+ if (!result.success && !options.dry) {
117
+ log.error(result.error);
118
+ }
119
+ }
120
+
121
+ return results;
122
+ }
123
+
124
+ export async function uninstallTemplate(templateKey, options = {}) {
125
+ const { root = process.cwd() } = options;
126
+
127
+ const componentName = resolveTemplateName(templateKey);
128
+
129
+ if (!componentName) {
130
+ return { success: false, error: `Unknown template: ${templateKey}` };
131
+ }
132
+
133
+ const projectInfo = getProjectInfo(root);
134
+
135
+ if (!projectInfo.valid) {
136
+ return { success: false, error: projectInfo.error };
137
+ }
138
+
139
+ const { componentsDir } = projectInfo;
140
+ const componentPath = getComponentPath(componentsDir, componentName);
141
+
142
+ if (!fileExists(componentPath)) {
143
+ return { success: false, error: `${componentName} is not installed.` };
144
+ }
145
+
146
+ const shouldDelete = await confirm(`Remove ${componentName}?`, true);
147
+
148
+ if (!shouldDelete) {
149
+ return { success: false, cancelled: true };
150
+ }
151
+
152
+ deleteFile(componentPath);
153
+ removeInstalledTemplate(componentName, root);
154
+
155
+ return { success: true, componentName };
156
+ }
@@ -0,0 +1,115 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export function validateNextProject(root = process.cwd()) {
5
+ const pkgPath = path.join(root, "package.json");
6
+
7
+ if (!fs.existsSync(pkgPath)) {
8
+ return {
9
+ valid: false,
10
+ error: "No package.json found. Are you in a project directory?",
11
+ };
12
+ }
13
+
14
+ let pkg;
15
+ try {
16
+ pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
17
+ } catch {
18
+ return {
19
+ valid: false,
20
+ error: "Invalid package.json file.",
21
+ };
22
+ }
23
+
24
+ const hasNext = pkg.dependencies?.next || pkg.devDependencies?.next;
25
+
26
+ if (!hasNext) {
27
+ return {
28
+ valid: false,
29
+ error: "Next.js not found in dependencies. Is this a Next.js project?",
30
+ };
31
+ }
32
+
33
+ return { valid: true };
34
+ }
35
+
36
+ export function detectRouter(root = process.cwd()) {
37
+ const appDir = path.join(root, "app");
38
+ const srcAppDir = path.join(root, "src", "app");
39
+ const pagesDir = path.join(root, "pages");
40
+ const srcPagesDir = path.join(root, "src", "pages");
41
+
42
+ if (fs.existsSync(appDir)) return { type: "app", path: appDir };
43
+ if (fs.existsSync(srcAppDir)) return { type: "app", path: srcAppDir };
44
+ if (fs.existsSync(pagesDir)) return { type: "pages", path: pagesDir };
45
+ if (fs.existsSync(srcPagesDir)) return { type: "pages", path: srcPagesDir };
46
+
47
+ return null;
48
+ }
49
+
50
+ export function detectAlias(root = process.cwd()) {
51
+ const tsconfigPath = path.join(root, "tsconfig.json");
52
+ const jsconfigPath = path.join(root, "jsconfig.json");
53
+
54
+ const configPath = fs.existsSync(tsconfigPath)
55
+ ? tsconfigPath
56
+ : fs.existsSync(jsconfigPath)
57
+ ? jsconfigPath
58
+ : null;
59
+
60
+ if (!configPath) {
61
+ return { hasAlias: false, alias: null };
62
+ }
63
+
64
+ try {
65
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
66
+ const paths = config.compilerOptions?.paths;
67
+
68
+ if (paths?.["@/*"]) {
69
+ return { hasAlias: true, alias: "@" };
70
+ }
71
+
72
+ return { hasAlias: false, alias: null };
73
+ } catch {
74
+ return { hasAlias: false, alias: null };
75
+ }
76
+ }
77
+
78
+ export function getComponentsDir(root = process.cwd()) {
79
+ const srcComponents = path.join(root, "src", "components");
80
+ const rootComponents = path.join(root, "components");
81
+
82
+ if (fs.existsSync(path.join(root, "src"))) {
83
+ return srcComponents;
84
+ }
85
+
86
+ return rootComponents;
87
+ }
88
+
89
+ export function getProjectInfo(root = process.cwd()) {
90
+ const validation = validateNextProject(root);
91
+
92
+ if (!validation.valid) {
93
+ return { valid: false, error: validation.error };
94
+ }
95
+
96
+ const router = detectRouter(root);
97
+
98
+ if (!router) {
99
+ return {
100
+ valid: false,
101
+ error: "No app/ or pages/ directory found. Create one first.",
102
+ };
103
+ }
104
+
105
+ const alias = detectAlias(root);
106
+ const componentsDir = getComponentsDir(root);
107
+
108
+ return {
109
+ valid: true,
110
+ router,
111
+ alias,
112
+ componentsDir,
113
+ root,
114
+ };
115
+ }
@@ -0,0 +1,65 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates");
8
+
9
+ export const TEMPLATES = {
10
+ stoneage: "StoneAge",
11
+ amongus: "AmongUs",
12
+ blueglitch: "BlueGlitch",
13
+ geeksforgeeks: "GeeksforGeeks",
14
+ google: "Google",
15
+ macos: "MacOs",
16
+ modern: "ModernPage",
17
+ particles: "Particles",
18
+ poet: "Poet",
19
+ retro: "RetroTv",
20
+ simple: "SimplePage",
21
+ snow: "Snow",
22
+ strangethings: "StrangerThings",
23
+ terminal: "Terminal404",
24
+ };
25
+
26
+ export function getTemplateKeys() {
27
+ return Object.keys(TEMPLATES).sort();
28
+ }
29
+
30
+ export function resolveTemplateName(key) {
31
+ const normalized = key.toLowerCase();
32
+ return TEMPLATES[normalized] || null;
33
+ }
34
+
35
+ export function templateExists(key) {
36
+ return resolveTemplateName(key) !== null;
37
+ }
38
+
39
+ export function getTemplateContent(componentName) {
40
+ const templatePath = path.join(TEMPLATES_DIR, `${componentName}.tsx`);
41
+
42
+ if (!fs.existsSync(templatePath)) {
43
+ throw new Error(`Template file not found: ${componentName}.tsx`);
44
+ }
45
+
46
+ return fs.readFileSync(templatePath, "utf8");
47
+ }
48
+
49
+ export function getTemplateInfo(key) {
50
+ const componentName = resolveTemplateName(key);
51
+
52
+ if (!componentName) {
53
+ return null;
54
+ }
55
+
56
+ return {
57
+ key,
58
+ componentName,
59
+ filename: `${componentName}.tsx`,
60
+ };
61
+ }
62
+
63
+ export function getAllTemplateInfo() {
64
+ return getTemplateKeys().map((key) => getTemplateInfo(key));
65
+ }
package/cli/index.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { addCommand } from "./commands/add.js";
4
+ import { removeCommand } from "./commands/remove.js";
5
+ import { listCommand } from "./commands/list.js";
6
+ import { colors, log } from "./ui/colors.js";
7
+
8
+ const VERSION = "2.0.0";
9
+
10
+ function parseArgs(argv) {
11
+ const args = argv.slice(2);
12
+ const command = args[0];
13
+ const positional = [];
14
+ const flags = {
15
+ help: false,
16
+ version: false,
17
+ force: false,
18
+ dry: false,
19
+ };
20
+
21
+ for (let i = 1; i < args.length; i++) {
22
+ const arg = args[i];
23
+
24
+ if (arg === "--help" || arg === "-h") {
25
+ flags.help = true;
26
+ } else if (arg === "--version" || arg === "-v") {
27
+ flags.version = true;
28
+ } else if (arg === "--force" || arg === "-f") {
29
+ flags.force = true;
30
+ } else if (arg === "--dry" || arg === "-d") {
31
+ flags.dry = true;
32
+ } else if (!arg.startsWith("-")) {
33
+ positional.push(arg.toLowerCase());
34
+ }
35
+ }
36
+
37
+ if (args.includes("--help") || args.includes("-h")) {
38
+ flags.help = true;
39
+ }
40
+
41
+ if (args.includes("--version") || args.includes("-v")) {
42
+ flags.version = true;
43
+ }
44
+
45
+ return { command, positional, flags };
46
+ }
47
+
48
+ function showHelp() {
49
+ console.log(`
50
+ ${colors.bold("404lab")} ${colors.dim(`v${VERSION}`)}
51
+ ${colors.cyan("Generate beautiful 404 pages for Next.js")}
52
+
53
+ ${colors.bold("Usage:")}
54
+ 404lab ${colors.cyan("<command>")} [options]
55
+
56
+ ${colors.bold("Commands:")}
57
+ ${colors.cyan("add")} <templates...> Install one or more templates
58
+ ${colors.cyan("remove")} <template> Remove an installed template
59
+ ${colors.cyan("list")} Show all available templates
60
+
61
+ ${colors.bold("Options:")}
62
+ ${colors.cyan("--force")}, ${colors.cyan("-f")} Overwrite existing files without asking
63
+ ${colors.cyan("--dry")}, ${colors.cyan("-d")} Simulate install (no files written)
64
+ ${colors.cyan("--help")}, ${colors.cyan("-h")} Show this help message
65
+ ${colors.cyan("--version")}, ${colors.cyan("-v")} Show version number
66
+
67
+ ${colors.bold("Examples:")}
68
+ ${colors.dim("$")} 404lab add macos
69
+ ${colors.dim("$")} 404lab add snow retro terminal
70
+ ${colors.dim("$")} 404lab add macos --dry
71
+ ${colors.dim("$")} 404lab add macos --force
72
+ ${colors.dim("$")} 404lab remove macos
73
+ ${colors.dim("$")} 404lab list
74
+
75
+ ${colors.dim("Documentation: https://github.com/yourusername/404lab")}
76
+ `);
77
+ }
78
+
79
+ function showVersion() {
80
+ console.log(`404lab v${VERSION}`);
81
+ }
82
+
83
+ async function main() {
84
+ const { command, positional, flags } = parseArgs(process.argv);
85
+
86
+ if (flags.version) {
87
+ showVersion();
88
+ return;
89
+ }
90
+
91
+ if (!command || flags.help) {
92
+ showHelp();
93
+ return;
94
+ }
95
+
96
+ switch (command) {
97
+ case "add":
98
+ await addCommand(positional, flags);
99
+ break;
100
+
101
+ case "remove":
102
+ await removeCommand(positional[0]);
103
+ break;
104
+
105
+ case "list":
106
+ listCommand();
107
+ break;
108
+
109
+ default:
110
+ log.error(`Unknown command: ${command}`);
111
+ log.info("Run '404lab --help' for usage information.");
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ main().catch((err) => {
117
+ log.error(err.message);
118
+ process.exit(1);
119
+ });
@@ -0,0 +1,38 @@
1
+ const codes = {
2
+ reset: "\x1b[0m",
3
+ bold: "\x1b[1m",
4
+ dim: "\x1b[2m",
5
+
6
+ red: "\x1b[31m",
7
+ green: "\x1b[32m",
8
+ yellow: "\x1b[33m",
9
+ blue: "\x1b[34m",
10
+ magenta: "\x1b[35m",
11
+ cyan: "\x1b[36m",
12
+ white: "\x1b[37m",
13
+ gray: "\x1b[90m",
14
+ };
15
+
16
+ const wrap = (code) => (text) => `${code}${text}${codes.reset}`;
17
+
18
+ export const colors = {
19
+ red: wrap(codes.red),
20
+ green: wrap(codes.green),
21
+ yellow: wrap(codes.yellow),
22
+ blue: wrap(codes.blue),
23
+ magenta: wrap(codes.magenta),
24
+ cyan: wrap(codes.cyan),
25
+ white: wrap(codes.white),
26
+ gray: wrap(codes.gray),
27
+ bold: wrap(codes.bold),
28
+ dim: wrap(codes.dim),
29
+ };
30
+
31
+ export const log = {
32
+ info: (msg) => console.log(`${codes.cyan}ℹ${codes.reset} ${msg}`),
33
+ success: (msg) => console.log(`${codes.green}✔${codes.reset} ${msg}`),
34
+ warn: (msg) => console.log(`${codes.yellow}⚠${codes.reset} ${msg}`),
35
+ error: (msg) => console.error(`${codes.red}✖${codes.reset} ${msg}`),
36
+ plain: (msg) => console.log(msg),
37
+ blank: () => console.log(""),
38
+ };
@@ -0,0 +1,50 @@
1
+ import readline from "readline";
2
+ import { colors } from "./colors.js";
3
+
4
+ export function prompt(question, defaultValue = "") {
5
+ const rl = readline.createInterface({
6
+ input: process.stdin,
7
+ output: process.stdout,
8
+ });
9
+
10
+ return new Promise((resolve) => {
11
+ rl.question(question, (answer) => {
12
+ rl.close();
13
+ const trimmed = answer.trim();
14
+ resolve(trimmed || defaultValue);
15
+ });
16
+ });
17
+ }
18
+
19
+ export async function confirm(message, defaultNo = true) {
20
+ const hint = defaultNo ? "y/N" : "Y/n";
21
+ const question = `${colors.cyan("?")} ${message} ${colors.dim(`(${hint})`)} `;
22
+
23
+ const answer = await prompt(question);
24
+ const lower = answer.toLowerCase();
25
+
26
+ if (defaultNo) {
27
+ return lower === "y" || lower === "yes";
28
+ }
29
+
30
+ return lower !== "n" && lower !== "no";
31
+ }
32
+
33
+ export async function select(message, options) {
34
+ console.log(`\n${colors.cyan("?")} ${message}\n`);
35
+
36
+ options.forEach((opt, i) => {
37
+ console.log(` ${colors.cyan(`${i + 1}.`)} ${opt}`);
38
+ });
39
+
40
+ console.log("");
41
+
42
+ const answer = await prompt(`${colors.dim("Enter number:")} `);
43
+ const index = parseInt(answer, 10) - 1;
44
+
45
+ if (index >= 0 && index < options.length) {
46
+ return options[index];
47
+ }
48
+
49
+ return null;
50
+ }
@@ -0,0 +1,63 @@
1
+ import { colors } from "./colors.js";
2
+
3
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
+
5
+ export function createSpinner(text) {
6
+ let frameIndex = 0;
7
+ let interval = null;
8
+ let currentText = text;
9
+
10
+ const clear = () => {
11
+ process.stdout.clearLine?.(0);
12
+ process.stdout.cursorTo?.(0);
13
+ };
14
+
15
+ const render = () => {
16
+ clear();
17
+ const frame = colors.cyan(frames[frameIndex]);
18
+ process.stdout.write(`${frame} ${currentText}`);
19
+ frameIndex = (frameIndex + 1) % frames.length;
20
+ };
21
+
22
+ return {
23
+ start(msg) {
24
+ if (msg) currentText = msg;
25
+ if (interval) return this;
26
+ interval = setInterval(render, 80);
27
+ render();
28
+ return this;
29
+ },
30
+
31
+ update(msg) {
32
+ currentText = msg;
33
+ return this;
34
+ },
35
+
36
+ stop() {
37
+ if (interval) {
38
+ clearInterval(interval);
39
+ interval = null;
40
+ clear();
41
+ }
42
+ return this;
43
+ },
44
+
45
+ success(msg) {
46
+ this.stop();
47
+ console.log(`${colors.green("✔")} ${msg || currentText}`);
48
+ return this;
49
+ },
50
+
51
+ fail(msg) {
52
+ this.stop();
53
+ console.log(`${colors.red("✖")} ${msg || currentText}`);
54
+ return this;
55
+ },
56
+
57
+ warn(msg) {
58
+ this.stop();
59
+ console.log(`${colors.yellow("⚠")} ${msg || currentText}`);
60
+ return this;
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,55 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const CONFIG_FILE = "404lab.config.json";
5
+
6
+ export function getConfigPath(root = process.cwd()) {
7
+ return path.join(root, CONFIG_FILE);
8
+ }
9
+
10
+ export function loadConfig(root = process.cwd()) {
11
+ const configPath = getConfigPath(root);
12
+
13
+ if (!fs.existsSync(configPath)) {
14
+ return { installed: [] };
15
+ }
16
+
17
+ try {
18
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
19
+ } catch {
20
+ return { installed: [] };
21
+ }
22
+ }
23
+
24
+ export function saveConfig(config, root = process.cwd()) {
25
+ const configPath = getConfigPath(root);
26
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
27
+ }
28
+
29
+ export function addInstalledTemplate(templateName, root = process.cwd()) {
30
+ const config = loadConfig(root);
31
+
32
+ if (!config.installed.includes(templateName)) {
33
+ config.installed.push(templateName);
34
+ saveConfig(config, root);
35
+ }
36
+
37
+ return config;
38
+ }
39
+
40
+ export function removeInstalledTemplate(templateName, root = process.cwd()) {
41
+ const config = loadConfig(root);
42
+ config.installed = config.installed.filter((t) => t !== templateName);
43
+ saveConfig(config, root);
44
+ return config;
45
+ }
46
+
47
+ export function isTemplateInstalled(templateName, root = process.cwd()) {
48
+ const config = loadConfig(root);
49
+ return config.installed.includes(templateName);
50
+ }
51
+
52
+ export function getInstalledTemplates(root = process.cwd()) {
53
+ const config = loadConfig(root);
54
+ return config.installed;
55
+ }
@@ -0,0 +1,69 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { log } from "../ui/colors.js";
4
+ import { confirm } from "../ui/prompt.js";
5
+
6
+ export function ensureDir(dirPath) {
7
+ if (!fs.existsSync(dirPath)) {
8
+ fs.mkdirSync(dirPath, { recursive: true });
9
+ }
10
+ }
11
+
12
+ export function fileExists(filePath) {
13
+ return fs.existsSync(filePath);
14
+ }
15
+
16
+ export function readFileContent(filePath) {
17
+ return fs.readFileSync(filePath, "utf8");
18
+ }
19
+
20
+ export function writeFileContent(filePath, content) {
21
+ ensureDir(path.dirname(filePath));
22
+ fs.writeFileSync(filePath, content, "utf8");
23
+ }
24
+
25
+ export function deleteFile(filePath) {
26
+ if (fs.existsSync(filePath)) {
27
+ fs.unlinkSync(filePath);
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+
33
+ export function relativePath(from, to) {
34
+ return path.relative(from, to);
35
+ }
36
+
37
+ export async function safeWrite(filePath, content, options = {}) {
38
+ const { force = false, dry = false, root = process.cwd() } = options;
39
+ const relPath = path.relative(root, filePath);
40
+
41
+ if (dry) {
42
+ log.info(`Would create: ${relPath}`);
43
+ return { written: false, skipped: false, dry: true };
44
+ }
45
+
46
+ if (fs.existsSync(filePath)) {
47
+ if (force) {
48
+ writeFileContent(filePath, content);
49
+ log.warn(`Overwritten: ${relPath}`);
50
+ return { written: true, skipped: false, overwritten: true };
51
+ }
52
+
53
+ log.warn(`File exists: ${relPath}`);
54
+ const shouldOverwrite = await confirm("Overwrite?", true);
55
+
56
+ if (!shouldOverwrite) {
57
+ log.info(`Skipped: ${relPath}`);
58
+ return { written: false, skipped: true };
59
+ }
60
+
61
+ writeFileContent(filePath, content);
62
+ log.success(`Overwritten: ${relPath}`);
63
+ return { written: true, skipped: false, overwritten: true };
64
+ }
65
+
66
+ writeFileContent(filePath, content);
67
+ log.success(`Created: ${relPath}`);
68
+ return { written: true, skipped: false };
69
+ }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "404lab",
3
- "version": "1.0.1",
3
+ "version": "2.0.1",
4
4
  "description": "A CLI tool for generating beautiful custom 404 pages in Next.js projects",
5
5
  "type": "module",
6
6
  "preferGlobal": true,
7
7
  "bin": {
8
- "404lab": "./bin/index.js"
8
+ "404lab": "./cli/index.js"
9
9
  },
10
10
  "files": [
11
- "bin",
11
+ "cli",
12
12
  "templates",
13
13
  "README.md"
14
14
  ],
package/bin/index.js DELETED
@@ -1,188 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "fs";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
-
7
- /**
8
- * Resolve __dirname equivalent for ES modules
9
- */
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
-
13
- /**
14
- * Supported templates mapped to component names
15
- */
16
- const TEMPLATES = {
17
- stoneage: "StoneAge",
18
- amongus: "AmongUs",
19
- blueglitch: "BlueGlitch",
20
- geeksforgeeks: "GeeksforGeeks",
21
- google: "Google",
22
- macos: "MacOs",
23
- modern: "ModernPage",
24
- particles: "Particles",
25
- poet: "Poet",
26
- retro: "RetroTv",
27
- simple: "SimplePage",
28
- snow: "Snow",
29
- strangethings: "StrangerThings",
30
- terminal: "Terminal404",
31
- };
32
-
33
- /**
34
- * Read template content from templates directory
35
- */
36
- function getTemplateContent(componentName) {
37
- const templatesDir = path.join(__dirname, "..", "templates");
38
- const templatePath = path.join(templatesDir, `${componentName}.tsx`);
39
-
40
- if (!fs.existsSync(templatePath)) {
41
- throw new Error(`Template "${componentName}" not found`);
42
- }
43
-
44
- return fs.readFileSync(templatePath, "utf-8");
45
- }
46
-
47
- /**
48
- * Create the 404 component file
49
- */
50
- function createComponentFile(componentName, templateContent) {
51
- const componentDir = path.join(process.cwd(), "components", "404");
52
- const componentPath = path.join(componentDir, `${componentName}.tsx`);
53
-
54
- fs.mkdirSync(componentDir, { recursive: true });
55
- fs.writeFileSync(componentPath, templateContent, "utf-8");
56
-
57
- return componentPath;
58
- }
59
-
60
- /**
61
- * Create or overwrite app/not-found.tsx
62
- */
63
- function createNotFoundPage(componentName) {
64
- const appDir = path.join(process.cwd(), "app");
65
- const notFoundPath = path.join(appDir, "not-found.tsx");
66
-
67
- fs.mkdirSync(appDir, { recursive: true });
68
-
69
- const content = `import ${componentName} from "@/components/404/${componentName}";
70
-
71
- export default function NotFoundPage() {
72
- return <${componentName} />;
73
- }
74
- `;
75
-
76
- fs.writeFileSync(notFoundPath, content, "utf-8");
77
-
78
- return notFoundPath;
79
- }
80
-
81
- /**
82
- * Display help information
83
- */
84
- function showHelp() {
85
- console.log(`
86
- 404lab — Custom 404 Page Generator for Next.js (App Router)
87
-
88
- Usage:
89
- 404lab add <template> Generate a 404 page from a template
90
- 404lab list List all available templates
91
- 404lab --help Show this help message
92
-
93
- Available Templates:
94
- ${Object.keys(TEMPLATES)
95
- .sort()
96
- .map((t) => ` ${t}`)
97
- .join("\n")}
98
-
99
- Examples:
100
- 404lab add stoneage
101
- 404lab add amongus
102
- 404lab add macos
103
-
104
- Generated Files:
105
- components/404/<Template>.tsx
106
- app/not-found.tsx (overwritten if it already exists)
107
- `);
108
- }
109
-
110
- /**
111
- * List available templates
112
- */
113
- function listTemplates() {
114
- console.log("\nAvailable 404 Templates:\n");
115
-
116
- Object.keys(TEMPLATES)
117
- .sort()
118
- .forEach((name) => {
119
- console.log(` ${name}`);
120
- });
121
-
122
- console.log("\nUsage: 404lab add <template>\n");
123
- }
124
-
125
- /**
126
- * Main CLI entry point
127
- */
128
- function main() {
129
- const args = process.argv.slice(2);
130
-
131
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
132
- showHelp();
133
- process.exit(0);
134
- }
135
-
136
- if (args[0] === "list" || args[0] === "ls") {
137
- listTemplates();
138
- process.exit(0);
139
- }
140
-
141
- if (args[0] !== "add") {
142
- console.error(`Error: Unknown command "${args[0]}"`);
143
- console.log('Run "404lab --help" for usage information.');
144
- process.exit(1);
145
- }
146
-
147
- if (!args[1]) {
148
- console.error("Error: Template name is required.");
149
- console.log("Usage: 404lab add <template>");
150
- console.log("Available templates:", Object.keys(TEMPLATES).join(", "));
151
- process.exit(1);
152
- }
153
-
154
- const templateKey = args[1].toLowerCase();
155
- const componentName = TEMPLATES[templateKey];
156
-
157
- if (!componentName) {
158
- console.error(`Error: Template "${templateKey}" does not exist.`);
159
- console.log("Available templates:", Object.keys(TEMPLATES).join(", "));
160
- console.log('Run "404lab list" to view all templates.');
161
- process.exit(1);
162
- }
163
-
164
- try {
165
- console.log(`Installing 404 template: ${componentName}`);
166
-
167
- const templateContent = getTemplateContent(componentName);
168
-
169
- const componentPath = createComponentFile(componentName, templateContent);
170
- console.log(`Created: ${path.relative(process.cwd(), componentPath)}`);
171
-
172
- const notFoundPath = createNotFoundPage(componentName);
173
- console.log(`Created: ${path.relative(process.cwd(), notFoundPath)}`);
174
-
175
- console.log("\n404 page successfully installed.");
176
- console.log("Next steps:");
177
- console.log(" 1. Start your Next.js development server.");
178
- console.log(" 2. Visit a non-existent route to view the 404 page.");
179
- console.log(
180
- ` 3. Customize components/404/${componentName}.tsx as needed.\n`,
181
- );
182
- } catch (error) {
183
- console.error("Error generating files:", error.message);
184
- process.exit(1);
185
- }
186
- }
187
-
188
- main();