spaceload 0.1.0__tar.gz
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.
- spaceload-0.1.0/LICENSE +21 -0
- spaceload-0.1.0/PKG-INFO +221 -0
- spaceload-0.1.0/README.md +164 -0
- spaceload-0.1.0/pyproject.toml +61 -0
- spaceload-0.1.0/setup.cfg +4 -0
- spaceload-0.1.0/spaceload/__init__.py +3 -0
- spaceload-0.1.0/spaceload/adapters/__init__.py +1 -0
- spaceload-0.1.0/spaceload/adapters/aerospace/__init__.py +17 -0
- spaceload-0.1.0/spaceload/adapters/aerospace/adapter.py +38 -0
- spaceload-0.1.0/spaceload/adapters/browser/__init__.py +6 -0
- spaceload-0.1.0/spaceload/adapters/browser/arc.py +53 -0
- spaceload-0.1.0/spaceload/adapters/browser/base.py +35 -0
- spaceload-0.1.0/spaceload/adapters/browser/chrome.py +53 -0
- spaceload-0.1.0/spaceload/adapters/browser/firefox.py +169 -0
- spaceload-0.1.0/spaceload/adapters/browser/registry.py +32 -0
- spaceload-0.1.0/spaceload/adapters/browser/safari.py +55 -0
- spaceload-0.1.0/spaceload/adapters/ide/__init__.py +6 -0
- spaceload-0.1.0/spaceload/adapters/ide/base.py +35 -0
- spaceload-0.1.0/spaceload/adapters/ide/cursor.py +209 -0
- spaceload-0.1.0/spaceload/adapters/ide/registry.py +30 -0
- spaceload-0.1.0/spaceload/adapters/ide/vscode.py +352 -0
- spaceload-0.1.0/spaceload/adapters/ide/zed.py +46 -0
- spaceload-0.1.0/spaceload/adapters/terminal/__init__.py +6 -0
- spaceload-0.1.0/spaceload/adapters/terminal/base.py +47 -0
- spaceload-0.1.0/spaceload/adapters/terminal/iterm2.py +261 -0
- spaceload-0.1.0/spaceload/adapters/terminal/kitty.py +54 -0
- spaceload-0.1.0/spaceload/adapters/terminal/registry.py +31 -0
- spaceload-0.1.0/spaceload/adapters/terminal/terminal_app.py +92 -0
- spaceload-0.1.0/spaceload/adapters/terminal/warp.py +33 -0
- spaceload-0.1.0/spaceload/adapters/vpn/__init__.py +6 -0
- spaceload-0.1.0/spaceload/adapters/vpn/base.py +60 -0
- spaceload-0.1.0/spaceload/adapters/vpn/cisco.py +94 -0
- spaceload-0.1.0/spaceload/adapters/vpn/mullvad.py +89 -0
- spaceload-0.1.0/spaceload/adapters/vpn/openvpn.py +94 -0
- spaceload-0.1.0/spaceload/adapters/vpn/registry.py +59 -0
- spaceload-0.1.0/spaceload/adapters/vpn/tailscale.py +91 -0
- spaceload-0.1.0/spaceload/adapters/vpn/tunnelblick.py +96 -0
- spaceload-0.1.0/spaceload/adapters/vpn/wireguard.py +98 -0
- spaceload-0.1.0/spaceload/adapters/wm/__init__.py +11 -0
- spaceload-0.1.0/spaceload/adapters/wm/aerospace.py +69 -0
- spaceload-0.1.0/spaceload/adapters/wm/app_names.py +26 -0
- spaceload-0.1.0/spaceload/adapters/wm/base.py +59 -0
- spaceload-0.1.0/spaceload/adapters/wm/registry.py +39 -0
- spaceload-0.1.0/spaceload/adapters/wm/yabai.py +63 -0
- spaceload-0.1.0/spaceload/cli/__init__.py +1 -0
- spaceload-0.1.0/spaceload/cli/main.py +302 -0
- spaceload-0.1.0/spaceload/daemon/__init__.py +1 -0
- spaceload-0.1.0/spaceload/daemon/server.py +1162 -0
- spaceload-0.1.0/spaceload/replayer/__init__.py +1 -0
- spaceload-0.1.0/spaceload/replayer/replayer.py +535 -0
- spaceload-0.1.0/spaceload/shell/__init__.py +1 -0
- spaceload-0.1.0/spaceload/shell/hooks.py +105 -0
- spaceload-0.1.0/spaceload/store/__init__.py +1 -0
- spaceload-0.1.0/spaceload/store/workspace_store.py +208 -0
- spaceload-0.1.0/spaceload.egg-info/PKG-INFO +221 -0
- spaceload-0.1.0/spaceload.egg-info/SOURCES.txt +73 -0
- spaceload-0.1.0/spaceload.egg-info/dependency_links.txt +1 -0
- spaceload-0.1.0/spaceload.egg-info/entry_points.txt +2 -0
- spaceload-0.1.0/spaceload.egg-info/requires.txt +12 -0
- spaceload-0.1.0/spaceload.egg-info/top_level.txt +1 -0
- spaceload-0.1.0/tests/test_aerospace_adapter.py +468 -0
- spaceload-0.1.0/tests/test_browser_adapters.py +255 -0
- spaceload-0.1.0/tests/test_browser_integration.py +323 -0
- spaceload-0.1.0/tests/test_cli.py +243 -0
- spaceload-0.1.0/tests/test_generic_app_tracking.py +313 -0
- spaceload-0.1.0/tests/test_ide_adapters.py +330 -0
- spaceload-0.1.0/tests/test_ide_integration.py +346 -0
- spaceload-0.1.0/tests/test_integration.py +277 -0
- spaceload-0.1.0/tests/test_shell_hooks.py +76 -0
- spaceload-0.1.0/tests/test_terminal_adapters.py +344 -0
- spaceload-0.1.0/tests/test_terminal_integration.py +348 -0
- spaceload-0.1.0/tests/test_vpn_adapters.py +501 -0
- spaceload-0.1.0/tests/test_vpn_integration.py +330 -0
- spaceload-0.1.0/tests/test_wm_adapters.py +297 -0
- spaceload-0.1.0/tests/test_workspace_store.py +237 -0
spaceload-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
spaceload-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spaceload
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Workspace Context Switcher — record and replay developer workspace setups
|
|
5
|
+
Author: tomjosetj31
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Tom
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/tomjosetj31/spaceload
|
|
29
|
+
Project-URL: Repository, https://github.com/tomjosetj31/spaceload
|
|
30
|
+
Project-URL: Bug Tracker, https://github.com/tomjosetj31/spaceload/issues
|
|
31
|
+
Project-URL: Changelog, https://github.com/tomjosetj31/spaceload/blob/master/CHANGELOG.md
|
|
32
|
+
Keywords: workspace,developer-tools,macos,productivity,cli
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Environment :: Console
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: MacOS
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
43
|
+
Classifier: Topic :: Utilities
|
|
44
|
+
Requires-Python: >=3.11
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE
|
|
47
|
+
Requires-Dist: click>=8.1
|
|
48
|
+
Requires-Dist: PyYAML>=6.0
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
51
|
+
Requires-Dist: pytest-timeout>=2.1; extra == "dev"
|
|
52
|
+
Provides-Extra: firefox
|
|
53
|
+
Requires-Dist: lz4>=4.0; extra == "firefox"
|
|
54
|
+
Provides-Extra: all
|
|
55
|
+
Requires-Dist: lz4>=4.0; extra == "all"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# Spaceload — Workspace Context Switcher
|
|
59
|
+
|
|
60
|
+
[](LICENSE)
|
|
61
|
+
[](https://www.python.org/downloads/)
|
|
62
|
+
[](CONTRIBUTING.md)
|
|
63
|
+
|
|
64
|
+
A macOS CLI tool that records and replays developer workspace setups: browser tabs, VPN connections, IDE projects, and terminal sessions.
|
|
65
|
+
|
|
66
|
+
## Overview
|
|
67
|
+
|
|
68
|
+
`spaceload` lets you snapshot your entire development environment and restore it later with a single command. Stop context-switching overhead and get back into flow faster.
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
- **Record** workspace sessions (browser tabs, VPN, IDE, terminals)
|
|
73
|
+
- **Replay** saved workspaces in one command
|
|
74
|
+
- **Export/Import** workspace definitions as YAML
|
|
75
|
+
- **List** and manage saved workspaces
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/tomjosetj31/spaceload
|
|
81
|
+
cd spaceload
|
|
82
|
+
python -m venv .venv
|
|
83
|
+
source .venv/bin/activate
|
|
84
|
+
pip install -e .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Start recording a workspace session
|
|
91
|
+
spaceload record my-project
|
|
92
|
+
|
|
93
|
+
# ... open your browser tabs, connect VPN, open IDE, etc. ...
|
|
94
|
+
|
|
95
|
+
# Stop recording and save the session
|
|
96
|
+
spaceload stop
|
|
97
|
+
|
|
98
|
+
# Replay a saved workspace
|
|
99
|
+
spaceload run my-project
|
|
100
|
+
|
|
101
|
+
# List all saved workspaces
|
|
102
|
+
spaceload list
|
|
103
|
+
|
|
104
|
+
# Inspect a workspace as YAML
|
|
105
|
+
spaceload show my-project
|
|
106
|
+
|
|
107
|
+
# Delete a workspace
|
|
108
|
+
spaceload delete my-project
|
|
109
|
+
|
|
110
|
+
# Import a workspace from a YAML file
|
|
111
|
+
spaceload import my-project.yaml
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Recording Options
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Record only new things opened during recording (default)
|
|
118
|
+
spaceload record my-project
|
|
119
|
+
|
|
120
|
+
# Also capture everything already open when recording starts
|
|
121
|
+
spaceload record my-project --include-open
|
|
122
|
+
# or
|
|
123
|
+
spaceload record my-project -i
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Supported Integrations
|
|
127
|
+
|
|
128
|
+
### Specialized Adapters (Rich Tracking)
|
|
129
|
+
|
|
130
|
+
These apps have dedicated adapters that track specific details:
|
|
131
|
+
|
|
132
|
+
| Category | Supported | What's Tracked |
|
|
133
|
+
|----------|-----------|----------------|
|
|
134
|
+
| **Browser** | Chrome, Safari, Arc, Firefox | Individual tab URLs |
|
|
135
|
+
| **VPN** | Tailscale, WireGuard, Cisco AnyConnect, Mullvad, OpenVPN, Tunnelblick | Connection state & profile |
|
|
136
|
+
| **IDE** | VS Code, Cursor, Zed | Open project/folder paths |
|
|
137
|
+
| **Terminal** | iTerm2, Terminal.app, Warp, Kitty | Working directories per session, commands (with shell hook) |
|
|
138
|
+
|
|
139
|
+
### Generic App Tracking
|
|
140
|
+
|
|
141
|
+
Any **other application** you open during recording is automatically tracked as an `app_open` action with the app name. This includes apps like Notes, Calendar, Slack, Spotify, etc.
|
|
142
|
+
|
|
143
|
+
### Firefox Support
|
|
144
|
+
|
|
145
|
+
Firefox tab reading requires the `lz4` library to parse session files:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Install with Firefox support
|
|
149
|
+
pip install -e ".[firefox]"
|
|
150
|
+
|
|
151
|
+
# Or install lz4 separately
|
|
152
|
+
pip install lz4
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Without `lz4`, Firefox tabs won't be read during recording, but URLs can still be opened during replay.
|
|
156
|
+
|
|
157
|
+
### Terminal Command Tracking
|
|
158
|
+
|
|
159
|
+
To track terminal commands during recording, add the shell hook to your shell config:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# For zsh (~/.zshrc):
|
|
163
|
+
eval "$(spaceload shell-hook zsh)"
|
|
164
|
+
|
|
165
|
+
# For bash (~/.bashrc):
|
|
166
|
+
eval "$(spaceload shell-hook bash)"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Then restart your shell or run `source ~/.zshrc`.
|
|
170
|
+
|
|
171
|
+
**How it works:**
|
|
172
|
+
- Commands are only tracked when a recording session is active
|
|
173
|
+
- The hook checks for the daemon socket before sending (no overhead when not recording)
|
|
174
|
+
- Commands run in the background so they don't slow down your shell
|
|
175
|
+
- During replay, commands are **displayed but not auto-executed** (for safety)
|
|
176
|
+
|
|
177
|
+
### Smart Browser Filtering
|
|
178
|
+
|
|
179
|
+
The recorder automatically filters out:
|
|
180
|
+
- **New tab pages** (`chrome://newtab/`, `about:newtab`, etc.)
|
|
181
|
+
- **Internal browser pages** (`chrome://`, `about:`, `safari-resource:`, etc.)
|
|
182
|
+
- **Intermediate URLs** (pages open less than 3 seconds)
|
|
183
|
+
- **Redirect chains** (multiple URLs from same domain in quick succession)
|
|
184
|
+
|
|
185
|
+
## Logs & Debugging
|
|
186
|
+
|
|
187
|
+
Logs are written to `~/.spaceload/` for debugging:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Daemon log (recording)
|
|
191
|
+
cat ~/.spaceload/daemon.log
|
|
192
|
+
|
|
193
|
+
# Replay log
|
|
194
|
+
cat ~/.spaceload/replay.log
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Architecture
|
|
198
|
+
|
|
199
|
+
- **CLI** (`spaceload/cli/`) — Click-based command interface
|
|
200
|
+
- **Daemon** (`spaceload/daemon/`) — Unix socket server that records actions in the background
|
|
201
|
+
- **Store** (`spaceload/store/`) — SQLite-backed persistence layer with YAML export/import
|
|
202
|
+
- **Replayer** (`spaceload/replayer/`) — Replays recorded action sequences
|
|
203
|
+
- **Adapters** (`spaceload/adapters/`) — Per-integration plugins (browser, VPN, IDE, terminal)
|
|
204
|
+
|
|
205
|
+
## Tech Stack
|
|
206
|
+
|
|
207
|
+
- Python 3.11+
|
|
208
|
+
- [Click](https://click.palletsprojects.com/) for the CLI
|
|
209
|
+
- SQLite (stdlib) for persistence
|
|
210
|
+
- PyYAML for export/import
|
|
211
|
+
- Unix domain sockets for CLI↔daemon IPC
|
|
212
|
+
|
|
213
|
+
## Contributing
|
|
214
|
+
|
|
215
|
+
Contributions are welcome! Whether it's a bug fix, a new adapter for an app you use, or a documentation improvement — all PRs are appreciated.
|
|
216
|
+
|
|
217
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to set up the project locally, run tests, and submit a pull request.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Spaceload — Workspace Context Switcher
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](CONTRIBUTING.md)
|
|
6
|
+
|
|
7
|
+
A macOS CLI tool that records and replays developer workspace setups: browser tabs, VPN connections, IDE projects, and terminal sessions.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`spaceload` lets you snapshot your entire development environment and restore it later with a single command. Stop context-switching overhead and get back into flow faster.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Record** workspace sessions (browser tabs, VPN, IDE, terminals)
|
|
16
|
+
- **Replay** saved workspaces in one command
|
|
17
|
+
- **Export/Import** workspace definitions as YAML
|
|
18
|
+
- **List** and manage saved workspaces
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/tomjosetj31/spaceload
|
|
24
|
+
cd spaceload
|
|
25
|
+
python -m venv .venv
|
|
26
|
+
source .venv/bin/activate
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Start recording a workspace session
|
|
34
|
+
spaceload record my-project
|
|
35
|
+
|
|
36
|
+
# ... open your browser tabs, connect VPN, open IDE, etc. ...
|
|
37
|
+
|
|
38
|
+
# Stop recording and save the session
|
|
39
|
+
spaceload stop
|
|
40
|
+
|
|
41
|
+
# Replay a saved workspace
|
|
42
|
+
spaceload run my-project
|
|
43
|
+
|
|
44
|
+
# List all saved workspaces
|
|
45
|
+
spaceload list
|
|
46
|
+
|
|
47
|
+
# Inspect a workspace as YAML
|
|
48
|
+
spaceload show my-project
|
|
49
|
+
|
|
50
|
+
# Delete a workspace
|
|
51
|
+
spaceload delete my-project
|
|
52
|
+
|
|
53
|
+
# Import a workspace from a YAML file
|
|
54
|
+
spaceload import my-project.yaml
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Recording Options
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Record only new things opened during recording (default)
|
|
61
|
+
spaceload record my-project
|
|
62
|
+
|
|
63
|
+
# Also capture everything already open when recording starts
|
|
64
|
+
spaceload record my-project --include-open
|
|
65
|
+
# or
|
|
66
|
+
spaceload record my-project -i
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Supported Integrations
|
|
70
|
+
|
|
71
|
+
### Specialized Adapters (Rich Tracking)
|
|
72
|
+
|
|
73
|
+
These apps have dedicated adapters that track specific details:
|
|
74
|
+
|
|
75
|
+
| Category | Supported | What's Tracked |
|
|
76
|
+
|----------|-----------|----------------|
|
|
77
|
+
| **Browser** | Chrome, Safari, Arc, Firefox | Individual tab URLs |
|
|
78
|
+
| **VPN** | Tailscale, WireGuard, Cisco AnyConnect, Mullvad, OpenVPN, Tunnelblick | Connection state & profile |
|
|
79
|
+
| **IDE** | VS Code, Cursor, Zed | Open project/folder paths |
|
|
80
|
+
| **Terminal** | iTerm2, Terminal.app, Warp, Kitty | Working directories per session, commands (with shell hook) |
|
|
81
|
+
|
|
82
|
+
### Generic App Tracking
|
|
83
|
+
|
|
84
|
+
Any **other application** you open during recording is automatically tracked as an `app_open` action with the app name. This includes apps like Notes, Calendar, Slack, Spotify, etc.
|
|
85
|
+
|
|
86
|
+
### Firefox Support
|
|
87
|
+
|
|
88
|
+
Firefox tab reading requires the `lz4` library to parse session files:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Install with Firefox support
|
|
92
|
+
pip install -e ".[firefox]"
|
|
93
|
+
|
|
94
|
+
# Or install lz4 separately
|
|
95
|
+
pip install lz4
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Without `lz4`, Firefox tabs won't be read during recording, but URLs can still be opened during replay.
|
|
99
|
+
|
|
100
|
+
### Terminal Command Tracking
|
|
101
|
+
|
|
102
|
+
To track terminal commands during recording, add the shell hook to your shell config:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# For zsh (~/.zshrc):
|
|
106
|
+
eval "$(spaceload shell-hook zsh)"
|
|
107
|
+
|
|
108
|
+
# For bash (~/.bashrc):
|
|
109
|
+
eval "$(spaceload shell-hook bash)"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Then restart your shell or run `source ~/.zshrc`.
|
|
113
|
+
|
|
114
|
+
**How it works:**
|
|
115
|
+
- Commands are only tracked when a recording session is active
|
|
116
|
+
- The hook checks for the daemon socket before sending (no overhead when not recording)
|
|
117
|
+
- Commands run in the background so they don't slow down your shell
|
|
118
|
+
- During replay, commands are **displayed but not auto-executed** (for safety)
|
|
119
|
+
|
|
120
|
+
### Smart Browser Filtering
|
|
121
|
+
|
|
122
|
+
The recorder automatically filters out:
|
|
123
|
+
- **New tab pages** (`chrome://newtab/`, `about:newtab`, etc.)
|
|
124
|
+
- **Internal browser pages** (`chrome://`, `about:`, `safari-resource:`, etc.)
|
|
125
|
+
- **Intermediate URLs** (pages open less than 3 seconds)
|
|
126
|
+
- **Redirect chains** (multiple URLs from same domain in quick succession)
|
|
127
|
+
|
|
128
|
+
## Logs & Debugging
|
|
129
|
+
|
|
130
|
+
Logs are written to `~/.spaceload/` for debugging:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Daemon log (recording)
|
|
134
|
+
cat ~/.spaceload/daemon.log
|
|
135
|
+
|
|
136
|
+
# Replay log
|
|
137
|
+
cat ~/.spaceload/replay.log
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Architecture
|
|
141
|
+
|
|
142
|
+
- **CLI** (`spaceload/cli/`) — Click-based command interface
|
|
143
|
+
- **Daemon** (`spaceload/daemon/`) — Unix socket server that records actions in the background
|
|
144
|
+
- **Store** (`spaceload/store/`) — SQLite-backed persistence layer with YAML export/import
|
|
145
|
+
- **Replayer** (`spaceload/replayer/`) — Replays recorded action sequences
|
|
146
|
+
- **Adapters** (`spaceload/adapters/`) — Per-integration plugins (browser, VPN, IDE, terminal)
|
|
147
|
+
|
|
148
|
+
## Tech Stack
|
|
149
|
+
|
|
150
|
+
- Python 3.11+
|
|
151
|
+
- [Click](https://click.palletsprojects.com/) for the CLI
|
|
152
|
+
- SQLite (stdlib) for persistence
|
|
153
|
+
- PyYAML for export/import
|
|
154
|
+
- Unix domain sockets for CLI↔daemon IPC
|
|
155
|
+
|
|
156
|
+
## Contributing
|
|
157
|
+
|
|
158
|
+
Contributions are welcome! Whether it's a bug fix, a new adapter for an app you use, or a documentation improvement — all PRs are appreciated.
|
|
159
|
+
|
|
160
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to set up the project locally, run tests, and submit a pull request.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spaceload"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Workspace Context Switcher — record and replay developer workspace setups"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "tomjosetj31" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["workspace", "developer-tools", "macos", "productivity", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: MacOS",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
27
|
+
"Topic :: Utilities",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"click>=8.1",
|
|
31
|
+
"PyYAML>=6.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/tomjosetj31/spaceload"
|
|
36
|
+
Repository = "https://github.com/tomjosetj31/spaceload"
|
|
37
|
+
"Bug Tracker" = "https://github.com/tomjosetj31/spaceload/issues"
|
|
38
|
+
Changelog = "https://github.com/tomjosetj31/spaceload/blob/master/CHANGELOG.md"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
spaceload = "spaceload.cli.main:cli"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=7.4",
|
|
46
|
+
"pytest-timeout>=2.1",
|
|
47
|
+
]
|
|
48
|
+
firefox = [
|
|
49
|
+
"lz4>=4.0", # Required for reading Firefox session files
|
|
50
|
+
]
|
|
51
|
+
all = [
|
|
52
|
+
"lz4>=4.0",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
where = ["."]
|
|
57
|
+
include = ["spaceload*"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
timeout = 30
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapters package for ctx."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""AeroSpace adapter package for ctx."""
|
|
2
|
+
|
|
3
|
+
from spaceload.adapters.aerospace.adapter import (
|
|
4
|
+
AeroSpaceAdapter,
|
|
5
|
+
AeroWindow,
|
|
6
|
+
BROWSER_APP_NAMES,
|
|
7
|
+
IDE_APP_NAMES,
|
|
8
|
+
TERMINAL_APP_NAMES,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AeroSpaceAdapter",
|
|
13
|
+
"AeroWindow",
|
|
14
|
+
"BROWSER_APP_NAMES",
|
|
15
|
+
"IDE_APP_NAMES",
|
|
16
|
+
"TERMINAL_APP_NAMES",
|
|
17
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""AeroSpace adapter — re-exported from spaceload.adapters.wm for backward compatibility.
|
|
2
|
+
|
|
3
|
+
New code should import from spaceload.adapters.wm directly.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from spaceload.adapters.wm.aerospace import AeroSpaceAdapter
|
|
9
|
+
from spaceload.adapters.wm.base import WMWindow as AeroWindow # legacy alias
|
|
10
|
+
|
|
11
|
+
# App name maps kept here for backward compat; canonical home is ctx.adapters.wm.app_names
|
|
12
|
+
BROWSER_APP_NAMES: dict[str, str] = {
|
|
13
|
+
"chrome": "Google Chrome",
|
|
14
|
+
"safari": "Safari",
|
|
15
|
+
"arc": "Arc",
|
|
16
|
+
"firefox": "Firefox",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
IDE_APP_NAMES: dict[str, str] = {
|
|
20
|
+
"vscode": "Code",
|
|
21
|
+
"cursor": "Cursor",
|
|
22
|
+
"zed": "Zed",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
TERMINAL_APP_NAMES: dict[str, str] = {
|
|
26
|
+
"iterm2": "iTerm2",
|
|
27
|
+
"terminal": "Terminal",
|
|
28
|
+
"warp": "Warp",
|
|
29
|
+
"kitty": "kitty",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AeroSpaceAdapter",
|
|
34
|
+
"AeroWindow",
|
|
35
|
+
"BROWSER_APP_NAMES",
|
|
36
|
+
"IDE_APP_NAMES",
|
|
37
|
+
"TERMINAL_APP_NAMES",
|
|
38
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Arc browser adapter using AppleScript."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from spaceload.adapters.browser.base import BrowserAdapter
|
|
8
|
+
|
|
9
|
+
_GET_TABS_SCRIPT = """\
|
|
10
|
+
tell application "Arc"
|
|
11
|
+
set tabURLs to {}
|
|
12
|
+
repeat with w in windows
|
|
13
|
+
repeat with t in tabs of w
|
|
14
|
+
set end of tabURLs to URL of t
|
|
15
|
+
end repeat
|
|
16
|
+
end repeat
|
|
17
|
+
set AppleScript's text item delimiters to "\\n"
|
|
18
|
+
return tabURLs as text
|
|
19
|
+
end tell
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ArcAdapter(BrowserAdapter):
|
|
24
|
+
"""Adapter for the Arc browser on macOS using AppleScript."""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
return "arc"
|
|
29
|
+
|
|
30
|
+
def is_available(self) -> bool:
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
["pgrep", "-x", "Arc"],
|
|
33
|
+
capture_output=True,
|
|
34
|
+
)
|
|
35
|
+
return result.returncode == 0
|
|
36
|
+
|
|
37
|
+
def get_open_tabs(self) -> list[str]:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["osascript", "-e", _GET_TABS_SCRIPT],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=5,
|
|
43
|
+
)
|
|
44
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
45
|
+
return []
|
|
46
|
+
return [u for u in result.stdout.strip().splitlines() if u.strip()]
|
|
47
|
+
|
|
48
|
+
def open_url(self, url: str) -> bool:
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
["open", "-a", "Arc", url],
|
|
51
|
+
capture_output=True,
|
|
52
|
+
)
|
|
53
|
+
return result.returncode == 0
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Browser adapter base class for ctx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class TabSet:
|
|
11
|
+
"""Represents the current open tabs in a browser."""
|
|
12
|
+
|
|
13
|
+
browser: str
|
|
14
|
+
urls: list[str] = field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BrowserAdapter(ABC):
|
|
18
|
+
"""Abstract base class for browser adapters."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
"""Unique identifier for this browser (e.g. 'chrome', 'safari')."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def is_available(self) -> bool:
|
|
27
|
+
"""Return True if this browser is currently running."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def get_open_tabs(self) -> list[str]:
|
|
31
|
+
"""Return the URLs of all currently open tabs."""
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def open_url(self, url: str) -> bool:
|
|
35
|
+
"""Open *url* in this browser. Return True on success."""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Google Chrome browser adapter using AppleScript."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from spaceload.adapters.browser.base import BrowserAdapter
|
|
8
|
+
|
|
9
|
+
_GET_TABS_SCRIPT = """\
|
|
10
|
+
tell application "Google Chrome"
|
|
11
|
+
set tabURLs to {}
|
|
12
|
+
repeat with w in windows
|
|
13
|
+
repeat with t in tabs of w
|
|
14
|
+
set end of tabURLs to URL of t
|
|
15
|
+
end repeat
|
|
16
|
+
end repeat
|
|
17
|
+
set AppleScript's text item delimiters to "\\n"
|
|
18
|
+
return tabURLs as text
|
|
19
|
+
end tell
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChromeAdapter(BrowserAdapter):
|
|
24
|
+
"""Adapter for Google Chrome on macOS using AppleScript."""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
return "chrome"
|
|
29
|
+
|
|
30
|
+
def is_available(self) -> bool:
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
["pgrep", "-x", "Google Chrome"],
|
|
33
|
+
capture_output=True,
|
|
34
|
+
)
|
|
35
|
+
return result.returncode == 0
|
|
36
|
+
|
|
37
|
+
def get_open_tabs(self) -> list[str]:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["osascript", "-e", _GET_TABS_SCRIPT],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=5,
|
|
43
|
+
)
|
|
44
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
45
|
+
return []
|
|
46
|
+
return [u for u in result.stdout.strip().splitlines() if u.strip()]
|
|
47
|
+
|
|
48
|
+
def open_url(self, url: str) -> bool:
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
["open", "-a", "Google Chrome", url],
|
|
51
|
+
capture_output=True,
|
|
52
|
+
)
|
|
53
|
+
return result.returncode == 0
|