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,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
|