taskdex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/package.json +16 -0
- package/scripts/taskdex.mjs +279 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dhruval Golakiya
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Codex Mobile
|
|
2
|
+
|
|
3
|
+
Control [OpenAI Codex](https://openai.com/index/openai-codex/) coding agents from your phone. Run multiple agents simultaneously, get push notifications when tasks complete, and reply directly from your lock screen.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌─────────────────┐ WebSocket ┌──────────────────┐
|
|
7
|
+
│ Mobile App │◄──────────────────►│ Bridge Server │
|
|
8
|
+
│ (React Native) │ ws://<ip>:3001 │ (Node.js) │
|
|
9
|
+
└─────────────────┘ │ │
|
|
10
|
+
│ Spawns & manages │
|
|
11
|
+
│ codex app-server │
|
|
12
|
+
│ processes (stdio)│
|
|
13
|
+
└───┬───┬───┬───────┘
|
|
14
|
+
│ │ │
|
|
15
|
+
codex1 codex2 codex3
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The **bridge server** runs on your Mac/PC alongside the Codex CLI. The **mobile app** connects to it over your local network (or remotely via Tailscale) and gives you a chat UI to interact with each agent.
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
- **Node.js** >= 18
|
|
23
|
+
- **OpenAI Codex CLI** installed and authenticated (`codex` command available in your terminal)
|
|
24
|
+
- **Expo CLI**: `npm install -g expo-cli`
|
|
25
|
+
- **iOS device** or Android device (for push notifications and Live Activity, a physical device is required)
|
|
26
|
+
- Both your computer and phone on the **same network** (or connected via [Tailscale](https://tailscale.com) for remote access)
|
|
27
|
+
|
|
28
|
+
## Install via npm (Easiest)
|
|
29
|
+
|
|
30
|
+
After publishing this package to npm, users can install and launch with:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm i -g taskdex
|
|
34
|
+
taskdex init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This will clone the repo and immediately run interactive terminal setup.
|
|
38
|
+
|
|
39
|
+
For an already-cloned repo:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx taskdex setup
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## One-Command Terminal Setup (Recommended)
|
|
46
|
+
|
|
47
|
+
Use this flow for first-time setup with terminal prompts for bridge values, automatic dependency install, and an Expo QR code at the end:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone <your-repo-url> codex-mobile
|
|
51
|
+
cd codex-mobile
|
|
52
|
+
node scripts/setup-and-start.mjs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
What this script does:
|
|
56
|
+
|
|
57
|
+
- prompts for `PORT`, `API key`, and Expo mode (`lan` or `tunnel`)
|
|
58
|
+
- installs dependencies in `bridge-server` and `mobile`
|
|
59
|
+
- starts the bridge server with your chosen values
|
|
60
|
+
- starts Expo and prints the QR code to open the app
|
|
61
|
+
- injects bridge URL and API key into Expo so the app opens pre-configured
|
|
62
|
+
|
|
63
|
+
## Bridge Server
|
|
64
|
+
|
|
65
|
+
The bridge server spawns and manages `codex app-server` child processes, exposing them over a WebSocket API.
|
|
66
|
+
|
|
67
|
+
### Setup
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cd bridge-server
|
|
71
|
+
npm install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Run
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm run dev
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This starts the server on port **3001**. You'll see output like:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Codex Bridge Server running
|
|
84
|
+
Local: ws://localhost:3001
|
|
85
|
+
Network: ws://192.168.1.42:3001
|
|
86
|
+
API key: <generated-key>
|
|
87
|
+
[terminal QR code]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Note the **Network** URL and **API key** — or scan the terminal QR from mobile settings (`Scan QR`) to auto-fill both.
|
|
91
|
+
|
|
92
|
+
### Production
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm run build
|
|
96
|
+
npm start
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Health Check (authenticated)
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
GET http://localhost:3001/health?key=<api-key>
|
|
103
|
+
# or
|
|
104
|
+
Authorization: Bearer <api-key>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Returns uptime, active agent count, connected client count, push counts, and system info.
|
|
108
|
+
|
|
109
|
+
### Docker
|
|
110
|
+
|
|
111
|
+
Run the bridge in Docker with a mounted code workspace:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
docker compose up -d --build
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Environment variables used by `docker-compose.yml`:
|
|
118
|
+
|
|
119
|
+
- `PORT` (default `3001`)
|
|
120
|
+
- `API_KEY` (required in production)
|
|
121
|
+
- `CODEX_CWD` (container path to workspace, default `/workspace`)
|
|
122
|
+
- `HOST_CODE_DIR` (host path mounted into `CODEX_CWD`)
|
|
123
|
+
- `REPOS_DIR` (path where remote-managed repos are cloned)
|
|
124
|
+
- `AUTO_PULL_REPOS` (`true`/`false`, optional: pull before `create_agent` when cwd is under `REPOS_DIR`)
|
|
125
|
+
- `OPENAI_API_KEY` (required for Codex CLI)
|
|
126
|
+
|
|
127
|
+
### VPS Deployment (Hetzner + Caddy)
|
|
128
|
+
|
|
129
|
+
Use the full step-by-step guide in `docs/vps-hetzner.md`:
|
|
130
|
+
|
|
131
|
+
- Hetzner server provisioning
|
|
132
|
+
- Docker installation and bridge deployment
|
|
133
|
+
- SSH deploy key setup for private repo cloning
|
|
134
|
+
- Caddy reverse proxy for `wss://...`
|
|
135
|
+
- systemd/PM2 service persistence
|
|
136
|
+
- Optional auto-pull before agent start
|
|
137
|
+
|
|
138
|
+
### API
|
|
139
|
+
|
|
140
|
+
The bridge accepts JSON messages over WebSocket:
|
|
141
|
+
|
|
142
|
+
| Action | Params | Description |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `create_agent` | `{ name, model, cwd, approvalPolicy?, systemPrompt? }` | Spawn a new Codex agent |
|
|
145
|
+
| `list_agents` | — | List all running agents |
|
|
146
|
+
| `send_message` | `{ agentId, text }` | Send a message to an agent |
|
|
147
|
+
| `interrupt` | `{ agentId }` | Interrupt an agent's current turn |
|
|
148
|
+
| `stop_agent` | `{ agentId }` | Stop and kill an agent process |
|
|
149
|
+
| `update_agent_model` | `{ agentId, model }` | Change an agent's model |
|
|
150
|
+
| `update_agent_config` | `{ agentId, model?, approvalPolicy?, systemPrompt? }` | Update agent runtime config |
|
|
151
|
+
| `get_agent` | `{ agentId }` | Get details for a specific agent |
|
|
152
|
+
| `register_push_token` | `{ token }` | Register an Expo push token for notifications |
|
|
153
|
+
| `update_notification_prefs` | `{ agentId, level }` | Set per-agent notifications (`all`, `errors`, `muted`) |
|
|
154
|
+
| `get_notification_prefs` | — | Get current per-agent notification preferences |
|
|
155
|
+
| `list_notification_history` | `{ limit? }` | List recent bridge notification send history |
|
|
156
|
+
| `list_files` | `{ cwd, path }` | List files/directories in workspace |
|
|
157
|
+
| `list_directories` | `{ cwd, path }` | List only directories for cwd browsing |
|
|
158
|
+
| `read_file` | `{ cwd, path }` | Read file contents |
|
|
159
|
+
| `git_status` | `{ cwd }` | Get git branch/dirty state |
|
|
160
|
+
| `git_log` | `{ cwd, limit? }` | Get commit history |
|
|
161
|
+
| `git_diff` | `{ cwd, file? }` | Get current diff |
|
|
162
|
+
| `git_commit` | `{ cwd, message }` | Commit all current changes |
|
|
163
|
+
| `git_branches` | `{ cwd }` | List local branches |
|
|
164
|
+
| `git_checkout` | `{ cwd, branch }` | Switch to local branch |
|
|
165
|
+
| `clone_repo` | `{ url }` | Clone repository to bridge `REPOS_DIR` |
|
|
166
|
+
| `list_repos` | — | List cloned repositories with paths/remotes |
|
|
167
|
+
| `pull_repo` | `{ path }` | Pull latest changes for a cloned repo |
|
|
168
|
+
|
|
169
|
+
The bridge streams events back to the mobile app (e.g. `turn/started`, `item/agentMessage/delta`, `turn/completed`).
|
|
170
|
+
|
|
171
|
+
## Mobile App
|
|
172
|
+
|
|
173
|
+
React Native app built with Expo.
|
|
174
|
+
|
|
175
|
+
### Setup
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
cd mobile
|
|
179
|
+
npm install
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Run (Expo Go)
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npx expo start
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Scan the QR code with your phone. On first launch, enter the bridge server's **Network URL** (e.g. `ws://192.168.1.42:3001`).
|
|
189
|
+
|
|
190
|
+
### Development Build (recommended)
|
|
191
|
+
|
|
192
|
+
A dev build is required for push notifications and Live Activity:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx expo prebuild --clean
|
|
196
|
+
npx expo run:ios
|
|
197
|
+
# or
|
|
198
|
+
npx expo run:android
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Features
|
|
202
|
+
|
|
203
|
+
- **Multiple agents** — Create and manage several Codex agents at once
|
|
204
|
+
- **Streaming responses** — See agent output in real time as it types
|
|
205
|
+
- **Message queue** — Send messages while agent is busy; they'll be delivered when it's ready
|
|
206
|
+
- **Agent persistence** — Agents survive app restarts; stopped agents auto-reconnect when you send a new message
|
|
207
|
+
- **Push notifications** — Get notified when agents finish tasks, even with the app closed
|
|
208
|
+
- **Interactive replies** — Reply to agents directly from notification (hold/long-press the notification)
|
|
209
|
+
- **Action buttons** — Stop Agent and Open Thread buttons on notifications
|
|
210
|
+
- **Live Activity (iOS)** — Real-time agent status in Dynamic Island and lock screen
|
|
211
|
+
- **QR connect** — Scan bridge terminal QR to auto-fill URL + API key
|
|
212
|
+
- **Dark mode** — Light and dark theme support
|
|
213
|
+
|
|
214
|
+
## Remote Access with Tailscale
|
|
215
|
+
|
|
216
|
+
To use the app outside your local network:
|
|
217
|
+
|
|
218
|
+
1. Install [Tailscale](https://tailscale.com) on both your computer and phone
|
|
219
|
+
2. Sign in with the same account on both devices
|
|
220
|
+
3. Use your computer's **Tailscale IP** (e.g. `ws://100.x.x.x:3001`) as the bridge URL in the mobile app
|
|
221
|
+
|
|
222
|
+
## Project Structure
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
codex-mobile/
|
|
226
|
+
├── bridge-server/
|
|
227
|
+
│ ├── src/
|
|
228
|
+
│ │ ├── index.ts # Express + WebSocket server
|
|
229
|
+
│ │ ├── agent-manager.ts # Spawns/manages codex app-server processes
|
|
230
|
+
│ │ ├── protocol.ts # JSON-RPC message helpers
|
|
231
|
+
│ │ └── push.ts # Expo Push Notification sender
|
|
232
|
+
│ ├── package.json
|
|
233
|
+
│ └── tsconfig.json
|
|
234
|
+
├── mobile/
|
|
235
|
+
│ ├── App.tsx # Main app entry (navigation, notifications, UI)
|
|
236
|
+
│ ├── components/
|
|
237
|
+
│ │ ├── AgentCard.tsx # Agent card for list view
|
|
238
|
+
│ │ ├── ChatBubble.tsx # Message bubble with markdown
|
|
239
|
+
│ │ ├── MessageInput.tsx # Text input + send button
|
|
240
|
+
│ │ ├── QueuePanel.tsx # Queued messages panel
|
|
241
|
+
│ │ └── TypingIndicator.tsx
|
|
242
|
+
│ ├── hooks/
|
|
243
|
+
│ │ ├── useWebSocket.ts # WebSocket connection & stream handling
|
|
244
|
+
│ │ └── useLiveActivity.ts # iOS Live Activity integration
|
|
245
|
+
│ ├── stores/
|
|
246
|
+
│ │ ├── agentStore.ts # Agent state (Zustand + AsyncStorage)
|
|
247
|
+
│ │ ├── workspaceStore.ts # Workspace management
|
|
248
|
+
│ │ └── themeStore.ts # Theme preferences
|
|
249
|
+
│ ├── types/index.ts # TypeScript types
|
|
250
|
+
│ ├── theme.ts # Colors, typography, palettes
|
|
251
|
+
│ ├── app.json
|
|
252
|
+
│ └── package.json
|
|
253
|
+
└── README.md
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "taskdex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal-first installer and launcher for Taskdex mobile + bridge",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"taskdex": "./scripts/taskdex.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"scripts/taskdex.mjs"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT"
|
|
16
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { createInterface } from 'node:readline/promises';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_REPO_URL = process.env.TASKDEX_REPO_URL || 'https://github.com/DhruvalGolakiya/pylon.git';
|
|
12
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
13
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log(`
|
|
17
|
+
Taskdex CLI
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
taskdex init [directory] [--repo <url>] [--no-start]
|
|
21
|
+
taskdex setup [directory]
|
|
22
|
+
taskdex --help
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
taskdex init
|
|
26
|
+
taskdex init my-taskdex
|
|
27
|
+
taskdex init my-taskdex --repo https://github.com/your-org/your-repo.git
|
|
28
|
+
taskdex setup
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runCommand(command, args, options = {}) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawn(command, args, { stdio: 'inherit', ...options });
|
|
35
|
+
child.on('error', reject);
|
|
36
|
+
child.on('close', (code) => {
|
|
37
|
+
if (code === 0) {
|
|
38
|
+
resolve();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 'unknown'}`));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isTaskdexRepo(dir) {
|
|
47
|
+
return (
|
|
48
|
+
existsSync(path.join(dir, 'bridge-server')) &&
|
|
49
|
+
existsSync(path.join(dir, 'mobile'))
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findTaskdexRoot(baseDir) {
|
|
54
|
+
const resolved = path.resolve(baseDir);
|
|
55
|
+
if (isTaskdexRepo(resolved)) return resolved;
|
|
56
|
+
|
|
57
|
+
let children = [];
|
|
58
|
+
try {
|
|
59
|
+
children = readdirSync(resolved, { withFileTypes: true });
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const child of children) {
|
|
65
|
+
if (!child.isDirectory()) continue;
|
|
66
|
+
const candidate = path.join(resolved, child.name);
|
|
67
|
+
if (isTaskdexRepo(candidate)) return candidate;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseInitArgs(argv) {
|
|
74
|
+
let directory = 'codex-mobile';
|
|
75
|
+
let repo = DEFAULT_REPO_URL;
|
|
76
|
+
let noStart = false;
|
|
77
|
+
const positional = [];
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
80
|
+
const arg = argv[i];
|
|
81
|
+
if (arg === '--repo') {
|
|
82
|
+
const next = argv[i + 1];
|
|
83
|
+
if (!next) throw new Error('--repo requires a value');
|
|
84
|
+
repo = next;
|
|
85
|
+
i += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg === '--no-start') {
|
|
89
|
+
noStart = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg.startsWith('-')) {
|
|
93
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
94
|
+
}
|
|
95
|
+
positional.push(arg);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (positional[0]) directory = positional[0];
|
|
99
|
+
return { directory, repo, noStart };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveTaskdexRoot(baseDir) {
|
|
103
|
+
const rootDir = findTaskdexRoot(baseDir);
|
|
104
|
+
if (!rootDir) {
|
|
105
|
+
const attempted = path.resolve(baseDir);
|
|
106
|
+
throw new Error(`Could not find Taskdex repo at ${attempted}. Expected bridge-server and mobile at root or one level below.`);
|
|
107
|
+
}
|
|
108
|
+
return rootDir;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getLocalIPv4() {
|
|
112
|
+
const interfaces = os.networkInterfaces();
|
|
113
|
+
for (const name of Object.keys(interfaces)) {
|
|
114
|
+
for (const iface of interfaces[name] || []) {
|
|
115
|
+
if (iface.family === 'IPv4' && !iface.internal) return iface.address;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return '127.0.0.1';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parsePort(input) {
|
|
122
|
+
const value = Number(input);
|
|
123
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
124
|
+
throw new Error('Port must be an integer between 1 and 65535.');
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseExpoMode(input) {
|
|
130
|
+
const normalized = input.trim().toLowerCase() || 'lan';
|
|
131
|
+
if (!['lan', 'tunnel'].includes(normalized)) {
|
|
132
|
+
throw new Error('Expo mode must be either "lan" or "tunnel".');
|
|
133
|
+
}
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseInstallChoice(input) {
|
|
138
|
+
const normalized = input.trim().toLowerCase();
|
|
139
|
+
if (!normalized || normalized === 'y' || normalized === 'yes') return true;
|
|
140
|
+
if (normalized === 'n' || normalized === 'no') return false;
|
|
141
|
+
throw new Error('Install choice must be Y or N.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wait(ms) {
|
|
145
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function readSetupConfig() {
|
|
149
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
150
|
+
try {
|
|
151
|
+
console.log('\nTaskdex terminal setup\n');
|
|
152
|
+
const portInput = await rl.question('Bridge port [3001]: ');
|
|
153
|
+
const apiKeyInput = await rl.question('Bridge API key [auto-generate]: ');
|
|
154
|
+
const expoModeInput = await rl.question('Expo mode (lan/tunnel) [lan]: ');
|
|
155
|
+
const installInput = await rl.question('Install npm dependencies first? [Y/n]: ');
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
port: parsePort((portInput || '3001').trim()),
|
|
159
|
+
apiKey: apiKeyInput.trim() || crypto.randomBytes(24).toString('hex'),
|
|
160
|
+
expoMode: parseExpoMode(expoModeInput),
|
|
161
|
+
installDeps: parseInstallChoice(installInput),
|
|
162
|
+
};
|
|
163
|
+
} finally {
|
|
164
|
+
rl.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function runInteractiveSetup(rootDir) {
|
|
169
|
+
const bridgeDir = path.join(rootDir, 'bridge-server');
|
|
170
|
+
const mobileDir = path.join(rootDir, 'mobile');
|
|
171
|
+
if (!existsSync(bridgeDir) || !existsSync(mobileDir)) {
|
|
172
|
+
throw new Error(`Invalid Taskdex repository at ${rootDir}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { port, apiKey, expoMode, installDeps } = await readSetupConfig();
|
|
176
|
+
const bridgeUrl = `ws://${getLocalIPv4()}:${port}`;
|
|
177
|
+
|
|
178
|
+
console.log(`\nBridge URL: ${bridgeUrl}`);
|
|
179
|
+
console.log(`Bridge API key: ${apiKey}`);
|
|
180
|
+
console.log(`Expo mode: ${expoMode}\n`);
|
|
181
|
+
|
|
182
|
+
if (installDeps) {
|
|
183
|
+
console.log('Installing bridge dependencies...\n');
|
|
184
|
+
await runCommand(npmCmd, ['install'], { cwd: bridgeDir });
|
|
185
|
+
console.log('\nInstalling mobile dependencies...\n');
|
|
186
|
+
await runCommand(npmCmd, ['install'], { cwd: mobileDir });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('\nStarting bridge server...\n');
|
|
190
|
+
const bridgeProcess = spawn(npmCmd, ['run', 'dev'], {
|
|
191
|
+
cwd: bridgeDir,
|
|
192
|
+
stdio: 'inherit',
|
|
193
|
+
env: {
|
|
194
|
+
...process.env,
|
|
195
|
+
PORT: String(port),
|
|
196
|
+
API_KEY: apiKey,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
bridgeProcess.on('error', (error) => {
|
|
201
|
+
console.error(`Bridge failed to start: ${error.message}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await wait(1200);
|
|
206
|
+
|
|
207
|
+
console.log('\nStarting Expo. Scan the QR code to open the app.');
|
|
208
|
+
console.log('Bridge URL and API key are prefilled from this terminal setup.\n');
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await runCommand(npxCmd, ['expo', 'start', `--${expoMode}`], {
|
|
212
|
+
cwd: mobileDir,
|
|
213
|
+
env: {
|
|
214
|
+
...process.env,
|
|
215
|
+
EXPO_PUBLIC_BRIDGE_URL: bridgeUrl,
|
|
216
|
+
EXPO_PUBLIC_BRIDGE_API_KEY: apiKey,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
} finally {
|
|
220
|
+
if (!bridgeProcess.killed) {
|
|
221
|
+
bridgeProcess.kill('SIGTERM');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handleInit(argv) {
|
|
227
|
+
const { directory, repo, noStart } = parseInitArgs(argv);
|
|
228
|
+
const targetDir = path.resolve(process.cwd(), directory);
|
|
229
|
+
|
|
230
|
+
if (existsSync(targetDir)) {
|
|
231
|
+
throw new Error(`Target directory already exists: ${targetDir}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`\nCloning Taskdex into ${targetDir}`);
|
|
235
|
+
await runCommand('git', ['clone', repo, targetDir]);
|
|
236
|
+
|
|
237
|
+
if (noStart) {
|
|
238
|
+
console.log('\nRepository installed.');
|
|
239
|
+
console.log(`Next: taskdex setup ${directory}`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const rootDir = resolveTaskdexRoot(targetDir);
|
|
244
|
+
console.log('\nStarting interactive setup...\n');
|
|
245
|
+
await runInteractiveSetup(rootDir);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function handleSetup(argv) {
|
|
249
|
+
const baseDir = argv[0] ? path.resolve(process.cwd(), argv[0]) : process.cwd();
|
|
250
|
+
const rootDir = resolveTaskdexRoot(baseDir);
|
|
251
|
+
await runInteractiveSetup(rootDir);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function main() {
|
|
255
|
+
const argv = process.argv.slice(2);
|
|
256
|
+
const command = argv[0];
|
|
257
|
+
|
|
258
|
+
if (!command || command === 'init') {
|
|
259
|
+
await handleInit(command === 'init' ? argv.slice(1) : argv);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (command === 'setup') {
|
|
264
|
+
await handleSetup(argv.slice(1));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
269
|
+
printHelp();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
throw new Error(`Unknown command: ${command}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
main().catch((error) => {
|
|
277
|
+
console.error(`\nTaskdex CLI error: ${error.message}`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|