systemview 2.0.0 → 2.2.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 +41 -47
- package/build/asset-manifest.json +6 -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/{main.72103448.js → main.c7a05ef5.js} +3 -3
- package/build/static/js/main.c7a05ef5.js.map +1 -0
- package/cli/connectService.js +143 -52
- package/cli/index.js +80 -45
- package/cli/launchApp.js +8 -5
- package/cli/listTests.js +31 -16
- package/cli/logs.js +288 -95
- package/cli/manifest.js +97 -0
- package/cli/openBrowser.js +1 -1
- package/cli/probe.js +30 -18
- package/cli/runTests.js +30 -36
- package/cli/startLineReader.js +140 -15
- package/cli/utils/cli.js +64 -17
- package/package.json +2 -2
- package/build/static/css/main.ff3d0549.css +0 -15
- package/build/static/css/main.ff3d0549.css.map +0 -1
- package/build/static/js/main.72103448.js.map +0 -1
- /package/build/static/js/{main.72103448.js.LICENSE.txt → main.c7a05ef5.js.LICENSE.txt} +0 -0
package/cli/logs.js
CHANGED
|
@@ -7,9 +7,11 @@ const { createCookieHttpClient } = require("./cookieClient");
|
|
|
7
7
|
const cookieHttpClient = createCookieHttpClient();
|
|
8
8
|
const Client = createClient(cookieHttpClient);
|
|
9
9
|
|
|
10
|
+
const DEFAULT_SNAPSHOT = "systemview-snapshot.ndjson";
|
|
11
|
+
|
|
10
12
|
const LEVEL_COLOR = {
|
|
11
13
|
trace: (s) => chalk.dim(s),
|
|
12
|
-
|
|
14
|
+
log: (s) => chalk.white(s),
|
|
13
15
|
warn: (s) => chalk.yellow(s),
|
|
14
16
|
error: (s) => chalk.red(s),
|
|
15
17
|
debug: (s) => chalk.blue(s),
|
|
@@ -17,21 +19,23 @@ const LEVEL_COLOR = {
|
|
|
17
19
|
|
|
18
20
|
function colorLevel(level) {
|
|
19
21
|
const fn = LEVEL_COLOR[level] || ((s) => s);
|
|
20
|
-
return fn((level || "
|
|
22
|
+
return fn((level || "log").padEnd(5));
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const SKIP_KEYS = new Set([
|
|
24
26
|
"timestamp",
|
|
25
27
|
"projectCode",
|
|
26
28
|
"serviceId",
|
|
29
|
+
"module",
|
|
30
|
+
"method",
|
|
27
31
|
"moduleMethod",
|
|
28
32
|
"traceId",
|
|
29
33
|
"level",
|
|
30
|
-
"
|
|
34
|
+
"scope",
|
|
31
35
|
"duration",
|
|
32
36
|
]);
|
|
33
37
|
|
|
34
|
-
function formatRow(entry, verbose) {
|
|
38
|
+
function formatRow(entry, verbose, extraFields) {
|
|
35
39
|
const d = new Date(entry.timestamp);
|
|
36
40
|
const time =
|
|
37
41
|
d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) +
|
|
@@ -41,10 +45,16 @@ function formatRow(entry, verbose) {
|
|
|
41
45
|
const service = entry.serviceId || "—";
|
|
42
46
|
const method = entry.moduleMethod || "—";
|
|
43
47
|
const level = colorLevel(entry.level);
|
|
44
|
-
const msg = entry.
|
|
48
|
+
const msg = entry.scope || "";
|
|
45
49
|
const dur = entry.duration != null ? chalk.dim(` ${entry.duration}ms`) : "";
|
|
46
50
|
const traceId = chalk.dim(entry.traceId || "—");
|
|
47
|
-
|
|
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
|
+
}
|
|
48
58
|
if (verbose) {
|
|
49
59
|
const extra = {};
|
|
50
60
|
Object.keys(entry).forEach((k) => {
|
|
@@ -52,7 +62,7 @@ function formatRow(entry, verbose) {
|
|
|
52
62
|
});
|
|
53
63
|
if (Object.keys(extra).length) {
|
|
54
64
|
return (
|
|
55
|
-
|
|
65
|
+
row +
|
|
56
66
|
"\n" +
|
|
57
67
|
JSON.stringify(extra, null, 2)
|
|
58
68
|
.split("\n")
|
|
@@ -61,7 +71,17 @@ function formatRow(entry, verbose) {
|
|
|
61
71
|
);
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
|
-
return
|
|
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);
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
function parseFilters(filters) {
|
|
@@ -76,7 +96,9 @@ function parseFilters(filters) {
|
|
|
76
96
|
|
|
77
97
|
function matchesFilters(entry, andFilters, orFilters) {
|
|
78
98
|
const check = ({ field, value }) => {
|
|
79
|
-
|
|
99
|
+
if (field === "has") return getAtPath(entry, value) != null;
|
|
100
|
+
if (field === "missing") return getAtPath(entry, value) == null;
|
|
101
|
+
const v = getAtPath(entry, field);
|
|
80
102
|
return v != null && String(v).includes(value);
|
|
81
103
|
};
|
|
82
104
|
const andPass = andFilters.length === 0 || andFilters.every(check);
|
|
@@ -85,129 +107,300 @@ function matchesFilters(entry, andFilters, orFilters) {
|
|
|
85
107
|
return andPass && orPass;
|
|
86
108
|
}
|
|
87
109
|
|
|
110
|
+
const DISPLAY_SKIP = new Set([
|
|
111
|
+
"timestamp",
|
|
112
|
+
"projectCode",
|
|
113
|
+
"serviceId",
|
|
114
|
+
"module",
|
|
115
|
+
"method",
|
|
116
|
+
"moduleMethod",
|
|
117
|
+
"traceId",
|
|
118
|
+
"level",
|
|
119
|
+
"scope",
|
|
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
|
+
|
|
88
140
|
module.exports = async function logsCommand(
|
|
89
|
-
url,
|
|
90
141
|
projectCode,
|
|
91
142
|
namespace,
|
|
92
|
-
{
|
|
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
|
+
},
|
|
93
159
|
) {
|
|
94
160
|
const andFilters = parseFilters(filter);
|
|
95
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);
|
|
96
172
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
log.warn("No manifest found. Use: systemview connect <serviceId> <url>");
|
|
173
|
+
const printEntry = (entry) => {
|
|
174
|
+
if (json) {
|
|
175
|
+
console.log(JSON.stringify(entry));
|
|
101
176
|
return;
|
|
102
177
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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}`);
|
|
107
185
|
return;
|
|
108
186
|
}
|
|
109
|
-
|
|
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);
|
|
110
206
|
console.log("");
|
|
111
|
-
console.log(chalk.bold(
|
|
207
|
+
console.log(chalk.bold(` Snapshot: ${savePath} (${entries.length} entries)`));
|
|
112
208
|
console.log("");
|
|
209
|
+
entries.forEach(printEntry);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
113
212
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
for (const { serviceId, system } of services) {
|
|
117
|
-
const { serviceUrl } = system.connectionData;
|
|
118
|
-
try {
|
|
119
|
-
const svc = await Client.loadService(serviceUrl);
|
|
120
|
-
svc.SystemViewLogs.on("log", (entry) => {
|
|
121
|
-
if (namespace && !(entry.moduleMethod && entry.moduleMethod.includes(namespace))) return;
|
|
122
|
-
if (andFilters.length || orFiltersParsed.length) {
|
|
123
|
-
if (!matchesFilters(entry, andFilters, orFiltersParsed)) return;
|
|
124
|
-
}
|
|
125
|
-
if (level && entry.level !== level) return;
|
|
126
|
-
console.log(formatRow(entry, verbose));
|
|
127
|
-
});
|
|
128
|
-
console.log(` ${chalk.green("✓")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)}`);
|
|
129
|
-
connected.push(serviceId);
|
|
130
|
-
} catch (err) {
|
|
131
|
-
console.log(` ${chalk.red("✗")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)} ${chalk.red("(failed to connect)")}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const activeFilters = [];
|
|
136
|
-
if (namespace) activeFilters.push(`namespace: ${chalk.white(namespace)}`);
|
|
137
|
-
if (level) activeFilters.push(`level: ${chalk.white(level)}`);
|
|
138
|
-
if (andFilters.length) activeFilters.push(`filter: ${chalk.white(andFilters.map((f) => `${f.field}=${f.value}`).join(", "))}`);
|
|
139
|
-
if (orFiltersParsed.length) activeFilters.push(`or: ${chalk.white(orFiltersParsed.map((f) => `${f.field}=${f.value}`).join(", "))}`);
|
|
140
|
-
if (verbose) activeFilters.push(`verbose: ${chalk.white("on")}`);
|
|
141
|
-
|
|
142
|
-
if (activeFilters.length) {
|
|
143
|
-
console.log("");
|
|
144
|
-
console.log(chalk.dim(` Filters:`));
|
|
145
|
-
activeFilters.forEach((f) => console.log(` ${chalk.dim(f)}`));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
console.log("");
|
|
213
|
+
let services = [];
|
|
214
|
+
let effectiveNamespace = namespace;
|
|
149
215
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
216
|
+
try {
|
|
217
|
+
const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
|
|
154
218
|
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
245
|
}
|
|
168
|
-
} catch (err) {
|
|
169
|
-
log.warn("Could not fetch current logs: " + err.message);
|
|
170
246
|
}
|
|
171
247
|
}
|
|
172
|
-
|
|
248
|
+
} catch (err) {
|
|
249
|
+
log.error("Failed to connect to SystemView: " + err.message);
|
|
173
250
|
return;
|
|
174
251
|
}
|
|
175
252
|
|
|
176
|
-
|
|
177
|
-
|
|
253
|
+
if (!services.length) {
|
|
254
|
+
log.warn("No services in store. Use: systemview connect <url>");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
178
257
|
|
|
179
258
|
if (clear) {
|
|
180
259
|
const confirmed = await promptConfirm("Clear all logs? (y/N) ");
|
|
181
|
-
if (!confirmed) {
|
|
182
|
-
|
|
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
|
+
}
|
|
183
270
|
log.success("Logs cleared.");
|
|
184
271
|
return;
|
|
185
272
|
}
|
|
186
273
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (andFilters.length || orFiltersParsed.length) filtered = filtered.filter((e) => matchesFilters(e, andFilters, orFiltersParsed));
|
|
192
|
-
return filtered;
|
|
193
|
-
})();
|
|
274
|
+
console.log("");
|
|
275
|
+
console.log(chalk.bold(" Streaming logs"));
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(chalk.dim(` Services (${services.length}):`));
|
|
194
278
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
311
|
}
|
|
199
312
|
|
|
200
|
-
if (
|
|
201
|
-
log.
|
|
202
|
-
return;
|
|
313
|
+
if (save) {
|
|
314
|
+
console.log(chalk.dim(` Snapshot: ${savePath} (limit ${saveLimit})`));
|
|
203
315
|
}
|
|
204
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
|
+
}
|
|
205
343
|
console.log("");
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
};
|
|
209
388
|
};
|
|
210
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
|
+
|
|
211
404
|
function promptConfirm(question) {
|
|
212
405
|
return new Promise((resolve) => {
|
|
213
406
|
process.stdout.write(question);
|
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
|
@@ -7,7 +7,7 @@ const log = require("./logger");
|
|
|
7
7
|
const cookieHttpClient = createCookieHttpClient();
|
|
8
8
|
const Client = createClient(cookieHttpClient);
|
|
9
9
|
|
|
10
|
-
module.exports = async function probe(namespace, argsStr, { json = false, manifest: manifestPath, headers: cliHeaders = {} } = {}) {
|
|
10
|
+
module.exports = async function probe(namespace, argsStr, { json = false, manifest: manifestPath, headers: cliHeaders = {}, uiUrl } = {}) {
|
|
11
11
|
if (!namespace) {
|
|
12
12
|
log.error("Usage: systemview probe <ServiceId.Module.method> [args]");
|
|
13
13
|
return 1;
|
|
@@ -31,33 +31,45 @@ module.exports = async function probe(namespace, argsStr, { json = false, manife
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
if (!fs.existsSync(manifestFile)) {
|
|
36
|
-
log.error(`No manifest found at ${manifestFile}. Run: systemview connect <serviceId> <url>`);
|
|
37
|
-
return 1;
|
|
38
|
-
}
|
|
34
|
+
let service = null;
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 {}
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
const service = services.find((s) => s.serviceId === serviceId);
|
|
46
|
+
// Fall back to manifest file
|
|
50
47
|
if (!service) {
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
|
53
66
|
}
|
|
54
67
|
|
|
55
|
-
const extraHeaders = { ...(manifest.probeHeaders || {}), ...cliHeaders };
|
|
56
68
|
if (!json) log.info(`${serviceId}.${moduleName}.${methodName}(${argsStr || ""})`);
|
|
57
69
|
|
|
58
70
|
try {
|
|
59
71
|
const client = Client.createService(service.system.connectionData);
|
|
60
|
-
if (Object.keys(
|
|
72
|
+
if (Object.keys(cliHeaders).length) client.setHeaders(cliHeaders);
|
|
61
73
|
const result = await client[moduleName][methodName](...args);
|
|
62
74
|
if (json) {
|
|
63
75
|
process.stdout.write(JSON.stringify({ serviceId, moduleName, methodName, args, result }, null, 2) + "\n");
|