@mod-computer/cli 0.1.1 → 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 +23743 -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 +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 +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
@@ -1,20 +1,14 @@
1
1
  #!/usr/bin/env node
2
- // glassware[type=implementation, id=cli-init-command, requirements=req-cli-init-ux-1,req-cli-init-ux-2,req-cli-init-ux-3,req-cli-init-ux-4,req-cli-init-ux-5a,req-cli-init-ux-5b,req-cli-init-ux-6,req-cli-init-ux-7,req-cli-init-app-1,req-cli-init-app-5]
3
- 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"]
4
3
  import path from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { createModWorkspace } from '@mod/mod-core';
4
+ import fs from 'fs';
5
+ import { createModWorkspace, createModUser, setTextContent, detectMimeType, mimeTypeToCanvasType } from '@mod/mod-core';
7
6
  import { readConfig, readWorkspaceConnection, writeWorkspaceConnection, ensureModDir, } from '../lib/storage.js';
8
7
  import { select, input, validateWorkspaceName } from '../lib/prompts.js';
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
8
+ import { addWorkspaceToSharePolicy, addFilesToSharePolicy } from '../daemon/share-policy.js';
9
+ import { FileImportService } from '../services/file-import-service.js';
11
10
  export async function initCommand(args, repo) {
12
- const subcommand = args[0];
13
11
  const isForce = args.includes('--force');
14
- if (subcommand === 'list' || args.includes('--list')) {
15
- await listAvailableCommands();
16
- process.exit(0);
17
- }
18
12
  try {
19
13
  ensureModDir();
20
14
  // Check for existing workspace connection
@@ -24,18 +18,33 @@ export async function initCommand(args, repo) {
24
18
  console.log('Already initialized');
25
19
  console.log(`Workspace: ${existingConnection.workspaceName}`);
26
20
  console.log('');
27
- console.log('Run `mod sync start` to begin syncing');
28
- console.log('');
29
- console.log("To connect to a different workspace, run `mod init --force`");
30
- process.exit(0);
31
- }
32
- // Install Claude commands
33
- await ensureClaudeCommandsDirectory();
34
- const availableCommands = await discoverAvailableCommands();
35
- const installedCommands = await getInstalledCommands();
36
- const commandsToInstall = await determineCommandsToInstall(availableCommands, installedCommands, isForce);
37
- if (commandsToInstall.length > 0) {
38
- await installCommands(commandsToInstall);
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') {
46
+ process.exit(0);
47
+ }
39
48
  }
40
49
  // Check auth state
41
50
  const config = readConfig();
@@ -47,26 +56,64 @@ export async function initCommand(args, repo) {
47
56
  else {
48
57
  workspaceConnection = await handleUnauthenticatedInit(repo);
49
58
  }
50
- // Save workspace connection
59
+ // Workspace connection already saved during creation
60
+ // Update last synced time
61
+ workspaceConnection.lastSyncedAt = new Date().toISOString();
51
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/');
52
66
  // Display success message
53
- displayInitializationSuccess(commandsToInstall, workspaceConnection, isAuthenticated);
67
+ displayInitializationSuccess(workspaceConnection, isAuthenticated);
54
68
  process.exit(0);
55
69
  }
56
70
  catch (error) {
57
- handleInstallationError(error);
71
+ console.error('Initialization failed:', error.message);
72
+ process.exit(1);
58
73
  }
59
74
  }
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"]
60
76
  async function handleAuthenticatedInit(repo, email) {
61
77
  console.log(`Signed in as ${email}`);
62
- // Get workspace list
63
- const modWorkspace = createModWorkspace(repo);
78
+ // Get workspace list from user document (app-2)
79
+ const config = readConfig();
64
80
  let workspaces = [];
65
81
  try {
66
- const handles = await modWorkspace.listWorkspaces();
67
- workspaces = handles.map((h) => ({ id: h.id, name: h.name }));
82
+ const userDocId = config.auth?.userDocId;
83
+ if (!userDocId) {
84
+ console.warn('User document not found. Creating local workspace.');
85
+ }
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
+ }
113
+ }
68
114
  }
69
115
  catch (error) {
116
+ // Handle network errors gracefully (qual-1)
70
117
  console.warn('Could not load cloud workspaces. Creating local workspace.');
71
118
  }
72
119
  // Build options
@@ -84,13 +131,32 @@ async function handleAuthenticatedInit(repo, email) {
84
131
  if (choice.type === 'create') {
85
132
  return await createNewWorkspace(repo);
86
133
  }
87
- return {
88
- path: process.cwd(),
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,
89
140
  workspaceId: choice.id,
90
141
  workspaceName: choice.name,
91
142
  connectedAt: new Date().toISOString(),
92
143
  lastSyncedAt: new Date().toISOString(),
93
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) {
149
+ try {
150
+ const modUser = createModUser(repo);
151
+ await modUser.addWorkspace(config.auth.userDocId, choice.id);
152
+ }
153
+ catch (error) {
154
+ // Silently ignore - workspace is already connected
155
+ }
156
+ }
157
+ // Import files from directory if this is a new connection
158
+ await importDirectoryFiles(repo, { id: choice.id });
159
+ return workspaceConnection;
94
160
  }
95
161
  async function handleUnauthenticatedInit(repo) {
96
162
  const choice = await select('Select option:', [
@@ -113,231 +179,419 @@ async function handleUnauthenticatedInit(repo) {
113
179
  }
114
180
  return await createNewWorkspace(repo);
115
181
  }
182
+ // glassware[type="implementation", id="impl-init-create-workspace--4ff92c7c", requirements="requirement-cli-init-app-4--761a37a6,requirement-cli-init-int-1--e3f9f2b6"]
116
183
  async function createNewWorkspace(repo) {
117
184
  const currentDirName = path.basename(process.cwd());
185
+ const currentDir = process.cwd();
118
186
  const defaultName = currentDirName.charAt(0).toUpperCase() + currentDirName.slice(1);
119
187
  const name = await input('Workspace name', {
120
188
  default: defaultName,
121
189
  validate: validateWorkspaceName,
122
190
  });
123
191
  console.log('Creating workspace...');
192
+ // Create new workspace - works with or without auth (app-4)
124
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
125
196
  const workspace = await modWorkspace.createWorkspace({ name });
126
- return {
127
- path: process.cwd(),
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,
128
203
  workspaceId: workspace.id,
129
204
  workspaceName: workspace.name,
130
205
  connectedAt: new Date().toISOString(),
131
206
  lastSyncedAt: new Date().toISOString(),
132
207
  };
133
- }
134
- function displayInitializationSuccess(commands, connection, isAuthenticated) {
135
- console.log('');
136
- console.log('Initialized Mod in ' + connection.path);
137
- console.log(`Workspace: ${connection.workspaceName}`);
138
- if (commands.length > 0) {
139
- console.log('');
140
- console.log(`Installed ${commands.length} Claude command(s)`);
141
- }
142
- console.log('');
143
- if (isAuthenticated) {
144
- console.log('Run `mod sync start` to begin syncing');
145
- }
146
- else {
147
- console.log('Run `mod auth login` to enable sync with collaborators');
148
- }
149
- }
150
- // ============ Claude Commands Installation ============
151
- async function ensureClaudeCommandsDirectory() {
152
- const claudeDir = path.join(process.cwd(), '.claude');
153
- const commandsDir = path.join(claudeDir, 'commands');
154
- try {
155
- if (!fs.existsSync(claudeDir)) {
156
- fs.mkdirSync(claudeDir, { recursive: true, mode: 0o755 });
157
- }
158
- if (!fs.existsSync(commandsDir)) {
159
- fs.mkdirSync(commandsDir, { recursive: true, mode: 0o755 });
160
- }
161
- }
162
- catch (error) {
163
- throw new Error(`Failed to create .claude/commands directory: ${error.message}`);
164
- }
165
- }
166
- async function discoverAvailableCommands() {
167
- const packageRoot = path.resolve(__dirname, '../..');
168
- const commandsSourceDir = path.join(packageRoot, 'commands');
169
- if (!fs.existsSync(commandsSourceDir)) {
170
- return [];
171
- }
172
- const commands = [];
173
- const files = fs.readdirSync(commandsSourceDir).filter((f) => f.endsWith('.md'));
174
- for (const filename of files) {
175
- const filePath = path.join(commandsSourceDir, filename);
176
- const content = fs.readFileSync(filePath, 'utf8');
177
- const metadata = parseFrontmatter(content);
178
- commands.push({
179
- filename,
180
- path: filePath,
181
- metadata,
182
- content,
183
- });
184
- }
185
- return commands;
186
- }
187
- async function getInstalledCommands() {
188
- const commandsDir = path.join(process.cwd(), '.claude', 'commands');
189
- const installed = new Map();
190
- if (!fs.existsSync(commandsDir)) {
191
- return installed;
192
- }
193
- const files = fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md'));
194
- for (const filename of files) {
195
- const filePath = path.join(commandsDir, filename);
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) {
196
216
  try {
197
- const content = fs.readFileSync(filePath, 'utf8');
198
- const metadata = parseFrontmatter(content);
199
- installed.set(filename, metadata);
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');
200
222
  }
201
223
  catch (error) {
202
- console.warn(`Could not read metadata from ${filename}: ${error.message}`);
203
- }
204
- }
205
- return installed;
206
- }
207
- function parseFrontmatter(content) {
208
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
209
- if (!frontmatterMatch) {
210
- return {
211
- version: '0.0.0',
212
- updated: '1970-01-01',
213
- checksum: '',
214
- description: undefined,
215
- };
216
- }
217
- const frontmatterText = frontmatterMatch[1];
218
- const metadata = {};
219
- const lines = frontmatterText.split('\n');
220
- for (const line of lines) {
221
- const match = line.match(/^(\w+):\s*(.+)$/);
222
- if (match) {
223
- const [, key, value] = match;
224
- metadata[key] = value
225
- .trim()
226
- .replace(/^["']|["']$/g, '');
227
- }
228
- }
229
- return {
230
- version: metadata.version || '1.0.0',
231
- updated: metadata.updated || new Date().toISOString(),
232
- checksum: metadata.checksum || '',
233
- description: metadata.description,
234
- };
235
- }
236
- async function determineCommandsToInstall(available, installed, force) {
237
- const toInstall = [];
238
- for (const command of available) {
239
- const installedMetadata = installed.get(command.filename);
240
- if (force || !installedMetadata) {
241
- toInstall.push(command);
242
- continue;
243
- }
244
- if (compareVersions(command.metadata.version, installedMetadata.version) > 0) {
245
- toInstall.push(command);
224
+ // Don't fail the init if registration fails - workspace still created
225
+ console.warn('Note: Could not register workspace for cross-device sync');
246
226
  }
247
227
  }
248
- return toInstall;
228
+ // Import existing files from the directory
229
+ await importDirectoryFiles(repo, workspace);
230
+ console.log('Workspace synced');
231
+ return workspaceConnection;
249
232
  }
250
- function compareVersions(v1, v2) {
251
- const parts1 = v1.split('.').map((n) => parseInt(n, 10));
252
- const parts2 = v2.split('.').map((n) => parseInt(n, 10));
253
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
254
- const part1 = parts1[i] || 0;
255
- const part2 = parts2[i] || 0;
256
- if (part1 > part2)
257
- return 1;
258
- if (part1 < part2)
259
- return -1;
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;
260
247
  }
261
- return 0;
262
- }
263
- async function installCommands(commands) {
264
- const commandsDir = path.join(process.cwd(), '.claude', 'commands');
265
- const tempFiles = [];
248
+ console.log('Importing files from directory...');
249
+ const importService = new FileImportService(repo);
266
250
  try {
267
- for (const command of commands) {
268
- const tempPath = path.join(commandsDir, `${command.filename}.tmp`);
269
- const contentWithChecksum = await updateChecksumInContent(command.content);
270
- fs.writeFileSync(tempPath, contentWithChecksum, 'utf8');
271
- tempFiles.push(tempPath);
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');
258
+ return;
272
259
  }
273
- for (let i = 0; i < commands.length; i++) {
274
- const tempPath = tempFiles[i];
275
- const finalPath = path.join(commandsDir, commands[i].filename);
276
- fs.renameSync(tempPath, finalPath);
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;
277
278
  }
278
- }
279
- catch (error) {
280
- cleanup(tempFiles);
281
- throw error;
282
- }
283
- }
284
- async function updateChecksumInContent(content) {
285
- const crypto = await import('crypto');
286
- const contentHash = crypto
287
- .createHash('sha256')
288
- .update(content)
289
- .digest('hex')
290
- .substring(0, 16);
291
- const withoutChecksum = content.replace(/^(\s*)checksum:\s*\S*$/gm, '');
292
- return withoutChecksum.replace(/^(---\n)([\s\S]*?)(---)/, `$1$2checksum: ${contentHash}\n$3`);
293
- }
294
- function cleanup(tempFiles) {
295
- for (const file of tempFiles) {
296
- try {
297
- if (fs.existsSync(file)) {
298
- fs.unlinkSync(file);
299
- }
279
+ const skippedCount = preview.filteredFiles.length - filesToImport.length;
280
+ if (skippedCount > 0) {
281
+ console.log(`Skipping ${skippedCount} files that already exist`);
300
282
  }
301
- catch (error) {
302
- console.warn(`Could not clean up temporary file ${file}: ${error.message}`);
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));
303
291
  }
304
- }
305
- }
306
- async function listAvailableCommands() {
307
- console.log('Available Claude Commands:\n');
308
- try {
309
- const commands = await discoverAvailableCommands();
310
- if (commands.length === 0) {
311
- console.log(' No commands available');
312
- return;
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) {
298
+ try {
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);
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));
338
+ }
339
+ catch (error) {
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
+ }
346
+ }
347
+ }
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
+ }
313
357
  }
314
- for (const command of commands) {
315
- console.log(` ${command.filename.replace('.md', '')}`);
316
- console.log(` Version: ${command.metadata.version}`);
317
- if (command.metadata.description) {
318
- console.log(` Description: ${command.metadata.description}`);
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);
319
364
  }
320
- console.log();
321
365
  }
366
+ console.log(`✓ Imported ${importedCount} files (${skippedCount} already existed)`);
322
367
  }
323
368
  catch (error) {
324
- console.error('Error listing commands:', error.message);
369
+ console.warn('Failed to import files:', error instanceof Error ? error.message : error);
370
+ throw error; // Re-throw so caller knows import failed
325
371
  }
326
372
  }
327
- function handleInstallationError(error) {
328
- console.error('Initialization failed:');
329
- if (error.code === 'EACCES' || error.code === 'EPERM') {
330
- console.error(' Permission denied.');
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 });
331
387
  }
332
- else if (error.code === 'ENOSPC') {
333
- console.error(' Insufficient disk space.');
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);
396
+ }
397
+ }
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');
334
593
  }
335
594
  else {
336
- console.error(` ${error.message}`);
595
+ console.log('Run `mod auth login` to enable sync with collaborators');
337
596
  }
338
- console.error('');
339
- console.error('Troubleshooting:');
340
- console.error(' - Ensure you have write permissions');
341
- console.error(' - Try running "mod init --force" to overwrite');
342
- process.exit(1);
343
597
  }