@devmarketplacenpm/devmp 0.1.1-beta.5
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 +208 -0
- package/bin/devmp.js +245 -0
- package/lib/api.js +231 -0
- package/lib/browser.js +21 -0
- package/lib/checkpoints.js +292 -0
- package/lib/command-runner.js +368 -0
- package/lib/commands.js +597 -0
- package/lib/completion.js +190 -0
- package/lib/config.js +172 -0
- package/lib/diff.js +216 -0
- package/lib/executor.js +208 -0
- package/lib/git.js +39 -0
- package/lib/instructions.js +69 -0
- package/lib/interactive-tunnel.js +420 -0
- package/lib/interactive.js +1269 -0
- package/lib/markdown.js +201 -0
- package/lib/mentions.js +68 -0
- package/lib/prompt.js +140 -0
- package/lib/routes.js +27 -0
- package/lib/session.js +107 -0
- package/lib/status.js +249 -0
- package/lib/tty/ansi.js +171 -0
- package/lib/tty/composer.js +680 -0
- package/lib/tty/screen.js +167 -0
- package/lib/ui.js +174 -0
- package/lib/version.js +134 -0
- package/lib/workspace.js +608 -0
- package/lib/ws-run.js +142 -0
- package/package.json +40 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { cursor, rowsFor, totalRows, displayWidth } = require("./ansi");
|
|
4
|
+
|
|
5
|
+
// The screen owns every byte written to the terminal during an interactive
|
|
6
|
+
// session, and splits it into two regions:
|
|
7
|
+
//
|
|
8
|
+
// scrollback — finished output. Painted exactly once, never redrawn, so the
|
|
9
|
+
// user keeps native terminal scroll, copy and search over it.
|
|
10
|
+
// live — the bottom few rows (status line, composer, queued messages).
|
|
11
|
+
// Erased and repainted on every frame.
|
|
12
|
+
//
|
|
13
|
+
// Anything that writes to stdout behind the screen's back will corrupt the
|
|
14
|
+
// row accounting, which is why the shell routes all output through `write`.
|
|
15
|
+
|
|
16
|
+
function createScreen({ stream = process.stdout } = {}) {
|
|
17
|
+
const isTty = Boolean(stream.isTTY);
|
|
18
|
+
|
|
19
|
+
let liveLines = [];
|
|
20
|
+
let liveCaret = null;
|
|
21
|
+
// Rows the live region currently occupies, and where the cursor sits within
|
|
22
|
+
// it (0 = first row of the live region).
|
|
23
|
+
let paintedRows = 0;
|
|
24
|
+
let cursorRow = 0;
|
|
25
|
+
let closed = false;
|
|
26
|
+
|
|
27
|
+
// Row accounting must use the terminal's real width. Clamping it to some
|
|
28
|
+
// comfortable minimum would make us predict a different wrap point than the
|
|
29
|
+
// terminal actually uses, and every erase after that would be off by rows.
|
|
30
|
+
const columns = () => Math.max(1, stream.columns || 80);
|
|
31
|
+
const rows = () => Math.max(2, stream.rows || 24);
|
|
32
|
+
|
|
33
|
+
/** Keep the live region strictly shorter than the viewport so the cursor-up
|
|
34
|
+
* arithmetic can never address a row that has already scrolled away. */
|
|
35
|
+
function clamp(lines) {
|
|
36
|
+
const limit = rows() - 1;
|
|
37
|
+
const width = columns();
|
|
38
|
+
let visible = lines;
|
|
39
|
+
while (visible.length > 1 && totalRows(visible, width) > limit) {
|
|
40
|
+
visible = visible.slice(1);
|
|
41
|
+
}
|
|
42
|
+
return visible;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Wrapped-row offset and column of the caret inside the live region. */
|
|
46
|
+
function caretPosition(lines, caret) {
|
|
47
|
+
if (!caret) return null;
|
|
48
|
+
const width = columns();
|
|
49
|
+
const lineIndex = Math.min(Math.max(caret.line | 0, 0), lines.length - 1);
|
|
50
|
+
let row = 0;
|
|
51
|
+
for (let i = 0; i < lineIndex; i += 1) row += rowsFor(lines[i], width);
|
|
52
|
+
const col = Math.max(0, caret.column | 0);
|
|
53
|
+
return { row: row + Math.floor(col / width), column: col % width };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function erase() {
|
|
57
|
+
if (paintedRows === 0) return;
|
|
58
|
+
stream.write(cursor.toColumn0 + cursor.up(cursorRow) + cursor.eraseDown);
|
|
59
|
+
paintedRows = 0;
|
|
60
|
+
cursorRow = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function paint() {
|
|
64
|
+
if (liveLines.length === 0) {
|
|
65
|
+
paintedRows = 0;
|
|
66
|
+
cursorRow = 0;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const visible = clamp(liveLines);
|
|
70
|
+
const width = columns();
|
|
71
|
+
|
|
72
|
+
// No trailing newline: the last row must not reserve an extra blank line
|
|
73
|
+
// at the bottom of the viewport.
|
|
74
|
+
stream.write(cursor.hide + visible.join("\n"));
|
|
75
|
+
paintedRows = totalRows(visible, width);
|
|
76
|
+
cursorRow = paintedRows - 1;
|
|
77
|
+
|
|
78
|
+
const caret = caretPosition(visible, liveCaret);
|
|
79
|
+
if (caret) {
|
|
80
|
+
const up = Math.max(0, paintedRows - 1 - caret.row);
|
|
81
|
+
stream.write(cursor.up(up) + cursor.toColumn0 + cursor.right(caret.column));
|
|
82
|
+
cursorRow = caret.row;
|
|
83
|
+
stream.write(cursor.show);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const api = {
|
|
88
|
+
get isTty() {
|
|
89
|
+
return isTty;
|
|
90
|
+
},
|
|
91
|
+
get columns() {
|
|
92
|
+
return columns();
|
|
93
|
+
},
|
|
94
|
+
get rows() {
|
|
95
|
+
return rows();
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/** Commit finished output to scrollback, above the live region. */
|
|
99
|
+
write(text) {
|
|
100
|
+
if (closed) return;
|
|
101
|
+
const body = String(text);
|
|
102
|
+
if (!body) return;
|
|
103
|
+
if (!isTty) {
|
|
104
|
+
stream.write(body.endsWith("\n") ? body : `${body}\n`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
erase();
|
|
108
|
+
stream.write(body.endsWith("\n") ? body : `${body}\n`);
|
|
109
|
+
paint();
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/** Commit a list of already-rendered logical lines to scrollback. */
|
|
113
|
+
writeLines(lines) {
|
|
114
|
+
if (!lines || lines.length === 0) return;
|
|
115
|
+
api.write(lines.join("\n"));
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Replace the live region. `caret` is `{ line, column }` in logical-line
|
|
120
|
+
* coordinates with `column` measured in display cells.
|
|
121
|
+
*/
|
|
122
|
+
setLive(lines, caret = null) {
|
|
123
|
+
if (closed || !isTty) return;
|
|
124
|
+
liveLines = Array.isArray(lines) ? lines : [lines];
|
|
125
|
+
liveCaret = caret;
|
|
126
|
+
erase();
|
|
127
|
+
paint();
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
clearLive() {
|
|
131
|
+
if (closed || !isTty) return;
|
|
132
|
+
liveLines = [];
|
|
133
|
+
liveCaret = null;
|
|
134
|
+
erase();
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/** Repaint after a terminal resize; the old geometry is no longer valid. */
|
|
138
|
+
resize() {
|
|
139
|
+
if (closed || !isTty || liveLines.length === 0) return;
|
|
140
|
+
// `paintedRows` was measured at the old width. The terminal has since
|
|
141
|
+
// reflowed the same content into a different number of rows, so erasing
|
|
142
|
+
// with the stale count clears too few of them and leaves a torn copy of
|
|
143
|
+
// the live region above the new one. Re-measure against the new width
|
|
144
|
+
// first, and assume the cursor rides at the bottom of the reflowed
|
|
145
|
+
// block — which is where it sits in every terminal that reflows at all.
|
|
146
|
+
const visible = clamp(liveLines);
|
|
147
|
+
paintedRows = totalRows(visible, columns());
|
|
148
|
+
cursorRow = Math.max(0, paintedRows - 1);
|
|
149
|
+
erase();
|
|
150
|
+
paint();
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
close() {
|
|
154
|
+
if (closed) return;
|
|
155
|
+
closed = true;
|
|
156
|
+
if (!isTty) return;
|
|
157
|
+
liveLines = [];
|
|
158
|
+
liveCaret = null;
|
|
159
|
+
erase();
|
|
160
|
+
stream.write(cursor.show);
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return api;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { createScreen, displayWidth };
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Minimal ANSI styling. Disabled when NO_COLOR is set or output is not a TTY,
|
|
4
|
+
// so piped/CI output stays clean.
|
|
5
|
+
//
|
|
6
|
+
// Two families live here on purpose:
|
|
7
|
+
// `fmt.*` build strings, for anything routed through the interactive
|
|
8
|
+
// screen (which must own every write to keep its row accounting).
|
|
9
|
+
// bare fns print directly, for the one-shot commands that have no screen.
|
|
10
|
+
|
|
11
|
+
const { displayWidth, stripAnsi } = require("./tty/ansi");
|
|
12
|
+
|
|
13
|
+
const enabled = !process.env.NO_COLOR && process.stdout.isTTY;
|
|
14
|
+
|
|
15
|
+
function style(open, close) {
|
|
16
|
+
return (text) =>
|
|
17
|
+
enabled ? `\x1b[${open}m${text}\x1b[${close}m` : String(text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const color = {
|
|
21
|
+
bold: style(1, 22),
|
|
22
|
+
dim: style(2, 22),
|
|
23
|
+
red: style(31, 39),
|
|
24
|
+
green: style(32, 39),
|
|
25
|
+
yellow: style(33, 39),
|
|
26
|
+
blue: style(34, 39),
|
|
27
|
+
cyan: style(36, 39),
|
|
28
|
+
gray: style(90, 39),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
32
|
+
|
|
33
|
+
const fmt = {
|
|
34
|
+
banner: () =>
|
|
35
|
+
color.bold(color.cyan("devmp")) + color.dim(" — server-side coding agent"),
|
|
36
|
+
success: (msg) => `${color.green("✓")} ${msg}`,
|
|
37
|
+
warn: (msg) => `${color.yellow("!")} ${msg}`,
|
|
38
|
+
fail: (msg) => `${color.red("✗")} ${msg}`,
|
|
39
|
+
line: (label, value) => ` ${color.dim(String(label).padEnd(11))} ${value}`,
|
|
40
|
+
// Same pair, without the leading indent — for rows inside a box, which
|
|
41
|
+
// supplies its own padding.
|
|
42
|
+
pair: (label, value) => `${color.dim(String(label).padEnd(10))} ${value}`,
|
|
43
|
+
section: (title) => ["", color.bold(title)],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Render a bordered card. Returns lines rather than printing so the caller can
|
|
48
|
+
* route them through the screen. `width` is measured in display columns, so
|
|
49
|
+
* styled labels inside the card still line up.
|
|
50
|
+
*/
|
|
51
|
+
/** Cut a row down to `width` columns. */
|
|
52
|
+
function fitToWidth(value, width) {
|
|
53
|
+
const text = String(value);
|
|
54
|
+
if (displayWidth(text) <= width) return text;
|
|
55
|
+
// A hard cut, not a word wrap: these rows are label/value pairs, and wrapping
|
|
56
|
+
// drops the value entirely when it is a single long token such as a path.
|
|
57
|
+
// Styling goes with the cut, because slicing through an escape sequence would
|
|
58
|
+
// spill raw codes onto the screen — and a trimmed row is only ever the
|
|
59
|
+
// fallback for a terminal too narrow to show it in full.
|
|
60
|
+
const plain = stripAnsi(text);
|
|
61
|
+
const limit = Math.max(1, width - 1); // leave a column for the ellipsis
|
|
62
|
+
let out = "";
|
|
63
|
+
let used = 0;
|
|
64
|
+
for (const char of plain) {
|
|
65
|
+
const charWidth = displayWidth(char);
|
|
66
|
+
if (used + charWidth > limit) break;
|
|
67
|
+
out += char;
|
|
68
|
+
used += charWidth;
|
|
69
|
+
}
|
|
70
|
+
return `${out}…`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function box(lines, { title, minWidth = 24, maxWidth = 76 } = {}) {
|
|
74
|
+
// Follow the terminal when it is narrower than the default. Rows were never
|
|
75
|
+
// trimmed, so a long workspace path pushed its own border off the right edge
|
|
76
|
+
// and the card read as broken — which is exactly where users look when
|
|
77
|
+
// something has already gone wrong.
|
|
78
|
+
const available = process.stdout.columns
|
|
79
|
+
? Math.max(minWidth, process.stdout.columns - 4)
|
|
80
|
+
: maxWidth;
|
|
81
|
+
const cap = Math.min(maxWidth, available);
|
|
82
|
+
|
|
83
|
+
const fitted = lines.map((item) => fitToWidth(item, cap));
|
|
84
|
+
const widest = Math.max(
|
|
85
|
+
minWidth,
|
|
86
|
+
title ? displayWidth(title) + 4 : 0,
|
|
87
|
+
...fitted.map((item) => displayWidth(item))
|
|
88
|
+
);
|
|
89
|
+
const width = Math.min(cap, widest);
|
|
90
|
+
lines = fitted;
|
|
91
|
+
// Body rows are `│ ` + content + padding + ` │`, i.e. width + 4 columns. The
|
|
92
|
+
// titled top edge is `╭─ ` + title + ` ` + dashes + `╮`, so it needs
|
|
93
|
+
// width - title - 1 dashes to land on the same column as everything else.
|
|
94
|
+
const top = title
|
|
95
|
+
? `╭─ ${title} ${"─".repeat(Math.max(1, width - displayWidth(title) - 1))}╮`
|
|
96
|
+
: `╭${"─".repeat(width + 2)}╮`;
|
|
97
|
+
const out = [color.dim(top)];
|
|
98
|
+
for (const item of lines) {
|
|
99
|
+
const pad = Math.max(0, width - displayWidth(item));
|
|
100
|
+
out.push(`${color.dim("│")} ${item}${" ".repeat(pad)} ${color.dim("│")}`);
|
|
101
|
+
}
|
|
102
|
+
out.push(color.dim(`╰${"─".repeat(width + 2)}╯`));
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatNumber(value) {
|
|
107
|
+
const n = Number(value);
|
|
108
|
+
if (!Number.isFinite(n)) return "—";
|
|
109
|
+
return Math.max(0, Math.round(n)).toLocaleString("en-US");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatDate(value) {
|
|
113
|
+
if (!value) return "unknown";
|
|
114
|
+
const date = new Date(value);
|
|
115
|
+
if (Number.isNaN(date.getTime())) return "unknown";
|
|
116
|
+
return date.toLocaleDateString("en-US", {
|
|
117
|
+
month: "long",
|
|
118
|
+
day: "numeric",
|
|
119
|
+
year: "numeric",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatDuration(ms) {
|
|
124
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
125
|
+
if (seconds < 60) return `${seconds}s`;
|
|
126
|
+
const minutes = Math.floor(seconds / 60);
|
|
127
|
+
return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function banner() {
|
|
131
|
+
console.log(fmt.banner());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function section(title) {
|
|
135
|
+
console.log("");
|
|
136
|
+
console.log(color.bold(title));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function info(msg) {
|
|
140
|
+
console.log(msg);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function success(msg) {
|
|
144
|
+
console.log(fmt.success(msg));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function warn(msg) {
|
|
148
|
+
console.log(fmt.warn(msg));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function fail(msg) {
|
|
152
|
+
console.error(fmt.fail(msg));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function line(label, value) {
|
|
156
|
+
console.log(fmt.line(label, value));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
color,
|
|
161
|
+
fmt,
|
|
162
|
+
box,
|
|
163
|
+
SPINNER_FRAMES,
|
|
164
|
+
formatNumber,
|
|
165
|
+
formatDate,
|
|
166
|
+
formatDuration,
|
|
167
|
+
banner,
|
|
168
|
+
section,
|
|
169
|
+
info,
|
|
170
|
+
success,
|
|
171
|
+
warn,
|
|
172
|
+
fail,
|
|
173
|
+
line,
|
|
174
|
+
};
|
package/lib/version.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Client version negotiation.
|
|
4
|
+
//
|
|
5
|
+
// The agent lives on the server, so most releases reach users the moment the
|
|
6
|
+
// server deploys — nothing to install. Only this client (terminal UI, file
|
|
7
|
+
// writing, tunnel protocol) ships through npm, and CLI users famously never
|
|
8
|
+
// run `npm update -g`. So the server tells us, on every authenticated call and
|
|
9
|
+
// on every session handshake, which client it expects:
|
|
10
|
+
//
|
|
11
|
+
// latest — newest published client; we nudge, nothing more
|
|
12
|
+
// min — oldest client this server still speaks to; below it we refuse
|
|
13
|
+
//
|
|
14
|
+
// The refusal is the important half: when the tunnel protocol changes, an old
|
|
15
|
+
// client would otherwise fail in some unreadable way mid-turn. Better to say
|
|
16
|
+
// so up front, in one line the user can act on.
|
|
17
|
+
//
|
|
18
|
+
// Both fields are optional. A server that sends neither (an older deploy) is
|
|
19
|
+
// handled exactly as before — no notice, no gate.
|
|
20
|
+
|
|
21
|
+
const { version: CURRENT, name: PACKAGE } = require("../package.json");
|
|
22
|
+
|
|
23
|
+
/** Split "1.2.3-beta.4" into comparable parts. Returns null if unparseable. */
|
|
24
|
+
function parse(value) {
|
|
25
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(
|
|
26
|
+
String(value || "").trim(),
|
|
27
|
+
);
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
return {
|
|
30
|
+
main: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
31
|
+
pre: match[4] ? match[4].split(".") : [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Compare two prerelease identifiers the way semver precedence requires. */
|
|
36
|
+
function comparePreParts(a, b) {
|
|
37
|
+
const aNum = /^\d+$/.test(a);
|
|
38
|
+
const bNum = /^\d+$/.test(b);
|
|
39
|
+
if (aNum && bNum) return Math.sign(Number(a) - Number(b));
|
|
40
|
+
if (aNum) return -1; // numeric identifiers rank below alphanumeric
|
|
41
|
+
if (bNum) return 1;
|
|
42
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Semver precedence: -1 if a < b, 1 if a > b, 0 if equal.
|
|
47
|
+
* Returns 0 for anything unparseable so a malformed server value can never
|
|
48
|
+
* lock a working client out.
|
|
49
|
+
*/
|
|
50
|
+
function compare(a, b) {
|
|
51
|
+
const left = parse(a);
|
|
52
|
+
const right = parse(b);
|
|
53
|
+
if (!left || !right) return 0;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < 3; i += 1) {
|
|
56
|
+
if (left.main[i] !== right.main[i]) {
|
|
57
|
+
return left.main[i] < right.main[i] ? -1 : 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 1.0.0-beta precedes 1.0.0; a release outranks any prerelease of itself.
|
|
62
|
+
if (left.pre.length && !right.pre.length) return -1;
|
|
63
|
+
if (!left.pre.length && right.pre.length) return 1;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
|
|
66
|
+
const l = left.pre[i];
|
|
67
|
+
const r = right.pre[i];
|
|
68
|
+
if (l === undefined) return -1; // fewer identifiers = lower precedence
|
|
69
|
+
if (r === undefined) return 1;
|
|
70
|
+
const cmp = comparePreParts(l, r);
|
|
71
|
+
if (cmp !== 0) return cmp;
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// What the server last told us. Any transport may fill this in — the HTTP
|
|
77
|
+
// usage call, the WebSocket handshake — and every surface reads the same copy.
|
|
78
|
+
let reported = null;
|
|
79
|
+
|
|
80
|
+
/** Record a `{ latest, min }` block from a server payload. Ignores junk. */
|
|
81
|
+
function record(info) {
|
|
82
|
+
if (!info || typeof info !== "object") return;
|
|
83
|
+
const latest = typeof info.latest === "string" ? info.latest : null;
|
|
84
|
+
const min = typeof info.min === "string" ? info.min : null;
|
|
85
|
+
if (!latest && !min) return;
|
|
86
|
+
reported = { latest, min };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Test seam: drop anything a previous call recorded. */
|
|
90
|
+
function reset() {
|
|
91
|
+
reported = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function state() {
|
|
95
|
+
const latest = reported?.latest ?? null;
|
|
96
|
+
const min = reported?.min ?? null;
|
|
97
|
+
return {
|
|
98
|
+
current: CURRENT,
|
|
99
|
+
latest,
|
|
100
|
+
min,
|
|
101
|
+
updateAvailable: Boolean(latest) && compare(CURRENT, latest) < 0,
|
|
102
|
+
blocked: Boolean(min) && compare(CURRENT, min) < 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One line for the end of a session. Null when there is nothing to say. */
|
|
107
|
+
function updateNotice() {
|
|
108
|
+
const { current, latest, updateAvailable, blocked } = state();
|
|
109
|
+
if (blocked || !updateAvailable) return null;
|
|
110
|
+
return `Update available: ${current} → ${latest} npm i -g ${PACKAGE}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The hard gate. Returns a ready-to-print message when this client is too old
|
|
115
|
+
* for the server, otherwise null.
|
|
116
|
+
*/
|
|
117
|
+
function blockedMessage() {
|
|
118
|
+
const { current, min, blocked } = state();
|
|
119
|
+
if (!blocked) return null;
|
|
120
|
+
return (
|
|
121
|
+
`This devmp is too old for the server (you have ${current}, ` +
|
|
122
|
+
`it needs ${min} or newer).\n Update with: npm i -g ${PACKAGE}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
CURRENT,
|
|
128
|
+
compare,
|
|
129
|
+
record,
|
|
130
|
+
reset,
|
|
131
|
+
state,
|
|
132
|
+
updateNotice,
|
|
133
|
+
blockedMessage,
|
|
134
|
+
};
|