@steipete/oracle 0.9.0 → 0.10.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/LICENSE +1 -1
- package/README.md +61 -48
- package/dist/bin/oracle-cli.js +455 -402
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/assistantResponse.js +149 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +275 -117
- package/dist/src/browser/actions/navigation.js +161 -137
- package/dist/src/browser/actions/promptComposer.js +100 -64
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/chromeLifecycle.js +62 -60
- package/dist/src/browser/config.js +34 -15
- package/dist/src/browser/constants.js +17 -12
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +62 -62
- package/dist/src/browser/domDebug.js +1 -1
- package/dist/src/browser/index.js +390 -295
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +44 -39
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +67 -34
- package/dist/src/browser/reattachHelpers.js +31 -26
- package/dist/src/browser/sessionRunner.js +37 -25
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +16 -16
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +62 -48
- package/dist/src/cli/browserDefaults.js +27 -26
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +29 -25
- package/dist/src/cli/duplicatePromptGuard.js +3 -3
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +127 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +31 -21
- package/dist/src/cli/sessionDisplay.js +95 -81
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +103 -93
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/tools/consult.js +51 -47
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +5 -5
- package/dist/src/mcp/utils.js +15 -7
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +156 -134
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +77 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +66 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import { createWriteStream } from 'node:fs';
|
|
5
|
-
const ORACLE_HOME = process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
|
|
6
|
-
const SESSIONS_DIR = path.join(ORACLE_HOME, 'sessions');
|
|
7
|
-
const MAX_STATUS_LIMIT = 1000;
|
|
8
|
-
const DEFAULT_SLUG = 'session';
|
|
9
|
-
const MAX_SLUG_WORDS = 5;
|
|
10
|
-
const MIN_CUSTOM_SLUG_WORDS = 3;
|
|
11
|
-
async function ensureDir(dirPath) {
|
|
12
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
13
|
-
}
|
|
14
|
-
export async function ensureSessionStorage() {
|
|
15
|
-
await ensureDir(SESSIONS_DIR);
|
|
16
|
-
}
|
|
17
|
-
function slugify(text, maxWords = MAX_SLUG_WORDS) {
|
|
18
|
-
const normalized = text?.toLowerCase() ?? '';
|
|
19
|
-
const words = normalized.match(/[a-z0-9]+/g) ?? [];
|
|
20
|
-
const trimmed = words.slice(0, maxWords);
|
|
21
|
-
return trimmed.length > 0 ? trimmed.join('-') : DEFAULT_SLUG;
|
|
22
|
-
}
|
|
23
|
-
function countSlugWords(slug) {
|
|
24
|
-
return slug.split('-').filter(Boolean).length;
|
|
25
|
-
}
|
|
26
|
-
function normalizeCustomSlug(candidate) {
|
|
27
|
-
const slug = slugify(candidate, MAX_SLUG_WORDS);
|
|
28
|
-
const wordCount = countSlugWords(slug);
|
|
29
|
-
if (wordCount < MIN_CUSTOM_SLUG_WORDS || wordCount > MAX_SLUG_WORDS) {
|
|
30
|
-
throw new Error(`Custom slug must include between ${MIN_CUSTOM_SLUG_WORDS} and ${MAX_SLUG_WORDS} words.`);
|
|
31
|
-
}
|
|
32
|
-
return slug;
|
|
33
|
-
}
|
|
34
|
-
export function createSessionId(prompt, customSlug) {
|
|
35
|
-
if (customSlug) {
|
|
36
|
-
return normalizeCustomSlug(customSlug);
|
|
37
|
-
}
|
|
38
|
-
return slugify(prompt);
|
|
39
|
-
}
|
|
40
|
-
function sessionDir(id) {
|
|
41
|
-
return path.join(SESSIONS_DIR, id);
|
|
42
|
-
}
|
|
43
|
-
function metaPath(id) {
|
|
44
|
-
return path.join(sessionDir(id), 'session.json');
|
|
45
|
-
}
|
|
46
|
-
function logPath(id) {
|
|
47
|
-
return path.join(sessionDir(id), 'output.log');
|
|
48
|
-
}
|
|
49
|
-
function requestPath(id) {
|
|
50
|
-
return path.join(sessionDir(id), 'request.json');
|
|
51
|
-
}
|
|
52
|
-
async function fileExists(targetPath) {
|
|
53
|
-
try {
|
|
54
|
-
await fs.access(targetPath);
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async function ensureUniqueSessionId(baseSlug) {
|
|
62
|
-
let candidate = baseSlug;
|
|
63
|
-
let suffix = 2;
|
|
64
|
-
while (await fileExists(sessionDir(candidate))) {
|
|
65
|
-
candidate = `${baseSlug}-${suffix}`;
|
|
66
|
-
suffix += 1;
|
|
67
|
-
}
|
|
68
|
-
return candidate;
|
|
69
|
-
}
|
|
70
|
-
export async function initializeSession(options, cwd) {
|
|
71
|
-
await ensureSessionStorage();
|
|
72
|
-
const baseSlug = createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
|
|
73
|
-
const sessionId = await ensureUniqueSessionId(baseSlug);
|
|
74
|
-
const dir = sessionDir(sessionId);
|
|
75
|
-
await ensureDir(dir);
|
|
76
|
-
const mode = options.mode ?? 'api';
|
|
77
|
-
const browserConfig = options.browserConfig;
|
|
78
|
-
const metadata = {
|
|
79
|
-
id: sessionId,
|
|
80
|
-
createdAt: new Date().toISOString(),
|
|
81
|
-
status: 'pending',
|
|
82
|
-
promptPreview: (options.prompt || '').slice(0, 160),
|
|
83
|
-
model: options.model,
|
|
84
|
-
cwd,
|
|
85
|
-
mode,
|
|
86
|
-
browser: browserConfig ? { config: browserConfig } : undefined,
|
|
87
|
-
options: {
|
|
88
|
-
prompt: options.prompt,
|
|
89
|
-
file: options.file ?? [],
|
|
90
|
-
model: options.model,
|
|
91
|
-
maxInput: options.maxInput,
|
|
92
|
-
system: options.system,
|
|
93
|
-
maxOutput: options.maxOutput,
|
|
94
|
-
silent: options.silent,
|
|
95
|
-
filesReport: options.filesReport,
|
|
96
|
-
slug: sessionId,
|
|
97
|
-
mode,
|
|
98
|
-
browserConfig,
|
|
99
|
-
verbose: options.verbose,
|
|
100
|
-
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
101
|
-
browserInlineFiles: options.browserInlineFiles,
|
|
102
|
-
background: options.background,
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
await fs.writeFile(metaPath(sessionId), JSON.stringify(metadata, null, 2), 'utf8');
|
|
106
|
-
await fs.writeFile(requestPath(sessionId), JSON.stringify(metadata.options, null, 2), 'utf8');
|
|
107
|
-
await fs.writeFile(logPath(sessionId), '', 'utf8');
|
|
108
|
-
return metadata;
|
|
109
|
-
}
|
|
110
|
-
export async function readSessionMetadata(sessionId) {
|
|
111
|
-
try {
|
|
112
|
-
const raw = await fs.readFile(metaPath(sessionId), 'utf8');
|
|
113
|
-
return JSON.parse(raw);
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
export async function updateSessionMetadata(sessionId, updates) {
|
|
120
|
-
const existing = (await readSessionMetadata(sessionId)) ?? { id: sessionId };
|
|
121
|
-
const next = { ...existing, ...updates };
|
|
122
|
-
await fs.writeFile(metaPath(sessionId), JSON.stringify(next, null, 2), 'utf8');
|
|
123
|
-
return next;
|
|
124
|
-
}
|
|
125
|
-
export function createSessionLogWriter(sessionId) {
|
|
126
|
-
const stream = createWriteStream(logPath(sessionId), { flags: 'a' });
|
|
127
|
-
const logLine = (line = '') => {
|
|
128
|
-
stream.write(`${line}\n`);
|
|
129
|
-
};
|
|
130
|
-
const writeChunk = (chunk) => {
|
|
131
|
-
stream.write(chunk);
|
|
132
|
-
return true;
|
|
133
|
-
};
|
|
134
|
-
return { stream, logLine, writeChunk, logPath: logPath(sessionId) };
|
|
135
|
-
}
|
|
136
|
-
export async function listSessionsMetadata() {
|
|
137
|
-
await ensureSessionStorage();
|
|
138
|
-
const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
|
|
139
|
-
const metas = [];
|
|
140
|
-
for (const entry of entries) {
|
|
141
|
-
const meta = await readSessionMetadata(entry);
|
|
142
|
-
if (meta) {
|
|
143
|
-
metas.push(meta);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return metas.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
147
|
-
}
|
|
148
|
-
export function filterSessionsByRange(metas, { hours = 24, includeAll = false, limit = 100 }) {
|
|
149
|
-
const maxLimit = Math.min(limit, MAX_STATUS_LIMIT);
|
|
150
|
-
let filtered = metas;
|
|
151
|
-
if (!includeAll) {
|
|
152
|
-
const cutoff = Date.now() - hours * 60 * 60 * 1000;
|
|
153
|
-
filtered = metas.filter((meta) => new Date(meta.createdAt).getTime() >= cutoff);
|
|
154
|
-
}
|
|
155
|
-
const limited = filtered.slice(0, maxLimit);
|
|
156
|
-
const truncated = filtered.length > maxLimit;
|
|
157
|
-
return { entries: limited, truncated, total: filtered.length };
|
|
158
|
-
}
|
|
159
|
-
export async function readSessionLog(sessionId) {
|
|
160
|
-
try {
|
|
161
|
-
return await fs.readFile(logPath(sessionId), 'utf8');
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return '';
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
export async function deleteSessionsOlderThan({ hours = 24, includeAll = false, } = {}) {
|
|
168
|
-
await ensureSessionStorage();
|
|
169
|
-
const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
|
|
170
|
-
if (!entries.length) {
|
|
171
|
-
return { deleted: 0, remaining: 0 };
|
|
172
|
-
}
|
|
173
|
-
const cutoff = includeAll ? Number.NEGATIVE_INFINITY : Date.now() - hours * 60 * 60 * 1000;
|
|
174
|
-
let deleted = 0;
|
|
175
|
-
for (const entry of entries) {
|
|
176
|
-
const dir = sessionDir(entry);
|
|
177
|
-
let createdMs;
|
|
178
|
-
const meta = await readSessionMetadata(entry);
|
|
179
|
-
if (meta?.createdAt) {
|
|
180
|
-
const parsed = Date.parse(meta.createdAt);
|
|
181
|
-
if (!Number.isNaN(parsed)) {
|
|
182
|
-
createdMs = parsed;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (createdMs == null) {
|
|
186
|
-
try {
|
|
187
|
-
const stats = await fs.stat(dir);
|
|
188
|
-
createdMs = stats.birthtimeMs || stats.mtimeMs;
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (includeAll || (createdMs != null && createdMs < cutoff)) {
|
|
195
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
196
|
-
deleted += 1;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const remaining = Math.max(entries.length - deleted, 0);
|
|
200
|
-
return { deleted, remaining };
|
|
201
|
-
}
|
|
202
|
-
export async function wait(ms) {
|
|
203
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
204
|
-
}
|
|
205
|
-
export { ORACLE_HOME, SESSIONS_DIR, MAX_STATUS_LIMIT };
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
let cachedVersion = null;
|
|
5
|
-
export function getCliVersion() {
|
|
6
|
-
if (cachedVersion) {
|
|
7
|
-
return cachedVersion;
|
|
8
|
-
}
|
|
9
|
-
cachedVersion = readVersionFromPackage();
|
|
10
|
-
return cachedVersion;
|
|
11
|
-
}
|
|
12
|
-
function readVersionFromPackage() {
|
|
13
|
-
const modulePath = fileURLToPath(import.meta.url);
|
|
14
|
-
let currentDir = path.dirname(modulePath);
|
|
15
|
-
const filesystemRoot = path.parse(currentDir).root;
|
|
16
|
-
// biome-ignore lint/nursery/noUnnecessaryConditions: deliberate sentinel loop to walk up directories
|
|
17
|
-
while (true) {
|
|
18
|
-
const candidate = path.join(currentDir, 'package.json');
|
|
19
|
-
try {
|
|
20
|
-
const raw = readFileSync(candidate, 'utf8');
|
|
21
|
-
const parsed = JSON.parse(raw);
|
|
22
|
-
const version = typeof parsed.version === 'string' && parsed.version.trim().length > 0
|
|
23
|
-
? parsed.version.trim()
|
|
24
|
-
: '0.0.0';
|
|
25
|
-
return version;
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
const code = error instanceof Error && 'code' in error ? error.code : undefined;
|
|
29
|
-
if (code && code !== 'ENOENT') {
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (currentDir === filesystemRoot) {
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
currentDir = path.dirname(currentDir);
|
|
37
|
-
}
|
|
38
|
-
return '0.0.0';
|
|
39
|
-
}
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { execSync, spawn } from 'node:child_process';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import puppeteer from 'puppeteer-core';
|
|
6
|
-
const DEFAULT_PORT = 9222;
|
|
7
|
-
const DEFAULT_PROFILE_DIR = path.join(os.homedir(), '.cache', 'scraping');
|
|
8
|
-
const DEFAULT_CHROME_BIN = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
9
|
-
function browserURL(port) {
|
|
10
|
-
return `http://localhost:${port}`;
|
|
11
|
-
}
|
|
12
|
-
async function connectBrowser(port) {
|
|
13
|
-
return puppeteer.connect({ browserURL: browserURL(port), defaultViewport: null });
|
|
14
|
-
}
|
|
15
|
-
async function getActivePage(port) {
|
|
16
|
-
const browser = await connectBrowser(port);
|
|
17
|
-
const pages = await browser.pages();
|
|
18
|
-
const page = pages.at(-1);
|
|
19
|
-
if (!page) {
|
|
20
|
-
await browser.disconnect();
|
|
21
|
-
throw new Error('No active tab found');
|
|
22
|
-
}
|
|
23
|
-
return { browser, page };
|
|
24
|
-
}
|
|
25
|
-
const program = new Command();
|
|
26
|
-
program
|
|
27
|
-
.name('browser-tools')
|
|
28
|
-
.description('Minimal Chrome DevTools helpers inspired by Mario Zechner’s “What if you don’t need MCP?” article.')
|
|
29
|
-
.configureHelp({
|
|
30
|
-
sortSubcommands: true,
|
|
31
|
-
})
|
|
32
|
-
.showSuggestionAfterError();
|
|
33
|
-
program
|
|
34
|
-
.command('start')
|
|
35
|
-
.description('Launch Chrome with remote debugging enabled.')
|
|
36
|
-
.option('-p, --port <number>', 'Remote debugging port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
37
|
-
.option('--profile', 'Copy your existing Chrome profile before launching.', false)
|
|
38
|
-
.option('--profile-dir <path>', 'Directory for the temporary Chrome profile.', DEFAULT_PROFILE_DIR)
|
|
39
|
-
.option('--chrome-path <path>', 'Path to the Chrome binary.', DEFAULT_CHROME_BIN)
|
|
40
|
-
.action(async (options) => {
|
|
41
|
-
const { port, profile, profileDir, chromePath } = options;
|
|
42
|
-
try {
|
|
43
|
-
execSync("killall 'Google Chrome'", { stdio: 'ignore' });
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// ignore
|
|
47
|
-
}
|
|
48
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
49
|
-
execSync(`mkdir -p "${profileDir}"`);
|
|
50
|
-
if (profile) {
|
|
51
|
-
const source = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome') + '/';
|
|
52
|
-
execSync(`rsync -a --delete "${source}" "${profileDir}/"`, { stdio: 'ignore' });
|
|
53
|
-
}
|
|
54
|
-
spawn(chromePath, ['--remote-debugging-port=' + port, `--user-data-dir=${profileDir}`, '--no-first-run', '--disable-popup-blocking'], {
|
|
55
|
-
detached: true,
|
|
56
|
-
stdio: 'ignore',
|
|
57
|
-
}).unref();
|
|
58
|
-
let connected = false;
|
|
59
|
-
for (let attempt = 0; attempt < 30; attempt++) {
|
|
60
|
-
try {
|
|
61
|
-
const browser = await connectBrowser(port);
|
|
62
|
-
await browser.disconnect();
|
|
63
|
-
connected = true;
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (!connected) {
|
|
71
|
-
console.error(`✗ Failed to start Chrome on port ${port}`);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
console.log(`✓ Chrome listening on http://localhost:${port}${profile ? ' (with your profile)' : ''}`);
|
|
75
|
-
});
|
|
76
|
-
program
|
|
77
|
-
.command('nav <url>')
|
|
78
|
-
.description('Navigate the current tab or open a new tab.')
|
|
79
|
-
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
80
|
-
.option('--new', 'Open in a new tab instead of reusing the current tab.', false)
|
|
81
|
-
.action(async (url, options) => {
|
|
82
|
-
const port = options.port;
|
|
83
|
-
const browser = await connectBrowser(port);
|
|
84
|
-
try {
|
|
85
|
-
if (options.new) {
|
|
86
|
-
const page = await browser.newPage();
|
|
87
|
-
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
88
|
-
console.log('✓ Opened in new tab:', url);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
const pages = await browser.pages();
|
|
92
|
-
const page = pages.at(-1);
|
|
93
|
-
if (!page) {
|
|
94
|
-
throw new Error('No active tab found');
|
|
95
|
-
}
|
|
96
|
-
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
97
|
-
console.log('✓ Navigated current tab to:', url);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
await browser.disconnect();
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
program
|
|
105
|
-
.command('eval <code...>')
|
|
106
|
-
.description('Evaluate JavaScript in the active page context.')
|
|
107
|
-
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
108
|
-
.action(async (code, options) => {
|
|
109
|
-
const snippet = code.join(' ');
|
|
110
|
-
const port = options.port;
|
|
111
|
-
const { browser, page } = await getActivePage(port);
|
|
112
|
-
try {
|
|
113
|
-
const result = await page.evaluate((body) => {
|
|
114
|
-
const AsyncFunctionConstructor = Object.getPrototypeOf(async function () { }).constructor;
|
|
115
|
-
return new AsyncFunctionConstructor(`return (${body})`)();
|
|
116
|
-
}, snippet);
|
|
117
|
-
if (Array.isArray(result)) {
|
|
118
|
-
result.forEach((entry, index) => {
|
|
119
|
-
if (index > 0)
|
|
120
|
-
console.log('');
|
|
121
|
-
Object.entries(entry).forEach(([key, value]) => console.log(`${key}: ${value}`));
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
else if (typeof result === 'object' && result !== null) {
|
|
125
|
-
Object.entries(result).forEach(([key, value]) => console.log(`${key}: ${value}`));
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
console.log(result);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
await browser.disconnect();
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
program
|
|
136
|
-
.command('screenshot')
|
|
137
|
-
.description('Capture the current viewport and print the temp PNG path.')
|
|
138
|
-
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
139
|
-
.action(async (options) => {
|
|
140
|
-
const port = options.port;
|
|
141
|
-
const { browser, page } = await getActivePage(port);
|
|
142
|
-
try {
|
|
143
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
144
|
-
const filePath = path.join(os.tmpdir(), `screenshot-${timestamp}.png`);
|
|
145
|
-
await page.screenshot({ path: filePath });
|
|
146
|
-
console.log(filePath);
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
await browser.disconnect();
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
program
|
|
153
|
-
.command('pick <message...>')
|
|
154
|
-
.description('Interactive DOM picker that prints metadata for clicked elements.')
|
|
155
|
-
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
156
|
-
.action(async (messageParts, options) => {
|
|
157
|
-
const message = messageParts.join(' ');
|
|
158
|
-
const port = options.port;
|
|
159
|
-
const { browser, page } = await getActivePage(port);
|
|
160
|
-
try {
|
|
161
|
-
await page.evaluate(() => {
|
|
162
|
-
const scope = globalThis;
|
|
163
|
-
if (scope.pickOverlayInjected) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
scope.pickOverlayInjected = true;
|
|
167
|
-
scope.pick = async (prompt) => new Promise((resolve) => {
|
|
168
|
-
const selections = [];
|
|
169
|
-
const selectedElements = new Set();
|
|
170
|
-
const overlay = document.createElement('div');
|
|
171
|
-
overlay.style.cssText =
|
|
172
|
-
'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none';
|
|
173
|
-
const highlight = document.createElement('div');
|
|
174
|
-
highlight.style.cssText =
|
|
175
|
-
'position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.05s ease';
|
|
176
|
-
overlay.appendChild(highlight);
|
|
177
|
-
const banner = document.createElement('div');
|
|
178
|
-
banner.style.cssText =
|
|
179
|
-
'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:#fff;padding:12px 24px;border-radius:8px;font:14px system-ui;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647';
|
|
180
|
-
const updateBanner = () => {
|
|
181
|
-
banner.textContent = `${prompt} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`;
|
|
182
|
-
};
|
|
183
|
-
const cleanup = () => {
|
|
184
|
-
document.removeEventListener('mousemove', onMove, true);
|
|
185
|
-
document.removeEventListener('click', onClick, true);
|
|
186
|
-
document.removeEventListener('keydown', onKey, true);
|
|
187
|
-
overlay.remove();
|
|
188
|
-
banner.remove();
|
|
189
|
-
selectedElements.forEach((el) => (el.style.outline = ''));
|
|
190
|
-
};
|
|
191
|
-
const serialize = (el) => {
|
|
192
|
-
const parents = [];
|
|
193
|
-
let current = el.parentElement;
|
|
194
|
-
while (current && current !== document.body) {
|
|
195
|
-
const id = current.id ? `#${current.id}` : '';
|
|
196
|
-
const cls = current.className ? `.${current.className.trim().split(/\\s+/).join('.')}` : '';
|
|
197
|
-
parents.push(`${current.tagName.toLowerCase()}${id}${cls}`);
|
|
198
|
-
current = current.parentElement;
|
|
199
|
-
}
|
|
200
|
-
return {
|
|
201
|
-
tag: el.tagName.toLowerCase(),
|
|
202
|
-
id: el.id || null,
|
|
203
|
-
class: el.className || null,
|
|
204
|
-
text: el.textContent?.trim()?.slice(0, 200) || null,
|
|
205
|
-
html: el.outerHTML.slice(0, 500),
|
|
206
|
-
parents: parents.join(' > '),
|
|
207
|
-
};
|
|
208
|
-
};
|
|
209
|
-
const onMove = (event) => {
|
|
210
|
-
const node = document.elementFromPoint(event.clientX, event.clientY);
|
|
211
|
-
if (!node || overlay.contains(node) || banner.contains(node))
|
|
212
|
-
return;
|
|
213
|
-
const rect = node.getBoundingClientRect();
|
|
214
|
-
highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px`;
|
|
215
|
-
};
|
|
216
|
-
const onClick = (event) => {
|
|
217
|
-
if (banner.contains(event.target))
|
|
218
|
-
return;
|
|
219
|
-
event.preventDefault();
|
|
220
|
-
event.stopPropagation();
|
|
221
|
-
const node = document.elementFromPoint(event.clientX, event.clientY);
|
|
222
|
-
if (!node || overlay.contains(node) || banner.contains(node))
|
|
223
|
-
return;
|
|
224
|
-
if (event.metaKey || event.ctrlKey) {
|
|
225
|
-
if (!selectedElements.has(node)) {
|
|
226
|
-
selectedElements.add(node);
|
|
227
|
-
node.style.outline = '3px solid #10b981';
|
|
228
|
-
selections.push(serialize(node));
|
|
229
|
-
updateBanner();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
cleanup();
|
|
234
|
-
const info = serialize(node);
|
|
235
|
-
resolve(selections.length > 0 ? selections : info);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
const onKey = (event) => {
|
|
239
|
-
if (event.key === 'Escape') {
|
|
240
|
-
cleanup();
|
|
241
|
-
resolve(null);
|
|
242
|
-
}
|
|
243
|
-
else if (event.key === 'Enter' && selections.length > 0) {
|
|
244
|
-
cleanup();
|
|
245
|
-
resolve(selections);
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
document.addEventListener('mousemove', onMove, true);
|
|
249
|
-
document.addEventListener('click', onClick, true);
|
|
250
|
-
document.addEventListener('keydown', onKey, true);
|
|
251
|
-
document.body.append(overlay, banner);
|
|
252
|
-
updateBanner();
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
const result = await page.evaluate((msg) => {
|
|
256
|
-
const pickFn = window.pick;
|
|
257
|
-
if (!pickFn) {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
return pickFn(msg);
|
|
261
|
-
}, message);
|
|
262
|
-
if (Array.isArray(result)) {
|
|
263
|
-
result.forEach((entry, index) => {
|
|
264
|
-
if (index > 0)
|
|
265
|
-
console.log('');
|
|
266
|
-
Object.entries(entry).forEach(([key, value]) => console.log(`${key}: ${value}`));
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
else if (result && typeof result === 'object') {
|
|
270
|
-
Object.entries(result).forEach(([key, value]) => console.log(`${key}: ${value}`));
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
console.log(result);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
await browser.disconnect();
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
program
|
|
281
|
-
.command('cookies')
|
|
282
|
-
.description('Dump cookies from the active tab as JSON.')
|
|
283
|
-
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
284
|
-
.action(async (options) => {
|
|
285
|
-
const port = options.port;
|
|
286
|
-
const { browser, page } = await getActivePage(port);
|
|
287
|
-
try {
|
|
288
|
-
const cookies = await page.cookies();
|
|
289
|
-
console.log(JSON.stringify(cookies, null, 2));
|
|
290
|
-
}
|
|
291
|
-
finally {
|
|
292
|
-
await browser.disconnect();
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
program.parseAsync(process.argv);
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { mkdir, rm, cp as copyDir } from 'node:fs/promises';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { defaultProfileRoot, expandPath, looksLikePath } from './chromeCookies.js';
|
|
6
|
-
const DIR_EXCLUDES = [
|
|
7
|
-
'Cache',
|
|
8
|
-
'Code Cache',
|
|
9
|
-
'GPUCache',
|
|
10
|
-
'Service Worker',
|
|
11
|
-
'Crashpad',
|
|
12
|
-
'BrowserMetrics*',
|
|
13
|
-
'GrShaderCache',
|
|
14
|
-
'ShaderCache',
|
|
15
|
-
'OptimizationGuide',
|
|
16
|
-
];
|
|
17
|
-
const FILE_EXCLUDES = [
|
|
18
|
-
'SingletonLock',
|
|
19
|
-
'SingletonSocket',
|
|
20
|
-
'SingletonCookie',
|
|
21
|
-
'*.lock',
|
|
22
|
-
'lockfile',
|
|
23
|
-
'Lock',
|
|
24
|
-
'*.tmp',
|
|
25
|
-
'DevToolsActivePort',
|
|
26
|
-
path.join('Default', 'DevToolsActivePort'),
|
|
27
|
-
path.join('Sessions', '*'),
|
|
28
|
-
'Current Session',
|
|
29
|
-
'Current Tabs',
|
|
30
|
-
'Last Session',
|
|
31
|
-
'Last Tabs',
|
|
32
|
-
];
|
|
33
|
-
export async function syncChromeProfile(options) {
|
|
34
|
-
const { targetDir } = options;
|
|
35
|
-
await mkdir(targetDir, { recursive: true });
|
|
36
|
-
const { sourceDir, profileName } = await resolveProfileSource(options.profile, options.explicitPath);
|
|
37
|
-
const logger = options.logger;
|
|
38
|
-
if (!existsSync(sourceDir)) {
|
|
39
|
-
throw new Error(`Chrome profile not found at ${sourceDir}. Log in once in Chrome, then retry.`);
|
|
40
|
-
}
|
|
41
|
-
// Clean any stale DevTools ports/locks in the target before copying.
|
|
42
|
-
await rm(targetDir, { recursive: true, force: true }).catch(() => undefined);
|
|
43
|
-
await mkdir(targetDir, { recursive: true });
|
|
44
|
-
const result = process.platform === 'win32'
|
|
45
|
-
? await copyWithRobocopy(sourceDir, targetDir, logger)
|
|
46
|
-
: await copyWithRsync(sourceDir, targetDir, logger);
|
|
47
|
-
// Remove lock files in the copied profile to avoid "already running" errors.
|
|
48
|
-
await removeLocks(targetDir);
|
|
49
|
-
return {
|
|
50
|
-
source: sourceDir,
|
|
51
|
-
profileName,
|
|
52
|
-
method: result.method,
|
|
53
|
-
status: result.copied ? 'copied' : 'skipped',
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
async function copyWithRsync(sourceDir, targetDir, logger) {
|
|
57
|
-
const rsyncArgs = [
|
|
58
|
-
'-a',
|
|
59
|
-
'--delete',
|
|
60
|
-
...DIR_EXCLUDES.flatMap((entry) => ['--exclude', entry]),
|
|
61
|
-
...FILE_EXCLUDES.flatMap((entry) => ['--exclude', entry]),
|
|
62
|
-
`${sourceDir}/`,
|
|
63
|
-
`${targetDir}/`,
|
|
64
|
-
];
|
|
65
|
-
const attempt = spawnSync('rsync', rsyncArgs, { stdio: 'pipe' });
|
|
66
|
-
if (!attempt.error && (attempt.status ?? 0) === 0) {
|
|
67
|
-
return { copied: true, method: 'rsync' };
|
|
68
|
-
}
|
|
69
|
-
logger?.('rsync unavailable or failed; falling back to Node copy');
|
|
70
|
-
await copyDirWithFilter(sourceDir, targetDir);
|
|
71
|
-
return copyWithNodeFs();
|
|
72
|
-
}
|
|
73
|
-
async function copyWithRobocopy(sourceDir, targetDir, logger) {
|
|
74
|
-
const args = [sourceDir, targetDir, '/MIR', '/NFL', '/NDL', '/NJH', '/NJS', '/NP', '/Z'];
|
|
75
|
-
if (DIR_EXCLUDES.length) {
|
|
76
|
-
args.push('/XD', ...DIR_EXCLUDES);
|
|
77
|
-
}
|
|
78
|
-
if (FILE_EXCLUDES.length) {
|
|
79
|
-
args.push('/XF', ...FILE_EXCLUDES);
|
|
80
|
-
}
|
|
81
|
-
const attempt = spawnSync('robocopy', args, { stdio: 'pipe' });
|
|
82
|
-
const exitCode = attempt.status ?? 0;
|
|
83
|
-
// Robocopy treats 0-7 as success/partial success; >=8 is failure.
|
|
84
|
-
if (!attempt.error && exitCode < 8) {
|
|
85
|
-
return { copied: true, method: 'robocopy' };
|
|
86
|
-
}
|
|
87
|
-
logger?.('robocopy failed; falling back to Node copy');
|
|
88
|
-
await copyDirWithFilter(sourceDir, targetDir);
|
|
89
|
-
return copyWithNodeFs();
|
|
90
|
-
}
|
|
91
|
-
function copyWithNodeFs() {
|
|
92
|
-
return { copied: true, method: 'node' };
|
|
93
|
-
}
|
|
94
|
-
function shouldExclude(relativePath) {
|
|
95
|
-
const normalized = relativePath.replace(/\\/g, '/');
|
|
96
|
-
return DIR_EXCLUDES.some((entry) => normalized === entry || normalized.startsWith(`${entry}/`)) ||
|
|
97
|
-
FILE_EXCLUDES.some((entry) => {
|
|
98
|
-
if (entry.endsWith('*')) {
|
|
99
|
-
return normalized.startsWith(entry.slice(0, -1));
|
|
100
|
-
}
|
|
101
|
-
if (entry.includes('*')) {
|
|
102
|
-
// simple glob support for BrowserMetrics*
|
|
103
|
-
const prefix = entry.replace('*', '');
|
|
104
|
-
return normalized.startsWith(prefix);
|
|
105
|
-
}
|
|
106
|
-
return path.basename(normalized) === entry || normalized.endsWith(`/${entry}`);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
async function removeLocks(targetDir) {
|
|
110
|
-
const lockNames = ['SingletonLock', 'SingletonSocket', 'SingletonCookie', 'DevToolsActivePort'];
|
|
111
|
-
for (const lock of lockNames) {
|
|
112
|
-
await rm(path.join(targetDir, lock), { force: true }).catch(() => undefined);
|
|
113
|
-
await rm(path.join(targetDir, 'Default', lock), { force: true }).catch(() => undefined);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
async function resolveProfileSource(profile, explicitPath) {
|
|
117
|
-
const profileName = profile?.trim() ? profile.trim() : 'Default';
|
|
118
|
-
if (explicitPath?.trim()) {
|
|
119
|
-
const resolved = expandPath(explicitPath.trim());
|
|
120
|
-
if (resolved.toLowerCase().endsWith('cookies')) {
|
|
121
|
-
return { sourceDir: path.dirname(resolved), profileName };
|
|
122
|
-
}
|
|
123
|
-
return { sourceDir: resolved, profileName };
|
|
124
|
-
}
|
|
125
|
-
if (looksLikePath(profileName)) {
|
|
126
|
-
return { sourceDir: expandPath(profileName), profileName };
|
|
127
|
-
}
|
|
128
|
-
const baseRoot = await defaultProfileRoot();
|
|
129
|
-
return { sourceDir: path.join(baseRoot, profileName), profileName };
|
|
130
|
-
}
|
|
131
|
-
async function copyDirWithFilter(sourceDir, targetDir) {
|
|
132
|
-
await copyDir(sourceDir, targetDir, {
|
|
133
|
-
recursive: true,
|
|
134
|
-
filter: async (source) => {
|
|
135
|
-
const rel = path.relative(sourceDir, source);
|
|
136
|
-
if (!rel)
|
|
137
|
-
return true;
|
|
138
|
-
return !shouldExclude(rel);
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
}
|