@noorslvt/shev 0.0.0-stage → 0.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/bin/shev.cjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../scripts/launch.cjs')('shev');
package/install.sh ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ # shev installer — https://github.com/aditauqir/shev
3
+ #
4
+ # curl -fsSL https://raw.githubusercontent.com/aditauqir/shev/main/install.sh | bash
5
+ #
6
+ # Installs the `shev` (TUI) and `shev-cli` (pipe) binaries. Prefers a prebuilt
7
+ # release for this platform; otherwise builds from source with cargo.
8
+ set -euo pipefail
9
+
10
+ REPO="aditauqir/shev"
11
+ VERSION="0.2.1"
12
+ if [ -n "${SHEV_PREFIX:-}" ]; then
13
+ PREFIX="$SHEV_PREFIX"
14
+ elif [[ ":$PATH:" == *":$HOME/.local/bin:"* ]]; then
15
+ PREFIX="$HOME/.local"
16
+ elif [[ ":$PATH:" == *":$HOME/.cargo/bin:"* ]]; then
17
+ PREFIX="$HOME/.cargo"
18
+ else
19
+ PREFIX="$HOME/.local"
20
+ fi
21
+ DEST="$PREFIX/bin"
22
+
23
+ say() { printf '%s\n' "$*"; }
24
+ die() { printf 'error: %s\n' "$*" >&2; exit 1; }
25
+
26
+ need() { command -v "$1" >/dev/null 2>&1; }
27
+
28
+ finish() {
29
+ "$DEST/shev" --register-install "$DEST"
30
+ say ""
31
+ say "shev is installed in $DEST"
32
+ if [ "${SHEV_NO_PATH_SETUP:-0}" = 1 ]; then return; fi
33
+ case ":$PATH:" in
34
+ *":$DEST:"*) : ;;
35
+ *)
36
+ local profiles=() quoted block profile created existing login_profile shell_name="${SHELL:-sh}"
37
+ printf -v quoted '%q' "$DEST"
38
+ printf -v block '# >>> shev PATH >>>\nexport PATH=%s:$PATH\n# <<< shev PATH <<<\n' "$quoted"
39
+ case "${shell_name##*/}" in
40
+ zsh) profiles=("${ZDOTDIR:-$HOME}/.zshrc") ;;
41
+ bash)
42
+ login_profile="$HOME/.bash_profile"
43
+ if [ ! -f "$login_profile" ]; then
44
+ if [ -f "$HOME/.bash_login" ]; then login_profile="$HOME/.bash_login"
45
+ elif [ -f "$HOME/.profile" ]; then login_profile="$HOME/.profile"; fi
46
+ fi
47
+ profiles=("$HOME/.bashrc" "$login_profile")
48
+ ;;
49
+ fish)
50
+ profiles=("${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/shev.fish")
51
+ printf -v block '# >>> shev PATH >>>\nfish_add_path --prepend %s\n# <<< shev PATH <<<\n' "$quoted"
52
+ ;;
53
+ *) profiles=("$HOME/.profile") ;;
54
+ esac
55
+ for profile in "${profiles[@]}"; do
56
+ mkdir -p "$(dirname "$profile")"
57
+ existing="$(cat "$profile" 2>/dev/null || true)"
58
+ if [[ "$existing" != *"${block%$'\n'}"* ]]; then
59
+ created=0
60
+ [ -e "$profile" ] || created=1
61
+ printf '\n%s' "$block" >> "$profile"
62
+ SHEV_PATH_PROFILE="$profile" SHEV_PATH_BLOCK="$block" SHEV_PATH_PROFILE_CREATED="$created" "$DEST/shev" --register-install "$DEST"
63
+ fi
64
+ done
65
+ say "PATH is configured. Open a new terminal, then type shev."
66
+ ;;
67
+ esac
68
+ say ""
69
+ say "Run it with:"
70
+ say " shev"
71
+ }
72
+
73
+ os="$(uname -s)"
74
+ arch="$(uname -m)"
75
+ case "$os-$arch" in
76
+ Darwin-arm64) target="aarch64-apple-darwin" ;;
77
+ Darwin-x86_64) target="x86_64-apple-darwin" ;;
78
+ Linux-x86_64) target="x86_64-unknown-linux-gnu" ;;
79
+ Linux-aarch64) target="aarch64-unknown-linux-gnu" ;;
80
+ *) target="" ;;
81
+ esac
82
+
83
+ mkdir -p "$DEST"
84
+
85
+ # --- 1. prebuilt release, if there is one for this platform ----------------
86
+ if [ -n "$target" ] && need curl && need tar; then
87
+ url="https://github.com/$REPO/releases/download/v$VERSION/shev-$target.tar.gz"
88
+ tmp="$(mktemp -d)"
89
+ trap 'rm -rf "$tmp"' EXIT
90
+ if curl -fsSL "$url" -o "$tmp/shev.tar.gz" 2>/dev/null && tar -xzf "$tmp/shev.tar.gz" -C "$tmp"; then
91
+ [ -f "$tmp/shev" ] || die "the downloaded archive had no shev binary"
92
+ if [ "$("$tmp/shev" --version 2>/dev/null)" = "shev $VERSION" ]; then
93
+ install -m 0755 "$tmp/shev" "$DEST/shev"
94
+ if [ -f "$tmp/shev-cli" ]; then
95
+ install -m 0755 "$tmp/shev-cli" "$DEST/shev-cli"
96
+ fi
97
+ finish
98
+ exit 0
99
+ fi
100
+ fi
101
+ rm -rf "$tmp"
102
+ trap - EXIT
103
+ fi
104
+
105
+ # --- 2. build from source ---------------------------------------------------
106
+ if ! need cargo; then
107
+ need curl || die "no cargo and no curl — install Rust from https://rustup.rs first"
108
+ say "No cargo found. Installing Rust (rustup, minimal profile)…"
109
+ curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal
110
+ # shellcheck disable=SC1091
111
+ . "$HOME/.cargo/env"
112
+ fi
113
+
114
+ say "Building shev from source with cargo (this takes a minute)…"
115
+ if [ -n "${SHEV_SOURCE_DIR:-}" ]; then
116
+ cargo install --path "$SHEV_SOURCE_DIR" --locked --force --root "$PREFIX"
117
+ else
118
+ cargo install --git "https://github.com/$REPO" --locked --force --root "$PREFIX"
119
+ fi
120
+
121
+ [ -x "$DEST/shev" ] || die "cargo finished but $DEST/shev is missing"
122
+ finish
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@noorslvt/shev",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "A simple Jev-like decision making tool, without the hype.",
5
+ "license": "Apache-2.0",
6
+ "repository": { "type": "git", "url": "https://github.com/aditauqir/shev.git" },
7
+ "engines": { "node": ">=18" },
8
+ "bin": { "shev": "bin/shev.cjs", "shev-cli": "bin/shev-cli.cjs" },
9
+ "scripts": { "postinstall": "node scripts/install.cjs", "test": "node --test scripts/*.test.cjs" },
10
+ "files": ["bin", "scripts", "src", "assets", "Cargo.toml", "Cargo.lock", "install.sh", "README.md", "LICENSE"]
11
+ }
@@ -0,0 +1,28 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { spawnSync } = require('node:child_process');
4
+ const { randomUUID } = require('node:crypto');
5
+
6
+ const root = path.resolve(__dirname, '..');
7
+ const state = path.join(root, '.install-state');
8
+ const installId = randomUUID();
9
+ fs.writeFileSync(path.join(root, '.shev-install-id'), installId);
10
+ if (process.platform === 'win32') {
11
+ console.error('shev currently supports macOS/Linux terminals; on Windows use WSL.');
12
+ process.exit(1);
13
+ }
14
+ fs.mkdirSync(state, { recursive: true });
15
+ fs.writeFileSync(path.join(state, '.shev-owned'), 'shev managed state\n');
16
+ const result = spawnSync('bash', [path.join(root, 'install.sh')], {
17
+ stdio: 'inherit',
18
+ env: {
19
+ ...process.env,
20
+ SHEV_PREFIX: path.join(root, 'native'),
21
+ SHEV_SOURCE_DIR: root,
22
+ SHEV_HOME: state,
23
+ SHEV_NO_PATH_SETUP: '1',
24
+ SHEV_INSTALL_ID: installId,
25
+ },
26
+ });
27
+ if (result.error) console.error(`Could not install shev: ${result.error.message}`);
28
+ process.exit(result.status ?? 1);
@@ -0,0 +1,64 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { spawnSync } = require('node:child_process');
4
+ const os = require('node:os');
5
+ const packageName = require('../package.json').name;
6
+
7
+ module.exports = function launch(binary) {
8
+ const root = path.resolve(__dirname, '..');
9
+ const native = path.join(root, 'native', 'bin', binary);
10
+ if (!fs.existsSync(native)) {
11
+ console.error(`shev's native binary is missing. Run npm rebuild -g ${packageName} --foreground-scripts.`);
12
+ process.exit(1);
13
+ }
14
+ const args = process.argv.slice(2);
15
+ const state = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'shev-npm', 'state');
16
+ const marker = path.join(state, '.shev-owned');
17
+ try {
18
+ if (!fs.existsSync(state)) {
19
+ fs.mkdirSync(state, { recursive: true, mode: 0o700 });
20
+ fs.writeFileSync(marker, 'shev managed state\n', { flag: 'wx', mode: 0o600 });
21
+ }
22
+ if (fs.readFileSync(marker, 'utf8') !== 'shev managed state\n') throw new Error('unrecognized state directory');
23
+ const installId = fs.readFileSync(path.join(root, '.shev-install-id'), 'utf8');
24
+ const manifestPath = path.join(state, 'config', 'install.json');
25
+ const manifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, 'utf8')) : {};
26
+ if (manifest.installation_id !== installId) {
27
+ const registration = spawnSync(native, ['--register-install', path.dirname(native)], {
28
+ encoding: 'utf8', env: { ...process.env, SHEV_HOME: state, SHEV_INSTALL_ID: installId },
29
+ });
30
+ if (registration.status !== 0) throw new Error(registration.stderr || 'could not register installation ownership');
31
+ }
32
+ } catch (error) {
33
+ console.error(`Could not initialize shev safely: ${error.message}`);
34
+ process.exit(1);
35
+ }
36
+ const result = spawnSync(native, args, {
37
+ stdio: 'inherit',
38
+ env: { ...process.env, SHEV_HOME: state },
39
+ });
40
+ if (result.error) console.error(result.error.message);
41
+ if (result.status !== 0 || args[0] !== '--uninstall' || args.includes('--dry-run')) {
42
+ process.exit(result.status ?? 1);
43
+ }
44
+ // npm >=7 intentionally does not run uninstall lifecycle hooks. The explicit
45
+ // native uninstaller cleans external registrations first; only then remove npm.
46
+ const npmRoot = spawnSync('npm', ['root', '--global'], { encoding: 'utf8' });
47
+ const sameRoot = (candidate) => {
48
+ try { return fs.realpathSync(candidate) === fs.realpathSync(root); } catch { return false; }
49
+ };
50
+ let uninstallArgs;
51
+ if (npmRoot.status === 0 && sameRoot(path.join(npmRoot.stdout.trim(), packageName))) {
52
+ uninstallArgs = ['uninstall', '--global', packageName];
53
+ } else {
54
+ const modules = path.dirname(path.dirname(root));
55
+ if (path.basename(modules) === 'node_modules') {
56
+ uninstallArgs = ['uninstall', '--prefix', path.dirname(modules), packageName];
57
+ }
58
+ }
59
+ if (uninstallArgs) {
60
+ const removal = spawnSync('npm', uninstallArgs, { stdio: 'inherit' });
61
+ process.exit(removal.status ?? 1);
62
+ }
63
+ console.log('Native shev installation removed. Source checkout kept.');
64
+ };
@@ -0,0 +1,56 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ function fixture() {
9
+ const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'shev-npm-test-')));
10
+ const root = path.join(home, 'global', 'lib', 'node_modules', '@noorslvt', 'shev');
11
+ for (const name of ['scripts', 'bin', 'native/bin']) fs.mkdirSync(path.join(root, name), { recursive: true });
12
+ for (const name of ['scripts/launch.cjs', 'bin/shev.cjs', 'bin/shev-cli.cjs', 'package.json']) {
13
+ fs.copyFileSync(path.join(__dirname, '..', name), path.join(root, name));
14
+ }
15
+ fs.writeFileSync(path.join(root, '.shev-install-id'), 'fixture-install');
16
+ const native = `#!${process.execPath}\nconst fs=require('node:fs'),path=require('node:path');if(process.argv[2]==='--register-install'){const dir=path.join(process.env.SHEV_HOME,'config');fs.mkdirSync(dir,{recursive:true});fs.writeFileSync(path.join(dir,'install.json'),JSON.stringify({installation_id:process.env.SHEV_INSTALL_ID}));process.exit(0);}console.log(JSON.stringify({args:process.argv.slice(2),home:process.env.SHEV_HOME})); process.exit(Number(process.env.FIXTURE_EXIT||0));\n`;
17
+ for (const name of ['shev', 'shev-cli']) fs.writeFileSync(path.join(root, 'native/bin', name), native, { mode: 0o755 });
18
+ const mockBin = path.join(home, 'mock-bin'); fs.mkdirSync(mockBin);
19
+ const npmLog = path.join(home, 'npm-log.json');
20
+ fs.writeFileSync(path.join(mockBin, 'npm'), `#!${process.execPath}\nconst fs=require('node:fs');const args=process.argv.slice(2);if(args[0]==='root')console.log(${JSON.stringify(path.join(home,'global/lib/node_modules'))});else fs.writeFileSync(${JSON.stringify(npmLog)},JSON.stringify(args));\n`, { mode: 0o755 });
21
+ const env = { ...process.env, HOME: home, PATH: mockBin + path.delimiter + process.env.PATH };
22
+ return { home, root, env, npmLog };
23
+ }
24
+
25
+ test('npm exposes shev and shev-cli, forwarding arguments and isolating its state', () => {
26
+ const f = fixture();
27
+ try {
28
+ for (const name of ['shev', 'shev-cli']) {
29
+ const result = spawnSync(process.execPath, [path.join(f.root, 'bin', `${name}.cjs`), '--file', 'a file.json'], { env: f.env, encoding: 'utf8' });
30
+ assert.equal(result.status, 0, result.stderr);
31
+ const output = JSON.parse(result.stdout);
32
+ assert.deepEqual(output.args, ['--file', 'a file.json']);
33
+ assert.equal(output.home, path.join(f.home, '.local/share/shev-npm/state'));
34
+ }
35
+ } finally { fs.rmSync(f.home, { recursive: true }); }
36
+ });
37
+
38
+ test('dry-run and cancelled removal never invoke npm uninstall', () => {
39
+ const f = fixture();
40
+ try {
41
+ const wrapper = path.join(f.root, 'bin/shev.cjs');
42
+ let result = spawnSync(process.execPath, [wrapper, '--uninstall', '--dry-run'], { env: f.env });
43
+ assert.equal(result.status, 0); assert.equal(fs.existsSync(f.npmLog), false);
44
+ result = spawnSync(process.execPath, [wrapper, '--uninstall'], { env: { ...f.env, FIXTURE_EXIT: '2' } });
45
+ assert.equal(result.status, 2); assert.equal(fs.existsSync(f.npmLog), false);
46
+ } finally { fs.rmSync(f.home, { recursive: true }); }
47
+ });
48
+
49
+ test('full confirmed removal invokes npm only for the package owning this wrapper', () => {
50
+ const f = fixture();
51
+ try {
52
+ const result = spawnSync(process.execPath, [path.join(f.root, 'bin/shev.cjs'), '--uninstall', '--yes'], { env: f.env });
53
+ assert.equal(result.status, 0);
54
+ assert.deepEqual(JSON.parse(fs.readFileSync(f.npmLog)), ['uninstall', '--global', '@noorslvt/shev']);
55
+ } finally { fs.rmSync(f.home, { recursive: true }); }
56
+ });