pnpfucius 2.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/README.md +396 -0
- package/bin/claude-predict.js +5 -0
- package/bin/pnpfucius.js +8 -0
- package/package.json +71 -0
- package/src/agent.js +1037 -0
- package/src/ai/index.js +6 -0
- package/src/ai/market-generator.js +186 -0
- package/src/ai/resolver.js +172 -0
- package/src/ai/scorer.js +184 -0
- package/src/analytics/aggregator.js +198 -0
- package/src/cli.js +948 -0
- package/src/collateral/privacy-tokens.js +183 -0
- package/src/config.js +128 -0
- package/src/daemon/index.js +321 -0
- package/src/daemon/lifecycle.js +168 -0
- package/src/daemon/scheduler.js +252 -0
- package/src/events/emitter.js +147 -0
- package/src/helius/client.js +221 -0
- package/src/helius/transaction-tracker.js +192 -0
- package/src/helius/webhooks.js +233 -0
- package/src/index.js +139 -0
- package/src/monitoring/news-monitor.js +262 -0
- package/src/monitoring/news-scorer.js +236 -0
- package/src/predict/agent.js +291 -0
- package/src/predict/prompts.js +69 -0
- package/src/predict/slash-commands.js +361 -0
- package/src/predict/tools/analytics-tools.js +83 -0
- package/src/predict/tools/bash-tool.js +87 -0
- package/src/predict/tools/file-tools.js +140 -0
- package/src/predict/tools/index.js +120 -0
- package/src/predict/tools/market-tools.js +851 -0
- package/src/predict/tools/news-tools.js +130 -0
- package/src/predict/ui/renderer.js +215 -0
- package/src/predict/ui/welcome.js +146 -0
- package/src/privacy-markets.js +194 -0
- package/src/storage/market-store.js +418 -0
- package/src/utils/spinner.js +172 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// SQLite storage for market tracking
|
|
2
|
+
// Uses better-sqlite3 for synchronous, fast SQLite operations
|
|
3
|
+
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
import { agentEvents, AgentEvents } from '../events/emitter.js';
|
|
8
|
+
|
|
9
|
+
export class MarketStore {
|
|
10
|
+
constructor(dbPath = null) {
|
|
11
|
+
this.dbPath = dbPath;
|
|
12
|
+
|
|
13
|
+
if (dbPath && dbPath !== ':memory:') {
|
|
14
|
+
const dir = dirname(dbPath);
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.db = new Database(dbPath || ':memory:');
|
|
21
|
+
this.db.pragma('journal_mode = WAL');
|
|
22
|
+
this._initSchema();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_initSchema() {
|
|
26
|
+
this.db.exec(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS markets (
|
|
28
|
+
address TEXT PRIMARY KEY,
|
|
29
|
+
question TEXT NOT NULL,
|
|
30
|
+
category TEXT,
|
|
31
|
+
category_key TEXT,
|
|
32
|
+
creation_time INTEGER,
|
|
33
|
+
creation_signature TEXT,
|
|
34
|
+
initial_liquidity TEXT,
|
|
35
|
+
duration_days INTEGER,
|
|
36
|
+
end_time INTEGER,
|
|
37
|
+
status TEXT DEFAULT 'active',
|
|
38
|
+
outcome TEXT,
|
|
39
|
+
volume TEXT,
|
|
40
|
+
resolution_time INTEGER,
|
|
41
|
+
metadata TEXT
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS daemon_state (
|
|
45
|
+
key TEXT PRIMARY KEY,
|
|
46
|
+
value TEXT,
|
|
47
|
+
updated_at INTEGER
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS webhook_events (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
event_type TEXT,
|
|
53
|
+
signature TEXT,
|
|
54
|
+
data TEXT,
|
|
55
|
+
processed INTEGER DEFAULT 0,
|
|
56
|
+
timestamp INTEGER
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_markets_status ON markets(status);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_markets_category ON markets(category_key);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_markets_creation ON markets(creation_time);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_processed ON webhook_events(processed);
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize (for API compatibility)
|
|
67
|
+
async initialize() {
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Save a market record
|
|
72
|
+
saveMarket(record) {
|
|
73
|
+
const endTime = record.endTime || (record.creationTime + (record.durationDays * 24 * 60 * 60 * 1000));
|
|
74
|
+
|
|
75
|
+
const stmt = this.db.prepare(`
|
|
76
|
+
INSERT OR REPLACE INTO markets
|
|
77
|
+
(address, question, category, category_key, creation_time, creation_signature,
|
|
78
|
+
initial_liquidity, duration_days, end_time, status, outcome, volume, resolution_time, metadata)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
stmt.run(
|
|
83
|
+
record.address,
|
|
84
|
+
record.question,
|
|
85
|
+
record.category || null,
|
|
86
|
+
record.categoryKey || null,
|
|
87
|
+
record.creationTime || Date.now(),
|
|
88
|
+
record.creationSignature || null,
|
|
89
|
+
record.initialLiquidity?.toString() || null,
|
|
90
|
+
record.durationDays || null,
|
|
91
|
+
endTime,
|
|
92
|
+
record.status || 'active',
|
|
93
|
+
record.outcome || null,
|
|
94
|
+
record.volume?.toString() || null,
|
|
95
|
+
record.resolutionTime || null,
|
|
96
|
+
JSON.stringify(record.metadata || {})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
agentEvents.emitTyped(AgentEvents.MARKET_UPDATED, { address: record.address });
|
|
100
|
+
|
|
101
|
+
return this.getMarket(record.address);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get a single market by address
|
|
105
|
+
getMarket(address) {
|
|
106
|
+
const stmt = this.db.prepare('SELECT * FROM markets WHERE address = ?');
|
|
107
|
+
const row = stmt.get(address);
|
|
108
|
+
|
|
109
|
+
if (!row) return null;
|
|
110
|
+
|
|
111
|
+
return this._rowToMarket(row);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Convert database row to market object
|
|
115
|
+
_rowToMarket(row) {
|
|
116
|
+
return {
|
|
117
|
+
address: row.address,
|
|
118
|
+
question: row.question,
|
|
119
|
+
category: row.category,
|
|
120
|
+
categoryKey: row.category_key,
|
|
121
|
+
creationTime: row.creation_time,
|
|
122
|
+
creationSignature: row.creation_signature,
|
|
123
|
+
initialLiquidity: row.initial_liquidity,
|
|
124
|
+
durationDays: row.duration_days,
|
|
125
|
+
endTime: row.end_time,
|
|
126
|
+
status: row.status,
|
|
127
|
+
outcome: row.outcome,
|
|
128
|
+
volume: row.volume,
|
|
129
|
+
resolutionTime: row.resolution_time,
|
|
130
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Get all markets with optional filters
|
|
135
|
+
getAllMarkets(options = {}) {
|
|
136
|
+
let sql = 'SELECT * FROM markets WHERE 1=1';
|
|
137
|
+
const params = [];
|
|
138
|
+
|
|
139
|
+
if (options.status) {
|
|
140
|
+
sql += ' AND status = ?';
|
|
141
|
+
params.push(options.status);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.category) {
|
|
145
|
+
sql += ' AND category_key = ?';
|
|
146
|
+
params.push(options.category);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options.since) {
|
|
150
|
+
sql += ' AND creation_time >= ?';
|
|
151
|
+
params.push(options.since);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (options.until) {
|
|
155
|
+
sql += ' AND creation_time <= ?';
|
|
156
|
+
params.push(options.until);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
sql += ' ORDER BY creation_time DESC';
|
|
160
|
+
|
|
161
|
+
if (options.limit) {
|
|
162
|
+
sql += ' LIMIT ?';
|
|
163
|
+
params.push(options.limit);
|
|
164
|
+
|
|
165
|
+
if (options.offset) {
|
|
166
|
+
sql += ' OFFSET ?';
|
|
167
|
+
params.push(options.offset);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const stmt = this.db.prepare(sql);
|
|
172
|
+
const rows = stmt.all(...params);
|
|
173
|
+
|
|
174
|
+
return rows.map(row => this._rowToMarket(row));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update a market
|
|
178
|
+
updateMarket(address, updates) {
|
|
179
|
+
const market = this.getMarket(address);
|
|
180
|
+
if (!market) return null;
|
|
181
|
+
|
|
182
|
+
const fieldMap = {
|
|
183
|
+
status: 'status',
|
|
184
|
+
outcome: 'outcome',
|
|
185
|
+
volume: 'volume',
|
|
186
|
+
resolutionTime: 'resolution_time',
|
|
187
|
+
resolution_time: 'resolution_time'
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const setClauses = [];
|
|
191
|
+
const params = [];
|
|
192
|
+
|
|
193
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
194
|
+
const dbField = fieldMap[key] || key;
|
|
195
|
+
setClauses.push(`${dbField} = ?`);
|
|
196
|
+
params.push(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (setClauses.length > 0) {
|
|
200
|
+
params.push(address);
|
|
201
|
+
const stmt = this.db.prepare(`UPDATE markets SET ${setClauses.join(', ')} WHERE address = ?`);
|
|
202
|
+
stmt.run(...params);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
agentEvents.emitTyped(AgentEvents.MARKET_UPDATED, { address, updates });
|
|
206
|
+
|
|
207
|
+
return this.getMarket(address);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Delete a market
|
|
211
|
+
deleteMarket(address) {
|
|
212
|
+
const stmt = this.db.prepare('DELETE FROM markets WHERE address = ?');
|
|
213
|
+
const result = stmt.run(address);
|
|
214
|
+
return result.changes > 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Get statistics
|
|
218
|
+
getStats() {
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const weekAgo = now - (7 * 24 * 60 * 60 * 1000);
|
|
221
|
+
|
|
222
|
+
const total = this.db.prepare('SELECT COUNT(*) as count FROM markets').get().count;
|
|
223
|
+
const active = this.db.prepare('SELECT COUNT(*) as count FROM markets WHERE status = ?').get('active').count;
|
|
224
|
+
const resolved = this.db.prepare('SELECT COUNT(*) as count FROM markets WHERE status = ?').get('resolved').count;
|
|
225
|
+
const cancelled = this.db.prepare('SELECT COUNT(*) as count FROM markets WHERE status = ?').get('cancelled').count;
|
|
226
|
+
const recentCount = this.db.prepare('SELECT COUNT(*) as count FROM markets WHERE creation_time >= ?').get(weekAgo).count;
|
|
227
|
+
|
|
228
|
+
const byCategory = this.db.prepare(`
|
|
229
|
+
SELECT category, category_key, COUNT(*) as count
|
|
230
|
+
FROM markets
|
|
231
|
+
GROUP BY category_key
|
|
232
|
+
`).all();
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
total,
|
|
236
|
+
active,
|
|
237
|
+
resolved,
|
|
238
|
+
cancelled,
|
|
239
|
+
recentCount,
|
|
240
|
+
byCategory,
|
|
241
|
+
lastUpdated: Date.now()
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Get performance metrics
|
|
246
|
+
getPerformanceMetrics() {
|
|
247
|
+
const resolved = this.db.prepare('SELECT * FROM markets WHERE status = ?').all('resolved');
|
|
248
|
+
|
|
249
|
+
const volumeResult = this.db.prepare('SELECT SUM(CAST(volume AS INTEGER)) as total FROM markets').get();
|
|
250
|
+
const totalVolume = BigInt(volumeResult.total || 0);
|
|
251
|
+
|
|
252
|
+
let averageDuration = 0;
|
|
253
|
+
if (resolved.length > 0) {
|
|
254
|
+
const totalDays = resolved.reduce((sum, m) => sum + (m.duration_days || 0), 0);
|
|
255
|
+
averageDuration = totalDays / resolved.length;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const total = this.db.prepare('SELECT COUNT(*) as count FROM markets').get().count;
|
|
259
|
+
const resolutionRate = total > 0 ? resolved.length / total : 0;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
totalVolume: totalVolume.toString(),
|
|
263
|
+
averageDuration: Math.round(averageDuration),
|
|
264
|
+
resolutionRate: Math.round(resolutionRate * 100) / 100,
|
|
265
|
+
marketCount: resolved.length
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Save daemon state
|
|
270
|
+
saveState(keyOrState, value) {
|
|
271
|
+
const stmt = this.db.prepare(`
|
|
272
|
+
INSERT OR REPLACE INTO daemon_state (key, value, updated_at)
|
|
273
|
+
VALUES (?, ?, ?)
|
|
274
|
+
`);
|
|
275
|
+
|
|
276
|
+
if (typeof keyOrState === 'string') {
|
|
277
|
+
stmt.run(keyOrState, JSON.stringify(value), Date.now());
|
|
278
|
+
} else {
|
|
279
|
+
// Old API: saveState(stateObject)
|
|
280
|
+
const stateObj = { ...keyOrState, savedAt: Date.now() };
|
|
281
|
+
stmt.run('__full_state__', JSON.stringify(stateObj), Date.now());
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
agentEvents.emitTyped(AgentEvents.STATE_SAVED, {});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Get daemon state
|
|
288
|
+
getState(key) {
|
|
289
|
+
if (key) {
|
|
290
|
+
const stmt = this.db.prepare('SELECT value FROM daemon_state WHERE key = ?');
|
|
291
|
+
const row = stmt.get(key);
|
|
292
|
+
return row ? JSON.parse(row.value) : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Return full state object for old API
|
|
296
|
+
const stmt = this.db.prepare('SELECT value FROM daemon_state WHERE key = ?');
|
|
297
|
+
const row = stmt.get('__full_state__');
|
|
298
|
+
return row ? JSON.parse(row.value) : null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Get all state
|
|
302
|
+
getAllState() {
|
|
303
|
+
const stmt = this.db.prepare('SELECT key, value FROM daemon_state');
|
|
304
|
+
const rows = stmt.all();
|
|
305
|
+
|
|
306
|
+
const state = {};
|
|
307
|
+
for (const row of rows) {
|
|
308
|
+
if (row.key === '__full_state__') {
|
|
309
|
+
Object.assign(state, JSON.parse(row.value));
|
|
310
|
+
} else {
|
|
311
|
+
state[row.key] = JSON.parse(row.value);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return state;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Save webhook event
|
|
319
|
+
saveWebhookEvent(event) {
|
|
320
|
+
const stmt = this.db.prepare(`
|
|
321
|
+
INSERT INTO webhook_events (event_type, signature, data, processed, timestamp)
|
|
322
|
+
VALUES (?, ?, ?, 0, ?)
|
|
323
|
+
`);
|
|
324
|
+
|
|
325
|
+
stmt.run(
|
|
326
|
+
event.type,
|
|
327
|
+
event.signature,
|
|
328
|
+
JSON.stringify(event.data),
|
|
329
|
+
Date.now()
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Log webhook event (alias)
|
|
334
|
+
logWebhookEvent(eventType, signature, data) {
|
|
335
|
+
this.saveWebhookEvent({ type: eventType, signature, data });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Get webhook events
|
|
339
|
+
getWebhookEvents(filtersOrLimit = {}) {
|
|
340
|
+
if (typeof filtersOrLimit === 'number') {
|
|
341
|
+
const stmt = this.db.prepare('SELECT * FROM webhook_events ORDER BY timestamp DESC LIMIT ?');
|
|
342
|
+
return stmt.all(filtersOrLimit);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const filters = filtersOrLimit;
|
|
346
|
+
let sql = 'SELECT * FROM webhook_events WHERE 1=1';
|
|
347
|
+
const params = [];
|
|
348
|
+
|
|
349
|
+
if (filters.eventType) {
|
|
350
|
+
sql += ' AND event_type = ?';
|
|
351
|
+
params.push(filters.eventType);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (filters.processed !== undefined) {
|
|
355
|
+
sql += ' AND processed = ?';
|
|
356
|
+
params.push(filters.processed ? 1 : 0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
sql += ' ORDER BY timestamp DESC';
|
|
360
|
+
|
|
361
|
+
if (filters.limit) {
|
|
362
|
+
sql += ' LIMIT ?';
|
|
363
|
+
params.push(filters.limit);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const stmt = this.db.prepare(sql);
|
|
367
|
+
return stmt.all(...params);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Mark webhook as processed
|
|
371
|
+
markWebhookProcessed(id) {
|
|
372
|
+
const stmt = this.db.prepare('UPDATE webhook_events SET processed = 1 WHERE id = ?');
|
|
373
|
+
stmt.run(id);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Close database
|
|
377
|
+
close() {
|
|
378
|
+
this.db.close();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Clear all data
|
|
382
|
+
clear() {
|
|
383
|
+
this.db.exec('DELETE FROM markets');
|
|
384
|
+
this.db.exec('DELETE FROM daemon_state');
|
|
385
|
+
this.db.exec('DELETE FROM webhook_events');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Export data as JSON string
|
|
389
|
+
export() {
|
|
390
|
+
const markets = this.getAllMarkets();
|
|
391
|
+
const state = this.getAllState();
|
|
392
|
+
const events = this.getWebhookEvents(1000);
|
|
393
|
+
|
|
394
|
+
return JSON.stringify({ markets, state, events }, null, 2);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Import data from JSON string
|
|
398
|
+
import(jsonString) {
|
|
399
|
+
try {
|
|
400
|
+
const data = JSON.parse(jsonString);
|
|
401
|
+
|
|
402
|
+
if (data.markets) {
|
|
403
|
+
for (const market of data.markets) {
|
|
404
|
+
this.saveMarket(market);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return true;
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Factory function
|
|
416
|
+
export function createMarketStore(dbPath = null) {
|
|
417
|
+
return new MarketStore(dbPath);
|
|
418
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// CLI spinner and progress utilities
|
|
2
|
+
// Provides elegant loading indicators for long operations
|
|
3
|
+
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
|
|
6
|
+
// Default spinner configuration
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
spinner: 'dots',
|
|
9
|
+
color: 'cyan'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Create a new spinner
|
|
13
|
+
export function createSpinner(text, config = {}) {
|
|
14
|
+
return ora({
|
|
15
|
+
text,
|
|
16
|
+
...DEFAULT_CONFIG,
|
|
17
|
+
...config
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Execute a task with a spinner
|
|
22
|
+
export async function withSpinner(text, task, options = {}) {
|
|
23
|
+
const spinner = createSpinner(text, options);
|
|
24
|
+
spinner.start();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await task(spinner);
|
|
28
|
+
spinner.succeed(options.successText || text);
|
|
29
|
+
return result;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
spinner.fail(options.failText || `${text} - ${error.message}`);
|
|
32
|
+
if (!options.silent) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Execute multiple tasks with progress
|
|
40
|
+
export async function withProgress(tasks, options = {}) {
|
|
41
|
+
const results = [];
|
|
42
|
+
const total = tasks.length;
|
|
43
|
+
const spinner = createSpinner(`Processing 0/${total}...`, options);
|
|
44
|
+
|
|
45
|
+
spinner.start();
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
48
|
+
const { name, task } = tasks[i];
|
|
49
|
+
spinner.text = `${name} (${i + 1}/${total})...`;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = await task();
|
|
53
|
+
results.push({ name, success: true, result });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
results.push({ name, success: false, error: error.message });
|
|
56
|
+
|
|
57
|
+
if (options.stopOnError) {
|
|
58
|
+
spinner.fail(`Failed at: ${name}`);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const successCount = results.filter(r => r.success).length;
|
|
65
|
+
spinner.succeed(`Completed ${successCount}/${total} tasks`);
|
|
66
|
+
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Indeterminate progress for unknown duration tasks
|
|
71
|
+
export function createIndeterminateProgress(text) {
|
|
72
|
+
const spinner = createSpinner(text);
|
|
73
|
+
let elapsed = 0;
|
|
74
|
+
let timer = null;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
start() {
|
|
78
|
+
spinner.start();
|
|
79
|
+
timer = setInterval(() => {
|
|
80
|
+
elapsed++;
|
|
81
|
+
spinner.text = `${text} (${elapsed}s)`;
|
|
82
|
+
}, 1000);
|
|
83
|
+
return this;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
update(newText) {
|
|
87
|
+
spinner.text = `${newText} (${elapsed}s)`;
|
|
88
|
+
return this;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
succeed(successText) {
|
|
92
|
+
if (timer) clearInterval(timer);
|
|
93
|
+
spinner.succeed(successText || `${text} (${elapsed}s)`);
|
|
94
|
+
return this;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
fail(failText) {
|
|
98
|
+
if (timer) clearInterval(timer);
|
|
99
|
+
spinner.fail(failText || `${text} failed after ${elapsed}s`);
|
|
100
|
+
return this;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
stop() {
|
|
104
|
+
if (timer) clearInterval(timer);
|
|
105
|
+
spinner.stop();
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Step-by-step progress indicator
|
|
112
|
+
export class StepProgress {
|
|
113
|
+
constructor(steps, options = {}) {
|
|
114
|
+
this.steps = steps;
|
|
115
|
+
this.currentStep = 0;
|
|
116
|
+
this.spinner = createSpinner('', options);
|
|
117
|
+
this.startTime = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
start() {
|
|
121
|
+
this.startTime = Date.now();
|
|
122
|
+
this.updateSpinner();
|
|
123
|
+
this.spinner.start();
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
updateSpinner() {
|
|
128
|
+
const step = this.steps[this.currentStep];
|
|
129
|
+
const progress = `[${this.currentStep + 1}/${this.steps.length}]`;
|
|
130
|
+
this.spinner.text = `${progress} ${step}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
next(customText) {
|
|
134
|
+
if (this.currentStep < this.steps.length - 1) {
|
|
135
|
+
this.currentStep++;
|
|
136
|
+
if (customText) {
|
|
137
|
+
this.steps[this.currentStep] = customText;
|
|
138
|
+
}
|
|
139
|
+
this.updateSpinner();
|
|
140
|
+
}
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
succeed(text) {
|
|
145
|
+
const elapsed = Math.round((Date.now() - this.startTime) / 1000);
|
|
146
|
+
this.spinner.succeed(text || `Completed ${this.steps.length} steps in ${elapsed}s`);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fail(text) {
|
|
151
|
+
const elapsed = Math.round((Date.now() - this.startTime) / 1000);
|
|
152
|
+
this.spinner.fail(text || `Failed at step ${this.currentStep + 1} after ${elapsed}s`);
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Simple text status updates (no spinner)
|
|
158
|
+
export function statusLine(text, symbol = '-') {
|
|
159
|
+
console.log(` ${symbol} ${text}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function successLine(text) {
|
|
163
|
+
statusLine(text, '\u2713');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function errorLine(text) {
|
|
167
|
+
statusLine(text, '\u2717');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function infoLine(text) {
|
|
171
|
+
statusLine(text, '\u2022');
|
|
172
|
+
}
|