stackkit-cli 0.1.0
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 +119 -0
- package/bin/stackkit.js +2 -0
- package/dist/commands/add.d.ts +9 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +201 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +153 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +7 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +107 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/code-inject.d.ts +15 -0
- package/dist/utils/code-inject.d.ts.map +1 -0
- package/dist/utils/code-inject.js +71 -0
- package/dist/utils/code-inject.js.map +1 -0
- package/dist/utils/detect.d.ts +5 -0
- package/dist/utils/detect.d.ts.map +1 -0
- package/dist/utils/detect.js +79 -0
- package/dist/utils/detect.js.map +1 -0
- package/dist/utils/env-editor.d.ts +11 -0
- package/dist/utils/env-editor.d.ts.map +1 -0
- package/dist/utils/env-editor.js +92 -0
- package/dist/utils/env-editor.js.map +1 -0
- package/dist/utils/files.d.ts +7 -0
- package/dist/utils/files.d.ts.map +1 -0
- package/dist/utils/files.js +51 -0
- package/dist/utils/files.js.map +1 -0
- package/dist/utils/json-editor.d.ts +9 -0
- package/dist/utils/json-editor.d.ts.map +1 -0
- package/dist/utils/json-editor.js +50 -0
- package/dist/utils/json-editor.js.map +1 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +58 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package-manager.d.ts +6 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +79 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/package.json +51 -0
- package/src/commands/add.ts +261 -0
- package/src/commands/init.ts +182 -0
- package/src/commands/list.ts +124 -0
- package/src/index.ts +53 -0
- package/src/types/index.ts +71 -0
- package/src/utils/code-inject.ts +85 -0
- package/src/utils/detect.ts +89 -0
- package/src/utils/env-editor.ts +127 -0
- package/src/utils/files.ts +59 -0
- package/src/utils/json-editor.ts +64 -0
- package/src/utils/logger.ts +62 -0
- package/src/utils/package-manager.ts +85 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ModuleMetadata, TemplateMetadata } from '../types';
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
|
|
7
|
+
interface ListOptions {
|
|
8
|
+
templates?: boolean;
|
|
9
|
+
modules?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function listCommand(options: ListOptions): Promise<void> {
|
|
13
|
+
const showTemplates = !options.modules || options.templates;
|
|
14
|
+
const showModules = !options.templates || options.modules;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// List templates
|
|
18
|
+
if (showTemplates) {
|
|
19
|
+
const templatesDir = path.join(__dirname, '..', '..', '..', '..', 'templates');
|
|
20
|
+
const templates = await getAvailableTemplates(templatesDir);
|
|
21
|
+
|
|
22
|
+
logger.log(chalk.bold('📦 Templates'));
|
|
23
|
+
logger.newLine();
|
|
24
|
+
|
|
25
|
+
if (templates.length === 0) {
|
|
26
|
+
logger.warn(' No templates found');
|
|
27
|
+
} else {
|
|
28
|
+
for (const template of templates) {
|
|
29
|
+
logger.log(chalk.bold(` ${template.displayName}`));
|
|
30
|
+
logger.log(chalk.gray(` ${template.description}`));
|
|
31
|
+
logger.log(chalk.blue(` Command: stackkit init --template ${template.name}`));
|
|
32
|
+
logger.log(chalk.gray(` Features: ${template.features.join(', ')}`));
|
|
33
|
+
logger.newLine();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// List modules
|
|
39
|
+
if (showModules) {
|
|
40
|
+
const modulesDir = path.join(__dirname, '..', '..', '..', '..', 'modules');
|
|
41
|
+
const modules = await getAvailableModules(modulesDir);
|
|
42
|
+
|
|
43
|
+
logger.log(chalk.bold('🔧 Modules'));
|
|
44
|
+
logger.newLine();
|
|
45
|
+
|
|
46
|
+
if (modules.length === 0) {
|
|
47
|
+
logger.warn(' No modules found');
|
|
48
|
+
} else {
|
|
49
|
+
// Group by category
|
|
50
|
+
const grouped = modules.reduce(
|
|
51
|
+
(acc, mod) => {
|
|
52
|
+
if (!acc[mod.category]) {
|
|
53
|
+
acc[mod.category] = [];
|
|
54
|
+
}
|
|
55
|
+
acc[mod.category].push(mod);
|
|
56
|
+
return acc;
|
|
57
|
+
},
|
|
58
|
+
{} as Record<string, ModuleMetadata[]>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
for (const [category, mods] of Object.entries(grouped)) {
|
|
62
|
+
logger.log(chalk.yellow(` ${category.toUpperCase()}:`));
|
|
63
|
+
for (const mod of mods) {
|
|
64
|
+
logger.log(chalk.bold(` ${mod.displayName}`));
|
|
65
|
+
logger.log(chalk.gray(` ${mod.description}`));
|
|
66
|
+
logger.log(chalk.blue(` Command: stackkit add ${mod.name}`));
|
|
67
|
+
logger.log(chalk.gray(` Supports: ${mod.supportedFrameworks.join(', ')}`));
|
|
68
|
+
logger.newLine();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.error(`Failed to list resources: ${(error as Error).message}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function getAvailableTemplates(templatesDir: string): Promise<TemplateMetadata[]> {
|
|
80
|
+
if (!(await fs.pathExists(templatesDir))) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const templateDirs = await fs.readdir(templatesDir);
|
|
85
|
+
const templates: TemplateMetadata[] = [];
|
|
86
|
+
|
|
87
|
+
for (const dir of templateDirs) {
|
|
88
|
+
const metadataPath = path.join(templatesDir, dir, 'template.json');
|
|
89
|
+
if (await fs.pathExists(metadataPath)) {
|
|
90
|
+
const metadata = await fs.readJSON(metadataPath);
|
|
91
|
+
templates.push(metadata);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return templates;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function getAvailableModules(modulesDir: string): Promise<ModuleMetadata[]> {
|
|
99
|
+
if (!(await fs.pathExists(modulesDir))) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const modules: ModuleMetadata[] = [];
|
|
104
|
+
const categories = await fs.readdir(modulesDir);
|
|
105
|
+
|
|
106
|
+
for (const category of categories) {
|
|
107
|
+
const categoryPath = path.join(modulesDir, category);
|
|
108
|
+
const stat = await fs.stat(categoryPath);
|
|
109
|
+
|
|
110
|
+
if (!stat.isDirectory()) continue;
|
|
111
|
+
|
|
112
|
+
const moduleDirs = await fs.readdir(categoryPath);
|
|
113
|
+
|
|
114
|
+
for (const moduleDir of moduleDirs) {
|
|
115
|
+
const metadataPath = path.join(categoryPath, moduleDir, 'module.json');
|
|
116
|
+
if (await fs.pathExists(metadataPath)) {
|
|
117
|
+
const metadata = await fs.readJSON(metadataPath);
|
|
118
|
+
modules.push(metadata);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return modules;
|
|
124
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { addCommand } from './commands/add';
|
|
5
|
+
import { initCommand } from './commands/init';
|
|
6
|
+
import { listCommand } from './commands/list';
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('stackkit')
|
|
12
|
+
.description('Production-ready project generator and module CLI')
|
|
13
|
+
.version('0.1.0');
|
|
14
|
+
|
|
15
|
+
// Init command
|
|
16
|
+
program
|
|
17
|
+
.command('init [project-name]')
|
|
18
|
+
.description('Create a new project from a template')
|
|
19
|
+
.option('-t, --template <template>', 'Template to use')
|
|
20
|
+
.option('--pm <pm>', 'Package manager to use (npm, yarn, pnpm)')
|
|
21
|
+
.option('--no-install', 'Skip installing dependencies')
|
|
22
|
+
.option('--no-git', 'Skip git initialization')
|
|
23
|
+
.option('-y, --yes', 'Skip prompts and use defaults')
|
|
24
|
+
.action(initCommand);
|
|
25
|
+
|
|
26
|
+
// List command
|
|
27
|
+
program
|
|
28
|
+
.command('list')
|
|
29
|
+
.description('List available templates and modules')
|
|
30
|
+
.option('-t, --templates', 'List only templates')
|
|
31
|
+
.option('-m, --modules', 'List only modules')
|
|
32
|
+
.action(listCommand);
|
|
33
|
+
|
|
34
|
+
// Add command
|
|
35
|
+
program
|
|
36
|
+
.command('add <module>')
|
|
37
|
+
.description('Add a module to your existing project')
|
|
38
|
+
.option('--provider <provider>', 'Specific provider/variant to use')
|
|
39
|
+
.option('--force', 'Overwrite existing files')
|
|
40
|
+
.option('--dry-run', 'Show what would be changed without making changes')
|
|
41
|
+
.option('--no-install', 'Skip installing dependencies')
|
|
42
|
+
.action(addCommand);
|
|
43
|
+
|
|
44
|
+
// Error handling
|
|
45
|
+
program.on('command:*', () => {
|
|
46
|
+
console.error(
|
|
47
|
+
chalk.red(`\nInvalid command: ${program.args.join(' ')}\n`)
|
|
48
|
+
);
|
|
49
|
+
console.log(chalk.yellow('Run stackkit --help for a list of available commands.\n'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program.parse();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface TemplateMetadata {
|
|
2
|
+
name: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
description: string;
|
|
5
|
+
tags: string[];
|
|
6
|
+
defaultPackageManager: 'pnpm' | 'npm' | 'yarn';
|
|
7
|
+
features: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ModuleMetadata {
|
|
11
|
+
name: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
description: string;
|
|
14
|
+
category: 'auth' | 'database' | 'ui' | 'other';
|
|
15
|
+
supportedFrameworks: string[];
|
|
16
|
+
dependencies: Record<string, string>;
|
|
17
|
+
devDependencies?: Record<string, string>;
|
|
18
|
+
envVars: EnvVar[];
|
|
19
|
+
patches: ModulePatch[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EnvVar {
|
|
23
|
+
key: string;
|
|
24
|
+
value?: string;
|
|
25
|
+
description: string;
|
|
26
|
+
required: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ModulePatch {
|
|
30
|
+
type: 'create-file' | 'modify-json' | 'append-env' | 'inject-code';
|
|
31
|
+
description: string;
|
|
32
|
+
[key: string]: any;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CreateFilePatch extends ModulePatch {
|
|
36
|
+
type: 'create-file';
|
|
37
|
+
source: string;
|
|
38
|
+
destination: string;
|
|
39
|
+
condition?: {
|
|
40
|
+
router?: 'app' | 'pages';
|
|
41
|
+
language?: 'ts' | 'js';
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ModifyJsonPatch extends ModulePatch {
|
|
46
|
+
type: 'modify-json';
|
|
47
|
+
file: string;
|
|
48
|
+
operations: {
|
|
49
|
+
path: string;
|
|
50
|
+
value: any;
|
|
51
|
+
merge?: boolean;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProjectInfo {
|
|
56
|
+
framework: 'nextjs' | 'unknown';
|
|
57
|
+
router: 'app' | 'pages' | 'unknown';
|
|
58
|
+
language: 'ts' | 'js';
|
|
59
|
+
packageManager: 'npm' | 'yarn' | 'pnpm';
|
|
60
|
+
hasAuth: boolean;
|
|
61
|
+
hasPrisma: boolean;
|
|
62
|
+
rootDir: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CLIOptions {
|
|
66
|
+
force?: boolean;
|
|
67
|
+
dryRun?: boolean;
|
|
68
|
+
yes?: boolean;
|
|
69
|
+
noInstall?: boolean;
|
|
70
|
+
pm?: 'npm' | 'yarn' | 'pnpm';
|
|
71
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const CODE_MARKER_START = (id: string) => `// StackKit:${id}:start`;
|
|
5
|
+
const CODE_MARKER_END = (id: string) => `// StackKit:${id}:end`;
|
|
6
|
+
|
|
7
|
+
export interface CodeInjection {
|
|
8
|
+
id: string;
|
|
9
|
+
code: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function injectCode(
|
|
14
|
+
filePath: string,
|
|
15
|
+
injection: CodeInjection,
|
|
16
|
+
position: 'append' | 'prepend' | { after: string } | { before: string },
|
|
17
|
+
options: { force?: boolean } = {}
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
if (!await fs.pathExists(filePath)) {
|
|
20
|
+
throw new Error(`File not found: ${filePath}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
24
|
+
|
|
25
|
+
// Check if already injected
|
|
26
|
+
const startMarker = CODE_MARKER_START(injection.id);
|
|
27
|
+
if (content.includes(startMarker) && !options.force) {
|
|
28
|
+
return; // Already injected, skip
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Remove old injection if force is true
|
|
32
|
+
if (options.force) {
|
|
33
|
+
content = removeInjection(content, injection.id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Prepare the code block with markers
|
|
37
|
+
const markedCode = `\n${startMarker}\n${injection.code}\n${CODE_MARKER_END(injection.id)}\n`;
|
|
38
|
+
|
|
39
|
+
// Inject based on position
|
|
40
|
+
if (position === 'append') {
|
|
41
|
+
content += markedCode;
|
|
42
|
+
} else if (position === 'prepend') {
|
|
43
|
+
content = markedCode + content;
|
|
44
|
+
} else if ('after' in position) {
|
|
45
|
+
const index = content.indexOf(position.after);
|
|
46
|
+
if (index === -1) {
|
|
47
|
+
throw new Error(`Could not find marker: ${position.after}`);
|
|
48
|
+
}
|
|
49
|
+
const insertPos = index + position.after.length;
|
|
50
|
+
content = content.slice(0, insertPos) + markedCode + content.slice(insertPos);
|
|
51
|
+
} else if ('before' in position) {
|
|
52
|
+
const index = content.indexOf(position.before);
|
|
53
|
+
if (index === -1) {
|
|
54
|
+
throw new Error(`Could not find marker: ${position.before}`);
|
|
55
|
+
}
|
|
56
|
+
content = content.slice(0, index) + markedCode + content.slice(index);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function removeInjection(content: string, id: string): string {
|
|
63
|
+
const startMarker = CODE_MARKER_START(id);
|
|
64
|
+
const endMarker = CODE_MARKER_END(id);
|
|
65
|
+
|
|
66
|
+
const startIndex = content.indexOf(startMarker);
|
|
67
|
+
if (startIndex === -1) {
|
|
68
|
+
return content;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const endIndex = content.indexOf(endMarker, startIndex);
|
|
72
|
+
if (endIndex === -1) {
|
|
73
|
+
return content;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Remove everything from start marker to end marker (inclusive)
|
|
77
|
+
const before = content.slice(0, startIndex);
|
|
78
|
+
const after = content.slice(endIndex + endMarker.length);
|
|
79
|
+
|
|
80
|
+
return before + after;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function hasInjection(content: string, id: string): boolean {
|
|
84
|
+
return content.includes(CODE_MARKER_START(id));
|
|
85
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ProjectInfo } from '../types';
|
|
4
|
+
|
|
5
|
+
export async function detectProjectInfo(targetDir: string): Promise<ProjectInfo> {
|
|
6
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
7
|
+
|
|
8
|
+
if (!await fs.pathExists(packageJsonPath)) {
|
|
9
|
+
throw new Error('No package.json found. This does not appear to be a Node.js project.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const packageJson = await fs.readJSON(packageJsonPath);
|
|
13
|
+
|
|
14
|
+
// Detect framework
|
|
15
|
+
const isNextJs = packageJson.dependencies?.next || packageJson.devDependencies?.next;
|
|
16
|
+
const framework = isNextJs ? 'nextjs' : 'unknown';
|
|
17
|
+
|
|
18
|
+
if (framework === 'unknown') {
|
|
19
|
+
throw new Error('Only Next.js projects are currently supported.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Detect router type
|
|
23
|
+
const appDirExists = await fs.pathExists(path.join(targetDir, 'app'));
|
|
24
|
+
const pagesDirExists = await fs.pathExists(path.join(targetDir, 'pages'));
|
|
25
|
+
const srcAppDirExists = await fs.pathExists(path.join(targetDir, 'src', 'app'));
|
|
26
|
+
const srcPagesDirExists = await fs.pathExists(path.join(targetDir, 'src', 'pages'));
|
|
27
|
+
|
|
28
|
+
let router: 'app' | 'pages' | 'unknown' = 'unknown';
|
|
29
|
+
if (appDirExists || srcAppDirExists) {
|
|
30
|
+
router = 'app';
|
|
31
|
+
} else if (pagesDirExists || srcPagesDirExists) {
|
|
32
|
+
router = 'pages';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Detect TypeScript vs JavaScript
|
|
36
|
+
const tsconfigExists = await fs.pathExists(path.join(targetDir, 'tsconfig.json'));
|
|
37
|
+
const language = tsconfigExists ? 'ts' : 'js';
|
|
38
|
+
|
|
39
|
+
// Detect package manager
|
|
40
|
+
const yarnLockExists = await fs.pathExists(path.join(targetDir, 'yarn.lock'));
|
|
41
|
+
const pnpmLockExists = await fs.pathExists(path.join(targetDir, 'pnpm-lock.yaml'));
|
|
42
|
+
let packageManager: 'npm' | 'yarn' | 'pnpm' = 'npm';
|
|
43
|
+
|
|
44
|
+
if (pnpmLockExists) {
|
|
45
|
+
packageManager = 'pnpm';
|
|
46
|
+
} else if (yarnLockExists) {
|
|
47
|
+
packageManager = 'yarn';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for existing integrations
|
|
51
|
+
const hasAuth = !!(
|
|
52
|
+
packageJson.dependencies?.['next-auth'] ||
|
|
53
|
+
packageJson.dependencies?.['@auth/core'] ||
|
|
54
|
+
packageJson.dependencies?.['@clerk/nextjs'] ||
|
|
55
|
+
packageJson.dependencies?.['@kinde-oss/kinde-auth-nextjs']
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const hasPrisma = !!(
|
|
59
|
+
packageJson.dependencies?.['@prisma/client'] ||
|
|
60
|
+
packageJson.devDependencies?.['prisma']
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
framework,
|
|
65
|
+
router,
|
|
66
|
+
language,
|
|
67
|
+
packageManager,
|
|
68
|
+
hasAuth,
|
|
69
|
+
hasPrisma,
|
|
70
|
+
rootDir: targetDir,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getRouterBasePath(projectInfo: ProjectInfo): string {
|
|
75
|
+
const srcExists = fs.existsSync(path.join(projectInfo.rootDir, 'src'));
|
|
76
|
+
|
|
77
|
+
if (projectInfo.router === 'app') {
|
|
78
|
+
return srcExists ? 'src/app' : 'app';
|
|
79
|
+
} else if (projectInfo.router === 'pages') {
|
|
80
|
+
return srcExists ? 'src/pages' : 'pages';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new Error('Unknown router type');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getLibPath(projectInfo: ProjectInfo): string {
|
|
87
|
+
const srcExists = fs.existsSync(path.join(projectInfo.rootDir, 'src'));
|
|
88
|
+
return srcExists ? 'src/lib' : 'lib';
|
|
89
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
|
|
5
|
+
const ENV_MARKER_START = '# StackKit:';
|
|
6
|
+
const ENV_MARKER_END = '# End StackKit';
|
|
7
|
+
|
|
8
|
+
export interface EnvVariable {
|
|
9
|
+
key: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
description: string;
|
|
12
|
+
required: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function addEnvVariables(
|
|
16
|
+
projectRoot: string,
|
|
17
|
+
variables: EnvVariable[],
|
|
18
|
+
options: { force?: boolean } = {}
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const envExamplePath = path.join(projectRoot, '.env.example');
|
|
21
|
+
const envPath = path.join(projectRoot, '.env');
|
|
22
|
+
|
|
23
|
+
// Add to .env.example
|
|
24
|
+
await appendToEnvFile(envExamplePath, variables, 'example', options);
|
|
25
|
+
|
|
26
|
+
// Add to .env if it exists or create it
|
|
27
|
+
const envExists = await fs.pathExists(envPath);
|
|
28
|
+
if (envExists || options.force) {
|
|
29
|
+
await appendToEnvFile(envPath, variables, 'local', options);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger.success('Environment variables added');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function appendToEnvFile(
|
|
36
|
+
filePath: string,
|
|
37
|
+
variables: EnvVariable[],
|
|
38
|
+
fileType: 'example' | 'local',
|
|
39
|
+
options: { force?: boolean } = {}
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
let content = '';
|
|
42
|
+
|
|
43
|
+
if (await fs.pathExists(filePath)) {
|
|
44
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if variables already exist
|
|
48
|
+
const existingKeys = new Set<string>();
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
53
|
+
if (match) {
|
|
54
|
+
existingKeys.add(match[1]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const newVariables = variables.filter((v) => {
|
|
59
|
+
if (existingKeys.has(v.key)) {
|
|
60
|
+
if (!options.force) {
|
|
61
|
+
logger.warn(`Variable ${v.key} already exists in ${filePath}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (newVariables.length === 0) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Ensure file ends with newline
|
|
73
|
+
if (content && !content.endsWith('\n')) {
|
|
74
|
+
content += '\n';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add marker and variables
|
|
78
|
+
content += '\n';
|
|
79
|
+
content += `${ENV_MARKER_START} Added by StackKit\n`;
|
|
80
|
+
|
|
81
|
+
for (const variable of newVariables) {
|
|
82
|
+
if (variable.description) {
|
|
83
|
+
content += `# ${variable.description}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const value = fileType === 'example' ? (variable.value || '') : (variable.value || '');
|
|
87
|
+
content += `${variable.key}=${value}\n`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
content += `${ENV_MARKER_END}\n`;
|
|
91
|
+
|
|
92
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function removeEnvVariables(
|
|
96
|
+
projectRoot: string,
|
|
97
|
+
keys: string[]
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const envExamplePath = path.join(projectRoot, '.env.example');
|
|
100
|
+
const envPath = path.join(projectRoot, '.env');
|
|
101
|
+
|
|
102
|
+
await removeFromEnvFile(envExamplePath, keys);
|
|
103
|
+
|
|
104
|
+
if (await fs.pathExists(envPath)) {
|
|
105
|
+
await removeFromEnvFile(envPath, keys);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function removeFromEnvFile(filePath: string, keys: string[]): Promise<void> {
|
|
110
|
+
if (!await fs.pathExists(filePath)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
115
|
+
const lines = content.split('\n');
|
|
116
|
+
const newLines: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
120
|
+
if (match && keys.includes(match[1])) {
|
|
121
|
+
continue; // Skip this line
|
|
122
|
+
}
|
|
123
|
+
newLines.push(line);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await fs.writeFile(filePath, newLines.join('\n'), 'utf-8');
|
|
127
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
|
|
5
|
+
export async function copyTemplate(
|
|
6
|
+
templatePath: string,
|
|
7
|
+
targetPath: string,
|
|
8
|
+
projectName: string
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
if (!await fs.pathExists(templatePath)) {
|
|
11
|
+
throw new Error(`Template not found: ${templatePath}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Create target directory
|
|
15
|
+
await fs.ensureDir(targetPath);
|
|
16
|
+
|
|
17
|
+
// Copy all files
|
|
18
|
+
await fs.copy(templatePath, targetPath, {
|
|
19
|
+
filter: (src) => {
|
|
20
|
+
const basename = path.basename(src);
|
|
21
|
+
// Skip template.json metadata file and node_modules
|
|
22
|
+
return basename !== 'template.json' && basename !== 'node_modules';
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Update package.json with project name
|
|
27
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
28
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
29
|
+
const packageJson = await fs.readJSON(packageJsonPath);
|
|
30
|
+
packageJson.name = projectName;
|
|
31
|
+
await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
logger.success(`Template copied to ${targetPath}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function createFile(
|
|
38
|
+
targetPath: string,
|
|
39
|
+
content: string,
|
|
40
|
+
options: { force?: boolean } = {}
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const exists = await fs.pathExists(targetPath);
|
|
43
|
+
|
|
44
|
+
if (exists && !options.force) {
|
|
45
|
+
logger.warn(`File already exists: ${targetPath} (use --force to overwrite)`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
50
|
+
await fs.writeFile(targetPath, content, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function readFile(filePath: string): Promise<string> {
|
|
54
|
+
return fs.readFile(filePath, 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
58
|
+
return fs.pathExists(filePath);
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { logger } from './logger';
|
|
3
|
+
|
|
4
|
+
export async function modifyJson(
|
|
5
|
+
filePath: string,
|
|
6
|
+
modifier: (json: any) => any,
|
|
7
|
+
options: { create?: boolean; force?: boolean } = {}
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
const exists = await fs.pathExists(filePath);
|
|
10
|
+
|
|
11
|
+
if (!exists && !options.create) {
|
|
12
|
+
throw new Error(`File not found: ${filePath}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let json = {};
|
|
16
|
+
if (exists) {
|
|
17
|
+
json = await fs.readJSON(filePath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const modified = modifier(json);
|
|
21
|
+
await fs.writeJSON(filePath, modified, { spaces: 2 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function addToPackageJson(
|
|
25
|
+
filePath: string,
|
|
26
|
+
section: 'dependencies' | 'devDependencies' | 'scripts',
|
|
27
|
+
additions: Record<string, string>
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
await modifyJson(filePath, (json) => {
|
|
30
|
+
json[section] = json[section] || {};
|
|
31
|
+
Object.assign(json[section], additions);
|
|
32
|
+
return json;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function setJsonValue(
|
|
37
|
+
filePath: string,
|
|
38
|
+
path: string,
|
|
39
|
+
value: any,
|
|
40
|
+
options: { merge?: boolean } = {}
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
await modifyJson(filePath, (json) => {
|
|
43
|
+
const keys = path.split('.');
|
|
44
|
+
let current = json;
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
47
|
+
const key = keys[i];
|
|
48
|
+
if (!current[key]) {
|
|
49
|
+
current[key] = {};
|
|
50
|
+
}
|
|
51
|
+
current = current[key];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lastKey = keys[keys.length - 1];
|
|
55
|
+
|
|
56
|
+
if (options.merge && typeof current[lastKey] === 'object' && typeof value === 'object') {
|
|
57
|
+
current[lastKey] = { ...current[lastKey], ...value };
|
|
58
|
+
} else {
|
|
59
|
+
current[lastKey] = value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return json;
|
|
63
|
+
});
|
|
64
|
+
}
|