prjct-cli 0.55.4 → 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 +46 -0
- package/core/__tests__/utils/output.test.ts +119 -1
- package/core/cli/linear.ts +93 -34
- package/core/integrations/linear/client.ts +2 -4
- package/core/utils/output.ts +128 -3
- package/dist/bin/prjct.mjs +83 -5
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,57 @@
|
|
|
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
|
+
|
|
3
34
|
## [0.55.4] - 2026-02-05
|
|
4
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
|
+
|
|
5
48
|
### Bug Fixes
|
|
6
49
|
|
|
7
50
|
- remove legacy p.*.md commands on sync
|
|
8
51
|
|
|
52
|
+
### Tests
|
|
53
|
+
|
|
54
|
+
- Added 11 new tests for output tier functionality
|
|
9
55
|
|
|
10
56
|
## [0.55.3] - 2026-02-05
|
|
11
57
|
|
|
@@ -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
|
}
|
|
@@ -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
|
|
@@ -23931,8 +24008,7 @@ var init_client = __esm({
|
|
|
23931
24008
|
}
|
|
23932
24009
|
this.sdk = new LinearSDK({ apiKey });
|
|
23933
24010
|
try {
|
|
23934
|
-
|
|
23935
|
-
console.error(`[linear] Connected as ${viewer.name} (${viewer.email})`);
|
|
24011
|
+
await this.sdk.viewer;
|
|
23936
24012
|
} catch (error) {
|
|
23937
24013
|
this.sdk = null;
|
|
23938
24014
|
throw new Error(`Linear connection failed: ${error.message}`);
|
|
@@ -25221,7 +25297,7 @@ var require_package = __commonJS({
|
|
|
25221
25297
|
"package.json"(exports, module) {
|
|
25222
25298
|
module.exports = {
|
|
25223
25299
|
name: "prjct-cli",
|
|
25224
|
-
version: "0.
|
|
25300
|
+
version: "0.56.0",
|
|
25225
25301
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
25226
25302
|
main: "core/index.ts",
|
|
25227
25303
|
bin: {
|
|
@@ -25239,6 +25315,7 @@ var require_package = __commonJS({
|
|
|
25239
25315
|
"release:minor": "node scripts/release.js minor",
|
|
25240
25316
|
"release:major": "node scripts/release.js major",
|
|
25241
25317
|
postinstall: "node scripts/postinstall.js",
|
|
25318
|
+
prepare: "lefthook install",
|
|
25242
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))"`,
|
|
25243
25320
|
"install-global": "./scripts/install.sh",
|
|
25244
25321
|
update: "./scripts/update.sh",
|
|
@@ -25288,6 +25365,7 @@ var require_package = __commonJS({
|
|
|
25288
25365
|
"@types/bun": "latest",
|
|
25289
25366
|
"@types/chokidar": "^2.1.7",
|
|
25290
25367
|
"@types/prompts": "^2.4.9",
|
|
25368
|
+
lefthook: "^2.1.0",
|
|
25291
25369
|
typescript: "^5.9.3"
|
|
25292
25370
|
},
|
|
25293
25371
|
repository: {
|
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": {
|