idlewatch 0.1.0

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.
Files changed (110) hide show
  1. package/.env.example +73 -0
  2. package/.github/workflows/ci.yml +99 -0
  3. package/.github/workflows/release-macos-trusted.yml +103 -0
  4. package/README.md +336 -0
  5. package/bin/idlewatch-agent.js +1053 -0
  6. package/docs/onboarding-external.md +58 -0
  7. package/docs/packaging/macos-dmg.md +199 -0
  8. package/docs/packaging/macos-launch-agent.md +70 -0
  9. package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
  10. package/docs/qa/mac-qa-log.md +2864 -0
  11. package/docs/telemetry/idle-stale-policy.md +57 -0
  12. package/docs/telemetry/openclaw-mapping.md +80 -0
  13. package/package.json +76 -0
  14. package/scripts/build-dmg.sh +65 -0
  15. package/scripts/install-macos-launch-agent.sh +78 -0
  16. package/scripts/lib/telemetry-row-parser.mjs +100 -0
  17. package/scripts/package-macos.sh +228 -0
  18. package/scripts/uninstall-macos-launch-agent.sh +30 -0
  19. package/scripts/validate-all.sh +142 -0
  20. package/scripts/validate-bin.mjs +25 -0
  21. package/scripts/validate-dmg-checksum.sh +37 -0
  22. package/scripts/validate-dmg-install.sh +155 -0
  23. package/scripts/validate-dry-run-schema.mjs +257 -0
  24. package/scripts/validate-onboarding.mjs +63 -0
  25. package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
  26. package/scripts/validate-openclaw-release-gates.mjs +51 -0
  27. package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
  28. package/scripts/validate-openclaw-usage-health.mjs +95 -0
  29. package/scripts/validate-packaged-artifact.mjs +233 -0
  30. package/scripts/validate-packaged-bundled-runtime.sh +191 -0
  31. package/scripts/validate-packaged-metadata.sh +43 -0
  32. package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
  33. package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
  34. package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
  35. package/scripts/validate-packaged-sourcemaps.mjs +82 -0
  36. package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
  37. package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
  38. package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
  39. package/scripts/validate-trusted-prereqs.sh +44 -0
  40. package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
  41. package/scripts/validate-usage-freshness-e2e.mjs +81 -0
  42. package/skill/SKILL.md +43 -0
  43. package/src/config.js +100 -0
  44. package/src/enrollment.js +176 -0
  45. package/src/gpu.js +115 -0
  46. package/src/memory.js +67 -0
  47. package/src/openclaw-cache.js +51 -0
  48. package/src/openclaw-usage.js +1020 -0
  49. package/src/telemetry-mapping.js +54 -0
  50. package/src/usage-alert.js +41 -0
  51. package/src/usage-freshness.js +31 -0
  52. package/test/config.test.mjs +112 -0
  53. package/test/fixtures/gpu-agx.txt +2 -0
  54. package/test/fixtures/gpu-iogpu.txt +2 -0
  55. package/test/fixtures/gpu-top-grep.txt +2 -0
  56. package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
  57. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
  58. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
  59. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
  60. package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
  61. package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
  62. package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
  63. package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
  64. package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
  65. package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
  66. package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
  67. package/test/fixtures/openclaw-stats.json +17 -0
  68. package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
  69. package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
  70. package/test/fixtures/openclaw-status-control-noise.txt +1 -0
  71. package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
  72. package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
  73. package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
  74. package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
  75. package/test/fixtures/openclaw-status-multi-json.txt +3 -0
  76. package/test/fixtures/openclaw-status-nested-recent.json +19 -0
  77. package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
  78. package/test/fixtures/openclaw-status-noisy.txt +3 -0
  79. package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
  80. package/test/fixtures/openclaw-status-result-session.json +15 -0
  81. package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
  82. package/test/fixtures/openclaw-status-session-map.json +28 -0
  83. package/test/fixtures/openclaw-status-session-model-name.json +18 -0
  84. package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
  85. package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
  86. package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
  87. package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
  88. package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
  89. package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
  90. package/test/fixtures/openclaw-status-strings.json +38 -0
  91. package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
  92. package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
  93. package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
  94. package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
  95. package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
  96. package/test/fixtures/openclaw-status.json +41 -0
  97. package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
  98. package/test/gpu.test.mjs +58 -0
  99. package/test/memory.test.mjs +35 -0
  100. package/test/openclaw-cache.test.mjs +48 -0
  101. package/test/openclaw-env.test.mjs +365 -0
  102. package/test/openclaw-usage.test.mjs +555 -0
  103. package/test/telemetry-mapping.test.mjs +69 -0
  104. package/test/telemetry-row-parser.test.mjs +44 -0
  105. package/test/usage-alert.test.mjs +73 -0
  106. package/test/usage-freshness.test.mjs +63 -0
  107. package/test/validate-dry-run-schema.test.mjs +146 -0
  108. package/tui/Cargo.lock +801 -0
  109. package/tui/Cargo.toml +11 -0
  110. package/tui/src/main.rs +368 -0
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ DIST_DIR="$ROOT_DIR/dist"
6
+ APP_DIR="$DIST_DIR/IdleWatch.app"
7
+ CONTENTS_DIR="$APP_DIR/Contents"
8
+ RESOURCES_DIR="$CONTENTS_DIR/Resources"
9
+ MACOS_DIR="$CONTENTS_DIR/MacOS"
10
+ VERSION="$(node -p "require('./package.json').version" 2>/dev/null || node -e "import('./package.json',{with:{type:'json'}}).then(m=>console.log(m.default.version))")"
11
+ SOURCE_GIT_COMMIT="$(git -C "$ROOT_DIR" rev-parse HEAD 2>/dev/null || true)"
12
+ SOURCE_GIT_DIRTY="false"
13
+ SOURCE_GIT_DIRTY_KNOWN="false"
14
+ if [[ -n "$SOURCE_GIT_COMMIT" ]]; then
15
+ if git -C "$ROOT_DIR" diff --quiet --ignore-submodules -- . && [[ -z "$(git -C "$ROOT_DIR" status --porcelain 2>/dev/null || true)" ]]; then
16
+ SOURCE_GIT_DIRTY_KNOWN="true"
17
+ SOURCE_GIT_DIRTY="false"
18
+ else
19
+ SOURCE_GIT_DIRTY_KNOWN="true"
20
+ SOURCE_GIT_DIRTY="true"
21
+ fi
22
+ fi
23
+ CODESIGN_IDENTITY="${MACOS_CODESIGN_IDENTITY:-}"
24
+ REQUIRE_TRUSTED="${IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION:-0}"
25
+ NODE_RUNTIME_DIR="${IDLEWATCH_NODE_RUNTIME_DIR:-}"
26
+ OPENCLAW_BIN_HINT="${IDLEWATCH_OPENCLAW_BIN:-${IDLEWATCH_OPENCLAW_BIN_HINT:-}}"
27
+ ALLOW_UNSIGNED_TAG_RELEASE="${IDLEWATCH_ALLOW_UNSIGNED_TAG_RELEASE:-0}"
28
+ SKIP_SOURCEMAP_VALIDATION="${IDLEWATCH_SKIP_SOURCEMAP_VALIDATION:-0}"
29
+
30
+ if [[ "$REQUIRE_TRUSTED" != "1" && "${GITHUB_ACTIONS:-}" == "true" ]]; then
31
+ REF_NAME="${GITHUB_REF:-}"
32
+ REF_TYPE="${GITHUB_REF_TYPE:-}"
33
+ if [[ "$REF_TYPE" == "tag" || "$REF_NAME" == refs/tags/* ]]; then
34
+ if [[ "$ALLOW_UNSIGNED_TAG_RELEASE" != "1" ]]; then
35
+ REQUIRE_TRUSTED="1"
36
+ echo "Detected CI tag build; enforcing trusted distribution requirements (set IDLEWATCH_ALLOW_UNSIGNED_TAG_RELEASE=1 to bypass intentionally)."
37
+ fi
38
+ fi
39
+ fi
40
+
41
+ if [[ "$REQUIRE_TRUSTED" == "1" && -z "$CODESIGN_IDENTITY" ]]; then
42
+ echo "IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1 requires MACOS_CODESIGN_IDENTITY to be set." >&2
43
+ exit 1
44
+ fi
45
+
46
+ rm -rf "$APP_DIR" "$DIST_DIR/dmg-root"
47
+ mkdir -p "$RESOURCES_DIR" "$MACOS_DIR"
48
+
49
+ pushd "$ROOT_DIR" >/dev/null
50
+ TMP_PACK_INFO="$(mktemp)"
51
+ npm pack --silent --json >"$TMP_PACK_INFO"
52
+ PKG_TGZ="$(node -e 'const fs = require("fs"); const txt = fs.readFileSync(0, "utf8"); let data = null; try { data = JSON.parse(txt.trim()); } catch { process.exit(1) } const entry = Array.isArray(data) ? data[0] : data; const filename = entry && (entry.filename || entry.name); if (!filename) process.exit(1); process.stdout.write(String(filename));' <"$TMP_PACK_INFO")"
53
+ rm -f "$TMP_PACK_INFO"
54
+ cp "$PKG_TGZ" "$RESOURCES_DIR/"
55
+ PAYLOAD_DIR="$RESOURCES_DIR/payload"
56
+ rm -rf "$PAYLOAD_DIR"
57
+ mkdir -p "$PAYLOAD_DIR"
58
+ tar -xzf "$RESOURCES_DIR/$PKG_TGZ" -C "$PAYLOAD_DIR"
59
+ rm -f "$PKG_TGZ"
60
+
61
+ PAYLOAD_PKG_DIR="$PAYLOAD_DIR/package"
62
+ if [[ ! -f "$PAYLOAD_PKG_DIR/package.json" ]]; then
63
+ echo "IdleWatch package payload missing package.json ($PAYLOAD_PKG_DIR/package.json)" >&2
64
+ exit 1
65
+ fi
66
+
67
+ # Ensure runtime dependencies are present inside the packaged payload so DMG installs
68
+ # run without relying on workspace-level node_modules.
69
+ (
70
+ cd "$PAYLOAD_PKG_DIR"
71
+
72
+ # Prefer lockfile-based install when available for reproducible dependency snapshots.
73
+ if [[ -f "$ROOT_DIR/package-lock.json" ]]; then
74
+ cp "$ROOT_DIR/package-lock.json" package-lock.json
75
+ fi
76
+
77
+ if [[ -f package-lock.json ]]; then
78
+ npm ci --omit=dev --ignore-scripts --no-audit --no-fund --silent
79
+ else
80
+ npm install --omit=dev --ignore-scripts --no-audit --no-fund --silent
81
+ fi
82
+ )
83
+
84
+ if [[ "$SKIP_SOURCEMAP_VALIDATION" != "1" ]]; then
85
+ node "$ROOT_DIR/scripts/validate-packaged-sourcemaps.mjs" "$PAYLOAD_PKG_DIR"
86
+ fi
87
+
88
+ if [[ -n "$NODE_RUNTIME_DIR" ]]; then
89
+ RUNTIME_NODE_BIN="$NODE_RUNTIME_DIR/bin/node"
90
+ if [[ ! -x "$RUNTIME_NODE_BIN" ]]; then
91
+ echo "IDLEWATCH_NODE_RUNTIME_DIR must contain an executable bin/node (missing: $RUNTIME_NODE_BIN)" >&2
92
+ exit 1
93
+ fi
94
+
95
+ RUNTIME_DEST_DIR="$RESOURCES_DIR/runtime/node"
96
+ rm -rf "$RUNTIME_DEST_DIR"
97
+ mkdir -p "$(dirname "$RUNTIME_DEST_DIR")"
98
+ # Copy essential runtime directories with symlink dereference so packaged runtime is portable even if host runtime is a symlink.
99
+ # This avoids pulling in nonessential completion/doc symlink trees that can create copy noise.
100
+ for runtimeDir in bin lib include; do
101
+ if [[ -d "$NODE_RUNTIME_DIR/$runtimeDir" ]]; then
102
+ mkdir -p "$RUNTIME_DEST_DIR/$runtimeDir"
103
+ cp -R -L "$NODE_RUNTIME_DIR/$runtimeDir/" "$RUNTIME_DEST_DIR/$runtimeDir/"
104
+ fi
105
+ done
106
+ fi
107
+ NODE_RUNTIME_BUNDLED=false
108
+ SIGNED_ARTIFACT=false
109
+ if [[ -n "$NODE_RUNTIME_DIR" ]]; then
110
+ NODE_RUNTIME_BUNDLED=true
111
+ fi
112
+ if [[ -n "$CODESIGN_IDENTITY" ]]; then
113
+ SIGNED_ARTIFACT=true
114
+ fi
115
+
116
+ cat > "$RESOURCES_DIR/packaging-metadata.json" <<METADATA
117
+ {
118
+ "name": "idlewatch-agent",
119
+ "version": "${VERSION}",
120
+ "builtAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
121
+ "platform": "darwin",
122
+ "bundleName": "IdleWatch.app",
123
+ "nodeRuntimeBundled": ${NODE_RUNTIME_BUNDLED},
124
+ "nodeRuntimeSource": "${NODE_RUNTIME_DIR:-}",
125
+ "signed": ${SIGNED_ARTIFACT},
126
+ "codesignIdentity": "${CODESIGN_IDENTITY:-}",
127
+ "openclawBinHint": "${OPENCLAW_BIN_HINT:-}",
128
+ "launcher": "Contents/MacOS/IdleWatch",
129
+ "payloadTarball": "${PKG_TGZ}",
130
+ "payloadNode": "$(node -v 2>/dev/null || echo unknown)",
131
+ "sourceGitCommit": "${SOURCE_GIT_COMMIT}",
132
+ "sourceGitDirty": ${SOURCE_GIT_DIRTY},
133
+ "sourceGitDirtyKnown": ${SOURCE_GIT_DIRTY_KNOWN}
134
+ }
135
+ METADATA
136
+
137
+ cat > "$CONTENTS_DIR/Info.plist" <<PLIST
138
+ <?xml version="1.0" encoding="UTF-8"?>
139
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
140
+ <plist version="1.0">
141
+ <dict>
142
+ <key>CFBundleName</key><string>IdleWatch</string>
143
+ <key>CFBundleDisplayName</key><string>IdleWatch</string>
144
+ <key>CFBundleIdentifier</key><string>com.idlewatch.agent</string>
145
+ <key>CFBundleVersion</key><string>${VERSION}</string>
146
+ <key>CFBundleShortVersionString</key><string>${VERSION}</string>
147
+ <key>CFBundleExecutable</key><string>IdleWatch</string>
148
+ <key>LSMinimumSystemVersion</key><string>13.0</string>
149
+ </dict>
150
+ </plist>
151
+ PLIST
152
+
153
+ cat > "$MACOS_DIR/IdleWatch" <<'SH'
154
+ #!/usr/bin/env bash
155
+ set -euo pipefail
156
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
157
+ RESOURCES_DIR="$(cd "$SCRIPT_DIR/../Resources" && pwd)"
158
+ NODE_BIN="${IDLEWATCH_NODE_BIN:-}"
159
+ if [[ -z "$NODE_BIN" ]]; then
160
+ BUNDLED_NODE_BIN="$RESOURCES_DIR/runtime/node/bin/node"
161
+ if [[ -x "$BUNDLED_NODE_BIN" ]]; then
162
+ NODE_BIN="$BUNDLED_NODE_BIN"
163
+ else
164
+ NODE_BIN="$(command -v node || true)"
165
+ fi
166
+ fi
167
+
168
+ if [[ -z "$NODE_BIN" || ! -x "$NODE_BIN" ]]; then
169
+ echo "IdleWatch requires Node.js 20+ (node binary not found). Install Node.js or set IDLEWATCH_NODE_BIN and retry." >&2
170
+ exit 1
171
+ fi
172
+
173
+ NODE_MAJOR="$($NODE_BIN -p "process.versions.node.split('.')[0]" 2>/dev/null || echo "")"
174
+ if [[ -z "$NODE_MAJOR" || "$NODE_MAJOR" -lt 20 ]]; then
175
+ NODE_VERSION="$($NODE_BIN -v 2>/dev/null || echo "unknown")"
176
+ echo "IdleWatch requires Node.js 20+ (found $NODE_VERSION at $NODE_BIN). Upgrade Node.js or set IDLEWATCH_NODE_BIN to a compatible runtime." >&2
177
+ exit 1
178
+ fi
179
+
180
+ # Backward-compatible OpenClaw launcher hint precedence:
181
+ # 1) IDLEWATCH_OPENCLAW_BIN (runtime override)
182
+ # 2) IDLEWATCH_OPENCLAW_BIN_HINT (legacy launcher hint)
183
+ # 3) packaging metadata fallback (build-time hint)
184
+ OPENCLAW_BIN_HINT="${IDLEWATCH_OPENCLAW_BIN:-${IDLEWATCH_OPENCLAW_BIN_HINT:-}}"
185
+ if [[ -z "$OPENCLAW_BIN_HINT" && -f "$RESOURCES_DIR/packaging-metadata.json" ]]; then
186
+ OPENCLAW_BIN_HINT="$($NODE_BIN -e 'const fs = require("fs"); const filePath = process.argv[1]; try { const data = JSON.parse(fs.readFileSync(filePath, "utf8")); const hint = data && data.openclawBinHint ? data.openclawBinHint : ""; if (hint) process.stdout.write(String(hint)); } catch (_) {}' "$RESOURCES_DIR/packaging-metadata.json")"
187
+ fi
188
+
189
+ if [[ -n "$OPENCLAW_BIN_HINT" && -x "$OPENCLAW_BIN_HINT" ]]; then
190
+ export IDLEWATCH_OPENCLAW_BIN="$OPENCLAW_BIN_HINT"
191
+ elif [[ -n "$OPENCLAW_BIN_HINT" ]]; then
192
+ echo "Warning: packaged OpenClaw binary hint is not executable at: $OPENCLAW_BIN_HINT" >&2
193
+ fi
194
+
195
+ PAYLOAD_BIN="$RESOURCES_DIR/payload/package/bin/idlewatch-agent.js"
196
+ if [[ ! -f "$PAYLOAD_BIN" ]]; then
197
+ echo "IdleWatch package payload missing ($PAYLOAD_BIN)" >&2
198
+ exit 1
199
+ fi
200
+
201
+ exec "$NODE_BIN" "$PAYLOAD_BIN" "$@"
202
+ SH
203
+ chmod +x "$MACOS_DIR/IdleWatch"
204
+
205
+ if [[ -n "$CODESIGN_IDENTITY" ]]; then
206
+ echo "Codesigning IdleWatch.app with identity: $CODESIGN_IDENTITY"
207
+ codesign --deep --force --options runtime --sign "$CODESIGN_IDENTITY" "$APP_DIR"
208
+ codesign --verify --deep --strict "$APP_DIR"
209
+ else
210
+ echo "MACOS_CODESIGN_IDENTITY not set; leaving app unsigned."
211
+ fi
212
+
213
+ mkdir -p "$DIST_DIR/dmg-root"
214
+ cp -R "$APP_DIR" "$DIST_DIR/dmg-root/"
215
+ ln -s /Applications "$DIST_DIR/dmg-root/Applications"
216
+
217
+ cat <<'EOF'
218
+ Mac app scaffold package complete.
219
+ Next steps:
220
+ 1) Test: ./dist/IdleWatch.app/Contents/MacOS/IdleWatch --dry-run
221
+ 2) Build DMG: ./scripts/build-dmg.sh
222
+ 3) Optional bundle runtime for node-less targets: export IDLEWATCH_NODE_RUNTIME_DIR="/path/to/node-runtime"
223
+ 4) Optional signing: export MACOS_CODESIGN_IDENTITY="Developer ID Application: ..." then rerun package-macos
224
+ 5) Optional notarize+staple DMG: set MACOS_NOTARY_PROFILE and rerun build-dmg
225
+ 6) Enforce trusted artifacts: export IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1
226
+ EOF
227
+
228
+ popd >/dev/null
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PLIST_LABEL="${IDLEWATCH_LAUNCH_AGENT_LABEL:-com.idlewatch.agent}"
5
+ PLIST_ROOT="${IDLEWATCH_LAUNCH_AGENT_PLIST_ROOT:-$HOME/Library/LaunchAgents}"
6
+ PLIST_PATH="$PLIST_ROOT/$PLIST_LABEL.plist"
7
+
8
+ if ! command -v launchctl >/dev/null 2>&1; then
9
+ echo "launchctl not available; this script must be run on macOS." >&2
10
+ exit 1
11
+ fi
12
+
13
+ if [[ -z "${PLIST_LABEL}" ]]; then
14
+ echo "IDLEWATCH_LAUNCH_AGENT_LABEL is required" >&2
15
+ exit 1
16
+ fi
17
+
18
+ USER_GUID="$(id -u)"
19
+ PLIST_ID="gui/$USER_GUID/$PLIST_LABEL"
20
+
21
+ if launchctl print "$PLIST_ID" >/dev/null 2>&1; then
22
+ launchctl bootout "$PLIST_ID" || true
23
+ fi
24
+
25
+ if [[ -f "$PLIST_PATH" ]]; then
26
+ rm -f "$PLIST_PATH"
27
+ fi
28
+
29
+ echo "Uninstalled LaunchAgent: $PLIST_ID"
30
+ echo "Removed plist: $PLIST_PATH"
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env bash
2
+ # validate-all.sh — Run all IdleWatch validators in one pass.
3
+ # Usage: ./scripts/validate-all.sh [--skip-packaging]
4
+ #
5
+ # Exit codes:
6
+ # 0 All validators passed
7
+ # 1 One or more validators failed (summary printed at end)
8
+ set -uo pipefail
9
+
10
+ SKIP_PACKAGING=0
11
+ for arg in "$@"; do
12
+ case "$arg" in
13
+ --skip-packaging) SKIP_PACKAGING=1 ;;
14
+ esac
15
+ done
16
+
17
+ IS_MACOS=0
18
+ if [[ "$(uname -s)" == "Darwin" ]]; then
19
+ IS_MACOS=1
20
+ fi
21
+
22
+ PASS=0
23
+ FAIL=0
24
+ SKIP=0
25
+ FAILED_NAMES=()
26
+
27
+ run_validator() {
28
+ local name="$1"
29
+ shift
30
+ printf "%55s " "$name"
31
+ if "$@" >/dev/null 2>&1; then
32
+ echo "✅ pass"
33
+ PASS=$((PASS + 1))
34
+ else
35
+ echo "❌ FAIL"
36
+ FAIL=$((FAIL + 1))
37
+ FAILED_NAMES+=("$name")
38
+ fi
39
+ }
40
+
41
+ skip_validator() {
42
+ local name="$1"
43
+ local reason=${2:--skip-packaging}
44
+ printf "%55s ⏭ skip (%s)\n" "$name" "$reason"
45
+ SKIP=$((SKIP + 1))
46
+ }
47
+
48
+ can_run_trusted_prereqs() {
49
+ [[ "$IS_MACOS" -eq 1 ]] || return 1
50
+ [[ -n "${MACOS_CODESIGN_IDENTITY:-}" ]] || return 1
51
+ [[ -n "${MACOS_NOTARY_PROFILE:-}" ]] || return 1
52
+ return 0
53
+ }
54
+
55
+ has_service_account_path() {
56
+ local candidate="$1"
57
+ [[ -n "$candidate" ]] && [[ -f "$candidate" ]]
58
+ }
59
+
60
+ can_run_firebase_write_once() {
61
+ [[ -n "${FIREBASE_PROJECT_ID:-}" ]] || return 1
62
+ if has_service_account_path "${FIREBASE_SERVICE_ACCOUNT_FILE:-}"; then
63
+ return 0
64
+ fi
65
+ [[ -n "${FIREBASE_SERVICE_ACCOUNT_JSON:-}" ]] || \
66
+ [[ -n "${FIREBASE_SERVICE_ACCOUNT_B64:-}" ]] || \
67
+ [[ -n "${FIRESTORE_EMULATOR_HOST:-}" ]] || \
68
+ return 1
69
+
70
+ return 0
71
+ }
72
+
73
+ echo "=== IdleWatch full validation sweep ==="
74
+ echo ""
75
+
76
+ # --- Core ---
77
+ run_validator "validate:bin" npm run validate:bin --silent
78
+ run_validator "test:unit" npm run test:unit --silent
79
+ run_validator "smoke:help" npm run smoke:help --silent
80
+ run_validator "smoke:dry-run" npm run smoke:dry-run --silent
81
+ run_validator "smoke:once" npm run smoke:once --silent
82
+ run_validator "validate:dry-run-schema" npm run validate:dry-run-schema --silent
83
+ run_validator "validate:usage-freshness-e2e" npm run validate:usage-freshness-e2e --silent
84
+ run_validator "validate:usage-alert-rate-e2e" npm run validate:usage-alert-rate-e2e --silent
85
+ run_validator "validate:openclaw-release-gates" npm run validate:openclaw-release-gates --silent
86
+
87
+ if can_run_trusted_prereqs; then
88
+ run_validator "validate:trusted-prereqs" npm run validate:trusted-prereqs --silent
89
+ else
90
+ if [[ "$IS_MACOS" -ne 1 ]]; then
91
+ skip_validator "validate:trusted-prereqs" "non-macOS host"
92
+ elif [[ -z "${MACOS_CODESIGN_IDENTITY:-}" || -z "${MACOS_NOTARY_PROFILE:-}" ]]; then
93
+ skip_validator "validate:trusted-prereqs" "missing MACOS_CODESIGN_IDENTITY/MACOS_NOTARY_PROFILE"
94
+ else
95
+ skip_validator "validate:trusted-prereqs" "trusted tooling unavailable"
96
+ fi
97
+ fi
98
+
99
+ if can_run_firebase_write_once; then
100
+ run_validator "validate:firebase-write-required-once" npm run validate:firebase-write-required-once --silent
101
+ else
102
+ skip_validator "validate:firebase-write-required-once" "missing FIREBASE write credentials"
103
+ fi
104
+
105
+ # --- Packaging ---
106
+ if [[ "$SKIP_PACKAGING" -eq 1 ]]; then
107
+ skip_validator "package:macos"
108
+ skip_validator "validate:packaged-metadata"
109
+ skip_validator "validate:packaged-bundled-runtime"
110
+ skip_validator "validate:packaged-bundled-runtime:reuse-artifact"
111
+ skip_validator "validate:packaged-dry-run-schema:reuse-artifact"
112
+ skip_validator "validate:packaged-openclaw-robustness:reuse-artifact"
113
+ skip_validator "validate:dmg-install"
114
+ skip_validator "validate:dmg-checksum"
115
+ elif [[ "$IS_MACOS" -ne 1 ]]; then
116
+ skip_validator "package:macos" "non-macOS host"
117
+ skip_validator "validate:packaged-metadata" "non-macOS host"
118
+ skip_validator "validate:packaged-bundled-runtime" "non-macOS host"
119
+ skip_validator "validate:packaged-bundled-runtime:reuse-artifact" "non-macOS host"
120
+ skip_validator "validate:packaged-dry-run-schema:reuse-artifact" "non-macOS host"
121
+ skip_validator "validate:packaged-openclaw-robustness:reuse-artifact" "non-macOS host"
122
+ skip_validator "validate:dmg-install" "non-macOS host"
123
+ skip_validator "validate:dmg-checksum" "non-macOS host"
124
+ else
125
+ run_validator "validate:packaged-bundled-runtime" npm run validate:packaged-bundled-runtime --silent
126
+ run_validator "validate:packaged-metadata" npm run validate:packaged-metadata --silent
127
+ run_validator "validate:packaged-dry-run-schema:reuse-artifact" npm run validate:packaged-dry-run-schema:reuse-artifact --silent
128
+ run_validator "validate:packaged-openclaw-robustness:reuse-artifact" npm run validate:packaged-openclaw-robustness:reuse-artifact --silent
129
+ run_validator "validate:dmg-install" npm run validate:dmg-install --silent
130
+ run_validator "validate:dmg-checksum" npm run validate:dmg-checksum --silent
131
+ fi
132
+
133
+ echo ""
134
+ echo "=== Summary: $PASS pass, $FAIL fail, $SKIP skip ==="
135
+ if [[ ${#FAILED_NAMES[@]} -gt 0 ]]; then
136
+ echo "Failed:"
137
+ for n in "${FAILED_NAMES[@]}"; do
138
+ echo " - $n"
139
+ done
140
+ exit 1
141
+ fi
142
+ exit 0
@@ -0,0 +1,25 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ const pkgPath = new URL('../package.json', import.meta.url)
5
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
6
+
7
+ if (!pkg.bin || typeof pkg.bin !== 'object') {
8
+ console.error('package.json is missing a valid "bin" object')
9
+ process.exit(1)
10
+ }
11
+
12
+ let errors = 0
13
+ for (const [name, rel] of Object.entries(pkg.bin)) {
14
+ const abs = path.resolve(path.dirname(pkgPath.pathname), rel)
15
+ if (!fs.existsSync(abs)) {
16
+ console.error(`bin entry "${name}" points to missing file: ${rel}`)
17
+ errors++
18
+ }
19
+ }
20
+
21
+ if (errors > 0) {
22
+ process.exit(1)
23
+ }
24
+
25
+ console.log(`Validated ${Object.keys(pkg.bin).length} bin entries.`)
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ DIST_DIR="$ROOT_DIR/dist"
6
+
7
+ DMG_PATH="${1:-}"
8
+ if [[ -z "$DMG_PATH" ]]; then
9
+ DMG_PATH="$(ls -1 "$DIST_DIR"/IdleWatch-*.dmg 2>/dev/null | sort | tail -n 1 || true)"
10
+ fi
11
+
12
+ if [[ -z "$DMG_PATH" || ! -f "$DMG_PATH" ]]; then
13
+ echo "No DMG found to validate. Run package:dmg or pass path: ./scripts/validate-dmg-checksum.sh <path>" >&2
14
+ exit 1
15
+ fi
16
+
17
+ CHECKSUM_PATH="${DMG_PATH}.sha256"
18
+ if [[ ! -f "$CHECKSUM_PATH" ]]; then
19
+ echo "Missing checksum file: $CHECKSUM_PATH" >&2
20
+ exit 1
21
+ fi
22
+
23
+ if ! command -v shasum >/dev/null 2>&1; then
24
+ echo "shasum command unavailable; cannot validate checksum." >&2
25
+ exit 1
26
+ fi
27
+
28
+ EXPECTED=$(cut -d' ' -f1 < "$CHECKSUM_PATH")
29
+ ACTUAL=$(shasum -a 256 "$DMG_PATH" | cut -d' ' -f1)
30
+ if [[ "$ACTUAL" != "$EXPECTED" ]]; then
31
+ echo "Checksum mismatch for $DMG_PATH" >&2
32
+ echo "Expected: $EXPECTED" >&2
33
+ echo "Actual: $ACTUAL" >&2
34
+ exit 1
35
+ fi
36
+
37
+ echo "DMG checksum OK: $DMG_PATH"
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ set -o pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ DIST_DIR="$ROOT_DIR/dist"
7
+
8
+ INPUT_DMG="${1:-}"
9
+ if [[ -z "$INPUT_DMG" ]]; then
10
+ INPUT_DMG="$(ls -t "$DIST_DIR"/IdleWatch-*.dmg 2>/dev/null | head -n1 || true)"
11
+ fi
12
+
13
+ if [[ -z "$INPUT_DMG" || ! -f "$INPUT_DMG" ]]; then
14
+ echo "No DMG found. Build one first (npm run package:dmg) or pass a DMG path." >&2
15
+ exit 1
16
+ fi
17
+
18
+ MOUNT_POINT=""
19
+ DEV_ENTRY=""
20
+ TMP_APPS="$(mktemp -d -t idlewatch-apps.XXXXXX)"
21
+ IDLEWATCH_DRY_RUN_TIMEOUT_MS="${IDLEWATCH_DRY_RUN_TIMEOUT_MS:-90000}"
22
+ IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS="${IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS:-4000}"
23
+ DRY_RUN_TIMEOUT_RETRY_BONUS_MS="${DRY_RUN_TIMEOUT_RETRY_BONUS_MS:-10000}"
24
+ DRY_RUN_TIMEOUT_MAX_ATTEMPTS="${DRY_RUN_TIMEOUT_MAX_ATTEMPTS:-3}"
25
+ DRY_RUN_TIMEOUT_BACKOFF_MS="${DRY_RUN_TIMEOUT_BACKOFF_MS:-2000}"
26
+ IDLEWATCH_DMG_ATTACH_TIMEOUT_MS="${IDLEWATCH_DMG_ATTACH_TIMEOUT_MS:-30000}"
27
+ IDLEWATCH_DMG_DETACH_TIMEOUT_MS="${IDLEWATCH_DMG_DETACH_TIMEOUT_MS:-8000}"
28
+
29
+ TOOL_TIMEOUT="$(command -v timeout || command -v gtimeout || true)"
30
+ TOOL_TIMEOUT_OPTS=()
31
+ if [[ -n "$TOOL_TIMEOUT" ]]; then
32
+ if "$TOOL_TIMEOUT" --help 2>&1 | grep -q -- '--signal'; then
33
+ TOOL_TIMEOUT_OPTS=(--foreground --signal=SIGINT)
34
+ fi
35
+ fi
36
+
37
+ ms_to_seconds() {
38
+ awk -v ms="$1" 'BEGIN { if (ms <= 0) { print 1; exit }; printf "%d", (ms + 999) / 1000 }'
39
+ }
40
+
41
+ run_with_timeout_ms() {
42
+ local timeout_ms="$1"
43
+ shift
44
+
45
+ if [[ -n "$TOOL_TIMEOUT" ]]; then
46
+ local timeout_s
47
+ timeout_s="$(ms_to_seconds "$timeout_ms")"
48
+ "$TOOL_TIMEOUT" "${TOOL_TIMEOUT_OPTS[@]}" "${timeout_s}s" "$@"
49
+ return $?
50
+ fi
51
+
52
+ "$@"
53
+ }
54
+
55
+ cleanup() {
56
+ if [[ -n "$DEV_ENTRY" ]]; then
57
+ if [[ -n "$TOOL_TIMEOUT" ]]; then
58
+ run_with_timeout_ms "$IDLEWATCH_DMG_DETACH_TIMEOUT_MS" hdiutil detach "$DEV_ENTRY" -quiet || true
59
+ else
60
+ hdiutil detach "$DEV_ENTRY" -quiet || true
61
+ fi
62
+ fi
63
+ rm -rf "$TMP_APPS"
64
+ }
65
+ trap cleanup EXIT
66
+
67
+ ATTACH_OUTPUT="$(run_with_timeout_ms "$IDLEWATCH_DMG_ATTACH_TIMEOUT_MS" hdiutil attach "$INPUT_DMG" -nobrowse -readonly)"
68
+ DEV_ENTRY="$(echo "$ATTACH_OUTPUT" | awk '/Apple_HFS|Apple_APFS/ {print $1; exit}')"
69
+ MOUNT_POINT="$(echo "$ATTACH_OUTPUT" | awk '/\/Volumes\// {sub(/^.*\t/, ""); print; exit}')"
70
+
71
+ if [[ -z "$DEV_ENTRY" || -z "$MOUNT_POINT" ]]; then
72
+ echo "Failed to parse mounted DMG device or mount point." >&2
73
+ echo "$ATTACH_OUTPUT" >&2
74
+ exit 1
75
+ fi
76
+
77
+ APP_PATH="$MOUNT_POINT/IdleWatch.app"
78
+ if [[ ! -d "$APP_PATH" ]]; then
79
+ echo "Mounted DMG does not contain IdleWatch.app at expected path: $APP_PATH" >&2
80
+ exit 1
81
+ fi
82
+
83
+ if ! IDLEWATCH_ARTIFACT_DIR="$APP_PATH" node "$ROOT_DIR/scripts/validate-packaged-artifact.mjs"; then
84
+ echo "Mounted DMG artifact metadata/compatibility preflight failed." >&2
85
+ exit 1
86
+ fi
87
+
88
+ ditto "$APP_PATH" "$TMP_APPS/IdleWatch.app"
89
+ INSTALLED_LAUNCHER="$TMP_APPS/IdleWatch.app/Contents/MacOS/IdleWatch"
90
+
91
+ if [[ ! -x "$INSTALLED_LAUNCHER" ]]; then
92
+ echo "Installed launcher missing or not executable: $INSTALLED_LAUNCHER" >&2
93
+ exit 1
94
+ fi
95
+
96
+ run_dmg_dry_run() {
97
+ local openclaw_usage=${1:-auto}
98
+ local timeout_ms=${2}
99
+ local attempt=$3
100
+ local attempt_log="$TMP_APPS/dry-run-${openclaw_usage}-attempt-${attempt}.log"
101
+
102
+ if ! env \
103
+ IDLEWATCH_DRY_RUN_TIMEOUT_MS="$timeout_ms" \
104
+ IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS="$IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS" \
105
+ IDLEWATCH_OPENCLAW_USAGE="$openclaw_usage" \
106
+ node "$ROOT_DIR/scripts/validate-dry-run-schema.mjs" "$INSTALLED_LAUNCHER" --dry-run --once >"$attempt_log" 2>&1; then
107
+ echo "Attempt ${attempt} failed for IDLEWATCH_OPENCLAW_USAGE=${openclaw_usage}. Last output:" >&2
108
+ tail -n 60 "$attempt_log" >&2 || true
109
+ return 1
110
+ fi
111
+
112
+ cat "$attempt_log" >&2
113
+ return 0
114
+ }
115
+
116
+ run_dmg_dry_run_with_retries() {
117
+ local openclaw_usage=$1
118
+ local attempt=1
119
+ local timeout_ms="$IDLEWATCH_DRY_RUN_TIMEOUT_MS"
120
+
121
+ while (( attempt <= DRY_RUN_TIMEOUT_MAX_ATTEMPTS )); do
122
+ local sleep_ms=0
123
+ echo "Attempt ${attempt}/${DRY_RUN_TIMEOUT_MAX_ATTEMPTS}: validating DMG-installed launcher dry-run with IDLEWATCH_OPENCLAW_USAGE=${openclaw_usage} timeout=${timeout_ms}ms" >&2
124
+ if run_dmg_dry_run "$openclaw_usage" "$timeout_ms" "$attempt"; then
125
+ return 0
126
+ fi
127
+
128
+ if (( attempt < DRY_RUN_TIMEOUT_MAX_ATTEMPTS )); then
129
+ timeout_ms=$((timeout_ms + DRY_RUN_TIMEOUT_RETRY_BONUS_MS))
130
+ sleep_ms=$((DRY_RUN_TIMEOUT_BACKOFF_MS < 0 ? 0 : DRY_RUN_TIMEOUT_BACKOFF_MS))
131
+ if (( sleep_ms > 0 )); then
132
+ sleep $(awk -v t="$sleep_ms" 'BEGIN { printf "%0.3f", t / 1000 }')
133
+ fi
134
+ fi
135
+ attempt=$((attempt + 1))
136
+ done
137
+
138
+ return 1
139
+ }
140
+
141
+ set +e
142
+ if run_dmg_dry_run_with_retries auto; then
143
+ echo "dmg install validation ok ($INPUT_DMG)"
144
+ exit 0
145
+ fi
146
+
147
+ if run_dmg_dry_run_with_retries off; then
148
+ echo "dmg install validation ok ($INPUT_DMG)" >&2
149
+ echo "OpenClaw-enabled dry-run did not emit telemetry within timeout/backoff window; launchability path remains healthy." >&2
150
+ exit 0
151
+ fi
152
+
153
+ set -e
154
+ echo "dmg install validation failed for both Openclaw-on and OpenClaw-off modes" >&2
155
+ exit 1