dhurandhar 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/.dhurandhar-session-start.md +242 -0
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/docs/ARCHITECTURE_V2.md +249 -0
- package/docs/DECISION_REGISTRY.md +357 -0
- package/docs/IMPLEMENTATION_PERSONAS.md +406 -0
- package/docs/PLUGGABLE_STRATEGIES.md +439 -0
- package/docs/SYSTEM_OBSERVER.md +433 -0
- package/docs/TEST_FIRST_AGILE.md +359 -0
- package/docs/architecture.md +279 -0
- package/docs/engineering-first-philosophy.md +263 -0
- package/docs/getting-started.md +218 -0
- package/docs/module-development.md +323 -0
- package/docs/strategy-example.md +299 -0
- package/docs/test-first-example.md +392 -0
- package/package.json +79 -0
- package/src/core/README.md +92 -0
- package/src/core/agent-instructions/backend-developer.md +412 -0
- package/src/core/agent-instructions/devops-engineer.md +372 -0
- package/src/core/agent-instructions/dhurandhar-council.md +547 -0
- package/src/core/agent-instructions/edge-case-hunter.md +322 -0
- package/src/core/agent-instructions/frontend-developer.md +494 -0
- package/src/core/agent-instructions/lead-system-architect.md +631 -0
- package/src/core/agent-instructions/system-observer.md +319 -0
- package/src/core/agent-instructions/test-architect.md +284 -0
- package/src/core/module.yaml +54 -0
- package/src/core/schemas/design-module-schema.yaml +995 -0
- package/src/core/schemas/system-design-map-schema.yaml +324 -0
- package/src/modules/example/README.md +130 -0
- package/src/modules/example/module.yaml +252 -0
- package/tools/cli/commands/audit.js +267 -0
- package/tools/cli/commands/config.js +113 -0
- package/tools/cli/commands/context.js +170 -0
- package/tools/cli/commands/decisions.js +398 -0
- package/tools/cli/commands/entity.js +218 -0
- package/tools/cli/commands/epic.js +125 -0
- package/tools/cli/commands/install.js +172 -0
- package/tools/cli/commands/module.js +109 -0
- package/tools/cli/commands/service.js +167 -0
- package/tools/cli/commands/story.js +225 -0
- package/tools/cli/commands/strategy.js +294 -0
- package/tools/cli/commands/test.js +277 -0
- package/tools/cli/commands/validate.js +107 -0
- package/tools/cli/dhurandhar.js +212 -0
- package/tools/lib/config-manager.js +170 -0
- package/tools/lib/filesystem.js +126 -0
- package/tools/lib/module-installer.js +61 -0
- package/tools/lib/module-manager.js +149 -0
- package/tools/lib/sdm-manager.js +982 -0
- package/tools/lib/test-engine.js +255 -0
- package/tools/lib/test-templates/api-client.template.js +100 -0
- package/tools/lib/test-templates/vitest.config.template.js +37 -0
- package/tools/lib/validators/config-validator.js +113 -0
- package/tools/lib/validators/module-validator.js +137 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decisions Command - Technical Decision Registry Management
|
|
3
|
+
* "Stock of Decisions" - Bmad-Method inspired decision management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as clack from '@clack/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { SDMManager } from '../../lib/sdm-manager.js';
|
|
9
|
+
import { ConfigManager } from '../../lib/config-manager.js';
|
|
10
|
+
|
|
11
|
+
export async function decisionsCommand(options) {
|
|
12
|
+
try {
|
|
13
|
+
const sdmManager = new SDMManager(process.cwd());
|
|
14
|
+
const configManager = new ConfigManager(process.cwd());
|
|
15
|
+
|
|
16
|
+
if (!configManager.exists()) {
|
|
17
|
+
clack.log.error(chalk.red('Framework not installed.'));
|
|
18
|
+
clack.log.info('Run: dhurandhar install');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (options.show) {
|
|
23
|
+
await showDecisions(sdmManager);
|
|
24
|
+
} else if (options.set) {
|
|
25
|
+
await setDecision(sdmManager, options.set);
|
|
26
|
+
} else if (options.stock) {
|
|
27
|
+
await stockAllDecisions(sdmManager);
|
|
28
|
+
} else {
|
|
29
|
+
console.log(chalk.cyan('Usage:'));
|
|
30
|
+
console.log(' dhurandhar decisions --show Show all registered decisions');
|
|
31
|
+
console.log(' dhurandhar decisions --set <category> Set decisions for category');
|
|
32
|
+
console.log(' dhurandhar decisions --stock Stock all decisions (full setup)');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
} catch (error) {
|
|
36
|
+
clack.log.error(chalk.red('Decision operation failed:'), error.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function showDecisions(sdmManager) {
|
|
42
|
+
const sdm = await sdmManager.load();
|
|
43
|
+
const decisions = sdm.decision_registry || {};
|
|
44
|
+
|
|
45
|
+
clack.intro(chalk.cyan.bold('📋 Technical Decision Registry'));
|
|
46
|
+
|
|
47
|
+
console.log('');
|
|
48
|
+
|
|
49
|
+
// Naming Conventions
|
|
50
|
+
if (decisions.naming_conventions) {
|
|
51
|
+
console.log(chalk.bold('Naming Conventions:'));
|
|
52
|
+
if (decisions.naming_conventions.case_styles) {
|
|
53
|
+
console.log(' Case Styles:');
|
|
54
|
+
Object.entries(decisions.naming_conventions.case_styles).forEach(([key, value]) => {
|
|
55
|
+
console.log(` ${key}: ${chalk.cyan(value)}`);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (decisions.naming_conventions.file_patterns) {
|
|
59
|
+
console.log(' File Patterns:');
|
|
60
|
+
Object.entries(decisions.naming_conventions.file_patterns).forEach(([key, value]) => {
|
|
61
|
+
console.log(` ${key}: ${chalk.cyan(value)}`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
console.log('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Design Patterns
|
|
68
|
+
if (decisions.design_patterns) {
|
|
69
|
+
console.log(chalk.bold('Design Patterns:'));
|
|
70
|
+
Object.entries(decisions.design_patterns).forEach(([key, value]) => {
|
|
71
|
+
if (key !== 'service_specific_patterns') {
|
|
72
|
+
console.log(` ${key}: ${chalk.cyan(value)}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (decisions.design_patterns.service_specific_patterns?.length > 0) {
|
|
76
|
+
console.log(' Service-Specific Patterns:');
|
|
77
|
+
decisions.design_patterns.service_specific_patterns.forEach(p => {
|
|
78
|
+
console.log(` ${chalk.bold(p.service)}: ${chalk.cyan(p.pattern)}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Error Standards
|
|
85
|
+
if (decisions.error_response_standards) {
|
|
86
|
+
console.log(chalk.bold('Error Response Standards:'));
|
|
87
|
+
if (decisions.error_response_standards.error_code_mapping) {
|
|
88
|
+
console.log(' HTTP Status Mapping:');
|
|
89
|
+
Object.entries(decisions.error_response_standards.error_code_mapping).forEach(([key, value]) => {
|
|
90
|
+
console.log(` ${key}: ${chalk.cyan(value)}`);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (decisions.error_response_standards.response_envelope) {
|
|
94
|
+
console.log(' Response Envelope:');
|
|
95
|
+
console.log(` Success: ${chalk.dim(decisions.error_response_standards.response_envelope.success_format)}`);
|
|
96
|
+
console.log(` Error: ${chalk.dim(decisions.error_response_standards.response_envelope.error_format)}`);
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Observability Standards
|
|
102
|
+
if (decisions.observability_standards) {
|
|
103
|
+
console.log(chalk.bold('Observability Standards:'));
|
|
104
|
+
if (decisions.observability_standards.logging) {
|
|
105
|
+
console.log(' Logging:');
|
|
106
|
+
console.log(` Structured: ${decisions.observability_standards.logging.structured_logging ? chalk.green('yes') : chalk.dim('no')}`);
|
|
107
|
+
if (decisions.observability_standards.logging.mandatory_fields) {
|
|
108
|
+
console.log(` Mandatory Fields: ${decisions.observability_standards.logging.mandatory_fields.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (decisions.observability_standards.tracing) {
|
|
112
|
+
console.log(' Tracing:');
|
|
113
|
+
console.log(` Trace ID Header: ${chalk.cyan(decisions.observability_standards.tracing.trace_id_header)}`);
|
|
114
|
+
console.log(` Correlation ID Header: ${chalk.cyan(decisions.observability_standards.tracing.correlation_id_header)}`);
|
|
115
|
+
}
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
clack.outro('');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function setDecision(sdmManager, category) {
|
|
123
|
+
clack.intro(chalk.cyan.bold(`Set ${category} Decisions`));
|
|
124
|
+
|
|
125
|
+
let decisions = {};
|
|
126
|
+
|
|
127
|
+
switch (category) {
|
|
128
|
+
case 'naming':
|
|
129
|
+
decisions = await defineNamingConventions();
|
|
130
|
+
break;
|
|
131
|
+
case 'patterns':
|
|
132
|
+
decisions = await defineDesignPatterns();
|
|
133
|
+
break;
|
|
134
|
+
case 'errors':
|
|
135
|
+
decisions = await defineErrorStandards();
|
|
136
|
+
break;
|
|
137
|
+
case 'observability':
|
|
138
|
+
decisions = await defineObservabilityStandards();
|
|
139
|
+
break;
|
|
140
|
+
default:
|
|
141
|
+
clack.log.error(`Unknown category: ${category}`);
|
|
142
|
+
clack.log.info('Valid: naming, patterns, errors, observability');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Save decisions
|
|
147
|
+
const sdm = await sdmManager.load();
|
|
148
|
+
if (!sdm.decision_registry) {
|
|
149
|
+
sdm.decision_registry = {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const categoryKey = getCategoryKey(category);
|
|
153
|
+
sdm.decision_registry[categoryKey] = decisions;
|
|
154
|
+
|
|
155
|
+
await sdmManager.save(sdm);
|
|
156
|
+
|
|
157
|
+
clack.outro(chalk.green(`✓ ${category} decisions registered`));
|
|
158
|
+
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(chalk.cyan('These decisions will be automatically applied to:'));
|
|
161
|
+
console.log(' - All new services');
|
|
162
|
+
console.log(' - All new entities');
|
|
163
|
+
console.log(' - All new stories');
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function stockAllDecisions(sdmManager) {
|
|
168
|
+
clack.intro(chalk.cyan.bold('📦 Stock All Technical Decisions'));
|
|
169
|
+
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(chalk.dim('This will set up your complete technical decision registry.'));
|
|
172
|
+
console.log(chalk.dim('Answer once, apply everywhere.'));
|
|
173
|
+
console.log('');
|
|
174
|
+
|
|
175
|
+
// Stock naming conventions
|
|
176
|
+
console.log(chalk.bold('1/4: Naming Conventions'));
|
|
177
|
+
const naming = await defineNamingConventions();
|
|
178
|
+
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(chalk.bold('2/4: Design Patterns'));
|
|
181
|
+
const patterns = await defineDesignPatterns();
|
|
182
|
+
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log(chalk.bold('3/4: Error Standards'));
|
|
185
|
+
const errors = await defineErrorStandards();
|
|
186
|
+
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(chalk.bold('4/4: Observability Standards'));
|
|
189
|
+
const observability = await defineObservabilityStandards();
|
|
190
|
+
|
|
191
|
+
// Save all decisions
|
|
192
|
+
const sdm = await sdmManager.load();
|
|
193
|
+
sdm.decision_registry = {
|
|
194
|
+
naming_conventions: naming,
|
|
195
|
+
design_patterns: patterns,
|
|
196
|
+
error_response_standards: errors,
|
|
197
|
+
observability_standards: observability,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await sdmManager.save(sdm);
|
|
201
|
+
|
|
202
|
+
clack.outro(chalk.green('✓ All decisions stocked'));
|
|
203
|
+
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(chalk.cyan('Decision registry complete. These standards will be:'));
|
|
206
|
+
console.log(' - Automatically applied to all new code');
|
|
207
|
+
console.log(' - Enforced by drift detection');
|
|
208
|
+
console.log(' - Used as constraints by implementation personas');
|
|
209
|
+
console.log('');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Helper functions
|
|
213
|
+
|
|
214
|
+
function getCategoryKey(category) {
|
|
215
|
+
const mapping = {
|
|
216
|
+
'naming': 'naming_conventions',
|
|
217
|
+
'patterns': 'design_patterns',
|
|
218
|
+
'errors': 'error_response_standards',
|
|
219
|
+
'observability': 'observability_standards',
|
|
220
|
+
};
|
|
221
|
+
return mapping[category] || category;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function defineNamingConventions() {
|
|
225
|
+
const classes = await clack.select({
|
|
226
|
+
message: 'Class naming style:',
|
|
227
|
+
options: [
|
|
228
|
+
{ value: 'PascalCase', label: 'PascalCase', hint: 'UserService, OrderController' },
|
|
229
|
+
{ value: 'camelCase', label: 'camelCase', hint: 'userService, orderController' },
|
|
230
|
+
{ value: 'snake_case', label: 'snake_case', hint: 'user_service, order_controller' },
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const functions = await clack.select({
|
|
235
|
+
message: 'Function naming style:',
|
|
236
|
+
options: [
|
|
237
|
+
{ value: 'camelCase', label: 'camelCase', hint: 'getUserById, createOrder' },
|
|
238
|
+
{ value: 'snake_case', label: 'snake_case', hint: 'get_user_by_id, create_order' },
|
|
239
|
+
{ value: 'PascalCase', label: 'PascalCase', hint: 'GetUserById, CreateOrder' },
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const dbTables = await clack.select({
|
|
244
|
+
message: 'Database table naming:',
|
|
245
|
+
options: [
|
|
246
|
+
{ value: 'snake_case', label: 'snake_case', hint: 'users, order_items' },
|
|
247
|
+
{ value: 'PascalCase', label: 'PascalCase', hint: 'Users, OrderItems' },
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const apiEndpoints = await clack.select({
|
|
252
|
+
message: 'API endpoint naming:',
|
|
253
|
+
options: [
|
|
254
|
+
{ value: 'kebab-case', label: 'kebab-case', hint: '/api/v1/user-profiles' },
|
|
255
|
+
{ value: 'snake_case', label: 'snake_case', hint: '/api/v1/user_profiles' },
|
|
256
|
+
{ value: 'camelCase', label: 'camelCase', hint: '/api/v1/userProfiles' },
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
case_styles: {
|
|
262
|
+
classes,
|
|
263
|
+
functions,
|
|
264
|
+
variables: functions, // Same as functions by default
|
|
265
|
+
constants: 'SCREAMING_SNAKE_CASE',
|
|
266
|
+
database_tables: dbTables,
|
|
267
|
+
database_columns: dbTables,
|
|
268
|
+
api_endpoints: apiEndpoints,
|
|
269
|
+
},
|
|
270
|
+
file_patterns: {
|
|
271
|
+
components: classes === 'PascalCase' ? 'ComponentName.tsx' : 'component-name.tsx',
|
|
272
|
+
services: classes === 'PascalCase' ? 'ServiceName.service.ts' : 'service-name.service.ts',
|
|
273
|
+
models: classes === 'PascalCase' ? 'ModelName.model.ts' : 'model-name.model.ts',
|
|
274
|
+
tests: 'filename.test.ts',
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function defineDesignPatterns() {
|
|
280
|
+
const dataAccess = await clack.select({
|
|
281
|
+
message: 'Data access pattern:',
|
|
282
|
+
options: [
|
|
283
|
+
{ value: 'repository_pattern', label: 'Repository Pattern', hint: 'UserRepository abstraction' },
|
|
284
|
+
{ value: 'active_record', label: 'Active Record', hint: 'Models with DB methods' },
|
|
285
|
+
{ value: 'data_mapper', label: 'Data Mapper', hint: 'Separate mapper layer' },
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const di = await clack.select({
|
|
290
|
+
message: 'Dependency injection:',
|
|
291
|
+
options: [
|
|
292
|
+
{ value: 'constructor_injection', label: 'Constructor Injection', hint: 'Dependencies in constructor' },
|
|
293
|
+
{ value: 'property_injection', label: 'Property Injection', hint: 'Set after instantiation' },
|
|
294
|
+
{ value: 'none', label: 'None', hint: 'Direct imports/requires' },
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const validation = await clack.select({
|
|
299
|
+
message: 'Validation strategy:',
|
|
300
|
+
options: [
|
|
301
|
+
{ value: 'dto_validation', label: 'DTO Validation', hint: 'Validate at DTO layer' },
|
|
302
|
+
{ value: 'middleware_validation', label: 'Middleware Validation', hint: 'Validate in middleware' },
|
|
303
|
+
{ value: 'domain_validation', label: 'Domain Validation', hint: 'Validate in domain layer' },
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
data_access: dataAccess,
|
|
309
|
+
service_layer: 'service_classes',
|
|
310
|
+
dependency_injection: di,
|
|
311
|
+
validation,
|
|
312
|
+
cqrs: false,
|
|
313
|
+
event_sourcing: false,
|
|
314
|
+
domain_driven_design: false,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function defineErrorStandards() {
|
|
319
|
+
const envelopeChoice = await clack.select({
|
|
320
|
+
message: 'Error response format:',
|
|
321
|
+
options: [
|
|
322
|
+
{ value: 'standard', label: 'Standard Envelope', hint: '{"error": {"code": "", "message": ""}, "meta": {}}' },
|
|
323
|
+
{ value: 'simple', label: 'Simple', hint: '{"error": "message"}' },
|
|
324
|
+
],
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const includeRequestId = await clack.confirm({
|
|
328
|
+
message: 'Include request_id in all responses?',
|
|
329
|
+
initialValue: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const includeTimestamp = await clack.confirm({
|
|
333
|
+
message: 'Include timestamp in all responses?',
|
|
334
|
+
initialValue: true,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
let successFormat, errorFormat;
|
|
338
|
+
if (envelopeChoice === 'standard') {
|
|
339
|
+
successFormat = '{"data": {}, "meta": {"request_id": "", "timestamp": ""}}';
|
|
340
|
+
errorFormat = '{"error": {"code": "", "message": "", "details": []}, "meta": {"request_id": "", "timestamp": ""}}';
|
|
341
|
+
} else {
|
|
342
|
+
successFormat = '{"data": {}}';
|
|
343
|
+
errorFormat = '{"error": "message"}';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
error_code_mapping: {
|
|
348
|
+
validation_error: 400,
|
|
349
|
+
authentication_error: 401,
|
|
350
|
+
authorization_error: 403,
|
|
351
|
+
not_found_error: 404,
|
|
352
|
+
conflict_error: 409,
|
|
353
|
+
internal_error: 500,
|
|
354
|
+
},
|
|
355
|
+
response_envelope: {
|
|
356
|
+
success_format: successFormat,
|
|
357
|
+
error_format: errorFormat,
|
|
358
|
+
include_request_id: includeRequestId,
|
|
359
|
+
include_timestamp: includeTimestamp,
|
|
360
|
+
},
|
|
361
|
+
error_detail_level: 'standard',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function defineObservabilityStandards() {
|
|
366
|
+
const structuredLogging = await clack.confirm({
|
|
367
|
+
message: 'Use structured JSON logging?',
|
|
368
|
+
initialValue: true,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const traceIdHeader = await clack.text({
|
|
372
|
+
message: 'Trace ID header name:',
|
|
373
|
+
initialValue: 'X-Trace-ID',
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const correlationIdHeader = await clack.text({
|
|
377
|
+
message: 'Correlation ID header name:',
|
|
378
|
+
initialValue: 'X-Correlation-ID',
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
logging: {
|
|
383
|
+
levels: ['DEBUG', 'INFO', 'WARN', 'ERROR'],
|
|
384
|
+
structured_logging: structuredLogging,
|
|
385
|
+
mandatory_fields: ['trace_id', 'correlation_id', 'service_name', 'timestamp', 'level'],
|
|
386
|
+
sensitive_data_masking: true,
|
|
387
|
+
},
|
|
388
|
+
tracing: {
|
|
389
|
+
trace_id_header: traceIdHeader,
|
|
390
|
+
correlation_id_header: correlationIdHeader,
|
|
391
|
+
span_naming_format: 'service.operation',
|
|
392
|
+
},
|
|
393
|
+
metrics: {
|
|
394
|
+
naming_format: 'service_name_metric_name_unit',
|
|
395
|
+
mandatory_labels: ['service', 'environment', 'version'],
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity Command - Direct-Action Entity Management
|
|
3
|
+
* Define database entities and relationships without ceremony
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as clack from '@clack/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { SDMManager } from '../../lib/sdm-manager.js';
|
|
9
|
+
import { ConfigManager } from '../../lib/config-manager.js';
|
|
10
|
+
|
|
11
|
+
export async function entityCommand(options) {
|
|
12
|
+
try {
|
|
13
|
+
const sdmManager = new SDMManager(process.cwd());
|
|
14
|
+
const configManager = new ConfigManager(process.cwd());
|
|
15
|
+
|
|
16
|
+
if (!configManager.exists()) {
|
|
17
|
+
clack.log.error(chalk.red('Framework not installed.'));
|
|
18
|
+
clack.log.info('Run: dhurandhar install');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (options.list) {
|
|
23
|
+
// List entities
|
|
24
|
+
const sdm = await sdmManager.load();
|
|
25
|
+
|
|
26
|
+
clack.intro(chalk.cyan.bold(`Entities (${sdm.entities.length})`));
|
|
27
|
+
|
|
28
|
+
if (sdm.entities.length === 0) {
|
|
29
|
+
console.log(chalk.dim(' No entities defined yet.'));
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log(chalk.cyan('Add one:'));
|
|
32
|
+
console.log(' dhurandhar entity add User');
|
|
33
|
+
} else {
|
|
34
|
+
console.log('');
|
|
35
|
+
sdm.entities.forEach(e => {
|
|
36
|
+
console.log(chalk.bold(`${e.name}`));
|
|
37
|
+
console.log(` Attributes (${e.attributes.length}):`);
|
|
38
|
+
e.attributes.forEach(attr => {
|
|
39
|
+
const constraints = attr.constraints ? ` [${attr.constraints.join(', ')}]` : '';
|
|
40
|
+
console.log(` ${chalk.cyan(attr.name)}: ${attr.type}${constraints}`);
|
|
41
|
+
});
|
|
42
|
+
if (e.relationships && e.relationships.length > 0) {
|
|
43
|
+
console.log(` Relationships (${e.relationships.length}):`);
|
|
44
|
+
e.relationships.forEach(rel => {
|
|
45
|
+
console.log(` ${rel.type} → ${chalk.cyan(rel.target)}`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
clack.outro('');
|
|
53
|
+
|
|
54
|
+
} else if (options.add) {
|
|
55
|
+
// Add entity - DIRECT ACTION
|
|
56
|
+
clack.intro(chalk.cyan.bold('Add Entity'));
|
|
57
|
+
|
|
58
|
+
const entityName = typeof options.add === 'string' ? options.add : null;
|
|
59
|
+
|
|
60
|
+
let name = entityName;
|
|
61
|
+
|
|
62
|
+
// Question 1: Entity name
|
|
63
|
+
if (!name) {
|
|
64
|
+
name = await clack.text({
|
|
65
|
+
message: 'Entity name (PascalCase):',
|
|
66
|
+
placeholder: 'User',
|
|
67
|
+
validate: (v) => !v ? 'Required' : !/^[A-Z][a-zA-Z0-9]*$/.test(v) ? 'Use PascalCase' : undefined,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (clack.isCancel(name)) {
|
|
71
|
+
clack.cancel('Cancelled.');
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Auto-generate common attributes
|
|
77
|
+
const commonAttributes = [
|
|
78
|
+
{ name: 'id', type: 'uuid', constraints: ['primary_key', 'not_null'] },
|
|
79
|
+
{ name: 'created_at', type: 'timestamp', constraints: ['not_null'] },
|
|
80
|
+
{ name: 'updated_at', type: 'timestamp', constraints: ['not_null'] },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// Ask for custom attributes
|
|
84
|
+
const addCustom = await clack.confirm({
|
|
85
|
+
message: 'Add custom attributes now? (or edit YAML later)',
|
|
86
|
+
initialValue: false,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const customAttributes = [];
|
|
90
|
+
|
|
91
|
+
if (!clack.isCancel(addCustom) && addCustom) {
|
|
92
|
+
let addMore = true;
|
|
93
|
+
while (addMore) {
|
|
94
|
+
const attrName = await clack.text({
|
|
95
|
+
message: 'Attribute name:',
|
|
96
|
+
placeholder: 'email',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (clack.isCancel(attrName)) break;
|
|
100
|
+
|
|
101
|
+
const attrType = await clack.select({
|
|
102
|
+
message: `Type for ${attrName}:`,
|
|
103
|
+
options: [
|
|
104
|
+
{ value: 'varchar', label: 'varchar (string)' },
|
|
105
|
+
{ value: 'int', label: 'int' },
|
|
106
|
+
{ value: 'uuid', label: 'uuid' },
|
|
107
|
+
{ value: 'boolean', label: 'boolean' },
|
|
108
|
+
{ value: 'timestamp', label: 'timestamp' },
|
|
109
|
+
{ value: 'text', label: 'text (long string)' },
|
|
110
|
+
{ value: 'jsonb', label: 'jsonb (JSON)' },
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (clack.isCancel(attrType)) break;
|
|
115
|
+
|
|
116
|
+
customAttributes.push({
|
|
117
|
+
name: attrName,
|
|
118
|
+
type: attrType,
|
|
119
|
+
constraints: ['not_null'],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
addMore = await clack.confirm({
|
|
123
|
+
message: 'Add another attribute?',
|
|
124
|
+
initialValue: false,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (clack.isCancel(addMore)) break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Build entity
|
|
132
|
+
const entity = {
|
|
133
|
+
name,
|
|
134
|
+
table_name: name.replace(/([A-Z])/g, '_$1').toLowerCase().substring(1),
|
|
135
|
+
attributes: [...commonAttributes, ...customAttributes],
|
|
136
|
+
relationships: [],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Add to SDM
|
|
140
|
+
await sdmManager.addEntity(entity);
|
|
141
|
+
|
|
142
|
+
clack.outro(chalk.green(`✓ Entity "${name}" added to data model`));
|
|
143
|
+
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(chalk.cyan('Updated SYSTEM_DESIGN_MAP.yaml'));
|
|
146
|
+
console.log(chalk.dim('Add relationships with: dhurandhar entity relate'));
|
|
147
|
+
console.log('');
|
|
148
|
+
|
|
149
|
+
} else if (options.relate) {
|
|
150
|
+
// Add relationship between entities
|
|
151
|
+
const sdm = await sdmManager.load();
|
|
152
|
+
|
|
153
|
+
if (sdm.entities.length < 2) {
|
|
154
|
+
clack.log.error('Need at least 2 entities to create relationships.');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
clack.intro(chalk.cyan.bold('Add Relationship'));
|
|
159
|
+
|
|
160
|
+
const entityOptions = sdm.entities.map(e => ({ value: e.name, label: e.name }));
|
|
161
|
+
|
|
162
|
+
const from = await clack.select({
|
|
163
|
+
message: 'From entity:',
|
|
164
|
+
options: entityOptions,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (clack.isCancel(from)) {
|
|
168
|
+
clack.cancel('Cancelled.');
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const to = await clack.select({
|
|
173
|
+
message: 'To entity:',
|
|
174
|
+
options: entityOptions.filter(e => e.value !== from),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (clack.isCancel(to)) {
|
|
178
|
+
clack.cancel('Cancelled.');
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const relType = await clack.select({
|
|
183
|
+
message: 'Relationship type:',
|
|
184
|
+
options: [
|
|
185
|
+
{ value: 'one_to_many', label: 'One-to-Many', hint: `One ${from} has many ${to}` },
|
|
186
|
+
{ value: 'many_to_one', label: 'Many-to-One', hint: `Many ${from} belong to one ${to}` },
|
|
187
|
+
{ value: 'one_to_one', label: 'One-to-One' },
|
|
188
|
+
{ value: 'many_to_many', label: 'Many-to-Many', hint: 'Requires junction table' },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Update entity with relationship
|
|
193
|
+
const fromEntity = sdm.entities.find(e => e.name === from);
|
|
194
|
+
if (!fromEntity.relationships) fromEntity.relationships = [];
|
|
195
|
+
|
|
196
|
+
fromEntity.relationships.push({
|
|
197
|
+
type: relType,
|
|
198
|
+
target: to,
|
|
199
|
+
foreign_key: `${to.toLowerCase()}_id`,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await sdmManager.save(sdm);
|
|
203
|
+
|
|
204
|
+
clack.outro(chalk.green(`✓ Relationship added: ${from} → ${to}`));
|
|
205
|
+
|
|
206
|
+
} else {
|
|
207
|
+
// Show help
|
|
208
|
+
console.log(chalk.cyan('Usage:'));
|
|
209
|
+
console.log(' dhurandhar entity --list List all entities');
|
|
210
|
+
console.log(' dhurandhar entity --add User Add entity');
|
|
211
|
+
console.log(' dhurandhar entity --relate Add relationship');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
clack.log.error(chalk.red('Entity operation failed:'), error.message);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|