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/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
- info: (s) => chalk.white(s),
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 || "info").padEnd(5));
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
- "message",
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.message || "";
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
- const main = ` ${chalk.dim(time)} ${chalk.cyan(project)} › ${chalk.cyan(service)} ${method.padEnd(15)} ${level} ${traceId} ${msg}${dur}`;
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
- main +
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 main;
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
- const v = entry[field];
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
- { level, limit = 50, clear, json, follow, current, verbose, filter, or: orFilters },
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
- if (follow) {
98
- const manifestFile = path.join(process.cwd(), "systemview.manifest.json");
99
- if (!fs.existsSync(manifestFile)) {
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
- const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
104
- const services = manifest.services || [manifest];
105
- if (!services.length) {
106
- log.warn("No services in manifest. Use: systemview connect <serviceId> <url>");
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(" Streaming logs"));
207
+ console.log(chalk.bold(` Snapshot: ${savePath} (${entries.length} entries)`));
112
208
  console.log("");
209
+ entries.forEach(printEntry);
210
+ return;
211
+ }
113
212
 
114
- console.log(chalk.dim(` Services (${services.length}):`));
115
- const connected = [];
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
- if (!connected.length) {
151
- log.warn("No services connected.");
152
- return;
153
- }
216
+ try {
217
+ const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
154
218
 
155
- if (current) {
156
- try {
157
- const api = `${url}/systemview/api`;
158
- const { SystemView } = await Client.loadService(api);
159
- const all = await SystemView.getLogs({ projectCode: manifest.projectCode, level, limit });
160
- let entries = all || [];
161
- if (namespace) entries = entries.filter((e) => e.moduleMethod && e.moduleMethod.includes(namespace));
162
- if (andFilters.length || orFiltersParsed.length) entries = entries.filter((e) => matchesFilters(e, andFilters, orFiltersParsed));
163
- if (entries.length) {
164
- console.log(chalk.dim(` ── current (${entries.length}) ──`));
165
- entries.forEach((e) => console.log(formatRow(e, verbose)));
166
- console.log(chalk.dim(` ── streaming ──`));
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
- const api = `${url}/systemview/api`;
177
- const { SystemView } = await Client.loadService(api);
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) { log.warn("Aborted."); return; }
182
- await SystemView.clearLogs();
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
- const entries = await (async () => {
188
- const all = await SystemView.getLogs({ projectCode, level, limit });
189
- let filtered = all || [];
190
- if (namespace) filtered = filtered.filter((e) => e.moduleMethod && e.moduleMethod.includes(namespace));
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
- if (json) {
196
- console.log(JSON.stringify(entries, null, 2));
197
- return;
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 (!entries.length) {
201
- log.warn("No logs found.");
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
- entries.forEach((e) => console.log(formatRow(e, verbose)));
207
- console.log("");
208
- log.plain(chalk.dim(` ${entries.length} entries`));
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);
@@ -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
+ };
@@ -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
- const manifestFile = manifestPath || path.join(process.cwd(), "systemview.manifest.json");
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
- let manifest;
41
- try {
42
- manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
43
- } catch (err) {
44
- log.error(`Failed to read manifest: ${err.message}`);
45
- 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 {}
46
44
  }
47
45
 
48
- const services = manifest.services || [manifest];
49
- const service = services.find((s) => s.serviceId === serviceId);
46
+ // Fall back to manifest file
50
47
  if (!service) {
51
- log.error(`Service "${serviceId}" not found in manifest`);
52
- 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
+ }
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(extraHeaders).length) client.setHeaders(extraHeaders);
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");