systemview 1.20.0 → 1.21.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/logs.js ADDED
@@ -0,0 +1,251 @@
1
+ const chalk = require("chalk");
2
+ const log = require("./logger");
3
+ const { createClient } = require("systemlynx");
4
+ const { createCookieHttpClient } = require("./cookieClient");
5
+ const cookieHttpClient = createCookieHttpClient();
6
+ const Client = createClient(cookieHttpClient);
7
+
8
+ const LEVEL_COLOR = {
9
+ trace: (s) => chalk.dim(s),
10
+ info: (s) => chalk.white(s),
11
+ warn: (s) => chalk.yellow(s),
12
+ error: (s) => chalk.red(s),
13
+ debug: (s) => chalk.blue(s),
14
+ };
15
+
16
+ function colorLevel(level) {
17
+ const fn = LEVEL_COLOR[level] || ((s) => s);
18
+ return fn((level || "info").padEnd(5));
19
+ }
20
+
21
+ const SKIP_KEYS = new Set([
22
+ "timestamp",
23
+ "projectCode",
24
+ "serviceId",
25
+ "moduleMethod",
26
+ "traceId",
27
+ "level",
28
+ "message",
29
+ "duration",
30
+ ]);
31
+
32
+ function formatRow(entry, verbose, extraFields) {
33
+ const d = new Date(entry.timestamp);
34
+ const time =
35
+ d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) +
36
+ " " +
37
+ d.toLocaleTimeString("en-US", { hour12: false });
38
+ const project = entry.projectCode || "—";
39
+ const service = entry.serviceId || "—";
40
+ const method = entry.moduleMethod || "—";
41
+ const level = colorLevel(entry.level);
42
+ const msg = entry.message || "";
43
+ const dur = entry.duration != null ? chalk.dim(` ${entry.duration}ms`) : "";
44
+ const traceId = chalk.dim(entry.traceId || "—");
45
+ let row = ` ${chalk.dim(time)} ${chalk.cyan(project)} › ${chalk.cyan(service)} ${method.padEnd(15)} ${level} ${traceId} ${msg}${dur}`;
46
+ if (extraFields && extraFields.length) {
47
+ const parts = extraFields.map((p) => `${chalk.dim(p + ":")} ${cellStr(getAtPath(entry, p))}`);
48
+ row += " " + parts.join(" ");
49
+ }
50
+ if (verbose) {
51
+ const extra = {};
52
+ Object.keys(entry).forEach((k) => {
53
+ if (!SKIP_KEYS.has(k)) extra[k] = entry[k];
54
+ });
55
+ if (Object.keys(extra).length) {
56
+ return (
57
+ row +
58
+ "\n" +
59
+ JSON.stringify(extra, null, 2)
60
+ .split("\n")
61
+ .map((l) => " " + chalk.yellow(l))
62
+ .join("\n")
63
+ );
64
+ }
65
+ }
66
+ return row;
67
+ }
68
+
69
+ function getAtPath(entry, field) {
70
+ return field.split(".").reduce((o, k) => (o != null ? o[k] : undefined), entry);
71
+ }
72
+
73
+ function cellStr(v) {
74
+ if (v === null || v === undefined) return "—";
75
+ if (typeof v === "object") return Array.isArray(v) ? `[${v.length}]` : "{…}";
76
+ return String(v);
77
+ }
78
+
79
+ function parseFilters(filters) {
80
+ return (filters || [])
81
+ .map((f) => {
82
+ const eq = f.indexOf("=");
83
+ if (eq === -1) return null;
84
+ return { field: f.slice(0, eq), value: f.slice(eq + 1) };
85
+ })
86
+ .filter(Boolean);
87
+ }
88
+
89
+ function matchesFilters(entry, andFilters, orFilters) {
90
+ const check = ({ field, value }) => {
91
+ if (field === "has") return getAtPath(entry, value) != null;
92
+ if (field === "missing") return getAtPath(entry, value) == null;
93
+ const v = getAtPath(entry, field);
94
+ return v != null && String(v).includes(value);
95
+ };
96
+ const andPass = andFilters.length === 0 || andFilters.every(check);
97
+ const orPass = orFilters.length === 0 || orFilters.some(check);
98
+ if (andFilters.length && orFilters.length) return andPass || orPass;
99
+ return andPass && orPass;
100
+ }
101
+
102
+ const DISPLAY_SKIP = new Set(["timestamp", "projectCode", "serviceId", "moduleMethod", "traceId", "level", "message", "duration"]);
103
+
104
+ function filterDisplayField(f) {
105
+ if (f.field === "has" || f.field === "missing") return f.value;
106
+ return f.field;
107
+ }
108
+
109
+ module.exports = async function logsCommand(
110
+ projectCode,
111
+ namespace,
112
+ { uiUrl, level, limit = 50, clear, current, verbose, filter, or: orFilters, include },
113
+ ) {
114
+ const andFilters = parseFilters(filter);
115
+ const orFiltersParsed = parseFilters(orFilters);
116
+ const extraFields = [
117
+ ...andFilters.map(filterDisplayField),
118
+ ...orFiltersParsed.map(filterDisplayField),
119
+ ...(include || []),
120
+ ].filter((f, i, arr) => !DISPLAY_SKIP.has(f) && arr.indexOf(f) === i);
121
+
122
+ let services = [];
123
+ let effectiveNamespace = namespace;
124
+
125
+ try {
126
+ const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
127
+
128
+ if (!projectCode) {
129
+ const projects = await SystemView.getProjects();
130
+ services = Object.values(projects).flat();
131
+ } else {
132
+ const matches = await SystemView.getServices(projectCode);
133
+ if (matches && matches.length) {
134
+ services = matches;
135
+ } else {
136
+ // Fuzzy namespace: check if arg is a module/method name
137
+ const projects = await SystemView.getProjects();
138
+ const all = Object.values(projects).flat();
139
+ 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
+ )
144
+ );
145
+ if (isNamespace) {
146
+ services = all;
147
+ effectiveNamespace = projectCode;
148
+ } else {
149
+ log.warn(`No services found for: ${projectCode}`);
150
+ return;
151
+ }
152
+ }
153
+ }
154
+ } catch (err) {
155
+ log.error("Failed to connect to SystemView: " + err.message);
156
+ return;
157
+ }
158
+
159
+ if (!services.length) {
160
+ log.warn("No services in store. Use: systemview connect <url>");
161
+ return;
162
+ }
163
+
164
+ if (clear) {
165
+ const confirmed = await promptConfirm("Clear all logs? (y/N) ");
166
+ if (!confirmed) { log.warn("Aborted."); return; }
167
+ for (const { system } of services) {
168
+ try {
169
+ const svc = await Client.loadService(system.connectionData.serviceUrl);
170
+ await svc.SystemView.clearLog();
171
+ } catch {}
172
+ }
173
+ log.success("Logs cleared.");
174
+ return;
175
+ }
176
+
177
+ console.log("");
178
+ console.log(chalk.bold(" Streaming logs"));
179
+ console.log("");
180
+ console.log(chalk.dim(` Services (${services.length}):`));
181
+
182
+ const connected = [];
183
+ for (const { serviceId, system } of services) {
184
+ const { serviceUrl } = system.connectionData;
185
+ try {
186
+ const svc = await Client.loadService(serviceUrl);
187
+ const unsub = svc.SystemView.on("log", (entry) => {
188
+ if (effectiveNamespace && !(entry.moduleMethod && entry.moduleMethod.includes(effectiveNamespace))) return;
189
+ if (andFilters.length || orFiltersParsed.length) {
190
+ if (!matchesFilters(entry, andFilters, orFiltersParsed)) return;
191
+ }
192
+ if (level && entry.level !== level) return;
193
+ console.log(formatRow(entry, verbose, extraFields));
194
+ });
195
+ console.log(` ${chalk.green("✓")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)}`);
196
+ connected.push({ serviceId, svc, unsub });
197
+ } catch {
198
+ console.log(` ${chalk.red("✗")} ${chalk.cyan(serviceId).padEnd(20)} ${chalk.dim(serviceUrl)} ${chalk.red("(failed to connect)")}`);
199
+ }
200
+ }
201
+
202
+ const activeFilters = [];
203
+ if (effectiveNamespace) activeFilters.push(`namespace: ${chalk.white(effectiveNamespace)}`);
204
+ 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(", "))}`);
208
+ if (verbose) activeFilters.push(`verbose: ${chalk.white("on")}`);
209
+ if (activeFilters.length) {
210
+ console.log("");
211
+ console.log(chalk.dim(` Filters:`));
212
+ activeFilters.forEach((f) => console.log(` ${chalk.dim(f)}`));
213
+ }
214
+ console.log("");
215
+
216
+ if (!connected.length) {
217
+ log.warn("No services connected.");
218
+ return () => {};
219
+ }
220
+
221
+ if (current) {
222
+ const allEntries = [];
223
+ for (const { svc } of connected) {
224
+ try {
225
+ let entries = await svc.SystemView.getLog({ limit });
226
+ 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));
229
+ if (level) entries = entries.filter((e) => e.level === level);
230
+ allEntries.push(...entries);
231
+ } catch {}
232
+ }
233
+ if (allEntries.length) {
234
+ console.log(chalk.dim(` ── current (${allEntries.length}) ──`));
235
+ allEntries.forEach((e) => console.log(formatRow(e, verbose, extraFields)));
236
+ console.log(chalk.dim(` ── streaming ──`));
237
+ }
238
+ }
239
+
240
+ return () => { connected.forEach(({ unsub }) => unsub()); };
241
+ };
242
+
243
+ function promptConfirm(question) {
244
+ return new Promise((resolve) => {
245
+ process.stdout.write(question);
246
+ process.stdin.setEncoding("utf8");
247
+ process.stdin.once("data", (data) => {
248
+ resolve(data.trim().toLowerCase() === "y");
249
+ });
250
+ });
251
+ }
@@ -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/probe.js CHANGED
@@ -1,9 +1,13 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const { createCookieClient } = require("./cookieClient");
3
+ const { createClient } = require("systemlynx");
4
+ const { createCookieHttpClient } = require("./cookieClient");
4
5
  const log = require("./logger");
5
6
 
6
- module.exports = async function probe(namespace, argsStr, { json = false, manifest: manifestPath, headers: cliHeaders = {} } = {}) {
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
- const manifestFile = manifestPath || path.join(process.cwd(), "systemview.manifest.json");
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
- let manifest;
37
- try {
38
- manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
39
- } catch (err) {
40
- log.error(`Failed to read manifest: ${err.message}`);
41
- return 1;
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
- const services = manifest.services || [manifest];
45
- const service = services.find((s) => s.serviceId === serviceId);
46
+ // Fall back to manifest file
46
47
  if (!service) {
47
- log.error(`Service "${serviceId}" not found in manifest`);
48
- return 1;
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 = createCookieClient(extraHeaders).createService(service.system.connectionData);
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");
package/cli/runTests.js CHANGED
@@ -1,6 +1,7 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { createCookieClient } = require("./cookieClient");
1
+ const { createClient } = require("systemlynx");
2
+ const { createCookieHttpClient } = require("./cookieClient");
3
+ const cookieHttpClient = createCookieHttpClient();
4
+ const Client = createClient(cookieHttpClient);
4
5
  const { initializeSavedTests } = require("../testing-utilities/transformTests");
5
6
  const FullTestController = require("../testing-utilities/FullTestController");
6
7
  const log = require("./logger");
@@ -45,7 +46,6 @@ module.exports = async function runTests(
45
46
  {
46
47
  json = false,
47
48
  verbose = false,
48
- manifest: manifestPath,
49
49
  headers: cliHeaders = {},
50
50
  bail = false,
51
51
  dryRun = false,
@@ -61,23 +61,25 @@ module.exports = async function runTests(
61
61
  return 1;
62
62
  }
63
63
 
64
- const { services: connectedServices, probeHeaders: manifestHeaders } = await resolveServices(
65
- api,
66
- project_code,
67
- manifestPath
68
- );
64
+ const { services: connectedServices, resolvedNamespace } = await resolveServices(api, project_code);
65
+ if (resolvedNamespace && !namespace) namespace = resolvedNamespace;
69
66
  if (!connectedServices || !connectedServices.length) {
70
67
  log.warn("No connected services found for project: " + project_code);
71
68
  return 1;
72
69
  }
73
70
 
74
- const extraHeaders = { ...manifestHeaders, ...cliHeaders };
75
- const Client = createCookieClient(extraHeaders);
71
+ const extraHeaders = { ...cliHeaders };
76
72
 
77
73
  if (!json) {
78
74
  connectedServices.forEach(({ serviceId, system }) => {
79
75
  log.success(`connected: ${serviceId} @ ${system.connectionData.serviceUrl}`);
80
76
  });
77
+ if (namespace || skipPatterns.length) {
78
+ console.log("");
79
+ console.log(chalk.dim(` Filters:`));
80
+ if (namespace) console.log(` ${chalk.dim(`namespace: ${chalk.white(namespace)}`)}`);
81
+ if (skipPatterns.length) console.log(` ${chalk.dim(`skip: ${chalk.white(skipPatterns.join(", "))}`)}`);
82
+ }
81
83
  }
82
84
 
83
85
  const allTestLists = await getTests(connectedServices, Client);
@@ -121,7 +123,7 @@ module.exports = async function runTests(
121
123
  if (!json) log.info(`Initializing tests for ${serviceId}...`);
122
124
 
123
125
  try {
124
- const initialized = initializeSavedTests(testList, connectedServices, Client);
126
+ const initialized = initializeSavedTests(testList, connectedServices, Client, extraHeaders);
125
127
  const { passed, failed, jsonTests, failures } = await runAllTests(
126
128
  initialized,
127
129
  url,
@@ -354,39 +356,32 @@ async function getTests(connectedServices, Client) {
354
356
  return results;
355
357
  }
356
358
 
357
- async function resolveServices(api, project_code, manifestPath) {
358
- const manifestFile = manifestPath || path.join(process.cwd(), "systemview.manifest.json");
359
+ async function resolveServices(api, project_code) {
360
+ try {
361
+ const { SystemView } = await Client.loadService(api);
359
362
 
360
- if (fs.existsSync(manifestFile)) {
361
- try {
362
- const raw = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
363
- const services = raw.services
364
- ? raw.services
365
- : [{ serviceId: raw.serviceId, system: raw.system, specList: raw.specList }];
366
-
367
- if (project_code && raw.projectCode && raw.projectCode !== project_code) {
368
- log.warn(
369
- `Manifest projectCode "${raw.projectCode}" doesn't match "${project_code}", falling back to API`
370
- );
371
- } else {
372
- return { services, probeHeaders: raw.probeHeaders || {} };
373
- }
374
- } catch (err) {
375
- log.warn(`Failed to read manifest: ${err.message}`);
363
+ // Try exact project code match first
364
+ const services = await SystemView.getServices(project_code);
365
+ if (services && services.length) {
366
+ return { services };
376
367
  }
377
- }
378
368
 
379
- const BaseClient = createCookieClient();
380
- const services = await getConnectedServices(api, project_code, BaseClient);
381
- return { services, probeHeaders: {} };
382
- }
369
+ // Fuzzy namespace: check if project_code matches any module or method across all services
370
+ const projects = await SystemView.getProjects();
371
+ const all = Object.values(projects).flat();
372
+ const isNamespace = all.some(({ system }) =>
373
+ (system.connectionData.modules || []).some(({ name, methods }) =>
374
+ name.toLowerCase().includes(project_code.toLowerCase()) ||
375
+ (methods || []).some(({ fn }) => fn.toLowerCase().includes(project_code.toLowerCase()))
376
+ )
377
+ );
378
+ if (isNamespace) {
379
+ return { services: all, resolvedNamespace: project_code };
380
+ }
383
381
 
384
- async function getConnectedServices(api, project_code, Client) {
385
- try {
386
- const { SystemView } = await Client.loadService(api);
387
- return await SystemView.getServices(project_code);
382
+ return { services: [] };
388
383
  } catch (error) {
389
384
  log.error("Failed to connect to SystemView: " + error.message);
390
- return [];
385
+ return { services: [] };
391
386
  }
392
387
  }