prjct-cli 0.15.1 → 0.18.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/CHANGELOG.md +35 -0
- package/bin/dev.js +0 -1
- package/bin/serve.js +19 -20
- 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 +79 -14
- package/core/agentic/command-executor/command-executor.ts +2 -74
- package/core/agentic/services.ts +0 -48
- package/core/agentic/template-loader.ts +35 -1
- package/core/command-registry/setup-commands.ts +15 -0
- package/core/commands/base.ts +96 -77
- package/core/commands/planning.ts +13 -2
- package/core/commands/setup.ts +3 -85
- package/core/domain/agent-generator.ts +9 -17
- package/core/errors.ts +209 -0
- package/core/infrastructure/config-manager.ts +22 -5
- package/core/infrastructure/path-manager.ts +23 -1
- package/core/infrastructure/setup.ts +5 -50
- 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 +52 -13
- 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/core/utils/logger.ts +19 -12
- package/package.json +2 -4
- package/templates/agentic/subagent-generation.md +109 -0
- package/templates/commands/auth.md +234 -0
- package/templates/commands/sync.md +129 -13
- package/templates/subagents/domain/backend.md +105 -0
- package/templates/subagents/domain/database.md +118 -0
- package/templates/subagents/domain/devops.md +148 -0
- package/templates/subagents/domain/frontend.md +99 -0
- package/templates/subagents/domain/testing.md +169 -0
- package/templates/subagents/workflow/prjct-planner.md +158 -0
- package/templates/subagents/workflow/prjct-shipper.md +179 -0
- package/templates/subagents/workflow/prjct-workflow.md +98 -0
- package/bin/generate-views.js +0 -209
- package/bin/migrate-to-json.js +0 -742
- package/core/agentic/context-filter.ts +0 -365
- package/core/agentic/parallel-tools.ts +0 -165
- package/core/agentic/response-templates.ts +0 -164
- package/core/agentic/semantic-compression.ts +0 -273
- package/core/agentic/think-blocks.ts +0 -202
- package/core/agentic/validation-rules.ts +0 -313
- package/core/domain/agent-matcher.ts +0 -130
- package/core/domain/agent-validator.ts +0 -250
- package/core/domain/architect-session.ts +0 -315
- package/core/domain/product-standards.ts +0 -106
- package/core/domain/smart-cache.ts +0 -167
- package/core/domain/task-analyzer.ts +0 -296
- package/core/infrastructure/legacy-installer-detector/cleanup.ts +0 -216
- package/core/infrastructure/legacy-installer-detector/detection.ts +0 -95
- package/core/infrastructure/legacy-installer-detector/index.ts +0 -171
- package/core/infrastructure/legacy-installer-detector/migration.ts +0 -87
- package/core/infrastructure/legacy-installer-detector/types.ts +0 -42
- package/core/infrastructure/legacy-installer-detector.ts +0 -7
- package/core/infrastructure/migrator/file-operations.ts +0 -125
- package/core/infrastructure/migrator/index.ts +0 -288
- package/core/infrastructure/migrator/project-scanner.ts +0 -90
- package/core/infrastructure/migrator/reports.ts +0 -117
- package/core/infrastructure/migrator/types.ts +0 -124
- package/core/infrastructure/migrator/validation.ts +0 -94
- package/core/infrastructure/migrator/version-migration.ts +0 -117
- package/core/infrastructure/migrator.ts +0 -10
- package/core/infrastructure/uuid-migration.ts +0 -750
- package/templates/commands/migrate-all.md +0 -96
- package/templates/commands/migrate.md +0 -140
|
@@ -114,7 +114,7 @@ class PathManager {
|
|
|
114
114
|
|
|
115
115
|
const projectPath = this.getGlobalProjectPath(projectId)
|
|
116
116
|
|
|
117
|
-
const layers = ['core', 'progress', 'planning', 'analysis', 'memory']
|
|
117
|
+
const layers = ['core', 'progress', 'planning', 'analysis', 'memory', 'agents']
|
|
118
118
|
|
|
119
119
|
for (const layer of layers) {
|
|
120
120
|
await fileHelper.ensureDir(path.join(projectPath, layer))
|
|
@@ -252,6 +252,28 @@ class PathManager {
|
|
|
252
252
|
}
|
|
253
253
|
return absolutePath
|
|
254
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get the auth config file path for cloud sync
|
|
258
|
+
* Stored in global config directory, not project-specific
|
|
259
|
+
*/
|
|
260
|
+
getAuthConfigPath(): string {
|
|
261
|
+
return path.join(this.globalConfigDir, 'auth.json')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get the sync pending events file path for a project
|
|
266
|
+
*/
|
|
267
|
+
getSyncPendingPath(projectId: string): string {
|
|
268
|
+
return path.join(this.getGlobalProjectPath(projectId), 'sync', 'pending.json')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get the last sync timestamp file path for a project
|
|
273
|
+
*/
|
|
274
|
+
getLastSyncPath(projectId: string): string {
|
|
275
|
+
return path.join(this.getGlobalProjectPath(projectId), 'sync', 'last-sync.json')
|
|
276
|
+
}
|
|
255
277
|
}
|
|
256
278
|
|
|
257
279
|
const pathManager = new PathManager()
|
|
@@ -2,24 +2,18 @@
|
|
|
2
2
|
* Setup Module - Core installation logic
|
|
3
3
|
*
|
|
4
4
|
* Executes ALL setup needed for prjct-cli:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
9
|
-
* 5. Migrate legacy projects automatically
|
|
10
|
-
* 6. Save version in editors-config
|
|
5
|
+
* 1. Install Claude Code CLI if missing
|
|
6
|
+
* 2. Sync commands to ~/.claude/commands/p/
|
|
7
|
+
* 3. Install global config ~/.claude/CLAUDE.md
|
|
8
|
+
* 4. Save version in editors-config
|
|
11
9
|
*
|
|
12
10
|
* This module is called from:
|
|
13
11
|
* - core/index.js (on first CLI use)
|
|
14
12
|
* - scripts/postinstall.js (if npm scripts are enabled)
|
|
15
|
-
*
|
|
16
|
-
* @version 0.8.8
|
|
17
13
|
*/
|
|
18
14
|
|
|
19
15
|
import { execSync } from 'child_process'
|
|
20
16
|
import installer from './command-installer'
|
|
21
|
-
import migrator from './migrator'
|
|
22
|
-
import legacyDetector from './legacy-installer-detector'
|
|
23
17
|
import editorsConfig from './editors-config'
|
|
24
18
|
import { VERSION } from '../utils/version'
|
|
25
19
|
|
|
@@ -30,13 +24,10 @@ const DIM = '\x1b[2m'
|
|
|
30
24
|
const NC = '\x1b[0m'
|
|
31
25
|
|
|
32
26
|
interface SetupResults {
|
|
33
|
-
legacyCleaned: boolean
|
|
34
|
-
legacyProjectsMigrated: number
|
|
35
27
|
claudeInstalled: boolean
|
|
36
28
|
commandsAdded: number
|
|
37
29
|
commandsUpdated: number
|
|
38
30
|
configAction: string | null
|
|
39
|
-
projectsMigrated: number
|
|
40
31
|
}
|
|
41
32
|
|
|
42
33
|
/**
|
|
@@ -76,21 +67,10 @@ async function installClaudeCode(): Promise<boolean> {
|
|
|
76
67
|
*/
|
|
77
68
|
export async function run(): Promise<SetupResults> {
|
|
78
69
|
const results: SetupResults = {
|
|
79
|
-
legacyCleaned: false,
|
|
80
|
-
legacyProjectsMigrated: 0,
|
|
81
70
|
claudeInstalled: false,
|
|
82
71
|
commandsAdded: 0,
|
|
83
72
|
commandsUpdated: 0,
|
|
84
73
|
configAction: null,
|
|
85
|
-
projectsMigrated: 0,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Step 0: Detect and clean legacy curl installation
|
|
89
|
-
const needsLegacyCleanup = await legacyDetector.needsCleanup()
|
|
90
|
-
if (needsLegacyCleanup) {
|
|
91
|
-
const cleanupResult = await legacyDetector.performCleanup({ verbose: true })
|
|
92
|
-
results.legacyCleaned = cleanupResult.success
|
|
93
|
-
results.legacyProjectsMigrated = cleanupResult.steps.projectsMigrated
|
|
94
74
|
}
|
|
95
75
|
|
|
96
76
|
// Step 1: Ensure Claude Code CLI is installed
|
|
@@ -123,20 +103,9 @@ export async function run(): Promise<SetupResults> {
|
|
|
123
103
|
if (configResult.success) {
|
|
124
104
|
results.configAction = configResult.action
|
|
125
105
|
}
|
|
126
|
-
|
|
127
|
-
// Step 5: Migrate legacy projects automatically
|
|
128
|
-
const migrationResult = await migrator.migrateAll({
|
|
129
|
-
deepScan: false,
|
|
130
|
-
cleanupLegacy: true,
|
|
131
|
-
dryRun: false,
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
if (migrationResult.successfullyMigrated > 0) {
|
|
135
|
-
results.projectsMigrated = migrationResult.successfullyMigrated
|
|
136
|
-
}
|
|
137
106
|
}
|
|
138
107
|
|
|
139
|
-
// Step
|
|
108
|
+
// Step 5: Save version in editors-config
|
|
140
109
|
await editorsConfig.saveConfig(VERSION, installer.getInstallPath())
|
|
141
110
|
|
|
142
111
|
// Show results
|
|
@@ -154,16 +123,6 @@ export default { run }
|
|
|
154
123
|
function showResults(results: SetupResults): void {
|
|
155
124
|
console.log('')
|
|
156
125
|
|
|
157
|
-
// Show what was done
|
|
158
|
-
if (results.legacyCleaned) {
|
|
159
|
-
console.log(` ${GREEN}✓${NC} Legacy curl installation cleaned up`)
|
|
160
|
-
if (results.legacyProjectsMigrated > 0) {
|
|
161
|
-
console.log(
|
|
162
|
-
` ${GREEN}✓${NC} ${results.legacyProjectsMigrated} project(s) migrated from legacy`
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
126
|
if (results.claudeInstalled) {
|
|
168
127
|
console.log(` ${GREEN}✓${NC} Claude Code CLI installed`)
|
|
169
128
|
} else {
|
|
@@ -188,9 +147,5 @@ function showResults(results: SetupResults): void {
|
|
|
188
147
|
console.log(` ${GREEN}✓${NC} Global config merged`)
|
|
189
148
|
}
|
|
190
149
|
|
|
191
|
-
if (results.projectsMigrated > 0) {
|
|
192
|
-
console.log(` ${GREEN}✓${NC} ${results.projectsMigrated} projects migrated to global storage`)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
150
|
console.log('')
|
|
196
151
|
}
|
|
@@ -42,6 +42,10 @@ class IdeasStorage extends StorageManager<IdeasJson> {
|
|
|
42
42
|
return 'ideas.md'
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
protected getLayer(): string {
|
|
46
|
+
return 'planning'
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
protected getEventType(action: 'update' | 'create' | 'delete'): string {
|
|
46
50
|
return `ideas.${action}d`
|
|
47
51
|
}
|
|
@@ -39,6 +39,10 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
39
39
|
return 'shipped.md'
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
protected getLayer(): string {
|
|
43
|
+
return 'progress'
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
protected getEventType(action: 'update' | 'create' | 'delete'): string {
|
|
43
47
|
return `shipped.${action}d`
|
|
44
48
|
}
|
|
@@ -13,16 +13,46 @@ import fs from 'fs/promises'
|
|
|
13
13
|
import path from 'path'
|
|
14
14
|
import os from 'os'
|
|
15
15
|
import { eventBus, type SyncEvent } from '../events'
|
|
16
|
+
import pathManager from '../infrastructure/path-manager'
|
|
17
|
+
|
|
18
|
+
interface CacheEntry<T> {
|
|
19
|
+
data: T
|
|
20
|
+
timestamp: number
|
|
21
|
+
}
|
|
16
22
|
|
|
17
23
|
export abstract class StorageManager<T> {
|
|
18
24
|
protected filename: string
|
|
19
|
-
protected cache: Map<string, T
|
|
25
|
+
protected cache: Map<string, CacheEntry<T>> = new Map()
|
|
20
26
|
protected cacheTimeout = 5000 // 5 seconds
|
|
27
|
+
protected maxCacheSize = 50 // Max projects to cache
|
|
21
28
|
|
|
22
29
|
constructor(filename: string) {
|
|
23
30
|
this.filename = filename
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Check if cache entry is still valid
|
|
35
|
+
*/
|
|
36
|
+
private isCacheValid(entry: CacheEntry<T>): boolean {
|
|
37
|
+
return Date.now() - entry.timestamp < this.cacheTimeout
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Evict oldest entries if cache exceeds max size
|
|
42
|
+
*/
|
|
43
|
+
private evictOldEntries(): void {
|
|
44
|
+
if (this.cache.size <= this.maxCacheSize) return
|
|
45
|
+
|
|
46
|
+
// Sort by timestamp and remove oldest
|
|
47
|
+
const entries = Array.from(this.cache.entries())
|
|
48
|
+
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
|
49
|
+
|
|
50
|
+
const toRemove = entries.slice(0, this.cache.size - this.maxCacheSize)
|
|
51
|
+
for (const [key] of toRemove) {
|
|
52
|
+
this.cache.delete(key)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
/**
|
|
27
57
|
* Get file path for storage JSON
|
|
28
58
|
*/
|
|
@@ -38,17 +68,19 @@ export abstract class StorageManager<T> {
|
|
|
38
68
|
|
|
39
69
|
/**
|
|
40
70
|
* Get file path for context MD
|
|
71
|
+
* Uses layer-based paths to match MdBaseManager structure
|
|
41
72
|
*/
|
|
42
73
|
protected getContextPath(projectId: string, mdFilename: string): string {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'.prjct-cli/projects',
|
|
46
|
-
projectId,
|
|
47
|
-
'context',
|
|
48
|
-
mdFilename
|
|
49
|
-
)
|
|
74
|
+
const layer = this.getLayer()
|
|
75
|
+
return pathManager.getFilePath(projectId, layer, mdFilename)
|
|
50
76
|
}
|
|
51
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Get the layer for context MD files
|
|
80
|
+
* Override in subclasses: 'core' | 'planning' | 'progress'
|
|
81
|
+
*/
|
|
82
|
+
protected abstract getLayer(): string
|
|
83
|
+
|
|
52
84
|
/**
|
|
53
85
|
* Get default data structure
|
|
54
86
|
*/
|
|
@@ -73,10 +105,15 @@ export abstract class StorageManager<T> {
|
|
|
73
105
|
* Read data from storage
|
|
74
106
|
*/
|
|
75
107
|
async read(projectId: string): Promise<T> {
|
|
76
|
-
// Check cache first
|
|
108
|
+
// Check cache first (with expiration)
|
|
77
109
|
const cached = this.cache.get(projectId)
|
|
110
|
+
if (cached && this.isCacheValid(cached)) {
|
|
111
|
+
return cached.data
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Remove expired entry
|
|
78
115
|
if (cached) {
|
|
79
|
-
|
|
116
|
+
this.cache.delete(projectId)
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
const filePath = this.getStoragePath(projectId)
|
|
@@ -84,7 +121,8 @@ export abstract class StorageManager<T> {
|
|
|
84
121
|
try {
|
|
85
122
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
86
123
|
const data = JSON.parse(content) as T
|
|
87
|
-
this.cache.set(projectId, data)
|
|
124
|
+
this.cache.set(projectId, { data, timestamp: Date.now() })
|
|
125
|
+
this.evictOldEntries()
|
|
88
126
|
return data
|
|
89
127
|
} catch {
|
|
90
128
|
// Return default if file doesn't exist
|
|
@@ -112,8 +150,9 @@ export abstract class StorageManager<T> {
|
|
|
112
150
|
const md = this.toMarkdown(data)
|
|
113
151
|
await fs.writeFile(contextPath, md, 'utf-8')
|
|
114
152
|
|
|
115
|
-
// 3. Update cache
|
|
116
|
-
this.cache.set(projectId, data)
|
|
153
|
+
// 3. Update cache with timestamp
|
|
154
|
+
this.cache.set(projectId, { data, timestamp: Date.now() })
|
|
155
|
+
this.evictOldEntries()
|
|
117
156
|
|
|
118
157
|
// 4. Publish event for backend sync (NOT included in this call - subclass handles)
|
|
119
158
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Config - Manages API key storage for cloud sync
|
|
3
|
+
*
|
|
4
|
+
* Stores credentials in ~/.prjct-cli/config/auth.json
|
|
5
|
+
* Used by SyncClient to authenticate with prjct API
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import os from 'os'
|
|
10
|
+
import * as fileHelper from '../utils/file-helper'
|
|
11
|
+
|
|
12
|
+
export interface AuthConfig {
|
|
13
|
+
apiKey: string | null
|
|
14
|
+
apiUrl: string
|
|
15
|
+
userId: string | null
|
|
16
|
+
email: string | null
|
|
17
|
+
lastAuth: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_API_URL = 'https://api.prjct.app'
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CONFIG: AuthConfig = {
|
|
23
|
+
apiKey: null,
|
|
24
|
+
apiUrl: DEFAULT_API_URL,
|
|
25
|
+
userId: null,
|
|
26
|
+
email: null,
|
|
27
|
+
lastAuth: null,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class AuthConfigManager {
|
|
31
|
+
private configPath: string
|
|
32
|
+
private cachedConfig: AuthConfig | null = null
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
this.configPath = path.join(os.homedir(), '.prjct-cli', 'config', 'auth.json')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the auth config file path
|
|
40
|
+
*/
|
|
41
|
+
getConfigPath(): string {
|
|
42
|
+
return this.configPath
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read auth config from disk
|
|
47
|
+
*/
|
|
48
|
+
async read(): Promise<AuthConfig> {
|
|
49
|
+
if (this.cachedConfig) {
|
|
50
|
+
return this.cachedConfig
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const config = await fileHelper.readJson<AuthConfig>(this.configPath)
|
|
54
|
+
this.cachedConfig = config ?? { ...DEFAULT_CONFIG }
|
|
55
|
+
return this.cachedConfig
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Write auth config to disk
|
|
60
|
+
*/
|
|
61
|
+
async write(config: Partial<AuthConfig>): Promise<void> {
|
|
62
|
+
const current = await this.read()
|
|
63
|
+
const updated: AuthConfig = {
|
|
64
|
+
...current,
|
|
65
|
+
...config,
|
|
66
|
+
lastAuth: new Date().toISOString(),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await fileHelper.ensureDir(path.dirname(this.configPath))
|
|
70
|
+
await fileHelper.writeJson(this.configPath, updated)
|
|
71
|
+
this.cachedConfig = updated
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if user is authenticated (has valid API key)
|
|
76
|
+
*/
|
|
77
|
+
async hasAuth(): Promise<boolean> {
|
|
78
|
+
const config = await this.read()
|
|
79
|
+
return config.apiKey !== null && config.apiKey.length > 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the API key if available
|
|
84
|
+
*/
|
|
85
|
+
async getApiKey(): Promise<string | null> {
|
|
86
|
+
const config = await this.read()
|
|
87
|
+
return config.apiKey
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the API URL (allows override for dev/staging)
|
|
92
|
+
*/
|
|
93
|
+
async getApiUrl(): Promise<string> {
|
|
94
|
+
const config = await this.read()
|
|
95
|
+
return config.apiUrl || DEFAULT_API_URL
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Save API key and user info after successful auth
|
|
100
|
+
*/
|
|
101
|
+
async saveAuth(apiKey: string, userId: string, email: string): Promise<void> {
|
|
102
|
+
await this.write({
|
|
103
|
+
apiKey,
|
|
104
|
+
userId,
|
|
105
|
+
email,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear all auth data (logout)
|
|
111
|
+
*/
|
|
112
|
+
async clearAuth(): Promise<void> {
|
|
113
|
+
this.cachedConfig = { ...DEFAULT_CONFIG }
|
|
114
|
+
await fileHelper.writeJson(this.configPath, this.cachedConfig)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get auth status for display
|
|
119
|
+
*/
|
|
120
|
+
async getStatus(): Promise<{
|
|
121
|
+
authenticated: boolean
|
|
122
|
+
email: string | null
|
|
123
|
+
apiKeyPrefix: string | null
|
|
124
|
+
lastAuth: string | null
|
|
125
|
+
}> {
|
|
126
|
+
const config = await this.read()
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
authenticated: config.apiKey !== null,
|
|
130
|
+
email: config.email,
|
|
131
|
+
apiKeyPrefix: config.apiKey ? config.apiKey.substring(0, 12) + '...' : null,
|
|
132
|
+
lastAuth: config.lastAuth,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clear cache (useful for testing)
|
|
138
|
+
*/
|
|
139
|
+
clearCache(): void {
|
|
140
|
+
this.cachedConfig = null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const authConfig = new AuthConfigManager()
|
|
145
|
+
export default authConfig
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Module - Cloud synchronization for prjct-cli
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - AuthConfig: API key storage and management
|
|
6
|
+
* - SyncClient: HTTP client for prjct API
|
|
7
|
+
* - SyncManager: Orchestrates push/pull operations
|
|
8
|
+
* - OAuthHandler: Authentication flow management
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Auth
|
|
12
|
+
export { authConfig, type AuthConfig } from './auth-config'
|
|
13
|
+
|
|
14
|
+
// OAuth
|
|
15
|
+
export { oauthHandler, type AuthResult } from './oauth-handler'
|
|
16
|
+
|
|
17
|
+
// Client
|
|
18
|
+
export {
|
|
19
|
+
syncClient,
|
|
20
|
+
type SyncBatchResult,
|
|
21
|
+
type SyncPullResult,
|
|
22
|
+
type SyncStatus,
|
|
23
|
+
type SyncClientError,
|
|
24
|
+
} from './sync-client'
|
|
25
|
+
|
|
26
|
+
// Manager
|
|
27
|
+
export { syncManager, type SyncResult, type PushResult, type PullResult } from './sync-manager'
|
|
28
|
+
|
|
29
|
+
// Default export is the main sync manager
|
|
30
|
+
export { syncManager as default } from './sync-manager'
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Handler - Manages authentication flow for CLI
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. Simple: User copies API key from web dashboard
|
|
6
|
+
* 2. Browser (future): Full OAuth device flow
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { authConfig } from './auth-config'
|
|
10
|
+
import { syncClient } from './sync-client'
|
|
11
|
+
|
|
12
|
+
export interface AuthResult {
|
|
13
|
+
success: boolean
|
|
14
|
+
email?: string
|
|
15
|
+
error?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class OAuthHandler {
|
|
19
|
+
/**
|
|
20
|
+
* Start authentication flow
|
|
21
|
+
* Opens browser to dashboard where user can create/copy API key
|
|
22
|
+
*/
|
|
23
|
+
async startAuthFlow(): Promise<{ url: string }> {
|
|
24
|
+
const apiUrl = await authConfig.getApiUrl()
|
|
25
|
+
// Dashboard URL where user can get their API key
|
|
26
|
+
const dashboardUrl = apiUrl.replace('api.', 'app.')
|
|
27
|
+
const authUrl = `${dashboardUrl}/settings/api-keys`
|
|
28
|
+
|
|
29
|
+
return { url: authUrl }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Save API key after user provides it
|
|
34
|
+
*/
|
|
35
|
+
async saveApiKey(apiKey: string): Promise<AuthResult> {
|
|
36
|
+
// Validate format
|
|
37
|
+
if (!apiKey.startsWith('prjct_')) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: 'Invalid API key format. Keys start with "prjct_"',
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Save temporarily to test connection
|
|
45
|
+
await authConfig.write({ apiKey })
|
|
46
|
+
|
|
47
|
+
// Test the key by making a request
|
|
48
|
+
const isValid = await syncClient.testConnection()
|
|
49
|
+
|
|
50
|
+
if (!isValid) {
|
|
51
|
+
// Clear the invalid key
|
|
52
|
+
await authConfig.clearAuth()
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: 'API key is invalid or expired',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Key is valid - fetch user info
|
|
60
|
+
try {
|
|
61
|
+
const userInfo = await this.fetchUserInfo(apiKey)
|
|
62
|
+
|
|
63
|
+
await authConfig.saveAuth(apiKey, userInfo.id, userInfo.email)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
email: userInfo.email,
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Key works but couldn't fetch user info - still save it
|
|
71
|
+
await authConfig.write({ apiKey })
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fetch user info using the API key
|
|
80
|
+
*/
|
|
81
|
+
private async fetchUserInfo(
|
|
82
|
+
apiKey: string
|
|
83
|
+
): Promise<{ id: string; email: string; name: string }> {
|
|
84
|
+
const apiUrl = await authConfig.getApiUrl()
|
|
85
|
+
|
|
86
|
+
const response = await fetch(`${apiUrl}/auth/me`, {
|
|
87
|
+
headers: {
|
|
88
|
+
'X-Api-Key': apiKey,
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error('Failed to fetch user info')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const data = (await response.json()) as {
|
|
97
|
+
user: { id: string; email: string; name: string }
|
|
98
|
+
}
|
|
99
|
+
return data.user
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if currently authenticated
|
|
104
|
+
*/
|
|
105
|
+
async isAuthenticated(): Promise<boolean> {
|
|
106
|
+
return await authConfig.hasAuth()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get current auth status
|
|
111
|
+
*/
|
|
112
|
+
async getStatus(): Promise<{
|
|
113
|
+
authenticated: boolean
|
|
114
|
+
email: string | null
|
|
115
|
+
apiKeyPrefix: string | null
|
|
116
|
+
}> {
|
|
117
|
+
return await authConfig.getStatus()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Logout - clear all auth data
|
|
122
|
+
*/
|
|
123
|
+
async logout(): Promise<void> {
|
|
124
|
+
await authConfig.clearAuth()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Open URL in default browser
|
|
129
|
+
*/
|
|
130
|
+
async openBrowser(url: string): Promise<void> {
|
|
131
|
+
const { exec } = await import('child_process')
|
|
132
|
+
const { promisify } = await import('util')
|
|
133
|
+
const execAsync = promisify(exec)
|
|
134
|
+
|
|
135
|
+
const platform = process.platform
|
|
136
|
+
const command =
|
|
137
|
+
platform === 'darwin'
|
|
138
|
+
? `open "${url}"`
|
|
139
|
+
: platform === 'win32'
|
|
140
|
+
? `start "${url}"`
|
|
141
|
+
: `xdg-open "${url}"`
|
|
142
|
+
|
|
143
|
+
await execAsync(command)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const oauthHandler = new OAuthHandler()
|
|
148
|
+
export default oauthHandler
|