systemview 2.2.5 → 2.3.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.
@@ -3,6 +3,7 @@ const { createCookieHttpClient } = require("./cookieClient");
3
3
  const cookieHttpClient = createCookieHttpClient();
4
4
  const Client = createClient(cookieHttpClient);
5
5
  const log = require("./logger");
6
+ const manifestCommands = require("./manifest");
6
7
 
7
8
  async function getUiSvc(uiUrl) {
8
9
  try {
@@ -33,7 +34,7 @@ function printSummary(entries) {
33
34
  console.log("");
34
35
  }
35
36
 
36
- module.exports = async function connectService(target, { useManifest, force, connectedUrls, uiUrl } = {}) {
37
+ module.exports = async function connectService(target, { useManifest, save, force, connectedUrls, uiUrl } = {}) {
37
38
  const uiSvc = await getUiSvc(uiUrl);
38
39
 
39
40
  // connect (no args) — connect to every service stored in UI
@@ -133,6 +134,17 @@ module.exports = async function connectService(target, { useManifest, force, con
133
134
  }
134
135
  }
135
136
  printSummary(summary);
137
+ if (save) {
138
+ // --save persists THIS connection to the local manifest. The subject is the connection
139
+ // (the services); the applied headers + captured session cookie tag along via save().
140
+ const services = manifest.services.map((s) => ({
141
+ projectCode: manifest.projectCode,
142
+ serviceId: s.serviceId,
143
+ system: s.system,
144
+ specList: s.specList || { tests: [], docs: [] },
145
+ }));
146
+ manifestCommands.save(services);
147
+ }
136
148
  } else {
137
149
  log.warn("No plugin manifest found. Storing as connected-services.");
138
150
  const connectionData = await HttpClient.request({ url: target });
@@ -165,6 +177,11 @@ module.exports = async function connectService(target, { useManifest, force, con
165
177
  await notifyUi(uiSvc, project);
166
178
  log.success(`connected-services › ${serviceId}`);
167
179
  printSummary([{ projectCode: "connected-services", serviceId, url: target }]);
180
+ if (save) {
181
+ manifestCommands.save([
182
+ { projectCode: "connected-services", serviceId, system: { connectionData }, specList: { tests: [], docs: [] } },
183
+ ]);
184
+ }
168
185
  }
169
186
  } catch (err) {
170
187
  log.error(`Connection failed: ${err.message}`);
@@ -1,55 +1,25 @@
1
1
  const axios = require("axios").default;
2
2
  const FormData = require("form-data");
3
- const fs = require("fs");
4
3
  const path = require("path");
5
- const { createClient } = require("systemlynx");
4
+ const { headersFor, captureCookie } = require("./manifestHeaders");
6
5
 
7
- const COOKIE_FILE = path.resolve(process.cwd(), "systemview.cookies.json");
8
-
9
- function readCookies() {
10
- try {
11
- return JSON.parse(fs.readFileSync(COOKIE_FILE, "utf8"));
12
- } catch {
13
- return {};
14
- }
15
- }
16
-
17
- function captureCookies(setCookieHeaders) {
18
- if (!setCookieHeaders || !setCookieHeaders.length) return;
19
- const existing = readCookies();
20
- let changed = false;
21
- setCookieHeaders.forEach((header) => {
22
- const [pair] = header.split(";");
23
- const eqIdx = pair.indexOf("=");
24
- if (eqIdx === -1) return;
25
- const name = pair.slice(0, eqIdx).trim();
26
- const value = pair.slice(eqIdx + 1).trim();
27
- if (name) {
28
- existing[name] = value;
29
- changed = true;
30
- }
31
- });
32
- if (changed) fs.writeFileSync(COOKIE_FILE, JSON.stringify(existing, null, 2));
33
- }
34
-
35
- function cookieHeader() {
36
- const cookies = readCookies();
37
- const pairs = Object.entries(cookies)
38
- .map(([k, v]) => `${k}=${v}`)
39
- .join("; ");
40
- return pairs || null;
41
- }
6
+ // HTTP client for the SystemLynx CLI. Per request it attaches the manifest's headers for
7
+ // the target origin (auth tokens, cookies, any scheme — see manifestHeaders.js) and folds
8
+ // any Set-Cookie response back into that store. `extraHeaders` (e.g. --header flags) win on
9
+ // conflict. Cookies + tokens now live in the manifest; there is no separate cookie jar.
42
10
 
43
11
  function createCookieHttpClient(extraHeaders = {}) {
44
12
  return {
45
13
  request: async ({ method = "get", url, body: data, headers }) => {
46
14
  method = method.toLowerCase();
47
- const cookie = cookieHeader();
48
- headers = { ...headers, ...extraHeaders };
49
- if (cookie) headers.Cookie = cookie;
15
+ // Manifest headers are the base/default; caller-set headers (systemlynx setHeaders, i.e. the
16
+ // CLI `--header` flag) and constructor extraHeaders override them by name. (Matches the upload
17
+ // path below the request path previously had these flipped, letting the manifest win over
18
+ // an explicit --header.)
19
+ headers = { ...headersFor(url), ...headers, ...extraHeaders };
50
20
  try {
51
21
  const res = await axios({ url, method, headers, data });
52
- captureCookies(res.headers["set-cookie"]);
22
+ captureCookie(url, res.headers["set-cookie"]);
53
23
  if (res.status >= 400) throw res.data;
54
24
  return res.data;
55
25
  } catch (error) {
@@ -65,16 +35,15 @@ function createCookieHttpClient(extraHeaders = {}) {
65
35
  if (file) form.append("file", file, path.basename(file.path));
66
36
  if (files) files.forEach((f) => form.append("files", f, path.basename(f.path)));
67
37
  if (__arguments) form.append("__arguments", JSON.stringify(__arguments));
68
- const cookie = cookieHeader();
69
38
  const allHeaders = {
70
- ...extraHeaders,
71
- ...(cookie ? { Cookie: cookie } : {}),
39
+ ...headersFor(url),
72
40
  ...headers,
41
+ ...extraHeaders,
73
42
  "Content-Type": "multipart/form-data",
74
43
  };
75
44
  try {
76
45
  const res = await axios.post(url, form, { headers: allHeaders });
77
- captureCookies(res.headers["set-cookie"]);
46
+ captureCookie(url, res.headers["set-cookie"]);
78
47
  if (res.status >= 400) throw res.data;
79
48
  return res.data;
80
49
  } catch (error) {
package/cli/index.js CHANGED
@@ -25,6 +25,17 @@ const DEFAULT_PORT = 3000;
25
25
  const VERSION = require("../package.json").version;
26
26
  const UI_URL = `http://localhost:${DEFAULT_PORT}`;
27
27
 
28
+ // process.exit() truncates buffered stdout when stdout is a pipe (agents/CI, and large `--json`
29
+ // output). Drain stdout + stderr first, then exit — otherwise big results get cut off mid-stream.
30
+ function flushAndExit(code = 0) {
31
+ let pending = 2;
32
+ const done = () => {
33
+ if (--pending === 0) process.exit(code);
34
+ };
35
+ process.stdout.write("", done);
36
+ process.stderr.write("", done);
37
+ }
38
+
28
39
  const MANIFEST_FILE =
29
40
  flags.manifest || path.join(process.cwd(), "systemview.manifest.json");
30
41
  const connectedUrls = new Set();
@@ -80,10 +91,10 @@ async function startTest() {
80
91
  index: flags.index,
81
92
  skip: flags.skip,
82
93
  });
83
- process.exit(exitCode);
94
+ flushAndExit(exitCode);
84
95
  } catch (error) {
85
96
  log.error("Error executing tests: " + error.message);
86
- process.exit(1);
97
+ flushAndExit(1);
87
98
  }
88
99
  }
89
100
 
@@ -99,7 +110,7 @@ async function list() {
99
110
  verbose: flags.verbose,
100
111
  json: flags.json,
101
112
  });
102
- process.exit(0);
113
+ flushAndExit(0);
103
114
  }
104
115
 
105
116
  async function open() {
@@ -119,7 +130,7 @@ async function open() {
119
130
  }
120
131
 
121
132
  openBrowser(UI_URL, project_code, namespace, connectedServices);
122
- process.exit(0);
133
+ flushAndExit(0);
123
134
  }
124
135
 
125
136
  async function quitApp() {
@@ -136,18 +147,18 @@ async function quitApp() {
136
147
  log.success("SystemView shutdown successful!");
137
148
  } else {
138
149
  log.error("Remote shutdown failed!");
139
- process.exit(1);
150
+ flushAndExit(1);
140
151
  }
141
152
  } else {
142
153
  log.warn(`No SystemView instance found @ ${api}`);
143
154
  }
144
- process.exit(0);
155
+ flushAndExit(0);
145
156
  }
146
157
 
147
158
  (async () => {
148
159
  if (process.argv.includes("--version") || process.argv.includes("-v")) {
149
160
  console.log(VERSION);
150
- process.exit(0);
161
+ flushAndExit(0);
151
162
  }
152
163
 
153
164
  init();
@@ -168,17 +179,18 @@ async function quitApp() {
168
179
  const useManifest = process.argv.includes("--manifest");
169
180
  await connectService(input[1], {
170
181
  useManifest,
182
+ save: process.argv.includes("--save"),
171
183
  force: flags.force,
172
184
  connectedUrls,
173
185
  uiUrl: UI_URL,
174
186
  });
175
- process.exit(0);
187
+ flushAndExit(0);
176
188
  } else if (command === "disconnect") {
177
189
  await manifestCommands.disconnect(input[1], input[2], {
178
190
  connectedUrls,
179
191
  uiUrl: UI_URL,
180
192
  });
181
- process.exit(0);
193
+ flushAndExit(0);
182
194
  } else if (command === "manifest") {
183
195
  const sub = input[1];
184
196
  if (sub === "save") {
@@ -188,7 +200,7 @@ async function quitApp() {
188
200
  } else {
189
201
  log.warn("Usage: systemview manifest <save|clean>");
190
202
  }
191
- process.exit(0);
203
+ flushAndExit(0);
192
204
  } else if (command === "probe") {
193
205
  await launchApp(DEFAULT_PORT);
194
206
  const exitCode = await probe(input[1], input[2], {
@@ -197,7 +209,7 @@ async function quitApp() {
197
209
  headers: flags.headers,
198
210
  uiUrl: UI_URL,
199
211
  });
200
- process.exit(exitCode || 0);
212
+ flushAndExit(exitCode || 0);
201
213
  } else if (command === "logs" || command === "log") {
202
214
  await launchApp(DEFAULT_PORT);
203
215
  await logsCommand(input[1], input[2], {
@@ -216,7 +228,7 @@ async function quitApp() {
216
228
  saved: flags.saved,
217
229
  saveLimit: flags.saveLimit,
218
230
  });
219
- if (!flags.follow) process.exit(0);
231
+ if (!flags.follow) flushAndExit(0);
220
232
  } else {
221
233
  await startApp();
222
234
  }
package/cli/manifest.js CHANGED
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { HttpClient, createClient } = require("systemlynx");
4
4
  const { createCookieHttpClient } = require("./cookieClient");
5
+ const { getHeaders } = require("./manifestHeaders");
5
6
  const cookieHttpClient = createCookieHttpClient();
6
7
  const Client = createClient(cookieHttpClient);
7
8
  const log = require("./logger");
@@ -17,7 +18,7 @@ async function getUiSvc(uiUrl) {
17
18
  }
18
19
  }
19
20
 
20
- module.exports.save = function saveManifest(manifestServices, manifestFile = DEFAULT_MANIFEST, probeHeaders = {}) {
21
+ module.exports.save = function saveManifest(manifestServices, manifestFile = DEFAULT_MANIFEST) {
21
22
  if (!manifestServices || !manifestServices.length) {
22
23
  log.warn("No services in session to save.");
23
24
  return;
@@ -32,13 +33,19 @@ module.exports.save = function saveManifest(manifestServices, manifestFile = DEF
32
33
  log.warn("Multiple project codes in session — saving first project only: " + projectCodes[0]);
33
34
  }
34
35
  const projectCode = projectCodes[0];
36
+ const headers = getHeaders();
35
37
  const manifest = {
36
38
  projectCode,
37
39
  services: byProject[projectCode],
38
- ...(Object.keys(probeHeaders).length && { probeHeaders }),
40
+ ...(headers && Object.keys(headers).length && { headers }),
39
41
  };
40
42
  fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2));
41
- log.success(`Manifest saved to ${manifestFile} (${manifest.services.length} service(s))`);
43
+ const headerOrigins = Object.keys(headers || {});
44
+ const cookieOrigins = headerOrigins.filter((o) => headers[o] && headers[o].Cookie);
45
+ const parts = [`${manifest.services.length} service(s)`];
46
+ if (headerOrigins.length) parts.push(`headers for ${headerOrigins.length} origin(s)`);
47
+ if (cookieOrigins.length) parts.push(`cookies for ${cookieOrigins.length} origin(s)`);
48
+ log.success(`Manifest saved to ${manifestFile} — ${parts.join(", ")}`);
42
49
  };
43
50
 
44
51
  module.exports.clean = async function cleanManifest(manifestFile = DEFAULT_MANIFEST) {
@@ -0,0 +1,117 @@
1
+ // Manifest-backed request headers (RFC-010 Phase 1).
2
+ //
3
+ // Headers live in the manifest, indexed by URL origin. Each value is either a literal
4
+ // ("Bearer abc") or a file-pointer ("@./token") that keeps the secret out of the manifest.
5
+ // Cookies are just the "Cookie" header — a captured Set-Cookie folds into the same store.
6
+ //
7
+ // Read on load, written only on `save`. This module holds the manifest's `headers` in
8
+ // memory for the life of the CLI process (shared across every command via Node's module
9
+ // cache), so a cookie captured by one request is re-sent by the next, and `save` persists
10
+ // whatever accumulated.
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+
15
+ const MANIFEST_FILE = path.join(process.cwd(), "systemview.manifest.json");
16
+
17
+ // { "<origin>": { "Header-Name": "value" | "@file", "Cookie": "a=1; b=2" } }
18
+ // This is the ONLY header store — user-authored, indexed by service URL origin. The plugin never
19
+ // writes headers; there is no separate project-level default (the old `probeHeaders` is gone).
20
+ let headers = null;
21
+
22
+ // One-time, de-duplicated notices surfaced by CLI commands so the user gets confirmation
23
+ // that something happened ("using headers for X", "captured cookie for X"). Drained via takeNotices().
24
+ const _notified = new Set();
25
+ const _notices = [];
26
+ function note(key, msg) {
27
+ if (_notified.has(key)) return;
28
+ _notified.add(key);
29
+ _notices.push(msg);
30
+ }
31
+
32
+ function load() {
33
+ if (headers) return headers;
34
+ headers = {};
35
+ try {
36
+ const m = JSON.parse(fs.readFileSync(MANIFEST_FILE, "utf8"));
37
+ if (m && m.headers && typeof m.headers === "object") headers = m.headers;
38
+ } catch {
39
+ /* no manifest / unreadable — start empty */
40
+ }
41
+ return headers;
42
+ }
43
+
44
+ function originOf(url) {
45
+ try {
46
+ return new URL(url).origin;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // "@path" → file contents (trimmed); anything else returned as-is.
53
+ function deref(value) {
54
+ if (typeof value === "string" && value.startsWith("@")) {
55
+ try {
56
+ return fs.readFileSync(path.resolve(process.cwd(), value.slice(1)), "utf8").trim();
57
+ } catch {
58
+ return "";
59
+ }
60
+ }
61
+ return value;
62
+ }
63
+
64
+ // Resolved header name→value pairs for a request URL, matched by origin.
65
+ function headersFor(url) {
66
+ const origin = originOf(url);
67
+ load();
68
+ const bucket = headers[origin] || {};
69
+ const out = {};
70
+ for (const [name, raw] of Object.entries(bucket)) {
71
+ const value = deref(raw);
72
+ if (value != null && value !== "") out[name] = value;
73
+ }
74
+ const names = Object.keys(out);
75
+ if (names.length) note("attach:" + origin, `using ${names.length} manifest header(s) for ${origin}`);
76
+ return out;
77
+ }
78
+
79
+ // Fold Set-Cookie response headers into the in-memory Cookie header for the url's origin.
80
+ function captureCookie(url, setCookieHeaders) {
81
+ if (!setCookieHeaders || !setCookieHeaders.length) return;
82
+ const origin = originOf(url);
83
+ if (!origin) return;
84
+ const store = load();
85
+ if (!store[origin]) store[origin] = {};
86
+
87
+ const jar = {};
88
+ const existing = store[origin].Cookie;
89
+ if (typeof existing === "string") {
90
+ existing.split(";").forEach((p) => {
91
+ const i = p.indexOf("=");
92
+ if (i > -1) jar[p.slice(0, i).trim()] = p.slice(i + 1).trim();
93
+ });
94
+ }
95
+ setCookieHeaders.forEach((h) => {
96
+ const [pair] = h.split(";");
97
+ const i = pair.indexOf("=");
98
+ if (i > -1) jar[pair.slice(0, i).trim()] = pair.slice(i + 1).trim();
99
+ });
100
+ store[origin].Cookie = Object.entries(jar)
101
+ .map(([k, v]) => `${k}=${v}`)
102
+ .join("; ");
103
+ note("cookie:" + origin, `captured cookie for ${origin}`);
104
+ }
105
+
106
+ // The live headers map — for `save` to persist. (Not dereferenced: @file pointers and
107
+ // literals are written back as authored; captured cookies are written as literal values.)
108
+ function getHeaders() {
109
+ return load();
110
+ }
111
+
112
+ // Drain the accumulated one-time notices (for a command to print as confirmations).
113
+ function takeNotices() {
114
+ return _notices.splice(0, _notices.length);
115
+ }
116
+
117
+ module.exports = { headersFor, captureCookie, getHeaders, takeNotices, MANIFEST_FILE };
package/cli/probe.js CHANGED
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { createClient } = require("systemlynx");
4
4
  const { createCookieHttpClient } = require("./cookieClient");
5
+ const { takeNotices } = require("./manifestHeaders");
5
6
  const log = require("./logger");
6
7
 
7
8
  const cookieHttpClient = createCookieHttpClient();
@@ -76,6 +77,7 @@ module.exports = async function probe(namespace, argsStr, { json = false, manife
76
77
  } else {
77
78
  log.success("result:");
78
79
  console.log(JSON.stringify(result, null, 2));
80
+ takeNotices().forEach((n) => log.info(n));
79
81
  }
80
82
  return 0;
81
83
  } catch (err) {
@@ -83,6 +85,7 @@ module.exports = async function probe(namespace, argsStr, { json = false, manife
83
85
  process.stdout.write(JSON.stringify({ serviceId, moduleName, methodName, args, error: err.message }, null, 2) + "\n");
84
86
  } else {
85
87
  log.error(err.message);
88
+ takeNotices().forEach((n) => log.info(n));
86
89
  }
87
90
  return 1;
88
91
  }
package/cli/utils/cli.js CHANGED
@@ -13,7 +13,9 @@ const HELP_TEXT = `
13
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
+ connect <url> --manifest --save Connect, then persist the connection (services + headers + cookie) to the manifest
16
17
  disconnect [projectCode] [serviceId] Remove a project or service from the UI store
18
+ manifest save Persist session manifest — services, auth headers, cookies [interactive]
17
19
  manifest clean Re-probe manifest entries, remove stale ones
18
20
  probe <ServiceId.Module.method> [args] Call a service method ad-hoc
19
21
  open [projectCode] [namespace] Open the browser UI
@@ -26,7 +28,9 @@ const HELP_TEXT = `
26
28
  --verbose test: full results and args for all phases; list: expand hierarchy
27
29
  --manifest connect: use plugin manifest to get real projectCode
28
30
  --manifest <path> probe: path to manifest file (default: ./systemview.manifest.json)
29
- --header "Name: Value" Extra request header (repeatable)
31
+ --header "Name: Value" Extra request header (repeatable; overrides manifest headers).
32
+ For a standing token, add it to the manifest "headers"
33
+ (literal or "@file") — see docs/cli.md.
30
34
  --skip <pattern> Exclude tests matching pattern (repeatable)
31
35
  --bail Stop after first failure
32
36
  --dry-run Print which tests would run without executing
@@ -42,6 +46,7 @@ const HELP_TEXT = `
42
46
  --filter missing=<field> → only entries where field is absent
43
47
  --or <field=value> logs: OR filter on a field (repeatable)
44
48
  --include <field> logs: include extra field as a column (repeatable)
49
+ --save connect: persist this connection to the manifest (headers + cookie tag along)
45
50
  --save [path] logs: append streamed entries to a local snapshot file
46
51
  --saved logs: read from local snapshot instead of live service
47
52
  --save-limit <n> logs: max entries to keep in snapshot (default 500)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "systemview",
3
3
  "description": "A documentation and testing suite for SystemLynx",
4
- "version": "2.2.5",
4
+ "version": "2.3.0",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
7
7
  "systemview": "cli/index.js"