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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idlewatch",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Host telemetry collector for IdleWatch",
5
5
  "type": "module",
6
6
  "files": [
@@ -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 { spawnSync } from 'node:child_process'
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 = spawnSync(process.execPath, [binPath, 'quickstart'], {
44
- env: {
45
- ...process.env,
46
- IDLEWATCH_ENROLL_NON_INTERACTIVE: '1',
47
- IDLEWATCH_ENROLL_MODE: 'production',
48
- IDLEWATCH_CLOUD_API_KEY: 'iwk_abcdefghijklmnopqrstuvwxyz123456',
49
- IDLEWATCH_CLOUD_INGEST_URL: cloudIngestUrl,
50
- IDLEWATCH_ENROLL_OUTPUT_ENV_FILE: envOut,
51
- IDLEWATCH_ENROLL_CONFIG_DIR: configDir,
52
- IDLEWATCH_DEVICE_NAME: 'Validator Box',
53
- IDLEWATCH_DEVICE_ID: 'validator-box',
54
- IDLEWATCH_MONITOR_TARGETS: 'cpu,memory',
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.status !== 0) {
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
- ? path.resolve(process.env.IDLEWATCH_LOCAL_LOG_PATH)
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
- ? path.resolve(process.env.IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH)
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.resolve(process.cwd(), 'tui', 'Cargo.toml')
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 && tryRustTui({ configDir, outputEnvFile })) {
141
- return {
142
- mode: 'tui',
143
- configDir,
144
- outputEnvFile
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