Merge pull request #529 from pikasTech/feat/web-probe-observe-id
feat: add short ids for web observer commands
This commit is contained in:
@@ -68,10 +68,10 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --url https://hwlab.pikapython.com --fresh-session --message 'ping'",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 --script-file .state/probes/workbench.mjs",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command --node D601 --lane v03 --state-dir .state/web-observe/D601/v03/YYYY/MM/DD/<run> --type selectProvider --provider codex-api",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command --node D601 --lane v03 --state-dir .state/web-observe/D601/v03/YYYY/MM/DD/<run> --type sendPrompt --text 'ping'",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe status --node D601 --lane v03 --state-dir .state/web-observe/D601/v03/YYYY/MM/DD/<run>",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe analyze --node D601 --lane v03 --state-dir .state/web-observe/D601/v03/YYYY/MM/DD/<run>",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectProvider --provider codex-api",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe status webobs-xxxx",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 <<'JS'\nexport default async ({ waitWorkbenchReady, fetchJson, fetchApiMatrix, recordStep, collectText, safeEvaluate, screenshot }) => {\n const ready = await waitWorkbenchReady();\n const workspace = await fetchJson('/v1/workbench/workspace?projectId=prj_hwpod_workbench');\n const apiMatrix = await fetchApiMatrix(['/v1/workbench/workspace?projectId=prj_hwpod_workbench', '/auth/session']);\n const workspaceText = await collectText('#workspace');\n const evaluated = await safeEvaluate(({ a, b }) => ({ sum: a + b }), { a: 1, b: 2 });\n await screenshot('workbench.png');\n recordStep('workbench-summary', { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, apiMatrixOk: apiMatrix.ok });\n return { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, workspaceText, evaluated };\n};\nJS",
|
||||
],
|
||||
actions: {
|
||||
@@ -83,6 +83,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
"Prefer --script-file for reusable probes; stdin heredocs remain supported for one-off probes.",
|
||||
"Issue-ready evidence is available under issueEvidence and summary.issueEvidence; full script report is persisted under probe.reportPath with a SHA-256 fingerprint.",
|
||||
"observe sampling is passive by default: it records DOM summaries and natural page request/response/requestfailed events with observerInitiated=false; it does not actively fetch Workbench APIs, reload, switch sessions, route/intercept, or call repair helpers.",
|
||||
"observe start registers a local UniDesk-side observer id under .state/web-observe/index.json; after start, prefer observe status|command|stop|collect|analyze <id> instead of repeating --node/--lane/--state-dir.",
|
||||
"observe command actions are explicit user/control actions and are appended to control.jsonl; use --type selectProvider/sendPrompt/goto/screenshot/mark/stop and keep prompt text out of issue comments by citing textHash/textBytes.",
|
||||
"observe analyze is offline-only: it reads artifact JSONL and writes analysis/report.md plus analysis/report.json without accessing Workbench APIs or driving the browser.",
|
||||
"Use recordStep(name, data) or fetchApiMatrix(paths) to keep structured partial evidence when a later step fails.",
|
||||
|
||||
+212
-16
@@ -1,8 +1,8 @@
|
||||
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
||||
// Responsibility: YAML-first node/lane operations, including Workbench observability control commands.
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { repoRoot, rootPath, type Config } from "./config";
|
||||
import { runCommand, type CommandResult } from "./command";
|
||||
import { startJob } from "./jobs";
|
||||
@@ -63,6 +63,7 @@ type NodeWebProbeObserveCommandType = "login" | "preflight" | "goto" | "sendProm
|
||||
interface NodeWebProbeObserveOptions {
|
||||
action: "observe";
|
||||
observeAction: NodeWebProbeObserveAction;
|
||||
id: string | null;
|
||||
node: string;
|
||||
lane: string;
|
||||
url: string;
|
||||
@@ -88,6 +89,20 @@ interface NodeWebProbeObserveOptions {
|
||||
|
||||
type NodeWebProbeOptions = NodeWebProbeRunOptions | NodeWebProbeScriptOptions | NodeWebProbeObserveOptions;
|
||||
|
||||
interface WebObserveIndexEntry {
|
||||
id: string;
|
||||
node: string;
|
||||
lane: string;
|
||||
workspace: string;
|
||||
stateDir: string;
|
||||
url: string;
|
||||
targetPath: string;
|
||||
status: string | null;
|
||||
pid: number | null;
|
||||
startedAt: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
type NodeObservabilityAction = "plan" | "apply" | "status" | "workbench-summary" | "performance-summary";
|
||||
|
||||
interface NodeObservabilityOptions {
|
||||
@@ -5927,13 +5942,26 @@ function rewriteDelegatedNodeString(value: string, scoped: ReturnType<typeof par
|
||||
function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
const [actionRaw] = args;
|
||||
if (actionRaw !== "run" && actionRaw !== "script" && actionRaw !== "observe") throw new Error("web-probe usage: run|script|observe --node NODE --lane vNN [--url URL]");
|
||||
if (actionRaw === "observe") {
|
||||
const normalized = normalizeNodeWebProbeObserveArgs(args.slice(1));
|
||||
const indexed = normalized.id === null ? null : readWebObserveIndexEntry(normalized.id);
|
||||
const node = optionValue(args, "--node") ?? indexed?.node;
|
||||
const lane = optionValue(args, "--lane") ?? indexed?.lane;
|
||||
if (node === undefined || lane === undefined) {
|
||||
throw new Error("web-probe observe requires --node/--lane for start, or a known observer id from observe start for status|command|stop|collect|analyze");
|
||||
}
|
||||
assertNodeId(node);
|
||||
assertLane(lane);
|
||||
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
|
||||
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
|
||||
return parseNodeWebProbeObserveOptions(normalized.args, node, lane, spec, normalized.id, indexed);
|
||||
}
|
||||
const node = requiredOption(args, "--node");
|
||||
assertNodeId(node);
|
||||
const lane = requiredOption(args, "--lane");
|
||||
assertLane(lane);
|
||||
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
|
||||
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
|
||||
if (actionRaw === "observe") return parseNodeWebProbeObserveOptions(args.slice(1), node, lane, spec);
|
||||
if (actionRaw === "script") {
|
||||
assertKnownOptions(args.slice(1), new Set([
|
||||
"--node",
|
||||
@@ -6032,7 +6060,23 @@ function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
};
|
||||
}
|
||||
|
||||
function parseNodeWebProbeObserveOptions(args: string[], node: string, lane: string, spec: HwlabRuntimeLaneSpec): NodeWebProbeObserveOptions {
|
||||
function normalizeNodeWebProbeObserveArgs(args: string[]): { args: string[]; id: string | null } {
|
||||
const [observeActionRaw, maybeId, ...rest] = args;
|
||||
if (observeActionRaw !== "start" && maybeId !== undefined && !maybeId.startsWith("--")) {
|
||||
if (!isSafeWebObserveJobId(maybeId)) throw new Error(`unsafe web-probe observe id: ${maybeId}`);
|
||||
return { args: [observeActionRaw, ...rest], id: maybeId };
|
||||
}
|
||||
return { args, id: null };
|
||||
}
|
||||
|
||||
function parseNodeWebProbeObserveOptions(
|
||||
args: string[],
|
||||
node: string,
|
||||
lane: string,
|
||||
spec: HwlabRuntimeLaneSpec,
|
||||
observeId: string | null,
|
||||
indexed: WebObserveIndexEntry | null,
|
||||
): NodeWebProbeObserveOptions {
|
||||
const [observeActionRaw] = args;
|
||||
if (
|
||||
observeActionRaw !== "start"
|
||||
@@ -6042,7 +6086,7 @@ function parseNodeWebProbeObserveOptions(args: string[], node: string, lane: str
|
||||
&& observeActionRaw !== "collect"
|
||||
&& observeActionRaw !== "analyze"
|
||||
) {
|
||||
throw new Error("web-probe observe usage: observe start|status|command|stop|collect|analyze --node NODE --lane vNN [--state-dir DIR|--job-id ID]");
|
||||
throw new Error("web-probe observe usage: observe start --node NODE --lane vNN [...]; observe status|command|stop|collect|analyze <id> [...]");
|
||||
}
|
||||
assertKnownOptions(args.slice(1), new Set([
|
||||
"--node",
|
||||
@@ -6068,8 +6112,8 @@ function parseNodeWebProbeObserveOptions(args: string[], node: string, lane: str
|
||||
]), new Set(["--force"]));
|
||||
const commandTypeRaw = optionValue(args, "--type") ?? null;
|
||||
const commandType = commandTypeRaw === null ? null : parseNodeWebProbeObserveCommandType(commandTypeRaw);
|
||||
const stateDir = optionValue(args, "--state-dir") ?? null;
|
||||
const jobId = optionValue(args, "--job-id") ?? null;
|
||||
const stateDir = optionValue(args, "--state-dir") ?? indexed?.stateDir ?? null;
|
||||
const jobId = optionValue(args, "--job-id") ?? observeId ?? indexed?.id ?? null;
|
||||
if (stateDir !== null && !isSafeWebObserveStateDir(stateDir)) throw new Error(`unsafe web-probe observe --state-dir: ${stateDir}`);
|
||||
if (jobId !== null && !isSafeWebObserveJobId(jobId)) throw new Error(`unsafe web-probe observe --job-id: ${jobId}`);
|
||||
if (observeActionRaw !== "start" && stateDir === null && jobId === null) {
|
||||
@@ -6078,6 +6122,7 @@ function parseNodeWebProbeObserveOptions(args: string[], node: string, lane: str
|
||||
return {
|
||||
action: "observe",
|
||||
observeAction: observeActionRaw,
|
||||
id: observeId ?? jobId,
|
||||
node,
|
||||
lane,
|
||||
url: optionValue(args, "--url") ?? spec.publicWebUrl,
|
||||
@@ -6457,6 +6502,22 @@ function runNodeWebProbeObserveStart(
|
||||
].join("\n");
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||
const started = parseJsonObject(result.stdout);
|
||||
const observerId = typeof started?.jobId === "string" ? started.jobId : jobId;
|
||||
const index = result.exitCode === 0 && started?.ok === true
|
||||
? upsertWebObserveIndexEntry({
|
||||
id: observerId,
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
stateDir,
|
||||
url: options.url,
|
||||
targetPath: options.targetPath,
|
||||
status: "running",
|
||||
pid: typeof started.pid === "number" ? started.pid : null,
|
||||
startedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
: null;
|
||||
return {
|
||||
ok: result.exitCode === 0 && started?.ok === true,
|
||||
status: result.exitCode === 0 && started?.ok === true ? "started" : "blocked",
|
||||
@@ -6466,8 +6527,11 @@ function runNodeWebProbeObserveStart(
|
||||
workspace: spec.workspace,
|
||||
url: options.url,
|
||||
targetPath: options.targetPath,
|
||||
id: observerId,
|
||||
credential,
|
||||
observer: started,
|
||||
observer: withWebObserveShortcuts(started, observerId),
|
||||
index,
|
||||
next: webObserveNextCommands(observerId),
|
||||
result: compactCommandResultRedacted(result, [material.password ?? ""]),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
@@ -6481,14 +6545,21 @@ function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec:
|
||||
].join("\n");
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||
const status = parseJsonObject(result.stdout);
|
||||
const observerId = webObserveIdFromStatus(status, options);
|
||||
const index = status?.ok !== false && observerId !== null && options.stateDir !== null
|
||||
? upsertWebObserveIndexEntry(webObserveIndexEntryFromOptions(options, spec, observerId, status))
|
||||
: null;
|
||||
return {
|
||||
ok: result.exitCode === 0 && status?.ok !== false,
|
||||
status: result.exitCode === 0 ? "observed" : "blocked",
|
||||
command: `hwlab nodes web-probe observe status --node ${options.node} --lane ${options.lane}`,
|
||||
command: webObserveCommandLabel("status", options),
|
||||
id: observerId,
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
observer: status,
|
||||
observer: withWebObserveShortcuts(status, observerId),
|
||||
index,
|
||||
next: observerId === null ? null : webObserveNextCommands(observerId),
|
||||
result: compactCommandResult(result),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
@@ -6529,7 +6600,8 @@ function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOptions, spec
|
||||
return {
|
||||
ok: killResult.exitCode === 0,
|
||||
status: killResult.exitCode === 0 ? "forced-stop-requested" : "blocked",
|
||||
command: `hwlab nodes web-probe observe stop --node ${options.node} --lane ${options.lane}`,
|
||||
command: webObserveCommandLabel("stop", options),
|
||||
id: webObserveIdFromOptions(options),
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
@@ -6543,9 +6615,8 @@ function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOptions, spec
|
||||
return {
|
||||
ok: result.exitCode === 0 && commandResult?.ok !== false,
|
||||
status: result.exitCode === 0 ? (options.waitMs > 0 ? "completed-or-queued" : "queued") : "blocked",
|
||||
command: stopCommand
|
||||
? `hwlab nodes web-probe observe stop --node ${options.node} --lane ${options.lane}`
|
||||
: `hwlab nodes web-probe observe command --node ${options.node} --lane ${options.lane}`,
|
||||
command: webObserveCommandLabel(stopCommand ? "stop" : "command", options),
|
||||
id: webObserveIdFromOptions(options),
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
@@ -6568,7 +6639,8 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec
|
||||
return {
|
||||
ok: result.exitCode === 0 && collect?.ok !== false,
|
||||
status: result.exitCode === 0 ? "collected" : "blocked",
|
||||
command: `hwlab nodes web-probe observe collect --node ${options.node} --lane ${options.lane}`,
|
||||
command: webObserveCommandLabel("collect", options),
|
||||
id: webObserveIdFromOptions(options),
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
@@ -6593,7 +6665,8 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
|
||||
return {
|
||||
ok: result.exitCode === 0 && analysis?.ok === true,
|
||||
status: result.exitCode === 0 && analysis?.ok === true ? "analyzed" : "blocked",
|
||||
command: `hwlab nodes web-probe observe analyze --node ${options.node} --lane ${options.lane}`,
|
||||
command: webObserveCommandLabel("analyze", options),
|
||||
id: webObserveIdFromOptions(options),
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
@@ -6620,6 +6693,129 @@ function nodeWebObserveResolveStateDirShell(options: Pick<NodeWebProbeObserveOpt
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function webObserveIndexPath(): string {
|
||||
return rootPath(".state/web-observe/index.json");
|
||||
}
|
||||
|
||||
function readWebObserveIndex(): Record<string, WebObserveIndexEntry> {
|
||||
const path = webObserveIndexPath();
|
||||
if (!existsSync(path)) return {};
|
||||
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`web-probe observe index is invalid: ${path}`);
|
||||
const entries: Record<string, WebObserveIndexEntry> = {};
|
||||
for (const [id, value] of Object.entries(parsed)) {
|
||||
if (!isSafeWebObserveJobId(id)) continue;
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
||||
const recordValue = value as Record<string, unknown>;
|
||||
const stateDir = typeof recordValue.stateDir === "string" ? recordValue.stateDir : "";
|
||||
const node = typeof recordValue.node === "string" ? recordValue.node : "";
|
||||
const lane = typeof recordValue.lane === "string" ? recordValue.lane : "";
|
||||
const workspace = typeof recordValue.workspace === "string" ? recordValue.workspace : "";
|
||||
if (!isSafeWebObserveStateDir(stateDir) || node.length === 0 || lane.length === 0 || workspace.length === 0) continue;
|
||||
entries[id] = {
|
||||
id,
|
||||
node,
|
||||
lane,
|
||||
workspace,
|
||||
stateDir,
|
||||
url: typeof recordValue.url === "string" ? recordValue.url : "",
|
||||
targetPath: typeof recordValue.targetPath === "string" ? recordValue.targetPath : "",
|
||||
status: typeof recordValue.status === "string" ? recordValue.status : null,
|
||||
pid: typeof recordValue.pid === "number" ? recordValue.pid : null,
|
||||
startedAt: typeof recordValue.startedAt === "string" ? recordValue.startedAt : null,
|
||||
updatedAt: typeof recordValue.updatedAt === "string" ? recordValue.updatedAt : "",
|
||||
};
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function readWebObserveIndexEntry(id: string): WebObserveIndexEntry | null {
|
||||
return readWebObserveIndex()[id] ?? null;
|
||||
}
|
||||
|
||||
function upsertWebObserveIndexEntry(entry: WebObserveIndexEntry): Record<string, unknown> {
|
||||
const path = webObserveIndexPath();
|
||||
try {
|
||||
const current = readWebObserveIndex();
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
const next = { ...current, [entry.id]: entry };
|
||||
const tmp = `${path}.tmp-${process.pid}`;
|
||||
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
||||
renameSync(tmp, path);
|
||||
return {
|
||||
ok: true,
|
||||
id: entry.id,
|
||||
path,
|
||||
statusCommand: `bun scripts/cli.ts hwlab nodes web-probe observe status ${entry.id}`,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
id: entry.id,
|
||||
path,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
fallback: `bun scripts/cli.ts hwlab nodes web-probe observe status --node ${entry.node} --lane ${entry.lane} --state-dir ${entry.stateDir}`,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function webObserveIdFromOptions(options: Pick<NodeWebProbeObserveOptions, "id" | "jobId">): string | null {
|
||||
return options.id ?? options.jobId;
|
||||
}
|
||||
|
||||
function webObserveIdFromStatus(status: Record<string, unknown> | null, options: Pick<NodeWebProbeObserveOptions, "id" | "jobId">): string | null {
|
||||
const manifest = record(status?.manifest);
|
||||
const heartbeat = record(status?.heartbeat);
|
||||
const manifestJobId = typeof manifest.jobId === "string" ? manifest.jobId : null;
|
||||
const heartbeatJobId = typeof heartbeat.jobId === "string" ? heartbeat.jobId : null;
|
||||
return manifestJobId ?? heartbeatJobId ?? webObserveIdFromOptions(options);
|
||||
}
|
||||
|
||||
function webObserveIndexEntryFromOptions(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, id: string, status: Record<string, unknown>): WebObserveIndexEntry {
|
||||
const manifest = record(status.manifest);
|
||||
const heartbeat = record(status.heartbeat);
|
||||
return {
|
||||
id,
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
stateDir: options.stateDir ?? "",
|
||||
url: typeof manifest.baseUrl === "string" ? manifest.baseUrl : options.url,
|
||||
targetPath: typeof manifest.targetPath === "string" ? manifest.targetPath : options.targetPath,
|
||||
status: typeof manifest.status === "string" ? manifest.status : null,
|
||||
pid: typeof status.pid === "number" ? status.pid : null,
|
||||
startedAt: typeof manifest.startedAt === "string" ? manifest.startedAt : null,
|
||||
updatedAt: typeof heartbeat.updatedAt === "string" ? heartbeat.updatedAt : new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function webObserveCommandLabel(action: NodeWebProbeObserveAction, options: Pick<NodeWebProbeObserveOptions, "id" | "jobId" | "node" | "lane">): string {
|
||||
const id = webObserveIdFromOptions(options);
|
||||
return id === null
|
||||
? `hwlab nodes web-probe observe ${action} --node ${options.node} --lane ${options.lane}`
|
||||
: `hwlab nodes web-probe observe ${action} ${id}`;
|
||||
}
|
||||
|
||||
function webObserveNextCommands(id: string): Record<string, string> {
|
||||
return {
|
||||
status: `bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`,
|
||||
command: `bun scripts/cli.ts hwlab nodes web-probe observe command ${id} --type mark --label checkpoint`,
|
||||
stop: `bun scripts/cli.ts hwlab nodes web-probe observe stop ${id}`,
|
||||
analyze: `bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
function withWebObserveShortcuts(value: Record<string, unknown> | null, id: string | null): Record<string, unknown> | null {
|
||||
if (value === null || id === null) return value;
|
||||
return {
|
||||
...value,
|
||||
id,
|
||||
next: webObserveNextCommands(id),
|
||||
};
|
||||
}
|
||||
|
||||
function nodeWebObserveStatusNodeScript(tailLines: number, node: string, lane: string): string {
|
||||
return `node -e ${shellQuote(`
|
||||
const fs=require('fs'),path=require('path');
|
||||
|
||||
Reference in New Issue
Block a user