theamify-cli 2.1.0 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theamify-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
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 } 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,9 @@ 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
+
148
193
  // Companion tools (chafa, grub-customizer) are intentionally LEFT in place —
149
194
  // the user may want them for later; uninstall only removes theamify itself.
150
195
  const themeNote = grubReset
@@ -152,6 +197,6 @@ export async function runUninstallWizard() {
152
197
  : (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
198
 
154
199
  p.outro(pc.green(
155
- `Uninstalled.${removed ? ' Runtime + all downloaded themes removed.' : ''} ${themeNote} Companion tools (chafa, grub-customizer) were kept for your use.`,
200
+ `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
201
  ));
157
202
  }
@@ -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,94 @@
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
+ * Check whether the running local version is behind the latest published one.
35
+ * @returns {Promise<{outdated:boolean, latest:string|null, current:string}>}
36
+ */
37
+ export async function checkForUpdate() {
38
+ const latest = await getLatestVersion();
39
+ if (!latest) return { outdated: false, latest: null, current: pkg.version };
40
+ return { outdated: compareVersions(latest, pkg.version) > 0, latest, current: pkg.version };
41
+ }
42
+
43
+ /**
44
+ * If a newer version is available, offer to self-update via `npm install -g`.
45
+ * @returns {Promise<boolean>} true when an update was performed
46
+ */
47
+ export async function promptSelfUpdate() {
48
+ const { outdated, latest, current } = await checkForUpdate();
49
+ if (!outdated || !latest) return false;
50
+
51
+ const p = await import('@clack/prompts');
52
+ const want = await p.confirm({
53
+ message: `A new version (${pc.cyan('v' + latest)}) is available — you have ${pc.dim('v' + current)}. Update now?`,
54
+ initialValue: true,
55
+ });
56
+ if (p.isCancel(want) || !want) {
57
+ p.log.message(pc.dim(`Keeping v${current} — update later with: npm install -g ${NPM_NAME}@latest`));
58
+ return false;
59
+ }
60
+
61
+ // Release the terminal so npm's output is visible & interruptible.
62
+ console.log();
63
+ const res = await execa('npm', ['install', '-g', `${NPM_NAME}@latest`], { stdio: 'inherit', reject: false });
64
+ if (res.exitCode !== 0) {
65
+ p.log.warn('Update failed. You can retry with: npm install -g ' + NPM_NAME + '@latest');
66
+ return false;
67
+ }
68
+ p.log.success(`Updated to v${latest}. Restart ${BIN_NAME} to use the new version.`);
69
+ return true;
70
+ }
71
+
72
+ /**
73
+ * Fully remove the npm package so the `theamify` command disappears from PATH.
74
+ * @returns {Promise<boolean>} true when uninstalled
75
+ */
76
+ export async function selfUninstall() {
77
+ const p = await import('@clack/prompts');
78
+ const want = await p.confirm({
79
+ message: `Also remove the ${NPM_NAME} npm package, so the ${pc.cyan(BIN_NAME)} command is gone from your system?`,
80
+ initialValue: true,
81
+ });
82
+ if (p.isCancel(want) || !want) {
83
+ p.log.message(pc.dim(`Keeping the npm package — the ${BIN_NAME} command will remain.`));
84
+ return false;
85
+ }
86
+ console.log();
87
+ const res = await execa('npm', ['uninstall', '-g', NPM_NAME], { stdio: 'inherit', reject: false });
88
+ if (res.exitCode === 0) {
89
+ p.log.success(`${NPM_NAME} removed. The ${BIN_NAME} command is no longer available.`);
90
+ return true;
91
+ }
92
+ p.log.warn(`Could not uninstall ${NPM_NAME} automatically. Run: npm uninstall -g ${NPM_NAME}`);
93
+ return false;
94
+ }
@@ -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.0"
14
14
  readonly TOOL="theamify"
15
15
 
16
16
  # -----------------------------------------------------------------------------