@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,378 +1,597 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Init command for installing Claude commands and workspace initialization
4
- * Installs versioned command files from CLI package to user's .claude/commands directory
5
- * Creates a new workspace if none exists in the current directory
6
- */
7
- import fs from 'fs';
2
+ // glassware[type="implementation", id="cli-init-command--da781f68", requirements="requirement-cli-init-ux-1--1e5666e7,requirement-cli-init-ux-2--b69b045f,requirement-cli-init-ux-3--3dde4846,requirement-cli-init-ux-4--140d9249,requirement-cli-init-ux-5a--09a5bdab,requirement-cli-init-ux-5b--4cb7bb13,requirement-cli-init-ux-6--1627332e,requirement-cli-init-ux-7--97fe5eff,requirement-cli-init-app-1--1c2b11b4,requirement-cli-init-app-5--74a2ea93"]
8
3
  import path from 'path';
9
- import { fileURLToPath } from 'url';
10
- import { createModWorkspace } from '@mod/mod-core';
11
- import { readModConfig, writeModConfig } from '../services/mod-config.js';
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = path.dirname(__filename);
4
+ import fs from 'fs';
5
+ import { createModWorkspace, createModUser, setTextContent, detectMimeType, mimeTypeToCanvasType } from '@mod/mod-core';
6
+ import { readConfig, readWorkspaceConnection, writeWorkspaceConnection, ensureModDir, } from '../lib/storage.js';
7
+ import { select, input, validateWorkspaceName } from '../lib/prompts.js';
8
+ import { addWorkspaceToSharePolicy, addFilesToSharePolicy } from '../daemon/share-policy.js';
9
+ import { FileImportService } from '../services/file-import-service.js';
14
10
  export async function initCommand(args, repo) {
15
- const subcommand = args[0];
16
- if (subcommand === 'list' || args.includes('--list')) {
17
- await listAvailableCommands();
18
- process.exit(0);
19
- }
11
+ const isForce = args.includes('--force');
20
12
  try {
21
- const workspaceStatus = await checkWorkspaceStatus(repo);
22
- await ensureClaudeCommandsDirectory();
23
- const availableCommands = await discoverAvailableCommands();
24
- const installedCommands = await getInstalledCommands();
25
- const commandsToInstall = await determineCommandsToInstall(availableCommands, installedCommands, args.includes('--force'));
26
- if (commandsToInstall.length > 0) {
27
- displayInstallationPlan(commandsToInstall);
28
- const shouldProceed = args.includes('--force') || await confirmInstallation(commandsToInstall);
29
- if (!shouldProceed) {
30
- console.log('Installation cancelled');
13
+ ensureModDir();
14
+ // Check for existing workspace connection
15
+ const currentDir = process.cwd();
16
+ const existingConnection = readWorkspaceConnection(currentDir);
17
+ if (existingConnection && !isForce) {
18
+ console.log('Already initialized');
19
+ console.log(`Workspace: ${existingConnection.workspaceName}`);
20
+ console.log('');
21
+ // Offer to resume import if there are files not yet synced
22
+ const resumeChoice = await select('What would you like to do?', [
23
+ { label: 'Resume file import (if interrupted)', value: 'resume' },
24
+ { label: 'Start syncing', value: 'sync' },
25
+ { label: 'Reinitialize with different workspace', value: 'force' },
26
+ ]);
27
+ if (resumeChoice === 'resume') {
28
+ console.log('Checking for files to import...');
29
+ try {
30
+ await importDirectoryFiles(repo, { id: existingConnection.workspaceId });
31
+ console.log('Resume complete');
32
+ }
33
+ catch (error) {
34
+ console.error('Resume failed:', error instanceof Error ? error.message : error);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ else if (resumeChoice === 'sync') {
39
+ console.log('Run `mod sync start` to begin syncing');
40
+ }
41
+ else if (resumeChoice === 'force') {
42
+ console.log('Reinitializing...');
43
+ // Continue with force init below
44
+ }
45
+ if (resumeChoice !== 'force') {
31
46
  process.exit(0);
32
47
  }
33
- await installCommands(commandsToInstall);
34
48
  }
35
- const finalWorkspaceStatus = await handleWorkspaceInitialization(workspaceStatus, repo);
36
- displayInitializationSuccess(commandsToInstall, finalWorkspaceStatus);
49
+ // Check auth state
50
+ const config = readConfig();
51
+ const isAuthenticated = !!config.auth;
52
+ let workspaceConnection;
53
+ if (isAuthenticated) {
54
+ workspaceConnection = await handleAuthenticatedInit(repo, config.auth.email);
55
+ }
56
+ else {
57
+ workspaceConnection = await handleUnauthenticatedInit(repo);
58
+ }
59
+ // Workspace connection already saved during creation
60
+ // Update last synced time
61
+ workspaceConnection.lastSyncedAt = new Date().toISOString();
62
+ writeWorkspaceConnection(currentDir, workspaceConnection);
63
+ // Install the /mod agent skill for spec-driven development
64
+ installModSkill();
65
+ console.log('Installed /mod skill to .claude/skills/mod/');
66
+ // Display success message
67
+ displayInitializationSuccess(workspaceConnection, isAuthenticated);
37
68
  process.exit(0);
38
69
  }
39
70
  catch (error) {
40
- handleInstallationError(error);
71
+ console.error('Initialization failed:', error.message);
72
+ process.exit(1);
41
73
  }
42
74
  }
43
- async function ensureClaudeCommandsDirectory() {
44
- const claudeDir = path.join(process.cwd(), '.claude');
45
- const commandsDir = path.join(claudeDir, 'commands');
75
+ // glassware[type="implementation", id="impl-init-authenticated--1fe6c419", requirements="requirement-cli-init-app-2--51f0306f,requirement-cli-init-app-3--76844b98,requirement-cli-init-qual-1--ca672702"]
76
+ async function handleAuthenticatedInit(repo, email) {
77
+ console.log(`Signed in as ${email}`);
78
+ // Get workspace list from user document (app-2)
79
+ const config = readConfig();
80
+ let workspaces = [];
46
81
  try {
47
- if (!fs.existsSync(claudeDir)) {
48
- fs.mkdirSync(claudeDir, { recursive: true, mode: 0o755 });
82
+ const userDocId = config.auth?.userDocId;
83
+ if (!userDocId) {
84
+ console.warn('User document not found. Creating local workspace.');
49
85
  }
50
- if (!fs.existsSync(commandsDir)) {
51
- fs.mkdirSync(commandsDir, { recursive: true, mode: 0o755 });
86
+ else {
87
+ // Fetch workspaces from user document (app-3)
88
+ const userHandle = await repo.find(userDocId);
89
+ await userHandle.whenReady();
90
+ const userDoc = userHandle.doc();
91
+ if (userDoc) {
92
+ const workspaceIds = userDoc.workspaceIds || [];
93
+ // Load workspace metadata for each workspace
94
+ for (const wsId of workspaceIds) {
95
+ try {
96
+ const wsHandle = await repo.find(wsId);
97
+ await wsHandle.whenReady();
98
+ const ws = wsHandle.doc();
99
+ workspaces.push({
100
+ id: wsId,
101
+ name: ws?.title || ws?.name || 'Untitled',
102
+ });
103
+ }
104
+ catch {
105
+ // Workspace might not be available, add with just ID
106
+ workspaces.push({
107
+ id: wsId,
108
+ name: `Workspace ${String(wsId).slice(0, 8)}`,
109
+ });
110
+ }
111
+ }
112
+ }
52
113
  }
53
114
  }
54
115
  catch (error) {
55
- throw new Error(`Failed to create .claude/commands directory: ${error.message}`);
56
- }
57
- }
58
- async function discoverAvailableCommands() {
59
- // Navigate from dist/commands/init.js to package root commands/
60
- // __dirname is dist/commands, so go up two levels to package root
61
- const packageRoot = path.resolve(__dirname, '../..');
62
- const commandsSourceDir = path.join(packageRoot, 'commands');
63
- if (!fs.existsSync(commandsSourceDir)) {
64
- console.warn('āš ļø No commands directory found in CLI package');
65
- return [];
66
- }
67
- const commands = [];
68
- const files = fs.readdirSync(commandsSourceDir).filter(f => f.endsWith('.md'));
69
- for (const filename of files) {
70
- const filePath = path.join(commandsSourceDir, filename);
71
- const content = fs.readFileSync(filePath, 'utf8');
72
- const metadata = parseFrontmatter(content);
73
- commands.push({
74
- filename,
75
- path: filePath,
76
- metadata,
77
- content
78
- });
79
- }
80
- return commands;
81
- }
82
- async function getInstalledCommands() {
83
- const commandsDir = path.join(process.cwd(), '.claude', 'commands');
84
- const installed = new Map();
85
- if (!fs.existsSync(commandsDir)) {
86
- return installed;
87
- }
88
- const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
89
- for (const filename of files) {
90
- const filePath = path.join(commandsDir, filename);
116
+ // Handle network errors gracefully (qual-1)
117
+ console.warn('Could not load cloud workspaces. Creating local workspace.');
118
+ }
119
+ // Build options
120
+ const options = [
121
+ ...workspaces.map((w) => ({
122
+ label: w.name,
123
+ value: { type: 'existing', id: w.id, name: w.name },
124
+ })),
125
+ {
126
+ label: '+ Create new workspace',
127
+ value: { type: 'create', id: '', name: '' },
128
+ },
129
+ ];
130
+ const choice = await select('Select workspace:', options);
131
+ if (choice.type === 'create') {
132
+ return await createNewWorkspace(repo);
133
+ }
134
+ // Add existing workspace to share policy so it can sync
135
+ addWorkspaceToSharePolicy(choice.id);
136
+ // Save workspace connection immediately
137
+ const currentDir = process.cwd();
138
+ const workspaceConnection = {
139
+ path: currentDir,
140
+ workspaceId: choice.id,
141
+ workspaceName: choice.name,
142
+ connectedAt: new Date().toISOString(),
143
+ lastSyncedAt: new Date().toISOString(),
144
+ };
145
+ writeWorkspaceConnection(currentDir, workspaceConnection);
146
+ console.log('Workspace connection saved');
147
+ // Register existing workspace on UserDoc (in case it wasn't registered before)
148
+ if (config.auth?.userDocId) {
91
149
  try {
92
- const content = fs.readFileSync(filePath, 'utf8');
93
- const metadata = parseFrontmatter(content);
94
- installed.set(filename, metadata);
150
+ const modUser = createModUser(repo);
151
+ await modUser.addWorkspace(config.auth.userDocId, choice.id);
95
152
  }
96
153
  catch (error) {
97
- console.warn(`āš ļø Could not read metadata from ${filename}: ${error.message}`);
154
+ // Silently ignore - workspace is already connected
98
155
  }
99
156
  }
100
- return installed;
157
+ // Import files from directory if this is a new connection
158
+ await importDirectoryFiles(repo, { id: choice.id });
159
+ return workspaceConnection;
101
160
  }
102
- function parseFrontmatter(content) {
103
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
104
- if (!frontmatterMatch) {
105
- // Return default metadata for files without frontmatter - they need to be updated
106
- return {
107
- version: '0.0.0', // Old version to trigger update
108
- updated: '1970-01-01',
109
- checksum: '',
110
- description: undefined
111
- };
112
- }
113
- const frontmatterText = frontmatterMatch[1];
114
- const metadata = {};
115
- // Simple YAML parsing for required fields
116
- const lines = frontmatterText.split('\n');
117
- for (const line of lines) {
118
- const match = line.match(/^(\w+):\s*(.+)$/);
119
- if (match) {
120
- const [, key, value] = match;
121
- metadata[key] = value.trim().replace(/^["']|["']$/g, '');
122
- }
161
+ async function handleUnauthenticatedInit(repo) {
162
+ const choice = await select('Select option:', [
163
+ {
164
+ label: 'Create local workspace',
165
+ value: 'create',
166
+ description: 'Work offline, sync later',
167
+ },
168
+ {
169
+ label: 'Sign in to sync with team',
170
+ value: 'signin',
171
+ description: 'Access cloud workspaces',
172
+ },
173
+ ]);
174
+ if (choice === 'signin') {
175
+ // Trigger auth flow
176
+ console.log('');
177
+ console.log('Please run `mod auth login` to sign in, then run `mod init` again.');
178
+ process.exit(0);
123
179
  }
124
- return {
125
- version: metadata.version || '1.0.0',
126
- updated: metadata.updated || new Date().toISOString(),
127
- checksum: metadata.checksum || '',
128
- description: metadata.description
129
- };
180
+ return await createNewWorkspace(repo);
130
181
  }
131
- async function determineCommandsToInstall(available, installed, force) {
132
- const toInstall = [];
133
- for (const command of available) {
134
- const installedMetadata = installed.get(command.filename);
135
- if (force || !installedMetadata) {
136
- toInstall.push(command);
137
- continue;
182
+ // glassware[type="implementation", id="impl-init-create-workspace--4ff92c7c", requirements="requirement-cli-init-app-4--761a37a6,requirement-cli-init-int-1--e3f9f2b6"]
183
+ async function createNewWorkspace(repo) {
184
+ const currentDirName = path.basename(process.cwd());
185
+ const currentDir = process.cwd();
186
+ const defaultName = currentDirName.charAt(0).toUpperCase() + currentDirName.slice(1);
187
+ const name = await input('Workspace name', {
188
+ default: defaultName,
189
+ validate: validateWorkspaceName,
190
+ });
191
+ console.log('Creating workspace...');
192
+ // Create new workspace - works with or without auth (app-4)
193
+ const modWorkspace = createModWorkspace(repo);
194
+ // No branching by default (enableBranching: true to opt-in)
195
+ // The CLI stores workspace IDs in its local storage instead
196
+ const workspace = await modWorkspace.createWorkspace({ name });
197
+ // Add workspace to share policy so it can sync
198
+ addWorkspaceToSharePolicy(workspace.id);
199
+ // CRITICAL: Save workspace connection immediately before import
200
+ // This allows resuming if import is interrupted
201
+ const workspaceConnection = {
202
+ path: currentDir,
203
+ workspaceId: workspace.id,
204
+ workspaceName: workspace.name,
205
+ connectedAt: new Date().toISOString(),
206
+ lastSyncedAt: new Date().toISOString(),
207
+ };
208
+ writeWorkspaceConnection(currentDir, workspaceConnection);
209
+ console.log('Workspace connection saved');
210
+ // Wait for workspace document to sync to server
211
+ console.log('Syncing workspace...');
212
+ await new Promise(resolve => setTimeout(resolve, 2000));
213
+ // Register workspace on UserDoc for cross-device discovery (do this before import)
214
+ const config = readConfig();
215
+ if (config.auth?.userDocId) {
216
+ try {
217
+ const modUser = createModUser(repo);
218
+ await modUser.addWorkspace(config.auth.userDocId, workspace.id);
219
+ // Wait for user doc to sync
220
+ await new Promise(resolve => setTimeout(resolve, 1000));
221
+ console.log('Workspace registered for sync');
138
222
  }
139
- // Compare versions
140
- if (compareVersions(command.metadata.version, installedMetadata.version) > 0) {
141
- toInstall.push(command);
223
+ catch (error) {
224
+ // Don't fail the init if registration fails - workspace still created
225
+ console.warn('Note: Could not register workspace for cross-device sync');
142
226
  }
143
227
  }
144
- return toInstall;
145
- }
146
- function compareVersions(v1, v2) {
147
- const parts1 = v1.split('.').map(n => parseInt(n, 10));
148
- const parts2 = v2.split('.').map(n => parseInt(n, 10));
149
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
150
- const part1 = parts1[i] || 0;
151
- const part2 = parts2[i] || 0;
152
- if (part1 > part2)
153
- return 1;
154
- if (part1 < part2)
155
- return -1;
156
- }
157
- return 0;
228
+ // Import existing files from the directory
229
+ await importDirectoryFiles(repo, workspace);
230
+ console.log('Workspace synced');
231
+ return workspaceConnection;
158
232
  }
159
- async function listAvailableCommands() {
160
- console.log('šŸ“‹ Available Claude Commands:\n');
233
+ // glassware[type="implementation", id="impl-cli-init-import--74b5c00f", requirements="requirement-cli-add-init-integration--e953a345"]
234
+ // TODO: Migrate to use AddService instead of FileImportService for consistency with `mod add`
235
+ async function importDirectoryFiles(repo, workspace) {
236
+ const currentDir = process.cwd();
237
+ // Check if directory has any files to import
238
+ const entries = fs.readdirSync(currentDir);
239
+ const hasFiles = entries.some(entry => {
240
+ const fullPath = path.join(currentDir, entry);
241
+ const stats = fs.statSync(fullPath);
242
+ return stats.isFile() && entry !== '.modconfig';
243
+ });
244
+ if (!hasFiles) {
245
+ console.log('No files found to import');
246
+ return;
247
+ }
248
+ console.log('Importing files from directory...');
249
+ const importService = new FileImportService(repo);
161
250
  try {
162
- const commands = await discoverAvailableCommands();
163
- if (commands.length === 0) {
164
- console.log(' No commands available');
251
+ // Scan for files to import
252
+ const preview = await importService.previewImport({
253
+ workingDirectory: currentDir,
254
+ verbose: false
255
+ });
256
+ if (preview.filteredFiles.length === 0) {
257
+ console.log('No trackable files found to import');
165
258
  return;
166
259
  }
167
- for (const command of commands) {
168
- console.log(` ${command.filename.replace('.md', '')}`);
169
- console.log(` ā”œā”€ā”€ Version: ${command.metadata.version}`);
170
- if (command.metadata.description) {
171
- console.log(` └── Description: ${command.metadata.description}`);
172
- }
173
- else {
174
- console.log(` └── Updated: ${command.metadata.updated}`);
175
- }
176
- console.log();
177
- }
178
- }
179
- catch (error) {
180
- console.error('āŒ Error listing commands:', error.message);
181
- }
182
- }
183
- function displayInstallationPlan(commands) {
184
- console.log('šŸ“¦ Installing Claude Commands:\n');
185
- for (const command of commands) {
186
- console.log(` ${command.filename.replace('.md', '')} (v${command.metadata.version})`);
187
- }
188
- console.log();
189
- }
190
- async function confirmInstallation(commands) {
191
- // In a real implementation, this would use an interactive prompt library
192
- // For now, we'll assume confirmation
193
- return true;
194
- }
195
- async function installCommands(commands) {
196
- const commandsDir = path.join(process.cwd(), '.claude', 'commands');
197
- const tempFiles = [];
198
- try {
199
- // Install to temporary files first for atomic operation
200
- for (const command of commands) {
201
- const tempPath = path.join(commandsDir, `${command.filename}.tmp`);
202
- const finalPath = path.join(commandsDir, command.filename);
203
- const contentWithChecksum = await updateChecksumInContent(command.content);
204
- fs.writeFileSync(tempPath, contentWithChecksum, 'utf8');
205
- tempFiles.push(tempPath);
206
- }
207
- // Move all temp files to final locations atomically
208
- for (let i = 0; i < commands.length; i++) {
209
- const tempPath = tempFiles[i];
210
- const finalPath = path.join(commandsDir, commands[i].filename);
211
- fs.renameSync(tempPath, finalPath);
212
- }
213
- }
214
- catch (error) {
215
- cleanup(tempFiles);
216
- throw error;
217
- }
218
- }
219
- async function updateChecksumInContent(content) {
220
- // Use dynamic import for crypto in ESM
221
- const crypto = await import('crypto');
222
- const contentHash = crypto
223
- .createHash('sha256')
224
- .update(content)
225
- .digest('hex')
226
- .substring(0, 16);
227
- // Update checksum in frontmatter
228
- // First, remove any existing checksum lines
229
- const withoutChecksum = content.replace(/^(\s*)checksum:\s*\S*$/gm, '');
230
- // Then add the new checksum after the frontmatter opening
231
- return withoutChecksum.replace(/^(---\n)([\s\S]*?)(---)/, `$1$2checksum: ${contentHash}\n$3`);
232
- }
233
- function cleanup(tempFiles) {
234
- for (const file of tempFiles) {
235
- try {
236
- if (fs.existsSync(file)) {
237
- fs.unlinkSync(file);
238
- }
260
+ console.log(`Found ${preview.filteredFiles.length} files to scan`);
261
+ // Import the files with batching to avoid memory issues
262
+ const modWorkspace = createModWorkspace(repo);
263
+ const workspaceHandle = await modWorkspace.openWorkspace(workspace.id);
264
+ // Query existing files to enable resume capability
265
+ console.log('Checking for existing files...');
266
+ const existingFiles = await workspaceHandle.file.list();
267
+ const existingPaths = new Set(existingFiles
268
+ .map(f => f.metadata?.typeData?.path || f.metadata?.typeData?.relativePath)
269
+ .filter(Boolean));
270
+ // Filter out files that already exist
271
+ const filesToImport = preview.filteredFiles.filter(filePath => {
272
+ const relativePath = path.relative(currentDir, filePath);
273
+ return !existingPaths.has(relativePath);
274
+ });
275
+ if (filesToImport.length === 0) {
276
+ console.log('All files already imported, nothing to do');
277
+ return;
239
278
  }
240
- catch (error) {
241
- console.warn(`āš ļø Could not clean up temporary file ${file}: ${error.message}`);
279
+ const skippedCount = preview.filteredFiles.length - filesToImport.length;
280
+ if (skippedCount > 0) {
281
+ console.log(`Skipping ${skippedCount} files that already exist`);
242
282
  }
243
- }
244
- }
245
- function displayInitializationSuccess(commands, workspaceStatus) {
246
- console.log('āœ… Initialization Complete!\n');
247
- if (commands.length > 0) {
248
- console.log(`šŸ“¦ Installed ${commands.length} Claude command(s):\n`);
249
- for (const command of commands) {
250
- const commandName = command.filename.replace('.md', '');
251
- console.log(` /${commandName} - ${command.metadata.description || 'Mod workflow command'}`);
283
+ console.log(`Importing ${filesToImport.length} new files`);
284
+ const importedFileIds = [];
285
+ let importedCount = 0;
286
+ const BATCH_SIZE = 20; // Reduced batch size to avoid Automerge concurrency issues
287
+ // Split files into batches
288
+ const batches = [];
289
+ for (let i = 0; i < filesToImport.length; i += BATCH_SIZE) {
290
+ batches.push(filesToImport.slice(i, i + BATCH_SIZE));
252
291
  }
253
- console.log();
254
- }
255
- else {
256
- console.log('āœ… All Claude commands are up to date\n');
257
- }
258
- if (workspaceStatus.created) {
259
- console.log(`šŸš€ Created new workspace: "${workspaceStatus.workspaceName}"`);
260
- }
261
- else {
262
- console.log(`šŸ“ Using existing workspace: "${workspaceStatus.workspaceName}"`);
263
- }
264
- console.log('\nšŸ’” Usage:');
265
- console.log(' Open Claude Code and type "/" to see available commands');
266
- console.log(' Use "mod workspace list" to see all workspaces');
267
- console.log(' Commands are available in your .claude/commands directory\n');
268
- }
269
- async function checkWorkspaceStatus(repo) {
270
- try {
271
- const config = await readModConfig();
272
- if (config && config.workspaceId) {
273
- let workspaceName = 'Existing Workspace';
274
- // Try to get the actual workspace name from the workspace service
275
- if (repo) {
292
+ // Process each batch
293
+ for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
294
+ const batch = batches[batchIndex];
295
+ // Process files sequentially to avoid Automerge concurrency issues
296
+ // Concurrent modifications cause "recursive use of an object" errors
297
+ for (const filePath of batch) {
276
298
  try {
277
- const modWorkspace = createModWorkspace(repo);
278
- const workspaces = await modWorkspace.listWorkspaces();
279
- const currentWorkspace = workspaces.find(w => w.id === config.workspaceId);
280
- if (currentWorkspace) {
281
- workspaceName = currentWorkspace.name;
299
+ const content = fs.readFileSync(filePath, 'utf-8');
300
+ const relativePath = path.relative(currentDir, filePath);
301
+ const pathParts = relativePath.split(path.sep);
302
+ const fileName = pathParts.pop();
303
+ // Detect proper mime type instead of hardcoding markdown
304
+ const mimeType = detectMimeType(fileName);
305
+ const canvasType = mimeTypeToCanvasType(mimeType);
306
+ const isCodeFile = canvasType === 'code';
307
+ const isTextFile = mimeType.startsWith('text/') ||
308
+ mimeType === 'application/json' ||
309
+ mimeType === 'application/javascript';
310
+ // Create file in workspace
311
+ // Code files: Store content directly (no prosemirror formatting)
312
+ // Text/markdown files: Create empty first, then apply setTextContent for proper formatting
313
+ const fileDoc = await workspaceHandle.file.create({
314
+ text: isCodeFile ? content : '',
315
+ metadata: {
316
+ type: 'text',
317
+ path: relativePath,
318
+ name: fileName,
319
+ mimeType,
320
+ createdAt: new Date().toISOString(),
321
+ updatedAt: new Date().toISOString(),
322
+ createdBy: 'cli'
323
+ }
324
+ }, {
325
+ name: fileName,
326
+ mimeType
327
+ });
328
+ // Apply proper richtext formatting using setTextContent for text/markdown files only
329
+ // Code files should NOT use setTextContent - they need plain text for CodeMirror
330
+ if (isTextFile && !isCodeFile && content.length > 0) {
331
+ await setTextContent(fileDoc, ['text'], content);
282
332
  }
333
+ importedFileIds.push(fileDoc.documentId);
334
+ importedCount++;
335
+ // Longer delay to ensure Automerge completes save cycle
336
+ // The "recursive use" error happens when save is triggered while another save is in progress
337
+ await new Promise(resolve => setTimeout(resolve, 100));
283
338
  }
284
339
  catch (error) {
285
- // Fallback to generic name if we can't fetch workspace details
286
- console.warn('āš ļø Could not fetch workspace name, using generic name');
340
+ console.warn(` Failed to import ${filePath}:`, error instanceof Error ? error.message : error);
341
+ // On Automerge error, wait longer before continuing
342
+ if (error instanceof Error && error.message.includes('recursive')) {
343
+ console.warn(' Automerge concurrency detected, waiting 2s...');
344
+ await new Promise(resolve => setTimeout(resolve, 2000));
345
+ }
287
346
  }
288
347
  }
289
- return {
290
- exists: true,
291
- created: false,
292
- workspaceName,
293
- workspaceId: config.workspaceId
294
- };
348
+ console.log(` Imported ${importedCount}/${filesToImport.length} files...`);
349
+ // Trigger garbage collection hint between batches to free memory
350
+ if (global.gc && batchIndex < batches.length - 1) {
351
+ global.gc();
352
+ }
353
+ // Small delay between batches to allow memory cleanup
354
+ if (batchIndex < batches.length - 1) {
355
+ await new Promise(resolve => setTimeout(resolve, 100));
356
+ }
295
357
  }
296
- // No existing workspace found
297
- return {
298
- exists: false,
299
- created: false,
300
- workspaceName: 'New Workspace',
301
- workspaceId: undefined
302
- };
303
- }
304
- catch (error) {
305
- // No config file exists, so no workspace
306
- return {
307
- exists: false,
308
- created: false,
309
- workspaceName: 'New Workspace',
310
- workspaceId: undefined
311
- };
312
- }
313
- }
314
- async function handleWorkspaceInitialization(workspaceStatus, repo) {
315
- if (workspaceStatus.exists) {
316
- return workspaceStatus;
317
- }
318
- try {
319
- const modWorkspace = createModWorkspace(repo);
320
- const workspaceName = generateWorkspaceName();
321
- console.log(`šŸš€ Creating new workspace: "${workspaceName}"...`);
322
- const workspace = await modWorkspace.createWorkspace({
323
- name: workspaceName
324
- });
325
- const config = {
326
- workspaceId: workspace.id,
327
- activeBranchId: undefined
328
- };
329
- await writeModConfig(config);
330
- return {
331
- exists: false,
332
- created: true,
333
- workspaceName: workspace.name,
334
- workspaceId: workspace.id
335
- };
358
+ // Add imported files to share policy in batches
359
+ if (importedFileIds.length > 0) {
360
+ const SHARE_POLICY_BATCH_SIZE = 100;
361
+ for (let i = 0; i < importedFileIds.length; i += SHARE_POLICY_BATCH_SIZE) {
362
+ const batch = importedFileIds.slice(i, i + SHARE_POLICY_BATCH_SIZE);
363
+ addFilesToSharePolicy(batch);
364
+ }
365
+ }
366
+ console.log(`āœ“ Imported ${importedCount} files (${skippedCount} already existed)`);
336
367
  }
337
368
  catch (error) {
338
- console.warn(`āš ļø Failed to create workspace: ${error.message}`);
339
- console.log(' Continuing with command installation only.');
340
- return {
341
- exists: false,
342
- created: false,
343
- workspaceName: 'Failed to create',
344
- workspaceId: undefined
345
- };
369
+ console.warn('Failed to import files:', error instanceof Error ? error.message : error);
370
+ throw error; // Re-throw so caller knows import failed
346
371
  }
347
372
  }
348
- function generateWorkspaceName() {
349
- const currentDir = path.basename(process.cwd());
350
- const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
351
- // Use directory name if it's meaningful, otherwise use timestamp
352
- if (currentDir && currentDir !== '.' && currentDir !== '/' && currentDir.length > 1) {
353
- return currentDir.charAt(0).toUpperCase() + currentDir.slice(1);
373
+ /**
374
+ * Install the /mod agent skill to .claude/skills/mod/
375
+ * This enables coding agents to use spec-driven development workflows
376
+ */
377
+ function installModSkill() {
378
+ const currentDir = process.cwd();
379
+ const skillDir = path.join(currentDir, '.claude', 'skills', 'mod');
380
+ const referencesDir = path.join(skillDir, 'references');
381
+ // Create directories if they don't exist
382
+ if (!fs.existsSync(skillDir)) {
383
+ fs.mkdirSync(skillDir, { recursive: true });
384
+ }
385
+ if (!fs.existsSync(referencesDir)) {
386
+ fs.mkdirSync(referencesDir, { recursive: true });
387
+ }
388
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
389
+ const commandsMdPath = path.join(referencesDir, 'commands.md');
390
+ // Only write if files don't exist (don't overwrite user modifications)
391
+ if (!fs.existsSync(skillMdPath)) {
392
+ fs.writeFileSync(skillMdPath, SKILL_MD_CONTENT);
393
+ }
394
+ if (!fs.existsSync(commandsMdPath)) {
395
+ fs.writeFileSync(commandsMdPath, COMMANDS_MD_CONTENT);
354
396
  }
355
- return `Workspace-${timestamp}`;
356
397
  }
357
- function handleInstallationError(error) {
358
- console.error('āŒ Initialization failed:');
359
- if (error.code === 'EACCES' || error.code === 'EPERM') {
360
- console.error(' Permission denied. Try running with appropriate permissions.');
361
- console.error(' Make sure you have write access to the .claude/commands directory.');
362
- }
363
- else if (error.code === 'ENOSPC') {
364
- console.error(' Insufficient disk space.');
365
- }
366
- else if (error.code === 'ENOTDIR') {
367
- console.error(' Invalid directory structure. Ensure .claude is a directory, not a file.');
398
+ const SKILL_MD_CONTENT = `---
399
+ name: mod
400
+ description: Spec-driven development with traceability. Implements specs, adds traces, verifies coverage. Use when implementing from specifications, adding requirement traceability, or checking trace coverage before merging.
401
+ ---
402
+
403
+ # Mod: Spec-Driven Development
404
+
405
+ Use this skill when the user asks to implement from specs, add traceability, or verify coverage.
406
+
407
+ ## Triggers
408
+
409
+ - "implement this spec"
410
+ - "implement specs/*.md"
411
+ - "/mod implement <file>"
412
+ - "add tests for this spec"
413
+ - "check trace coverage"
414
+
415
+ ## Workflow
416
+
417
+ ### 1. Setup (if needed)
418
+
419
+ Check if workspace exists:
420
+ \`\`\`bash
421
+ mod status
422
+ \`\`\`
423
+
424
+ If not initialized:
425
+ \`\`\`bash
426
+ mod init
427
+ mod auth login
428
+ \`\`\`
429
+
430
+ ### 2. Read the Spec
431
+
432
+ Parse the spec file for requirements. Requirements may be marked with glassware annotations or be plain markdown sections.
433
+
434
+ ### 3. Implement with Traces
435
+
436
+ For each requirement:
437
+
438
+ 1. Write the implementation
439
+ 2. Add a trace connecting implementation to requirement:
440
+ \`\`\`bash
441
+ mod trace add <file>:<line> --type=implementation --link=<requirement-id>
442
+ \`\`\`
443
+
444
+ 3. Verify the trace was added:
445
+ \`\`\`bash
446
+ mod trace report <spec-file>
447
+ \`\`\`
448
+
449
+ ### 4. Add Tests with Traces
450
+
451
+ For each implementation:
452
+
453
+ 1. Write tests
454
+ 2. Add traces connecting tests to implementations:
455
+ \`\`\`bash
456
+ mod trace add <test-file>:<line> --type=test --link=<implementation-id>
457
+ \`\`\`
458
+
459
+ ### 5. Verify Coverage
460
+
461
+ Before completing, always run:
462
+ \`\`\`bash
463
+ mod trace report <spec-file>
464
+ mod trace unmet
465
+ mod trace diff
466
+ \`\`\`
467
+
468
+ Address any gaps:
469
+ - Unmet requirements -> implement them
470
+ - Untraced files -> add traces or mark as utility
471
+
472
+ ### 6. Final Validation
473
+
474
+ \`\`\`bash
475
+ mod trace diff && mod trace unmet
476
+ \`\`\`
477
+
478
+ Only report completion when both commands exit 0 (all files traced, all requirements implemented).
479
+
480
+ ## Commands Reference
481
+
482
+ | Command | Purpose |
483
+ |---------|---------|
484
+ | \`mod init\` | Initialize workspace |
485
+ | \`mod auth login\` | Authenticate |
486
+ | \`mod trace add <file>:<line> --type=<type> [--link=<id>]\` | Add trace |
487
+ | \`mod trace link <source> <target>\` | Link two traces |
488
+ | \`mod trace report <file>\` | Show trace coverage for spec |
489
+ | \`mod trace coverage\` | Workspace-wide stats |
490
+ | \`mod trace unmet\` | Requirements without implementations |
491
+ | \`mod trace diff\` | Untraced files on branch (auto-detects base) |
492
+
493
+ ## Types
494
+
495
+ - \`requirement\` - Spec requirement
496
+ - \`specification\` - Technical spec detail
497
+ - \`implementation\` - Code that builds
498
+ - \`test\` - Code that verifies
499
+ - \`utility\` - Intentionally untraced helpers
500
+
501
+ ## Important
502
+
503
+ - Always verify coverage before completing
504
+ - If \`mod trace diff\` or \`mod trace unmet\` fails, address gaps before finishing
505
+ - Use \`mod trace unmet\` to find requirements that still need implementation
506
+ - Mark helper files as \`utility\` type to avoid false positives in diff
507
+ `;
508
+ const COMMANDS_MD_CONTENT = `# Mod CLI Commands Reference
509
+
510
+ ## Authentication
511
+
512
+ \`\`\`bash
513
+ mod auth login # OAuth login (opens browser)
514
+ mod auth login --dev # Dev mode (no OAuth)
515
+ mod auth logout
516
+ mod auth status
517
+ \`\`\`
518
+
519
+ ## Workspace
520
+
521
+ \`\`\`bash
522
+ mod init # Initialize workspace in current directory
523
+ mod status # Show workspace status
524
+ \`\`\`
525
+
526
+ ## Traces
527
+
528
+ \`\`\`bash
529
+ # Add traces
530
+ mod trace add <file>:<line> --type=<type> ["description"]
531
+ mod trace add <file>:<line> --type=<type> --link=<trace-id>
532
+
533
+ # Link traces
534
+ mod trace link <source-id> <target-id>
535
+
536
+ # View traces
537
+ mod trace list # All traces
538
+ mod trace list --type=requirement # Filter by type
539
+ mod trace list --file=<path> # Filter by file
540
+ mod trace get <trace-id> # Get trace details
541
+
542
+ # Reports
543
+ mod trace report <file> # Per-document coverage
544
+ mod trace coverage # Workspace-wide stats
545
+
546
+ # Find gaps
547
+ mod trace diff # Untraced files on branch (auto-detects base)
548
+ mod trace diff main..HEAD # Explicit range
549
+ mod trace unmet # Requirements without implementations
550
+ \`\`\`
551
+
552
+ ## Trace Types
553
+
554
+ | Type | Use For |
555
+ |------|---------|
556
+ | \`requirement\` | Specs, user stories, acceptance criteria |
557
+ | \`specification\` | Detailed technical specs |
558
+ | \`implementation\` | Code that builds something |
559
+ | \`test\` | Code that verifies something |
560
+ | \`design\` | Design docs, architecture notes |
561
+ | \`decision\` | ADRs, decision records |
562
+ | \`utility\` | Helpers that don't need tracing |
563
+
564
+ ## Comments
565
+
566
+ \`\`\`bash
567
+ mod comment add <file>:<line> "text"
568
+ mod comment list [file]
569
+ \`\`\`
570
+
571
+ ## Exit Codes for CI
572
+
573
+ Commands exit non-zero when issues exist:
574
+
575
+ | Command | Exit 0 | Exit 1 |
576
+ |---------|--------|--------|
577
+ | \`mod trace diff\` | All changed files traced | Untraced files exist |
578
+ | \`mod trace unmet\` | All requirements implemented | Unmet requirements |
579
+ | \`mod trace report\` | Always (informational) | - |
580
+
581
+ Pre-merge validation:
582
+ \`\`\`bash
583
+ mod trace diff && mod trace unmet && git push
584
+ \`\`\`
585
+ `;
586
+ function displayInitializationSuccess(connection, isAuthenticated) {
587
+ console.log('');
588
+ console.log('Initialized Mod in ' + connection.path);
589
+ console.log(`Workspace: ${connection.workspaceName}`);
590
+ console.log('');
591
+ if (isAuthenticated) {
592
+ console.log('Run `mod sync start` to begin syncing');
368
593
  }
369
594
  else {
370
- console.error(` ${error.message}`);
595
+ console.log('Run `mod auth login` to enable sync with collaborators');
371
596
  }
372
- console.error('\nšŸ”§ Troubleshooting:');
373
- console.error(' - Ensure you have write permissions in the current directory');
374
- console.error(' - Try running "mod init --force" to overwrite existing files');
375
- console.error(' - Check that .claude is not a file (should be a directory)');
376
- console.error(' - For workspace issues, try "mod workspace create <name>" separately');
377
- process.exit(1);
378
597
  }