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.
Files changed (33) hide show
  1. package/CHANGELOG.md +123 -1
  2. package/bin/prjct.ts +23 -14
  3. package/core/__tests__/agentic/command-executor.test.ts +19 -19
  4. package/core/__tests__/agentic/prompt-builder.test.ts +16 -16
  5. package/core/__tests__/ai-tools/formatters.test.ts +118 -0
  6. package/core/agentic/command-executor.ts +18 -17
  7. package/core/agentic/prompt-builder.ts +18 -17
  8. package/core/agentic/template-executor.ts +2 -2
  9. package/core/ai-tools/formatters.ts +18 -0
  10. package/core/ai-tools/registry.ts +17 -14
  11. package/core/cli/start.ts +18 -17
  12. package/core/commands/analysis.ts +1 -1
  13. package/core/commands/setup.ts +8 -8
  14. package/core/commands/uninstall.ts +11 -11
  15. package/core/index.ts +103 -21
  16. package/core/infrastructure/agent-detector.ts +8 -8
  17. package/core/infrastructure/ai-provider.ts +49 -37
  18. package/core/infrastructure/command-installer.ts +18 -10
  19. package/core/infrastructure/path-manager.ts +4 -4
  20. package/core/infrastructure/setup.ts +124 -119
  21. package/core/infrastructure/update-checker.ts +14 -13
  22. package/core/integrations/linear/sync.ts +4 -4
  23. package/core/services/context-generator.ts +12 -3
  24. package/core/services/hooks-service.ts +78 -68
  25. package/core/services/sync-service.ts +64 -6
  26. package/core/utils/citations.ts +53 -0
  27. package/core/utils/error-messages.ts +11 -0
  28. package/core/utils/fs-helpers.ts +14 -0
  29. package/core/utils/project-credentials.ts +8 -7
  30. package/dist/bin/prjct.mjs +854 -643
  31. package/dist/core/infrastructure/command-installer.js +118 -87
  32. package/dist/core/infrastructure/setup.js +246 -210
  33. 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.readFileSync(packageJsonPath, 'utf8'))
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 (fs.existsSync(this.cacheFile)) {
125
- const cache = JSON.parse(fs.readFileSync(this.cacheFile, 'utf8'))
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 (!fs.existsSync(this.cacheDir)) {
141
- fs.mkdirSync(this.cacheDir, { recursive: true })
141
+ if (!(await fileExists(this.cacheDir))) {
142
+ await fs.mkdir(this.cacheDir, { recursive: true })
142
143
  }
143
144
 
144
- fs.writeFileSync(this.cacheFile, JSON.stringify(data, null, 2), 'utf8')
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 (!existsSync(storagePath)) {
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 (!existsSync(issuesPath)) {
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 (!existsSync(storagePath)) {
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
- fs.existsSync(path.join(projectPath, 'lefthook.yml')) ||
138
- fs.existsSync(path.join(projectPath, 'lefthook.yaml'))
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
- fs.existsSync(path.join(projectPath, '.husky')) ||
146
- fs.existsSync(path.join(projectPath, '.husky', '_'))
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 (fs.existsSync(path.join(projectPath, '.git'))) {
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 = fs.existsSync(path.join(projectPath, 'lefthook.yml'))
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.readFileSync(configPath, 'utf-8')
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.writeFileSync(configPath, content, 'utf-8')
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 (fs.existsSync(hookPath)) {
230
+ if (await fileExists(hookPath)) {
230
231
  // Append to existing hook if not already present
231
- const existing = fs.readFileSync(hookPath, 'utf-8')
232
+ const existing = await fs.readFile(hookPath, 'utf-8')
232
233
  if (existing.includes('prjct sync')) {
233
234
  continue
234
235
  }
235
- fs.appendFileSync(hookPath, '\n# prjct auto-sync\nprjct sync --quiet --yes &\n')
236
+ await fs.appendFile(hookPath, '\n# prjct auto-sync\nprjct sync --quiet --yes &\n')
236
237
  } else {
237
- fs.writeFileSync(hookPath, script, { mode: 0o755 })
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 (!fs.existsSync(hooksDir)) {
251
- fs.mkdirSync(hooksDir, { recursive: true })
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 (fs.existsSync(hookPath)) {
259
- const existing = fs.readFileSync(hookPath, 'utf-8')
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.appendFileSync(hookPath, `\n# prjct auto-sync\n${script.split('\n').slice(1).join('\n')}`)
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.writeFileSync(hookPath, script, { mode: 0o755 })
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 = fs.existsSync(path.join(projectPath, 'lefthook.yml'))
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 (!fs.existsSync(configPath)) return false
287
+ if (!(await fileExists(configPath))) return false
284
288
 
285
- let content = fs.readFileSync(configPath, 'utf-8')
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.writeFileSync(configPath, `${content.trimEnd()}\n`, 'utf-8')
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 (!fs.existsSync(hookPath)) continue
306
+ if (!(await fileExists(hookPath))) continue
303
307
 
304
- const content = fs.readFileSync(hookPath, 'utf-8')
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.unlinkSync(hookPath)
319
+ await fs.unlink(hookPath)
316
320
  } else {
317
- fs.writeFileSync(hookPath, cleaned, { mode: 0o755 })
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 (!fs.existsSync(hookPath)) continue
333
+ if (!(await fileExists(hookPath))) continue
330
334
 
331
- const content = fs.readFileSync(hookPath, 'utf-8')
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.unlinkSync(hookPath)
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.writeFileSync(hookPath, cleaned, { mode: 0o755 })
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 = hookNames.map((name) => ({
465
- name,
466
- installed: this.isHookInstalled(projectPath, name, config?.strategy || null),
467
- path: this.getHookPath(projectPath, name, config?.strategy || null),
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 = fs.existsSync(path.join(projectPath, 'lefthook.yml'))
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 (!fs.existsSync(configPath)) return false
603
- const content = fs.readFileSync(configPath, 'utf-8')
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 (!fs.existsSync(hookPath)) return false
610
- return fs.readFileSync(hookPath, 'utf-8').includes('prjct sync')
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 (!fs.existsSync(hookPath)) return false
616
- return fs.readFileSync(hookPath, 'utf-8').includes('prjct sync')
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(projectPath: string, hook: HookName, strategy: HookStrategy | null): string {
625
+ private async getHookPath(
626
+ projectPath: string,
627
+ hook: HookName,
628
+ strategy: HookStrategy | null
629
+ ): Promise<string> {
620
630
  if (strategy === 'lefthook') {
621
- return fs.existsSync(path.join(projectPath, 'lefthook.yml'))
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 (!fs.existsSync(projectJsonPath)) return null
644
- const project = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'))
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 (!fs.existsSync(projectJsonPath)) return
673
+ if (!(await fileExists(projectJsonPath))) return
664
674
 
665
- const project = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'))
675
+ const project = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
666
676
  project.hooks = config
667
- fs.writeFileSync(projectJsonPath, JSON.stringify(project, null, 2))
677
+ await fs.writeFile(projectJsonPath, JSON.stringify(project, null, 2))
668
678
  } catch {
669
679
  // Non-fatal
670
680
  }