visigraph 0.0.4 → 0.0.6

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/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  Command line for [VisiGraph](https://github.com/textologylabs/visigraph):
4
4
  architecture models you can play through.
5
5
 
6
- Early, pre-1.0. Today it validates a model; the journey player follows.
6
+ Early, pre-1.0. Today it validates a model. Next it builds a static,
7
+ playable site from one: no server, works as a file, a GitLab Pages site or
8
+ a Confluence embed.
7
9
 
8
10
  ```sh
9
11
  npm install -g visigraph
@@ -18,3 +20,29 @@ reference, with a path to each one:
18
20
  ```
19
21
  error: model.yaml:journeys[0].steps[2].endpoint: container 'auth-service' has no endpoint 'token'
20
22
  ```
23
+
24
+ ## Self-update
25
+
26
+ On an interactive launch the CLI checks the npm registry for a newer
27
+ version and offers to update. Saying yes installs the new version and
28
+ re-runs the command you typed on it. Nothing is ever installed without
29
+ that answer.
30
+
31
+ - Skipped entirely in CI, pipes and non-terminal shells.
32
+ - The network check uses the registry npm is configured with (so a
33
+ corporate mirror works), is cached for 24 hours, and gives up silently
34
+ after two seconds. A failed check never blocks or errors the command.
35
+ - Under `npx`, it only prints the newer version and suggests
36
+ `npx visigraph@latest`.
37
+ - `--version` and `--help` never check.
38
+
39
+ Disable it:
40
+
41
+ - per shell or container image: `VISIGRAPH_NO_UPDATE_CHECK=1`
42
+ - centrally, for locked-down or air-gapped environments, in
43
+ `~/.visigraph/config.yaml`:
44
+
45
+ ```yaml
46
+ update:
47
+ check: false
48
+ ```
package/dist/main.js CHANGED
@@ -3662,13 +3662,13 @@ var require_directives = __commonJS({
3662
3662
  onError("Verbatim tags must end with a >");
3663
3663
  return verbatim;
3664
3664
  }
3665
- const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
3666
- if (!suffix)
3665
+ const [, handle, suffix2] = source.match(/^(.*!)([^!]*)$/s);
3666
+ if (!suffix2)
3667
3667
  onError(`The ${source} tag has no suffix`);
3668
3668
  const prefix = this.tags[handle];
3669
3669
  if (prefix) {
3670
3670
  try {
3671
- return prefix + decodeURIComponent(suffix);
3671
+ return prefix + decodeURIComponent(suffix2);
3672
3672
  } catch (error) {
3673
3673
  onError(String(error));
3674
3674
  return null;
@@ -3956,36 +3956,38 @@ var require_Alias = __commonJS({
3956
3956
  if (node.anchor === this.source)
3957
3957
  found = node;
3958
3958
  }
3959
+ if (found && ctx) {
3960
+ const { anchors: anchors2, doc: doc2, maxAliasCount } = ctx;
3961
+ let data = anchors2.get(found);
3962
+ if (!data) {
3963
+ toJS.toJS(found, null, ctx);
3964
+ data = anchors2.get(found);
3965
+ }
3966
+ if (data?.res === void 0) {
3967
+ const msg = "This should not happen: Alias anchor was not resolved?";
3968
+ throw new ReferenceError(msg);
3969
+ }
3970
+ if (maxAliasCount >= 0) {
3971
+ data.count += 1;
3972
+ if (data.aliasCount === 0)
3973
+ data.aliasCount = getAliasCount(doc2, found, anchors2);
3974
+ if (data.count * data.aliasCount > maxAliasCount) {
3975
+ const msg = "Excessive alias count indicates a resource exhaustion attack";
3976
+ throw new ReferenceError(msg);
3977
+ }
3978
+ }
3979
+ }
3959
3980
  return found;
3960
3981
  }
3961
3982
  toJSON(_arg, ctx) {
3962
3983
  if (!ctx)
3963
3984
  return { source: this.source };
3964
- const { anchors: anchors2, doc, maxAliasCount } = ctx;
3965
- const source = this.resolve(doc, ctx);
3985
+ const source = this.resolve(ctx.doc, ctx);
3966
3986
  if (!source) {
3967
3987
  const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`;
3968
3988
  throw new ReferenceError(msg);
3969
3989
  }
3970
- let data = anchors2.get(source);
3971
- if (!data) {
3972
- toJS.toJS(source, null, ctx);
3973
- data = anchors2.get(source);
3974
- }
3975
- if (data?.res === void 0) {
3976
- const msg = "This should not happen: Alias anchor was not resolved?";
3977
- throw new ReferenceError(msg);
3978
- }
3979
- if (maxAliasCount >= 0) {
3980
- data.count += 1;
3981
- if (data.aliasCount === 0)
3982
- data.aliasCount = getAliasCount(doc, source, anchors2);
3983
- if (data.count * data.aliasCount > maxAliasCount) {
3984
- const msg = "Excessive alias count indicates a resource exhaustion attack";
3985
- throw new ReferenceError(msg);
3986
- }
3987
- }
3988
- return data.res;
3990
+ return ctx.anchors.get(source).res;
3989
3991
  }
3990
3992
  toString(ctx, _onComment, _onChompKeep) {
3991
3993
  const src = `*${this.source}`;
@@ -7987,37 +7989,38 @@ var require_resolve_flow_scalar = __commonJS({
7987
7989
  }
7988
7990
  if (badChar)
7989
7991
  onError(0, "BAD_SCALAR_START", `Plain value cannot start with ${badChar}`);
7990
- return foldLines(source);
7992
+ return unfoldLines(source);
7991
7993
  }
7992
7994
  function singleQuotedValue(source, onError) {
7993
7995
  if (source[source.length - 1] !== "'" || source.length === 1)
7994
7996
  onError(source.length, "MISSING_CHAR", "Missing closing 'quote");
7995
- return foldLines(source.slice(1, -1)).replace(/''/g, "'");
7997
+ return unfoldLines(source.slice(1, -1)).replace(/''/g, "'");
7996
7998
  }
7997
- function foldLines(source) {
7998
- let first, line;
7999
+ function unfoldLines(source) {
8000
+ const line = /(.*?)\r?\n/sy;
8001
+ let match = line.exec(source);
8002
+ if (!match)
8003
+ return source;
8004
+ let trimEnd, trimBoth;
7999
8005
  try {
8000
- first = new RegExp("(.*?)(?<![ ])[ ]*\r?\n", "sy");
8001
- line = new RegExp("[ ]*(.*?)(?:(?<![ ])[ ]*)?\r?\n", "sy");
8006
+ trimEnd = new RegExp("(?<![ ])[ ]+$");
8007
+ trimBoth = new RegExp("^[ ]+|(?<![ ])[ ]+$", "g");
8002
8008
  } catch {
8003
- first = /(.*?)[ \t]*\r?\n/sy;
8004
- line = /[ \t]*(.*?)[ \t]*\r?\n/sy;
8009
+ trimEnd = /[ \t]+$/;
8010
+ trimBoth = /^[ \t]+|[ \t]+$/g;
8005
8011
  }
8006
- let match = first.exec(source);
8007
- if (!match)
8008
- return source;
8009
- let res = match[1];
8012
+ let res = match[1].replace(trimEnd, "");
8010
8013
  let sep = " ";
8011
- let pos = first.lastIndex;
8012
- line.lastIndex = pos;
8014
+ let pos = line.lastIndex;
8013
8015
  while (match = line.exec(source)) {
8014
- if (match[1] === "") {
8016
+ const lm = match[1].replace(trimBoth, "");
8017
+ if (lm === "") {
8015
8018
  if (sep === "\n")
8016
8019
  res += sep;
8017
8020
  else
8018
8021
  sep = "\n";
8019
8022
  } else {
8020
- res += sep + match[1];
8023
+ res += sep + lm;
8021
8024
  sep = " ";
8022
8025
  }
8023
8026
  pos = line.lastIndex;
@@ -11126,6 +11129,101 @@ async function validate(file) {
11126
11129
  return EXIT.ok;
11127
11130
  }
11128
11131
 
11132
+ // src/update.ts
11133
+ var import_yaml2 = __toESM(require_dist(), 1);
11134
+ import { spawn } from "child_process";
11135
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
11136
+ import { homedir } from "os";
11137
+ import { join as join2 } from "path";
11138
+ import readline from "readline";
11139
+
11140
+ // src/progress.ts
11141
+ var CELLS = 6;
11142
+ var GLYPHS = {
11143
+ unicode: { filled: "\u25B0", empty: "\u25B1" },
11144
+ // ▰ ▱ black / white parallelogram
11145
+ ascii: { filled: "#", empty: ".", open: "[", close: "]" }
11146
+ };
11147
+ function renderProgress(done, total, opts = {}) {
11148
+ const cells = opts.cells ?? CELLS;
11149
+ const safeTotal = Math.max(total, 0);
11150
+ const safeDone = Math.min(Math.max(done, 0), safeTotal);
11151
+ const filled = safeTotal === 0 ? 0 : Math.round(safeDone / safeTotal * cells);
11152
+ return `${bar(filled, cells, opts.unicode ?? true)} ${safeDone}/${safeTotal}${suffix(opts.label)}`;
11153
+ }
11154
+ function renderSweep(tick, opts = {}) {
11155
+ const cells = opts.cells ?? CELLS;
11156
+ const unicode = opts.unicode ?? true;
11157
+ const g = unicode ? GLYPHS.unicode : GLYPHS.ascii;
11158
+ const pos = (tick % cells + cells) % cells;
11159
+ const body = Array.from({ length: cells }, (_, i) => i === pos ? g.filled : g.empty).join("");
11160
+ return `${unicode ? body : `${GLYPHS.ascii.open}${body}${GLYPHS.ascii.close}`}${suffix(opts.label)}`;
11161
+ }
11162
+ function bar(filled, cells, unicode) {
11163
+ const g = unicode ? GLYPHS.unicode : GLYPHS.ascii;
11164
+ const body = g.filled.repeat(filled) + g.empty.repeat(cells - filled);
11165
+ return unicode ? body : `${GLYPHS.ascii.open}${body}${GLYPHS.ascii.close}`;
11166
+ }
11167
+ function suffix(label) {
11168
+ return label ? ` ${label}` : "";
11169
+ }
11170
+ function supportsUnicode(env = process.env, platform = process.platform) {
11171
+ if (platform === "win32") {
11172
+ return Boolean(env.WT_SESSION || env.TERM_PROGRAM === "vscode" || env.ConEmuANSI === "ON");
11173
+ }
11174
+ if (env.TERM === "dumb") return false;
11175
+ const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
11176
+ return /utf-?8/i.test(locale) || locale === "";
11177
+ }
11178
+ var ProgressLine = class {
11179
+ constructor(out = process.stderr) {
11180
+ this.out = out;
11181
+ this.enabled = Boolean(out.isTTY);
11182
+ this.unicode = supportsUnicode();
11183
+ }
11184
+ out;
11185
+ timer;
11186
+ tick = 0;
11187
+ enabled;
11188
+ unicode;
11189
+ /** Animate a sweep with a label until `stop` or `finish` is called. */
11190
+ sweep(label, intervalMs = 120) {
11191
+ if (!this.enabled) return;
11192
+ this.clearTimer();
11193
+ this.draw(renderSweep(this.tick, { unicode: this.unicode, label }));
11194
+ this.timer = setInterval(() => {
11195
+ this.tick += 1;
11196
+ this.draw(renderSweep(this.tick, { unicode: this.unicode, label }));
11197
+ }, intervalMs);
11198
+ }
11199
+ /** Show `done` of `total` with a label. */
11200
+ update(done, total, label) {
11201
+ if (!this.enabled) return;
11202
+ this.clearTimer();
11203
+ this.draw(renderProgress(done, total, label === void 0 ? { unicode: this.unicode } : { unicode: this.unicode, label }));
11204
+ }
11205
+ /** Fill the bar, print the final label, and move to a new line. */
11206
+ finish(label) {
11207
+ if (!this.enabled) return;
11208
+ this.clearTimer();
11209
+ this.draw(renderProgress(CELLS, CELLS, { unicode: this.unicode, label }));
11210
+ this.out.write("\n");
11211
+ }
11212
+ /** Erase the line without a final message. */
11213
+ stop() {
11214
+ if (!this.enabled) return;
11215
+ this.clearTimer();
11216
+ this.out.write("\r\x1B[2K");
11217
+ }
11218
+ draw(text) {
11219
+ this.out.write(`\r\x1B[2K${text}`);
11220
+ }
11221
+ clearTimer() {
11222
+ if (this.timer) clearInterval(this.timer);
11223
+ this.timer = void 0;
11224
+ }
11225
+ };
11226
+
11129
11227
  // src/version.ts
11130
11228
  import { createRequire } from "module";
11131
11229
  var load = createRequire(import.meta.url);
@@ -11133,15 +11231,174 @@ var pkg = load("../package.json");
11133
11231
  var VERSION = pkg.version;
11134
11232
  var DESCRIPTION = pkg.description;
11135
11233
 
11234
+ // src/update.ts
11235
+ var PKG_NAME = "visigraph";
11236
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
11237
+ var FETCH_TIMEOUT_MS = 2e3;
11238
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
11239
+ var NO_UPDATE_ENV = "VISIGRAPH_NO_UPDATE_CHECK";
11240
+ var configDir = () => process.env.VISIGRAPH_HOME ?? join2(homedir(), ".visigraph");
11241
+ async function maybeUpdate() {
11242
+ const gate = shouldCheck({
11243
+ noUpdateEnv: process.env[NO_UPDATE_ENV] === "1",
11244
+ stdinTTY: Boolean(process.stdin.isTTY),
11245
+ stdoutTTY: Boolean(process.stdout.isTTY),
11246
+ configCheckDisabled: await configDisablesCheck()
11247
+ });
11248
+ if (!gate) return;
11249
+ const latest = await latestVersion();
11250
+ if (!latest || !isNewerVersion(latest, VERSION)) return;
11251
+ const viaNpx = invokedViaNpx({ npmCommand: process.env.npm_command, script: process.argv[1] ?? "" });
11252
+ console.log(`visigraph ${latest} is available; you have ${VERSION}.`);
11253
+ if (viaNpx) {
11254
+ console.log(`Run it with: npx ${PKG_NAME}@latest`);
11255
+ return;
11256
+ }
11257
+ if (!await confirm("Update now?")) return;
11258
+ const progress = new ProgressLine();
11259
+ progress.sweep(`installing visigraph ${latest}`);
11260
+ const result = await runInstall();
11261
+ if (!result.ok) {
11262
+ progress.stop();
11263
+ console.error("Update failed. Continuing with the current version.");
11264
+ if (result.output.trim()) console.error(result.output.trim());
11265
+ return;
11266
+ }
11267
+ progress.finish(`updated to ${latest}, relaunching`);
11268
+ await relaunch();
11269
+ }
11270
+ function shouldCheck(input) {
11271
+ if (input.noUpdateEnv) return false;
11272
+ if (input.configCheckDisabled) return false;
11273
+ if (!input.stdinTTY || !input.stdoutTTY) return false;
11274
+ return true;
11275
+ }
11276
+ function compareVersions(a, b) {
11277
+ const pa = a.split("-")[0].split(".").map(Number);
11278
+ const pb = b.split("-")[0].split(".").map(Number);
11279
+ for (let i = 0; i < 3; i++) {
11280
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
11281
+ if (d !== 0) return d;
11282
+ }
11283
+ return 0;
11284
+ }
11285
+ function isNewerVersion(latest, current) {
11286
+ return compareVersions(latest, current) > 0;
11287
+ }
11288
+ function invokedViaNpx(input) {
11289
+ return input.npmCommand === "exec" || /[\\/]_npx[\\/]/.test(input.script);
11290
+ }
11291
+ function registryFrom(input) {
11292
+ const fromEnv = input.env?.trim();
11293
+ if (fromEnv) return fromEnv.replace(/\/+$/, "");
11294
+ const line = input.npmrc?.split(/\r?\n/).find((l) => /^\s*registry\s*=/.test(l));
11295
+ const fromRc = line?.split("=").slice(1).join("=").trim();
11296
+ if (fromRc) return fromRc.replace(/\/+$/, "");
11297
+ return DEFAULT_REGISTRY;
11298
+ }
11299
+ function cacheIsFresh(entry, now, ttl = CACHE_TTL_MS) {
11300
+ return entry !== void 0 && typeof entry.latest === "string" && now - entry.checkedAt < ttl;
11301
+ }
11302
+ async function fetchLatestVersion(registry, fetchImpl = fetch) {
11303
+ try {
11304
+ const ctrl = new AbortController();
11305
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
11306
+ const res = await fetchImpl(`${registry}/${PKG_NAME}/latest`, {
11307
+ signal: ctrl.signal,
11308
+ headers: { "User-Agent": `${PKG_NAME}/${VERSION}`, accept: "application/json" }
11309
+ });
11310
+ clearTimeout(timer);
11311
+ if (!res.ok) return null;
11312
+ const data = await res.json();
11313
+ return typeof data.version === "string" ? data.version : null;
11314
+ } catch {
11315
+ return null;
11316
+ }
11317
+ }
11318
+ async function configDisablesCheck() {
11319
+ try {
11320
+ const text = await readFile2(join2(configDir(), "config.yaml"), "utf8");
11321
+ const cfg = (0, import_yaml2.parse)(text);
11322
+ return cfg?.update?.check === false;
11323
+ } catch {
11324
+ return false;
11325
+ }
11326
+ }
11327
+ async function latestVersion() {
11328
+ const cachePath = join2(configDir(), "update-check.json");
11329
+ const now = Date.now();
11330
+ try {
11331
+ const cached = JSON.parse(await readFile2(cachePath, "utf8"));
11332
+ if (cacheIsFresh(cached, now)) return cached.latest;
11333
+ } catch {
11334
+ }
11335
+ let npmrc;
11336
+ try {
11337
+ npmrc = await readFile2(join2(homedir(), ".npmrc"), "utf8");
11338
+ } catch {
11339
+ npmrc = void 0;
11340
+ }
11341
+ const registry = registryFrom({ env: process.env.npm_config_registry, npmrc });
11342
+ const latest = await fetchLatestVersion(registry);
11343
+ if (latest) {
11344
+ try {
11345
+ await mkdir(configDir(), { recursive: true });
11346
+ await writeFile(cachePath, JSON.stringify({ checkedAt: now, latest }));
11347
+ } catch {
11348
+ }
11349
+ }
11350
+ return latest;
11351
+ }
11352
+ function confirm(question) {
11353
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
11354
+ return new Promise((resolve2) => {
11355
+ rl.on("close", () => resolve2(false));
11356
+ rl.question(`${question} (y/N) `, (answer) => {
11357
+ resolve2(/^y(es)?$/i.test(answer.trim()));
11358
+ rl.close();
11359
+ });
11360
+ });
11361
+ }
11362
+ function runInstall() {
11363
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
11364
+ return new Promise((resolve2) => {
11365
+ const chunks = [];
11366
+ const child = spawn(npm, ["install", "-g", `${PKG_NAME}@latest`, "--no-fund", "--no-audit"], {
11367
+ stdio: ["ignore", "pipe", "pipe"],
11368
+ shell: false
11369
+ });
11370
+ child.stdout.on("data", (d) => chunks.push(d.toString()));
11371
+ child.stderr.on("data", (d) => chunks.push(d.toString()));
11372
+ child.on("exit", (code) => resolve2({ ok: code === 0, output: chunks.join("") }));
11373
+ child.on("error", (e) => resolve2({ ok: false, output: e.message }));
11374
+ });
11375
+ }
11376
+ function relaunch() {
11377
+ const script = process.argv[1];
11378
+ const args = process.argv.slice(2);
11379
+ return new Promise((_, reject) => {
11380
+ const child = spawn(process.execPath, [script, ...args], {
11381
+ stdio: "inherit",
11382
+ shell: false,
11383
+ env: { ...process.env, [NO_UPDATE_ENV]: "1" }
11384
+ });
11385
+ child.on("exit", (code) => process.exit(code ?? 0));
11386
+ child.on("error", reject);
11387
+ });
11388
+ }
11389
+
11136
11390
  // src/main.ts
11137
11391
  var program2 = new Command();
11138
- program2.name("visigraph").description(DESCRIPTION).version(VERSION, "-v, --version", "print the version and exit").helpOption("-h, --help", "print help and exit").helpCommand("help [command]", "print help for a command").showHelpAfterError("(run with --help for usage)").exitOverride();
11392
+ program2.name("visigraph").addHelpText("before", `${DESCRIPTION}
11393
+ `).version(VERSION, "-v, --version", "print the version and exit").helpOption("-h, --help", "print help and exit").helpCommand("help [command]", "print help for a command").showHelpAfterError("(run with --help for usage)").exitOverride();
11139
11394
  registerValidate(program2);
11395
+ var INSTANT = /* @__PURE__ */ new Set(["-v", "--version", "-h", "--help", "help"]);
11140
11396
  try {
11141
11397
  if (process.argv.length <= 2) {
11142
11398
  program2.outputHelp();
11143
11399
  process.exitCode = EXIT.usage;
11144
11400
  } else {
11401
+ if (!process.argv.slice(2).some((a) => INSTANT.has(a))) await maybeUpdate();
11145
11402
  await program2.parseAsync(process.argv);
11146
11403
  }
11147
11404
  } catch (e) {