@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.
- package/README.md +72 -0
- package/dist/cli.bundle.js +24633 -13744
- package/dist/cli.bundle.js.map +4 -4
- package/dist/cli.js +23 -12
- package/dist/commands/add.js +245 -0
- package/dist/commands/auth.js +129 -21
- package/dist/commands/comment.js +568 -0
- package/dist/commands/diff.js +182 -0
- package/dist/commands/index.js +33 -3
- package/dist/commands/init.js +545 -326
- package/dist/commands/ls.js +135 -0
- package/dist/commands/members.js +687 -0
- package/dist/commands/mv.js +282 -0
- package/dist/commands/rm.js +257 -0
- package/dist/commands/status.js +273 -306
- package/dist/commands/sync.js +99 -75
- package/dist/commands/trace.js +1752 -0
- package/dist/commands/workspace.js +354 -330
- package/dist/config/features.js +8 -3
- package/dist/config/release-profiles/development.json +4 -1
- package/dist/config/release-profiles/mvp.json +4 -2
- package/dist/daemon/conflict-resolution.js +172 -0
- package/dist/daemon/content-hash.js +31 -0
- package/dist/daemon/file-sync.js +985 -0
- package/dist/daemon/index.js +203 -0
- package/dist/daemon/mime-types.js +166 -0
- package/dist/daemon/offline-queue.js +211 -0
- package/dist/daemon/path-utils.js +64 -0
- package/dist/daemon/share-policy.js +83 -0
- package/dist/daemon/wasm-errors.js +189 -0
- package/dist/daemon/worker.js +557 -0
- package/dist/daemon-worker.js +3 -2
- package/dist/errors/workspace-errors.js +48 -0
- package/dist/lib/auth-server.js +89 -26
- package/dist/lib/browser.js +1 -1
- package/dist/lib/diff.js +284 -0
- package/dist/lib/formatters.js +204 -0
- package/dist/lib/git.js +137 -0
- package/dist/lib/local-fs.js +201 -0
- package/dist/lib/prompts.js +56 -0
- package/dist/lib/storage.js +11 -1
- package/dist/lib/trace-formatters.js +314 -0
- package/dist/services/add-service.js +554 -0
- package/dist/services/add-validation.js +124 -0
- package/dist/services/mod-config.js +8 -2
- package/dist/services/modignore-service.js +2 -0
- package/dist/stores/use-workspaces-store.js +36 -14
- package/dist/types/add-types.js +99 -0
- package/dist/types/config.js +1 -1
- package/dist/types/workspace-connection.js +53 -2
- package/package.json +7 -5
- package/commands/execute.md +0 -156
- package/commands/overview.md +0 -233
- package/commands/review.md +0 -151
- package/commands/spec.md +0 -169
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
// glassware[type="implementation", id="impl-cli-members-command--9bf91cf6", specifications="specification-spec-cli-members-list--16a2b152,specification-spec-cli-members-invite--1f4fbec5,specification-spec-cli-members-invite-role--09fe52ee,specification-spec-cli-members-remove--f5ff40de,specification-spec-cli-members-leave--9e394f51,specification-spec-cli-members-persist--e2cb6ec5"]
|
|
2
|
+
// spec: packages/mod-cli/specs/members.md
|
|
3
|
+
import { MemberService, createModUser } from '@mod/mod-core';
|
|
4
|
+
import { readWorkspaceConnection, readConfig, writeConfig, } from '../lib/storage.js';
|
|
5
|
+
import { confirm } from '../lib/prompts.js';
|
|
6
|
+
import { fetchAuthProfile } from './auth.js';
|
|
7
|
+
const AUTH_WORKER_URL = process.env.MOD_AUTH_WORKER_URL || 'https://auth.mod.dev';
|
|
8
|
+
// glassware[type="implementation", id="impl-cli-members-handler--f38bbfd8", specifications="specification-spec-cli-members-list--16a2b152,specification-spec-cli-members-invite--1f4fbec5,specification-spec-cli-members-remove--f5ff40de,specification-spec-cli-members-leave--9e394f51"]
|
|
9
|
+
export async function membersCommand(args, repo) {
|
|
10
|
+
const [subcommand, ...rest] = args;
|
|
11
|
+
switch (subcommand) {
|
|
12
|
+
case 'list':
|
|
13
|
+
await handleListMembers(rest, repo);
|
|
14
|
+
break;
|
|
15
|
+
case 'invite':
|
|
16
|
+
await handleInviteMember(rest, repo);
|
|
17
|
+
break;
|
|
18
|
+
case 'remove':
|
|
19
|
+
await handleRemoveMember(rest, repo);
|
|
20
|
+
break;
|
|
21
|
+
case 'join':
|
|
22
|
+
await handleJoinWorkspace(rest, repo);
|
|
23
|
+
break;
|
|
24
|
+
case 'leave':
|
|
25
|
+
await handleLeaveWorkspace(rest, repo);
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
if (!subcommand) {
|
|
29
|
+
// Default to list
|
|
30
|
+
await handleListMembers([], repo);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
printUsage();
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
// glassware[type="implementation", id="impl-cli-members-persist--bc16a9bc", specifications="specification-spec-cli-members-persist--e2cb6ec5"]
|
|
40
|
+
async function flushRepo(repo, documentIds) {
|
|
41
|
+
const flush = repo.flush;
|
|
42
|
+
if (!flush)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
await flush.call(repo, documentIds);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error('Error: Failed to persist member changes');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function printUsage() {
|
|
53
|
+
console.error('Usage: mod members <subcommand>');
|
|
54
|
+
console.error('');
|
|
55
|
+
console.error('Subcommands:');
|
|
56
|
+
console.error(' list List workspace members');
|
|
57
|
+
console.error(' invite Generate an invite link for the workspace');
|
|
58
|
+
console.error(' remove Remove a member from the workspace');
|
|
59
|
+
console.error(' join Join a workspace via invite link');
|
|
60
|
+
console.error(' leave Leave the current workspace');
|
|
61
|
+
console.error('');
|
|
62
|
+
console.error("Run 'mod members <subcommand> --help' for usage details.");
|
|
63
|
+
}
|
|
64
|
+
// glassware[type="implementation", id="impl-cli-list-members--638745e1", specifications="specification-spec-cli-members-list--16a2b152,specification-spec-cli-table-format--66d8b9df,specification-spec-cli-json-format--ef8c7089,specification-spec-cli-error-no-workspace--c8207c72"]
|
|
65
|
+
async function handleListMembers(args, repo) {
|
|
66
|
+
const isJson = args.includes('--json');
|
|
67
|
+
const isHelp = args.includes('--help');
|
|
68
|
+
if (isHelp) {
|
|
69
|
+
console.log('mod members list [options]');
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log('List all members of the current workspace.');
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log('Options:');
|
|
74
|
+
console.log(' --json Output as JSON');
|
|
75
|
+
console.log(' --help Show help');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const currentDir = process.cwd();
|
|
79
|
+
const connection = readWorkspaceConnection(currentDir);
|
|
80
|
+
if (!connection) {
|
|
81
|
+
if (isJson) {
|
|
82
|
+
console.log(JSON.stringify({ error: 'Not connected to a workspace.' }));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.error('Error: Not connected to a workspace.');
|
|
86
|
+
console.error("Run 'mod workspace connect <workspace-id>' to connect to a workspace.");
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const config = readConfig();
|
|
91
|
+
if (!config.auth) {
|
|
92
|
+
if (isJson) {
|
|
93
|
+
console.log(JSON.stringify({ error: 'Not signed in.' }));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error('Error: Not signed in.');
|
|
97
|
+
console.error('Run `mod auth login` to sign in.');
|
|
98
|
+
}
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const currentUserId = await ensureAuthUserId(config);
|
|
102
|
+
const memberService = new MemberService(repo);
|
|
103
|
+
try {
|
|
104
|
+
const members = await memberService.getMembers(connection.workspaceId);
|
|
105
|
+
if (isJson) {
|
|
106
|
+
const output = {
|
|
107
|
+
workspace: {
|
|
108
|
+
id: connection.workspaceId,
|
|
109
|
+
name: connection.workspaceName,
|
|
110
|
+
},
|
|
111
|
+
members: members.map((m) => ({
|
|
112
|
+
userId: m.userId,
|
|
113
|
+
name: m.name || 'Unknown',
|
|
114
|
+
email: m.email,
|
|
115
|
+
role: m.role,
|
|
116
|
+
isCurrentUser: m.userId === currentUserId,
|
|
117
|
+
})),
|
|
118
|
+
totalCount: members.length,
|
|
119
|
+
};
|
|
120
|
+
console.log(JSON.stringify(output, null, 2));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
printMemberTable(members, connection.workspaceName, currentUserId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (isJson) {
|
|
128
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.error('Error:', error.message);
|
|
132
|
+
}
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// glassware[type="implementation", id="impl-cli-print-member-table--e87b3149", specifications="specification-spec-cli-table-format--66d8b9df"]
|
|
137
|
+
function printMemberTable(members, workspaceName, currentUserId) {
|
|
138
|
+
console.log(`Members of "${workspaceName}":`);
|
|
139
|
+
console.log('');
|
|
140
|
+
// Calculate column widths
|
|
141
|
+
const nameWidth = Math.max(16, ...members.map((m) => (m.name || 'Unknown').length + 4));
|
|
142
|
+
const emailWidth = Math.max(24, ...members.map((m) => m.email.length));
|
|
143
|
+
// Print header
|
|
144
|
+
console.log(` ${'NAME'.padEnd(nameWidth)}${'EMAIL'.padEnd(emailWidth)}ROLE`);
|
|
145
|
+
// Print members
|
|
146
|
+
for (const member of members) {
|
|
147
|
+
const isCurrentUser = member.userId === currentUserId;
|
|
148
|
+
const prefix = isCurrentUser ? '* ' : ' ';
|
|
149
|
+
const displayName = isCurrentUser ? 'You' : member.name || 'Unknown';
|
|
150
|
+
console.log(`${prefix}${displayName.padEnd(nameWidth)}${member.email.padEnd(emailWidth)}${member.role}`);
|
|
151
|
+
}
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(`${members.length} member${members.length === 1 ? '' : 's'} total`);
|
|
154
|
+
}
|
|
155
|
+
// glassware[type="implementation", id="impl-cli-invite-member--ab9b9ea3", specifications="specification-spec-cli-members-invite--1f4fbec5,specification-spec-cli-members-invite-role--09fe52ee,specification-spec-cli-error-no-workspace--c8207c72,specification-spec-cli-error-permission--356d06c7"]
|
|
156
|
+
async function handleInviteMember(args, repo) {
|
|
157
|
+
const isJson = args.includes('--json');
|
|
158
|
+
const isHelp = args.includes('--help');
|
|
159
|
+
// Parse --role option
|
|
160
|
+
let role = 'editor';
|
|
161
|
+
const roleIndex = args.indexOf('--role');
|
|
162
|
+
if (roleIndex !== -1 && args[roleIndex + 1]) {
|
|
163
|
+
const specifiedRole = args[roleIndex + 1];
|
|
164
|
+
if (specifiedRole === 'owner') {
|
|
165
|
+
if (isJson) {
|
|
166
|
+
console.log(JSON.stringify({ error: "Cannot create invite link for 'owner' role." }));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.error("Error: Cannot create invite link for 'owner' role.");
|
|
170
|
+
console.error("Valid roles: 'editor', 'viewer'");
|
|
171
|
+
}
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
if (specifiedRole !== 'editor' && specifiedRole !== 'viewer') {
|
|
175
|
+
if (isJson) {
|
|
176
|
+
console.log(JSON.stringify({ error: `Invalid role: ${specifiedRole}` }));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.error(`Error: Invalid role '${specifiedRole}'.`);
|
|
180
|
+
console.error("Valid roles: 'editor', 'viewer'");
|
|
181
|
+
}
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
role = specifiedRole;
|
|
185
|
+
}
|
|
186
|
+
if (isHelp) {
|
|
187
|
+
console.log('mod members invite [options]');
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('Generate a shareable invite link for the workspace.');
|
|
190
|
+
console.log('');
|
|
191
|
+
console.log('Options:');
|
|
192
|
+
console.log(' --role Role for invitees: editor (default), viewer');
|
|
193
|
+
console.log(' --json Output as JSON');
|
|
194
|
+
console.log(' --help Show help');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const currentDir = process.cwd();
|
|
198
|
+
const connection = readWorkspaceConnection(currentDir);
|
|
199
|
+
if (!connection) {
|
|
200
|
+
if (isJson) {
|
|
201
|
+
console.log(JSON.stringify({ error: 'Not connected to a workspace.' }));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.error('Error: Not connected to a workspace.');
|
|
205
|
+
console.error("Run 'mod workspace connect <workspace-id>' to connect to a workspace.");
|
|
206
|
+
}
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
const config = readConfig();
|
|
210
|
+
if (!config.auth) {
|
|
211
|
+
if (isJson) {
|
|
212
|
+
console.log(JSON.stringify({ error: 'Not signed in.' }));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.error('Error: Not signed in.');
|
|
216
|
+
console.error('Run `mod auth login` to sign in.');
|
|
217
|
+
}
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
// Check if user is owner
|
|
221
|
+
const memberService = new MemberService(repo);
|
|
222
|
+
const currentUserId = await ensureAuthUserId(config);
|
|
223
|
+
const isOwner = await memberService.isWorkspaceOwner(connection.workspaceId, currentUserId);
|
|
224
|
+
if (!isOwner) {
|
|
225
|
+
if (isJson) {
|
|
226
|
+
console.log(JSON.stringify({ error: 'Permission denied. Only workspace owners can create invite links.' }));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.error('Error: Permission denied.');
|
|
230
|
+
console.error('Only workspace owners can create invite links.');
|
|
231
|
+
}
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
// Call auth-worker to create invite link
|
|
236
|
+
const response = await fetch(`${AUTH_WORKER_URL}/auth/invite`, {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: {
|
|
239
|
+
'Content-Type': 'application/json',
|
|
240
|
+
Authorization: `Bearer ${config.auth.googleIdToken}`,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
workspaceId: connection.workspaceId,
|
|
244
|
+
workspaceName: connection.workspaceName,
|
|
245
|
+
role,
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
250
|
+
throw new Error(errorData.error || `Failed to create invite link: ${response.status}`);
|
|
251
|
+
}
|
|
252
|
+
const { token, url, expiresAt } = (await response.json());
|
|
253
|
+
if (isJson) {
|
|
254
|
+
console.log(JSON.stringify({
|
|
255
|
+
token,
|
|
256
|
+
url,
|
|
257
|
+
role,
|
|
258
|
+
expiresAt,
|
|
259
|
+
workspace: {
|
|
260
|
+
id: connection.workspaceId,
|
|
261
|
+
name: connection.workspaceName,
|
|
262
|
+
},
|
|
263
|
+
}, null, 2));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
console.log(`Invite link created for "${connection.workspaceName}":`);
|
|
267
|
+
console.log('');
|
|
268
|
+
console.log(` ${url}`);
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log(`Role: ${role}${role === 'editor' ? ' (default)' : ''}`);
|
|
271
|
+
console.log('Share this link with collaborators to invite them.');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (isJson) {
|
|
276
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
console.error('Error:', error.message);
|
|
280
|
+
}
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// glassware[type="implementation", id="impl-cli-remove-member--0985711d", specifications="specification-spec-cli-members-remove--f5ff40de,specification-spec-cli-error-no-workspace--c8207c72,specification-spec-cli-error-permission--356d06c7,specification-spec-cli-error-not-found--eb727feb"]
|
|
285
|
+
async function handleRemoveMember(args, repo) {
|
|
286
|
+
const isJson = args.includes('--json');
|
|
287
|
+
const isForce = args.includes('--force');
|
|
288
|
+
const isHelp = args.includes('--help');
|
|
289
|
+
// Find email argument (first non-flag argument)
|
|
290
|
+
const email = args.find((arg) => !arg.startsWith('--'));
|
|
291
|
+
if (isHelp) {
|
|
292
|
+
console.log('mod members remove <email> [options]');
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log('Remove a member from the workspace.');
|
|
295
|
+
console.log('');
|
|
296
|
+
console.log('Arguments:');
|
|
297
|
+
console.log(' email Email address of the member to remove');
|
|
298
|
+
console.log('');
|
|
299
|
+
console.log('Options:');
|
|
300
|
+
console.log(' --force Skip confirmation prompt');
|
|
301
|
+
console.log(' --json Output as JSON');
|
|
302
|
+
console.log(' --help Show help');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!email) {
|
|
306
|
+
if (isJson) {
|
|
307
|
+
console.log(JSON.stringify({ error: 'Email address required.' }));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
console.error('Error: Email address required.');
|
|
311
|
+
console.error('Usage: mod members remove <email>');
|
|
312
|
+
}
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
const currentDir = process.cwd();
|
|
316
|
+
const connection = readWorkspaceConnection(currentDir);
|
|
317
|
+
if (!connection) {
|
|
318
|
+
if (isJson) {
|
|
319
|
+
console.log(JSON.stringify({ error: 'Not connected to a workspace.' }));
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.error('Error: Not connected to a workspace.');
|
|
323
|
+
console.error("Run 'mod workspace connect <workspace-id>' to connect to a workspace.");
|
|
324
|
+
}
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const config = readConfig();
|
|
328
|
+
if (!config.auth) {
|
|
329
|
+
if (isJson) {
|
|
330
|
+
console.log(JSON.stringify({ error: 'Not signed in.' }));
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
console.error('Error: Not signed in.');
|
|
334
|
+
console.error('Run `mod auth login` to sign in.');
|
|
335
|
+
}
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
const memberService = new MemberService(repo);
|
|
339
|
+
const currentUserId = await ensureAuthUserId(config);
|
|
340
|
+
const workspaceId = connection.workspaceId;
|
|
341
|
+
// Check if user is owner
|
|
342
|
+
const isOwner = await memberService.isWorkspaceOwner(workspaceId, currentUserId);
|
|
343
|
+
if (!isOwner) {
|
|
344
|
+
if (isJson) {
|
|
345
|
+
console.log(JSON.stringify({ error: 'Permission denied. Only workspace owners can remove members.' }));
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
console.error('Error: Permission denied.');
|
|
349
|
+
console.error('Only workspace owners can remove members.');
|
|
350
|
+
}
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
// Find member by email
|
|
354
|
+
const members = await memberService.getMembers(workspaceId);
|
|
355
|
+
const member = members.find((m) => m.email === email);
|
|
356
|
+
if (!member) {
|
|
357
|
+
if (isJson) {
|
|
358
|
+
console.log(JSON.stringify({ error: `${email} is not a member of this workspace.` }));
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.error(`Error: ${email} is not a member of this workspace.`);
|
|
362
|
+
}
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
// Cannot remove self (use leave instead)
|
|
366
|
+
if (member.userId === currentUserId) {
|
|
367
|
+
if (isJson) {
|
|
368
|
+
console.log(JSON.stringify({ error: 'Cannot remove yourself. Use `mod members leave` instead.' }));
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.error('Error: Cannot remove yourself. Use `mod members leave` instead.');
|
|
372
|
+
}
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
// Confirm removal (unless --force)
|
|
376
|
+
if (!isForce && !isJson) {
|
|
377
|
+
const confirmed = await confirm(`Remove ${email} from "${connection.workspaceName}"?`, { default: false });
|
|
378
|
+
if (!confirmed) {
|
|
379
|
+
console.log('Cancelled.');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
await memberService.removeMember(workspaceId, member.userId, currentUserId);
|
|
385
|
+
await flushRepo(repo, [workspaceId]);
|
|
386
|
+
if (isJson) {
|
|
387
|
+
console.log(JSON.stringify({
|
|
388
|
+
success: true,
|
|
389
|
+
message: `${email} has been removed from the workspace.`,
|
|
390
|
+
removedUser: {
|
|
391
|
+
userId: member.userId,
|
|
392
|
+
email: member.email,
|
|
393
|
+
name: member.name,
|
|
394
|
+
},
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
console.log('');
|
|
399
|
+
console.log(`${email} has been removed from the workspace.`);
|
|
400
|
+
console.log('They no longer have access to workspace documents.');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (isJson) {
|
|
405
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
console.error('Error:', error.message);
|
|
409
|
+
}
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// glassware[type="implementation", id="impl-cli-leave-workspace--1ae0fcc8", specifications="specification-spec-cli-members-leave--9e394f51,specification-spec-cli-error-no-workspace--c8207c72,specification-spec-cli-error-owner-leave--b733604c"]
|
|
414
|
+
async function handleLeaveWorkspace(args, repo) {
|
|
415
|
+
const isJson = args.includes('--json');
|
|
416
|
+
const isForce = args.includes('--force');
|
|
417
|
+
const isHelp = args.includes('--help');
|
|
418
|
+
if (isHelp) {
|
|
419
|
+
console.log('mod members leave [options]');
|
|
420
|
+
console.log('');
|
|
421
|
+
console.log('Leave the current workspace.');
|
|
422
|
+
console.log('');
|
|
423
|
+
console.log('Options:');
|
|
424
|
+
console.log(' --force Skip confirmation prompt');
|
|
425
|
+
console.log(' --json Output as JSON');
|
|
426
|
+
console.log(' --help Show help');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const currentDir = process.cwd();
|
|
430
|
+
const connection = readWorkspaceConnection(currentDir);
|
|
431
|
+
if (!connection) {
|
|
432
|
+
if (isJson) {
|
|
433
|
+
console.log(JSON.stringify({ error: 'Not connected to a workspace.' }));
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
console.error('Error: Not connected to a workspace.');
|
|
437
|
+
console.error("Run 'mod workspace connect <workspace-id>' to connect to a workspace.");
|
|
438
|
+
}
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
const config = readConfig();
|
|
442
|
+
if (!config.auth) {
|
|
443
|
+
if (isJson) {
|
|
444
|
+
console.log(JSON.stringify({ error: 'Not signed in.' }));
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
console.error('Error: Not signed in.');
|
|
448
|
+
console.error('Run `mod auth login` to sign in.');
|
|
449
|
+
}
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
const memberService = new MemberService(repo);
|
|
453
|
+
const currentUserId = await ensureAuthUserId(config);
|
|
454
|
+
const workspaceId = connection.workspaceId;
|
|
455
|
+
// Check if user is sole owner (cannot leave)
|
|
456
|
+
const isOwner = await memberService.isWorkspaceOwner(workspaceId, currentUserId);
|
|
457
|
+
if (isOwner) {
|
|
458
|
+
const ownerCount = await memberService.getOwnerCount(workspaceId);
|
|
459
|
+
if (ownerCount <= 1) {
|
|
460
|
+
if (isJson) {
|
|
461
|
+
console.log(JSON.stringify({
|
|
462
|
+
error: 'Workspace owners cannot leave. Transfer ownership first or delete the workspace.',
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
console.error('Error: Workspace owners cannot leave.');
|
|
467
|
+
console.error('Transfer ownership first or delete the workspace.');
|
|
468
|
+
}
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Confirm leaving (unless --force)
|
|
473
|
+
if (!isForce && !isJson) {
|
|
474
|
+
const confirmed = await confirm(`Leave workspace "${connection.workspaceName}"? You will lose access to all documents.`, { default: false });
|
|
475
|
+
if (!confirmed) {
|
|
476
|
+
console.log('Cancelled.');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
await memberService.leaveWorkspace(workspaceId, currentUserId);
|
|
482
|
+
// Note: We don't clear the local workspace connection here.
|
|
483
|
+
// The connection is per-directory, not per-user, so other users
|
|
484
|
+
// working in this directory should retain their connection.
|
|
485
|
+
// Users can run `mod workspace disconnect` to clear it manually.
|
|
486
|
+
await flushRepo(repo, [workspaceId]);
|
|
487
|
+
if (isJson) {
|
|
488
|
+
console.log(JSON.stringify({
|
|
489
|
+
success: true,
|
|
490
|
+
message: `You have left "${connection.workspaceName}".`,
|
|
491
|
+
workspaceId: connection.workspaceId,
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
console.log('');
|
|
496
|
+
console.log(`You have left "${connection.workspaceName}".`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
if (isJson) {
|
|
501
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
console.error('Error:', error.message);
|
|
505
|
+
}
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// glassware[type="implementation", id="impl-cli-join-member--c5082b11", specifications="specification-spec-cli-members-join--4244a5d3,specification-spec-cli-error-not-authenticated--24136da7"]
|
|
510
|
+
async function handleJoinWorkspace(args, repo) {
|
|
511
|
+
const isJson = args.includes('--json');
|
|
512
|
+
const isHelp = args.includes('--help');
|
|
513
|
+
if (isHelp) {
|
|
514
|
+
console.log('mod members join <invite-url> [options]');
|
|
515
|
+
console.log('');
|
|
516
|
+
console.log('Join a workspace using an invite link.');
|
|
517
|
+
console.log('');
|
|
518
|
+
console.log('Options:');
|
|
519
|
+
console.log(' --json Output as JSON');
|
|
520
|
+
console.log(' --help Show help');
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const inviteUrl = args.find((arg) => !arg.startsWith('--'));
|
|
524
|
+
if (!inviteUrl) {
|
|
525
|
+
console.error('Usage: mod members join <invite-url>');
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
const config = readConfig();
|
|
529
|
+
if (!config.auth) {
|
|
530
|
+
if (isJson) {
|
|
531
|
+
console.log(JSON.stringify({ error: 'Not signed in.' }));
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.error('Error: Not signed in.');
|
|
535
|
+
console.error('Run `mod auth login` to sign in.');
|
|
536
|
+
}
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
const token = extractInviteToken(inviteUrl);
|
|
540
|
+
if (!token) {
|
|
541
|
+
if (isJson) {
|
|
542
|
+
console.log(JSON.stringify({ error: 'Invalid invite URL.' }));
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
console.error('Error: Invalid invite URL.');
|
|
546
|
+
}
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const previewResponse = await fetch(`${AUTH_WORKER_URL}/auth/invite/${encodeURIComponent(token)}/preview`, {
|
|
551
|
+
method: 'GET',
|
|
552
|
+
});
|
|
553
|
+
const preview = (await previewResponse.json());
|
|
554
|
+
if (!preview.isValid) {
|
|
555
|
+
if (isJson) {
|
|
556
|
+
console.log(JSON.stringify({ error: preview.reason || 'Invalid invite link.' }));
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
console.error('Error: Invalid invite link.');
|
|
560
|
+
}
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
const validateResponse = await fetch(`${AUTH_WORKER_URL}/auth/invite/${encodeURIComponent(token)}/validate`, {
|
|
564
|
+
method: 'POST',
|
|
565
|
+
headers: {
|
|
566
|
+
Authorization: `Bearer ${config.auth.googleIdToken}`,
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
if (!validateResponse.ok) {
|
|
570
|
+
const errorData = (await validateResponse.json().catch(() => ({})));
|
|
571
|
+
throw new Error(errorData.error || 'Invalid invite link.');
|
|
572
|
+
}
|
|
573
|
+
const validated = (await validateResponse.json());
|
|
574
|
+
if (!validated.valid || !validated.workspaceId) {
|
|
575
|
+
throw new Error(validated.error || 'Invalid invite link.');
|
|
576
|
+
}
|
|
577
|
+
const memberService = new MemberService(repo);
|
|
578
|
+
const authUserId = await ensureAuthUserId(config);
|
|
579
|
+
await memberService.addMember(validated.workspaceId, {
|
|
580
|
+
userId: authUserId,
|
|
581
|
+
email: config.auth.email,
|
|
582
|
+
name: config.auth.name,
|
|
583
|
+
role: validated.role || 'editor',
|
|
584
|
+
joinedAt: new Date().toISOString(),
|
|
585
|
+
});
|
|
586
|
+
await waitForMembership(memberService, validated.workspaceId, authUserId, 10000);
|
|
587
|
+
const userDocId = await ensureUserDocId(config);
|
|
588
|
+
const documentsToFlush = [validated.workspaceId];
|
|
589
|
+
if (userDocId) {
|
|
590
|
+
const modUser = createModUser(repo);
|
|
591
|
+
const userDoc = await modUser.findOrCreate({
|
|
592
|
+
email: config.auth.email,
|
|
593
|
+
name: config.auth.name,
|
|
594
|
+
googleId: config.auth.googleId,
|
|
595
|
+
avatar: null,
|
|
596
|
+
});
|
|
597
|
+
await modUser.addWorkspace(userDoc.id, validated.workspaceId);
|
|
598
|
+
documentsToFlush.push(userDoc.id);
|
|
599
|
+
}
|
|
600
|
+
await flushRepo(repo, documentsToFlush);
|
|
601
|
+
if (isJson) {
|
|
602
|
+
console.log(JSON.stringify({ workspaceId: validated.workspaceId, workspaceName: validated.workspaceName, role: validated.role }, null, 2));
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
console.log(`Joined "${validated.workspaceName || 'workspace'}".`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
if (isJson) {
|
|
610
|
+
console.log(JSON.stringify({ error: error.message || 'Failed to join workspace.' }));
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
console.error('Error:', error.message || 'Failed to join workspace.');
|
|
614
|
+
}
|
|
615
|
+
process.exit(1);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function extractInviteToken(inviteUrl) {
|
|
619
|
+
if (!inviteUrl)
|
|
620
|
+
return null;
|
|
621
|
+
if (!inviteUrl.includes('/'))
|
|
622
|
+
return inviteUrl;
|
|
623
|
+
try {
|
|
624
|
+
const parsed = new URL(inviteUrl);
|
|
625
|
+
const parts = parsed.pathname.split('/');
|
|
626
|
+
const token = parts[parts.length - 1];
|
|
627
|
+
return token || null;
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function ensureUserDocId(config) {
|
|
634
|
+
if (config.auth?.userDocId)
|
|
635
|
+
return config.auth.userDocId;
|
|
636
|
+
try {
|
|
637
|
+
const profile = await fetchAuthProfile(config.auth.googleIdToken);
|
|
638
|
+
if (profile.userDocId) {
|
|
639
|
+
config.auth.userDocId = profile.userDocId;
|
|
640
|
+
if (!config.auth.userId && profile.userId) {
|
|
641
|
+
config.auth.userId = profile.userId;
|
|
642
|
+
}
|
|
643
|
+
writeConfig(config);
|
|
644
|
+
return profile.userDocId;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
async function ensureAuthUserId(config) {
|
|
653
|
+
if (config.auth?.userId) {
|
|
654
|
+
return config.auth.userId;
|
|
655
|
+
}
|
|
656
|
+
if (config.auth?.googleIdToken === 'dev-token') {
|
|
657
|
+
config.auth.userId = 'dev-user';
|
|
658
|
+
writeConfig(config);
|
|
659
|
+
return config.auth.userId;
|
|
660
|
+
}
|
|
661
|
+
if (config.auth?.googleIdToken) {
|
|
662
|
+
try {
|
|
663
|
+
const profile = await fetchAuthProfile(config.auth.googleIdToken);
|
|
664
|
+
if (profile.userId) {
|
|
665
|
+
config.auth.userId = profile.userId;
|
|
666
|
+
if (!config.auth.userDocId && profile.userDocId) {
|
|
667
|
+
config.auth.userDocId = profile.userDocId;
|
|
668
|
+
}
|
|
669
|
+
writeConfig(config);
|
|
670
|
+
return profile.userId;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch { }
|
|
674
|
+
}
|
|
675
|
+
throw new Error('Unable to determine user identity. Run `mod auth login`.');
|
|
676
|
+
}
|
|
677
|
+
async function waitForMembership(memberService, workspaceId, userId, timeoutMs) {
|
|
678
|
+
const start = Date.now();
|
|
679
|
+
while (Date.now() - start < timeoutMs) {
|
|
680
|
+
const members = await memberService.getMembers(workspaceId);
|
|
681
|
+
if (members.some((m) => m.userId === userId)) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
685
|
+
}
|
|
686
|
+
throw new Error('Membership did not appear in workspace before timeout.');
|
|
687
|
+
}
|