prjct-cli 1.4.0 → 1.5.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/CHANGELOG.md +123 -1
- package/bin/prjct.ts +23 -14
- package/core/__tests__/agentic/command-executor.test.ts +19 -19
- package/core/__tests__/agentic/prompt-builder.test.ts +16 -16
- package/core/__tests__/ai-tools/formatters.test.ts +118 -0
- package/core/agentic/command-executor.ts +18 -17
- package/core/agentic/prompt-builder.ts +18 -17
- package/core/agentic/template-executor.ts +2 -2
- package/core/ai-tools/formatters.ts +18 -0
- package/core/ai-tools/registry.ts +17 -14
- package/core/cli/start.ts +18 -17
- package/core/commands/analysis.ts +1 -1
- package/core/commands/setup.ts +8 -8
- package/core/commands/uninstall.ts +11 -11
- package/core/index.ts +103 -21
- package/core/infrastructure/agent-detector.ts +8 -8
- package/core/infrastructure/ai-provider.ts +49 -37
- package/core/infrastructure/command-installer.ts +18 -10
- package/core/infrastructure/path-manager.ts +4 -4
- package/core/infrastructure/setup.ts +124 -119
- package/core/infrastructure/update-checker.ts +14 -13
- package/core/integrations/linear/sync.ts +4 -4
- package/core/services/context-generator.ts +12 -3
- package/core/services/hooks-service.ts +78 -68
- package/core/services/sync-service.ts +64 -6
- package/core/utils/citations.ts +53 -0
- package/core/utils/error-messages.ts +11 -0
- package/core/utils/fs-helpers.ts +14 -0
- package/core/utils/project-credentials.ts +8 -7
- package/dist/bin/prjct.mjs +854 -643
- package/dist/core/infrastructure/command-installer.js +118 -87
- package/dist/core/infrastructure/setup.js +246 -210
- package/package.json +1 -1
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* @version 0.5.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import fs from 'node:fs'
|
|
7
|
+
import fs from 'node:fs/promises'
|
|
8
8
|
import https from 'node:https'
|
|
9
9
|
import os from 'node:os'
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
import chalk from 'chalk'
|
|
12
|
+
import { fileExists } from '../utils/fs-helpers'
|
|
12
13
|
|
|
13
14
|
interface UpdateCache {
|
|
14
15
|
lastCheck: number
|
|
@@ -37,10 +38,10 @@ class UpdateChecker {
|
|
|
37
38
|
/**
|
|
38
39
|
* Get current installed version from package.json
|
|
39
40
|
*/
|
|
40
|
-
getCurrentVersion(): string | null {
|
|
41
|
+
async getCurrentVersion(): Promise<string | null> {
|
|
41
42
|
try {
|
|
42
43
|
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json')
|
|
43
|
-
const packageJson = JSON.parse(fs.
|
|
44
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
|
|
44
45
|
return packageJson.version
|
|
45
46
|
} catch (error) {
|
|
46
47
|
console.error('Error reading package version:', (error as Error).message)
|
|
@@ -119,10 +120,10 @@ class UpdateChecker {
|
|
|
119
120
|
/**
|
|
120
121
|
* Read cache file
|
|
121
122
|
*/
|
|
122
|
-
readCache(): UpdateCache | null {
|
|
123
|
+
async readCache(): Promise<UpdateCache | null> {
|
|
123
124
|
try {
|
|
124
|
-
if (
|
|
125
|
-
const cache = JSON.parse(fs.
|
|
125
|
+
if (await fileExists(this.cacheFile)) {
|
|
126
|
+
const cache = JSON.parse(await fs.readFile(this.cacheFile, 'utf8'))
|
|
126
127
|
return cache
|
|
127
128
|
}
|
|
128
129
|
} catch (_error) {
|
|
@@ -134,14 +135,14 @@ class UpdateChecker {
|
|
|
134
135
|
/**
|
|
135
136
|
* Write cache file
|
|
136
137
|
*/
|
|
137
|
-
writeCache(data: UpdateCache): void {
|
|
138
|
+
async writeCache(data: UpdateCache): Promise<void> {
|
|
138
139
|
try {
|
|
139
140
|
// Ensure cache directory exists
|
|
140
|
-
if (!
|
|
141
|
-
fs.
|
|
141
|
+
if (!(await fileExists(this.cacheDir))) {
|
|
142
|
+
await fs.mkdir(this.cacheDir, { recursive: true })
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
fs.
|
|
145
|
+
await fs.writeFile(this.cacheFile, JSON.stringify(data, null, 2), 'utf8')
|
|
145
146
|
} catch (_error) {
|
|
146
147
|
// Fail silently - cache is not critical
|
|
147
148
|
}
|
|
@@ -153,13 +154,13 @@ class UpdateChecker {
|
|
|
153
154
|
*/
|
|
154
155
|
async checkForUpdates(): Promise<UpdateResult | null> {
|
|
155
156
|
try {
|
|
156
|
-
const currentVersion = this.getCurrentVersion()
|
|
157
|
+
const currentVersion = await this.getCurrentVersion()
|
|
157
158
|
if (!currentVersion) {
|
|
158
159
|
return null
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
// Check cache first
|
|
162
|
-
const cache = this.readCache()
|
|
163
|
+
const cache = await this.readCache()
|
|
163
164
|
const now = Date.now()
|
|
164
165
|
|
|
165
166
|
if (cache?.lastCheck && now - cache.lastCheck < this.checkInterval) {
|
|
@@ -182,7 +183,7 @@ class UpdateChecker {
|
|
|
182
183
|
const latestVersion = await this.getLatestVersion()
|
|
183
184
|
|
|
184
185
|
// Update cache
|
|
185
|
-
this.writeCache({
|
|
186
|
+
await this.writeCache({
|
|
186
187
|
lastCheck: now,
|
|
187
188
|
latestVersion,
|
|
188
189
|
})
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* state.json.currentTask.linearId ← DIRECT LINK
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { existsSync } from 'node:fs'
|
|
18
17
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
19
18
|
import { join } from 'node:path'
|
|
20
19
|
import {
|
|
@@ -25,6 +24,7 @@ import {
|
|
|
25
24
|
type SyncResult,
|
|
26
25
|
} from '../../schemas/issues'
|
|
27
26
|
import { getProjectPath } from '../../schemas/schemas'
|
|
27
|
+
import { fileExists } from '../../utils/fs-helpers'
|
|
28
28
|
import type { Issue } from '../issue-tracker/types'
|
|
29
29
|
import { linearService } from './service'
|
|
30
30
|
|
|
@@ -41,7 +41,7 @@ export class LinearSync {
|
|
|
41
41
|
const issuesPath = join(storagePath, 'issues.json')
|
|
42
42
|
|
|
43
43
|
// Ensure storage directory exists
|
|
44
|
-
if (!
|
|
44
|
+
if (!(await fileExists(storagePath))) {
|
|
45
45
|
await mkdir(storagePath, { recursive: true })
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -241,7 +241,7 @@ export class LinearSync {
|
|
|
241
241
|
private async loadIssues(projectId: string): Promise<IssuesJson | null> {
|
|
242
242
|
const issuesPath = join(getProjectPath(projectId), 'storage', 'issues.json')
|
|
243
243
|
|
|
244
|
-
if (!
|
|
244
|
+
if (!(await fileExists(issuesPath))) {
|
|
245
245
|
return null
|
|
246
246
|
}
|
|
247
247
|
|
|
@@ -260,7 +260,7 @@ export class LinearSync {
|
|
|
260
260
|
const storagePath = join(getProjectPath(projectId), 'storage')
|
|
261
261
|
const issuesPath = join(storagePath, 'issues.json')
|
|
262
262
|
|
|
263
|
-
if (!
|
|
263
|
+
if (!(await fileExists(storagePath))) {
|
|
264
264
|
await mkdir(storagePath, { recursive: true })
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs/promises'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import pathManager from '../infrastructure/path-manager'
|
|
15
|
+
import { type ContextSources, cite, defaultSources } from '../utils/citations'
|
|
15
16
|
import dateHelper from '../utils/date-helper'
|
|
16
17
|
import { mergePreservedSections, validatePreserveBlocks } from '../utils/preserve-sections'
|
|
17
18
|
import { NestedContextResolver } from './nested-context-resolver'
|
|
@@ -103,13 +104,14 @@ export class ContextFileGenerator {
|
|
|
103
104
|
git: GitData,
|
|
104
105
|
stats: ProjectStats,
|
|
105
106
|
commands: Commands,
|
|
106
|
-
agents: AgentInfo[]
|
|
107
|
+
agents: AgentInfo[],
|
|
108
|
+
sources?: ContextSources
|
|
107
109
|
): Promise<string[]> {
|
|
108
110
|
const contextPath = path.join(this.config.globalPath, 'context')
|
|
109
111
|
|
|
110
112
|
// Generate all context files IN PARALLEL
|
|
111
113
|
await Promise.all([
|
|
112
|
-
this.generateClaudeMd(contextPath, git, stats, commands, agents),
|
|
114
|
+
this.generateClaudeMd(contextPath, git, stats, commands, agents, sources),
|
|
113
115
|
this.generateNowMd(contextPath),
|
|
114
116
|
this.generateNextMd(contextPath),
|
|
115
117
|
this.generateIdeasMd(contextPath),
|
|
@@ -137,10 +139,12 @@ export class ContextFileGenerator {
|
|
|
137
139
|
git: GitData,
|
|
138
140
|
stats: ProjectStats,
|
|
139
141
|
commands: Commands,
|
|
140
|
-
agents: AgentInfo[]
|
|
142
|
+
agents: AgentInfo[],
|
|
143
|
+
sources?: ContextSources
|
|
141
144
|
): Promise<void> {
|
|
142
145
|
const workflowAgents = agents.filter((a) => a.type === 'workflow').map((a) => a.name)
|
|
143
146
|
const domainAgents = agents.filter((a) => a.type === 'domain').map((a) => a.name)
|
|
147
|
+
const s = sources || defaultSources()
|
|
144
148
|
|
|
145
149
|
const content = `# ${stats.name} - Project Rules
|
|
146
150
|
<!-- projectId: ${this.config.projectId} -->
|
|
@@ -149,11 +153,13 @@ export class ContextFileGenerator {
|
|
|
149
153
|
|
|
150
154
|
## THIS PROJECT (${stats.ecosystem})
|
|
151
155
|
|
|
156
|
+
${cite(s.ecosystem)}
|
|
152
157
|
**Type:** ${stats.projectType}
|
|
153
158
|
**Path:** ${this.config.projectPath}
|
|
154
159
|
|
|
155
160
|
### Commands (USE THESE, NOT OTHERS)
|
|
156
161
|
|
|
162
|
+
${cite(s.commands)}
|
|
157
163
|
| Action | Command |
|
|
158
164
|
|--------|---------|
|
|
159
165
|
| Install dependencies | \`${commands.install}\` |
|
|
@@ -165,7 +171,9 @@ export class ContextFileGenerator {
|
|
|
165
171
|
|
|
166
172
|
### Code Conventions
|
|
167
173
|
|
|
174
|
+
${cite(s.languages)}
|
|
168
175
|
- **Languages**: ${stats.languages.join(', ') || 'Not detected'}
|
|
176
|
+
${cite(s.frameworks)}
|
|
169
177
|
- **Frameworks**: ${stats.frameworks.join(', ') || 'Not detected'}
|
|
170
178
|
|
|
171
179
|
---
|
|
@@ -193,6 +201,7 @@ p. sync → p. task "desc" → [work] → p. done → p. ship
|
|
|
193
201
|
|
|
194
202
|
## PROJECT STATE
|
|
195
203
|
|
|
204
|
+
${cite(s.name)}
|
|
196
205
|
| Field | Value |
|
|
197
206
|
|-------|-------|
|
|
198
207
|
| Name | ${stats.name} |
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* @module services/hooks-service
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import fs from 'node:fs'
|
|
14
|
+
import fs from 'node:fs/promises'
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import chalk from 'chalk'
|
|
17
17
|
import configManager from '../infrastructure/config-manager'
|
|
18
|
+
import { fileExists } from '../utils/fs-helpers'
|
|
18
19
|
import out from '../utils/output'
|
|
19
20
|
|
|
20
21
|
// ============================================================================
|
|
@@ -129,27 +130,27 @@ exit 0
|
|
|
129
130
|
/**
|
|
130
131
|
* Detect which hook managers are available in the project
|
|
131
132
|
*/
|
|
132
|
-
function detectHookManagers(projectPath: string): HookStrategy[] {
|
|
133
|
+
async function detectHookManagers(projectPath: string): Promise<HookStrategy[]> {
|
|
133
134
|
const detected: HookStrategy[] = []
|
|
134
135
|
|
|
135
136
|
// Check for lefthook
|
|
136
137
|
if (
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
(await fileExists(path.join(projectPath, 'lefthook.yml'))) ||
|
|
139
|
+
(await fileExists(path.join(projectPath, 'lefthook.yaml')))
|
|
139
140
|
) {
|
|
140
141
|
detected.push('lefthook')
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
// Check for husky
|
|
144
145
|
if (
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
(await fileExists(path.join(projectPath, '.husky'))) ||
|
|
147
|
+
(await fileExists(path.join(projectPath, '.husky', '_')))
|
|
147
148
|
) {
|
|
148
149
|
detected.push('husky')
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
// Direct .git/hooks is always available if it's a git repo
|
|
152
|
-
if (
|
|
153
|
+
if (await fileExists(path.join(projectPath, '.git'))) {
|
|
153
154
|
detected.push('direct')
|
|
154
155
|
}
|
|
155
156
|
|
|
@@ -173,13 +174,13 @@ function selectStrategy(detected: HookStrategy[]): HookStrategy {
|
|
|
173
174
|
/**
|
|
174
175
|
* Install hooks via lefthook (append to existing config)
|
|
175
176
|
*/
|
|
176
|
-
function installLefthook(projectPath: string, hooks: HookName[]): boolean {
|
|
177
|
-
const configFile =
|
|
177
|
+
async function installLefthook(projectPath: string, hooks: HookName[]): Promise<boolean> {
|
|
178
|
+
const configFile = (await fileExists(path.join(projectPath, 'lefthook.yml')))
|
|
178
179
|
? 'lefthook.yml'
|
|
179
180
|
: 'lefthook.yaml'
|
|
180
181
|
const configPath = path.join(projectPath, configFile)
|
|
181
182
|
|
|
182
|
-
let content = fs.
|
|
183
|
+
let content = await fs.readFile(configPath, 'utf-8')
|
|
183
184
|
|
|
184
185
|
for (const hook of hooks) {
|
|
185
186
|
const sectionName = hook // e.g. "post-commit"
|
|
@@ -212,29 +213,29 @@ ${sectionName}:
|
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
fs.
|
|
216
|
+
await fs.writeFile(configPath, content, 'utf-8')
|
|
216
217
|
return true
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
/**
|
|
220
221
|
* Install hooks via husky
|
|
221
222
|
*/
|
|
222
|
-
function installHusky(projectPath: string, hooks: HookName[]): boolean {
|
|
223
|
+
async function installHusky(projectPath: string, hooks: HookName[]): Promise<boolean> {
|
|
223
224
|
const huskyDir = path.join(projectPath, '.husky')
|
|
224
225
|
|
|
225
226
|
for (const hook of hooks) {
|
|
226
227
|
const hookPath = path.join(huskyDir, hook)
|
|
227
228
|
const script = hook === 'post-commit' ? getPostCommitScript() : getPostCheckoutScript()
|
|
228
229
|
|
|
229
|
-
if (
|
|
230
|
+
if (await fileExists(hookPath)) {
|
|
230
231
|
// Append to existing hook if not already present
|
|
231
|
-
const existing = fs.
|
|
232
|
+
const existing = await fs.readFile(hookPath, 'utf-8')
|
|
232
233
|
if (existing.includes('prjct sync')) {
|
|
233
234
|
continue
|
|
234
235
|
}
|
|
235
|
-
fs.
|
|
236
|
+
await fs.appendFile(hookPath, '\n# prjct auto-sync\nprjct sync --quiet --yes &\n')
|
|
236
237
|
} else {
|
|
237
|
-
fs.
|
|
238
|
+
await fs.writeFile(hookPath, script, { mode: 0o755 })
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
|
|
@@ -244,26 +245,29 @@ function installHusky(projectPath: string, hooks: HookName[]): boolean {
|
|
|
244
245
|
/**
|
|
245
246
|
* Install hooks directly into .git/hooks/
|
|
246
247
|
*/
|
|
247
|
-
function installDirect(projectPath: string, hooks: HookName[]): boolean {
|
|
248
|
+
async function installDirect(projectPath: string, hooks: HookName[]): Promise<boolean> {
|
|
248
249
|
const hooksDir = path.join(projectPath, '.git', 'hooks')
|
|
249
250
|
|
|
250
|
-
if (!
|
|
251
|
-
fs.
|
|
251
|
+
if (!(await fileExists(hooksDir))) {
|
|
252
|
+
await fs.mkdir(hooksDir, { recursive: true })
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
for (const hook of hooks) {
|
|
255
256
|
const hookPath = path.join(hooksDir, hook)
|
|
256
257
|
const script = hook === 'post-commit' ? getPostCommitScript() : getPostCheckoutScript()
|
|
257
258
|
|
|
258
|
-
if (
|
|
259
|
-
const existing = fs.
|
|
259
|
+
if (await fileExists(hookPath)) {
|
|
260
|
+
const existing = await fs.readFile(hookPath, 'utf-8')
|
|
260
261
|
if (existing.includes('prjct sync')) {
|
|
261
262
|
continue // Already installed
|
|
262
263
|
}
|
|
263
264
|
// Append to existing hook
|
|
264
|
-
fs.
|
|
265
|
+
await fs.appendFile(
|
|
266
|
+
hookPath,
|
|
267
|
+
`\n# prjct auto-sync\n${script.split('\n').slice(1).join('\n')}`
|
|
268
|
+
)
|
|
265
269
|
} else {
|
|
266
|
-
fs.
|
|
270
|
+
await fs.writeFile(hookPath, script, { mode: 0o755 })
|
|
267
271
|
}
|
|
268
272
|
}
|
|
269
273
|
|
|
@@ -274,15 +278,15 @@ function installDirect(projectPath: string, hooks: HookName[]): boolean {
|
|
|
274
278
|
// UNINSTALL STRATEGIES
|
|
275
279
|
// ============================================================================
|
|
276
280
|
|
|
277
|
-
function uninstallLefthook(projectPath: string): boolean {
|
|
278
|
-
const configFile =
|
|
281
|
+
async function uninstallLefthook(projectPath: string): Promise<boolean> {
|
|
282
|
+
const configFile = (await fileExists(path.join(projectPath, 'lefthook.yml')))
|
|
279
283
|
? 'lefthook.yml'
|
|
280
284
|
: 'lefthook.yaml'
|
|
281
285
|
const configPath = path.join(projectPath, configFile)
|
|
282
286
|
|
|
283
|
-
if (!
|
|
287
|
+
if (!(await fileExists(configPath))) return false
|
|
284
288
|
|
|
285
|
-
let content = fs.
|
|
289
|
+
let content = await fs.readFile(configPath, 'utf-8')
|
|
286
290
|
|
|
287
291
|
// Remove prjct-sync commands
|
|
288
292
|
content = content.replace(/\s*prjct-sync-[\w-]+:[\s\S]*?(?=\n\S|\n*$)/g, '')
|
|
@@ -290,18 +294,18 @@ function uninstallLefthook(projectPath: string): boolean {
|
|
|
290
294
|
// Clean up empty sections
|
|
291
295
|
content = content.replace(/^(post-commit|post-checkout):\s*commands:\s*$/gm, '')
|
|
292
296
|
|
|
293
|
-
fs.
|
|
297
|
+
await fs.writeFile(configPath, `${content.trimEnd()}\n`, 'utf-8')
|
|
294
298
|
return true
|
|
295
299
|
}
|
|
296
300
|
|
|
297
|
-
function uninstallHusky(projectPath: string): boolean {
|
|
301
|
+
async function uninstallHusky(projectPath: string): Promise<boolean> {
|
|
298
302
|
const huskyDir = path.join(projectPath, '.husky')
|
|
299
303
|
|
|
300
304
|
for (const hook of ['post-commit', 'post-checkout'] as HookName[]) {
|
|
301
305
|
const hookPath = path.join(huskyDir, hook)
|
|
302
|
-
if (!
|
|
306
|
+
if (!(await fileExists(hookPath))) continue
|
|
303
307
|
|
|
304
|
-
const content = fs.
|
|
308
|
+
const content = await fs.readFile(hookPath, 'utf-8')
|
|
305
309
|
if (!content.includes('prjct sync')) continue
|
|
306
310
|
|
|
307
311
|
// Remove prjct lines
|
|
@@ -312,35 +316,35 @@ function uninstallHusky(projectPath: string): boolean {
|
|
|
312
316
|
|
|
313
317
|
if (cleaned.trim() === '#!/bin/sh' || cleaned.trim() === '#!/usr/bin/env sh') {
|
|
314
318
|
// Hook is now empty, remove it
|
|
315
|
-
fs.
|
|
319
|
+
await fs.unlink(hookPath)
|
|
316
320
|
} else {
|
|
317
|
-
fs.
|
|
321
|
+
await fs.writeFile(hookPath, cleaned, { mode: 0o755 })
|
|
318
322
|
}
|
|
319
323
|
}
|
|
320
324
|
|
|
321
325
|
return true
|
|
322
326
|
}
|
|
323
327
|
|
|
324
|
-
function uninstallDirect(projectPath: string): boolean {
|
|
328
|
+
async function uninstallDirect(projectPath: string): Promise<boolean> {
|
|
325
329
|
const hooksDir = path.join(projectPath, '.git', 'hooks')
|
|
326
330
|
|
|
327
331
|
for (const hook of ['post-commit', 'post-checkout'] as HookName[]) {
|
|
328
332
|
const hookPath = path.join(hooksDir, hook)
|
|
329
|
-
if (!
|
|
333
|
+
if (!(await fileExists(hookPath))) continue
|
|
330
334
|
|
|
331
|
-
const content = fs.
|
|
335
|
+
const content = await fs.readFile(hookPath, 'utf-8')
|
|
332
336
|
if (!content.includes('prjct sync')) continue
|
|
333
337
|
|
|
334
338
|
if (content.includes('Installed by: prjct hooks install')) {
|
|
335
339
|
// Entirely ours, remove it
|
|
336
|
-
fs.
|
|
340
|
+
await fs.unlink(hookPath)
|
|
337
341
|
} else {
|
|
338
342
|
// Shared hook, just remove our lines
|
|
339
343
|
const cleaned = content
|
|
340
344
|
.split('\n')
|
|
341
345
|
.filter((line) => !line.includes('prjct sync') && !line.includes('prjct auto-sync'))
|
|
342
346
|
.join('\n')
|
|
343
|
-
fs.
|
|
347
|
+
await fs.writeFile(hookPath, cleaned, { mode: 0o755 })
|
|
344
348
|
}
|
|
345
349
|
}
|
|
346
350
|
|
|
@@ -362,7 +366,7 @@ class HooksService {
|
|
|
362
366
|
const hooks: HookName[] = options.hooks || ['post-commit', 'post-checkout']
|
|
363
367
|
|
|
364
368
|
// Detect available managers
|
|
365
|
-
const detected = detectHookManagers(projectPath)
|
|
369
|
+
const detected = await detectHookManagers(projectPath)
|
|
366
370
|
|
|
367
371
|
if (detected.length === 0) {
|
|
368
372
|
return {
|
|
@@ -380,13 +384,13 @@ class HooksService {
|
|
|
380
384
|
|
|
381
385
|
switch (strategy) {
|
|
382
386
|
case 'lefthook':
|
|
383
|
-
success = installLefthook(projectPath, hooks)
|
|
387
|
+
success = await installLefthook(projectPath, hooks)
|
|
384
388
|
break
|
|
385
389
|
case 'husky':
|
|
386
|
-
success = installHusky(projectPath, hooks)
|
|
390
|
+
success = await installHusky(projectPath, hooks)
|
|
387
391
|
break
|
|
388
392
|
case 'direct':
|
|
389
|
-
success = installDirect(projectPath, hooks)
|
|
393
|
+
success = await installDirect(projectPath, hooks)
|
|
390
394
|
break
|
|
391
395
|
}
|
|
392
396
|
|
|
@@ -428,13 +432,13 @@ class HooksService {
|
|
|
428
432
|
|
|
429
433
|
switch (strategy) {
|
|
430
434
|
case 'lefthook':
|
|
431
|
-
success = uninstallLefthook(projectPath)
|
|
435
|
+
success = await uninstallLefthook(projectPath)
|
|
432
436
|
break
|
|
433
437
|
case 'husky':
|
|
434
|
-
success = uninstallHusky(projectPath)
|
|
438
|
+
success = await uninstallHusky(projectPath)
|
|
435
439
|
break
|
|
436
440
|
case 'direct':
|
|
437
|
-
success = uninstallDirect(projectPath)
|
|
441
|
+
success = await uninstallDirect(projectPath)
|
|
438
442
|
break
|
|
439
443
|
}
|
|
440
444
|
|
|
@@ -457,15 +461,17 @@ class HooksService {
|
|
|
457
461
|
* Get hook installation status
|
|
458
462
|
*/
|
|
459
463
|
async status(projectPath: string): Promise<HooksStatusResult> {
|
|
460
|
-
const detected = detectHookManagers(projectPath)
|
|
464
|
+
const detected = await detectHookManagers(projectPath)
|
|
461
465
|
const config = await this.getHookConfig(projectPath)
|
|
462
466
|
|
|
463
467
|
const hookNames: HookName[] = ['post-commit', 'post-checkout']
|
|
464
|
-
const hooks =
|
|
465
|
-
name
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
468
|
+
const hooks = await Promise.all(
|
|
469
|
+
hookNames.map(async (name) => ({
|
|
470
|
+
name,
|
|
471
|
+
installed: await this.isHookInstalled(projectPath, name, config?.strategy || null),
|
|
472
|
+
path: await this.getHookPath(projectPath, name, config?.strategy || null),
|
|
473
|
+
}))
|
|
474
|
+
)
|
|
469
475
|
|
|
470
476
|
return {
|
|
471
477
|
installed: hooks.some((h) => h.installed),
|
|
@@ -506,7 +512,7 @@ class HooksService {
|
|
|
506
512
|
out.start()
|
|
507
513
|
out.section('Git Hooks Installation')
|
|
508
514
|
|
|
509
|
-
const detected = detectHookManagers(projectPath)
|
|
515
|
+
const detected = await detectHookManagers(projectPath)
|
|
510
516
|
const strategy = selectStrategy(detected)
|
|
511
517
|
|
|
512
518
|
console.log(` Strategy: ${chalk.cyan(strategy)}`)
|
|
@@ -589,36 +595,40 @@ class HooksService {
|
|
|
589
595
|
// HELPERS
|
|
590
596
|
// ==========================================================================
|
|
591
597
|
|
|
592
|
-
private isHookInstalled(
|
|
598
|
+
private async isHookInstalled(
|
|
593
599
|
projectPath: string,
|
|
594
600
|
hook: HookName,
|
|
595
601
|
strategy: HookStrategy | null
|
|
596
|
-
): boolean {
|
|
602
|
+
): Promise<boolean> {
|
|
597
603
|
if (strategy === 'lefthook') {
|
|
598
|
-
const configFile =
|
|
604
|
+
const configFile = (await fileExists(path.join(projectPath, 'lefthook.yml')))
|
|
599
605
|
? 'lefthook.yml'
|
|
600
606
|
: 'lefthook.yaml'
|
|
601
607
|
const configPath = path.join(projectPath, configFile)
|
|
602
|
-
if (!
|
|
603
|
-
const content = fs.
|
|
608
|
+
if (!(await fileExists(configPath))) return false
|
|
609
|
+
const content = await fs.readFile(configPath, 'utf-8')
|
|
604
610
|
return content.includes(`prjct-sync-${hook}`)
|
|
605
611
|
}
|
|
606
612
|
|
|
607
613
|
if (strategy === 'husky') {
|
|
608
614
|
const hookPath = path.join(projectPath, '.husky', hook)
|
|
609
|
-
if (!
|
|
610
|
-
return fs.
|
|
615
|
+
if (!(await fileExists(hookPath))) return false
|
|
616
|
+
return (await fs.readFile(hookPath, 'utf-8')).includes('prjct sync')
|
|
611
617
|
}
|
|
612
618
|
|
|
613
619
|
// Direct
|
|
614
620
|
const hookPath = path.join(projectPath, '.git', 'hooks', hook)
|
|
615
|
-
if (!
|
|
616
|
-
return fs.
|
|
621
|
+
if (!(await fileExists(hookPath))) return false
|
|
622
|
+
return (await fs.readFile(hookPath, 'utf-8')).includes('prjct sync')
|
|
617
623
|
}
|
|
618
624
|
|
|
619
|
-
private getHookPath(
|
|
625
|
+
private async getHookPath(
|
|
626
|
+
projectPath: string,
|
|
627
|
+
hook: HookName,
|
|
628
|
+
strategy: HookStrategy | null
|
|
629
|
+
): Promise<string> {
|
|
620
630
|
if (strategy === 'lefthook') {
|
|
621
|
-
return
|
|
631
|
+
return (await fileExists(path.join(projectPath, 'lefthook.yml')))
|
|
622
632
|
? 'lefthook.yml'
|
|
623
633
|
: 'lefthook.yaml'
|
|
624
634
|
}
|
|
@@ -640,8 +650,8 @@ class HooksService {
|
|
|
640
650
|
projectId,
|
|
641
651
|
'project.json'
|
|
642
652
|
)
|
|
643
|
-
if (!
|
|
644
|
-
const project = JSON.parse(fs.
|
|
653
|
+
if (!(await fileExists(projectJsonPath))) return null
|
|
654
|
+
const project = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
|
|
645
655
|
return project.hooks || null
|
|
646
656
|
} catch {
|
|
647
657
|
return null
|
|
@@ -660,11 +670,11 @@ class HooksService {
|
|
|
660
670
|
projectId,
|
|
661
671
|
'project.json'
|
|
662
672
|
)
|
|
663
|
-
if (!
|
|
673
|
+
if (!(await fileExists(projectJsonPath))) return
|
|
664
674
|
|
|
665
|
-
const project = JSON.parse(fs.
|
|
675
|
+
const project = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
|
|
666
676
|
project.hooks = config
|
|
667
|
-
fs.
|
|
677
|
+
await fs.writeFile(projectJsonPath, JSON.stringify(project, null, 2))
|
|
668
678
|
} catch {
|
|
669
679
|
// Non-fatal
|
|
670
680
|
}
|