@ryuenn3123/agentic-senior-core 2.0.19 → 2.0.21
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/.agent-context/state/benchmark-comparison-schema.json +181 -0
- package/.cursorrules +1 -1
- package/.gemini/instructions.md +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/.windsurfrules +1 -1
- package/AGENTS.md +1 -1
- package/lib/cli/commands/init.mjs +123 -0
- package/lib/cli/commands/upgrade.mjs +16 -0
- package/lib/cli/compiler.mjs +31 -0
- package/lib/cli/project-scaffolder.mjs +465 -0
- package/lib/cli/templates/api-contract.md.id.tmpl +143 -0
- package/lib/cli/templates/api-contract.md.tmpl +143 -0
- package/lib/cli/templates/architecture-decision-record.md.id.tmpl +106 -0
- package/lib/cli/templates/architecture-decision-record.md.tmpl +106 -0
- package/lib/cli/templates/database-schema.md.id.tmpl +74 -0
- package/lib/cli/templates/database-schema.md.tmpl +74 -0
- package/lib/cli/templates/flow-overview.md.id.tmpl +118 -0
- package/lib/cli/templates/flow-overview.md.tmpl +119 -0
- package/lib/cli/templates/project-brief.md.id.tmpl +53 -0
- package/lib/cli/templates/project-brief.md.tmpl +53 -0
- package/lib/cli/utils.mjs +5 -1
- package/package.json +2 -2
- package/scripts/bump-version.mjs +101 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Scaffolder — Dynamic project documentation generator.
|
|
3
|
+
* Generates project-specific docs during init when the target folder is empty.
|
|
4
|
+
* Depends on: constants.mjs, utils.mjs
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import { CLI_VERSION } from './constants.mjs';
|
|
11
|
+
import { ensureDirectory, askChoice, toTitleCase, pathExists } from './utils.mjs';
|
|
12
|
+
|
|
13
|
+
const CURRENT_FILE_PATH = fileURLToPath(import.meta.url);
|
|
14
|
+
const CURRENT_DIRECTORY_PATH = path.dirname(CURRENT_FILE_PATH);
|
|
15
|
+
const TEMPLATES_DIRECTORY_PATH = path.join(CURRENT_DIRECTORY_PATH, 'templates');
|
|
16
|
+
const SUPPORTED_DOC_LANGUAGES = new Set(['en', 'id']);
|
|
17
|
+
const PROJECT_DOC_FILE_NAMES = [
|
|
18
|
+
'project-brief.md',
|
|
19
|
+
'architecture-decision-record.md',
|
|
20
|
+
'database-schema.md',
|
|
21
|
+
'api-contract.md',
|
|
22
|
+
'flow-overview.md',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const PROJECT_DOC_TEMPLATE_VERSION = '1.1.0';
|
|
26
|
+
|
|
27
|
+
const DOMAIN_CHOICES = [
|
|
28
|
+
'API service',
|
|
29
|
+
'Web application',
|
|
30
|
+
'Mobile app',
|
|
31
|
+
'CLI tool',
|
|
32
|
+
'Library / SDK',
|
|
33
|
+
'Other',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const DATABASE_CHOICES = [
|
|
37
|
+
'None (stateless service)',
|
|
38
|
+
'SQL (PostgreSQL, MySQL, SQLite)',
|
|
39
|
+
'NoSQL (MongoDB, Redis, DynamoDB)',
|
|
40
|
+
'Both (SQL primary + cache layer)',
|
|
41
|
+
'Other',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const AUTH_CHOICES = [
|
|
45
|
+
'None (public service)',
|
|
46
|
+
'JWT (stateless token auth)',
|
|
47
|
+
'OAuth 2.0 (third-party login)',
|
|
48
|
+
'Session-based (server-side sessions)',
|
|
49
|
+
'API Key (simple key auth)',
|
|
50
|
+
'Other',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export function normalizeDocsLanguage(rawDocsLanguage = 'en') {
|
|
54
|
+
const normalizedDocsLanguage = String(rawDocsLanguage || 'en').trim().toLowerCase();
|
|
55
|
+
return SUPPORTED_DOC_LANGUAGES.has(normalizedDocsLanguage) ? normalizedDocsLanguage : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveLocalizedTemplateFileName(templateFileName, docsLanguage) {
|
|
59
|
+
if (docsLanguage === 'en') {
|
|
60
|
+
return templateFileName;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return templateFileName.replace(/\.md\.tmpl$/i, `.md.${docsLanguage}.tmpl`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function resolveTemplateFilePath(templateFileName, docsLanguage) {
|
|
67
|
+
const localizedTemplateFileName = resolveLocalizedTemplateFileName(templateFileName, docsLanguage);
|
|
68
|
+
const localizedTemplateFilePath = path.join(TEMPLATES_DIRECTORY_PATH, localizedTemplateFileName);
|
|
69
|
+
|
|
70
|
+
if (await pathExists(localizedTemplateFilePath)) {
|
|
71
|
+
return localizedTemplateFilePath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const defaultTemplateFilePath = path.join(TEMPLATES_DIRECTORY_PATH, templateFileName);
|
|
75
|
+
if (await pathExists(defaultTemplateFilePath)) {
|
|
76
|
+
return defaultTemplateFilePath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Run the project discovery interview.
|
|
84
|
+
* Returns a structured object with all user responses.
|
|
85
|
+
*/
|
|
86
|
+
export async function runProjectDiscovery(userInterface) {
|
|
87
|
+
console.log('\n--- Project Discovery ---');
|
|
88
|
+
console.log('I will ask a few questions to generate project-specific documentation.');
|
|
89
|
+
console.log('This helps AI agents understand your project before writing code.\n');
|
|
90
|
+
|
|
91
|
+
const projectName = (await userInterface.question('Project name: ')).trim();
|
|
92
|
+
if (!projectName) {
|
|
93
|
+
throw new Error('Project name is required for documentation scaffolding.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const projectDescription = (await userInterface.question('One-line description: ')).trim() || `A ${projectName} project.`;
|
|
97
|
+
|
|
98
|
+
const domainSelection = await askChoice(
|
|
99
|
+
'Primary domain:',
|
|
100
|
+
DOMAIN_CHOICES,
|
|
101
|
+
userInterface
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
let primaryDomain = domainSelection;
|
|
105
|
+
if (domainSelection === 'Other') {
|
|
106
|
+
primaryDomain = (await userInterface.question('Describe your domain: ')).trim() || 'Custom domain';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const databaseSelection = await askChoice(
|
|
110
|
+
'Database needs:',
|
|
111
|
+
DATABASE_CHOICES,
|
|
112
|
+
userInterface
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
let databaseChoice = databaseSelection;
|
|
116
|
+
if (databaseSelection === 'Other') {
|
|
117
|
+
databaseChoice = (await userInterface.question('Describe your database setup: ')).trim() || 'Custom database';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const authSelection = await askChoice(
|
|
121
|
+
'Auth strategy:',
|
|
122
|
+
AUTH_CHOICES,
|
|
123
|
+
userInterface
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
let authStrategy = authSelection;
|
|
127
|
+
if (authSelection === 'Other') {
|
|
128
|
+
authStrategy = (await userInterface.question('Describe your auth setup: ')).trim() || 'Custom auth';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('\nList your key features (one per line, press Enter twice to finish):');
|
|
132
|
+
const features = [];
|
|
133
|
+
let consecutiveEmptyLineCount = 0;
|
|
134
|
+
|
|
135
|
+
while (features.length < 10) {
|
|
136
|
+
const featureLine = (await userInterface.question(` Feature ${features.length + 1}: `)).trim();
|
|
137
|
+
|
|
138
|
+
if (!featureLine) {
|
|
139
|
+
consecutiveEmptyLineCount += 1;
|
|
140
|
+
if (consecutiveEmptyLineCount >= 1 && features.length >= 1) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
consecutiveEmptyLineCount = 0;
|
|
147
|
+
features.push(featureLine);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (features.length === 0) {
|
|
151
|
+
features.push('Core functionality (define during development)');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const additionalContext = (await userInterface.question('\nAdditional context (optional, press Enter to skip): ')).trim()
|
|
155
|
+
|| 'No additional context provided.';
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
projectName,
|
|
159
|
+
projectDescription,
|
|
160
|
+
primaryDomain,
|
|
161
|
+
databaseChoice,
|
|
162
|
+
authStrategy,
|
|
163
|
+
features,
|
|
164
|
+
additionalContext,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Determine which documents to generate based on project discovery answers.
|
|
170
|
+
*/
|
|
171
|
+
export function resolveDocumentManifest(discoveryAnswers) {
|
|
172
|
+
const hasDatabase = !discoveryAnswers.databaseChoice.toLowerCase().startsWith('none');
|
|
173
|
+
const hasAuth = !discoveryAnswers.authStrategy.toLowerCase().startsWith('none');
|
|
174
|
+
const isApiOrWebDomain = ['API service', 'Web application'].includes(discoveryAnswers.primaryDomain)
|
|
175
|
+
|| discoveryAnswers.primaryDomain.toLowerCase().includes('api')
|
|
176
|
+
|| discoveryAnswers.primaryDomain.toLowerCase().includes('web');
|
|
177
|
+
|
|
178
|
+
const documentManifest = [
|
|
179
|
+
{ templateFileName: 'project-brief.md.tmpl', outputFileName: 'project-brief.md', alwaysInclude: true },
|
|
180
|
+
{ templateFileName: 'architecture-decision-record.md.tmpl', outputFileName: 'architecture-decision-record.md', alwaysInclude: true },
|
|
181
|
+
{ templateFileName: 'flow-overview.md.tmpl', outputFileName: 'flow-overview.md', alwaysInclude: true },
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
if (hasDatabase) {
|
|
185
|
+
documentManifest.push({
|
|
186
|
+
templateFileName: 'database-schema.md.tmpl',
|
|
187
|
+
outputFileName: 'database-schema.md',
|
|
188
|
+
alwaysInclude: false,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isApiOrWebDomain) {
|
|
193
|
+
documentManifest.push({
|
|
194
|
+
templateFileName: 'api-contract.md.tmpl',
|
|
195
|
+
outputFileName: 'api-contract.md',
|
|
196
|
+
alwaysInclude: false,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { documentManifest, hasDatabase, hasAuth };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Build template context from discovery answers and init selections.
|
|
205
|
+
*/
|
|
206
|
+
export function buildTemplateContext(discoveryAnswers, initContext) {
|
|
207
|
+
const hasDatabase = !discoveryAnswers.databaseChoice.toLowerCase().startsWith('none');
|
|
208
|
+
const hasAuth = !discoveryAnswers.authStrategy.toLowerCase().startsWith('none');
|
|
209
|
+
|
|
210
|
+
const baseUrlMap = {
|
|
211
|
+
'API service': 'http://localhost:3000',
|
|
212
|
+
'Web application': 'http://localhost:3000/api',
|
|
213
|
+
'Mobile app': 'http://localhost:3000/api',
|
|
214
|
+
'CLI tool': 'N/A',
|
|
215
|
+
'Library / SDK': 'N/A',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
projectName: discoveryAnswers.projectName,
|
|
220
|
+
projectDescription: discoveryAnswers.projectDescription,
|
|
221
|
+
primaryDomain: discoveryAnswers.primaryDomain,
|
|
222
|
+
databaseChoice: discoveryAnswers.databaseChoice,
|
|
223
|
+
authStrategy: discoveryAnswers.authStrategy,
|
|
224
|
+
features: discoveryAnswers.features,
|
|
225
|
+
additionalContext: discoveryAnswers.additionalContext,
|
|
226
|
+
stackFileName: initContext.stackFileName,
|
|
227
|
+
stackDisplayName: toTitleCase(initContext.stackFileName),
|
|
228
|
+
blueprintFileName: initContext.blueprintFileName,
|
|
229
|
+
blueprintDisplayName: toTitleCase(initContext.blueprintFileName),
|
|
230
|
+
cliVersion: CLI_VERSION,
|
|
231
|
+
templateVersion: PROJECT_DOC_TEMPLATE_VERSION,
|
|
232
|
+
generatedAt: new Date().toISOString(),
|
|
233
|
+
generatedDate: new Date().toISOString().split('T')[0],
|
|
234
|
+
hasDatabase,
|
|
235
|
+
hasAuth,
|
|
236
|
+
baseUrl: baseUrlMap[discoveryAnswers.primaryDomain] || 'http://localhost:3000',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Render a template string by replacing {{placeholder}} tokens with context values.
|
|
242
|
+
* Supports:
|
|
243
|
+
* - {{key}} for simple values
|
|
244
|
+
* - {{#each key}}...{{this}}...{{/each}} for arrays
|
|
245
|
+
* - {{#if key}}...{{/if}} for conditionals
|
|
246
|
+
*/
|
|
247
|
+
export function renderTemplate(templateContent, templateContext) {
|
|
248
|
+
let renderedContent = templateContent;
|
|
249
|
+
|
|
250
|
+
// Process {{#each key}}...{{/each}} blocks
|
|
251
|
+
renderedContent = renderedContent.replace(
|
|
252
|
+
/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
|
|
253
|
+
(_fullMatch, iteratorKey, iteratorBody) => {
|
|
254
|
+
const iteratorValues = templateContext[iteratorKey];
|
|
255
|
+
|
|
256
|
+
if (!Array.isArray(iteratorValues) || iteratorValues.length === 0) {
|
|
257
|
+
return '';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return iteratorValues
|
|
261
|
+
.map((iteratorValue) => iteratorBody.replace(/\{\{this\}\}/g, iteratorValue))
|
|
262
|
+
.join('');
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Process {{#if key}}...{{/if}} blocks
|
|
267
|
+
renderedContent = renderedContent.replace(
|
|
268
|
+
/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
|
|
269
|
+
(_fullMatch, conditionKey, conditionBody) => {
|
|
270
|
+
const conditionValue = templateContext[conditionKey];
|
|
271
|
+
return conditionValue ? conditionBody : '';
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Process simple {{key}} replacements
|
|
276
|
+
renderedContent = renderedContent.replace(
|
|
277
|
+
/\{\{(\w+)\}\}/g,
|
|
278
|
+
(_fullMatch, placeholderKey) => {
|
|
279
|
+
const placeholderValue = templateContext[placeholderKey];
|
|
280
|
+
|
|
281
|
+
if (placeholderValue === undefined || placeholderValue === null) {
|
|
282
|
+
return `{{${placeholderKey}}}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (Array.isArray(placeholderValue)) {
|
|
286
|
+
return placeholderValue.join(', ');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return String(placeholderValue);
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
return renderedContent;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Generate project documentation files from templates.
|
|
298
|
+
*/
|
|
299
|
+
export async function generateProjectDocumentation(
|
|
300
|
+
targetDirectoryPath,
|
|
301
|
+
discoveryAnswers,
|
|
302
|
+
initContext,
|
|
303
|
+
options = {}
|
|
304
|
+
) {
|
|
305
|
+
const normalizedDocsLanguage = normalizeDocsLanguage(options.docsLanguage || 'en');
|
|
306
|
+
if (!normalizedDocsLanguage) {
|
|
307
|
+
throw new Error(`Unsupported docs language: ${options.docsLanguage}. Supported values: en, id`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const docsDirectoryPath = path.join(targetDirectoryPath, 'docs');
|
|
311
|
+
await ensureDirectory(docsDirectoryPath);
|
|
312
|
+
|
|
313
|
+
const templateContext = buildTemplateContext(discoveryAnswers, initContext);
|
|
314
|
+
const { documentManifest } = resolveDocumentManifest(discoveryAnswers);
|
|
315
|
+
const generatedFileNames = [];
|
|
316
|
+
|
|
317
|
+
for (const documentEntry of documentManifest) {
|
|
318
|
+
const templateFilePath = await resolveTemplateFilePath(
|
|
319
|
+
documentEntry.templateFileName,
|
|
320
|
+
normalizedDocsLanguage
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (!templateFilePath) {
|
|
324
|
+
console.log(`[WARN] Template not found: ${documentEntry.templateFileName}, skipping.`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const templateContent = await fs.readFile(templateFilePath, 'utf8');
|
|
329
|
+
const renderedContent = renderTemplate(templateContent, templateContext);
|
|
330
|
+
const outputFilePath = path.join(docsDirectoryPath, documentEntry.outputFileName);
|
|
331
|
+
|
|
332
|
+
await fs.writeFile(outputFilePath, renderedContent, 'utf8');
|
|
333
|
+
generatedFileNames.push(documentEntry.outputFileName);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
docsDirectoryPath,
|
|
338
|
+
generatedFileNames,
|
|
339
|
+
templateVersion: PROJECT_DOC_TEMPLATE_VERSION,
|
|
340
|
+
docsLanguage: normalizedDocsLanguage,
|
|
341
|
+
discoveryAnswers,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Check if the target directory qualifies as "empty" for scaffolding purposes.
|
|
347
|
+
* A directory with only .git is still considered empty.
|
|
348
|
+
*/
|
|
349
|
+
export async function isDirectoryEffectivelyEmpty(targetDirectoryPath) {
|
|
350
|
+
try {
|
|
351
|
+
const directoryEntries = await fs.readdir(targetDirectoryPath);
|
|
352
|
+
const meaningfulEntries = directoryEntries.filter(
|
|
353
|
+
(entryName) => entryName !== '.git' && entryName !== '.gitignore'
|
|
354
|
+
);
|
|
355
|
+
return meaningfulEntries.length === 0;
|
|
356
|
+
} catch {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Check if project docs already exist in the target directory.
|
|
363
|
+
*/
|
|
364
|
+
export async function hasExistingProjectDocs(targetDirectoryPath) {
|
|
365
|
+
const projectBriefPath = path.join(targetDirectoryPath, 'docs', 'project-brief.md');
|
|
366
|
+
return pathExists(projectBriefPath);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function extractTemplateVersion(documentContent) {
|
|
370
|
+
const templateVersionMatch = documentContent.match(/^(?:Template version|Versi template):\s*(.+)$/im);
|
|
371
|
+
return templateVersionMatch ? templateVersionMatch[1].trim() : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function detectProjectDocTemplateStaleness(targetDirectoryPath) {
|
|
375
|
+
const docsDirectoryPath = path.join(targetDirectoryPath, 'docs');
|
|
376
|
+
const checkedFileNames = [];
|
|
377
|
+
const staleFiles = [];
|
|
378
|
+
|
|
379
|
+
for (const projectDocFileName of PROJECT_DOC_FILE_NAMES) {
|
|
380
|
+
const projectDocFilePath = path.join(docsDirectoryPath, projectDocFileName);
|
|
381
|
+
if (!(await pathExists(projectDocFilePath))) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
checkedFileNames.push(projectDocFileName);
|
|
386
|
+
const projectDocContent = await fs.readFile(projectDocFilePath, 'utf8');
|
|
387
|
+
const detectedTemplateVersion = extractTemplateVersion(projectDocContent);
|
|
388
|
+
|
|
389
|
+
if (!detectedTemplateVersion || detectedTemplateVersion !== PROJECT_DOC_TEMPLATE_VERSION) {
|
|
390
|
+
staleFiles.push({
|
|
391
|
+
fileName: projectDocFileName,
|
|
392
|
+
detectedTemplateVersion,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
hasProjectDocs: checkedFileNames.length > 0,
|
|
399
|
+
expectedTemplateVersion: PROJECT_DOC_TEMPLATE_VERSION,
|
|
400
|
+
checkedFileNames,
|
|
401
|
+
staleFiles,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Load project config from a YAML-like file for non-interactive mode.
|
|
407
|
+
* Uses a simple key: value format (one per line) for zero-dependency parsing.
|
|
408
|
+
*/
|
|
409
|
+
export async function loadProjectConfig(configFilePath) {
|
|
410
|
+
const configContent = await fs.readFile(configFilePath, 'utf8');
|
|
411
|
+
const configLines = configContent.split(/\r?\n/);
|
|
412
|
+
const configEntries = {};
|
|
413
|
+
let currentKey = null;
|
|
414
|
+
let currentArrayValues = null;
|
|
415
|
+
|
|
416
|
+
for (const configLine of configLines) {
|
|
417
|
+
const trimmedLine = configLine.trim();
|
|
418
|
+
|
|
419
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (trimmedLine.startsWith('- ') && currentKey && currentArrayValues !== null) {
|
|
424
|
+
currentArrayValues.push(trimmedLine.slice(2).trim());
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (currentKey && currentArrayValues !== null) {
|
|
429
|
+
configEntries[currentKey] = currentArrayValues;
|
|
430
|
+
currentKey = null;
|
|
431
|
+
currentArrayValues = null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const colonIndex = trimmedLine.indexOf(':');
|
|
435
|
+
if (colonIndex === -1) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const entryKey = trimmedLine.slice(0, colonIndex).trim();
|
|
440
|
+
const entryValue = trimmedLine.slice(colonIndex + 1).trim();
|
|
441
|
+
|
|
442
|
+
if (!entryValue) {
|
|
443
|
+
currentKey = entryKey;
|
|
444
|
+
currentArrayValues = [];
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
configEntries[entryKey] = entryValue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (currentKey && currentArrayValues !== null) {
|
|
452
|
+
configEntries[currentKey] = currentArrayValues;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
projectName: configEntries.projectName || configEntries.name || '',
|
|
457
|
+
projectDescription: configEntries.projectDescription || configEntries.description || '',
|
|
458
|
+
primaryDomain: configEntries.primaryDomain || configEntries.domain || 'API service',
|
|
459
|
+
databaseChoice: configEntries.databaseChoice || configEntries.database || 'None (stateless service)',
|
|
460
|
+
authStrategy: configEntries.authStrategy || configEntries.auth || 'None (public service)',
|
|
461
|
+
features: Array.isArray(configEntries.features) ? configEntries.features : [],
|
|
462
|
+
additionalContext: configEntries.additionalContext || configEntries.context || 'No additional context provided.',
|
|
463
|
+
docsLang: configEntries.docsLang || configEntries.docsLanguage || 'en',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Kontrak API: {{projectName}}
|
|
2
|
+
|
|
3
|
+
Dibuat oleh Agentic-Senior-Core CLI v{{cliVersion}}
|
|
4
|
+
Dibuat pada: {{generatedAt}}
|
|
5
|
+
Versi template: {{templateVersion}}
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Prinsip Desain API
|
|
10
|
+
|
|
11
|
+
Mengacu ke `.agent-context/rules/api-docs.md`:
|
|
12
|
+
- OpenAPI 3.1 wajib untuk semua HTTP API.
|
|
13
|
+
- Setiap endpoint harus mendokumentasikan skema request/response.
|
|
14
|
+
- Error response harus pakai error code terstruktur, bukan string mentah.
|
|
15
|
+
- Validasi input wajib di semua endpoint.
|
|
16
|
+
|
|
17
|
+
## Base URL
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
{{baseUrl}}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Endpoint
|
|
26
|
+
|
|
27
|
+
### Health Check
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
GET /health
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Response `200`:
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"status": "ok",
|
|
37
|
+
"version": "1.0.0",
|
|
38
|
+
"timestamp": "2026-01-01T00:00:00Z"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
{{#if hasAuth}}
|
|
43
|
+
### Autentikasi
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
POST /auth/register
|
|
47
|
+
```
|
|
48
|
+
Request:
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"email": "user@example.com",
|
|
52
|
+
"password": "secure-password",
|
|
53
|
+
"displayName": "User Name"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
POST /auth/login
|
|
59
|
+
```
|
|
60
|
+
Request:
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"email": "user@example.com",
|
|
64
|
+
"password": "secure-password"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
Response `200`:
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"token": "jwt-token-here",
|
|
71
|
+
"expiresIn": 3600
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
{{/if}}
|
|
76
|
+
### Endpoint Fitur
|
|
77
|
+
|
|
78
|
+
Berdasarkan fitur proyek, rencanakan grup endpoint berikut:
|
|
79
|
+
|
|
80
|
+
{{#each features}}
|
|
81
|
+
#### {{this}}
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
GET /api/[resource] - List
|
|
85
|
+
GET /api/[resource]/:id - Detail
|
|
86
|
+
POST /api/[resource] - Create
|
|
87
|
+
PUT /api/[resource]/:id - Update
|
|
88
|
+
DELETE /api/[resource]/:id - Delete
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
{{/each}}
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Format Error Response
|
|
96
|
+
|
|
97
|
+
Semua error response mengikuti struktur ini:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"error": {
|
|
102
|
+
"code": "VALIDATION_ERROR",
|
|
103
|
+
"message": "Deskripsi yang bisa dibaca manusia",
|
|
104
|
+
"details": []
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Kode error standar:
|
|
110
|
+
- `VALIDATION_ERROR` (400)
|
|
111
|
+
- `UNAUTHORIZED` (401)
|
|
112
|
+
- `FORBIDDEN` (403)
|
|
113
|
+
- `NOT_FOUND` (404)
|
|
114
|
+
- `CONFLICT` (409)
|
|
115
|
+
- `INTERNAL_ERROR` (500)
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Pagination
|
|
120
|
+
|
|
121
|
+
Endpoint list menggunakan cursor-based atau offset-based pagination:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
GET /api/[resource]?page=1&limit=20
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Response mencakup:
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"data": [],
|
|
131
|
+
"pagination": {
|
|
132
|
+
"page": 1,
|
|
133
|
+
"limit": 20,
|
|
134
|
+
"total": 100,
|
|
135
|
+
"totalPages": 5
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
Dokumen ini adalah referensi hidup. Perbarui saat endpoint API diimplementasikan.
|
|
143
|
+
Jaga file OpenAPI 3.1 tetap sinkron dengan dokumen ini.
|