idlewatch 0.1.2 → 0.1.3
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/idlewatch-agent.js +1 -1
- package/package.json +1 -1
- package/scripts/validate-onboarding.mjs +39 -18
- package/src/config.js +18 -2
- package/src/enrollment.js +21 -11
package/bin/idlewatch-agent.js
CHANGED
|
@@ -391,7 +391,7 @@ if (quickstartRequested) {
|
|
|
391
391
|
|
|
392
392
|
console.error(`⚠️ Setup is not finished yet. Mode=${result.mode} device=${result.deviceName} envFile=${result.outputEnvFile}`)
|
|
393
393
|
console.error('The first required telemetry sample did not publish successfully, so this device may not be linked yet.')
|
|
394
|
-
console.error(`Retry with: idlewatch --once`)
|
|
394
|
+
console.error(`Retry with: set -a; source "${result.outputEnvFile}"; set +a && idlewatch --once`)
|
|
395
395
|
console.error('Or rerun: idlewatch quickstart')
|
|
396
396
|
process.exit(onceRun.status ?? 1)
|
|
397
397
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs'
|
|
|
2
2
|
import http from 'node:http'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import {
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
6
|
|
|
7
7
|
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..')
|
|
8
8
|
const binPath = path.join(repoRoot, 'bin', 'idlewatch-agent.js')
|
|
@@ -34,30 +34,47 @@ await new Promise((resolve, reject) => {
|
|
|
34
34
|
const address = server.address()
|
|
35
35
|
const cloudIngestUrl = `http://127.0.0.1:${address.port}/api/ingest`
|
|
36
36
|
|
|
37
|
+
function runQuickstart(env) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const child = spawn(process.execPath, [binPath, 'quickstart'], {
|
|
40
|
+
env,
|
|
41
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
let stdout = ''
|
|
45
|
+
let stderr = ''
|
|
46
|
+
child.stdout.on('data', (chunk) => {
|
|
47
|
+
stdout += chunk
|
|
48
|
+
})
|
|
49
|
+
child.stderr.on('data', (chunk) => {
|
|
50
|
+
stderr += chunk
|
|
51
|
+
})
|
|
52
|
+
child.on('error', reject)
|
|
53
|
+
child.on('close', (code) => resolve({ code, stdout, stderr }))
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
try {
|
|
38
58
|
const envOut = path.join(tmpRoot, 'generated.env')
|
|
39
59
|
const configDir = path.join(tmpRoot, 'config')
|
|
40
60
|
const localLogPath = path.join(configDir, 'logs', 'validator-box-metrics.ndjson')
|
|
41
61
|
const lastGoodCachePath = path.join(configDir, 'cache', 'validator-box-openclaw-last-good.json')
|
|
42
62
|
|
|
43
|
-
const run =
|
|
44
|
-
env
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
IDLEWATCH_OPENCLAW_USAGE: 'off'
|
|
56
|
-
},
|
|
57
|
-
encoding: 'utf8'
|
|
63
|
+
const run = await runQuickstart({
|
|
64
|
+
...process.env,
|
|
65
|
+
IDLEWATCH_ENROLL_NON_INTERACTIVE: '1',
|
|
66
|
+
IDLEWATCH_ENROLL_MODE: 'production',
|
|
67
|
+
IDLEWATCH_CLOUD_API_KEY: 'iwk_abcdefghijklmnopqrstuvwxyz123456',
|
|
68
|
+
IDLEWATCH_CLOUD_INGEST_URL: cloudIngestUrl,
|
|
69
|
+
IDLEWATCH_ENROLL_OUTPUT_ENV_FILE: envOut,
|
|
70
|
+
IDLEWATCH_ENROLL_CONFIG_DIR: configDir,
|
|
71
|
+
IDLEWATCH_DEVICE_NAME: 'Validator Box',
|
|
72
|
+
IDLEWATCH_DEVICE_ID: 'validator-box',
|
|
73
|
+
IDLEWATCH_MONITOR_TARGETS: 'cpu,memory',
|
|
74
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off'
|
|
58
75
|
})
|
|
59
76
|
|
|
60
|
-
if (run.
|
|
77
|
+
if (run.code !== 0) {
|
|
61
78
|
throw new Error(`quickstart failed\nstdout:\n${run.stdout}\nstderr:\n${run.stderr}`)
|
|
62
79
|
}
|
|
63
80
|
|
|
@@ -82,6 +99,10 @@ try {
|
|
|
82
99
|
}
|
|
83
100
|
}
|
|
84
101
|
|
|
102
|
+
if (!run.stdout.includes('✅ Setup complete.')) {
|
|
103
|
+
throw new Error('quickstart success output did not include setup completion summary')
|
|
104
|
+
}
|
|
105
|
+
|
|
85
106
|
if (requests.length === 0) {
|
|
86
107
|
throw new Error('quickstart did not send the initial telemetry sample')
|
|
87
108
|
}
|
|
@@ -93,6 +114,6 @@ try {
|
|
|
93
114
|
|
|
94
115
|
console.log('onboarding validation passed')
|
|
95
116
|
} finally {
|
|
96
|
-
server.close()
|
|
117
|
+
await new Promise((resolve) => server.close(resolve))
|
|
97
118
|
fs.rmSync(tmpRoot, { recursive: true, force: true })
|
|
98
119
|
}
|
package/src/config.js
CHANGED
|
@@ -21,6 +21,22 @@ const isNonNegFinite = (v) => Number.isFinite(v) && v >= 0
|
|
|
21
21
|
const isNonNegInt = (v) => Number.isInteger(v) && v >= 0
|
|
22
22
|
const isBool01 = (v) => v === 0 || v === 1
|
|
23
23
|
|
|
24
|
+
function expandSupportedPathVars(value) {
|
|
25
|
+
if (typeof value !== 'string' || !value) return value
|
|
26
|
+
|
|
27
|
+
const home = process.env.HOME || os.homedir()
|
|
28
|
+
const tmpdir = process.env.TMPDIR || os.tmpdir()
|
|
29
|
+
|
|
30
|
+
return value
|
|
31
|
+
.replace(/^~(?=$|\/)/, home)
|
|
32
|
+
.replace(/\$\{HOME\}|\$HOME/g, home)
|
|
33
|
+
.replace(/\$\{TMPDIR\}|\$TMPDIR/g, tmpdir)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveEnvPath(value) {
|
|
37
|
+
return path.resolve(expandSupportedPathVars(value))
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
/**
|
|
25
41
|
* Build the full IdleWatch configuration from environment variables.
|
|
26
42
|
* Throws on invalid values.
|
|
@@ -63,11 +79,11 @@ export function buildConfig() {
|
|
|
63
79
|
const BASE_DIR = path.join(os.homedir(), '.idlewatch')
|
|
64
80
|
|
|
65
81
|
const LOCAL_LOG_PATH = process.env.IDLEWATCH_LOCAL_LOG_PATH
|
|
66
|
-
?
|
|
82
|
+
? resolveEnvPath(process.env.IDLEWATCH_LOCAL_LOG_PATH)
|
|
67
83
|
: path.join(BASE_DIR, 'logs', `${SAFE_HOST}-metrics.ndjson`)
|
|
68
84
|
|
|
69
85
|
const OPENCLAW_LAST_GOOD_CACHE_PATH = process.env.IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH
|
|
70
|
-
?
|
|
86
|
+
? resolveEnvPath(process.env.IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH)
|
|
71
87
|
: path.join(BASE_DIR, 'cache', `${SAFE_HOST}-openclaw-last-good.json`)
|
|
72
88
|
|
|
73
89
|
return Object.freeze({
|
package/src/enrollment.js
CHANGED
|
@@ -3,6 +3,7 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import readline from 'node:readline/promises'
|
|
5
5
|
import process from 'node:process'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
6
7
|
import { spawnSync } from 'node:child_process'
|
|
7
8
|
|
|
8
9
|
function defaultConfigDir() {
|
|
@@ -24,6 +25,8 @@ function writeSecureFile(filePath, content) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const MONITOR_TARGET_CHOICES = ['cpu', 'memory', 'gpu', 'openclaw']
|
|
28
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
29
|
+
const PACKAGE_ROOT = path.resolve(MODULE_DIR, '..')
|
|
27
30
|
|
|
28
31
|
function commandExists(bin, args = ['--version']) {
|
|
29
32
|
const result = spawnSync(bin, args, { stdio: 'ignore' })
|
|
@@ -93,13 +96,13 @@ function sanitizeDeviceId(raw, fallback = os.hostname()) {
|
|
|
93
96
|
|
|
94
97
|
function tryRustTui({ configDir, outputEnvFile }) {
|
|
95
98
|
const disabled = process.env.IDLEWATCH_DISABLE_RUST_TUI === '1'
|
|
96
|
-
if (disabled) return false
|
|
99
|
+
if (disabled) return { ok: false, reason: 'disabled' }
|
|
97
100
|
|
|
98
101
|
const cargoProbe = spawnSync('cargo', ['--version'], { stdio: 'ignore' })
|
|
99
|
-
if (cargoProbe.status !== 0) return false
|
|
102
|
+
if (cargoProbe.status !== 0) return { ok: false, reason: 'cargo-missing' }
|
|
100
103
|
|
|
101
|
-
const manifestPath = path.
|
|
102
|
-
if (!fs.existsSync(manifestPath)) return false
|
|
104
|
+
const manifestPath = path.join(PACKAGE_ROOT, 'tui', 'Cargo.toml')
|
|
105
|
+
if (!fs.existsSync(manifestPath)) return { ok: false, reason: 'manifest-missing', manifestPath }
|
|
103
106
|
|
|
104
107
|
const run = spawnSync('cargo', ['run', '--quiet', '--manifest-path', manifestPath], {
|
|
105
108
|
stdio: 'inherit',
|
|
@@ -111,10 +114,10 @@ function tryRustTui({ configDir, outputEnvFile }) {
|
|
|
111
114
|
})
|
|
112
115
|
|
|
113
116
|
if (run.status === 0) {
|
|
114
|
-
return true
|
|
117
|
+
return { ok: true, manifestPath }
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
return false
|
|
120
|
+
return { ok: false, reason: `cargo-run-failed:${run.status ?? 'unknown'}`, manifestPath }
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
function promptModeText() {
|
|
@@ -137,11 +140,18 @@ export async function runEnrollmentWizard(options = {}) {
|
|
|
137
140
|
availableMonitorTargets
|
|
138
141
|
)
|
|
139
142
|
|
|
140
|
-
if (!nonInteractive
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
if (!nonInteractive) {
|
|
144
|
+
const tuiResult = tryRustTui({ configDir, outputEnvFile })
|
|
145
|
+
if (tuiResult.ok) {
|
|
146
|
+
return {
|
|
147
|
+
mode: 'tui',
|
|
148
|
+
configDir,
|
|
149
|
+
outputEnvFile
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!['disabled', 'cargo-missing'].includes(tuiResult.reason || '')) {
|
|
154
|
+
console.warn(`IdleWatch TUI unavailable (${tuiResult.reason || 'unknown'}). Falling back to text setup.`)
|
|
145
155
|
}
|
|
146
156
|
}
|
|
147
157
|
|