systemview 1.20.1 → 1.22.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 +76 -40
- package/api/CLIHistory.js +29 -0
- package/api/Connections.js +14 -0
- package/api/cli-history.json +1 -0
- package/api/connections.json +1 -1
- package/api/index.js +45 -12
- package/build/asset-manifest.json +8 -6
- package/build/index.html +1 -1
- package/build/static/css/main.1916c1d1.css +15 -0
- package/build/static/css/main.1916c1d1.css.map +1 -0
- package/build/static/js/422.05c538a9.chunk.js +2 -0
- package/build/static/js/422.05c538a9.chunk.js.map +1 -0
- package/build/static/js/main.e9f9f4f8.js +3 -0
- package/build/static/js/{main.c3485d98.js.LICENSE.txt → main.e9f9f4f8.js.LICENSE.txt} +26 -48
- package/build/static/js/main.e9f9f4f8.js.map +1 -0
- package/cli/connectService.js +147 -55
- package/cli/cookieClient.js +1 -4
- package/cli/index.js +92 -37
- package/cli/launchApp.js +8 -4
- package/cli/listTests.js +37 -19
- package/cli/logs.js +412 -0
- package/cli/manifest.js +97 -0
- package/cli/openBrowser.js +1 -1
- package/cli/probe.js +36 -19
- package/cli/runTests.js +35 -40
- package/cli/startLineReader.js +160 -15
- package/cli/utils/cli.js +86 -12
- package/package.json +3 -3
- package/testing-utilities/Test.class.js +2 -1
- package/testing-utilities/transformTests.js +7 -6
- package/build/static/css/main.3396e24d.css +0 -15
- package/build/static/css/main.3396e24d.css.map +0 -1
- package/build/static/js/main.c3485d98.js +0 -3
- package/build/static/js/main.c3485d98.js.map +0 -1
package/cli/logs.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const log = require("./logger");
|
|
5
|
+
const { createClient } = require("systemlynx");
|
|
6
|
+
const { createCookieHttpClient } = require("./cookieClient");
|
|
7
|
+
const cookieHttpClient = createCookieHttpClient();
|
|
8
|
+
const Client = createClient(cookieHttpClient);
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SNAPSHOT = "systemview-snapshot.ndjson";
|
|
11
|
+
|
|
12
|
+
const LEVEL_COLOR = {
|
|
13
|
+
trace: (s) => chalk.dim(s),
|
|
14
|
+
log: (s) => chalk.white(s),
|
|
15
|
+
warn: (s) => chalk.yellow(s),
|
|
16
|
+
error: (s) => chalk.red(s),
|
|
17
|
+
debug: (s) => chalk.blue(s),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function colorLevel(level) {
|
|
21
|
+
const fn = LEVEL_COLOR[level] || ((s) => s);
|
|
22
|
+
return fn((level || "log").padEnd(5));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SKIP_KEYS = new Set([
|
|
26
|
+
"timestamp",
|
|
27
|
+
"projectCode",
|
|
28
|
+
"serviceId",
|
|
29
|
+
"module",
|
|
30
|
+
"method",
|
|
31
|
+
"moduleMethod",
|
|
32
|
+
"traceId",
|
|
33
|
+
"level",
|
|
34
|
+
"message",
|
|
35
|
+
"duration",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function formatRow(entry, verbose, extraFields) {
|
|
39
|
+
const d = new Date(entry.timestamp);
|
|
40
|
+
const time =
|
|
41
|
+
d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) +
|
|
42
|
+
" " +
|
|
43
|
+
d.toLocaleTimeString("en-US", { hour12: false });
|
|
44
|
+
const project = entry.projectCode || "—";
|
|
45
|
+
const service = entry.serviceId || "—";
|
|
46
|
+
const method = entry.moduleMethod || "—";
|
|
47
|
+
const level = colorLevel(entry.level);
|
|
48
|
+
const msg = entry.message || "";
|
|
49
|
+
const dur = entry.duration != null ? chalk.dim(` ${entry.duration}ms`) : "";
|
|
50
|
+
const traceId = chalk.dim(entry.traceId || "—");
|
|
51
|
+
let row = ` ${chalk.dim(time)} ${chalk.cyan(project)} › ${chalk.cyan(service)} ${method.padEnd(15)} ${level} ${traceId} ${msg}${dur}`;
|
|
52
|
+
if (extraFields && extraFields.length) {
|
|
53
|
+
const parts = extraFields.map(
|
|
54
|
+
(p) => `${chalk.dim(p + ":")} ${cellStr(getAtPath(entry, p))}`,
|
|
55
|
+
);
|
|
56
|
+
row += " " + parts.join(" ");
|
|
57
|
+
}
|
|
58
|
+
if (verbose) {
|
|
59
|
+
const extra = {};
|
|
60
|
+
Object.keys(entry).forEach((k) => {
|
|
61
|
+
if (!SKIP_KEYS.has(k)) extra[k] = entry[k];
|
|
62
|
+
});
|
|
63
|
+
if (Object.keys(extra).length) {
|
|
64
|
+
return (
|
|
65
|
+
row +
|
|
66
|
+
"\n" +
|
|
67
|
+
JSON.stringify(extra, null, 2)
|
|
68
|
+
.split("\n")
|
|
69
|
+
.map((l) => " " + chalk.yellow(l))
|
|
70
|
+
.join("\n")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return row;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getAtPath(entry, field) {
|
|
78
|
+
return field.split(".").reduce((o, k) => (o != null ? o[k] : undefined), entry);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cellStr(v) {
|
|
82
|
+
if (v === null || v === undefined) return "—";
|
|
83
|
+
if (typeof v === "object") return Array.isArray(v) ? `[${v.length}]` : "{…}";
|
|
84
|
+
return String(v);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseFilters(filters) {
|
|
88
|
+
return (filters || [])
|
|
89
|
+
.map((f) => {
|
|
90
|
+
const eq = f.indexOf("=");
|
|
91
|
+
if (eq === -1) return null;
|
|
92
|
+
return { field: f.slice(0, eq), value: f.slice(eq + 1) };
|
|
93
|
+
})
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function matchesFilters(entry, andFilters, orFilters) {
|
|
98
|
+
const check = ({ field, value }) => {
|
|
99
|
+
if (field === "has") return getAtPath(entry, value) != null;
|
|
100
|
+
if (field === "missing") return getAtPath(entry, value) == null;
|
|
101
|
+
const v = getAtPath(entry, field);
|
|
102
|
+
return v != null && String(v).includes(value);
|
|
103
|
+
};
|
|
104
|
+
const andPass = andFilters.length === 0 || andFilters.every(check);
|
|
105
|
+
const orPass = orFilters.length === 0 || orFilters.some(check);
|
|
106
|
+
if (andFilters.length && orFilters.length) return andPass || orPass;
|
|
107
|
+
return andPass && orPass;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const DISPLAY_SKIP = new Set([
|
|
111
|
+
"timestamp",
|
|
112
|
+
"projectCode",
|
|
113
|
+
"serviceId",
|
|
114
|
+
"module",
|
|
115
|
+
"method",
|
|
116
|
+
"moduleMethod",
|
|
117
|
+
"traceId",
|
|
118
|
+
"level",
|
|
119
|
+
"message",
|
|
120
|
+
"duration",
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
function filterDisplayField(f) {
|
|
124
|
+
if (f.field === "has" || f.field === "missing") return f.value;
|
|
125
|
+
return f.field;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function appendSnapshot(filePath, entry, saveLimit) {
|
|
129
|
+
try {
|
|
130
|
+
let lines = [];
|
|
131
|
+
if (fs.existsSync(filePath)) {
|
|
132
|
+
lines = fs.readFileSync(filePath, "utf8").split("\n").filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
lines.push(JSON.stringify(entry));
|
|
135
|
+
if (lines.length > saveLimit) lines = lines.slice(lines.length - saveLimit);
|
|
136
|
+
fs.writeFileSync(filePath, lines.join("\n") + "\n");
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = async function logsCommand(
|
|
141
|
+
projectCode,
|
|
142
|
+
namespace,
|
|
143
|
+
{
|
|
144
|
+
uiUrl,
|
|
145
|
+
level,
|
|
146
|
+
limit = 300,
|
|
147
|
+
clear,
|
|
148
|
+
current,
|
|
149
|
+
follow,
|
|
150
|
+
verbose,
|
|
151
|
+
filter,
|
|
152
|
+
or: orFilters,
|
|
153
|
+
include,
|
|
154
|
+
json,
|
|
155
|
+
save,
|
|
156
|
+
saved,
|
|
157
|
+
saveLimit = 500,
|
|
158
|
+
},
|
|
159
|
+
) {
|
|
160
|
+
const andFilters = parseFilters(filter);
|
|
161
|
+
const orFiltersParsed = parseFilters(orFilters);
|
|
162
|
+
const extraFields = [
|
|
163
|
+
...andFilters.map(filterDisplayField),
|
|
164
|
+
...orFiltersParsed.map(filterDisplayField),
|
|
165
|
+
...(include || []),
|
|
166
|
+
].filter((f, i, arr) => !DISPLAY_SKIP.has(f) && arr.indexOf(f) === i);
|
|
167
|
+
|
|
168
|
+
const savePath =
|
|
169
|
+
save && typeof save === "string"
|
|
170
|
+
? path.resolve(process.cwd(), save)
|
|
171
|
+
: path.resolve(process.cwd(), DEFAULT_SNAPSHOT);
|
|
172
|
+
|
|
173
|
+
const printEntry = (entry) => {
|
|
174
|
+
if (json) {
|
|
175
|
+
console.log(JSON.stringify(entry));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log(formatRow(entry, verbose, extraFields));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// --saved: read local snapshot, no live streaming
|
|
182
|
+
if (saved) {
|
|
183
|
+
if (!fs.existsSync(savePath)) {
|
|
184
|
+
log.warn(`No snapshot found at: ${savePath}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let entries = [];
|
|
188
|
+
try {
|
|
189
|
+
entries = fs
|
|
190
|
+
.readFileSync(savePath, "utf8")
|
|
191
|
+
.split("\n")
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.map((l) => JSON.parse(l));
|
|
194
|
+
} catch (err) {
|
|
195
|
+
log.error("Failed to read snapshot: " + err.message);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (namespace)
|
|
199
|
+
entries = entries.filter(
|
|
200
|
+
(e) => e.moduleMethod && e.moduleMethod.includes(namespace),
|
|
201
|
+
);
|
|
202
|
+
if (andFilters.length || orFiltersParsed.length)
|
|
203
|
+
entries = entries.filter((e) => matchesFilters(e, andFilters, orFiltersParsed));
|
|
204
|
+
if (level) entries = entries.filter((e) => e.level === level);
|
|
205
|
+
entries = entries.slice(-limit);
|
|
206
|
+
console.log("");
|
|
207
|
+
console.log(chalk.bold(` Snapshot: ${savePath} (${entries.length} entries)`));
|
|
208
|
+
console.log("");
|
|
209
|
+
entries.forEach(printEntry);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let services = [];
|
|
214
|
+
let effectiveNamespace = namespace;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
|
|
218
|
+
|
|
219
|
+
if (!projectCode) {
|
|
220
|
+
const projects = await SystemView.getProjects();
|
|
221
|
+
services = Object.values(projects).flat();
|
|
222
|
+
} else {
|
|
223
|
+
const matches = await SystemView.getServices(projectCode);
|
|
224
|
+
if (matches && matches.length) {
|
|
225
|
+
services = matches;
|
|
226
|
+
} else {
|
|
227
|
+
// Fuzzy namespace: check if arg is a module/method name
|
|
228
|
+
const projects = await SystemView.getProjects();
|
|
229
|
+
const all = Object.values(projects).flat();
|
|
230
|
+
const isNamespace = all.some(({ system }) =>
|
|
231
|
+
(system.connectionData.modules || []).some(
|
|
232
|
+
({ name, methods }) =>
|
|
233
|
+
name.toLowerCase().includes(projectCode.toLowerCase()) ||
|
|
234
|
+
(methods || []).some(({ fn }) =>
|
|
235
|
+
fn.toLowerCase().includes(projectCode.toLowerCase()),
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
if (isNamespace) {
|
|
240
|
+
services = all;
|
|
241
|
+
effectiveNamespace = projectCode;
|
|
242
|
+
} else {
|
|
243
|
+
log.warn(`No services found for: ${projectCode}`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
log.error("Failed to connect to SystemView: " + err.message);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!services.length) {
|
|
254
|
+
log.warn("No services in store. Use: systemview connect <url>");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (clear) {
|
|
259
|
+
const confirmed = await promptConfirm("Clear all logs? (y/N) ");
|
|
260
|
+
if (!confirmed) {
|
|
261
|
+
log.warn("Aborted.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
for (const { system } of services) {
|
|
265
|
+
try {
|
|
266
|
+
const svc = await Client.loadService(system.connectionData.serviceUrl);
|
|
267
|
+
await svc.SystemView.clearLog();
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
log.success("Logs cleared.");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log("");
|
|
275
|
+
console.log(chalk.bold(" Streaming logs"));
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(chalk.dim(` Services (${services.length}):`));
|
|
278
|
+
|
|
279
|
+
const connected = [];
|
|
280
|
+
for (const { serviceId, system } of services) {
|
|
281
|
+
const { serviceUrl } = system.connectionData;
|
|
282
|
+
try {
|
|
283
|
+
const svc = await Client.loadService(serviceUrl);
|
|
284
|
+
const unsub = svc.SystemView.on("log", (entry) => {
|
|
285
|
+
if (
|
|
286
|
+
effectiveNamespace &&
|
|
287
|
+
!(entry.moduleMethod && entry.moduleMethod.includes(effectiveNamespace))
|
|
288
|
+
)
|
|
289
|
+
return;
|
|
290
|
+
if (andFilters.length || orFiltersParsed.length) {
|
|
291
|
+
if (!matchesFilters(entry, andFilters, orFiltersParsed)) return;
|
|
292
|
+
}
|
|
293
|
+
if (level && entry.level !== level) return;
|
|
294
|
+
if (save) appendSnapshot(savePath, entry, saveLimit);
|
|
295
|
+
printEntry(entry);
|
|
296
|
+
if (queryUrl && !json && entry.traceId) {
|
|
297
|
+
const sep = queryUrl.includes("?") ? "&" : "?";
|
|
298
|
+
console.log(` ${chalk.dim(queryUrl + sep + "traceId=" + encodeURIComponent(entry.traceId))}`);
|
|
299
|
+
}
|
|
300
|
+
console.log("");
|
|
301
|
+
});
|
|
302
|
+
console.log(
|
|
303
|
+
` ${chalk.green("✓")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)}`,
|
|
304
|
+
);
|
|
305
|
+
connected.push({ serviceId, svc, unsub });
|
|
306
|
+
} catch {
|
|
307
|
+
console.log(
|
|
308
|
+
` ${chalk.red("✗")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)} ${chalk.red("(failed to connect)")}`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (save) {
|
|
314
|
+
console.log(chalk.dim(` Snapshot: ${savePath} (limit ${saveLimit})`));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const activeFilters = [];
|
|
318
|
+
if (effectiveNamespace)
|
|
319
|
+
activeFilters.push(`namespace: ${chalk.white(effectiveNamespace)}`);
|
|
320
|
+
if (level) activeFilters.push(`level: ${chalk.white(level)}`);
|
|
321
|
+
if (andFilters.length)
|
|
322
|
+
activeFilters.push(
|
|
323
|
+
`filter: ${chalk.white(andFilters.map((f) => `${f.field}=${f.value}`).join(", "))}`,
|
|
324
|
+
);
|
|
325
|
+
if (orFiltersParsed.length)
|
|
326
|
+
activeFilters.push(
|
|
327
|
+
`or: ${chalk.white(orFiltersParsed.map((f) => `${f.field}=${f.value}`).join(", "))}`,
|
|
328
|
+
);
|
|
329
|
+
if (extraFields.length)
|
|
330
|
+
activeFilters.push(`include: ${chalk.white(extraFields.join(", "))}`);
|
|
331
|
+
if (verbose) activeFilters.push(`verbose: ${chalk.white("on")}`);
|
|
332
|
+
const queryUrl = uiUrl ? buildLogsUrl(uiUrl, { projectCode, effectiveNamespace, level, andFilters, orFilters }) : null;
|
|
333
|
+
|
|
334
|
+
if (activeFilters.length) {
|
|
335
|
+
console.log("");
|
|
336
|
+
console.log(chalk.dim(` Filters:`));
|
|
337
|
+
activeFilters.forEach((f) => console.log(` ${chalk.dim(f)}`));
|
|
338
|
+
}
|
|
339
|
+
if (queryUrl && !json) {
|
|
340
|
+
console.log("");
|
|
341
|
+
console.log(` ${chalk.dim("UI:")} ${chalk.cyan(queryUrl)}`);
|
|
342
|
+
}
|
|
343
|
+
console.log("");
|
|
344
|
+
|
|
345
|
+
if (!connected.length) {
|
|
346
|
+
log.warn("No services connected.");
|
|
347
|
+
return () => {};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (current) {
|
|
351
|
+
const allEntries = [];
|
|
352
|
+
for (const { svc } of connected) {
|
|
353
|
+
try {
|
|
354
|
+
let entries = await svc.SystemView.getLog({ limit });
|
|
355
|
+
entries = entries || [];
|
|
356
|
+
if (effectiveNamespace)
|
|
357
|
+
entries = entries.filter(
|
|
358
|
+
(e) => e.moduleMethod && e.moduleMethod.includes(effectiveNamespace),
|
|
359
|
+
);
|
|
360
|
+
if (andFilters.length || orFiltersParsed.length)
|
|
361
|
+
entries = entries.filter((e) => matchesFilters(e, andFilters, orFiltersParsed));
|
|
362
|
+
if (level) entries = entries.filter((e) => e.level === level);
|
|
363
|
+
allEntries.push(...entries);
|
|
364
|
+
} catch {}
|
|
365
|
+
}
|
|
366
|
+
if (allEntries.length) {
|
|
367
|
+
console.log(chalk.dim(` ── current (${allEntries.length}) ──`));
|
|
368
|
+
allEntries.forEach((entry) => {
|
|
369
|
+
printEntry(entry);
|
|
370
|
+
if (queryUrl && !json && entry.traceId) {
|
|
371
|
+
const sep = queryUrl.includes("?") ? "&" : "?";
|
|
372
|
+
console.log(` ${chalk.dim(queryUrl + sep + "traceId=" + encodeURIComponent(entry.traceId))}`);
|
|
373
|
+
}
|
|
374
|
+
console.log("");
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!follow) {
|
|
379
|
+
connected.forEach(({ unsub }) => unsub());
|
|
380
|
+
return () => {};
|
|
381
|
+
}
|
|
382
|
+
console.log(chalk.dim(` ── streaming ──`));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return () => {
|
|
386
|
+
connected.forEach(({ unsub }) => unsub());
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
function buildLogsUrl(uiUrl, { projectCode, effectiveNamespace, level, andFilters, orFilters }) {
|
|
391
|
+
const params = new URLSearchParams();
|
|
392
|
+
if (projectCode && effectiveNamespace !== projectCode) params.set("project", projectCode);
|
|
393
|
+
if (effectiveNamespace) params.set("method", effectiveNamespace);
|
|
394
|
+
if (level) params.set("level", level);
|
|
395
|
+
for (const f of andFilters || []) {
|
|
396
|
+
if (f.field === "has") params.append("has", f.value);
|
|
397
|
+
else if (f.field === "missing") params.append("missing", f.value);
|
|
398
|
+
else params.set(f.field, f.value);
|
|
399
|
+
}
|
|
400
|
+
const q = params.toString();
|
|
401
|
+
return `${uiUrl}/logs${q ? "?" + q : ""}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function promptConfirm(question) {
|
|
405
|
+
return new Promise((resolve) => {
|
|
406
|
+
process.stdout.write(question);
|
|
407
|
+
process.stdin.setEncoding("utf8");
|
|
408
|
+
process.stdin.once("data", (data) => {
|
|
409
|
+
resolve(data.trim().toLowerCase() === "y");
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
package/cli/manifest.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { HttpClient, createClient } = require("systemlynx");
|
|
4
|
+
const { createCookieHttpClient } = require("./cookieClient");
|
|
5
|
+
const cookieHttpClient = createCookieHttpClient();
|
|
6
|
+
const Client = createClient(cookieHttpClient);
|
|
7
|
+
const log = require("./logger");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MANIFEST = path.join(process.cwd(), "systemview.manifest.json");
|
|
10
|
+
|
|
11
|
+
async function getUiSvc(uiUrl) {
|
|
12
|
+
try {
|
|
13
|
+
const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
|
|
14
|
+
return SystemView;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports.save = function saveManifest(manifestServices, manifestFile = DEFAULT_MANIFEST, probeHeaders = {}) {
|
|
21
|
+
if (!manifestServices || !manifestServices.length) {
|
|
22
|
+
log.warn("No services in session to save.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const byProject = {};
|
|
26
|
+
for (const { projectCode, serviceId, system, specList } of manifestServices) {
|
|
27
|
+
if (!byProject[projectCode]) byProject[projectCode] = [];
|
|
28
|
+
byProject[projectCode].push({ serviceId, system, specList });
|
|
29
|
+
}
|
|
30
|
+
const projectCodes = Object.keys(byProject);
|
|
31
|
+
if (projectCodes.length > 1) {
|
|
32
|
+
log.warn("Multiple project codes in session — saving first project only: " + projectCodes[0]);
|
|
33
|
+
}
|
|
34
|
+
const projectCode = projectCodes[0];
|
|
35
|
+
const manifest = {
|
|
36
|
+
projectCode,
|
|
37
|
+
services: byProject[projectCode],
|
|
38
|
+
...(Object.keys(probeHeaders).length && { probeHeaders }),
|
|
39
|
+
};
|
|
40
|
+
fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2));
|
|
41
|
+
log.success(`Manifest saved to ${manifestFile} (${manifest.services.length} service(s))`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
module.exports.clean = async function cleanManifest(manifestFile = DEFAULT_MANIFEST) {
|
|
45
|
+
if (!fs.existsSync(manifestFile)) {
|
|
46
|
+
log.warn("No manifest file found at " + manifestFile);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let manifest;
|
|
50
|
+
try {
|
|
51
|
+
manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
|
|
52
|
+
} catch {
|
|
53
|
+
log.error("Failed to parse manifest.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const services = manifest.services || [manifest];
|
|
57
|
+
log.info(`Re-probing ${services.length} service(s)...`);
|
|
58
|
+
const alive = [];
|
|
59
|
+
for (const entry of services) {
|
|
60
|
+
const url = entry.system && entry.system.connectionData && entry.system.connectionData.serviceUrl;
|
|
61
|
+
if (!url) continue;
|
|
62
|
+
try {
|
|
63
|
+
await HttpClient.request({ url });
|
|
64
|
+
alive.push(entry);
|
|
65
|
+
log.success(`alive: ${entry.serviceId}`);
|
|
66
|
+
} catch {
|
|
67
|
+
log.warn(`stale: ${entry.serviceId} (removed)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
manifest.services = alive;
|
|
71
|
+
fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2));
|
|
72
|
+
log.success(`Manifest cleaned. ${alive.length} service(s) remaining.`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports.disconnect = async function disconnect(projectCode, serviceId, { connectedUrls, uiUrl } = {}) {
|
|
76
|
+
if (!projectCode) {
|
|
77
|
+
log.warn("Usage: systemview disconnect <projectCode> [serviceId]");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const uiSvc = await getUiSvc(uiUrl);
|
|
81
|
+
if (serviceId) {
|
|
82
|
+
if (uiSvc) {
|
|
83
|
+
try { await uiSvc.deleteService(projectCode, serviceId); } catch {}
|
|
84
|
+
}
|
|
85
|
+
if (connectedUrls) {
|
|
86
|
+
for (const url of connectedUrls) {
|
|
87
|
+
if (url.includes(serviceId)) connectedUrls.delete(url);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
log.success(`Disconnected: ${projectCode} › ${serviceId}`);
|
|
91
|
+
} else {
|
|
92
|
+
if (uiSvc) {
|
|
93
|
+
try { await uiSvc.deleteProject(projectCode); } catch {}
|
|
94
|
+
}
|
|
95
|
+
log.success(`Disconnected project: ${projectCode}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
package/cli/openBrowser.js
CHANGED
|
@@ -8,7 +8,7 @@ module.exports = function openBrowser(url, project_code, namespace, connectedSer
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const browserCommand = process.platform === "win32" ? "start" : "open";
|
|
11
|
-
const code = project_code ? "/" + project_code : "";
|
|
11
|
+
const code = project_code ? "/specs/" + project_code : "/specs";
|
|
12
12
|
|
|
13
13
|
if (namespace && connectedServices && connectedServices.length) {
|
|
14
14
|
const matches = resolveNamespace(namespace, connectedServices);
|
package/cli/probe.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const {
|
|
3
|
+
const { createClient } = require("systemlynx");
|
|
4
|
+
const { createCookieHttpClient } = require("./cookieClient");
|
|
4
5
|
const log = require("./logger");
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const cookieHttpClient = createCookieHttpClient();
|
|
8
|
+
const Client = createClient(cookieHttpClient);
|
|
9
|
+
|
|
10
|
+
module.exports = async function probe(namespace, argsStr, { json = false, manifest: manifestPath, headers: cliHeaders = {}, uiUrl } = {}) {
|
|
7
11
|
if (!namespace) {
|
|
8
12
|
log.error("Usage: systemview probe <ServiceId.Module.method> [args]");
|
|
9
13
|
return 1;
|
|
@@ -27,32 +31,45 @@ module.exports = async function probe(namespace, argsStr, { json = false, manife
|
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
if (!fs.existsSync(manifestFile)) {
|
|
32
|
-
log.error(`No manifest found at ${manifestFile}. Run: systemview connect <serviceId> <url>`);
|
|
33
|
-
return 1;
|
|
34
|
-
}
|
|
34
|
+
let service = null;
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
// Try UI server first
|
|
37
|
+
if (uiUrl) {
|
|
38
|
+
try {
|
|
39
|
+
const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
|
|
40
|
+
const projects = await SystemView.getProjects();
|
|
41
|
+
const all = Object.values(projects).flat();
|
|
42
|
+
service = all.find((s) => s.serviceId === serviceId);
|
|
43
|
+
} catch {}
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
const service = services.find((s) => s.serviceId === serviceId);
|
|
46
|
+
// Fall back to manifest file
|
|
46
47
|
if (!service) {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const manifestFile = manifestPath || path.join(process.cwd(), "systemview.manifest.json");
|
|
49
|
+
if (!fs.existsSync(manifestFile)) {
|
|
50
|
+
log.error(`Service "${serviceId}" not found. Connect it first with: systemview connect <url>`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
let manifest;
|
|
54
|
+
try {
|
|
55
|
+
manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
log.error(`Failed to read manifest: ${err.message}`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const services = manifest.services || [manifest];
|
|
61
|
+
service = services.find((s) => s.serviceId === serviceId);
|
|
62
|
+
if (!service) {
|
|
63
|
+
log.error(`Service "${serviceId}" not found in manifest`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
const extraHeaders = { ...(manifest.probeHeaders || {}), ...cliHeaders };
|
|
52
68
|
if (!json) log.info(`${serviceId}.${moduleName}.${methodName}(${argsStr || ""})`);
|
|
53
69
|
|
|
54
70
|
try {
|
|
55
|
-
const client =
|
|
71
|
+
const client = Client.createService(service.system.connectionData);
|
|
72
|
+
if (Object.keys(cliHeaders).length) client.setHeaders(cliHeaders);
|
|
56
73
|
const result = await client[moduleName][methodName](...args);
|
|
57
74
|
if (json) {
|
|
58
75
|
process.stdout.write(JSON.stringify({ serviceId, moduleName, methodName, args, result }, null, 2) + "\n");
|