summrise-agent 1.2.461 → 1.2.463
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/summrise.js +221 -53
- package/package.json +2 -2
- package/summrise-agent.exe +0 -0
- package/summrise-desktop-electron/src/main.js +22 -0
package/bin/summrise.js
CHANGED
|
@@ -70,10 +70,12 @@ exports.sha256File = sha256File;
|
|
|
70
70
|
exports.resolveComponent = resolveComponent;
|
|
71
71
|
exports.ensureElectron = ensureElectron;
|
|
72
72
|
exports.statusReport = statusReport;
|
|
73
|
+
exports.updateWouldNotMove = updateWouldNotMove;
|
|
73
74
|
exports.isBehind = isBehind;
|
|
74
75
|
exports.behindBy = behindBy;
|
|
75
76
|
exports.updateReceiptPs = updateReceiptPs;
|
|
76
77
|
exports.updateBusyPath = updateBusyPath;
|
|
78
|
+
exports.busyMarkerPs = busyMarkerPs;
|
|
77
79
|
exports.awaitReleaseMarker = awaitReleaseMarker;
|
|
78
80
|
exports.releaseMarkerVerdict = releaseMarkerVerdict;
|
|
79
81
|
exports.boxedVersions = boxedVersions;
|
|
@@ -353,8 +355,8 @@ function desktopStartPs(installQ) {
|
|
|
353
355
|
"if (Get-Process electron -ErrorAction SilentlyContinue) { Write-Output 'already-running'; exit 0 }",
|
|
354
356
|
"# DO NOT RE-REGISTER THE TASK TO START IT. Register-ScheduledTask needs the interactive",
|
|
355
357
|
"# user's principal as DOMAIN\\user, and a shell running as a service account has no such",
|
|
356
|
-
|
|
357
|
-
|
|
358
|
+
'# mapping -- the device answered "No mapping between account names and security IDs was',
|
|
359
|
+
'# done ... UserId" from a PTY running as systemprofile, while the task itself was Ready.',
|
|
358
360
|
"# The task already carries the right principal -- `summrise setup` creates it -- so starting",
|
|
359
361
|
"# it is all that is needed.",
|
|
360
362
|
"$t = Get-ScheduledTask -TaskName SummriseDesktop -ErrorAction SilentlyContinue",
|
|
@@ -421,7 +423,12 @@ function parseTargetArg(arg) {
|
|
|
421
423
|
const port = Number(m[2]);
|
|
422
424
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
423
425
|
return null;
|
|
424
|
-
return {
|
|
426
|
+
return {
|
|
427
|
+
host: m[1],
|
|
428
|
+
port,
|
|
429
|
+
path: m[3] || "",
|
|
430
|
+
id: `${m[1]}:${port}${m[3] || ""}`,
|
|
431
|
+
};
|
|
425
432
|
}
|
|
426
433
|
/**
|
|
427
434
|
* JSON that survives the command line.
|
|
@@ -443,16 +450,31 @@ function deviceApi(method, pathname, body) {
|
|
|
443
450
|
const dir = ETC_DIR;
|
|
444
451
|
const token = deviceToken(dir);
|
|
445
452
|
if (!token)
|
|
446
|
-
return {
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
error: "no device token in " + path.join(dir, "config.yaml"),
|
|
456
|
+
};
|
|
447
457
|
const port = agentPort(dir);
|
|
448
|
-
const args = [
|
|
458
|
+
const args = [
|
|
459
|
+
"-sS",
|
|
460
|
+
"-m",
|
|
461
|
+
"15",
|
|
462
|
+
"-X",
|
|
463
|
+
method,
|
|
464
|
+
"-H",
|
|
465
|
+
"Authorization: Bearer " + token,
|
|
466
|
+
];
|
|
449
467
|
if (body !== undefined) {
|
|
450
468
|
args.push("-H", "content-type: application/json", "-d", asciiJson(body));
|
|
451
469
|
}
|
|
452
470
|
args.push(`http://127.0.0.1:${port}${pathname}`);
|
|
453
471
|
const r = (0, child_process_1.spawnSync)("curl", args, { encoding: "utf8", timeout: 20000 });
|
|
454
472
|
if (r.error || r.status !== 0) {
|
|
455
|
-
return {
|
|
473
|
+
return {
|
|
474
|
+
ok: false,
|
|
475
|
+
error: `device unreachable on 127.0.0.1:${port}` +
|
|
476
|
+
(r.error ? ` (${r.error.message})` : ""),
|
|
477
|
+
};
|
|
456
478
|
}
|
|
457
479
|
try {
|
|
458
480
|
return { ok: true, body: JSON.parse(String(r.stdout || "").trim()) };
|
|
@@ -461,21 +483,15 @@ function deviceApi(method, pathname, body) {
|
|
|
461
483
|
return { ok: false, error: "device sent something that is not JSON" };
|
|
462
484
|
}
|
|
463
485
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
// THE RENDER IS PURE (`reportText`) and the fetching is three lines of `deviceApi`, so
|
|
475
|
-
// the format is testable without a device. Two rules it keeps: a server that did not
|
|
476
|
-
// answer is printed as NOT READ, never as an invented value; and a target that is down
|
|
477
|
-
// brings its outage log with it, because "it is down" without "since when, and how
|
|
478
|
-
// often" is the half of the answer that starts an argument.
|
|
486
|
+
/**
|
|
487
|
+
* A duration in the shapes the rest of the panel uses: `45s`, `4m`, `1h 04m`, `2d 4h`.
|
|
488
|
+
*
|
|
489
|
+
* IT USED TO CARRY FOURTEEN LINES ABOUT `summrise report`, a verb pruned in round 25 — including "THE
|
|
490
|
+
* RENDER IS PURE (`reportText`)", naming a function that occurs nowhere else in this file. The doc
|
|
491
|
+
* described a command an operator cannot run, on a function whose actual job went unsaid. Two rules from
|
|
492
|
+
* that block are worth keeping, and this is not the place they belong: a server that did not answer is
|
|
493
|
+
* printed as NOT READ, never as an invented value, and a target that is down brings its outage log.
|
|
494
|
+
*/
|
|
479
495
|
function fmtDuration(ms) {
|
|
480
496
|
const s = Math.max(0, Math.floor(Number(ms || 0) / 1000));
|
|
481
497
|
if (s < 60)
|
|
@@ -483,7 +499,9 @@ function fmtDuration(ms) {
|
|
|
483
499
|
if (s < 3600)
|
|
484
500
|
return `${Math.floor(s / 60)}m`;
|
|
485
501
|
const h = Math.floor(s / 3600);
|
|
486
|
-
return h >= 24
|
|
502
|
+
return h >= 24
|
|
503
|
+
? `${Math.floor(h / 24)}d ${h % 24}h`
|
|
504
|
+
: `${h}h ${String(Math.floor((s % 3600) / 60)).padStart(2, "0")}m`;
|
|
487
505
|
}
|
|
488
506
|
function targetLine(t, nowMs, width = 30) {
|
|
489
507
|
const s = t.summary || {};
|
|
@@ -491,19 +509,27 @@ function targetLine(t, nowMs, width = 30) {
|
|
|
491
509
|
const state = up ? "up" : s.up_now === false ? "down" : "no readings";
|
|
492
510
|
const id = String(t.id || "");
|
|
493
511
|
const since = s.since_ms ? ` ${fmtDuration(nowMs - s.since_ms)}` : "";
|
|
494
|
-
const status = s.last_status === null || s.last_status === undefined
|
|
512
|
+
const status = s.last_status === null || s.last_status === undefined
|
|
513
|
+
? ""
|
|
514
|
+
: ` HTTP ${s.last_status}`;
|
|
495
515
|
// A content check that did not find its text is the case a status code cannot express, so it
|
|
496
516
|
// is printed as its own word next to the code.
|
|
497
|
-
const match = s.last_expect_ok === false
|
|
517
|
+
const match = s.last_expect_ok === false
|
|
518
|
+
? " no match"
|
|
519
|
+
: s.last_expect_ok === true
|
|
520
|
+
? " matches"
|
|
521
|
+
: "";
|
|
498
522
|
const lat = s.latency ? ` ${s.latency.avg}ms avg` : "";
|
|
499
523
|
const pct = s.up_pct === null || s.up_pct === undefined ? "" : ` ${s.up_pct}% up`;
|
|
500
|
-
const drops = s.drops
|
|
524
|
+
const drops = s.drops
|
|
525
|
+
? ` ${s.drops} ${s.drops === 1 ? "drop" : "drops"}`
|
|
526
|
+
: "";
|
|
501
527
|
// The operator's own words belong on the line they explain, not in a separate view.
|
|
502
528
|
const note = t.note && t.note.text ? ` — ${t.note.text}` : "";
|
|
503
529
|
return `${up ? "UP " : s.up_now === false ? "DOWN" : "? "} ${id.padEnd(width)} ${state}${since}${status}${match}${lat}${pct}${drops}${note}`;
|
|
504
530
|
}
|
|
505
531
|
// ── MACHINE-READABLE OUTPUT ────────────────────────────────────────────────
|
|
506
|
-
// `summrise monitor list --json`
|
|
532
|
+
// `summrise monitor list --json` prints the DEVICE'S OWN ANSWER,
|
|
507
533
|
// projected rather than re-derived: the summaries, the transitions and the samples are the ones
|
|
508
534
|
// `/api/monitors` returns, so a script and the panel (and the AI) can never disagree about whether
|
|
509
535
|
// a target is up. The CLI adds only what the device cannot know — which device answered, and when
|
|
@@ -528,13 +554,23 @@ function monitorsJson({ device, askedAtMs, payload, only }) {
|
|
|
528
554
|
// The device's numbers, verbatim. `up: null` means "not read yet" and is NOT false.
|
|
529
555
|
up: t.summary && t.summary.up_now !== undefined ? t.summary.up_now : null,
|
|
530
556
|
up_pct: t.summary && t.summary.up_pct !== undefined ? t.summary.up_pct : null,
|
|
531
|
-
since_ms: t.summary && t.summary.since_ms !== undefined
|
|
557
|
+
since_ms: t.summary && t.summary.since_ms !== undefined
|
|
558
|
+
? t.summary.since_ms
|
|
559
|
+
: null,
|
|
532
560
|
drops: t.summary && t.summary.drops !== undefined ? t.summary.drops : null,
|
|
533
561
|
latency_ms: t.summary && t.summary.latency ? t.summary.latency.avg : null,
|
|
534
|
-
last_status: t.summary && t.summary.last_status !== undefined
|
|
535
|
-
|
|
562
|
+
last_status: t.summary && t.summary.last_status !== undefined
|
|
563
|
+
? t.summary.last_status
|
|
564
|
+
: null,
|
|
565
|
+
last_expect_ok: t.summary && t.summary.last_expect_ok !== undefined
|
|
566
|
+
? t.summary.last_expect_ok
|
|
567
|
+
: null,
|
|
536
568
|
probes: t.summary && t.summary.probes !== undefined ? t.summary.probes : null,
|
|
537
|
-
transitions: (t.transitions || []).map((x) => ({
|
|
569
|
+
transitions: (t.transitions || []).map((x) => ({
|
|
570
|
+
at_ms: x.at_ms,
|
|
571
|
+
up: x.up,
|
|
572
|
+
lasted_ms: x.lasted_ms,
|
|
573
|
+
})),
|
|
538
574
|
}));
|
|
539
575
|
return {
|
|
540
576
|
device: device || "",
|
|
@@ -919,6 +955,13 @@ function sha256File(p) {
|
|
|
919
955
|
*
|
|
920
956
|
* `null` when neither source works: every component here is optional to the
|
|
921
957
|
* AGENT, and a failed fetch must not fail the install.
|
|
958
|
+
*
|
|
959
|
+
* THE FIRST ARM IS A SEAM, NOT A LIVE PATH — measured, because a reader deserves to know which: no
|
|
960
|
+
* component is in `package.json`'s `files[]` or in `required-in-tgz.txt`, and `tar tzf` on the published
|
|
961
|
+
* tarball confirms none of the three is there. So today the release host is the ONLY source, and the
|
|
962
|
+
* `existsSync` above exists so that a future release which DOES box one gets it from the package without
|
|
963
|
+
* touching this function. The test beside it pins the fact, so boxing one is a deliberate act that also
|
|
964
|
+
* updates this sentence.
|
|
922
965
|
*/
|
|
923
966
|
function resolveComponent(name, pkgPath) {
|
|
924
967
|
if (fs.existsSync(pkgPath))
|
|
@@ -1069,9 +1112,22 @@ function statusReport(f) {
|
|
|
1069
1112
|
* sentence's truthiness — where "0 releases" and "-1 releases" are both TRUTHY, so it would have refused every update,
|
|
1070
1113
|
* including the correct one. Caught by asking the artefact before shipping it. */
|
|
1071
1114
|
function versionTriple(v) {
|
|
1072
|
-
const t = String(v || "")
|
|
1115
|
+
const t = String(v || "")
|
|
1116
|
+
.split(".")
|
|
1117
|
+
.map(Number);
|
|
1073
1118
|
return t.length === 3 && t.every((n) => Number.isFinite(n)) ? t : null;
|
|
1074
1119
|
}
|
|
1120
|
+
/** WOULD THIS UPDATE MOVE THE DEVICE AT ALL? The parity fact, and it needs NO NETWORK: the CLI stamps
|
|
1121
|
+
* `<install>/.summrise-release` with ITS OWN version, and the device's `agent_update` reads that file as
|
|
1122
|
+
* the local version — so when the device is already on the version this CLI carries, the stamp equals
|
|
1123
|
+
* what is there, the swap installs the same build, and nothing moves. That is round 201's measured
|
|
1124
|
+
* defect ("update requested 1.2.438 -> 1.2.438", "copy ok=True", release unchanged), which is why the
|
|
1125
|
+
* guard below exists at all. THE GUARD THAT EXISTS CANNOT SEE IT: it compares the CLI against the
|
|
1126
|
+
* RELEASE CHANNEL, and when the CDN is unreadable `latest` is empty and the whole check is skipped, so a
|
|
1127
|
+
* network blip re-opens the defect. This one compares two facts already on the machine. */
|
|
1128
|
+
function updateWouldNotMove(fromVersion, selfVersion) {
|
|
1129
|
+
return Boolean(fromVersion) && fromVersion === selfVersion;
|
|
1130
|
+
}
|
|
1075
1131
|
/** Is `latest` ahead of `device` on the SAME release line? */
|
|
1076
1132
|
function isBehind(device, latest) {
|
|
1077
1133
|
const a = versionTriple(device);
|
|
@@ -1131,6 +1187,21 @@ function updateReceiptPs(dataDirQ, fromVersion, toVersion) {
|
|
|
1131
1187
|
function updateBusyPath() {
|
|
1132
1188
|
return path.join(process.env.ProgramData || "C:\\ProgramData", "SummriseAgent", "update-busy");
|
|
1133
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* The SAME marker as the PowerShell text the swap script embeds, DERIVED from `updateBusyPath()` rather
|
|
1192
|
+
* than written out by hand — which is what this file did, three times, inside the generated script. The
|
|
1193
|
+
* agent fixed exactly this on its own side and says why (tools.rs, above BUSY_MARKER_REL): "It used to be
|
|
1194
|
+
* spelled out TWICE: the Rust acquirer built <ProgramData>\SummriseAgent\update-busy from PathBuf joins
|
|
1195
|
+
* while the generated PowerShell swap script carried the same location as two hand-written string
|
|
1196
|
+
* literals. A drift between the two is invisible until an update actually runs — and then the swap
|
|
1197
|
+
* releases a file the agent never created, the marker survives, and every later update is refused for up
|
|
1198
|
+
* to an hour." The CLI's own doc above claims ONE owner for this path; this is what makes that true.
|
|
1199
|
+
*/
|
|
1200
|
+
function busyMarkerPs() {
|
|
1201
|
+
const root = process.env.ProgramData || "C:\\ProgramData";
|
|
1202
|
+
const rel = path.relative(root, updateBusyPath()).split(path.sep).join("\\");
|
|
1203
|
+
return `(Join-Path $env:ProgramData '${rel}')`;
|
|
1204
|
+
}
|
|
1134
1205
|
/**
|
|
1135
1206
|
* Poll `etc\.summrise-release` until it shows `want`, or the budget expires.
|
|
1136
1207
|
*
|
|
@@ -1167,11 +1238,17 @@ async function awaitReleaseMarker(o) {
|
|
|
1167
1238
|
* misreporting its own version and refusing the update that would fix it.
|
|
1168
1239
|
*/
|
|
1169
1240
|
function releaseMarkerVerdict(c) {
|
|
1241
|
+
// WHOSE FAILURE IS THIS? The message is printed by TWO callers — `rollback`, which staged a release in
|
|
1242
|
+
// order to PIN it, and `update`, which staged one to INSTALL it — and it said "rollback:" with
|
|
1243
|
+
// "re-run `summrise rollback <want>`" for both. On the update path that advice is worse than useless:
|
|
1244
|
+
// it names the version that just failed to install, so following it asks the device to pin a release
|
|
1245
|
+
// it is not running. The verb decides the sentence now.
|
|
1246
|
+
const verb = c.verb ?? "rollback";
|
|
1170
1247
|
if (c.ok) {
|
|
1171
1248
|
return {
|
|
1172
1249
|
writePin: true,
|
|
1173
1250
|
exitCode: 0,
|
|
1174
|
-
message:
|
|
1251
|
+
message: `${verb}: pinned to ${c.want} -- auto-upgrade refused until 'summrise rollback --clear' or a forced agent_update`,
|
|
1175
1252
|
};
|
|
1176
1253
|
}
|
|
1177
1254
|
return {
|
|
@@ -1181,12 +1258,17 @@ function releaseMarkerVerdict(c) {
|
|
|
1181
1258
|
// failed, or it is still running (it kills and restarts the agent), or the marker
|
|
1182
1259
|
// could not be read at all — and `saw === null` is the weakest of the three. The old
|
|
1183
1260
|
// wording asserted "the swap did NOT take" for all of them.
|
|
1184
|
-
message:
|
|
1261
|
+
message: `${verb}: no release marker showing ${c.want} within ${Number.isFinite(c.waitedMs)
|
|
1185
1262
|
? Math.round(c.waitedMs / 1000) + "s"
|
|
1186
1263
|
: "the read-back window"} ` +
|
|
1187
1264
|
`(last read: ${c.saw ?? "empty or unreadable"}). ` +
|
|
1188
1265
|
`NOT pinned (a pin would claim a version this device may not be running) and no release ` +
|
|
1189
|
-
`marker written.
|
|
1266
|
+
`marker written. ` +
|
|
1267
|
+
(verb === "update"
|
|
1268
|
+
? `Check the update log and \`summrise status\`, then re-run \`summrise update\`. If it fails ` +
|
|
1269
|
+
`the same way, \`summrise rollback ${c.from ?? "<the version it was on>"}\` pins the release ` +
|
|
1270
|
+
`this device is ACTUALLY running — which is not the one that failed to install.`
|
|
1271
|
+
: `Check the update log and \`summrise status\`, then re-run 'summrise rollback ${c.want}'.`),
|
|
1190
1272
|
};
|
|
1191
1273
|
}
|
|
1192
1274
|
function boxedVersions(installDir, pkgDir) {
|
|
@@ -1375,7 +1457,12 @@ function initTunnel(hostname, regKey) {
|
|
|
1375
1457
|
const cfg = path.join(ETC_DIR, "tunnel.yml");
|
|
1376
1458
|
if (!fs.existsSync(cf)) {
|
|
1377
1459
|
console.error("tunnel: cloudflared.exe not staged at", cf);
|
|
1378
|
-
|
|
1460
|
+
// NOT "REINSTALL THE PACKAGE". The npm package carries NO boxed components BY DESIGN (that is what
|
|
1461
|
+
// keeps it ~6.7 MB), so a reinstall cannot stage this and the advice sent the operator in a circle.
|
|
1462
|
+
// `setup` is what stages it: resolveComponent fetches it from the RELEASE HOST and the agent's own
|
|
1463
|
+
// sha256 pin checks it again on the path it uses. Its doc records what this costs when nobody does —
|
|
1464
|
+
// "a device with no tunnel is INVISIBLE TO THE CONSOLE while looking perfectly healthy from inside."
|
|
1465
|
+
console.error(" run 'summrise setup' to stage it -- the component comes from the release host, not from the npm package.");
|
|
1379
1466
|
process.exit(1);
|
|
1380
1467
|
}
|
|
1381
1468
|
const host = hostname || "d1.agent.saisi.online";
|
|
@@ -1613,7 +1700,13 @@ const commands = {
|
|
|
1613
1700
|
// `regOk` is collected here and summarised at the end of setup: best-effort, but the
|
|
1614
1701
|
// operator should be told once, with the consequence, rather than not at all.
|
|
1615
1702
|
regOk.push(regWrite("InstallDir", DIR));
|
|
1616
|
-
|
|
1703
|
+
// ...AND ECHO THE RESOLVED ONE, NOT THE DEFAULT. `resolveDataDir()` is registry-first and
|
|
1704
|
+
// `DATA_DIR` is its answer, so a device whose data dir was remapped has that path here. This used to
|
|
1705
|
+
// WRITE THE LITERAL DEFAULT back to the registry — while the tree below was created at `DATA_DIR` —
|
|
1706
|
+
// so the new tree landed at the remapped path and the registry then named the default, which is the
|
|
1707
|
+
// path the AGENT reads. The comment two lines up says "DataDir defaults to %ProgramData%\Summrise",
|
|
1708
|
+
// i.e. exactly what the code did not do: a default applies when nothing is set, not over a remap.
|
|
1709
|
+
regOk.push(regWrite("DataDir", DATA_DIR));
|
|
1617
1710
|
// Pre-create the data dir tree (sessions/memory/logs — C1 separation).
|
|
1618
1711
|
const DATA = DATA_DIR;
|
|
1619
1712
|
for (const sub of ["sessions", "memory", "logs"]) {
|
|
@@ -1760,7 +1853,10 @@ const commands = {
|
|
|
1760
1853
|
// shims rather than hand-written ones; when npm cannot, say the command.
|
|
1761
1854
|
{
|
|
1762
1855
|
const selfVer = String(require("../package.json").version || "");
|
|
1763
|
-
const pfx = (0, child_process_1.spawnSync)("npm", ["prefix", "-g"], {
|
|
1856
|
+
const pfx = (0, child_process_1.spawnSync)("npm", ["prefix", "-g"], {
|
|
1857
|
+
encoding: "utf8",
|
|
1858
|
+
shell: true,
|
|
1859
|
+
});
|
|
1764
1860
|
const pre = pfx.status === 0 ? String(pfx.stdout || "").trim() : "";
|
|
1765
1861
|
if (selfVer && pre) {
|
|
1766
1862
|
const inst = (0, child_process_1.spawnSync)("npm", [
|
|
@@ -1890,10 +1986,10 @@ const commands = {
|
|
|
1890
1986
|
console.log("setup: no tunnel configured (local mode). Enable later with `summrise tunnel install <hostname>`.");
|
|
1891
1987
|
}
|
|
1892
1988
|
},
|
|
1893
|
-
// ── monitor
|
|
1989
|
+
// ── monitor ────────────────────────────────────────────────────────────
|
|
1894
1990
|
// The device's reachability instrument, in the terminal the operator already
|
|
1895
1991
|
// works in. The panel draws the same numbers; this runs where the ssh session
|
|
1896
|
-
// is, needs no browser, and
|
|
1992
|
+
// is, needs no browser, and runs its live view until Ctrl+C.
|
|
1897
1993
|
// async: `wait` blocks on probes (the dispatcher already awaits every command).
|
|
1898
1994
|
async monitor(args) {
|
|
1899
1995
|
const sub = String(args[0] || "list").toLowerCase();
|
|
@@ -1903,7 +1999,7 @@ const commands = {
|
|
|
1903
1999
|
const expect = expectAt >= 0 ? String(args[expectAt + 1] || "") : "";
|
|
1904
2000
|
const positional = args.filter((a, i) => i > 0 && a !== "--expect" && i !== expectAt + 1);
|
|
1905
2001
|
if (expectAt >= 0 && !expect) {
|
|
1906
|
-
console.error(
|
|
2002
|
+
console.error('usage: summrise monitor add <host:port[/path]> --expect "<text>"');
|
|
1907
2003
|
process.exit(1);
|
|
1908
2004
|
}
|
|
1909
2005
|
if (sub === "add" || sub === "rm" || sub === "remove") {
|
|
@@ -1913,7 +2009,12 @@ const commands = {
|
|
|
1913
2009
|
process.exit(1);
|
|
1914
2010
|
}
|
|
1915
2011
|
if (sub === "add") {
|
|
1916
|
-
const r = deviceApi("POST", "/api/monitors/add", {
|
|
2012
|
+
const r = deviceApi("POST", "/api/monitors/add", {
|
|
2013
|
+
host: t.host,
|
|
2014
|
+
port: t.port,
|
|
2015
|
+
path: t.path,
|
|
2016
|
+
expect,
|
|
2017
|
+
});
|
|
1917
2018
|
if (!r.ok) {
|
|
1918
2019
|
console.error(`monitor add: ${r.error}`);
|
|
1919
2020
|
process.exit(1);
|
|
@@ -1934,7 +2035,9 @@ const commands = {
|
|
|
1934
2035
|
console.error(`monitor rm: ${r.error}`);
|
|
1935
2036
|
process.exit(1);
|
|
1936
2037
|
}
|
|
1937
|
-
console.log(r.body && r.body.removed
|
|
2038
|
+
console.log(r.body && r.body.removed
|
|
2039
|
+
? `stopped watching ${t.id}`
|
|
2040
|
+
: `monitor rm: ${t.id} was not being watched`);
|
|
1938
2041
|
return;
|
|
1939
2042
|
}
|
|
1940
2043
|
if (sub === "probe") {
|
|
@@ -1964,7 +2067,10 @@ const commands = {
|
|
|
1964
2067
|
const probe = payload.result && payload.result.probe;
|
|
1965
2068
|
// The device sends the CRITERION with the answer (`expect`), so "no match" can name the
|
|
1966
2069
|
// text it looked for — an unreadable verdict is a verdict nobody can act on.
|
|
1967
|
-
console.log(probeLine({
|
|
2070
|
+
console.log(probeLine({
|
|
2071
|
+
id: t.id,
|
|
2072
|
+
expect: (payload.result && payload.result.expect) || null,
|
|
2073
|
+
}, probe, Date.now()));
|
|
1968
2074
|
return;
|
|
1969
2075
|
}
|
|
1970
2076
|
if (sub !== "list") {
|
|
@@ -1985,7 +2091,12 @@ const commands = {
|
|
|
1985
2091
|
// encoding boundary too. PowerShell decodes a child's output with the console code page
|
|
1986
2092
|
// (CP936 on d1), so raw UTF-8 through a pipe came back as mojibake while the same text
|
|
1987
2093
|
// printed directly read fine — the second face of this bug.
|
|
1988
|
-
console.log(asciiJson(monitorsJson({
|
|
2094
|
+
console.log(asciiJson(monitorsJson({
|
|
2095
|
+
device: os.hostname(),
|
|
2096
|
+
askedAtMs: Date.now(),
|
|
2097
|
+
payload: r.body,
|
|
2098
|
+
only: null,
|
|
2099
|
+
}), 2));
|
|
1989
2100
|
return;
|
|
1990
2101
|
}
|
|
1991
2102
|
const targets = (r.body && r.body.targets) || [];
|
|
@@ -1999,7 +2110,7 @@ const commands = {
|
|
|
1999
2110
|
for (const t of targets)
|
|
2000
2111
|
console.log(targetLine(t, now));
|
|
2001
2112
|
},
|
|
2002
|
-
// Live view: redraw in place every few seconds until Ctrl+C. `summrise
|
|
2113
|
+
// Live view: redraw in place every few seconds until Ctrl+C. `summrise monitor
|
|
2003
2114
|
// <host:port[/path]>` narrows it to one target and adds its outage log.
|
|
2004
2115
|
// `summrise desktop` = put the window back, or say it is already there.
|
|
2005
2116
|
//
|
|
@@ -2109,7 +2220,15 @@ const commands = {
|
|
|
2109
2220
|
}
|
|
2110
2221
|
},
|
|
2111
2222
|
start() {
|
|
2112
|
-
svc
|
|
2223
|
+
// CHECK IT, LIKE `stop` DOES. The comment on `svc` above says the return value exists because
|
|
2224
|
+
// discarding it made `start`/`restart` "silent either way" — and two of the four call sites still
|
|
2225
|
+
// discarded it, including this one, which is step 2 of every documented journey. An operator whose
|
|
2226
|
+
// agent never came up got exit 0 and no sentence at all.
|
|
2227
|
+
const r = svc("Run");
|
|
2228
|
+
if (r && r.status !== 0) {
|
|
2229
|
+
console.error(`summrise start: schtasks /Run failed (status ${r.status}) -- the agent is NOT running`);
|
|
2230
|
+
process.exit(1);
|
|
2231
|
+
}
|
|
2113
2232
|
},
|
|
2114
2233
|
stop() {
|
|
2115
2234
|
// Say WHY before the process goes: see markDeliberateStop.
|
|
@@ -2123,9 +2242,18 @@ const commands = {
|
|
|
2123
2242
|
},
|
|
2124
2243
|
restart() {
|
|
2125
2244
|
markDeliberateStop();
|
|
2126
|
-
|
|
2245
|
+
// THE STOP IS REPORTED BUT NOT FATAL: a task that was not running has nothing to end, so a non-zero
|
|
2246
|
+
// `/End` is the ordinary case for "restart a stopped agent", and the operator asked for the START.
|
|
2247
|
+
const end = svc("End");
|
|
2248
|
+
if (end && end.status !== 0) {
|
|
2249
|
+
console.error(`summrise restart: schtasks /End failed (status ${end.status}) -- the old process may still be running`);
|
|
2250
|
+
}
|
|
2127
2251
|
sh("timeout /t 2 >nul");
|
|
2128
|
-
svc("Run");
|
|
2252
|
+
const run = svc("Run");
|
|
2253
|
+
if (run && run.status !== 0) {
|
|
2254
|
+
console.error(`summrise restart: schtasks /Run failed (status ${run.status}) -- the agent is NOT running`);
|
|
2255
|
+
process.exit(1);
|
|
2256
|
+
}
|
|
2129
2257
|
},
|
|
2130
2258
|
// Boot switch: `summrise autostart on|off|status` (default status). Flips the
|
|
2131
2259
|
// ENABLED flag on BOTH boot tasks (service + desktop shell) — this is the
|
|
@@ -2225,6 +2353,28 @@ const commands = {
|
|
|
2225
2353
|
"\n summrise update");
|
|
2226
2354
|
process.exit(1);
|
|
2227
2355
|
}
|
|
2356
|
+
// AND THE SAME REFUSAL WITHOUT THE NETWORK. The guard above cannot fire when the CDN is
|
|
2357
|
+
// unreadable, and the defect it exists for is exactly what happens then: an update that stamps the
|
|
2358
|
+
// install with a version it already has, swaps in the same build, and reports success while
|
|
2359
|
+
// `status` keeps saying the device is behind.
|
|
2360
|
+
let deviceNow = "";
|
|
2361
|
+
try {
|
|
2362
|
+
deviceNow = fs
|
|
2363
|
+
.readFileSync(path.join(ETC_DIR, ".summrise-release"), "utf8")
|
|
2364
|
+
.trim();
|
|
2365
|
+
}
|
|
2366
|
+
catch {
|
|
2367
|
+
// No marker is not a no-op; the update below writes one.
|
|
2368
|
+
}
|
|
2369
|
+
if (updateWouldNotMove(deviceNow, selfVersion)) {
|
|
2370
|
+
console.error(`update: this CLI is ${selfVersion} and the device is ALREADY on ${deviceNow}.` +
|
|
2371
|
+
"\n Updating from here would stamp the device with the version it already has and change nothing," +
|
|
2372
|
+
"\n because the device reads <install>/.summrise-release (written by this package) as its version." +
|
|
2373
|
+
"\n If the device is meant to be newer, this CLI is too old to deliver it: install the new one first." +
|
|
2374
|
+
"\n npm i -g summrise-agent" +
|
|
2375
|
+
"\n summrise update");
|
|
2376
|
+
process.exit(1);
|
|
2377
|
+
}
|
|
2228
2378
|
// npm audit #10: no mutual exclusion — two updates (or setup racing a
|
|
2229
2379
|
// swap) interleave Copy-Item on *.new, leaving a half-written exe "ok".
|
|
2230
2380
|
// setup REMOVES the marker; update now CREATES it (refuse if <10 min
|
|
@@ -2410,7 +2560,7 @@ const commands = {
|
|
|
2410
2560
|
// BEFORE touching anything (fail-closed — a config-path argument boots
|
|
2411
2561
|
// old AND new agents alike, so aborting here leaves the old version
|
|
2412
2562
|
// running untouched). Without this the moved config strands the boot.
|
|
2413
|
-
`try { ${bootTaskPs(`${q}\\summrise-agent.exe`, `${q}\\etc\\config.yaml`, false).join("; ")} } catch { "[$(Get-Date -Format o)] task repoint FAILED: $($_.Exception.Message)" | ${log}; try { Remove-Item -Force (
|
|
2563
|
+
`try { ${bootTaskPs(`${q}\\summrise-agent.exe`, `${q}\\etc\\config.yaml`, false).join("; ")} } catch { "[$(Get-Date -Format o)] task repoint FAILED: $($_.Exception.Message)" | ${log}; try { Remove-Item -Force (${busyMarkerPs()}) } catch {}; exit 1 }`,
|
|
2414
2564
|
`"[$(Get-Date -Format o)] task repointed at etc\\config.yaml" | ${log}`,
|
|
2415
2565
|
// Layout-v2 migration (ADR 0008): move pre-v2 root paths into their
|
|
2416
2566
|
// v2 homes. Best-effort per item; the gate below is fail-closed.
|
|
@@ -2418,7 +2568,7 @@ const commands = {
|
|
|
2418
2568
|
// Fail-closed gate: the new agent reads ONLY the v2 homes. A missing
|
|
2419
2569
|
// config/hostname here means migration failed — do NOT swap (the old
|
|
2420
2570
|
// exe keeps running the old layout until the next update).
|
|
2421
|
-
`if ((-not (Test-Path '${q}\\etc\\config.yaml')) -or (-not (Test-Path '${q}\\etc\\summrise-agent.hostname'))) { "[$(Get-Date -Format o)] migration gate FAILED (etc\\config.yaml/hostname missing) -- aborting, old version keeps running" | ${log}; try { Remove-Item -Force (
|
|
2571
|
+
`if ((-not (Test-Path '${q}\\etc\\config.yaml')) -or (-not (Test-Path '${q}\\etc\\summrise-agent.hostname'))) { "[$(Get-Date -Format o)] migration gate FAILED (etc\\config.yaml/hostname missing) -- aborting, old version keeps running" | ${log}; try { Remove-Item -Force (${busyMarkerPs()}) } catch {}; exit 1 }`,
|
|
2422
2572
|
// A running exe cannot be overwritten on Windows — stop the service
|
|
2423
2573
|
// first (task end + process kill), THEN swap with retry.
|
|
2424
2574
|
"try { Stop-ScheduledTask SummriseAgent -ErrorAction Stop } catch {}",
|
|
@@ -2442,7 +2592,7 @@ const commands = {
|
|
|
2442
2592
|
// back up (it will run the old exe until the next update).
|
|
2443
2593
|
`try { Start-ScheduledTask SummriseAgent -ErrorAction Stop } catch { schtasks /Run /TN SummriseAgent }`,
|
|
2444
2594
|
`"[$(Get-Date -Format o)] task restarted" | ${log}`,
|
|
2445
|
-
`try { Remove-Item -Force (
|
|
2595
|
+
`try { Remove-Item -Force (${busyMarkerPs()}) } catch {}`,
|
|
2446
2596
|
// Custom-port installs: the firewall rule must track the configured
|
|
2447
2597
|
// bind port (baked at update time from the live config.yaml — the
|
|
2448
2598
|
// swap itself runs from a static file and cannot read it).
|
|
@@ -2605,7 +2755,14 @@ const commands = {
|
|
|
2605
2755
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
2606
2756
|
now: () => Date.now(),
|
|
2607
2757
|
});
|
|
2608
|
-
const verdict = releaseMarkerVerdict({
|
|
2758
|
+
const verdict = releaseMarkerVerdict({
|
|
2759
|
+
...check,
|
|
2760
|
+
want: toVersion,
|
|
2761
|
+
// THE VERB MATTERS: this is an INSTALL that failed, so the advice names the previous version as
|
|
2762
|
+
// the thing to pin — not the one that just failed to install.
|
|
2763
|
+
verb: "update",
|
|
2764
|
+
from: fromVersion,
|
|
2765
|
+
});
|
|
2609
2766
|
if (verdict.writePin) {
|
|
2610
2767
|
console.log(`update: ${fromVersion || "?"} -> ${toVersion} COMPLETE (the device reported the new release)`);
|
|
2611
2768
|
}
|
|
@@ -3023,11 +3180,22 @@ const commands = {
|
|
|
3023
3180
|
// WITHOUT tripping the usage print + process.exit at module load.
|
|
3024
3181
|
if (require.main === module) {
|
|
3025
3182
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
3183
|
+
// `--version` ANSWERS THE CHECK THE INSTALLER'S OWN CHECKLIST MAKES, and it used to fail it. It is not
|
|
3184
|
+
// a verb, so it fell through to the usage branch below, printed the verb list and exited 1 — while
|
|
3185
|
+
// step 4 of `deploy/README-installer.md` BEGINS with "`summrise --version` / the panel opens". A fresh
|
|
3186
|
+
// install that worked therefore reported a failure in the one place the operator is told to look,
|
|
3187
|
+
// which is the same defect as a comment promising more than the code does.
|
|
3188
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
3189
|
+
console.log(String(require("../package.json").version || ""));
|
|
3190
|
+
process.exit(0);
|
|
3191
|
+
}
|
|
3026
3192
|
if (!cmd || !commands[cmd]) {
|
|
3027
3193
|
// The list is DERIVED from `commands`, so the help cannot promise a verb that was pruned
|
|
3028
3194
|
// (`report` and `watch` were still advertised after their round-25 removal — a usage line is a
|
|
3029
3195
|
// contract, and one that lies is worse than none).
|
|
3030
|
-
console.log("summrise <" +
|
|
3196
|
+
console.log("summrise <" +
|
|
3197
|
+
Object.keys(commands).join("|") +
|
|
3198
|
+
"> -- Summrise Agent control");
|
|
3031
3199
|
Object.keys(commands).forEach((k) => console.log(" ", k));
|
|
3032
3200
|
process.exit(cmd ? 1 : 0);
|
|
3033
3201
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summrise-agent",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.463",
|
|
4
4
|
"description": "Summrise Agent — Windows device MCP server (terminal/SSH/serial) + panel. Self-hosted npm distribution.",
|
|
5
5
|
"//build": "唯一构建路径:在本目录执行 npm run build。它是自包含的:经 npm exec 按需拉取 typescript@5,无需预装任何 node_modules(干净检出可跑;旧脚本引用 ../summrise-desktop-electron/node_modules/.bin/tsc,干净检出必 ENOENT)。必须用 npm exec --package … -- tsc 的形式:npx 会吃掉传给 tsc 的 -p/--outDir 等参数(round-435:npx 写法从未真正跑通过)。先编 electron TS(--noCheck 纯 emit,与 release.yml 一致)同步两处 src/*.js,再编 src/summrise.ts 到 bin/summrise.js 并校验 round-298 标记(弱 oracle,只证某次构建发生,新鲜性由 publish-release.sh 的 tsc 重编 + cmp 门保证)。",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "npm exec --yes --package typescript@5 -- tsc -p ../summrise-desktop-electron/tsconfig.json --outDir /tmp/summrise-electron-fresh --noCheck && cp /tmp/summrise-electron-fresh/main.js ../summrise-desktop-electron/src/main.js && cp /tmp/summrise-electron-fresh/preload.js ../summrise-desktop-electron/src/preload.js && cp /tmp/summrise-electron-fresh/url-policy.js ../summrise-desktop-electron/src/url-policy.js && cp ../summrise-desktop-electron/src/main.js ../summrise-desktop-electron/src/preload.js ../summrise-desktop-electron/src/url-policy.js ./summrise-desktop-electron/src/ && npm exec --yes --package typescript@5 -- tsc -p tsconfig.json --outDir /tmp/summrise-fresh-bin && cp /tmp/summrise-fresh-bin/summrise.js bin/summrise.js && grep -q \"summrise-release\" bin/summrise.js && rm -rf /tmp/summrise-electron-fresh /tmp/summrise-fresh-bin",
|
|
27
|
+
"build": "npm exec --yes --package typescript@5.9.3 -- tsc -p ../summrise-desktop-electron/tsconfig.json --outDir /tmp/summrise-electron-fresh --noCheck && cp /tmp/summrise-electron-fresh/main.js ../summrise-desktop-electron/src/main.js && cp /tmp/summrise-electron-fresh/preload.js ../summrise-desktop-electron/src/preload.js && cp /tmp/summrise-electron-fresh/url-policy.js ../summrise-desktop-electron/src/url-policy.js && cp ../summrise-desktop-electron/src/main.js ../summrise-desktop-electron/src/preload.js ../summrise-desktop-electron/src/url-policy.js ./summrise-desktop-electron/src/ && npm exec --yes --package typescript@5.9.3 -- tsc -p tsconfig.json --outDir /tmp/summrise-fresh-bin && cp /tmp/summrise-fresh-bin/summrise.js bin/summrise.js && grep -q \"summrise-release\" bin/summrise.js && rm -rf /tmp/summrise-electron-fresh /tmp/summrise-fresh-bin",
|
|
28
28
|
"test": "node --test"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/summrise-agent.exe
CHANGED
|
Binary file
|
|
@@ -70,6 +70,13 @@ function agentToken() {
|
|
|
70
70
|
let tok = null;
|
|
71
71
|
try {
|
|
72
72
|
const raw = fs.readFileSync(path.join(INSTALL_ROOT, "etc", "config.yaml"), "utf8");
|
|
73
|
+
// THIS PATTERN IS NARROWER THAN THE TWO OTHER PARSERS OF THE SAME LINE: the CLI's
|
|
74
|
+
// (bin/summrise.js, `[A-Za-z0-9._-]+`) and the agent's Rust recovery both accept more than
|
|
75
|
+
// lowercase hex. Measured against a live device on 2026-09-24 — the token is 64 hex characters
|
|
76
|
+
// with no space before the colon, so all three agree TODAY, and hex is what the worker issues.
|
|
77
|
+
// The failure mode if that ever stops being true is SILENT: no credential, a 401, a title that
|
|
78
|
+
// stays "Summrise" and vitals that stay blank — the state the comment above claims to have
|
|
79
|
+
// fixed. Widen this line before debugging that.
|
|
73
80
|
const m = /device_token:\s*"?([0-9a-f]{16,})"?/.exec(raw);
|
|
74
81
|
if (m)
|
|
75
82
|
tok = m[1];
|
|
@@ -705,6 +712,21 @@ async function autoLaunchTaskSet(enabled) {
|
|
|
705
712
|
// interactive user is not resolvable from the service session. A bare
|
|
706
713
|
// STRING invocation happens to work (shell default), but spawn arrays
|
|
707
714
|
// need an explicit account: use Administrator (the d1 console user).
|
|
715
|
+
//
|
|
716
|
+
// THIS PATH CANNOT REPRODUCE WHAT `summrise` REGISTERS, and the difference matters. The CLI
|
|
717
|
+
// gives SummriseDesktop TWO triggers — AtLogOn plus a guarded 5-minute pulse (bin/summrise.js:
|
|
718
|
+
// `New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(3) -RepetitionInterval 5min`, run
|
|
719
|
+
// through desktop-pulse.vbs → ensure-desktop.ps1, which exits when electron is already up).
|
|
720
|
+
// `schtasks /create` has no way to express a repetition interval at all, so what this writes is
|
|
721
|
+
// a LOGON-ONLY task: toggling auto-launch OFF deletes the hardened task, and toggling it ON
|
|
722
|
+
// again replaces it with one that cannot recover a wedged shell. `summrise update` heals it and
|
|
723
|
+
// says so in its log — "desk: SummriseDesktop hardened (guarded 5-min pulse)" — which is how
|
|
724
|
+
// this collision was found in the first place.
|
|
725
|
+
//
|
|
726
|
+
// Doing it properly means New-ScheduledTaskTrigger through PowerShell (the CLI's shape,
|
|
727
|
+
// duplicated in JS) or delegating to the CLI; neither is a one-liner, and the update-time repair
|
|
728
|
+
// bounds the damage to "until the next update". Recorded where the downgrade happens, so the
|
|
729
|
+
// next reader sees the trap instead of rediscovering it from a shell that stopped recovering.
|
|
708
730
|
const r = await runSchtasks([
|
|
709
731
|
"/create", "/tn", AUTOSTART_TASK,
|
|
710
732
|
// schtasks re-parses the /tr VALUE as a command line — it needs its
|