stackpatch 1.1.2 → 1.1.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 +123 -114
- package/bin/stackpatch.ts +2 -2441
- package/boilerplate/auth/app/auth/login/page.tsx +50 -24
- package/boilerplate/auth/app/auth/signup/page.tsx +69 -56
- package/boilerplate/auth/app/stackpatch/page.tsx +269 -0
- package/boilerplate/auth/components/auth-wrapper.tsx +61 -0
- package/package.json +4 -2
- package/src/auth/generator.ts +569 -0
- package/src/auth/index.ts +372 -0
- package/src/auth/setup.ts +293 -0
- package/src/commands/add.ts +112 -0
- package/src/commands/create.ts +128 -0
- package/src/commands/revert.ts +389 -0
- package/src/config.ts +52 -0
- package/src/fileOps/copy.ts +224 -0
- package/src/fileOps/layout.ts +304 -0
- package/src/fileOps/protected.ts +67 -0
- package/src/index.ts +215 -0
- package/src/manifest.ts +87 -0
- package/src/ui/logo.ts +24 -0
- package/src/ui/progress.ts +82 -0
- package/src/utils/dependencies.ts +114 -0
- package/src/utils/deps-check.ts +45 -0
- package/src/utils/files.ts +58 -0
- package/src/utils/paths.ts +217 -0
- package/src/utils/scanner.ts +109 -0
- package/boilerplate/auth/app/api/auth/[...nextauth]/route.ts +0 -124
- package/boilerplate/auth/app/api/auth/signup/route.ts +0 -45
- package/boilerplate/auth/app/dashboard/page.tsx +0 -82
- package/boilerplate/auth/app/login/page.tsx +0 -136
- package/boilerplate/auth/app/page.tsx +0 -48
- package/boilerplate/auth/components/auth-button.tsx +0 -43
- package/boilerplate/auth/components/auth-navbar.tsx +0 -118
- package/boilerplate/auth/components/protected-route.tsx +0 -74
- package/boilerplate/auth/components/session-provider.tsx +0 -11
- package/boilerplate/auth/middleware.ts +0 -51
package/src/index.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// Try to import dependencies and show helpful error if they're missing
|
|
4
|
+
let chalk: typeof import("chalk").default;
|
|
5
|
+
let inquirer: any;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const chalkModule = await import("chalk");
|
|
9
|
+
const inquirerModule = await import("inquirer");
|
|
10
|
+
chalk = chalkModule.default;
|
|
11
|
+
// Inquirer v13 uses default export
|
|
12
|
+
inquirer = inquirerModule.default || inquirerModule;
|
|
13
|
+
} catch (error: any) {
|
|
14
|
+
if (error?.code === "ENOENT" || error?.message?.includes("Cannot find module")) {
|
|
15
|
+
console.error("\n❌ Missing Dependencies\n");
|
|
16
|
+
console.error("Required dependencies are not installed.");
|
|
17
|
+
console.error("\nTo fix this, run:");
|
|
18
|
+
console.error(" cd packages/cli");
|
|
19
|
+
console.error(" pnpm install");
|
|
20
|
+
console.error("\nOr from the project root:");
|
|
21
|
+
console.error(" pnpm install");
|
|
22
|
+
console.error();
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
import { PATCHES } from "./config.js";
|
|
28
|
+
import { showLogo } from "./ui/logo.js";
|
|
29
|
+
import { createProject } from "./commands/create.js";
|
|
30
|
+
import { addPatch } from "./commands/add.js";
|
|
31
|
+
import { revertPatch } from "./commands/revert.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Main CLI entry point
|
|
35
|
+
*/
|
|
36
|
+
async function main(): Promise<void> {
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const command = args[0];
|
|
39
|
+
const projectName = args[1];
|
|
40
|
+
const skipPrompts = args.includes("--yes") || args.includes("-y");
|
|
41
|
+
|
|
42
|
+
// Show logo on startup
|
|
43
|
+
showLogo();
|
|
44
|
+
|
|
45
|
+
// Handle: bun create stackpatch@latest (no project name)
|
|
46
|
+
// Show welcome and prompt for project name
|
|
47
|
+
if (!command || command.startsWith("-")) {
|
|
48
|
+
const { name } = await inquirer.prompt([
|
|
49
|
+
{
|
|
50
|
+
type: "input",
|
|
51
|
+
name: "name",
|
|
52
|
+
message: chalk.white("Enter your project name or path (relative to current directory)"),
|
|
53
|
+
default: "my-stackpatch-app",
|
|
54
|
+
validate: (input: string) => {
|
|
55
|
+
if (!input.trim()) {
|
|
56
|
+
return "Project name cannot be empty";
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
await createProject(name.trim(), false, skipPrompts); // Don't show welcome again
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle: npx stackpatch revert
|
|
67
|
+
if (command === "revert") {
|
|
68
|
+
await revertPatch();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle: bun create stackpatch@latest my-app
|
|
73
|
+
// When bun runs create, it passes project name as first arg (not "create")
|
|
74
|
+
// Check if first arg looks like a project name (not a known command)
|
|
75
|
+
// Always ask for project name first, even if provided
|
|
76
|
+
if (
|
|
77
|
+
command &&
|
|
78
|
+
!["add", "create", "revert"].includes(command) &&
|
|
79
|
+
!PATCHES[command] &&
|
|
80
|
+
!command.startsWith("-")
|
|
81
|
+
) {
|
|
82
|
+
// Likely called as: bun create stackpatch@latest my-app
|
|
83
|
+
// But we'll ask for project name anyway to be consistent
|
|
84
|
+
await showLogo();
|
|
85
|
+
const { name } = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: "input",
|
|
88
|
+
name: "name",
|
|
89
|
+
message: chalk.white("Enter your project name or path (relative to current directory)"),
|
|
90
|
+
default: command || "my-stackpatch-app", // Use provided name as default
|
|
91
|
+
validate: (input: string) => {
|
|
92
|
+
if (!input.trim()) {
|
|
93
|
+
return "Project name cannot be empty";
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
await createProject(name.trim(), false, skipPrompts); // Welcome already shown
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle: npx stackpatch create my-app
|
|
104
|
+
if (command === "create") {
|
|
105
|
+
if (!projectName) {
|
|
106
|
+
showLogo();
|
|
107
|
+
const { name } = await inquirer.prompt([
|
|
108
|
+
{
|
|
109
|
+
type: "input",
|
|
110
|
+
name: "name",
|
|
111
|
+
message: chalk.white("Enter your project name or path (relative to current directory)"),
|
|
112
|
+
default: "my-stackpatch-app",
|
|
113
|
+
validate: (input: string) => {
|
|
114
|
+
if (!input.trim()) {
|
|
115
|
+
return "Project name cannot be empty";
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
await createProject(name.trim(), false); // Logo already shown
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await createProject(projectName, false, skipPrompts); // Logo already shown
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle: npx stackpatch add auth-ui
|
|
129
|
+
const patchName = args[1];
|
|
130
|
+
if (command === "add" && patchName) {
|
|
131
|
+
await addPatch(patchName);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If no command, show help or interactive mode
|
|
136
|
+
if (!command) {
|
|
137
|
+
await showLogo();
|
|
138
|
+
console.log(chalk.yellow("Usage:"));
|
|
139
|
+
console.log(chalk.white(" ") + chalk.cyan("npm create stackpatch@latest") + chalk.gray(" [project-name]"));
|
|
140
|
+
console.log(chalk.white(" ") + chalk.cyan("npx create-stackpatch@latest") + chalk.gray(" [project-name]"));
|
|
141
|
+
console.log(chalk.white(" ") + chalk.cyan("bunx create-stackpatch@latest") + chalk.gray(" [project-name]"));
|
|
142
|
+
console.log(chalk.white(" ") + chalk.cyan("npx stackpatch create") + chalk.gray(" [project-name]"));
|
|
143
|
+
console.log(chalk.white(" ") + chalk.cyan("npx stackpatch add") + chalk.white(" <patch-name>"));
|
|
144
|
+
console.log(
|
|
145
|
+
chalk.white(" ") + chalk.cyan("npx stackpatch revert") + chalk.gray(" - Revert a patch installation")
|
|
146
|
+
);
|
|
147
|
+
console.log(chalk.white("\nExamples:"));
|
|
148
|
+
console.log(chalk.gray(" npm create stackpatch@latest my-app"));
|
|
149
|
+
console.log(chalk.gray(" npx create-stackpatch@latest my-app"));
|
|
150
|
+
console.log(chalk.gray(" bunx create-stackpatch@latest my-app"));
|
|
151
|
+
console.log(chalk.gray(" npx stackpatch create my-app"));
|
|
152
|
+
console.log(chalk.gray(" npx stackpatch add auth-ui"));
|
|
153
|
+
console.log(chalk.gray("\n"));
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Interactive mode (fallback)
|
|
158
|
+
console.log(chalk.blue.bold("\n🚀 Welcome to StackPatch CLI\n"));
|
|
159
|
+
|
|
160
|
+
let selectedPatch: string | null = null;
|
|
161
|
+
let goBack = false;
|
|
162
|
+
|
|
163
|
+
// 1️⃣ Select patch with back option
|
|
164
|
+
do {
|
|
165
|
+
const response = await inquirer.prompt([
|
|
166
|
+
{
|
|
167
|
+
type: "list",
|
|
168
|
+
name: "patch",
|
|
169
|
+
message: "Which patch do you want to add?",
|
|
170
|
+
choices: [
|
|
171
|
+
...Object.keys(PATCHES)
|
|
172
|
+
.filter((p) => p !== "auth-ui") // Don't show duplicate
|
|
173
|
+
.map((p) => ({ name: p, value: p })),
|
|
174
|
+
new inquirer.Separator(),
|
|
175
|
+
{
|
|
176
|
+
name: chalk.gray("← Go back / Cancel"),
|
|
177
|
+
value: "back",
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
if (response.patch === "back") {
|
|
184
|
+
goBack = true;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
selectedPatch = response.patch;
|
|
189
|
+
} while (!selectedPatch);
|
|
190
|
+
|
|
191
|
+
if (goBack || !selectedPatch) {
|
|
192
|
+
console.log(chalk.yellow("\n← Cancelled"));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const patch = selectedPatch;
|
|
197
|
+
|
|
198
|
+
// 2️⃣ Enter target Next.js app folder
|
|
199
|
+
const { target } = await inquirer.prompt([
|
|
200
|
+
{
|
|
201
|
+
type: "input",
|
|
202
|
+
name: "target",
|
|
203
|
+
message:
|
|
204
|
+
"Enter the relative path to your Next.js app folder (e.g., ../../apps/stackpatch-frontend):",
|
|
205
|
+
default: process.cwd(),
|
|
206
|
+
},
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
await addPatch(patch, target);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
main().catch((error) => {
|
|
213
|
+
console.error(chalk.red("❌ Error:"), error);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { StackPatchManifest } from "./config.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manifest management for tracking StackPatch installations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get manifest path for a target directory
|
|
11
|
+
*/
|
|
12
|
+
export function getManifestPath(target: string): string {
|
|
13
|
+
return path.join(target, ".stackpatch", "manifest.json");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read manifest if it exists
|
|
18
|
+
*/
|
|
19
|
+
export function readManifest(target: string): StackPatchManifest | null {
|
|
20
|
+
const manifestPath = getManifestPath(target);
|
|
21
|
+
if (!fs.existsSync(manifestPath)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const content = fs.readFileSync(manifestPath, "utf-8");
|
|
26
|
+
return JSON.parse(content) as StackPatchManifest;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Write manifest
|
|
34
|
+
*/
|
|
35
|
+
export function writeManifest(target: string, manifest: StackPatchManifest): void {
|
|
36
|
+
const manifestDir = path.join(target, ".stackpatch");
|
|
37
|
+
const manifestPath = getManifestPath(target);
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(manifestDir)) {
|
|
40
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Backup a file before modifying it
|
|
48
|
+
*/
|
|
49
|
+
export function backupFile(filePath: string, target: string): string | null {
|
|
50
|
+
if (!fs.existsSync(filePath)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const backupDir = path.join(target, ".stackpatch", "backups");
|
|
55
|
+
if (!fs.existsSync(backupDir)) {
|
|
56
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const relativePath = path.relative(target, filePath);
|
|
60
|
+
const backupPath = path.join(backupDir, relativePath.replace(/\//g, "_").replace(/\\/g, "_"));
|
|
61
|
+
|
|
62
|
+
// Create directory structure in backup
|
|
63
|
+
const backupFileDir = path.dirname(backupPath);
|
|
64
|
+
if (!fs.existsSync(backupFileDir)) {
|
|
65
|
+
fs.mkdirSync(backupFileDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fs.copyFileSync(filePath, backupPath);
|
|
69
|
+
return backupPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Restore a file from backup
|
|
74
|
+
*/
|
|
75
|
+
export function restoreFile(backupPath: string, originalPath: string): boolean {
|
|
76
|
+
if (!fs.existsSync(backupPath)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const originalDir = path.dirname(originalPath);
|
|
81
|
+
if (!fs.existsSync(originalDir)) {
|
|
82
|
+
fs.mkdirSync(originalDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fs.copyFileSync(backupPath, originalPath);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
package/src/ui/logo.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display StackPatch logo
|
|
5
|
+
*/
|
|
6
|
+
export function showLogo(): void {
|
|
7
|
+
console.log("\n");
|
|
8
|
+
|
|
9
|
+
// StackPatch logo ASCII art
|
|
10
|
+
const logo = [
|
|
11
|
+
chalk.magentaBright(" _________ __ __ __________ __ .__"),
|
|
12
|
+
chalk.magentaBright(" / _____// |______ ____ | | __ \\\\______ \\_____ _/ |_ ____ | |__"),
|
|
13
|
+
chalk.magentaBright(" \\_____ \\\\ __\\__ \\ _/ ___\\| |/ / | ___/\\__ \\\\ __\\/ ___\\| | \\"),
|
|
14
|
+
chalk.magentaBright(" / \\| | / __ \\\\ \\___| < | | / __ \\| | \\ \\___| Y \\"),
|
|
15
|
+
chalk.magentaBright("/_______ /|__| (____ /\\___ >__|_ \\ |____| (____ /__| \\___ >___| /"),
|
|
16
|
+
chalk.magentaBright(" \\/ \\/ \\/ \\/ \\/ \\/ \\/"),
|
|
17
|
+
"",
|
|
18
|
+
chalk.white(" Composable frontend features for modern React & Next.js"),
|
|
19
|
+
chalk.gray(" Add authentication, UI components, and more with zero configuration"),
|
|
20
|
+
"",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
logo.forEach((line) => console.log(line));
|
|
24
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Progress tracker with checkmarks
|
|
5
|
+
*/
|
|
6
|
+
export class ProgressTracker {
|
|
7
|
+
private steps: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
status: "pending" | "processing" | "completed" | "failed";
|
|
10
|
+
interval?: NodeJS.Timeout;
|
|
11
|
+
}> = [];
|
|
12
|
+
|
|
13
|
+
addStep(name: string): void {
|
|
14
|
+
this.steps.push({ name, status: "pending" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
startStep(index: number): void {
|
|
18
|
+
if (index >= 0 && index < this.steps.length) {
|
|
19
|
+
this.steps[index].status = "processing";
|
|
20
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
21
|
+
let frameIndex = 0;
|
|
22
|
+
const step = this.steps[index];
|
|
23
|
+
|
|
24
|
+
const interval = setInterval(() => {
|
|
25
|
+
process.stdout.write(`\r${chalk.yellow(frames[frameIndex])} ${step.name}`);
|
|
26
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
27
|
+
}, 100);
|
|
28
|
+
|
|
29
|
+
this.steps[index].interval = interval;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
completeStep(index: number): void {
|
|
34
|
+
if (index >= 0 && index < this.steps.length) {
|
|
35
|
+
if (this.steps[index].interval) {
|
|
36
|
+
clearInterval(this.steps[index].interval);
|
|
37
|
+
this.steps[index].interval = undefined;
|
|
38
|
+
}
|
|
39
|
+
process.stdout.write(`\r${chalk.green("✓")} ${this.steps[index].name}\n`);
|
|
40
|
+
this.steps[index].status = "completed";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
failStep(index: number): void {
|
|
45
|
+
if (index >= 0 && index < this.steps.length) {
|
|
46
|
+
if (this.steps[index].interval) {
|
|
47
|
+
clearInterval(this.steps[index].interval);
|
|
48
|
+
this.steps[index].interval = undefined;
|
|
49
|
+
}
|
|
50
|
+
process.stdout.write(`\r${chalk.red("✗")} ${this.steps[index].name}\n`);
|
|
51
|
+
this.steps[index].status = "failed";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getSteps() {
|
|
56
|
+
return this.steps;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Helper function with spinner and checkmark
|
|
62
|
+
*/
|
|
63
|
+
export async function withSpinner<T>(text: string, fn: () => Promise<T> | T): Promise<T> {
|
|
64
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
65
|
+
let frameIndex = 0;
|
|
66
|
+
|
|
67
|
+
const interval = setInterval(() => {
|
|
68
|
+
process.stdout.write(`\r${chalk.yellow(frames[frameIndex])} ${text}`);
|
|
69
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
70
|
+
}, 100);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await fn();
|
|
74
|
+
clearInterval(interval);
|
|
75
|
+
process.stdout.write(`\r${chalk.green("✓")} ${text}\n`);
|
|
76
|
+
return result;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
clearInterval(interval);
|
|
79
|
+
process.stdout.write(`\r${chalk.red("✗")} ${text}\n`);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Utility functions for managing dependencies
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if dependency exists in package.json
|
|
11
|
+
*/
|
|
12
|
+
export function hasDependency(target: string, depName: string): boolean {
|
|
13
|
+
const packageJsonPath = path.join(target, "package.json");
|
|
14
|
+
if (!fs.existsSync(packageJsonPath)) return false;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
18
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
19
|
+
return !!deps[depName];
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Install dependencies (only missing ones)
|
|
27
|
+
*/
|
|
28
|
+
export function installDependencies(target: string, deps: string[]): void {
|
|
29
|
+
if (deps.length === 0) return;
|
|
30
|
+
|
|
31
|
+
// Check if target directory and package.json exist
|
|
32
|
+
const packageJsonPath = path.join(target, "package.json");
|
|
33
|
+
if (!fs.existsSync(target)) {
|
|
34
|
+
throw new Error(`Target directory does not exist: ${target}`);
|
|
35
|
+
}
|
|
36
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
37
|
+
throw new Error(`package.json not found in ${target}. Make sure you're in a valid project directory.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const missingDeps = deps.filter((dep) => !hasDependency(target, dep));
|
|
41
|
+
|
|
42
|
+
if (missingDeps.length === 0) {
|
|
43
|
+
return; // Already installed, spinner will show completion
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Use non-interactive flags to prevent credential prompts
|
|
47
|
+
const result = spawnSync("pnpm", ["add", ...missingDeps], {
|
|
48
|
+
cwd: target,
|
|
49
|
+
stdio: "pipe",
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
// Prevent Git credential prompts
|
|
53
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
54
|
+
GIT_ASKPASS: "",
|
|
55
|
+
// Prevent npm/pnpm credential prompts
|
|
56
|
+
NPM_CONFIG_PROGRESS: "false",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (result.status !== 0) {
|
|
61
|
+
const errorOutput = result.stderr?.toString() || result.stdout?.toString() || "Unknown error";
|
|
62
|
+
// Extract the actual error message if available
|
|
63
|
+
const errorLines = errorOutput.split("\n").filter((line: string) =>
|
|
64
|
+
line.trim() && (line.includes("error") || line.includes("Error") || line.includes("ERR"))
|
|
65
|
+
);
|
|
66
|
+
const errorMessage = errorLines.length > 0 ? errorLines.join("\n") : errorOutput;
|
|
67
|
+
throw new Error(`Failed to install dependencies: ${missingDeps.join(", ")}\n${errorMessage}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Remove dependencies from package.json
|
|
73
|
+
*/
|
|
74
|
+
export function removeDependencies(target: string, deps: string[]): boolean {
|
|
75
|
+
if (deps.length === 0) return true;
|
|
76
|
+
|
|
77
|
+
const packageJsonPath = path.join(target, "package.json");
|
|
78
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
84
|
+
let modified = false;
|
|
85
|
+
|
|
86
|
+
// Remove from dependencies
|
|
87
|
+
if (packageJson.dependencies) {
|
|
88
|
+
for (const dep of deps) {
|
|
89
|
+
if (packageJson.dependencies[dep]) {
|
|
90
|
+
delete packageJson.dependencies[dep];
|
|
91
|
+
modified = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Remove from devDependencies
|
|
97
|
+
if (packageJson.devDependencies) {
|
|
98
|
+
for (const dep of deps) {
|
|
99
|
+
if (packageJson.devDependencies[dep]) {
|
|
100
|
+
delete packageJson.devDependencies[dep];
|
|
101
|
+
modified = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (modified) {
|
|
107
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return modified;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if required dependencies are installed
|
|
6
|
+
*/
|
|
7
|
+
export function checkDependencies(): { missing: string[]; allInstalled: boolean } {
|
|
8
|
+
const requiredDeps = ["inquirer", "chalk", "fs-extra"];
|
|
9
|
+
const missing: string[] = [];
|
|
10
|
+
|
|
11
|
+
// Get the CLI directory
|
|
12
|
+
// @ts-expect-error - Bun-specific API
|
|
13
|
+
const CLI_DIR = import.meta.dir || path.dirname(new URL(import.meta.url).pathname);
|
|
14
|
+
const nodeModulesPath = path.resolve(CLI_DIR, "../node_modules");
|
|
15
|
+
|
|
16
|
+
for (const dep of requiredDeps) {
|
|
17
|
+
const depPath = path.join(nodeModulesPath, dep);
|
|
18
|
+
// Check if it exists (could be a symlink or directory)
|
|
19
|
+
if (!fs.existsSync(depPath)) {
|
|
20
|
+
missing.push(dep);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
missing,
|
|
26
|
+
allInstalled: missing.length === 0,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Show helpful error message if dependencies are missing
|
|
32
|
+
* Uses plain console.error to avoid importing chalk (which might not be installed)
|
|
33
|
+
*/
|
|
34
|
+
export function showDependencyError(missing: string[]): void {
|
|
35
|
+
console.error("\n❌ Missing Dependencies\n");
|
|
36
|
+
console.error("The following dependencies are not installed:");
|
|
37
|
+
missing.forEach((dep) => console.error(` - ${dep}`));
|
|
38
|
+
console.error("\nTo fix this, run:");
|
|
39
|
+
console.error(" cd packages/cli");
|
|
40
|
+
console.error(" pnpm install");
|
|
41
|
+
console.error("\nOr from the project root:");
|
|
42
|
+
console.error(" pnpm install");
|
|
43
|
+
console.error();
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Utility functions for file operations
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Remove empty directories recursively
|
|
10
|
+
*/
|
|
11
|
+
export function removeEmptyDirectories(dirPath: string, rootPath: string): void {
|
|
12
|
+
if (!fs.existsSync(dirPath)) return;
|
|
13
|
+
|
|
14
|
+
// Don't remove the root directory or .stackpatch
|
|
15
|
+
if (dirPath === rootPath || dirPath.includes(".stackpatch")) return;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const entries = fs.readdirSync(dirPath);
|
|
19
|
+
|
|
20
|
+
// Recursively remove empty subdirectories
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = path.join(dirPath, entry);
|
|
23
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
24
|
+
removeEmptyDirectories(fullPath, rootPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if directory is now empty (after removing subdirectories)
|
|
29
|
+
const remainingEntries = fs.readdirSync(dirPath);
|
|
30
|
+
if (remainingEntries.length === 0) {
|
|
31
|
+
fs.rmdirSync(dirPath);
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore errors when removing directories
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find all TypeScript/TSX files in a directory recursively
|
|
40
|
+
*/
|
|
41
|
+
export function findTypeScriptFiles(dir: string, fileList: string[] = []): string[] {
|
|
42
|
+
if (!fs.existsSync(dir)) return fileList;
|
|
43
|
+
|
|
44
|
+
const files = fs.readdirSync(dir);
|
|
45
|
+
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const filePath = path.join(dir, file);
|
|
48
|
+
const stat = fs.statSync(filePath);
|
|
49
|
+
|
|
50
|
+
if (stat.isDirectory()) {
|
|
51
|
+
findTypeScriptFiles(filePath, fileList);
|
|
52
|
+
} else if (file.endsWith(".tsx") || file.endsWith(".ts")) {
|
|
53
|
+
fileList.push(filePath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return fileList;
|
|
58
|
+
}
|