@mod-computer/cli 0.1.0 → 0.2.1

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 +72 -0
  2. package/dist/cli.bundle.js +24633 -13744
  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 +545 -326
  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 +8 -3
  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 +56 -0
  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
@@ -1,7 +1,8 @@
1
- import { createModWorkspace } from '@mod/mod-core';
2
- import { readModConfig, writeModConfig } from '../services/mod-config.js';
3
- import fs from 'fs';
4
- import path from 'path';
1
+ // glassware[type="implementation", id="cli-workspace-command--7ef7e657", requirements="requirement-cli-init-ux-2--b69b045f,requirement-cli-init-ux-3--3dde4846,requirement-cli-init-app-1--1c2b11b4"]
2
+ // spec: packages/mod-cli/specs/workspaces.md
3
+ import { createModWorkspace, createModUser } from '@mod/mod-core';
4
+ import { listWorkspaceConnections, readWorkspaceConnection, writeWorkspaceConnection, deleteWorkspaceConnection, readConfig, } from '../lib/storage.js';
5
+ import { input, select, validateWorkspaceName } from '../lib/prompts.js';
5
6
  export async function workspaceCommand(args, repo) {
6
7
  const [subcommand, ...rest] = args;
7
8
  switch (subcommand) {
@@ -9,415 +10,438 @@ export async function workspaceCommand(args, repo) {
9
10
  await handleCreateWorkspace(rest, repo);
10
11
  break;
11
12
  case 'list':
12
- await handleListWorkspaces(rest, repo);
13
+ await handleListWorkspaces();
13
14
  break;
14
- case 'switch':
15
- await handleSwitchWorkspace(rest, repo);
15
+ case 'list-remote':
16
+ await handleListRemoteWorkspaces(repo);
17
+ break;
18
+ case 'connect':
19
+ await handleConnectWorkspace(rest, repo);
16
20
  break;
17
21
  case 'info':
18
- await handleWorkspaceInfo(rest, repo);
22
+ await handleWorkspaceInfo();
23
+ break;
24
+ case 'disconnect':
25
+ await handleDisconnectWorkspace();
19
26
  break;
20
- case 'delete':
21
- await handleDeleteWorkspace(rest, repo);
27
+ case 'register':
28
+ await handleRegisterWorkspaces(repo);
22
29
  break;
23
- case 'rename':
24
- await handleRenameWorkspace(rest, repo);
30
+ case 'clear':
31
+ await handleClearWorkspace(rest, repo);
25
32
  break;
26
33
  default:
27
34
  if (!subcommand) {
28
- await handleListWorkspaces([], repo);
35
+ await handleListWorkspaces();
29
36
  }
30
37
  else {
31
- console.error('Usage: mod workspace <create|list|switch|info|delete|rename> [options]');
32
- console.error('Available commands:');
33
- console.error(' create <name> Create new workspace and set as active');
34
- console.error(' list List all available workspaces');
35
- console.error(' switch <name-or-id> Switch to a different workspace');
36
- console.error(' info [name-or-id] Show workspace details');
37
- console.error(' delete <name-or-id> Delete a workspace');
38
- console.error(' rename <name-or-id> <new-name> Rename a workspace');
38
+ console.error('Usage: mod workspace <create|list|list-remote|connect|info|disconnect|register|clear>');
39
+ console.error('');
40
+ console.error('Commands:');
41
+ console.error(' create [name] Create new workspace in current directory');
42
+ console.error(' list List all locally connected workspaces');
43
+ console.error(' list-remote List all workspaces from your user account');
44
+ console.error(' connect <id> [--name ...] Connect current directory to existing workspace');
45
+ console.error(' info Show current directory workspace info');
46
+ console.error(' disconnect Disconnect current directory from workspace');
47
+ console.error(' register Register connected workspaces to your account');
48
+ console.error(' clear [id] Delete all files from a workspace (DESTRUCTIVE)');
39
49
  process.exit(1);
40
50
  }
41
51
  }
42
52
  process.exit(0);
43
53
  }
44
54
  async function handleCreateWorkspace(args, repo) {
45
- const [name, ...flags] = args;
46
- if (!name) {
47
- console.error('Usage: mod workspace create <name> [--description <desc>]');
55
+ const currentDir = process.cwd();
56
+ // Check if already connected
57
+ const existing = readWorkspaceConnection(currentDir);
58
+ if (existing) {
59
+ console.log(`Already connected to workspace: ${existing.workspaceName}`);
60
+ console.log('');
61
+ console.log('To create a new workspace, first disconnect:');
62
+ console.log(' mod workspace disconnect');
48
63
  process.exit(1);
49
64
  }
50
- let description;
51
- const descIndex = flags.indexOf('--description');
52
- if (descIndex !== -1 && descIndex + 1 < flags.length) {
53
- description = flags[descIndex + 1];
65
+ // Get workspace name
66
+ let name = args[0];
67
+ if (!name) {
68
+ const dirName = currentDir.split('/').pop() || 'workspace';
69
+ const defaultName = dirName.charAt(0).toUpperCase() + dirName.slice(1);
70
+ name = await input('Workspace name', {
71
+ default: defaultName,
72
+ validate: validateWorkspaceName,
73
+ });
74
+ }
75
+ else {
76
+ const validationError = validateWorkspaceName(name);
77
+ if (validationError) {
78
+ console.error(`Invalid workspace name: ${validationError}`);
79
+ process.exit(1);
80
+ }
54
81
  }
82
+ console.log('Creating workspace...');
55
83
  try {
56
84
  const modWorkspace = createModWorkspace(repo);
57
- console.log(`Creating workspace: ${name}...`);
58
- const workspaceHandle = await modWorkspace.createWorkspace({
59
- name: name,
60
- description: description
61
- });
62
- // Get the main branch ID for the newly created workspace
63
- const branches = await workspaceHandle.branch.list();
64
- const mainBranch = branches.find(b => b.metadata?.type === 'main') || branches[0];
65
- if (!mainBranch) {
66
- throw new Error('Failed to find main branch in newly created workspace');
67
- }
68
- const config = (readModConfig() || {});
69
- const updatedConfig = {
70
- ...config,
71
- workspaceId: workspaceHandle.id,
72
- workspaceName: name,
73
- activeBranchId: mainBranch.id, // Set to main branch ID
74
- lastWorkspaceSwitch: new Date().toISOString()
75
- };
76
- const recentWorkspaces = updatedConfig.recentWorkspaces || [];
77
- const existingIndex = recentWorkspaces.findIndex(w => w.id === workspaceHandle.id);
78
- const workspaceRef = {
79
- id: workspaceHandle.id,
80
- name: name,
81
- lastAccessed: new Date().toISOString(),
82
- accessCount: existingIndex !== -1 ? recentWorkspaces[existingIndex].accessCount + 1 : 1
85
+ // No branching by default (enableBranching: true to opt-in)
86
+ const workspace = await modWorkspace.createWorkspace({ name });
87
+ // Wait for workspace document to sync to server
88
+ console.log('Syncing to server...');
89
+ await new Promise(resolve => setTimeout(resolve, 3000));
90
+ const connection = {
91
+ path: currentDir,
92
+ workspaceId: workspace.id,
93
+ workspaceName: workspace.name,
94
+ connectedAt: new Date().toISOString(),
95
+ lastSyncedAt: new Date().toISOString(),
83
96
  };
84
- if (existingIndex !== -1) {
85
- recentWorkspaces[existingIndex] = workspaceRef;
86
- }
87
- else {
88
- recentWorkspaces.unshift(workspaceRef);
89
- // Keep only last 10 workspaces
90
- updatedConfig.recentWorkspaces = recentWorkspaces.slice(0, 10);
91
- }
92
- writeModConfig(updatedConfig);
93
- await invalidateWorkspaceCache();
94
- console.log(`✓ Created and switched to workspace: ${name} (${workspaceHandle.id})`);
95
- if (description) {
96
- console.log(` Description: ${description}`);
97
- }
97
+ writeWorkspaceConnection(currentDir, connection);
98
+ console.log('');
99
+ console.log(`Created workspace: ${workspace.name}`);
100
+ console.log(`ID: ${workspace.id}`);
101
+ console.log(`Path: ${currentDir}`);
102
+ console.log('');
103
+ console.log('Run `mod sync start` to begin tracking changes');
98
104
  }
99
105
  catch (error) {
100
- console.error('Failed to create workspace:', error);
101
- console.log('Try running: mod workspace list to check available workspaces');
106
+ console.error('Failed to create workspace:', error.message);
102
107
  process.exit(1);
103
108
  }
104
109
  }
105
- async function handleListWorkspaces(args, repo) {
106
- try {
107
- const modWorkspace = createModWorkspace(repo);
108
- const workspaces = await getWorkspacesWithCache(modWorkspace);
109
- if (workspaces.length === 0) {
110
- console.log('No workspaces found.');
111
- console.log('Create a new workspace with: mod workspace create <name>');
112
- return;
113
- }
114
- const config = readModConfig();
115
- const activeWorkspaceId = config?.workspaceId;
116
- console.log('Available workspaces:');
117
- workspaces.forEach((workspace, index) => {
118
- const isActive = activeWorkspaceId && workspace.id === activeWorkspaceId;
119
- const indicator = isActive ? '* ' : ' ';
120
- const quickNumber = index < 9 ? `[${index + 1}] ` : ' ';
121
- console.log(`${indicator}${quickNumber}${workspace.name} (${workspace.id})`);
122
- if (workspace.description) {
123
- console.log(` ${workspace.description}`);
124
- }
125
- const stats = [
126
- `${workspace.fileCount} files`,
127
- `${workspace.branchCount} branches`,
128
- `updated ${formatRelativeTime(workspace.lastModified)}`
129
- ].join(', ');
130
- console.log(` ${stats}`);
131
- });
132
- if (workspaces.length > 1) {
133
- console.log('\nQuick switch: mod workspace switch <number>');
134
- }
110
+ async function handleListWorkspaces() {
111
+ const connections = listWorkspaceConnections();
112
+ if (connections.length === 0) {
113
+ console.log('No workspaces connected.');
114
+ console.log('');
115
+ console.log('To connect a directory to a workspace:');
116
+ console.log(' cd /path/to/project');
117
+ console.log(' mod init');
118
+ return;
135
119
  }
136
- catch (error) {
137
- console.error('Failed to list workspaces:', error);
120
+ const currentDir = process.cwd();
121
+ const currentConnection = readWorkspaceConnection(currentDir);
122
+ console.log('Connected workspaces:');
123
+ console.log('');
124
+ for (const conn of connections) {
125
+ const isCurrent = currentConnection?.workspaceId === conn.workspaceId &&
126
+ currentConnection?.path === conn.path;
127
+ const marker = isCurrent ? '* ' : ' ';
128
+ console.log(`${marker}${conn.workspaceName}`);
129
+ console.log(` Path: ${conn.path}`);
130
+ console.log(` ID: ${conn.workspaceId}`);
131
+ console.log(` Connected: ${formatDate(conn.connectedAt)}`);
132
+ console.log('');
133
+ }
134
+ if (currentConnection) {
135
+ console.log(`Current directory is connected to: ${currentConnection.workspaceName}`);
136
+ }
137
+ else {
138
+ console.log('Current directory is not connected to any workspace.');
139
+ console.log('Run `mod init` to connect.');
140
+ }
141
+ }
142
+ async function handleWorkspaceInfo() {
143
+ const currentDir = process.cwd();
144
+ const connection = readWorkspaceConnection(currentDir);
145
+ if (!connection) {
146
+ console.log('Current directory is not connected to a workspace.');
147
+ console.log('');
148
+ console.log('To connect:');
149
+ console.log(' mod init');
138
150
  process.exit(1);
139
151
  }
152
+ console.log(`Workspace: ${connection.workspaceName}`);
153
+ console.log(`ID: ${connection.workspaceId}`);
154
+ console.log(`Path: ${connection.path}`);
155
+ console.log(`Connected: ${formatDate(connection.connectedAt)}`);
156
+ console.log(`Last synced: ${formatDate(connection.lastSyncedAt)}`);
140
157
  }
141
- async function handleSwitchWorkspace(args, repo) {
142
- const [nameOrId] = args;
143
- if (!nameOrId) {
144
- console.error('Usage: mod workspace switch <name-or-id>');
158
+ async function handleDisconnectWorkspace() {
159
+ const currentDir = process.cwd();
160
+ const connection = readWorkspaceConnection(currentDir);
161
+ if (!connection) {
162
+ console.log('Current directory is not connected to a workspace.');
145
163
  process.exit(1);
146
164
  }
147
- try {
148
- const modWorkspace = createModWorkspace(repo);
149
- const targetWorkspace = await resolveWorkspace(nameOrId, modWorkspace);
150
- if (!targetWorkspace) {
151
- console.error(`Workspace not found: ${nameOrId}`);
152
- console.log('Available workspaces:');
153
- const workspaces = await getWorkspacesWithCache(modWorkspace);
154
- workspaces.forEach((w) => console.log(` ${w.name} (${w.id})`));
155
- process.exit(1);
156
- }
157
- const config = (readModConfig() || {});
158
- const updatedConfig = {
159
- ...config,
160
- workspaceId: targetWorkspace.id,
161
- workspaceName: targetWorkspace.name,
162
- activeBranchId: undefined, // Reset to main branch
163
- lastWorkspaceSwitch: new Date().toISOString()
164
- };
165
- const recentWorkspaces = updatedConfig.recentWorkspaces || [];
166
- const existingIndex = recentWorkspaces.findIndex(w => w.id === targetWorkspace.id);
167
- const workspaceRef = {
168
- id: targetWorkspace.id,
169
- name: targetWorkspace.name,
170
- lastAccessed: new Date().toISOString(),
171
- accessCount: existingIndex !== -1 ? recentWorkspaces[existingIndex].accessCount + 1 : 1
172
- };
173
- if (existingIndex !== -1) {
174
- recentWorkspaces[existingIndex] = workspaceRef;
175
- }
176
- else {
177
- recentWorkspaces.unshift(workspaceRef);
178
- updatedConfig.recentWorkspaces = recentWorkspaces.slice(0, 10);
179
- }
180
- writeModConfig(updatedConfig);
181
- console.log(`✓ Switched to workspace: ${targetWorkspace.name} (${targetWorkspace.id})`);
165
+ const choice = await select('Disconnect from workspace?', [
166
+ { label: 'Yes, disconnect', value: 'yes' },
167
+ { label: 'Cancel', value: 'no' },
168
+ ]);
169
+ if (choice === 'no') {
170
+ console.log('Cancelled.');
171
+ return;
182
172
  }
183
- catch (error) {
184
- console.error('Failed to switch workspace:', error);
173
+ const deleted = deleteWorkspaceConnection(currentDir);
174
+ if (deleted) {
175
+ console.log(`Disconnected from workspace: ${connection.workspaceName}`);
176
+ console.log('');
177
+ console.log('The workspace data is preserved. To reconnect:');
178
+ console.log(' mod init');
179
+ }
180
+ else {
181
+ console.error('Failed to disconnect.');
185
182
  process.exit(1);
186
183
  }
187
184
  }
188
- async function handleWorkspaceInfo(args, repo) {
189
- const [nameOrId] = args;
190
- try {
191
- const modWorkspace = createModWorkspace(repo);
192
- let targetWorkspace;
193
- if (nameOrId) {
194
- const resolved = await resolveWorkspace(nameOrId, modWorkspace);
195
- if (!resolved) {
196
- console.error(`Workspace not found: ${nameOrId}`);
197
- process.exit(1);
198
- }
199
- targetWorkspace = resolved;
200
- }
201
- else {
202
- const config = readModConfig();
203
- if (!config?.workspaceId) {
204
- console.error('No active workspace configured');
205
- console.log('Set a workspace with: mod workspace switch <name>');
206
- process.exit(1);
207
- }
208
- const resolved = await resolveWorkspace(config.workspaceId, modWorkspace);
209
- if (!resolved) {
210
- console.error('Current workspace not found');
211
- process.exit(1);
212
- }
213
- targetWorkspace = resolved;
214
- }
215
- console.log(`Workspace: ${targetWorkspace.name}`);
216
- console.log(`ID: ${targetWorkspace.id}`);
217
- if (targetWorkspace.description) {
218
- console.log(`Description: ${targetWorkspace.description}`);
219
- }
220
- console.log(`Files: ${targetWorkspace.fileCount}`);
221
- console.log(`Branches: ${targetWorkspace.branchCount}`);
222
- console.log(`Last Updated: ${formatAbsoluteTime(targetWorkspace.lastModified)}`);
223
- if (targetWorkspace.permissions.length > 0) {
224
- console.log(`Permissions: ${targetWorkspace.permissions.join(', ')}`);
225
- }
226
- const config = readModConfig();
227
- if (config?.workspaceId === targetWorkspace.id && config?.activeBranchId) {
228
- console.log(`Active Branch: ${config.activeBranchId}`);
185
+ async function handleRegisterWorkspaces(repo) {
186
+ const config = readConfig();
187
+ if (!config.auth) {
188
+ console.log('Not signed in.');
189
+ console.log('');
190
+ console.log('Sign in first with: mod auth login');
191
+ process.exit(1);
192
+ }
193
+ if (!config.auth.userDocId) {
194
+ console.log('User document ID not found.');
195
+ console.log('');
196
+ console.log('Try logging in again: mod auth login');
197
+ process.exit(1);
198
+ }
199
+ const connections = listWorkspaceConnections();
200
+ if (connections.length === 0) {
201
+ console.log('No connected workspaces found.');
202
+ console.log('');
203
+ console.log('Connect a directory to a workspace with: mod init');
204
+ return;
205
+ }
206
+ console.log(`Registering ${connections.length} workspace(s) to your user account...`);
207
+ console.log('');
208
+ const modUser = createModUser(repo);
209
+ const userDocId = config.auth.userDocId;
210
+ console.log('Using user document:', userDocId);
211
+ let registered = 0;
212
+ for (const conn of connections) {
213
+ try {
214
+ await modUser.addWorkspace(userDocId, conn.workspaceId);
215
+ console.log(`✓ Registered: ${conn.workspaceName}`);
216
+ registered++;
229
217
  }
230
- else {
231
- console.log('Active Branch: main');
218
+ catch (error) {
219
+ console.warn(` Failed to register ${conn.workspaceName}: ${error.message}`);
232
220
  }
233
221
  }
234
- catch (error) {
235
- console.error('Failed to get workspace info:', error);
222
+ console.log('');
223
+ console.log(`✓ Registered ${registered} of ${connections.length} workspaces`);
224
+ console.log('');
225
+ // Wait for changes to sync to server
226
+ console.log('Syncing changes to server...');
227
+ await new Promise(resolve => setTimeout(resolve, 2000));
228
+ console.log('');
229
+ console.log('These workspaces should now appear in the web app.');
230
+ }
231
+ async function handleListRemoteWorkspaces(repo) {
232
+ const config = readConfig();
233
+ if (!config.auth) {
234
+ console.log('Not signed in.');
235
+ console.log('');
236
+ console.log('Sign in first with: mod auth login');
236
237
  process.exit(1);
237
238
  }
238
- }
239
- async function handleDeleteWorkspace(args, repo) {
240
- const [nameOrId] = args;
241
- if (!nameOrId) {
242
- console.error('Usage: mod workspace delete <name-or-id>');
239
+ if (!config.auth.userDocId) {
240
+ console.log('User document ID not found.');
241
+ console.log('');
242
+ console.log('Try logging in again: mod auth login');
243
243
  process.exit(1);
244
244
  }
245
+ console.log('Fetching workspaces from your user account...');
246
+ console.log('');
245
247
  try {
246
- const modWorkspace = createModWorkspace(repo);
247
- const targetWorkspace = await resolveWorkspace(nameOrId, modWorkspace);
248
- if (!targetWorkspace) {
249
- console.error(`Workspace not found: ${nameOrId}`);
248
+ const modUser = createModUser(repo);
249
+ const userDocId = config.auth.userDocId;
250
+ // Get the user doc
251
+ const userHandle = await repo.find(userDocId);
252
+ await userHandle.whenReady();
253
+ const userDoc = userHandle.doc();
254
+ if (!userDoc) {
255
+ console.log('Could not load user document.');
250
256
  process.exit(1);
251
257
  }
252
- console.log(`WARNING: This will permanently delete workspace "${targetWorkspace.name}"`);
253
- console.log(`Files: ${targetWorkspace.fileCount}, Branches: ${targetWorkspace.branchCount}`);
254
- console.log('Type "yes" to confirm deletion:');
255
- // Simple confirmation - in a real implementation you might want to use a proper prompt library
256
- const confirm = process.env.MOD_AUTO_CONFIRM === 'yes' ? 'yes' : 'no';
257
- if (confirm !== 'yes') {
258
- console.log('Deletion cancelled');
259
- return;
258
+ // Access workspaces from user doc
259
+ const workspaceRefs = userDoc.workspaceIds || [];
260
+ if (workspaceRefs.length === 0) {
261
+ console.log('No workspaces found in your account.');
262
+ console.log('');
263
+ console.log('To register your local workspaces:');
264
+ console.log(' mod workspace register');
265
+ process.exit(0);
260
266
  }
261
- // Note: Actual deletion would need to be implemented in ModWorkspace interface
262
- // For now, just remove from config if it's the active workspace
263
- const config = readModConfig();
264
- if (config?.workspaceId === targetWorkspace.id) {
265
- const updatedConfig = { ...config };
266
- delete updatedConfig.workspaceId;
267
- delete updatedConfig.workspaceName;
268
- delete updatedConfig.activeBranchId;
269
- writeModConfig(updatedConfig);
267
+ console.log(`Found ${workspaceRefs.length} workspace(s):\n`);
268
+ for (const wsId of workspaceRefs) {
269
+ console.log(` ${wsId}`);
270
+ // Try to load workspace name
271
+ try {
272
+ const wsHandle = await repo.find(wsId);
273
+ await wsHandle.whenReady();
274
+ const ws = wsHandle.doc();
275
+ if (ws && ws.name) {
276
+ console.log(` Name: ${ws.name}`);
277
+ }
278
+ }
279
+ catch (e) {
280
+ // Workspace might not be available
281
+ }
282
+ console.log('');
270
283
  }
271
- await invalidateWorkspaceCache();
272
- console.log(`✓ Workspace "${targetWorkspace.name}" scheduled for deletion`);
273
- console.log('Note: Full deletion implementation pending ModWorkspace.deleteWorkspace() interface');
284
+ console.log('To connect to a workspace:');
285
+ console.log(' mod workspace connect <workspace-id> <workspace-name>');
274
286
  }
275
287
  catch (error) {
276
- console.error('Failed to delete workspace:', error);
288
+ console.error('Failed to fetch workspaces:', error.message);
277
289
  process.exit(1);
278
290
  }
279
291
  }
280
- async function handleRenameWorkspace(args, repo) {
281
- const [nameOrId, newName] = args;
282
- if (!nameOrId || !newName) {
283
- console.error('Usage: mod workspace rename <name-or-id> <new-name>');
292
+ async function handleConnectWorkspace(args, repo) {
293
+ const currentDir = process.cwd();
294
+ // Check if already connected
295
+ const existing = readWorkspaceConnection(currentDir);
296
+ if (existing) {
297
+ console.log(`Already connected to workspace: ${existing.workspaceName}`);
298
+ console.log('');
299
+ console.log('To connect to a different workspace, first disconnect:');
300
+ console.log(' mod workspace disconnect');
284
301
  process.exit(1);
285
302
  }
286
- try {
287
- const modWorkspace = createModWorkspace(repo);
288
- const targetWorkspace = await resolveWorkspace(nameOrId, modWorkspace);
289
- if (!targetWorkspace) {
290
- console.error(`Workspace not found: ${nameOrId}`);
291
- process.exit(1);
292
- }
293
- const existingWorkspaces = await getWorkspacesWithCache(modWorkspace);
294
- const isDuplicate = existingWorkspaces.some((w) => w.name === newName && w.id !== targetWorkspace.id);
295
- if (isDuplicate) {
296
- console.error(`Workspace name "${newName}" already exists`);
297
- process.exit(1);
303
+ // Parse args: first positional is workspace ID, --name is optional override
304
+ let workspaceId;
305
+ let nameOverride;
306
+ for (let i = 0; i < args.length; i++) {
307
+ if (args[i] === '--name' && args[i + 1]) {
308
+ nameOverride = args[i + 1];
309
+ i++; // Skip the next arg
298
310
  }
299
- // Note: Actual rename would need to be implemented in ModWorkspace interface
300
- // For now, update local config if this is the active workspace
301
- const config = readModConfig();
302
- if (config?.workspaceId === targetWorkspace.id) {
303
- const updatedConfig = { ...config, workspaceName: newName };
304
- writeModConfig(updatedConfig);
311
+ else if (!workspaceId && !args[i].startsWith('--')) {
312
+ workspaceId = args[i];
305
313
  }
306
- await invalidateWorkspaceCache();
307
- console.log(`✓ Workspace renamed from "${targetWorkspace.name}" to "${newName}"`);
308
- console.log('Note: Full rename implementation pending ModWorkspace.renameWorkspace() interface');
309
314
  }
310
- catch (error) {
311
- console.error('Failed to rename workspace:', error);
315
+ if (!workspaceId) {
316
+ console.error('Usage: mod workspace connect <workspace-id> [--name <display-name>]');
317
+ console.error('');
318
+ console.error('To see available workspaces:');
319
+ console.error(' mod workspace list-remote');
312
320
  process.exit(1);
313
321
  }
314
- }
315
- async function resolveWorkspace(nameOrId, modWorkspace) {
316
- const workspaces = await getWorkspacesWithCache(modWorkspace);
317
- // First try exact ID match
318
- let match = workspaces.find((w) => w.id === nameOrId);
319
- if (match)
320
- return match;
321
- // Then try exact name match
322
- match = workspaces.find((w) => w.name === nameOrId);
323
- if (match)
324
- return match;
325
- // Try numbered quick-switch (1-9)
326
- const num = parseInt(nameOrId);
327
- if (!isNaN(num) && num >= 1 && num <= workspaces.length) {
328
- return workspaces[num - 1];
329
- }
330
- // Finally try fuzzy name matching
331
- const fuzzyMatches = workspaces.filter((w) => w.name.toLowerCase().includes(nameOrId.toLowerCase()));
332
- if (fuzzyMatches.length === 1) {
333
- return fuzzyMatches[0];
334
- }
335
- return null;
336
- }
337
- async function getWorkspacesWithCache(modWorkspace) {
338
- const cacheDir = '.mod/.cache';
339
- const cachePath = path.join(cacheDir, 'workspaces.json');
340
- // Ensure cache directory exists
341
- if (!fs.existsSync(cacheDir)) {
342
- fs.mkdirSync(cacheDir, { recursive: true });
343
- }
344
- // Check if cache exists and is recent (under 5 minutes old)
345
- if (fs.existsSync(cachePath)) {
346
- const stats = fs.statSync(cachePath);
347
- const ageMinutes = (Date.now() - stats.mtime.getTime()) / (1000 * 60);
348
- if (ageMinutes < 5) {
322
+ console.log(`Connecting to workspace...`);
323
+ console.log(`ID: ${workspaceId}`);
324
+ console.log('');
325
+ try {
326
+ // Fetch workspace to verify it exists and get name
327
+ const modWorkspace = createModWorkspace(repo);
328
+ const workspaceHandle = await modWorkspace.openWorkspace(workspaceId);
329
+ // Use override name if provided, otherwise fetch from workspace
330
+ const workspaceName = nameOverride || workspaceHandle.name || 'Untitled Workspace';
331
+ console.log(`Workspace: ${workspaceName}`);
332
+ // Create connection
333
+ const connection = {
334
+ path: currentDir,
335
+ workspaceId: workspaceId,
336
+ workspaceName: workspaceName,
337
+ connectedAt: new Date().toISOString(),
338
+ lastSyncedAt: new Date().toISOString(),
339
+ };
340
+ writeWorkspaceConnection(currentDir, connection);
341
+ // Register workspace to user document so it shows in web app
342
+ const config = readConfig();
343
+ if (config.auth?.userDocId) {
349
344
  try {
350
- const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
351
- return cache.workspaces;
345
+ console.log('Registering workspace to your account...');
346
+ const modUser = createModUser(repo);
347
+ await modUser.addWorkspace(config.auth.userDocId, workspaceId);
348
+ // Wait for sync
349
+ await new Promise(resolve => setTimeout(resolve, 1000));
350
+ console.log('✓ Workspace registered to your account');
352
351
  }
353
352
  catch (error) {
354
- // Cache is corrupted, fall through to refresh
353
+ console.warn('Note: Could not register workspace to account:', error.message);
355
354
  }
356
355
  }
356
+ console.log('✓ Connected successfully');
357
+ console.log('');
358
+ console.log('To import files from this directory:');
359
+ console.log(' mod init');
360
+ console.log(' > Select "Resume file import"');
361
+ }
362
+ catch (error) {
363
+ console.error('Failed to connect to workspace:', error.message);
364
+ process.exit(1);
365
+ }
366
+ }
367
+ async function handleClearWorkspace(args, repo) {
368
+ const [workspaceId] = args;
369
+ if (!workspaceId) {
370
+ const currentDir = process.cwd();
371
+ const connection = readWorkspaceConnection(currentDir);
372
+ if (!connection) {
373
+ console.error('No workspace specified and current directory is not connected.');
374
+ console.error('');
375
+ console.error('Usage: mod workspace clear <workspace-id>');
376
+ console.error(' or: mod workspace clear (when in a connected directory)');
377
+ process.exit(1);
378
+ }
379
+ console.log(`Current workspace: ${connection.workspaceName}`);
380
+ console.log(`ID: ${connection.workspaceId}`);
357
381
  }
382
+ else {
383
+ console.log(`Workspace ID: ${workspaceId}`);
384
+ }
385
+ console.log('');
386
+ console.warn('⚠️ WARNING: This will delete ALL files from the workspace.');
387
+ console.warn('⚠️ This action CANNOT be undone.');
388
+ console.log('');
389
+ const confirmation = await select('Are you absolutely sure?', [
390
+ { label: 'No, cancel', value: 'no' },
391
+ { label: 'Yes, delete all files', value: 'yes' },
392
+ ]);
393
+ if (confirmation === 'no') {
394
+ console.log('Cancelled.');
395
+ return;
396
+ }
397
+ const targetWorkspaceId = workspaceId || readWorkspaceConnection(process.cwd()).workspaceId;
358
398
  try {
359
- const workspaceHandles = await modWorkspace.listWorkspaces();
360
- // Convert WorkspaceHandle[] to CachedWorkspace format
361
- const workspaces = [];
362
- for (const handle of workspaceHandles) {
363
- // Get additional workspace data if available
364
- let fileCount = 0;
365
- let branchCount = 1;
399
+ console.log('Loading workspace...');
400
+ const modWorkspace = createModWorkspace(repo);
401
+ const workspaceHandle = await modWorkspace.openWorkspace(targetWorkspaceId);
402
+ console.log('Fetching files...');
403
+ const files = await workspaceHandle.file.list();
404
+ console.log(`Found ${files.length} files to delete`);
405
+ if (files.length === 0) {
406
+ console.log('Workspace is already empty.');
407
+ return;
408
+ }
409
+ console.log('Deleting files...');
410
+ let deleted = 0;
411
+ for (const file of files) {
366
412
  try {
367
- const files = await handle.file.list();
368
- fileCount = files.length;
369
- const branches = await handle.branch.list();
370
- branchCount = branches.length || 1;
413
+ await workspaceHandle.file.delete(file.id);
414
+ deleted++;
415
+ if (deleted % 50 === 0) {
416
+ console.log(` Deleted ${deleted}/${files.length} files...`);
417
+ }
371
418
  }
372
419
  catch (error) {
373
- // Ignore errors when fetching additional data
420
+ console.warn(` Failed to delete ${file.name}:`, error instanceof Error ? error.message : error);
374
421
  }
375
- workspaces.push({
376
- id: handle.id,
377
- name: handle.name,
378
- description: undefined, // Not available from handle
379
- fileCount,
380
- branchCount,
381
- lastModified: new Date().toISOString(), // Would need to be tracked properly
382
- permissions: [] // Would need to be populated from permission system
383
- });
384
422
  }
385
- const cache = {
386
- lastUpdate: new Date().toISOString(),
387
- workspaces: workspaces,
388
- nameIndex: workspaces.reduce((index, w) => {
389
- index[w.name] = w.id;
390
- return index;
391
- }, {})
392
- };
393
- fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
394
- return workspaces;
423
+ console.log('');
424
+ console.log(`✓ Deleted ${deleted} files`);
425
+ console.log('');
426
+ console.log('The workspace is now empty. You can import files again with:');
427
+ console.log(' mod init');
428
+ console.log(' > Select "Resume file import"');
395
429
  }
396
430
  catch (error) {
397
- console.warn('Failed to fetch workspaces:', error);
398
- return [];
399
- }
400
- }
401
- async function invalidateWorkspaceCache() {
402
- const cachePath = '.mod/.cache/workspaces.json';
403
- if (fs.existsSync(cachePath)) {
404
- fs.unlinkSync(cachePath);
431
+ console.error('Failed to clear workspace:', error.message);
432
+ process.exit(1);
405
433
  }
406
434
  }
407
- // Utility functions for time formatting
408
- function formatRelativeTime(isoString) {
435
+ function formatDate(isoString) {
409
436
  const date = new Date(isoString);
410
437
  const now = new Date();
411
- const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
438
+ const diffMs = now.getTime() - date.getTime();
439
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
412
440
  if (diffHours < 1)
413
441
  return 'just now';
414
442
  if (diffHours < 24)
415
443
  return `${diffHours}h ago`;
416
444
  if (diffHours < 24 * 7)
417
445
  return `${Math.floor(diffHours / 24)}d ago`;
418
- return `${Math.floor(diffHours / (24 * 7))}w ago`;
419
- }
420
- function formatAbsoluteTime(isoString) {
421
- const date = new Date(isoString);
422
- return date.toLocaleString();
446
+ return date.toLocaleDateString();
423
447
  }