systemview 1.21.0 → 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/api/CLIHistory.js +29 -0
- package/api/cli-history.json +1 -0
- package/api/connections.json +1 -1
- package/api/index.js +5 -0
- package/build/asset-manifest.json +6 -6
- package/build/index.html +1 -1
- package/build/static/css/{main.8e85f0c8.css → main.1916c1d1.css} +2 -2
- package/build/static/css/main.1916c1d1.css.map +1 -0
- package/build/static/js/{main.6240c0f1.js → main.e9f9f4f8.js} +3 -3
- package/build/static/js/main.e9f9f4f8.js.map +1 -0
- package/cli/index.js +17 -16
- package/cli/launchApp.js +4 -1
- package/cli/listTests.js +25 -3
- package/cli/logs.js +184 -23
- package/cli/openBrowser.js +1 -1
- package/cli/startLineReader.js +40 -4
- package/cli/utils/cli.js +34 -8
- package/package.json +1 -1
- package/build/static/css/main.8e85f0c8.css.map +0 -1
- package/build/static/js/main.6240c0f1.js.map +0 -1
- /package/build/static/js/{main.6240c0f1.js.LICENSE.txt → main.e9f9f4f8.js.LICENSE.txt} +0 -0
package/cli/index.js
CHANGED
|
@@ -16,7 +16,7 @@ const connectService = require("./connectService");
|
|
|
16
16
|
const manifestCommands = require("./manifest");
|
|
17
17
|
const probe = require("./probe");
|
|
18
18
|
const logsCommand = require("./logs");
|
|
19
|
-
const {
|
|
19
|
+
const { createClient } = require("systemlynx");
|
|
20
20
|
const { createCookieHttpClient } = require("./cookieClient");
|
|
21
21
|
const cookieHttpClient = createCookieHttpClient();
|
|
22
22
|
const Client = createClient(cookieHttpClient);
|
|
@@ -45,7 +45,7 @@ function loadManifest() {
|
|
|
45
45
|
async function startApp() {
|
|
46
46
|
const port = isNaN(input[1]) ? DEFAULT_PORT : Number(input[1]);
|
|
47
47
|
try {
|
|
48
|
-
await launchApp(port, { interactive: true, connectedUrls });
|
|
48
|
+
await launchApp(port, { interactive: true, connectedUrls, client: Client });
|
|
49
49
|
} catch (error) {
|
|
50
50
|
log.error("Launch failed: " + error.message);
|
|
51
51
|
}
|
|
@@ -83,6 +83,7 @@ async function list() {
|
|
|
83
83
|
await listTests(UI_URL, project_code, namespace, {
|
|
84
84
|
connectedUrls,
|
|
85
85
|
verbose: flags.verbose,
|
|
86
|
+
json: flags.json,
|
|
86
87
|
});
|
|
87
88
|
process.exit(0);
|
|
88
89
|
}
|
|
@@ -113,24 +114,20 @@ async function quitApp() {
|
|
|
113
114
|
|
|
114
115
|
if (await appIsRunning(api)) {
|
|
115
116
|
log.info("Attempting remote shutdown...");
|
|
116
|
-
const url = `${api}/SystemView/shutdown`;
|
|
117
117
|
try {
|
|
118
|
-
|
|
118
|
+
const { SystemView } = await Client.loadService(api);
|
|
119
|
+
await SystemView.shutdown();
|
|
120
|
+
} catch {}
|
|
121
|
+
if (!(await appIsRunning(api))) {
|
|
119
122
|
log.success("SystemView shutdown successful!");
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
log.success("SystemView shutdown successful!");
|
|
124
|
-
process.exit(0);
|
|
125
|
-
} else {
|
|
126
|
-
log.error("Remote shutdown failed!");
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
123
|
+
} else {
|
|
124
|
+
log.error("Remote shutdown failed!");
|
|
125
|
+
process.exit(1);
|
|
129
126
|
}
|
|
130
127
|
} else {
|
|
131
128
|
log.warn(`No SystemView instance found @ ${api}`);
|
|
132
|
-
process.exit(0);
|
|
133
129
|
}
|
|
130
|
+
process.exit(0);
|
|
134
131
|
}
|
|
135
132
|
|
|
136
133
|
(async () => {
|
|
@@ -188,7 +185,7 @@ async function quitApp() {
|
|
|
188
185
|
uiUrl: UI_URL,
|
|
189
186
|
});
|
|
190
187
|
process.exit(exitCode || 0);
|
|
191
|
-
} else if (command === "logs") {
|
|
188
|
+
} else if (command === "logs" || command === "log") {
|
|
192
189
|
await launchApp(DEFAULT_PORT);
|
|
193
190
|
await logsCommand(input[1], input[2], {
|
|
194
191
|
uiUrl: UI_URL,
|
|
@@ -197,12 +194,16 @@ async function quitApp() {
|
|
|
197
194
|
clear: flags.clear,
|
|
198
195
|
json: flags.json,
|
|
199
196
|
current: flags.current,
|
|
197
|
+
follow: flags.follow,
|
|
200
198
|
verbose: flags.verbose,
|
|
201
199
|
filter: flags.filter,
|
|
202
200
|
or: flags.or,
|
|
203
201
|
include: flags.include,
|
|
202
|
+
save: flags.save,
|
|
203
|
+
saved: flags.saved,
|
|
204
|
+
saveLimit: flags.saveLimit,
|
|
204
205
|
});
|
|
205
|
-
process.exit(0);
|
|
206
|
+
if (!flags.follow) process.exit(0);
|
|
206
207
|
} else {
|
|
207
208
|
await startApp();
|
|
208
209
|
}
|
package/cli/launchApp.js
CHANGED
|
@@ -2,6 +2,7 @@ const log = require("./logger");
|
|
|
2
2
|
const appIsRunning = require("./appIsRunning");
|
|
3
3
|
const launchSystemView = require("../api");
|
|
4
4
|
const startLineReader = require("./startLineReader");
|
|
5
|
+
const listTests = require("./listTests");
|
|
5
6
|
|
|
6
7
|
module.exports = async function launchApp(port, { interactive = false, connectedUrls = new Set() } = {}) {
|
|
7
8
|
const ui = `http://localhost:${port}`;
|
|
@@ -15,8 +16,9 @@ module.exports = async function launchApp(port, { interactive = false, connected
|
|
|
15
16
|
|
|
16
17
|
if (await appIsRunning(api)) {
|
|
17
18
|
if (interactive) {
|
|
18
|
-
log.
|
|
19
|
+
log.warn(`SystemView already running on port ${port} — attaching.`);
|
|
19
20
|
logConnection();
|
|
21
|
+
await listTests(ui, null, null, { connectedUrls });
|
|
20
22
|
return startLineReader(ui, { connectedUrls });
|
|
21
23
|
}
|
|
22
24
|
return;
|
|
@@ -27,6 +29,7 @@ module.exports = async function launchApp(port, { interactive = false, connected
|
|
|
27
29
|
logConnection();
|
|
28
30
|
|
|
29
31
|
if (interactive) {
|
|
32
|
+
await listTests(ui, null, null, { connectedUrls });
|
|
30
33
|
return startLineReader(ui, { connectedUrls });
|
|
31
34
|
}
|
|
32
35
|
};
|
package/cli/listTests.js
CHANGED
|
@@ -6,11 +6,11 @@ const chalk = require("chalk");
|
|
|
6
6
|
const cookieHttpClient = createCookieHttpClient();
|
|
7
7
|
const Client = createClient(cookieHttpClient);
|
|
8
8
|
|
|
9
|
-
module.exports = async function listTests(url, project_code, namespace, { connectedUrls = new Set(), verbose = false } = {}) {
|
|
9
|
+
module.exports = async function listTests(url, project_code, namespace, { connectedUrls = new Set(), verbose = false, json = false } = {}) {
|
|
10
10
|
const api = `${url}/systemview/api`;
|
|
11
11
|
|
|
12
12
|
if (!project_code) {
|
|
13
|
-
await listAllProjects(api, Client, verbose, connectedUrls);
|
|
13
|
+
await listAllProjects(api, Client, verbose, connectedUrls, json);
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -20,6 +20,25 @@ module.exports = async function listTests(url, project_code, namespace, { connec
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
if (json) {
|
|
24
|
+
const output = [];
|
|
25
|
+
for (const { serviceId, system } of connectedServices) {
|
|
26
|
+
let testList = [];
|
|
27
|
+
try {
|
|
28
|
+
const svc = Client.createService(system.connectionData);
|
|
29
|
+
testList = await svc.Plugin.getTests();
|
|
30
|
+
} catch { continue; }
|
|
31
|
+
const filtered = namespace
|
|
32
|
+
? testList.filter(({ namespace: n }) =>
|
|
33
|
+
`${n.serviceId}.${n.moduleName}.${n.methodName}`.includes(namespace)
|
|
34
|
+
)
|
|
35
|
+
: testList;
|
|
36
|
+
output.push({ serviceId, tests: filtered });
|
|
37
|
+
}
|
|
38
|
+
console.log(JSON.stringify(output, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
console.log("");
|
|
24
43
|
console.log(` ${chalk.bold(project_code)}`);
|
|
25
44
|
|
|
@@ -94,7 +113,7 @@ module.exports = async function listTests(url, project_code, namespace, { connec
|
|
|
94
113
|
console.log("");
|
|
95
114
|
};
|
|
96
115
|
|
|
97
|
-
async function listAllProjects(api, Client, verbose = false, connectedUrls = new Set()) {
|
|
116
|
+
async function listAllProjects(api, Client, verbose = false, connectedUrls = new Set(), json = false) {
|
|
98
117
|
let projects;
|
|
99
118
|
try {
|
|
100
119
|
const { SystemView } = await Client.loadService(api);
|
|
@@ -106,10 +125,13 @@ async function listAllProjects(api, Client, verbose = false, connectedUrls = new
|
|
|
106
125
|
|
|
107
126
|
const codes = Object.keys(projects);
|
|
108
127
|
if (!codes.length) {
|
|
128
|
+
if (json) { console.log(JSON.stringify({})); return; }
|
|
109
129
|
log.warn("No connected projects found.");
|
|
110
130
|
return;
|
|
111
131
|
}
|
|
112
132
|
|
|
133
|
+
if (json) { console.log(JSON.stringify(projects, null, 2)); return; }
|
|
134
|
+
|
|
113
135
|
const total = codes.reduce((n, c) => n + projects[c].length, 0);
|
|
114
136
|
console.log("");
|
|
115
137
|
console.log(chalk.dim(` ${codes.length} ${codes.length === 1 ? "project" : "projects"}, ${total} ${total === 1 ? "service" : "services"}`));
|
package/cli/logs.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
1
3
|
const chalk = require("chalk");
|
|
2
4
|
const log = require("./logger");
|
|
3
5
|
const { createClient } = require("systemlynx");
|
|
@@ -5,9 +7,11 @@ const { createCookieHttpClient } = require("./cookieClient");
|
|
|
5
7
|
const cookieHttpClient = createCookieHttpClient();
|
|
6
8
|
const Client = createClient(cookieHttpClient);
|
|
7
9
|
|
|
10
|
+
const DEFAULT_SNAPSHOT = "systemview-snapshot.ndjson";
|
|
11
|
+
|
|
8
12
|
const LEVEL_COLOR = {
|
|
9
13
|
trace: (s) => chalk.dim(s),
|
|
10
|
-
|
|
14
|
+
log: (s) => chalk.white(s),
|
|
11
15
|
warn: (s) => chalk.yellow(s),
|
|
12
16
|
error: (s) => chalk.red(s),
|
|
13
17
|
debug: (s) => chalk.blue(s),
|
|
@@ -15,13 +19,15 @@ const LEVEL_COLOR = {
|
|
|
15
19
|
|
|
16
20
|
function colorLevel(level) {
|
|
17
21
|
const fn = LEVEL_COLOR[level] || ((s) => s);
|
|
18
|
-
return fn((level || "
|
|
22
|
+
return fn((level || "log").padEnd(5));
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
const SKIP_KEYS = new Set([
|
|
22
26
|
"timestamp",
|
|
23
27
|
"projectCode",
|
|
24
28
|
"serviceId",
|
|
29
|
+
"module",
|
|
30
|
+
"method",
|
|
25
31
|
"moduleMethod",
|
|
26
32
|
"traceId",
|
|
27
33
|
"level",
|
|
@@ -44,7 +50,9 @@ function formatRow(entry, verbose, extraFields) {
|
|
|
44
50
|
const traceId = chalk.dim(entry.traceId || "—");
|
|
45
51
|
let row = ` ${chalk.dim(time)} ${chalk.cyan(project)} › ${chalk.cyan(service)} ${method.padEnd(15)} ${level} ${traceId} ${msg}${dur}`;
|
|
46
52
|
if (extraFields && extraFields.length) {
|
|
47
|
-
const parts = extraFields.map(
|
|
53
|
+
const parts = extraFields.map(
|
|
54
|
+
(p) => `${chalk.dim(p + ":")} ${cellStr(getAtPath(entry, p))}`,
|
|
55
|
+
);
|
|
48
56
|
row += " " + parts.join(" ");
|
|
49
57
|
}
|
|
50
58
|
if (verbose) {
|
|
@@ -99,17 +107,55 @@ function matchesFilters(entry, andFilters, orFilters) {
|
|
|
99
107
|
return andPass && orPass;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
const DISPLAY_SKIP = new Set([
|
|
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
|
+
]);
|
|
103
122
|
|
|
104
123
|
function filterDisplayField(f) {
|
|
105
124
|
if (f.field === "has" || f.field === "missing") return f.value;
|
|
106
125
|
return f.field;
|
|
107
126
|
}
|
|
108
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
|
+
|
|
109
140
|
module.exports = async function logsCommand(
|
|
110
141
|
projectCode,
|
|
111
142
|
namespace,
|
|
112
|
-
{
|
|
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
|
+
},
|
|
113
159
|
) {
|
|
114
160
|
const andFilters = parseFilters(filter);
|
|
115
161
|
const orFiltersParsed = parseFilters(orFilters);
|
|
@@ -119,6 +165,51 @@ module.exports = async function logsCommand(
|
|
|
119
165
|
...(include || []),
|
|
120
166
|
].filter((f, i, arr) => !DISPLAY_SKIP.has(f) && arr.indexOf(f) === i);
|
|
121
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
|
+
|
|
122
213
|
let services = [];
|
|
123
214
|
let effectiveNamespace = namespace;
|
|
124
215
|
|
|
@@ -137,10 +228,13 @@ module.exports = async function logsCommand(
|
|
|
137
228
|
const projects = await SystemView.getProjects();
|
|
138
229
|
const all = Object.values(projects).flat();
|
|
139
230
|
const isNamespace = all.some(({ system }) =>
|
|
140
|
-
(system.connectionData.modules || []).some(
|
|
141
|
-
name
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
),
|
|
144
238
|
);
|
|
145
239
|
if (isNamespace) {
|
|
146
240
|
services = all;
|
|
@@ -163,7 +257,10 @@ module.exports = async function logsCommand(
|
|
|
163
257
|
|
|
164
258
|
if (clear) {
|
|
165
259
|
const confirmed = await promptConfirm("Clear all logs? (y/N) ");
|
|
166
|
-
if (!confirmed) {
|
|
260
|
+
if (!confirmed) {
|
|
261
|
+
log.warn("Aborted.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
167
264
|
for (const { system } of services) {
|
|
168
265
|
try {
|
|
169
266
|
const svc = await Client.loadService(system.connectionData.serviceUrl);
|
|
@@ -185,32 +282,64 @@ module.exports = async function logsCommand(
|
|
|
185
282
|
try {
|
|
186
283
|
const svc = await Client.loadService(serviceUrl);
|
|
187
284
|
const unsub = svc.SystemView.on("log", (entry) => {
|
|
188
|
-
if (
|
|
285
|
+
if (
|
|
286
|
+
effectiveNamespace &&
|
|
287
|
+
!(entry.moduleMethod && entry.moduleMethod.includes(effectiveNamespace))
|
|
288
|
+
)
|
|
289
|
+
return;
|
|
189
290
|
if (andFilters.length || orFiltersParsed.length) {
|
|
190
291
|
if (!matchesFilters(entry, andFilters, orFiltersParsed)) return;
|
|
191
292
|
}
|
|
192
293
|
if (level && entry.level !== level) return;
|
|
193
|
-
|
|
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("");
|
|
194
301
|
});
|
|
195
|
-
console.log(
|
|
302
|
+
console.log(
|
|
303
|
+
` ${chalk.green("✓")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)}`,
|
|
304
|
+
);
|
|
196
305
|
connected.push({ serviceId, svc, unsub });
|
|
197
306
|
} catch {
|
|
198
|
-
console.log(
|
|
307
|
+
console.log(
|
|
308
|
+
` ${chalk.red("✗")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)} ${chalk.red("(failed to connect)")}`,
|
|
309
|
+
);
|
|
199
310
|
}
|
|
200
311
|
}
|
|
201
312
|
|
|
313
|
+
if (save) {
|
|
314
|
+
console.log(chalk.dim(` Snapshot: ${savePath} (limit ${saveLimit})`));
|
|
315
|
+
}
|
|
316
|
+
|
|
202
317
|
const activeFilters = [];
|
|
203
|
-
if (effectiveNamespace)
|
|
318
|
+
if (effectiveNamespace)
|
|
319
|
+
activeFilters.push(`namespace: ${chalk.white(effectiveNamespace)}`);
|
|
204
320
|
if (level) activeFilters.push(`level: ${chalk.white(level)}`);
|
|
205
|
-
if (andFilters.length)
|
|
206
|
-
|
|
207
|
-
|
|
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(", "))}`);
|
|
208
331
|
if (verbose) activeFilters.push(`verbose: ${chalk.white("on")}`);
|
|
332
|
+
const queryUrl = uiUrl ? buildLogsUrl(uiUrl, { projectCode, effectiveNamespace, level, andFilters, orFilters }) : null;
|
|
333
|
+
|
|
209
334
|
if (activeFilters.length) {
|
|
210
335
|
console.log("");
|
|
211
336
|
console.log(chalk.dim(` Filters:`));
|
|
212
337
|
activeFilters.forEach((f) => console.log(` ${chalk.dim(f)}`));
|
|
213
338
|
}
|
|
339
|
+
if (queryUrl && !json) {
|
|
340
|
+
console.log("");
|
|
341
|
+
console.log(` ${chalk.dim("UI:")} ${chalk.cyan(queryUrl)}`);
|
|
342
|
+
}
|
|
214
343
|
console.log("");
|
|
215
344
|
|
|
216
345
|
if (!connected.length) {
|
|
@@ -224,22 +353,54 @@ module.exports = async function logsCommand(
|
|
|
224
353
|
try {
|
|
225
354
|
let entries = await svc.SystemView.getLog({ limit });
|
|
226
355
|
entries = entries || [];
|
|
227
|
-
if (effectiveNamespace)
|
|
228
|
-
|
|
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));
|
|
229
362
|
if (level) entries = entries.filter((e) => e.level === level);
|
|
230
363
|
allEntries.push(...entries);
|
|
231
364
|
} catch {}
|
|
232
365
|
}
|
|
233
366
|
if (allEntries.length) {
|
|
234
367
|
console.log(chalk.dim(` ── current (${allEntries.length}) ──`));
|
|
235
|
-
allEntries.forEach((
|
|
236
|
-
|
|
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
|
+
});
|
|
237
376
|
}
|
|
377
|
+
|
|
378
|
+
if (!follow) {
|
|
379
|
+
connected.forEach(({ unsub }) => unsub());
|
|
380
|
+
return () => {};
|
|
381
|
+
}
|
|
382
|
+
console.log(chalk.dim(` ── streaming ──`));
|
|
238
383
|
}
|
|
239
384
|
|
|
240
|
-
return () => {
|
|
385
|
+
return () => {
|
|
386
|
+
connected.forEach(({ unsub }) => unsub());
|
|
387
|
+
};
|
|
241
388
|
};
|
|
242
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
|
+
|
|
243
404
|
function promptConfirm(question) {
|
|
244
405
|
return new Promise((resolve) => {
|
|
245
406
|
process.stdout.write(question);
|
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/startLineReader.js
CHANGED
|
@@ -8,13 +8,23 @@ const logsCommand = require("./logs");
|
|
|
8
8
|
const log = require("./logger");
|
|
9
9
|
const cli = require("./utils/cli");
|
|
10
10
|
const readline = require("readline");
|
|
11
|
+
const { createClient } = require("systemlynx");
|
|
12
|
+
const { createCookieHttpClient } = require("./cookieClient");
|
|
13
|
+
const Client = createClient(createCookieHttpClient());
|
|
11
14
|
|
|
12
|
-
module.exports = function startLineReader(url, { connectedUrls = new Set() } = {}) {
|
|
15
|
+
module.exports = async function startLineReader(url, { connectedUrls = new Set() } = {}) {
|
|
13
16
|
const lineReader = readline.createInterface({
|
|
14
17
|
input: process.stdin,
|
|
15
18
|
output: process.stdout,
|
|
16
19
|
});
|
|
17
20
|
|
|
21
|
+
let CLI = null;
|
|
22
|
+
try {
|
|
23
|
+
({ CLI } = await Client.loadService(`${url}/systemview/api`));
|
|
24
|
+
const history = await CLI.getHistory();
|
|
25
|
+
lineReader.history = [...history].reverse();
|
|
26
|
+
} catch {}
|
|
27
|
+
|
|
18
28
|
let stopLogs = null;
|
|
19
29
|
|
|
20
30
|
const parseArgs = (args) => {
|
|
@@ -36,12 +46,18 @@ module.exports = function startLineReader(url, { connectedUrls = new Set() } = {
|
|
|
36
46
|
}
|
|
37
47
|
else if (args[i] === "--verbose") { flags.verbose = true; }
|
|
38
48
|
else if (args[i] === "--current") { flags.current = true; }
|
|
49
|
+
else if (args[i] === "--follow" || args[i] === "-f") { flags.follow = true; }
|
|
50
|
+
else if (args[i] === "--clear") { flags.clear = true; }
|
|
39
51
|
else if (args[i] === "--json") { flags.json = true; }
|
|
40
52
|
else if (args[i] === "--bail") { flags.bail = true; }
|
|
41
53
|
else if (args[i] === "--dry-run") { flags.dryRun = true; }
|
|
42
54
|
else if (args[i] === "--force") { flags.force = true; }
|
|
43
55
|
else if (args[i] === "--manifest") { flags.useManifest = true; }
|
|
44
|
-
else if (
|
|
56
|
+
else if (args[i] === "--saved") { flags.saved = true; }
|
|
57
|
+
else if (args[i] === "--save" && args[i + 1] && !args[i + 1].startsWith("-")) { flags.save = args[++i]; }
|
|
58
|
+
else if (args[i] === "--save") { flags.save = true; }
|
|
59
|
+
else if (args[i] === "--save-limit" && args[i + 1]) { flags.saveLimit = parseInt(args[++i], 10); }
|
|
60
|
+
else if (!args[i].startsWith("-")) { positional.push(args[i]); }
|
|
45
61
|
}
|
|
46
62
|
return { flags, positional };
|
|
47
63
|
};
|
|
@@ -52,8 +68,22 @@ module.exports = function startLineReader(url, { connectedUrls = new Set() } = {
|
|
|
52
68
|
if (!command) { lineReader.prompt(); return; }
|
|
53
69
|
const { flags, positional } = parseArgs(parts.slice(1));
|
|
54
70
|
|
|
55
|
-
if (["exit", "q"
|
|
71
|
+
if (["exit", "q"].includes(command)) {
|
|
56
72
|
process.exit(0);
|
|
73
|
+
} else if (["stop", "shutdown"].includes(command)) {
|
|
74
|
+
const api = `${url}/systemview/api`;
|
|
75
|
+
try {
|
|
76
|
+
const { SystemView } = await Client.loadService(api);
|
|
77
|
+
await SystemView.shutdown();
|
|
78
|
+
} catch {}
|
|
79
|
+
const appIsRunning = require("./appIsRunning");
|
|
80
|
+
if (!(await appIsRunning(api))) {
|
|
81
|
+
log.success("SystemView stopped.");
|
|
82
|
+
process.exit(0);
|
|
83
|
+
} else {
|
|
84
|
+
log.error("Shutdown failed.");
|
|
85
|
+
lineReader.prompt();
|
|
86
|
+
}
|
|
57
87
|
} else if (command === "test") {
|
|
58
88
|
try {
|
|
59
89
|
await runTests(url, positional[0], positional[1], {
|
|
@@ -137,7 +167,13 @@ module.exports = function startLineReader(url, { connectedUrls = new Set() } = {
|
|
|
137
167
|
};
|
|
138
168
|
|
|
139
169
|
lineReader.prompt();
|
|
140
|
-
lineReader.on("line",
|
|
170
|
+
lineReader.on("line", async (input) => {
|
|
171
|
+
const trimmed = input.trim();
|
|
172
|
+
if (trimmed && CLI) {
|
|
173
|
+
try { await CLI.saveHistory(trimmed); } catch {}
|
|
174
|
+
}
|
|
175
|
+
handleInput(trimmed);
|
|
176
|
+
});
|
|
141
177
|
lineReader.on("close", () => process.exit(0));
|
|
142
178
|
return lineReader;
|
|
143
179
|
};
|
package/cli/utils/cli.js
CHANGED
|
@@ -9,18 +9,19 @@ const HELP_TEXT = `
|
|
|
9
9
|
test <projectCode> [namespace] Run saved tests for a project
|
|
10
10
|
list [projectCode] [namespace] List projects, services, or tests
|
|
11
11
|
logs [projectCode] [namespace] Stream log entries from connected services
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
unsubscribe Stop streaming logs (keeps log file) [alias: stoplogs]
|
|
13
|
+
flush Wipe the log file and stop streaming [alias: clearlogs]
|
|
14
14
|
connect [url|projectCode] Connect a service URL, reconnect a stored project, or connect all
|
|
15
15
|
connect <url> --manifest Connect via plugin manifest — registers under real projectCode
|
|
16
16
|
disconnect [projectCode] [serviceId] Remove a project or service from the UI store
|
|
17
17
|
manifest clean Re-probe manifest entries, remove stale ones
|
|
18
18
|
probe <ServiceId.Module.method> [args] Call a service method ad-hoc
|
|
19
19
|
open [projectCode] [namespace] Open the browser UI
|
|
20
|
-
shutdown [port] Stop a running SystemView instance
|
|
20
|
+
shutdown [port] Stop a running SystemView instance [aliases: exit, stop, q]
|
|
21
21
|
|
|
22
22
|
Flags:
|
|
23
23
|
--version Print version and exit
|
|
24
|
+
-d, --debug Verbose debug output
|
|
24
25
|
--json Output results as JSON (for agents/CI)
|
|
25
26
|
--verbose test: full results and args for all phases; list: expand hierarchy
|
|
26
27
|
--manifest connect: use plugin manifest to get real projectCode
|
|
@@ -31,12 +32,19 @@ const HELP_TEXT = `
|
|
|
31
32
|
--dry-run Print which tests would run without executing
|
|
32
33
|
--phase <before|main|events|after> Run only specified phase(s), comma-separated
|
|
33
34
|
--index <n> Run only action at index n within each phase (0-based)
|
|
34
|
-
--level <trace|
|
|
35
|
+
--level <trace|log|warn|error|debug> logs: filter by level
|
|
35
36
|
--limit <n> logs: max entries to show with --current (default 50)
|
|
36
|
-
--current logs: show existing entries
|
|
37
|
-
--
|
|
37
|
+
--current logs: show existing entries (use --follow to also stream)
|
|
38
|
+
-f, --follow logs: keep streaming after --current
|
|
39
|
+
--clear logs: wipe log store then stream
|
|
40
|
+
--filter <field=value> logs: AND filter on a field (repeatable); field can be a dot path
|
|
41
|
+
--filter has=<field> → only entries where field is present
|
|
42
|
+
--filter missing=<field> → only entries where field is absent
|
|
38
43
|
--or <field=value> logs: OR filter on a field (repeatable)
|
|
39
|
-
--include <field> logs: include extra field
|
|
44
|
+
--include <field> logs: include extra field as a column (repeatable)
|
|
45
|
+
--save [path] logs: append streamed entries to a local snapshot file
|
|
46
|
+
--saved logs: read from local snapshot instead of live service
|
|
47
|
+
--save-limit <n> logs: max entries to keep in snapshot (default 500)
|
|
40
48
|
--force connect: re-probe even if already connected
|
|
41
49
|
|
|
42
50
|
Examples:
|
|
@@ -50,7 +58,12 @@ const HELP_TEXT = `
|
|
|
50
58
|
systemview list buAPI --verbose
|
|
51
59
|
systemview logs buAPI
|
|
52
60
|
systemview logs buAPI --current --limit 20
|
|
61
|
+
systemview logs buAPI --current --follow
|
|
53
62
|
systemview logs buAPI --filter level=error
|
|
63
|
+
systemview logs buAPI --filter has=log.userId
|
|
64
|
+
systemview logs buAPI --filter missing=error --include log
|
|
65
|
+
systemview logs buAPI --save
|
|
66
|
+
systemview logs buAPI --saved
|
|
54
67
|
systemview connect http://localhost:4100/bu/api/profiles
|
|
55
68
|
systemview connect http://localhost:4100/bu/api/profiles --manifest
|
|
56
69
|
systemview connect buAPI
|
|
@@ -63,7 +76,7 @@ const HELP_TEXT = `
|
|
|
63
76
|
|
|
64
77
|
const rawArgs = process.argv.slice(2);
|
|
65
78
|
|
|
66
|
-
const flagValueArgs = ["--manifest", "--header", "--skip", "--phase", "--index", "--level", "--limit", "--follow", "--filter", "--or", "--include"];
|
|
79
|
+
const flagValueArgs = ["--manifest", "--header", "--skip", "--phase", "--index", "--level", "--limit", "--follow", "--filter", "--or", "--include", "--save", "--save-limit"];
|
|
67
80
|
|
|
68
81
|
const flags = {
|
|
69
82
|
json: rawArgs.includes("--json"),
|
|
@@ -121,6 +134,19 @@ const flags = {
|
|
|
121
134
|
})(),
|
|
122
135
|
clear: rawArgs.includes("--clear"),
|
|
123
136
|
force: rawArgs.includes("--force"),
|
|
137
|
+
save: (() => {
|
|
138
|
+
const i = rawArgs.indexOf("--save");
|
|
139
|
+
if (i === -1) return false;
|
|
140
|
+
const next = rawArgs[i + 1];
|
|
141
|
+
return (next && !next.startsWith("-")) ? next : true;
|
|
142
|
+
})(),
|
|
143
|
+
saved: rawArgs.includes("--saved"),
|
|
144
|
+
saveLimit: (() => {
|
|
145
|
+
const i = rawArgs.indexOf("--save-limit");
|
|
146
|
+
if (i === -1) return 500;
|
|
147
|
+
const val = parseInt(rawArgs[i + 1], 10);
|
|
148
|
+
return isNaN(val) ? 500 : val;
|
|
149
|
+
})(),
|
|
124
150
|
headers: (() => {
|
|
125
151
|
const result = {};
|
|
126
152
|
rawArgs.forEach((a, i) => {
|