@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
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { CONFIG_DIR, ensureDir } from './config.mjs';
|
|
4
|
-
|
|
5
|
-
export const COMMAND_LOG_DIR = path.join(CONFIG_DIR, 'logs');
|
|
6
|
-
export const COMMAND_LOG_FILE = path.join(COMMAND_LOG_DIR, 'command-log.jsonl');
|
|
7
|
-
|
|
8
|
-
function safeSerialize(value) {
|
|
9
|
-
if (value === undefined) return undefined;
|
|
10
|
-
if (value === null) return null;
|
|
11
|
-
try {
|
|
12
|
-
return JSON.parse(JSON.stringify(value));
|
|
13
|
-
} catch {
|
|
14
|
-
return { text: String(value) };
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizeMeta(meta = {}) {
|
|
19
|
-
const sender = meta?.sender && typeof meta.sender === 'object' ? meta.sender : {};
|
|
20
|
-
return {
|
|
21
|
-
source: String(meta?.source || '').trim() || 'unknown',
|
|
22
|
-
cwd: String(meta?.cwd || sender?.cwd || '').trim() || process.cwd(),
|
|
23
|
-
pid: Number(meta?.pid || sender?.pid || process.pid) || process.pid,
|
|
24
|
-
ppid: Number(meta?.ppid || sender?.ppid || process.ppid) || process.ppid,
|
|
25
|
-
argv: Array.isArray(meta?.argv) ? meta.argv.map((item) => String(item)) : undefined,
|
|
26
|
-
sender: {
|
|
27
|
-
source: String(sender?.source || meta?.source || '').trim() || 'unknown',
|
|
28
|
-
cwd: String(sender?.cwd || meta?.cwd || '').trim() || process.cwd(),
|
|
29
|
-
pid: Number(sender?.pid || meta?.pid || process.pid) || process.pid,
|
|
30
|
-
ppid: Number(sender?.ppid || meta?.ppid || process.ppid) || process.ppid,
|
|
31
|
-
argv: Array.isArray(sender?.argv) ? sender.argv.map((item) => String(item)) : undefined,
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function appendCommandLog(entry = {}) {
|
|
37
|
-
try {
|
|
38
|
-
ensureDir(COMMAND_LOG_DIR);
|
|
39
|
-
const meta = normalizeMeta(entry?.meta || {});
|
|
40
|
-
const line = {
|
|
41
|
-
ts: new Date().toISOString(),
|
|
42
|
-
action: String(entry?.action || '').trim() || null,
|
|
43
|
-
profileId: String(entry?.profileId || '').trim() || null,
|
|
44
|
-
command: String(entry?.command || '').trim() || null,
|
|
45
|
-
args: Array.isArray(entry?.args) ? entry.args.map((item) => String(item)) : undefined,
|
|
46
|
-
payload: safeSerialize(entry?.payload),
|
|
47
|
-
meta,
|
|
48
|
-
};
|
|
49
|
-
fs.appendFileSync(COMMAND_LOG_FILE, `${JSON.stringify(line)}\n`, 'utf8');
|
|
50
|
-
return line;
|
|
51
|
-
} catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function buildCommandSenderMeta(overrides = {}) {
|
|
57
|
-
return {
|
|
58
|
-
source: String(overrides?.source || '').trim() || 'unknown',
|
|
59
|
-
cwd: String(overrides?.cwd || '').trim() || process.cwd(),
|
|
60
|
-
pid: Number(overrides?.pid || process.pid) || process.pid,
|
|
61
|
-
ppid: Number(overrides?.ppid || process.ppid) || process.ppid,
|
|
62
|
-
argv: Array.isArray(overrides?.argv) ? overrides.argv.map((item) => String(item)) : process.argv.slice(),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CONFIG_DIR, ensureDir } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
export const COMMAND_LOG_DIR = path.join(CONFIG_DIR, 'logs');
|
|
6
|
+
export const COMMAND_LOG_FILE = path.join(COMMAND_LOG_DIR, 'command-log.jsonl');
|
|
7
|
+
|
|
8
|
+
function safeSerialize(value) {
|
|
9
|
+
if (value === undefined) return undefined;
|
|
10
|
+
if (value === null) return null;
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(JSON.stringify(value));
|
|
13
|
+
} catch {
|
|
14
|
+
return { text: String(value) };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeMeta(meta = {}) {
|
|
19
|
+
const sender = meta?.sender && typeof meta.sender === 'object' ? meta.sender : {};
|
|
20
|
+
return {
|
|
21
|
+
source: String(meta?.source || '').trim() || 'unknown',
|
|
22
|
+
cwd: String(meta?.cwd || sender?.cwd || '').trim() || process.cwd(),
|
|
23
|
+
pid: Number(meta?.pid || sender?.pid || process.pid) || process.pid,
|
|
24
|
+
ppid: Number(meta?.ppid || sender?.ppid || process.ppid) || process.ppid,
|
|
25
|
+
argv: Array.isArray(meta?.argv) ? meta.argv.map((item) => String(item)) : undefined,
|
|
26
|
+
sender: {
|
|
27
|
+
source: String(sender?.source || meta?.source || '').trim() || 'unknown',
|
|
28
|
+
cwd: String(sender?.cwd || meta?.cwd || '').trim() || process.cwd(),
|
|
29
|
+
pid: Number(sender?.pid || meta?.pid || process.pid) || process.pid,
|
|
30
|
+
ppid: Number(sender?.ppid || meta?.ppid || process.ppid) || process.ppid,
|
|
31
|
+
argv: Array.isArray(sender?.argv) ? sender.argv.map((item) => String(item)) : undefined,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function appendCommandLog(entry = {}) {
|
|
37
|
+
try {
|
|
38
|
+
ensureDir(COMMAND_LOG_DIR);
|
|
39
|
+
const meta = normalizeMeta(entry?.meta || {});
|
|
40
|
+
const line = {
|
|
41
|
+
ts: new Date().toISOString(),
|
|
42
|
+
action: String(entry?.action || '').trim() || null,
|
|
43
|
+
profileId: String(entry?.profileId || '').trim() || null,
|
|
44
|
+
command: String(entry?.command || '').trim() || null,
|
|
45
|
+
args: Array.isArray(entry?.args) ? entry.args.map((item) => String(item)) : undefined,
|
|
46
|
+
payload: safeSerialize(entry?.payload),
|
|
47
|
+
meta,
|
|
48
|
+
};
|
|
49
|
+
fs.appendFileSync(COMMAND_LOG_FILE, `${JSON.stringify(line)}\n`, 'utf8');
|
|
50
|
+
return line;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildCommandSenderMeta(overrides = {}) {
|
|
57
|
+
return {
|
|
58
|
+
source: String(overrides?.source || '').trim() || 'unknown',
|
|
59
|
+
cwd: String(overrides?.cwd || '').trim() || process.cwd(),
|
|
60
|
+
pid: Number(overrides?.pid || process.pid) || process.pid,
|
|
61
|
+
ppid: Number(overrides?.ppid || process.ppid) || process.ppid,
|
|
62
|
+
argv: Array.isArray(overrides?.argv) ? overrides.argv.map((item) => String(item)) : process.argv.slice(),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/utils/config.mjs
CHANGED
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
|
|
6
|
-
function hasDrive(letter) {
|
|
7
|
-
if (process.platform !== 'win32') return false;
|
|
8
|
-
try {
|
|
9
|
-
return fs.existsSync(`${String(letter || '').replace(/[^A-Za-z]/g, '').toUpperCase()}:\\`);
|
|
10
|
-
} catch {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function normalizePathForPlatform(input, platform = process.platform) {
|
|
16
|
-
const raw = String(input || '').trim();
|
|
17
|
-
const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(raw);
|
|
18
|
-
const pathApi = isWinPath ? path.win32 : path;
|
|
19
|
-
return isWinPath ? pathApi.normalize(raw) : path.resolve(raw);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function normalizeLegacyCamoRoot(input, platform = process.platform) {
|
|
23
|
-
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
24
|
-
const resolved = normalizePathForPlatform(input, platform);
|
|
25
|
-
const base = pathApi.basename(resolved).toLowerCase();
|
|
26
|
-
if (base === '.camo' || base === 'camo') return resolved;
|
|
27
|
-
return pathApi.join(resolved, '.camo');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function resolveCamoRoot(options = {}) {
|
|
31
|
-
const env = options.env || process.env;
|
|
32
|
-
const platform = String(options.platform || process.platform);
|
|
33
|
-
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
34
|
-
const homeDir = String(options.homeDir || os.homedir());
|
|
35
|
-
const explicitDataRoot = String(env.CAMO_DATA_ROOT || env.CAMO_HOME || '').trim();
|
|
36
|
-
if (explicitDataRoot) return normalizePathForPlatform(explicitDataRoot, platform);
|
|
37
|
-
|
|
38
|
-
const legacyRoot = String(env.CAMO_ROOT || env.CAMO_PORTABLE_ROOT || '').trim();
|
|
39
|
-
if (legacyRoot) return normalizeLegacyCamoRoot(legacyRoot, platform);
|
|
40
|
-
|
|
41
|
-
const dDriveExists = typeof options.hasDDrive === 'boolean'
|
|
42
|
-
? options.hasDDrive
|
|
43
|
-
: hasDrive('D');
|
|
44
|
-
if (platform === 'win32') {
|
|
45
|
-
return dDriveExists ? 'D:\\camo' : pathApi.join(homeDir, '.camo');
|
|
46
|
-
}
|
|
47
|
-
return pathApi.join(homeDir, '.camo');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Backward-compatible export name; camo no longer uses legacy paths.
|
|
51
|
-
export function resolveLegacyRoot(options = {}) {
|
|
52
|
-
return resolveCamoRoot(options);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function resolveProfilesDir(options = {}) {
|
|
56
|
-
const env = options.env || process.env;
|
|
57
|
-
const platform = String(options.platform || process.platform);
|
|
58
|
-
const explicitProfileRoot = String(env.CAMO_PROFILE_ROOT || env.CAMO_PATHS_PROFILES || '').trim();
|
|
59
|
-
if (explicitProfileRoot) {
|
|
60
|
-
return normalizePathForPlatform(explicitProfileRoot, platform);
|
|
61
|
-
}
|
|
62
|
-
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
63
|
-
return pathApi.join(resolveCamoRoot(options), 'profiles');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const CONFIG_DIR = resolveCamoRoot();
|
|
67
|
-
export const PROFILES_DIR = resolveProfilesDir();
|
|
68
|
-
export const CONFIG_FILE = path.join(CONFIG_DIR, 'camo-cli.json');
|
|
69
|
-
export const PROFILE_META_FILE = 'camo-profile.json';
|
|
70
|
-
export const BROWSER_SERVICE_URL = process.env.CAMO_BROWSER_URL
|
|
71
|
-
|| process.env.CAMO_BROWSER_HTTP_URL
|
|
72
|
-
|| (process.env.CAMO_BROWSER_HOST ? `http://${process.env.CAMO_BROWSER_HOST}` : '')
|
|
73
|
-
|| 'http://127.0.0.1:7704';
|
|
74
|
-
|
|
75
|
-
export function ensureDir(p) {
|
|
76
|
-
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function readJson(p) {
|
|
80
|
-
try {
|
|
81
|
-
if (!fs.existsSync(p)) return null;
|
|
82
|
-
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function writeJson(p, data) {
|
|
89
|
-
ensureDir(path.dirname(p));
|
|
90
|
-
fs.writeFileSync(p, JSON.stringify(data, null, 2));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function loadConfig() {
|
|
94
|
-
const raw = readJson(CONFIG_FILE) || {};
|
|
95
|
-
return {
|
|
96
|
-
defaultProfile: typeof raw.defaultProfile === 'string' ? raw.defaultProfile : null,
|
|
97
|
-
repoRoot: typeof raw.repoRoot === 'string' ? raw.repoRoot : null,
|
|
98
|
-
highlightMode: typeof raw.highlightMode === 'boolean' ? raw.highlightMode : true,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function saveConfig(config) {
|
|
103
|
-
writeJson(CONFIG_FILE, config);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function listProfiles() {
|
|
107
|
-
if (!fs.existsSync(PROFILES_DIR)) return [];
|
|
108
|
-
return fs.readdirSync(PROFILES_DIR, { withFileTypes: true })
|
|
109
|
-
.filter((d) => d.isDirectory())
|
|
110
|
-
.map((d) => d.name)
|
|
111
|
-
.filter((name) => !name.includes(':') && !name.includes('/') && !name.startsWith('.'))
|
|
112
|
-
.sort();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function isValidProfileId(profileId) {
|
|
116
|
-
return typeof profileId === 'string' && /^[a-zA-Z0-9._-]+$/.test(profileId);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function createProfile(profileId) {
|
|
120
|
-
if (!isValidProfileId(profileId)) {
|
|
121
|
-
throw new Error('Invalid profileId. Use only letters, numbers, dot, underscore, dash.');
|
|
122
|
-
}
|
|
123
|
-
const profileDir = getProfileDir(profileId);
|
|
124
|
-
if (fs.existsSync(profileDir)) throw new Error(`Profile already exists: ${profileId}`);
|
|
125
|
-
ensureDir(profileDir);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function deleteProfile(profileId) {
|
|
129
|
-
const profileDir = getProfileDir(profileId);
|
|
130
|
-
if (!fs.existsSync(profileDir)) throw new Error(`Profile not found: ${profileId}`);
|
|
131
|
-
fs.rmSync(profileDir, { recursive: true, force: true });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function setDefaultProfile(profileId) {
|
|
135
|
-
const cfg = loadConfig();
|
|
136
|
-
cfg.defaultProfile = profileId;
|
|
137
|
-
saveConfig(cfg);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function setRepoRoot(repoRoot) {
|
|
141
|
-
const cfg = loadConfig();
|
|
142
|
-
cfg.repoRoot = repoRoot;
|
|
143
|
-
saveConfig(cfg);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function getDefaultProfile() {
|
|
147
|
-
return loadConfig().defaultProfile;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function getHighlightMode() {
|
|
151
|
-
return loadConfig().highlightMode !== false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function setHighlightMode(enabled) {
|
|
155
|
-
const cfg = loadConfig();
|
|
156
|
-
cfg.highlightMode = enabled !== false;
|
|
157
|
-
saveConfig(cfg);
|
|
158
|
-
return cfg.highlightMode;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function getProfileDir(profileId) {
|
|
162
|
-
return path.join(PROFILES_DIR, String(profileId || '').trim());
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function getProfileMetaFile(profileId) {
|
|
166
|
-
return path.join(getProfileDir(profileId), PROFILE_META_FILE);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function loadProfileMeta(profileId) {
|
|
170
|
-
if (!isValidProfileId(profileId)) return {};
|
|
171
|
-
return readJson(getProfileMetaFile(profileId)) || {};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function saveProfileMeta(profileId, patch) {
|
|
175
|
-
if (!isValidProfileId(profileId)) return null;
|
|
176
|
-
const profileDir = getProfileDir(profileId);
|
|
177
|
-
ensureDir(profileDir);
|
|
178
|
-
const current = loadProfileMeta(profileId);
|
|
179
|
-
const next = {
|
|
180
|
-
...current,
|
|
181
|
-
...patch,
|
|
182
|
-
updatedAt: Date.now(),
|
|
183
|
-
};
|
|
184
|
-
writeJson(getProfileMetaFile(profileId), next);
|
|
185
|
-
return next;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function getProfileWindowSize(profileId) {
|
|
189
|
-
const meta = loadProfileMeta(profileId);
|
|
190
|
-
const width = Number(meta?.window?.width);
|
|
191
|
-
const height = Number(meta?.window?.height);
|
|
192
|
-
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
193
|
-
if (width < 320 || height < 240) return null;
|
|
194
|
-
return {
|
|
195
|
-
width: Math.floor(width),
|
|
196
|
-
height: Math.floor(height),
|
|
197
|
-
updatedAt: Number(meta?.window?.updatedAt) || Number(meta?.updatedAt) || null,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function setProfileWindowSize(profileId, width, height) {
|
|
202
|
-
const parsedWidth = Number(width);
|
|
203
|
-
const parsedHeight = Number(height);
|
|
204
|
-
if (!Number.isFinite(parsedWidth) || !Number.isFinite(parsedHeight)) return null;
|
|
205
|
-
if (parsedWidth < 320 || parsedHeight < 240) return null;
|
|
206
|
-
const now = Date.now();
|
|
207
|
-
return saveProfileMeta(profileId, {
|
|
208
|
-
window: {
|
|
209
|
-
width: Math.floor(parsedWidth),
|
|
210
|
-
height: Math.floor(parsedHeight),
|
|
211
|
-
updatedAt: now,
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
|
|
6
|
+
function hasDrive(letter) {
|
|
7
|
+
if (process.platform !== 'win32') return false;
|
|
8
|
+
try {
|
|
9
|
+
return fs.existsSync(`${String(letter || '').replace(/[^A-Za-z]/g, '').toUpperCase()}:\\`);
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizePathForPlatform(input, platform = process.platform) {
|
|
16
|
+
const raw = String(input || '').trim();
|
|
17
|
+
const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(raw);
|
|
18
|
+
const pathApi = isWinPath ? path.win32 : path;
|
|
19
|
+
return isWinPath ? pathApi.normalize(raw) : path.resolve(raw);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeLegacyCamoRoot(input, platform = process.platform) {
|
|
23
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
24
|
+
const resolved = normalizePathForPlatform(input, platform);
|
|
25
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
26
|
+
if (base === '.camo' || base === 'camo') return resolved;
|
|
27
|
+
return pathApi.join(resolved, '.camo');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveCamoRoot(options = {}) {
|
|
31
|
+
const env = options.env || process.env;
|
|
32
|
+
const platform = String(options.platform || process.platform);
|
|
33
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
34
|
+
const homeDir = String(options.homeDir || os.homedir());
|
|
35
|
+
const explicitDataRoot = String(env.CAMO_DATA_ROOT || env.CAMO_HOME || '').trim();
|
|
36
|
+
if (explicitDataRoot) return normalizePathForPlatform(explicitDataRoot, platform);
|
|
37
|
+
|
|
38
|
+
const legacyRoot = String(env.CAMO_ROOT || env.CAMO_PORTABLE_ROOT || '').trim();
|
|
39
|
+
if (legacyRoot) return normalizeLegacyCamoRoot(legacyRoot, platform);
|
|
40
|
+
|
|
41
|
+
const dDriveExists = typeof options.hasDDrive === 'boolean'
|
|
42
|
+
? options.hasDDrive
|
|
43
|
+
: hasDrive('D');
|
|
44
|
+
if (platform === 'win32') {
|
|
45
|
+
return dDriveExists ? 'D:\\camo' : pathApi.join(homeDir, '.camo');
|
|
46
|
+
}
|
|
47
|
+
return pathApi.join(homeDir, '.camo');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Backward-compatible export name; camo no longer uses legacy paths.
|
|
51
|
+
export function resolveLegacyRoot(options = {}) {
|
|
52
|
+
return resolveCamoRoot(options);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveProfilesDir(options = {}) {
|
|
56
|
+
const env = options.env || process.env;
|
|
57
|
+
const platform = String(options.platform || process.platform);
|
|
58
|
+
const explicitProfileRoot = String(env.CAMO_PROFILE_ROOT || env.CAMO_PATHS_PROFILES || '').trim();
|
|
59
|
+
if (explicitProfileRoot) {
|
|
60
|
+
return normalizePathForPlatform(explicitProfileRoot, platform);
|
|
61
|
+
}
|
|
62
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
63
|
+
return pathApi.join(resolveCamoRoot(options), 'profiles');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const CONFIG_DIR = resolveCamoRoot();
|
|
67
|
+
export const PROFILES_DIR = resolveProfilesDir();
|
|
68
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, 'camo-cli.json');
|
|
69
|
+
export const PROFILE_META_FILE = 'camo-profile.json';
|
|
70
|
+
export const BROWSER_SERVICE_URL = process.env.CAMO_BROWSER_URL
|
|
71
|
+
|| process.env.CAMO_BROWSER_HTTP_URL
|
|
72
|
+
|| (process.env.CAMO_BROWSER_HOST ? `http://${process.env.CAMO_BROWSER_HOST}` : '')
|
|
73
|
+
|| 'http://127.0.0.1:7704';
|
|
74
|
+
|
|
75
|
+
export function ensureDir(p) {
|
|
76
|
+
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readJson(p) {
|
|
80
|
+
try {
|
|
81
|
+
if (!fs.existsSync(p)) return null;
|
|
82
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function writeJson(p, data) {
|
|
89
|
+
ensureDir(path.dirname(p));
|
|
90
|
+
fs.writeFileSync(p, JSON.stringify(data, null, 2));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function loadConfig() {
|
|
94
|
+
const raw = readJson(CONFIG_FILE) || {};
|
|
95
|
+
return {
|
|
96
|
+
defaultProfile: typeof raw.defaultProfile === 'string' ? raw.defaultProfile : null,
|
|
97
|
+
repoRoot: typeof raw.repoRoot === 'string' ? raw.repoRoot : null,
|
|
98
|
+
highlightMode: typeof raw.highlightMode === 'boolean' ? raw.highlightMode : true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function saveConfig(config) {
|
|
103
|
+
writeJson(CONFIG_FILE, config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function listProfiles() {
|
|
107
|
+
if (!fs.existsSync(PROFILES_DIR)) return [];
|
|
108
|
+
return fs.readdirSync(PROFILES_DIR, { withFileTypes: true })
|
|
109
|
+
.filter((d) => d.isDirectory())
|
|
110
|
+
.map((d) => d.name)
|
|
111
|
+
.filter((name) => !name.includes(':') && !name.includes('/') && !name.startsWith('.'))
|
|
112
|
+
.sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isValidProfileId(profileId) {
|
|
116
|
+
return typeof profileId === 'string' && /^[a-zA-Z0-9._-]+$/.test(profileId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createProfile(profileId) {
|
|
120
|
+
if (!isValidProfileId(profileId)) {
|
|
121
|
+
throw new Error('Invalid profileId. Use only letters, numbers, dot, underscore, dash.');
|
|
122
|
+
}
|
|
123
|
+
const profileDir = getProfileDir(profileId);
|
|
124
|
+
if (fs.existsSync(profileDir)) throw new Error(`Profile already exists: ${profileId}`);
|
|
125
|
+
ensureDir(profileDir);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function deleteProfile(profileId) {
|
|
129
|
+
const profileDir = getProfileDir(profileId);
|
|
130
|
+
if (!fs.existsSync(profileDir)) throw new Error(`Profile not found: ${profileId}`);
|
|
131
|
+
fs.rmSync(profileDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function setDefaultProfile(profileId) {
|
|
135
|
+
const cfg = loadConfig();
|
|
136
|
+
cfg.defaultProfile = profileId;
|
|
137
|
+
saveConfig(cfg);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function setRepoRoot(repoRoot) {
|
|
141
|
+
const cfg = loadConfig();
|
|
142
|
+
cfg.repoRoot = repoRoot;
|
|
143
|
+
saveConfig(cfg);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getDefaultProfile() {
|
|
147
|
+
return loadConfig().defaultProfile;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getHighlightMode() {
|
|
151
|
+
return loadConfig().highlightMode !== false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function setHighlightMode(enabled) {
|
|
155
|
+
const cfg = loadConfig();
|
|
156
|
+
cfg.highlightMode = enabled !== false;
|
|
157
|
+
saveConfig(cfg);
|
|
158
|
+
return cfg.highlightMode;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getProfileDir(profileId) {
|
|
162
|
+
return path.join(PROFILES_DIR, String(profileId || '').trim());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getProfileMetaFile(profileId) {
|
|
166
|
+
return path.join(getProfileDir(profileId), PROFILE_META_FILE);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function loadProfileMeta(profileId) {
|
|
170
|
+
if (!isValidProfileId(profileId)) return {};
|
|
171
|
+
return readJson(getProfileMetaFile(profileId)) || {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function saveProfileMeta(profileId, patch) {
|
|
175
|
+
if (!isValidProfileId(profileId)) return null;
|
|
176
|
+
const profileDir = getProfileDir(profileId);
|
|
177
|
+
ensureDir(profileDir);
|
|
178
|
+
const current = loadProfileMeta(profileId);
|
|
179
|
+
const next = {
|
|
180
|
+
...current,
|
|
181
|
+
...patch,
|
|
182
|
+
updatedAt: Date.now(),
|
|
183
|
+
};
|
|
184
|
+
writeJson(getProfileMetaFile(profileId), next);
|
|
185
|
+
return next;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function getProfileWindowSize(profileId) {
|
|
189
|
+
const meta = loadProfileMeta(profileId);
|
|
190
|
+
const width = Number(meta?.window?.width);
|
|
191
|
+
const height = Number(meta?.window?.height);
|
|
192
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
193
|
+
if (width < 320 || height < 240) return null;
|
|
194
|
+
return {
|
|
195
|
+
width: Math.floor(width),
|
|
196
|
+
height: Math.floor(height),
|
|
197
|
+
updatedAt: Number(meta?.window?.updatedAt) || Number(meta?.updatedAt) || null,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function setProfileWindowSize(profileId, width, height) {
|
|
202
|
+
const parsedWidth = Number(width);
|
|
203
|
+
const parsedHeight = Number(height);
|
|
204
|
+
if (!Number.isFinite(parsedWidth) || !Number.isFinite(parsedHeight)) return null;
|
|
205
|
+
if (parsedWidth < 320 || parsedHeight < 240) return null;
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
return saveProfileMeta(profileId, {
|
|
208
|
+
window: {
|
|
209
|
+
width: Math.floor(parsedWidth),
|
|
210
|
+
height: Math.floor(parsedHeight),
|
|
211
|
+
updatedAt: now,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|