termdeck-cli 1.0.2 → 2.0.2
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 +195 -174
- package/package.json +9 -7
- package/sample-config.json +176 -0
- package/src/agentManager.js +217 -0
- package/src/config.js +636 -427
- package/src/dashboard.js +502 -198
- package/src/devServer.js +51 -2
- package/src/index.js +103 -9
- package/src/processMonitor.js +168 -0
- package/src/projectManager.js +159 -0
- package/src/updater.js +172 -0
- package/src/util.js +23 -0
- package/bin/termdeck.js +0 -22
package/src/updater.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Silent auto-updater for termdeck-cli.
|
|
5
|
+
*
|
|
6
|
+
* On every interactive dashboard launch we ask the npm registry for the latest
|
|
7
|
+
* termdeck-cli version. When a newer one exists we print one short notice, kick
|
|
8
|
+
* off a detached `npm install -g termdeck-cli@latest` in the background and let
|
|
9
|
+
* the user keep working. Every failure path (offline, timeout, registry hiccup,
|
|
10
|
+
* spawn error) is silent: the app simply runs with the installed version.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately dependency-free on purpose: the check is a plain stdlib `https`
|
|
13
|
+
* GET against the npm registry (configurable for tests via TERMDECK_REGISTRY_URL)
|
|
14
|
+
* and the install reuses `cross-spawn`, which termdeck-cli already depends on.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const https = require('https');
|
|
19
|
+
const crossSpawn = require('cross-spawn');
|
|
20
|
+
|
|
21
|
+
const pkg = require('../package.json');
|
|
22
|
+
|
|
23
|
+
const PACKAGE_NAME = 'termdeck-cli';
|
|
24
|
+
const DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
25
|
+
const CHECK_TIMEOUT_MS = 2000;
|
|
26
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
27
|
+
|
|
28
|
+
/** The version of the code that is currently running. */
|
|
29
|
+
function currentVersion() {
|
|
30
|
+
return pkg.version;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Registry endpoint, overridable so tests (and mirrors) can point elsewhere. */
|
|
34
|
+
function registryUrl() {
|
|
35
|
+
return process.env.TERMDECK_REGISTRY_URL || DEFAULT_REGISTRY_URL;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clientFor(url) {
|
|
39
|
+
return url.indexOf('https:') === 0 ? https : http;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fetch the latest published version of termdeck-cli from the registry.
|
|
44
|
+
* Resolves with the version string, or `null` on any failure (offline,
|
|
45
|
+
* timeout, non-2xx, bad JSON). Never rejects and never takes longer than the
|
|
46
|
+
* configured timeout.
|
|
47
|
+
*/
|
|
48
|
+
function fetchLatestVersion(options = {}) {
|
|
49
|
+
const url = options.url || registryUrl();
|
|
50
|
+
const timeoutMs = options.timeoutMs == null ? CHECK_TIMEOUT_MS : options.timeoutMs;
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
let finished = false;
|
|
54
|
+
let body = '';
|
|
55
|
+
let req;
|
|
56
|
+
|
|
57
|
+
const finish = (value) => {
|
|
58
|
+
if (finished) return;
|
|
59
|
+
finished = true;
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
resolve(value);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
finish(null);
|
|
66
|
+
if (req && !req.destroyed) req.destroy();
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
if (timer.unref) timer.unref();
|
|
69
|
+
|
|
70
|
+
const onError = () => finish(null);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
req = clientFor(url).get(url, (res) => {
|
|
74
|
+
res.setEncoding('utf8');
|
|
75
|
+
res.on('data', (chunk) => {
|
|
76
|
+
if (body.length < MAX_BODY_BYTES) body += chunk;
|
|
77
|
+
});
|
|
78
|
+
res.on('error', onError);
|
|
79
|
+
res.on('end', () => {
|
|
80
|
+
const status = res.statusCode || 0;
|
|
81
|
+
if (status < 200 || status >= 300 || body.length > MAX_BODY_BYTES) return finish(null);
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(body);
|
|
84
|
+
finish(parsed && typeof parsed.version === 'string' ? parsed.version : null);
|
|
85
|
+
} catch (_) {
|
|
86
|
+
finish(null);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
} catch (_) {
|
|
91
|
+
finish(null);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
req.on('error', onError);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Start a silent, detached `npm install -g termdeck-cli@latest` in the
|
|
101
|
+
* background. The child is `unref()`ed so the dashboard can quit while npm
|
|
102
|
+
* keeps working. Returns the child, or `null` when spawning failed.
|
|
103
|
+
*/
|
|
104
|
+
function installUpdate(options = {}) {
|
|
105
|
+
const spawn = options.spawn || crossSpawn;
|
|
106
|
+
try {
|
|
107
|
+
const child = spawn('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
|
|
108
|
+
detached: true,
|
|
109
|
+
stdio: 'ignore',
|
|
110
|
+
windowsHide: true,
|
|
111
|
+
});
|
|
112
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
113
|
+
return child;
|
|
114
|
+
} catch (_) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One-shot, fire-and-forget update check used at the top of every dashboard
|
|
121
|
+
* launch. Never rejects and never blocks the caller: the fetch is capped at
|
|
122
|
+
* CHECK_TIMEOUT_MS and the install is detached.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} [options]
|
|
125
|
+
* @param {boolean} [options.enabled=true] false disables checking entirely (--no-update)
|
|
126
|
+
* @param {object} [options.stdout] stream for the one-line banner (defaults to process.stdout)
|
|
127
|
+
* @param {Function} [options.onUpdating] called with the new version once an install starts
|
|
128
|
+
* @param {Function} [options.spawn] injectable spawn (tests)
|
|
129
|
+
* @param {Function} [options.fetch] injectable fetch (tests)
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
*/
|
|
132
|
+
async function runAutoUpdate(options = {}) {
|
|
133
|
+
const {
|
|
134
|
+
enabled = true,
|
|
135
|
+
stdout = process.stdout,
|
|
136
|
+
onUpdating = null,
|
|
137
|
+
spawn = null,
|
|
138
|
+
fetch: fetchFn = fetchLatestVersion,
|
|
139
|
+
} = options;
|
|
140
|
+
if (!enabled) return;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
stdout.write('✨ Checking for updates\u2026\n');
|
|
144
|
+
} catch (_) {
|
|
145
|
+
/* terminal already gone - nothing to tell the user */
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const latest = await fetchFn();
|
|
149
|
+
if (!latest) return; // offline / timeout / registry down -> stay silent
|
|
150
|
+
if (latest === currentVersion()) return; // up to date -> stay silent
|
|
151
|
+
|
|
152
|
+
const child = installUpdate({ spawn });
|
|
153
|
+
if (child && typeof onUpdating === 'function') {
|
|
154
|
+
try {
|
|
155
|
+
onUpdating(latest);
|
|
156
|
+
} catch (_) {
|
|
157
|
+
/* the banner itself must never crash the session */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
PACKAGE_NAME,
|
|
164
|
+
DEFAULT_REGISTRY_URL,
|
|
165
|
+
CHECK_TIMEOUT_MS,
|
|
166
|
+
MAX_BODY_BYTES,
|
|
167
|
+
currentVersion,
|
|
168
|
+
registryUrl,
|
|
169
|
+
fetchLatestVersion,
|
|
170
|
+
installUpdate,
|
|
171
|
+
runAutoUpdate,
|
|
172
|
+
};
|
package/src/util.js
CHANGED
|
@@ -55,6 +55,28 @@ function timestamp(date = new Date()) {
|
|
|
55
55
|
return `${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Relative time like "12m ago". Accepts an ISO string, Date or epoch ms and
|
|
60
|
+
* returns null when it cannot be parsed.
|
|
61
|
+
*/
|
|
62
|
+
function timeAgo(value, now = new Date()) {
|
|
63
|
+
if (value == null || value === '') return null;
|
|
64
|
+
const then = new Date(value);
|
|
65
|
+
if (Number.isNaN(then.getTime())) return null;
|
|
66
|
+
|
|
67
|
+
const seconds = Math.max(0, Math.floor((now.getTime() - then.getTime()) / 1000));
|
|
68
|
+
if (seconds < 45) return 'just now';
|
|
69
|
+
const minutes = Math.round(seconds / 60);
|
|
70
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
71
|
+
const hours = Math.round(minutes / 60);
|
|
72
|
+
if (hours < 24) return `${hours}h ago`;
|
|
73
|
+
const days = Math.round(hours / 24);
|
|
74
|
+
if (days < 30) return `${days}d ago`;
|
|
75
|
+
const months = Math.round(days / 30);
|
|
76
|
+
if (months < 12) return `${months}mo ago`;
|
|
77
|
+
return `${Math.round(months / 12)}y ago`;
|
|
78
|
+
}
|
|
79
|
+
|
|
58
80
|
/* ------------------------------------------------------------------ *
|
|
59
81
|
* Dev-server output parsing
|
|
60
82
|
* ------------------------------------------------------------------ */
|
|
@@ -215,4 +237,5 @@ module.exports = {
|
|
|
215
237
|
shellQuote,
|
|
216
238
|
appleScriptString,
|
|
217
239
|
killTree,
|
|
240
|
+
timeAgo,
|
|
218
241
|
};
|
package/bin/termdeck.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* termdeck binary entry point.
|
|
6
|
-
*
|
|
7
|
-
* The shebang above (plus the `bin` field in package.json) is what makes
|
|
8
|
-
* `npm i -g termdeck` give you a global `termdeck` command.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const { main } = require('../src/index.js');
|
|
12
|
-
|
|
13
|
-
main(process.argv.slice(2))
|
|
14
|
-
.then((code) => {
|
|
15
|
-
// The dashboard keeps the process alive; only surface real exit codes.
|
|
16
|
-
if (typeof code === 'number' && code !== 0) process.exitCode = code;
|
|
17
|
-
})
|
|
18
|
-
.catch((err) => {
|
|
19
|
-
const message = err && err.message ? err.message : String(err);
|
|
20
|
-
process.stderr.write(`termdeck: ${message}\n`);
|
|
21
|
-
process.exitCode = 1;
|
|
22
|
-
});
|