idlewatch 0.1.4 → 0.1.6
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 +3 -1
- package/bin/idlewatch-agent.js +17 -3
- package/package.json +2 -1
- package/src/enrollment.js +54 -30
- package/tui/bin/darwin-arm64/idlewatch-setup +0 -0
- package/tui/src/main.rs +33 -5
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npx idlewatch quickstart
|
|
|
17
17
|
npx idlewatch --dry-run
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`quickstart` is the happy path: create an API key on idlewatch.com/api, run the wizard, pick a device name + metrics, and IdleWatch saves local config before sending a first sample.
|
|
20
|
+
`quickstart` is the happy path: create an API key on idlewatch.com/api, run the wizard, pick a device name + metrics, and IdleWatch saves local config before sending a first sample. It prefers a bundled TUI binary when available, otherwise falls back to a local Cargo build only on developer machines that already have Cargo installed. Use `idlewatch quickstart --no-tui` to skip the TUI and stay on the plain text setup path.
|
|
21
21
|
|
|
22
22
|
## CLI options
|
|
23
23
|
|
|
@@ -57,6 +57,8 @@ Use `gpuSource` + `gpuConfidence` in dashboards to decide whether to trust value
|
|
|
57
57
|
npx idlewatch quickstart
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
`idlewatch` is the primary package/command name. `idlewatch-skill` still works as a compatibility alias, but treat it as legacy in user-facing docs.
|
|
61
|
+
|
|
60
62
|
The wizard keeps setup small:
|
|
61
63
|
- asks for a **device name**
|
|
62
64
|
- asks for your **API key** from `idlewatch.com/api`
|
package/bin/idlewatch-agent.js
CHANGED
|
@@ -374,6 +374,10 @@ if (quickstartRequested) {
|
|
|
374
374
|
try {
|
|
375
375
|
const result = await runEnrollmentWizard({ noTui: args.has('--no-tui') })
|
|
376
376
|
|
|
377
|
+
if (!result?.outputEnvFile || !fs.existsSync(result.outputEnvFile)) {
|
|
378
|
+
throw new Error(`setup_did_not_write_env_file:${result?.outputEnvFile || 'unknown'}`)
|
|
379
|
+
}
|
|
380
|
+
|
|
377
381
|
const enrolledEnv = parseEnvFileToObject(result.outputEnvFile)
|
|
378
382
|
const onceRun = spawnSync(process.execPath, [process.argv[1], '--once'], {
|
|
379
383
|
stdio: 'inherit',
|
|
@@ -395,7 +399,13 @@ if (quickstartRequested) {
|
|
|
395
399
|
console.error('Or rerun: idlewatch quickstart')
|
|
396
400
|
process.exit(onceRun.status ?? 1)
|
|
397
401
|
} catch (err) {
|
|
398
|
-
|
|
402
|
+
if (String(err?.message || '') === 'setup_cancelled') {
|
|
403
|
+
console.error('Enrollment cancelled before saving config.')
|
|
404
|
+
} else if (String(err?.message || '').startsWith('setup_did_not_write_env_file:')) {
|
|
405
|
+
console.error(`Enrollment failed: setup did not save idlewatch.env (${String(err.message).split(':').slice(1).join(':')}).`)
|
|
406
|
+
} else {
|
|
407
|
+
console.error(`Enrollment failed: ${err.message}`)
|
|
408
|
+
}
|
|
399
409
|
process.exit(1)
|
|
400
410
|
}
|
|
401
411
|
}
|
|
@@ -624,9 +634,13 @@ if (firebaseConfigError) {
|
|
|
624
634
|
process.exit(1)
|
|
625
635
|
}
|
|
626
636
|
|
|
627
|
-
|
|
637
|
+
const hasAnyFirebaseConfig = Boolean(PROJECT_ID || CREDS_FILE || CREDS_JSON || CREDS_B64 || FIRESTORE_EMULATOR_HOST)
|
|
638
|
+
const hasCloudConfig = Boolean(CLOUD_INGEST_URL && CLOUD_API_KEY)
|
|
639
|
+
const shouldWarnAboutMissingPublishConfig = !appReady && !hasCloudConfig && !DRY_RUN && !hasAnyFirebaseConfig
|
|
640
|
+
|
|
641
|
+
if (shouldWarnAboutMissingPublishConfig) {
|
|
628
642
|
console.error(
|
|
629
|
-
'
|
|
643
|
+
'No publish target is configured yet. Running in local-only mode. Run idlewatch quickstart to link cloud ingest, or configure Firebase/emulator mode if you need that path.'
|
|
630
644
|
)
|
|
631
645
|
}
|
|
632
646
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idlewatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Host telemetry collector for IdleWatch",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"scripts",
|
|
10
10
|
"skill",
|
|
11
11
|
"tui/src",
|
|
12
|
+
"tui/bin",
|
|
12
13
|
"tui/Cargo.toml",
|
|
13
14
|
"tui/Cargo.lock",
|
|
14
15
|
"README.md",
|
package/src/enrollment.js
CHANGED
|
@@ -10,6 +10,24 @@ function defaultConfigDir() {
|
|
|
10
10
|
return path.join(os.homedir(), '.idlewatch')
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function machineName() {
|
|
14
|
+
if (process.platform === 'darwin') {
|
|
15
|
+
const macName = spawnSync('scutil', ['--get', 'ComputerName'], { encoding: 'utf8' })
|
|
16
|
+
if (macName.status === 0) {
|
|
17
|
+
const value = String(macName.stdout || '').trim()
|
|
18
|
+
if (value) return value
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hostName = String(os.hostname() || '').trim()
|
|
23
|
+
if (hostName) return hostName
|
|
24
|
+
|
|
25
|
+
const envHost = String(process.env.HOSTNAME || '').trim()
|
|
26
|
+
if (envHost) return envHost
|
|
27
|
+
|
|
28
|
+
return 'IdleWatch Device'
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
function ensureDir(dirPath) {
|
|
14
32
|
fs.mkdirSync(dirPath, { recursive: true })
|
|
15
33
|
}
|
|
@@ -80,12 +98,12 @@ function looksLikeCloudApiKey(value) {
|
|
|
80
98
|
return /^iwk_[A-Za-z0-9_-]{20,}$/.test(String(value || '').trim())
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
function normalizeDeviceName(raw, fallback =
|
|
101
|
+
function normalizeDeviceName(raw, fallback = machineName()) {
|
|
84
102
|
const value = String(raw || '').trim().replace(/\s+/g, ' ')
|
|
85
103
|
return value || fallback
|
|
86
104
|
}
|
|
87
105
|
|
|
88
|
-
function sanitizeDeviceId(raw, fallback =
|
|
106
|
+
function sanitizeDeviceId(raw, fallback = machineName()) {
|
|
89
107
|
const base = normalizeDeviceName(raw, fallback).toLowerCase()
|
|
90
108
|
const sanitized = base
|
|
91
109
|
.replace(/[^a-z0-9._-]+/g, '-')
|
|
@@ -99,43 +117,47 @@ function cargoAvailable() {
|
|
|
99
117
|
return cargoProbe.status === 0
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
function bundledTuiBinaryPath() {
|
|
121
|
+
const platform = process.platform
|
|
122
|
+
const arch = process.arch
|
|
123
|
+
const ext = platform === 'win32' ? '.exe' : ''
|
|
124
|
+
return path.join(PACKAGE_ROOT, 'tui', 'bin', `${platform}-${arch}`, `idlewatch-setup${ext}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function tryBundledRustTui({ configDir, outputEnvFile }) {
|
|
128
|
+
const binPath = bundledTuiBinaryPath()
|
|
129
|
+
if (!fs.existsSync(binPath)) return { ok: false, reason: 'bundled-binary-missing', binPath }
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
fs.chmodSync(binPath, 0o755)
|
|
133
|
+
} catch {
|
|
134
|
+
// best effort
|
|
135
|
+
}
|
|
107
136
|
|
|
108
|
-
|
|
109
|
-
const install = spawnSync('sh', ['-c', 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y'], {
|
|
137
|
+
const run = spawnSync(binPath, [], {
|
|
110
138
|
stdio: 'inherit',
|
|
111
139
|
env: {
|
|
112
140
|
...process.env,
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
IDLEWATCH_ENROLL_CONFIG_DIR: configDir,
|
|
142
|
+
IDLEWATCH_ENROLL_OUTPUT_ENV_FILE: outputEnvFile
|
|
115
143
|
}
|
|
116
144
|
})
|
|
117
145
|
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
const cargoBinDir = path.join(process.env.CARGO_HOME || path.join(os.homedir(), '.cargo'), 'bin')
|
|
121
|
-
if (!process.env.PATH?.split(path.delimiter).includes(cargoBinDir)) {
|
|
122
|
-
process.env.PATH = `${cargoBinDir}${path.delimiter}${process.env.PATH || ''}`
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return cargoAvailable() ? { ok: true, installed: true } : { ok: false, reason: 'cargo-still-missing' }
|
|
146
|
+
if (run.status === 0) return { ok: true, binPath }
|
|
147
|
+
return { ok: false, reason: `bundled-binary-failed:${run.status ?? 'unknown'}`, binPath }
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
function tryRustTui({ configDir, outputEnvFile
|
|
150
|
+
function tryRustTui({ configDir, outputEnvFile }) {
|
|
129
151
|
const disabled = process.env.IDLEWATCH_DISABLE_RUST_TUI === '1'
|
|
130
152
|
if (disabled) return { ok: false, reason: 'disabled' }
|
|
131
153
|
|
|
154
|
+
const bundled = tryBundledRustTui({ configDir, outputEnvFile })
|
|
155
|
+
if (bundled.ok) return bundled
|
|
156
|
+
|
|
132
157
|
if (!cargoAvailable()) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} else {
|
|
137
|
-
return { ok: false, reason: 'cargo-missing' }
|
|
138
|
-
}
|
|
158
|
+
return bundled.reason === 'bundled-binary-missing'
|
|
159
|
+
? { ok: false, reason: 'bundled-binary-missing-and-cargo-missing', binPath: bundled.binPath }
|
|
160
|
+
: { ok: false, reason: 'cargo-missing' }
|
|
139
161
|
}
|
|
140
162
|
|
|
141
163
|
const manifestPath = path.join(PACKAGE_ROOT, 'tui', 'Cargo.toml')
|
|
@@ -170,7 +192,7 @@ export async function runEnrollmentWizard(options = {}) {
|
|
|
170
192
|
let mode = options.mode || process.env.IDLEWATCH_ENROLL_MODE || null
|
|
171
193
|
let cloudApiKey = normalizeCloudApiKey(options.cloudApiKey || process.env.IDLEWATCH_CLOUD_API_KEY || null)
|
|
172
194
|
let cloudIngestUrl = options.cloudIngestUrl || process.env.IDLEWATCH_CLOUD_INGEST_URL || 'https://api.idlewatch.com/api/ingest'
|
|
173
|
-
let deviceName = normalizeDeviceName(options.deviceName || process.env.IDLEWATCH_DEVICE_NAME ||
|
|
195
|
+
let deviceName = normalizeDeviceName(options.deviceName || process.env.IDLEWATCH_DEVICE_NAME || machineName())
|
|
174
196
|
|
|
175
197
|
const availableMonitorTargets = detectAvailableMonitorTargets()
|
|
176
198
|
let monitorTargets = normalizeMonitorTargets(
|
|
@@ -179,7 +201,7 @@ export async function runEnrollmentWizard(options = {}) {
|
|
|
179
201
|
)
|
|
180
202
|
|
|
181
203
|
if (!nonInteractive && !noTui) {
|
|
182
|
-
const tuiResult = tryRustTui({ configDir, outputEnvFile
|
|
204
|
+
const tuiResult = tryRustTui({ configDir, outputEnvFile })
|
|
183
205
|
if (tuiResult.ok) {
|
|
184
206
|
return {
|
|
185
207
|
mode: 'tui',
|
|
@@ -188,7 +210,9 @@ export async function runEnrollmentWizard(options = {}) {
|
|
|
188
210
|
}
|
|
189
211
|
}
|
|
190
212
|
|
|
191
|
-
if (
|
|
213
|
+
if (tuiResult.reason === 'bundled-binary-missing-and-cargo-missing') {
|
|
214
|
+
console.warn('IdleWatch TUI is not bundled for this platform and Cargo is not installed. Falling back to text setup. Use --no-tui to skip this check.')
|
|
215
|
+
} else if (!['disabled', 'cargo-missing', 'bundled-binary-missing'].includes(tuiResult.reason || '')) {
|
|
192
216
|
console.warn(`IdleWatch TUI unavailable (${tuiResult.reason || 'unknown'}). Falling back to text setup.`)
|
|
193
217
|
}
|
|
194
218
|
}
|
|
@@ -223,7 +247,7 @@ export async function runEnrollmentWizard(options = {}) {
|
|
|
223
247
|
monitorTargets = normalizeMonitorTargets(monitorInput || suggested, availableMonitorTargets)
|
|
224
248
|
}
|
|
225
249
|
|
|
226
|
-
const safeDeviceId = sanitizeDeviceId(options.deviceId || process.env.IDLEWATCH_DEVICE_ID || deviceName,
|
|
250
|
+
const safeDeviceId = sanitizeDeviceId(options.deviceId || process.env.IDLEWATCH_DEVICE_ID || deviceName, machineName())
|
|
227
251
|
const localLogPath = path.join(configDir, 'logs', `${safeDeviceId}-metrics.ndjson`)
|
|
228
252
|
const localCachePath = path.join(configDir, 'cache', `${safeDeviceId}-openclaw-last-good.json`)
|
|
229
253
|
|
|
Binary file
|
package/tui/src/main.rs
CHANGED
|
@@ -70,6 +70,34 @@ fn command_exists(cmd: &str, args: &[&str]) -> bool {
|
|
|
70
70
|
.unwrap_or(false)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
fn machine_name() -> String {
|
|
74
|
+
if cfg!(target_os = "macos") {
|
|
75
|
+
if let Ok(output) = Command::new("scutil").args(["--get", "ComputerName"]).output() {
|
|
76
|
+
if output.status.success() {
|
|
77
|
+
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
78
|
+
if !name.is_empty() {
|
|
79
|
+
return name;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if let Ok(output) = Command::new("hostname").output() {
|
|
86
|
+
if output.status.success() {
|
|
87
|
+
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
88
|
+
if !name.is_empty() {
|
|
89
|
+
return name;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
std::env::var("HOSTNAME")
|
|
95
|
+
.ok()
|
|
96
|
+
.map(|value| value.trim().to_string())
|
|
97
|
+
.filter(|value| !value.is_empty())
|
|
98
|
+
.unwrap_or_else(|| "IdleWatch Device".to_string())
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
fn parse_env_file(path: &Path) -> ExistingConfig {
|
|
74
102
|
let mut config = ExistingConfig::default();
|
|
75
103
|
let Ok(raw) = fs::read_to_string(path) else {
|
|
@@ -443,7 +471,7 @@ fn main() -> Result<()> {
|
|
|
443
471
|
let env_file = std::env::var("IDLEWATCH_ENROLL_OUTPUT_ENV_FILE")
|
|
444
472
|
.map(PathBuf::from)
|
|
445
473
|
.unwrap_or_else(|_| config_dir.join("idlewatch.env"));
|
|
446
|
-
let host = sanitize_host(&
|
|
474
|
+
let host = sanitize_host(&machine_name());
|
|
447
475
|
let existing = parse_env_file(&env_file);
|
|
448
476
|
|
|
449
477
|
enable_raw_mode()?;
|
|
@@ -464,7 +492,7 @@ fn main() -> Result<()> {
|
|
|
464
492
|
KeyCode::Char('q') | KeyCode::Esc => {
|
|
465
493
|
disable_raw_mode()?;
|
|
466
494
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
467
|
-
return
|
|
495
|
+
return Err(anyhow!("setup_cancelled"));
|
|
468
496
|
}
|
|
469
497
|
_ => {}
|
|
470
498
|
}
|
|
@@ -503,7 +531,7 @@ fn main() -> Result<()> {
|
|
|
503
531
|
KeyCode::Char('q') | KeyCode::Esc => {
|
|
504
532
|
disable_raw_mode()?;
|
|
505
533
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
506
|
-
return
|
|
534
|
+
return Err(anyhow!("setup_cancelled"));
|
|
507
535
|
}
|
|
508
536
|
_ => {}
|
|
509
537
|
}
|
|
@@ -545,7 +573,7 @@ fn main() -> Result<()> {
|
|
|
545
573
|
KeyCode::Char('q') | KeyCode::Esc => {
|
|
546
574
|
disable_raw_mode()?;
|
|
547
575
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
548
|
-
return
|
|
576
|
+
return Err(anyhow!("setup_cancelled"));
|
|
549
577
|
}
|
|
550
578
|
KeyCode::Char(c) => {
|
|
551
579
|
device_name_input.push(c);
|
|
@@ -592,7 +620,7 @@ fn main() -> Result<()> {
|
|
|
592
620
|
KeyCode::Char('q') | KeyCode::Esc => {
|
|
593
621
|
disable_raw_mode()?;
|
|
594
622
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
595
|
-
return
|
|
623
|
+
return Err(anyhow!("setup_cancelled"));
|
|
596
624
|
}
|
|
597
625
|
KeyCode::Char(c) => {
|
|
598
626
|
cloud_api_key_input.push(c);
|