@gjermundgaraba/upup 0.5.1 → 0.5.2

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
@@ -107,6 +107,8 @@ Agents can't choose the audit model or add credentials; set those in the TUI. Wi
107
107
  | `q` | Quit, after stopping a running update |
108
108
  | Ctrl+C | Quit immediately |
109
109
 
110
+ Text you mark with the mouse, such as an error message, is copied to the clipboard as you let go, and the status line says so.
111
+
110
112
  Details have four tabs. **About** shows what the package is: its provider, versions and declaration, plus a description, release dates, popularity, deprecations and links from its registry, cached for a day under your user cache directory. **Changes** shows the verified diff between the installed and offered versions without needing a model; for an app update, the release notes from the app's feed. **Report** and **Transcript** show the latest Audit's verdict and what the model did.
111
113
 
112
114
  When npm's allow-scripts policy blocks an update, upup asks before retrying: it names the blocked install scripts. For a global install, approving adds those packages to your allow list for that one command. For a project, npm accepts no allow list from the command line, so approving turns its strict check off for that one install, and any install script in the project's dependencies may run. If npm's strict check crashes instead (`ENOENT` on a `lib` folder, which happens with `vp add -g` and `strict-allow-scripts=true`), retrying turns the strict check off for that one command, and the popup says so. Saying no skips the update, unless upup was already installing a project's edits: those are written to its lockfile by then, so the update fails, saying npm blocked its install scripts.
@@ -3,8 +3,8 @@ import { Deferred, Effect, FiberSet, Stream } from "effect";
3
3
  import { spawn } from "node:child_process";
4
4
  import { homedir } from "node:os";
5
5
  import semver from "semver";
6
- import { createComponent, createElement, createTextNode, effect, insert, insertNode, memo, onAnimationTick, render, setProp, use, useKeyboard, usePaste, useTerminalDimensions } from "@opentui/solid";
7
- import { TextAttributes, createCliRenderer } from "@opentui/core";
6
+ import { createComponent, createElement, createTextNode, effect, insert, insertNode, memo, onAnimationTick, render, setProp, use, useKeyboard, usePaste, useSelectionHandler, useTerminalDimensions } from "@opentui/solid";
7
+ import { TextAttributes, createCliRenderer, createClipboard, createHostClipboard, createRendererClipboardAdapter } from "@opentui/core";
8
8
  import { For, Match, Show, Switch, createEffect, createMemo, createRenderEffect, createSignal, on, onCleanup, untrack } from "solid-js/dist/solid.js";
9
9
  // src/tui/text.ts
10
10
  const segmenter = new Intl.Segmenter();
@@ -1493,7 +1493,7 @@ const verdictIcons = {
1493
1493
  danger: "✗",
1494
1494
  "no-verdict": "?"
1495
1495
  };
1496
- const ran = /* @__PURE__ */ new Set([
1496
+ const ranTags = /* @__PURE__ */ new Set([
1497
1497
  "Completed",
1498
1498
  "Failed",
1499
1499
  "Unexpected"
@@ -1506,12 +1506,13 @@ const status = (state, row) => {
1506
1506
  busy: true
1507
1507
  };
1508
1508
  const audit = auditOf(state, row.pkg);
1509
- const outcome = state.outcomes[row.key];
1509
+ const last = state.ran[row.key];
1510
+ const outcome = last?.outcome;
1510
1511
  if (state.removed[row.key] !== void 0) return {
1511
1512
  text: "– removed",
1512
1513
  color: theme.muted
1513
1514
  };
1514
- if (outcome !== void 0 && (audit === void 0 || ran.has(outcome._tag))) {
1515
+ if (outcome !== void 0 && (audit === void 0 || ranTags.has(outcome._tag))) {
1515
1516
  if (outcome._tag === "Completed" && auditedAs(state, row.pkg) !== row.pkg) {
1516
1517
  const verdict = audit?._tag === "Done" && audit.result._tag !== "Failed" ? audit.result.report.verdict : void 0;
1517
1518
  return verdict === void 0 ? {
@@ -1522,7 +1523,7 @@ const status = (state, row) => {
1522
1523
  color: verdictColor(verdict)
1523
1524
  };
1524
1525
  }
1525
- const unremoved = state.actions[row.key] === "uninstall" && outcome._tag === "Failed";
1526
+ const unremoved = last?.action === "uninstall" && outcome._tag === "Failed";
1526
1527
  return {
1527
1528
  text: `${outcomeIcon(outcome)} ${unremoved ? "not removed" : outcomeLabel(outcome)}`,
1528
1529
  color: outcomeColor(outcome)
@@ -1590,7 +1591,7 @@ const flaggedFirst = (pending) => {
1590
1591
  const flagged = new Set(pending.flagged.map((entry) => entry.row.key));
1591
1592
  return [...pending.rows.filter((row) => flagged.has(row.key)), ...pending.rows.filter((row) => !flagged.has(row.key))];
1592
1593
  };
1593
- const markOf = (pending, row) => {
1594
+ const markOf = (pending, row, auditFirst) => {
1594
1595
  const verdict = pending.flagged.find((entry) => entry.row.key === row.key)?.verdict;
1595
1596
  const mixed = pending.unaudited.length > 0 && pending.unaudited.length < pending.rows.length;
1596
1597
  if (verdict === void 0 && pending.unauditable.includes(row.key)) {
@@ -1605,7 +1606,7 @@ const markOf = (pending, row) => {
1605
1606
  text: `${verdictIcons.ok} ${verdictLabels.ok}`,
1606
1607
  color: theme.ok
1607
1608
  };
1608
- const unread = pending.action === "update" && pending.unaudited.includes(row.key) && (mixed || !pending.auditFirst);
1609
+ const unread = pending.action === "update" && pending.unaudited.includes(row.key) && (mixed || !auditFirst);
1609
1610
  if (pending.action === "update" && pending.breaking.includes(row.key)) return {
1610
1611
  text: `${breakingMark}${unread ? " · not audited" : ""}`,
1611
1612
  color: theme.warn
@@ -1673,7 +1674,7 @@ const ConfirmPopup = (props) => {
1673
1674
  const version = (row) => removing() ? row.pkg.current : `→ ${row.pkg.target ?? ""}`;
1674
1675
  const layout = createMemo(() => {
1675
1676
  const rows = flaggedFirst(pending());
1676
- const marks = rows.map((row) => markOf(pending(), row));
1677
+ const marks = rows.map((row) => markOf(pending(), row, props.state.preferences.auditFirst));
1677
1678
  const versions = Math.min(16, widest$1(rows.map(version)));
1678
1679
  const verdicts = widest$1(marks.map((mark) => mark?.text ?? ""));
1679
1680
  const longest = widest$1(rows.map((row) => row.pkg.name));
@@ -2803,7 +2804,7 @@ const tabTitles = {
2803
2804
  report: "Report",
2804
2805
  transcript: "Transcript"
2805
2806
  };
2806
- const storedTranscript = (audit) => audit?._tag === "Done" && audit.result._tag === "Reported" ? audit.result.file : void 0;
2807
+ const storedTranscript = (audit) => audit?._tag === "Done" && audit.result._tag === "Reported" ? audit.result : void 0;
2807
2808
  const liveTranscript = (audit) => {
2808
2809
  if (audit?._tag === "Running") return audit.transcript;
2809
2810
  if (audit?._tag === "Done" && audit.result._tag === "Failed") return audit.result.transcript;
@@ -2829,13 +2830,15 @@ const Details = (props) => (() => {
2829
2830
  const pkg = () => row().pkg;
2830
2831
  const reviewed = () => auditedAs(props.state, pkg());
2831
2832
  const notices = () => props.state.providers.flatMap((entry) => entry.notices).filter((notice) => notice.rowKey === row().key);
2832
- const outcome = () => props.state.outcomes[row().key];
2833
+ const ran = () => props.state.ran[row().key];
2834
+ const outcome = () => ran()?.outcome;
2835
+ const removal = () => ran()?.action === "uninstall";
2833
2836
  const gone = () => props.state.removed[row().key] !== void 0;
2834
2837
  const nothing = () => gone() ? "it was removed" : unauditable(reviewed());
2835
2838
  const failure = () => {
2836
2839
  const latest = outcome();
2837
2840
  if (latest?._tag !== "Failed" && latest?._tag !== "Unexpected") return;
2838
- const what = props.state.actions[row().key] === "uninstall" ? "Removal" : "Update";
2841
+ const what = removal() ? "Removal" : "Update";
2839
2842
  const told = describeOutcome(latest);
2840
2843
  const whole = `✗ ${what} ${told.charAt(0).toLowerCase()}${told.slice(1)}`;
2841
2844
  const room = props.width - 4;
@@ -2849,7 +2852,7 @@ const Details = (props) => (() => {
2849
2852
  return `Updated from ${reviewed().current} · audited before installing`;
2850
2853
  };
2851
2854
  const updateLabel = (latest) => {
2852
- if (props.state.actions[row().key] === "uninstall") return "Removal";
2855
+ if (removal()) return "Removal";
2853
2856
  if (latest._tag === "Completed") return "Updated";
2854
2857
  return "Update";
2855
2858
  };
@@ -3172,8 +3175,8 @@ const Details = (props) => (() => {
3172
3175
  return auditOf(props.state, pkg())?._tag === "Running";
3173
3176
  },
3174
3177
  get stored() {
3175
- const file = storedTranscript(auditOf(props.state, pkg()));
3176
- return file === void 0 ? void 0 : props.state.transcripts[file];
3178
+ const stored = storedTranscript(auditOf(props.state, pkg()));
3179
+ return stored === void 0 ? void 0 : props.state.transcripts[stored.file];
3177
3180
  },
3178
3181
  get unauditable() {
3179
3182
  return nothing();
@@ -3429,7 +3432,7 @@ const PackageLine = (props) => {
3429
3432
  const { target } = pkg();
3430
3433
  return target === void 0 ? void 0 : bump(pkg().current, target, pkg().ecosystem);
3431
3434
  };
3432
- const outcome = () => props.state.outcomes[props.row.key];
3435
+ const outcome = () => props.state.ran[props.row.key]?.outcome;
3433
3436
  const marker = () => {
3434
3437
  if (selected()) return {
3435
3438
  text: "●",
@@ -4609,6 +4612,19 @@ const App = (props) => {
4609
4612
  onCleanup(() => {
4610
4613
  clearTimeout(toastTimer);
4611
4614
  });
4615
+ useSelectionHandler((selection) => {
4616
+ const text = selection.getSelectedText();
4617
+ if (text.length === 0) return;
4618
+ props.run(props.clipboard.copy(text).pipe(Effect.flatMap((copied) => Effect.sync(() => {
4619
+ showToast(copied ? {
4620
+ color: theme.accent,
4621
+ text: "Copied to the clipboard"
4622
+ } : {
4623
+ color: theme.danger,
4624
+ text: "✗ Could not copy to the clipboard"
4625
+ });
4626
+ }))));
4627
+ });
4612
4628
  let scroller;
4613
4629
  const loadedOnce = createMemo((was) => !props.state.discovering && (was || !props.state.providers.some((entry) => entry.status === "loading")), false);
4614
4630
  const [fold, setFold] = createSignal("auto");
@@ -4617,7 +4633,7 @@ const App = (props) => {
4617
4633
  const updatesToDecideOn = !loadedOnce() || found.some((group) => group.kind === "updates");
4618
4634
  const foldsUntilToggled = filter() === "" && updatesToDecideOn;
4619
4635
  const folds = fold() === "auto" ? foldsUntilToggled : fold() === "hidden";
4620
- const kept = (row) => props.state.outcomes[row.key] !== void 0 || props.state.selected.includes(row.key);
4636
+ const kept = (row) => props.state.ran[row.key] !== void 0 || props.state.selected.includes(row.key);
4621
4637
  return found.map((group) => group.kind === "current" && folds ? {
4622
4638
  ...group,
4623
4639
  collapsed: true,
@@ -4771,7 +4787,7 @@ const App = (props) => {
4771
4787
  const transcript = createMemo(() => {
4772
4788
  const row = shown("transcript");
4773
4789
  return row === void 0 ? void 0 : storedTranscript(auditOf(props.state, row.pkg));
4774
- });
4790
+ }, void 0, { equals: (previous, next) => previous?.file === next?.file });
4775
4791
  createEffect(on(described, (key) => {
4776
4792
  const row = untrack(focused);
4777
4793
  if (key !== void 0 && row !== void 0) fire(props.session.describe(row.pkg));
@@ -4799,15 +4815,15 @@ const App = (props) => {
4799
4815
  if (now === "transcript" || scroller === void 0) return;
4800
4816
  scroller.scrollTo(readTo.get(now) ?? 0);
4801
4817
  }));
4802
- createEffect(on(transcript, (file) => {
4803
- if (file !== void 0) fire(props.session.readTranscript(file));
4818
+ createEffect(on(transcript, (stored) => {
4819
+ if (stored !== void 0) fire(props.session.readTranscript(stored));
4804
4820
  }));
4805
4821
  createEffect(on(operating, (now, before) => {
4806
4822
  if (before !== true || now) return;
4807
4823
  const last = untrack(run);
4808
4824
  if (last === void 0) return;
4809
4825
  const outcomes = last.targets.flatMap((target) => {
4810
- const outcome = props.state.outcomes[target.key];
4826
+ const outcome = props.state.ran[target.key]?.outcome;
4811
4827
  return outcome === void 0 ? [] : [outcome];
4812
4828
  });
4813
4829
  if (outcomes.length === 0) return;
@@ -4898,7 +4914,6 @@ const App = (props) => {
4898
4914
  action,
4899
4915
  rows: actionable,
4900
4916
  flagged,
4901
- auditFirst: props.state.preferences.auditFirst,
4902
4917
  breaking: action === "update" ? actionable.filter((row) => majorsOf(props.state, row.pkg).length > 0).map((row) => row.key) : [],
4903
4918
  unaudited: action === "update" ? actionable.filter((row) => !isUnauditedUpdate(row.pkg) && auditOf(props.state, row.pkg)?._tag !== "Done").map((row) => row.key) : [],
4904
4919
  unauditable: actionable.filter((row) => isUnauditedUpdate(row.pkg)).map((row) => row.key),
@@ -5100,7 +5115,7 @@ const App = (props) => {
5100
5115
  };
5101
5116
  const progress = () => {
5102
5117
  const targets = run()?.targets ?? [];
5103
- const done = targets.filter((target) => props.state.outcomes[target.key] !== void 0).length;
5118
+ const done = targets.filter((target) => props.state.ran[target.key] !== void 0).length;
5104
5119
  const total = Math.max(targets.length, done + Object.keys(props.state.activity).length);
5105
5120
  return `${run()?.action === "uninstall" ? "Removing" : "Updating"} ${done} of ${total}`;
5106
5121
  };
@@ -5747,8 +5762,25 @@ const App = (props) => {
5747
5762
  return _el$;
5748
5763
  })();
5749
5764
  };
5765
+ // src/tui/clipboard.ts
5766
+ const took = ({ host, terminal }) => host.status === "written" || terminal.status === "attempted" && terminal.capability !== "unsupported";
5767
+ /**
5768
+ * The clipboard of the machine the terminal shows: the system pasteboard
5769
+ * when the terminal is local, falling back to OSC 52, and OSC 52 when it is
5770
+ * remote. OSC 52 can't say whether the terminal took the text, so a terminal
5771
+ * that doesn't say it lacks OSC 52 counts as having taken it.
5772
+ */
5773
+ const systemClipboard = Effect.fn("Tui.systemClipboard")(function* (renderer) {
5774
+ const service = yield* Effect.acquireRelease(Effect.sync(() => createClipboard({
5775
+ host: createHostClipboard(),
5776
+ terminal: createRendererClipboardAdapter(renderer)
5777
+ })), (created) => Effect.promise(async () => {
5778
+ await created.dispose();
5779
+ }));
5780
+ return { copy: (text) => Effect.tryPromise(async () => await service.writeText(text, { destination: "best-available" })).pipe(Effect.map(took), Effect.orElseSucceed(() => false)) };
5781
+ });
5750
5782
  // src/tui/Tui.tsx
5751
- const connect = Effect.fn("Tui.connect")(function* (projectDir) {
5783
+ const connect = Effect.fn("Tui.connect")(function* (projectDir, clipboard) {
5752
5784
  const session = yield* Session;
5753
5785
  const run = yield* FiberSet.makeRuntime();
5754
5786
  const [state, setState] = createSignal(yield* session.state);
@@ -5766,6 +5798,7 @@ const connect = Effect.fn("Tui.connect")(function* (projectDir) {
5766
5798
  run(effect);
5767
5799
  },
5768
5800
  projectDir,
5801
+ clipboard,
5769
5802
  quit
5770
5803
  });
5771
5804
  return {
@@ -5775,10 +5808,10 @@ const connect = Effect.fn("Tui.connect")(function* (projectDir) {
5775
5808
  };
5776
5809
  });
5777
5810
  const launch = Effect.fn("Tui.launch")(function* (projectDir) {
5778
- const { view, quit, done } = yield* connect(projectDir);
5779
5811
  const renderer = yield* Effect.acquireRelease(Effect.promise(async () => await createCliRenderer({ exitOnCtrlC: true })), (created) => Effect.sync(() => {
5780
5812
  created.destroy();
5781
5813
  }));
5814
+ const { view, quit, done } = yield* connect(projectDir, yield* systemClipboard(renderer));
5782
5815
  renderer.on("destroy", quit);
5783
5816
  const adopt = Effect.promise(async () => {
5784
5817
  const colors = await renderer.getPalette({ timeout: 300 }).catch(() => void 0);
@@ -1,4 +1,4 @@
1
- import { n as openFlags, r as reason } from "./io-ns6xhFdF.mjs";
1
+ import { i as reason, n as decodeUtf8, r as openFlags, t as byName } from "./io-Dgi-IiVu.mjs";
2
2
  import path from "node:path";
3
3
  import { closeSync, fstatSync, openSync, readSync, readdirSync } from "node:fs";
4
4
  import { parentPort } from "node:worker_threads";
@@ -10,16 +10,6 @@ var GrepError = class extends Error {
10
10
  }
11
11
  };
12
12
  const tooLarge = "grep result exceeds 256 KiB; narrow the path or pattern";
13
- const decode = (bytes) => {
14
- try {
15
- return new TextDecoder("utf-8", {
16
- fatal: true,
17
- ignoreBOM: true
18
- }).decode(bytes);
19
- } catch {
20
- return;
21
- }
22
- };
23
13
  const utf8Length = (text, from, to) => {
24
14
  let bytes = 0;
25
15
  for (let i = from; i < to; i += 1) {
@@ -119,7 +109,7 @@ const scan = (job, every, fd, name, used) => {
119
109
  continue;
120
110
  }
121
111
  if (line.length > 0 && line.at(-1) === 13) line = line.subarray(0, -1);
122
- const text = line.includes(0) ? void 0 : decode(line);
112
+ const text = line.includes(0) ? void 0 : decodeUtf8(line);
123
113
  if (text === void 0) return { notText: true };
124
114
  const before = out.length;
125
115
  grepLine(job, every, out, name, number, lineAt, line, text);
@@ -176,10 +166,7 @@ const runGrep = (job) => {
176
166
  add(`skipped: open ${name}: ${reason(error)}\n`);
177
167
  return;
178
168
  }
179
- entries.sort((left, right) => {
180
- if (left.name < right.name) return -1;
181
- return left.name > right.name ? 1 : 0;
182
- });
169
+ entries.sort(byName);
183
170
  for (const entry of entries) {
184
171
  const child = name === "." ? entry.name : `${name}/${entry.name}`;
185
172
  const absoluteChild = path.join(absolute, entry.name);
@@ -15,9 +15,19 @@ const reason = (cause) => {
15
15
  const code = cause instanceof Error && "code" in cause && Predicate.isString(cause.code) ? cause.code : void 0;
16
16
  return code === void 0 ? "I/O error" : reasons.get(code) ?? code;
17
17
  };
18
+ const decodeUtf8 = (bytes) => {
19
+ try {
20
+ return new TextDecoder("utf-8", {
21
+ fatal: true,
22
+ ignoreBOM: true
23
+ }).decode(bytes);
24
+ } catch {
25
+ return;
26
+ }
27
+ };
18
28
  const byName = (left, right) => {
19
29
  if (left.name < right.name) return -1;
20
30
  return left.name > right.name ? 1 : 0;
21
31
  };
22
32
  const openFlags = constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW;
23
- export { openFlags as n, reason as r, byName as t };
33
+ export { reason as i, decodeUtf8 as n, openFlags as r, byName as t };