tui(v0.3): add LiveValuesPayload type + liveValuesFilePath()
Task 1 of the v0.3 inline-live-values plan. LiveValuesPayload is the on-disk shape the server pump writes and the TUI reads. Same versioning + envelope approach as the existing selection state file: version=1, ISO updated_at, project_dir + device + pou_name match the user's current selection so the TUI can reject stale snapshots. liveValuesFilePath() lives next to stateFilePath() and resolves to %LOCALAPPDATA%/codesys-mcp/tui-live-values.json on Windows or $XDG_STATE_HOME/codesys-mcp/tui-live-values.json (default ~/.local/state) on POSIX. Path joining stays platform-flavoured even when the unit test forces a different process.platform, so the suite is host-independent.
This commit is contained in:
parent
0fdb0046d8
commit
6f9cc46547
3 changed files with 79 additions and 5 deletions
31
src/tui/shared/live-values.ts
Normal file
31
src/tui/shared/live-values.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Shape of `tui-live-values.json` written by the server's live-values pump.
|
||||
* The TUI Viewer reads this to overlay live runtime values inline next to
|
||||
* declared variable names.
|
||||
*
|
||||
* One file per machine; keyed by which POU the user is currently looking at
|
||||
* in `tui-state.json` so the TUI knows whether the values match its current
|
||||
* view and can ignore stale snapshots from a previous selection.
|
||||
*/
|
||||
export interface LiveValuesPayload {
|
||||
version: 1;
|
||||
/** ISO 8601 timestamp of the moment the snapshot was written. */
|
||||
updated_at: string;
|
||||
/** Project root dir the runtime was queried for. Matches the project_dir field in tui-state.json. */
|
||||
project_dir: string;
|
||||
/** Device name the runtime belongs to. Matches the device field in tui-state.json. */
|
||||
device: string;
|
||||
/** POU the values were read from. Bare name (no path); matches tui-state.json's selection.name. */
|
||||
pou_name: string;
|
||||
/** Variable name -> value snapshot. Bare names (no `PLC_PRG.` prefix). */
|
||||
values: Record<string, LiveValueSnapshot>;
|
||||
}
|
||||
|
||||
export interface LiveValueSnapshot {
|
||||
/** Live value as a string (whatever read_variable returned). */
|
||||
value: string;
|
||||
/** Optional declared type, populated when the pump can determine it. */
|
||||
type?: string;
|
||||
/** Per-value timestamp in ms since epoch. Useful when the snapshot was assembled across several reads. */
|
||||
ts: number;
|
||||
}
|
||||
|
|
@ -2,18 +2,31 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
|
||||
const APP_DIR = 'codesys-mcp';
|
||||
const FILE_NAME = 'tui-state.json';
|
||||
const STATE_FILE = 'tui-state.json';
|
||||
const LIVE_VALUES_FILE = 'tui-live-values.json';
|
||||
|
||||
export function stateFilePath(): string {
|
||||
function resolveStateDir(): string {
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA;
|
||||
if (!localAppData) {
|
||||
throw new Error('LOCALAPPDATA is not set; cannot resolve TUI state file path');
|
||||
}
|
||||
return path.win32.join(localAppData, APP_DIR, FILE_NAME);
|
||||
return path.win32.join(localAppData, APP_DIR);
|
||||
}
|
||||
const xdg = process.env.XDG_STATE_HOME;
|
||||
const home = process.env.HOME ?? os.homedir();
|
||||
const base = xdg ?? path.posix.join(home, '.local', 'state');
|
||||
return path.posix.join(base, APP_DIR, FILE_NAME);
|
||||
return path.posix.join(base, APP_DIR);
|
||||
}
|
||||
|
||||
function joinForPlatform(dir: string, file: string): string {
|
||||
return process.platform === 'win32' ? path.win32.join(dir, file) : path.posix.join(dir, file);
|
||||
}
|
||||
|
||||
export function stateFilePath(): string {
|
||||
return joinForPlatform(resolveStateDir(), STATE_FILE);
|
||||
}
|
||||
|
||||
export function liveValuesFilePath(): string {
|
||||
return joinForPlatform(resolveStateDir(), LIVE_VALUES_FILE);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as path from 'path';
|
||||
import { stateFilePath } from '../../src/tui/shared/state-paths.ts';
|
||||
import { stateFilePath, liveValuesFilePath } from '../../src/tui/shared/state-paths.ts';
|
||||
|
||||
const ORIG_PLATFORM = Object.getOwnPropertyDescriptor(process, 'platform')!;
|
||||
const ORIG_ENV = { ...process.env };
|
||||
|
|
@ -49,3 +49,33 @@ describe('stateFilePath', () => {
|
|||
expect(() => stateFilePath()).toThrow(/LOCALAPPDATA/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('liveValuesFilePath', () => {
|
||||
it('uses %LOCALAPPDATA%/codesys-mcp/tui-live-values.json on Windows', () => {
|
||||
setPlatform('win32');
|
||||
process.env.LOCALAPPDATA = 'C:\\\\Users\\\\u\\\\AppData\\\\Local';
|
||||
const p = liveValuesFilePath();
|
||||
expect(p).toBe(
|
||||
path.join('C:\\\\Users\\\\u\\\\AppData\\\\Local', 'codesys-mcp', 'tui-live-values.json')
|
||||
);
|
||||
});
|
||||
|
||||
it('uses $XDG_STATE_HOME/codesys-mcp/tui-live-values.json when set', () => {
|
||||
setPlatform('linux');
|
||||
process.env.XDG_STATE_HOME = '/tmp/xdg-state';
|
||||
expect(liveValuesFilePath()).toBe('/tmp/xdg-state/codesys-mcp/tui-live-values.json');
|
||||
});
|
||||
|
||||
it('falls back to ~/.local/state on Linux without XDG_STATE_HOME', () => {
|
||||
setPlatform('linux');
|
||||
delete process.env.XDG_STATE_HOME;
|
||||
process.env.HOME = '/home/u';
|
||||
expect(liveValuesFilePath()).toBe('/home/u/.local/state/codesys-mcp/tui-live-values.json');
|
||||
});
|
||||
|
||||
it('throws on Windows when LOCALAPPDATA is unset', () => {
|
||||
setPlatform('win32');
|
||||
delete process.env.LOCALAPPDATA;
|
||||
expect(() => liveValuesFilePath()).toThrow(/LOCALAPPDATA/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue