@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.
- package/dist/cli.bundle.js +468 -391
- package/dist/cli.bundle.js.map +4 -4
- package/dist/commands/init.js +157 -192
- package/dist/lib/prompts.js +116 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
36
|
-
|
|
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(
|
|
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',
|
|
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
|
|
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(
|
|
302
|
+
console.warn(`Could not clean up temporary file ${file}: ${error.message}`);
|
|
242
303
|
}
|
|
243
304
|
}
|
|
244
305
|
}
|
|
245
|
-
function
|
|
246
|
-
console.log('
|
|
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
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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.
|
|
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('
|
|
328
|
+
console.error('Initialization failed:');
|
|
359
329
|
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
360
|
-
console.error(' Permission denied.
|
|
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('
|
|
373
|
-
console.error('
|
|
374
|
-
console.error(' -
|
|
375
|
-
console.error(' -
|
|
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
|
+
}
|