wogiflow 1.0.14 → 1.0.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.
- package/lib/installer.js +8 -2
- package/lib/upgrader.js +2 -2
- package/package.json +1 -1
- package/scripts/flow-context-orchestrator.js +470 -0
- package/scripts/flow-product-scanner.js +466 -0
- package/scripts/flow-section-index.js +174 -1
- package/scripts/flow-story.js +48 -5
- package/templates/context/product-placeholder.md +72 -0
- package/templates/context/product.md +68 -0
package/lib/installer.js
CHANGED
|
@@ -477,8 +477,14 @@ async function init(args) {
|
|
|
477
477
|
console.log('\n✅ Wogi Flow initialized successfully!\n');
|
|
478
478
|
console.log('Next steps:');
|
|
479
479
|
console.log(' 1. Review .workflow/config.json');
|
|
480
|
-
console.log(' 2. Run
|
|
481
|
-
console.log(' 3. Create your first task with
|
|
480
|
+
console.log(' 2. Run `/wogi-status` to see project status');
|
|
481
|
+
console.log(' 3. Create your first task with `/wogi-story "Task title"`');
|
|
482
|
+
console.log('');
|
|
483
|
+
console.log('Available commands:');
|
|
484
|
+
console.log(' /wogi-ready - View available tasks');
|
|
485
|
+
console.log(' /wogi-status - Project overview');
|
|
486
|
+
console.log(' /wogi-health - Check workflow health');
|
|
487
|
+
console.log(' /wogi-story - Create a new story');
|
|
482
488
|
console.log('');
|
|
483
489
|
}
|
|
484
490
|
|
package/lib/upgrader.js
CHANGED
|
@@ -163,7 +163,7 @@ function updateScripts(projectRoot, dryRun) {
|
|
|
163
163
|
// Make flow script executable
|
|
164
164
|
const flowScript = path.join(projectScripts, 'flow');
|
|
165
165
|
if (fs.existsSync(flowScript)) {
|
|
166
|
-
fs.chmodSync(flowScript,
|
|
166
|
+
fs.chmodSync(flowScript, 0o755);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
console.log(' Updated scripts/');
|
|
@@ -393,7 +393,7 @@ async function upgrade(args) {
|
|
|
393
393
|
console.log('\n✅ Upgrade complete!\n');
|
|
394
394
|
console.log('Next steps:');
|
|
395
395
|
console.log(' 1. Review changes in .workflow/');
|
|
396
|
-
console.log(' 2. Run
|
|
396
|
+
console.log(' 2. Run `/wogi-health` to verify installation');
|
|
397
397
|
console.log(' 3. Commit the upgraded files');
|
|
398
398
|
}
|
|
399
399
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Context Orchestrator
|
|
5
|
+
*
|
|
6
|
+
* Enables targeted context loading for tasks using the PIN system.
|
|
7
|
+
* Supports orchestrator pattern where cheaper models (Haiku) gather
|
|
8
|
+
* relevant context for expensive models (Opus).
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - PIN-based section lookup
|
|
12
|
+
* - Task description to relevant sections mapping
|
|
13
|
+
* - Token-aware context truncation
|
|
14
|
+
* - Product context integration
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const { getTargetedContext } = require('./flow-context-orchestrator');
|
|
18
|
+
* const context = await getTargetedContext({ task: "Add user auth" });
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const {
|
|
23
|
+
PATHS,
|
|
24
|
+
fileExists,
|
|
25
|
+
readFile,
|
|
26
|
+
parseFlags,
|
|
27
|
+
outputJson,
|
|
28
|
+
info,
|
|
29
|
+
warn,
|
|
30
|
+
safeJsonParse
|
|
31
|
+
} = require('./flow-utils');
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
getSectionsForTask,
|
|
35
|
+
getSectionsByPins,
|
|
36
|
+
formatSectionsAsContext,
|
|
37
|
+
formatSectionsAsReferences
|
|
38
|
+
} = require('./flow-section-resolver');
|
|
39
|
+
|
|
40
|
+
// ============================================================
|
|
41
|
+
// Configuration
|
|
42
|
+
// ============================================================
|
|
43
|
+
|
|
44
|
+
// Approximate tokens per character (conservative estimate)
|
|
45
|
+
const CHARS_PER_TOKEN = 4;
|
|
46
|
+
|
|
47
|
+
// Default limits
|
|
48
|
+
const DEFAULT_MAX_TOKENS = 8000;
|
|
49
|
+
const DEFAULT_SECTION_LIMIT = 10;
|
|
50
|
+
|
|
51
|
+
// ============================================================
|
|
52
|
+
// Token Estimation
|
|
53
|
+
// ============================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Estimate token count for a string
|
|
57
|
+
* @param {string} text - Text to estimate
|
|
58
|
+
* @returns {number} - Estimated token count
|
|
59
|
+
*/
|
|
60
|
+
function estimateTokens(text) {
|
|
61
|
+
if (!text) return 0;
|
|
62
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Truncate sections to fit within token limit
|
|
67
|
+
* @param {Object[]} sections - Sections to truncate
|
|
68
|
+
* @param {number} maxTokens - Max tokens
|
|
69
|
+
* @returns {Object[]} - Truncated sections
|
|
70
|
+
*/
|
|
71
|
+
function truncateToTokenLimit(sections, maxTokens) {
|
|
72
|
+
const result = [];
|
|
73
|
+
let currentTokens = 0;
|
|
74
|
+
|
|
75
|
+
for (const section of sections) {
|
|
76
|
+
const sectionTokens = estimateTokens(section.content || '');
|
|
77
|
+
|
|
78
|
+
if (currentTokens + sectionTokens <= maxTokens) {
|
|
79
|
+
result.push(section);
|
|
80
|
+
currentTokens += sectionTokens;
|
|
81
|
+
} else if (result.length === 0) {
|
|
82
|
+
// Always include at least one section, even if truncated
|
|
83
|
+
const availableChars = (maxTokens - currentTokens) * CHARS_PER_TOKEN;
|
|
84
|
+
result.push({
|
|
85
|
+
...section,
|
|
86
|
+
content: (section.content || '').substring(0, availableChars) + '...[truncated]',
|
|
87
|
+
truncated: true
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
} else {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================
|
|
99
|
+
// Section Merging
|
|
100
|
+
// ============================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Merge and deduplicate sections from multiple sources
|
|
104
|
+
* @param {Object[][]} sectionArrays - Arrays of sections to merge
|
|
105
|
+
* @returns {Object[]} - Merged and deduplicated sections
|
|
106
|
+
*/
|
|
107
|
+
function mergeSections(...sectionArrays) {
|
|
108
|
+
const seen = new Map();
|
|
109
|
+
|
|
110
|
+
for (const sections of sectionArrays) {
|
|
111
|
+
for (const section of sections) {
|
|
112
|
+
if (!seen.has(section.id)) {
|
|
113
|
+
seen.set(section.id, section);
|
|
114
|
+
} else {
|
|
115
|
+
// Keep the one with higher score
|
|
116
|
+
const existing = seen.get(section.id);
|
|
117
|
+
const existingScore = existing.score || existing.matchScore || 0;
|
|
118
|
+
const newScore = section.score || section.matchScore || 0;
|
|
119
|
+
if (newScore > existingScore) {
|
|
120
|
+
seen.set(section.id, section);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return Array.from(seen.values());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================
|
|
130
|
+
// Product Context
|
|
131
|
+
// ============================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get product context from product.md
|
|
135
|
+
* @param {Object} options - { format: 'full' | 'summary' }
|
|
136
|
+
* @returns {Object|null} - Product context or null
|
|
137
|
+
*/
|
|
138
|
+
async function getProductContext(options = {}) {
|
|
139
|
+
const { format = 'summary' } = options;
|
|
140
|
+
const productPath = path.join(PATHS.specs, 'product.md');
|
|
141
|
+
|
|
142
|
+
if (!fileExists(productPath)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Get product sections via PINs
|
|
148
|
+
const productPins = ['product-name', 'target-users', 'value-prop', 'core-features'];
|
|
149
|
+
const sections = await getSectionsByPins(productPins, { limit: 5 });
|
|
150
|
+
|
|
151
|
+
if (sections.length === 0) {
|
|
152
|
+
// Fall back to reading the file directly
|
|
153
|
+
const content = readFile(productPath);
|
|
154
|
+
return {
|
|
155
|
+
context: content,
|
|
156
|
+
source: 'file',
|
|
157
|
+
tokenEstimate: estimateTokens(content)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const context = formatSectionsAsContext(sections, { format });
|
|
162
|
+
return {
|
|
163
|
+
context,
|
|
164
|
+
sections: sections.map(s => s.id),
|
|
165
|
+
source: 'pins',
|
|
166
|
+
tokenEstimate: estimateTokens(context)
|
|
167
|
+
};
|
|
168
|
+
} catch (err) {
|
|
169
|
+
warn(`Error loading product context: ${err.message}`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get product overview (name, tagline, type only)
|
|
176
|
+
* @returns {Object|null} - Brief product info
|
|
177
|
+
*/
|
|
178
|
+
function getProductOverview() {
|
|
179
|
+
const productPath = path.join(PATHS.specs, 'product.md');
|
|
180
|
+
|
|
181
|
+
if (!fileExists(productPath)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const content = readFile(productPath);
|
|
187
|
+
|
|
188
|
+
// Extract key fields using regex
|
|
189
|
+
const nameMatch = content.match(/\*\*Name\*\*:\s*(.+)/);
|
|
190
|
+
const taglineMatch = content.match(/\*\*Tagline\*\*:\s*(.+)/);
|
|
191
|
+
const typeMatch = content.match(/\*\*Type\*\*:\s*(.+)/);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
name: nameMatch ? nameMatch[1].trim() : null,
|
|
195
|
+
tagline: taglineMatch ? taglineMatch[1].trim() : null,
|
|
196
|
+
type: typeMatch ? typeMatch[1].trim() : null
|
|
197
|
+
};
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================
|
|
204
|
+
// Main Context Gathering
|
|
205
|
+
// ============================================================
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get targeted context for a task
|
|
209
|
+
* @param {Object} options
|
|
210
|
+
* @param {string} options.task - Task description
|
|
211
|
+
* @param {string[]} options.pins - Explicit pins to include
|
|
212
|
+
* @param {number} options.maxTokens - Max tokens for context
|
|
213
|
+
* @param {string} options.format - 'full' | 'summary' | 'reference'
|
|
214
|
+
* @param {boolean} options.includeProduct - Include product context
|
|
215
|
+
* @returns {Object} - { context, sections, tokenEstimate }
|
|
216
|
+
*/
|
|
217
|
+
async function getTargetedContext(options = {}) {
|
|
218
|
+
const {
|
|
219
|
+
task = '',
|
|
220
|
+
pins = [],
|
|
221
|
+
maxTokens = DEFAULT_MAX_TOKENS,
|
|
222
|
+
format = 'full',
|
|
223
|
+
includeProduct = true
|
|
224
|
+
} = options;
|
|
225
|
+
|
|
226
|
+
// Get sections by task description
|
|
227
|
+
let taskSections = [];
|
|
228
|
+
if (task) {
|
|
229
|
+
taskSections = await getSectionsForTask(task, {
|
|
230
|
+
limit: DEFAULT_SECTION_LIMIT
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Get sections by explicit pins
|
|
235
|
+
let pinSections = [];
|
|
236
|
+
if (pins.length > 0) {
|
|
237
|
+
pinSections = await getSectionsByPins(pins, {
|
|
238
|
+
limit: Math.floor(DEFAULT_SECTION_LIMIT / 2)
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Merge and deduplicate (pass arrays separately as intended by mergeSections)
|
|
243
|
+
const mergedSections = mergeSections(taskSections, pinSections);
|
|
244
|
+
|
|
245
|
+
// Calculate available tokens for sections
|
|
246
|
+
let availableTokens = maxTokens;
|
|
247
|
+
let productContextResult = null;
|
|
248
|
+
|
|
249
|
+
// Include product context if requested
|
|
250
|
+
if (includeProduct) {
|
|
251
|
+
productContextResult = await getProductContext({ format: 'summary' });
|
|
252
|
+
if (productContextResult) {
|
|
253
|
+
availableTokens -= productContextResult.tokenEstimate;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Truncate sections to fit
|
|
258
|
+
const truncatedSections = truncateToTokenLimit(mergedSections, availableTokens);
|
|
259
|
+
|
|
260
|
+
// Format sections
|
|
261
|
+
const sectionsContext = formatSectionsAsContext(truncatedSections, { format });
|
|
262
|
+
|
|
263
|
+
// Combine contexts
|
|
264
|
+
let fullContext = '';
|
|
265
|
+
if (productContextResult) {
|
|
266
|
+
fullContext += '## Product Context\n\n' + productContextResult.context + '\n\n';
|
|
267
|
+
}
|
|
268
|
+
if (sectionsContext) {
|
|
269
|
+
fullContext += sectionsContext;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
context: fullContext.trim(),
|
|
274
|
+
sections: truncatedSections.map(s => ({
|
|
275
|
+
id: s.id,
|
|
276
|
+
score: s.score || s.matchScore || 0,
|
|
277
|
+
truncated: s.truncated || false
|
|
278
|
+
})),
|
|
279
|
+
productIncluded: !!productContextResult,
|
|
280
|
+
tokenEstimate: estimateTokens(fullContext)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get context for a specific task ID
|
|
286
|
+
* Loads task details and gathers relevant context
|
|
287
|
+
* @param {string} taskId - Task ID (e.g., "wf-abc123")
|
|
288
|
+
* @returns {Object} - Task context
|
|
289
|
+
*/
|
|
290
|
+
async function getContextForTaskId(taskId) {
|
|
291
|
+
// Try to find task in ready.json
|
|
292
|
+
const readyPath = path.join(PATHS.state, 'ready.json');
|
|
293
|
+
if (!fileExists(readyPath)) {
|
|
294
|
+
return getTargetedContext({ task: taskId });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const ready = safeJsonParse(readyPath, {});
|
|
298
|
+
const allTasks = [
|
|
299
|
+
...(ready.ready || []),
|
|
300
|
+
...(ready.inProgress || []),
|
|
301
|
+
...(ready.blocked || [])
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const task = allTasks.find(t =>
|
|
305
|
+
(typeof t === 'string' && t === taskId) ||
|
|
306
|
+
(typeof t === 'object' && t.id === taskId)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (task && typeof task === 'object' && task.title) {
|
|
310
|
+
return getTargetedContext({
|
|
311
|
+
task: `${task.title} ${task.description || ''}`,
|
|
312
|
+
pins: task.tags || []
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return getTargetedContext({ task: taskId });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get minimal context references (for orchestrator hints)
|
|
321
|
+
* @param {string} task - Task description
|
|
322
|
+
* @returns {string} - Reference string
|
|
323
|
+
*/
|
|
324
|
+
async function getContextReferences(task) {
|
|
325
|
+
const sections = await getSectionsForTask(task, { limit: 5 });
|
|
326
|
+
return formatSectionsAsReferences(sections);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ============================================================
|
|
330
|
+
// CLI
|
|
331
|
+
// ============================================================
|
|
332
|
+
|
|
333
|
+
async function main() {
|
|
334
|
+
const { args, flags } = parseFlags(process.argv.slice(2));
|
|
335
|
+
|
|
336
|
+
if (flags.help) {
|
|
337
|
+
console.log(`
|
|
338
|
+
Usage: node scripts/flow-context-orchestrator.js [command] [options]
|
|
339
|
+
|
|
340
|
+
Get targeted context for tasks using the PIN system.
|
|
341
|
+
|
|
342
|
+
Commands:
|
|
343
|
+
task "<description>" Get context for a task description
|
|
344
|
+
taskid <id> Get context for a task ID
|
|
345
|
+
product Get product context only
|
|
346
|
+
refs "<description>" Get section references only
|
|
347
|
+
|
|
348
|
+
Options:
|
|
349
|
+
--max-tokens <n> Max tokens for context (default: 8000)
|
|
350
|
+
--format <type> Output format: full, summary, reference
|
|
351
|
+
--no-product Exclude product context
|
|
352
|
+
--json Output as JSON
|
|
353
|
+
--help Show this help
|
|
354
|
+
|
|
355
|
+
Examples:
|
|
356
|
+
node scripts/flow-context-orchestrator.js task "Add user authentication"
|
|
357
|
+
node scripts/flow-context-orchestrator.js taskid wf-abc123 --json
|
|
358
|
+
node scripts/flow-context-orchestrator.js product --format summary
|
|
359
|
+
`);
|
|
360
|
+
process.exit(0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const command = args[0];
|
|
364
|
+
const maxTokens = parseInt(flags['max-tokens']) || DEFAULT_MAX_TOKENS;
|
|
365
|
+
const format = flags.format || 'full';
|
|
366
|
+
const includeProduct = flags.product !== false;
|
|
367
|
+
|
|
368
|
+
switch (command) {
|
|
369
|
+
case 'task': {
|
|
370
|
+
const task = args.slice(1).join(' ');
|
|
371
|
+
if (!task) {
|
|
372
|
+
console.error('Usage: flow-context-orchestrator task "<description>"');
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const result = await getTargetedContext({
|
|
377
|
+
task,
|
|
378
|
+
maxTokens,
|
|
379
|
+
format,
|
|
380
|
+
includeProduct
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (flags.json) {
|
|
384
|
+
outputJson(result);
|
|
385
|
+
} else {
|
|
386
|
+
console.log(result.context);
|
|
387
|
+
console.log(`\n--- ${result.sections.length} sections, ~${result.tokenEstimate} tokens ---`);
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
case 'taskid': {
|
|
393
|
+
const taskId = args[1];
|
|
394
|
+
if (!taskId) {
|
|
395
|
+
console.error('Usage: flow-context-orchestrator taskid <id>');
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = await getContextForTaskId(taskId);
|
|
400
|
+
|
|
401
|
+
if (flags.json) {
|
|
402
|
+
outputJson(result);
|
|
403
|
+
} else {
|
|
404
|
+
console.log(result.context);
|
|
405
|
+
console.log(`\n--- ${result.sections.length} sections, ~${result.tokenEstimate} tokens ---`);
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case 'product': {
|
|
411
|
+
const result = await getProductContext({ format });
|
|
412
|
+
|
|
413
|
+
if (!result) {
|
|
414
|
+
console.log('No product.md found');
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (flags.json) {
|
|
419
|
+
outputJson(result);
|
|
420
|
+
} else {
|
|
421
|
+
console.log(result.context);
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
case 'refs': {
|
|
427
|
+
const task = args.slice(1).join(' ');
|
|
428
|
+
if (!task) {
|
|
429
|
+
console.error('Usage: flow-context-orchestrator refs "<description>"');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const refs = await getContextReferences(task);
|
|
434
|
+
console.log(refs || 'No relevant sections found');
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
default:
|
|
439
|
+
console.error('Unknown command. Use --help for usage.');
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ============================================================
|
|
445
|
+
// Exports
|
|
446
|
+
// ============================================================
|
|
447
|
+
|
|
448
|
+
module.exports = {
|
|
449
|
+
// Main functions
|
|
450
|
+
getTargetedContext,
|
|
451
|
+
getContextForTaskId,
|
|
452
|
+
getContextReferences,
|
|
453
|
+
|
|
454
|
+
// Product context
|
|
455
|
+
getProductContext,
|
|
456
|
+
getProductOverview,
|
|
457
|
+
|
|
458
|
+
// Utilities
|
|
459
|
+
estimateTokens,
|
|
460
|
+
truncateToTokenLimit,
|
|
461
|
+
mergeSections
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Run if called directly
|
|
465
|
+
if (require.main === module) {
|
|
466
|
+
main().catch(err => {
|
|
467
|
+
console.error(`Error: ${err.message}`);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
});
|
|
470
|
+
}
|