@web-auto/camo 0.1.26 → 0.2.1
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/LICENSE +21 -21
- package/README.md +586 -586
- package/bin/browser-service.mjs +11 -11
- package/bin/camo.mjs +22 -22
- package/package.json +48 -48
- package/scripts/build.mjs +19 -19
- package/scripts/bump-version.mjs +34 -34
- package/scripts/check-file-size.mjs +80 -80
- package/scripts/file-size-policy.json +12 -2
- package/scripts/install.mjs +76 -76
- package/scripts/release.sh +54 -54
- package/src/autoscript/action-providers/index.mjs +6 -6
- package/src/autoscript/impact-engine.mjs +78 -78
- package/src/autoscript/runtime.mjs +1017 -1017
- package/src/autoscript/schema.mjs +376 -376
- package/src/cli.mjs +405 -405
- package/src/commands/attach.mjs +141 -141
- package/src/commands/autoscript.mjs +1011 -1011
- package/src/commands/browser.mjs +1255 -1257
- package/src/commands/container.mjs +401 -401
- package/src/commands/cookies.mjs +69 -69
- package/src/commands/create.mjs +98 -98
- package/src/commands/devtools.mjs +349 -349
- package/src/commands/events.mjs +152 -152
- package/src/commands/highlight-mode.mjs +24 -24
- package/src/commands/init.mjs +68 -68
- package/src/commands/lifecycle.mjs +275 -275
- package/src/commands/mouse.mjs +45 -45
- package/src/commands/profile.mjs +46 -46
- package/src/commands/record.mjs +115 -115
- package/src/commands/system.mjs +14 -14
- package/src/commands/window.mjs +123 -123
- package/src/container/change-notifier.mjs +362 -362
- package/src/container/element-filter.mjs +143 -143
- package/src/container/index.mjs +3 -3
- package/src/container/runtime-core/checkpoint.mjs +209 -209
- package/src/container/runtime-core/index.mjs +21 -21
- package/src/container/runtime-core/operations/index.mjs +774 -774
- package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
- package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
- package/src/container/runtime-core/operations/viewport.mjs +189 -189
- package/src/container/runtime-core/search.mjs +190 -190
- package/src/container/runtime-core/subscription.mjs +224 -224
- package/src/container/runtime-core/utils.mjs +94 -94
- package/src/container/runtime-core/validation.mjs +127 -184
- package/src/container/runtime-core.mjs +1 -1
- package/src/container/subscription-registry.mjs +459 -459
- package/src/core/actions.mjs +561 -561
- package/src/core/browser.mjs +266 -266
- package/src/core/index.mjs +52 -52
- package/src/core/utils.mjs +91 -91
- package/src/events/daemon-entry.mjs +33 -33
- package/src/events/daemon.mjs +80 -80
- package/src/events/progress-log.mjs +109 -109
- package/src/events/ws-server.mjs +239 -239
- package/src/lib/client.mjs +200 -200
- package/src/lifecycle/cleanup.mjs +83 -83
- package/src/lifecycle/lock.mjs +126 -126
- package/src/lifecycle/session-registry.mjs +279 -279
- package/src/lifecycle/session-view.mjs +76 -76
- package/src/lifecycle/session-watchdog.mjs +281 -281
- package/src/services/browser-service/index.js +671 -674
- package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
- package/src/services/browser-service/internal/BrowserSession.js +325 -336
- package/src/services/browser-service/internal/ElementRegistry.js +60 -60
- package/src/services/browser-service/internal/ProfileLock.js +84 -84
- package/src/services/browser-service/internal/SessionManager.js +184 -184
- package/src/services/browser-service/internal/SessionManager.test.js +39 -39
- package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
- package/src/services/browser-service/internal/browser-session/input-ops.js +222 -219
- package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
- package/src/services/browser-service/internal/browser-session/logging.js +46 -46
- package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
- package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
- package/src/services/browser-service/internal/browser-session/page-management.js +302 -336
- package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
- package/src/services/browser-service/internal/browser-session/recording.js +198 -198
- package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
- package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
- package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
- package/src/services/browser-service/internal/browser-session/types.js +14 -14
- package/src/services/browser-service/internal/browser-session/utils.js +95 -95
- package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
- package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
- package/src/services/browser-service/internal/container-matcher.js +851 -851
- package/src/services/browser-service/internal/container-registry.js +182 -182
- package/src/services/browser-service/internal/engine-manager.js +259 -259
- package/src/services/browser-service/internal/fingerprint.js +203 -203
- package/src/services/browser-service/internal/heartbeat.js +137 -137
- package/src/services/browser-service/internal/logging.js +46 -46
- package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
- package/src/services/browser-service/internal/pageRuntime.js +28 -28
- package/src/services/browser-service/internal/runtimeInjector.js +31 -31
- package/src/services/browser-service/internal/service-process-logger.js +140 -140
- package/src/services/browser-service/internal/state-bus.js +45 -45
- package/src/services/browser-service/internal/storage-paths.js +42 -42
- package/src/services/browser-service/internal/ws-server.js +1194 -1194
- package/src/services/browser-service/internal/ws-server.test.js +58 -58
- package/src/services/browser-service/server.mjs +6 -6
- package/src/services/controller/cli-bridge.js +93 -93
- package/src/services/controller/container-index.js +50 -50
- package/src/services/controller/container-storage.js +36 -36
- package/src/services/controller/controller-actions.js +207 -207
- package/src/services/controller/controller.js +1138 -1138
- package/src/services/controller/selectors.js +54 -54
- package/src/services/controller/transport.js +125 -125
- package/src/utils/args.mjs +26 -26
- package/src/utils/browser-service.mjs +544 -544
- package/src/utils/command-log.mjs +64 -64
- package/src/utils/config.mjs +214 -214
- package/src/utils/fingerprint.mjs +181 -181
- package/src/utils/help.mjs +216 -216
- package/src/utils/js-policy.mjs +13 -13
- package/src/utils/ws-client.mjs +30 -30
- package/src/container/runtime-core/operations/tab-pool.mjs.bak +0 -762
- package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +0 -762
- package/src/services/browser-service/index.js.bak +0 -671
package/src/core/utils.mjs
CHANGED
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Wait helper
|
|
7
|
-
*/
|
|
8
|
-
export function waitFor(ms) {
|
|
9
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Retry with backoff
|
|
14
|
-
*/
|
|
15
|
-
export async function retry(fn, options = {}) {
|
|
16
|
-
const maxAttempts = options.maxAttempts || 3;
|
|
17
|
-
const delay = options.delay || 1000;
|
|
18
|
-
const backoff = options.backoff || 2;
|
|
19
|
-
|
|
20
|
-
let lastError;
|
|
21
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
22
|
-
try {
|
|
23
|
-
return await fn();
|
|
24
|
-
} catch (err) {
|
|
25
|
-
lastError = err;
|
|
26
|
-
if (attempt < maxAttempts) {
|
|
27
|
-
await waitFor(delay * Math.pow(backoff, attempt - 1));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
throw lastError;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Timeout wrapper
|
|
37
|
-
*/
|
|
38
|
-
export async function withTimeout(promise, ms, message = 'Timeout') {
|
|
39
|
-
const timeout = new Promise((_, reject) => {
|
|
40
|
-
setTimeout(() => reject(new Error(message)), ms);
|
|
41
|
-
});
|
|
42
|
-
return Promise.race([promise, timeout]);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Format URL (ensure scheme)
|
|
47
|
-
*/
|
|
48
|
-
export function ensureUrlScheme(url) {
|
|
49
|
-
if (!url) return url;
|
|
50
|
-
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
|
51
|
-
// Skip special browser URLs that don't need scheme
|
|
52
|
-
if (url.startsWith('about:') || url.startsWith('chrome:') || url.startsWith('file:')) {
|
|
53
|
-
return url;
|
|
54
|
-
}
|
|
55
|
-
if (url.startsWith('localhost') || url.match(/^\\d+\\.\\d+/)) {
|
|
56
|
-
return `http://${url}`;
|
|
57
|
-
}
|
|
58
|
-
return `https://${url}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Looks like URL token
|
|
63
|
-
*/
|
|
64
|
-
export function looksLikeUrlToken(token) {
|
|
65
|
-
if (!token || typeof token !== 'string') return false;
|
|
66
|
-
if (token.startsWith('http://') || token.startsWith('https://')) return true;
|
|
67
|
-
if (token.includes('.') && !token.includes(' ')) return true;
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get positional args (exclude flags)
|
|
73
|
-
*/
|
|
74
|
-
export function getPositionals(args, excludeFlags = []) {
|
|
75
|
-
const result = [];
|
|
76
|
-
for (let i = 0; i < args.length; i++) {
|
|
77
|
-
const arg = args[i];
|
|
78
|
-
if (arg.startsWith('--')) {
|
|
79
|
-
if (!excludeFlags.includes(arg)) {
|
|
80
|
-
i++; // skip value
|
|
81
|
-
}
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (excludeFlags.includes(arg)) {
|
|
85
|
-
i++; // skip value
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
result.push(arg);
|
|
89
|
-
}
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Core utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wait helper
|
|
7
|
+
*/
|
|
8
|
+
export function waitFor(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Retry with backoff
|
|
14
|
+
*/
|
|
15
|
+
export async function retry(fn, options = {}) {
|
|
16
|
+
const maxAttempts = options.maxAttempts || 3;
|
|
17
|
+
const delay = options.delay || 1000;
|
|
18
|
+
const backoff = options.backoff || 2;
|
|
19
|
+
|
|
20
|
+
let lastError;
|
|
21
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
return await fn();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
lastError = err;
|
|
26
|
+
if (attempt < maxAttempts) {
|
|
27
|
+
await waitFor(delay * Math.pow(backoff, attempt - 1));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw lastError;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Timeout wrapper
|
|
37
|
+
*/
|
|
38
|
+
export async function withTimeout(promise, ms, message = 'Timeout') {
|
|
39
|
+
const timeout = new Promise((_, reject) => {
|
|
40
|
+
setTimeout(() => reject(new Error(message)), ms);
|
|
41
|
+
});
|
|
42
|
+
return Promise.race([promise, timeout]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format URL (ensure scheme)
|
|
47
|
+
*/
|
|
48
|
+
export function ensureUrlScheme(url) {
|
|
49
|
+
if (!url) return url;
|
|
50
|
+
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
|
51
|
+
// Skip special browser URLs that don't need scheme
|
|
52
|
+
if (url.startsWith('about:') || url.startsWith('chrome:') || url.startsWith('file:')) {
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
if (url.startsWith('localhost') || url.match(/^\\d+\\.\\d+/)) {
|
|
56
|
+
return `http://${url}`;
|
|
57
|
+
}
|
|
58
|
+
return `https://${url}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Looks like URL token
|
|
63
|
+
*/
|
|
64
|
+
export function looksLikeUrlToken(token) {
|
|
65
|
+
if (!token || typeof token !== 'string') return false;
|
|
66
|
+
if (token.startsWith('http://') || token.startsWith('https://')) return true;
|
|
67
|
+
if (token.includes('.') && !token.includes(' ')) return true;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get positional args (exclude flags)
|
|
73
|
+
*/
|
|
74
|
+
export function getPositionals(args, excludeFlags = []) {
|
|
75
|
+
const result = [];
|
|
76
|
+
for (let i = 0; i < args.length; i++) {
|
|
77
|
+
const arg = args[i];
|
|
78
|
+
if (arg.startsWith('--')) {
|
|
79
|
+
if (!excludeFlags.includes(arg)) {
|
|
80
|
+
i++; // skip value
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (excludeFlags.includes(arg)) {
|
|
85
|
+
i++; // skip value
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
result.push(arg);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { createProgressWsServer } from './ws-server.mjs';
|
|
2
|
-
|
|
3
|
-
function readFlagValue(args, names) {
|
|
4
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
5
|
-
if (!names.includes(args[i])) continue;
|
|
6
|
-
const value = args[i + 1];
|
|
7
|
-
if (!value || String(value).startsWith('-')) return null;
|
|
8
|
-
return value;
|
|
9
|
-
}
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function main() {
|
|
14
|
-
const args = process.argv.slice(2);
|
|
15
|
-
const host = readFlagValue(args, ['--host']) || process.env.CAMO_PROGRESS_WS_HOST || '127.0.0.1';
|
|
16
|
-
const port = Math.max(1, Number(readFlagValue(args, ['--port']) || process.env.CAMO_PROGRESS_WS_PORT || 7788) || 7788);
|
|
17
|
-
const server = createProgressWsServer({ host, port });
|
|
18
|
-
await server.start();
|
|
19
|
-
|
|
20
|
-
const stop = async () => {
|
|
21
|
-
await server.stop();
|
|
22
|
-
process.exit(0);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
process.on('SIGINT', stop);
|
|
26
|
-
process.on('SIGTERM', stop);
|
|
27
|
-
await new Promise(() => {});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
main().catch(() => {
|
|
31
|
-
process.exit(1);
|
|
32
|
-
});
|
|
33
|
-
|
|
1
|
+
import { createProgressWsServer } from './ws-server.mjs';
|
|
2
|
+
|
|
3
|
+
function readFlagValue(args, names) {
|
|
4
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
5
|
+
if (!names.includes(args[i])) continue;
|
|
6
|
+
const value = args[i + 1];
|
|
7
|
+
if (!value || String(value).startsWith('-')) return null;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const host = readFlagValue(args, ['--host']) || process.env.CAMO_PROGRESS_WS_HOST || '127.0.0.1';
|
|
16
|
+
const port = Math.max(1, Number(readFlagValue(args, ['--port']) || process.env.CAMO_PROGRESS_WS_PORT || 7788) || 7788);
|
|
17
|
+
const server = createProgressWsServer({ host, port });
|
|
18
|
+
await server.start();
|
|
19
|
+
|
|
20
|
+
const stop = async () => {
|
|
21
|
+
await server.stop();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
process.on('SIGINT', stop);
|
|
26
|
+
process.on('SIGTERM', stop);
|
|
27
|
+
await new Promise(() => {});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
main().catch(() => {
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
33
|
+
|
package/src/events/daemon.mjs
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_HOST = process.env.CAMO_PROGRESS_WS_HOST || '127.0.0.1';
|
|
6
|
-
const DEFAULT_PORT = Math.max(1, Number(process.env.CAMO_PROGRESS_WS_PORT || 7788) || 7788);
|
|
7
|
-
const DEFAULT_HEALTH_TIMEOUT_MS = 800;
|
|
8
|
-
const DEFAULT_START_TIMEOUT_MS = 4000;
|
|
9
|
-
const HEALTH_POLL_INTERVAL_MS = 140;
|
|
10
|
-
|
|
11
|
-
function sleep(ms) {
|
|
12
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function resolveProgressWsConfig(options = {}) {
|
|
16
|
-
const host = String(options.host || DEFAULT_HOST).trim() || DEFAULT_HOST;
|
|
17
|
-
const port = Math.max(1, Number(options.port || DEFAULT_PORT) || DEFAULT_PORT);
|
|
18
|
-
return { host, port };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function buildProgressHealthUrl(options = {}) {
|
|
22
|
-
const { host, port } = resolveProgressWsConfig(options);
|
|
23
|
-
return `http://${host}:${port}/health`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function checkProgressEventDaemon(options = {}) {
|
|
27
|
-
const timeoutMs = Math.max(150, Number(options.timeoutMs || DEFAULT_HEALTH_TIMEOUT_MS) || DEFAULT_HEALTH_TIMEOUT_MS);
|
|
28
|
-
const healthUrl = buildProgressHealthUrl(options);
|
|
29
|
-
try {
|
|
30
|
-
const response = await fetch(healthUrl, { signal: AbortSignal.timeout(timeoutMs) });
|
|
31
|
-
if (!response.ok) return false;
|
|
32
|
-
const body = await response.json().catch(() => null);
|
|
33
|
-
return Boolean(body?.ok);
|
|
34
|
-
} catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getDaemonEntryPath() {
|
|
40
|
-
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
-
return path.join(dir, 'daemon-entry.mjs');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function spawnProgressDaemon({ host, port }) {
|
|
45
|
-
const entry = getDaemonEntryPath();
|
|
46
|
-
const child = spawn(
|
|
47
|
-
process.execPath,
|
|
48
|
-
[entry, '--host', host, '--port', String(port)],
|
|
49
|
-
{
|
|
50
|
-
detached: true,
|
|
51
|
-
stdio: 'ignore',
|
|
52
|
-
env: {
|
|
53
|
-
...process.env,
|
|
54
|
-
CAMO_PROGRESS_DAEMON: '1',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
);
|
|
58
|
-
child.unref();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function ensureProgressEventDaemon(options = {}) {
|
|
62
|
-
const { host, port } = resolveProgressWsConfig(options);
|
|
63
|
-
const startTimeoutMs = Math.max(400, Number(options.startTimeoutMs || DEFAULT_START_TIMEOUT_MS) || DEFAULT_START_TIMEOUT_MS);
|
|
64
|
-
if (await checkProgressEventDaemon({ host, port })) {
|
|
65
|
-
return { ok: true, started: false, host, port };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
spawnProgressDaemon({ host, port });
|
|
69
|
-
|
|
70
|
-
const deadline = Date.now() + startTimeoutMs;
|
|
71
|
-
while (Date.now() < deadline) {
|
|
72
|
-
if (await checkProgressEventDaemon({ host, port })) {
|
|
73
|
-
return { ok: true, started: true, host, port };
|
|
74
|
-
}
|
|
75
|
-
await sleep(HEALTH_POLL_INTERVAL_MS);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { ok: false, started: true, host, port, error: 'progress_daemon_start_timeout' };
|
|
79
|
-
}
|
|
80
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_HOST = process.env.CAMO_PROGRESS_WS_HOST || '127.0.0.1';
|
|
6
|
+
const DEFAULT_PORT = Math.max(1, Number(process.env.CAMO_PROGRESS_WS_PORT || 7788) || 7788);
|
|
7
|
+
const DEFAULT_HEALTH_TIMEOUT_MS = 800;
|
|
8
|
+
const DEFAULT_START_TIMEOUT_MS = 4000;
|
|
9
|
+
const HEALTH_POLL_INTERVAL_MS = 140;
|
|
10
|
+
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveProgressWsConfig(options = {}) {
|
|
16
|
+
const host = String(options.host || DEFAULT_HOST).trim() || DEFAULT_HOST;
|
|
17
|
+
const port = Math.max(1, Number(options.port || DEFAULT_PORT) || DEFAULT_PORT);
|
|
18
|
+
return { host, port };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildProgressHealthUrl(options = {}) {
|
|
22
|
+
const { host, port } = resolveProgressWsConfig(options);
|
|
23
|
+
return `http://${host}:${port}/health`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function checkProgressEventDaemon(options = {}) {
|
|
27
|
+
const timeoutMs = Math.max(150, Number(options.timeoutMs || DEFAULT_HEALTH_TIMEOUT_MS) || DEFAULT_HEALTH_TIMEOUT_MS);
|
|
28
|
+
const healthUrl = buildProgressHealthUrl(options);
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(healthUrl, { signal: AbortSignal.timeout(timeoutMs) });
|
|
31
|
+
if (!response.ok) return false;
|
|
32
|
+
const body = await response.json().catch(() => null);
|
|
33
|
+
return Boolean(body?.ok);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getDaemonEntryPath() {
|
|
40
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
return path.join(dir, 'daemon-entry.mjs');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function spawnProgressDaemon({ host, port }) {
|
|
45
|
+
const entry = getDaemonEntryPath();
|
|
46
|
+
const child = spawn(
|
|
47
|
+
process.execPath,
|
|
48
|
+
[entry, '--host', host, '--port', String(port)],
|
|
49
|
+
{
|
|
50
|
+
detached: true,
|
|
51
|
+
stdio: 'ignore',
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
CAMO_PROGRESS_DAEMON: '1',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
child.unref();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function ensureProgressEventDaemon(options = {}) {
|
|
62
|
+
const { host, port } = resolveProgressWsConfig(options);
|
|
63
|
+
const startTimeoutMs = Math.max(400, Number(options.startTimeoutMs || DEFAULT_START_TIMEOUT_MS) || DEFAULT_START_TIMEOUT_MS);
|
|
64
|
+
if (await checkProgressEventDaemon({ host, port })) {
|
|
65
|
+
return { ok: true, started: false, host, port };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
spawnProgressDaemon({ host, port });
|
|
69
|
+
|
|
70
|
+
const deadline = Date.now() + startTimeoutMs;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
if (await checkProgressEventDaemon({ host, port })) {
|
|
73
|
+
return { ok: true, started: true, host, port };
|
|
74
|
+
}
|
|
75
|
+
await sleep(HEALTH_POLL_INTERVAL_MS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ok: false, started: true, host, port, error: 'progress_daemon_start_timeout' };
|
|
79
|
+
}
|
|
80
|
+
|
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { CONFIG_DIR, ensureDir } from '../utils/config.mjs';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_EVENTS_DIR = path.join(CONFIG_DIR, 'run', 'events');
|
|
6
|
-
const DEFAULT_EVENTS_FILE = path.join(DEFAULT_EVENTS_DIR, 'progress-events.jsonl');
|
|
7
|
-
const MAX_REPLAY_BYTES = Math.max(64 * 1024, Number(process.env.CAMO_PROGRESS_REPLAY_MAX_BYTES) || (2 * 1024 * 1024));
|
|
8
|
-
|
|
9
|
-
let localSeq = 0;
|
|
10
|
-
|
|
11
|
-
function resolveEventsFile() {
|
|
12
|
-
const raw = String(process.env.CAMO_PROGRESS_EVENTS_FILE || DEFAULT_EVENTS_FILE).trim();
|
|
13
|
-
return raw || DEFAULT_EVENTS_FILE;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function nextSeq() {
|
|
17
|
-
localSeq = (localSeq + 1) % 1_000_000_000;
|
|
18
|
-
return `${Date.now()}-${process.pid}-${localSeq}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function normalizePayload(payload) {
|
|
22
|
-
if (payload === undefined) return null;
|
|
23
|
-
if (payload === null) return null;
|
|
24
|
-
if (typeof payload === 'object') return payload;
|
|
25
|
-
return { value: payload };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function getProgressEventsFile() {
|
|
29
|
-
return resolveEventsFile();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function ensureProgressEventStore() {
|
|
33
|
-
const eventFile = resolveEventsFile();
|
|
34
|
-
ensureDir(path.dirname(eventFile));
|
|
35
|
-
if (!fs.existsSync(eventFile)) {
|
|
36
|
-
fs.writeFileSync(eventFile, '', 'utf8');
|
|
37
|
-
}
|
|
38
|
-
return eventFile;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function buildProgressEvent({
|
|
42
|
-
ts = null,
|
|
43
|
-
seq = null,
|
|
44
|
-
source = 'camo',
|
|
45
|
-
mode = 'normal',
|
|
46
|
-
profileId = null,
|
|
47
|
-
runId = null,
|
|
48
|
-
event = 'unknown',
|
|
49
|
-
payload = null,
|
|
50
|
-
} = {}) {
|
|
51
|
-
return {
|
|
52
|
-
ts: ts || new Date().toISOString(),
|
|
53
|
-
seq: seq || nextSeq(),
|
|
54
|
-
source: String(source || 'camo'),
|
|
55
|
-
mode: String(mode || 'normal'),
|
|
56
|
-
profileId: profileId ? String(profileId) : null,
|
|
57
|
-
runId: runId ? String(runId) : null,
|
|
58
|
-
event: String(event || 'unknown'),
|
|
59
|
-
payload: normalizePayload(payload),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function appendProgressEvent(input = {}) {
|
|
64
|
-
const eventFile = ensureProgressEventStore();
|
|
65
|
-
const event = buildProgressEvent(input);
|
|
66
|
-
fs.appendFileSync(eventFile, `${JSON.stringify(event)}\n`, 'utf8');
|
|
67
|
-
return event;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function safeAppendProgressEvent(input = {}) {
|
|
71
|
-
try {
|
|
72
|
-
return appendProgressEvent(input);
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function readRecentProgressEvents(limit = 100) {
|
|
79
|
-
const eventFile = ensureProgressEventStore();
|
|
80
|
-
const maxItems = Math.max(0, Number(limit) || 0);
|
|
81
|
-
if (maxItems === 0) return [];
|
|
82
|
-
const stat = fs.statSync(eventFile);
|
|
83
|
-
if (stat.size <= 0) return [];
|
|
84
|
-
|
|
85
|
-
const start = Math.max(0, stat.size - MAX_REPLAY_BYTES);
|
|
86
|
-
const fd = fs.openSync(eventFile, 'r');
|
|
87
|
-
try {
|
|
88
|
-
const length = stat.size - start;
|
|
89
|
-
const buffer = Buffer.alloc(length);
|
|
90
|
-
fs.readSync(fd, buffer, 0, length, start);
|
|
91
|
-
const raw = buffer.toString('utf8');
|
|
92
|
-
return raw
|
|
93
|
-
.split('\n')
|
|
94
|
-
.map((line) => line.trim())
|
|
95
|
-
.filter(Boolean)
|
|
96
|
-
.slice(-maxItems)
|
|
97
|
-
.map((line) => {
|
|
98
|
-
try {
|
|
99
|
-
return JSON.parse(line);
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
.filter(Boolean);
|
|
105
|
-
} finally {
|
|
106
|
-
fs.closeSync(fd);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CONFIG_DIR, ensureDir } from '../utils/config.mjs';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_EVENTS_DIR = path.join(CONFIG_DIR, 'run', 'events');
|
|
6
|
+
const DEFAULT_EVENTS_FILE = path.join(DEFAULT_EVENTS_DIR, 'progress-events.jsonl');
|
|
7
|
+
const MAX_REPLAY_BYTES = Math.max(64 * 1024, Number(process.env.CAMO_PROGRESS_REPLAY_MAX_BYTES) || (2 * 1024 * 1024));
|
|
8
|
+
|
|
9
|
+
let localSeq = 0;
|
|
10
|
+
|
|
11
|
+
function resolveEventsFile() {
|
|
12
|
+
const raw = String(process.env.CAMO_PROGRESS_EVENTS_FILE || DEFAULT_EVENTS_FILE).trim();
|
|
13
|
+
return raw || DEFAULT_EVENTS_FILE;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nextSeq() {
|
|
17
|
+
localSeq = (localSeq + 1) % 1_000_000_000;
|
|
18
|
+
return `${Date.now()}-${process.pid}-${localSeq}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizePayload(payload) {
|
|
22
|
+
if (payload === undefined) return null;
|
|
23
|
+
if (payload === null) return null;
|
|
24
|
+
if (typeof payload === 'object') return payload;
|
|
25
|
+
return { value: payload };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getProgressEventsFile() {
|
|
29
|
+
return resolveEventsFile();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ensureProgressEventStore() {
|
|
33
|
+
const eventFile = resolveEventsFile();
|
|
34
|
+
ensureDir(path.dirname(eventFile));
|
|
35
|
+
if (!fs.existsSync(eventFile)) {
|
|
36
|
+
fs.writeFileSync(eventFile, '', 'utf8');
|
|
37
|
+
}
|
|
38
|
+
return eventFile;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildProgressEvent({
|
|
42
|
+
ts = null,
|
|
43
|
+
seq = null,
|
|
44
|
+
source = 'camo',
|
|
45
|
+
mode = 'normal',
|
|
46
|
+
profileId = null,
|
|
47
|
+
runId = null,
|
|
48
|
+
event = 'unknown',
|
|
49
|
+
payload = null,
|
|
50
|
+
} = {}) {
|
|
51
|
+
return {
|
|
52
|
+
ts: ts || new Date().toISOString(),
|
|
53
|
+
seq: seq || nextSeq(),
|
|
54
|
+
source: String(source || 'camo'),
|
|
55
|
+
mode: String(mode || 'normal'),
|
|
56
|
+
profileId: profileId ? String(profileId) : null,
|
|
57
|
+
runId: runId ? String(runId) : null,
|
|
58
|
+
event: String(event || 'unknown'),
|
|
59
|
+
payload: normalizePayload(payload),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function appendProgressEvent(input = {}) {
|
|
64
|
+
const eventFile = ensureProgressEventStore();
|
|
65
|
+
const event = buildProgressEvent(input);
|
|
66
|
+
fs.appendFileSync(eventFile, `${JSON.stringify(event)}\n`, 'utf8');
|
|
67
|
+
return event;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function safeAppendProgressEvent(input = {}) {
|
|
71
|
+
try {
|
|
72
|
+
return appendProgressEvent(input);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function readRecentProgressEvents(limit = 100) {
|
|
79
|
+
const eventFile = ensureProgressEventStore();
|
|
80
|
+
const maxItems = Math.max(0, Number(limit) || 0);
|
|
81
|
+
if (maxItems === 0) return [];
|
|
82
|
+
const stat = fs.statSync(eventFile);
|
|
83
|
+
if (stat.size <= 0) return [];
|
|
84
|
+
|
|
85
|
+
const start = Math.max(0, stat.size - MAX_REPLAY_BYTES);
|
|
86
|
+
const fd = fs.openSync(eventFile, 'r');
|
|
87
|
+
try {
|
|
88
|
+
const length = stat.size - start;
|
|
89
|
+
const buffer = Buffer.alloc(length);
|
|
90
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
91
|
+
const raw = buffer.toString('utf8');
|
|
92
|
+
return raw
|
|
93
|
+
.split('\n')
|
|
94
|
+
.map((line) => line.trim())
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.slice(-maxItems)
|
|
97
|
+
.map((line) => {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(line);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
} finally {
|
|
106
|
+
fs.closeSync(fd);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|