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.
@@ -1,80 +1,172 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { HttpClient } = require("systemlynx");
4
- const { createCookieClient } = require("./cookieClient");
5
- const Client = createCookieClient();
1
+ const { HttpClient, createClient } = require("systemlynx");
2
+ const { createCookieHttpClient } = require("./cookieClient");
3
+ const cookieHttpClient = createCookieHttpClient();
4
+ const Client = createClient(cookieHttpClient);
6
5
  const log = require("./logger");
7
6
 
8
- async function probeService(url) {
9
- const connectionData = await HttpClient.request({ url });
10
- if (!connectionData.SystemLynxService) {
11
- throw new Error(`No SystemLynx service found at ${url}`);
12
- }
13
- const client = Client.createService(connectionData);
14
- const connection = await client.Plugin.getConnection();
15
- return connection; // { projectCode, serviceId, system, specList }
16
- }
17
-
18
- function readManifest(manifestFile) {
19
- if (!fs.existsSync(manifestFile)) return null;
7
+ async function getUiSvc(uiUrl) {
20
8
  try {
21
- return JSON.parse(fs.readFileSync(manifestFile, "utf8"));
9
+ const { SystemView } = await Client.loadService(`${uiUrl}/systemview/api`);
10
+ return SystemView;
22
11
  } catch {
23
12
  return null;
24
13
  }
25
14
  }
26
15
 
27
- function writeManifest(manifestFile, connection) {
28
- const manifest = readManifest(manifestFile) || { projectCode: connection.projectCode, services: [] };
29
- if (!manifest.services) manifest.services = [];
30
- const entry = { serviceId: connection.serviceId, system: connection.system, specList: connection.specList };
31
- const idx = manifest.services.findIndex((s) => s.serviceId === connection.serviceId);
32
- if (idx > -1) manifest.services[idx] = entry;
33
- else manifest.services.push(entry);
34
- manifest.projectCode = connection.projectCode;
35
- fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2));
16
+ async function notifyUi(uiSvc, project) {
17
+ if (!uiSvc) return;
18
+ try {
19
+ await uiSvc.connect(project);
20
+ } catch {}
21
+ }
22
+
23
+ function serviceIdFromRoute(route) {
24
+ const segments = (route || "").split("/").filter(Boolean);
25
+ return [...segments].reverse().find((s) => s.toLowerCase() !== "api") || "Service";
36
26
  }
37
27
 
38
- module.exports = async function connectService(serviceId, url, { manifest: manifestPath } = {}) {
39
- const manifestFile = manifestPath || path.join(process.cwd(), "systemview.manifest.json");
28
+ function printSummary(entries) {
29
+ console.log("");
30
+ entries.forEach(({ projectCode, serviceId, url }) => {
31
+ log.plain(` ${projectCode} › ${serviceId}${url ? " " + url : ""}`);
32
+ });
33
+ console.log("");
34
+ }
40
35
 
41
- if (!url) {
42
- // Re-probe all services already in the manifest
43
- const manifest = readManifest(manifestFile);
44
- if (!manifest || !manifest.services || !manifest.services.length) {
45
- log.warn("No manifest found. Use: systemview connect <serviceId> <url>");
36
+ module.exports = async function connectService(target, { useManifest, force, connectedUrls, uiUrl } = {}) {
37
+ const uiSvc = await getUiSvc(uiUrl);
38
+
39
+ // connect (no args) connect to every service stored in UI
40
+ if (!target) {
41
+ if (!uiSvc) {
42
+ log.warn("SystemView UI not running. Start it first or provide a URL.");
43
+ return;
44
+ }
45
+ const projects = await uiSvc.getProjects();
46
+ const all = Object.entries(projects).flatMap(([pc, svcs]) =>
47
+ svcs.map((s) => ({ projectCode: pc, ...s }))
48
+ );
49
+ if (!all.length) {
50
+ log.warn("No services in UI store. Use: systemview connect <url>");
46
51
  return;
47
52
  }
48
- log.info(`Re-probing ${manifest.services.length} service(s)...`);
49
- for (const { serviceId: id, system } of manifest.services) {
53
+ log.info(`Connecting to ${all.length} stored service(s)...`);
54
+ const summary = [];
55
+ for (const { projectCode, serviceId, connectionData, serviceUrl } of all) {
56
+ const url = serviceUrl || (connectionData && connectionData.serviceUrl);
57
+ if (!url) continue;
50
58
  try {
51
- const connection = await probeService(system.connectionData.serviceUrl);
52
- writeManifest(manifestFile, connection);
53
- log.success(`${id} updated`);
59
+ if (force) {
60
+ await Client.loadService(url);
61
+ } else {
62
+ Client.createService(connectionData);
63
+ }
64
+ connectedUrls.add(url);
65
+ log.success(`${projectCode} › ${serviceId}`);
66
+ summary.push({ projectCode, serviceId, url });
54
67
  } catch (err) {
55
- log.error(`${id} failed: ${err.message}`);
68
+ log.error(`${serviceId} failed: ${err.message}`);
56
69
  }
57
70
  }
58
- printManifestSummary(readManifest(manifestFile));
71
+ printSummary(summary);
59
72
  return;
60
73
  }
61
74
 
62
- log.info(`Connecting to ${serviceId || "service"} @ ${url}...`);
75
+ const isUrl = /^https?:\/\//i.test(target);
76
+
77
+ // connect <projectCode> — look up from UI store
78
+ if (!isUrl) {
79
+ if (!uiSvc) {
80
+ log.warn("SystemView UI not running. Cannot look up project by code.");
81
+ return;
82
+ }
83
+ const services = await uiSvc.getServices(target);
84
+ if (!services || !services.length) {
85
+ log.warn(`No services found for project: ${target}`);
86
+ return;
87
+ }
88
+ log.info(`Connecting to ${services.length} service(s) for ${target}...`);
89
+ const summary = [];
90
+ for (const { serviceId, system } of services) {
91
+ const { serviceUrl } = system.connectionData;
92
+ try {
93
+ if (force) {
94
+ await Client.loadService(serviceUrl);
95
+ } else {
96
+ Client.createService(system.connectionData);
97
+ }
98
+ connectedUrls.add(serviceUrl);
99
+ log.success(`${target} › ${serviceId}`);
100
+ summary.push({ projectCode: target, serviceId, url: serviceUrl });
101
+ } catch (err) {
102
+ log.error(`${serviceId} failed: ${err.message}`);
103
+ }
104
+ }
105
+ printSummary(summary);
106
+ return;
107
+ }
108
+
109
+ // connect <url> [--manifest]
110
+ log.info(`Connecting to ${target}...`);
63
111
  try {
64
- const connection = await probeService(url);
65
- writeManifest(manifestFile, connection);
66
- printManifestSummary(readManifest(manifestFile));
112
+ if (useManifest) {
113
+ const svc = await Client.loadService(target);
114
+ let manifest = null;
115
+ try { manifest = await svc.Plugin.getManifest(); } catch {}
116
+ if (manifest && manifest.services && manifest.services.length) {
117
+ log.info(`Plugin manifest found: ${manifest.projectCode} (${manifest.services.length} service(s))`);
118
+ const summary = [];
119
+ for (const service of manifest.services) {
120
+ try {
121
+ Client.createService(service.system.connectionData);
122
+ connectedUrls.add(service.system.connectionData.serviceUrl);
123
+ await notifyUi(uiSvc, {
124
+ system: service.system,
125
+ projectCode: manifest.projectCode,
126
+ serviceId: service.serviceId,
127
+ specList: service.specList || { tests: [], docs: [] },
128
+ });
129
+ log.success(`${manifest.projectCode} › ${service.serviceId}`);
130
+ summary.push({ projectCode: manifest.projectCode, serviceId: service.serviceId, url: service.system.connectionData.serviceUrl });
131
+ } catch (err) {
132
+ log.error(`${service.serviceId} failed: ${err.message}`);
133
+ }
134
+ }
135
+ printSummary(summary);
136
+ } else {
137
+ log.warn("No plugin manifest found. Storing as connected-services.");
138
+ const connectionData = await HttpClient.request({ url: target });
139
+ const serviceId = serviceIdFromRoute(connectionData.route);
140
+ const project = {
141
+ system: { connectionData },
142
+ projectCode: "connected-services",
143
+ serviceId,
144
+ specList: { tests: [], docs: [] },
145
+ };
146
+ Client.createService(connectionData);
147
+ connectedUrls.add(target);
148
+ await notifyUi(uiSvc, project);
149
+ printSummary([{ projectCode: "connected-services", serviceId, url: target }]);
150
+ }
151
+ } else {
152
+ const connectionData = await HttpClient.request({ url: target });
153
+ if (!connectionData || !connectionData.SystemLynxService) {
154
+ throw new Error("No SystemLynx service found at " + target);
155
+ }
156
+ const serviceId = serviceIdFromRoute(connectionData.route);
157
+ const project = {
158
+ system: { connectionData },
159
+ projectCode: "connected-services",
160
+ serviceId,
161
+ specList: { tests: [], docs: [] },
162
+ };
163
+ Client.createService(connectionData);
164
+ connectedUrls.add(target);
165
+ await notifyUi(uiSvc, project);
166
+ log.success(`connected-services › ${serviceId}`);
167
+ printSummary([{ projectCode: "connected-services", serviceId, url: target }]);
168
+ }
67
169
  } catch (err) {
68
170
  log.error(`Connection failed: ${err.message}`);
69
171
  }
70
172
  };
71
-
72
- function printManifestSummary(manifest) {
73
- if (!manifest) return;
74
- log.plain(`\n Project: ${manifest.projectCode}`);
75
- log.plain(` Services:`);
76
- (manifest.services || []).forEach(({ serviceId, system }) => {
77
- log.plain(` - ${serviceId} @ ${system.connectionData.serviceUrl}`);
78
- });
79
- log.plain("");
80
- }
@@ -87,7 +87,4 @@ function createCookieHttpClient(extraHeaders = {}) {
87
87
  };
88
88
  }
89
89
 
90
- module.exports = {
91
- createCookieHttpClient,
92
- createCookieClient: (extraHeaders = {}) => createClient(createCookieHttpClient(extraHeaders)),
93
- };
90
+ module.exports = { createCookieHttpClient };
package/cli/index.js CHANGED
@@ -13,17 +13,39 @@ const listTests = require("./listTests");
13
13
  const appIsRunning = require("./appIsRunning");
14
14
  const openBrowser = require("./openBrowser");
15
15
  const connectService = require("./connectService");
16
+ const manifestCommands = require("./manifest");
16
17
  const probe = require("./probe");
17
- const { HttpClient } = require("systemlynx");
18
- const { createCookieClient } = require("./cookieClient");
18
+ const logsCommand = require("./logs");
19
+ const { HttpClient, createClient } = require("systemlynx");
20
+ const { createCookieHttpClient } = require("./cookieClient");
21
+ const cookieHttpClient = createCookieHttpClient();
22
+ const Client = createClient(cookieHttpClient);
19
23
 
20
24
  const DEFAULT_PORT = 3000;
21
25
  const VERSION = require("../package.json").version;
26
+ const UI_URL = `http://localhost:${DEFAULT_PORT}`;
27
+
28
+ const MANIFEST_FILE = flags.manifest || path.join(process.cwd(), "systemview.manifest.json");
29
+ const connectedUrls = new Set();
30
+
31
+ function loadManifest() {
32
+ if (!fs.existsSync(MANIFEST_FILE)) return;
33
+ try {
34
+ const manifest = JSON.parse(fs.readFileSync(MANIFEST_FILE, "utf8"));
35
+ const services = manifest.services || [manifest];
36
+ for (const { system } of services) {
37
+ if (system && system.connectionData) {
38
+ Client.createService(system.connectionData);
39
+ connectedUrls.add(system.connectionData.serviceUrl);
40
+ }
41
+ }
42
+ } catch {}
43
+ }
22
44
 
23
45
  async function startApp() {
24
46
  const port = isNaN(input[1]) ? DEFAULT_PORT : Number(input[1]);
25
47
  try {
26
- await launchApp(port, { interactive: true });
48
+ await launchApp(port, { interactive: true, connectedUrls });
27
49
  } catch (error) {
28
50
  log.error("Launch failed: " + error.message);
29
51
  }
@@ -32,13 +54,11 @@ async function startApp() {
32
54
  async function startTest() {
33
55
  const project_code = input[1];
34
56
  const namespace = input[2];
35
- const url = `http://localhost:${DEFAULT_PORT}`;
36
57
  try {
37
58
  await launchApp(DEFAULT_PORT);
38
- const exitCode = await runTests(url, project_code, namespace, {
59
+ const exitCode = await runTests(UI_URL, project_code, namespace, {
39
60
  json: flags.json,
40
61
  verbose: flags.verbose,
41
- manifest: flags.manifest,
42
62
  headers: flags.headers,
43
63
  bail: flags.bail,
44
64
  dryRun: flags.dryRun,
@@ -56,41 +76,34 @@ async function startTest() {
56
76
  async function list() {
57
77
  const project_code = input[1];
58
78
  const namespace = input[2];
59
- const url = `http://localhost:${DEFAULT_PORT}`;
60
- const api = `${url}/systemview/api`;
79
+ const api = `${UI_URL}/systemview/api`;
61
80
  if (!(await appIsRunning(api))) {
62
81
  await launchApp(DEFAULT_PORT);
63
82
  }
64
- await listTests(url, project_code, namespace, { manifest: flags.manifest, verbose: flags.verbose });
83
+ await listTests(UI_URL, project_code, namespace, {
84
+ connectedUrls,
85
+ verbose: flags.verbose,
86
+ });
65
87
  process.exit(0);
66
88
  }
67
89
 
68
90
  async function open() {
69
91
  const project_code = input[1];
70
92
  const namespace = input[2];
71
- const ui = `http://localhost:${DEFAULT_PORT}`;
72
- const api = `${ui}/systemview/api`;
93
+ const api = `${UI_URL}/systemview/api`;
73
94
  if (!(await appIsRunning(api))) {
74
95
  await launchApp(DEFAULT_PORT);
75
96
  }
76
97
 
77
98
  let connectedServices = [];
78
99
  if (namespace && project_code) {
79
- const manifestFile = flags.manifest || path.join(process.cwd(), "systemview.manifest.json");
80
- if (fs.existsSync(manifestFile)) {
81
- try {
82
- const raw = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
83
- connectedServices = raw.services ? raw.services : [raw];
84
- } catch {}
85
- } else {
86
- try {
87
- const { SystemView } = await createCookieClient().loadService(api);
88
- connectedServices = (await SystemView.getServices(project_code)) || [];
89
- } catch {}
90
- }
100
+ try {
101
+ const { SystemView } = await Client.loadService(api);
102
+ connectedServices = (await SystemView.getServices(project_code)) || [];
103
+ } catch {}
91
104
  }
92
105
 
93
- openBrowser(ui, project_code, namespace, connectedServices);
106
+ openBrowser(UI_URL, project_code, namespace, connectedServices);
94
107
  process.exit(0);
95
108
  }
96
109
 
@@ -121,13 +134,17 @@ async function quitApp() {
121
134
  }
122
135
 
123
136
  (async () => {
137
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
138
+ console.log(VERSION);
139
+ process.exit(0);
140
+ }
141
+
124
142
  init();
143
+ loadManifest();
144
+
125
145
  const command = input[0];
126
146
 
127
- if (process.argv.includes("--version")) {
128
- console.log(VERSION);
129
- process.exit(0);
130
- } else if (command === "open") {
147
+ if (command === "open") {
131
148
  await open();
132
149
  } else if (command === "list") {
133
150
  await list();
@@ -138,15 +155,54 @@ async function quitApp() {
138
155
  } else if (["exit", "q", "shutdown", "stop"].includes(command)) {
139
156
  await quitApp();
140
157
  } else if (command === "connect") {
141
- await connectService(input[1], input[2], { manifest: flags.manifest });
158
+ const useManifest = process.argv.includes("--manifest");
159
+ await connectService(input[1], {
160
+ useManifest,
161
+ force: flags.force,
162
+ connectedUrls,
163
+ uiUrl: UI_URL,
164
+ });
165
+ process.exit(0);
166
+ } else if (command === "disconnect") {
167
+ await manifestCommands.disconnect(input[1], input[2], {
168
+ connectedUrls,
169
+ uiUrl: UI_URL,
170
+ });
171
+ process.exit(0);
172
+ } else if (command === "manifest") {
173
+ const sub = input[1];
174
+ if (sub === "save") {
175
+ log.warn("manifest save requires an interactive session. Run: systemview start");
176
+ } else if (sub === "clean") {
177
+ await manifestCommands.clean(MANIFEST_FILE);
178
+ } else {
179
+ log.warn("Usage: systemview manifest <save|clean>");
180
+ }
142
181
  process.exit(0);
143
182
  } else if (command === "probe") {
183
+ await launchApp(DEFAULT_PORT);
144
184
  const exitCode = await probe(input[1], input[2], {
145
185
  json: flags.json,
146
186
  manifest: flags.manifest,
147
187
  headers: flags.headers,
188
+ uiUrl: UI_URL,
148
189
  });
149
190
  process.exit(exitCode || 0);
191
+ } else if (command === "logs") {
192
+ await launchApp(DEFAULT_PORT);
193
+ await logsCommand(input[1], input[2], {
194
+ uiUrl: UI_URL,
195
+ level: flags.level,
196
+ limit: flags.limit,
197
+ clear: flags.clear,
198
+ json: flags.json,
199
+ current: flags.current,
200
+ verbose: flags.verbose,
201
+ filter: flags.filter,
202
+ or: flags.or,
203
+ include: flags.include,
204
+ });
205
+ process.exit(0);
150
206
  } else {
151
207
  await startApp();
152
208
  }
package/cli/launchApp.js CHANGED
@@ -3,7 +3,7 @@ const appIsRunning = require("./appIsRunning");
3
3
  const launchSystemView = require("../api");
4
4
  const startLineReader = require("./startLineReader");
5
5
 
6
- module.exports = async function launchApp(port, { interactive = false } = {}) {
6
+ module.exports = async function launchApp(port, { interactive = false, connectedUrls = new Set() } = {}) {
7
7
  const ui = `http://localhost:${port}`;
8
8
  const api = `${ui}/systemview/api`;
9
9
 
@@ -15,17 +15,18 @@ module.exports = async function launchApp(port, { interactive = false } = {}) {
15
15
 
16
16
  if (await appIsRunning(api)) {
17
17
  if (interactive) {
18
- log.info("SystemView is running from another terminal");
18
+ log.info(`SystemView already running on port ${port} — attaching.`);
19
19
  logConnection();
20
+ return startLineReader(ui, { connectedUrls });
20
21
  }
21
22
  return;
22
23
  }
23
24
 
24
- if (interactive) log.info("Launching...");
25
+ if (interactive) log.info(`Launching on port ${port}...`);
25
26
  await launchSystemView(port);
26
27
  logConnection();
27
28
 
28
29
  if (interactive) {
29
- return startLineReader(ui);
30
+ return startLineReader(ui, { connectedUrls });
30
31
  }
31
32
  };
package/cli/listTests.js CHANGED
@@ -1,19 +1,20 @@
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");
4
3
  const log = require("./logger");
5
4
  const chalk = require("chalk");
6
5
 
7
- module.exports = async function listTests(url, project_code, namespace, { manifest: manifestPath, verbose = false } = {}) {
6
+ const cookieHttpClient = createCookieHttpClient();
7
+ const Client = createClient(cookieHttpClient);
8
+
9
+ module.exports = async function listTests(url, project_code, namespace, { connectedUrls = new Set(), verbose = false } = {}) {
8
10
  const api = `${url}/systemview/api`;
9
- const Client = createCookieClient();
10
11
 
11
12
  if (!project_code) {
12
- await listAllProjects(api, Client, verbose);
13
+ await listAllProjects(api, Client, verbose, connectedUrls);
13
14
  return;
14
15
  }
15
16
 
16
- const connectedServices = await loadServices(api, project_code, manifestPath, Client);
17
+ const connectedServices = await loadServices(api, project_code, Client);
17
18
  if (!connectedServices.length) {
18
19
  log.warn("No connected services found for project: " + project_code);
19
20
  return;
@@ -30,7 +31,7 @@ module.exports = async function listTests(url, project_code, namespace, { manife
30
31
 
31
32
  let testList = [];
32
33
  try {
33
- const svc = createCookieClient().createService(system.connectionData);
34
+ const svc = Client.createService(system.connectionData);
34
35
  testList = await svc.Plugin.getTests();
35
36
  } catch {
36
37
  console.log(`${servicePrefix}${chalk.dim(serviceId)} ${chalk.dim("(could not load tests)")}`);
@@ -93,7 +94,7 @@ module.exports = async function listTests(url, project_code, namespace, { manife
93
94
  console.log("");
94
95
  };
95
96
 
96
- async function listAllProjects(api, Client, verbose = false) {
97
+ async function listAllProjects(api, Client, verbose = false, connectedUrls = new Set()) {
97
98
  let projects;
98
99
  try {
99
100
  const { SystemView } = await Client.loadService(api);
@@ -120,9 +121,11 @@ async function listAllProjects(api, Client, verbose = false) {
120
121
  const isLastService = si === services.length - 1;
121
122
  const servicePrefix = isLastService ? " └── " : " ├── ";
122
123
  const childPad = isLastService ? " " : " │ ";
124
+ const connected = connectedUrls.has(serviceUrl);
125
+ const connectedMark = connected ? chalk.green(" ●") : chalk.dim(" ○");
123
126
 
124
127
  if (verbose && connectionData && connectionData.modules) {
125
- console.log(`${servicePrefix}${chalk.bold(serviceId)} ${chalk.dim(serviceUrl)}`);
128
+ console.log(`${servicePrefix}${chalk.bold(serviceId)}${connectedMark} ${chalk.dim(serviceUrl)}`);
126
129
  const mods = connectionData.modules.filter((m) => m.name !== "Plugin");
127
130
  mods.forEach((mod, mi) => {
128
131
  const isLastMod = mi === mods.length - 1;
@@ -137,7 +140,7 @@ async function listAllProjects(api, Client, verbose = false) {
137
140
  });
138
141
  });
139
142
  } else {
140
- console.log(`${servicePrefix}${serviceId} ${chalk.dim(serviceUrl)}`);
143
+ console.log(`${servicePrefix}${serviceId}${connectedMark} ${chalk.dim(serviceUrl)}`);
141
144
  }
142
145
  });
143
146
  if (pi < codes.length - 1) console.log("");
@@ -145,14 +148,7 @@ async function listAllProjects(api, Client, verbose = false) {
145
148
  console.log("");
146
149
  }
147
150
 
148
- async function loadServices(api, project_code, manifestPath, Client) {
149
- const manifestFile = manifestPath || path.join(process.cwd(), "systemview.manifest.json");
150
- if (fs.existsSync(manifestFile)) {
151
- try {
152
- const raw = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
153
- return raw.services ? raw.services : [raw];
154
- } catch {}
155
- }
151
+ async function loadServices(api, project_code, Client) {
156
152
  try {
157
153
  const { SystemView } = await Client.loadService(api);
158
154
  return (await SystemView.getServices(project_code)) || [];