loopengine 0.1.88 → 0.1.89
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/dist/core/messaging/connector.d.ts +2 -2
- package/dist/core/messaging/connectors/slack/events.js +1 -0
- package/dist/core/messaging/engine.d.ts +3 -0
- package/dist/core/messaging/engine.js +36 -7
- package/dist/core/messaging/threads.d.ts +4 -0
- package/dist/core/messaging/threads.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/web/global-config-page.js +26 -15
- package/dist/web/upgrade-admin.d.ts +6 -0
- package/dist/web/upgrade-admin.js +20 -10
- package/package.json +1 -1
|
@@ -48,8 +48,8 @@ export interface MessagingConnector {
|
|
|
48
48
|
/** The thread's messages, oldest first, its first message included. */
|
|
49
49
|
fetchThread(conversation: string, threadTs: string): Promise<ThreadMessage[]>;
|
|
50
50
|
postInThread(conversation: string, threadTs: string, text: string): Promise<void>;
|
|
51
|
-
/** A thread reply with buttons. */
|
|
52
|
-
postChoice(conversation: string, threadTs: string, text: string, choices: MessagingChoice[]): Promise<
|
|
51
|
+
/** A thread reply with buttons; resolves with the new message's id. */
|
|
52
|
+
postChoice(conversation: string, threadTs: string, text: string, choices: MessagingChoice[]): Promise<string | undefined>;
|
|
53
53
|
/** Replaces the request message itself (and its buttons) with the
|
|
54
54
|
* outcome, so nobody acts on a decision already made. Optional. */
|
|
55
55
|
replaceMessage?(conversation: string, ts: string, text: string): Promise<void>;
|
|
@@ -77,6 +77,7 @@ export function createSlackConnector(botToken) {
|
|
|
77
77
|
const body = (await res.json());
|
|
78
78
|
if (!body.ok)
|
|
79
79
|
console.error('[loopengine] chat.postMessage (choice) failed:', body.error);
|
|
80
|
+
return body.ok ? body.ts : undefined;
|
|
80
81
|
},
|
|
81
82
|
async replaceMessage(conversation, ts, text) {
|
|
82
83
|
const mrkdwn = toSlackMrkdwn(text);
|
|
@@ -60,6 +60,9 @@ export declare function handleInboundMessage(message: InboundMessage, deps: Mess
|
|
|
60
60
|
* (full-width ones too) ignored, then compared with the fixed list. A
|
|
61
61
|
* trailing question mark keeps it from matching ("yes?"). */
|
|
62
62
|
export declare function isPlainApproval(text: string): boolean;
|
|
63
|
+
/** For a decision made outside the thread (the request's own buttons):
|
|
64
|
+
* replaces the thread's message with buttons, if any, with `text`. */
|
|
65
|
+
export declare function settleThreadChoice(deps: MessagingDeps, pendingId: string, text: string): Promise<void>;
|
|
63
66
|
/** A click on one of the engine's own buttons (Approve as changed, Keep
|
|
64
67
|
* original, Confirm, Cancel) — the caller has already checked the
|
|
65
68
|
* person may decide. Returns what the clicked message should now say. */
|
|
@@ -84,8 +84,7 @@ export async function handleInboundMessage(message, deps) {
|
|
|
84
84
|
await say(`I can't change it that way: ${problem}. The request is unchanged.`);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
await connector.postChoice(message.conversation, threadTs, `Here's the change ${who} asked for:\n${formatChange(item.args, proposed, connector.escape, await deps.toolSchema(item.tool))}\nNothing else changes.`, [
|
|
87
|
+
await postChoice(deps, { ...link, proposedArgs: proposed }, `Here's the change ${who} asked for:\n${formatChange(item.args, proposed, connector.escape, await deps.toolSchema(item.tool))}\nNothing else changes.`, [
|
|
89
88
|
{ action: 'approve_edited', label: 'Approve as changed', style: 'primary', pendingId: link.pendingId },
|
|
90
89
|
{ action: 'keep_original', label: 'Keep original', pendingId: link.pendingId },
|
|
91
90
|
{ action: 'deny', label: 'Deny', style: 'danger', pendingId: link.pendingId },
|
|
@@ -100,7 +99,7 @@ export async function handleInboundMessage(message, deps) {
|
|
|
100
99
|
await runApproved(deps, link, item.tool, args, who);
|
|
101
100
|
return;
|
|
102
101
|
}
|
|
103
|
-
await
|
|
102
|
+
await postChoice(deps, link, `Confirm to run \`${item.tool}\` with:\n${formatArgs(args, connector.escape, await deps.toolSchema(item.tool))}`, [
|
|
104
103
|
{ action: 'confirm', label: 'Confirm', style: 'primary', pendingId: link.pendingId },
|
|
105
104
|
{ action: 'cancel_confirm', label: 'Cancel', pendingId: link.pendingId },
|
|
106
105
|
]);
|
|
@@ -112,6 +111,7 @@ export async function handleInboundMessage(message, deps) {
|
|
|
112
111
|
if (!outcome.error && !outcome.alreadyResolved) {
|
|
113
112
|
const what = link.kind === 'approval' ? `\`${item.tool}\`` : 'The question';
|
|
114
113
|
await replaceRequest(connector, link, `${what} was ${verdict.decision === 'deny' ? 'denied' : `answered ("${answer}")`} by ${who} in the thread.`);
|
|
114
|
+
await settleChoice(deps, link, verdict.decision === 'deny' ? `Denied by ${who}.` : `Answered by ${who}.`);
|
|
115
115
|
}
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
@@ -200,10 +200,35 @@ async function runApproved(deps, link, tool, args, who) {
|
|
|
200
200
|
await connector.postInThread(link.conversation, link.ts, `Approved${edited ? ' as changed' : ''} by ${who}. Running \`${tool}\` with:\n${formatArgs(args, connector.escape, await deps.toolSchema(tool))}`);
|
|
201
201
|
const outcome = await deps.decide(link.pendingId, { kind: 'approve', ...(edited ? { editedArgs: args } : {}) });
|
|
202
202
|
await connector.postInThread(link.conversation, link.ts, outcomeText('approve', who, outcome));
|
|
203
|
-
if (!outcome.error && !outcome.alreadyResolved)
|
|
203
|
+
if (!outcome.error && !outcome.alreadyResolved) {
|
|
204
204
|
await replaceRequest(connector, link, `\`${tool}\` was approved${edited ? ' as changed' : ''} by ${who} in the thread.`);
|
|
205
|
+
await settleChoice(deps, link, `Approved${edited ? ' as changed' : ''} by ${who}.`);
|
|
206
|
+
}
|
|
205
207
|
return outcome;
|
|
206
208
|
}
|
|
209
|
+
/** Posts a message with buttons and remembers it, replacing the previous
|
|
210
|
+
* one (whose buttons no longer apply). `link` carries any other change
|
|
211
|
+
* to record with it, such as a proposed change. */
|
|
212
|
+
async function postChoice(deps, link, text, choices) {
|
|
213
|
+
await settleChoice(deps, link, 'Replaced by the message below.');
|
|
214
|
+
const choiceTs = await deps.connector.postChoice(link.conversation, link.ts, text, choices);
|
|
215
|
+
recordThread(deps.agentDir, { ...link, choiceTs });
|
|
216
|
+
}
|
|
217
|
+
/** Replaces the thread's message with buttons, if there is one, so its
|
|
218
|
+
* buttons don't linger after the request is decided. */
|
|
219
|
+
async function settleChoice(deps, link, text) {
|
|
220
|
+
if (!link.choiceTs || !deps.connector.replaceMessage)
|
|
221
|
+
return;
|
|
222
|
+
await deps.connector.replaceMessage(link.conversation, link.choiceTs, text).catch((err) => console.error('[loopengine] could not update a message with buttons:', err));
|
|
223
|
+
recordThread(deps.agentDir, { ...link, choiceTs: undefined });
|
|
224
|
+
}
|
|
225
|
+
/** For a decision made outside the thread (the request's own buttons):
|
|
226
|
+
* replaces the thread's message with buttons, if any, with `text`. */
|
|
227
|
+
export async function settleThreadChoice(deps, pendingId, text) {
|
|
228
|
+
const link = findThreadByPending(deps.agentDir, pendingId);
|
|
229
|
+
if (link)
|
|
230
|
+
await settleChoice(deps, link, text);
|
|
231
|
+
}
|
|
207
232
|
async function replaceRequest(connector, link, text) {
|
|
208
233
|
if (!connector.replaceMessage)
|
|
209
234
|
return;
|
|
@@ -222,15 +247,19 @@ export async function handleMessagingAction(action, pendingId, user, deps) {
|
|
|
222
247
|
if (!item)
|
|
223
248
|
return 'This request has already been decided.';
|
|
224
249
|
const who = deps.connector.mention(user);
|
|
250
|
+
// The clicked message is the one with buttons; the caller replaces it
|
|
251
|
+
// with what this returns, so it's no longer tracked.
|
|
225
252
|
if (action === 'keep_original') {
|
|
226
|
-
recordThread(deps.agentDir, { ...link, proposedArgs: undefined });
|
|
253
|
+
recordThread(deps.agentDir, { ...link, proposedArgs: undefined, choiceTs: undefined });
|
|
227
254
|
return `${who} kept the original request. It's still waiting for a decision.`;
|
|
228
255
|
}
|
|
229
|
-
if (action === 'cancel_confirm')
|
|
256
|
+
if (action === 'cancel_confirm') {
|
|
257
|
+
recordThread(deps.agentDir, { ...link, choiceTs: undefined });
|
|
230
258
|
return `${who} cancelled. Nothing ran; the request is still waiting for a decision.`;
|
|
259
|
+
}
|
|
231
260
|
if (action === 'approve_edited' && !link.proposedArgs)
|
|
232
261
|
return 'That change was dropped, so there is nothing to approve as changed.';
|
|
233
|
-
const outcome = await runApproved(deps, link, item.tool, link.proposedArgs ?? item.args, who);
|
|
262
|
+
const outcome = await runApproved(deps, { ...link, choiceTs: undefined }, item.tool, link.proposedArgs ?? item.args, who);
|
|
234
263
|
if (outcome.alreadyResolved)
|
|
235
264
|
return 'This request has already been decided.';
|
|
236
265
|
return action === 'confirm' ? `Confirmed by ${who}.` : `Approved as changed by ${who}.`;
|
|
@@ -10,6 +10,10 @@ export interface ThreadLink {
|
|
|
10
10
|
/** A change an approver asked for, waiting to be approved as changed.
|
|
11
11
|
* Recording the thread again without it drops the change. */
|
|
12
12
|
proposedArgs?: Record<string, unknown>;
|
|
13
|
+
/** The latest message the agent posted with buttons in this thread (a
|
|
14
|
+
* proposed change, a Confirm), so it can be replaced once the request
|
|
15
|
+
* is decided and its buttons don't linger. */
|
|
16
|
+
choiceTs?: string;
|
|
13
17
|
}
|
|
14
18
|
/** Appends one request's thread. Never throws: a request that can't be
|
|
15
19
|
* mapped still works through its buttons and link. */
|
|
@@ -12,8 +12,8 @@ function threadsFile(agentDir) {
|
|
|
12
12
|
export function recordThread(agentDir, link) {
|
|
13
13
|
try {
|
|
14
14
|
mkdirSync(join(agentDir, '.messaging'), { recursive: true });
|
|
15
|
-
const { app, conversation, ts, pendingId, kind, proposedArgs } = link;
|
|
16
|
-
appendFileSync(threadsFile(agentDir), JSON.stringify({ app, conversation, ts, pendingId, kind, ...(proposedArgs ? { proposedArgs } : {}), at: new Date().toISOString() }) + '\n');
|
|
15
|
+
const { app, conversation, ts, pendingId, kind, proposedArgs, choiceTs } = link;
|
|
16
|
+
appendFileSync(threadsFile(agentDir), JSON.stringify({ app, conversation, ts, pendingId, kind, ...(proposedArgs ? { proposedArgs } : {}), ...(choiceTs ? { choiceTs } : {}), at: new Date().toISOString() }) + '\n');
|
|
17
17
|
}
|
|
18
18
|
catch (err) {
|
|
19
19
|
console.error('[loopengine] could not record the thread:', err instanceof Error ? err.message : err);
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { listDeliveries, recordDelivery, type NotifierDelivery } from '#core/not
|
|
|
12
12
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
13
13
|
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, importLegacyScheduledTasks, parseEvery, describeSchedule, type ScheduledTask, type ScheduledTaskRun, type ScheduledTaskStatus, type Schedule, type SchedulerStore, type FireTask, type CreateScheduledTaskInput } from '#core/scheduler.js';
|
|
14
14
|
export { NOTIFIER_SLACK_SIGNING_SECRET_VAR } from '#core/notifier-file.js';
|
|
15
|
-
export { firstSighting, handleInboundMessage, handleMessagingAction, isPlainApproval, type MessagingDeps, type MessagingDecision, type DecisionOutcome } from '#core/messaging/engine.js';
|
|
15
|
+
export { firstSighting, handleInboundMessage, handleMessagingAction, settleThreadChoice, isPlainApproval, type MessagingDeps, type MessagingDecision, type DecisionOutcome } from '#core/messaging/engine.js';
|
|
16
16
|
export { formatArgs, formatChange, labelFor } from '#core/messaging/format-args.js';
|
|
17
17
|
export { decideReply, proposeEdit, describeChange, type ReplyDecision, type DecisionRequest } from '#core/messaging/decide.js';
|
|
18
18
|
export { recordThread, findThread, findThreadByPending, type ThreadLink } from '#core/messaging/threads.js';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { listDeliveries, recordDelivery } from '#core/notifier-deliveries.js';
|
|
|
14
14
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
15
15
|
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, importLegacyScheduledTasks, parseEvery, describeSchedule } from '#core/scheduler.js';
|
|
16
16
|
export { NOTIFIER_SLACK_SIGNING_SECRET_VAR } from '#core/notifier-file.js';
|
|
17
|
-
export { firstSighting, handleInboundMessage, handleMessagingAction, isPlainApproval } from '#core/messaging/engine.js';
|
|
17
|
+
export { firstSighting, handleInboundMessage, handleMessagingAction, settleThreadChoice, isPlainApproval } from '#core/messaging/engine.js';
|
|
18
18
|
export { formatArgs, formatChange, labelFor } from '#core/messaging/format-args.js';
|
|
19
19
|
export { decideReply, proposeEdit, describeChange } from '#core/messaging/decide.js';
|
|
20
20
|
export { recordThread, findThread, findThreadByPending } from '#core/messaging/threads.js';
|
|
@@ -327,15 +327,23 @@ ${consoleShellScript}
|
|
|
327
327
|
box.innerHTML = '<p class="hint">' + escapeHtml(u.unavailableReason) + '</p>' + (u.templateVersion ? upgradeCheckedLine(u) : '');
|
|
328
328
|
} else if (!u.updateAvailable) {
|
|
329
329
|
box.innerHTML = '<p>You' + String.fromCharCode(8217) + 're on the latest version, <code>' + escapeHtml(u.current) + '</code>.</p>' + upgradeCheckedLine(u);
|
|
330
|
-
} else if (u.unavailableReason) {
|
|
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>';
|
|
332
|
-
} else if (!u.canUpgrade) {
|
|
333
|
-
box.innerHTML = '<p>LoopEngine <code>' + escapeHtml(u.latest) + '</code> is available (this server runs <code>' + escapeHtml(u.current) + '</code>).</p><p class="hint">Set LOOPENGINE_ADMIN_AUTH to upgrade from here, or run <code>npx create-loopengine@latest upgrade</code>.</p>';
|
|
334
330
|
} else {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
'<
|
|
331
|
+
// Only the template behind (a fix in adapters/http.ts and the like,
|
|
332
|
+
// with loopengine already current) is offered too.
|
|
333
|
+
var headline = u.templateUpdateAvailable
|
|
334
|
+
? '<p>A template update is available: <code>' + escapeHtml(u.latestTemplate) + '</code> (this project' + String.fromCharCode(8217) + 's template is <code>' + escapeHtml(u.templateVersion) + '</code>). LoopEngine stays at <code>' + escapeHtml(u.current) + '</code>.</p>'
|
|
335
|
+
: '<p>LoopEngine <code>' + escapeHtml(u.latest) + '</code> is available (this server runs <code>' + escapeHtml(u.current) + '</code>).</p>';
|
|
336
|
+
var label = u.templateUpdateAvailable ? 'Update the template to ' + escapeHtml(u.latestTemplate) : 'Upgrade to ' + escapeHtml(u.latest);
|
|
337
|
+
if (u.unavailableReason) {
|
|
338
|
+
box.innerHTML = headline + '<p class="hint">' + escapeHtml(u.unavailableReason) + '</p>';
|
|
339
|
+
} else if (!u.canUpgrade) {
|
|
340
|
+
box.innerHTML = headline + '<p class="hint">Set LOOPENGINE_ADMIN_AUTH to upgrade from here, or run <code>npx create-loopengine@latest upgrade</code>.</p>';
|
|
341
|
+
} else {
|
|
342
|
+
box.innerHTML = headline +
|
|
343
|
+
'<p class="hint">' + UPGRADE_NOTE + (u.restartsItself ? '' : ' This server has nothing to restart it, so restart it yourself afterwards.') + '</p>' +
|
|
344
|
+
'<p><button type="button" class="primary" id="upgradeBtn">' + label + '</button></p>' +
|
|
345
|
+
'<div id="upgradeMsg" class="hint"></div>';
|
|
346
|
+
}
|
|
339
347
|
}
|
|
340
348
|
}
|
|
341
349
|
|
|
@@ -357,14 +365,16 @@ ${consoleShellScript}
|
|
|
357
365
|
}
|
|
358
366
|
|
|
359
367
|
// After a successful upgrade the server restarts: wait for it to answer
|
|
360
|
-
// again on the new
|
|
368
|
+
// again on the new versions (loopengine, and the template's), then
|
|
369
|
+
// reload so the page runs the new code.
|
|
361
370
|
function waitForRestart(target, tries) {
|
|
362
371
|
if (tries > 90) { upgradeMsg('The server hasn' + String.fromCharCode(8217) + 't come back yet. Reload this page in a minute.', true); return; }
|
|
363
372
|
setTimeout(function () {
|
|
364
|
-
fetch('/config/
|
|
373
|
+
fetch('/config/upgrade', { cache: 'no-store' })
|
|
365
374
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
366
|
-
.then(function (
|
|
367
|
-
|
|
375
|
+
.then(function (u) {
|
|
376
|
+
var ready = u && !u.running && (!target.version || u.current === target.version) && (!target.template || u.templateVersion === target.template);
|
|
377
|
+
if (ready && tries > 0) location.reload();
|
|
368
378
|
else waitForRestart(target, tries + 1);
|
|
369
379
|
})
|
|
370
380
|
.catch(function () { waitForRestart(target, tries + 1); });
|
|
@@ -398,14 +408,15 @@ ${consoleShellScript}
|
|
|
398
408
|
var changed = o.files.filter(function (f) { return f.status === 'updated'; }).map(function (f) { return f.file; });
|
|
399
409
|
var kept = o.files.filter(function (f) { return f.status === 'kept'; }).map(function (f) { return f.file; });
|
|
400
410
|
var deps = o.dependencies.map(function (d) { return d.name + ' ' + d.from + ' ' + String.fromCharCode(8594) + ' ' + d.to; });
|
|
401
|
-
var
|
|
411
|
+
var loopengineTo = (o.dependencies.filter(function (d) { return d.name === 'loopengine'; })[0] || {}).to;
|
|
412
|
+
var target = { version: loopengineTo ? loopengineTo.replace(/[^0-9.]/g, '') : null, template: o.toVersion || null };
|
|
402
413
|
var summary = (deps.length ? 'Installed ' + escapeHtml(deps.join(', ')) + '. ' : '') +
|
|
403
414
|
(changed.length ? 'Updated ' + escapeHtml(changed.join(', ')) + '. ' : '') +
|
|
404
415
|
(kept.length ? 'Left as you edited them: ' + escapeHtml(kept.join(', ')) + '. ' : '');
|
|
405
416
|
btn.textContent = 'Upgraded';
|
|
406
|
-
if (r.body.restarting && target) {
|
|
417
|
+
if (r.body.restarting && (target.version || target.template)) {
|
|
407
418
|
upgradeMsg(summary + 'Restarting' + String.fromCharCode(8230), false);
|
|
408
|
-
waitForRestart(target
|
|
419
|
+
waitForRestart(target, 0);
|
|
409
420
|
} else {
|
|
410
421
|
upgradeMsg(summary + 'Restart the server to finish.', false);
|
|
411
422
|
}
|
|
@@ -6,11 +6,17 @@ export interface UpgradeStatus {
|
|
|
6
6
|
latest: string | null;
|
|
7
7
|
/** When npm was last asked for `latest`. */
|
|
8
8
|
checkedAt: string | null;
|
|
9
|
+
/** A newer loopengine, or a newer template, or both. */
|
|
9
10
|
updateAvailable: boolean;
|
|
10
11
|
/** The create-loopengine version this project was scaffolded or last
|
|
11
12
|
* upgraded from (.create-loopengine.json); null when the project
|
|
12
13
|
* wasn't made by create-loopengine, which the button can't upgrade. */
|
|
13
14
|
templateVersion: string | null;
|
|
15
|
+
/** Latest create-loopengine on npm; null when npm couldn't be reached. */
|
|
16
|
+
latestTemplate: string | null;
|
|
17
|
+
/** Only the template is behind: a fix in adapters/http.ts and the
|
|
18
|
+
* other template files, with loopengine already current. */
|
|
19
|
+
templateUpdateAvailable: boolean;
|
|
14
20
|
/** Why the button is off, when it is. */
|
|
15
21
|
unavailableReason?: string;
|
|
16
22
|
running: boolean;
|
|
@@ -43,18 +43,24 @@ export function setUpgradeRunner(next) {
|
|
|
43
43
|
// A minute, so reloading Settings doesn't ask npm every time while a
|
|
44
44
|
// release that just came out shows up soon (the page's Check again skips it).
|
|
45
45
|
let latestCache;
|
|
46
|
-
async function
|
|
47
|
-
if (latestCache && latestCache.until > Date.now())
|
|
48
|
-
return latestCache.value;
|
|
49
|
-
let value = null;
|
|
46
|
+
async function npmLatest(name, projectDir) {
|
|
50
47
|
try {
|
|
51
|
-
|
|
48
|
+
return (await run('npm', ['view', name, 'version'], projectDir, 15_000)).trim() || null;
|
|
52
49
|
}
|
|
53
50
|
catch {
|
|
54
|
-
|
|
51
|
+
return null;
|
|
55
52
|
}
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
}
|
|
54
|
+
/** The latest loopengine and create-loopengine on npm, asked together. */
|
|
55
|
+
async function latestVersions(projectDir) {
|
|
56
|
+
if (latestCache && latestCache.until > Date.now())
|
|
57
|
+
return latestCache;
|
|
58
|
+
const [value, template] = await Promise.all([npmLatest('loopengine', projectDir), npmLatest('create-loopengine', projectDir)]);
|
|
59
|
+
latestCache = { value, template, until: Date.now() + 60_000, at: new Date().toISOString() };
|
|
60
|
+
return latestCache;
|
|
61
|
+
}
|
|
62
|
+
async function latestLoopengine(projectDir) {
|
|
63
|
+
return (await latestVersions(projectDir)).value;
|
|
58
64
|
}
|
|
59
65
|
function templateVersion(projectDir) {
|
|
60
66
|
const path = join(projectDir, '.create-loopengine.json');
|
|
@@ -73,14 +79,18 @@ export async function describeUpgrade(projectDir = process.cwd(), options = {})
|
|
|
73
79
|
if (options.refresh)
|
|
74
80
|
latestCache = undefined;
|
|
75
81
|
const current = loopengineVersion();
|
|
76
|
-
const latest = await
|
|
82
|
+
const { value: latest, template: latestTemplate } = await latestVersions(projectDir);
|
|
77
83
|
const template = templateVersion(projectDir);
|
|
84
|
+
const loopengineBehind = Boolean(latest && semver.valid(current) && semver.gt(latest, current));
|
|
85
|
+
const templateBehind = Boolean(template && latestTemplate && semver.valid(template) && semver.valid(latestTemplate) && semver.gt(latestTemplate, template));
|
|
78
86
|
const status = {
|
|
79
87
|
current,
|
|
80
88
|
latest,
|
|
81
89
|
checkedAt: latestCache?.at ?? null,
|
|
82
|
-
updateAvailable:
|
|
90
|
+
updateAvailable: loopengineBehind || templateBehind,
|
|
83
91
|
templateVersion: template,
|
|
92
|
+
latestTemplate,
|
|
93
|
+
templateUpdateAvailable: templateBehind && !loopengineBehind,
|
|
84
94
|
running,
|
|
85
95
|
};
|
|
86
96
|
if (!template)
|