@pm-2001/shellup 0.1.0 → 0.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/README.md CHANGED
@@ -26,6 +26,15 @@ background process, repainting when the result lands.
26
26
  > npm test 4s
27
27
  ```
28
28
 
29
+ **Suggestions as you type.** The rest of the command appears in grey, pulled from
30
+ what you've actually run before — press <kbd>→</kbd> to accept it, <kbd>Alt</kbd>+<kbd>→</kbd>
31
+ to take one word. When history has nothing, it falls back to the completion system, so
32
+ `git ` still suggests a subcommand on a brand-new machine.
33
+
34
+ ```
35
+ $ git pus­h origin main ← "h origin main" is grey; → accepts
36
+ ```
37
+
29
38
  **Three themes**, two of which need no special font:
30
39
 
31
40
  | theme | needs a Nerd Font | |
@@ -38,7 +47,7 @@ background process, repainting when the result lands.
38
47
  [eza](https://github.com/eza-community/eza), [bat](https://github.com/sharkdp/bat),
39
48
  [fzf](https://github.com/junegunn/fzf), [zoxide](https://github.com/ajeetdsouza/zoxide),
40
49
  [fd](https://github.com/sharkdp/fd), [ripgrep](https://github.com/BurntSushi/ripgrep),
41
- [git-delta](https://github.com/dandavison/delta), lazygit, btop, jq, tldr.
50
+ [git-delta](https://github.com/dandavison/delta), [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions), lazygit, btop, jq, tldr.
42
51
 
43
52
  **zsh defaults worth having**: 100k lines of shared, deduplicated history; completion
44
53
  with case-insensitive matching and a menu; `auto_cd` and a directory stack; prefix-aware
@@ -83,6 +92,10 @@ leaves the file byte-for-byte as it was.
83
92
  - **`custom.zsh` is sourced last**, so anything you write there wins over shellup's
84
93
  defaults without forking anything.
85
94
  - **`compinit` checks its cache once a day** rather than rebuilding on every shell start.
95
+ - **macOS per-session history is switched off.** `/etc/zshrc_Apple_Terminal` runs before
96
+ your `.zshrc` and, on a restored Terminal window, repoints `HISTFILE` into
97
+ `~/.zsh_sessions` — which quietly breaks a single shared history. shellup takes Apple's
98
+ documented opt-out and reclaims the file.
86
99
 
87
100
  ## Requirements
88
101
 
package/dist/cli.js CHANGED
@@ -20,6 +20,10 @@ function has(bin) {
20
20
  return false;
21
21
  }
22
22
  }
23
+ function hasFile(paths) {
24
+ const prefix = process.env.HOMEBREW_PREFIX ?? "/opt/homebrew";
25
+ return paths.some((path) => existsSync(path.replace("${HOMEBREW_PREFIX:-/opt/homebrew}", prefix)));
26
+ }
23
27
  function detectPackageManager() {
24
28
  for (const pm of ["brew", "apt", "dnf", "pacman"]) if (has(pm)) return pm;
25
29
  return "none";
@@ -261,9 +265,40 @@ var TOOLS = [
261
265
  dnf: "tealdeer",
262
266
  pacman: "tealdeer",
263
267
  recommended: false
268
+ },
269
+ // Last on purpose: it wraps ZLE widgets, so it has to load after anything that
270
+ // defines its own (fzf, zoxide).
271
+ {
272
+ id: "autosuggestions",
273
+ label: "autosuggestions",
274
+ hint: "greys in the rest of the command as you type \u2014 press \u2192 to accept",
275
+ brew: "zsh-autosuggestions",
276
+ apt: "zsh-autosuggestions",
277
+ dnf: "zsh-autosuggestions",
278
+ pacman: "zsh-autosuggestions",
279
+ recommended: true,
280
+ sourceFiles: [
281
+ "${HOMEBREW_PREFIX:-/opt/homebrew}/share/zsh-autosuggestions/zsh-autosuggestions.zsh",
282
+ "/usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh",
283
+ "/usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh",
284
+ "/usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh"
285
+ ],
286
+ // Set before sourcing, because the plugin reads some of these at load time.
287
+ snippet: [
288
+ `# 'history' replays what you actually ran; 'completion' falls back to the`,
289
+ `# completion system, so \`git \` suggests a subcommand on a fresh machine.`,
290
+ `ZSH_AUTOSUGGEST_STRATEGY=(history completion)`,
291
+ `ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8'`,
292
+ `# Stops the plugin doing work on pasted blobs, where it can't help anyway.`,
293
+ `ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20`
294
+ ].join("\n")
264
295
  }
265
296
  ];
266
297
  var toolById = (id) => TOOLS.find((t) => t.id === id);
298
+ function isPresent(tool) {
299
+ if (tool.sourceFiles) return hasFile(tool.sourceFiles);
300
+ return tool.bin ? has(tool.bin) : false;
301
+ }
267
302
  function packageFor(tool, pm) {
268
303
  return pm === "brew" ? tool.brew : pm === "apt" ? tool.apt : pm === "dnf" ? tool.dnf : pm === "pacman" ? tool.pacman : void 0;
269
304
  }
@@ -288,7 +323,7 @@ function installTools(tools, pm) {
288
323
  const result = { installed: [], alreadyPresent: [], failed: [], unsupported: [], command: null };
289
324
  const wanted = [];
290
325
  for (const tool of tools) {
291
- if (has(tool.bin)) {
326
+ if (isPresent(tool)) {
292
327
  result.alreadyPresent.push(tool.id);
293
328
  continue;
294
329
  }
@@ -310,7 +345,7 @@ function installTools(tools, pm) {
310
345
  execFileSync2(cmd[0], cmd[1], { stdio: ["ignore", "pipe", "pipe"], timeout: 15 * 60 * 1e3 });
311
346
  } catch {
312
347
  }
313
- for (const tool of wanted) (has(tool.bin) ? result.installed : result.failed).push(tool.id);
348
+ for (const tool of wanted) (isPresent(tool) ? result.installed : result.failed).push(tool.id);
314
349
  return result;
315
350
  }
316
351
  function applyGitConfig(entries) {
@@ -342,15 +377,29 @@ function renderTools(config) {
342
377
  const parts = [HEADER(version())];
343
378
  for (const id of config.tools) {
344
379
  const tool = toolById(id);
345
- if (!tool?.snippet) continue;
346
- parts.push(
347
- `
380
+ if (!tool || !tool.snippet && !tool.sourceFiles) continue;
381
+ const rule = `
348
382
  # \u2500\u2500 ${tool.label} ${"\u2500".repeat(Math.max(0, 68 - tool.label.length))}
349
- if command -v ${tool.bin} >/dev/null 2>&1; then
350
- ` + tool.snippet.split("\n").map((l) => l.trim() ? ` ${l}` : l).join("\n") + `
351
- fi
383
+ `;
384
+ const indent = (body) => body.split("\n").map((l) => l.trim() ? ` ${l}` : l).join("\n");
385
+ if (tool.sourceFiles) {
386
+ parts.push(
387
+ rule + (tool.snippet ? tool.snippet + "\n" : "") + `() {
388
+ local p
389
+ for p in \\
390
+ ` + tool.sourceFiles.map((f) => ` "${f}"`).join(" \\\n") + `
391
+ do
392
+ [[ -r $p ]] && { source $p; break }
393
+ done
394
+ }
352
395
  `
353
- );
396
+ );
397
+ continue;
398
+ }
399
+ parts.push(rule + `if command -v ${tool.bin} >/dev/null 2>&1; then
400
+ ` + indent(tool.snippet) + `
401
+ fi
402
+ `);
354
403
  }
355
404
  return parts.join("");
356
405
  }
@@ -543,6 +592,13 @@ async function init(opts = {}) {
543
592
  }
544
593
  if (existing) {
545
594
  p.log.info(`Existing setup found (theme ${pc3.cyan(existing.theme)}). This will reconfigure it.`);
595
+ const added = TOOLS.filter((t) => t.recommended && !existing.tools.includes(t.id));
596
+ if (added.length) {
597
+ p.log.info(
598
+ `New since your last run: ${added.map((t) => pc3.cyan(t.label)).join(", ")}
599
+ Press ${pc3.cyan("Space")} on the list below to add ${added.length > 1 ? "them" : "it"}.`
600
+ );
601
+ }
546
602
  }
547
603
  const theme2 = await p.select({
548
604
  message: "Pick a prompt theme",
@@ -569,7 +625,7 @@ async function init(opts = {}) {
569
625
  initialValues: existing?.tools ?? TOOLS.filter((t) => t.recommended).map((t) => t.id),
570
626
  options: TOOLS.map((t) => ({
571
627
  value: t.id,
572
- label: t.label + (has(t.bin) ? pc3.green(" (installed)") : ""),
628
+ label: t.label + (isPresent(t) ? pc3.green(" (installed)") : ""),
573
629
  hint: t.hint
574
630
  }))
575
631
  });
@@ -600,7 +656,7 @@ async function init(opts = {}) {
600
656
  nerdFont: env.nerdFont,
601
657
  installedAt: existing?.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
602
658
  };
603
- const missing = selectedTools.map((id) => toolById(id)).filter((t) => t && !has(t.bin));
659
+ const missing = selectedTools.map((id) => toolById(id)).filter((t) => t && !isPresent(t));
604
660
  if (missing.length && env.packageManager !== "none") {
605
661
  const doInstall = opts.yes || await p.confirm({
606
662
  message: `Install ${missing.length} missing tool${missing.length > 1 ? "s" : ""} with ${env.packageManager}? (${missing.map((t) => t.label).join(", ")})`,
@@ -626,7 +682,7 @@ async function init(opts = {}) {
626
682
  );
627
683
  }
628
684
  const delta = toolById("delta");
629
- if (selectedTools.includes("delta") && has(delta.bin) && delta.gitConfig) {
685
+ if (selectedTools.includes("delta") && isPresent(delta) && delta.gitConfig) {
630
686
  const doGit = await p.confirm({
631
687
  message: "Set git to use delta for diffs? (writes to your global ~/.gitconfig)",
632
688
  initialValue: true
@@ -694,13 +750,13 @@ ${pc4.bold(" Installation")}`);
694
750
  ${pc4.bold(" Tools")}`);
695
751
  const selected = new Set(config?.tools ?? []);
696
752
  for (const tool of TOOLS) {
697
- const present = has(tool.bin);
753
+ const present = isPresent(tool);
698
754
  if (!selected.has(tool.id) && !present) continue;
699
755
  const state = present ? "ok" : "warn";
700
756
  const detail = present ? selected.has(tool.id) ? "installed, integration active" : "installed, not managed by shellup" : "selected but not installed \u2014 integration is dormant";
701
757
  console.log(line(state, tool.label, detail));
702
758
  }
703
- const missing = [...selected].map((id) => toolById(id)).filter((t) => t && !has(t.bin));
759
+ const missing = [...selected].map((id) => toolById(id)).filter((t) => t && !isPresent(t));
704
760
  if (missing.length) problems.push(`install missing tools: ${missing.map((t) => t.label).join(", ")}`);
705
761
  console.log();
706
762
  if (problems.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pm-2001/shellup",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Level up your terminal in 60 seconds — a beautiful prompt, modern tool swaps, and sane defaults, installed by one command and reversible by another.",
5
5
  "keywords": [
6
6
  "terminal",
@@ -6,7 +6,18 @@
6
6
  [[ -o interactive ]] || return 0
7
7
 
8
8
  # ── History ────────────────────────────────────────────────────────────────
9
- HISTFILE="${HISTFILE:-$HOME/.zsh_history}"
9
+ # macOS ships per-session history in /etc/zshrc_Apple_Terminal, which runs BEFORE
10
+ # this file. It normally stands down when SHARE_HISTORY is set — but on a restored
11
+ # Terminal window it repoints HISTFILE into ~/.zsh_sessions straight away, before
12
+ # it can see our options, silently defeating the shared history set up below.
13
+ # SHELL_SESSION_HISTORY=0 is Apple's documented opt-out; the reclaim handles the
14
+ # window that has already been hijacked.
15
+ SHELL_SESSION_HISTORY=0
16
+ if [[ $HISTFILE == */.zsh_sessions/* ]]; then
17
+ HISTFILE="${ZDOTDIR:-$HOME}/.zsh_history"
18
+ [[ -r $HISTFILE ]] && fc -R "$HISTFILE"
19
+ fi
20
+ HISTFILE="${HISTFILE:-${ZDOTDIR:-$HOME}/.zsh_history}"
10
21
  HISTSIZE=100000
11
22
  SAVEHIST=100000
12
23
  setopt append_history # never truncate another shell's writes