404lab 1.0.1 → 2.0.2

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,66 @@
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
+ vercel: "Vercel"
25
+ };
26
+
27
+ export function getTemplateKeys() {
28
+ return Object.keys(TEMPLATES).sort();
29
+ }
30
+
31
+ export function resolveTemplateName(key) {
32
+ const normalized = key.toLowerCase();
33
+ return TEMPLATES[normalized] || null;
34
+ }
35
+
36
+ export function templateExists(key) {
37
+ return resolveTemplateName(key) !== null;
38
+ }
39
+
40
+ export function getTemplateContent(componentName) {
41
+ const templatePath = path.join(TEMPLATES_DIR, `${componentName}.tsx`);
42
+
43
+ if (!fs.existsSync(templatePath)) {
44
+ throw new Error(`Template file not found: ${componentName}.tsx`);
45
+ }
46
+
47
+ return fs.readFileSync(templatePath, "utf8");
48
+ }
49
+
50
+ export function getTemplateInfo(key) {
51
+ const componentName = resolveTemplateName(key);
52
+
53
+ if (!componentName) {
54
+ return null;
55
+ }
56
+
57
+ return {
58
+ key,
59
+ componentName,
60
+ filename: `${componentName}.tsx`,
61
+ };
62
+ }
63
+
64
+ export function getAllTemplateInfo() {
65
+ return getTemplateKeys().map((key) => getTemplateInfo(key));
66
+ }
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
+ }