@ghostly-ui/cli 0.2.4

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 ADDED
@@ -0,0 +1,41 @@
1
+ # @ghostly-ui/cli
2
+
3
+ CLI for [Ghostly](https://github.com/AdanSerrano/ghostly) — set up your project, generate loading states, diagnose issues.
4
+
5
+ ## Commands
6
+
7
+ ### `ghostly init`
8
+
9
+ Interactive setup wizard. Installs packages, adds CSS import, configures GhostlyProvider and Tailwind plugin.
10
+
11
+ ```bash
12
+ npx @ghostly-ui/cli init
13
+ ```
14
+
15
+ ### `ghostly add loading [path]`
16
+
17
+ Generates `loading.tsx` files for Next.js App Router routes. Scans your `page.tsx` files, detects components, and creates skeleton loading states automatically.
18
+
19
+ ```bash
20
+ # Interactive — scans all routes, lets you pick
21
+ npx @ghostly-ui/cli add loading
22
+
23
+ # Specific route
24
+ npx @ghostly-ui/cli add loading app/dashboard
25
+ ```
26
+
27
+ ### `ghostly doctor`
28
+
29
+ Health check for your Ghostly setup. Verifies packages, CSS import, provider, React version, loading.tsx coverage, and more.
30
+
31
+ ```bash
32
+ npx @ghostly-ui/cli doctor
33
+ ```
34
+
35
+ ## Documentation
36
+
37
+ Full docs at [ghostly.adanulissess.com](https://ghostly.adanulissess.com)
38
+
39
+ ## License
40
+
41
+ MIT
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GHOSTLY,
4
+ error,
5
+ getAppDir,
6
+ log,
7
+ success,
8
+ warn
9
+ } from "./chunk-Q4PDOIND.js";
10
+
11
+ // src/add-loading.ts
12
+ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from "fs";
13
+ import { join, relative } from "path";
14
+ import prompts from "prompts";
15
+ import pc from "picocolors";
16
+ function extractImports(filePath) {
17
+ const content = readFileSync(filePath, "utf-8");
18
+ const imports = [];
19
+ const importRegex = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
20
+ let match;
21
+ while ((match = importRegex.exec(content)) !== null) {
22
+ const importPath = match[3];
23
+ if (!importPath.startsWith(".") && !importPath.startsWith("@/") && !importPath.startsWith("~/")) continue;
24
+ if (importPath.includes("react") || importPath.includes("next")) continue;
25
+ const names = match[1] ? match[1].split(",").map((n) => n.trim().split(" as ")[0].trim()).filter(Boolean) : match[2] ? [match[2]] : [];
26
+ for (const name of names) {
27
+ if (/^[A-Z]/.test(name)) {
28
+ imports.push({ name, path: importPath, isList: false });
29
+ }
30
+ }
31
+ }
32
+ const mapPattern = /(\w+)(?:\.data)?(?:\?)?\.\w*map\s*\(\s*(?:\(?\w+\)?\s*=>\s*)?<(\w+)/g;
33
+ while ((match = mapPattern.exec(content)) !== null) {
34
+ const component = match[2];
35
+ const existing = imports.find((i) => i.name === component);
36
+ if (existing) existing.isList = true;
37
+ }
38
+ return imports;
39
+ }
40
+ function generateLoadingFile(components, routePath) {
41
+ const lines = [];
42
+ const ghostlyImports = ["Ghostly"];
43
+ const hasLists = components.some((c) => c.isList);
44
+ if (hasLists) ghostlyImports.push("GhostlyList");
45
+ lines.push(`import { ${ghostlyImports.join(", ")} } from '@ghostly-ui/react'`);
46
+ for (const comp of components) {
47
+ lines.push(`import { ${comp.name} } from '${comp.path}'`);
48
+ }
49
+ lines.push("");
50
+ lines.push("export default function Loading() {");
51
+ lines.push(" return (");
52
+ if (components.length === 1 && !components[0].isList) {
53
+ lines.push(" <Ghostly loading={true}>");
54
+ lines.push(` <${components[0].name} />`);
55
+ lines.push(" </Ghostly>");
56
+ } else if (components.length === 1 && components[0].isList) {
57
+ lines.push(` <GhostlyList loading={true} count={6} item={<${components[0].name} />}>`);
58
+ lines.push(" <></>");
59
+ lines.push(" </GhostlyList>");
60
+ } else {
61
+ lines.push(' <div className="space-y-6">');
62
+ for (const comp of components) {
63
+ if (comp.isList) {
64
+ lines.push(` <GhostlyList loading={true} count={4} item={<${comp.name} />}>`);
65
+ lines.push(" <></>");
66
+ lines.push(" </GhostlyList>");
67
+ } else {
68
+ lines.push(" <Ghostly loading={true}>");
69
+ lines.push(` <${comp.name} />`);
70
+ lines.push(" </Ghostly>");
71
+ }
72
+ }
73
+ lines.push(" </div>");
74
+ }
75
+ lines.push(" )");
76
+ lines.push("}");
77
+ lines.push("");
78
+ return lines.join("\n");
79
+ }
80
+ async function addLoading(routePath) {
81
+ console.log(`
82
+ ${GHOSTLY} ${pc.dim("\u2014 Generate loading.tsx")}
83
+ `);
84
+ const appDir = getAppDir();
85
+ if (!appDir) {
86
+ error("No app/ directory found. This command requires Next.js App Router.");
87
+ process.exit(1);
88
+ }
89
+ if (!routePath) {
90
+ const routes = scanRoutes(appDir);
91
+ const missing = routes.filter((r) => !r.hasLoading);
92
+ if (missing.length === 0) {
93
+ success("All routes already have loading.tsx files!");
94
+ return;
95
+ }
96
+ log(`Found ${pc.bold(String(missing.length))} route(s) without loading.tsx:
97
+ `);
98
+ missing.forEach((r) => log(` ${pc.dim("\u2022")} ${r.path}`));
99
+ console.log();
100
+ const { selected } = await prompts({
101
+ type: "multiselect",
102
+ name: "selected",
103
+ message: "Select routes to generate loading.tsx for:",
104
+ choices: missing.map((r) => ({
105
+ title: r.path,
106
+ value: r.path,
107
+ selected: true
108
+ }))
109
+ });
110
+ if (!selected || selected.length === 0) return;
111
+ for (const route of selected) {
112
+ await generateForRoute(route, appDir);
113
+ }
114
+ } else {
115
+ const fullPath = routePath.startsWith(appDir) ? routePath : join(appDir, routePath);
116
+ await generateForRoute(fullPath, appDir);
117
+ }
118
+ console.log();
119
+ }
120
+ function scanRoutes(dir, routes = []) {
121
+ if (!existsSync(dir)) return routes;
122
+ const entries = readdirSync(dir, { withFileTypes: true });
123
+ const hasPage = entries.some((e) => e.name.match(/^page\.(tsx|jsx|ts|js)$/));
124
+ const hasLoading = entries.some((e) => e.name.match(/^loading\.(tsx|jsx|ts|js)$/));
125
+ if (hasPage) {
126
+ routes.push({ path: dir, hasLoading, hasPage });
127
+ }
128
+ for (const entry of entries) {
129
+ if (entry.isDirectory() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
130
+ scanRoutes(join(dir, entry.name), routes);
131
+ }
132
+ }
133
+ return routes;
134
+ }
135
+ async function generateForRoute(routePath, appDir) {
136
+ const pageFile = findPageFile(routePath);
137
+ if (!pageFile) {
138
+ warn(`No page.tsx found in ${routePath}`);
139
+ return;
140
+ }
141
+ const loadingFile = join(routePath, "loading.tsx");
142
+ if (existsSync(loadingFile)) {
143
+ warn(`${relative(".", loadingFile)} already exists \u2014 skipping`);
144
+ return;
145
+ }
146
+ const components = extractImports(pageFile);
147
+ if (components.length === 0) {
148
+ warn(`No components found in ${relative(".", pageFile)}`);
149
+ const { manual } = await prompts({
150
+ type: "text",
151
+ name: "manual",
152
+ message: "Enter component name(s) to wrap (comma-separated):"
153
+ });
154
+ if (manual) {
155
+ const names = manual.split(",").map((n) => n.trim()).filter(Boolean);
156
+ for (const name of names) {
157
+ components.push({ name, path: `@/components/${name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`, isList: false });
158
+ }
159
+ }
160
+ }
161
+ if (components.length === 0) return;
162
+ const { selected } = await prompts({
163
+ type: "multiselect",
164
+ name: "selected",
165
+ message: `Components in ${relative(".", pageFile)}:`,
166
+ choices: components.map((c) => ({
167
+ title: `${c.name}${c.isList ? pc.dim(" (list)") : ""}`,
168
+ value: c.name,
169
+ selected: true
170
+ }))
171
+ });
172
+ if (!selected || selected.length === 0) return;
173
+ const finalComponents = components.filter((c) => selected.includes(c.name));
174
+ const content = generateLoadingFile(finalComponents, routePath);
175
+ if (!existsSync(routePath)) {
176
+ mkdirSync(routePath, { recursive: true });
177
+ }
178
+ writeFileSync(loadingFile, content);
179
+ success(`Created ${pc.bold(relative(".", loadingFile))}`);
180
+ log(pc.dim(content));
181
+ }
182
+ function findPageFile(dir) {
183
+ for (const ext of ["tsx", "jsx", "ts", "js"]) {
184
+ const f = join(dir, `page.${ext}`);
185
+ if (existsSync(f)) return f;
186
+ }
187
+ return null;
188
+ }
189
+ export {
190
+ addLoading
191
+ };
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/utils.ts
10
+ import { existsSync, readFileSync } from "fs";
11
+ import { join } from "path";
12
+ import pc from "picocolors";
13
+ var GHOSTLY = pc.bold(pc.cyan("ghostly"));
14
+ function log(msg) {
15
+ console.log(` ${msg}`);
16
+ }
17
+ function success(msg) {
18
+ console.log(` ${pc.green("\u2713")} ${msg}`);
19
+ }
20
+ function warn(msg) {
21
+ console.log(` ${pc.yellow("!")} ${msg}`);
22
+ }
23
+ function error(msg) {
24
+ console.log(` ${pc.red("\u2717")} ${msg}`);
25
+ }
26
+ function step(n, msg) {
27
+ console.log(`
28
+ ${pc.cyan(`[${n}]`)} ${msg}`);
29
+ }
30
+ function detectPackageManager() {
31
+ const cwd = process.cwd();
32
+ if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) return "bun";
33
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
34
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
35
+ return "npm";
36
+ }
37
+ function installCmd(pm, ...pkgs) {
38
+ const cmd = pm === "npm" ? "npm install" : `${pm} add`;
39
+ return `${cmd} ${pkgs.join(" ")}`;
40
+ }
41
+ function fileContains(path, text) {
42
+ if (!existsSync(path)) return false;
43
+ return readFileSync(path, "utf-8").includes(text);
44
+ }
45
+ function findFile(...candidates) {
46
+ for (const c of candidates) {
47
+ if (existsSync(c)) return c;
48
+ }
49
+ return null;
50
+ }
51
+ function readJson(path) {
52
+ if (!existsSync(path)) return null;
53
+ try {
54
+ return JSON.parse(readFileSync(path, "utf-8"));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function isNextJs() {
60
+ const pkg = readJson("package.json");
61
+ if (!pkg) return false;
62
+ const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
63
+ return "next" in deps;
64
+ }
65
+ function getAppDir() {
66
+ if (existsSync("src/app")) return "src/app";
67
+ if (existsSync("app")) return "app";
68
+ return null;
69
+ }
70
+
71
+ export {
72
+ __require,
73
+ GHOSTLY,
74
+ log,
75
+ success,
76
+ warn,
77
+ error,
78
+ step,
79
+ detectPackageManager,
80
+ installCmd,
81
+ fileContains,
82
+ findFile,
83
+ readJson,
84
+ isNextJs,
85
+ getAppDir
86
+ };
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GHOSTLY,
4
+ __require,
5
+ error,
6
+ fileContains,
7
+ findFile,
8
+ getAppDir,
9
+ isNextJs,
10
+ readJson,
11
+ success,
12
+ warn
13
+ } from "./chunk-Q4PDOIND.js";
14
+
15
+ // src/doctor.ts
16
+ import { existsSync } from "fs";
17
+ import pc from "picocolors";
18
+ async function doctor() {
19
+ console.log(`
20
+ ${GHOSTLY} ${pc.dim("\u2014 Health check")}
21
+ `);
22
+ const checks = [];
23
+ const pkg = readJson("package.json");
24
+ checks.push({
25
+ name: "package.json",
26
+ ok: !!pkg,
27
+ message: pkg ? "Found" : "No package.json \u2014 run from project root"
28
+ });
29
+ if (!pkg) {
30
+ printResults(checks);
31
+ return;
32
+ }
33
+ const deps = {
34
+ ...pkg.dependencies || {},
35
+ ...pkg.devDependencies || {}
36
+ };
37
+ const hasCore = "@ghostly-ui/core" in deps;
38
+ checks.push({
39
+ name: "@ghostly-ui/core",
40
+ ok: hasCore,
41
+ message: hasCore ? `Installed (${deps["@ghostly-ui/core"]})` : "Not installed \u2014 run: npx @ghostly-ui/cli init"
42
+ });
43
+ const hasReact = "@ghostly-ui/react" in deps;
44
+ checks.push({
45
+ name: "@ghostly-ui/react",
46
+ ok: hasReact,
47
+ message: hasReact ? `Installed (${deps["@ghostly-ui/react"]})` : "Not installed \u2014 run: npx @ghostly-ui/cli init"
48
+ });
49
+ const cssFile = findFile(
50
+ "src/app/globals.css",
51
+ "app/globals.css",
52
+ "src/styles/globals.css",
53
+ "styles/globals.css",
54
+ "src/index.css",
55
+ "index.css"
56
+ );
57
+ const cssImported = cssFile ? fileContains(cssFile, "@ghostly-ui/core/css") : false;
58
+ checks.push({
59
+ name: "CSS import",
60
+ ok: cssImported,
61
+ message: cssImported ? `Found in ${cssFile}` : cssFile ? `Missing in ${cssFile} \u2014 add: @import '@ghostly-ui/core/css'` : "No globals.css found"
62
+ });
63
+ const reactVersion = deps["react"] || "";
64
+ const reactOk = reactVersion && parseInt(reactVersion.replace(/[^0-9]/g, "")) >= 18;
65
+ checks.push({
66
+ name: "React version",
67
+ ok: !!reactOk,
68
+ message: reactVersion ? `${reactVersion} ${reactOk ? "" : "(needs >=18)"}` : "Not found"
69
+ });
70
+ const nextjs = isNextJs();
71
+ if (nextjs) {
72
+ checks.push({ name: "Framework", ok: true, message: "Next.js detected" });
73
+ const appDir = getAppDir();
74
+ checks.push({
75
+ name: "App Router",
76
+ ok: !!appDir,
77
+ message: appDir ? `Found at ${appDir}/` : "Not found (Pages Router?)"
78
+ });
79
+ const layoutFile = findFile("src/app/layout.tsx", "app/layout.tsx");
80
+ const hasProvider = layoutFile ? fileContains(layoutFile, "GhostlyProvider") : false;
81
+ checks.push({
82
+ name: "GhostlyProvider",
83
+ ok: hasProvider,
84
+ message: hasProvider ? `Found in ${layoutFile}` : "Optional \u2014 add to layout.tsx for global defaults"
85
+ });
86
+ if (appDir) {
87
+ const { total, withLoading } = countRoutes(appDir);
88
+ checks.push({
89
+ name: "loading.tsx coverage",
90
+ ok: withLoading === total,
91
+ message: `${withLoading}/${total} routes have loading.tsx${withLoading < total ? ` \u2014 run: npx @ghostly-ui/cli add loading` : ""}`
92
+ });
93
+ }
94
+ }
95
+ const hasTailwind = "tailwindcss" in deps;
96
+ if (hasTailwind) {
97
+ const twConfig = findFile("tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs");
98
+ const hasPlugin = twConfig ? fileContains(twConfig, "@ghostly-ui/core/tailwind") : false;
99
+ checks.push({
100
+ name: "Tailwind plugin",
101
+ ok: hasPlugin,
102
+ message: hasPlugin ? `Found in ${twConfig}` : "Optional \u2014 add for ghostly-* utility classes"
103
+ });
104
+ }
105
+ const coreExists = existsSync("node_modules/@ghostly-ui/core/dist/ghostly.css");
106
+ checks.push({
107
+ name: "Built CSS",
108
+ ok: coreExists,
109
+ message: coreExists ? "ghostly.css found in node_modules" : "CSS file missing \u2014 try reinstalling"
110
+ });
111
+ printResults(checks);
112
+ }
113
+ function printResults(checks) {
114
+ const passed = checks.filter((c) => c.ok).length;
115
+ const total = checks.length;
116
+ for (const check of checks) {
117
+ if (check.ok) {
118
+ success(`${check.name}: ${pc.dim(check.message)}`);
119
+ } else {
120
+ const fn = check.message.includes("Optional") ? warn : error;
121
+ fn(`${check.name}: ${check.message}`);
122
+ }
123
+ }
124
+ console.log();
125
+ if (passed === total) {
126
+ console.log(` ${pc.green(pc.bold(`All ${total} checks passed!`))} Ghostly is healthy.
127
+ `);
128
+ } else {
129
+ console.log(` ${pc.yellow(`${passed}/${total} checks passed.`)} Fix the issues above.
130
+ `);
131
+ }
132
+ }
133
+ function countRoutes(dir) {
134
+ let total = 0;
135
+ let withLoading = 0;
136
+ const { readdirSync } = __require("fs");
137
+ const { join } = __require("path");
138
+ function scan(d) {
139
+ if (!existsSync(d)) return;
140
+ const entries = readdirSync(d, { withFileTypes: true });
141
+ const hasPage = entries.some((e) => e.name.match(/^page\.(tsx|jsx|ts|js)$/));
142
+ const hasLoading = entries.some((e) => e.name.match(/^loading\.(tsx|jsx|ts|js)$/));
143
+ if (hasPage) {
144
+ total++;
145
+ if (hasLoading) withLoading++;
146
+ }
147
+ for (const entry of entries) {
148
+ if (entry.isDirectory() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
149
+ scan(join(d, entry.name));
150
+ }
151
+ }
152
+ }
153
+ scan(dir);
154
+ return { total, withLoading };
155
+ }
156
+ export {
157
+ doctor
158
+ };
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import pc from "picocolors";
5
+ var GHOSTLY = pc.bold(pc.cyan("ghostly"));
6
+ var VERSION = "0.2.4";
7
+ var args = process.argv.slice(2);
8
+ var command = args[0];
9
+ async function main() {
10
+ switch (command) {
11
+ case "init": {
12
+ const { init } = await import("./init-JZMB5KKV.js");
13
+ await init();
14
+ break;
15
+ }
16
+ case "add": {
17
+ const subcommand = args[1];
18
+ if (subcommand === "loading") {
19
+ const { addLoading } = await import("./add-loading-BOTTW5GC.js");
20
+ await addLoading(args[2]);
21
+ } else {
22
+ console.log(`
23
+ ${GHOSTLY} Unknown: ${pc.red(`add ${subcommand || "?"}`)}
24
+ `);
25
+ console.log(` Available:`);
26
+ console.log(` ${pc.cyan("ghostly add loading")} ${pc.dim("[path]")} Generate loading.tsx files`);
27
+ console.log();
28
+ }
29
+ break;
30
+ }
31
+ case "doctor": {
32
+ const { doctor } = await import("./doctor-JXBQFCU2.js");
33
+ await doctor();
34
+ break;
35
+ }
36
+ case "--version":
37
+ case "-v": {
38
+ console.log(VERSION);
39
+ break;
40
+ }
41
+ case "--help":
42
+ case "-h":
43
+ case void 0: {
44
+ console.log(`
45
+ ${GHOSTLY} ${pc.dim(`v${VERSION}`)} \u2014 Zero-config skeleton loaders
46
+
47
+ ${pc.bold("Commands:")}
48
+
49
+ ${pc.cyan("ghostly init")} Set up Ghostly in your project
50
+ ${pc.cyan("ghostly add loading")} ${pc.dim("[path]")} Generate loading.tsx for Next.js routes
51
+ ${pc.cyan("ghostly doctor")} Check your Ghostly setup
52
+
53
+ ${pc.bold("Examples:")}
54
+
55
+ ${pc.dim("$")} npx @ghostly-ui/cli init
56
+ ${pc.dim("$")} npx @ghostly-ui/cli add loading app/dashboard
57
+ ${pc.dim("$")} npx @ghostly-ui/cli add loading ${pc.dim("(interactive \u2014 scans all routes)")}
58
+ ${pc.dim("$")} npx @ghostly-ui/cli doctor
59
+
60
+ ${pc.dim("Docs: https://ghostly.adanulissess.com")}
61
+ `);
62
+ break;
63
+ }
64
+ default: {
65
+ console.log(`
66
+ ${GHOSTLY} Unknown command: ${pc.red(command)}`);
67
+ console.log(` Run ${pc.cyan("ghostly --help")} for available commands.
68
+ `);
69
+ process.exit(1);
70
+ }
71
+ }
72
+ }
73
+ main().catch((err) => {
74
+ console.error(pc.red("Error:"), err.message);
75
+ process.exit(1);
76
+ });
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GHOSTLY,
4
+ detectPackageManager,
5
+ error,
6
+ fileContains,
7
+ findFile,
8
+ installCmd,
9
+ isNextJs,
10
+ log,
11
+ readJson,
12
+ step,
13
+ success,
14
+ warn
15
+ } from "./chunk-Q4PDOIND.js";
16
+
17
+ // src/init.ts
18
+ import { readFileSync, writeFileSync } from "fs";
19
+ import { execSync } from "child_process";
20
+ import prompts from "prompts";
21
+ import pc from "picocolors";
22
+ async function init() {
23
+ console.log(`
24
+ ${GHOSTLY} ${pc.dim("\u2014 Setup wizard")}
25
+ `);
26
+ const pm = detectPackageManager();
27
+ const nextjs = isNextJs();
28
+ const pkg = readJson("package.json");
29
+ if (!pkg) {
30
+ error("No package.json found. Run this from your project root.");
31
+ process.exit(1);
32
+ }
33
+ log(`Package manager: ${pc.bold(pm)}`);
34
+ log(`Framework: ${pc.bold(nextjs ? "Next.js" : "React")}`);
35
+ const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
36
+ const hasCore = "@ghostly-ui/core" in deps;
37
+ const hasReact = "@ghostly-ui/react" in deps;
38
+ if (hasCore && hasReact) {
39
+ success("Ghostly packages already installed");
40
+ }
41
+ const { features } = await prompts({
42
+ type: "multiselect",
43
+ name: "features",
44
+ message: "What should we set up?",
45
+ choices: [
46
+ { title: "Install packages", value: "install", selected: !hasCore || !hasReact },
47
+ { title: "Import CSS in globals.css", value: "css", selected: true },
48
+ { title: "Add GhostlyProvider to layout", value: "provider", selected: true },
49
+ { title: "Add Tailwind plugin", value: "tailwind", selected: "tailwindcss" in deps }
50
+ ]
51
+ });
52
+ if (!features || features.length === 0) {
53
+ log(pc.dim("Nothing selected. Exiting."));
54
+ return;
55
+ }
56
+ let stepN = 0;
57
+ if (features.includes("install")) {
58
+ stepN++;
59
+ step(stepN, "Installing packages...");
60
+ const cmd = installCmd(pm, "@ghostly-ui/core", "@ghostly-ui/react");
61
+ log(pc.dim(`$ ${cmd}`));
62
+ try {
63
+ execSync(cmd, { stdio: "inherit" });
64
+ success("Packages installed");
65
+ } catch {
66
+ error("Failed to install packages. Try manually:");
67
+ log(pc.dim(cmd));
68
+ }
69
+ }
70
+ if (features.includes("css")) {
71
+ stepN++;
72
+ step(stepN, "Adding CSS import...");
73
+ const cssFile = findFile(
74
+ "src/app/globals.css",
75
+ "app/globals.css",
76
+ "src/styles/globals.css",
77
+ "styles/globals.css",
78
+ "src/index.css",
79
+ "index.css"
80
+ );
81
+ if (!cssFile) {
82
+ warn("Could not find globals.css. Add this manually:");
83
+ log(pc.dim(" @import '@ghostly-ui/core/css';"));
84
+ } else if (fileContains(cssFile, "@ghostly-ui/core/css")) {
85
+ success(`CSS already imported in ${cssFile}`);
86
+ } else {
87
+ const content = readFileSync(cssFile, "utf-8");
88
+ const lastImport = content.lastIndexOf("@import");
89
+ if (lastImport >= 0) {
90
+ const lineEnd = content.indexOf("\n", lastImport);
91
+ const before = content.substring(0, lineEnd + 1);
92
+ const after = content.substring(lineEnd + 1);
93
+ writeFileSync(cssFile, `${before}@import '@ghostly-ui/core/css';
94
+ ${after}`);
95
+ } else {
96
+ writeFileSync(cssFile, `@import '@ghostly-ui/core/css';
97
+ ${content}`);
98
+ }
99
+ success(`Added CSS import to ${pc.bold(cssFile)}`);
100
+ }
101
+ }
102
+ if (features.includes("provider")) {
103
+ stepN++;
104
+ step(stepN, "Adding GhostlyProvider...");
105
+ const layoutFile = findFile(
106
+ "src/app/layout.tsx",
107
+ "app/layout.tsx",
108
+ "src/app/layout.jsx",
109
+ "app/layout.jsx"
110
+ );
111
+ if (!layoutFile) {
112
+ warn("Could not find layout.tsx. Add GhostlyProvider manually:");
113
+ log(pc.dim(" import { GhostlyProvider } from '@ghostly-ui/react'"));
114
+ log(pc.dim(" <GhostlyProvider>{children}</GhostlyProvider>"));
115
+ } else if (fileContains(layoutFile, "GhostlyProvider")) {
116
+ success(`GhostlyProvider already in ${layoutFile}`);
117
+ } else {
118
+ const content = readFileSync(layoutFile, "utf-8");
119
+ const importLine = "import { GhostlyProvider } from '@ghostly-ui/react'\n";
120
+ const wrapped = content.replace(
121
+ /^/,
122
+ importLine
123
+ ).replace(
124
+ /\{children\}/,
125
+ "<GhostlyProvider>{children}</GhostlyProvider>"
126
+ );
127
+ writeFileSync(layoutFile, wrapped);
128
+ success(`Added GhostlyProvider to ${pc.bold(layoutFile)}`);
129
+ }
130
+ }
131
+ if (features.includes("tailwind")) {
132
+ stepN++;
133
+ step(stepN, "Adding Tailwind plugin...");
134
+ const twConfig = findFile(
135
+ "tailwind.config.ts",
136
+ "tailwind.config.js",
137
+ "tailwind.config.mjs"
138
+ );
139
+ if (!twConfig) {
140
+ warn("Could not find tailwind.config. Add manually:");
141
+ log(pc.dim(" import ghostly from '@ghostly-ui/core/tailwind'"));
142
+ log(pc.dim(" plugins: [ghostly]"));
143
+ } else if (fileContains(twConfig, "@ghostly-ui/core/tailwind")) {
144
+ success(`Tailwind plugin already in ${twConfig}`);
145
+ } else {
146
+ warn(`Add to ${pc.bold(twConfig)} manually:`);
147
+ log(pc.dim(" import ghostly from '@ghostly-ui/core/tailwind'"));
148
+ log(pc.dim(" plugins: [ghostly]"));
149
+ log(pc.dim(" (Auto-editing Tailwind configs is risky \u2014 too many formats)"));
150
+ }
151
+ }
152
+ console.log(`
153
+ ${pc.green("Done!")} Ghostly is ready.
154
+ `);
155
+ log(`Next steps:`);
156
+ log(` ${pc.dim("1.")} Wrap a component: ${pc.cyan("<Ghostly loading={isLoading}><Card /></Ghostly>")}`);
157
+ log(` ${pc.dim("2.")} Generate loading.tsx: ${pc.cyan("npx @ghostly-ui/cli add loading app/dashboard")}`);
158
+ log(` ${pc.dim("3.")} Read the docs: ${pc.cyan("https://ghostly.adanulissess.com/docs")}`);
159
+ console.log();
160
+ }
161
+ export {
162
+ init
163
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@ghostly-ui/cli",
3
+ "version": "0.2.4",
4
+ "description": "CLI for Ghostly — init, generate loading.tsx, diagnose issues",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "ghostly": "./dist/index.js"
9
+ },
10
+ "files": ["dist", "README.md"],
11
+ "scripts": {
12
+ "build": "tsup",
13
+ "dev": "tsup --watch"
14
+ },
15
+ "dependencies": {
16
+ "prompts": "^2.4.2",
17
+ "picocolors": "^1.1.1"
18
+ },
19
+ "devDependencies": {
20
+ "@types/prompts": "^2.4.9",
21
+ "@types/node": "^22.19.15",
22
+ "tsup": "^8.5.0",
23
+ "typescript": "^5.9.3"
24
+ },
25
+ "keywords": ["ghostly", "skeleton", "cli", "loading", "react", "nextjs"],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/AdanSerrano/ghostly",
29
+ "directory": "packages/cli"
30
+ },
31
+ "homepage": "https://ghostly.adanulissess.com"
32
+ }