aiwf 0.3.14 → 0.3.16

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.
Files changed (54) hide show
  1. package/README.ko.md +19 -10
  2. package/README.md +54 -9
  3. package/docs/AI-WORKFLOW.ko.md +1 -1
  4. package/docs/AI-WORKFLOW.md +1 -1
  5. package/docs/API_REFERENCE_FULL.ko.md +979 -0
  6. package/docs/API_REFERENCE_FULL.md +979 -0
  7. package/docs/ARCHITECTURE.ko.md +314 -0
  8. package/docs/ARCHITECTURE.md +314 -0
  9. package/docs/GETTING_STARTED.ko.md +219 -0
  10. package/docs/GETTING_STARTED.md +1 -2
  11. package/docs/PERFORMANCE_GUIDELINES.ko.md +388 -0
  12. package/docs/ROADMAP_v0.4.0.md +286 -0
  13. package/docs/TROUBLESHOOTING.ko.md +366 -0
  14. package/package.json +50 -2
  15. package/src/DEPENDENCY_MAP.md +90 -0
  16. package/src/cli/index.js +1 -1
  17. package/src/lib/installer.js +204 -15
  18. package/src/lib/resources/templates/api-server/template/src/controllers/aiwfController.ts +0 -14
  19. package/src/lib/resources/templates/api-server/template/src/routes/aiwf.ts +0 -11
  20. package/src/lib/resources/templates/web-app/template/src/pages/AiwfDashboard.tsx +2 -5
  21. package/src/lib/resources/utils/token-reporter.js +2 -3
  22. package/src/utils/checkpoint-manager.js +11 -1
  23. package/src/utils/engineering-guard.js +11 -1
  24. package/src/utils/messages.js +16 -0
  25. package/src/utils/paths.js +2 -2
  26. package/templates/api-server/template/src/controllers/aiwfController.ts +0 -14
  27. package/templates/api-server/template/src/routes/aiwf.ts +0 -11
  28. package/templates/web-app/template/src/pages/AiwfDashboard.tsx +2 -5
  29. package/docs/CLEANUP_RECOMMENDATIONS.md +0 -86
  30. package/docs/git-feature-integration-guide.md +0 -253
  31. package/docs/guides/feature-git-integration-guide-ko.md +0 -481
  32. package/docs/guides/feature-git-integration-guide.md +0 -481
  33. package/docs/lightweight-evaluation.md +0 -198
  34. package/docs/performance-benchmark-report.md +0 -159
  35. package/docs/reports/S02-sprint-report.md +0 -190
  36. package/docs/usability-test-report.md +0 -170
  37. package/src/commands/cache-templates.js +0 -209
  38. package/src/commands/create-offline.js +0 -86
  39. package/src/commands/state-refactored.js +0 -419
  40. package/src/lib/resources/commands/feature-ledger.js +0 -565
  41. package/src/lib/resources/commands/feature_commit_report.js +0 -370
  42. package/src/lib/resources/commands/scan_git_history.js +0 -234
  43. package/src/lib/resources/commands/sync_feature_commits.js +0 -154
  44. package/src/lib/resources/templates/web-app/template/src/components/aiwf/FeatureLedger.tsx +0 -93
  45. package/src/lib/resources/utils/feature-updater.js +0 -271
  46. package/src/utils/git-integration.js +0 -173
  47. package/templates/web-app/template/src/components/aiwf/FeatureLedger.tsx +0 -93
  48. /package/docs/{AI_PERSONA_SYSTEM_DESIGN.md → designs/AI_PERSONA_SYSTEM_DESIGN.md} +0 -0
  49. /package/docs/{API_DOCUMENTATION.md → designs/API_DOCUMENTATION.md} +0 -0
  50. /package/docs/{API_REFERENCE.md → designs/API_REFERENCE.md} +0 -0
  51. /package/docs/{Enhanced_Installation_Flow_Design.md → designs/Enhanced_Installation_Flow_Design.md} +0 -0
  52. /package/docs/{moonklabs-metadata-system-prd.md → designs/aiwf-metadata-system-prd.md} +0 -0
  53. /package/docs/{offline-template-cache.md → designs/offline-template-cache.md} +0 -0
  54. /package/docs/{persona-aware-compression.md → designs/persona-aware-compression.md} +0 -0
@@ -1,565 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Feature Ledger 관리 명령어 (한국어)
5
- * Feature 생성, 업데이트, 조회, 연결 기능 제공
6
- */
7
-
8
- import fs from 'fs';
9
- import path from 'path';
10
- import chalk from 'chalk';
11
- import prompts from 'prompts';
12
-
13
- // 상수 정의
14
- const FEATURE_STATES = {
15
- active: '개발 중',
16
- completed: '완료',
17
- paused: '일시 중지',
18
- archived: '보관됨'
19
- };
20
-
21
- const PRIORITIES = {
22
- critical: '긴급',
23
- high: '높음',
24
- medium: '보통',
25
- low: '낮음'
26
- };
27
-
28
- const CATEGORIES = {
29
- feature: '기능',
30
- enhancement: '개선',
31
- bugfix: '버그 수정',
32
- refactor: '리팩토링'
33
- };
34
-
35
- // 헬퍼 함수들
36
- function getProjectRoot() {
37
- const cwd = process.cwd();
38
- return cwd.endsWith('.aiwf') ? path.dirname(cwd) : cwd;
39
- }
40
-
41
- function getFeatureLedgerPath() {
42
- return path.join(getProjectRoot(), '.aiwf', '06_FEATURE_LEDGERS');
43
- }
44
-
45
- function getNextFeatureId() {
46
- const ledgerPath = getFeatureLedgerPath();
47
- const activePath = path.join(ledgerPath, 'active');
48
-
49
- if (!fs.existsSync(activePath)) {
50
- fs.mkdirSync(activePath, { recursive: true });
51
- }
52
-
53
- const files = fs.readdirSync(activePath).filter(f => f.match(/^FL\d{3}/));
54
- if (files.length === 0) return 'FL001';
55
-
56
- const ids = files.map(f => parseInt(f.match(/FL(\d{3})/)[1]));
57
- const nextId = Math.max(...ids) + 1;
58
- return `FL${String(nextId).padStart(3, '0')}`;
59
- }
60
-
61
- function sanitizeFilename(name) {
62
- return name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
63
- }
64
-
65
- // Feature 생성
66
- async function createFeature(name) {
67
- try {
68
- // 대화형 모드
69
- if (!name) {
70
- const response = await prompts([
71
- {
72
- type: 'text',
73
- name: 'title',
74
- message: 'Feature 제목을 입력하세요:',
75
- validate: value => value.length > 0 || '제목은 필수입니다'
76
- },
77
- {
78
- type: 'text',
79
- name: 'description',
80
- message: 'Feature 설명을 입력하세요:'
81
- },
82
- {
83
- type: 'select',
84
- name: 'priority',
85
- message: '우선순위를 선택하세요:',
86
- choices: Object.entries(PRIORITIES).map(([value, title]) => ({
87
- title: title,
88
- value: value
89
- }))
90
- },
91
- {
92
- type: 'select',
93
- name: 'category',
94
- message: '카테고리를 선택하세요:',
95
- choices: Object.entries(CATEGORIES).map(([value, title]) => ({
96
- title: title,
97
- value: value
98
- }))
99
- }
100
- ]);
101
-
102
- if (!response.title) return;
103
-
104
- name = response.title;
105
- var { description, priority, category } = response;
106
- } else {
107
- // 기본값 설정
108
- var description = '';
109
- var priority = 'medium';
110
- var category = 'feature';
111
- }
112
-
113
- const featureId = getNextFeatureId();
114
- const filename = `${featureId}_${sanitizeFilename(name)}.md`;
115
- const filepath = path.join(getFeatureLedgerPath(), 'active', filename);
116
-
117
- // Feature 파일 내용
118
- const content = `# ${featureId}: ${name}
119
-
120
- ## 메타데이터
121
-
122
- - **Feature ID**: ${featureId}
123
- - **제목**: ${name}
124
- - **상태**: active
125
- - **우선순위**: ${priority}
126
- - **카테고리**: ${category}
127
- - **생성일**: ${new Date().toISOString()}
128
- - **업데이트**: ${new Date().toISOString()}
129
-
130
- ## 설명
131
-
132
- ${description || '(설명을 추가해주세요)'}
133
-
134
- ## 진행 상황
135
-
136
- - **진행률**: 0%
137
- - **시작일**: ${new Date().toISOString().split('T')[0]}
138
- - **예상 완료일**: TBD
139
-
140
- ## 마일스톤 & 스프린트
141
-
142
- - **마일스톤**: (미지정)
143
- - **스프린트**: (미지정)
144
-
145
- ## 태스크
146
-
147
- - [ ] 초기 설계
148
- - [ ] 구현
149
- - [ ] 테스트
150
- - [ ] 문서화
151
-
152
- ## 기술 스택
153
-
154
- (사용할 기술 스택을 명시해주세요)
155
-
156
- ## 진행 내역
157
-
158
- - [${new Date().toISOString().split('T')[0]}] Feature 생성
159
-
160
- ## 관련 링크
161
-
162
- - Git 브랜치: \`feature/${featureId}-${sanitizeFilename(name)}\`
163
- - PR: (미생성)
164
- - 이슈: (미생성)
165
- `;
166
-
167
- // 파일 생성
168
- fs.writeFileSync(filepath, content);
169
-
170
- // 인덱스 업데이트
171
- await updateFeatureIndex();
172
-
173
- console.log(chalk.green('🎯 Feature Ledger 생성 완료!'));
174
- console.log(chalk.gray('━'.repeat(50)));
175
- console.log(`Feature ID: ${chalk.yellow(featureId)}`);
176
- console.log(`제목: ${name}`);
177
- console.log(`상태: ${chalk.green('active')}`);
178
- console.log(`우선순위: ${priority}`);
179
- console.log(`카테고리: ${category}`);
180
- console.log('');
181
- console.log(`📁 파일 위치: ${chalk.blue(path.relative(getProjectRoot(), filepath))}`);
182
- console.log('');
183
- console.log(chalk.yellow('💡 다음 단계:'));
184
- console.log(`git checkout -b feature/${featureId}-${sanitizeFilename(name)}`);
185
-
186
- } catch (error) {
187
- console.error(chalk.red(`❌ 오류: ${error.message}`));
188
- }
189
- }
190
-
191
- // Feature 상태 업데이트
192
- async function updateFeatureStatus(featureId, newStatus) {
193
- try {
194
- // 대화형 모드
195
- if (!featureId) {
196
- const features = await getActiveFeatures();
197
- if (features.length === 0) {
198
- console.log(chalk.yellow('활성 Feature가 없습니다.'));
199
- return;
200
- }
201
-
202
- const response = await prompts([
203
- {
204
- type: 'select',
205
- name: 'featureId',
206
- message: 'Feature를 선택하세요:',
207
- choices: features.map(f => ({
208
- title: `${f.id} - ${f.title}`,
209
- value: f.id
210
- }))
211
- },
212
- {
213
- type: 'select',
214
- name: 'newStatus',
215
- message: '새로운 상태를 선택하세요:',
216
- choices: Object.entries(FEATURE_STATES).map(([value, title]) => ({
217
- title: title,
218
- value: value
219
- }))
220
- }
221
- ]);
222
-
223
- if (!response.featureId) return;
224
-
225
- featureId = response.featureId;
226
- newStatus = response.newStatus;
227
- }
228
-
229
- // Feature 파일 찾기
230
- const feature = await findFeature(featureId);
231
- if (!feature) {
232
- console.log(chalk.red(`❌ Feature ${featureId}를 찾을 수 없습니다.`));
233
- return;
234
- }
235
-
236
- // 상태 전이 검증
237
- const validTransitions = {
238
- active: ['completed', 'paused', 'archived'],
239
- paused: ['active', 'archived'],
240
- completed: ['archived'],
241
- archived: []
242
- };
243
-
244
- if (!validTransitions[feature.status].includes(newStatus)) {
245
- console.log(chalk.red(`❌ ${feature.status} → ${newStatus} 전이는 허용되지 않습니다.`));
246
- return;
247
- }
248
-
249
- // 파일 이동 (필요한 경우)
250
- const oldPath = feature.path;
251
- let newPath = oldPath;
252
-
253
- if (newStatus === 'archived') {
254
- const archivePath = path.join(getFeatureLedgerPath(), 'archived');
255
- if (!fs.existsSync(archivePath)) {
256
- fs.mkdirSync(archivePath, { recursive: true });
257
- }
258
- newPath = path.join(archivePath, path.basename(oldPath));
259
- fs.renameSync(oldPath, newPath);
260
- }
261
-
262
- // 파일 내용 업데이트
263
- let content = fs.readFileSync(newPath, 'utf8');
264
- content = content.replace(/- \*\*상태\*\*: \w+/, `- **상태**: ${newStatus}`);
265
- content = content.replace(/- \*\*업데이트\*\*: .+/, `- **업데이트**: ${new Date().toISOString()}`);
266
-
267
- // 진행 내역 추가
268
- const historyMarker = '## 진행 내역';
269
- const historyIndex = content.indexOf(historyMarker);
270
- if (historyIndex !== -1) {
271
- const nextSection = content.indexOf('\n##', historyIndex + 1);
272
- const insertPoint = nextSection !== -1 ? nextSection : content.length;
273
- const historyEntry = `\n- [${new Date().toISOString().split('T')[0]}] 상태 변경: ${feature.status} → ${newStatus}`;
274
- content = content.slice(0, insertPoint) + historyEntry + content.slice(insertPoint);
275
- }
276
-
277
- fs.writeFileSync(newPath, content);
278
-
279
- // 인덱스 업데이트
280
- await updateFeatureIndex();
281
-
282
- console.log(chalk.green('✅ Feature 상태 업데이트 완료'));
283
- console.log(chalk.gray('━'.repeat(50)));
284
- console.log(`Feature ID: ${featureId}`);
285
- console.log(`이전 상태: ${feature.status} → 새 상태: ${chalk.yellow(newStatus)}`);
286
- console.log(`업데이트 시간: ${new Date().toLocaleString('ko-KR')}`);
287
-
288
- } catch (error) {
289
- console.error(chalk.red(`❌ 오류: ${error.message}`));
290
- }
291
- }
292
-
293
- // Feature 목록 조회
294
- async function listFeatures(options = {}) {
295
- try {
296
- const features = await getAllFeatures();
297
-
298
- // 필터링
299
- let filtered = features;
300
-
301
- if (options.status && options.status !== 'all') {
302
- filtered = filtered.filter(f => f.status === options.status);
303
- }
304
-
305
- if (options.priority) {
306
- filtered = filtered.filter(f => f.priority === options.priority);
307
- }
308
-
309
- if (options.category) {
310
- filtered = filtered.filter(f => f.category === options.category);
311
- }
312
-
313
- // 정렬
314
- const sortBy = options.sort || 'id';
315
- filtered.sort((a, b) => {
316
- switch (sortBy) {
317
- case 'id':
318
- return a.id.localeCompare(b.id);
319
- case 'updated':
320
- return new Date(b.updated) - new Date(a.updated);
321
- case 'priority':
322
- const priorityOrder = ['critical', 'high', 'medium', 'low'];
323
- return priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority);
324
- default:
325
- return 0;
326
- }
327
- });
328
-
329
- // 출력
330
- const format = options.format || 'table';
331
-
332
- if (format === 'table') {
333
- console.log(chalk.cyan('📋 Feature Ledger 목록'));
334
- console.log(chalk.gray('━'.repeat(60)));
335
- console.log('ID | Title | Status | Priority | Progress');
336
- console.log('------+------------------------+-----------+----------+---------');
337
-
338
- filtered.forEach(f => {
339
- const statusColor = f.status === 'active' ? 'green' :
340
- f.status === 'completed' ? 'blue' : 'gray';
341
- console.log(
342
- `${f.id.padEnd(5)} | ${f.title.padEnd(22).slice(0, 22)} | ${
343
- chalk[statusColor](f.status.padEnd(9))
344
- } | ${f.priority.padEnd(8)} | ${f.progress || '0%'}`
345
- );
346
- });
347
-
348
- console.log(chalk.gray('━'.repeat(60)));
349
-
350
- // 통계
351
- const stats = {
352
- total: filtered.length,
353
- active: filtered.filter(f => f.status === 'active').length,
354
- completed: filtered.filter(f => f.status === 'completed').length,
355
- paused: filtered.filter(f => f.status === 'paused').length
356
- };
357
-
358
- console.log(`\n총 ${stats.total}개 Feature | Active: ${stats.active} | Completed: ${stats.completed} | Paused: ${stats.paused}`);
359
-
360
- } else if (format === 'list') {
361
- filtered.forEach(f => {
362
- console.log(`\n${chalk.yellow(f.id)}: ${f.title}`);
363
- console.log(` 상태: ${f.status} | 우선순위: ${f.priority} | 진행률: ${f.progress || '0%'}`);
364
- });
365
- }
366
-
367
- } catch (error) {
368
- console.error(chalk.red(`❌ 오류: ${error.message}`));
369
- }
370
- }
371
-
372
- // Feature 상세 정보
373
- async function getFeatureDetails(featureId, options = {}) {
374
- try {
375
- if (!featureId) {
376
- const features = await getAllFeatures();
377
- if (features.length === 0) {
378
- console.log(chalk.yellow('Feature가 없습니다.'));
379
- return;
380
- }
381
-
382
- const response = await prompts({
383
- type: 'select',
384
- name: 'featureId',
385
- message: 'Feature를 선택하세요:',
386
- choices: features.map(f => ({
387
- title: `${f.id} - ${f.title}`,
388
- value: f.id
389
- }))
390
- });
391
-
392
- if (!response.featureId) return;
393
- featureId = response.featureId;
394
- }
395
-
396
- const feature = await findFeature(featureId);
397
- if (!feature) {
398
- console.log(chalk.red(`❌ Feature ${featureId}를 찾을 수 없습니다.`));
399
- return;
400
- }
401
-
402
- // 파일 내용 읽기
403
- const content = fs.readFileSync(feature.path, 'utf8');
404
-
405
- console.log(chalk.cyan('🎯 Feature 상세 정보'));
406
- console.log(chalk.gray('━'.repeat(50)));
407
- console.log(content);
408
-
409
- } catch (error) {
410
- console.error(chalk.red(`❌ 오류: ${error.message}`));
411
- }
412
- }
413
-
414
- // 헬퍼: Feature 찾기
415
- async function findFeature(featureId) {
416
- const features = await getAllFeatures();
417
- return features.find(f => f.id === featureId);
418
- }
419
-
420
- // 헬퍼: 모든 Feature 가져오기
421
- async function getAllFeatures() {
422
- const features = [];
423
- const ledgerPath = getFeatureLedgerPath();
424
-
425
- // active 디렉토리
426
- const activePath = path.join(ledgerPath, 'active');
427
- if (fs.existsSync(activePath)) {
428
- const files = fs.readdirSync(activePath).filter(f => f.endsWith('.md'));
429
- files.forEach(file => {
430
- const content = fs.readFileSync(path.join(activePath, file), 'utf8');
431
- const feature = parseFeatureFile(content, path.join(activePath, file));
432
- if (feature) features.push(feature);
433
- });
434
- }
435
-
436
- // archived 디렉토리
437
- const archivedPath = path.join(ledgerPath, 'archived');
438
- if (fs.existsSync(archivedPath)) {
439
- const files = fs.readdirSync(archivedPath).filter(f => f.endsWith('.md'));
440
- files.forEach(file => {
441
- const content = fs.readFileSync(path.join(archivedPath, file), 'utf8');
442
- const feature = parseFeatureFile(content, path.join(archivedPath, file));
443
- if (feature) features.push(feature);
444
- });
445
- }
446
-
447
- return features;
448
- }
449
-
450
- // 헬퍼: active Feature 가져오기
451
- async function getActiveFeatures() {
452
- const features = await getAllFeatures();
453
- return features.filter(f => f.status === 'active');
454
- }
455
-
456
- // 헬퍼: Feature 파일 파싱
457
- function parseFeatureFile(content, filepath) {
458
- const idMatch = content.match(/- \*\*Feature ID\*\*: (FL\d{3})/);
459
- const titleMatch = content.match(/# FL\d{3}: (.+)/);
460
- const statusMatch = content.match(/- \*\*상태\*\*: (\w+)/);
461
- const priorityMatch = content.match(/- \*\*우선순위\*\*: (\w+)/);
462
- const categoryMatch = content.match(/- \*\*카테고리\*\*: (\w+)/);
463
- const progressMatch = content.match(/- \*\*진행률\*\*: (\d+%)/);
464
- const updatedMatch = content.match(/- \*\*업데이트\*\*: (.+)/);
465
-
466
- if (!idMatch || !titleMatch) return null;
467
-
468
- return {
469
- id: idMatch[1],
470
- title: titleMatch[1],
471
- status: statusMatch ? statusMatch[1] : 'active',
472
- priority: priorityMatch ? priorityMatch[1] : 'medium',
473
- category: categoryMatch ? categoryMatch[1] : 'feature',
474
- progress: progressMatch ? progressMatch[1] : '0%',
475
- updated: updatedMatch ? updatedMatch[1] : '',
476
- path: filepath
477
- };
478
- }
479
-
480
- // 헬퍼: Feature 인덱스 업데이트
481
- async function updateFeatureIndex() {
482
- const indexPath = path.join(getFeatureLedgerPath(), 'FEATURE_LEDGER_INDEX.md');
483
- const features = await getAllFeatures();
484
-
485
- let content = `# Feature Ledger 인덱스
486
-
487
- 마지막 업데이트: ${new Date().toISOString()}
488
-
489
- ## 활성 Features
490
-
491
- | ID | 제목 | 상태 | 우선순위 | 진행률 | 업데이트 |
492
- |----|------|------|----------|--------|----------|
493
- `;
494
-
495
- const activeFeatures = features.filter(f => f.status !== 'archived');
496
- activeFeatures.forEach(f => {
497
- content += `| ${f.id} | ${f.title} | ${f.status} | ${f.priority} | ${f.progress} | ${f.updated.split('T')[0]} |\n`;
498
- });
499
-
500
- content += `\n## 보관된 Features\n\n`;
501
- content += `| ID | 제목 | 완료일 |\n`;
502
- content += `|----|------|--------|\n`;
503
-
504
- const archivedFeatures = features.filter(f => f.status === 'archived');
505
- archivedFeatures.forEach(f => {
506
- content += `| ${f.id} | ${f.title} | ${f.updated.split('T')[0]} |\n`;
507
- });
508
-
509
- content += `\n## 통계\n\n`;
510
- content += `- 총 Features: ${features.length}\n`;
511
- content += `- 활성: ${features.filter(f => f.status === 'active').length}\n`;
512
- content += `- 완료: ${features.filter(f => f.status === 'completed').length}\n`;
513
- content += `- 일시 중지: ${features.filter(f => f.status === 'paused').length}\n`;
514
- content += `- 보관됨: ${features.filter(f => f.status === 'archived').length}\n`;
515
-
516
- fs.writeFileSync(indexPath, content);
517
- }
518
-
519
- // 명령행 인자 처리
520
- const command = process.argv[2];
521
- const args = process.argv.slice(3);
522
-
523
- switch (command) {
524
- case 'create':
525
- createFeature(args[0]);
526
- break;
527
-
528
- case 'update-status':
529
- updateFeatureStatus(args[0], args[1]);
530
- break;
531
-
532
- case 'list':
533
- const listOptions = {};
534
- args.forEach(arg => {
535
- if (arg.startsWith('--')) {
536
- const [key, value] = arg.slice(2).split('=');
537
- listOptions[key] = value || true;
538
- }
539
- });
540
- listFeatures(listOptions);
541
- break;
542
-
543
- case 'details':
544
- const detailsOptions = {};
545
- args.slice(1).forEach(arg => {
546
- if (arg.startsWith('--')) {
547
- const [key, value] = arg.slice(2).split('=');
548
- detailsOptions[key] = value || true;
549
- }
550
- });
551
- getFeatureDetails(args[0], detailsOptions);
552
- break;
553
-
554
- case 'link-milestone':
555
- console.log(chalk.yellow('마일스톤 연결 기능은 개발 중입니다.'));
556
- break;
557
-
558
- default:
559
- console.log(chalk.yellow('사용법:'));
560
- console.log(' feature-ledger.js create [name]');
561
- console.log(' feature-ledger.js update-status [id] [status]');
562
- console.log(' feature-ledger.js list [options]');
563
- console.log(' feature-ledger.js details [id]');
564
- console.log(' feature-ledger.js link-milestone [id] [milestone]');
565
- }