launch-ih 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.
Files changed (68) hide show
  1. package/README.md +117 -0
  2. package/dist/browser.d.ts +20 -0
  3. package/dist/browser.js +86 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/cli/commands/draft.command.d.ts +10 -0
  6. package/dist/cli/commands/draft.command.js +32 -0
  7. package/dist/cli/commands/draft.command.js.map +1 -0
  8. package/dist/cli/commands/history.command.d.ts +9 -0
  9. package/dist/cli/commands/history.command.js +30 -0
  10. package/dist/cli/commands/history.command.js.map +1 -0
  11. package/dist/cli/commands/login.command.d.ts +10 -0
  12. package/dist/cli/commands/login.command.js +64 -0
  13. package/dist/cli/commands/login.command.js.map +1 -0
  14. package/dist/cli/commands/new.command.d.ts +17 -0
  15. package/dist/cli/commands/new.command.js +151 -0
  16. package/dist/cli/commands/new.command.js.map +1 -0
  17. package/dist/cli/commands/preview.command.d.ts +13 -0
  18. package/dist/cli/commands/preview.command.js +48 -0
  19. package/dist/cli/commands/preview.command.js.map +1 -0
  20. package/dist/cli/commands/publish.command.d.ts +12 -0
  21. package/dist/cli/commands/publish.command.js +70 -0
  22. package/dist/cli/commands/publish.command.js.map +1 -0
  23. package/dist/cli/commands/status.command.d.ts +9 -0
  24. package/dist/cli/commands/status.command.js +47 -0
  25. package/dist/cli/commands/status.command.js.map +1 -0
  26. package/dist/cli/index.d.ts +2 -0
  27. package/dist/cli/index.js +168 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/cli/ui/editor.d.ts +1 -0
  30. package/dist/cli/ui/editor.js +39 -0
  31. package/dist/cli/ui/editor.js.map +1 -0
  32. package/dist/cli/ui/output.d.ts +23 -0
  33. package/dist/cli/ui/output.js +96 -0
  34. package/dist/cli/ui/output.js.map +1 -0
  35. package/dist/cli/ui/prompts.d.ts +11 -0
  36. package/dist/cli/ui/prompts.js +28 -0
  37. package/dist/cli/ui/prompts.js.map +1 -0
  38. package/dist/cli/utils/errors.d.ts +20 -0
  39. package/dist/cli/utils/errors.js +48 -0
  40. package/dist/cli/utils/errors.js.map +1 -0
  41. package/dist/cli/utils/session-helpers.d.ts +4 -0
  42. package/dist/cli/utils/session-helpers.js +37 -0
  43. package/dist/cli/utils/session-helpers.js.map +1 -0
  44. package/dist/cli.d.ts +2 -0
  45. package/dist/cli.js +6 -0
  46. package/dist/cli.js.map +1 -0
  47. package/dist/env.d.ts +10 -0
  48. package/dist/env.js +27 -0
  49. package/dist/env.js.map +1 -0
  50. package/dist/ih-auth.d.ts +32 -0
  51. package/dist/ih-auth.js +240 -0
  52. package/dist/ih-auth.js.map +1 -0
  53. package/dist/ih-poster.d.ts +21 -0
  54. package/dist/ih-poster.js +217 -0
  55. package/dist/ih-poster.js.map +1 -0
  56. package/dist/launch-workflow.d.ts +17 -0
  57. package/dist/launch-workflow.js +80 -0
  58. package/dist/launch-workflow.js.map +1 -0
  59. package/dist/post-drafter.d.ts +6 -0
  60. package/dist/post-drafter.js +103 -0
  61. package/dist/post-drafter.js.map +1 -0
  62. package/dist/types.d.ts +55 -0
  63. package/dist/types.js +5 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/utils.d.ts +21 -0
  66. package/dist/utils.js +54 -0
  67. package/dist/utils.js.map +1 -0
  68. package/package.json +52 -0
@@ -0,0 +1,37 @@
1
+ // ============================================================
2
+ // LAUNCH — Session Loading / Validation Helpers
3
+ // ============================================================
4
+ import { existsSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { loadLatestSession } from '../../launch-workflow.js';
7
+ import { loadSession } from '../../utils.js';
8
+ import { CorruptedSessionError, SessionNotFoundError, WrongSessionStateError, NoDraftError, InvalidSessionIdError } from './errors.js';
9
+ const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
+ export function resolveSession(sessionId) {
11
+ if (sessionId && !UUID_V4_REGEX.test(sessionId)) {
12
+ throw new InvalidSessionIdError(sessionId);
13
+ }
14
+ const session = sessionId ? loadSession(sessionId) : loadLatestSession();
15
+ if (!session) {
16
+ if (sessionId) {
17
+ const filePath = resolve(process.cwd(), 'sessions', `${sessionId}.json`);
18
+ if (existsSync(filePath)) {
19
+ throw new CorruptedSessionError(sessionId, 'Failed to parse session file');
20
+ }
21
+ }
22
+ throw new SessionNotFoundError(sessionId);
23
+ }
24
+ return session;
25
+ }
26
+ export function assertState(session, ...expected) {
27
+ if (!expected.includes(session.status)) {
28
+ throw new WrongSessionStateError(expected, session.status);
29
+ }
30
+ }
31
+ export function assertHasDraft(session) {
32
+ if (!session.draft) {
33
+ throw new NoDraftError();
34
+ }
35
+ return session.draft;
36
+ }
37
+ //# sourceMappingURL=session-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-helpers.js","sourceRoot":"","sources":["../../../src/cli/utils/session-helpers.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,gDAAgD;AAChD,+DAA+D;AAE/D,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEvI,MAAM,aAAa,GAAG,wEAAwE,CAAC;AAE/F,MAAM,UAAU,cAAc,CAAC,SAAkB;IAC/C,IAAI,SAAS,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC;IACD,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC;IACzE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,GAAG,SAAS,OAAO,CAAC,CAAC;YACzE,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,qBAAqB,CAAC,SAAS,EAAE,8BAA8B,CAAC,CAAC;YAC7E,CAAC;QACH,CAAC;QACD,MAAM,IAAI,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAsB,EAAE,GAAG,QAAwB;IAC7E,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,sBAAsB,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAsB;IACnD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,IAAI,YAAY,EAAE,CAAC;IAC3B,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC;AACvB,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './cli/index.js';
package/dist/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================
3
+ // LAUNCH CLI — Thin entry point (delegates to cli/index.ts)
4
+ // ============================================================
5
+ import './cli/index.js';
6
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,+DAA+D;AAC/D,4DAA4D;AAC5D,+DAA+D;AAE/D,OAAO,gBAAgB,CAAC"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface LaunchConfig {
2
+ ihEmail: string;
3
+ ihPassword: string;
4
+ browserHeadless: boolean;
5
+ browserSlowMo: number;
6
+ cookiePath: string;
7
+ screenshotDir: string;
8
+ }
9
+ export declare function getEnvOrThrow(key: string): string;
10
+ export declare function loadConfig(): LaunchConfig;
package/dist/env.js ADDED
@@ -0,0 +1,27 @@
1
+ import { config } from 'dotenv';
2
+ import { existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ // Load .env from project root
5
+ const envPath = resolve(process.cwd(), '.env');
6
+ if (existsSync(envPath)) {
7
+ config({ path: envPath });
8
+ }
9
+ export function getEnvOrThrow(key) {
10
+ const value = process.env[key];
11
+ if (!value) {
12
+ throw new Error(`Missing required environment variable: ${key}\n` +
13
+ `Add it to the .env file at the project root.`);
14
+ }
15
+ return value;
16
+ }
17
+ export function loadConfig() {
18
+ return {
19
+ ihEmail: getEnvOrThrow('IH_EMAIL'),
20
+ ihPassword: getEnvOrThrow('IH_PASSWORD'),
21
+ browserHeadless: process.env.BROWSER_HEADLESS !== 'false',
22
+ browserSlowMo: parseInt(process.env.BROWSER_SLOWMO || '100', 10),
23
+ cookiePath: resolve(process.cwd(), process.env.COOKIE_PATH || './cookies/ih-cookies.json'),
24
+ screenshotDir: resolve(process.cwd(), process.env.SCREENSHOT_DIR || './screenshots'),
25
+ };
26
+ }
27
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B,8BAA8B;AAC9B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;AAC/C,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5B,CAAC;AAWD,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,0CAA0C,GAAG,IAAI;YACjD,8CAA8C,CAC/C,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC;QAClC,UAAU,EAAE,aAAa,CAAC,aAAa,CAAC;QACxC,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,OAAO;QACzD,aAAa,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,KAAK,EAAE,EAAE,CAAC;QAChE,UAAU,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,2BAA2B,CAAC;QAC1F,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,eAAe,CAAC;KACrF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { BrowserContext, Page } from 'playwright';
2
+ import { IHCredentials, IHAuthSession } from './types.js';
3
+ import { LaunchConfig } from './env.js';
4
+ /**
5
+ * Log into Indie Hackers via the browser-based login form.
6
+ * Navigates to /login, fills email + password, submits, and
7
+ * waits for a logged-in redirect.
8
+ *
9
+ * Cookies are saved to disk after successful login.
10
+ *
11
+ * @throws If login fails (includes screenshot path + page text in message)
12
+ */
13
+ export declare function login(context: BrowserContext, credentials: IHCredentials, config: LaunchConfig): Promise<IHAuthSession>;
14
+ /**
15
+ * Save Playwright storage state (cookies) to disk.
16
+ */
17
+ export declare function saveCookies(context: BrowserContext, filePath: string): Promise<void>;
18
+ /**
19
+ * Load cookies from disk into the browser context.
20
+ * Returns true if cookies were loaded, false if the file doesn't exist.
21
+ */
22
+ export declare function loadCookies(context: BrowserContext, filePath: string): Promise<boolean>;
23
+ /**
24
+ * Check if the current page shows a logged-in Indie Hackers session.
25
+ * Looks for logged-in indicators like avatar images, "New Post" links, etc.
26
+ */
27
+ export declare function checkLoggedIn(page: Page): Promise<boolean>;
28
+ /**
29
+ * Extract the username from the Indie Hackers page.
30
+ * Tries several strategies to find it.
31
+ */
32
+ export declare function getUsername(page: Page): Promise<string>;
@@ -0,0 +1,240 @@
1
+ // ============================================================
2
+ // LAUNCH — Indie Hackers Authentication (Playwright-based)
3
+ // ============================================================
4
+ // Handles login to Indie Hackers via browser automation.
5
+ // Indie Hackers uses Firebase Auth under the hood, but the
6
+ // login form is a standard email+password form on the page.
7
+ // Session cookies are saved to disk for reuse across runs.
8
+ // ============================================================
9
+ import { ensureDir } from './utils.js';
10
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
11
+ import { dirname, basename } from 'path';
12
+ const IH_BASE_URL = 'https://www.indiehackers.com';
13
+ const RENDER_WAIT = 2000; // ms to wait for Ember.js SPA to boot
14
+ // ============================================================
15
+ // Public API
16
+ // ============================================================
17
+ /**
18
+ * Log into Indie Hackers via the browser-based login form.
19
+ * Navigates to /login, fills email + password, submits, and
20
+ * waits for a logged-in redirect.
21
+ *
22
+ * Cookies are saved to disk after successful login.
23
+ *
24
+ * @throws If login fails (includes screenshot path + page text in message)
25
+ */
26
+ export async function login(context, credentials, config) {
27
+ const page = await context.newPage();
28
+ try {
29
+ await page.goto(`${IH_BASE_URL}/login`, {
30
+ waitUntil: 'domcontentloaded',
31
+ timeout: 30000,
32
+ });
33
+ // Let Ember boot and render the auth form
34
+ await page.waitForTimeout(RENDER_WAIT);
35
+ // Fill email field
36
+ const emailField = await firstVisible(page, [
37
+ 'input[type="email"]',
38
+ 'input[name="email"]',
39
+ 'input[autocomplete="email"]',
40
+ 'input[placeholder*="email" i]',
41
+ '#email',
42
+ ]);
43
+ if (!emailField || !(await emailField.isVisible().catch(() => false))) {
44
+ throw new Error('Could not find the email input field on the login page.');
45
+ }
46
+ await emailField.fill(credentials.email);
47
+ // Fill password field
48
+ const passwordField = await firstVisible(page, [
49
+ 'input[type="password"]',
50
+ 'input[name="password"]',
51
+ 'input[autocomplete="current-password"]',
52
+ '#password',
53
+ ]);
54
+ if (!passwordField || !(await passwordField.isVisible().catch(() => false))) {
55
+ throw new Error('Could not find the password input field on the login page.');
56
+ }
57
+ await passwordField.fill(credentials.password);
58
+ // Click submit
59
+ const submitBtn = await firstVisible(page, [
60
+ 'button[type="submit"]',
61
+ 'input[type="submit"]',
62
+ 'button:has-text("Sign in")',
63
+ 'button:has-text("Log in")',
64
+ 'button:has-text("Continue")',
65
+ ]);
66
+ if (!submitBtn || !(await submitBtn.isVisible().catch(() => false))) {
67
+ throw new Error('Could not find the submit button on the login page.');
68
+ }
69
+ await submitBtn.click();
70
+ // Wait for navigation/redirect indicating successful login
71
+ await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 15000 }).catch(() => { });
72
+ await page.waitForTimeout(RENDER_WAIT);
73
+ // Verify we're logged in
74
+ const loggedIn = await checkLoggedIn(page);
75
+ if (!loggedIn) {
76
+ const bodyText = await page.textContent('body').catch(() => '');
77
+ throw new Error(`Login may have failed. Page text: ${(bodyText || '').slice(0, 500)}`);
78
+ }
79
+ // Save cookies to disk
80
+ await saveCookies(context, config.cookiePath);
81
+ // Extract username
82
+ const username = await getUsername(page);
83
+ return {
84
+ loggedIn: true,
85
+ cookieFile: config.cookiePath,
86
+ email: credentials.email,
87
+ username: username || credentials.email.split('@')[0],
88
+ createdAt: new Date().toISOString(),
89
+ };
90
+ }
91
+ catch (err) {
92
+ // Take a screenshot to help debug
93
+ const screenshotDir = dirname(config.cookiePath).replace('/cookies', '/screenshots');
94
+ ensureDir(screenshotDir);
95
+ const sp = `${screenshotDir}/login-failed-${Date.now()}.png`;
96
+ await page.screenshot({ path: sp, fullPage: true }).catch(() => { });
97
+ throw new Error(`IH login failed: ${err instanceof Error ? err.message : String(err)}. ` +
98
+ `Screenshot saved at: ${basename(sp)}`);
99
+ }
100
+ finally {
101
+ await page.close().catch(() => { });
102
+ }
103
+ }
104
+ /**
105
+ * Save Playwright storage state (cookies) to disk.
106
+ */
107
+ export async function saveCookies(context, filePath) {
108
+ ensureDir(dirname(filePath));
109
+ const storageState = await context.storageState();
110
+ writeFileSync(filePath, JSON.stringify(storageState, null, 2));
111
+ }
112
+ /**
113
+ * Load cookies from disk into the browser context.
114
+ * Returns true if cookies were loaded, false if the file doesn't exist.
115
+ */
116
+ export async function loadCookies(context, filePath) {
117
+ if (!existsSync(filePath))
118
+ return false;
119
+ try {
120
+ const data = readFileSync(filePath, 'utf-8');
121
+ const storage = JSON.parse(data);
122
+ if (storage.cookies && Array.isArray(storage.cookies) && storage.cookies.length > 0) {
123
+ await context.addCookies(storage.cookies);
124
+ return true;
125
+ }
126
+ return false;
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ /**
133
+ * Check if the current page shows a logged-in Indie Hackers session.
134
+ * Looks for logged-in indicators like avatar images, "New Post" links, etc.
135
+ */
136
+ export async function checkLoggedIn(page) {
137
+ try {
138
+ // Check for various logged-in indicators
139
+ const loggedInIndicators = [
140
+ // User avatar image (common IH indicator)
141
+ () => page.locator('img[alt*="avatar" i], img[alt*="profile" i], img[class*="avatar"]').first().isVisible(),
142
+ // "New Post" link/button (only shows when logged in)
143
+ () => page.locator('a[href="/new-post"], a:has-text("New Post")').first().isVisible(),
144
+ // Profile/user menu dropdown
145
+ () => page.locator('[data-testid*="user-menu"], [class*="user-menu"], [class*="profile-dropdown"]').first().isVisible(),
146
+ // User avatar in nav (typically a small circular image)
147
+ () => page.locator('nav img[src*="avatar"], header img[src*="avatar"], nav img[class*="user"]').first().isVisible(),
148
+ // Logged-in user name/text
149
+ () => page.locator('[class*="logged-in"], [class*="authenticated"], a[href*="/settings"]').first().isVisible(),
150
+ // "My Posts" or "Dashboard" link
151
+ () => page.locator('a[href*="/dashboard"], a[href*="/posts"], a[href*="/profile"]').first().isVisible(),
152
+ ];
153
+ for (const check of loggedInIndicators) {
154
+ try {
155
+ if (await check())
156
+ return true;
157
+ }
158
+ catch {
159
+ continue;
160
+ }
161
+ }
162
+ // Fallback: check that we're NOT on a login/signup page
163
+ const currentUrl = page.url();
164
+ if (currentUrl.includes('/login') || currentUrl.includes('/sign-up')) {
165
+ return false;
166
+ }
167
+ // Final fallback: check page for "sign in" appearing as a primary action
168
+ const bodyText = await page.textContent('body').catch(() => '') || '';
169
+ const hasSignInLink = /sign\s*in|log\s*in/i.test(bodyText);
170
+ if (hasSignInLink) {
171
+ // Double check: is there a "sign in" button prominently visible?
172
+ try {
173
+ const signInBtn = page.locator('a[href="/login"], a:has-text("Sign in"), a:has-text("Log in")').first();
174
+ if (await signInBtn.isVisible().catch(() => false)) {
175
+ return false; // Sign in button is visible, so we're not logged in
176
+ }
177
+ }
178
+ catch {
179
+ // Can't determine, fall through
180
+ }
181
+ }
182
+ // If we got here, there's no clear logged-in indicator but also no clear
183
+ // logged-out indicator. Default to checking URL.
184
+ return !currentUrl.includes('/login');
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }
190
+ /**
191
+ * Extract the username from the Indie Hackers page.
192
+ * Tries several strategies to find it.
193
+ */
194
+ export async function getUsername(page) {
195
+ try {
196
+ // Strategy 1: Look for a profile link with username in the href
197
+ const profileLink = page.locator('a[href*="/user/"], a[href*="/profile/"]').first();
198
+ if (await profileLink.isVisible().catch(() => false)) {
199
+ const href = await profileLink.getAttribute('href').catch(() => '');
200
+ if (href) {
201
+ const match = href.match(/\/user\/([^/]+)/);
202
+ if (match)
203
+ return match[1];
204
+ }
205
+ }
206
+ // Strategy 2: Check avatar alt text
207
+ const avatar = page.locator('img[alt*="avatar" i], img[alt*="user" i]').first();
208
+ if (await avatar.isVisible().catch(() => false)) {
209
+ const alt = await avatar.getAttribute('alt').catch(() => '');
210
+ if (alt && alt.length > 2 && alt.length < 50)
211
+ return alt.replace(/'s avatar/i, '').trim();
212
+ }
213
+ // Strategy 3: Look at the page for any username pattern
214
+ const bodyText = (await page.textContent('body').catch(() => '')) || '';
215
+ const userMatch = bodyText.match(/@([a-zA-Z][a-zA-Z0-9_-]{2,20})/);
216
+ if (userMatch)
217
+ return userMatch[1];
218
+ return '';
219
+ }
220
+ catch {
221
+ return '';
222
+ }
223
+ }
224
+ // ============================================================
225
+ // Internal helpers
226
+ // ============================================================
227
+ /**
228
+ * Return the locator for the first visible element matching one of
229
+ * the given CSS selectors, or null if none are visible.
230
+ */
231
+ async function firstVisible(page, selectors) {
232
+ for (const sel of selectors) {
233
+ const loc = page.locator(sel).first();
234
+ if (await loc.isVisible().catch(() => false)) {
235
+ return loc;
236
+ }
237
+ }
238
+ return null;
239
+ }
240
+ //# sourceMappingURL=ih-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ih-auth.js","sourceRoot":"","sources":["../src/ih-auth.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,2DAA2D;AAC3D,+DAA+D;AAC/D,yDAAyD;AACzD,2DAA2D;AAC3D,4DAA4D;AAC5D,2DAA2D;AAC3D,+DAA+D;AAK/D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAEzC,MAAM,WAAW,GAAG,8BAA8B,CAAC;AACnD,MAAM,WAAW,GAAG,IAAI,CAAC,CAAC,sCAAsC;AAEhE,+DAA+D;AAC/D,aAAa;AACb,+DAA+D;AAE/D;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,OAAuB,EACvB,WAA0B,EAC1B,MAAoB;IAEpB,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IAErC,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,WAAW,QAAQ,EAAE;YACtC,SAAS,EAAE,kBAAkB;YAC7B,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;QAEH,0CAA0C;QAC1C,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QAEvC,mBAAmB;QACnB,MAAM,UAAU,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE;YAC1C,qBAAqB;YACrB,qBAAqB;YACrB,6BAA6B;YAC7B,+BAA+B;YAC/B,QAAQ;SACT,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACtE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAEzC,sBAAsB;QACtB,MAAM,aAAa,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE;YAC7C,wBAAwB;YACxB,wBAAwB;YACxB,wCAAwC;YACxC,WAAW;SACZ,CAAC,CAAC;QAEH,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAChF,CAAC;QACD,MAAM,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAE/C,eAAe;QACf,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE;YACzC,uBAAuB;YACvB,sBAAsB;YACtB,4BAA4B;YAC5B,2BAA2B;YAC3B,6BAA6B;SAC9B,CAAC,CAAC;QAEH,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;QACzE,CAAC;QAED,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;QAExB,2DAA2D;QAC3D,MAAM,IAAI,CAAC,UAAU,CACnB,CAAC,GAAQ,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAC9C,EAAE,OAAO,EAAE,KAAK,EAAE,CACnB,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAElB,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QAEvC,yBAAyB;QACzB,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAChE,MAAM,IAAI,KAAK,CACb,qCAAqC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CACtE,CAAC;QACJ,CAAC;QAED,uBAAuB;QACvB,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;QAE9C,mBAAmB;QACnB,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;QAEzC,OAAO;YACL,QAAQ,EAAE,IAAI;YACd,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,KAAK,EAAE,WAAW,CAAC,KAAK;YACxB,QAAQ,EAAE,QAAQ,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACrD,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,kCAAkC;QAClC,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QACrF,SAAS,CAAC,aAAa,CAAC,CAAC;QACzB,MAAM,EAAE,GAAG,GAAG,aAAa,iBAAiB,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC;QAC7D,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAEpE,MAAM,IAAI,KAAK,CACb,oBAAoB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI;YACxE,wBAAwB,QAAQ,CAAC,EAAE,CAAC,EAAE,CACvC,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAuB,EAAE,QAAgB;IACzE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7B,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;IAClD,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAuB,EAAE,QAAgB;IACzE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpF,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAC1C,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAU;IAC5C,IAAI,CAAC;QACH,yCAAyC;QACzC,MAAM,kBAAkB,GAAG;YACzB,0CAA0C;YAC1C,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,mEAAmE,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE;YAC3G,qDAAqD;YACrD,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,6CAA6C,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE;YACrF,6BAA6B;YAC7B,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,+EAA+E,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE;YACvH,wDAAwD;YACxD,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,2EAA2E,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE;YACnH,2BAA2B;YAC3B,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,sEAAsE,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE;YAC9G,iCAAiC;YACjC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,+DAA+D,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE;SACxG,CAAC;QAEF,KAAK,MAAM,KAAK,IAAI,kBAAkB,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,IAAI,MAAM,KAAK,EAAE;oBAAE,OAAO,IAAI,CAAC;YACjC,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;QAED,wDAAwD;QACxD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC9B,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACrE,OAAO,KAAK,CAAC;QACf,CAAC;QAED,yEAAyE;QACzE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;QACtE,MAAM,aAAa,GAAG,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3D,IAAI,aAAa,EAAE,CAAC;YAClB,iEAAiE;YACjE,IAAI,CAAC;gBACH,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,+DAA+D,CAAC,CAAC,KAAK,EAAE,CAAC;gBACxG,IAAI,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;oBACnD,OAAO,KAAK,CAAC,CAAC,oDAAoD;gBACpE,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,gCAAgC;YAClC,CAAC;QACH,CAAC;QAED,yEAAyE;QACzE,iDAAiD;QACjD,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAU;IAC1C,IAAI,CAAC;QACH,gEAAgE;QAChE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,yCAAyC,CAAC,CAAC,KAAK,EAAE,CAAC;QACpF,IAAI,MAAM,WAAW,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YACpE,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;gBAC5C,IAAI,KAAK;oBAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,oCAAoC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,0CAA0C,CAAC,CAAC,KAAK,EAAE,CAAC;QAChF,IAAI,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;YAChD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC7D,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,EAAE;gBAAE,OAAO,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5F,CAAC;QAED,wDAAwD;QACxD,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACxE,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACnE,IAAI,SAAS;YAAE,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC;QAEnC,OAAO,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,+DAA+D;AAC/D,mBAAmB;AACnB,+DAA+D;AAE/D;;;GAGG;AACH,KAAK,UAAU,YAAY,CACzB,IAAU,EACV,SAAmB;IAEnB,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC;QACtC,IAAI,MAAM,GAAG,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7C,OAAO,GAAG,CAAC;QACb,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { BrowserContext, Page } from 'playwright';
2
+ import { IHPostDraft, IHPostResult } from './types.js';
3
+ import { LaunchConfig } from './env.js';
4
+ /**
5
+ * Post a draft to Indie Hackers via Playwright browser automation.
6
+ *
7
+ * @param context A Playwright BrowserContext (should have auth cookies loaded).
8
+ * @param draft The post draft to publish.
9
+ * @param config App config with screenshotDir, cookiePath, etc.
10
+ * @param options Optional. Set `{ dryRun: true }` to fill the form
11
+ * and take a screenshot without submitting.
12
+ */
13
+ export declare function postToIH(context: BrowserContext, draft: IHPostDraft, config: LaunchConfig, options?: {
14
+ dryRun?: boolean;
15
+ }): Promise<IHPostResult>;
16
+ /**
17
+ * Verify the current page is a published Indie Hackers post.
18
+ * Returns the post URL if the page matches an IH post pattern,
19
+ * otherwise returns null.
20
+ */
21
+ export declare function verifyPost(page: Page, _expectedTitle: string): Promise<string | null>;
@@ -0,0 +1,217 @@
1
+ // ============================================================
2
+ // LAUNCH — Indie Hackers Poster (Playwright-based)
3
+ // ============================================================
4
+ // Posts to Indie Hackers (indiehackers.com) using Playwright
5
+ // browser automation. Supports a dry-run mode that fills the
6
+ // form and takes a screenshot without submitting.
7
+ // ============================================================
8
+ import { ensureDir } from './utils.js';
9
+ import { join, basename } from 'path';
10
+ import { loadCookies, checkLoggedIn } from './ih-auth.js';
11
+ const IH_BASE_URL = 'https://www.indiehackers.com';
12
+ const IH_NEW_POST_URL = `${IH_BASE_URL}/new-post`;
13
+ function timestamp() {
14
+ return new Date().toISOString();
15
+ }
16
+ function screenshotPath(dir, label) {
17
+ return join(dir, `ih-${label}-${Date.now()}.png`);
18
+ }
19
+ /**
20
+ * Post a draft to Indie Hackers via Playwright browser automation.
21
+ *
22
+ * @param context A Playwright BrowserContext (should have auth cookies loaded).
23
+ * @param draft The post draft to publish.
24
+ * @param config App config with screenshotDir, cookiePath, etc.
25
+ * @param options Optional. Set `{ dryRun: true }` to fill the form
26
+ * and take a screenshot without submitting.
27
+ */
28
+ export async function postToIH(context, draft, config, options) {
29
+ const dryRun = options?.dryRun ?? false;
30
+ ensureDir(config.screenshotDir);
31
+ let page = null;
32
+ try {
33
+ // Restore saved session cookies so IH knows who we are
34
+ await loadCookies(context, config.cookiePath);
35
+ page = await context.newPage();
36
+ await page.goto(IH_NEW_POST_URL, {
37
+ waitUntil: 'domcontentloaded',
38
+ timeout: 30000,
39
+ });
40
+ // Indie Hackers is an Ember.js SPA — needs extra time to boot
41
+ await page.waitForTimeout(3000);
42
+ // Verify we're authenticated
43
+ const loggedIn = await checkLoggedIn(page).catch(() => false);
44
+ if (!loggedIn) {
45
+ const bodyText = await page.textContent('body').catch(() => '');
46
+ if (/sign in|log in|you need to/i.test(bodyText ?? '')) {
47
+ const sp = screenshotPath(config.screenshotDir, 'not-logged-in');
48
+ await page.screenshot({ path: sp, fullPage: true }).catch(() => { });
49
+ return {
50
+ success: false,
51
+ postUrl: null,
52
+ title: draft.title,
53
+ timestamp: timestamp(),
54
+ error: 'Not logged in. Run setup first.',
55
+ screenshotPath: basename(sp),
56
+ dryRun,
57
+ };
58
+ }
59
+ }
60
+ // ---- Fill title ----
61
+ const titleField = (await firstVisible(page, [
62
+ 'input#title',
63
+ 'input[name="title"]',
64
+ 'input[type="text"]#title',
65
+ '[placeholder*="title" i]',
66
+ ])) ?? page.getByRole('textbox', { name: /title/i }).first();
67
+ if (await titleField.isVisible().catch(() => false)) {
68
+ await titleField.fill(draft.title);
69
+ }
70
+ // ---- Fill body ----
71
+ const bodyField = (await firstVisible(page, [
72
+ 'textarea#body',
73
+ 'textarea[name="body"]',
74
+ 'textarea',
75
+ 'div[contenteditable="true"]',
76
+ '[contenteditable="true"]',
77
+ '[placeholder*="body" i]',
78
+ ])) ?? page.getByRole('textbox', { name: /body|text|content/i }).first();
79
+ if (await bodyField.isVisible().catch(() => false)) {
80
+ await bodyField.fill(draft.body);
81
+ }
82
+ // ---- Fill link (optional) ----
83
+ if (draft.link) {
84
+ const linkField = await firstVisible(page, [
85
+ 'input[type="url"]',
86
+ 'input[name="link"]',
87
+ 'input[name="url"]',
88
+ 'input[placeholder*="link" i]',
89
+ 'input[placeholder*="url" i]',
90
+ ]);
91
+ if (linkField && (await linkField.isVisible().catch(() => false))) {
92
+ await linkField.fill(draft.link);
93
+ }
94
+ }
95
+ // ---- Fill tags (optional) ----
96
+ if (draft.tags && draft.tags.length > 0) {
97
+ const tagField = await firstVisible(page, [
98
+ 'input[name="tags"]',
99
+ 'input[placeholder*="tag" i]',
100
+ '[data-testid="tag-input"]',
101
+ ]);
102
+ if (tagField && (await tagField.isVisible().catch(() => false))) {
103
+ await tagField.fill(draft.tags.join(', '));
104
+ }
105
+ }
106
+ // Let the form settle before proceeding
107
+ await page.waitForTimeout(1000);
108
+ // ---- Dry-run: screenshot only, no submit ----
109
+ if (dryRun) {
110
+ const sp = screenshotPath(config.screenshotDir, 'dry-run');
111
+ await page.screenshot({ path: sp, fullPage: true });
112
+ return {
113
+ success: true,
114
+ postUrl: null,
115
+ title: draft.title,
116
+ timestamp: timestamp(),
117
+ screenshotPath: basename(sp),
118
+ dryRun: true,
119
+ };
120
+ }
121
+ // ---- Submit ----
122
+ const submitBtn = (await firstVisible(page, [
123
+ 'button[type="submit"]',
124
+ 'button:has-text("Submit")',
125
+ 'button:has-text("Post")',
126
+ 'button:has-text("Publish")',
127
+ 'input[type="submit"]',
128
+ ])) ?? page.getByRole('button', { name: /submit|post|publish/i }).first();
129
+ if (!(await submitBtn.isVisible().catch(() => false))) {
130
+ const sp = screenshotPath(config.screenshotDir, 'no-submit-btn');
131
+ await page.screenshot({ path: sp, fullPage: true }).catch(() => { });
132
+ return {
133
+ success: false,
134
+ postUrl: null,
135
+ title: draft.title,
136
+ timestamp: timestamp(),
137
+ error: 'Could not find the submit button on the page.',
138
+ screenshotPath: basename(sp),
139
+ dryRun,
140
+ };
141
+ }
142
+ await submitBtn.click();
143
+ // Wait for redirect to the new post
144
+ await page.waitForURL(/indiehackers\.com\/post\//, { timeout: 15000 }).catch(() => { });
145
+ await page.waitForTimeout(3000);
146
+ const currentUrl = page.url();
147
+ const postUrl = currentUrl && currentUrl.includes('/post/') ? currentUrl : null;
148
+ const sp = screenshotPath(config.screenshotDir, 'posted');
149
+ await page.screenshot({ path: sp, fullPage: true }).catch(() => { });
150
+ if (postUrl) {
151
+ return {
152
+ success: true,
153
+ postUrl,
154
+ title: draft.title,
155
+ timestamp: timestamp(),
156
+ screenshotPath: basename(sp),
157
+ dryRun: false,
158
+ };
159
+ }
160
+ return {
161
+ success: true,
162
+ postUrl: null,
163
+ title: draft.title,
164
+ timestamp: timestamp(),
165
+ error: 'Post was submitted but the post URL could not be verified from the browser location.',
166
+ screenshotPath: basename(sp),
167
+ dryRun: false,
168
+ };
169
+ }
170
+ catch (err) {
171
+ const sp = screenshotPath(config.screenshotDir, 'error');
172
+ if (page) {
173
+ await page.screenshot({ path: sp, fullPage: true }).catch(() => { });
174
+ }
175
+ return {
176
+ success: false,
177
+ postUrl: null,
178
+ title: draft.title,
179
+ timestamp: timestamp(),
180
+ error: `Posting failed: ${err instanceof Error ? err.message : String(err)}`,
181
+ screenshotPath: basename(sp),
182
+ dryRun,
183
+ };
184
+ }
185
+ }
186
+ /**
187
+ * Verify the current page is a published Indie Hackers post.
188
+ * Returns the post URL if the page matches an IH post pattern,
189
+ * otherwise returns null.
190
+ */
191
+ export async function verifyPost(page, _expectedTitle) {
192
+ try {
193
+ const url = page.url();
194
+ if (/\/post\//.test(url)) {
195
+ return url;
196
+ }
197
+ return null;
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ }
203
+ // ---- Helpers ----
204
+ /**
205
+ * Return the locator for the first visible element matching one of
206
+ * the given CSS selectors, or null if none are visible.
207
+ */
208
+ async function firstVisible(page, selectors) {
209
+ for (const sel of selectors) {
210
+ const loc = page.locator(sel).first();
211
+ if (await loc.isVisible().catch(() => false)) {
212
+ return loc;
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ //# sourceMappingURL=ih-poster.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ih-poster.js","sourceRoot":"","sources":["../src/ih-poster.ts"],"names":[],"mappings":"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"}