@masslessai/push-todo 3.0.0
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/.claude-plugin/plugin.json +5 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/SKILL.md +180 -0
- package/bin/push-todo.js +23 -0
- package/commands/push-todo.md +78 -0
- package/hooks/hooks.json +26 -0
- package/hooks/session-end.js +99 -0
- package/hooks/session-start.js +134 -0
- package/lib/api.js +325 -0
- package/lib/cli.js +191 -0
- package/lib/config.js +279 -0
- package/lib/connect.js +380 -0
- package/lib/encryption.js +201 -0
- package/lib/fetch.js +371 -0
- package/lib/index.js +114 -0
- package/lib/machine-id.js +101 -0
- package/lib/project-registry.js +279 -0
- package/lib/utils/colors.js +126 -0
- package/lib/utils/format.js +253 -0
- package/lib/utils/git.js +149 -0
- package/lib/watch.js +343 -0
- package/natives/KeychainHelper.swift +134 -0
- package/package.json +54 -0
- package/scripts/postinstall.js +136 -0
package/lib/config.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file helpers for Push CLI.
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes config from ~/.config/push/config
|
|
5
|
+
* Format: bash-style exports (export PUSH_KEY="value")
|
|
6
|
+
*
|
|
7
|
+
* Compatible with the Python version's config format.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
|
|
14
|
+
const CONFIG_DIR = join(homedir(), '.config', 'push');
|
|
15
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get a configuration value from the config file.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} key - Config key name (without PUSH_ prefix)
|
|
21
|
+
* @param {string} defaultValue - Default value if not found
|
|
22
|
+
* @returns {string} The config value or default
|
|
23
|
+
*/
|
|
24
|
+
export function getConfigValue(key, defaultValue = '') {
|
|
25
|
+
const fullKey = `PUSH_${key}`;
|
|
26
|
+
|
|
27
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(CONFIG_FILE, 'utf8');
|
|
33
|
+
for (const line of content.split('\n')) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (trimmed.startsWith(`export ${fullKey}=`)) {
|
|
36
|
+
// Extract value after = and remove quotes
|
|
37
|
+
let value = trimmed.split('=')[1] || '';
|
|
38
|
+
value = value.trim();
|
|
39
|
+
// Remove surrounding quotes (single or double)
|
|
40
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
41
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
42
|
+
value = value.slice(1, -1);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Config file exists but couldn't read
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return defaultValue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set a configuration value in the config file.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} key - Config key name (without PUSH_ prefix)
|
|
58
|
+
* @param {string} value - Value to set
|
|
59
|
+
* @returns {boolean} True if successful
|
|
60
|
+
*/
|
|
61
|
+
export function setConfigValue(key, value) {
|
|
62
|
+
const fullKey = `PUSH_${key}`;
|
|
63
|
+
|
|
64
|
+
// Ensure config directory exists
|
|
65
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
66
|
+
|
|
67
|
+
// Read existing config
|
|
68
|
+
let lines = [];
|
|
69
|
+
if (existsSync(CONFIG_FILE)) {
|
|
70
|
+
try {
|
|
71
|
+
lines = readFileSync(CONFIG_FILE, 'utf8').split('\n');
|
|
72
|
+
} catch {
|
|
73
|
+
lines = [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Update or add the key
|
|
78
|
+
let found = false;
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
if (lines[i].trim().startsWith(`export ${fullKey}=`)) {
|
|
81
|
+
lines[i] = `export ${fullKey}="${value}"`;
|
|
82
|
+
found = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!found) {
|
|
88
|
+
lines.push(`export ${fullKey}="${value}"`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Write back
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(CONFIG_FILE, lines.join('\n') + '\n');
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the API key from environment or config file.
|
|
102
|
+
*
|
|
103
|
+
* Priority:
|
|
104
|
+
* 1. PUSH_API_KEY environment variable
|
|
105
|
+
* 2. Config file at ~/.config/push/config
|
|
106
|
+
*
|
|
107
|
+
* @returns {string} The API key
|
|
108
|
+
* @throws {Error} If API key is not found
|
|
109
|
+
*/
|
|
110
|
+
export function getApiKey() {
|
|
111
|
+
// 1. Try environment first (for CI/testing)
|
|
112
|
+
const envKey = process.env.PUSH_API_KEY;
|
|
113
|
+
if (envKey) {
|
|
114
|
+
return envKey;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. Try config file
|
|
118
|
+
const configKey = getConfigValue('API_KEY');
|
|
119
|
+
if (configKey) {
|
|
120
|
+
return configKey;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 3. Not found
|
|
124
|
+
throw new Error(
|
|
125
|
+
'PUSH_API_KEY not configured.\n' +
|
|
126
|
+
'Run: push-todo connect\n' +
|
|
127
|
+
'Or manually add to ~/.config/push/config:\n' +
|
|
128
|
+
' export PUSH_API_KEY="your-key-here"'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if auto-commit is enabled.
|
|
134
|
+
* Default: true (enabled by default)
|
|
135
|
+
*
|
|
136
|
+
* @returns {boolean}
|
|
137
|
+
*/
|
|
138
|
+
export function getAutoCommitEnabled() {
|
|
139
|
+
const value = getConfigValue('AUTO_COMMIT', 'true');
|
|
140
|
+
return value.toLowerCase() === 'true' || value === '1' || value.toLowerCase() === 'yes';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set auto-commit setting.
|
|
145
|
+
*
|
|
146
|
+
* @param {boolean} enabled
|
|
147
|
+
* @returns {boolean} True if successful
|
|
148
|
+
*/
|
|
149
|
+
export function setAutoCommitEnabled(enabled) {
|
|
150
|
+
return setConfigValue('AUTO_COMMIT', enabled ? 'true' : 'false');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the maximum batch size for queuing tasks.
|
|
155
|
+
* Default: 5
|
|
156
|
+
*
|
|
157
|
+
* @returns {number}
|
|
158
|
+
*/
|
|
159
|
+
export function getMaxBatchSize() {
|
|
160
|
+
const value = getConfigValue('MAX_BATCH_SIZE', '5');
|
|
161
|
+
const parsed = parseInt(value, 10);
|
|
162
|
+
return isNaN(parsed) ? 5 : parsed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set the maximum batch size.
|
|
167
|
+
*
|
|
168
|
+
* @param {number} size - Must be 1-20
|
|
169
|
+
* @returns {boolean} True if successful
|
|
170
|
+
*/
|
|
171
|
+
export function setMaxBatchSize(size) {
|
|
172
|
+
if (size < 1 || size > 20) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return setConfigValue('MAX_BATCH_SIZE', String(size));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get the email from config.
|
|
180
|
+
*
|
|
181
|
+
* @returns {string|null}
|
|
182
|
+
*/
|
|
183
|
+
export function getEmail() {
|
|
184
|
+
const email = getConfigValue('EMAIL');
|
|
185
|
+
return email || null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Save API key and email to config.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} apiKey
|
|
192
|
+
* @param {string} email
|
|
193
|
+
*/
|
|
194
|
+
export function saveCredentials(apiKey, email) {
|
|
195
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
196
|
+
setConfigValue('API_KEY', apiKey);
|
|
197
|
+
setConfigValue('EMAIL', email);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Clear all credentials from config.
|
|
202
|
+
*/
|
|
203
|
+
export function clearCredentials() {
|
|
204
|
+
if (existsSync(CONFIG_FILE)) {
|
|
205
|
+
try {
|
|
206
|
+
const content = readFileSync(CONFIG_FILE, 'utf8');
|
|
207
|
+
const lines = content.split('\n').filter(line => {
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
return !trimmed.startsWith('export PUSH_API_KEY=') &&
|
|
210
|
+
!trimmed.startsWith('export PUSH_EMAIL=');
|
|
211
|
+
});
|
|
212
|
+
writeFileSync(CONFIG_FILE, lines.join('\n'));
|
|
213
|
+
} catch {
|
|
214
|
+
// Ignore errors
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Show all settings.
|
|
221
|
+
*/
|
|
222
|
+
export function showSettings() {
|
|
223
|
+
console.log();
|
|
224
|
+
console.log(' Push Settings');
|
|
225
|
+
console.log(' ' + '='.repeat(40));
|
|
226
|
+
console.log();
|
|
227
|
+
|
|
228
|
+
const autoCommit = getAutoCommitEnabled();
|
|
229
|
+
const batchSize = getMaxBatchSize();
|
|
230
|
+
|
|
231
|
+
console.log(` auto-commit: ${autoCommit ? 'ON' : 'OFF'}`);
|
|
232
|
+
console.log(' Auto-commit when task completes');
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(` batch-size: ${batchSize}`);
|
|
235
|
+
console.log(' Max tasks for batch queue (1-20)');
|
|
236
|
+
console.log();
|
|
237
|
+
console.log(' Toggle with: push-todo setting <name>');
|
|
238
|
+
console.log(' Example: push-todo setting auto-commit');
|
|
239
|
+
console.log();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Toggle a setting by name.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} settingName
|
|
246
|
+
* @returns {boolean} True if setting was toggled
|
|
247
|
+
*/
|
|
248
|
+
export function toggleSetting(settingName) {
|
|
249
|
+
const normalized = settingName.toLowerCase().replace(/_/g, '-');
|
|
250
|
+
|
|
251
|
+
if (normalized === 'auto-commit') {
|
|
252
|
+
const current = getAutoCommitEnabled();
|
|
253
|
+
const newValue = !current;
|
|
254
|
+
if (setAutoCommitEnabled(newValue)) {
|
|
255
|
+
console.log(`Auto-commit is now ${newValue ? 'ON' : 'OFF'}`);
|
|
256
|
+
if (newValue) {
|
|
257
|
+
console.log('Tasks will be auto-committed (without push) when completed.');
|
|
258
|
+
} else {
|
|
259
|
+
console.log('Tasks will NOT be auto-committed when completed.');
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
console.error('Failed to update setting');
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (normalized === 'batch-size') {
|
|
268
|
+
const batchSize = getMaxBatchSize();
|
|
269
|
+
console.log(`Current batch size: ${batchSize}`);
|
|
270
|
+
console.log('Change with: push-todo --set-batch-size N');
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.error(`Unknown setting: ${settingName}`);
|
|
275
|
+
console.error('Available settings: auto-commit, batch-size');
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { CONFIG_DIR, CONFIG_FILE };
|
package/lib/connect.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect and authentication module for Push CLI.
|
|
3
|
+
*
|
|
4
|
+
* Handles the "doctor" flow for setting up and validating
|
|
5
|
+
* the CLI connection to Push backend.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { setTimeout } from 'timers/promises';
|
|
10
|
+
import * as api from './api.js';
|
|
11
|
+
import { getApiKey, saveCredentials, getConfigValue } from './config.js';
|
|
12
|
+
import { getMachineId, getMachineName } from './machine-id.js';
|
|
13
|
+
import { getRegistry } from './project-registry.js';
|
|
14
|
+
import { getGitRemote, isGitRepo, getGitRoot } from './utils/git.js';
|
|
15
|
+
import { isE2EEAvailable } from './encryption.js';
|
|
16
|
+
import { bold, green, yellow, red, cyan, dim, muted } from './utils/colors.js';
|
|
17
|
+
|
|
18
|
+
// Supabase anonymous key for auth flow
|
|
19
|
+
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imp4dXpxY2JxaGlheG1maXR6eGxvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzI0ODA5MjIsImV4cCI6MjA0ODA1NjkyMn0.Qxov5qJTVLWmseyFNhBQBJN7-t5sXlHZyzFKhSN_e5g';
|
|
20
|
+
|
|
21
|
+
const VERSION = '3.0.0';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a newer version is available.
|
|
25
|
+
*
|
|
26
|
+
* @returns {Promise<Object>} Version check result
|
|
27
|
+
*/
|
|
28
|
+
async function checkVersion() {
|
|
29
|
+
const latest = await api.getLatestVersion();
|
|
30
|
+
|
|
31
|
+
if (!latest) {
|
|
32
|
+
return { current: VERSION, latest: null, updateAvailable: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const currentParts = VERSION.split('.').map(Number);
|
|
36
|
+
const latestParts = latest.split('.').map(Number);
|
|
37
|
+
|
|
38
|
+
let updateAvailable = false;
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
if (latestParts[i] > currentParts[i]) {
|
|
41
|
+
updateAvailable = true;
|
|
42
|
+
break;
|
|
43
|
+
} else if (latestParts[i] < currentParts[i]) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { current: VERSION, latest, updateAvailable };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate the current API key.
|
|
53
|
+
*
|
|
54
|
+
* @returns {Promise<Object>} Validation result
|
|
55
|
+
*/
|
|
56
|
+
async function validateApiKeyStatus() {
|
|
57
|
+
const apiKey = getApiKey();
|
|
58
|
+
|
|
59
|
+
if (!apiKey) {
|
|
60
|
+
return { status: 'missing', message: 'No API key configured' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await api.validateApiKey();
|
|
64
|
+
|
|
65
|
+
if (result.valid) {
|
|
66
|
+
return { status: 'valid', email: result.email, userId: result.userId };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { status: 'invalid', message: result.reason };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate machine registration.
|
|
74
|
+
*
|
|
75
|
+
* @returns {Promise<Object>} Validation result
|
|
76
|
+
*/
|
|
77
|
+
async function validateMachineStatus() {
|
|
78
|
+
const machineId = getMachineId();
|
|
79
|
+
const machineName = getMachineName();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await api.validateMachine(machineId);
|
|
83
|
+
return {
|
|
84
|
+
status: 'valid',
|
|
85
|
+
machineId,
|
|
86
|
+
machineName,
|
|
87
|
+
...result
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
status: 'error',
|
|
92
|
+
machineId,
|
|
93
|
+
machineName,
|
|
94
|
+
message: error.message
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate project registration.
|
|
101
|
+
*
|
|
102
|
+
* @returns {Object} Validation result
|
|
103
|
+
*/
|
|
104
|
+
function validateProjectStatus() {
|
|
105
|
+
if (!isGitRepo()) {
|
|
106
|
+
return { status: 'not_git', message: 'Not in a git repository' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const gitRemote = getGitRemote();
|
|
110
|
+
if (!gitRemote) {
|
|
111
|
+
return { status: 'no_remote', message: 'No git remote configured' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const registry = getRegistry();
|
|
115
|
+
const isRegistered = registry.isRegistered(gitRemote);
|
|
116
|
+
const localPath = registry.getPathWithoutUpdate(gitRemote);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
status: isRegistered ? 'registered' : 'unregistered',
|
|
120
|
+
gitRemote,
|
|
121
|
+
localPath,
|
|
122
|
+
gitRoot: getGitRoot()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate a random auth code for the authentication flow.
|
|
128
|
+
*
|
|
129
|
+
* @returns {string} 6-character alphanumeric code
|
|
130
|
+
*/
|
|
131
|
+
function generateAuthCode() {
|
|
132
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Omit confusing chars
|
|
133
|
+
let code = '';
|
|
134
|
+
for (let i = 0; i < 6; i++) {
|
|
135
|
+
code += chars[Math.floor(Math.random() * chars.length)];
|
|
136
|
+
}
|
|
137
|
+
return code;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Open a URL in the default browser.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} url - URL to open
|
|
144
|
+
*/
|
|
145
|
+
function openBrowser(url) {
|
|
146
|
+
try {
|
|
147
|
+
if (process.platform === 'darwin') {
|
|
148
|
+
execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
149
|
+
} else if (process.platform === 'linux') {
|
|
150
|
+
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
151
|
+
} else if (process.platform === 'win32') {
|
|
152
|
+
execSync(`start "" "${url}"`, { stdio: 'ignore' });
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
console.log(`Please open this URL manually: ${url}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Poll for authentication completion.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} authCode - The auth code to poll for
|
|
163
|
+
* @param {number} timeout - Timeout in seconds
|
|
164
|
+
* @returns {Promise<Object|null>} Credentials or null if timeout
|
|
165
|
+
*/
|
|
166
|
+
async function pollForAuth(authCode, timeout = 300) {
|
|
167
|
+
const startTime = Date.now();
|
|
168
|
+
const pollInterval = 2000; // 2 seconds
|
|
169
|
+
|
|
170
|
+
while ((Date.now() - startTime) < timeout * 1000) {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(
|
|
173
|
+
`https://jxuzqcbqhiaxmfitzxlo.supabase.co/functions/v1/poll-auth?code=${authCode}`,
|
|
174
|
+
{
|
|
175
|
+
headers: {
|
|
176
|
+
'Authorization': `Bearer ${ANON_KEY}`,
|
|
177
|
+
'Content-Type': 'application/json'
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (response.ok) {
|
|
183
|
+
const data = await response.json();
|
|
184
|
+
if (data.api_key) {
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// Ignore errors during polling
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await setTimeout(pollInterval);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Run the authentication flow.
|
|
200
|
+
*
|
|
201
|
+
* @returns {Promise<boolean>} True if successful
|
|
202
|
+
*/
|
|
203
|
+
async function runAuthFlow() {
|
|
204
|
+
const authCode = generateAuthCode();
|
|
205
|
+
const authUrl = `https://pushto.do/connect?code=${authCode}`;
|
|
206
|
+
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(bold('Authentication Required'));
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(`Opening browser to: ${cyan(authUrl)}`);
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(`Or enter this code manually: ${bold(authCode)}`);
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log(dim('Waiting for authentication...'));
|
|
215
|
+
|
|
216
|
+
openBrowser(authUrl);
|
|
217
|
+
|
|
218
|
+
const credentials = await pollForAuth(authCode);
|
|
219
|
+
|
|
220
|
+
if (!credentials) {
|
|
221
|
+
console.log(red('Authentication timed out. Please try again.'));
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Save credentials
|
|
226
|
+
saveCredentials(credentials.api_key, credentials.user_id, credentials.email);
|
|
227
|
+
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log(green('✓ Authentication successful!'));
|
|
230
|
+
console.log(` Logged in as: ${credentials.email}`);
|
|
231
|
+
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Register the current project.
|
|
237
|
+
*
|
|
238
|
+
* @param {string[]} keywords - Project keywords
|
|
239
|
+
* @param {string} description - Project description
|
|
240
|
+
* @returns {Promise<boolean>} True if successful
|
|
241
|
+
*/
|
|
242
|
+
async function registerCurrentProject(keywords = [], description = '') {
|
|
243
|
+
const gitRemote = getGitRemote();
|
|
244
|
+
const gitRoot = getGitRoot();
|
|
245
|
+
|
|
246
|
+
if (!gitRemote || !gitRoot) {
|
|
247
|
+
console.log(yellow('Cannot register: not in a git repository with a remote.'));
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Register locally
|
|
252
|
+
const registry = getRegistry();
|
|
253
|
+
const isNew = registry.register(gitRemote, gitRoot);
|
|
254
|
+
|
|
255
|
+
// Register with backend
|
|
256
|
+
try {
|
|
257
|
+
await api.registerProject(gitRemote, keywords, description);
|
|
258
|
+
console.log(green(`✓ Project registered: ${gitRemote}`));
|
|
259
|
+
return true;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.log(yellow(`Local registration OK, but backend registration failed: ${error.message}`));
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Print a status line with icon.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} icon - Status icon
|
|
270
|
+
* @param {string} label - Status label
|
|
271
|
+
* @param {string} value - Status value
|
|
272
|
+
*/
|
|
273
|
+
function printStatus(icon, label, value) {
|
|
274
|
+
console.log(` ${icon} ${label}: ${value}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Run the connect/doctor flow.
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} options - Options from CLI
|
|
281
|
+
* @returns {Promise<void>}
|
|
282
|
+
*/
|
|
283
|
+
export async function runConnect(options = {}) {
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(bold('Push Connect - Diagnostic Check'));
|
|
286
|
+
console.log(dim('=' .repeat(40)));
|
|
287
|
+
console.log('');
|
|
288
|
+
|
|
289
|
+
let allPassed = true;
|
|
290
|
+
|
|
291
|
+
// Step 1: Version check
|
|
292
|
+
console.log(bold('1. Version Check'));
|
|
293
|
+
const versionStatus = await checkVersion();
|
|
294
|
+
if (versionStatus.updateAvailable) {
|
|
295
|
+
printStatus(yellow('⚠'), 'Version', `${versionStatus.current} → ${versionStatus.latest} available`);
|
|
296
|
+
console.log(dim(` Update: npm update -g @masslessai/push-todo`));
|
|
297
|
+
allPassed = false;
|
|
298
|
+
} else {
|
|
299
|
+
printStatus(green('✓'), 'Version', `${versionStatus.current} (latest)`);
|
|
300
|
+
}
|
|
301
|
+
console.log('');
|
|
302
|
+
|
|
303
|
+
// Step 2: API Key validation
|
|
304
|
+
console.log(bold('2. API Key'));
|
|
305
|
+
let keyStatus = await validateApiKeyStatus();
|
|
306
|
+
|
|
307
|
+
if (keyStatus.status === 'missing' || keyStatus.status === 'invalid') {
|
|
308
|
+
printStatus(red('✗'), 'API Key', keyStatus.message || 'Invalid');
|
|
309
|
+
console.log('');
|
|
310
|
+
|
|
311
|
+
// Run auth flow
|
|
312
|
+
const authSuccess = await runAuthFlow();
|
|
313
|
+
if (authSuccess) {
|
|
314
|
+
keyStatus = await validateApiKeyStatus();
|
|
315
|
+
} else {
|
|
316
|
+
allPassed = false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (keyStatus.status === 'valid') {
|
|
321
|
+
printStatus(green('✓'), 'API Key', `Valid (${keyStatus.email})`);
|
|
322
|
+
}
|
|
323
|
+
console.log('');
|
|
324
|
+
|
|
325
|
+
// Step 3: Machine validation
|
|
326
|
+
console.log(bold('3. Machine'));
|
|
327
|
+
const machineStatus = await validateMachineStatus();
|
|
328
|
+
if (machineStatus.status === 'valid') {
|
|
329
|
+
printStatus(green('✓'), 'Machine', machineStatus.machineName);
|
|
330
|
+
printStatus(dim('·'), 'ID', machineStatus.machineId);
|
|
331
|
+
} else {
|
|
332
|
+
printStatus(yellow('⚠'), 'Machine', machineStatus.message || 'Not validated');
|
|
333
|
+
allPassed = false;
|
|
334
|
+
}
|
|
335
|
+
console.log('');
|
|
336
|
+
|
|
337
|
+
// Step 4: Project validation
|
|
338
|
+
console.log(bold('4. Project'));
|
|
339
|
+
const projectStatus = validateProjectStatus();
|
|
340
|
+
|
|
341
|
+
if (projectStatus.status === 'registered') {
|
|
342
|
+
printStatus(green('✓'), 'Project', projectStatus.gitRemote);
|
|
343
|
+
printStatus(dim('·'), 'Path', projectStatus.localPath);
|
|
344
|
+
} else if (projectStatus.status === 'unregistered') {
|
|
345
|
+
printStatus(yellow('⚠'), 'Project', `${projectStatus.gitRemote} (not registered)`);
|
|
346
|
+
console.log('');
|
|
347
|
+
|
|
348
|
+
// Offer to register
|
|
349
|
+
if (keyStatus.status === 'valid') {
|
|
350
|
+
const keywords = options.keywords ? options.keywords.split(',') : [];
|
|
351
|
+
const description = options.description || '';
|
|
352
|
+
await registerCurrentProject(keywords, description);
|
|
353
|
+
}
|
|
354
|
+
} else if (projectStatus.status === 'no_remote') {
|
|
355
|
+
printStatus(yellow('⚠'), 'Project', 'No git remote configured');
|
|
356
|
+
allPassed = false;
|
|
357
|
+
} else {
|
|
358
|
+
printStatus(dim('·'), 'Project', 'Not in a git repository');
|
|
359
|
+
}
|
|
360
|
+
console.log('');
|
|
361
|
+
|
|
362
|
+
// Step 5: E2EE check
|
|
363
|
+
console.log(bold('5. E2EE (End-to-End Encryption)'));
|
|
364
|
+
const [e2eeAvailable, e2eeMessage] = isE2EEAvailable();
|
|
365
|
+
if (e2eeAvailable) {
|
|
366
|
+
printStatus(green('✓'), 'E2EE', 'Available');
|
|
367
|
+
} else {
|
|
368
|
+
printStatus(yellow('⚠'), 'E2EE', e2eeMessage);
|
|
369
|
+
}
|
|
370
|
+
console.log('');
|
|
371
|
+
|
|
372
|
+
// Summary
|
|
373
|
+
console.log(dim('=' .repeat(40)));
|
|
374
|
+
if (allPassed) {
|
|
375
|
+
console.log(green(bold('All checks passed!')));
|
|
376
|
+
} else {
|
|
377
|
+
console.log(yellow('Some checks need attention. See above for details.'));
|
|
378
|
+
}
|
|
379
|
+
console.log('');
|
|
380
|
+
}
|