@mod-computer/cli 0.1.0 → 0.1.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.
@@ -1,45 +1,153 @@
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
- */
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]
7
3
  import fs from 'fs';
8
4
  import path from 'path';
9
5
  import { fileURLToPath } from 'url';
10
6
  import { createModWorkspace } from '@mod/mod-core';
11
- import { readModConfig, writeModConfig } from '../services/mod-config.js';
7
+ import { readConfig, readWorkspaceConnection, writeWorkspaceConnection, ensureModDir, } from '../lib/storage.js';
8
+ import { select, input, validateWorkspaceName } from '../lib/prompts.js';
12
9
  const __filename = fileURLToPath(import.meta.url);
13
10
  const __dirname = path.dirname(__filename);
14
11
  export async function initCommand(args, repo) {
15
12
  const subcommand = args[0];
13
+ const isForce = args.includes('--force');
16
14
  if (subcommand === 'list' || args.includes('--list')) {
17
15
  await listAvailableCommands();
18
16
  process.exit(0);
19
17
  }
20
18
  try {
21
- const workspaceStatus = await checkWorkspaceStatus(repo);
19
+ ensureModDir();
20
+ // Check for existing workspace connection
21
+ const currentDir = process.cwd();
22
+ const existingConnection = readWorkspaceConnection(currentDir);
23
+ if (existingConnection && !isForce) {
24
+ console.log('Already initialized');
25
+ console.log(`Workspace: ${existingConnection.workspaceName}`);
26
+ 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
22
33
  await ensureClaudeCommandsDirectory();
23
34
  const availableCommands = await discoverAvailableCommands();
24
35
  const installedCommands = await getInstalledCommands();
25
- const commandsToInstall = await determineCommandsToInstall(availableCommands, installedCommands, args.includes('--force'));
36
+ const commandsToInstall = await determineCommandsToInstall(availableCommands, installedCommands, isForce);
26
37
  if (commandsToInstall.length > 0) {
27
- displayInstallationPlan(commandsToInstall);
28
- const shouldProceed = args.includes('--force') || await confirmInstallation(commandsToInstall);
29
- if (!shouldProceed) {
30
- console.log('Installation cancelled');
31
- process.exit(0);
32
- }
33
38
  await installCommands(commandsToInstall);
34
39
  }
35
- const finalWorkspaceStatus = await handleWorkspaceInitialization(workspaceStatus, repo);
36
- displayInitializationSuccess(commandsToInstall, finalWorkspaceStatus);
40
+ // Check auth state
41
+ const config = readConfig();
42
+ const isAuthenticated = !!config.auth;
43
+ let workspaceConnection;
44
+ if (isAuthenticated) {
45
+ workspaceConnection = await handleAuthenticatedInit(repo, config.auth.email);
46
+ }
47
+ else {
48
+ workspaceConnection = await handleUnauthenticatedInit(repo);
49
+ }
50
+ // Save workspace connection
51
+ writeWorkspaceConnection(currentDir, workspaceConnection);
52
+ // Display success message
53
+ displayInitializationSuccess(commandsToInstall, workspaceConnection, isAuthenticated);
37
54
  process.exit(0);
38
55
  }
39
56
  catch (error) {
40
57
  handleInstallationError(error);
41
58
  }
42
59
  }
60
+ async function handleAuthenticatedInit(repo, email) {
61
+ console.log(`Signed in as ${email}`);
62
+ // Get workspace list
63
+ const modWorkspace = createModWorkspace(repo);
64
+ let workspaces = [];
65
+ try {
66
+ const handles = await modWorkspace.listWorkspaces();
67
+ workspaces = handles.map((h) => ({ id: h.id, name: h.name }));
68
+ }
69
+ catch (error) {
70
+ console.warn('Could not load cloud workspaces. Creating local workspace.');
71
+ }
72
+ // Build options
73
+ const options = [
74
+ ...workspaces.map((w) => ({
75
+ label: w.name,
76
+ value: { type: 'existing', id: w.id, name: w.name },
77
+ })),
78
+ {
79
+ label: '+ Create new workspace',
80
+ value: { type: 'create', id: '', name: '' },
81
+ },
82
+ ];
83
+ const choice = await select('Select workspace:', options);
84
+ if (choice.type === 'create') {
85
+ return await createNewWorkspace(repo);
86
+ }
87
+ return {
88
+ path: process.cwd(),
89
+ workspaceId: choice.id,
90
+ workspaceName: choice.name,
91
+ connectedAt: new Date().toISOString(),
92
+ lastSyncedAt: new Date().toISOString(),
93
+ };
94
+ }
95
+ async function handleUnauthenticatedInit(repo) {
96
+ const choice = await select('Select option:', [
97
+ {
98
+ label: 'Create local workspace',
99
+ value: 'create',
100
+ description: 'Work offline, sync later',
101
+ },
102
+ {
103
+ label: 'Sign in to sync with team',
104
+ value: 'signin',
105
+ description: 'Access cloud workspaces',
106
+ },
107
+ ]);
108
+ if (choice === 'signin') {
109
+ // Trigger auth flow
110
+ console.log('');
111
+ console.log('Please run `mod auth login` to sign in, then run `mod init` again.');
112
+ process.exit(0);
113
+ }
114
+ return await createNewWorkspace(repo);
115
+ }
116
+ async function createNewWorkspace(repo) {
117
+ const currentDirName = path.basename(process.cwd());
118
+ const defaultName = currentDirName.charAt(0).toUpperCase() + currentDirName.slice(1);
119
+ const name = await input('Workspace name', {
120
+ default: defaultName,
121
+ validate: validateWorkspaceName,
122
+ });
123
+ console.log('Creating workspace...');
124
+ const modWorkspace = createModWorkspace(repo);
125
+ const workspace = await modWorkspace.createWorkspace({ name });
126
+ return {
127
+ path: process.cwd(),
128
+ workspaceId: workspace.id,
129
+ workspaceName: workspace.name,
130
+ connectedAt: new Date().toISOString(),
131
+ lastSyncedAt: new Date().toISOString(),
132
+ };
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 ============
43
151
  async function ensureClaudeCommandsDirectory() {
44
152
  const claudeDir = path.join(process.cwd(), '.claude');
45
153
  const commandsDir = path.join(claudeDir, 'commands');
@@ -56,16 +164,13 @@ async function ensureClaudeCommandsDirectory() {
56
164
  }
57
165
  }
58
166
  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
167
  const packageRoot = path.resolve(__dirname, '../..');
62
168
  const commandsSourceDir = path.join(packageRoot, 'commands');
63
169
  if (!fs.existsSync(commandsSourceDir)) {
64
- console.warn('āš ļø No commands directory found in CLI package');
65
170
  return [];
66
171
  }
67
172
  const commands = [];
68
- const files = fs.readdirSync(commandsSourceDir).filter(f => f.endsWith('.md'));
173
+ const files = fs.readdirSync(commandsSourceDir).filter((f) => f.endsWith('.md'));
69
174
  for (const filename of files) {
70
175
  const filePath = path.join(commandsSourceDir, filename);
71
176
  const content = fs.readFileSync(filePath, 'utf8');
@@ -74,7 +179,7 @@ async function discoverAvailableCommands() {
74
179
  filename,
75
180
  path: filePath,
76
181
  metadata,
77
- content
182
+ content,
78
183
  });
79
184
  }
80
185
  return commands;
@@ -85,7 +190,7 @@ async function getInstalledCommands() {
85
190
  if (!fs.existsSync(commandsDir)) {
86
191
  return installed;
87
192
  }
88
- const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
193
+ const files = fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md'));
89
194
  for (const filename of files) {
90
195
  const filePath = path.join(commandsDir, filename);
91
196
  try {
@@ -94,7 +199,7 @@ async function getInstalledCommands() {
94
199
  installed.set(filename, metadata);
95
200
  }
96
201
  catch (error) {
97
- console.warn(`āš ļø Could not read metadata from ${filename}: ${error.message}`);
202
+ console.warn(`Could not read metadata from ${filename}: ${error.message}`);
98
203
  }
99
204
  }
100
205
  return installed;
@@ -102,30 +207,30 @@ async function getInstalledCommands() {
102
207
  function parseFrontmatter(content) {
103
208
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
104
209
  if (!frontmatterMatch) {
105
- // Return default metadata for files without frontmatter - they need to be updated
106
210
  return {
107
- version: '0.0.0', // Old version to trigger update
211
+ version: '0.0.0',
108
212
  updated: '1970-01-01',
109
213
  checksum: '',
110
- description: undefined
214
+ description: undefined,
111
215
  };
112
216
  }
113
217
  const frontmatterText = frontmatterMatch[1];
114
218
  const metadata = {};
115
- // Simple YAML parsing for required fields
116
219
  const lines = frontmatterText.split('\n');
117
220
  for (const line of lines) {
118
221
  const match = line.match(/^(\w+):\s*(.+)$/);
119
222
  if (match) {
120
223
  const [, key, value] = match;
121
- metadata[key] = value.trim().replace(/^["']|["']$/g, '');
224
+ metadata[key] = value
225
+ .trim()
226
+ .replace(/^["']|["']$/g, '');
122
227
  }
123
228
  }
124
229
  return {
125
230
  version: metadata.version || '1.0.0',
126
231
  updated: metadata.updated || new Date().toISOString(),
127
232
  checksum: metadata.checksum || '',
128
- description: metadata.description
233
+ description: metadata.description,
129
234
  };
130
235
  }
131
236
  async function determineCommandsToInstall(available, installed, force) {
@@ -136,7 +241,6 @@ async function determineCommandsToInstall(available, installed, force) {
136
241
  toInstall.push(command);
137
242
  continue;
138
243
  }
139
- // Compare versions
140
244
  if (compareVersions(command.metadata.version, installedMetadata.version) > 0) {
141
245
  toInstall.push(command);
142
246
  }
@@ -144,8 +248,8 @@ async function determineCommandsToInstall(available, installed, force) {
144
248
  return toInstall;
145
249
  }
146
250
  function compareVersions(v1, v2) {
147
- const parts1 = v1.split('.').map(n => parseInt(n, 10));
148
- const parts2 = v2.split('.').map(n => parseInt(n, 10));
251
+ const parts1 = v1.split('.').map((n) => parseInt(n, 10));
252
+ const parts2 = v2.split('.').map((n) => parseInt(n, 10));
149
253
  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
150
254
  const part1 = parts1[i] || 0;
151
255
  const part2 = parts2[i] || 0;
@@ -156,55 +260,16 @@ function compareVersions(v1, v2) {
156
260
  }
157
261
  return 0;
158
262
  }
159
- async function listAvailableCommands() {
160
- console.log('šŸ“‹ Available Claude Commands:\n');
161
- try {
162
- const commands = await discoverAvailableCommands();
163
- if (commands.length === 0) {
164
- console.log(' No commands available');
165
- return;
166
- }
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
263
  async function installCommands(commands) {
196
264
  const commandsDir = path.join(process.cwd(), '.claude', 'commands');
197
265
  const tempFiles = [];
198
266
  try {
199
- // Install to temporary files first for atomic operation
200
267
  for (const command of commands) {
201
268
  const tempPath = path.join(commandsDir, `${command.filename}.tmp`);
202
- const finalPath = path.join(commandsDir, command.filename);
203
269
  const contentWithChecksum = await updateChecksumInContent(command.content);
204
270
  fs.writeFileSync(tempPath, contentWithChecksum, 'utf8');
205
271
  tempFiles.push(tempPath);
206
272
  }
207
- // Move all temp files to final locations atomically
208
273
  for (let i = 0; i < commands.length; i++) {
209
274
  const tempPath = tempFiles[i];
210
275
  const finalPath = path.join(commandsDir, commands[i].filename);
@@ -217,17 +282,13 @@ async function installCommands(commands) {
217
282
  }
218
283
  }
219
284
  async function updateChecksumInContent(content) {
220
- // Use dynamic import for crypto in ESM
221
285
  const crypto = await import('crypto');
222
286
  const contentHash = crypto
223
287
  .createHash('sha256')
224
288
  .update(content)
225
289
  .digest('hex')
226
290
  .substring(0, 16);
227
- // Update checksum in frontmatter
228
- // First, remove any existing checksum lines
229
291
  const withoutChecksum = content.replace(/^(\s*)checksum:\s*\S*$/gm, '');
230
- // Then add the new checksum after the frontmatter opening
231
292
  return withoutChecksum.replace(/^(---\n)([\s\S]*?)(---)/, `$1$2checksum: ${contentHash}\n$3`);
232
293
  }
233
294
  function cleanup(tempFiles) {
@@ -238,141 +299,45 @@ function cleanup(tempFiles) {
238
299
  }
239
300
  }
240
301
  catch (error) {
241
- console.warn(`āš ļø Could not clean up temporary file ${file}: ${error.message}`);
302
+ console.warn(`Could not clean up temporary file ${file}: ${error.message}`);
242
303
  }
243
304
  }
244
305
  }
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'}`);
252
- }
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) {
306
+ async function listAvailableCommands() {
307
+ console.log('Available Claude Commands:\n');
270
308
  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) {
276
- 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;
282
- }
283
- }
284
- 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');
287
- }
309
+ const commands = await discoverAvailableCommands();
310
+ if (commands.length === 0) {
311
+ console.log(' No commands available');
312
+ return;
313
+ }
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}`);
288
319
  }
289
- return {
290
- exists: true,
291
- created: false,
292
- workspaceName,
293
- workspaceId: config.workspaceId
294
- };
320
+ console.log();
295
321
  }
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
- };
336
322
  }
337
323
  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
- };
324
+ console.error('Error listing commands:', error.message);
346
325
  }
347
326
  }
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);
354
- }
355
- return `Workspace-${timestamp}`;
356
- }
357
327
  function handleInstallationError(error) {
358
- console.error('āŒ Initialization failed:');
328
+ console.error('Initialization failed:');
359
329
  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.');
330
+ console.error(' Permission denied.');
362
331
  }
363
332
  else if (error.code === 'ENOSPC') {
364
333
  console.error(' Insufficient disk space.');
365
334
  }
366
- else if (error.code === 'ENOTDIR') {
367
- console.error(' Invalid directory structure. Ensure .claude is a directory, not a file.');
368
- }
369
335
  else {
370
336
  console.error(` ${error.message}`);
371
337
  }
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');
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');
377
342
  process.exit(1);
378
343
  }
@@ -0,0 +1,116 @@
1
+ // glassware[type=implementation, id=cli-prompts, requirements=req-cli-init-ux-2,req-cli-init-ux-3,req-cli-init-ux-4]
2
+ import readline from 'readline';
3
+ /**
4
+ * Display a selection prompt and return the chosen value.
5
+ * Uses arrow keys for navigation in terminals that support it,
6
+ * or numbered selection as fallback.
7
+ */
8
+ export async function select(question, options) {
9
+ const rl = readline.createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ });
13
+ return new Promise((resolve) => {
14
+ console.log(`\n${question}`);
15
+ // Display numbered options
16
+ options.forEach((option, index) => {
17
+ const number = `${index + 1}.`.padEnd(3);
18
+ if (option.description) {
19
+ console.log(` ${number} ${option.label}`);
20
+ console.log(` ${option.description}`);
21
+ }
22
+ else {
23
+ console.log(` ${number} ${option.label}`);
24
+ }
25
+ });
26
+ console.log();
27
+ const prompt = () => {
28
+ rl.question('Select [1-' + options.length + ']: ', (answer) => {
29
+ const num = parseInt(answer.trim(), 10);
30
+ if (num >= 1 && num <= options.length) {
31
+ rl.close();
32
+ resolve(options[num - 1].value);
33
+ }
34
+ else {
35
+ console.log(`Please enter a number between 1 and ${options.length}`);
36
+ prompt();
37
+ }
38
+ });
39
+ };
40
+ prompt();
41
+ });
42
+ }
43
+ /**
44
+ * Display a text input prompt and return the value.
45
+ */
46
+ export async function input(question, options) {
47
+ const rl = readline.createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout,
50
+ });
51
+ return new Promise((resolve) => {
52
+ const defaultSuffix = options?.default ? ` (${options.default})` : '';
53
+ const promptText = `${question}${defaultSuffix}: `;
54
+ const prompt = () => {
55
+ rl.question(promptText, (answer) => {
56
+ const value = answer.trim() || options?.default || '';
57
+ if (options?.validate) {
58
+ const error = options.validate(value);
59
+ if (error) {
60
+ console.log(` ${error}`);
61
+ prompt();
62
+ return;
63
+ }
64
+ }
65
+ rl.close();
66
+ resolve(value);
67
+ });
68
+ };
69
+ prompt();
70
+ });
71
+ }
72
+ /**
73
+ * Display a yes/no confirmation prompt.
74
+ */
75
+ export async function confirm(question, options) {
76
+ const rl = readline.createInterface({
77
+ input: process.stdin,
78
+ output: process.stdout,
79
+ });
80
+ const defaultValue = options?.default ?? true;
81
+ const hint = defaultValue ? '[Y/n]' : '[y/N]';
82
+ return new Promise((resolve) => {
83
+ rl.question(`${question} ${hint} `, (answer) => {
84
+ rl.close();
85
+ const normalized = answer.trim().toLowerCase();
86
+ if (normalized === '') {
87
+ resolve(defaultValue);
88
+ }
89
+ else if (normalized === 'y' || normalized === 'yes') {
90
+ resolve(true);
91
+ }
92
+ else if (normalized === 'n' || normalized === 'no') {
93
+ resolve(false);
94
+ }
95
+ else {
96
+ resolve(defaultValue);
97
+ }
98
+ });
99
+ });
100
+ }
101
+ /**
102
+ * Validate a workspace name.
103
+ * Returns error message if invalid, null if valid.
104
+ */
105
+ export function validateWorkspaceName(name) {
106
+ if (!name || name.trim().length === 0) {
107
+ return 'Workspace name cannot be empty';
108
+ }
109
+ if (name.length > 100) {
110
+ return 'Workspace name must be 100 characters or less';
111
+ }
112
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9-_ ]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/.test(name.trim())) {
113
+ return 'Workspace name must start and end with alphanumeric characters';
114
+ }
115
+ return null;
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mod-computer/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "mod": "dist/cli.bundle.js"