terminal-plus 0.0.0-stage → 1.1.15

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
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # Terminal+ 1.1.15 for Linux
2
+
3
+ This guide covers **Terminal+ 1.1.15**. See [release status](https://github.com/Liang-Chu/Even-G2-Terminal-plus/blob/main/docs/release-status.md) for verification and publication details.
4
+
5
+ Install the Linux companion to watch local Pi, Codex and Claude Code sessions through a browser, Even Hub on your phone and Even G2. Continue using your native terminal, with optional voice input and per-session completion notifications through Glance.
6
+
7
+ Requires Linux **x64/glibc**, Node **22+** and npm for this installation method. Run as the same ordinary user who runs your CLI, without `sudo`.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install -g terminal-plus
13
+ terminal-plus-setup
14
+ ```
15
+
16
+ Open a new shell, then run:
17
+
18
+ ```sh
19
+ terminal-plus
20
+ terminal-plus pair
21
+ ```
22
+
23
+ The setup helper works even when npm skips install scripts. It installs the bundled, verified companion under `~/.local/lib/even-pilot` and its normal command at `~/.local/bin/terminal-plus`. The installation directory stays stable so upgrades reuse existing settings and services. In the current shell, use that full launcher path. The separate `terminal-plus-setup` command keeps npm from replacing the companion's launcher.
24
+
25
+ If npm reports `EACCES`, use a user-owned prefix instead of `sudo`:
26
+
27
+ ```sh
28
+ npm install -g --prefix "$HOME/.local" terminal-plus
29
+ ~/.local/bin/terminal-plus-setup
30
+ ```
31
+
32
+ First installation deploys files without starting monitoring; `terminal-plus` starts it. Existing older installations are upgraded and restarted; equal or newer versions are retained. Your native CLI login, terminal processes and saved settings remain in place. The companion includes its own Node runtime and backend dependencies.
33
+
34
+ ## Connect and use
35
+
36
+ Install the matching Even Hub package from [GitHub Releases](https://github.com/Liang-Chu/Even-G2-Terminal-plus/releases). For internet access, keep the phone and computers connected to the same Tailscale network. Enter the URL and connection key printed by `terminal-plus pair` in the phone app's **Connection** settings.
37
+
38
+ Use `terminal-plus sessions`, `watch SESSION`, `unwatch SESSION` and `settings` for session and notification management. Unwatch does not stop a terminal. Run `terminal-plus --help` for commands.
39
+
40
+ For optional voice input, save your own transcription API key in the phone's **Voice** settings. New or model-less OpenAI configurations default to **GPT Transcribe** with streamed text; existing provider, keys and explicit Whisper choices are retained.
41
+
42
+ [Step-by-step setup, CLI integration and controls](https://github.com/Liang-Chu/Even-G2-Terminal-plus#readme) · [Glance notifications](https://github.com/Liang-Chu/Glance)
43
+
44
+ ## Update and remove
45
+
46
+ The companion automatically installs verified stable GitHub updates by default. Use `terminal-plus update off` to disable, `update on` to enable, or `update` to install now. You do not need `npm update`; rerunning setup will not downgrade a newer companion. Install the matching Hub package separately for the phone's 1.1.15 bounded retries and Status/Devices filters.
47
+
48
+ Close terminals that still use installation files, then remove the companion and its npm helper:
49
+
50
+ ```sh
51
+ terminal-plus uninstall
52
+ npm uninstall -g terminal-plus
53
+ ```
54
+
55
+ Saved connection, Watch and notification settings are retained. Removing only the npm package does not remove the companion.
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: package/artifacts/Terminal-plus-1.1.15-Setup-linux-x64.run
3
+ size: 51709080 bytes
4
+ sha256: c37818dace082f906dfa5cd2eafa92c43a93007062bef5c8142989c79eddb933
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { lstatSync, readFileSync, readlinkSync, realpathSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const stableVersion = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
10
+ const versionDirectory = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-[a-f0-9]{12}$/;
11
+ const packageRoot = fileURLToPath(new URL('../', import.meta.url));
12
+ const usage = 'Usage: terminal-plus-setup [install|--help]\nInstalls the bundled Linux release as your normal user. Then run terminal-plus.\nUse terminal-plus uninstall to remove the application before npm uninstall -g terminal-plus.';
13
+
14
+ export function compareStableVersions(left, right) {
15
+ if (typeof left !== 'string' || typeof right !== 'string' || !stableVersion.test(left) || !stableVersion.test(right))
16
+ throw new Error('Expected stable release versions such as 1.1.11');
17
+ const a = left.split('.').map(BigInt), b = right.split('.').map(BigInt);
18
+ for (let index = 0; index < a.length; index++) {
19
+ if (a[index] > b[index]) return 1;
20
+ if (a[index] < b[index]) return -1;
21
+ }
22
+ return 0;
23
+ }
24
+
25
+ export function installationPlan(version, installedVersion) {
26
+ compareStableVersions(version, version);
27
+ if (installedVersion === undefined) return { skip: false, argument: '--no-start' };
28
+ return { skip: compareStableVersions(installedVersion, version) >= 0, argument: '--update' };
29
+ }
30
+
31
+ export function nativeRootFromLink(bin, link) {
32
+ if (typeof link !== 'string') return undefined;
33
+ const target = resolve(dirname(bin), link);
34
+ if (!['terminal-plus', 'even-pilot'].some(name => target.endsWith(sep + join('current', 'bin', name)))) return undefined;
35
+ return dirname(dirname(dirname(target)));
36
+ }
37
+
38
+ export function resolveInstallRoot(home, env, nativeLink) {
39
+ const bin = resolve(env.EVEN_PILOT_BIN_DIR || join(home, '.local/bin'), 'terminal-plus');
40
+ return resolve(env.EVEN_PILOT_INSTALL_DIR || nativeRootFromLink(bin, nativeLink) || join(home, '.local/lib/even-pilot'));
41
+ }
42
+
43
+ // Keep installation effects in this entry point. Imports only define helpers.
44
+ export function runSetup(argv = [], options = {}) {
45
+ const { platform = process.platform, arch = process.arch, uid = process.getuid?.(), home = homedir(),
46
+ env = process.env, root: npmRoot = packageRoot, readFile = readFileSync, lstat = lstatSync,
47
+ readlink = readlinkSync, spawn = spawnSync, log = console.log } = options;
48
+ if (argv.length === 1 && argv[0] === '--help') { log(usage); return 0; }
49
+ const postinstall = argv.length === 1 && argv[0] === '--postinstall';
50
+ if (argv.length && !postinstall && !(argv.length === 1 && argv[0] === 'install')) throw new Error(usage);
51
+ if (postinstall && !['true', '1'].includes(env.npm_config_global)) {
52
+ log('Native setup skipped for a local npm dependency. Run terminal-plus-setup explicitly to install the application.');
53
+ return 0;
54
+ }
55
+ if (platform !== 'linux') throw new Error('Terminal+ setup requires Linux.');
56
+ if (!Number.isInteger(uid) || uid <= 0) throw new Error('Run terminal-plus-setup as your normal Linux user, without sudo.');
57
+
58
+ function info(path) {
59
+ try { return lstat(path); } catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
60
+ }
61
+ function regularFile(path, maxBytes, owned = false) {
62
+ const stat = info(path);
63
+ if (!stat?.isFile() || stat.isSymbolicLink() || stat.size > maxBytes || owned && stat.uid !== uid)
64
+ throw new Error('Expected a regular' + (owned ? ' user-owned' : '') + ' file: ' + path);
65
+ return stat;
66
+ }
67
+ function json(path, maxBytes, owned = false) {
68
+ regularFile(path, maxBytes, owned);
69
+ try { return JSON.parse(readFile(path, 'utf8')); } catch { throw new Error('Invalid installation metadata: ' + path); }
70
+ }
71
+ const metadata = json(join(npmRoot, 'payload.json'), 65536);
72
+ if (!metadata || typeof metadata.version !== 'string' || !stableVersion.test(metadata.version) || !['x64', 'arm64'].includes(metadata.arch)
73
+ || typeof metadata.installer !== 'string' || typeof metadata.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(metadata.sha256))
74
+ throw new Error('Invalid bundled release metadata.');
75
+ if (metadata.arch !== arch) throw new Error('Use the Terminal+ package for this Linux CPU architecture.');
76
+ const installer = resolve(npmRoot, metadata.installer), installerRelative = relative(resolve(npmRoot), installer);
77
+ if (isAbsolute(metadata.installer) || !installerRelative || installerRelative === '..' || installerRelative.startsWith('..' + sep)
78
+ || metadata.installer.split(/[\\/]/).some(part => part === '..')
79
+ || basename(installer) !== `Terminal-plus-${metadata.version}-Setup-linux-${metadata.arch}.run`)
80
+ throw new Error('Bundled installer must stay inside the npm package.');
81
+ let payloadPath = installer;
82
+ while (payloadPath !== resolve(npmRoot)) {
83
+ const stat = info(payloadPath);
84
+ if (!stat || stat.isSymbolicLink()) throw new Error('Bundled installer contains a missing or linked path.');
85
+ payloadPath = dirname(payloadPath);
86
+ }
87
+ regularFile(installer, 1024 * 1024 * 1024);
88
+
89
+ const binDirectory = resolve(env.EVEN_PILOT_BIN_DIR || join(home, '.local/bin'));
90
+ const bin = join(binDirectory, 'terminal-plus'), legacyBin = join(binDirectory, 'even-pilot');
91
+ const nativeLinks = [];
92
+ for (const launcher of [bin, legacyBin]) {
93
+ const stat = info(launcher);
94
+ if (!stat) continue;
95
+ if (!stat.isSymbolicLink() || stat.uid !== uid) throw new Error('Another executable occupies ' + launcher);
96
+ const link = readlink(launcher), linkedRoot = nativeRootFromLink(launcher, link);
97
+ if (!linkedRoot) throw new Error('Another executable occupies ' + launcher);
98
+ nativeLinks.push({ launcher, link, root: linkedRoot });
99
+ }
100
+ // Both names resolve relative to the same bin directory. Keep discovering a
101
+ // previous custom install through its legacy launcher before choosing a root.
102
+ const root = resolveInstallRoot(home, env, nativeLinks[0]?.link);
103
+ if (/[\r\n\0]/.test(root) || [resolve(sep), resolve(home), '/usr', '/opt', '/var', '/tmp'].includes(root))
104
+ throw new Error('Choose a dedicated Terminal+ installation directory.');
105
+ if (nativeLinks.some(link => link.root !== root))
106
+ throw new Error('An existing Terminal+ launcher belongs to another installation.');
107
+ let parent = root;
108
+ while (true) {
109
+ const stat = info(parent);
110
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) throw new Error('Installation parents must be regular directories.');
111
+ if (parent === dirname(parent)) break;
112
+ parent = dirname(parent);
113
+ }
114
+ const rootInfo = info(root);
115
+ if (rootInfo && rootInfo.uid !== uid) throw new Error('The existing installation must be owned by the current user.');
116
+ const recordPath = join(root, 'install.json');
117
+ let installedVersion, installedPayload, installedRelease;
118
+ if (info(recordPath)) {
119
+ const record = json(recordPath, 1024 * 1024, true);
120
+ if (!record || typeof record.version !== 'string' || !stableVersion.test(record.version)
121
+ || typeof record.current !== 'string' || !versionDirectory.test(record.current)
122
+ || !record.current.startsWith(record.version + '-') || !Array.isArray(record.versions)
123
+ || !record.versions.includes(record.current) || !record.versions.every(version => typeof version === 'string' && versionDirectory.test(version)))
124
+ throw new Error('Invalid existing Terminal+ installation record.');
125
+ const versions = join(root, 'versions'), versionsInfo = info(versions);
126
+ const current = join(root, 'current'), currentInfo = info(current);
127
+ const selected = join(versions, record.current), selectedInfo = info(selected);
128
+ if (!versionsInfo?.isDirectory() || versionsInfo.isSymbolicLink() || versionsInfo.uid !== uid
129
+ || !currentInfo?.isSymbolicLink() || currentInfo.uid !== uid || resolve(root, readlink(current)) !== selected
130
+ || !selectedInfo?.isDirectory() || selectedInfo.isSymbolicLink() || selectedInfo.uid !== uid)
131
+ throw new Error('The existing installation has an invalid current version.');
132
+ const marker = json(join(selected, 'installed.json'), 65536, true);
133
+ const release = json(join(selected, 'release.json'), 32 * 1024 * 1024, true);
134
+ if (marker?.format !== 1 || release?.version !== record.version || release?.platform !== 'linux' || release?.arch !== arch
135
+ || release?.buildId !== record.current.slice(record.version.length + 1)) throw new Error('The existing version metadata does not match its installation record.');
136
+ for (const name of ['runtime', 'bin']) {
137
+ const directory = info(join(selected, name));
138
+ if (!directory?.isDirectory() || directory.isSymbolicLink() || directory.uid !== uid)
139
+ throw new Error('The existing installation has an invalid launcher/runtime directory.');
140
+ }
141
+ const nativeLauncher = info(join(selected, 'bin/terminal-plus')) ? 'bin/terminal-plus' : 'bin/even-pilot';
142
+ for (const name of ['runtime/node', nativeLauncher]) {
143
+ const executable = regularFile(join(selected, name), 1024 * 1024 * 1024, true);
144
+ if (!executable.size || typeof executable.mode === 'number' && !(executable.mode & 0o111))
145
+ throw new Error('The existing installation has a missing or unusable launcher/runtime.');
146
+ }
147
+ installedVersion = record.version;
148
+ installedPayload = selected;
149
+ installedRelease = release;
150
+ } else if (nativeLinks.length || info(join(root, 'versions')) || info(join(root, 'current'))) {
151
+ throw new Error('The existing installation is incomplete. Repair it with its Linux installer before running npm setup.');
152
+ }
153
+ const plan = installationPlan(metadata.version, installedVersion);
154
+ if (plan.skip) {
155
+ if (!nativeLinks.length) throw new Error('The native terminal-plus launcher is missing. Restore it with this installation\'s Linux installer before running npm setup.');
156
+ if (!Array.isArray(installedRelease.files) || !installedRelease.files.length)
157
+ throw new Error('The current release has no file manifest. Restore a verified Linux release before running npm setup.');
158
+ const checkedDirectories = new Set([installedPayload]), declaredFiles = new Set();
159
+ for (const file of installedRelease.files) {
160
+ if (!file || typeof file.path !== 'string' || !file.path || isAbsolute(file.path)
161
+ || /[\\\r\n\0]/.test(file.path) || file.path.split('/').some(part => !part || part === '.' || part === '..')
162
+ || !Number.isSafeInteger(file.bytes) || file.bytes < 0 || typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256)
163
+ || declaredFiles.has(file.path)) throw new Error('The current release has an invalid file manifest. Restore a verified Linux release before running npm setup.');
164
+ declaredFiles.add(file.path);
165
+ const path = resolve(installedPayload, file.path);
166
+ if (!path.startsWith(installedPayload + sep)) throw new Error('The current release manifest escapes its installed version.');
167
+ let directoryPath = dirname(path);
168
+ while (!checkedDirectories.has(directoryPath)) {
169
+ const directory = info(directoryPath);
170
+ if (!directory?.isDirectory() || directory.isSymbolicLink() || directory.uid !== uid)
171
+ throw new Error('The current release contains a missing or linked directory: ' + directoryPath);
172
+ checkedDirectories.add(directoryPath);
173
+ directoryPath = dirname(directoryPath);
174
+ }
175
+ const stat = regularFile(path, 1024 * 1024 * 1024, true);
176
+ if (stat.size !== file.bytes || createHash('sha256').update(readFile(path)).digest('hex') !== file.sha256)
177
+ throw new Error('Current installation verification failed: ' + file.path + '. Restore a verified Linux release before running npm setup.');
178
+ }
179
+ const nativeLauncher = info(join(installedPayload, 'bin/terminal-plus')) ? 'bin/terminal-plus' : 'bin/even-pilot';
180
+ for (const required of ['runtime/node', nativeLauncher, 'apps/linux/src/desktop.ts', 'node_modules/tsx/dist/loader.mjs']) {
181
+ if (!declaredFiles.has(required)) throw new Error('The current release manifest is missing a required runtime file: ' + required);
182
+ }
183
+ log(`Terminal+ ${installedVersion} is already installed. Keeping its current release and settings.\nLauncher: ${nativeLinks[0].launcher}`);
184
+ return 0;
185
+ }
186
+ if (createHash('sha256').update(readFile(installer)).digest('hex') !== metadata.sha256)
187
+ throw new Error('Bundled Linux installer checksum mismatch. Reinstall the npm package.');
188
+ const result = spawn('sh', [installer, '--dir', root, plan.argument], { stdio: 'inherit', shell: false, env });
189
+ if (result.error) throw new Error('Could not run the verified Linux installer: ' + result.error.message);
190
+ if (result.status !== 0) return Number.isInteger(result.status) && result.status > 0 ? result.status : 1;
191
+ log('Linux setup complete. Run terminal-plus to start the companion. If this shell cannot find it, open a new terminal.');
192
+ return 0;
193
+ }
194
+
195
+ let invokedDirectly = false;
196
+ try { invokedDirectly = Boolean(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); } catch {}
197
+ if (invokedDirectly) {
198
+ try { process.exitCode = runSetup(process.argv.slice(2)); }
199
+ catch (error) { console.error('[Terminal+] ' + (error instanceof Error ? error.message : String(error))); process.exitCode = 1; }
200
+ }
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "terminal-plus",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.15",
4
+ "description": "Linux companion for monitoring Pi, Codex and Claude sessions, with access through Even Hub and G2.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "os": [
11
+ "linux"
12
+ ],
13
+ "cpu": [
14
+ "x64"
15
+ ],
16
+ "bin": {
17
+ "terminal-plus-setup": "bin/terminal-plus-setup.mjs"
18
+ },
19
+ "scripts": {
20
+ "postinstall": "node bin/terminal-plus-setup.mjs --postinstall"
21
+ },
22
+ "files": [
23
+ "bin/terminal-plus-setup.mjs",
24
+ "payload.json",
25
+ "artifacts/Terminal-plus-1.1.15-Setup-linux-x64.run",
26
+ "README.md"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/Liang-Chu/Even-G2-Terminal-plus.git"
31
+ },
32
+ "homepage": "https://github.com/Liang-Chu/Even-G2-Terminal-plus",
33
+ "bugs": {
34
+ "url": "https://github.com/Liang-Chu/Even-G2-Terminal-plus/issues"
35
+ },
36
+ "keywords": [
37
+ "even-realities",
38
+ "g2",
39
+ "pi",
40
+ "codex",
41
+ "claude",
42
+ "session-monitor",
43
+ "linux"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org/"
48
+ }
49
+ }
package/payload.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "1.1.15",
3
+ "arch": "x64",
4
+ "installer": "artifacts/Terminal-plus-1.1.15-Setup-linux-x64.run",
5
+ "sha256": "c37818dace082f906dfa5cd2eafa92c43a93007062bef5c8142989c79eddb933"
6
+ }