linkedin-automation-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/.github/workflows/ci.yml +66 -0
- package/.github/workflows/publish.yml +48 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +294 -0
- package/CHANGELOG.md +40 -0
- package/GIT_RELEASE.md +167 -0
- package/LICENSE +21 -0
- package/Makefile +30 -0
- package/NPM_PUBLISHING.md +230 -0
- package/PYEOF +0 -0
- package/README.md +295 -0
- package/TESTING-GUIDE.md +151 -0
- package/cmd/linkedin/main.go +9 -0
- package/dist/agent/action-executor.d.ts +81 -0
- package/dist/agent/action-executor.d.ts.map +1 -0
- package/dist/agent/action-executor.js +170 -0
- package/dist/agent/action-executor.js.map +1 -0
- package/dist/agent/action-executor.test.d.ts +2 -0
- package/dist/agent/action-executor.test.d.ts.map +1 -0
- package/dist/agent/action-executor.test.js +366 -0
- package/dist/agent/action-executor.test.js.map +1 -0
- package/dist/agent/claude-client.d.ts +74 -0
- package/dist/agent/claude-client.d.ts.map +1 -0
- package/dist/agent/claude-client.js +314 -0
- package/dist/agent/claude-client.js.map +1 -0
- package/dist/agent/claude-client.test.d.ts +2 -0
- package/dist/agent/claude-client.test.d.ts.map +1 -0
- package/dist/agent/claude-client.test.js +590 -0
- package/dist/agent/claude-client.test.js.map +1 -0
- package/dist/agent/dom-extractor.d.ts +50 -0
- package/dist/agent/dom-extractor.d.ts.map +1 -0
- package/dist/agent/dom-extractor.js +374 -0
- package/dist/agent/dom-extractor.js.map +1 -0
- package/dist/agent/dom-extractor.test.d.ts +7 -0
- package/dist/agent/dom-extractor.test.d.ts.map +1 -0
- package/dist/agent/dom-extractor.test.js +504 -0
- package/dist/agent/dom-extractor.test.js.map +1 -0
- package/dist/agent/extension-client.d.ts +75 -0
- package/dist/agent/extension-client.d.ts.map +1 -0
- package/dist/agent/extension-client.js +245 -0
- package/dist/agent/extension-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +16 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/page-agent.d.ts +76 -0
- package/dist/agent/page-agent.d.ts.map +1 -0
- package/dist/agent/page-agent.js +236 -0
- package/dist/agent/page-agent.js.map +1 -0
- package/dist/agent/types.d.ts +236 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +37 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.d.ts.map +1 -0
- package/dist/cli/agent-commands.js +250 -0
- package/dist/cli/agent-commands.js.map +1 -0
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +288 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/company.d.ts +3 -0
- package/dist/cli/company.d.ts.map +1 -0
- package/dist/cli/company.js +55 -0
- package/dist/cli/company.js.map +1 -0
- package/dist/cli/connection.d.ts +3 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +79 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +268 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -0
- package/dist/cli/profile.d.ts.map +1 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/profile.test.d.ts +2 -0
- package/dist/cli/profile.test.d.ts.map +1 -0
- package/dist/cli/profile.test.js +15 -0
- package/dist/cli/profile.test.js.map +1 -0
- package/dist/cli/reply.d.ts +3 -0
- package/dist/cli/reply.d.ts.map +1 -0
- package/dist/cli/reply.js +129 -0
- package/dist/cli/reply.js.map +1 -0
- package/dist/core/audit.d.ts +17 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +121 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/audit.test.d.ts +2 -0
- package/dist/core/audit.test.d.ts.map +1 -0
- package/dist/core/audit.test.js +142 -0
- package/dist/core/audit.test.js.map +1 -0
- package/dist/core/browser-cookies.d.ts +19 -0
- package/dist/core/browser-cookies.d.ts.map +1 -0
- package/dist/core/browser-cookies.js +181 -0
- package/dist/core/browser-cookies.js.map +1 -0
- package/dist/core/browser.d.ts +50 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +318 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +103 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.d.ts +2 -0
- package/dist/core/config.test.d.ts.map +1 -0
- package/dist/core/config.test.js +111 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/storage.d.ts +19 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +124 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/storage.test.d.ts +2 -0
- package/dist/core/storage.test.d.ts.map +1 -0
- package/dist/core/storage.test.js +142 -0
- package/dist/core/storage.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin/auth.d.ts +22 -0
- package/dist/linkedin/auth.d.ts.map +1 -0
- package/dist/linkedin/auth.js +167 -0
- package/dist/linkedin/auth.js.map +1 -0
- package/dist/linkedin/company-extractor.d.ts +36 -0
- package/dist/linkedin/company-extractor.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.js +211 -0
- package/dist/linkedin/company-extractor.js.map +1 -0
- package/dist/linkedin/company-extractor.test.d.ts +2 -0
- package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.test.js +52 -0
- package/dist/linkedin/company-extractor.test.js.map +1 -0
- package/dist/linkedin/connector.d.ts +45 -0
- package/dist/linkedin/connector.d.ts.map +1 -0
- package/dist/linkedin/connector.js +245 -0
- package/dist/linkedin/connector.js.map +1 -0
- package/dist/linkedin/message-sender.d.ts +32 -0
- package/dist/linkedin/message-sender.d.ts.map +1 -0
- package/dist/linkedin/message-sender.js +112 -0
- package/dist/linkedin/message-sender.js.map +1 -0
- package/dist/linkedin/messages.d.ts +78 -0
- package/dist/linkedin/messages.d.ts.map +1 -0
- package/dist/linkedin/messages.js +745 -0
- package/dist/linkedin/messages.js.map +1 -0
- package/dist/linkedin/profile.d.ts +37 -0
- package/dist/linkedin/profile.d.ts.map +1 -0
- package/dist/linkedin/profile.js +268 -0
- package/dist/linkedin/profile.js.map +1 -0
- package/dist/linkedin/profile.test.d.ts +2 -0
- package/dist/linkedin/profile.test.d.ts.map +1 -0
- package/dist/linkedin/profile.test.js +68 -0
- package/dist/linkedin/profile.test.js.map +1 -0
- package/dist/linkedin/reply.d.ts +21 -0
- package/dist/linkedin/reply.d.ts.map +1 -0
- package/dist/linkedin/reply.js +76 -0
- package/dist/linkedin/reply.js.map +1 -0
- package/dist/linkedin/selector-engine.d.ts +69 -0
- package/dist/linkedin/selector-engine.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.js +339 -0
- package/dist/linkedin/selector-engine.js.map +1 -0
- package/dist/linkedin/selector-engine.test.d.ts +2 -0
- package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.test.js +135 -0
- package/dist/linkedin/selector-engine.test.js.map +1 -0
- package/dist/linkedin/selectors.d.ts +65 -0
- package/dist/linkedin/selectors.d.ts.map +1 -0
- package/dist/linkedin/selectors.js +261 -0
- package/dist/linkedin/selectors.js.map +1 -0
- package/dist/templates/engine.d.ts +37 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/engine.js +215 -0
- package/dist/templates/engine.js.map +1 -0
- package/dist/templates/engine.test.d.ts +2 -0
- package/dist/templates/engine.test.d.ts.map +1 -0
- package/dist/templates/engine.test.js +212 -0
- package/dist/templates/engine.test.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/index.test.js +90 -0
- package/dist/types/index.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +68 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +22 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +57 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +18 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +49 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/connection-command.md +52 -0
- package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
- package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
- package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
- package/docs/plans/2026-02-28-messages-show-design.md +243 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
- package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
- package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
- package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
- package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
- package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
- package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
- package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
- package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
- package/eslint.config.mjs +58 -0
- package/go.mod +9 -0
- package/go.sum +10 -0
- package/import-cookies.js +376 -0
- package/internal/cmd/actions.go +123 -0
- package/internal/cmd/auth.go +108 -0
- package/internal/cmd/connect.go +42 -0
- package/internal/cmd/message.go +44 -0
- package/internal/cmd/people.go +454 -0
- package/internal/cmd/profiles.go +121 -0
- package/internal/cmd/root.go +89 -0
- package/internal/cmd/sequence.go +192 -0
- package/internal/config/config.go +187 -0
- package/internal/config/config_test.go +121 -0
- package/internal/config/profile.go +65 -0
- package/internal/linkedin/navigator.go +195 -0
- package/internal/linkedin/selectors.go +39 -0
- package/internal/linkedin/validator.go +69 -0
- package/internal/pinchtab/client.go +183 -0
- package/internal/pinchtab/client_test.go +67 -0
- package/internal/pinchtab/types.go +50 -0
- package/internal/ratelimit/limiter.go +115 -0
- package/internal/ratelimit/limits.go +32 -0
- package/package.json +67 -0
- package/release.sh +66 -0
- package/scripts/debug-linkedin.js +156 -0
- package/scripts/debug-login.js +193 -0
- package/scripts/extract-from-edge.js +96 -0
- package/scripts/import-cookies.js +101 -0
- package/scripts/poc-show-data.js +205 -0
- package/scripts/proof-of-access.js +87 -0
- package/scripts/prove-connection.js +110 -0
- package/scripts/show-linkedin-data.js +173 -0
- package/src/agent/action-executor.test.ts +464 -0
- package/src/agent/action-executor.ts +203 -0
- package/src/agent/claude-client.test.ts +707 -0
- package/src/agent/claude-client.ts +422 -0
- package/src/agent/dom-extractor.test.ts +574 -0
- package/src/agent/dom-extractor.ts +437 -0
- package/src/agent/extension-client.ts +306 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/page-agent.ts +292 -0
- package/src/agent/types.ts +288 -0
- package/src/cli/agent-commands.ts +274 -0
- package/src/cli/auth.ts +343 -0
- package/src/cli/company.ts +66 -0
- package/src/cli/connection.ts +89 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/messages.ts +338 -0
- package/src/cli/profile.test.ts +14 -0
- package/src/cli/profile.ts +95 -0
- package/src/cli/reply.ts +110 -0
- package/src/core/audit.test.ts +134 -0
- package/src/core/audit.ts +98 -0
- package/src/core/browser-cookies.ts +203 -0
- package/src/core/browser.ts +304 -0
- package/src/core/config.test.ts +90 -0
- package/src/core/config.ts +81 -0
- package/src/core/storage.test.ts +129 -0
- package/src/core/storage.ts +100 -0
- package/src/index.ts +70 -0
- package/src/linkedin/auth.ts +218 -0
- package/src/linkedin/company-extractor.test.ts +58 -0
- package/src/linkedin/company-extractor.ts +222 -0
- package/src/linkedin/connector.ts +336 -0
- package/src/linkedin/message-sender.ts +141 -0
- package/src/linkedin/messages.ts +894 -0
- package/src/linkedin/profile.test.ts +79 -0
- package/src/linkedin/profile.ts +314 -0
- package/src/linkedin/reply.ts +96 -0
- package/src/linkedin/selector-engine.test.ts +167 -0
- package/src/linkedin/selector-engine.ts +393 -0
- package/src/linkedin/selectors.ts +268 -0
- package/src/templates/defaults/followup.txt +14 -0
- package/src/templates/defaults/meeting.txt +16 -0
- package/src/templates/defaults/welcome.txt +14 -0
- package/src/templates/engine.test.ts +228 -0
- package/src/templates/engine.ts +208 -0
- package/src/templates/index.ts +1 -0
- package/src/types/index.test.ts +94 -0
- package/src/types/index.ts +143 -0
- package/src/types/sql.js.d.ts +23 -0
- package/src/utils/paths.ts +33 -0
- package/src/utils/rate-limiter.ts +75 -0
- package/src/utils/retry.ts +78 -0
- package/test-cli.sh +85 -0
- package/test-real-data.sh +97 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface AuditEntry {
|
|
6
|
+
timestamp: string;
|
|
7
|
+
action: string;
|
|
8
|
+
details: Record<string, unknown>;
|
|
9
|
+
success: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class AuditLogger {
|
|
13
|
+
private logPath: string;
|
|
14
|
+
private consoleEnabled: boolean;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
const dataDir = path.join(os.homedir(), '.linkedin-cli');
|
|
18
|
+
this.logPath = path.join(dataDir, 'audit.log');
|
|
19
|
+
this.consoleEnabled = process.env.DEBUG === 'true';
|
|
20
|
+
this.ensureLogDirectory();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private ensureLogDirectory(): void {
|
|
24
|
+
const dir = path.dirname(this.logPath);
|
|
25
|
+
if (!fs.existsSync(dir)) {
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
log(action: string, details: Record<string, unknown> = {}, success: boolean = true): void {
|
|
31
|
+
const entry: AuditEntry = {
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
action,
|
|
34
|
+
details,
|
|
35
|
+
success,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const logLine = JSON.stringify(entry) + '\n';
|
|
40
|
+
fs.appendFileSync(this.logPath, logLine);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn('Failed to write audit log:', error);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.consoleEnabled) {
|
|
46
|
+
const status = success ? 'OK' : 'FAIL';
|
|
47
|
+
console.log('[AUDIT] ' + status + ' ' + action, details);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
readRecent(count: number = 100): AuditEntry[] {
|
|
52
|
+
try {
|
|
53
|
+
if (!fs.existsSync(this.logPath)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const content = fs.readFileSync(this.logPath, 'utf-8');
|
|
58
|
+
const lines = content
|
|
59
|
+
.trim()
|
|
60
|
+
.split('\n')
|
|
61
|
+
.filter((line) => line.length > 0);
|
|
62
|
+
|
|
63
|
+
const entries: AuditEntry[] = [];
|
|
64
|
+
for (let i = Math.max(0, lines.length - count); i < lines.length; i++) {
|
|
65
|
+
try {
|
|
66
|
+
const entry = JSON.parse(lines[i]) as AuditEntry;
|
|
67
|
+
entries.push(entry);
|
|
68
|
+
} catch {
|
|
69
|
+
// Skip invalid lines
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return entries;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Failed to read audit log:', error);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
clear(): void {
|
|
81
|
+
try {
|
|
82
|
+
if (fs.existsSync(this.logPath)) {
|
|
83
|
+
fs.writeFileSync(this.logPath, '');
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to clear audit log:', error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let auditLogger: AuditLogger | null = null;
|
|
92
|
+
|
|
93
|
+
export function getAuditLogger(): AuditLogger {
|
|
94
|
+
if (!auditLogger) {
|
|
95
|
+
auditLogger = new AuditLogger();
|
|
96
|
+
}
|
|
97
|
+
return auditLogger;
|
|
98
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import initSqlJs, { Database } from 'sql.js';
|
|
5
|
+
|
|
6
|
+
export interface BrowserCookie {
|
|
7
|
+
name: string;
|
|
8
|
+
value: string;
|
|
9
|
+
domain: string;
|
|
10
|
+
path: string;
|
|
11
|
+
expires?: number;
|
|
12
|
+
httpOnly?: boolean;
|
|
13
|
+
secure?: boolean;
|
|
14
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BrowserProfile {
|
|
18
|
+
name: string;
|
|
19
|
+
cookiePath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BROWSER_PROFILES: Record<string, BrowserProfile[]> = {
|
|
23
|
+
chrome: [
|
|
24
|
+
{
|
|
25
|
+
name: 'Chrome Default',
|
|
26
|
+
cookiePath: path.join(
|
|
27
|
+
os.homedir(),
|
|
28
|
+
'Library/Application Support/Google/Chrome/Default/Cookies'
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Chrome Profile 1',
|
|
33
|
+
cookiePath: path.join(
|
|
34
|
+
os.homedir(),
|
|
35
|
+
'Library/Application Support/Google/Chrome/Profile 1/Cookies'
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
edge: [
|
|
40
|
+
{
|
|
41
|
+
name: 'Edge Default',
|
|
42
|
+
cookiePath: path.join(
|
|
43
|
+
os.homedir(),
|
|
44
|
+
'Library/Application Support/Microsoft Edge/Default/Cookies'
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Edge Profile 1',
|
|
49
|
+
cookiePath: path.join(
|
|
50
|
+
os.homedir(),
|
|
51
|
+
'Library/Application Support/Microsoft Edge/Profile 1/Cookies'
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
arc: [
|
|
56
|
+
{
|
|
57
|
+
name: 'Arc Default',
|
|
58
|
+
cookiePath: path.join(
|
|
59
|
+
os.homedir(),
|
|
60
|
+
'Library/Application Support/Arc/User Data/Default/Cookies'
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
brave: [
|
|
65
|
+
{
|
|
66
|
+
name: 'Brave Default',
|
|
67
|
+
cookiePath: path.join(
|
|
68
|
+
os.homedir(),
|
|
69
|
+
'Library/Application Support/BraveSoftware/Brave-Browser/Default/Cookies'
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Copy cookie database to temp location (Chrome locks the original)
|
|
77
|
+
*/
|
|
78
|
+
function copyCookieDatabase(sourcePath: string): string {
|
|
79
|
+
const tempPath = path.join(os.tmpdir(), `linkedin-cli-cookies-${Date.now()}.db`);
|
|
80
|
+
|
|
81
|
+
// Copy the file
|
|
82
|
+
fs.copyFileSync(sourcePath, tempPath);
|
|
83
|
+
|
|
84
|
+
return tempPath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract LinkedIn cookies from browser's cookie database
|
|
89
|
+
*/
|
|
90
|
+
export async function extractCookiesFromBrowser(browserName: string): Promise<BrowserCookie[]> {
|
|
91
|
+
const profiles = BROWSER_PROFILES[browserName.toLowerCase()];
|
|
92
|
+
if (!profiles) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Unsupported browser: ${browserName}. Supported: ${Object.keys(BROWSER_PROFILES).join(', ')}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Find existing cookie database
|
|
99
|
+
let cookiePath: string | null = null;
|
|
100
|
+
let profileName: string | null = null;
|
|
101
|
+
|
|
102
|
+
for (const profile of profiles) {
|
|
103
|
+
if (fs.existsSync(profile.cookiePath)) {
|
|
104
|
+
cookiePath = profile.cookiePath;
|
|
105
|
+
profileName = profile.name;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!cookiePath) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Cookie database not found for ${browserName}. Make sure the browser is installed and you're logged into LinkedIn.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`Found ${browserName} profile: ${profileName}`);
|
|
117
|
+
|
|
118
|
+
// Copy to temp location (original is locked by browser)
|
|
119
|
+
const tempPath = copyCookieDatabase(cookiePath);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Initialize sql.js
|
|
123
|
+
const SQL = await initSqlJs();
|
|
124
|
+
|
|
125
|
+
// Open the copied database
|
|
126
|
+
const fileBuffer = fs.readFileSync(tempPath);
|
|
127
|
+
const db: Database = new SQL.Database(fileBuffer);
|
|
128
|
+
|
|
129
|
+
// Check schema - older Chrome versions don't have same_site
|
|
130
|
+
const tableInfo = db.exec('PRAGMA table_info(cookies)');
|
|
131
|
+
const hasSameSite =
|
|
132
|
+
tableInfo.length > 0 && tableInfo[0].values.some((row: any[]) => row[1] === 'same_site');
|
|
133
|
+
|
|
134
|
+
// Query LinkedIn cookies (adapt to schema)
|
|
135
|
+
const query = hasSameSite
|
|
136
|
+
? `SELECT name, value, host_key, path, expires_utc, is_secure, is_httponly, same_site FROM cookies WHERE host_key LIKE '%linkedin.com%'`
|
|
137
|
+
: `SELECT name, value, host_key, path, expires_utc, is_secure, is_httponly FROM cookies WHERE host_key LIKE '%linkedin.com%'`;
|
|
138
|
+
|
|
139
|
+
const rows = db.exec(query);
|
|
140
|
+
|
|
141
|
+
db.close();
|
|
142
|
+
|
|
143
|
+
if (rows.length === 0 || rows[0].values.length === 0) {
|
|
144
|
+
db.close();
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const columns = rows[0].columns;
|
|
149
|
+
const values = rows[0].values;
|
|
150
|
+
|
|
151
|
+
return values.map((row: any[]) => {
|
|
152
|
+
const data: any = {};
|
|
153
|
+
columns.forEach((col: string, i: number) => {
|
|
154
|
+
data[col] = row[i];
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: data.name as string,
|
|
159
|
+
value: data.value as string,
|
|
160
|
+
domain: (data.host_key as string).startsWith('.') ? data.host_key : `.${data.host_key}`,
|
|
161
|
+
path: data.path as string,
|
|
162
|
+
expires:
|
|
163
|
+
(data.expires_utc as number) > 0
|
|
164
|
+
? Math.floor((data.expires_utc as number) / 1000000 - 11644473600)
|
|
165
|
+
: undefined,
|
|
166
|
+
httpOnly: (data.is_httponly as number) === 1,
|
|
167
|
+
secure: (data.is_secure as number) === 1,
|
|
168
|
+
sameSite:
|
|
169
|
+
data.same_site !== undefined
|
|
170
|
+
? sameSiteFromValue(data.same_site as number)
|
|
171
|
+
: ('Lax' as const),
|
|
172
|
+
} as BrowserCookie;
|
|
173
|
+
});
|
|
174
|
+
} finally {
|
|
175
|
+
// Clean up temp file
|
|
176
|
+
if (fs.existsSync(tempPath)) {
|
|
177
|
+
fs.unlinkSync(tempPath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sameSiteFromValue(value: number): 'Strict' | 'Lax' | 'None' {
|
|
183
|
+
switch (value) {
|
|
184
|
+
case 1:
|
|
185
|
+
return 'Lax';
|
|
186
|
+
case 2:
|
|
187
|
+
return 'Strict';
|
|
188
|
+
case 3:
|
|
189
|
+
return 'None';
|
|
190
|
+
default:
|
|
191
|
+
return 'Lax';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract LinkedIn cookies and return as Playwright-compatible format
|
|
197
|
+
*/
|
|
198
|
+
export async function getLinkedinCookies(browserName: string): Promise<BrowserCookie[]> {
|
|
199
|
+
const cookies = await extractCookiesFromBrowser(browserName);
|
|
200
|
+
|
|
201
|
+
// Get all LinkedIn cookies - we need more than just auth cookies
|
|
202
|
+
return cookies.filter(() => true);
|
|
203
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { chromium, Browser, BrowserContext, Page, type LaunchOptions } from 'playwright';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getConfig } from './config';
|
|
4
|
+
import type { Session } from '../types';
|
|
5
|
+
|
|
6
|
+
export interface BrowserOptions {
|
|
7
|
+
headless?: boolean;
|
|
8
|
+
slowMo?: number;
|
|
9
|
+
viewport?: { width: number; height: number };
|
|
10
|
+
debug?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class BrowserController {
|
|
14
|
+
private browser: Browser | null = null;
|
|
15
|
+
private context: BrowserContext | null = null;
|
|
16
|
+
private page: Page | null = null;
|
|
17
|
+
private options: BrowserOptions;
|
|
18
|
+
private debugDir: string;
|
|
19
|
+
private isUsingExistingBrowser: boolean = false;
|
|
20
|
+
|
|
21
|
+
constructor(options: BrowserOptions = {}) {
|
|
22
|
+
const config = getConfig().get();
|
|
23
|
+
this.options = {
|
|
24
|
+
headless: options.headless ?? config.headless,
|
|
25
|
+
slowMo: options.slowMo ?? 100,
|
|
26
|
+
viewport: options.viewport ?? { width: 1280, height: 720 },
|
|
27
|
+
debug: options.debug ?? false,
|
|
28
|
+
};
|
|
29
|
+
this.debugDir = `${process.env.HOME || process.env.USERPROFILE || '.'}/.linkedin-cli/debug`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async launch(): Promise<Page> {
|
|
33
|
+
const spinner = ora('Launching browser...').start();
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Try to connect to existing Chrome/Edge via CDP first
|
|
37
|
+
const cdpBrowser = await this.connectToExistingBrowser();
|
|
38
|
+
if (cdpBrowser) {
|
|
39
|
+
spinner.succeed('Connected to existing browser');
|
|
40
|
+
return this.page!;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fall back to launching Chromium
|
|
44
|
+
const launchOptions: LaunchOptions = {
|
|
45
|
+
headless: this.options.headless,
|
|
46
|
+
slowMo: this.options.slowMo,
|
|
47
|
+
args: [
|
|
48
|
+
'--disable-blink-features=AutomationControlled',
|
|
49
|
+
'--disable-web-security',
|
|
50
|
+
'--disable-features=IsolateOrigins,site-per-process',
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.browser = await chromium.launch(launchOptions);
|
|
55
|
+
|
|
56
|
+
this.context = await this.browser.newContext({
|
|
57
|
+
viewport: this.options.viewport,
|
|
58
|
+
userAgent:
|
|
59
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
60
|
+
extraHTTPHeaders: {
|
|
61
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Add stealth script to hide Playwright detection
|
|
66
|
+
await this.context.addInitScript(`
|
|
67
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
68
|
+
window.chrome = { runtime: {} };
|
|
69
|
+
const originalQuery = window.navigator.permissions.query;
|
|
70
|
+
window.navigator.permissions.query = (parameters) =>
|
|
71
|
+
parameters.name === 'notifications'
|
|
72
|
+
? Promise.resolve({ state: Notification.permission })
|
|
73
|
+
: originalQuery(parameters);
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
this.page = await this.context.newPage();
|
|
77
|
+
spinner.succeed('Browser launched');
|
|
78
|
+
return this.page;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
spinner.fail('Failed to launch browser');
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async restoreSession(session: Session): Promise<boolean> {
|
|
86
|
+
if (!this.context) throw new Error('Browser not launched');
|
|
87
|
+
try {
|
|
88
|
+
if (session.cookies?.length > 0) {
|
|
89
|
+
await this.context.addCookies(session.cookies);
|
|
90
|
+
}
|
|
91
|
+
await this.page?.evaluate((data) => {
|
|
92
|
+
Object.entries(data).forEach(([key, value]) =>
|
|
93
|
+
window.localStorage.setItem(key, value as string)
|
|
94
|
+
);
|
|
95
|
+
}, session.localStorage || {});
|
|
96
|
+
return true;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Failed to restore session:', error);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async saveSession(): Promise<Session | null> {
|
|
104
|
+
if (!this.context || !this.page) return null;
|
|
105
|
+
try {
|
|
106
|
+
const cookies = await this.context.cookies();
|
|
107
|
+
const localStorage = await this.page.evaluate(() => {
|
|
108
|
+
const data: Record<string, string> = {};
|
|
109
|
+
const storage = window.localStorage;
|
|
110
|
+
for (let i = 0; i < storage.length; i++) {
|
|
111
|
+
const key = storage.key(i);
|
|
112
|
+
if (key) data[key] = storage.getItem(key) || '';
|
|
113
|
+
}
|
|
114
|
+
return data;
|
|
115
|
+
});
|
|
116
|
+
return { cookies, localStorage, timestamp: new Date() };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Failed to save session:', error);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async close(): Promise<void> {
|
|
124
|
+
if (this.browser) {
|
|
125
|
+
if (this.isUsingExistingBrowser) {
|
|
126
|
+
// Don't close the browser - just disconnect from CDP
|
|
127
|
+
// The user's browser stays open with all cookies intact
|
|
128
|
+
console.log('Disconnecting from existing browser (keeping your session)');
|
|
129
|
+
this.browser = null;
|
|
130
|
+
this.context = null;
|
|
131
|
+
this.page = null;
|
|
132
|
+
} else {
|
|
133
|
+
// Close the browser we launched
|
|
134
|
+
await this.browser.close();
|
|
135
|
+
this.browser = null;
|
|
136
|
+
this.context = null;
|
|
137
|
+
this.page = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getPage(): Page | null {
|
|
143
|
+
return this.page;
|
|
144
|
+
}
|
|
145
|
+
isLaunched(): boolean {
|
|
146
|
+
return this.browser !== null && this.page !== null;
|
|
147
|
+
}
|
|
148
|
+
private async ensureDebugDir(): Promise<void> {
|
|
149
|
+
const fs = await import('fs/promises');
|
|
150
|
+
await fs.mkdir(this.debugDir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async takeScreenshot(stepName: string): Promise<void> {
|
|
154
|
+
if (!this.options.debug || !this.page) return;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await this.ensureDebugDir();
|
|
158
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
159
|
+
const filename = `${this.debugDir}/${timestamp}-${stepName}.png`;
|
|
160
|
+
await this.page.screenshot({ path: filename, fullPage: true });
|
|
161
|
+
console.log(`[DEBUG] Screenshot saved: ${filename}`);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('[DEBUG] Failed to save screenshot:', error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async saveHtmlSnapshot(stepName: string): Promise<void> {
|
|
168
|
+
if (!this.options.debug || !this.page) return;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await this.ensureDebugDir();
|
|
172
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
173
|
+
const filename = `${this.debugDir}/${timestamp}-${stepName}.html`;
|
|
174
|
+
const html = await this.page.content();
|
|
175
|
+
const fs = await import('fs/promises');
|
|
176
|
+
await fs.writeFile(filename, html, 'utf-8');
|
|
177
|
+
console.log(`[DEBUG] HTML snapshot saved: ${filename}`);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('[DEBUG] Failed to save HTML snapshot:', error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
debugLog(message: string): void {
|
|
184
|
+
if (this.options.debug) {
|
|
185
|
+
console.log(`[DEBUG] ${message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Try to connect to an existing Chrome/Edge browser via CDP
|
|
191
|
+
* Returns true if connected to existing browser with LinkedIn session
|
|
192
|
+
*/
|
|
193
|
+
private async connectToExistingBrowser(): Promise<boolean> {
|
|
194
|
+
try {
|
|
195
|
+
// Try common CDP ports for Chrome and Edge
|
|
196
|
+
const ports = [9222, 9223, 9224, 9225];
|
|
197
|
+
|
|
198
|
+
for (const port of ports) {
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(`http://localhost:${port}/json/version`);
|
|
201
|
+
if (response.ok) {
|
|
202
|
+
const data = await response.json();
|
|
203
|
+
console.log(`Found browser at port ${port}: ${data.Browser}`);
|
|
204
|
+
|
|
205
|
+
// Connect via CDP
|
|
206
|
+
this.browser = await chromium.connectOverCDP(`http://localhost:${port}`);
|
|
207
|
+
|
|
208
|
+
// Get the default context (first one is the user's actual browser session)
|
|
209
|
+
const contexts = this.browser.contexts();
|
|
210
|
+
this.context = contexts.length > 0 ? contexts[0] : await this.browser.newContext();
|
|
211
|
+
|
|
212
|
+
// Wait a moment for pages to be available
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
214
|
+
|
|
215
|
+
// Find a page/tab that's already on LinkedIn
|
|
216
|
+
const pages = this.context.pages();
|
|
217
|
+
console.log(`Found ${pages.length} pages in browser context`);
|
|
218
|
+
|
|
219
|
+
if (pages.length > 0) {
|
|
220
|
+
// Check if any tab is on LinkedIn - use the first LinkedIn tab we find
|
|
221
|
+
// Support both linkedin.com and linkedin.cn (China's LinkedIn)
|
|
222
|
+
for (const pg of pages) {
|
|
223
|
+
try {
|
|
224
|
+
const url = pg.url();
|
|
225
|
+
console.log(` Page URL: ${url}`);
|
|
226
|
+
if (url.includes('linkedin.com') || url.includes('linkedin.cn')) {
|
|
227
|
+
console.log(`Found LinkedIn tab: ${url}`);
|
|
228
|
+
this.page = pg;
|
|
229
|
+
this.isUsingExistingBrowser = true;
|
|
230
|
+
return true; // Connected to existing LinkedIn session
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
// Skip pages that can't be accessed
|
|
234
|
+
console.log(` Page access error: ${e}`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// No LinkedIn tab found, create a new tab in the existing context
|
|
241
|
+
console.log('Creating new tab in existing browser session...');
|
|
242
|
+
this.page = await this.context.newPage();
|
|
243
|
+
this.isUsingExistingBrowser = true;
|
|
244
|
+
|
|
245
|
+
return true; // Connected to browser (but may need to navigate to LinkedIn)
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// Port not available, try next
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.log('Could not connect to browser via CDP:', error);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if connected to existing browser via CDP
|
|
261
|
+
*/
|
|
262
|
+
isUsingCDP(): boolean {
|
|
263
|
+
return this.browser !== null && this.page !== null && this.context !== null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if the current page is on linkedin.cn (which has limited functionality)
|
|
268
|
+
* Returns true if on linkedin.cn, false if on linkedin.com or not on LinkedIn
|
|
269
|
+
*/
|
|
270
|
+
isOnLinkedInCN(): boolean {
|
|
271
|
+
if (!this.page) return false;
|
|
272
|
+
try {
|
|
273
|
+
const currentUrl = this.page.url();
|
|
274
|
+
return currentUrl.includes('linkedin.cn');
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Navigate to LinkedIn in the current page
|
|
282
|
+
*/
|
|
283
|
+
async ensureLinkedIn(): Promise<void> {
|
|
284
|
+
if (!this.page) throw new Error('Browser not launched');
|
|
285
|
+
|
|
286
|
+
const currentUrl = this.page.url();
|
|
287
|
+
if (currentUrl.includes('linkedin.com') || currentUrl.includes('linkedin.cn')) {
|
|
288
|
+
console.log('Already on LinkedIn:', currentUrl);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log('Navigating to LinkedIn...');
|
|
293
|
+
await this.page.goto('https://www.linkedin.com/feed', {
|
|
294
|
+
waitUntil: 'domcontentloaded',
|
|
295
|
+
timeout: 30000,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function createBrowser(options?: BrowserOptions): Promise<BrowserController> {
|
|
301
|
+
const controller = new BrowserController(options);
|
|
302
|
+
await controller.launch();
|
|
303
|
+
return controller;
|
|
304
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
import { ConfigManager, getConfig } from './config';
|
|
7
|
+
|
|
8
|
+
describe('ConfigManager', () => {
|
|
9
|
+
let configManager: ConfigManager;
|
|
10
|
+
const configPath = path.join(os.homedir(), '.linkedin-cli', 'config.json');
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Clean up before test
|
|
14
|
+
if (fs.existsSync(configPath)) {
|
|
15
|
+
fs.rmSync(configPath, { force: true });
|
|
16
|
+
}
|
|
17
|
+
configManager = new ConfigManager();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Clean up after test
|
|
22
|
+
if (fs.existsSync(configPath)) {
|
|
23
|
+
fs.rmSync(configPath, { force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('initialization', () => {
|
|
28
|
+
it('should load with default values', () => {
|
|
29
|
+
const config = configManager.get();
|
|
30
|
+
expect(config.headless).toBe(true);
|
|
31
|
+
expect(config.rateLimit).toBe(5000);
|
|
32
|
+
expect(config.sessionTimeout).toBe(86400000);
|
|
33
|
+
expect(config.dryRun).toBe(false);
|
|
34
|
+
expect(config.dataDir).toContain('.linkedin-cli');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('get', () => {
|
|
39
|
+
it('should return a copy of config', () => {
|
|
40
|
+
const config1 = configManager.get();
|
|
41
|
+
const config2 = configManager.get();
|
|
42
|
+
|
|
43
|
+
// Should be equal but not the same reference
|
|
44
|
+
expect(config1).toEqual(config2);
|
|
45
|
+
expect(config1).not.toBe(config2);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getValue', () => {
|
|
50
|
+
it('should return specific config value', () => {
|
|
51
|
+
expect(configManager.getValue('headless')).toBe(true);
|
|
52
|
+
expect(configManager.getValue('rateLimit')).toBe(5000);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('set', () => {
|
|
57
|
+
it('should set a single config value', () => {
|
|
58
|
+
configManager.set('headless', false);
|
|
59
|
+
expect(configManager.getValue('headless')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should persist to file', () => {
|
|
63
|
+
configManager.set('headless', false);
|
|
64
|
+
|
|
65
|
+
// Create new instance to verify persistence
|
|
66
|
+
const newManager = new ConfigManager();
|
|
67
|
+
expect(newManager.getValue('headless')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('reset', () => {
|
|
72
|
+
it('should reset to default values', () => {
|
|
73
|
+
configManager.set('headless', false);
|
|
74
|
+
configManager.set('rateLimit', 10000);
|
|
75
|
+
|
|
76
|
+
configManager.reset();
|
|
77
|
+
|
|
78
|
+
expect(configManager.getValue('headless')).toBe(true);
|
|
79
|
+
expect(configManager.getValue('rateLimit')).toBe(5000);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('getConfig singleton', () => {
|
|
85
|
+
it('should return same instance on multiple calls', () => {
|
|
86
|
+
const config1 = getConfig();
|
|
87
|
+
const config2 = getConfig();
|
|
88
|
+
expect(config1).toBe(config2);
|
|
89
|
+
});
|
|
90
|
+
});
|