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.
- package/.env.example +73 -0
- package/.github/workflows/ci.yml +99 -0
- package/.github/workflows/release-macos-trusted.yml +103 -0
- package/README.md +336 -0
- package/bin/idlewatch-agent.js +1053 -0
- package/docs/onboarding-external.md +58 -0
- package/docs/packaging/macos-dmg.md +199 -0
- package/docs/packaging/macos-launch-agent.md +70 -0
- package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
- package/docs/qa/mac-qa-log.md +2864 -0
- package/docs/telemetry/idle-stale-policy.md +57 -0
- package/docs/telemetry/openclaw-mapping.md +80 -0
- package/package.json +76 -0
- package/scripts/build-dmg.sh +65 -0
- package/scripts/install-macos-launch-agent.sh +78 -0
- package/scripts/lib/telemetry-row-parser.mjs +100 -0
- package/scripts/package-macos.sh +228 -0
- package/scripts/uninstall-macos-launch-agent.sh +30 -0
- package/scripts/validate-all.sh +142 -0
- package/scripts/validate-bin.mjs +25 -0
- package/scripts/validate-dmg-checksum.sh +37 -0
- package/scripts/validate-dmg-install.sh +155 -0
- package/scripts/validate-dry-run-schema.mjs +257 -0
- package/scripts/validate-onboarding.mjs +63 -0
- package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
- package/scripts/validate-openclaw-release-gates.mjs +51 -0
- package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
- package/scripts/validate-openclaw-usage-health.mjs +95 -0
- package/scripts/validate-packaged-artifact.mjs +233 -0
- package/scripts/validate-packaged-bundled-runtime.sh +191 -0
- package/scripts/validate-packaged-metadata.sh +43 -0
- package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
- package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
- package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
- package/scripts/validate-packaged-sourcemaps.mjs +82 -0
- package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
- package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
- package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
- package/scripts/validate-trusted-prereqs.sh +44 -0
- package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
- package/scripts/validate-usage-freshness-e2e.mjs +81 -0
- package/skill/SKILL.md +43 -0
- package/src/config.js +100 -0
- package/src/enrollment.js +176 -0
- package/src/gpu.js +115 -0
- package/src/memory.js +67 -0
- package/src/openclaw-cache.js +51 -0
- package/src/openclaw-usage.js +1020 -0
- package/src/telemetry-mapping.js +54 -0
- package/src/usage-alert.js +41 -0
- package/src/usage-freshness.js +31 -0
- package/test/config.test.mjs +112 -0
- package/test/fixtures/gpu-agx.txt +2 -0
- package/test/fixtures/gpu-iogpu.txt +2 -0
- package/test/fixtures/gpu-top-grep.txt +2 -0
- package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
- package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
- package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
- package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
- package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
- package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
- package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
- package/test/fixtures/openclaw-stats.json +17 -0
- package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
- package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
- package/test/fixtures/openclaw-status-control-noise.txt +1 -0
- package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
- package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
- package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
- package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
- package/test/fixtures/openclaw-status-multi-json.txt +3 -0
- package/test/fixtures/openclaw-status-nested-recent.json +19 -0
- package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
- package/test/fixtures/openclaw-status-noisy.txt +3 -0
- package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
- package/test/fixtures/openclaw-status-result-session.json +15 -0
- package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
- package/test/fixtures/openclaw-status-session-map.json +28 -0
- package/test/fixtures/openclaw-status-session-model-name.json +18 -0
- package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
- package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
- package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
- package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
- package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-strings.json +38 -0
- package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
- package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
- package/test/fixtures/openclaw-status.json +41 -0
- package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
- package/test/gpu.test.mjs +58 -0
- package/test/memory.test.mjs +35 -0
- package/test/openclaw-cache.test.mjs +48 -0
- package/test/openclaw-env.test.mjs +365 -0
- package/test/openclaw-usage.test.mjs +555 -0
- package/test/telemetry-mapping.test.mjs +69 -0
- package/test/telemetry-row-parser.test.mjs +44 -0
- package/test/usage-alert.test.mjs +73 -0
- package/test/usage-freshness.test.mjs +63 -0
- package/test/validate-dry-run-schema.test.mjs +146 -0
- package/tui/Cargo.lock +801 -0
- package/tui/Cargo.toml +11 -0
- package/tui/src/main.rs +368 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
DIST_LAUNCHER="$ROOT_DIR/dist/IdleWatch.app/Contents/MacOS/IdleWatch"
|
|
6
|
+
TMP_APPS="$(mktemp -d -t idlewatch-packaged-runtime.XXXXXX)"
|
|
7
|
+
|
|
8
|
+
cleanup_tmp() {
|
|
9
|
+
rm -rf "$TMP_APPS"
|
|
10
|
+
}
|
|
11
|
+
trap cleanup_tmp EXIT
|
|
12
|
+
|
|
13
|
+
NODE_BIN="$(command -v node || true)"
|
|
14
|
+
if [[ -z "$NODE_BIN" || ! -x "$NODE_BIN" ]]; then
|
|
15
|
+
echo "Node.js is required to run bundled-runtime validation." >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
read_metadata_field() {
|
|
20
|
+
local field=$1
|
|
21
|
+
"$NODE_BIN" - "$METADATA_PATH" "$field" <<'NODE' | tr -d '\r'
|
|
22
|
+
const fs = require('fs')
|
|
23
|
+
const metadataPath = process.argv[2]
|
|
24
|
+
const field = process.argv[3]
|
|
25
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'))
|
|
26
|
+
const value = metadata[field]
|
|
27
|
+
if (typeof value === 'undefined' || value === null) process.exit(0)
|
|
28
|
+
if (typeof value === 'boolean') process.stdout.write(value ? '1' : '0')
|
|
29
|
+
else process.stdout.write(String(value))
|
|
30
|
+
NODE
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
RUNTIME_DIR="$($NODE_BIN -e 'const path = require("path"); console.log(path.resolve(process.argv[1], "..", ".."))' "$NODE_BIN")"
|
|
34
|
+
METADATA_PATH="$ROOT_DIR/dist/IdleWatch.app/Contents/Resources/packaging-metadata.json"
|
|
35
|
+
BUNDLED_RUNTIME_REQUIRED_RAW="${IDLEWATCH_BUNDLED_RUNTIME_REQUIRED+x}"
|
|
36
|
+
BUNDLED_RUNTIME_REQUIRED="${IDLEWATCH_BUNDLED_RUNTIME_REQUIRED:-0}"
|
|
37
|
+
RESTRICTED_PATH="/usr/bin:/bin"
|
|
38
|
+
ORIGINAL_PATH="${PATH:-/usr/bin:/bin}"
|
|
39
|
+
|
|
40
|
+
if [[ "${IDLEWATCH_SKIP_PACKAGE_MACOS:-0}" != "1" ]]; then
|
|
41
|
+
if [[ ! -x "$RUNTIME_DIR/bin/node" ]]; then
|
|
42
|
+
echo "Resolved runtime dir is invalid (missing executable bin/node): $RUNTIME_DIR" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
echo "Packaging IdleWatch.app with bundled runtime: $RUNTIME_DIR"
|
|
46
|
+
IDLEWATCH_NODE_RUNTIME_DIR="$RUNTIME_DIR" npm run package:macos --silent
|
|
47
|
+
else
|
|
48
|
+
if [[ ! -f "$METADATA_PATH" ]]; then
|
|
49
|
+
echo "IDLEWATCH_SKIP_PACKAGE_MACOS=1 but metadata is missing: $METADATA_PATH" >&2
|
|
50
|
+
echo "Run with IDLEWATCH_SKIP_PACKAGE_MACOS unset or prebuild via npm run package:macos first." >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [[ ! -f "$DIST_LAUNCHER" ]]; then
|
|
55
|
+
echo "IDLEWATCH_SKIP_PACKAGE_MACOS=1 but packaged launcher is missing: $DIST_LAUNCHER" >&2
|
|
56
|
+
echo "Run with IDLEWATCH_SKIP_PACKAGE_MACOS unset or prebuild via npm run package:macos first." >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [[ "$BUNDLED_RUNTIME_REQUIRED" == "1" ]]; then
|
|
61
|
+
if [[ "$(read_metadata_field nodeRuntimeBundled)" != "1" ]]; then
|
|
62
|
+
echo "Reused packaged artifact is not bundled-runtime aware. Rebuild first:" >&2
|
|
63
|
+
echo " npm run package:macos" >&2
|
|
64
|
+
echo "(required for node-free PATH validation in bundled-runtime check)" >&2
|
|
65
|
+
echo "To force launchability-only non-bundled validation, set IDLEWATCH_BUNDLED_RUNTIME_REQUIRED=0." >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
elif [[ "$BUNDLED_RUNTIME_REQUIRED_RAW" != "1" ]]; then
|
|
69
|
+
if [[ "$(read_metadata_field nodeRuntimeBundled)" != "1" ]]; then
|
|
70
|
+
echo "Non-bundled artifact detected in reuse mode; running launchability fallback (recommended for non-deterministic reuse checks)." >&2
|
|
71
|
+
echo "This keeps non-bundled artifacts supportable while still validating launcher startup semantics." >&2
|
|
72
|
+
echo "Set IDLEWATCH_BUNDLED_RUNTIME_REQUIRED=1 to enforce strict node-free validation." >&2
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if ! IDLEWATCH_ARTIFACT_DIR="$ROOT_DIR/dist/IdleWatch.app" node "$ROOT_DIR/scripts/validate-packaged-artifact.mjs"; then
|
|
77
|
+
echo "Reusable packaged artifact preflight failed for validate-packaged-bundled-runtime." >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
echo "Using existing packaged app at: $DIST_LAUNCHER"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if [[ ! -x "$DIST_LAUNCHER" ]]; then
|
|
85
|
+
echo "Packaged launcher missing: $DIST_LAUNCHER" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
npm run validate:packaged-metadata --silent
|
|
90
|
+
|
|
91
|
+
NODE_RUNTIME_BUNDLED="$(read_metadata_field nodeRuntimeBundled)"
|
|
92
|
+
|
|
93
|
+
if [[ "$NODE_RUNTIME_BUNDLED" == "1" ]]; then
|
|
94
|
+
VALIDATION_PATH="$RESTRICTED_PATH"
|
|
95
|
+
echo "Running strict path-scrubbed launchability validation using PATH=$RESTRICTED_PATH"
|
|
96
|
+
elif [[ "${IDLEWATCH_USE_ORIGINAL_PATH_FOR_NON_BUNDLED:-1}" == "1" ]]; then
|
|
97
|
+
if node_path="$(PATH="$RESTRICTED_PATH" /usr/bin/env which node 2>/dev/null || true)" &&
|
|
98
|
+
[[ -n "$node_path" ]] &&
|
|
99
|
+
[[ "$node_path" == /* ]] &&
|
|
100
|
+
[[ -x "$node_path" ]]; then
|
|
101
|
+
VALIDATION_PATH="$RESTRICTED_PATH"
|
|
102
|
+
echo "Node is available in restricted PATH; still validating with PATH=$RESTRICTED_PATH to keep behavior deterministic."
|
|
103
|
+
else
|
|
104
|
+
VALIDATION_PATH="$ORIGINAL_PATH"
|
|
105
|
+
echo "Non-bundled artifact detected; node is not available in PATH=$RESTRICTED_PATH." >&2
|
|
106
|
+
echo "Falling back to current PATH for launchability verification in non-bundled mode." >&2
|
|
107
|
+
echo "Set IDLEWATCH_BUNDLED_RUNTIME_REQUIRED=1 and provide IDLEWATCH_NODE_RUNTIME_DIR for strict node-free validation." >&2
|
|
108
|
+
fi
|
|
109
|
+
else
|
|
110
|
+
VALIDATION_PATH="$RESTRICTED_PATH"
|
|
111
|
+
echo "Non-bundled artifact detected but strict path mode requested; this run requires bundled runtime validation." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
IDLEWATCH_DRY_RUN_TIMEOUT_MS="${IDLEWATCH_DRY_RUN_TIMEOUT_MS:-90000}"
|
|
116
|
+
IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS="${IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS:-4000}"
|
|
117
|
+
DRY_RUN_TIMEOUT_RETRY_BONUS_MS="${IDLEWATCH_DRY_RUN_TIMEOUT_RETRY_BONUS_MS:-10000}"
|
|
118
|
+
DRY_RUN_TIMEOUT_MAX_ATTEMPTS="${IDLEWATCH_DRY_RUN_TIMEOUT_MAX_ATTEMPTS:-3}"
|
|
119
|
+
DRY_RUN_TIMEOUT_BACKOFF_MS="${IDLEWATCH_DRY_RUN_TIMEOUT_BACKOFF_MS:-2000}"
|
|
120
|
+
|
|
121
|
+
run_packaged_dry_run() {
|
|
122
|
+
local openclaw_usage=${1:-auto}
|
|
123
|
+
local timeout_ms=${2}
|
|
124
|
+
local attempt=${3}
|
|
125
|
+
local validation_path=${4}
|
|
126
|
+
local attempt_log="$TMP_APPS/packaged-dry-run-${openclaw_usage}-attempt-${attempt}.log"
|
|
127
|
+
|
|
128
|
+
if ! env \
|
|
129
|
+
IDLEWATCH_DRY_RUN_TIMEOUT_MS="$timeout_ms" \
|
|
130
|
+
IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS="$IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS" \
|
|
131
|
+
IDLEWATCH_OPENCLAW_USAGE="$openclaw_usage" \
|
|
132
|
+
HOME="$HOME" \
|
|
133
|
+
PATH="$validation_path" \
|
|
134
|
+
"$NODE_BIN" "$ROOT_DIR/scripts/validate-dry-run-schema.mjs" \
|
|
135
|
+
"$DIST_LAUNCHER" --dry-run --once >"$attempt_log" 2>&1; then
|
|
136
|
+
echo "Attempt ${attempt} failed for IDLEWATCH_OPENCLAW_USAGE=${openclaw_usage} with PATH=$validation_path. Last output:" >&2
|
|
137
|
+
tail -n 60 "$attempt_log" >&2 || true
|
|
138
|
+
return 1
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
cat "$attempt_log" >&2
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
run_packaged_dry_run_with_retries() {
|
|
146
|
+
local openclaw_usage=$1
|
|
147
|
+
local attempt=1
|
|
148
|
+
local timeout_ms="$IDLEWATCH_DRY_RUN_TIMEOUT_MS"
|
|
149
|
+
|
|
150
|
+
while (( attempt <= DRY_RUN_TIMEOUT_MAX_ATTEMPTS )); do
|
|
151
|
+
local sleep_ms=0
|
|
152
|
+
echo "Attempt ${attempt}/${DRY_RUN_TIMEOUT_MAX_ATTEMPTS}: validating packaged launcher dry-run with IDLEWATCH_OPENCLAW_USAGE=${openclaw_usage} timeout=${timeout_ms}ms" >&2
|
|
153
|
+
if run_packaged_dry_run "$openclaw_usage" "$timeout_ms" "$attempt" "$VALIDATION_PATH"; then
|
|
154
|
+
return 0
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
if (( attempt < DRY_RUN_TIMEOUT_MAX_ATTEMPTS )); then
|
|
158
|
+
timeout_ms=$((timeout_ms + DRY_RUN_TIMEOUT_RETRY_BONUS_MS))
|
|
159
|
+
sleep_ms=$((DRY_RUN_TIMEOUT_BACKOFF_MS < 0 ? 0 : DRY_RUN_TIMEOUT_BACKOFF_MS))
|
|
160
|
+
if (( sleep_ms > 0 )); then
|
|
161
|
+
sleep $(awk -v t="$sleep_ms" 'BEGIN { printf "%.3f", t / 1000 }')
|
|
162
|
+
fi
|
|
163
|
+
fi
|
|
164
|
+
attempt=$((attempt + 1))
|
|
165
|
+
done
|
|
166
|
+
|
|
167
|
+
return 1
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
set +e
|
|
171
|
+
run_packaged_dry_run_with_retries auto
|
|
172
|
+
rc=$?
|
|
173
|
+
set -e
|
|
174
|
+
|
|
175
|
+
if [[ $rc -ne 0 ]]; then
|
|
176
|
+
if run_packaged_dry_run_with_retries off; then
|
|
177
|
+
echo "bundled runtime validation ok (launcher path-only check ${VALIDATION_PATH:+under PATH=$VALIDATION_PATH})" >&2
|
|
178
|
+
echo "OpenClaw-enabled dry-run did not emit telemetry within timeout/backoff window; launchability path remains healthy." >&2
|
|
179
|
+
exit 0
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
echo "bundled runtime validation failed for both OpenClaw-enabled and OpenClaw-disabled dry-runs" >&2
|
|
183
|
+
exit 1
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
echo "bundled runtime validation ok"
|
|
187
|
+
if [[ "$NODE_RUNTIME_BUNDLED" == "1" ]]; then
|
|
188
|
+
echo "validated launcher dry-run under restricted PATH in ${IDLEWATCH_DRY_RUN_TIMEOUT_MS}ms baseline (+${DRY_RUN_TIMEOUT_RETRY_BONUS_MS}ms retry increments, up to ${DRY_RUN_TIMEOUT_MAX_ATTEMPTS} attempts)"
|
|
189
|
+
else
|
|
190
|
+
echo "validated launcher dry-run for non-bundled artifact under PATH=${VALIDATION_PATH} in ${IDLEWATCH_DRY_RUN_TIMEOUT_MS}ms baseline (+${DRY_RUN_TIMEOUT_RETRY_BONUS_MS}ms retry increments, up to ${DRY_RUN_TIMEOUT_MAX_ATTEMPTS} attempts)"
|
|
191
|
+
fi
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
METADATA_PATH="$ROOT_DIR/dist/IdleWatch.app/Contents/Resources/packaging-metadata.json"
|
|
6
|
+
PAYLOAD_BIN="$ROOT_DIR/dist/IdleWatch.app/Contents/Resources/payload/package/bin/idlewatch-agent.js"
|
|
7
|
+
|
|
8
|
+
if [[ ! -f "$METADATA_PATH" ]]; then
|
|
9
|
+
echo "Missing packaging metadata: $METADATA_PATH" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
if [[ ! -f "$PAYLOAD_BIN" ]]; then
|
|
14
|
+
echo "Missing packaged payload entrypoint: $PAYLOAD_BIN" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
node - "$METADATA_PATH" <<'NODE'
|
|
19
|
+
const fs = require('fs')
|
|
20
|
+
const metadataPath = process.argv[2]
|
|
21
|
+
const data = JSON.parse(fs.readFileSync(metadataPath, 'utf8'))
|
|
22
|
+
if (!data.version) throw new Error('packaging metadata missing version')
|
|
23
|
+
if (!data.platform) throw new Error('packaging metadata missing platform')
|
|
24
|
+
if (data.platform !== 'darwin') throw new Error('packaging metadata platform is not darwin')
|
|
25
|
+
if (!data.bundleName) throw new Error('packaging metadata missing bundleName')
|
|
26
|
+
if (!data.payloadTarball) throw new Error('packaging metadata missing payloadTarball')
|
|
27
|
+
if (typeof data.nodeRuntimeBundled !== 'boolean') throw new Error('packaging metadata missing nodeRuntimeBundled')
|
|
28
|
+
if (typeof data.sourceGitCommit !== 'undefined' && typeof data.sourceGitCommit !== 'string') {
|
|
29
|
+
throw new Error('packaging metadata sourceGitCommit must be a string when present')
|
|
30
|
+
}
|
|
31
|
+
if (typeof data.sourceGitDirty !== 'undefined' && typeof data.sourceGitDirty !== 'boolean') {
|
|
32
|
+
throw new Error('packaging metadata sourceGitDirty must be a boolean when present')
|
|
33
|
+
}
|
|
34
|
+
if (typeof data.sourceGitDirtyKnown !== 'undefined' && typeof data.sourceGitDirtyKnown !== 'boolean') {
|
|
35
|
+
throw new Error('packaging metadata sourceGitDirtyKnown must be a boolean when present')
|
|
36
|
+
}
|
|
37
|
+
console.log(`packaging metadata ok for ${data.bundleName} ${data.version}`)
|
|
38
|
+
if (data.sourceGitCommit) {
|
|
39
|
+
console.log(`source commit: ${data.sourceGitCommit}${data.sourceGitDirty ? ' (dirty)' : ''}`)
|
|
40
|
+
}
|
|
41
|
+
NODE
|
|
42
|
+
|
|
43
|
+
echo "packaging metadata validation ok"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, writeFileSync, chmodSync, rmSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { readTelemetryJsonRow } from './lib/telemetry-row-parser.mjs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
8
|
+
|
|
9
|
+
const repoRoot = process.cwd()
|
|
10
|
+
const rootDir = process.cwd()
|
|
11
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'idlewatch-openclaw-cache-recover-pkg-'))
|
|
12
|
+
const mockBinPath = join(tempDir, 'openclaw-mock.sh')
|
|
13
|
+
const callLog = join(tempDir, 'calls.txt')
|
|
14
|
+
const cachePath = join(tempDir, 'openclaw-last-good.json')
|
|
15
|
+
|
|
16
|
+
function writeMockOpenClaw(pathToScript) {
|
|
17
|
+
const script = `#!/usr/bin/env bash
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
calls_file="${callLog}"
|
|
21
|
+
if [[ ! -f "$calls_file" ]]; then
|
|
22
|
+
echo 0 > "$calls_file"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
n=$(cat "$calls_file")
|
|
26
|
+
next=$((n + 1))
|
|
27
|
+
echo "$next" > "$calls_file"
|
|
28
|
+
|
|
29
|
+
if [[ "$n" -lt 7 ]]; then
|
|
30
|
+
echo "temporary probe failure \$n" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
now_ms=$(($(date +%s) * 1000))
|
|
35
|
+
updated_at=$((now_ms - 1500))
|
|
36
|
+
cat <<JSON
|
|
37
|
+
{
|
|
38
|
+
"sessions": {
|
|
39
|
+
"recent": [
|
|
40
|
+
{
|
|
41
|
+
"sessionId": "cached-recover-packaged",
|
|
42
|
+
"agentId": "agent-cache-pkg",
|
|
43
|
+
"model": "gpt-5.3-codex",
|
|
44
|
+
"totalTokens": 9090,
|
|
45
|
+
"updatedAt": $updated_at,
|
|
46
|
+
"updated_at": $updated_at
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"ts": $now_ms
|
|
51
|
+
}
|
|
52
|
+
JSON
|
|
53
|
+
`
|
|
54
|
+
writeFileSync(pathToScript, script, 'utf8')
|
|
55
|
+
chmodSync(pathToScript, 0o755)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readRow(output) {
|
|
59
|
+
return readTelemetryJsonRow(output)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function collectRow(env) {
|
|
63
|
+
const launcher = join(rootDir, 'dist', 'IdleWatch.app', 'Contents', 'MacOS', 'IdleWatch')
|
|
64
|
+
const out = spawnSync(launcher, ['--dry-run'], {
|
|
65
|
+
cwd: repoRoot,
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
timeout: 25000,
|
|
69
|
+
env,
|
|
70
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
71
|
+
killSignal: 'SIGINT'
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const output = String(out.stdout || '') + String(out.stderr || '')
|
|
75
|
+
assert.equal(out.status, 0, `packaged dry-run exited with ${out.status}`)
|
|
76
|
+
assert.ok(output.trim(), 'packaged dry-run produced no output')
|
|
77
|
+
return readRow(output)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function run() {
|
|
81
|
+
const nowMs = Date.now()
|
|
82
|
+
const staleAgeMs = 90000
|
|
83
|
+
|
|
84
|
+
if (process.env.IDLEWATCH_SKIP_PACKAGE_MACOS !== '1') {
|
|
85
|
+
execFileSync('npm', ['run', 'package:macos', '--silent'], {
|
|
86
|
+
cwd: repoRoot,
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
writeMockOpenClaw(mockBinPath)
|
|
93
|
+
|
|
94
|
+
const staleSnapshot = {
|
|
95
|
+
at: nowMs,
|
|
96
|
+
usage: {
|
|
97
|
+
model: 'gpt-5.3-codex',
|
|
98
|
+
totalTokens: 8080,
|
|
99
|
+
tokensPerMin: 18.2,
|
|
100
|
+
sessionId: 'cached-recover-packaged',
|
|
101
|
+
agentId: 'agent-cache-pkg',
|
|
102
|
+
usageTimestampMs: nowMs - staleAgeMs,
|
|
103
|
+
sourceCommand: 'cached-recovery-stale'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
writeFileSync(cachePath, JSON.stringify(staleSnapshot), 'utf8')
|
|
107
|
+
writeFileSync(callLog, '0', 'utf8')
|
|
108
|
+
|
|
109
|
+
const env = {
|
|
110
|
+
...process.env,
|
|
111
|
+
HOME: process.env.HOME || '',
|
|
112
|
+
IDLEWATCH_OPENCLAW_BIN: mockBinPath,
|
|
113
|
+
IDLEWATCH_OPENCLAW_BIN_STRICT: '1',
|
|
114
|
+
IDLEWATCH_OPENCLAW_PROBE_RETRIES: '0',
|
|
115
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH: cachePath,
|
|
116
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS: '120000',
|
|
117
|
+
IDLEWATCH_USAGE_STALE_MS: '30000',
|
|
118
|
+
IDLEWATCH_USAGE_NEAR_STALE_MS: '15000',
|
|
119
|
+
IDLEWATCH_USAGE_STALE_GRACE_MS: '3000',
|
|
120
|
+
IDLEWATCH_USAGE_REFRESH_REPROBES: '4',
|
|
121
|
+
IDLEWATCH_USAGE_REFRESH_DELAY_MS: '20',
|
|
122
|
+
IDLEWATCH_USAGE_REFRESH_ON_NEAR_STALE: '1',
|
|
123
|
+
IDLEWATCH_INTERVAL_MS: '1000',
|
|
124
|
+
IDLEWATCH_OPENCLAW_USAGE: 'auto',
|
|
125
|
+
IDLEWATCH_REQUIRE_OPENCLAW_USAGE: '1'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const row = collectRow(env)
|
|
129
|
+
|
|
130
|
+
assert.equal(row?.source?.usage, 'openclaw', 'usage source should remain openclaw during packaged cache recovery')
|
|
131
|
+
assert.equal(row?.source?.usageProbeAttempts >= 1, true, 'probe attempts should run for refresh path')
|
|
132
|
+
assert.equal(row?.source?.usageRefreshAttempted, true, 'packaged stale recovery should attempt refresh')
|
|
133
|
+
assert.equal(row?.source?.usageRefreshAttempts >= 1, true, 'at least one refresh reprobe attempt should run after stale cache')
|
|
134
|
+
assert.equal(row?.source?.usageRefreshRecovered, true, 'stale packaged cache should recover after successful reprobe')
|
|
135
|
+
assert.equal(row?.source?.usageProbeResult, 'ok', 'final usage probe result should be ok after recovery')
|
|
136
|
+
assert.equal(row?.source?.usageFreshnessState, 'fresh', 'usage should be fresh after successful recovery')
|
|
137
|
+
assert.equal(row?.source?.usageAlertLevel, 'ok', 'usage alert should settle to ok after recovery')
|
|
138
|
+
assert.equal(row?.openclawSessionId, 'cached-recover-packaged')
|
|
139
|
+
assert.equal(row?.openclawAgentId, 'agent-cache-pkg')
|
|
140
|
+
assert.equal(row?.openclawTotalTokens, 9090)
|
|
141
|
+
assert.equal(row?.source?.usageCommand?.includes('--json') || row?.source?.usageCommand?.includes('status'), true, 'command path should be OpenClaw command text')
|
|
142
|
+
|
|
143
|
+
const calls = Number(readFileSync(callLog, 'utf8').trim())
|
|
144
|
+
assert.ok(Number.isFinite(calls) && calls >= 3, `expected at least 3 probe calls, got ${calls}`)
|
|
145
|
+
|
|
146
|
+
console.log('validate-packaged-openclaw-cache-recovery-e2e: ok (packaged stale cache recovers through reprobe attempts)')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
run()
|
|
151
|
+
} finally {
|
|
152
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
153
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, accessSync, constants } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
const repoRoot = process.cwd()
|
|
7
|
+
const appPath = join(repoRoot, 'dist', 'IdleWatch.app', 'Contents', 'MacOS', 'IdleWatch')
|
|
8
|
+
|
|
9
|
+
function ensureReleaseArtifact() {
|
|
10
|
+
if (!existsSync(appPath)) {
|
|
11
|
+
console.error(`Missing packaged app launcher at ${appPath}`)
|
|
12
|
+
console.error('Run npm run package:macos --silent before running trusted release gates.')
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
accessSync(appPath, constants.X_OK)
|
|
18
|
+
} catch {
|
|
19
|
+
console.error(`Packaged launcher is not executable: ${appPath}`)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shouldRequireOpenClaw(rawValue) {
|
|
25
|
+
const raw = String(rawValue ?? '1').trim().toLowerCase()
|
|
26
|
+
if (raw === '0' || raw === 'false' || raw === 'off' || raw === 'no') return false
|
|
27
|
+
if (raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes') return true
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const RELEASE_GATE_TIMEOUT_MS = process.env.IDLEWATCH_DRY_RUN_TIMEOUT_MS || '60000'
|
|
32
|
+
|
|
33
|
+
function runValidator(name, extraEnv = {}) {
|
|
34
|
+
const requireOpenClawUsage = shouldRequireOpenClaw(process.env.IDLEWATCH_REQUIRE_OPENCLAW_USAGE)
|
|
35
|
+
const requireOpenClaw = requireOpenClawUsage ? '1' : '0'
|
|
36
|
+
|
|
37
|
+
const result = spawnSync('npm', ['run', name, '--silent'], {
|
|
38
|
+
cwd: repoRoot,
|
|
39
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
IDLEWATCH_SKIP_PACKAGE_MACOS: '1',
|
|
44
|
+
IDLEWATCH_REQUIRE_OPENCLAW_USAGE: requireOpenClaw,
|
|
45
|
+
IDLEWATCH_DRY_RUN_TIMEOUT_MS: RELEASE_GATE_TIMEOUT_MS,
|
|
46
|
+
...extraEnv
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const out = String(result.stdout || '') + String(result.stderr || '')
|
|
51
|
+
if (result.status !== 0) {
|
|
52
|
+
if (out.trim()) {
|
|
53
|
+
console.error(out.trim())
|
|
54
|
+
}
|
|
55
|
+
console.error(`Validator ${name} failed with exit code ${result.status}`)
|
|
56
|
+
process.exit(result.status)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.stdout.write(out)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
ensureReleaseArtifact()
|
|
64
|
+
|
|
65
|
+
runValidator('validate:packaged-usage-health')
|
|
66
|
+
runValidator('validate:packaged-openclaw-stats-ingestion')
|
|
67
|
+
runValidator('validate:packaged-openclaw-cache-recovery-e2e')
|
|
68
|
+
|
|
69
|
+
console.log('validate-packaged-openclaw-release-gates: ok (release artifact validates OpenClaw health, stats fallback, and cache recovery)')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main()
|