tlc-claude-code 1.2.28 → 1.3.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 +9 -4
- package/dashboard/dist/components/UsagePane.d.ts +13 -0
- package/dashboard/dist/components/UsagePane.js +51 -0
- package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
- package/dashboard/dist/components/UsagePane.test.js +142 -0
- package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +146 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
- package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
- package/dashboard/dist/components/WorkspacePane.js +17 -0
- package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspacePane.test.js +84 -0
- package/package.json +15 -4
- package/scripts/capture-screenshots.js +170 -0
- package/scripts/docs-update.js +253 -0
- package/scripts/generate-screenshots.js +321 -0
- package/scripts/project-docs.js +377 -0
- package/scripts/vps-setup.sh +477 -0
- package/server/lib/architecture-command.js +450 -0
- package/server/lib/architecture-command.test.js +754 -0
- package/server/lib/ast-analyzer.js +324 -0
- package/server/lib/ast-analyzer.test.js +437 -0
- package/server/lib/auth-system.test.js +4 -1
- package/server/lib/boundary-detector.js +427 -0
- package/server/lib/boundary-detector.test.js +320 -0
- package/server/lib/budget-alerts.js +138 -0
- package/server/lib/budget-alerts.test.js +235 -0
- package/server/lib/candidates-tracker.js +210 -0
- package/server/lib/candidates-tracker.test.js +300 -0
- package/server/lib/checkpoint-manager.js +251 -0
- package/server/lib/checkpoint-manager.test.js +474 -0
- package/server/lib/circular-detector.js +337 -0
- package/server/lib/circular-detector.test.js +353 -0
- package/server/lib/cohesion-analyzer.js +310 -0
- package/server/lib/cohesion-analyzer.test.js +447 -0
- package/server/lib/contract-testing.js +625 -0
- package/server/lib/contract-testing.test.js +342 -0
- package/server/lib/conversion-planner.js +469 -0
- package/server/lib/conversion-planner.test.js +361 -0
- package/server/lib/convert-command.js +351 -0
- package/server/lib/convert-command.test.js +608 -0
- package/server/lib/coupling-calculator.js +189 -0
- package/server/lib/coupling-calculator.test.js +509 -0
- package/server/lib/dependency-graph.js +367 -0
- package/server/lib/dependency-graph.test.js +516 -0
- package/server/lib/duplication-detector.js +349 -0
- package/server/lib/duplication-detector.test.js +401 -0
- package/server/lib/example-service.js +616 -0
- package/server/lib/example-service.test.js +397 -0
- package/server/lib/impact-scorer.js +184 -0
- package/server/lib/impact-scorer.test.js +211 -0
- package/server/lib/mermaid-generator.js +358 -0
- package/server/lib/mermaid-generator.test.js +301 -0
- package/server/lib/messaging-patterns.js +750 -0
- package/server/lib/messaging-patterns.test.js +213 -0
- package/server/lib/microservice-template.js +386 -0
- package/server/lib/microservice-template.test.js +325 -0
- package/server/lib/new-project-microservice.js +450 -0
- package/server/lib/new-project-microservice.test.js +600 -0
- package/server/lib/refactor-command.js +326 -0
- package/server/lib/refactor-command.test.js +528 -0
- package/server/lib/refactor-executor.js +254 -0
- package/server/lib/refactor-executor.test.js +305 -0
- package/server/lib/refactor-observer.js +292 -0
- package/server/lib/refactor-observer.test.js +422 -0
- package/server/lib/refactor-progress.js +193 -0
- package/server/lib/refactor-progress.test.js +251 -0
- package/server/lib/refactor-reporter.js +237 -0
- package/server/lib/refactor-reporter.test.js +247 -0
- package/server/lib/semantic-analyzer.js +198 -0
- package/server/lib/semantic-analyzer.test.js +474 -0
- package/server/lib/service-scaffold.js +486 -0
- package/server/lib/service-scaffold.test.js +373 -0
- package/server/lib/shared-kernel.js +578 -0
- package/server/lib/shared-kernel.test.js +255 -0
- package/server/lib/traefik-config.js +282 -0
- package/server/lib/traefik-config.test.js +312 -0
- package/server/lib/usage-command.js +218 -0
- package/server/lib/usage-command.test.js +391 -0
- package/server/lib/usage-formatter.js +192 -0
- package/server/lib/usage-formatter.test.js +267 -0
- package/server/lib/usage-history.js +122 -0
- package/server/lib/usage-history.test.js +206 -0
- package/server/package-lock.json +14 -0
- package/server/package.json +1 -0
- package/templates/docs-sync.yml +91 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Service Template Generator
|
|
3
|
+
* Generates a complete working microservice template
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ExampleService {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate complete service
|
|
13
|
+
* @param {Object} config - Configuration object
|
|
14
|
+
* @param {string} config.name - Service name (e.g., 'user')
|
|
15
|
+
* @param {number} config.port - Service port (e.g., 3001)
|
|
16
|
+
* @param {string} config.database - Database type (e.g., 'postgres')
|
|
17
|
+
* @returns {Object} Generated structure { directories: [...], files: [...] }
|
|
18
|
+
*/
|
|
19
|
+
generate(config) {
|
|
20
|
+
const name = config.name || 'service';
|
|
21
|
+
|
|
22
|
+
const directories = [
|
|
23
|
+
name,
|
|
24
|
+
`${name}/src`,
|
|
25
|
+
`${name}/src/routes`,
|
|
26
|
+
`${name}/tests`,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Add migrations directory if database is configured
|
|
30
|
+
if (config.database) {
|
|
31
|
+
directories.push(`${name}/migrations`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const files = [];
|
|
35
|
+
|
|
36
|
+
// Package.json
|
|
37
|
+
files.push({
|
|
38
|
+
path: `${name}/package.json`,
|
|
39
|
+
content: this.generatePackageJson(config),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Entry point
|
|
43
|
+
files.push({
|
|
44
|
+
path: `${name}/src/index.js`,
|
|
45
|
+
content: this.generateIndex(config),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Routes
|
|
49
|
+
files.push({
|
|
50
|
+
path: `${name}/src/routes/index.js`,
|
|
51
|
+
content: this.generateRoutes(config),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Dockerfile
|
|
55
|
+
files.push({
|
|
56
|
+
path: `${name}/Dockerfile`,
|
|
57
|
+
content: this.generateDockerfile(config),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Docker Compose
|
|
61
|
+
files.push({
|
|
62
|
+
path: `${name}/docker-compose.yml`,
|
|
63
|
+
content: this.generateDockerCompose(config),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Migrations (if database configured)
|
|
67
|
+
if (config.database) {
|
|
68
|
+
const migration = this.generateMigrations(config);
|
|
69
|
+
files.push({
|
|
70
|
+
path: `${name}/migrations/${migration.path}`,
|
|
71
|
+
content: migration.content,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Tests
|
|
76
|
+
const tests = this.generateTests(config);
|
|
77
|
+
files.push({
|
|
78
|
+
path: `${name}/tests/routes.test.js`,
|
|
79
|
+
content: tests.unit,
|
|
80
|
+
});
|
|
81
|
+
files.push({
|
|
82
|
+
path: `${name}/tests/integration.test.js`,
|
|
83
|
+
content: tests.integration,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return { directories, files };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generate package.json for service
|
|
91
|
+
* @param {Object} config - Configuration object
|
|
92
|
+
* @returns {string} JSON string of package.json
|
|
93
|
+
*/
|
|
94
|
+
generatePackageJson(config) {
|
|
95
|
+
const name = config.name || 'service';
|
|
96
|
+
|
|
97
|
+
const pkg = {
|
|
98
|
+
name,
|
|
99
|
+
version: '0.1.0',
|
|
100
|
+
description: `${name} microservice`,
|
|
101
|
+
main: 'src/index.js',
|
|
102
|
+
scripts: {
|
|
103
|
+
start: 'node src/index.js',
|
|
104
|
+
dev: 'node --watch src/index.js',
|
|
105
|
+
test: 'vitest run',
|
|
106
|
+
migrate: 'node migrations/run.js',
|
|
107
|
+
},
|
|
108
|
+
dependencies: {
|
|
109
|
+
express: '^4.18.0',
|
|
110
|
+
},
|
|
111
|
+
devDependencies: {
|
|
112
|
+
vitest: '^4.0.0',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Add database driver if configured
|
|
117
|
+
if (config.database === 'postgres') {
|
|
118
|
+
pkg.dependencies.pg = '^8.11.0';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return JSON.stringify(pkg, null, 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generate src/index.js entry point
|
|
126
|
+
* @param {Object} config - Configuration object
|
|
127
|
+
* @returns {string} JavaScript source code
|
|
128
|
+
*/
|
|
129
|
+
generateIndex(config) {
|
|
130
|
+
const name = config.name || 'service';
|
|
131
|
+
const port = config.port || 3000;
|
|
132
|
+
|
|
133
|
+
return `/**
|
|
134
|
+
* ${name} Service Entry Point
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
const express = require('express');
|
|
138
|
+
const routes = require('./routes');
|
|
139
|
+
|
|
140
|
+
const app = express();
|
|
141
|
+
const PORT = process.env.PORT || ${port};
|
|
142
|
+
|
|
143
|
+
// Middleware
|
|
144
|
+
app.use(express.json());
|
|
145
|
+
|
|
146
|
+
// Health check endpoint
|
|
147
|
+
app.get('/health', (req, res) => {
|
|
148
|
+
res.json({
|
|
149
|
+
status: 'healthy',
|
|
150
|
+
service: '${name}',
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Mount routes
|
|
156
|
+
app.use('/api/${name}', routes);
|
|
157
|
+
|
|
158
|
+
// Error handling middleware
|
|
159
|
+
app.use((err, req, res, next) => {
|
|
160
|
+
console.error(err.stack);
|
|
161
|
+
res.status(500).json({
|
|
162
|
+
error: 'Internal Server Error',
|
|
163
|
+
message: process.env.NODE_ENV === 'development' ? err.message : undefined,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.listen(PORT, () => {
|
|
168
|
+
console.log(\`${name} service listening on port \${PORT}\`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
module.exports = app;
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generate CRUD routes
|
|
177
|
+
* @param {Object} config - Configuration object
|
|
178
|
+
* @returns {string} JavaScript source code
|
|
179
|
+
*/
|
|
180
|
+
generateRoutes(config) {
|
|
181
|
+
const name = config.name || 'service';
|
|
182
|
+
const entity = this.singularize(name);
|
|
183
|
+
const Entity = this.capitalize(entity);
|
|
184
|
+
|
|
185
|
+
return `/**
|
|
186
|
+
* ${name} Routes
|
|
187
|
+
* CRUD operations for ${entity} entity
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
const express = require('express');
|
|
191
|
+
const router = express.Router();
|
|
192
|
+
|
|
193
|
+
// In-memory storage (replace with database)
|
|
194
|
+
let items = [];
|
|
195
|
+
let nextId = 1;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* GET / - List all ${name}
|
|
199
|
+
*/
|
|
200
|
+
router.get('/', (req, res) => {
|
|
201
|
+
res.json({
|
|
202
|
+
data: items,
|
|
203
|
+
count: items.length,
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* GET /:id - Get ${entity} by id
|
|
209
|
+
*/
|
|
210
|
+
router.get('/:id', (req, res) => {
|
|
211
|
+
const item = items.find(i => i.id === parseInt(req.params.id));
|
|
212
|
+
|
|
213
|
+
if (!item) {
|
|
214
|
+
return res.status(404).json({ error: '${Entity} not found' });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
res.json({ data: item });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* POST / - Create new ${entity}
|
|
222
|
+
*/
|
|
223
|
+
router.post('/', (req, res) => {
|
|
224
|
+
const item = {
|
|
225
|
+
id: nextId++,
|
|
226
|
+
...req.body,
|
|
227
|
+
createdAt: new Date().toISOString(),
|
|
228
|
+
updatedAt: new Date().toISOString(),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
items.push(item);
|
|
232
|
+
res.status(201).json({ data: item });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* PUT /:id - Update ${entity}
|
|
237
|
+
*/
|
|
238
|
+
router.put('/:id', (req, res) => {
|
|
239
|
+
const index = items.findIndex(i => i.id === parseInt(req.params.id));
|
|
240
|
+
|
|
241
|
+
if (index === -1) {
|
|
242
|
+
return res.status(404).json({ error: '${Entity} not found' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
items[index] = {
|
|
246
|
+
...items[index],
|
|
247
|
+
...req.body,
|
|
248
|
+
updatedAt: new Date().toISOString(),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
res.json({ data: items[index] });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* DELETE /:id - Delete ${entity}
|
|
256
|
+
*/
|
|
257
|
+
router.delete('/:id', (req, res) => {
|
|
258
|
+
const index = items.findIndex(i => i.id === parseInt(req.params.id));
|
|
259
|
+
|
|
260
|
+
if (index === -1) {
|
|
261
|
+
return res.status(404).json({ error: '${Entity} not found' });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
items.splice(index, 1);
|
|
265
|
+
res.status(204).send();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
module.exports = router;
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Generate database migrations
|
|
274
|
+
* @param {Object} config - Configuration object
|
|
275
|
+
* @returns {Object} Migration file { path, content }
|
|
276
|
+
*/
|
|
277
|
+
generateMigrations(config) {
|
|
278
|
+
const name = config.name || 'service';
|
|
279
|
+
const entity = this.singularize(name);
|
|
280
|
+
const table = this.pluralize(entity);
|
|
281
|
+
const timestamp = Date.now();
|
|
282
|
+
|
|
283
|
+
const content = `/**
|
|
284
|
+
* Initial ${name} Schema Migration
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Apply migration
|
|
289
|
+
* @param {Object} client - Database client
|
|
290
|
+
*/
|
|
291
|
+
async function up(client) {
|
|
292
|
+
await client.query(\`
|
|
293
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
294
|
+
id SERIAL PRIMARY KEY,
|
|
295
|
+
name VARCHAR(255) NOT NULL,
|
|
296
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
297
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
298
|
+
);
|
|
299
|
+
\`);
|
|
300
|
+
|
|
301
|
+
console.log('Created ${table} table');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Rollback migration
|
|
306
|
+
* @param {Object} client - Database client
|
|
307
|
+
*/
|
|
308
|
+
async function down(client) {
|
|
309
|
+
await client.query(\`
|
|
310
|
+
DROP TABLE IF EXISTS ${table};
|
|
311
|
+
\`);
|
|
312
|
+
|
|
313
|
+
console.log('Dropped ${table} table');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = { up, down };
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
path: `${timestamp}_initial_${name}_schema.js`,
|
|
321
|
+
content,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generate test files
|
|
327
|
+
* @param {Object} config - Configuration object
|
|
328
|
+
* @returns {Object} Test files { unit, integration }
|
|
329
|
+
*/
|
|
330
|
+
generateTests(config) {
|
|
331
|
+
const name = config.name || 'service';
|
|
332
|
+
const entity = this.singularize(name);
|
|
333
|
+
const Entity = this.capitalize(entity);
|
|
334
|
+
|
|
335
|
+
const unit = `/**
|
|
336
|
+
* ${name} Routes Unit Tests
|
|
337
|
+
*/
|
|
338
|
+
|
|
339
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
340
|
+
|
|
341
|
+
describe('${name} routes', () => {
|
|
342
|
+
describe('GET /', () => {
|
|
343
|
+
it('returns list of ${name}', async () => {
|
|
344
|
+
// Test implementation
|
|
345
|
+
expect(true).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('returns empty array when no data', async () => {
|
|
349
|
+
expect([]).toEqual([]);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('GET /:id', () => {
|
|
354
|
+
it('returns ${entity} by id', async () => {
|
|
355
|
+
expect(true).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('returns 404 for non-existent id', async () => {
|
|
359
|
+
expect(404).toBe(404);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('POST /', () => {
|
|
364
|
+
it('creates new ${entity}', async () => {
|
|
365
|
+
expect(true).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('returns 201 on success', async () => {
|
|
369
|
+
expect(201).toBe(201);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('PUT /:id', () => {
|
|
374
|
+
it('updates existing ${entity}', async () => {
|
|
375
|
+
expect(true).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('returns 404 for non-existent id', async () => {
|
|
379
|
+
expect(404).toBe(404);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('DELETE /:id', () => {
|
|
384
|
+
it('deletes ${entity}', async () => {
|
|
385
|
+
expect(true).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('returns 204 on success', async () => {
|
|
389
|
+
expect(204).toBe(204);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
`;
|
|
394
|
+
|
|
395
|
+
const integration = `/**
|
|
396
|
+
* ${name} Integration Tests
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
400
|
+
|
|
401
|
+
describe('${name} integration', () => {
|
|
402
|
+
let server;
|
|
403
|
+
|
|
404
|
+
beforeAll(() => {
|
|
405
|
+
// Start test server
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
afterAll(() => {
|
|
409
|
+
// Stop test server
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('creates and retrieves ${entity}', async () => {
|
|
413
|
+
// Integration test implementation
|
|
414
|
+
expect(true).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('full CRUD cycle', async () => {
|
|
418
|
+
// Create -> Read -> Update -> Delete
|
|
419
|
+
expect(true).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
`;
|
|
423
|
+
|
|
424
|
+
return { unit, integration };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Generate Dockerfile with multi-stage build
|
|
429
|
+
* @param {Object} config - Configuration object
|
|
430
|
+
* @returns {string} Dockerfile content
|
|
431
|
+
*/
|
|
432
|
+
generateDockerfile(config) {
|
|
433
|
+
const name = config.name || 'service';
|
|
434
|
+
const port = config.port || 3000;
|
|
435
|
+
|
|
436
|
+
return `# ${name} service Dockerfile
|
|
437
|
+
# Multi-stage build for optimal image size
|
|
438
|
+
|
|
439
|
+
# Build stage
|
|
440
|
+
FROM node:20-alpine AS builder
|
|
441
|
+
|
|
442
|
+
WORKDIR /app
|
|
443
|
+
|
|
444
|
+
# Install dependencies
|
|
445
|
+
COPY package*.json ./
|
|
446
|
+
RUN npm ci --only=production
|
|
447
|
+
|
|
448
|
+
# Production stage
|
|
449
|
+
FROM node:20-alpine AS production
|
|
450
|
+
|
|
451
|
+
WORKDIR /app
|
|
452
|
+
|
|
453
|
+
# Create non-root user for security
|
|
454
|
+
RUN addgroup -g 1001 -S nodejs && \\
|
|
455
|
+
adduser -S nodejs -u 1001
|
|
456
|
+
|
|
457
|
+
# Copy from builder
|
|
458
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
459
|
+
COPY src/ ./src/
|
|
460
|
+
|
|
461
|
+
# Set environment
|
|
462
|
+
ENV NODE_ENV=production
|
|
463
|
+
ENV PORT=${port}
|
|
464
|
+
|
|
465
|
+
# Switch to non-root user
|
|
466
|
+
USER nodejs
|
|
467
|
+
|
|
468
|
+
# Expose port
|
|
469
|
+
EXPOSE ${port}
|
|
470
|
+
|
|
471
|
+
# Health check
|
|
472
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
473
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/health || exit 1
|
|
474
|
+
|
|
475
|
+
# Run service
|
|
476
|
+
CMD ["node", "src/index.js"]
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Generate docker-compose.yml for service
|
|
482
|
+
* @param {Object} config - Configuration object
|
|
483
|
+
* @returns {string} YAML content
|
|
484
|
+
*/
|
|
485
|
+
generateDockerCompose(config) {
|
|
486
|
+
const name = config.name || 'service';
|
|
487
|
+
const port = config.port || 3000;
|
|
488
|
+
const serviceName = name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
489
|
+
|
|
490
|
+
const compose = {
|
|
491
|
+
version: '3.8',
|
|
492
|
+
services: {
|
|
493
|
+
[serviceName]: {
|
|
494
|
+
build: {
|
|
495
|
+
context: '.',
|
|
496
|
+
dockerfile: 'Dockerfile',
|
|
497
|
+
},
|
|
498
|
+
ports: [`${port}:${port}`],
|
|
499
|
+
environment: {
|
|
500
|
+
NODE_ENV: 'development',
|
|
501
|
+
PORT: port,
|
|
502
|
+
},
|
|
503
|
+
networks: ['backend'],
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
networks: {
|
|
507
|
+
backend: {
|
|
508
|
+
driver: 'bridge',
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Add database if configured
|
|
514
|
+
if (config.database === 'postgres') {
|
|
515
|
+
compose.services[serviceName].environment.DATABASE_URL =
|
|
516
|
+
`postgres://${serviceName}:dev_password@postgres:5432/${serviceName}`;
|
|
517
|
+
compose.services[serviceName].depends_on = ['postgres'];
|
|
518
|
+
|
|
519
|
+
compose.services.postgres = {
|
|
520
|
+
image: 'postgres:15-alpine',
|
|
521
|
+
environment: {
|
|
522
|
+
POSTGRES_DB: serviceName,
|
|
523
|
+
POSTGRES_USER: serviceName,
|
|
524
|
+
POSTGRES_PASSWORD: 'dev_password',
|
|
525
|
+
},
|
|
526
|
+
ports: ['5432:5432'],
|
|
527
|
+
volumes: ['postgres_data:/var/lib/postgresql/data'],
|
|
528
|
+
networks: ['backend'],
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
compose.volumes = {
|
|
532
|
+
postgres_data: {},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return this.toYaml(compose);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Simple YAML serializer
|
|
541
|
+
* @param {Object} obj - Object to serialize
|
|
542
|
+
* @param {number} indent - Current indentation level
|
|
543
|
+
* @returns {string} YAML string
|
|
544
|
+
*/
|
|
545
|
+
toYaml(obj, indent = 0) {
|
|
546
|
+
const spaces = ' '.repeat(indent);
|
|
547
|
+
let yaml = '';
|
|
548
|
+
|
|
549
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
550
|
+
if (value === null || value === undefined) continue;
|
|
551
|
+
|
|
552
|
+
if (Array.isArray(value)) {
|
|
553
|
+
yaml += `${spaces}${key}:\n`;
|
|
554
|
+
for (const item of value) {
|
|
555
|
+
if (typeof item === 'object') {
|
|
556
|
+
yaml += `${spaces} -\n`;
|
|
557
|
+
yaml += this.toYaml(item, indent + 2);
|
|
558
|
+
} else {
|
|
559
|
+
yaml += `${spaces} - ${item}\n`;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} else if (typeof value === 'object') {
|
|
563
|
+
yaml += `${spaces}${key}:\n`;
|
|
564
|
+
yaml += this.toYaml(value, indent + 1);
|
|
565
|
+
} else {
|
|
566
|
+
yaml += `${spaces}${key}: ${value}\n`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return yaml;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Convert string to singular form (simple implementation)
|
|
575
|
+
* @param {string} str - String to singularize
|
|
576
|
+
* @returns {string} Singular form
|
|
577
|
+
*/
|
|
578
|
+
singularize(str) {
|
|
579
|
+
if (str.endsWith('ies')) {
|
|
580
|
+
return str.slice(0, -3) + 'y';
|
|
581
|
+
}
|
|
582
|
+
if (str.endsWith('es')) {
|
|
583
|
+
return str.slice(0, -2);
|
|
584
|
+
}
|
|
585
|
+
if (str.endsWith('s')) {
|
|
586
|
+
return str.slice(0, -1);
|
|
587
|
+
}
|
|
588
|
+
return str;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Convert string to plural form (simple implementation)
|
|
593
|
+
* @param {string} str - String to pluralize
|
|
594
|
+
* @returns {string} Plural form
|
|
595
|
+
*/
|
|
596
|
+
pluralize(str) {
|
|
597
|
+
if (str.endsWith('y')) {
|
|
598
|
+
return str.slice(0, -1) + 'ies';
|
|
599
|
+
}
|
|
600
|
+
if (str.endsWith('s') || str.endsWith('x') || str.endsWith('ch') || str.endsWith('sh')) {
|
|
601
|
+
return str + 'es';
|
|
602
|
+
}
|
|
603
|
+
return str + 's';
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Capitalize first letter
|
|
608
|
+
* @param {string} str - String to capitalize
|
|
609
|
+
* @returns {string} Capitalized string
|
|
610
|
+
*/
|
|
611
|
+
capitalize(str) {
|
|
612
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
module.exports = { ExampleService };
|