theamify-cli 2.1.0 → 2.2.1

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
@@ -16,15 +16,18 @@ npm install -g theamify-cli
16
16
  The `-g` flag puts the `theamify` command on your system `PATH` (or provisions a user-local runtime under `~/.local/share/theamify`), so you can call it from **any folder and any terminal**:
17
17
 
18
18
  ```bash
19
- theamify # ✨ interactive wizard (detects tools → browsepreview → apply)
19
+ theamify # ✨ interactive wizard (self-check tools → downloadbrowse → apply)
20
20
  theamify list # list all themes with status
21
21
  theamify info <name> # details + terminal preview
22
22
  theamify get <name> # download & cache a theme
23
+ theamify get --all # download every theme up front
23
24
  sudo theamify use <name> # apply to GRUB + rebuild
24
25
  theamify update # update tools (chafa, grub-customizer) + re-download themes
25
26
  theamify status # GRUB & dependency status
26
27
  theamify doctor # diagnose install, GRUB, tools & dependencies
27
- theamify uninstall # remove only theamify (keeps chafa & grub-customizer)
28
+ theamify upgrade # check for & install the latest npm version
29
+ theamify repair # fix a broken install (re-provision the engine)
30
+ theamify uninstall # remove theamify entirely (incl. npm package)
28
31
  ```
29
32
 
30
33
  > 💡 Upgrading is the same command — `npm install -g theamify-cli@latest`, and your downloaded themes & registry edits are preserved.
@@ -43,19 +46,22 @@ theamify uninstall # remove only theamify (keeps chafa & grub-customize
43
46
  | 🩺 **`theamify doctor`** | Diagnose install, GRUB, active theme, registry, tools & dependencies |
44
47
  | 🗑 **Safe uninstall** | Removes **only theamify** — leaves chafa & grub-customizer for later use |
45
48
  | ⌨️ **Ctrl+C / Ctrl+Z safe** | Signal safety net; interactive `sudo` is never frozen |
49
+ | 🔄 **Self-manage (v2.2+)** | `upgrade`, `repair`, auto-update check on every wizard run |
46
50
 
47
51
  ---
48
52
 
49
53
  ## 🧭 Wizard Sequence (every `theamify` run)
50
54
 
51
- 1. **Detect companion tools** — `chafa` then `grub-customizer`. For each:
55
+ 1. **Self-check** — checks npm for a newer `theamify-cli`; offers to update if found. Also self-repairs the engine/runtime.
56
+ 2. **Detect companion tools** — `chafa` then `grub-customizer`. For each:
52
57
  - **Already installed?** → *“Update it now? (or just continue)”* — Continue moves on.
53
58
  - **Missing?** → *“Install `<name>`? Yes/No”* → installs via your package manager (`sudo`), then continues.
54
- 2. **Show the theme picker** — every registry theme with status (`[REMOTE]`/`[CACHED]`/`[ACTIVE]`).
55
- 3. **Pick a theme** optional **terminal thumbnail preview** (needs `chafa`).
56
- 4. **Act** download, apply to GRUB, or open in browser.
59
+ 3. **Download all themes** — asks *“Download all themes now?”* (default Yes) → fetches the whole registry so every theme is instantly previewable/applicable.
60
+ 4. **Show the theme picker** every registry theme with status (`[REMOTE]`/`[CACHED]`/`[ACTIVE]`).
61
+ 5. **Pick a theme** the **terminal thumbnail renders automatically** below the selection (no separate “preview” menu item).
62
+ 6. **Act** — download/update, apply to GRUB, or open in browser. Choose `← Back to theme list` to keep browsing (the wizard never hard-exits until you pick **Quit**).
57
63
 
58
- > Companion tools are installed strictly **before** the browser, so previews are ready when you need them.
64
+ > Companion tools and themes are ready **before** the browser, so thumbnails & applies are instant when you need them.
59
65
 
60
66
  ---
61
67
 
@@ -79,11 +85,12 @@ theamify/
79
85
  ├── bin/theamify.js # npm bin entry → src/cli.js
80
86
  ├── src/
81
87
  │ ├── cli.js # command router + signal safety net
82
- │ ├── core/engine.js # runtime provisioning & engine forwarding
83
- │ ├── commands/manage.js # doctor / status / uninstall
84
- │ ├── wizard/browse.js # interactive wizard (browse → preview → apply)
88
+ │ ├── core/engine.js # runtime provisioning, repair & engine forwarding
89
+ │ ├── commands/manage.js # doctor / status / upgrade / repair / uninstall
90
+ │ ├── wizard/browse.js # interactive wizard (self-check → browse → preview → apply)
85
91
  │ └── lib/
86
92
  │ ├── conf.js # themes.conf registry parser
93
+ │ ├── self.js # version-check / self-update / self-uninstall / rc cleanup
87
94
  │ └── tools.js # companion tools: chafa + grub-customizer
88
95
  ├── vendor/ # bundled bash engine (theamify + lib/ + config/)
89
96
  ├── test/ # unit tests (node:test)
@@ -122,7 +129,10 @@ The wizard manages two companion tools as first-class:
122
129
  - **Detect** → if present, offer to update; if absent, install (with your consent).
123
130
  - **Continue** regardless of Yes/No — the wizard proceeds to the theme picker.
124
131
  - **`theamify update`** updates tools then re-downloads themes.
125
- - **`theamify uninstall`** removes **only** theamify chafa and grub-customizer stay on the system for later use.
132
+ - **`theamify uninstall`** removes **everything theamify owns**: the runtime + all downloaded themes, resets GRUB to the default boot menu, removes the `theamify-cli` npm package, and strips leftover PATH markers from `~/.bashrc` / `~/.zshrc`. Companion tools **chafa** and **grub-customizer** stay on the system for later use — run the command after that and you get a normal `command not found`.
133
+ - **`theamify upgrade`** checks the npm registry and, if a newer version exists, offers to `npm install -g theamify-cli@latest`.
134
+ - **`theamify repair`** re-provisions the engine from the bundled package — the fix for a broken/missing install.
135
+ - Every `theamify` (wizard) run also **auto-checks for updates** and **self-repairs** before showing the theme picker.
126
136
 
127
137
  ---
128
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theamify-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "theamify — GRUB theme manager & interactive browser wizard. Browse, preview, download and apply GRUB boot themes from the terminal.",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
package/src/cli.js CHANGED
@@ -2,7 +2,7 @@ import { defineCommand, runMain } from 'citty';
2
2
  import pc from 'picocolors';
3
3
  import { createRequire } from 'node:module';
4
4
  import { runThemeBrowser } from './wizard/browse.js';
5
- import { runDoctor, runStatus, runUninstallWizard } from './commands/manage.js';
5
+ import { runDoctor, runStatus, runUninstallWizard, runSelfUpgrade, runRepair } from './commands/manage.js';
6
6
  import { updateManagedTools } from './lib/tools.js';
7
7
  import { runEngine } from './core/engine.js';
8
8
 
@@ -42,7 +42,9 @@ ${pc.bold('Usage:')}
42
42
  theamify open <name> Open the theme source page in a browser
43
43
  theamify status Show GRUB & dependency status
44
44
  theamify doctor Diagnose install, GRUB & dependencies
45
- theamify uninstall Remove theamify (asks before deleting anything)
45
+ theamify upgrade Check for & install the latest npm version
46
+ theamify repair Fix a broken install (re-provision the engine)
47
+ theamify uninstall Remove theamify entirely (incl. npm package)
46
48
  theamify clean Clear the repo clone cache
47
49
  `);
48
50
  }
@@ -96,6 +98,12 @@ const main = defineCommand({
96
98
  return runStatus();
97
99
  case 'doctor':
98
100
  return runDoctor();
101
+ case 'upgrade':
102
+ case 'self-update':
103
+ case 'selfupdate':
104
+ return runSelfUpgrade();
105
+ case 'repair':
106
+ return runRepair();
99
107
  case 'uninstall':
100
108
  return runUninstallWizard();
101
109
  case 'update':
@@ -7,10 +7,12 @@ import {
7
7
  findInstalledRuntime,
8
8
  USER_DIR,
9
9
  removeShadowBin,
10
+ repairRuntime,
10
11
  resolveEngine,
11
12
  } from '../core/engine.js';
12
13
  import { parseThemes, resolveConfPath } from '../lib/conf.js';
13
14
  import { companionToolStatus } from '../lib/tools.js';
15
+ import { checkForUpdate, promptSelfUpdate, selfUninstall, cleanRcMarkers } from '../lib/self.js';
14
16
 
15
17
  /** `theamify doctor` — installation, GRUB and dependency health. */
16
18
  export async function runDoctor() {
@@ -80,14 +82,54 @@ export async function runStatus() {
80
82
  if (res.exitCode !== 0) process.exit(res.exitCode);
81
83
  }
82
84
 
85
+ /** `theamify upgrade` — self-update the npm CLI if a newer version is published. */
86
+ export async function runSelfUpgrade() {
87
+ const { outdated, latest, current } = await checkForUpdate();
88
+ if (!outdated) {
89
+ console.log(pc.green(`Already on the latest version (v${current}).`));
90
+ return;
91
+ }
92
+ console.log(`Available: ${pc.cyan('v' + latest)} (you have ${pc.dim('v' + current)})`);
93
+ await promptSelfUpdate();
94
+ }
95
+
96
+ /** `theamify repair` — fix a broken install by re-provisioning the engine/runtime. */
97
+ export async function runRepair() {
98
+ console.log(pc.bold('\n🔧 theamify Repair\n'));
99
+ const engine = await repairRuntime();
100
+ console.log(` Engine : ${pc.cyan(engine)}`);
101
+ console.log(` Runtime : ${pc.cyan(path.join(USER_DIR))}`);
102
+ console.log(' Status : ' + pc.green('engine re-provisioned'));
103
+ console.log(' Downloads: ' + (fs.existsSync(path.join(USER_DIR, 'themes')) ? pc.green('themes preserved') : pc.dim('no cached themes yet')));
104
+ console.log();
105
+ }
106
+
83
107
  /**
84
- * Remove GRUB_THEME from /etc/default/grub and rebuild the boot menu so the
85
- * system returns to the default theme. Requires sudo when not root.
86
- * @returns {Promise<boolean>} true when GRUB was reset
108
+ * Remove a theamify runtime directory and everything in it (the engine PLUS all
109
+ * downloaded themes under themes/ and .repo_cache/). Uses sudo when the dir is
110
+ * root-owned.
111
+ * @param {string} dir absolute path to the runtime dir
112
+ * @returns {Promise<boolean>} true when the dir no longer exists
87
113
  */
88
- async function resetGrubTheme() {
89
- const script = `
90
- GRUB=/etc/default/grub
114
+ export async function removeRuntimeDir(dir) {
115
+ if (!fs.existsSync(dir)) return true;
116
+ try {
117
+ const owner = fs.statSync(dir).uid;
118
+ if (owner === 0 && process.getuid() !== 0) {
119
+ const res = await execa('sudo', ['rm', '-rf', dir], { stdio: 'inherit', reject: false });
120
+ return res.exitCode === 0 && !fs.existsSync(dir);
121
+ }
122
+ fs.rmSync(dir, { recursive: true, force: true });
123
+ return !fs.existsSync(dir);
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ /** Build the bash script that clears GRUB_THEME and rebuilds the boot menu. */
130
+ export function buildResetGrubScript(grubFile = '/etc/default/grub') {
131
+ return `
132
+ GRUB=${grubFile}
91
133
  [ -f "$GRUB" ] || exit 0
92
134
  sed -i '/^GRUB_THEME=/d' "$GRUB"
93
135
  if command -v update-grub >/dev/null 2>&1; then
@@ -98,7 +140,17 @@ elif command -v grub2-mkconfig >/dev/null 2>&1; then
98
140
  grub2-mkconfig -o /boot/grub2/grub.cfg
99
141
  fi
100
142
  `.trim();
101
- if (process.getuid() === 0) {
143
+ }
144
+
145
+ /**
146
+ * Remove GRUB_THEME from /etc/default/grub and rebuild the boot menu so the
147
+ * system returns to the default theme. Requires sudo when not root.
148
+ * @param {{grubFile?:string, asRoot?:boolean}} [opts] injectable for tests
149
+ * @returns {Promise<boolean>} true when GRUB was reset
150
+ */
151
+ export async function resetGrubTheme({ grubFile = '/etc/default/grub', asRoot = process.getuid() === 0 } = {}) {
152
+ const script = buildResetGrubScript(grubFile);
153
+ if (asRoot) {
102
154
  const res = await execa('bash', ['-c', script], { stdio: 'inherit', reject: false });
103
155
  return res.exitCode === 0;
104
156
  }
@@ -121,19 +173,9 @@ export async function runUninstallWizard() {
121
173
  initialValue: true,
122
174
  });
123
175
  if (!p.isCancel(confirm) && confirm) {
124
- try {
125
- const dir = found.dir;
126
- const owner = fs.statSync(dir).uid;
127
- if (owner === 0 && process.getuid() !== 0) {
128
- await execa('sudo', ['rm', '-rf', dir], { stdio: 'inherit' });
129
- } else {
130
- fs.rmSync(dir, { recursive: true, force: true });
131
- }
132
- removed = true;
133
- removeShadowBin();
134
- } catch {
135
- p.log.warn(`Could not remove ${found.dir}.`);
136
- }
176
+ removed = await removeRuntimeDir(found.dir);
177
+ if (removed) removeShadowBin();
178
+ else p.log.warn(`Could not remove ${found.dir}.`);
137
179
  }
138
180
  }
139
181
 
@@ -145,6 +187,12 @@ export async function runUninstallWizard() {
145
187
  if (p.isCancel(resetGrub)) { p.cancel('Aborted.'); process.exit(0); }
146
188
  const grubReset = resetGrub ? await resetGrubTheme() : false;
147
189
 
190
+ // Remove the npm package so the `theamify` command actually disappears.
191
+ const npmRemoved = await selfUninstall();
192
+
193
+ // Remove leftover PATH markers from ~/.bashrc / ~/.zshrc so zero traces remain.
194
+ await cleanRcMarkers();
195
+
148
196
  // Companion tools (chafa, grub-customizer) are intentionally LEFT in place —
149
197
  // the user may want them for later; uninstall only removes theamify itself.
150
198
  const themeNote = grubReset
@@ -152,6 +200,6 @@ export async function runUninstallWizard() {
152
200
  : (resetGrub ? 'GRUB reset could not be completed — remove GRUB_THEME= from /etc/default/grub and rebuild.' : 'Your applied GRUB theme was left in place.');
153
201
 
154
202
  p.outro(pc.green(
155
- `Uninstalled.${removed ? ' Runtime + all downloaded themes removed.' : ''} ${themeNote} Companion tools (chafa, grub-customizer) were kept for your use.`,
203
+ `Uninstalled.${removed ? ' Runtime + all downloaded themes removed.' : ''} ${themeNote} ${npmRemoved ? 'theamify npm package removed — command no longer available.' : 'theamify npm package kept.'} Companion tools (chafa, grub-customizer) were kept for your use.`,
156
204
  ));
157
205
  }
@@ -94,6 +94,24 @@ export async function installUserRuntime() {
94
94
  return USER_DIR;
95
95
  }
96
96
 
97
+ /**
98
+ * Repair a broken install: re-provision the engine/runtime from the bundled
99
+ * package (fixes a missing/corrupt engine or runtime tree) and remove any stale
100
+ * shadow symlink. Downloads are preserved unless the whole runtime is missing.
101
+ * @returns {Promise<string>} path to the repaired engine
102
+ */
103
+ export async function repairRuntime() {
104
+ const found = findInstalledRuntime();
105
+ if (found) {
106
+ syncEngineTo(found.dir);
107
+ removeShadowBin();
108
+ return path.join(found.dir, BIN_NAME);
109
+ }
110
+ const dir = await installUserRuntime();
111
+ removeShadowBin();
112
+ return path.join(dir, BIN_NAME);
113
+ }
114
+
97
115
  /**
98
116
  * Resolve the path to the engine to run for a subcommand.
99
117
  * Prefers an installed runtime; otherwise provisions the bundled one in the
@@ -0,0 +1,121 @@
1
+ import { execa } from 'execa';
2
+ import pc from 'picocolors';
3
+ import { createRequire } from 'node:module';
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const pkg = require('../../package.json');
7
+
8
+ export const NPM_NAME = pkg.name;
9
+ export const BIN_NAME = 'theamify';
10
+
11
+ /** Simple numeric semver compare: returns <0, 0, >0 when a is older/equal/newer. */
12
+ export function compareVersions(a, b) {
13
+ const pa = String(a || '').split('.').map((n) => parseInt(n, 10) || 0);
14
+ const pb = String(b || '').split('.').map((n) => parseInt(n, 10) || 0);
15
+ for (let i = 0; i < 3; i++) {
16
+ const d = (pa[i] || 0) - (pb[i] || 0);
17
+ if (d !== 0) return d < 0 ? -1 : 1;
18
+ }
19
+ return 0;
20
+ }
21
+
22
+ /** Query npm for the latest published version of this package. */
23
+ export async function getLatestVersion() {
24
+ try {
25
+ const { stdout } = await execa('npm', ['view', NPM_NAME, 'version'], { reject: false });
26
+ const v = (stdout || '').trim();
27
+ return /^\d+\.\d+\.\d+/.test(v) ? v : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Remove any leftover `# theamify CLI PATH` + follow-up `export PATH=` line from
35
+ * ~/.bashrc and ~/.zshrc (added by old installs). Called by uninstall so no
36
+ * traces of the tool remain in shell startup files.
37
+ */
38
+ export async function cleanRcMarkers() {
39
+ const rcs = ['.bashrc', '.zshrc'].map((f) => path.join(require('node:os').homedir(), f));
40
+ const marker = '# theamify CLI PATH';
41
+ for (const rc of rcs) {
42
+ try {
43
+ const fs = require('node:fs');
44
+ if (!fs.existsSync(rc)) continue;
45
+ const lines = fs.readFileSync(rc, 'utf8').split('\n');
46
+ const out = [];
47
+ for (let i = 0; i < lines.length; i++) {
48
+ if (lines[i].includes(marker)) {
49
+ // Also drop the immediately-following `export PATH=` line.
50
+ if (lines[i + 1] && /^\s*export PATH=/.test(lines[i + 1])) i++;
51
+ continue;
52
+ }
53
+ out.push(lines[i]);
54
+ }
55
+ fs.writeFileSync(rc, out.join('\n'));
56
+ } catch { /* rc not writable — skip */ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Check whether the running local version is behind the latest published one.
62
+ * @returns {Promise<{outdated:boolean, latest:string|null, current:string}>}
63
+ */
64
+ export async function checkForUpdate() {
65
+ const latest = await getLatestVersion();
66
+ if (!latest) return { outdated: false, latest: null, current: pkg.version };
67
+ return { outdated: compareVersions(latest, pkg.version) > 0, latest, current: pkg.version };
68
+ }
69
+
70
+ /**
71
+ * If a newer version is available, offer to self-update via `npm install -g`.
72
+ * @returns {Promise<boolean>} true when an update was performed
73
+ */
74
+ export async function promptSelfUpdate() {
75
+ const { outdated, latest, current } = await checkForUpdate();
76
+ if (!outdated || !latest) return false;
77
+
78
+ const p = await import('@clack/prompts');
79
+ const want = await p.confirm({
80
+ message: `A new version (${pc.cyan('v' + latest)}) is available — you have ${pc.dim('v' + current)}. Update now?`,
81
+ initialValue: true,
82
+ });
83
+ if (p.isCancel(want) || !want) {
84
+ p.log.message(pc.dim(`Keeping v${current} — update later with: npm install -g ${NPM_NAME}@latest`));
85
+ return false;
86
+ }
87
+
88
+ // Release the terminal so npm's output is visible & interruptible.
89
+ console.log();
90
+ const res = await execa('npm', ['install', '-g', `${NPM_NAME}@latest`], { stdio: 'inherit', reject: false });
91
+ if (res.exitCode !== 0) {
92
+ p.log.warn('Update failed. You can retry with: npm install -g ' + NPM_NAME + '@latest');
93
+ return false;
94
+ }
95
+ p.log.success(`Updated to v${latest}. Restart ${BIN_NAME} to use the new version.`);
96
+ return true;
97
+ }
98
+
99
+ /**
100
+ * Fully remove the npm package so the `theamify` command disappears from PATH.
101
+ * @returns {Promise<boolean>} true when uninstalled
102
+ */
103
+ export async function selfUninstall() {
104
+ const p = await import('@clack/prompts');
105
+ const want = await p.confirm({
106
+ message: `Also remove the ${NPM_NAME} npm package, so the ${pc.cyan(BIN_NAME)} command is gone from your system?`,
107
+ initialValue: true,
108
+ });
109
+ if (p.isCancel(want) || !want) {
110
+ p.log.message(pc.dim(`Keeping the npm package — the ${BIN_NAME} command will remain.`));
111
+ return false;
112
+ }
113
+ console.log();
114
+ const res = await execa('npm', ['uninstall', '-g', NPM_NAME], { stdio: 'inherit', reject: false });
115
+ if (res.exitCode === 0) {
116
+ p.log.success(`${NPM_NAME} removed. The ${BIN_NAME} command is no longer available.`);
117
+ return true;
118
+ }
119
+ p.log.warn(`Could not uninstall ${NPM_NAME} automatically. Run: npm uninstall -g ${NPM_NAME}`);
120
+ return false;
121
+ }
@@ -124,6 +124,14 @@ async function ensureAllThemes(engine, { prompt = true } = {}) {
124
124
  * back to the theme list — it NEVER hard-exits until you choose Quit.
125
125
  */
126
126
  export async function runThemeBrowser() {
127
+ // Step 0: self-check — offer an update if a newer version is published, and
128
+ // repair a broken/unprovisioned runtime before anything else.
129
+ const { checkForUpdate, promptSelfUpdate } = await import('../lib/self.js');
130
+ const { repairRuntime } = await import('../core/engine.js');
131
+ const { outdated } = await checkForUpdate();
132
+ if (outdated) await promptSelfUpdate();
133
+ await repairRuntime();
134
+
127
135
  // Step 1: ensure companion tools (chafa, grub-customizer) — detect, install
128
136
  // or update, then continue BEFORE showing the theme picker.
129
137
  await ensureManagedTools();
package/vendor/theamify CHANGED
@@ -10,7 +10,7 @@ set -euo pipefail
10
10
  # -----------------------------------------------------------------------------
11
11
  # VERSION & TOOL NAME
12
12
  # -----------------------------------------------------------------------------
13
- readonly VERSION="2.1.0"
13
+ readonly VERSION="2.2.1"
14
14
  readonly TOOL="theamify"
15
15
 
16
16
  # -----------------------------------------------------------------------------