@danypops/papyrus 0.52.1 → 0.53.1
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/package.json +2 -2
- package/src/constants.ts +10 -1
- package/src/daemon/daemon-state.ts +33 -1
- package/src/daemon/daemon.ts +13 -0
- package/src/ops.ts +15 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.1",
|
|
4
4
|
"description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@danypops/vehicle-client": "^0.6.1",
|
|
37
37
|
"@danypops/vehicle-core": "^0.12.3",
|
|
38
|
-
"@danypops/vehicle-server": "^0.
|
|
38
|
+
"@danypops/vehicle-server": "^0.22.0",
|
|
39
39
|
"@stricli/core": "^1.3.0"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/constants.ts
CHANGED
|
@@ -28,7 +28,16 @@ export const GATE_TEST_TIMEOUT_MS = 60_000;
|
|
|
28
28
|
* this ceiling has had a chance to.
|
|
29
29
|
*/
|
|
30
30
|
export const GATE_TIMEOUT_MAX_MS = 300_000;
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* How much of a command/test gate's own combined stdout+stderr is kept in the caller-facing
|
|
33
|
+
* `output` field -- taken from the END of the buffer (see ops.ts's callers), not the start.
|
|
34
|
+
* Real, confirmed bug this exists for (papyrus task d0eb81b7): at the previous value (200,
|
|
35
|
+
* head-sliced), a failing gate's `output` carried only the first line or two of banner/setup
|
|
36
|
+
* noise -- exactly the LEAST useful part of a real test run, whose actual pass/fail summary is
|
|
37
|
+
* always its last lines. GATE_OPERATION_LIMITS' own maxResponseBytes (262_144, handlers/tasks.ts)
|
|
38
|
+
* comfortably holds many gates at this size.
|
|
39
|
+
*/
|
|
40
|
+
export const GATE_OUTPUT_LIMIT = 8_000;
|
|
32
41
|
export const GATE_MAX_BUFFER_BYTES = 1_048_576;
|
|
33
42
|
export const GATE_FILE_MAX_BYTES = 1_048_576;
|
|
34
43
|
|
|
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { LOOPBACK_HOST, removeDaemonHandle, writeDaemonHandle } from "@danypops/vehicle-server/paths";
|
|
5
|
+
import { LOOPBACK_HOST, removeDaemonHandle, resolveSharedVehicleHandlePath, writeDaemonHandle } from "@danypops/vehicle-server/paths";
|
|
6
6
|
import {
|
|
7
7
|
DAEMON_DIR_ENV,
|
|
8
8
|
DAEMON_HANDLE_FILE,
|
|
@@ -69,6 +69,38 @@ export function clearVehicleHandle(dir: string): void {
|
|
|
69
69
|
removeDaemonHandle(vehicleHandlePath(dir));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** Where the private auth token lives -- passed as SharedVehicleHandleEntry.tokenPath so a broker-mode discoverer with read access to it can authenticate against Papyrus. */
|
|
73
|
+
export function tokenPath(dir: string): string {
|
|
74
|
+
return join(dir, DAEMON_TOKEN_FILE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Papyrus's own stable identity name in the shared Vehicle Handle Directory (see @danypops/vehicle-server's resolveSharedVehicleHandlePath) -- must match the ownVehicleName Vehicle Shell broker mode is registered under. */
|
|
78
|
+
export const PAPYRUS_VEHICLE_NAME = "papyrus";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Writes Papyrus's entry into the shared, cross-package Vehicle Handle Directory (independent of
|
|
82
|
+
* writeVehicleHandle's own private per-package handle file above) -- the seam a broker-mode
|
|
83
|
+
* tools_list/tools_man discovery scan reads without needing to already know Papyrus's own
|
|
84
|
+
* daemonStateDir convention in advance. env is injectable for tests; defaults to process.env.
|
|
85
|
+
*/
|
|
86
|
+
export function writeSharedVehicleHandle(
|
|
87
|
+
port: number,
|
|
88
|
+
tokenFilePath: string,
|
|
89
|
+
pid: number = process.pid,
|
|
90
|
+
env: Record<string, string | undefined> = process.env,
|
|
91
|
+
): void {
|
|
92
|
+
writeDaemonHandle(resolveSharedVehicleHandlePath(PAPYRUS_VEHICLE_NAME, { env }), {
|
|
93
|
+
host: LOOPBACK_HOST,
|
|
94
|
+
port,
|
|
95
|
+
pid,
|
|
96
|
+
tokenPath: tokenFilePath,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function clearSharedVehicleHandle(env: Record<string, string | undefined> = process.env): void {
|
|
101
|
+
removeDaemonHandle(resolveSharedVehicleHandlePath(PAPYRUS_VEHICLE_NAME, { env }));
|
|
102
|
+
}
|
|
103
|
+
|
|
72
104
|
export function readDaemonHandle(dir: string): DaemonHandle | undefined {
|
|
73
105
|
try {
|
|
74
106
|
const token = readFileSync(join(dir, DAEMON_TOKEN_FILE), "utf8").trim();
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -10,11 +10,14 @@ import { logEvent, vehicleLogger } from "../log/log.ts";
|
|
|
10
10
|
import { createApp, createPapyrusService } from "../service.ts";
|
|
11
11
|
import {
|
|
12
12
|
clearDaemonPort,
|
|
13
|
+
clearSharedVehicleHandle,
|
|
13
14
|
clearVehicleHandle,
|
|
14
15
|
daemonStateDir,
|
|
15
16
|
lifecyclePath,
|
|
16
17
|
loadOrCreateToken,
|
|
18
|
+
tokenPath,
|
|
17
19
|
writeDaemonPort,
|
|
20
|
+
writeSharedVehicleHandle,
|
|
18
21
|
writeVehicleHandle,
|
|
19
22
|
} from "./daemon-state.ts";
|
|
20
23
|
|
|
@@ -92,6 +95,11 @@ export async function serveMain(): Promise<void> {
|
|
|
92
95
|
}
|
|
93
96
|
writeDaemonPort(stateDir, server.port);
|
|
94
97
|
writeVehicleHandle(stateDir, server.port);
|
|
98
|
+
try {
|
|
99
|
+
writeSharedVehicleHandle(server.port, tokenPath(stateDir));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logEvent("error", "shared_vehicle_handle_write_failed", { message: error instanceof Error ? error.message : String(error) });
|
|
102
|
+
}
|
|
95
103
|
const checkpointTimer = setInterval(() => {
|
|
96
104
|
try {
|
|
97
105
|
service.checkpoint();
|
|
@@ -137,6 +145,11 @@ export async function serveMain(): Promise<void> {
|
|
|
137
145
|
clearInterval(purgeTrashTimer);
|
|
138
146
|
clearDaemonPort(stateDir);
|
|
139
147
|
clearVehicleHandle(stateDir);
|
|
148
|
+
try {
|
|
149
|
+
clearSharedVehicleHandle();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logEvent("error", "shared_vehicle_handle_remove_failed", { message: error instanceof Error ? error.message : String(error) });
|
|
152
|
+
}
|
|
140
153
|
releaseDaemonLock(lockPath);
|
|
141
154
|
service.close();
|
|
142
155
|
// .finally() re-throws rather than handling a rejection -- catching it first turns a bare
|
package/src/ops.ts
CHANGED
|
@@ -628,17 +628,26 @@ function processGateCommand(gate: Gate): { command: string; timeout: number } {
|
|
|
628
628
|
* previously "test" was a second, separately-maintained execSync path that never checked
|
|
629
629
|
* gate.expect at all.
|
|
630
630
|
*/
|
|
631
|
+
/**
|
|
632
|
+
* Keeps the LAST GATE_OUTPUT_LIMIT characters, not the first -- a real command's own meaningful
|
|
633
|
+
* pass/fail summary is its last lines, not its first (setup/banner noise). See GATE_OUTPUT_LIMIT's
|
|
634
|
+
* own doc comment (constants.ts) for the real incident this fixes.
|
|
635
|
+
*/
|
|
636
|
+
function gateOutputTail(text: string): string {
|
|
637
|
+
return text.length > GATE_OUTPUT_LIMIT ? text.slice(-GATE_OUTPUT_LIMIT) : text;
|
|
638
|
+
}
|
|
639
|
+
|
|
631
640
|
function runProcessGateSync(gate: Gate, cwd?: string): GateResult {
|
|
632
641
|
const { spawnSync } = require_("node:child_process");
|
|
633
642
|
const { command, timeout } = processGateCommand(gate);
|
|
634
643
|
const result = spawnSync(command, { shell: true, encoding: "utf-8", timeout, ...(cwd ? { cwd } : {}) });
|
|
635
|
-
if (result.error) return { gate, passed: false, output: result.error.message
|
|
644
|
+
if (result.error) return { gate, passed: false, output: gateOutputTail(result.error.message) };
|
|
636
645
|
const combined = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
637
646
|
const passed = result.status === 0 && (gate.expect ? combined.includes(gate.expect) : true);
|
|
638
647
|
return {
|
|
639
648
|
gate,
|
|
640
649
|
passed,
|
|
641
|
-
output: combined
|
|
650
|
+
output: gateOutputTail(combined) || (result.status === 0 ? "ok" : `command exited with code ${result.status}`),
|
|
642
651
|
};
|
|
643
652
|
}
|
|
644
653
|
|
|
@@ -695,13 +704,13 @@ function executeGateCommand(
|
|
|
695
704
|
resolve(result);
|
|
696
705
|
};
|
|
697
706
|
|
|
698
|
-
child.on("error", (error) => finish({ passed: false, output: error.message
|
|
707
|
+
child.on("error", (error) => finish({ passed: false, output: gateOutputTail(error.message), matchable: error.message }));
|
|
699
708
|
child.on("close", (code) => {
|
|
700
709
|
// `matchable` carries the full (GATE_MAX_BUFFER_BYTES-bounded) buffer so the caller's
|
|
701
|
-
// gate.expect substring check sees the whole run,
|
|
702
|
-
//
|
|
710
|
+
// gate.expect substring check sees the whole run, regardless of GATE_OUTPUT_LIMIT --
|
|
711
|
+
// `output` (below) is only ever the display copy.
|
|
703
712
|
const full = buffered.trim();
|
|
704
|
-
const output = full
|
|
713
|
+
const output = gateOutputTail(full);
|
|
705
714
|
finish({ passed: code === 0, output: output || (code === 0 ? "ok" : `command exited with code ${code}`), matchable: full });
|
|
706
715
|
});
|
|
707
716
|
|