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 +1 -1
- package/src/cli.js +10 -2
- package/src/commands/manage.js +66 -21
- package/src/core/engine.js +18 -0
- package/src/lib/self.js +94 -0
- package/src/wizard/browse.js +8 -0
- package/vendor/theamify +1 -1
package/package.json
CHANGED
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
|
|
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':
|
package/src/commands/manage.js
CHANGED
|
@@ -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
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
}
|
package/src/core/engine.js
CHANGED
|
@@ -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
|
package/src/lib/self.js
ADDED
|
@@ -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
|
+
}
|
package/src/wizard/browse.js
CHANGED
|
@@ -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.
|
|
13
|
+
readonly VERSION="2.2.0"
|
|
14
14
|
readonly TOOL="theamify"
|
|
15
15
|
|
|
16
16
|
# -----------------------------------------------------------------------------
|