@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
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import dotenv from 'dotenv';
|
|
4
4
|
import path from 'path';
|
|
@@ -75,29 +75,40 @@ const cli = meow(`
|
|
|
75
75
|
force: {
|
|
76
76
|
type: 'boolean',
|
|
77
77
|
default: false
|
|
78
|
+
},
|
|
79
|
+
dev: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
default: false
|
|
82
|
+
},
|
|
83
|
+
json: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
default: false
|
|
78
86
|
}
|
|
79
87
|
},
|
|
80
88
|
allowUnknownFlags: true
|
|
81
89
|
});
|
|
82
90
|
async function main() {
|
|
83
91
|
const repo = await getRepo();
|
|
92
|
+
// Load user document into share policy if logged in
|
|
93
|
+
const { readConfig } = await import('./lib/storage.js');
|
|
94
|
+
const { addUserToSharePolicy } = await import('./daemon/share-policy.js');
|
|
95
|
+
const config = readConfig();
|
|
96
|
+
if (config.auth?.userDocId) {
|
|
97
|
+
addUserToSharePolicy(config.auth.userDocId);
|
|
98
|
+
}
|
|
84
99
|
const availableCommands = buildCommandRegistry();
|
|
85
100
|
const [cmd, ...args] = cli.input;
|
|
86
101
|
// No user-facing banners or notifications about feature flags
|
|
87
102
|
// Background watch now handled by automatic-file-tracker service
|
|
88
103
|
// Handle command execution
|
|
89
104
|
if (cmd && typeof availableCommands[cmd] === 'function') {
|
|
90
|
-
// Pass
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (cli.flags.force)
|
|
98
|
-
allArgs.push('--force');
|
|
99
|
-
await availableCommands[cmd](allArgs, repo);
|
|
100
|
-
return;
|
|
105
|
+
// Pass raw args after the command name from process.argv
|
|
106
|
+
// This preserves all flags (--type, --file, --json, etc.) as-is
|
|
107
|
+
const cmdIndex = process.argv.findIndex(arg => arg === cmd);
|
|
108
|
+
const rawArgs = cmdIndex >= 0 ? process.argv.slice(cmdIndex + 1) : args;
|
|
109
|
+
await availableCommands[cmd](rawArgs, repo);
|
|
110
|
+
// Exit after command completes - WebSocket keeps event loop alive otherwise
|
|
111
|
+
process.exit(0);
|
|
101
112
|
}
|
|
102
113
|
if (!cmd) {
|
|
103
114
|
console.log('Mod CLI\n');
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// glassware[type="implementation", id="impl-cli-add-command--777b18e2", requirements="requirement-cli-add-cmd--aff0d740,requirement-cli-add-cmd-help--e0709244,requirement-cli-add-progress-small--5474ba10,requirement-cli-add-progress-medium--0f22e3e8,requirement-cli-add-progress-large--477f1e9a,requirement-cli-add-summary--3d0fb9b5,requirement-cli-add-dry-run--91bb0ff6,requirement-cli-add-verbose--3a958508"]
|
|
3
|
+
// spec: packages/mod-cli/specs/add.md
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { AddService } from '../services/add-service.js';
|
|
6
|
+
import { validateAddOptions, validateWorkspaceState } from '../services/add-validation.js';
|
|
7
|
+
import { ADD_CONSTANTS, } from '../types/add-types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parse command line arguments
|
|
10
|
+
*/
|
|
11
|
+
function parseArgs(args) {
|
|
12
|
+
const paths = [];
|
|
13
|
+
let dryRun = false;
|
|
14
|
+
let includeLargeBinary = false;
|
|
15
|
+
let verbose = false;
|
|
16
|
+
let quiet = false;
|
|
17
|
+
for (const arg of args) {
|
|
18
|
+
if (arg === '--dry-run') {
|
|
19
|
+
dryRun = true;
|
|
20
|
+
}
|
|
21
|
+
else if (arg === '--include-large-binary') {
|
|
22
|
+
includeLargeBinary = true;
|
|
23
|
+
}
|
|
24
|
+
else if (arg === '-v' || arg === '--verbose') {
|
|
25
|
+
verbose = true;
|
|
26
|
+
}
|
|
27
|
+
else if (arg === '-q' || arg === '--quiet') {
|
|
28
|
+
quiet = true;
|
|
29
|
+
}
|
|
30
|
+
else if (!arg.startsWith('-')) {
|
|
31
|
+
paths.push(arg);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Default to current directory if no paths
|
|
35
|
+
if (paths.length === 0) {
|
|
36
|
+
paths.push('.');
|
|
37
|
+
}
|
|
38
|
+
return { paths, dryRun, includeLargeBinary, verbose, quiet };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Add command entry point
|
|
42
|
+
*/
|
|
43
|
+
export async function addCommand(args, repo) {
|
|
44
|
+
// Parse arguments
|
|
45
|
+
const { paths, dryRun, includeLargeBinary, verbose, quiet } = parseArgs(args);
|
|
46
|
+
const options = {
|
|
47
|
+
paths,
|
|
48
|
+
dryRun,
|
|
49
|
+
includeLargeBinary,
|
|
50
|
+
verbose,
|
|
51
|
+
quiet
|
|
52
|
+
};
|
|
53
|
+
// Validate options
|
|
54
|
+
const optionsValidation = validateAddOptions(options);
|
|
55
|
+
if (!optionsValidation.valid) {
|
|
56
|
+
for (const error of optionsValidation.errors) {
|
|
57
|
+
console.error(`Error: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
// Validate workspace
|
|
62
|
+
const workspaceValidation = await validateWorkspaceState(process.cwd());
|
|
63
|
+
if (!workspaceValidation.valid) {
|
|
64
|
+
for (const error of workspaceValidation.errors) {
|
|
65
|
+
console.error(`Error: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
// Create add service
|
|
70
|
+
const addService = new AddService(repo);
|
|
71
|
+
// Set up cancellation handler
|
|
72
|
+
process.on('SIGINT', () => {
|
|
73
|
+
addService.cancel();
|
|
74
|
+
console.log('\nCancelling...');
|
|
75
|
+
});
|
|
76
|
+
// Progress tracking
|
|
77
|
+
let spinner = null;
|
|
78
|
+
let lastProgressUpdate = 0;
|
|
79
|
+
const progressBarWidth = 30;
|
|
80
|
+
const onProgress = (progress) => {
|
|
81
|
+
if (quiet)
|
|
82
|
+
return;
|
|
83
|
+
// Throttle progress updates (cli-add-progress-throttle)
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
if (now - lastProgressUpdate < ADD_CONSTANTS.PROGRESS_THROTTLE_MS)
|
|
86
|
+
return;
|
|
87
|
+
lastProgressUpdate = now;
|
|
88
|
+
if (progress.phase === 'scanning') {
|
|
89
|
+
if (!spinner) {
|
|
90
|
+
spinner = ora('Scanning...').start();
|
|
91
|
+
}
|
|
92
|
+
spinner.text = `Scanning... ${progress.total} files found`;
|
|
93
|
+
}
|
|
94
|
+
else if (progress.phase === 'comparing') {
|
|
95
|
+
if (spinner) {
|
|
96
|
+
spinner.text = `Comparing... ${progress.current}/${progress.total} directories`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (progress.phase === 'adding') {
|
|
100
|
+
const total = progress.total;
|
|
101
|
+
if (total < ADD_CONSTANTS.SMALL_ADD_THRESHOLD) {
|
|
102
|
+
// Small: no progress bar
|
|
103
|
+
if (spinner) {
|
|
104
|
+
spinner.text = `Adding... ${progress.current}/${total}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (total < ADD_CONSTANTS.MEDIUM_ADD_THRESHOLD) {
|
|
108
|
+
// Medium: spinner with count
|
|
109
|
+
if (spinner) {
|
|
110
|
+
spinner.text = `Adding... ${progress.current}/${total} files`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Large: progress bar
|
|
115
|
+
const percent = Math.round((progress.current / total) * 100);
|
|
116
|
+
const filled = Math.round((progress.current / total) * progressBarWidth);
|
|
117
|
+
const bar = '█'.repeat(filled) + '░'.repeat(progressBarWidth - filled);
|
|
118
|
+
const eta = progress.eta ? ` | ETA: ${formatEta(progress.eta)}` : '';
|
|
119
|
+
if (spinner) {
|
|
120
|
+
spinner.text = `Adding files...\n[${bar}] ${percent}% | ${progress.current.toLocaleString()}/${total.toLocaleString()}${eta}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (verbose && progress.currentFile) {
|
|
124
|
+
console.log(`+ ${progress.currentFile}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
// Execute
|
|
129
|
+
try {
|
|
130
|
+
const result = await addService.execute(options, onProgress);
|
|
131
|
+
// Stop spinner
|
|
132
|
+
if (spinner) {
|
|
133
|
+
spinner.stop();
|
|
134
|
+
}
|
|
135
|
+
// Print result
|
|
136
|
+
printResult(result, options);
|
|
137
|
+
if (!result.success) {
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (spinner) {
|
|
143
|
+
spinner.stop();
|
|
144
|
+
}
|
|
145
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Print the result of the add operation
|
|
151
|
+
*/
|
|
152
|
+
function printResult(result, options) {
|
|
153
|
+
const { summary, directories, duration } = result;
|
|
154
|
+
if (options.dryRun) {
|
|
155
|
+
// Dry run output
|
|
156
|
+
console.log(`Would add ${summary.totalFiles.toLocaleString()} files across ${directories.length} directories:`);
|
|
157
|
+
for (const dir of directories.slice(0, 10)) {
|
|
158
|
+
console.log(` ${dir.path}/ (${dir.created} files)`);
|
|
159
|
+
}
|
|
160
|
+
if (directories.length > 10) {
|
|
161
|
+
console.log(` ... (${directories.length - 10} more directories)`);
|
|
162
|
+
}
|
|
163
|
+
if (summary.skipped > 0) {
|
|
164
|
+
console.log(`\nWould skip ${summary.skipped} files`);
|
|
165
|
+
}
|
|
166
|
+
const estimatedMinutes = Math.ceil(summary.totalFiles * 50 / 1000 / 60);
|
|
167
|
+
console.log(`\nEstimated time: ~${estimatedMinutes} minutes`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Normal output
|
|
171
|
+
if (options.quiet) {
|
|
172
|
+
// Quiet: only show summary line
|
|
173
|
+
if (summary.errors > 0) {
|
|
174
|
+
console.log(`Added ${summary.created} files (${summary.errors} errors)`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(`Added ${summary.created} files`);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Standard output
|
|
182
|
+
const parts = [];
|
|
183
|
+
if (summary.created > 0) {
|
|
184
|
+
parts.push(`${summary.created} created`);
|
|
185
|
+
}
|
|
186
|
+
if (summary.updated > 0) {
|
|
187
|
+
parts.push(`${summary.updated} updated`);
|
|
188
|
+
}
|
|
189
|
+
if (summary.unchanged > 0) {
|
|
190
|
+
parts.push(`${summary.unchanged} unchanged`);
|
|
191
|
+
}
|
|
192
|
+
if (parts.length > 0) {
|
|
193
|
+
console.log(`\nAdded ${summary.totalFiles.toLocaleString()} files (${parts.join(', ')})`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(`\nNo files to add`);
|
|
197
|
+
}
|
|
198
|
+
// Show skipped files
|
|
199
|
+
if (summary.skipped > 0) {
|
|
200
|
+
console.log(`\nSkipped: ${summary.skipped} files`);
|
|
201
|
+
}
|
|
202
|
+
// Show errors
|
|
203
|
+
if (summary.errors > 0) {
|
|
204
|
+
console.log(`\nErrors: ${summary.errors} files`);
|
|
205
|
+
// Show first few errors
|
|
206
|
+
const allErrors = directories.flatMap(d => d.errors);
|
|
207
|
+
for (const error of allErrors.slice(0, 5)) {
|
|
208
|
+
console.log(` ${error.relativePath}: ${error.error?.message}`);
|
|
209
|
+
}
|
|
210
|
+
if (allErrors.length > 5) {
|
|
211
|
+
console.log(` ... (${allErrors.length - 5} more errors)`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Show duration
|
|
215
|
+
console.log(`\nCompleted in ${formatDuration(duration)}`);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Format duration in human readable format
|
|
219
|
+
*/
|
|
220
|
+
function formatDuration(ms) {
|
|
221
|
+
if (ms < 1000) {
|
|
222
|
+
return `${ms}ms`;
|
|
223
|
+
}
|
|
224
|
+
if (ms < 60000) {
|
|
225
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
226
|
+
}
|
|
227
|
+
const minutes = Math.floor(ms / 60000);
|
|
228
|
+
const seconds = Math.round((ms % 60000) / 1000);
|
|
229
|
+
return `${minutes}m ${seconds}s`;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Format ETA in human readable format
|
|
233
|
+
*/
|
|
234
|
+
function formatEta(ms) {
|
|
235
|
+
if (ms < 60000) {
|
|
236
|
+
return `${Math.round(ms / 1000)}s`;
|
|
237
|
+
}
|
|
238
|
+
const minutes = Math.floor(ms / 60000);
|
|
239
|
+
const seconds = Math.round((ms % 60000) / 1000);
|
|
240
|
+
if (seconds === 0) {
|
|
241
|
+
return `${minutes}m`;
|
|
242
|
+
}
|
|
243
|
+
return `${minutes}m ${seconds}s`;
|
|
244
|
+
}
|
|
245
|
+
export default addCommand;
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
// glassware[type=implementation, id=cli-auth-command,
|
|
1
|
+
// glassware[type="implementation", id="impl-cli-auth-command--649dd0cd", specifications="specification-spec-auth-login--c8f45e62,specification-spec-auth-status--36efb6a8,specification-spec-auth-logout--79cb4081"]
|
|
2
2
|
import { readConfig, writeConfig } from '../lib/storage.js';
|
|
3
3
|
import { openBrowser } from '../lib/browser.js';
|
|
4
4
|
import { startAuthServer } from '../lib/auth-server.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { createModUser } from '@mod/mod-core';
|
|
6
|
+
import { addUserToSharePolicy } from '../daemon/share-policy.js';
|
|
7
|
+
// Auth worker URL - configurable via environment variable
|
|
8
|
+
// Uses the Cloudflare Worker for OAuth
|
|
9
|
+
const AUTH_WORKER_URL = process.env.MOD_AUTH_URL || 'https://mod-auth-worker.mod-workers.workers.dev';
|
|
8
10
|
export async function authCommand(args, repo) {
|
|
9
|
-
const [subcommand] = args;
|
|
11
|
+
const [subcommand, ...rest] = args;
|
|
10
12
|
switch (subcommand) {
|
|
11
13
|
case 'login':
|
|
12
|
-
await handleLogin();
|
|
14
|
+
await handleLogin(rest, repo);
|
|
13
15
|
break;
|
|
14
16
|
case 'logout':
|
|
15
17
|
await handleLogout();
|
|
@@ -24,15 +26,18 @@ export async function authCommand(args, repo) {
|
|
|
24
26
|
else {
|
|
25
27
|
console.error('Usage: mod auth <login|logout|status>');
|
|
26
28
|
console.error('Available commands:');
|
|
27
|
-
console.error(' login
|
|
28
|
-
console.error(' logout
|
|
29
|
-
console.error(' status
|
|
29
|
+
console.error(' login [--dev] Sign in with Google OAuth (or use --dev for local testing)');
|
|
30
|
+
console.error(' logout Sign out and clear credentials');
|
|
31
|
+
console.error(' status Show current authentication state');
|
|
30
32
|
process.exit(1);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
process.exit(0);
|
|
34
36
|
}
|
|
35
|
-
|
|
37
|
+
// glassware[type="implementation", id="impl-handle-login--58bdb73b", specifications="specification-spec-auth-login--c8f45e62,specification-spec-open-browser--397b6a28,specification-spec-manual-url--b37b2760,specification-spec-validate-token--d3856b2d,specification-spec-success-message--b3bb1875,specification-spec-create-user-doc--282489ae,specification-spec-register-user-doc--e611f1db"]
|
|
38
|
+
async function handleLogin(args = [], repo) {
|
|
39
|
+
// Check for --dev flag
|
|
40
|
+
const isDev = args.includes('--dev');
|
|
36
41
|
// Check if already authenticated
|
|
37
42
|
const config = readConfig();
|
|
38
43
|
if (config.auth) {
|
|
@@ -41,10 +46,15 @@ async function handleLogin() {
|
|
|
41
46
|
console.log('Run `mod auth logout` to sign out first.');
|
|
42
47
|
return;
|
|
43
48
|
}
|
|
49
|
+
// Dev mode: create local user without OAuth
|
|
50
|
+
if (isDev) {
|
|
51
|
+
await handleDevLogin(repo);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
44
54
|
// Start localhost server to receive callback
|
|
45
55
|
const { port, result, close } = await startAuthServer();
|
|
46
56
|
// Build auth URL with callback port
|
|
47
|
-
const authUrl = `${
|
|
57
|
+
const authUrl = `${AUTH_WORKER_URL}/auth/cli?port=${port}`;
|
|
48
58
|
console.log('Opening browser to sign in...');
|
|
49
59
|
// Try to open browser
|
|
50
60
|
const opened = await openBrowser(authUrl);
|
|
@@ -58,19 +68,45 @@ async function handleLogin() {
|
|
|
58
68
|
try {
|
|
59
69
|
// Wait for auth callback
|
|
60
70
|
const authResult = await result;
|
|
61
|
-
// Validate token with server
|
|
71
|
+
// Validate token with server and get existing userDocId
|
|
62
72
|
let userDocId;
|
|
73
|
+
let authUserId;
|
|
63
74
|
try {
|
|
64
|
-
|
|
75
|
+
const authProfile = await validateAndGetAuthProfile(authResult.googleIdToken);
|
|
76
|
+
userDocId = authProfile.userDocId;
|
|
77
|
+
authUserId = authProfile.userId;
|
|
65
78
|
}
|
|
66
79
|
catch (error) {
|
|
67
|
-
// If validation fails, continue
|
|
68
|
-
|
|
80
|
+
// If validation fails, continue - we'll create user doc below
|
|
81
|
+
}
|
|
82
|
+
// If no userDocId (new user), create user document and register with server
|
|
83
|
+
if (!userDocId) {
|
|
84
|
+
try {
|
|
85
|
+
const modUser = createModUser(repo);
|
|
86
|
+
const userDoc = await modUser.findOrCreate({
|
|
87
|
+
email: authResult.email,
|
|
88
|
+
name: authResult.name,
|
|
89
|
+
googleId: authResult.googleId,
|
|
90
|
+
avatar: null,
|
|
91
|
+
});
|
|
92
|
+
userDocId = userDoc.id;
|
|
93
|
+
// Register the new userDocId with auth worker
|
|
94
|
+
await registerUserDoc(authResult.googleIdToken, userDocId);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.warn('Could not create/register user document:', error);
|
|
98
|
+
// Continue without userDocId - it can be set up later
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Add user doc to share policy if available
|
|
102
|
+
if (userDocId) {
|
|
103
|
+
addUserToSharePolicy(userDocId);
|
|
69
104
|
}
|
|
70
105
|
// Store credentials in config
|
|
71
106
|
const updatedConfig = readConfig();
|
|
72
107
|
updatedConfig.auth = {
|
|
73
108
|
googleIdToken: authResult.googleIdToken,
|
|
109
|
+
userId: authUserId || '',
|
|
74
110
|
googleId: authResult.googleId,
|
|
75
111
|
email: authResult.email,
|
|
76
112
|
name: authResult.name,
|
|
@@ -98,6 +134,7 @@ async function handleLogin() {
|
|
|
98
134
|
process.exit(1);
|
|
99
135
|
}
|
|
100
136
|
}
|
|
137
|
+
// glassware[type="implementation", id="impl-handle-logout--1f8e80ea", specifications="specification-spec-auth-logout--79cb4081,specification-spec-logout-message--2785cc24"]
|
|
101
138
|
async function handleLogout() {
|
|
102
139
|
const config = readConfig();
|
|
103
140
|
if (!config.auth) {
|
|
@@ -111,6 +148,7 @@ async function handleLogout() {
|
|
|
111
148
|
console.log('Signed out successfully');
|
|
112
149
|
console.log(`Removed credentials for ${email}`);
|
|
113
150
|
}
|
|
151
|
+
// glassware[type="implementation", id="impl-handle-status--b14ce725", specifications="specification-spec-auth-status--36efb6a8,specification-spec-status-output--b8935802"]
|
|
114
152
|
async function handleStatus() {
|
|
115
153
|
const config = readConfig();
|
|
116
154
|
if (!config.auth) {
|
|
@@ -126,19 +164,20 @@ async function handleStatus() {
|
|
|
126
164
|
console.log('Token expires: never');
|
|
127
165
|
// Check if token is still valid (optional)
|
|
128
166
|
try {
|
|
129
|
-
await
|
|
167
|
+
await validateAndGetAuthProfile(config.auth.googleIdToken);
|
|
130
168
|
console.log('Token status: valid');
|
|
131
169
|
}
|
|
132
170
|
catch (error) {
|
|
133
171
|
console.log('Token status: could not verify (server unavailable)');
|
|
134
172
|
}
|
|
135
173
|
}
|
|
174
|
+
// glassware[type="implementation", id="impl-validate-token--562d976d", specifications="specification-spec-validate-token--d3856b2d,specification-spec-load-user-doc--c7a835b0,specification-spec-auth-user-id--de3afa93"]
|
|
136
175
|
/**
|
|
137
|
-
* Validate token with server and get
|
|
138
|
-
* Returns existing userDocId for returning users, or
|
|
176
|
+
* Validate token with server and get auth profile.
|
|
177
|
+
* Returns existing userDocId for returning users, or empty string for new users.
|
|
139
178
|
*/
|
|
140
|
-
async function
|
|
141
|
-
const response = await fetch(`${
|
|
179
|
+
async function validateAndGetAuthProfile(token) {
|
|
180
|
+
const response = await fetch(`${AUTH_WORKER_URL}/auth/me`, {
|
|
142
181
|
headers: {
|
|
143
182
|
Authorization: `Bearer ${token}`,
|
|
144
183
|
},
|
|
@@ -147,5 +186,74 @@ async function validateAndGetUserDocId(token) {
|
|
|
147
186
|
throw new Error('Token validation failed');
|
|
148
187
|
}
|
|
149
188
|
const data = (await response.json());
|
|
150
|
-
return
|
|
189
|
+
return {
|
|
190
|
+
userId: data.id || '',
|
|
191
|
+
userDocId: data.userDocId || '',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export async function fetchAuthProfile(token) {
|
|
195
|
+
return validateAndGetAuthProfile(token);
|
|
196
|
+
}
|
|
197
|
+
// glassware[type="implementation", id="impl-register-user-doc--c699a7d3", specifications="specification-spec-register-user-doc--e611f1db"]
|
|
198
|
+
/**
|
|
199
|
+
* Register a new userDocId with the auth worker.
|
|
200
|
+
* Called for new users after creating their Automerge user document.
|
|
201
|
+
*/
|
|
202
|
+
async function registerUserDoc(token, userDocId) {
|
|
203
|
+
const response = await fetch(`${AUTH_WORKER_URL}/auth/register-doc`, {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: {
|
|
206
|
+
'Authorization': `Bearer ${token}`,
|
|
207
|
+
'Content-Type': 'application/json',
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify({ userDocId }),
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error(`Failed to register user doc: ${response.status}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// glassware[type="implementation", id="impl-dev-login--d3f76c9b", specifications="specification-spec-create-user-doc--282489ae,specification-spec-token-storage--26199fe8"]
|
|
216
|
+
/**
|
|
217
|
+
* Handle dev login - creates a local user document for testing without OAuth.
|
|
218
|
+
* This allows testing the full flow (auth → workspace creation → syncing) on localhost.
|
|
219
|
+
* Uses a shared dev@localhost user that both CLI and web app can access.
|
|
220
|
+
*/
|
|
221
|
+
async function handleDevLogin(repo) {
|
|
222
|
+
console.log('Setting up development authentication...');
|
|
223
|
+
const modUser = createModUser(repo);
|
|
224
|
+
try {
|
|
225
|
+
// Use findOrCreate to get shared dev user across CLI and web app
|
|
226
|
+
console.log('Looking for dev@localhost user...');
|
|
227
|
+
const userDoc = await modUser.findOrCreate({
|
|
228
|
+
email: 'dev@localhost',
|
|
229
|
+
name: 'Dev User',
|
|
230
|
+
googleId: 'dev-google-id',
|
|
231
|
+
avatar: null,
|
|
232
|
+
});
|
|
233
|
+
console.log('Using dev user document:', userDoc.id);
|
|
234
|
+
// Add user doc to share policy so it can sync
|
|
235
|
+
addUserToSharePolicy(userDoc.id);
|
|
236
|
+
// Store dev credentials in config
|
|
237
|
+
const config = readConfig();
|
|
238
|
+
config.auth = {
|
|
239
|
+
googleIdToken: 'dev-token',
|
|
240
|
+
userId: 'dev-user',
|
|
241
|
+
googleId: 'dev-google-id',
|
|
242
|
+
email: 'dev@localhost',
|
|
243
|
+
name: 'Dev User',
|
|
244
|
+
userDocId: userDoc.id,
|
|
245
|
+
};
|
|
246
|
+
writeConfig(config);
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log('✓ Signed in as dev@localhost');
|
|
249
|
+
console.log(` User Document ID: ${userDoc.id}`);
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log('Note: This is development mode for local testing.');
|
|
252
|
+
console.log('Workspaces will sync via ws://localhost:3030');
|
|
253
|
+
console.log('The web app will use the same dev@localhost user.');
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
console.error('Failed to setup dev user:', error.message);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
151
259
|
}
|