prjct-cli 0.17.0 → 0.18.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/core/__tests__/agentic/memory-system.test.ts +2 -1
- package/core/__tests__/agentic/plan-mode.test.ts +2 -1
- package/core/agentic/agent-router.ts +21 -75
- package/core/command-registry/setup-commands.ts +15 -0
- package/core/domain/agent-generator.ts +9 -17
- package/core/infrastructure/path-manager.ts +23 -1
- package/core/storage/ideas-storage.ts +4 -0
- package/core/storage/queue-storage.ts +4 -0
- package/core/storage/shipped-storage.ts +4 -0
- package/core/storage/state-storage.ts +4 -0
- package/core/storage/storage-manager.ts +10 -7
- package/core/sync/auth-config.ts +145 -0
- package/core/sync/index.ts +30 -0
- package/core/sync/oauth-handler.ts +148 -0
- package/core/sync/sync-client.ts +252 -0
- package/core/sync/sync-manager.ts +358 -0
- package/package.json +1 -1
- package/templates/commands/auth.md +234 -0
- package/templates/commands/sync.md +91 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Manager - Orchestrates push/pull operations
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for sync operations.
|
|
5
|
+
* Handles the coordination between local storage (EventBus) and remote API (SyncClient).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { syncClient, type SyncBatchResult, type SyncPullResult, type SyncStatus } from './sync-client'
|
|
9
|
+
import authConfig from './auth-config'
|
|
10
|
+
import eventBus, { type SyncEvent } from '../events'
|
|
11
|
+
import { stateStorage } from '../storage/state-storage'
|
|
12
|
+
import { queueStorage } from '../storage/queue-storage'
|
|
13
|
+
import { ideasStorage, type IdeaPriority } from '../storage/ideas-storage'
|
|
14
|
+
import { shippedStorage } from '../storage/shipped-storage'
|
|
15
|
+
import type { TaskType, Priority, TaskSection } from '../schemas/state'
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
export interface SyncResult {
|
|
22
|
+
success: boolean
|
|
23
|
+
skipped: boolean
|
|
24
|
+
reason?: 'no_auth' | 'no_pending' | 'error'
|
|
25
|
+
pushed?: {
|
|
26
|
+
count: number
|
|
27
|
+
syncedAt: string
|
|
28
|
+
}
|
|
29
|
+
pulled?: {
|
|
30
|
+
count: number
|
|
31
|
+
syncedAt: string
|
|
32
|
+
}
|
|
33
|
+
error?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PushResult {
|
|
37
|
+
success: boolean
|
|
38
|
+
skipped: boolean
|
|
39
|
+
reason?: 'no_auth' | 'no_pending' | 'error'
|
|
40
|
+
count?: number
|
|
41
|
+
syncedAt?: string
|
|
42
|
+
error?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PullResult {
|
|
46
|
+
success: boolean
|
|
47
|
+
skipped: boolean
|
|
48
|
+
reason?: 'no_auth' | 'error'
|
|
49
|
+
count?: number
|
|
50
|
+
applied?: number
|
|
51
|
+
syncedAt?: string
|
|
52
|
+
error?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================
|
|
56
|
+
// Sync Manager
|
|
57
|
+
// ============================================
|
|
58
|
+
|
|
59
|
+
class SyncManager {
|
|
60
|
+
/**
|
|
61
|
+
* Check if user is authenticated
|
|
62
|
+
*/
|
|
63
|
+
async hasAuth(): Promise<boolean> {
|
|
64
|
+
return await authConfig.hasAuth()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get sync status from API
|
|
69
|
+
*/
|
|
70
|
+
async getStatus(projectId: string): Promise<SyncStatus | null> {
|
|
71
|
+
if (!(await this.hasAuth())) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return await syncClient.getStatus(projectId)
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Full sync: push local changes, then pull remote changes
|
|
84
|
+
*/
|
|
85
|
+
async sync(projectId: string): Promise<SyncResult> {
|
|
86
|
+
// Check auth first
|
|
87
|
+
if (!(await this.hasAuth())) {
|
|
88
|
+
return { success: true, skipped: true, reason: 'no_auth' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result: SyncResult = { success: true, skipped: false }
|
|
92
|
+
|
|
93
|
+
// Push first
|
|
94
|
+
const pushResult = await this.push(projectId)
|
|
95
|
+
if (pushResult.success && !pushResult.skipped) {
|
|
96
|
+
result.pushed = {
|
|
97
|
+
count: pushResult.count || 0,
|
|
98
|
+
syncedAt: pushResult.syncedAt || new Date().toISOString(),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Then pull
|
|
103
|
+
const pullResult = await this.pull(projectId)
|
|
104
|
+
if (pullResult.success && !pullResult.skipped) {
|
|
105
|
+
result.pulled = {
|
|
106
|
+
count: pullResult.count || 0,
|
|
107
|
+
syncedAt: pullResult.syncedAt || new Date().toISOString(),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Determine overall success
|
|
112
|
+
if (!pushResult.success || !pullResult.success) {
|
|
113
|
+
result.success = false
|
|
114
|
+
result.error = pushResult.error || pullResult.error
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Push local pending events to the server
|
|
122
|
+
*/
|
|
123
|
+
async push(projectId: string): Promise<PushResult> {
|
|
124
|
+
// Check auth first
|
|
125
|
+
if (!(await this.hasAuth())) {
|
|
126
|
+
return { success: true, skipped: true, reason: 'no_auth' }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Get pending events
|
|
131
|
+
const pending = await eventBus.getPending(projectId)
|
|
132
|
+
|
|
133
|
+
if (pending.length === 0) {
|
|
134
|
+
return { success: true, skipped: true, reason: 'no_pending' }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Push to server
|
|
138
|
+
const result: SyncBatchResult = await syncClient.pushEvents(projectId, pending)
|
|
139
|
+
|
|
140
|
+
if (result.success) {
|
|
141
|
+
// Clear pending events on success
|
|
142
|
+
await eventBus.clearPending(projectId)
|
|
143
|
+
await eventBus.updateLastSync(projectId)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
skipped: false,
|
|
148
|
+
count: result.processed,
|
|
149
|
+
syncedAt: result.syncedAt,
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// Partial success - some events failed
|
|
153
|
+
const successCount = result.processed
|
|
154
|
+
const errorCount = result.errors.length
|
|
155
|
+
const errorMessages = result.errors.map((e) => e.error).join(', ')
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
skipped: false,
|
|
160
|
+
count: successCount,
|
|
161
|
+
syncedAt: result.syncedAt,
|
|
162
|
+
error: `${errorCount} events failed: ${errorMessages}`,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
skipped: false,
|
|
170
|
+
reason: 'error',
|
|
171
|
+
error: message,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Pull remote changes from the server
|
|
178
|
+
*/
|
|
179
|
+
async pull(projectId: string): Promise<PullResult> {
|
|
180
|
+
// Check auth first
|
|
181
|
+
if (!(await this.hasAuth())) {
|
|
182
|
+
return { success: true, skipped: true, reason: 'no_auth' }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// Get last sync timestamp
|
|
187
|
+
const lastSync = await eventBus.getLastSync(projectId)
|
|
188
|
+
const since = lastSync?.timestamp
|
|
189
|
+
|
|
190
|
+
// Pull from server
|
|
191
|
+
const result: SyncPullResult = await syncClient.pullEvents(projectId, since)
|
|
192
|
+
|
|
193
|
+
if (result.events.length === 0) {
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
skipped: false,
|
|
197
|
+
count: 0,
|
|
198
|
+
applied: 0,
|
|
199
|
+
syncedAt: result.syncedAt,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Apply pulled events to local storage
|
|
204
|
+
const applied = await this.applyPulledEvents(projectId, result.events)
|
|
205
|
+
|
|
206
|
+
// Update last sync timestamp
|
|
207
|
+
await eventBus.updateLastSync(projectId)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
success: true,
|
|
211
|
+
skipped: false,
|
|
212
|
+
count: result.events.length,
|
|
213
|
+
applied,
|
|
214
|
+
syncedAt: result.syncedAt,
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
skipped: false,
|
|
221
|
+
reason: 'error',
|
|
222
|
+
error: message,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Apply pulled events to local storage
|
|
229
|
+
* Returns number of events successfully applied
|
|
230
|
+
*/
|
|
231
|
+
async applyPulledEvents(
|
|
232
|
+
projectId: string,
|
|
233
|
+
events: Array<{ type: string; path: string[]; data: unknown; timestamp: string }>
|
|
234
|
+
): Promise<number> {
|
|
235
|
+
let applied = 0
|
|
236
|
+
|
|
237
|
+
for (const event of events) {
|
|
238
|
+
try {
|
|
239
|
+
await this.applyEvent(projectId, event)
|
|
240
|
+
applied++
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// Log but continue with other events
|
|
243
|
+
console.error(`Failed to apply event ${event.type}:`, error)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return applied
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Apply a single event to local storage
|
|
252
|
+
*/
|
|
253
|
+
private async applyEvent(
|
|
254
|
+
projectId: string,
|
|
255
|
+
event: { type: string; path: string[]; data: unknown; timestamp: string }
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const [entity, action] = event.type.split('.') as [string, string]
|
|
258
|
+
const data = event.data as Record<string, unknown>
|
|
259
|
+
|
|
260
|
+
switch (entity) {
|
|
261
|
+
case 'task':
|
|
262
|
+
await this.applyTaskEvent(projectId, action, data)
|
|
263
|
+
break
|
|
264
|
+
case 'idea':
|
|
265
|
+
await this.applyIdeaEvent(projectId, action, data)
|
|
266
|
+
break
|
|
267
|
+
case 'shipped':
|
|
268
|
+
await this.applyShippedEvent(projectId, action, data)
|
|
269
|
+
break
|
|
270
|
+
// Add more entity handlers as needed
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async applyTaskEvent(
|
|
275
|
+
projectId: string,
|
|
276
|
+
action: string,
|
|
277
|
+
data: Record<string, unknown>
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
switch (action) {
|
|
280
|
+
case 'started':
|
|
281
|
+
// Update state if this is a newer task
|
|
282
|
+
await stateStorage.update(projectId, (state) => {
|
|
283
|
+
if (!state.currentTask || (data.id as string) !== state.currentTask.id) {
|
|
284
|
+
return {
|
|
285
|
+
...state,
|
|
286
|
+
currentTask: {
|
|
287
|
+
id: data.id as string,
|
|
288
|
+
description: data.description as string,
|
|
289
|
+
startedAt: data.startedAt as string,
|
|
290
|
+
sessionId: data.sessionId as string,
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return state
|
|
295
|
+
})
|
|
296
|
+
break
|
|
297
|
+
case 'completed':
|
|
298
|
+
// Clear current task if it matches
|
|
299
|
+
await stateStorage.update(projectId, (state) => {
|
|
300
|
+
if (state.currentTask?.id === data.id) {
|
|
301
|
+
return { ...state, currentTask: null }
|
|
302
|
+
}
|
|
303
|
+
return state
|
|
304
|
+
})
|
|
305
|
+
break
|
|
306
|
+
case 'created':
|
|
307
|
+
// Add to queue
|
|
308
|
+
await queueStorage.addTask(projectId, {
|
|
309
|
+
description: data.description as string,
|
|
310
|
+
priority: (data.priority as Priority) || 'medium',
|
|
311
|
+
type: (data.type as TaskType) || 'feature',
|
|
312
|
+
section: 'backlog' as TaskSection,
|
|
313
|
+
})
|
|
314
|
+
break
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private async applyIdeaEvent(
|
|
319
|
+
projectId: string,
|
|
320
|
+
action: string,
|
|
321
|
+
data: Record<string, unknown>
|
|
322
|
+
): Promise<void> {
|
|
323
|
+
switch (action) {
|
|
324
|
+
case 'created':
|
|
325
|
+
await ideasStorage.addIdea(
|
|
326
|
+
projectId,
|
|
327
|
+
(data.title as string) || (data.text as string),
|
|
328
|
+
{ priority: (data.priority as IdeaPriority) || 'medium' }
|
|
329
|
+
)
|
|
330
|
+
break
|
|
331
|
+
case 'archived':
|
|
332
|
+
await ideasStorage.update(projectId, (ideas) => ({
|
|
333
|
+
...ideas,
|
|
334
|
+
ideas: ideas.ideas.map((idea) =>
|
|
335
|
+
idea.id === data.id ? { ...idea, status: 'archived' as const } : idea
|
|
336
|
+
),
|
|
337
|
+
}))
|
|
338
|
+
break
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private async applyShippedEvent(
|
|
343
|
+
projectId: string,
|
|
344
|
+
action: string,
|
|
345
|
+
data: Record<string, unknown>
|
|
346
|
+
): Promise<void> {
|
|
347
|
+
if (action === 'created') {
|
|
348
|
+
await shippedStorage.addShipped(projectId, {
|
|
349
|
+
name: (data.name as string) || (data.title as string),
|
|
350
|
+
version: data.version as string,
|
|
351
|
+
description: data.description as string,
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export const syncManager = new SyncManager()
|
|
358
|
+
export default syncManager
|
package/package.json
CHANGED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
---
|
|
2
|
+
allowed-tools: [Read, Write, Bash]
|
|
3
|
+
description: 'Manage Cloud Authentication'
|
|
4
|
+
timestamp-rule: 'GetTimestamp() for all timestamps'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /p:auth - Cloud Authentication
|
|
8
|
+
|
|
9
|
+
Manage authentication for prjct cloud sync.
|
|
10
|
+
|
|
11
|
+
## Subcommands
|
|
12
|
+
|
|
13
|
+
| Command | Purpose |
|
|
14
|
+
|---------|---------|
|
|
15
|
+
| `/p:auth` | Show current auth status |
|
|
16
|
+
| `/p:auth login` | Authenticate with prjct cloud |
|
|
17
|
+
| `/p:auth logout` | Clear authentication |
|
|
18
|
+
| `/p:auth status` | Detailed auth status |
|
|
19
|
+
|
|
20
|
+
## Context Variables
|
|
21
|
+
- `{authPath}`: `~/.prjct-cli/config/auth.json`
|
|
22
|
+
- `{apiUrl}`: API base URL (default: https://api.prjct.app)
|
|
23
|
+
- `{dashboardUrl}`: Web dashboard URL (https://app.prjct.app)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## /p:auth (default) - Show Status
|
|
28
|
+
|
|
29
|
+
### Flow
|
|
30
|
+
|
|
31
|
+
1. READ: `{authPath}`
|
|
32
|
+
2. IF authenticated:
|
|
33
|
+
- Show email and API key prefix
|
|
34
|
+
3. ELSE:
|
|
35
|
+
- Show "Not authenticated" message
|
|
36
|
+
|
|
37
|
+
### Output (Authenticated)
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
☁️ Cloud Sync: Connected
|
|
41
|
+
|
|
42
|
+
Email: {email}
|
|
43
|
+
API Key: {apiKeyPrefix}...
|
|
44
|
+
Last auth: {lastAuth}
|
|
45
|
+
|
|
46
|
+
Sync enabled for all projects.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Output (Not Authenticated)
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
☁️ Cloud Sync: Not connected
|
|
53
|
+
|
|
54
|
+
Run `/p:auth login` to enable cloud sync.
|
|
55
|
+
|
|
56
|
+
Benefits:
|
|
57
|
+
- Sync progress across devices
|
|
58
|
+
- Access from web dashboard
|
|
59
|
+
- Backup your project data
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## /p:auth login - Authenticate
|
|
65
|
+
|
|
66
|
+
### Flow
|
|
67
|
+
|
|
68
|
+
1. **Check existing auth**
|
|
69
|
+
READ: `{authPath}`
|
|
70
|
+
IF already authenticated:
|
|
71
|
+
ASK: "You're already logged in as {email}. Re-authenticate? (y/n)"
|
|
72
|
+
IF no: STOP
|
|
73
|
+
|
|
74
|
+
2. **Open dashboard**
|
|
75
|
+
OUTPUT: "Opening prjct dashboard to get your API key..."
|
|
76
|
+
OPEN browser: `{dashboardUrl}/settings/api-keys`
|
|
77
|
+
|
|
78
|
+
3. **Wait for API key**
|
|
79
|
+
OUTPUT instructions:
|
|
80
|
+
```
|
|
81
|
+
1. Log in to prjct.app (GitHub OAuth)
|
|
82
|
+
2. Go to Settings → API Keys
|
|
83
|
+
3. Click "Create New Key"
|
|
84
|
+
4. Copy the key (starts with prjct_)
|
|
85
|
+
5. Paste it below
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
4. **Get API key from user**
|
|
89
|
+
PROMPT: "Paste your API key: "
|
|
90
|
+
READ: `{apiKey}` from user input
|
|
91
|
+
|
|
92
|
+
5. **Validate key**
|
|
93
|
+
- Check format starts with "prjct_"
|
|
94
|
+
- Test connection with GET /health
|
|
95
|
+
- Fetch user info with GET /auth/me
|
|
96
|
+
|
|
97
|
+
IF invalid:
|
|
98
|
+
OUTPUT: "Invalid API key. Please try again."
|
|
99
|
+
STOP
|
|
100
|
+
|
|
101
|
+
6. **Save auth**
|
|
102
|
+
WRITE: `{authPath}`
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"apiKey": "{apiKey}",
|
|
106
|
+
"apiUrl": "https://api.prjct.app",
|
|
107
|
+
"userId": "{userId}",
|
|
108
|
+
"email": "{email}",
|
|
109
|
+
"lastAuth": "{GetTimestamp()}"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Output (Success)
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
✅ Authentication successful!
|
|
117
|
+
|
|
118
|
+
Logged in as: {email}
|
|
119
|
+
API Key: {apiKeyPrefix}...
|
|
120
|
+
|
|
121
|
+
Cloud sync is now enabled. Your projects will sync automatically
|
|
122
|
+
when you run /p:sync or /p:ship.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Output (Failure)
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
❌ Authentication failed
|
|
129
|
+
|
|
130
|
+
{error}
|
|
131
|
+
|
|
132
|
+
Please check your API key and try again.
|
|
133
|
+
Get a new key at: {dashboardUrl}/settings/api-keys
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## /p:auth logout - Clear Auth
|
|
139
|
+
|
|
140
|
+
### Flow
|
|
141
|
+
|
|
142
|
+
1. READ: `{authPath}`
|
|
143
|
+
IF not authenticated:
|
|
144
|
+
OUTPUT: "Not logged in. Nothing to do."
|
|
145
|
+
STOP
|
|
146
|
+
|
|
147
|
+
2. ASK: "Are you sure you want to log out? (y/n)"
|
|
148
|
+
IF no: STOP
|
|
149
|
+
|
|
150
|
+
3. DELETE or CLEAR: `{authPath}`
|
|
151
|
+
|
|
152
|
+
### Output
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
✅ Logged out successfully
|
|
156
|
+
|
|
157
|
+
Cloud sync is now disabled.
|
|
158
|
+
Run `/p:auth login` to re-enable.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## /p:auth status - Detailed Status
|
|
164
|
+
|
|
165
|
+
### Flow
|
|
166
|
+
|
|
167
|
+
1. READ: `{authPath}`
|
|
168
|
+
2. IF authenticated:
|
|
169
|
+
- Test connection
|
|
170
|
+
- Show detailed status
|
|
171
|
+
3. ELSE:
|
|
172
|
+
- Show not connected message
|
|
173
|
+
|
|
174
|
+
### Output (Connected)
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
☁️ Cloud Authentication Status
|
|
178
|
+
|
|
179
|
+
Connection: ✓ Connected
|
|
180
|
+
Email: {email}
|
|
181
|
+
User ID: {userId}
|
|
182
|
+
API Key: {apiKeyPrefix}...
|
|
183
|
+
API URL: {apiUrl}
|
|
184
|
+
Last Auth: {lastAuth}
|
|
185
|
+
|
|
186
|
+
API Status: ✓ Reachable
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Output (Connection Error)
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
☁️ Cloud Authentication Status
|
|
193
|
+
|
|
194
|
+
Connection: ⚠️ Error
|
|
195
|
+
Email: {email}
|
|
196
|
+
API Key: {apiKeyPrefix}...
|
|
197
|
+
API URL: {apiUrl}
|
|
198
|
+
|
|
199
|
+
Error: {connectionError}
|
|
200
|
+
|
|
201
|
+
Try `/p:auth login` to re-authenticate.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Error Handling
|
|
207
|
+
|
|
208
|
+
| Error | Response |
|
|
209
|
+
|-------|----------|
|
|
210
|
+
| Invalid key format | "API key must start with prjct_" |
|
|
211
|
+
| Key rejected by API | "Invalid or expired API key" |
|
|
212
|
+
| Network error | "Cannot connect to {apiUrl}. Check internet." |
|
|
213
|
+
| Already logged in | Offer to re-authenticate |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Auth File Structure
|
|
218
|
+
|
|
219
|
+
Location: `~/.prjct-cli/config/auth.json`
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"apiKey": "prjct_live_xxxxxxxxxxxxxxxxxxxx",
|
|
224
|
+
"apiUrl": "https://api.prjct.app",
|
|
225
|
+
"userId": "uuid-from-server",
|
|
226
|
+
"email": "user@example.com",
|
|
227
|
+
"lastAuth": "2024-01-15T10:00:00.000Z"
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Security Notes:**
|
|
232
|
+
- API key is stored in plain text (like git credentials)
|
|
233
|
+
- File permissions should be 600 (user read/write only)
|
|
234
|
+
- Never commit this file to version control
|
|
@@ -54,6 +54,42 @@ Git Analysis → Storage (JSON) → Context (MD) → Project Metadata
|
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
57
|
+
## Step 0: Migration Check (Legacy Projects)
|
|
58
|
+
|
|
59
|
+
CHECK: Does `.prjct/prjct.config.json` exist?
|
|
60
|
+
|
|
61
|
+
IF file exists:
|
|
62
|
+
READ: `.prjct/prjct.config.json`
|
|
63
|
+
CHECK: Does `projectId` exist and is it a valid UUID?
|
|
64
|
+
|
|
65
|
+
IF projectId is missing OR not a UUID:
|
|
66
|
+
MIGRATE to UUID:
|
|
67
|
+
1. Generate new UUID: `{newProjectId}`
|
|
68
|
+
2. Create global structure: `~/.prjct-cli/projects/{newProjectId}/`
|
|
69
|
+
3. Create subdirectories: storage/, context/, agents/, memory/, analysis/
|
|
70
|
+
4. IF legacy data exists in `.prjct/`:
|
|
71
|
+
- Migrate core/now.md → storage/state.json
|
|
72
|
+
- Migrate planning/ideas.md → storage/ideas.json
|
|
73
|
+
- Migrate progress/shipped.md → storage/shipped.json
|
|
74
|
+
5. Update `.prjct/prjct.config.json` with new `projectId`
|
|
75
|
+
OUTPUT: "🔄 Migrated to UUID format: {newProjectId}"
|
|
76
|
+
|
|
77
|
+
IF file not found:
|
|
78
|
+
CHECK: Does `.prjct/` directory exist? (legacy project without config)
|
|
79
|
+
|
|
80
|
+
IF `.prjct/` exists:
|
|
81
|
+
MIGRATE:
|
|
82
|
+
1. Generate new UUID: `{newProjectId}`
|
|
83
|
+
2. Create `.prjct/prjct.config.json` with `projectId`
|
|
84
|
+
3. Create global structure
|
|
85
|
+
4. Migrate legacy data
|
|
86
|
+
OUTPUT: "🔄 Migrated legacy project to UUID: {newProjectId}"
|
|
87
|
+
ELSE:
|
|
88
|
+
OUTPUT: "No prjct project. Run /p:init first."
|
|
89
|
+
STOP
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
57
93
|
## Step 1: Read Config
|
|
58
94
|
|
|
59
95
|
READ: `.prjct/prjct.config.json`
|
|
@@ -409,6 +445,52 @@ APPEND to: `{globalPath}/memory/events.jsonl`
|
|
|
409
445
|
|
|
410
446
|
---
|
|
411
447
|
|
|
448
|
+
## Step 9: Backend Sync (Cloud)
|
|
449
|
+
|
|
450
|
+
Sync with prjct API if authenticated.
|
|
451
|
+
|
|
452
|
+
### 9.1 Check Authentication
|
|
453
|
+
|
|
454
|
+
READ: `~/.prjct-cli/config/auth.json`
|
|
455
|
+
|
|
456
|
+
IF no auth OR no apiKey:
|
|
457
|
+
SET: `{cloudSync}` = false
|
|
458
|
+
OUTPUT TIP: "💡 Run `prjct auth` to enable cloud sync"
|
|
459
|
+
CONTINUE to output (skip 9.2, 9.3)
|
|
460
|
+
|
|
461
|
+
ELSE:
|
|
462
|
+
SET: `{cloudSync}` = true
|
|
463
|
+
|
|
464
|
+
### 9.2 Push Pending Events
|
|
465
|
+
|
|
466
|
+
READ: `{globalPath}/sync/pending.json`
|
|
467
|
+
COUNT: `{pendingCount}` events
|
|
468
|
+
|
|
469
|
+
IF pendingCount > 0:
|
|
470
|
+
CALL syncManager.push(projectId)
|
|
471
|
+
|
|
472
|
+
IF success:
|
|
473
|
+
SET: `{pushedCount}` = result.count
|
|
474
|
+
OUTPUT: "☁️ Pushed {pushedCount} events to cloud"
|
|
475
|
+
ELSE:
|
|
476
|
+
OUTPUT: "⚠️ Cloud sync failed: {error}. Events queued for retry."
|
|
477
|
+
SET: `{syncError}` = error
|
|
478
|
+
ELSE:
|
|
479
|
+
SET: `{pushedCount}` = 0
|
|
480
|
+
|
|
481
|
+
### 9.3 Pull Updates (if push succeeded)
|
|
482
|
+
|
|
483
|
+
IF cloudSync AND no syncError:
|
|
484
|
+
CALL syncManager.pull(projectId)
|
|
485
|
+
|
|
486
|
+
IF success AND result.count > 0:
|
|
487
|
+
SET: `{pulledCount}` = result.count
|
|
488
|
+
OUTPUT: "📥 Pulled {pulledCount} updates from cloud"
|
|
489
|
+
ELSE:
|
|
490
|
+
SET: `{pulledCount}` = 0
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
412
494
|
## Output
|
|
413
495
|
|
|
414
496
|
```
|
|
@@ -436,6 +518,15 @@ APPEND to: `{globalPath}/memory/events.jsonl`
|
|
|
436
518
|
├── Workflow: prjct-workflow, prjct-planner, prjct-shipper
|
|
437
519
|
└── Domain: {domainAgents.join(', ') || 'none'}
|
|
438
520
|
|
|
521
|
+
{IF cloudSync}
|
|
522
|
+
☁️ Cloud Sync
|
|
523
|
+
├── Pushed: {pushedCount} events
|
|
524
|
+
├── Pulled: {pulledCount} updates
|
|
525
|
+
└── Status: {syncError ? "⚠️ " + syncError : "✓ Synced"}
|
|
526
|
+
{ELSE}
|
|
527
|
+
💡 Cloud sync disabled. Run `prjct auth` to enable.
|
|
528
|
+
{ENDIF}
|
|
529
|
+
|
|
439
530
|
{IF hasUncommittedChanges}
|
|
440
531
|
⚠️ You have uncommitted changes
|
|
441
532
|
|