@juppytt/fws 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.
@@ -0,0 +1,72 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(gws --help)",
5
+ "Bash(gws gmail:*)",
6
+ "Bash(gws auth:*)",
7
+ "Bash(gws schema:*)",
8
+ "Bash(npm list:*)",
9
+ "Bash(mockoon-cli --help)",
10
+ "Bash(GOOGLE_WORKSPACE_CLI_LOG=gws=debug gws gmail users messages list --params '{\"userId\":\"me\",\"maxResults\":1}')",
11
+ "Bash(GOOGLE_WORKSPACE_CLI_LOG=gws=trace gws gmail users messages list --params '{\"userId\":\"me\",\"maxResults\":1}')",
12
+ "Bash(gws)",
13
+ "Bash(env)",
14
+ "Bash(HTTPS_PROXY=http://localhost:9999 GOOGLE_WORKSPACE_CLI_TOKEN=fake gws gmail users messages list --params '{\"userId\":\"me\",\"maxResults\":1}')",
15
+ "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(d.get\\('rootUrl','N/A'\\)\\); print\\(d.get\\('baseUrl','N/A'\\)\\); print\\(d.get\\('basePath','N/A'\\)\\); print\\(d.get\\('servicePath','N/A'\\)\\)\")",
16
+ "Bash(cp ~/.config/gws/cache/gmail_v1.json /tmp/gmail_v1_backup.json)",
17
+ "Bash(python3 -c ':*)",
18
+ "Bash(npm --version)",
19
+ "Bash(tsc --version)",
20
+ "Bash(ls:*)",
21
+ "Bash(python3:*)",
22
+ "Bash(npm install:*)",
23
+ "Bash(npx tsc:*)",
24
+ "Bash(npx vitest:*)",
25
+ "Bash(npx tsx:*)",
26
+ "Bash(git add:*)",
27
+ "Bash(git commit -m ':*)",
28
+ "Bash(chmod +x:*)",
29
+ "Bash(npm link:*)",
30
+ "Bash(fws --help)",
31
+ "Bash(fws drive:*)",
32
+ "Bash(fws server:*)",
33
+ "Bash(export GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/home/juhee/.local/share/fws/config)",
34
+ "Bash(export GOOGLE_WORKSPACE_CLI_TOKEN=fake)",
35
+ "Bash(curl -s http://localhost:4100/__fws/status)",
36
+ "Bash(kill %1)",
37
+ "Bash(kill 1088227)",
38
+ "Bash(gws drive:*)",
39
+ "Bash(GOOGLE_WORKSPACE_CLI_LOG=gws=debug gws gmail +triage --max 2)",
40
+ "Bash(GOOGLE_WORKSPACE_CLI_LOG=gws=trace gws gmail +triage --max 1)",
41
+ "Bash(kill 1093361)",
42
+ "Bash(wait)",
43
+ "Bash(cp /home/juhee/.config/gws/accounts.json /home/juhee/.local/share/fws/config/)",
44
+ "Bash(node -e ':*)",
45
+ "Bash(xargs kill:*)",
46
+ "Bash(HTTPS_PROXY=http://localhost:19999 gws gmail +triage --max 1)",
47
+ "Bash(HTTPS_PROXY=http://localhost:19999 gws gmail users messages list --params '{\"userId\":\"me\",\"maxResults\":1}')",
48
+ "Bash(find /home/juhee/headless-clawd/projects/agent-dfi/gws-src -name \"*.rs\" -exec grep -l \"config\\\\|Config\" {} \\\\;)",
49
+ "Bash(node -e \"const tls = require\\('tls'\\); const crypto = require\\('crypto'\\); console.log\\('crypto available:', !!crypto.generateKeyPairSync\\);\")",
50
+ "Bash(export HTTPS_PROXY=http://localhost:4101)",
51
+ "Bash(export SSL_CERT_FILE=/home/juhee/.local/share/fws/certs/ca.crt)",
52
+ "Bash(fws gmail:*)",
53
+ "Bash(gws calendar:*)",
54
+ "Bash(echo \"EXIT: $?\")",
55
+ "Bash(gh repo:*)",
56
+ "Bash(git:*)",
57
+ "Bash(gh issue create --repo pinchbench/skill --title 'Proposal: GWS-powered user tasks using fws \\(Fake Google Workspace\\)' --body ':*)",
58
+ "Bash(gws tasks:*)",
59
+ "Bash(./scripts/run.sh --suite task_26_gws_email_triage --model anthropic/claude-sonnet-4 --no-upload -v)",
60
+ "Bash(./scripts/run.sh --suite task_26_gws_email_triage --model openai/gpt-4o --no-upload -v)",
61
+ "Bash(./scripts/run.sh --suite task_26_gws_email_triage --model openai/gpt-4.1 --no-upload -v)",
62
+ "Bash(./scripts/run.sh --suite task_16_email_triage --model openai/gpt-4.1 --no-upload -v)",
63
+ "Bash(grep:*)",
64
+ "Bash(gh pr create --repo pinchbench/skill --title 'Fix judge API: use max_completion_tokens for OpenAI/OpenRouter' --body ':*)",
65
+ "Bash(npm whoami:*)",
66
+ "Bash(npm view:*)",
67
+ "Bash(mock-gws --help)",
68
+ "Bash(mock-gws gmail:*)",
69
+ "Bash(npm publish:*)"
70
+ ]
71
+ }
72
+ }
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # fws — Fake Google Workspace
2
+
3
+ A local mock server that redirects the `gws` CLI, enabling Google Workspace testing without OAuth authentication.
4
+
5
+ Built with [Claude Code](https://claude.ai/code).
6
+
7
+ ## How it works
8
+
9
+ `gws` sends API requests to the `rootUrl` defined in its discovery cache (`~/.config/gws/cache/*.json`). fws rewrites these URLs to `http://localhost:4100/` and sets `GOOGLE_WORKSPACE_CLI_TOKEN=fake` to bypass auth.
10
+
11
+ For helper commands (`+triage`, `+send`, `+reply`, etc.) that hardcode `googleapis.com` URLs, fws runs a MITM CONNECT proxy on port 4101 that intercepts HTTPS traffic and forwards it to the local mock server.
12
+
13
+ All data lives **in memory**. When the server stops, everything is lost unless you save a snapshot first. Use `fws snapshot save` to persist state.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install
19
+ npm link # makes `fws` command available globally
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ # Start the server (runs in background)
26
+ fws server start
27
+
28
+ # Set env vars (printed by the command above)
29
+ export GOOGLE_WORKSPACE_CLI_CONFIG_DIR=~/.local/share/fws/config
30
+ export GOOGLE_WORKSPACE_CLI_TOKEN=fake
31
+ export HTTPS_PROXY=http://localhost:4101
32
+ export SSL_CERT_FILE=~/.local/share/fws/certs/ca.crt
33
+
34
+ # Try some commands
35
+ gws gmail users messages list --params '{"userId":"me"}'
36
+ gws gmail +triage
37
+ gws calendar events list --params '{"calendarId":"primary"}'
38
+ gws drive files list
39
+ gws tasks tasklists list
40
+ gws sheets spreadsheets get --params '{"spreadsheetId":"sheet001"}'
41
+ gws people people connections list --params '{"resourceName":"people/me","personFields":"names"}'
42
+
43
+ # When done
44
+ fws server stop
45
+ ```
46
+
47
+ The server starts with sample seed data (5 emails, 4 calendar events, 5 drive files, 2 tasks, 1 spreadsheet, 2 contacts) so you can try gws commands immediately.
48
+
49
+ ## Usage
50
+
51
+ ### Proxy mode (one-shot)
52
+
53
+ Starts a temporary server, runs gws, exits. No separate server needed.
54
+
55
+ ```bash
56
+ fws gmail users messages list --params '{"userId":"me"}'
57
+ fws gmail +triage
58
+ fws calendar calendarList list
59
+ fws drive about get --params '{"fields":"*"}'
60
+ ```
61
+
62
+ ### Server mode (persistent)
63
+
64
+ ```bash
65
+ fws server start # Start in background
66
+ fws server status # Check if running
67
+ fws server stop # Stop
68
+ fws server start --foreground # Run in foreground (for debugging)
69
+ ```
70
+
71
+ ### Setup (add data to running server)
72
+
73
+ ```bash
74
+ fws setup gmail add-message --from alice@corp.com --subject "Meeting" --body "See you at 3pm"
75
+ fws setup calendar add-event --summary "Team sync" --start 2026-04-08T15:00:00 --duration 1h
76
+ fws setup drive add-file --name "report.pdf" --mimeType application/pdf
77
+ ```
78
+
79
+ ### Snapshots
80
+
81
+ Data is in-memory only. Save before stopping the server if you need to keep it.
82
+
83
+ ```bash
84
+ fws snapshot save my-scenario # Save current state to disk
85
+ fws snapshot load my-scenario # Restore saved state into running server
86
+ fws snapshot list # List saved snapshots
87
+ fws snapshot delete my-scenario # Delete a snapshot
88
+ fws reset # Reset to default seed data
89
+ fws reset --snapshot my-scenario # Reset to a specific snapshot
90
+ ```
91
+
92
+ Snapshots are stored in `~/.local/share/fws/snapshots/` (override with `FWS_DATA_DIR`).
93
+
94
+ ## Default seed data
95
+
96
+ | Service | Data |
97
+ |----------|------|
98
+ | Gmail | 5 messages (3 inbox, 1 sent, 1 read), system labels + "Projects" user label |
99
+ | Calendar | 4 events (Daily Standup, Q3 Planning, 1:1, Team Lunch) |
100
+ | Drive | 5 files (docs, spreadsheet, image, folder) |
101
+ | Tasks | 1 task list with 2 tasks (1 pending, 1 completed) |
102
+ | Sheets | 1 spreadsheet ("Budget 2026") |
103
+ | People | 2 contacts (Alice, Bob), 1 contact group |
104
+
105
+ ## API support
106
+
107
+ Gmail (28/79 + 5 helpers), Calendar (21/37), Drive (18/57), Tasks (14/14), Sheets (7/17), People (16/24). 135 tests, 89 gws CLI validated.
108
+
109
+ See [docs/gws-support.md](docs/gws-support.md) for the full endpoint-by-endpoint table.
110
+
111
+ ## Documentation
112
+
113
+ - [docs/cli-reference.md](docs/cli-reference.md) — Full CLI reference with all flags, HTTP API equivalents, and examples
114
+ - [docs/gws-support.md](docs/gws-support.md) — Endpoint-by-endpoint support table
115
+
116
+ ## Structure
117
+
118
+ ```
119
+ bin/fws.ts CLI entry point
120
+ src/server/routes/ Gmail, Calendar, Drive, and control API routes
121
+ src/store/ In-memory data store + seed data
122
+ src/config/ Discovery cache URL rewriting
123
+ src/proxy/ MITM proxy for helper commands
124
+ test/ Vitest tests (with gws CLI validation)
125
+ docs/ API support documentation
126
+ ```
package/bin/fws-cli.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper that runs fws.ts via tsx without requiring a build step
3
+ SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
4
+ exec "$SCRIPT_DIR/../node_modules/.bin/tsx" "$SCRIPT_DIR/fws.ts" "$@"
package/bin/fws.ts ADDED
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env tsx
2
+ import { Command } from 'commander';
3
+ import { createApp } from '../src/server/app.js';
4
+ import { resetStore, loadStore, serializeStore, deserializeStore } from '../src/store/index.js';
5
+ import { generateConfigDir } from '../src/config/rewrite-cache.js';
6
+ import { generateCACert, startMitmProxy } from '../src/proxy/mitm.js';
7
+ import { spawn, execFile } from 'node:child_process';
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import type { Server } from 'node:http';
12
+
13
+ const DEFAULT_PORT = 4100;
14
+ const DEFAULT_PROXY_PORT = 4101;
15
+
16
+ function getDataDir(): string {
17
+ return process.env.FWS_DATA_DIR || path.join(os.homedir(), '.local', 'share', 'fws');
18
+ }
19
+
20
+ function getSnapshotsDir(): string {
21
+ return path.join(getDataDir(), 'snapshots');
22
+ }
23
+
24
+ function getServerInfoPath(): string {
25
+ return path.join(getDataDir(), 'server.json');
26
+ }
27
+
28
+ async function ensureDir(dir: string): Promise<void> {
29
+ await fs.mkdir(dir, { recursive: true });
30
+ }
31
+
32
+ const program = new Command();
33
+ program
34
+ .name('fws')
35
+ .description('Fake Google Workspace — local mock server for gws CLI testing')
36
+ .version('0.1.0');
37
+
38
+ // === Server commands ===
39
+ const serverCmd = program.command('server');
40
+
41
+ serverCmd
42
+ .command('start')
43
+ .description('Start the mock server in the background')
44
+ .option('-p, --port <port>', 'Port number', String(DEFAULT_PORT))
45
+ .option('-s, --snapshot <name>', 'Load a snapshot on start')
46
+ .option('--foreground', 'Run in foreground (used internally)')
47
+ .action(async (opts) => {
48
+ const port = parseInt(opts.port);
49
+
50
+ if (opts.foreground) {
51
+ // Actually run the server (called by the background spawner below)
52
+ if (opts.snapshot) {
53
+ const snapshotPath = path.join(getSnapshotsDir(), opts.snapshot, 'store.json');
54
+ try {
55
+ const data = await fs.readFile(snapshotPath, 'utf-8');
56
+ loadStore(deserializeStore(data));
57
+ } catch {
58
+ console.error(`Snapshot not found: ${opts.snapshot}`);
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ const configDir = path.join(getDataDir(), 'config');
64
+ await generateConfigDir(port, configDir);
65
+
66
+ // Generate CA cert for MITM proxy
67
+ const { caPath } = await generateCACert(getDataDir());
68
+
69
+ const app = createApp();
70
+ const server: Server = await new Promise((resolve) => {
71
+ const s = app.listen(port, () => resolve(s));
72
+ });
73
+
74
+ // Start MITM proxy for helper commands (+triage, +send, etc.)
75
+ const proxyPort = port + 1;
76
+ const proxyServer = startMitmProxy(port, proxyPort);
77
+
78
+ await ensureDir(getDataDir());
79
+ await fs.writeFile(getServerInfoPath(), JSON.stringify({
80
+ port, proxyPort, pid: process.pid, caPath,
81
+ }));
82
+
83
+ const shutdown = () => {
84
+ server.close();
85
+ proxyServer.close();
86
+ fs.unlink(getServerInfoPath()).catch(() => {});
87
+ process.exit(0);
88
+ };
89
+ process.on('SIGINT', shutdown);
90
+ process.on('SIGTERM', shutdown);
91
+ return;
92
+ }
93
+
94
+ // Kill existing server if any
95
+ try {
96
+ const info = JSON.parse(await fs.readFile(getServerInfoPath(), 'utf-8'));
97
+ try {
98
+ process.kill(info.pid, 'SIGTERM');
99
+ await new Promise(r => setTimeout(r, 300));
100
+ } catch {}
101
+ await fs.unlink(getServerInfoPath()).catch(() => {});
102
+ } catch {}
103
+
104
+ // Spawn the server as a detached background process
105
+ const configDir = path.join(getDataDir(), 'config');
106
+ await ensureDir(getDataDir());
107
+ await generateConfigDir(port, configDir);
108
+
109
+ const logFile = path.join(getDataDir(), 'server.log');
110
+ // Truncate log on each start so it's fresh
111
+ const logFd = await fs.open(logFile, 'w');
112
+
113
+ const args = ['server', 'start', '--foreground', '-p', String(port)];
114
+ if (opts.snapshot) args.push('-s', opts.snapshot);
115
+
116
+ const tsxPath = path.join(import.meta.dirname, '..', 'node_modules', '.bin', 'tsx');
117
+ const scriptPath = path.join(import.meta.dirname, 'fws.ts');
118
+
119
+ const child = spawn(tsxPath, [scriptPath, ...args], {
120
+ detached: true,
121
+ stdio: ['ignore', logFd.fd, logFd.fd],
122
+ });
123
+ child.unref();
124
+
125
+ // Retry health check a few times (server needs time to start)
126
+ let started = false;
127
+ for (let i = 0; i < 10; i++) {
128
+ await new Promise(r => setTimeout(r, 300));
129
+ try {
130
+ const res = await fetch(`http://localhost:${port}/__fws/status`);
131
+ if (res.ok) {
132
+ started = true;
133
+ break;
134
+ }
135
+ } catch {}
136
+ }
137
+
138
+ await logFd.close();
139
+
140
+ if (started) {
141
+ // Read server info to get caPath and proxyPort
142
+ const serverInfo = JSON.parse(await fs.readFile(getServerInfoPath(), 'utf-8').catch(() => '{}'));
143
+ const proxyPort = serverInfo.proxyPort || port + 1;
144
+ const caPath = serverInfo.caPath || path.join(getDataDir(), 'certs', 'ca.crt');
145
+
146
+ console.log(`fws server started on port ${port} (pid ${child.pid})\n`);
147
+ console.log(`To use with gws:\n`);
148
+ console.log(` export GOOGLE_WORKSPACE_CLI_CONFIG_DIR=${configDir}`);
149
+ console.log(` export GOOGLE_WORKSPACE_CLI_TOKEN=fake`);
150
+ console.log(` export HTTPS_PROXY=http://localhost:${proxyPort}`);
151
+ console.log(` export SSL_CERT_FILE=${caPath}\n`);
152
+ console.log(`Then try:\n`);
153
+ console.log(` gws gmail users messages list --params '{"userId":"me"}'`);
154
+ console.log(` gws gmail +triage`);
155
+ console.log(` gws calendar events list --params '{"calendarId":"primary"}'`);
156
+ console.log(` gws drive files list\n`);
157
+ console.log(`Stop with: fws server stop`);
158
+ } else {
159
+ const log = await fs.readFile(logFile, 'utf-8').catch(() => '');
160
+ console.error('Failed to start server.');
161
+ if (log.trim()) {
162
+ console.error('\nServer log:\n' + log);
163
+ }
164
+ }
165
+ });
166
+
167
+ serverCmd
168
+ .command('stop')
169
+ .description('Stop the running mock server')
170
+ .action(async () => {
171
+ try {
172
+ const info = JSON.parse(await fs.readFile(getServerInfoPath(), 'utf-8'));
173
+ process.kill(info.pid, 'SIGTERM');
174
+ await fs.unlink(getServerInfoPath());
175
+ console.log(`Stopped fws server (pid ${info.pid})`);
176
+ } catch {
177
+ console.log('No running server found');
178
+ }
179
+ });
180
+
181
+ serverCmd
182
+ .command('status')
183
+ .description('Show server status')
184
+ .action(async () => {
185
+ try {
186
+ const info = JSON.parse(await fs.readFile(getServerInfoPath(), 'utf-8'));
187
+ try {
188
+ process.kill(info.pid, 0);
189
+ console.log(`Server running on port ${info.port} (pid ${info.pid})`);
190
+ } catch {
191
+ console.log('Server info exists but process is not running');
192
+ await fs.unlink(getServerInfoPath()).catch(() => {});
193
+ }
194
+ } catch {
195
+ console.log('No server running');
196
+ }
197
+ });
198
+
199
+ // === Snapshot commands ===
200
+ const snapshotCmd = program.command('snapshot');
201
+
202
+ snapshotCmd
203
+ .command('save <name>')
204
+ .description('Save current server state as a snapshot')
205
+ .option('-p, --port <port>', 'Server port', String(DEFAULT_PORT))
206
+ .action(async (name, opts) => {
207
+ const port = parseInt(opts.port);
208
+ const res = await fetch(`http://localhost:${port}/__fws/snapshot/save`, { method: 'POST' });
209
+ const data = await res.text();
210
+
211
+ const snapshotDir = path.join(getSnapshotsDir(), name);
212
+ await ensureDir(snapshotDir);
213
+ await fs.writeFile(path.join(snapshotDir, 'store.json'), data);
214
+ console.log(`Snapshot saved: ${name}`);
215
+ });
216
+
217
+ snapshotCmd
218
+ .command('load <name>')
219
+ .description('Load a snapshot into the running server')
220
+ .option('-p, --port <port>', 'Server port', String(DEFAULT_PORT))
221
+ .action(async (name, opts) => {
222
+ const port = parseInt(opts.port);
223
+ const snapshotPath = path.join(getSnapshotsDir(), name, 'store.json');
224
+ const data = await fs.readFile(snapshotPath, 'utf-8');
225
+
226
+ await fetch(`http://localhost:${port}/__fws/snapshot/load`, {
227
+ method: 'POST',
228
+ headers: { 'Content-Type': 'application/json' },
229
+ body: data,
230
+ });
231
+ console.log(`Snapshot loaded: ${name}`);
232
+ });
233
+
234
+ snapshotCmd
235
+ .command('list')
236
+ .description('List available snapshots')
237
+ .action(async () => {
238
+ const dir = getSnapshotsDir();
239
+ try {
240
+ const entries = await fs.readdir(dir);
241
+ if (entries.length === 0) {
242
+ console.log('No snapshots');
243
+ } else {
244
+ for (const name of entries) {
245
+ console.log(` ${name}`);
246
+ }
247
+ }
248
+ } catch {
249
+ console.log('No snapshots');
250
+ }
251
+ });
252
+
253
+ snapshotCmd
254
+ .command('delete <name>')
255
+ .description('Delete a snapshot')
256
+ .action(async (name) => {
257
+ const snapshotDir = path.join(getSnapshotsDir(), name);
258
+ await fs.rm(snapshotDir, { recursive: true, force: true });
259
+ console.log(`Snapshot deleted: ${name}`);
260
+ });
261
+
262
+ // === Setup commands ===
263
+ const setupCmd = program.command('setup');
264
+ const setupGmail = setupCmd.command('gmail');
265
+
266
+ setupGmail
267
+ .command('add-message')
268
+ .description('Add a message to the mailbox')
269
+ .requiredOption('--from <email>', 'From address')
270
+ .option('--to <email>', 'To address')
271
+ .option('--subject <text>', 'Subject line')
272
+ .option('--body <text>', 'Message body')
273
+ .option('--labels <list>', 'Comma-separated label IDs', 'INBOX,UNREAD')
274
+ .option('-p, --port <port>', 'Server port', String(DEFAULT_PORT))
275
+ .action(async (opts) => {
276
+ const port = parseInt(opts.port);
277
+ const res = await fetch(`http://localhost:${port}/__fws/setup/gmail/message`, {
278
+ method: 'POST',
279
+ headers: { 'Content-Type': 'application/json' },
280
+ body: JSON.stringify({
281
+ from: opts.from,
282
+ to: opts.to,
283
+ subject: opts.subject,
284
+ body: opts.body,
285
+ labels: opts.labels.split(','),
286
+ }),
287
+ });
288
+ const data = await res.json();
289
+ console.log(`Message added: ${data.id}`);
290
+ });
291
+
292
+ const setupCalendar = setupCmd.command('calendar');
293
+
294
+ setupCalendar
295
+ .command('add-event')
296
+ .description('Add an event to a calendar')
297
+ .requiredOption('--summary <text>', 'Event title')
298
+ .requiredOption('--start <datetime>', 'Start time (ISO 8601)')
299
+ .option('--duration <dur>', 'Duration (e.g. 30m, 1h, 2h)', '1h')
300
+ .option('--calendar <id>', 'Calendar ID', 'primary')
301
+ .option('--location <text>', 'Location')
302
+ .option('--attendees <list>', 'Comma-separated attendee emails')
303
+ .option('-p, --port <port>', 'Server port', String(DEFAULT_PORT))
304
+ .action(async (opts) => {
305
+ const port = parseInt(opts.port);
306
+ const res = await fetch(`http://localhost:${port}/__fws/setup/calendar/event`, {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify({
310
+ summary: opts.summary,
311
+ start: opts.start,
312
+ duration: opts.duration,
313
+ calendar: opts.calendar === 'primary' ? undefined : opts.calendar,
314
+ location: opts.location,
315
+ attendees: opts.attendees?.split(','),
316
+ }),
317
+ });
318
+ const data = await res.json();
319
+ console.log(`Event added: ${data.id}`);
320
+ });
321
+
322
+ const setupDrive = setupCmd.command('drive');
323
+
324
+ setupDrive
325
+ .command('add-file')
326
+ .description('Add a file to Drive')
327
+ .requiredOption('--name <text>', 'File name')
328
+ .option('--mimeType <type>', 'MIME type', 'application/octet-stream')
329
+ .option('--parent <id>', 'Parent folder ID', 'root')
330
+ .option('--size <bytes>', 'File size in bytes')
331
+ .option('-p, --port <port>', 'Server port', String(DEFAULT_PORT))
332
+ .action(async (opts) => {
333
+ const port = parseInt(opts.port);
334
+ const res = await fetch(`http://localhost:${port}/__fws/setup/drive/file`, {
335
+ method: 'POST',
336
+ headers: { 'Content-Type': 'application/json' },
337
+ body: JSON.stringify({
338
+ name: opts.name,
339
+ mimeType: opts.mimeType,
340
+ parent: opts.parent,
341
+ size: opts.size ? parseInt(opts.size) : undefined,
342
+ }),
343
+ });
344
+ const data = await res.json();
345
+ console.log(`File added: ${data.id}`);
346
+ });
347
+
348
+ // === Reset command ===
349
+ program
350
+ .command('reset')
351
+ .description('Reset server to seed data or a snapshot')
352
+ .option('-s, --snapshot <name>', 'Load this snapshot after reset')
353
+ .option('-p, --port <port>', 'Server port', String(DEFAULT_PORT))
354
+ .action(async (opts) => {
355
+ const port = parseInt(opts.port);
356
+
357
+ if (opts.snapshot) {
358
+ const snapshotPath = path.join(getSnapshotsDir(), opts.snapshot, 'store.json');
359
+ const data = await fs.readFile(snapshotPath, 'utf-8');
360
+ await fetch(`http://localhost:${port}/__fws/snapshot/load`, {
361
+ method: 'POST',
362
+ headers: { 'Content-Type': 'application/json' },
363
+ body: data,
364
+ });
365
+ console.log(`Reset to snapshot: ${opts.snapshot}`);
366
+ } else {
367
+ await fetch(`http://localhost:${port}/__fws/reset`, { method: 'POST' });
368
+ console.log('Reset to seed data');
369
+ }
370
+ });
371
+
372
+ // === Proxy mode (default): start server, run gws, exit ===
373
+ // If first arg is not a known subcommand, treat as gws proxy
374
+ const SUBCOMMANDS = ['server', 'snapshot', 'setup', 'reset', 'help', '--help', '-h', '--version', '-V'];
375
+
376
+ async function runProxy(args: string[]): Promise<void> {
377
+ const port = DEFAULT_PORT;
378
+ const proxyPort = DEFAULT_PROXY_PORT;
379
+
380
+ // Start server in-process
381
+ const configDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fws-proxy-'));
382
+ await generateConfigDir(port, configDir);
383
+
384
+ // Generate CA and start MITM proxy
385
+ const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fws-proxy-data-'));
386
+ const { caPath } = await generateCACert(dataDir);
387
+ const proxyServer = startMitmProxy(port, proxyPort);
388
+
389
+ const app = createApp();
390
+ const server: Server = await new Promise((resolve) => {
391
+ const s = app.listen(port, () => resolve(s));
392
+ });
393
+
394
+ const gwsPath = process.env.GWS_PATH || 'gws';
395
+ const env = {
396
+ ...process.env,
397
+ GOOGLE_WORKSPACE_CLI_CONFIG_DIR: configDir,
398
+ GOOGLE_WORKSPACE_CLI_TOKEN: 'fake',
399
+ HTTPS_PROXY: `http://localhost:${proxyPort}`,
400
+ SSL_CERT_FILE: caPath,
401
+ };
402
+
403
+ const child = spawn(gwsPath, args, { env, stdio: 'inherit' });
404
+
405
+ child.on('close', async (code) => {
406
+ server.close();
407
+ proxyServer.close();
408
+ await fs.rm(configDir, { recursive: true, force: true });
409
+ await fs.rm(dataDir, { recursive: true, force: true });
410
+ process.exit(code ?? 0);
411
+ });
412
+ }
413
+
414
+ // Main entry
415
+ const firstArg = process.argv[2];
416
+ if (firstArg && !SUBCOMMANDS.includes(firstArg)) {
417
+ // Proxy mode
418
+ runProxy(process.argv.slice(2));
419
+ } else {
420
+ program.parse();
421
+ }