prjct-cli 0.55.3 → 0.56.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 +53 -0
- package/core/__tests__/utils/output.test.ts +119 -1
- package/core/cli/linear.ts +93 -34
- package/core/infrastructure/command-installer.ts +33 -3
- package/core/integrations/linear/client.ts +2 -4
- package/core/utils/output.ts +128 -3
- package/dist/bin/prjct.mjs +107 -5
- package/dist/core/infrastructure/command-installer.js +24 -0
- package/dist/core/infrastructure/setup.js +24 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.56.0] - 2026-02-05
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- implement output tiers for cleaner CLI output (#88)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- make lefthook hooks portable (no bun required) (#92)
|
|
12
|
+
- skip lefthook in CI environments (#91)
|
|
13
|
+
- use npm instead of bun in lefthook hooks (#90)
|
|
14
|
+
- add pre-commit hooks to prevent commits with lint errors (#89)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.55.6] - 2026-02-04
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Portable git hooks**: Removed `skip_in_ci` and test from pre-push hook - hooks now work everywhere with npm-only commands (lint, typecheck). Tests run in CI where bun is properly installed.
|
|
22
|
+
|
|
23
|
+
## [0.55.5] - 2026-02-05
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Pre-commit hooks**: Added lefthook for git hooks - blocks commits with lint/format errors
|
|
28
|
+
- **Pre-push hooks**: Runs typecheck and tests before push
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed 3 lint errors (import ordering, formatting)
|
|
33
|
+
|
|
34
|
+
## [0.55.4] - 2026-02-05
|
|
35
|
+
|
|
36
|
+
### Features
|
|
37
|
+
|
|
38
|
+
- **Output tiers**: New tiered output system (silent/minimal/compact/verbose) for cleaner CLI output
|
|
39
|
+
- **Human-friendly Linear output**: `p. linear list` now shows concise table instead of raw JSON
|
|
40
|
+
- **--json flag**: Use `--json` to get machine-parseable JSON output when needed
|
|
41
|
+
- **--verbose flag**: Use `--verbose` for full untruncated output
|
|
42
|
+
|
|
43
|
+
### Improved
|
|
44
|
+
|
|
45
|
+
- Removed noisy `[linear] Connected as...` messages from every API call
|
|
46
|
+
- Added `limitLines()` and `formatForHuman()` utilities for consistent output formatting
|
|
47
|
+
|
|
48
|
+
### Bug Fixes
|
|
49
|
+
|
|
50
|
+
- remove legacy p.*.md commands on sync
|
|
51
|
+
|
|
52
|
+
### Tests
|
|
53
|
+
|
|
54
|
+
- Added 11 new tests for output tier functionality
|
|
55
|
+
|
|
3
56
|
## [0.55.3] - 2026-02-05
|
|
4
57
|
|
|
5
58
|
### Bug Fixes
|
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'
|
|
7
|
-
import out
|
|
7
|
+
import out, {
|
|
8
|
+
formatForHuman,
|
|
9
|
+
getOutputTier,
|
|
10
|
+
getTierConfig,
|
|
11
|
+
limitLines,
|
|
12
|
+
OUTPUT_TIERS,
|
|
13
|
+
setOutputTier,
|
|
14
|
+
} from '../../utils/output'
|
|
8
15
|
|
|
9
16
|
describe('Output Module', () => {
|
|
10
17
|
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
@@ -151,4 +158,115 @@ describe('Output Module', () => {
|
|
|
151
158
|
expect(consoleLogSpy).toHaveBeenCalled()
|
|
152
159
|
})
|
|
153
160
|
})
|
|
161
|
+
|
|
162
|
+
describe('Output Tiers', () => {
|
|
163
|
+
afterEach(() => {
|
|
164
|
+
setOutputTier('compact') // Reset to default
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should have correct tier configs', () => {
|
|
168
|
+
expect(OUTPUT_TIERS.silent.maxLines).toBe(0)
|
|
169
|
+
expect(OUTPUT_TIERS.minimal.maxLines).toBe(1)
|
|
170
|
+
expect(OUTPUT_TIERS.compact.maxLines).toBe(4)
|
|
171
|
+
expect(OUTPUT_TIERS.verbose.maxLines).toBe(Infinity)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should get and set output tier', () => {
|
|
175
|
+
setOutputTier('verbose')
|
|
176
|
+
expect(getOutputTier()).toBe('verbose')
|
|
177
|
+
|
|
178
|
+
setOutputTier('minimal')
|
|
179
|
+
expect(getOutputTier()).toBe('minimal')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should return correct tier config', () => {
|
|
183
|
+
setOutputTier('compact')
|
|
184
|
+
const config = getTierConfig()
|
|
185
|
+
expect(config.maxLines).toBe(4)
|
|
186
|
+
expect(config.maxCharsPerLine).toBe(80)
|
|
187
|
+
expect(config.showMetrics).toBe(true)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('limitLines()', () => {
|
|
192
|
+
it('should limit content to maxLines', () => {
|
|
193
|
+
setOutputTier('compact') // maxLines = 4
|
|
194
|
+
const content = 'line1\nline2\nline3\nline4\nline5\nline6'
|
|
195
|
+
const result = limitLines(content)
|
|
196
|
+
|
|
197
|
+
expect(result.split('\n').length).toBe(5) // 4 lines + "...2 more lines"
|
|
198
|
+
expect(result).toContain('...2 more lines')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should not truncate if under limit', () => {
|
|
202
|
+
setOutputTier('compact')
|
|
203
|
+
const content = 'line1\nline2'
|
|
204
|
+
const result = limitLines(content)
|
|
205
|
+
|
|
206
|
+
expect(result).toBe(content)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should respect custom maxLines parameter', () => {
|
|
210
|
+
const content = 'line1\nline2\nline3\nline4'
|
|
211
|
+
const result = limitLines(content, 2)
|
|
212
|
+
|
|
213
|
+
expect(result.split('\n').length).toBe(3) // 2 lines + indicator
|
|
214
|
+
expect(result).toContain('...2 more lines')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should return content unchanged for verbose tier', () => {
|
|
218
|
+
setOutputTier('verbose')
|
|
219
|
+
const content = 'line1\nline2\nline3\nline4\nline5'
|
|
220
|
+
const result = limitLines(content)
|
|
221
|
+
|
|
222
|
+
expect(result).toBe(content)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe('formatForHuman()', () => {
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
setOutputTier('compact')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should format Linear issue object', () => {
|
|
232
|
+
const issue = {
|
|
233
|
+
identifier: 'PRJ-123',
|
|
234
|
+
title: 'Test issue title',
|
|
235
|
+
status: 'in_progress',
|
|
236
|
+
priority: 'high',
|
|
237
|
+
url: 'https://linear.app/test',
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = formatForHuman(issue)
|
|
241
|
+
expect(result).toContain('PRJ-123')
|
|
242
|
+
expect(result).toContain('Test issue title')
|
|
243
|
+
expect(result).toContain('in_progress')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should format issue list', () => {
|
|
247
|
+
const list = {
|
|
248
|
+
issues: [
|
|
249
|
+
{ identifier: 'PRJ-1', title: 'First issue', priority: 'high' },
|
|
250
|
+
{ identifier: 'PRJ-2', title: 'Second issue', priority: 'none' },
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const result = formatForHuman(list)
|
|
255
|
+
expect(result).toContain('PRJ-1')
|
|
256
|
+
expect(result).toContain('PRJ-2')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('should return empty string for silent tier', () => {
|
|
260
|
+
setOutputTier('silent')
|
|
261
|
+
const result = formatForHuman({ test: 'data' })
|
|
262
|
+
expect(result).toBe('')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should return full JSON for verbose tier', () => {
|
|
266
|
+
setOutputTier('verbose')
|
|
267
|
+
const data = { test: 'data', nested: { key: 'value' } }
|
|
268
|
+
const result = formatForHuman(data)
|
|
269
|
+
expect(result).toBe(JSON.stringify(data, null, 2))
|
|
270
|
+
})
|
|
271
|
+
})
|
|
154
272
|
})
|
package/core/cli/linear.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage: bun core/cli/linear.ts --project <projectId> <command> [args...]
|
|
6
6
|
*
|
|
7
|
+
* Flags:
|
|
8
|
+
* --project <id> - Project ID (required)
|
|
9
|
+
* --json - Output raw JSON (default: human-readable)
|
|
10
|
+
* --verbose - Show all details (no truncation)
|
|
11
|
+
*
|
|
7
12
|
* Commands:
|
|
8
13
|
* setup <apiKey> [teamId] - Store API key in project credentials
|
|
9
14
|
* list - List my assigned issues
|
|
@@ -21,11 +26,12 @@
|
|
|
21
26
|
* projects - List available projects
|
|
22
27
|
* status - Check connection status
|
|
23
28
|
*
|
|
24
|
-
*
|
|
29
|
+
* Default output is human-readable. Use --json for machine parsing.
|
|
25
30
|
*/
|
|
26
31
|
|
|
27
32
|
import type { CreateIssueInput, Issue } from '../integrations/issue-tracker/types'
|
|
28
33
|
import { linearService, linearSync } from '../integrations/linear'
|
|
34
|
+
import { formatForHuman, setOutputTier } from '../utils/output'
|
|
29
35
|
import {
|
|
30
36
|
getCredentialSource,
|
|
31
37
|
getLinearApiKey,
|
|
@@ -44,20 +50,41 @@ if (projectIdx !== -1 && args[projectIdx + 1]) {
|
|
|
44
50
|
args.splice(projectIdx, 2)
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
// Extract --json flag (raw JSON output)
|
|
54
|
+
const jsonIdx = args.indexOf('--json')
|
|
55
|
+
const jsonMode = jsonIdx !== -1
|
|
56
|
+
if (jsonMode) args.splice(jsonIdx, 1)
|
|
57
|
+
|
|
58
|
+
// Extract --verbose flag
|
|
59
|
+
const verboseIdx = args.indexOf('--verbose')
|
|
60
|
+
const verboseMode = verboseIdx !== -1
|
|
61
|
+
if (verboseMode) {
|
|
62
|
+
args.splice(verboseIdx, 1)
|
|
63
|
+
setOutputTier('verbose')
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
const [command, ...commandArgs] = args
|
|
48
67
|
|
|
49
68
|
/**
|
|
50
|
-
* Output result
|
|
69
|
+
* Output result - human-readable by default, JSON with --json flag
|
|
51
70
|
*/
|
|
52
71
|
function output(data: unknown): void {
|
|
53
|
-
|
|
72
|
+
if (jsonMode) {
|
|
73
|
+
console.log(JSON.stringify(data, null, 2))
|
|
74
|
+
} else {
|
|
75
|
+
console.log(formatForHuman(data))
|
|
76
|
+
}
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
/**
|
|
57
|
-
* Output error
|
|
80
|
+
* Output error and exit
|
|
58
81
|
*/
|
|
59
82
|
function error(message: string, code = 1): never {
|
|
60
|
-
|
|
83
|
+
if (jsonMode) {
|
|
84
|
+
console.error(JSON.stringify({ error: message }))
|
|
85
|
+
} else {
|
|
86
|
+
console.error(`Error: ${message}`)
|
|
87
|
+
}
|
|
61
88
|
process.exit(code)
|
|
62
89
|
}
|
|
63
90
|
|
|
@@ -148,17 +175,28 @@ async function main(): Promise<void> {
|
|
|
148
175
|
issues = await linearService.fetchAssignedIssues({ limit })
|
|
149
176
|
}
|
|
150
177
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
178
|
+
const issueList = issues.map((issue) => ({
|
|
179
|
+
id: issue.id,
|
|
180
|
+
identifier: issue.externalId,
|
|
181
|
+
title: issue.title,
|
|
182
|
+
status: issue.status,
|
|
183
|
+
priority: issue.priority,
|
|
184
|
+
url: issue.url,
|
|
185
|
+
}))
|
|
186
|
+
|
|
187
|
+
if (jsonMode) {
|
|
188
|
+
output({ count: issues.length, issues: issueList })
|
|
189
|
+
} else {
|
|
190
|
+
// Human-friendly table output
|
|
191
|
+
console.log(`Your issues (${issues.length}):`)
|
|
192
|
+
for (const issue of issueList.slice(0, 10)) {
|
|
193
|
+
const p = issue.priority && issue.priority !== 'none' ? ` [${issue.priority}]` : ''
|
|
194
|
+
console.log(` ${issue.identifier} ${issue.title.slice(0, 50)}${p}`)
|
|
195
|
+
}
|
|
196
|
+
if (issues.length > 10) {
|
|
197
|
+
console.log(` ...${issues.length - 10} more`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
162
200
|
break
|
|
163
201
|
}
|
|
164
202
|
|
|
@@ -199,7 +237,18 @@ async function main(): Promise<void> {
|
|
|
199
237
|
error(`Issue not found: ${id}`)
|
|
200
238
|
}
|
|
201
239
|
|
|
202
|
-
|
|
240
|
+
if (jsonMode) {
|
|
241
|
+
output(issue)
|
|
242
|
+
} else {
|
|
243
|
+
// Human-friendly issue display
|
|
244
|
+
console.log(`${issue.externalId}: ${issue.title}`)
|
|
245
|
+
console.log(`Status: ${issue.status} | Priority: ${issue.priority || 'none'}`)
|
|
246
|
+
if (issue.description) {
|
|
247
|
+
const desc = issue.description.slice(0, 200)
|
|
248
|
+
console.log(`\n${desc}${issue.description.length > 200 ? '...' : ''}`)
|
|
249
|
+
}
|
|
250
|
+
console.log(`\n${issue.url}`)
|
|
251
|
+
}
|
|
203
252
|
break
|
|
204
253
|
}
|
|
205
254
|
|
|
@@ -371,11 +420,12 @@ async function main(): Promise<void> {
|
|
|
371
420
|
const creds = await getProjectCredentials(projectId)
|
|
372
421
|
|
|
373
422
|
if (!apiKey) {
|
|
374
|
-
|
|
375
|
-
configured: false,
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
423
|
+
if (jsonMode) {
|
|
424
|
+
output({ configured: false, source: 'none', message: 'Linear not configured' })
|
|
425
|
+
} else {
|
|
426
|
+
console.log('Linear: Not configured')
|
|
427
|
+
console.log('Run: p. linear setup')
|
|
428
|
+
}
|
|
379
429
|
break
|
|
380
430
|
}
|
|
381
431
|
|
|
@@ -384,19 +434,28 @@ async function main(): Promise<void> {
|
|
|
384
434
|
await linearService.initializeFromApiKey(apiKey, creds.linear?.teamId)
|
|
385
435
|
const teams = await linearService.getTeams()
|
|
386
436
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
437
|
+
if (jsonMode) {
|
|
438
|
+
output({
|
|
439
|
+
configured: true,
|
|
440
|
+
source,
|
|
441
|
+
teamId: creds.linear?.teamId,
|
|
442
|
+
teamKey: creds.linear?.teamKey,
|
|
443
|
+
teamsAvailable: teams.length,
|
|
444
|
+
})
|
|
445
|
+
} else {
|
|
446
|
+
console.log(`Linear: Connected`)
|
|
447
|
+
if (creds.linear?.teamKey) {
|
|
448
|
+
console.log(`Team: ${creds.linear.teamKey}`)
|
|
449
|
+
}
|
|
450
|
+
console.log(`Teams: ${teams.length} available`)
|
|
451
|
+
}
|
|
394
452
|
} catch (err) {
|
|
395
|
-
|
|
396
|
-
configured: true,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
453
|
+
if (jsonMode) {
|
|
454
|
+
output({ configured: true, source, connectionError: (err as Error).message })
|
|
455
|
+
} else {
|
|
456
|
+
console.log(`Linear: Connection error`)
|
|
457
|
+
console.log(`Error: ${(err as Error).message}`)
|
|
458
|
+
}
|
|
400
459
|
}
|
|
401
460
|
break
|
|
402
461
|
}
|
|
@@ -569,6 +569,36 @@ export class CommandInstaller {
|
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Remove legacy p.*.md files from commands root directory
|
|
574
|
+
* These were replaced by the p/ subdirectory structure in v0.50+
|
|
575
|
+
*/
|
|
576
|
+
async removeLegacyCommands(): Promise<number> {
|
|
577
|
+
const aiProvider = require('./ai-provider')
|
|
578
|
+
const activeProvider = aiProvider.getActiveProvider()
|
|
579
|
+
const commandsRoot = path.join(activeProvider.configDir, 'commands')
|
|
580
|
+
|
|
581
|
+
let removed = 0
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
const files = await fs.readdir(commandsRoot)
|
|
585
|
+
const legacyFiles = files.filter((f) => f.startsWith('p.') && f.endsWith('.md'))
|
|
586
|
+
|
|
587
|
+
for (const file of legacyFiles) {
|
|
588
|
+
try {
|
|
589
|
+
await fs.unlink(path.join(commandsRoot, file))
|
|
590
|
+
removed++
|
|
591
|
+
} catch {
|
|
592
|
+
// Ignore errors removing individual files
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
// Ignore errors if directory doesn't exist
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return removed
|
|
600
|
+
}
|
|
601
|
+
|
|
572
602
|
/**
|
|
573
603
|
* Sync commands - intelligent update that detects and removes orphans
|
|
574
604
|
*/
|
|
@@ -639,9 +669,9 @@ export class CommandInstaller {
|
|
|
639
669
|
}
|
|
640
670
|
}
|
|
641
671
|
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
|
|
672
|
+
// Remove legacy p.*.md files from commands root (old naming convention)
|
|
673
|
+
// These were replaced by p/ subdirectory structure
|
|
674
|
+
await this.removeLegacyCommands()
|
|
645
675
|
|
|
646
676
|
return results
|
|
647
677
|
} catch (error) {
|
|
@@ -79,11 +79,9 @@ export class LinearProvider implements IssueTrackerProvider {
|
|
|
79
79
|
|
|
80
80
|
this.sdk = new LinearSDK({ apiKey })
|
|
81
81
|
|
|
82
|
-
// Verify connection
|
|
82
|
+
// Verify connection silently (no output noise)
|
|
83
83
|
try {
|
|
84
|
-
|
|
85
|
-
// Use stderr for logs to not break JSON output
|
|
86
|
-
console.error(`[linear] Connected as ${viewer.name} (${viewer.email})`)
|
|
84
|
+
await this.sdk.viewer
|
|
87
85
|
} catch (error) {
|
|
88
86
|
this.sdk = null
|
|
89
87
|
throw new Error(`Linear connection failed: ${(error as Error).message}`)
|
package/core/utils/output.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* With prjct branding
|
|
5
5
|
*
|
|
6
6
|
* Supports --quiet mode for CI/CD and scripting
|
|
7
|
+
* Supports output tiers: silent, minimal, compact, verbose
|
|
7
8
|
*
|
|
8
|
-
* @see PRJ-130
|
|
9
|
+
* @see PRJ-105, PRJ-130
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import chalk from 'chalk'
|
|
@@ -16,6 +17,49 @@ import { getError } from './error-messages'
|
|
|
16
17
|
const _FRAMES = branding.spinner.frames
|
|
17
18
|
const SPEED = branding.spinner.speed
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Output tier configuration
|
|
22
|
+
* Controls verbosity of CLI output
|
|
23
|
+
*/
|
|
24
|
+
export type OutputTier = 'silent' | 'minimal' | 'compact' | 'verbose'
|
|
25
|
+
|
|
26
|
+
export interface TierConfig {
|
|
27
|
+
maxLines: number
|
|
28
|
+
maxCharsPerLine: number
|
|
29
|
+
showMetrics: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const OUTPUT_TIERS: Record<OutputTier, TierConfig> = {
|
|
33
|
+
silent: { maxLines: 0, maxCharsPerLine: 0, showMetrics: false },
|
|
34
|
+
minimal: { maxLines: 1, maxCharsPerLine: 65, showMetrics: false },
|
|
35
|
+
compact: { maxLines: 4, maxCharsPerLine: 80, showMetrics: true },
|
|
36
|
+
verbose: { maxLines: Infinity, maxCharsPerLine: Infinity, showMetrics: true },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Current output tier (default: compact for human-readable output)
|
|
40
|
+
let currentTier: OutputTier = 'compact'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set the output tier
|
|
44
|
+
*/
|
|
45
|
+
export function setOutputTier(tier: OutputTier): void {
|
|
46
|
+
currentTier = tier
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get current output tier
|
|
51
|
+
*/
|
|
52
|
+
export function getOutputTier(): OutputTier {
|
|
53
|
+
return currentTier
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get current tier config
|
|
58
|
+
*/
|
|
59
|
+
export function getTierConfig(): TierConfig {
|
|
60
|
+
return OUTPUT_TIERS[currentTier]
|
|
61
|
+
}
|
|
62
|
+
|
|
19
63
|
/**
|
|
20
64
|
* Centralized icons for consistent output
|
|
21
65
|
*/
|
|
@@ -52,8 +96,89 @@ export function isQuietMode(): boolean {
|
|
|
52
96
|
return quietMode
|
|
53
97
|
}
|
|
54
98
|
|
|
55
|
-
|
|
56
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Truncate string to max chars (uses tier config if no max specified)
|
|
101
|
+
*/
|
|
102
|
+
const truncate = (s: string | undefined | null, max?: number): string => {
|
|
103
|
+
const limit = max ?? (getTierConfig().maxCharsPerLine || 50)
|
|
104
|
+
return s && s.length > limit ? `${s.slice(0, limit - 1)}…` : s || ''
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Limit output to maxLines (respects tier config)
|
|
109
|
+
* Returns truncated content with "...N more lines" indicator
|
|
110
|
+
*/
|
|
111
|
+
/**
|
|
112
|
+
* Limit output to maxLines (respects tier config)
|
|
113
|
+
* Returns truncated content with "...N more lines" indicator
|
|
114
|
+
*/
|
|
115
|
+
export function limitLines(content: string, maxLines?: number): string {
|
|
116
|
+
const limit = maxLines ?? getTierConfig().maxLines
|
|
117
|
+
if (limit === Infinity || limit === 0) return content
|
|
118
|
+
|
|
119
|
+
const lines = content.split('\n')
|
|
120
|
+
if (lines.length <= limit) return content
|
|
121
|
+
|
|
122
|
+
const shown = lines.slice(0, limit)
|
|
123
|
+
const remaining = lines.length - limit
|
|
124
|
+
return `${shown.join('\n')}\n${chalk.dim(`...${remaining} more lines`)}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format data for human-readable output (respects tier)
|
|
129
|
+
* Use this instead of JSON.stringify for CLI output
|
|
130
|
+
*/
|
|
131
|
+
export function formatForHuman(data: unknown): string {
|
|
132
|
+
const tier = getTierConfig()
|
|
133
|
+
|
|
134
|
+
if (currentTier === 'silent') return ''
|
|
135
|
+
if (currentTier === 'verbose') return JSON.stringify(data, null, 2)
|
|
136
|
+
|
|
137
|
+
// For minimal/compact: extract key info
|
|
138
|
+
if (typeof data !== 'object' || data === null) {
|
|
139
|
+
return truncate(String(data), tier.maxCharsPerLine)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const obj = data as Record<string, unknown>
|
|
143
|
+
|
|
144
|
+
// Linear issue format
|
|
145
|
+
if ('identifier' in obj && 'title' in obj) {
|
|
146
|
+
const lines: string[] = []
|
|
147
|
+
lines.push(`${obj.identifier}: ${truncate(String(obj.title), tier.maxCharsPerLine - 10)}`)
|
|
148
|
+
if (obj.status) lines.push(`Status: ${obj.status}`)
|
|
149
|
+
if (obj.priority && obj.priority !== 'none') lines.push(`Priority: ${obj.priority}`)
|
|
150
|
+
if (obj.url && currentTier === 'compact') lines.push(chalk.dim(String(obj.url)))
|
|
151
|
+
return limitLines(lines.join('\n'), tier.maxLines)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Issue list format
|
|
155
|
+
if ('issues' in obj && Array.isArray(obj.issues)) {
|
|
156
|
+
const issues = obj.issues as Array<Record<string, unknown>>
|
|
157
|
+
const lines = issues.slice(0, tier.maxLines).map((i) => {
|
|
158
|
+
const priority = i.priority && i.priority !== 'none' ? ` [${i.priority}]` : ''
|
|
159
|
+
return `${i.identifier} ${truncate(String(i.title), 50)}${priority}`
|
|
160
|
+
})
|
|
161
|
+
if (issues.length > tier.maxLines) {
|
|
162
|
+
lines.push(chalk.dim(`...${issues.length - tier.maxLines} more`))
|
|
163
|
+
}
|
|
164
|
+
return lines.join('\n')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Generic object: show key fields only
|
|
168
|
+
const keyFields = ['id', 'name', 'title', 'status', 'message', 'success', 'error']
|
|
169
|
+
const relevant = keyFields.filter((k) => k in obj)
|
|
170
|
+
if (relevant.length > 0) {
|
|
171
|
+
return limitLines(
|
|
172
|
+
relevant
|
|
173
|
+
.map((k) => `${k}: ${truncate(String(obj[k]), tier.maxCharsPerLine - k.length - 2)}`)
|
|
174
|
+
.join('\n'),
|
|
175
|
+
tier.maxLines
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fallback: compact JSON
|
|
180
|
+
return limitLines(JSON.stringify(data, null, 2), tier.maxLines)
|
|
181
|
+
}
|
|
57
182
|
|
|
58
183
|
const clear = (): boolean => process.stdout.write(`\r${' '.repeat(80)}\r`)
|
|
59
184
|
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -2088,20 +2088,82 @@ var output_exports = {};
|
|
|
2088
2088
|
__export(output_exports, {
|
|
2089
2089
|
ERRORS: () => ERRORS,
|
|
2090
2090
|
ICONS: () => ICONS,
|
|
2091
|
+
OUTPUT_TIERS: () => OUTPUT_TIERS,
|
|
2091
2092
|
createError: () => createError,
|
|
2092
2093
|
default: () => output_default,
|
|
2094
|
+
formatForHuman: () => formatForHuman,
|
|
2093
2095
|
getError: () => getError,
|
|
2096
|
+
getOutputTier: () => getOutputTier,
|
|
2097
|
+
getTierConfig: () => getTierConfig,
|
|
2094
2098
|
isQuietMode: () => isQuietMode,
|
|
2099
|
+
limitLines: () => limitLines,
|
|
2100
|
+
setOutputTier: () => setOutputTier,
|
|
2095
2101
|
setQuietMode: () => setQuietMode
|
|
2096
2102
|
});
|
|
2097
2103
|
import chalk2 from "chalk";
|
|
2104
|
+
function setOutputTier(tier) {
|
|
2105
|
+
currentTier = tier;
|
|
2106
|
+
}
|
|
2107
|
+
function getOutputTier() {
|
|
2108
|
+
return currentTier;
|
|
2109
|
+
}
|
|
2110
|
+
function getTierConfig() {
|
|
2111
|
+
return OUTPUT_TIERS[currentTier];
|
|
2112
|
+
}
|
|
2098
2113
|
function setQuietMode(enabled) {
|
|
2099
2114
|
quietMode = enabled;
|
|
2100
2115
|
}
|
|
2101
2116
|
function isQuietMode() {
|
|
2102
2117
|
return quietMode;
|
|
2103
2118
|
}
|
|
2104
|
-
|
|
2119
|
+
function limitLines(content, maxLines) {
|
|
2120
|
+
const limit = maxLines ?? getTierConfig().maxLines;
|
|
2121
|
+
if (limit === Infinity || limit === 0) return content;
|
|
2122
|
+
const lines = content.split("\n");
|
|
2123
|
+
if (lines.length <= limit) return content;
|
|
2124
|
+
const shown = lines.slice(0, limit);
|
|
2125
|
+
const remaining = lines.length - limit;
|
|
2126
|
+
return `${shown.join("\n")}
|
|
2127
|
+
${chalk2.dim(`...${remaining} more lines`)}`;
|
|
2128
|
+
}
|
|
2129
|
+
function formatForHuman(data) {
|
|
2130
|
+
const tier = getTierConfig();
|
|
2131
|
+
if (currentTier === "silent") return "";
|
|
2132
|
+
if (currentTier === "verbose") return JSON.stringify(data, null, 2);
|
|
2133
|
+
if (typeof data !== "object" || data === null) {
|
|
2134
|
+
return truncate(String(data), tier.maxCharsPerLine);
|
|
2135
|
+
}
|
|
2136
|
+
const obj = data;
|
|
2137
|
+
if ("identifier" in obj && "title" in obj) {
|
|
2138
|
+
const lines = [];
|
|
2139
|
+
lines.push(`${obj.identifier}: ${truncate(String(obj.title), tier.maxCharsPerLine - 10)}`);
|
|
2140
|
+
if (obj.status) lines.push(`Status: ${obj.status}`);
|
|
2141
|
+
if (obj.priority && obj.priority !== "none") lines.push(`Priority: ${obj.priority}`);
|
|
2142
|
+
if (obj.url && currentTier === "compact") lines.push(chalk2.dim(String(obj.url)));
|
|
2143
|
+
return limitLines(lines.join("\n"), tier.maxLines);
|
|
2144
|
+
}
|
|
2145
|
+
if ("issues" in obj && Array.isArray(obj.issues)) {
|
|
2146
|
+
const issues = obj.issues;
|
|
2147
|
+
const lines = issues.slice(0, tier.maxLines).map((i) => {
|
|
2148
|
+
const priority = i.priority && i.priority !== "none" ? ` [${i.priority}]` : "";
|
|
2149
|
+
return `${i.identifier} ${truncate(String(i.title), 50)}${priority}`;
|
|
2150
|
+
});
|
|
2151
|
+
if (issues.length > tier.maxLines) {
|
|
2152
|
+
lines.push(chalk2.dim(`...${issues.length - tier.maxLines} more`));
|
|
2153
|
+
}
|
|
2154
|
+
return lines.join("\n");
|
|
2155
|
+
}
|
|
2156
|
+
const keyFields = ["id", "name", "title", "status", "message", "success", "error"];
|
|
2157
|
+
const relevant = keyFields.filter((k) => k in obj);
|
|
2158
|
+
if (relevant.length > 0) {
|
|
2159
|
+
return limitLines(
|
|
2160
|
+
relevant.map((k) => `${k}: ${truncate(String(obj[k]), tier.maxCharsPerLine - k.length - 2)}`).join("\n"),
|
|
2161
|
+
tier.maxLines
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
return limitLines(JSON.stringify(data, null, 2), tier.maxLines);
|
|
2165
|
+
}
|
|
2166
|
+
var _FRAMES, SPEED, OUTPUT_TIERS, currentTier, ICONS, interval, frame, quietMode, truncate, clear, out, output_default;
|
|
2105
2167
|
var init_output = __esm({
|
|
2106
2168
|
"core/utils/output.ts"() {
|
|
2107
2169
|
"use strict";
|
|
@@ -2110,6 +2172,16 @@ var init_output = __esm({
|
|
|
2110
2172
|
init_error_messages();
|
|
2111
2173
|
_FRAMES = branding_default.spinner.frames;
|
|
2112
2174
|
SPEED = branding_default.spinner.speed;
|
|
2175
|
+
OUTPUT_TIERS = {
|
|
2176
|
+
silent: { maxLines: 0, maxCharsPerLine: 0, showMetrics: false },
|
|
2177
|
+
minimal: { maxLines: 1, maxCharsPerLine: 65, showMetrics: false },
|
|
2178
|
+
compact: { maxLines: 4, maxCharsPerLine: 80, showMetrics: true },
|
|
2179
|
+
verbose: { maxLines: Infinity, maxCharsPerLine: Infinity, showMetrics: true }
|
|
2180
|
+
};
|
|
2181
|
+
currentTier = "compact";
|
|
2182
|
+
__name(setOutputTier, "setOutputTier");
|
|
2183
|
+
__name(getOutputTier, "getOutputTier");
|
|
2184
|
+
__name(getTierConfig, "getTierConfig");
|
|
2113
2185
|
ICONS = {
|
|
2114
2186
|
success: chalk2.green("\u2713"),
|
|
2115
2187
|
fail: chalk2.red("\u2717"),
|
|
@@ -2127,7 +2199,12 @@ var init_output = __esm({
|
|
|
2127
2199
|
quietMode = false;
|
|
2128
2200
|
__name(setQuietMode, "setQuietMode");
|
|
2129
2201
|
__name(isQuietMode, "isQuietMode");
|
|
2130
|
-
truncate = /* @__PURE__ */ __name((s, max
|
|
2202
|
+
truncate = /* @__PURE__ */ __name((s, max) => {
|
|
2203
|
+
const limit = max ?? (getTierConfig().maxCharsPerLine || 50);
|
|
2204
|
+
return s && s.length > limit ? `${s.slice(0, limit - 1)}\u2026` : s || "";
|
|
2205
|
+
}, "truncate");
|
|
2206
|
+
__name(limitLines, "limitLines");
|
|
2207
|
+
__name(formatForHuman, "formatForHuman");
|
|
2131
2208
|
clear = /* @__PURE__ */ __name(() => process.stdout.write(`\r${" ".repeat(80)}\r`), "clear");
|
|
2132
2209
|
out = {
|
|
2133
2210
|
// Branding: Show header at start
|
|
@@ -5952,6 +6029,29 @@ var init_command_installer = __esm({
|
|
|
5952
6029
|
throw error;
|
|
5953
6030
|
}
|
|
5954
6031
|
}
|
|
6032
|
+
/**
|
|
6033
|
+
* Remove legacy p.*.md files from commands root directory
|
|
6034
|
+
* These were replaced by the p/ subdirectory structure in v0.50+
|
|
6035
|
+
*/
|
|
6036
|
+
async removeLegacyCommands() {
|
|
6037
|
+
const aiProvider = (init_ai_provider(), __toCommonJS(ai_provider_exports));
|
|
6038
|
+
const activeProvider = aiProvider.getActiveProvider();
|
|
6039
|
+
const commandsRoot = path16.join(activeProvider.configDir, "commands");
|
|
6040
|
+
let removed = 0;
|
|
6041
|
+
try {
|
|
6042
|
+
const files = await fs16.readdir(commandsRoot);
|
|
6043
|
+
const legacyFiles = files.filter((f) => f.startsWith("p.") && f.endsWith(".md"));
|
|
6044
|
+
for (const file of legacyFiles) {
|
|
6045
|
+
try {
|
|
6046
|
+
await fs16.unlink(path16.join(commandsRoot, file));
|
|
6047
|
+
removed++;
|
|
6048
|
+
} catch {
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
} catch {
|
|
6052
|
+
}
|
|
6053
|
+
return removed;
|
|
6054
|
+
}
|
|
5955
6055
|
/**
|
|
5956
6056
|
* Sync commands - intelligent update that detects and removes orphans
|
|
5957
6057
|
*/
|
|
@@ -6004,6 +6104,7 @@ var init_command_installer = __esm({
|
|
|
6004
6104
|
results.errors.push({ file, error: error.message });
|
|
6005
6105
|
}
|
|
6006
6106
|
}
|
|
6107
|
+
await this.removeLegacyCommands();
|
|
6007
6108
|
return results;
|
|
6008
6109
|
} catch (error) {
|
|
6009
6110
|
return {
|
|
@@ -23907,8 +24008,7 @@ var init_client = __esm({
|
|
|
23907
24008
|
}
|
|
23908
24009
|
this.sdk = new LinearSDK({ apiKey });
|
|
23909
24010
|
try {
|
|
23910
|
-
|
|
23911
|
-
console.error(`[linear] Connected as ${viewer.name} (${viewer.email})`);
|
|
24011
|
+
await this.sdk.viewer;
|
|
23912
24012
|
} catch (error) {
|
|
23913
24013
|
this.sdk = null;
|
|
23914
24014
|
throw new Error(`Linear connection failed: ${error.message}`);
|
|
@@ -25197,7 +25297,7 @@ var require_package = __commonJS({
|
|
|
25197
25297
|
"package.json"(exports, module) {
|
|
25198
25298
|
module.exports = {
|
|
25199
25299
|
name: "prjct-cli",
|
|
25200
|
-
version: "0.
|
|
25300
|
+
version: "0.56.0",
|
|
25201
25301
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
25202
25302
|
main: "core/index.ts",
|
|
25203
25303
|
bin: {
|
|
@@ -25215,6 +25315,7 @@ var require_package = __commonJS({
|
|
|
25215
25315
|
"release:minor": "node scripts/release.js minor",
|
|
25216
25316
|
"release:major": "node scripts/release.js major",
|
|
25217
25317
|
postinstall: "node scripts/postinstall.js",
|
|
25318
|
+
prepare: "lefthook install",
|
|
25218
25319
|
"update-commands": `bun -e "const installer = require('./core/infrastructure/command-installer'); installer.syncCommands().then(r => console.log('Commands updated:', r)).catch(e => console.error('Error:', e.message))"`,
|
|
25219
25320
|
"install-global": "./scripts/install.sh",
|
|
25220
25321
|
update: "./scripts/update.sh",
|
|
@@ -25264,6 +25365,7 @@ var require_package = __commonJS({
|
|
|
25264
25365
|
"@types/bun": "latest",
|
|
25265
25366
|
"@types/chokidar": "^2.1.7",
|
|
25266
25367
|
"@types/prompts": "^2.4.9",
|
|
25368
|
+
lefthook: "^2.1.0",
|
|
25267
25369
|
typescript: "^5.9.3"
|
|
25268
25370
|
},
|
|
25269
25371
|
repository: {
|
|
@@ -873,6 +873,29 @@ var CommandInstaller = class {
|
|
|
873
873
|
throw error;
|
|
874
874
|
}
|
|
875
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Remove legacy p.*.md files from commands root directory
|
|
878
|
+
* These were replaced by the p/ subdirectory structure in v0.50+
|
|
879
|
+
*/
|
|
880
|
+
async removeLegacyCommands() {
|
|
881
|
+
const aiProvider = (init_ai_provider(), __toCommonJS(ai_provider_exports));
|
|
882
|
+
const activeProvider = aiProvider.getActiveProvider();
|
|
883
|
+
const commandsRoot = import_node_path3.default.join(activeProvider.configDir, "commands");
|
|
884
|
+
let removed = 0;
|
|
885
|
+
try {
|
|
886
|
+
const files = await import_promises.default.readdir(commandsRoot);
|
|
887
|
+
const legacyFiles = files.filter((f) => f.startsWith("p.") && f.endsWith(".md"));
|
|
888
|
+
for (const file of legacyFiles) {
|
|
889
|
+
try {
|
|
890
|
+
await import_promises.default.unlink(import_node_path3.default.join(commandsRoot, file));
|
|
891
|
+
removed++;
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
return removed;
|
|
898
|
+
}
|
|
876
899
|
/**
|
|
877
900
|
* Sync commands - intelligent update that detects and removes orphans
|
|
878
901
|
*/
|
|
@@ -925,6 +948,7 @@ var CommandInstaller = class {
|
|
|
925
948
|
results.errors.push({ file, error: error.message });
|
|
926
949
|
}
|
|
927
950
|
}
|
|
951
|
+
await this.removeLegacyCommands();
|
|
928
952
|
return results;
|
|
929
953
|
} catch (error) {
|
|
930
954
|
return {
|
|
@@ -876,6 +876,29 @@ var CommandInstaller = class {
|
|
|
876
876
|
throw error;
|
|
877
877
|
}
|
|
878
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Remove legacy p.*.md files from commands root directory
|
|
881
|
+
* These were replaced by the p/ subdirectory structure in v0.50+
|
|
882
|
+
*/
|
|
883
|
+
async removeLegacyCommands() {
|
|
884
|
+
const aiProvider = (init_ai_provider(), __toCommonJS(ai_provider_exports));
|
|
885
|
+
const activeProvider = aiProvider.getActiveProvider();
|
|
886
|
+
const commandsRoot = import_node_path3.default.join(activeProvider.configDir, "commands");
|
|
887
|
+
let removed = 0;
|
|
888
|
+
try {
|
|
889
|
+
const files = await import_promises.default.readdir(commandsRoot);
|
|
890
|
+
const legacyFiles = files.filter((f) => f.startsWith("p.") && f.endsWith(".md"));
|
|
891
|
+
for (const file of legacyFiles) {
|
|
892
|
+
try {
|
|
893
|
+
await import_promises.default.unlink(import_node_path3.default.join(commandsRoot, file));
|
|
894
|
+
removed++;
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
} catch {
|
|
899
|
+
}
|
|
900
|
+
return removed;
|
|
901
|
+
}
|
|
879
902
|
/**
|
|
880
903
|
* Sync commands - intelligent update that detects and removes orphans
|
|
881
904
|
*/
|
|
@@ -928,6 +951,7 @@ var CommandInstaller = class {
|
|
|
928
951
|
results.errors.push({ file, error: error.message });
|
|
929
952
|
}
|
|
930
953
|
}
|
|
954
|
+
await this.removeLegacyCommands();
|
|
931
955
|
return results;
|
|
932
956
|
} catch (error) {
|
|
933
957
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prjct-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
5
5
|
"main": "core/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"release:minor": "node scripts/release.js minor",
|
|
19
19
|
"release:major": "node scripts/release.js major",
|
|
20
20
|
"postinstall": "node scripts/postinstall.js",
|
|
21
|
+
"prepare": "lefthook install",
|
|
21
22
|
"update-commands": "bun -e \"const installer = require('./core/infrastructure/command-installer'); installer.syncCommands().then(r => console.log('Commands updated:', r)).catch(e => console.error('Error:', e.message))\"",
|
|
22
23
|
"install-global": "./scripts/install.sh",
|
|
23
24
|
"update": "./scripts/update.sh",
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"@types/bun": "latest",
|
|
68
69
|
"@types/chokidar": "^2.1.7",
|
|
69
70
|
"@types/prompts": "^2.4.9",
|
|
71
|
+
"lefthook": "^2.1.0",
|
|
70
72
|
"typescript": "^5.9.3"
|
|
71
73
|
},
|
|
72
74
|
"repository": {
|