@mod-computer/cli 0.1.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +98 -76
  2. package/dist/cli.bundle.js +23750 -12931
  3. package/dist/cli.bundle.js.map +4 -4
  4. package/dist/cli.js +23 -12
  5. package/dist/commands/add.js +245 -0
  6. package/dist/commands/auth.js +129 -21
  7. package/dist/commands/comment.js +568 -0
  8. package/dist/commands/diff.js +182 -0
  9. package/dist/commands/index.js +33 -3
  10. package/dist/commands/init.js +475 -221
  11. package/dist/commands/ls.js +135 -0
  12. package/dist/commands/members.js +687 -0
  13. package/dist/commands/mv.js +282 -0
  14. package/dist/commands/rm.js +257 -0
  15. package/dist/commands/status.js +273 -306
  16. package/dist/commands/sync.js +99 -75
  17. package/dist/commands/trace.js +1752 -0
  18. package/dist/commands/workspace.js +354 -330
  19. package/dist/config/features.js +18 -7
  20. package/dist/config/release-profiles/development.json +4 -1
  21. package/dist/config/release-profiles/mvp.json +4 -2
  22. package/dist/daemon/conflict-resolution.js +172 -0
  23. package/dist/daemon/content-hash.js +31 -0
  24. package/dist/daemon/file-sync.js +985 -0
  25. package/dist/daemon/index.js +203 -0
  26. package/dist/daemon/mime-types.js +166 -0
  27. package/dist/daemon/offline-queue.js +211 -0
  28. package/dist/daemon/path-utils.js +64 -0
  29. package/dist/daemon/share-policy.js +83 -0
  30. package/dist/daemon/wasm-errors.js +189 -0
  31. package/dist/daemon/worker.js +557 -0
  32. package/dist/daemon-worker.js +3 -2
  33. package/dist/errors/workspace-errors.js +48 -0
  34. package/dist/lib/auth-server.js +89 -26
  35. package/dist/lib/browser.js +1 -1
  36. package/dist/lib/diff.js +284 -0
  37. package/dist/lib/formatters.js +204 -0
  38. package/dist/lib/git.js +137 -0
  39. package/dist/lib/local-fs.js +201 -0
  40. package/dist/lib/prompts.js +23 -83
  41. package/dist/lib/storage.js +11 -1
  42. package/dist/lib/trace-formatters.js +314 -0
  43. package/dist/services/add-service.js +554 -0
  44. package/dist/services/add-validation.js +124 -0
  45. package/dist/services/mod-config.js +8 -2
  46. package/dist/services/modignore-service.js +2 -0
  47. package/dist/stores/use-workspaces-store.js +36 -14
  48. package/dist/types/add-types.js +99 -0
  49. package/dist/types/config.js +1 -1
  50. package/dist/types/workspace-connection.js +53 -2
  51. package/package.json +7 -5
  52. package/commands/execute.md +0 -156
  53. package/commands/overview.md +0 -233
  54. package/commands/review.md +0 -151
  55. package/commands/spec.md +0 -169
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import dotenv from 'dotenv';
4
4
  import path from 'path';
@@ -75,29 +75,40 @@ const cli = meow(`
75
75
  force: {
76
76
  type: 'boolean',
77
77
  default: false
78
+ },
79
+ dev: {
80
+ type: 'boolean',
81
+ default: false
82
+ },
83
+ json: {
84
+ type: 'boolean',
85
+ default: false
78
86
  }
79
87
  },
80
88
  allowUnknownFlags: true
81
89
  });
82
90
  async function main() {
83
91
  const repo = await getRepo();
92
+ // Load user document into share policy if logged in
93
+ const { readConfig } = await import('./lib/storage.js');
94
+ const { addUserToSharePolicy } = await import('./daemon/share-policy.js');
95
+ const config = readConfig();
96
+ if (config.auth?.userDocId) {
97
+ addUserToSharePolicy(config.auth.userDocId);
98
+ }
84
99
  const availableCommands = buildCommandRegistry();
85
100
  const [cmd, ...args] = cli.input;
86
101
  // No user-facing banners or notifications about feature flags
87
102
  // Background watch now handled by automatic-file-tracker service
88
103
  // Handle command execution
89
104
  if (cmd && typeof availableCommands[cmd] === 'function') {
90
- // Pass both positional args and flags as a combined array
91
- const allArgs = [...args];
92
- // Add flags back to args array for backward compatibility
93
- if (cli.flags.preview)
94
- allArgs.push('--preview');
95
- if (cli.flags.verbose)
96
- allArgs.push('--verbose');
97
- if (cli.flags.force)
98
- allArgs.push('--force');
99
- await availableCommands[cmd](allArgs, repo);
100
- return;
105
+ // Pass raw args after the command name from process.argv
106
+ // This preserves all flags (--type, --file, --json, etc.) as-is
107
+ const cmdIndex = process.argv.findIndex(arg => arg === cmd);
108
+ const rawArgs = cmdIndex >= 0 ? process.argv.slice(cmdIndex + 1) : args;
109
+ await availableCommands[cmd](rawArgs, repo);
110
+ // Exit after command completes - WebSocket keeps event loop alive otherwise
111
+ process.exit(0);
101
112
  }
102
113
  if (!cmd) {
103
114
  console.log('Mod CLI\n');
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ // glassware[type="implementation", id="impl-cli-add-command--777b18e2", requirements="requirement-cli-add-cmd--aff0d740,requirement-cli-add-cmd-help--e0709244,requirement-cli-add-progress-small--5474ba10,requirement-cli-add-progress-medium--0f22e3e8,requirement-cli-add-progress-large--477f1e9a,requirement-cli-add-summary--3d0fb9b5,requirement-cli-add-dry-run--91bb0ff6,requirement-cli-add-verbose--3a958508"]
3
+ // spec: packages/mod-cli/specs/add.md
4
+ import ora from 'ora';
5
+ import { AddService } from '../services/add-service.js';
6
+ import { validateAddOptions, validateWorkspaceState } from '../services/add-validation.js';
7
+ import { ADD_CONSTANTS, } from '../types/add-types.js';
8
+ /**
9
+ * Parse command line arguments
10
+ */
11
+ function parseArgs(args) {
12
+ const paths = [];
13
+ let dryRun = false;
14
+ let includeLargeBinary = false;
15
+ let verbose = false;
16
+ let quiet = false;
17
+ for (const arg of args) {
18
+ if (arg === '--dry-run') {
19
+ dryRun = true;
20
+ }
21
+ else if (arg === '--include-large-binary') {
22
+ includeLargeBinary = true;
23
+ }
24
+ else if (arg === '-v' || arg === '--verbose') {
25
+ verbose = true;
26
+ }
27
+ else if (arg === '-q' || arg === '--quiet') {
28
+ quiet = true;
29
+ }
30
+ else if (!arg.startsWith('-')) {
31
+ paths.push(arg);
32
+ }
33
+ }
34
+ // Default to current directory if no paths
35
+ if (paths.length === 0) {
36
+ paths.push('.');
37
+ }
38
+ return { paths, dryRun, includeLargeBinary, verbose, quiet };
39
+ }
40
+ /**
41
+ * Add command entry point
42
+ */
43
+ export async function addCommand(args, repo) {
44
+ // Parse arguments
45
+ const { paths, dryRun, includeLargeBinary, verbose, quiet } = parseArgs(args);
46
+ const options = {
47
+ paths,
48
+ dryRun,
49
+ includeLargeBinary,
50
+ verbose,
51
+ quiet
52
+ };
53
+ // Validate options
54
+ const optionsValidation = validateAddOptions(options);
55
+ if (!optionsValidation.valid) {
56
+ for (const error of optionsValidation.errors) {
57
+ console.error(`Error: ${error.message}`);
58
+ }
59
+ process.exit(1);
60
+ }
61
+ // Validate workspace
62
+ const workspaceValidation = await validateWorkspaceState(process.cwd());
63
+ if (!workspaceValidation.valid) {
64
+ for (const error of workspaceValidation.errors) {
65
+ console.error(`Error: ${error.message}`);
66
+ }
67
+ process.exit(1);
68
+ }
69
+ // Create add service
70
+ const addService = new AddService(repo);
71
+ // Set up cancellation handler
72
+ process.on('SIGINT', () => {
73
+ addService.cancel();
74
+ console.log('\nCancelling...');
75
+ });
76
+ // Progress tracking
77
+ let spinner = null;
78
+ let lastProgressUpdate = 0;
79
+ const progressBarWidth = 30;
80
+ const onProgress = (progress) => {
81
+ if (quiet)
82
+ return;
83
+ // Throttle progress updates (cli-add-progress-throttle)
84
+ const now = Date.now();
85
+ if (now - lastProgressUpdate < ADD_CONSTANTS.PROGRESS_THROTTLE_MS)
86
+ return;
87
+ lastProgressUpdate = now;
88
+ if (progress.phase === 'scanning') {
89
+ if (!spinner) {
90
+ spinner = ora('Scanning...').start();
91
+ }
92
+ spinner.text = `Scanning... ${progress.total} files found`;
93
+ }
94
+ else if (progress.phase === 'comparing') {
95
+ if (spinner) {
96
+ spinner.text = `Comparing... ${progress.current}/${progress.total} directories`;
97
+ }
98
+ }
99
+ else if (progress.phase === 'adding') {
100
+ const total = progress.total;
101
+ if (total < ADD_CONSTANTS.SMALL_ADD_THRESHOLD) {
102
+ // Small: no progress bar
103
+ if (spinner) {
104
+ spinner.text = `Adding... ${progress.current}/${total}`;
105
+ }
106
+ }
107
+ else if (total < ADD_CONSTANTS.MEDIUM_ADD_THRESHOLD) {
108
+ // Medium: spinner with count
109
+ if (spinner) {
110
+ spinner.text = `Adding... ${progress.current}/${total} files`;
111
+ }
112
+ }
113
+ else {
114
+ // Large: progress bar
115
+ const percent = Math.round((progress.current / total) * 100);
116
+ const filled = Math.round((progress.current / total) * progressBarWidth);
117
+ const bar = '█'.repeat(filled) + '░'.repeat(progressBarWidth - filled);
118
+ const eta = progress.eta ? ` | ETA: ${formatEta(progress.eta)}` : '';
119
+ if (spinner) {
120
+ spinner.text = `Adding files...\n[${bar}] ${percent}% | ${progress.current.toLocaleString()}/${total.toLocaleString()}${eta}`;
121
+ }
122
+ }
123
+ if (verbose && progress.currentFile) {
124
+ console.log(`+ ${progress.currentFile}`);
125
+ }
126
+ }
127
+ };
128
+ // Execute
129
+ try {
130
+ const result = await addService.execute(options, onProgress);
131
+ // Stop spinner
132
+ if (spinner) {
133
+ spinner.stop();
134
+ }
135
+ // Print result
136
+ printResult(result, options);
137
+ if (!result.success) {
138
+ process.exit(1);
139
+ }
140
+ }
141
+ catch (error) {
142
+ if (spinner) {
143
+ spinner.stop();
144
+ }
145
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
146
+ process.exit(1);
147
+ }
148
+ }
149
+ /**
150
+ * Print the result of the add operation
151
+ */
152
+ function printResult(result, options) {
153
+ const { summary, directories, duration } = result;
154
+ if (options.dryRun) {
155
+ // Dry run output
156
+ console.log(`Would add ${summary.totalFiles.toLocaleString()} files across ${directories.length} directories:`);
157
+ for (const dir of directories.slice(0, 10)) {
158
+ console.log(` ${dir.path}/ (${dir.created} files)`);
159
+ }
160
+ if (directories.length > 10) {
161
+ console.log(` ... (${directories.length - 10} more directories)`);
162
+ }
163
+ if (summary.skipped > 0) {
164
+ console.log(`\nWould skip ${summary.skipped} files`);
165
+ }
166
+ const estimatedMinutes = Math.ceil(summary.totalFiles * 50 / 1000 / 60);
167
+ console.log(`\nEstimated time: ~${estimatedMinutes} minutes`);
168
+ return;
169
+ }
170
+ // Normal output
171
+ if (options.quiet) {
172
+ // Quiet: only show summary line
173
+ if (summary.errors > 0) {
174
+ console.log(`Added ${summary.created} files (${summary.errors} errors)`);
175
+ }
176
+ else {
177
+ console.log(`Added ${summary.created} files`);
178
+ }
179
+ return;
180
+ }
181
+ // Standard output
182
+ const parts = [];
183
+ if (summary.created > 0) {
184
+ parts.push(`${summary.created} created`);
185
+ }
186
+ if (summary.updated > 0) {
187
+ parts.push(`${summary.updated} updated`);
188
+ }
189
+ if (summary.unchanged > 0) {
190
+ parts.push(`${summary.unchanged} unchanged`);
191
+ }
192
+ if (parts.length > 0) {
193
+ console.log(`\nAdded ${summary.totalFiles.toLocaleString()} files (${parts.join(', ')})`);
194
+ }
195
+ else {
196
+ console.log(`\nNo files to add`);
197
+ }
198
+ // Show skipped files
199
+ if (summary.skipped > 0) {
200
+ console.log(`\nSkipped: ${summary.skipped} files`);
201
+ }
202
+ // Show errors
203
+ if (summary.errors > 0) {
204
+ console.log(`\nErrors: ${summary.errors} files`);
205
+ // Show first few errors
206
+ const allErrors = directories.flatMap(d => d.errors);
207
+ for (const error of allErrors.slice(0, 5)) {
208
+ console.log(` ${error.relativePath}: ${error.error?.message}`);
209
+ }
210
+ if (allErrors.length > 5) {
211
+ console.log(` ... (${allErrors.length - 5} more errors)`);
212
+ }
213
+ }
214
+ // Show duration
215
+ console.log(`\nCompleted in ${formatDuration(duration)}`);
216
+ }
217
+ /**
218
+ * Format duration in human readable format
219
+ */
220
+ function formatDuration(ms) {
221
+ if (ms < 1000) {
222
+ return `${ms}ms`;
223
+ }
224
+ if (ms < 60000) {
225
+ return `${(ms / 1000).toFixed(1)}s`;
226
+ }
227
+ const minutes = Math.floor(ms / 60000);
228
+ const seconds = Math.round((ms % 60000) / 1000);
229
+ return `${minutes}m ${seconds}s`;
230
+ }
231
+ /**
232
+ * Format ETA in human readable format
233
+ */
234
+ function formatEta(ms) {
235
+ if (ms < 60000) {
236
+ return `${Math.round(ms / 1000)}s`;
237
+ }
238
+ const minutes = Math.floor(ms / 60000);
239
+ const seconds = Math.round((ms % 60000) / 1000);
240
+ if (seconds === 0) {
241
+ return `${minutes}m`;
242
+ }
243
+ return `${minutes}m ${seconds}s`;
244
+ }
245
+ export default addCommand;
@@ -1,15 +1,17 @@
1
- // glassware[type=implementation, id=cli-auth-command, requirements=req-cli-auth-ux-2,req-cli-auth-ux-3,req-cli-auth-ux-4,req-cli-auth-ux-5,req-cli-auth-ux-6,req-cli-auth-app-1,req-cli-auth-app-5]
1
+ // glassware[type="implementation", id="impl-cli-auth-command--649dd0cd", specifications="specification-spec-auth-login--c8f45e62,specification-spec-auth-status--36efb6a8,specification-spec-auth-logout--79cb4081"]
2
2
  import { readConfig, writeConfig } from '../lib/storage.js';
3
3
  import { openBrowser } from '../lib/browser.js';
4
4
  import { startAuthServer } from '../lib/auth-server.js';
5
- // Auth server base URL - configurable via environment variable
6
- const AUTH_BASE_URL = process.env.MOD_AUTH_URL || 'https://auth.mod.app';
7
- const API_BASE_URL = process.env.MOD_API_URL || 'https://api.mod.app';
5
+ import { createModUser } from '@mod/mod-core';
6
+ import { addUserToSharePolicy } from '../daemon/share-policy.js';
7
+ // Auth worker URL - configurable via environment variable
8
+ // Uses the Cloudflare Worker for OAuth
9
+ const AUTH_WORKER_URL = process.env.MOD_AUTH_URL || 'https://mod-auth-worker.mod-workers.workers.dev';
8
10
  export async function authCommand(args, repo) {
9
- const [subcommand] = args;
11
+ const [subcommand, ...rest] = args;
10
12
  switch (subcommand) {
11
13
  case 'login':
12
- await handleLogin();
14
+ await handleLogin(rest, repo);
13
15
  break;
14
16
  case 'logout':
15
17
  await handleLogout();
@@ -24,15 +26,18 @@ export async function authCommand(args, repo) {
24
26
  else {
25
27
  console.error('Usage: mod auth <login|logout|status>');
26
28
  console.error('Available commands:');
27
- console.error(' login Sign in with Google OAuth');
28
- console.error(' logout Sign out and clear credentials');
29
- console.error(' status Show current authentication state');
29
+ console.error(' login [--dev] Sign in with Google OAuth (or use --dev for local testing)');
30
+ console.error(' logout Sign out and clear credentials');
31
+ console.error(' status Show current authentication state');
30
32
  process.exit(1);
31
33
  }
32
34
  }
33
35
  process.exit(0);
34
36
  }
35
- async function handleLogin() {
37
+ // glassware[type="implementation", id="impl-handle-login--58bdb73b", specifications="specification-spec-auth-login--c8f45e62,specification-spec-open-browser--397b6a28,specification-spec-manual-url--b37b2760,specification-spec-validate-token--d3856b2d,specification-spec-success-message--b3bb1875,specification-spec-create-user-doc--282489ae,specification-spec-register-user-doc--e611f1db"]
38
+ async function handleLogin(args = [], repo) {
39
+ // Check for --dev flag
40
+ const isDev = args.includes('--dev');
36
41
  // Check if already authenticated
37
42
  const config = readConfig();
38
43
  if (config.auth) {
@@ -41,10 +46,15 @@ async function handleLogin() {
41
46
  console.log('Run `mod auth logout` to sign out first.');
42
47
  return;
43
48
  }
49
+ // Dev mode: create local user without OAuth
50
+ if (isDev) {
51
+ await handleDevLogin(repo);
52
+ return;
53
+ }
44
54
  // Start localhost server to receive callback
45
55
  const { port, result, close } = await startAuthServer();
46
56
  // Build auth URL with callback port
47
- const authUrl = `${AUTH_BASE_URL}/cli?port=${port}`;
57
+ const authUrl = `${AUTH_WORKER_URL}/auth/cli?port=${port}`;
48
58
  console.log('Opening browser to sign in...');
49
59
  // Try to open browser
50
60
  const opened = await openBrowser(authUrl);
@@ -58,19 +68,45 @@ async function handleLogin() {
58
68
  try {
59
69
  // Wait for auth callback
60
70
  const authResult = await result;
61
- // Validate token with server (optional - server may not be available)
71
+ // Validate token with server and get existing userDocId
62
72
  let userDocId;
73
+ let authUserId;
63
74
  try {
64
- userDocId = await validateAndGetUserDocId(authResult.googleIdToken);
75
+ const authProfile = await validateAndGetAuthProfile(authResult.googleIdToken);
76
+ userDocId = authProfile.userDocId;
77
+ authUserId = authProfile.userId;
65
78
  }
66
79
  catch (error) {
67
- // If validation fails, continue without userDocId
68
- // It will be set on first sync
80
+ // If validation fails, continue - we'll create user doc below
81
+ }
82
+ // If no userDocId (new user), create user document and register with server
83
+ if (!userDocId) {
84
+ try {
85
+ const modUser = createModUser(repo);
86
+ const userDoc = await modUser.findOrCreate({
87
+ email: authResult.email,
88
+ name: authResult.name,
89
+ googleId: authResult.googleId,
90
+ avatar: null,
91
+ });
92
+ userDocId = userDoc.id;
93
+ // Register the new userDocId with auth worker
94
+ await registerUserDoc(authResult.googleIdToken, userDocId);
95
+ }
96
+ catch (error) {
97
+ console.warn('Could not create/register user document:', error);
98
+ // Continue without userDocId - it can be set up later
99
+ }
100
+ }
101
+ // Add user doc to share policy if available
102
+ if (userDocId) {
103
+ addUserToSharePolicy(userDocId);
69
104
  }
70
105
  // Store credentials in config
71
106
  const updatedConfig = readConfig();
72
107
  updatedConfig.auth = {
73
108
  googleIdToken: authResult.googleIdToken,
109
+ userId: authUserId || '',
74
110
  googleId: authResult.googleId,
75
111
  email: authResult.email,
76
112
  name: authResult.name,
@@ -98,6 +134,7 @@ async function handleLogin() {
98
134
  process.exit(1);
99
135
  }
100
136
  }
137
+ // glassware[type="implementation", id="impl-handle-logout--1f8e80ea", specifications="specification-spec-auth-logout--79cb4081,specification-spec-logout-message--2785cc24"]
101
138
  async function handleLogout() {
102
139
  const config = readConfig();
103
140
  if (!config.auth) {
@@ -111,6 +148,7 @@ async function handleLogout() {
111
148
  console.log('Signed out successfully');
112
149
  console.log(`Removed credentials for ${email}`);
113
150
  }
151
+ // glassware[type="implementation", id="impl-handle-status--b14ce725", specifications="specification-spec-auth-status--36efb6a8,specification-spec-status-output--b8935802"]
114
152
  async function handleStatus() {
115
153
  const config = readConfig();
116
154
  if (!config.auth) {
@@ -126,19 +164,20 @@ async function handleStatus() {
126
164
  console.log('Token expires: never');
127
165
  // Check if token is still valid (optional)
128
166
  try {
129
- await validateAndGetUserDocId(config.auth.googleIdToken);
167
+ await validateAndGetAuthProfile(config.auth.googleIdToken);
130
168
  console.log('Token status: valid');
131
169
  }
132
170
  catch (error) {
133
171
  console.log('Token status: could not verify (server unavailable)');
134
172
  }
135
173
  }
174
+ // glassware[type="implementation", id="impl-validate-token--562d976d", specifications="specification-spec-validate-token--d3856b2d,specification-spec-load-user-doc--c7a835b0,specification-spec-auth-user-id--de3afa93"]
136
175
  /**
137
- * Validate token with server and get user document ID.
138
- * Returns existing userDocId for returning users, or undefined for new users.
176
+ * Validate token with server and get auth profile.
177
+ * Returns existing userDocId for returning users, or empty string for new users.
139
178
  */
140
- async function validateAndGetUserDocId(token) {
141
- const response = await fetch(`${API_BASE_URL}/auth/me`, {
179
+ async function validateAndGetAuthProfile(token) {
180
+ const response = await fetch(`${AUTH_WORKER_URL}/auth/me`, {
142
181
  headers: {
143
182
  Authorization: `Bearer ${token}`,
144
183
  },
@@ -147,5 +186,74 @@ async function validateAndGetUserDocId(token) {
147
186
  throw new Error('Token validation failed');
148
187
  }
149
188
  const data = (await response.json());
150
- return data.userDocId;
189
+ return {
190
+ userId: data.id || '',
191
+ userDocId: data.userDocId || '',
192
+ };
193
+ }
194
+ export async function fetchAuthProfile(token) {
195
+ return validateAndGetAuthProfile(token);
196
+ }
197
+ // glassware[type="implementation", id="impl-register-user-doc--c699a7d3", specifications="specification-spec-register-user-doc--e611f1db"]
198
+ /**
199
+ * Register a new userDocId with the auth worker.
200
+ * Called for new users after creating their Automerge user document.
201
+ */
202
+ async function registerUserDoc(token, userDocId) {
203
+ const response = await fetch(`${AUTH_WORKER_URL}/auth/register-doc`, {
204
+ method: 'POST',
205
+ headers: {
206
+ 'Authorization': `Bearer ${token}`,
207
+ 'Content-Type': 'application/json',
208
+ },
209
+ body: JSON.stringify({ userDocId }),
210
+ });
211
+ if (!response.ok) {
212
+ throw new Error(`Failed to register user doc: ${response.status}`);
213
+ }
214
+ }
215
+ // glassware[type="implementation", id="impl-dev-login--d3f76c9b", specifications="specification-spec-create-user-doc--282489ae,specification-spec-token-storage--26199fe8"]
216
+ /**
217
+ * Handle dev login - creates a local user document for testing without OAuth.
218
+ * This allows testing the full flow (auth → workspace creation → syncing) on localhost.
219
+ * Uses a shared dev@localhost user that both CLI and web app can access.
220
+ */
221
+ async function handleDevLogin(repo) {
222
+ console.log('Setting up development authentication...');
223
+ const modUser = createModUser(repo);
224
+ try {
225
+ // Use findOrCreate to get shared dev user across CLI and web app
226
+ console.log('Looking for dev@localhost user...');
227
+ const userDoc = await modUser.findOrCreate({
228
+ email: 'dev@localhost',
229
+ name: 'Dev User',
230
+ googleId: 'dev-google-id',
231
+ avatar: null,
232
+ });
233
+ console.log('Using dev user document:', userDoc.id);
234
+ // Add user doc to share policy so it can sync
235
+ addUserToSharePolicy(userDoc.id);
236
+ // Store dev credentials in config
237
+ const config = readConfig();
238
+ config.auth = {
239
+ googleIdToken: 'dev-token',
240
+ userId: 'dev-user',
241
+ googleId: 'dev-google-id',
242
+ email: 'dev@localhost',
243
+ name: 'Dev User',
244
+ userDocId: userDoc.id,
245
+ };
246
+ writeConfig(config);
247
+ console.log('');
248
+ console.log('✓ Signed in as dev@localhost');
249
+ console.log(` User Document ID: ${userDoc.id}`);
250
+ console.log('');
251
+ console.log('Note: This is development mode for local testing.');
252
+ console.log('Workspaces will sync via ws://localhost:3030');
253
+ console.log('The web app will use the same dev@localhost user.');
254
+ }
255
+ catch (error) {
256
+ console.error('Failed to setup dev user:', error.message);
257
+ process.exit(1);
258
+ }
151
259
  }