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/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 { HttpClient, createClient } = require("systemlynx");
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
- await HttpClient.request({ url, method: "put" });
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
- process.exit(0);
121
- } catch (error) {
122
- if (!(await appIsRunning(api))) {
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.info(`SystemView already running on port ${port} — attaching.`);
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
- info: (s) => chalk.white(s),
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 || "info").padEnd(5));
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((p) => `${chalk.dim(p + ":")} ${cellStr(getAtPath(entry, p))}`);
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(["timestamp", "projectCode", "serviceId", "moduleMethod", "traceId", "level", "message", "duration"]);
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
- { uiUrl, level, limit = 50, clear, current, verbose, filter, or: orFilters, include },
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(({ name, methods }) =>
141
- name.toLowerCase().includes(projectCode.toLowerCase()) ||
142
- (methods || []).some(({ fn }) => fn.toLowerCase().includes(projectCode.toLowerCase()))
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) { log.warn("Aborted."); return; }
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 (effectiveNamespace && !(entry.moduleMethod && entry.moduleMethod.includes(effectiveNamespace))) return;
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
- console.log(formatRow(entry, verbose, extraFields));
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(` ${chalk.green("✓")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)}`);
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(` ${chalk.red("✗")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)} ${chalk.red("(failed to connect)")}`);
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) activeFilters.push(`namespace: ${chalk.white(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) activeFilters.push(`filter: ${chalk.white(andFilters.map((f) => `${f.field}=${f.value}`).join(", "))}`);
206
- if (orFiltersParsed.length) activeFilters.push(`or: ${chalk.white(orFiltersParsed.map((f) => `${f.field}=${f.value}`).join(", "))}`);
207
- if (extraFields.length) activeFilters.push(`include: ${chalk.white(extraFields.join(", "))}`);
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) entries = entries.filter((e) => e.moduleMethod && e.moduleMethod.includes(effectiveNamespace));
228
- if (andFilters.length || orFiltersParsed.length) entries = entries.filter((e) => matchesFilters(e, andFilters, orFiltersParsed));
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((e) => console.log(formatRow(e, verbose, extraFields)));
236
- console.log(chalk.dim(` ── streaming ──`));
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 () => { connected.forEach(({ unsub }) => unsub()); };
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);
@@ -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);
@@ -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 (!args[i].startsWith("--")) { positional.push(args[i]); }
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", "shutdown", "stop"].includes(command)) {
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", handleInput);
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
- stoplogs Stop streaming logs (keeps log file)
13
- clearlogs Wipe the log file and stop streaming
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|info|warn|error|debug> logs: filter by level
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 before streaming
37
- --filter <field=value> logs: AND filter on a field (repeatable)
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 in output (repeatable)
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) => {