dual-brain 0.2.14 → 0.2.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/bin/dual-brain.mjs +149 -4
- package/hooks/auto-update-wrapper.mjs +56 -58
- package/hooks/diagnostic-companion.mjs +422 -0
- package/hooks/precompact.mjs +53 -0
- package/hooks/session-end.mjs +122 -0
- package/package.json +26 -2
- package/src/cognitive-loop.mjs +557 -0
- package/src/continuity.mjs +9 -8
- package/src/cost-tracker.mjs +3 -3
- package/src/debrief.mjs +228 -0
- package/src/doctor.mjs +13 -13
- package/src/envelope.mjs +139 -0
- package/src/head-protocol.mjs +128 -0
- package/src/head.mjs +114 -79
- package/src/inbox.mjs +195 -0
- package/src/ledger.mjs +2 -2
- package/src/living-docs.mjs +2 -2
- package/src/memory-tiers.mjs +193 -0
- package/src/narrative.mjs +169 -0
- package/src/predictive.mjs +250 -0
- package/src/provider-context.mjs +2 -2
- package/src/receipt.mjs +2 -2
- package/src/session-lock.mjs +154 -0
- package/src/simmer.mjs +241 -0
- package/src/wave-planner.mjs +294 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -95,6 +95,18 @@ async function getLivingDocs() {
|
|
|
95
95
|
return _livingDocs;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
let _cognitiveLoopCache = null;
|
|
99
|
+
async function _getCognitiveLoop() {
|
|
100
|
+
if (!_cognitiveLoopCache) {
|
|
101
|
+
try {
|
|
102
|
+
_cognitiveLoopCache = await import('../src/cognitive-loop.mjs');
|
|
103
|
+
} catch {
|
|
104
|
+
_cognitiveLoopCache = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return _cognitiveLoopCache;
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
let _fx = null;
|
|
99
111
|
async function getFx() {
|
|
100
112
|
if (_fx !== null) return _fx;
|
|
@@ -492,6 +504,54 @@ async function cmdGo(args, opts = {}) {
|
|
|
492
504
|
} catch { /* non-fatal */ }
|
|
493
505
|
}
|
|
494
506
|
|
|
507
|
+
// ── Cognitive loop: drive dispatch decisions ──────────────────────────────
|
|
508
|
+
let loopEnhancedPrompt = prompt;
|
|
509
|
+
let loopDispatchMeta = null;
|
|
510
|
+
try {
|
|
511
|
+
const cogLoop = await _getCognitiveLoop();
|
|
512
|
+
if (cogLoop) {
|
|
513
|
+
const loopResult = cogLoop.enter(prompt, { files });
|
|
514
|
+
|
|
515
|
+
if (loopResult.phase === 'readonly') {
|
|
516
|
+
console.log('\n⚠ Another dual-brain session is active. This session is read-only.');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (loopResult.phase === 'dispatch' && loopResult.nextDispatch) {
|
|
521
|
+
loopDispatchMeta = loopResult;
|
|
522
|
+
// Use the full envelope prompt (includes context, preventions, debrief)
|
|
523
|
+
const firstAgent = loopResult.nextDispatch.agents?.[0];
|
|
524
|
+
if (firstAgent?.prompt) {
|
|
525
|
+
loopEnhancedPrompt = firstAgent.prompt;
|
|
526
|
+
}
|
|
527
|
+
if (verbose && loopResult.plan) {
|
|
528
|
+
const wc = loopResult.plan.waves?.length || 0;
|
|
529
|
+
console.log(` [cognitive-loop] Plan: ${wc} wave(s), phase: ${loopResult.phase}`);
|
|
530
|
+
}
|
|
531
|
+
} else if (loopResult.phase === 'blocked') {
|
|
532
|
+
console.log(`\n⚠ Dispatch blocked: ${loopResult.suggestion || 'readiness check failed'}`);
|
|
533
|
+
if (loopResult.surfaceNoticings?.length) {
|
|
534
|
+
loopResult.surfaceNoticings.forEach(n => console.log(` → ${n}`));
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
} else if (loopResult.phase === 'respond') {
|
|
538
|
+
// HEAD decided no dispatch needed — show rationale
|
|
539
|
+
if (loopResult.rationale) console.log(`\n${loopResult.rationale}`);
|
|
540
|
+
if (loopResult.surfaceNoticings?.length) {
|
|
541
|
+
loopResult.surfaceNoticings.forEach(n => console.log(` → ${n}`));
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Surface noticings (includes update notices, diagnostics)
|
|
547
|
+
if (loopResult.surfaceNoticings?.length && verbose) {
|
|
548
|
+
loopResult.surfaceNoticings.forEach(n => console.log(` → ${n}`));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
// Cognitive loop unavailable or errored — proceed with original prompt
|
|
553
|
+
}
|
|
554
|
+
|
|
495
555
|
// ── Dispatch visualization ─────────────────────────────────────────────────
|
|
496
556
|
const fxGo = await getFx();
|
|
497
557
|
let dispatchSpinner = null;
|
|
@@ -499,7 +559,7 @@ async function cmdGo(args, opts = {}) {
|
|
|
499
559
|
dispatchSpinner = fxGo.spinner(`Dispatching agent...`).start();
|
|
500
560
|
}
|
|
501
561
|
|
|
502
|
-
const { plan, result } = await runPipeline('go',
|
|
562
|
+
const { plan, result } = await runPipeline('go', loopEnhancedPrompt, {
|
|
503
563
|
files,
|
|
504
564
|
cwd,
|
|
505
565
|
verbose,
|
|
@@ -511,6 +571,23 @@ async function cmdGo(args, opts = {}) {
|
|
|
511
571
|
dispatchSpinner.succeed(`Agent dispatched: ${prompt.slice(0, 50)}`);
|
|
512
572
|
}
|
|
513
573
|
|
|
574
|
+
// ── Cognitive loop: advance after dispatch completes ─────────────────────────
|
|
575
|
+
if (loopDispatchMeta && result && !dryRun) {
|
|
576
|
+
try {
|
|
577
|
+
const cogLoop = await _getCognitiveLoop();
|
|
578
|
+
if (cogLoop) {
|
|
579
|
+
const waveId = loopDispatchMeta.nextDispatch.waveId;
|
|
580
|
+
const rawResults = [result.summary || result.output || ''];
|
|
581
|
+
const advanceResult = cogLoop.advance(rawResults, waveId, { files });
|
|
582
|
+
if (verbose && advanceResult) {
|
|
583
|
+
console.log(` [cognitive-loop] Next phase: ${advanceResult.phase}, rationale: ${advanceResult.rationale || '-'}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
// Non-fatal — loop advance failure doesn't affect the completed dispatch
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
514
591
|
if (dryRun) {
|
|
515
592
|
// formatExecutionPlan already printed by pipeline when verbose/dryRun=true
|
|
516
593
|
console.log('\n(dry-run — not executing)');
|
|
@@ -2149,6 +2226,48 @@ function classifyInput(input) {
|
|
|
2149
2226
|
}
|
|
2150
2227
|
|
|
2151
2228
|
// ── HEAD cognitive pipeline: replaces regex-based cheap/full split ──────
|
|
2229
|
+
// Try cognitive loop first (wraps HEAD with wave planning + predictions)
|
|
2230
|
+
if (_cognitiveLoopCache) {
|
|
2231
|
+
try {
|
|
2232
|
+
const loopResult = _cognitiveLoopCache.enter(trimmed, {});
|
|
2233
|
+
|
|
2234
|
+
const judgment = {
|
|
2235
|
+
depth: loopResult.action?.depth || 'full',
|
|
2236
|
+
action: loopResult.action,
|
|
2237
|
+
shouldAskUser: loopResult.shouldAskUser,
|
|
2238
|
+
shouldDispatch: loopResult.phase === 'dispatch',
|
|
2239
|
+
shouldClarify: loopResult.action?.type === 'clarify',
|
|
2240
|
+
shouldThink: loopResult.action?.type === 'think',
|
|
2241
|
+
rationale: loopResult.rationale,
|
|
2242
|
+
confidence: loopResult.action?.confidence,
|
|
2243
|
+
obligations: loopResult.action?.obligations,
|
|
2244
|
+
surfaceNoticings: loopResult.surfaceNoticings,
|
|
2245
|
+
// Cognitive loop extensions
|
|
2246
|
+
_loopResult: loopResult,
|
|
2247
|
+
_plan: loopResult.plan,
|
|
2248
|
+
_nextDispatch: loopResult.nextDispatch,
|
|
2249
|
+
};
|
|
2250
|
+
|
|
2251
|
+
// Loop says respond — no dispatch needed
|
|
2252
|
+
if (loopResult.phase === 'respond') {
|
|
2253
|
+
return { tier: 'cheap', headJudgment: judgment };
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Loop says dispatch — full tier, use plan's first agent tier to pick model
|
|
2257
|
+
if (loopResult.phase === 'dispatch') {
|
|
2258
|
+
const firstAgent = loopResult.nextDispatch?.agents?.[0];
|
|
2259
|
+
const model = firstAgent?.tier === 'deep' || firstAgent?.tier === 'opus' ? 'opus' : 'sonnet';
|
|
2260
|
+
return { tier: 'full', headJudgment: judgment, model };
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// Default: cheap
|
|
2264
|
+
return { tier: 'cheap', headJudgment: judgment };
|
|
2265
|
+
} catch {
|
|
2266
|
+
// Cognitive loop failed — fall through to direct HEAD
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// Direct HEAD fallback (when cognitive loop unavailable or errored)
|
|
2152
2271
|
const head = _headModuleCache;
|
|
2153
2272
|
if (head) {
|
|
2154
2273
|
const state = _getHeadState() || head.freshState();
|
|
@@ -2722,6 +2841,19 @@ async function mainScreen(rl, ask) {
|
|
|
2722
2841
|
return signalLine(item.ok ? 'success' : 'warning', `${DIM}${item.text}${RST}`);
|
|
2723
2842
|
});
|
|
2724
2843
|
|
|
2844
|
+
// ── Cognitive loop status (appended to signals) ────────────────────────────
|
|
2845
|
+
try {
|
|
2846
|
+
const cogLoop = await _getCognitiveLoop();
|
|
2847
|
+
if (cogLoop) {
|
|
2848
|
+
const loopStatus = cogLoop.getLoopStatus();
|
|
2849
|
+
if (loopStatus.hasActivePlan) {
|
|
2850
|
+
const wavePart = `${loopStatus.completedWaves}/${loopStatus.totalWaves} waves`;
|
|
2851
|
+
const replanPart = loopStatus.replans > 0 ? ` · ${loopStatus.replans} replan${loopStatus.replans !== 1 ? 's' : ''}` : '';
|
|
2852
|
+
recentLines.push(signalLine('info', `${DIM}[loop] ${wavePart}${replanPart}${RST}`));
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
} catch { /* non-fatal */ }
|
|
2856
|
+
|
|
2725
2857
|
// ── Resolve dashboard spinner before rendering ────────────────────────────
|
|
2726
2858
|
if (_spinnerTimeout) clearTimeout(_spinnerTimeout);
|
|
2727
2859
|
if (dashSpinner) dashSpinner.succeed('Dashboard ready');
|
|
@@ -3064,6 +3196,18 @@ async function mainScreen(rl, ask) {
|
|
|
3064
3196
|
process.stdout.write('\n');
|
|
3065
3197
|
}
|
|
3066
3198
|
|
|
3199
|
+
// Show cognitive loop plan info if available
|
|
3200
|
+
if (hj?._plan) {
|
|
3201
|
+
const plan = hj._plan;
|
|
3202
|
+
const waveCount = plan.waves?.length || 0;
|
|
3203
|
+
const agentCount = plan.waves?.reduce((sum, w) => sum + (w.agents?.length || 0), 0) || 0;
|
|
3204
|
+
process.stdout.write(`\n \x1b[2m[plan] ${waveCount} wave${waveCount !== 1 ? 's' : ''}, ${agentCount} agent${agentCount !== 1 ? 's' : ''}\x1b[0m`);
|
|
3205
|
+
if (hj._nextDispatch?.warnings?.length > 0) {
|
|
3206
|
+
process.stdout.write(` \x1b[33m${hj._nextDispatch.warnings.length} warning(s)\x1b[0m`);
|
|
3207
|
+
}
|
|
3208
|
+
process.stdout.write('\n');
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3067
3211
|
// HEAD's shouldAskUser gates the dispatch — dangerous/irreversible ops
|
|
3068
3212
|
if (hj?.shouldAskUser) {
|
|
3069
3213
|
const reason = hj.obligations?.find(o => o.type === 'askBeforeIrreversi')?.description || hj.rationale;
|
|
@@ -3073,14 +3217,14 @@ async function mainScreen(rl, ask) {
|
|
|
3073
3217
|
process.stdout.write(` \x1b[36mEnter\x1b[0m proceed \x1b[36mn\x1b[0m cancel\n\n`);
|
|
3074
3218
|
const confirm = (await ask(' > ')).trim().toLowerCase();
|
|
3075
3219
|
if (confirm === 'n' || confirm === 'no') return { next: 'main' };
|
|
3076
|
-
return { next: 'go', prompt: input, model };
|
|
3220
|
+
return { next: 'go', prompt: input, model, _loopResult: hj._loopResult };
|
|
3077
3221
|
}
|
|
3078
3222
|
|
|
3079
3223
|
// Automode: if HEAD says it's safe, just go — no confirmation needed
|
|
3080
3224
|
const automode = profile.automode ?? profile.settings?.automode ?? false;
|
|
3081
3225
|
if (automode) {
|
|
3082
3226
|
process.stdout.write(`\n \x1b[36m⚡\x1b[0m ${summary} (${model}, depth: ${hj?.depth || '?'})\n`);
|
|
3083
|
-
return { next: 'go', prompt: input, model };
|
|
3227
|
+
return { next: 'go', prompt: input, model, _loopResult: hj._loopResult };
|
|
3084
3228
|
}
|
|
3085
3229
|
|
|
3086
3230
|
// Manual mode — show depth, wait for confirmation
|
|
@@ -3089,7 +3233,7 @@ async function mainScreen(rl, ask) {
|
|
|
3089
3233
|
process.stdout.write(` \x1b[36mEnter\x1b[0m go \x1b[36mn\x1b[0m cancel\n\n`);
|
|
3090
3234
|
const confirm = (await ask(' > ')).trim().toLowerCase();
|
|
3091
3235
|
if (confirm === 'n' || confirm === 'no') return { next: 'main' };
|
|
3092
|
-
return { next: 'go', prompt: input, model };
|
|
3236
|
+
return { next: 'go', prompt: input, model, _loopResult: hj._loopResult };
|
|
3093
3237
|
}
|
|
3094
3238
|
|
|
3095
3239
|
// Default fallback
|
|
@@ -6202,6 +6346,7 @@ async function main() {
|
|
|
6202
6346
|
primeAgentRegistry().catch(() => {});
|
|
6203
6347
|
_primeRegistryCache().catch(() => {});
|
|
6204
6348
|
_getHeadModule().catch(() => {});
|
|
6349
|
+
_getCognitiveLoop().catch(() => {});
|
|
6205
6350
|
|
|
6206
6351
|
const args = process.argv.slice(2);
|
|
6207
6352
|
const cmd = args[0];
|
|
@@ -1,59 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// auto-update-wrapper.mjs — PostToolUse hook
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// auto-update-wrapper.mjs — PostToolUse hook.
|
|
3
|
+
// Checks for updates once per session. If found, installs immediately (no wait,
|
|
4
|
+
// no question) and writes a notice so HEAD can mention it naturally.
|
|
5
|
+
// "Oh, I updated to 0.2.16" — not a prompt, just awareness.
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
7
8
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
9
|
import { join, dirname, resolve } from 'path';
|
|
9
10
|
import { fileURLToPath } from 'url';
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const WORKSPACE
|
|
13
|
-
const STATE_DIR
|
|
14
|
-
const LOCK_FILE
|
|
15
|
-
const
|
|
13
|
+
const WORKSPACE = resolve(__dirname, '..');
|
|
14
|
+
const STATE_DIR = join(WORKSPACE, '.dualbrain');
|
|
15
|
+
const LOCK_FILE = join(STATE_DIR, '.update-checked');
|
|
16
|
+
const NOTICE_FILE = join(STATE_DIR, '.update-notice');
|
|
17
|
+
const SESSION_TTL = 30 * 60 * 1000; // Once per 30 min session
|
|
16
18
|
|
|
17
|
-
// ── 1. Already checked
|
|
19
|
+
// ── 1. Already checked this session? ─────────────────────────────────────────
|
|
18
20
|
if (existsSync(LOCK_FILE)) {
|
|
19
21
|
try {
|
|
20
22
|
const lastCheck = parseInt(readFileSync(LOCK_FILE, 'utf8').trim(), 10);
|
|
21
|
-
if (Number.isFinite(lastCheck) && Date.now() - lastCheck <
|
|
23
|
+
if (Number.isFinite(lastCheck) && Date.now() - lastCheck < SESSION_TTL) {
|
|
24
|
+
// Output empty JSON (no hook action)
|
|
25
|
+
process.stdout.write('{}\n');
|
|
22
26
|
process.exit(0);
|
|
23
27
|
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Corrupt lock file — proceed with check
|
|
26
|
-
}
|
|
28
|
+
} catch {}
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
// ── 2.
|
|
31
|
+
// ── 2. Mark as checked ───────────────────────────────────────────────────────
|
|
30
32
|
try {
|
|
31
33
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
32
34
|
writeFileSync(LOCK_FILE, String(Date.now()));
|
|
33
35
|
} catch {
|
|
34
|
-
process.
|
|
36
|
+
process.stdout.write('{}\n');
|
|
37
|
+
process.exit(0);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
// ── 3.
|
|
40
|
+
// ── 3. Get local version ─────────────────────────────────────────────────────
|
|
38
41
|
let localVersion = '';
|
|
39
42
|
try {
|
|
40
43
|
const pkg = JSON.parse(readFileSync(join(WORKSPACE, 'package.json'), 'utf8'));
|
|
41
44
|
localVersion = pkg.version || '';
|
|
42
45
|
} catch {
|
|
46
|
+
process.stdout.write('{}\n');
|
|
43
47
|
process.exit(0);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
if (!localVersion)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const workerScript = `
|
|
51
|
-
import { spawnSync } from 'child_process';
|
|
52
|
-
import { readFileSync } from 'fs';
|
|
53
|
-
|
|
54
|
-
const localVersion = ${JSON.stringify(localVersion)};
|
|
50
|
+
if (!localVersion) {
|
|
51
|
+
process.stdout.write('{}\n');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
55
54
|
|
|
56
|
-
//
|
|
55
|
+
// ── 4. Check registry (fast, 3s timeout) ─────────────────────────────────────
|
|
57
56
|
const npmResult = spawnSync('npm', ['view', 'dual-brain', 'version'], {
|
|
58
57
|
encoding: 'utf8',
|
|
59
58
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -61,42 +60,41 @@ const npmResult = spawnSync('npm', ['view', 'dual-brain', 'version'], {
|
|
|
61
60
|
});
|
|
62
61
|
|
|
63
62
|
const latestVersion = (npmResult.stdout || '').trim();
|
|
64
|
-
if (!latestVersion
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
function versionGt(a, b) {
|
|
68
|
-
const ap = a.replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
69
|
-
const bp = b.replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
70
|
-
const len = Math.max(ap.length, bp.length);
|
|
71
|
-
for (let i = 0; i < len; i++) {
|
|
72
|
-
const diff = (bp[i] || 0) - (ap[i] || 0);
|
|
73
|
-
if (diff !== 0) return diff > 0;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
63
|
+
if (!latestVersion || !_isNewer(localVersion, latestVersion)) {
|
|
64
|
+
process.stdout.write('{}\n');
|
|
65
|
+
process.exit(0);
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
// ── 5. Install immediately — no detach, no background, just do it ────────────
|
|
69
|
+
process.stderr.write(`[dual-brain] updating ${localVersion} → ${latestVersion}\n`);
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
stdio: 'ignore',
|
|
85
|
-
timeout: 120000,
|
|
71
|
+
const installResult = spawnSync('npm', ['install', '-g', `dual-brain@${latestVersion}`], {
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
timeout: 30000,
|
|
86
75
|
});
|
|
87
|
-
`;
|
|
88
76
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
child.stdin.write(workerScript);
|
|
99
|
-
child.stdin.end();
|
|
100
|
-
child.unref(); // Let parent exit without waiting
|
|
77
|
+
if (installResult.status === 0) {
|
|
78
|
+
// Write notice for HEAD to pick up
|
|
79
|
+
const notice = { from: localVersion, to: latestVersion, ts: Date.now() };
|
|
80
|
+
writeFileSync(NOTICE_FILE, JSON.stringify(notice));
|
|
81
|
+
process.stderr.write(`[dual-brain] updated to ${latestVersion}\n`);
|
|
82
|
+
} else {
|
|
83
|
+
process.stderr.write(`[dual-brain] update failed, continuing with ${localVersion}\n`);
|
|
84
|
+
}
|
|
101
85
|
|
|
86
|
+
process.stdout.write('{}\n');
|
|
102
87
|
process.exit(0);
|
|
88
|
+
|
|
89
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function _isNewer(local, remote) {
|
|
92
|
+
const lp = local.split('.').map(Number);
|
|
93
|
+
const rp = remote.split('.').map(Number);
|
|
94
|
+
for (let i = 0; i < Math.max(lp.length, rp.length); i++) {
|
|
95
|
+
const diff = (rp[i] || 0) - (lp[i] || 0);
|
|
96
|
+
if (diff > 0) return true;
|
|
97
|
+
if (diff < 0) return false;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|