+18
-10
docs/git-history.json
+18
-10
docs/git-history.json
···
1
1
[
2
2
{
3
+
"hash": "603cf0a187850664336a12c9e5cbb49038906f53",
4
+
"short_hash": "603cf0a",
5
+
"author": "Ariel M. Lighty",
6
+
"date": "2025-12-27T22:42:43-05:00",
7
+
"message": "fix: CORS for extension credentialed requests\n\nUpdated CORS headers to support credentials from Chrome extensions:\n- Added getCorsHeaders() to detect chrome-extension:// origins\n- Changed from wildcard Access-Control-Allow-Origin to specific origin\n- Added Access-Control-Allow-Credentials: true for credentialed requests\n- Updated session endpoint to pass event for CORS header detection",
8
+
"files_changed": 4
9
+
},
10
+
{
3
11
"hash": "bd3aabb75abb1875aef125610fcdccb14967a8e3",
4
12
"short_hash": "bd3aabb",
5
13
"author": "Ariel M. Lighty",
···
8
16
"files_changed": 5
9
17
},
10
18
{
11
-
"hash": "d07180cd3a19328b82b35118e525b59d4e2e060b",
12
-
"short_hash": "d07180c",
19
+
"hash": "bd3aabb75abb1875aef125610fcdccb14967a8e3",
20
+
"short_hash": "bd3aabb",
13
21
"author": "Ariel M. Lighty",
14
-
"date": "2025-12-27T18:38:39-05:00",
15
-
"message": "feat: add Tailwind CSS to extension\n\nReplaced 299 lines of vanilla CSS with Tailwind for design consistency with web app. Production build minified to 13KB.",
16
-
"files_changed": 9
22
+
"date": "2025-12-27T22:10:11-05:00",
23
+
"message": "fix: extension dark mode and build mode messaging\n\n- Changed darkMode from 'class' to 'media' for automatic system preference detection\n- Made server offline message conditional on build mode (dev vs prod)\n- Hide dev server instructions in production builds",
24
+
"files_changed": 5
17
25
},
18
26
{
19
27
"hash": "d07180cd3a19328b82b35118e525b59d4e2e060b",
···
32
40
"files_changed": 6
33
41
},
34
42
{
35
-
"hash": "fcf682bb8969aca108262348e7e17531077713be",
36
-
"short_hash": "fcf682b",
43
+
"hash": "2a163c5f033a79324b100a236ea26c905909bfc6",
44
+
"short_hash": "2a163c5",
37
45
"author": "Ariel M. Lighty",
38
-
"date": "2025-12-27T15:48:44-05:00",
39
-
"message": "docs: improve decision graph workflow with lifecycle management\n\nUpdated CLAUDE.md with comprehensive node lifecycle management:\n- Added node status transitions (pending → in_progress → completed)\n- Correct orphan detection commands (awk instead of cut)\n- Common mistakes section with examples\n- Enhanced audit checklist with status verification\n- Verification workflow after node creation\n\nAlso updated extension popup with ATmosphere branding.\n\nDecision graph now at 331 nodes, 332 edges - all orphans resolved.",
40
-
"files_changed": 4
46
+
"date": "2025-12-27T15:49:08-05:00",
47
+
"message": "docs: update decision graph after documentation improvements",
48
+
"files_changed": 2
41
49
},
42
50
{
43
51
"hash": "fcf682bb8969aca108262348e7e17531077713be",
+231
docs/graph-data.json
+231
docs/graph-data.json
···
4014
4014
"created_at": "2025-12-27T22:41:38.430661200-05:00",
4015
4015
"updated_at": "2025-12-27T22:41:48.981429600-05:00",
4016
4016
"metadata_json": "{\"branch\":\"master\",\"confidence\":95}"
4017
+
},
4018
+
{
4019
+
"id": 366,
4020
+
"change_id": "77b7ed7e-a113-41f6-a677-50d376f3f008",
4021
+
"node_type": "outcome",
4022
+
"title": "Committed CORS fixes to git",
4023
+
"description": null,
4024
+
"status": "completed",
4025
+
"created_at": "2025-12-27T22:42:49.037783-05:00",
4026
+
"updated_at": "2025-12-27T22:42:54.162857-05:00",
4027
+
"metadata_json": "{\"branch\":\"master\",\"commit\":\"603cf0a\",\"confidence\":95}"
4028
+
},
4029
+
{
4030
+
"id": 367,
4031
+
"change_id": "df6abf7a-e7a4-45f3-8485-b933319416d9",
4032
+
"node_type": "goal",
4033
+
"title": "Create Firefox-compatible version of Twitter scraper extension",
4034
+
"description": null,
4035
+
"status": "pending",
4036
+
"created_at": "2025-12-28T18:09:33.241860800-05:00",
4037
+
"updated_at": "2025-12-28T18:09:33.241860800-05:00",
4038
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":85,\"prompt\":\"let's make the extension have a firefox compatible version too.\"}"
4039
+
},
4040
+
{
4041
+
"id": 368,
4042
+
"change_id": "79721edf-aa05-4580-8c28-7d20941ef155",
4043
+
"node_type": "observation",
4044
+
"title": "Current extension uses Manifest V3 with Chrome-specific APIs",
4045
+
"description": null,
4046
+
"status": "pending",
4047
+
"created_at": "2025-12-28T18:10:08.441348100-05:00",
4048
+
"updated_at": "2025-12-28T18:10:08.441348100-05:00",
4049
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":95}"
4050
+
},
4051
+
{
4052
+
"id": 369,
4053
+
"change_id": "783841d0-c096-48f6-be18-193a9dcc7d4b",
4054
+
"node_type": "observation",
4055
+
"title": "Firefox compatibility analysis: Extension uses chrome.* APIs (runtime.sendMessage, storage.local, tabs.query/sendMessage), MV3 service worker. Firefox supports MV3 but has differences. Options: 1) Use webextension-polyfill for cross-browser, 2) Dual manifests (MV3 Chrome + MV2 Firefox), 3) Keep MV3 for both with minimal changes. Current build outputs to dist/chrome only.",
4056
+
"description": null,
4057
+
"status": "pending",
4058
+
"created_at": "2025-12-28T18:10:48.087066800-05:00",
4059
+
"updated_at": "2025-12-28T18:10:48.087066800-05:00",
4060
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":90}"
4061
+
},
4062
+
{
4063
+
"id": 370,
4064
+
"change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6",
4065
+
"node_type": "decision",
4066
+
"title": "Choose Firefox compatibility approach: webextension-polyfill, dual manifests, or minimal MV3 changes",
4067
+
"description": null,
4068
+
"status": "pending",
4069
+
"created_at": "2025-12-28T18:10:50.375270400-05:00",
4070
+
"updated_at": "2025-12-28T18:10:50.375270400-05:00",
4071
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":80}"
4072
+
},
4073
+
{
4074
+
"id": 371,
4075
+
"change_id": "159906da-984f-4a1d-a1a6-98e0fc0cf369",
4076
+
"node_type": "option",
4077
+
"title": "Use webextension-polyfill library for unified cross-browser API",
4078
+
"description": null,
4079
+
"status": "pending",
4080
+
"created_at": "2025-12-28T18:11:05.947924200-05:00",
4081
+
"updated_at": "2025-12-28T18:11:05.947924200-05:00",
4082
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":85}"
4083
+
},
4084
+
{
4085
+
"id": 372,
4086
+
"change_id": "df5e42e6-53c1-4b30-8b6f-f2385cd9e247",
4087
+
"node_type": "option",
4088
+
"title": "Dual manifests: MV3 for Chrome, MV2 for Firefox with separate builds",
4089
+
"description": null,
4090
+
"status": "pending",
4091
+
"created_at": "2025-12-28T18:11:08.179938100-05:00",
4092
+
"updated_at": "2025-12-28T18:11:08.179938100-05:00",
4093
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":85}"
4094
+
},
4095
+
{
4096
+
"id": 373,
4097
+
"change_id": "7bb58202-7a9b-4e8b-8b9e-927e5106bce7",
4098
+
"node_type": "option",
4099
+
"title": "Keep MV3 for both browsers with minimal manifest tweaks",
4100
+
"description": null,
4101
+
"status": "pending",
4102
+
"created_at": "2025-12-28T18:11:10.370113600-05:00",
4103
+
"updated_at": "2025-12-28T18:11:10.370113600-05:00",
4104
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":85}"
4105
+
},
4106
+
{
4107
+
"id": 374,
4108
+
"change_id": "d41b29e0-cd48-4dac-a6c8-c6179612702e",
4109
+
"node_type": "outcome",
4110
+
"title": "Chose webextension-polyfill approach. Provides unified browser.* API, Promise-based, future-proof MV3 for both browsers, +20KB but cleaner codebase",
4111
+
"description": null,
4112
+
"status": "pending",
4113
+
"created_at": "2025-12-28T19:04:24.676770900-05:00",
4114
+
"updated_at": "2025-12-28T19:04:24.676770900-05:00",
4115
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":95}"
4116
+
},
4117
+
{
4118
+
"id": 375,
4119
+
"change_id": "5bb34b8b-aec4-4f84-993e-eb9bf7a2d13f",
4120
+
"node_type": "action",
4121
+
"title": "Installing webextension-polyfill and updating source files to use browser.* API",
4122
+
"description": null,
4123
+
"status": "pending",
4124
+
"created_at": "2025-12-28T19:08:14.642882400-05:00",
4125
+
"updated_at": "2025-12-28T19:08:14.642882400-05:00",
4126
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":90}"
4127
+
},
4128
+
{
4129
+
"id": 376,
4130
+
"change_id": "644181ee-5a44-4967-9657-e9dd5f648c5e",
4131
+
"node_type": "outcome",
4132
+
"title": "Successfully implemented Firefox compatibility with webextension-polyfill. Both Chrome and Firefox builds compile successfully. Chrome uses service_worker (MV3), Firefox uses scripts array with browser_specific_settings. All chrome.* API calls replaced with browser.* imports.",
4133
+
"description": null,
4134
+
"status": "pending",
4135
+
"created_at": "2025-12-28T19:14:22.309457600-05:00",
4136
+
"updated_at": "2025-12-28T19:14:22.309457600-05:00",
4137
+
"metadata_json": "{\"branch\":\"master\",\"confidence\":95}"
4017
4138
}
4018
4139
],
4019
4140
"edges": [
···
8009
8130
"weight": 1.0,
8010
8131
"rationale": "CORS fix completed",
8011
8132
"created_at": "2025-12-27T22:41:44.160528300-05:00"
8133
+
},
8134
+
{
8135
+
"id": 364,
8136
+
"from_node_id": 360,
8137
+
"to_node_id": 366,
8138
+
"from_change_id": "706d5a7f-08ed-43f7-aee5-0bed28d9402a",
8139
+
"to_change_id": "77b7ed7e-a113-41f6-a677-50d376f3f008",
8140
+
"edge_type": "leads_to",
8141
+
"weight": 1.0,
8142
+
"rationale": "Git commit for CORS fixes",
8143
+
"created_at": "2025-12-27T22:42:51.663598100-05:00"
8144
+
},
8145
+
{
8146
+
"id": 365,
8147
+
"from_node_id": 367,
8148
+
"to_node_id": 368,
8149
+
"from_change_id": "df6abf7a-e7a4-45f3-8485-b933319416d9",
8150
+
"to_change_id": "79721edf-aa05-4580-8c28-7d20941ef155",
8151
+
"edge_type": "leads_to",
8152
+
"weight": 1.0,
8153
+
"rationale": "Analysis step for Firefox compatibility",
8154
+
"created_at": "2025-12-28T18:10:09.484445500-05:00"
8155
+
},
8156
+
{
8157
+
"id": 366,
8158
+
"from_node_id": 368,
8159
+
"to_node_id": 369,
8160
+
"from_change_id": "79721edf-aa05-4580-8c28-7d20941ef155",
8161
+
"to_change_id": "783841d0-c096-48f6-be18-193a9dcc7d4b",
8162
+
"edge_type": "leads_to",
8163
+
"weight": 1.0,
8164
+
"rationale": "Detailed analysis of compatibility issues",
8165
+
"created_at": "2025-12-28T18:10:49.163552300-05:00"
8166
+
},
8167
+
{
8168
+
"id": 367,
8169
+
"from_node_id": 369,
8170
+
"to_node_id": 370,
8171
+
"from_change_id": "783841d0-c096-48f6-be18-193a9dcc7d4b",
8172
+
"to_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6",
8173
+
"edge_type": "leads_to",
8174
+
"weight": 1.0,
8175
+
"rationale": "Need to decide implementation strategy",
8176
+
"created_at": "2025-12-28T18:10:51.434960600-05:00"
8177
+
},
8178
+
{
8179
+
"id": 368,
8180
+
"from_node_id": 370,
8181
+
"to_node_id": 371,
8182
+
"from_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6",
8183
+
"to_change_id": "159906da-984f-4a1d-a1a6-98e0fc0cf369",
8184
+
"edge_type": "leads_to",
8185
+
"weight": 1.0,
8186
+
"rationale": "Option A",
8187
+
"created_at": "2025-12-28T18:11:07.060637-05:00"
8188
+
},
8189
+
{
8190
+
"id": 369,
8191
+
"from_node_id": 370,
8192
+
"to_node_id": 372,
8193
+
"from_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6",
8194
+
"to_change_id": "df5e42e6-53c1-4b30-8b6f-f2385cd9e247",
8195
+
"edge_type": "leads_to",
8196
+
"weight": 1.0,
8197
+
"rationale": "Option B",
8198
+
"created_at": "2025-12-28T18:11:09.223792400-05:00"
8199
+
},
8200
+
{
8201
+
"id": 370,
8202
+
"from_node_id": 370,
8203
+
"to_node_id": 373,
8204
+
"from_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6",
8205
+
"to_change_id": "7bb58202-7a9b-4e8b-8b9e-927e5106bce7",
8206
+
"edge_type": "leads_to",
8207
+
"weight": 1.0,
8208
+
"rationale": "Option C",
8209
+
"created_at": "2025-12-28T18:11:11.439827800-05:00"
8210
+
},
8211
+
{
8212
+
"id": 371,
8213
+
"from_node_id": 370,
8214
+
"to_node_id": 374,
8215
+
"from_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6",
8216
+
"to_change_id": "d41b29e0-cd48-4dac-a6c8-c6179612702e",
8217
+
"edge_type": "leads_to",
8218
+
"weight": 1.0,
8219
+
"rationale": "User selected option 1",
8220
+
"created_at": "2025-12-28T19:04:26.708742600-05:00"
8221
+
},
8222
+
{
8223
+
"id": 372,
8224
+
"from_node_id": 374,
8225
+
"to_node_id": 375,
8226
+
"from_change_id": "d41b29e0-cd48-4dac-a6c8-c6179612702e",
8227
+
"to_change_id": "5bb34b8b-aec4-4f84-993e-eb9bf7a2d13f",
8228
+
"edge_type": "leads_to",
8229
+
"weight": 1.0,
8230
+
"rationale": "Implementation based on decision",
8231
+
"created_at": "2025-12-28T19:08:16.677078600-05:00"
8232
+
},
8233
+
{
8234
+
"id": 373,
8235
+
"from_node_id": 375,
8236
+
"to_node_id": 376,
8237
+
"from_change_id": "5bb34b8b-aec4-4f84-993e-eb9bf7a2d13f",
8238
+
"to_change_id": "644181ee-5a44-4967-9657-e9dd5f648c5e",
8239
+
"edge_type": "leads_to",
8240
+
"weight": 1.0,
8241
+
"rationale": "Implementation completed successfully",
8242
+
"created_at": "2025-12-28T19:14:24.961595600-05:00"
8012
8243
}
8013
8244
]
8014
8245
}
+173
packages/extension/FIREFOX.md
+173
packages/extension/FIREFOX.md
···
1
+
# Firefox Extension Installation Guide
2
+
3
+
The ATlast Importer extension now supports both Chrome and Firefox!
4
+
5
+
## Building for Firefox
6
+
7
+
The build system automatically creates both Chrome and Firefox versions:
8
+
9
+
```bash
10
+
pnpm run build # Development build for both browsers
11
+
pnpm run build:prod # Production build for both browsers
12
+
```
13
+
14
+
Output directories:
15
+
- `dist/chrome/` - Chrome/Edge version (Manifest V3 with service worker)
16
+
- `dist/firefox/` - Firefox version (Manifest V3 with scripts array)
17
+
18
+
## Installing in Firefox (Development)
19
+
20
+
### Option 1: Temporary Installation (for testing)
21
+
22
+
1. Open Firefox
23
+
2. Navigate to `about:debugging#/runtime/this-firefox`
24
+
3. Click "Load Temporary Add-on..."
25
+
4. Navigate to `packages/extension/dist/firefox/`
26
+
5. Select the `manifest.json` file
27
+
28
+
**Note:** Temporary extensions are removed when Firefox restarts.
29
+
30
+
### Option 2: Loading from ZIP (for distribution)
31
+
32
+
1. Build the production version:
33
+
```bash
34
+
pnpm run build:prod
35
+
pnpm run package:firefox
36
+
```
37
+
38
+
2. This creates `dist/firefox.zip`
39
+
40
+
3. For testing:
41
+
- Go to `about:debugging#/runtime/this-firefox`
42
+
- Click "Load Temporary Add-on..."
43
+
- Select the `firefox.zip` file
44
+
45
+
4. For publishing:
46
+
- Submit `firefox.zip` to [addons.mozilla.org](https://addons.mozilla.org/developers/)
47
+
48
+
## Key Differences from Chrome Version
49
+
50
+
### Manifest Differences
51
+
52
+
**Chrome (`manifest.chrome.json`):**
53
+
```json
54
+
{
55
+
"manifest_version": 3,
56
+
"background": {
57
+
"service_worker": "background/service-worker.js",
58
+
"type": "module"
59
+
}
60
+
}
61
+
```
62
+
63
+
**Firefox (`manifest.firefox.json`):**
64
+
```json
65
+
{
66
+
"manifest_version": 3,
67
+
"background": {
68
+
"scripts": ["background/service-worker.js"],
69
+
"type": "module"
70
+
},
71
+
"browser_specific_settings": {
72
+
"gecko": {
73
+
"id": "atlast-importer@byarielm.fyi",
74
+
"strict_min_version": "109.0"
75
+
}
76
+
}
77
+
}
78
+
```
79
+
80
+
### Cross-Browser Compatibility
81
+
82
+
- All code uses `webextension-polyfill` library
83
+
- Chrome-specific `chrome.*` APIs replaced with unified `browser.*` API
84
+
- Promise-based instead of callback-based
85
+
- Single codebase works across both browsers
86
+
87
+
### Requirements
88
+
89
+
- **Firefox:** Version 109+ (for Manifest V3 support)
90
+
- **Chrome/Edge:** Latest version
91
+
92
+
## Testing
93
+
94
+
After loading the extension in Firefox:
95
+
96
+
1. Navigate to Twitter/X Following page (e.g., `https://twitter.com/username/following`)
97
+
2. Click the extension icon in the toolbar
98
+
3. The popup should show "Ready to scan" state
99
+
4. Click "Start Scan" to scrape usernames
100
+
5. Click "Open on ATlast" to upload results
101
+
102
+
## Debugging
103
+
104
+
### View Console Logs
105
+
106
+
**Background Script:**
107
+
- Go to `about:debugging#/runtime/this-firefox`
108
+
- Find "ATlast Importer" in the list
109
+
- Click "Inspect"
110
+
111
+
**Popup:**
112
+
- Right-click extension icon → "Inspect Extension"
113
+
114
+
**Content Script:**
115
+
- Open DevTools on Twitter/X page (F12)
116
+
- Look for `[ATlast]` prefixed logs in Console
117
+
118
+
### Common Issues
119
+
120
+
1. **Extension not loading:**
121
+
- Check Firefox version is 109+
122
+
- Ensure manifest.json is valid
123
+
- Check browser console for errors
124
+
125
+
2. **Scan not starting:**
126
+
- Verify you're on Twitter/X Following page
127
+
- Check content script is injected (look for console logs)
128
+
- Ensure page is fully loaded
129
+
130
+
3. **"Server offline" message:**
131
+
- Make sure dev server is running (`netlify dev`)
132
+
- Check API URL in extension settings
133
+
134
+
## Packaging for Distribution
135
+
136
+
Create production builds for both browsers:
137
+
138
+
```bash
139
+
pnpm run package:prod
140
+
```
141
+
142
+
This creates:
143
+
- `dist/chrome.zip` - Ready for Chrome Web Store
144
+
- `dist/firefox.zip` - Ready for Firefox Add-ons
145
+
146
+
## Development Workflow
147
+
148
+
```bash
149
+
# Watch mode (auto-rebuild on changes)
150
+
pnpm run dev
151
+
152
+
# In Firefox:
153
+
# 1. about:debugging → Reload extension after each rebuild
154
+
# 2. Or use web-ext for auto-reload:
155
+
156
+
npx web-ext run --source-dir=dist/firefox
157
+
```
158
+
159
+
## Differences You Might Notice
160
+
161
+
1. **Background page persistence:**
162
+
- Chrome: Service worker (non-persistent)
163
+
- Firefox: Scripts array (similar behavior in MV3)
164
+
165
+
2. **API behavior:**
166
+
- Firefox: Native Promise support
167
+
- Chrome: Promises via polyfill
168
+
169
+
3. **Extension ID:**
170
+
- Chrome: Auto-generated
171
+
- Firefox: Explicitly set as `atlast-importer@byarielm.fyi`
172
+
173
+
Both versions use the same source code and should behave identically!
+61
-42
packages/extension/build.js
+61
-42
packages/extension/build.js
···
21
21
console.log(`🔗 API URL: ${ATLAST_API_URL}`);
22
22
23
23
// Clean dist directory
24
-
const distDir = path.join(__dirname, 'dist', 'chrome');
25
-
if (fs.existsSync(distDir)) {
26
-
fs.rmSync(distDir, { recursive: true });
24
+
const distBaseDir = path.join(__dirname, 'dist');
25
+
if (fs.existsSync(distBaseDir)) {
26
+
fs.rmSync(distBaseDir, { recursive: true });
27
27
}
28
-
fs.mkdirSync(distDir, { recursive: true });
28
+
fs.mkdirSync(distBaseDir, { recursive: true });
29
29
30
30
// Build configuration base
31
31
const buildConfigBase = {
···
38
38
'__ATLAST_API_URL__': JSON.stringify(ATLAST_API_URL),
39
39
'__BUILD_MODE__': JSON.stringify(mode),
40
40
},
41
+
// Include webextension-polyfill in the bundle
42
+
external: [],
41
43
};
42
44
43
-
// Build scripts
44
-
const scripts = [
45
-
{
46
-
...buildConfigBase,
47
-
entryPoints: ['src/content/index.ts'],
48
-
outfile: path.join(distDir, 'content', 'index.js'),
49
-
},
50
-
{
51
-
...buildConfigBase,
52
-
entryPoints: ['src/background/service-worker.ts'],
53
-
outfile: path.join(distDir, 'background', 'service-worker.js'),
54
-
},
55
-
{
56
-
...buildConfigBase,
57
-
entryPoints: ['src/popup/popup.ts'],
58
-
outfile: path.join(distDir, 'popup', 'popup.js'),
59
-
},
60
-
];
45
+
// Build scripts for a specific browser
46
+
function getScripts(browser) {
47
+
const distDir = path.join(distBaseDir, browser);
48
+
return [
49
+
{
50
+
...buildConfigBase,
51
+
entryPoints: ['src/content/index.ts'],
52
+
outfile: path.join(distDir, 'content', 'index.js'),
53
+
},
54
+
{
55
+
...buildConfigBase,
56
+
entryPoints: ['src/background/service-worker.ts'],
57
+
outfile: path.join(distDir, 'background', 'service-worker.js'),
58
+
},
59
+
{
60
+
...buildConfigBase,
61
+
entryPoints: ['src/popup/popup.ts'],
62
+
outfile: path.join(distDir, 'popup', 'popup.js'),
63
+
},
64
+
];
65
+
}
61
66
62
67
// Build function
63
68
async function build() {
64
69
try {
65
-
console.log('🔨 Building extension...');
70
+
console.log('🔨 Building extension for Chrome and Firefox...');
71
+
72
+
const browsers = ['chrome', 'firefox'];
66
73
67
-
// Build all scripts
68
-
for (const config of scripts) {
69
-
if (watch) {
70
-
const ctx = await esbuild.context(config);
71
-
await ctx.watch();
72
-
console.log(`👀 Watching ${path.basename(config.entryPoints[0])}...`);
73
-
} else {
74
-
await esbuild.build(config);
75
-
console.log(`✅ Built ${path.basename(config.entryPoints[0])}`);
74
+
for (const browser of browsers) {
75
+
console.log(`\n📦 Building ${browser} version...`);
76
+
const scripts = getScripts(browser);
77
+
78
+
// Build all scripts
79
+
for (const config of scripts) {
80
+
if (watch) {
81
+
const ctx = await esbuild.context(config);
82
+
await ctx.watch();
83
+
console.log(`👀 Watching ${browser}/${path.basename(config.entryPoints[0])}...`);
84
+
} else {
85
+
await esbuild.build(config);
86
+
console.log(`✅ Built ${browser}/${path.basename(config.entryPoints[0])}`);
87
+
}
76
88
}
77
-
}
78
89
79
-
// Copy static files
80
-
copyStaticFiles();
90
+
// Copy static files
91
+
copyStaticFiles(browser);
81
92
82
-
// Process CSS with Tailwind
83
-
await processCSS();
93
+
// Process CSS with Tailwind
94
+
await processCSS(browser);
95
+
}
84
96
85
97
if (!watch) {
86
-
console.log('✨ Build complete!');
98
+
console.log('\n✨ Build complete for both browsers!');
87
99
}
88
100
} catch (error) {
89
101
console.error('❌ Build failed:', error);
···
92
104
}
93
105
94
106
// Process CSS with PostCSS (Tailwind + Autoprefixer)
95
-
async function processCSS() {
107
+
async function processCSS(browser) {
96
108
const cssPath = path.join(__dirname, 'src/popup/popup.css');
109
+
const distDir = path.join(distBaseDir, browser);
97
110
const outputPath = path.join(distDir, 'popup/popup.css');
98
111
99
112
const css = fs.readFileSync(cssPath, 'utf8');
···
121
134
}
122
135
123
136
// Copy static files
124
-
function copyStaticFiles() {
137
+
function copyStaticFiles(browser) {
138
+
const distDir = path.join(distBaseDir, browser);
139
+
125
140
const filesToCopy = [
126
-
{ from: 'manifest.json', to: 'manifest.json' },
141
+
{ from: `manifest.${browser}.json`, to: 'manifest.json', fallback: 'manifest.json' },
127
142
{ from: 'src/popup/popup.html', to: 'popup/popup.html' },
128
143
];
129
144
130
145
for (const file of filesToCopy) {
131
-
const srcPath = path.join(__dirname, file.from);
146
+
// Try to use browser-specific file first, fall back to default
147
+
let srcPath = path.join(__dirname, file.from);
148
+
if (file.fallback && !fs.existsSync(srcPath)) {
149
+
srcPath = path.join(__dirname, file.fallback);
150
+
}
132
151
const destPath = path.join(distDir, file.to);
133
152
134
153
// Create directory if it doesn't exist
+44
packages/extension/manifest.chrome.json
+44
packages/extension/manifest.chrome.json
···
1
+
{
2
+
"manifest_version": 3,
3
+
"name": "ATlast Importer",
4
+
"version": "1.0.0",
5
+
"description": "Import your Twitter/X follows to find them on Bluesky",
6
+
"permissions": [
7
+
"activeTab",
8
+
"storage"
9
+
],
10
+
"host_permissions": [
11
+
"https://twitter.com/*",
12
+
"https://x.com/*",
13
+
"http://127.0.0.1:8888/*",
14
+
"http://localhost:8888/*",
15
+
"https://atlast.byarielm.fyi/*"
16
+
],
17
+
"background": {
18
+
"service_worker": "background/service-worker.js",
19
+
"type": "module"
20
+
},
21
+
"content_scripts": [
22
+
{
23
+
"matches": [
24
+
"https://twitter.com/*",
25
+
"https://x.com/*"
26
+
],
27
+
"js": ["content/index.js"],
28
+
"run_at": "document_idle"
29
+
}
30
+
],
31
+
"action": {
32
+
"default_popup": "popup/popup.html",
33
+
"default_icon": {
34
+
"16": "assets/icon-16.png",
35
+
"48": "assets/icon-48.png",
36
+
"128": "assets/icon-128.png"
37
+
}
38
+
},
39
+
"icons": {
40
+
"16": "assets/icon-16.png",
41
+
"48": "assets/icon-48.png",
42
+
"128": "assets/icon-128.png"
43
+
}
44
+
}
+50
packages/extension/manifest.firefox.json
+50
packages/extension/manifest.firefox.json
···
1
+
{
2
+
"manifest_version": 3,
3
+
"name": "ATlast Importer",
4
+
"version": "1.0.0",
5
+
"description": "Import your Twitter/X follows to find them on Bluesky",
6
+
"permissions": [
7
+
"activeTab",
8
+
"storage"
9
+
],
10
+
"host_permissions": [
11
+
"https://twitter.com/*",
12
+
"https://x.com/*",
13
+
"http://127.0.0.1:8888/*",
14
+
"http://localhost:8888/*",
15
+
"https://atlast.byarielm.fyi/*"
16
+
],
17
+
"background": {
18
+
"scripts": ["background/service-worker.js"],
19
+
"type": "module"
20
+
},
21
+
"content_scripts": [
22
+
{
23
+
"matches": [
24
+
"https://twitter.com/*",
25
+
"https://x.com/*"
26
+
],
27
+
"js": ["content/index.js"],
28
+
"run_at": "document_idle"
29
+
}
30
+
],
31
+
"action": {
32
+
"default_popup": "popup/popup.html",
33
+
"default_icon": {
34
+
"16": "assets/icon-16.png",
35
+
"48": "assets/icon-48.png",
36
+
"128": "assets/icon-128.png"
37
+
}
38
+
},
39
+
"icons": {
40
+
"16": "assets/icon-16.png",
41
+
"48": "assets/icon-48.png",
42
+
"128": "assets/icon-128.png"
43
+
},
44
+
"browser_specific_settings": {
45
+
"gecko": {
46
+
"id": "atlast-importer@byarielm.fyi",
47
+
"strict_min_version": "109.0"
48
+
}
49
+
}
50
+
}
+6
-2
packages/extension/package.json
+6
-2
packages/extension/package.json
···
9
9
"build:prod": "node build.js --prod",
10
10
"dev": "node build.js --watch",
11
11
"package:chrome": "cd dist/chrome && zip -r ../chrome.zip .",
12
-
"package:prod": "npm run build:prod && npm run package:chrome"
12
+
"package:firefox": "cd dist/firefox && zip -r ../firefox.zip .",
13
+
"package:all": "pnpm run package:chrome && pnpm run package:firefox",
14
+
"package:prod": "pnpm run build:prod && pnpm run package:all"
13
15
},
14
16
"dependencies": {
15
-
"@atlast/shared": "workspace:*"
17
+
"@atlast/shared": "workspace:*",
18
+
"webextension-polyfill": "^0.12.0"
16
19
},
17
20
"devDependencies": {
18
21
"@types/chrome": "^0.0.256",
22
+
"@types/webextension-polyfill": "^0.12.4",
19
23
"autoprefixer": "^10.4.23",
20
24
"cssnano": "^7.1.2",
21
25
"esbuild": "^0.19.11",
+2
-1
packages/extension/src/background/service-worker.ts
+2
-1
packages/extension/src/background/service-worker.ts
···
1
+
import browser from 'webextension-polyfill';
1
2
import {
2
3
MessageType,
3
4
onMessage,
···
150
151
/**
151
152
* Log extension installation
152
153
*/
153
-
chrome.runtime.onInstalled.addListener((details) => {
154
+
browser.runtime.onInstalled.addListener((details) => {
154
155
console.log('[Background] Extension installed:', details.reason);
155
156
156
157
if (details.reason === 'install') {
+3
-1
packages/extension/src/lib/api-client.ts
+3
-1
packages/extension/src/lib/api-client.ts
···
2
2
* ATlast API client for extension
3
3
*/
4
4
5
+
import browser from 'webextension-polyfill';
6
+
5
7
// These are replaced at build time by esbuild
6
8
declare const __ATLAST_API_URL__: string;
7
9
declare const __BUILD_MODE__: string;
···
67
69
* Get extension version from manifest
68
70
*/
69
71
export function getExtensionVersion(): string {
70
-
return chrome.runtime.getManifest().version;
72
+
return browser.runtime.getManifest().version;
71
73
}
72
74
73
75
/**
+6
-5
packages/extension/src/lib/messaging.ts
+6
-5
packages/extension/src/lib/messaging.ts
···
1
+
import browser from 'webextension-polyfill';
1
2
import type { ScraperProgress, ScraperResult } from '../content/scrapers/base-scraper.js';
2
3
3
4
/**
···
87
88
* Send message to background script
88
89
*/
89
90
export function sendToBackground<T = any>(message: Message): Promise<T> {
90
-
return chrome.runtime.sendMessage(message);
91
+
return browser.runtime.sendMessage(message);
91
92
}
92
93
93
94
/**
94
95
* Send message to active tab's content script
95
96
*/
96
97
export async function sendToContent(message: Message): Promise<any> {
97
-
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
98
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
98
99
if (!tab.id) {
99
100
throw new Error('No active tab found');
100
101
}
101
-
return chrome.tabs.sendMessage(tab.id, message);
102
+
return browser.tabs.sendMessage(tab.id, message);
102
103
}
103
104
104
105
/**
105
106
* Listen for messages
106
107
*/
107
108
export function onMessage(
108
-
handler: (message: Message, sender: chrome.runtime.MessageSender) => any | Promise<any>
109
+
handler: (message: Message, sender: browser.Runtime.MessageSender) => any | Promise<any>
109
110
): void {
110
-
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
111
+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
111
112
const result = handler(message, sender);
112
113
113
114
// Handle async handlers
+4
-3
packages/extension/src/lib/storage.ts
+4
-3
packages/extension/src/lib/storage.ts
···
1
+
import browser from 'webextension-polyfill';
1
2
import type { ExtensionState } from './messaging.js';
2
3
3
4
/**
···
11
12
* Get extension state from storage
12
13
*/
13
14
export async function getState(): Promise<ExtensionState> {
14
-
const result = await chrome.storage.local.get(STORAGE_KEYS.STATE);
15
+
const result = await browser.storage.local.get(STORAGE_KEYS.STATE);
15
16
return result[STORAGE_KEYS.STATE] || { status: 'idle' };
16
17
}
17
18
···
19
20
* Save extension state to storage
20
21
*/
21
22
export async function setState(state: ExtensionState): Promise<void> {
22
-
await chrome.storage.local.set({ [STORAGE_KEYS.STATE]: state });
23
+
await browser.storage.local.set({ [STORAGE_KEYS.STATE]: state });
23
24
}
24
25
25
26
/**
26
27
* Clear extension state
27
28
*/
28
29
export async function clearState(): Promise<void> {
29
-
await chrome.storage.local.remove(STORAGE_KEYS.STATE);
30
+
await browser.storage.local.remove(STORAGE_KEYS.STATE);
30
31
}
+4
-3
packages/extension/src/popup/popup.ts
+4
-3
packages/extension/src/popup/popup.ts
···
1
+
import browser from 'webextension-polyfill';
1
2
import {
2
3
MessageType,
3
4
sendToBackground,
···
167
168
// Open ATlast at results page with upload data
168
169
const { getApiUrl } = await import('../lib/api-client.js');
169
170
const resultsUrl = `${getApiUrl()}${response.redirectUrl}`;
170
-
chrome.tabs.create({ url: resultsUrl });
171
+
browser.tabs.create({ url: resultsUrl });
171
172
172
173
} catch (error) {
173
174
console.error('[Popup] Error uploading:', error);
···
281
282
282
283
// Set up login buttons
283
284
elements.btnOpenAtlast.addEventListener('click', () => {
284
-
chrome.tabs.create({ url: getApiUrl() });
285
+
browser.tabs.create({ url: getApiUrl() });
285
286
});
286
287
287
288
elements.btnRetryLogin.addEventListener('click', async () => {
···
322
323
});
323
324
324
325
// Listen for storage changes (when background updates state)
325
-
chrome.storage.onChanged.addListener((changes, areaName) => {
326
+
browser.storage.onChanged.addListener((changes, areaName) => {
326
327
if (areaName === 'local' && changes.extensionState) {
327
328
const newState = changes.extensionState.newValue;
328
329
console.log('[Popup] 🔄 Storage changed, new state:', newState);