@filsilva/helios-cli 0.10.0
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 +171 -0
- package/bin/helios.js +34 -0
- package/dist/client/assets/HeliosSessionWorker.browser-BYYjDIKH.js +3 -0
- package/dist/client/assets/HeliosSessionWorker.browser-BYYjDIKH.js.map +1 -0
- package/dist/client/assets/d3force3dWorker-BKANL9of.js +2 -0
- package/dist/client/assets/d3force3dWorker-BKANL9of.js.map +1 -0
- package/dist/client/assets/index-CP7mSmLx.js +9530 -0
- package/dist/client/assets/index-CP7mSmLx.js.map +1 -0
- package/dist/client/assets/layoutWorker-Lc8iIdmf.js +2 -0
- package/dist/client/assets/layoutWorker-Lc8iIdmf.js.map +1 -0
- package/dist/client/index.html +27 -0
- package/package.json +40 -0
- package/skills/helios-cli/SKILL.md +118 -0
- package/skills/helios-cli/references/behaviors.md +47 -0
- package/skills/helios-cli/references/layouts.md +77 -0
- package/skills/helios-cli/references/mappers.md +119 -0
- package/skills/helios-cli/references/metrics.md +83 -0
- package/skills/helios-cli/references/networks.md +53 -0
- package/skills/helios-cli/references/persistence.md +136 -0
- package/skills/helios-cli/references/positions.md +63 -0
- package/skills/helios-cli/references/rendering-export.md +56 -0
- package/skills/helios-cli/references/rpc-methods.md +83 -0
- package/src/cli.js +488 -0
- package/src/client/index.html +27 -0
- package/src/client/main.js +2210 -0
- package/src/daemon/SessionDaemon.js +1065 -0
- package/src/daemon/entry.js +36 -0
- package/src/protocol/jsonl.js +88 -0
- package/src/shared/cliConfig.js +52 -0
- package/src/shared/fileSessionStore.js +202 -0
- package/src/shared/fs.js +59 -0
- package/src/shared/networkFormats.js +55 -0
- package/src/shared/networkInspect.js +81 -0
- package/src/shared/paths.js +43 -0
- package/src/shared/sessionClient.js +88 -0
- package/src/shared/sessionId.js +5 -0
- package/src/shared/sessionRegistry.js +53 -0
- package/src/shared/sessionSurfaces.js +199 -0
- package/vite.config.js +47 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const rawConfig = process.argv[2];
|
|
2
|
+
if (!rawConfig) {
|
|
3
|
+
throw new Error('Missing daemon configuration payload');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const config = JSON.parse(Buffer.from(rawConfig, 'base64url').toString('utf8'));
|
|
7
|
+
if (config.storageDir) process.env.HELIOS_CLI_STORAGE_DIR = config.storageDir;
|
|
8
|
+
|
|
9
|
+
const { SessionDaemon } = await import('./SessionDaemon.js');
|
|
10
|
+
const { saveSessionMeta } = await import('../shared/sessionRegistry.js');
|
|
11
|
+
const daemon = new SessionDaemon(config);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await daemon.start();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
await saveSessionMeta(config.sessionId, {
|
|
17
|
+
sessionId: config.sessionId,
|
|
18
|
+
pid: process.pid,
|
|
19
|
+
status: 'error',
|
|
20
|
+
mode: config.mode,
|
|
21
|
+
renderer: config.renderer,
|
|
22
|
+
layout: config.layout,
|
|
23
|
+
runtime: config.runtime,
|
|
24
|
+
surface: config.surface ?? null,
|
|
25
|
+
client: config.client ?? null,
|
|
26
|
+
browserChannel: config.browserChannel ?? null,
|
|
27
|
+
noGpu: config.noGpu === true,
|
|
28
|
+
bridgeConnected: false,
|
|
29
|
+
gpu: error?.data ?? null,
|
|
30
|
+
networkPath: config.networkPath ?? null,
|
|
31
|
+
createdAt: new Date().toISOString(),
|
|
32
|
+
updatedAt: new Date().toISOString(),
|
|
33
|
+
lastError: error?.stack ?? error?.message ?? String(error),
|
|
34
|
+
});
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
|
|
3
|
+
export function encodeMessage(message) {
|
|
4
|
+
return `${JSON.stringify(message)}\n`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createJsonLineParser(onMessage) {
|
|
8
|
+
let buffer = '';
|
|
9
|
+
return (chunk) => {
|
|
10
|
+
buffer += chunk;
|
|
11
|
+
while (true) {
|
|
12
|
+
const lineEnd = buffer.indexOf('\n');
|
|
13
|
+
if (lineEnd === -1) break;
|
|
14
|
+
const line = buffer.slice(0, lineEnd).trim();
|
|
15
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
16
|
+
if (!line) continue;
|
|
17
|
+
const parsed = JSON.parse(line);
|
|
18
|
+
onMessage(parsed);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeJsonLine(stream, value) {
|
|
24
|
+
stream.write(encodeMessage(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function callJsonRpc(stream, reader, payload, { timeoutMs = 30_000 } = {}) {
|
|
28
|
+
const id = payload.id;
|
|
29
|
+
const response = await reader.waitForResponse(id, { timeoutMs }, () => {
|
|
30
|
+
writeJsonLine(stream, payload);
|
|
31
|
+
});
|
|
32
|
+
if (response?.error) {
|
|
33
|
+
const error = new Error(response.error.message ?? 'RPC request failed');
|
|
34
|
+
error.code = response.error.code;
|
|
35
|
+
error.data = response.error.data;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return response?.result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class JsonRpcResponseReader {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.pending = new Map();
|
|
44
|
+
this.notifications = new Set();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onNotification(handler) {
|
|
48
|
+
this.notifications.add(handler);
|
|
49
|
+
return () => this.notifications.delete(handler);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
handleMessage(message) {
|
|
53
|
+
if (Object.prototype.hasOwnProperty.call(message ?? {}, 'id') && this.pending.has(message.id)) {
|
|
54
|
+
const pending = this.pending.get(message.id);
|
|
55
|
+
this.pending.delete(message.id);
|
|
56
|
+
pending.resolve(message);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const handler of this.notifications) handler(message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
waitForResponse(id, { timeoutMs = 30_000 } = {}, send) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const timeout = setTimeout(() => {
|
|
65
|
+
this.pending.delete(id);
|
|
66
|
+
reject(new Error(`Timed out waiting for RPC response ${id}`));
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
this.pending.set(id, {
|
|
69
|
+
resolve: (value) => {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
resolve(value);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
send();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
this.pending.delete(id);
|
|
79
|
+
reject(error);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function pipeStreams(source, destination) {
|
|
86
|
+
source.pipe(destination);
|
|
87
|
+
await once(source, 'close');
|
|
88
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureStateDirs, readJsonFile, writeJsonFile } from './fs.js';
|
|
3
|
+
import { cliConfigPath } from './paths.js';
|
|
4
|
+
|
|
5
|
+
const defaultConfig = Object.freeze({
|
|
6
|
+
version: 1,
|
|
7
|
+
apps: {},
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function normalizeConfig(value) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return { ...defaultConfig, apps: {} };
|
|
12
|
+
const apps = value.apps && typeof value.apps === 'object' && !Array.isArray(value.apps)
|
|
13
|
+
? { ...value.apps }
|
|
14
|
+
: {};
|
|
15
|
+
return {
|
|
16
|
+
version: Number.isInteger(value.version) ? value.version : 1,
|
|
17
|
+
...value,
|
|
18
|
+
apps,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function loadCliConfig() {
|
|
23
|
+
await ensureStateDirs();
|
|
24
|
+
return normalizeConfig(await readJsonFile(cliConfigPath, defaultConfig));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function saveCliConfig(config) {
|
|
28
|
+
await ensureStateDirs();
|
|
29
|
+
const normalized = normalizeConfig(config);
|
|
30
|
+
await writeJsonFile(cliConfigPath, normalized);
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function getConfiguredDesktopAppPath() {
|
|
35
|
+
const config = await loadCliConfig();
|
|
36
|
+
const value = config.apps?.desktop?.appPath;
|
|
37
|
+
return value == null ? null : String(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function setConfiguredDesktopAppPath(appPath) {
|
|
41
|
+
const resolvedPath = path.resolve(String(appPath));
|
|
42
|
+
const config = await loadCliConfig();
|
|
43
|
+
config.apps = config.apps ?? {};
|
|
44
|
+
config.apps.desktop = {
|
|
45
|
+
...(config.apps.desktop && typeof config.apps.desktop === 'object' ? config.apps.desktop : {}),
|
|
46
|
+
appPath: resolvedPath,
|
|
47
|
+
};
|
|
48
|
+
await saveCliConfig(config);
|
|
49
|
+
return resolvedPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { cliConfigPath };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureStateDirs, readJsonFile, writeJsonFile } from './fs.js';
|
|
4
|
+
import {
|
|
5
|
+
sessionIndexPath,
|
|
6
|
+
sessionNetworksDir,
|
|
7
|
+
sessionPositionsDir,
|
|
8
|
+
sessionRecordsDir,
|
|
9
|
+
unfinishedSessionsPath,
|
|
10
|
+
} from './paths.js';
|
|
11
|
+
|
|
12
|
+
function filenameForId(id, extension = '.json') {
|
|
13
|
+
return `${Buffer.from(String(id), 'utf8').toString('base64url')}${extension}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function recordPath(id) {
|
|
17
|
+
return path.join(sessionRecordsDir, filenameForId(id));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sidecarPath(directory, sessionId, extension) {
|
|
21
|
+
return path.join(directory, filenameForId(sessionId, extension));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function asUint8Array(value) {
|
|
25
|
+
if (value == null) return null;
|
|
26
|
+
if (value instanceof Uint8Array) return value;
|
|
27
|
+
if (Buffer.isBuffer(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
28
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
29
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
30
|
+
if (value.__heliosBinary === 'base64') return new Uint8Array(Buffer.from(String(value.data ?? ''), 'base64'));
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function encodeBinaryForJson(value) {
|
|
35
|
+
if (value == null || typeof value !== 'object') return value;
|
|
36
|
+
if (Array.isArray(value)) return value.map((entry) => encodeBinaryForJson(entry));
|
|
37
|
+
const bytes = asUint8Array(value);
|
|
38
|
+
if (bytes) {
|
|
39
|
+
return {
|
|
40
|
+
__heliosBinary: 'base64',
|
|
41
|
+
type: value.constructor?.name ?? 'Uint8Array',
|
|
42
|
+
byteLength: bytes.byteLength,
|
|
43
|
+
data: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const output = {};
|
|
47
|
+
for (const [key, entry] of Object.entries(value)) output[key] = encodeBinaryForJson(entry);
|
|
48
|
+
return output;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function decodeBinaryFromJson(value) {
|
|
52
|
+
if (value == null || typeof value !== 'object') return value;
|
|
53
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value) || Buffer.isBuffer(value)) return value;
|
|
54
|
+
if (value.__heliosBinary === 'base64') {
|
|
55
|
+
return new Uint8Array(Buffer.from(String(value.data ?? ''), 'base64'));
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(value)) return value.map((entry) => decodeBinaryFromJson(entry));
|
|
58
|
+
const output = {};
|
|
59
|
+
for (const [key, entry] of Object.entries(value)) output[key] = decodeBinaryFromJson(entry);
|
|
60
|
+
return output;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function networkExtension(format) {
|
|
64
|
+
const normalized = String(format ?? 'zxnet').trim().toLowerCase().replace(/^\./, '');
|
|
65
|
+
if (normalized === 'bxnet') return '.bxnet';
|
|
66
|
+
if (normalized === 'xnet') return '.xnet';
|
|
67
|
+
return '.zxnet';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readIndex() {
|
|
71
|
+
const parsed = await readJsonFile(sessionIndexPath, []);
|
|
72
|
+
return Array.isArray(parsed) ? parsed.map((id) => String(id)).filter(Boolean) : [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function writeIndex(ids) {
|
|
76
|
+
await writeJsonFile(sessionIndexPath, Array.from(new Set(ids.map((id) => String(id)).filter(Boolean))));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function addToIndex(id) {
|
|
80
|
+
if (!id) return;
|
|
81
|
+
const ids = await readIndex();
|
|
82
|
+
if (!ids.includes(String(id))) await writeIndex([...ids, String(id)]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function removeFromIndex(id) {
|
|
86
|
+
if (!id) return;
|
|
87
|
+
const target = String(id);
|
|
88
|
+
await writeIndex((await readIndex()).filter((entry) => entry !== target));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function unlinkIfExists(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
await fs.unlink(filePath);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sidecarMetadata(record, dataFile, byteLength) {
|
|
100
|
+
const { data, ...metadata } = record;
|
|
101
|
+
return {
|
|
102
|
+
...metadata,
|
|
103
|
+
data: null,
|
|
104
|
+
dataFile,
|
|
105
|
+
byteLength: Number.isFinite(Number(record.byteLength)) ? Number(record.byteLength) : byteLength,
|
|
106
|
+
updatedAt: record.updatedAt ?? Date.now(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class FileSessionStore {
|
|
111
|
+
async putSession(record) {
|
|
112
|
+
await ensureStateDirs();
|
|
113
|
+
const decoded = decodeBinaryFromJson(record);
|
|
114
|
+
const id = decoded?.id;
|
|
115
|
+
if (!id) throw new Error('Session record requires an id');
|
|
116
|
+
|
|
117
|
+
if (decoded.kind === 'session-network-data') {
|
|
118
|
+
const bytes = asUint8Array(decoded.data);
|
|
119
|
+
if (!bytes) throw new Error(`Network side record ${id} is missing binary data`);
|
|
120
|
+
const filePath = sidecarPath(sessionNetworksDir, decoded.sessionId ?? id, networkExtension(decoded.format));
|
|
121
|
+
await fs.writeFile(filePath, Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));
|
|
122
|
+
const metadata = sidecarMetadata(decoded, filePath, bytes.byteLength);
|
|
123
|
+
await writeJsonFile(recordPath(id), encodeBinaryForJson(metadata));
|
|
124
|
+
return encodeBinaryForJson(metadata);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (decoded.kind === 'session-position-data') {
|
|
128
|
+
const bytes = asUint8Array(decoded.data);
|
|
129
|
+
if (!bytes) throw new Error(`Position side record ${id} is missing binary data`);
|
|
130
|
+
const filePath = sidecarPath(sessionPositionsDir, decoded.sessionId ?? id, '.positions.bin');
|
|
131
|
+
await fs.writeFile(filePath, Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));
|
|
132
|
+
const metadata = sidecarMetadata(decoded, filePath, bytes.byteLength);
|
|
133
|
+
await writeJsonFile(recordPath(id), encodeBinaryForJson(metadata));
|
|
134
|
+
return encodeBinaryForJson(metadata);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await writeJsonFile(recordPath(id), encodeBinaryForJson(decoded));
|
|
138
|
+
await addToIndex(id);
|
|
139
|
+
return encodeBinaryForJson(decoded);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async getSession(id) {
|
|
143
|
+
await ensureStateDirs();
|
|
144
|
+
const record = decodeBinaryFromJson(await readJsonFile(recordPath(id), null));
|
|
145
|
+
if (!record) return null;
|
|
146
|
+
if ((record.kind === 'session-network-data' || record.kind === 'session-position-data') && record.dataFile) {
|
|
147
|
+
try {
|
|
148
|
+
const bytes = await fs.readFile(record.dataFile);
|
|
149
|
+
record.data = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
150
|
+
record.byteLength = record.byteLength ?? bytes.byteLength;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
153
|
+
record.data = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return encodeBinaryForJson(record);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async listSessions() {
|
|
160
|
+
await ensureStateDirs();
|
|
161
|
+
const records = [];
|
|
162
|
+
for (const id of await readIndex()) {
|
|
163
|
+
const record = await this.getSession(id);
|
|
164
|
+
if (record) records.push(record);
|
|
165
|
+
}
|
|
166
|
+
return records;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async deleteSession(id) {
|
|
170
|
+
await ensureStateDirs();
|
|
171
|
+
const session = decodeBinaryFromJson(await readJsonFile(recordPath(id), null));
|
|
172
|
+
const networkId = session?.payload?.networkData?.dataRef ?? `${id}::network-data`;
|
|
173
|
+
const positionId = session?.payload?.positionData?.dataRef ?? `${id}::position-data`;
|
|
174
|
+
const network = decodeBinaryFromJson(await readJsonFile(recordPath(networkId), null));
|
|
175
|
+
const position = decodeBinaryFromJson(await readJsonFile(recordPath(positionId), null));
|
|
176
|
+
await Promise.all([
|
|
177
|
+
unlinkIfExists(recordPath(id)),
|
|
178
|
+
unlinkIfExists(recordPath(networkId)),
|
|
179
|
+
unlinkIfExists(recordPath(positionId)),
|
|
180
|
+
network?.dataFile ? unlinkIfExists(network.dataFile) : Promise.resolve(),
|
|
181
|
+
position?.dataFile ? unlinkIfExists(position.dataFile) : Promise.resolve(),
|
|
182
|
+
]);
|
|
183
|
+
await removeFromIndex(id);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getUnfinishedSessionId(workspaceId = null) {
|
|
188
|
+
const records = await readJsonFile(unfinishedSessionsPath, {});
|
|
189
|
+
const key = workspaceId == null || workspaceId === '' ? 'default' : String(workspaceId);
|
|
190
|
+
return records?.[key] ?? null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async setUnfinishedSessionId(id, workspaceId = null) {
|
|
194
|
+
await ensureStateDirs();
|
|
195
|
+
const records = await readJsonFile(unfinishedSessionsPath, {});
|
|
196
|
+
const key = workspaceId == null || workspaceId === '' ? 'default' : String(workspaceId);
|
|
197
|
+
if (id == null || id === '') delete records[key];
|
|
198
|
+
else records[key] = String(id);
|
|
199
|
+
await writeJsonFile(unfinishedSessionsPath, records);
|
|
200
|
+
return id ?? null;
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/shared/fs.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import {
|
|
3
|
+
clientDistDir,
|
|
4
|
+
logsDir,
|
|
5
|
+
sessionNetworksDir,
|
|
6
|
+
sessionPositionsDir,
|
|
7
|
+
sessionRecordsDir,
|
|
8
|
+
sessionsDir,
|
|
9
|
+
sessionStateDir,
|
|
10
|
+
socketsDir,
|
|
11
|
+
stateRoot,
|
|
12
|
+
storageSessionsDir,
|
|
13
|
+
} from './paths.js';
|
|
14
|
+
|
|
15
|
+
export async function ensureStateDirs() {
|
|
16
|
+
await Promise.all([
|
|
17
|
+
fs.mkdir(stateRoot, { recursive: true }),
|
|
18
|
+
fs.mkdir(sessionsDir, { recursive: true }),
|
|
19
|
+
fs.mkdir(sessionStateDir, { recursive: true }),
|
|
20
|
+
fs.mkdir(socketsDir, { recursive: true }),
|
|
21
|
+
fs.mkdir(logsDir, { recursive: true }),
|
|
22
|
+
fs.mkdir(storageSessionsDir, { recursive: true }),
|
|
23
|
+
fs.mkdir(sessionRecordsDir, { recursive: true }),
|
|
24
|
+
fs.mkdir(sessionNetworksDir, { recursive: true }),
|
|
25
|
+
fs.mkdir(sessionPositionsDir, { recursive: true }),
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function ensureClientBundle() {
|
|
30
|
+
try {
|
|
31
|
+
const stat = await fs.stat(clientDistDir);
|
|
32
|
+
if (!stat.isDirectory()) {
|
|
33
|
+
throw new Error(`Client bundle path is not a directory: ${clientDistDir}`);
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Missing built client bundle at ${clientDistDir}. Run "npm run build-client" in helios-cli before starting sessions.`,
|
|
38
|
+
{ cause: error },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function readJsonFile(filePath, fallback = null) {
|
|
44
|
+
try {
|
|
45
|
+
const text = await fs.readFile(filePath, 'utf8');
|
|
46
|
+
if (text.trim() === '') return fallback;
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error?.code === 'ENOENT') return fallback;
|
|
50
|
+
if (error instanceof SyntaxError) return fallback;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function writeJsonFile(filePath, value) {
|
|
56
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
57
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
58
|
+
await fs.rename(tmpPath, filePath);
|
|
59
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const HELIOS_NETWORK_FORMATS = Object.freeze({
|
|
2
|
+
bxnet: Object.freeze({
|
|
3
|
+
id: 'bxnet',
|
|
4
|
+
extension: '.bxnet',
|
|
5
|
+
mimeType: 'application/vnd.helios.bxnet',
|
|
6
|
+
label: 'Helios Binary Network',
|
|
7
|
+
}),
|
|
8
|
+
zxnet: Object.freeze({
|
|
9
|
+
id: 'zxnet',
|
|
10
|
+
extension: '.zxnet',
|
|
11
|
+
mimeType: 'application/vnd.helios.zxnet',
|
|
12
|
+
label: 'Helios Compressed Network',
|
|
13
|
+
}),
|
|
14
|
+
xnet: Object.freeze({
|
|
15
|
+
id: 'xnet',
|
|
16
|
+
extension: '.xnet',
|
|
17
|
+
mimeType: 'application/vnd.helios.xnet',
|
|
18
|
+
label: 'Helios XNet Network',
|
|
19
|
+
}),
|
|
20
|
+
gml: Object.freeze({
|
|
21
|
+
id: 'gml',
|
|
22
|
+
extension: '.gml',
|
|
23
|
+
mimeType: 'application/gml+xml',
|
|
24
|
+
label: 'Graph Modeling Language Network',
|
|
25
|
+
}),
|
|
26
|
+
gt: Object.freeze({
|
|
27
|
+
id: 'gt',
|
|
28
|
+
extension: '.gt',
|
|
29
|
+
extensions: Object.freeze(['.gt', '.gt.zst']),
|
|
30
|
+
mimeType: 'application/vnd.graph-tool.gt',
|
|
31
|
+
label: 'Graph-tool Network',
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const HELIOS_NETWORK_EXTENSIONS = Object.freeze(
|
|
36
|
+
Object.values(HELIOS_NETWORK_FORMATS).flatMap((entry) => entry.extensions ?? [entry.extension]),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export function inferNetworkFormat(filePath, fallback = 'bxnet') {
|
|
40
|
+
const normalizedPath = String(filePath ?? '').toLowerCase();
|
|
41
|
+
for (const format of Object.values(HELIOS_NETWORK_FORMATS)) {
|
|
42
|
+
const extensions = format.extensions ?? [format.extension];
|
|
43
|
+
if (extensions.some((extension) => normalizedPath.endsWith(extension))) return format.id;
|
|
44
|
+
}
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isHeliosNetworkPath(filePath) {
|
|
49
|
+
const normalizedPath = String(filePath ?? '').toLowerCase();
|
|
50
|
+
return HELIOS_NETWORK_EXTENSIONS.some((extension) => normalizedPath.endsWith(extension));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function networkMimeTypeForFormat(format) {
|
|
54
|
+
return HELIOS_NETWORK_FORMATS[format]?.mimeType ?? 'application/octet-stream';
|
|
55
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import HeliosNetwork, { AttributeType } from 'helios-network';
|
|
4
|
+
import { inferNetworkFormat } from './networkFormats.js';
|
|
5
|
+
|
|
6
|
+
const ATTRIBUTE_TYPE_NAMES = new Map(
|
|
7
|
+
Object.entries(AttributeType).map(([name, value]) => [value, name]),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
function attributeTypeName(type) {
|
|
11
|
+
return ATTRIBUTE_TYPE_NAMES.get(type) ?? `Unknown(${type})`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function serializeAttributeInfo(name, info) {
|
|
15
|
+
const type = info?.type ?? AttributeType.Unknown;
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
type,
|
|
19
|
+
typeName: attributeTypeName(type),
|
|
20
|
+
dimension: Number(info?.dimension ?? 1),
|
|
21
|
+
complex: info?.complex === true,
|
|
22
|
+
categorical: type === AttributeType.Category || type === AttributeType.MultiCategory,
|
|
23
|
+
stringLike: type === AttributeType.String,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function inspectAttributes(network, scope) {
|
|
28
|
+
const namesMethod = `get${scope}AttributeNames`;
|
|
29
|
+
const infoMethod = `get${scope}AttributeInfo`;
|
|
30
|
+
const names = typeof network?.[namesMethod] === 'function' ? network[namesMethod]() : [];
|
|
31
|
+
return names.map((name) => serializeAttributeInfo(name, network?.[infoMethod]?.(name)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadNetwork(bytes, format, options = {}) {
|
|
35
|
+
if (format === 'bxnet') return HeliosNetwork.fromBXNet(bytes, options);
|
|
36
|
+
if (format === 'zxnet') return HeliosNetwork.fromZXNet(bytes, options);
|
|
37
|
+
if (format === 'xnet') return HeliosNetwork.fromXNet(bytes, options);
|
|
38
|
+
if (format === 'gt') return HeliosNetwork.fromGT(bytes, options);
|
|
39
|
+
throw new Error(`Unsupported Helios network format "${format}"`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function inspectNetworkFile(filePath, options = {}) {
|
|
43
|
+
const resolvedPath = path.resolve(filePath);
|
|
44
|
+
const format = options.format ?? inferNetworkFormat(resolvedPath, null);
|
|
45
|
+
if (!format) throw new Error(`Cannot infer Helios network format from "${filePath}"`);
|
|
46
|
+
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const originalWarn = console.warn;
|
|
49
|
+
let network = null;
|
|
50
|
+
try {
|
|
51
|
+
const stat = await fs.stat(resolvedPath);
|
|
52
|
+
const bytes = await fs.readFile(resolvedPath);
|
|
53
|
+
console.warn = (...args) => {
|
|
54
|
+
const message = args.map((entry) => String(entry)).join(' ');
|
|
55
|
+
if (message.includes('[Helios serialization]')) warnings.push(message);
|
|
56
|
+
else originalWarn(...args);
|
|
57
|
+
};
|
|
58
|
+
network = await loadNetwork(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), format, options.loadOptions ?? {});
|
|
59
|
+
return {
|
|
60
|
+
kind: 'helios-network-inspection',
|
|
61
|
+
version: 1,
|
|
62
|
+
path: resolvedPath,
|
|
63
|
+
name: path.basename(resolvedPath),
|
|
64
|
+
format,
|
|
65
|
+
fileSize: stat.size,
|
|
66
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
67
|
+
nodeCount: network.nodeCount ?? 0,
|
|
68
|
+
edgeCount: network.edgeCount ?? 0,
|
|
69
|
+
directed: Boolean(network.directed),
|
|
70
|
+
attributes: {
|
|
71
|
+
node: inspectAttributes(network, 'Node'),
|
|
72
|
+
edge: inspectAttributes(network, 'Edge'),
|
|
73
|
+
network: inspectAttributes(network, 'Network'),
|
|
74
|
+
},
|
|
75
|
+
warnings,
|
|
76
|
+
};
|
|
77
|
+
} finally {
|
|
78
|
+
console.warn = originalWarn;
|
|
79
|
+
network?.dispose?.();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
export const packageRoot = path.resolve(__dirname, '..', '..');
|
|
9
|
+
export const clientDistDir = path.join(packageRoot, 'dist', 'client');
|
|
10
|
+
export const stateRoot = path.resolve(
|
|
11
|
+
process.env.HELIOS_CLI_STORAGE_DIR
|
|
12
|
+
?? process.env.HELIOS_HOME
|
|
13
|
+
?? path.join(os.homedir(), '.helios'),
|
|
14
|
+
);
|
|
15
|
+
export const runtimeDir = path.join(stateRoot, 'runtime');
|
|
16
|
+
export const sessionsDir = path.join(runtimeDir, 'sessions');
|
|
17
|
+
export const sessionStateDir = path.join(runtimeDir, 'session-state');
|
|
18
|
+
export const socketsDir = path.join(runtimeDir, 'sockets');
|
|
19
|
+
export const logsDir = path.join(runtimeDir, 'logs');
|
|
20
|
+
export const storageSessionsDir = path.join(stateRoot, 'sessions');
|
|
21
|
+
export const sessionRecordsDir = path.join(storageSessionsDir, 'records');
|
|
22
|
+
export const sessionNetworksDir = path.join(storageSessionsDir, 'networks');
|
|
23
|
+
export const sessionPositionsDir = path.join(storageSessionsDir, 'positions');
|
|
24
|
+
export const sessionIndexPath = path.join(storageSessionsDir, 'index.json');
|
|
25
|
+
export const unfinishedSessionsPath = path.join(storageSessionsDir, 'unfinished.json');
|
|
26
|
+
export const cliConfigPath = path.join(stateRoot, 'config.json');
|
|
27
|
+
|
|
28
|
+
export function sessionMetaPath(sessionId) {
|
|
29
|
+
return path.join(sessionsDir, `${sessionId}.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sessionStatePath(sessionId) {
|
|
33
|
+
return path.join(sessionStateDir, `${sessionId}.json`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sessionSocketPath(sessionId) {
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
return `\\\\.\\pipe\\helios-cli-${sessionId}`;
|
|
39
|
+
}
|
|
40
|
+
const candidate = path.join(socketsDir, `${sessionId}.sock`);
|
|
41
|
+
if (Buffer.byteLength(candidate, 'utf8') < 100) return candidate;
|
|
42
|
+
return path.join(os.tmpdir(), 'helios-cli-sockets', `${sessionId}.sock`);
|
|
43
|
+
}
|