claudeforge-cli 1.0.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/LICENSE +21 -0
- package/README.md +431 -0
- package/bin/cli.js +155 -0
- package/package.json +43 -0
- package/src/commands/add.js +205 -0
- package/src/commands/create.js +218 -0
- package/src/commands/github.js +479 -0
- package/src/commands/init.js +107 -0
- package/src/commands/project.js +123 -0
- package/src/commands/status.js +183 -0
- package/src/commands/upgrade.js +114 -0
- package/src/index.js +6 -0
- package/src/logger.js +90 -0
- package/src/scaffolder.js +45 -0
- package/src/stack-detector.js +62 -0
- package/templates/.env.example.tpl +21 -0
- package/templates/.gitignore.tpl +40 -0
- package/templates/CLAUDE.local.md.tpl +32 -0
- package/templates/CLAUDE.md.tpl +112 -0
- package/templates/claude/README.md.tpl +94 -0
- package/templates/claude/agents/code-reviewer.md.tpl +142 -0
- package/templates/claude/commands/commit.md.tpl +34 -0
- package/templates/claude/commands/explain-codebase.md.tpl +37 -0
- package/templates/claude/commands/fix-issue.md.tpl +43 -0
- package/templates/claude/commands/memory-sync.md.tpl +49 -0
- package/templates/claude/commands/project-health.md.tpl +70 -0
- package/templates/claude/commands/review-pr.md.tpl +43 -0
- package/templates/claude/commands/scaffold-structure.md.tpl +308 -0
- package/templates/claude/commands/setup-project.md.tpl +253 -0
- package/templates/claude/commands/standup.md.tpl +34 -0
- package/templates/claude/hooks/post-tool-use.sh.tpl +44 -0
- package/templates/claude/hooks/pre-tool-use.sh.tpl +64 -0
- package/templates/claude/rules/no-sensitive-files.md.tpl +29 -0
- package/templates/claude/settings.json.tpl +50 -0
- package/templates/claude/settings.local.json.tpl +4 -0
- package/templates/claude/skills/project-conventions/SKILL.md.tpl +39 -0
- package/templates/mcp.json.tpl +9 -0
- package/templates/memory/MEMORY.md.tpl +37 -0
- package/templates/memory/feedback_communication.md.tpl +29 -0
- package/templates/memory/project_ai_workflow.md.tpl +43 -0
- package/templates/memory/user_profile.md.tpl +30 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const logger = require('../logger');
|
|
7
|
+
|
|
8
|
+
const TEMPLATES_DIR = path.join(__dirname, '../../templates');
|
|
9
|
+
|
|
10
|
+
async function addAgent(name, options) {
|
|
11
|
+
const targetDir = path.resolve(options.dir || process.cwd());
|
|
12
|
+
|
|
13
|
+
if (!name || !name.trim()) {
|
|
14
|
+
logger.error('Please provide an agent name.');
|
|
15
|
+
console.log(chalk.dim(' Example: claudeforge add agent api-reviewer'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const slug = toKebab(name);
|
|
20
|
+
const destPath = path.join(targetDir, '.claude/agents', `${slug}.md`);
|
|
21
|
+
|
|
22
|
+
await assertNotExists(destPath, options.force);
|
|
23
|
+
|
|
24
|
+
const description = options.description || `Use this agent to [describe when Claude should invoke ${slug}].`;
|
|
25
|
+
const model = options.model || 'sonnet';
|
|
26
|
+
const color = options.color || 'blue';
|
|
27
|
+
|
|
28
|
+
const content = `---
|
|
29
|
+
name: ${slug}
|
|
30
|
+
description: ${description}
|
|
31
|
+
model: ${model}
|
|
32
|
+
color: ${color}
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# ${toTitle(name)} Agent
|
|
36
|
+
|
|
37
|
+
<!-- Replace this with the agent's system prompt. Be specific about:
|
|
38
|
+
- What the agent specializes in
|
|
39
|
+
- What inputs it expects from the caller
|
|
40
|
+
- What output format it produces
|
|
41
|
+
- Any rules or constraints it follows
|
|
42
|
+
-->
|
|
43
|
+
|
|
44
|
+
You are a specialized agent for [task].
|
|
45
|
+
|
|
46
|
+
## Instructions
|
|
47
|
+
|
|
48
|
+
1. [Step 1]
|
|
49
|
+
2. [Step 2]
|
|
50
|
+
|
|
51
|
+
## Output Format
|
|
52
|
+
|
|
53
|
+
[Describe the expected output]
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
await fs.ensureFile(destPath);
|
|
57
|
+
await fs.writeFile(destPath, content, 'utf8');
|
|
58
|
+
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(` ${chalk.green('✓')} Created ${chalk.cyan(`.claude/agents/${slug}.md`)}`);
|
|
61
|
+
logger.hints('What to do next:', [
|
|
62
|
+
{ cmd: `open .claude/agents/${slug}.md`, note: 'edit the system prompt — be specific about what it reviews' },
|
|
63
|
+
{ cmd: `/review-pr`, note: 'test the agent by having it review your current branch' },
|
|
64
|
+
{ cmd: `claudeforge status`, note: 'confirm the agent appears in your project' },
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function addCommand(name, options) {
|
|
69
|
+
const targetDir = path.resolve(options.dir || process.cwd());
|
|
70
|
+
|
|
71
|
+
if (!name || !name.trim()) {
|
|
72
|
+
logger.error('Please provide a command name.');
|
|
73
|
+
console.log(chalk.dim(' Example: claudeforge add command deploy'));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const slug = toKebab(name);
|
|
78
|
+
const destPath = path.join(targetDir, '.claude/commands', `${slug}.md`);
|
|
79
|
+
|
|
80
|
+
await assertNotExists(destPath, options.force);
|
|
81
|
+
|
|
82
|
+
const description = options.description || `[Describe what /${slug} does]`;
|
|
83
|
+
|
|
84
|
+
const content = `---
|
|
85
|
+
description: ${description}
|
|
86
|
+
allowed-tools: Bash, Read, Edit
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Context
|
|
90
|
+
|
|
91
|
+
<!-- Add dynamic context using !command syntax if needed -->
|
|
92
|
+
<!-- Example: - Git status: !\`git status\` -->
|
|
93
|
+
|
|
94
|
+
## Task
|
|
95
|
+
|
|
96
|
+
<!-- Describe what Claude should do when this command is invoked. -->
|
|
97
|
+
<!-- Be specific: what to read first, what to check, what to produce. -->
|
|
98
|
+
|
|
99
|
+
[Instructions for Claude when /${slug} is invoked]
|
|
100
|
+
|
|
101
|
+
## Rules
|
|
102
|
+
|
|
103
|
+
- [Rule 1]
|
|
104
|
+
- [Rule 2]
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
await fs.ensureFile(destPath);
|
|
108
|
+
await fs.writeFile(destPath, content, 'utf8');
|
|
109
|
+
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(` ${chalk.green('✓')} Created ${chalk.cyan(`.claude/commands/${slug}.md`)}`);
|
|
112
|
+
logger.hints('What to do next:', [
|
|
113
|
+
{ cmd: `open .claude/commands/${slug}.md`, note: 'edit the ## Task section — add real instructions' },
|
|
114
|
+
{ cmd: `/${slug}`, note: 'run in Claude Code chat to test it' },
|
|
115
|
+
{ cmd: `claudeforge add command <name>`, note: 'add another command for your workflow' },
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function addSkill(name, options) {
|
|
120
|
+
const targetDir = path.resolve(options.dir || process.cwd());
|
|
121
|
+
|
|
122
|
+
if (!name || !name.trim()) {
|
|
123
|
+
logger.error('Please provide a skill name.');
|
|
124
|
+
console.log(chalk.dim(' Example: claudeforge add skill database-patterns'));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const slug = toKebab(name);
|
|
129
|
+
const skillDir = path.join(targetDir, '.claude/skills', slug);
|
|
130
|
+
const destPath = path.join(skillDir, 'SKILL.md');
|
|
131
|
+
|
|
132
|
+
await assertNotExists(destPath, options.force);
|
|
133
|
+
|
|
134
|
+
const description = options.description || `Apply ${toTitle(name)} patterns and conventions. Invoke when [specific trigger condition].`;
|
|
135
|
+
const userInvocable = options.userInvocable !== false;
|
|
136
|
+
|
|
137
|
+
const content = `---
|
|
138
|
+
name: ${slug}
|
|
139
|
+
description: ${description}
|
|
140
|
+
user-invocable: ${userInvocable}
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
# ${toTitle(name)} Skill
|
|
144
|
+
|
|
145
|
+
<!-- Define what Claude should know and do when this skill is active. -->
|
|
146
|
+
<!-- Be specific: which files to read, patterns to follow, rules to apply. -->
|
|
147
|
+
|
|
148
|
+
## When to Apply
|
|
149
|
+
|
|
150
|
+
[Describe the exact conditions under which this skill should be active]
|
|
151
|
+
|
|
152
|
+
## Instructions
|
|
153
|
+
|
|
154
|
+
1. Read [relevant files or context first]
|
|
155
|
+
2. Apply [specific patterns or conventions]
|
|
156
|
+
3. [Additional steps]
|
|
157
|
+
|
|
158
|
+
## Rules
|
|
159
|
+
|
|
160
|
+
- [Rule 1 — specific to this skill]
|
|
161
|
+
- [Rule 2]
|
|
162
|
+
|
|
163
|
+
## Do Not
|
|
164
|
+
|
|
165
|
+
- [Common mistake to avoid]
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
await fs.ensureDir(skillDir);
|
|
169
|
+
await fs.writeFile(destPath, content, 'utf8');
|
|
170
|
+
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(` ${chalk.green('✓')} Created ${chalk.cyan(`.claude/skills/${slug}/SKILL.md`)}`);
|
|
173
|
+
logger.hints('What to do next:', [
|
|
174
|
+
{ cmd: `open .claude/skills/${slug}/SKILL.md`, note: 'fill in the Instructions section with specific conventions' },
|
|
175
|
+
{ cmd: `claudeforge status`, note: 'verify the skill appears in your project summary' },
|
|
176
|
+
{ cmd: `/project-health`, note: 'run in Claude Code chat to audit your full setup' },
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
async function assertNotExists(filePath, force) {
|
|
183
|
+
if (!force && await fs.pathExists(filePath)) {
|
|
184
|
+
logger.error(`File already exists: ${path.relative(process.cwd(), filePath)}`);
|
|
185
|
+
console.log(chalk.dim(' Use --force to overwrite.'));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function toKebab(str) {
|
|
191
|
+
return str
|
|
192
|
+
.trim()
|
|
193
|
+
.toLowerCase()
|
|
194
|
+
.replace(/[\s_]+/g, '-')
|
|
195
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function toTitle(str) {
|
|
199
|
+
return str
|
|
200
|
+
.split(/[-_\s]+/)
|
|
201
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
202
|
+
.join(' ');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { addAgent, addCommand, addSkill };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const initCommand = require('./init');
|
|
8
|
+
const projectCommand = require('./project');
|
|
9
|
+
const logger = require('../logger');
|
|
10
|
+
|
|
11
|
+
// ── Readline helpers ──────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function ask(rl, question) {
|
|
14
|
+
return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function askDefault(rl, question, defaultVal) {
|
|
18
|
+
const answer = await ask(rl, question);
|
|
19
|
+
return answer || defaultVal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function askChoice(rl, question, choices, defaultIdx = 0) {
|
|
23
|
+
const labels = choices.map((c, i) => `${chalk.dim(`${i + 1}.`)} ${c.label}`).join('\n ');
|
|
24
|
+
const answer = await ask(rl, `${question}\n\n ${labels}\n\n ${chalk.dim('Enter number')} [${defaultIdx + 1}]: `);
|
|
25
|
+
const idx = parseInt(answer, 10) - 1;
|
|
26
|
+
if (idx >= 0 && idx < choices.length) return choices[idx].value;
|
|
27
|
+
return choices[defaultIdx].value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function askMultiChoice(rl, question, choices) {
|
|
31
|
+
const labels = choices.map((c, i) => `${chalk.dim(`${i + 1}.`)} ${c.label}`).join('\n ');
|
|
32
|
+
const answer = await ask(rl, `${question}\n\n ${labels}\n\n ${chalk.dim('Enter numbers separated by commas, or press Enter to skip')}: `);
|
|
33
|
+
if (!answer) return [];
|
|
34
|
+
const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < choices.length);
|
|
35
|
+
return indices.map(i => choices[i].value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
async function create(name, options) {
|
|
41
|
+
const parentDir = path.resolve(options.dir || process.cwd());
|
|
42
|
+
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(chalk.bold.cyan(' claudeforge') + chalk.dim(' — Create New Project'));
|
|
45
|
+
console.log(chalk.dim(' ─────────────────────────────────────────'));
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(chalk.dim(' Answer a few questions and claudeforge will scaffold a fully configured'));
|
|
48
|
+
console.log(chalk.dim(' Claude Code project ready for AI-assisted development.'));
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// 1. Project name
|
|
55
|
+
const defaultName = name || 'my-project';
|
|
56
|
+
const projectName = await askDefault(rl, ' ' + chalk.bold('Project name') + ' ' + chalk.dim('[' + defaultName + ']') + ': ', defaultName);
|
|
57
|
+
|
|
58
|
+
// 2. Description
|
|
59
|
+
const description = await askDefault(
|
|
60
|
+
rl,
|
|
61
|
+
`\n ${chalk.bold('Describe your project')} ${chalk.dim('(used to AI-configure everything)')}:\n `,
|
|
62
|
+
`${projectName} — a new project`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// 3. Stack
|
|
66
|
+
const stack = await askChoice(rl, `\n ${chalk.bold('Primary tech stack')}:`, [
|
|
67
|
+
{ label: 'Node.js / TypeScript', value: 'node' },
|
|
68
|
+
{ label: 'Python (FastAPI / Django)', value: 'python' },
|
|
69
|
+
{ label: 'Go', value: 'go' },
|
|
70
|
+
{ label: 'Rust', value: 'rust' },
|
|
71
|
+
{ label: 'Other / Mixed', value: 'generic' },
|
|
72
|
+
], 0);
|
|
73
|
+
|
|
74
|
+
// 4. Optional features
|
|
75
|
+
const features = await askMultiChoice(rl, `\n ${chalk.bold('Optional features')} ${chalk.dim('(select all that apply)')}:`, [
|
|
76
|
+
{ label: 'GitHub Actions CI/CD + PR templates', value: 'github' },
|
|
77
|
+
{ label: 'VS Code devcontainer', value: 'devcontainer' },
|
|
78
|
+
{ label: 'Docker + docker-compose', value: 'docker' },
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
rl.close();
|
|
82
|
+
|
|
83
|
+
// ── Scaffold ────────────────────────────────────────────────────────────
|
|
84
|
+
const targetDir = path.join(parentDir, projectName);
|
|
85
|
+
await fs.ensureDir(targetDir);
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(chalk.dim(' ─────────────────────────────────────────'));
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(` ${chalk.green('✓')} Creating project at ${chalk.cyan(targetDir)}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
// Run init
|
|
94
|
+
await initCommand({ dir: targetDir, force: false, dryRun: false });
|
|
95
|
+
|
|
96
|
+
// Run project
|
|
97
|
+
await projectCommand(description, { dir: targetDir });
|
|
98
|
+
|
|
99
|
+
// Optional: GitHub
|
|
100
|
+
if (features.includes('github')) {
|
|
101
|
+
const githubCommand = require('./github');
|
|
102
|
+
await githubCommand({ dir: targetDir, dryRun: false, stack, devcontainer: features.includes('devcontainer'), prTemplate: true, issueTemplates: true });
|
|
103
|
+
} else if (features.includes('devcontainer')) {
|
|
104
|
+
const githubCommand = require('./github');
|
|
105
|
+
await githubCommand({ dir: targetDir, dryRun: false, stack, devcontainer: true, prTemplate: false, issueTemplates: false });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Optional: Docker stubs
|
|
109
|
+
if (features.includes('docker')) {
|
|
110
|
+
await writeDockerFiles(targetDir, stack, projectName);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Final summary
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(chalk.dim(' ─────────────────────────────────────────'));
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(chalk.green.bold(` ✓ Project "${projectName}" is ready!`));
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(chalk.bold(' What to do next:'));
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(` ${chalk.dim('1.')} ${chalk.cyan(`cd ${projectName}`)}`);
|
|
122
|
+
console.log(` ${chalk.dim('2.')} Open in VS Code / your IDE with Claude Code`);
|
|
123
|
+
console.log(` ${chalk.dim('3.')} In the Claude Code chat, run:`);
|
|
124
|
+
console.log(` ${chalk.cyan.bold(`/setup-project "${truncate(description, 50)}"`)}`);
|
|
125
|
+
console.log(` ${chalk.dim('— Claude will fill in CLAUDE.md, agents, commands, and more')}`);
|
|
126
|
+
console.log(` ${chalk.dim('4.')} Then run ${chalk.cyan('/scaffold-structure')} to create src/, tests/, etc.`);
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (!rl.closed) rl.close();
|
|
131
|
+
logger.error(err.message || String(err));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function writeDockerFiles(targetDir, stack, projectName) {
|
|
137
|
+
const dockerfile = buildDockerfile(stack, projectName);
|
|
138
|
+
const compose = buildDockerCompose(stack, projectName);
|
|
139
|
+
await fs.writeFile(path.join(targetDir, 'Dockerfile'), dockerfile, 'utf8');
|
|
140
|
+
await fs.writeFile(path.join(targetDir, 'docker-compose.yml'), compose, 'utf8');
|
|
141
|
+
console.log(` ${chalk.green('✓')} Written ${chalk.cyan('Dockerfile')}`);
|
|
142
|
+
console.log(` ${chalk.green('✓')} Written ${chalk.cyan('docker-compose.yml')}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildDockerfile(stack, name) {
|
|
146
|
+
if (stack === 'node') {
|
|
147
|
+
return `FROM node:20-alpine
|
|
148
|
+
WORKDIR /app
|
|
149
|
+
COPY package*.json ./
|
|
150
|
+
RUN npm ci --production
|
|
151
|
+
COPY . .
|
|
152
|
+
EXPOSE 3000
|
|
153
|
+
CMD ["node", "src/index.js"]
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
if (stack === 'python') {
|
|
157
|
+
return `FROM python:3.12-slim
|
|
158
|
+
WORKDIR /app
|
|
159
|
+
COPY requirements*.txt ./
|
|
160
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
161
|
+
COPY . .
|
|
162
|
+
EXPOSE 8000
|
|
163
|
+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
if (stack === 'go') {
|
|
167
|
+
return `FROM golang:1.22-alpine AS builder
|
|
168
|
+
WORKDIR /app
|
|
169
|
+
COPY go.* ./
|
|
170
|
+
RUN go mod download
|
|
171
|
+
COPY . .
|
|
172
|
+
RUN go build -o /app/server ./cmd/server
|
|
173
|
+
|
|
174
|
+
FROM alpine:3.19
|
|
175
|
+
COPY --from=builder /app/server /server
|
|
176
|
+
EXPOSE 8080
|
|
177
|
+
CMD ["/server"]
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
if (stack === 'rust') {
|
|
181
|
+
return `FROM rust:1.77-slim AS builder
|
|
182
|
+
WORKDIR /app
|
|
183
|
+
COPY Cargo.* ./
|
|
184
|
+
COPY src ./src
|
|
185
|
+
RUN cargo build --release
|
|
186
|
+
|
|
187
|
+
FROM debian:bookworm-slim
|
|
188
|
+
COPY --from=builder /app/target/release/${name} /${name}
|
|
189
|
+
EXPOSE 8080
|
|
190
|
+
CMD ["/${name}"]
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
return `FROM ubuntu:22.04
|
|
194
|
+
WORKDIR /app
|
|
195
|
+
COPY . .
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildDockerCompose(stack, name) {
|
|
200
|
+
const port = stack === 'node' ? '3000' : stack === 'python' ? '8000' : '8080';
|
|
201
|
+
return `services:
|
|
202
|
+
app:
|
|
203
|
+
build: .
|
|
204
|
+
ports:
|
|
205
|
+
- "${port}:${port}"
|
|
206
|
+
environment:
|
|
207
|
+
- NODE_ENV=development
|
|
208
|
+
volumes:
|
|
209
|
+
- .:/app
|
|
210
|
+
- /app/node_modules
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function truncate(str, max) {
|
|
215
|
+
return str.length > max ? str.slice(0, max - 1) + '…' : str;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = create;
|