loopengine 0.1.81 → 0.1.82
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 +3 -3
- package/dist/web/agents-config-page.js +88 -15
- package/dist/web/console-shell.js +1 -1
- package/dist/web/decide-page.js +1 -1
- package/dist/web/global-config-page.js +18 -4
- package/dist/web/upgrade-admin.d.ts +6 -1
- package/dist/web/upgrade-admin.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,9 +178,9 @@ webhook, Slack or Lark target for one agent (saved to
|
|
|
178
178
|
an older `notifier.yml` is still read), sends a test and shows recent
|
|
179
179
|
deliveries. With Slack, people can also reply in a request's thread to
|
|
180
180
|
ask the agent about it, and it answers there (**Answer replies in a
|
|
181
|
-
request's thread**, on by default). The
|
|
182
|
-
|
|
183
|
-
agent has `ask` rules but nobody would be told.
|
|
181
|
+
request's thread**, on by default). The same tab lists every decision
|
|
182
|
+
still waiting with Approve/Deny buttons (its count shows on the tab), and
|
|
183
|
+
warns you when an agent has `ask` rules but nobody would be told.
|
|
184
184
|
|
|
185
185
|
Every notification links to a **decision page**: a signed, single-use
|
|
186
186
|
link (`/decide/<token>`, valid 7 days) where whoever got the message can
|
|
@@ -615,10 +615,49 @@ ${consoleShellScript}
|
|
|
615
615
|
card('skills', 'Skills', String(cfg.skills.length), escapeHtml(cfg.skills.map(function (k) { return k.name; }).join(', ')) || 'None yet') +
|
|
616
616
|
card('actauth', 'Permissions', rules.length + (rules.length === 1 ? ' rule' : ' rules'),
|
|
617
617
|
(asks ? asks + ' need approval \u00b7 ' : '') + 'unmatched calls: ' + escapeHtml(cfg.permissions.defaultDecision)) +
|
|
618
|
+
card('messaging', 'Messaging', '<span id="ovMessagingValue">…</span>', '<span id="ovMessagingDetail">Loading…</span>') +
|
|
619
|
+
card('schedules', 'Schedules', '<span id="ovSchedulesValue">…</span>', '<span id="ovSchedulesDetail">Loading…</span>') +
|
|
618
620
|
card('env', 'Environment', 'Settings', '<span data-le-env-count="' + escapeHtml(cfg.name) + '">' + leShell.envCountHtml(cfg.name) + '</span> Values your installed abilities read') +
|
|
619
621
|
'</div>';
|
|
620
622
|
}
|
|
621
623
|
|
|
624
|
+
var APP_NAMES = { slack: 'Slack', lark: 'Lark', webhook: 'Webhook', email: 'Email', database: 'Database', redis: 'Redis' };
|
|
625
|
+
|
|
626
|
+
// Messaging and Schedules come from their own endpoints, so their
|
|
627
|
+
// cards are filled in once those answer. Called after every render of
|
|
628
|
+
// Overview, which is also re-rendered after edits.
|
|
629
|
+
function fillOverviewCards(name) {
|
|
630
|
+
Promise.all([
|
|
631
|
+
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/notifier'),
|
|
632
|
+
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/pending'),
|
|
633
|
+
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/schedules'),
|
|
634
|
+
]).then(function (results) {
|
|
635
|
+
if (currentName !== name) return;
|
|
636
|
+
var set = function (id, html) { var el = detail.querySelector('#' + id); if (el) el.innerHTML = html; };
|
|
637
|
+
var n = results[0].ok ? results[0].body : null;
|
|
638
|
+
var pending = (results[1].ok && results[1].body.pending) || [];
|
|
639
|
+
var connected = n && (n.source === 'file' || n.source === 'code' || n.source === 'default');
|
|
640
|
+
set('ovMessagingValue', !n ? '—' : connected ? escapeHtml(APP_NAMES[n.channel] || n.channel) + (n.source === 'default' ? ' (default)' : '') : 'Not connected');
|
|
641
|
+
var messagingDetail = [];
|
|
642
|
+
if (pending.length) messagingDetail.push(pending.length + ' waiting for a decision');
|
|
643
|
+
if (n && connected && n.channel === 'slack' && n.plan !== 'free' && (!n.file || n.file.conversations !== false)) messagingDetail.push('answers replies in threads');
|
|
644
|
+
if (n && !connected) messagingDetail.push(n.missingSecret ? 'Saved, but its secret isn' + String.fromCharCode(8217) + 't set' : 'Send approvals and questions to Slack, Lark or a webhook');
|
|
645
|
+
if (!messagingDetail.length && n) messagingDetail.push('Sends ' + escapeHtml((n.events || []).join(', ')));
|
|
646
|
+
set('ovMessagingDetail', messagingDetail.join(' \u00b7 '));
|
|
647
|
+
showPendingCount(pending.length);
|
|
648
|
+
|
|
649
|
+
var tasks = (results[2].ok && results[2].body.tasks) || [];
|
|
650
|
+
var active = tasks.filter(function (t) { return t.status !== 'cancelled' && t.status !== 'done'; });
|
|
651
|
+
var waiting = active.filter(function (t) { return t.status === 'awaiting_approval'; }).length;
|
|
652
|
+
var next = active.map(function (t) { return t.next_run_at; }).filter(Boolean).sort()[0];
|
|
653
|
+
set('ovSchedulesValue', String(active.length));
|
|
654
|
+
var schedulesDetail = [];
|
|
655
|
+
if (waiting) schedulesDetail.push(waiting + ' waiting for approval');
|
|
656
|
+
if (next) schedulesDetail.push('Next run ' + escapeHtml(new Date(next).toLocaleString()));
|
|
657
|
+
set('ovSchedulesDetail', schedulesDetail.join(' \u00b7 ') || (active.length ? 'None due' : 'Run this agent on a timer'));
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
622
661
|
function renderOverviewHtml(cfg) {
|
|
623
662
|
var systemPromptSection =
|
|
624
663
|
'<h3>System prompt <button type="button" class="edit-btn" id="editSystemPromptBtn">Edit</button></h3>' +
|
|
@@ -754,6 +793,7 @@ ${consoleShellScript}
|
|
|
754
793
|
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
755
794
|
currentCfg = result.body;
|
|
756
795
|
overviewPanel.innerHTML = renderOverviewHtml(result.body);
|
|
796
|
+
fillOverviewCards(currentName);
|
|
757
797
|
wireOverviewHandlers(name);
|
|
758
798
|
})
|
|
759
799
|
.catch(function (err) {
|
|
@@ -802,6 +842,7 @@ ${consoleShellScript}
|
|
|
802
842
|
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
803
843
|
currentCfg = result.body;
|
|
804
844
|
overviewPanel.innerHTML = renderOverviewHtml(result.body);
|
|
845
|
+
fillOverviewCards(currentName);
|
|
805
846
|
wireOverviewHandlers(name);
|
|
806
847
|
})
|
|
807
848
|
.catch(function (err) {
|
|
@@ -849,6 +890,7 @@ ${consoleShellScript}
|
|
|
849
890
|
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
850
891
|
currentCfg = result.body;
|
|
851
892
|
overviewPanel.innerHTML = renderOverviewHtml(result.body);
|
|
893
|
+
fillOverviewCards(currentName);
|
|
852
894
|
wireOverviewHandlers(name);
|
|
853
895
|
})
|
|
854
896
|
.catch(function (err) {
|
|
@@ -897,14 +939,40 @@ ${consoleShellScript}
|
|
|
897
939
|
// opened. ----
|
|
898
940
|
|
|
899
941
|
function renderActauthTabPlaceholder() {
|
|
900
|
-
return '<div id="
|
|
942
|
+
return '<div id="pendingNote"></div>' +
|
|
901
943
|
'<section id="actauthSection"><div id="actauthContent"><p class="hint">Loading…</p></div></section>';
|
|
902
944
|
}
|
|
903
945
|
|
|
904
|
-
// The Messaging tab:
|
|
905
|
-
// whether the agent answers replies
|
|
946
|
+
// The Messaging tab: the decisions still waiting, where approval
|
|
947
|
+
// requests and questions go, and whether the agent answers replies
|
|
948
|
+
// there. Filled by loadApprovalsSection.
|
|
906
949
|
function renderMessagingTabPlaceholder() {
|
|
907
|
-
return '<div id="
|
|
950
|
+
return '<div id="pendingContent"></div>' +
|
|
951
|
+
'<div id="approvalsContent"><p class="hint">Loading…</p></div>';
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// How many decisions are waiting: a badge on the Messaging tab, seen
|
|
955
|
+
// from every tab, and a line at the top of Permissions.
|
|
956
|
+
function showPendingCount(n) {
|
|
957
|
+
leShell.setCount('messaging', n);
|
|
958
|
+
var link = detail.querySelector('.le-tab[data-tab="messaging"]');
|
|
959
|
+
var badge = link && link.querySelector('.le-count');
|
|
960
|
+
if (badge) {
|
|
961
|
+
badge.className = 'le-count warn';
|
|
962
|
+
if (!n) badge.remove();
|
|
963
|
+
}
|
|
964
|
+
var note = detail.querySelector('#pendingNote');
|
|
965
|
+
if (note) {
|
|
966
|
+
note.innerHTML = n
|
|
967
|
+
? '<div class="approvals-nudge">' + (n === 1 ? '1 request is' : n + ' requests are') + ' waiting for a decision. <a href="?tab=messaging" data-tab="messaging">See Messaging</a></div>'
|
|
968
|
+
: '';
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function refreshPendingCount(name) {
|
|
973
|
+
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/pending').then(function (r) {
|
|
974
|
+
if (currentName === name && r.ok) showPendingCount((r.body.pending || []).length);
|
|
975
|
+
});
|
|
908
976
|
}
|
|
909
977
|
|
|
910
978
|
function renderActauthRuleRow(r) {
|
|
@@ -968,12 +1036,11 @@ ${consoleShellScript}
|
|
|
968
1036
|
'</form></section>';
|
|
969
1037
|
}
|
|
970
1038
|
|
|
971
|
-
// ---- Messaging tab:
|
|
972
|
-
//
|
|
973
|
-
//
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
// list is current. ----
|
|
1039
|
+
// ---- Messaging tab: the decisions still waiting (GET .../pending),
|
|
1040
|
+
// where "ask" decisions go when nobody is in Chat, and whether the
|
|
1041
|
+
// agent answers replies there (web/notifier-admin.ts behind
|
|
1042
|
+
// GET/PUT/DELETE .../notifier), plus recent deliveries. Reloaded every
|
|
1043
|
+
// time the tab opens, so the waiting list is current. ----
|
|
977
1044
|
|
|
978
1045
|
var lastNotifier = null;
|
|
979
1046
|
var NOTIFIER_EVENTS = [
|
|
@@ -1111,7 +1178,7 @@ ${consoleShellScript}
|
|
|
1111
1178
|
if (tools.length && n && n.source === 'none') {
|
|
1112
1179
|
html += '<div class="approvals-nudge"><strong>Approval requests aren't sent anywhere yet.</strong> ' +
|
|
1113
1180
|
escapeHtml(tools.slice(0, 3).join(', ')) + (tools.length > 3 ? ' and more' : '') + ' can stop for approval, but when an app calls this agent over HTTP nobody is notified. ' +
|
|
1114
|
-
'
|
|
1181
|
+
'Choose where they go below.</div>';
|
|
1115
1182
|
}
|
|
1116
1183
|
if (pending.length) html += '<section><h3>Waiting for a decision (' + pending.length + ')</h3>' + renderPendingTable(pending) + '</section>';
|
|
1117
1184
|
return html;
|
|
@@ -1289,10 +1356,12 @@ ${consoleShellScript}
|
|
|
1289
1356
|
content.innerHTML = renderApprovalsHtml(results[0].body);
|
|
1290
1357
|
wireApprovalsHandlers(name);
|
|
1291
1358
|
var pendingEl = detail.querySelector('#pendingContent');
|
|
1359
|
+
var pending = (results[1].ok && results[1].body.pending) || [];
|
|
1292
1360
|
if (pendingEl) {
|
|
1293
|
-
pendingEl.innerHTML = renderPendingHtml(
|
|
1361
|
+
pendingEl.innerHTML = renderPendingHtml(pending, results[0].body);
|
|
1294
1362
|
wirePendingHandlers(name);
|
|
1295
1363
|
}
|
|
1364
|
+
showPendingCount(pending.length);
|
|
1296
1365
|
if (message) { var r = content.querySelector('#notifierResult'); if (r) r.textContent = message; }
|
|
1297
1366
|
}).catch(function (err) {
|
|
1298
1367
|
if (currentName !== name) return;
|
|
@@ -1347,7 +1416,7 @@ ${consoleShellScript}
|
|
|
1347
1416
|
'<div class="schedule-result hint">' + escapeHtml(t.message) + '</div></td>' +
|
|
1348
1417
|
'<td>' + scheduleText(t.schedule) + (t.next_run_at ? '<div class="hint">next ' + escapeHtml(new Date(t.next_run_at).toLocaleString()) + '</div>' : '') + '</td>' +
|
|
1349
1418
|
'<td><span class="le-pill ' + (SCHEDULE_PILLS[t.status] || 'neutral') + '">' + escapeHtml(SCHEDULE_LABELS[t.status] || t.status) + '</span>' +
|
|
1350
|
-
(t.status === 'awaiting_approval' ? '<div class="hint"><a href="#" class="schedule-to-approvals">Decide in
|
|
1419
|
+
(t.status === 'awaiting_approval' ? '<div class="hint"><a href="#" class="schedule-to-approvals">Decide in Messaging</a></div>' : '') + '</td>' +
|
|
1351
1420
|
'<td style="min-width:200px">' + last + (history ? '<details><summary class="hint">History (' + (t.history || []).length + ')</summary><ul class="schedule-history">' + history + '</ul></details>' : '') + '</td>' +
|
|
1352
1421
|
'<td><div style="display:flex;gap:6px;flex-wrap:wrap">' + actions.map(function (a) {
|
|
1353
1422
|
return '<button type="button" class="schedule-action' + (a[0] === 'run' ? ' primary' : '') + (a[0] === 'cancel' || a[0] === 'delete' ? ' delete-btn' : '') + '" data-id="' + escapeHtml(t.task_id) + '" data-action="' + a[0] + '">' + a[1] + '</button>';
|
|
@@ -1400,7 +1469,7 @@ ${consoleShellScript}
|
|
|
1400
1469
|
}
|
|
1401
1470
|
var toApprovals = content.querySelectorAll('.schedule-to-approvals');
|
|
1402
1471
|
for (var j = 0; j < toApprovals.length; j++) {
|
|
1403
|
-
toApprovals[j].addEventListener('click', function (ev) { ev.preventDefault(); switchTab('
|
|
1472
|
+
toApprovals[j].addEventListener('click', function (ev) { ev.preventDefault(); switchTab('messaging'); });
|
|
1404
1473
|
}
|
|
1405
1474
|
var form = content.querySelector('#scheduleForm');
|
|
1406
1475
|
var result = content.querySelector('#scheduleResult');
|
|
@@ -2940,7 +3009,8 @@ ${consoleShellScript}
|
|
|
2940
3009
|
if (tab === 'actauth' && actauthLoadedFor !== currentName) {
|
|
2941
3010
|
loadActauthTab(currentName);
|
|
2942
3011
|
}
|
|
2943
|
-
if (tab === '
|
|
3012
|
+
if (tab === 'messaging') loadApprovalsSection(currentName);
|
|
3013
|
+
if (tab === 'actauth') refreshPendingCount(currentName);
|
|
2944
3014
|
if (tab === 'env' && envLoadedFor !== currentName) {
|
|
2945
3015
|
loadEnvTab(currentName);
|
|
2946
3016
|
}
|
|
@@ -2969,6 +3039,7 @@ ${consoleShellScript}
|
|
|
2969
3039
|
var overviewPanel = detail.querySelector('[data-tab-panel="overview"]');
|
|
2970
3040
|
if (overviewPanel) {
|
|
2971
3041
|
overviewPanel.innerHTML = renderOverviewHtml(cfg);
|
|
3042
|
+
fillOverviewCards(currentName);
|
|
2972
3043
|
wireOverviewHandlers(name);
|
|
2973
3044
|
}
|
|
2974
3045
|
})
|
|
@@ -3012,6 +3083,7 @@ ${consoleShellScript}
|
|
|
3012
3083
|
var skillsPanel = skillsPanelEl();
|
|
3013
3084
|
if (overviewPanel) {
|
|
3014
3085
|
overviewPanel.innerHTML = renderOverviewHtml(cfg);
|
|
3086
|
+
fillOverviewCards(currentName);
|
|
3015
3087
|
wireOverviewHandlers(name);
|
|
3016
3088
|
}
|
|
3017
3089
|
if (skillsPanel) {
|
|
@@ -3059,6 +3131,7 @@ ${consoleShellScript}
|
|
|
3059
3131
|
empty.style.display = 'none';
|
|
3060
3132
|
detail.style.display = 'flex';
|
|
3061
3133
|
switchTab(currentTab);
|
|
3134
|
+
fillOverviewCards(cfg.name);
|
|
3062
3135
|
}
|
|
3063
3136
|
|
|
3064
3137
|
function selectAgent(name) {
|
|
@@ -43,7 +43,7 @@ export const consoleShellScript = `<script>
|
|
|
43
43
|
// after the list loads.
|
|
44
44
|
var toSet = {};
|
|
45
45
|
var TABS = [
|
|
46
|
-
['
|
|
46
|
+
['overview', 'Overview'], ['chat', 'Chat'], ['abilities', 'Abilities'], ['tools', 'Tools'],
|
|
47
47
|
['skills', 'Skills'], ['actauth', 'Permissions'], ['messaging', 'Messaging'], ['schedules', 'Schedules'], ['env', 'Environment']
|
|
48
48
|
];
|
|
49
49
|
|
package/dist/web/decide-page.js
CHANGED
|
@@ -53,7 +53,7 @@ ${script}
|
|
|
53
53
|
}
|
|
54
54
|
/** A link that's malformed, tampered with, or past its expiry. */
|
|
55
55
|
export function renderDecideInvalidPage() {
|
|
56
|
-
return shell('Link not valid', '<h1>This link isn't valid</h1><p class="muted">It may have expired or been copied incompletely. Ask for a new one, or decide from the agent's
|
|
56
|
+
return shell('Link not valid', '<h1>This link isn't valid</h1><p class="muted">It may have expired or been copied incompletely. Ask for a new one, or decide from the agent's Messaging tab.</p>');
|
|
57
57
|
}
|
|
58
58
|
/** A valid link whose decision has already been made. */
|
|
59
59
|
export function renderDecideGonePage() {
|
|
@@ -314,13 +314,19 @@ ${consoleShellScript}
|
|
|
314
314
|
|
|
315
315
|
function upgradeBox() { return document.getElementById('upgradeBox'); }
|
|
316
316
|
|
|
317
|
+
// When npm was last asked, and a link to ask again now.
|
|
318
|
+
function upgradeCheckedLine(u) {
|
|
319
|
+
var when = u.checkedAt ? 'Checked at ' + escapeHtml(new Date(u.checkedAt).toLocaleTimeString()) + '. ' : '';
|
|
320
|
+
return '<p class="hint" style="margin:6px 0 0">' + when + '<a href="#" id="upgradeRecheck">Check again</a></p>';
|
|
321
|
+
}
|
|
322
|
+
|
|
317
323
|
function renderUpgrade(u) {
|
|
318
324
|
var box = upgradeBox();
|
|
319
325
|
if (!box) return;
|
|
320
326
|
if (u.unavailableReason && !u.updateAvailable) {
|
|
321
|
-
box.innerHTML = '<p class="hint">' + escapeHtml(u.unavailableReason) + '</p>';
|
|
327
|
+
box.innerHTML = '<p class="hint">' + escapeHtml(u.unavailableReason) + '</p>' + (u.templateVersion ? upgradeCheckedLine(u) : '');
|
|
322
328
|
} else if (!u.updateAvailable) {
|
|
323
|
-
box.innerHTML = '<p>You' + String.fromCharCode(8217) + 're on the latest version, <code>' + escapeHtml(u.current) + '</code>.</p>';
|
|
329
|
+
box.innerHTML = '<p>You' + String.fromCharCode(8217) + 're on the latest version, <code>' + escapeHtml(u.current) + '</code>.</p>' + upgradeCheckedLine(u);
|
|
324
330
|
} else if (u.unavailableReason) {
|
|
325
331
|
box.innerHTML = '<p>LoopEngine <code>' + escapeHtml(u.latest) + '</code> is available (this server runs <code>' + escapeHtml(u.current) + '</code>).</p><p class="hint">' + escapeHtml(u.unavailableReason) + '</p>';
|
|
326
332
|
} else if (!u.canUpgrade) {
|
|
@@ -333,8 +339,8 @@ ${consoleShellScript}
|
|
|
333
339
|
}
|
|
334
340
|
}
|
|
335
341
|
|
|
336
|
-
function loadUpgrade() {
|
|
337
|
-
fetch('/config/upgrade')
|
|
342
|
+
function loadUpgrade(refresh) {
|
|
343
|
+
fetch('/config/upgrade' + (refresh ? '?refresh=1' : ''), { cache: 'no-store' })
|
|
338
344
|
.then(function (r) { return r.json(); })
|
|
339
345
|
.then(renderUpgrade)
|
|
340
346
|
.catch(function (err) {
|
|
@@ -366,6 +372,14 @@ ${consoleShellScript}
|
|
|
366
372
|
}
|
|
367
373
|
|
|
368
374
|
document.getElementById('settingsContent').addEventListener('click', function (ev) {
|
|
375
|
+
var recheck = ev.target.closest('#upgradeRecheck');
|
|
376
|
+
if (recheck) {
|
|
377
|
+
ev.preventDefault();
|
|
378
|
+
var box = upgradeBox();
|
|
379
|
+
if (box) box.innerHTML = '<p class="hint">Checking for a newer LoopEngine' + String.fromCharCode(8230) + '</p>';
|
|
380
|
+
loadUpgrade(true);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
369
383
|
var btn = ev.target.closest('#upgradeBtn');
|
|
370
384
|
if (!btn) return;
|
|
371
385
|
if (btn.getAttribute('data-armed') !== '1') {
|
|
@@ -4,6 +4,8 @@ export interface UpgradeStatus {
|
|
|
4
4
|
current: string;
|
|
5
5
|
/** Latest loopengine on npm; null when npm couldn't be reached. */
|
|
6
6
|
latest: string | null;
|
|
7
|
+
/** When npm was last asked for `latest`. */
|
|
8
|
+
checkedAt: string | null;
|
|
7
9
|
updateAvailable: boolean;
|
|
8
10
|
/** The create-loopengine version this project was scaffolded or last
|
|
9
11
|
* upgraded from (.create-loopengine.json); null when the project
|
|
@@ -29,7 +31,10 @@ export interface UpgradeOutcome {
|
|
|
29
31
|
type Runner = (cmd: string, args: string[], cwd: string, timeoutMs: number) => Promise<string>;
|
|
30
32
|
/** Tests replace npm/npx with a fake. */
|
|
31
33
|
export declare function setUpgradeRunner(next: Runner | undefined): void;
|
|
32
|
-
|
|
34
|
+
/** `refresh`: ask npm again even if it was asked a moment ago. */
|
|
35
|
+
export declare function describeUpgrade(projectDir?: string, options?: {
|
|
36
|
+
refresh?: boolean;
|
|
37
|
+
}): Promise<UpgradeStatus>;
|
|
33
38
|
/**
|
|
34
39
|
* Runs the upgrade. `busy` is checked first: upgrading under a running
|
|
35
40
|
* chat turn or scheduled run would restart the server mid-reply. Resolves
|
|
@@ -40,6 +40,8 @@ export function setUpgradeRunner(next) {
|
|
|
40
40
|
run = next ?? realRun;
|
|
41
41
|
latestCache = undefined;
|
|
42
42
|
}
|
|
43
|
+
// A minute, so reloading Settings doesn't ask npm every time while a
|
|
44
|
+
// release that just came out shows up soon (the page's Check again skips it).
|
|
43
45
|
let latestCache;
|
|
44
46
|
async function latestLoopengine(projectDir) {
|
|
45
47
|
if (latestCache && latestCache.until > Date.now())
|
|
@@ -51,7 +53,7 @@ async function latestLoopengine(projectDir) {
|
|
|
51
53
|
catch {
|
|
52
54
|
value = null;
|
|
53
55
|
}
|
|
54
|
-
latestCache = { value, until: Date.now() +
|
|
56
|
+
latestCache = { value, until: Date.now() + 60_000, at: new Date().toISOString() };
|
|
55
57
|
return value;
|
|
56
58
|
}
|
|
57
59
|
function templateVersion(projectDir) {
|
|
@@ -66,13 +68,17 @@ function templateVersion(projectDir) {
|
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
let running = false;
|
|
69
|
-
|
|
71
|
+
/** `refresh`: ask npm again even if it was asked a moment ago. */
|
|
72
|
+
export async function describeUpgrade(projectDir = process.cwd(), options = {}) {
|
|
73
|
+
if (options.refresh)
|
|
74
|
+
latestCache = undefined;
|
|
70
75
|
const current = loopengineVersion();
|
|
71
76
|
const latest = await latestLoopengine(projectDir);
|
|
72
77
|
const template = templateVersion(projectDir);
|
|
73
78
|
const status = {
|
|
74
79
|
current,
|
|
75
80
|
latest,
|
|
81
|
+
checkedAt: latestCache?.at ?? null,
|
|
76
82
|
updateAvailable: Boolean(latest && semver.valid(current) && semver.gt(latest, current)),
|
|
77
83
|
templateVersion: template,
|
|
78
84
|
running,
|