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.
- package/cli/commands/add.js +71 -0
- package/cli/commands/list.js +30 -0
- package/cli/commands/remove.js +31 -0
- package/cli/core/installer.js +156 -0
- package/cli/core/project.js +115 -0
- package/cli/core/templates.js +65 -0
- package/cli/index.js +119 -0
- package/cli/ui/colors.js +38 -0
- package/cli/ui/prompt.js +50 -0
- package/cli/ui/spinner.js +63 -0
- package/cli/utils/config.js +55 -0
- package/cli/utils/fs.js +69 -0
- package/package.json +3 -3
- package/bin/index.js +0 -188
|
@@ -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
|
+
});
|
package/cli/ui/colors.js
ADDED
|
@@ -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
|
+
};
|
package/cli/ui/prompt.js
ADDED
|
@@ -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
|
+
}
|
package/cli/utils/fs.js
ADDED
|
@@ -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": "
|
|
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": "./
|
|
8
|
+
"404lab": "./cli/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"
|
|
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();
|