jackherd 1.12.6 → 1.13.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 +14 -0
- package/INSTALL.md +1 -1
- package/README.md +37 -19
- package/dist/config.d.ts +32 -11
- package/dist/config.js +38 -82
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/merge-guard/board-settings.ts +7 -32
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +2 -29
- package/kit/hooks/hooks/plain-question/code-cache.ts +70 -0
- package/kit/hooks/hooks/plain-question/index.ts +80 -0
- package/kit/hooks/hooks/plain-question/run.ts +50 -0
- package/kit/hooks/hooks/shared/board-lanes.ts +19 -0
- package/kit/hooks/hooks/shared/boards.ts +96 -0
- package/kit/hooks/hooks/shared/plan-codes.ts +68 -0
- package/kit/hooks/hooks.json +9 -0
- package/kit/review/skills/diagnose/SKILL.md +2 -2
- package/kit/review/skills/diagnose/references/feedback-loops.md +4 -13
- package/kit/rules/rules/kit/code-style.md +15 -18
- package/kit/rules/rules/kit/testing.md +7 -28
- package/kit/workflow/skills/implement/SKILL.md +3 -3
- package/kit/workflow/skills/orchestrate/SKILL.md +2 -0
- package/kit/workflow/skills/start/SKILL.md +4 -5
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +14 -11
- package/src/check/command.ts +121 -35
- package/src/check/plan-structure-check.ts +34 -1
- package/src/check/plan.ts +127 -44
- package/src/check/report.ts +67 -2
- package/src/check/result.ts +13 -3
- package/src/check/roots.ts +38 -10
- package/src/check/schema.ts +8 -3
- package/src/check/settings.ts +18 -1
- package/src/check/skill-budget-defaults.ts +3 -2
- package/src/config/base-branch.ts +2 -0
- package/src/config/command.ts +32 -13
- package/src/config/detect.ts +242 -74
- package/src/config/location.ts +6 -0
- package/src/config/repository-facts.ts +32 -7
- package/src/config/repository-values.ts +162 -5
- package/src/config/schema.ts +34 -16
- package/src/lane-prompt/texts.ts +2 -2
- package/src/plan-format/bare-codes.ts +28 -0
- package/src/plans/your-turn.ts +6 -5
- package/src/sync/command-context.ts +4 -2
- package/src/sync/herdr-plugin.ts +30 -6
- package/src/sync/init.ts +17 -16
- package/src/sync/render.ts +4 -6
- package/src/watch/newest-first.ts +8 -0
- package/src/watch/source-statuses.ts +5 -2
package/src/check/report.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { STACKS, type StackDetection, stackOfCommand } from '../config/detect'
|
|
2
|
+
import { CLI_NAME } from '../version/release'
|
|
1
3
|
import { type ChangedFile, STATUS_COMMAND } from './changes'
|
|
2
4
|
import type { CheckPlan } from './plan'
|
|
3
|
-
import { CHECK_STATUS, type CheckResult, SKIP_SLOW_FLAG } from './result'
|
|
5
|
+
import { CHECK_STATUS, type CheckResult, type CheckStatus, RUN_MODE, type RunMode, SKIP_SLOW_FLAG } from './result'
|
|
4
6
|
|
|
5
7
|
const STATUS_ICON = { pass: '✅', fail: '❌', warn: '⚠️ ', skipped: '⏭️ ' } as const
|
|
6
8
|
|
|
@@ -20,10 +22,73 @@ export const skippedSlowResult = (): CheckResult => ({
|
|
|
20
22
|
durationMs: 0,
|
|
21
23
|
})
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
const plural = (count: number, noun: string): string => `${count} ${noun}${count === 1 ? '' : 's'}`
|
|
26
|
+
|
|
27
|
+
/** A full run fails on a changed source file no test covers; a quick one runs no test by design, so it warns. */
|
|
28
|
+
export const UNTESTED_STATUS: Record<RunMode, CheckStatus> = {
|
|
29
|
+
[RUN_MODE.full]: CHECK_STATUS.fail,
|
|
30
|
+
[RUN_MODE.quick]: CHECK_STATUS.warn,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A changed source file of the stack with no test planned, with the status `UNTESTED_STATUS` gives the run mode. */
|
|
34
|
+
export const untestedResult = (sources: readonly string[], mode: RunMode): CheckResult | null =>
|
|
35
|
+
sources.length === 0
|
|
36
|
+
? null
|
|
37
|
+
: {
|
|
38
|
+
name: 'Tests',
|
|
39
|
+
command: 'commands.test',
|
|
40
|
+
status: UNTESTED_STATUS[mode],
|
|
41
|
+
detail: [
|
|
42
|
+
`no test ran for ${plural(sources.length, 'changed source file')}; set \`commands.test\` or a \`check.checks\` entry with \`kind: "tests"\``,
|
|
43
|
+
...sources,
|
|
44
|
+
].join('\n'),
|
|
45
|
+
durationMs: 0,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The lock files of known stacks the repository holds; none means it has not installed yet. */
|
|
49
|
+
const presentLockFiles = (detection: StackDetection): readonly string[] => {
|
|
50
|
+
if (detection.kind === 'found') return [detection.lockFile]
|
|
51
|
+
return detection.kind === 'ambiguous' ? detection.lockFiles : []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A failure when `commands.test` belongs to a known stack whose lock files the repository lacks (`bun test` beside
|
|
56
|
+
* only `composer.lock`): such a run finds no test and would read as green. Null for a command of no known stack,
|
|
57
|
+
* for no command, and in a repository without any lock file.
|
|
58
|
+
*/
|
|
59
|
+
export const testCommandResult = (command: string | undefined, detection: StackDetection): CheckResult | null => {
|
|
60
|
+
const stack = command === undefined ? undefined : stackOfCommand(command)
|
|
61
|
+
const present = presentLockFiles(detection)
|
|
62
|
+
if (stack === undefined || present.length === 0) return null
|
|
63
|
+
const { lockFiles } = STACKS[stack]
|
|
64
|
+
if (lockFiles.some((file) => present.includes(file))) return null
|
|
65
|
+
return {
|
|
66
|
+
name: 'Test command',
|
|
67
|
+
command: 'commands.test',
|
|
68
|
+
status: CHECK_STATUS.fail,
|
|
69
|
+
detail: `\`${command}\` needs ${lockFiles.join(' or ')}, which is missing; set \`commands.test\` for this repository's stack (\`${CLI_NAME} config test\` names the line)`,
|
|
70
|
+
durationMs: 0,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The stack line says how the stack was decided: by lock file, by manifest alone, none, or several lock files and so every stack's markers. */
|
|
75
|
+
export const stackLine = (detection: StackDetection): string => {
|
|
76
|
+
if (detection.kind === 'found') return `Stack: ${detection.stack}`
|
|
77
|
+
if (detection.kind === 'manifest') return `Stack: ${detection.stack} (from ${detection.manifest}, no lock file yet)`
|
|
78
|
+
if (detection.kind === 'none')
|
|
79
|
+
return 'Stack: none detected (no lock file of a known stack, no manifest of a single stack)'
|
|
80
|
+
return `Stack: ambiguous (lock files of several stacks: ${detection.lockFiles.join(', ')}), checking with every stack's markers`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const formatPlanHeader = (
|
|
84
|
+
files: readonly ChangedFile[],
|
|
85
|
+
plan: CheckPlan,
|
|
86
|
+
detection: StackDetection,
|
|
87
|
+
): string[] => [
|
|
24
88
|
`Changed files (${files.length}, ${files.filter((file) => file.staged).length} staged):`,
|
|
25
89
|
...files.map((file) => ` ${file.staged ? 'S' : ' '} ${file.path}`),
|
|
26
90
|
'',
|
|
91
|
+
stackLine(detection),
|
|
27
92
|
`Package roots: ${plan.roots.join(', ') || 'none'}`,
|
|
28
93
|
`Reviewers: ${plan.reviewers.join(', ') || 'none'}`,
|
|
29
94
|
...(plan.generated.length > 0
|
package/src/check/result.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** What a planned check is; `types` and `tests` are the slow ones, and a project check may stand in for either. */
|
|
2
|
+
export const CHECK_KIND = { lint: 'lint', project: 'project', types: 'types', tests: 'tests' } as const
|
|
3
|
+
|
|
4
|
+
export type CheckKind = (typeof CHECK_KIND)[keyof typeof CHECK_KIND]
|
|
3
5
|
|
|
4
6
|
/** `warn` reports a finding without failing the run. */
|
|
5
7
|
export const CHECK_STATUS = { pass: 'pass', fail: 'fail', warn: 'warn', skipped: 'skipped' } as const
|
|
@@ -18,6 +20,14 @@ export type ProcessResult = { ok: boolean; output: string }
|
|
|
18
20
|
|
|
19
21
|
export const SKIP_SLOW_FLAG = '--skip-slow'
|
|
20
22
|
|
|
23
|
+
/** `quick` leaves every type check and test out (`--skip-slow`); `full` runs them. */
|
|
24
|
+
export const RUN_MODE = { full: 'full', quick: 'quick' } as const
|
|
25
|
+
|
|
26
|
+
export type RunMode = (typeof RUN_MODE)[keyof typeof RUN_MODE]
|
|
27
|
+
|
|
28
|
+
export const runModeOf = (args: readonly string[]): RunMode =>
|
|
29
|
+
args.includes(SKIP_SLOW_FLAG) ? RUN_MODE.quick : RUN_MODE.full
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* Bun reports "there was nothing to run" in two shapes: a package whose files hold no test cases
|
|
23
33
|
* summarises as `0 tests`, one with no matching files exits non-zero with `0 test files matching`.
|
|
@@ -30,7 +40,7 @@ export const lastLines = (output: string, count: number): string =>
|
|
|
30
40
|
|
|
31
41
|
/** How a finished process reads for its check: an absent test suite is skipped, not failed. */
|
|
32
42
|
export const outcomeOf = (
|
|
33
|
-
check:
|
|
43
|
+
check: { kind: CheckKind },
|
|
34
44
|
{ ok, output }: ProcessResult,
|
|
35
45
|
): Pick<CheckResult, 'status' | 'detail'> => {
|
|
36
46
|
if (check.kind === CHECK_KIND.tests && reportsNoTests(output)) {
|
package/src/check/roots.ts
CHANGED
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
import { dirname } from 'node:path'
|
|
2
|
+
import { CLAUDE_DIRECTORY } from '../claude/paths'
|
|
3
|
+
import { parseManifest } from '../sync/manifest'
|
|
4
|
+
|
|
5
|
+
/** The manifest whose `workspaces` field gathers workspaces (npm, Yarn, Bun). */
|
|
6
|
+
export const WORKSPACE_MANIFEST = 'package.json'
|
|
7
|
+
|
|
8
|
+
/** pnpm lists its workspaces in a file of its own instead of `WORKSPACE_MANIFEST`. */
|
|
9
|
+
export const WORKSPACE_LIST_FILE = 'pnpm-workspace.yaml'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a parsed `WORKSPACE_MANIFEST` gathers workspaces. Such a root only holds packages: a test run from
|
|
13
|
+
* there walks the whole monorepo.
|
|
14
|
+
*/
|
|
15
|
+
export const isWorkspaceManifest = (manifest: unknown): boolean =>
|
|
16
|
+
typeof manifest === 'object' && manifest !== null && Object.hasOwn(manifest, 'workspaces')
|
|
2
17
|
|
|
3
18
|
/**
|
|
4
|
-
* The nearest ancestor holding
|
|
5
|
-
*
|
|
6
|
-
* silently compiles `.tsx` against the
|
|
19
|
+
* The nearest ancestor holding the stack's root marker (`package.json`, `composer.json`), the repository
|
|
20
|
+
* root included. Tests and type checks run from there and nowhere else: Bun resolves `tsconfig.json`
|
|
21
|
+
* from the working directory, so descending below a package root silently compiles `.tsx` against the
|
|
22
|
+
* wrong JSX runtime.
|
|
7
23
|
*/
|
|
8
|
-
export const packageRootOf = (path: string,
|
|
24
|
+
export const packageRootOf = (path: string, isProjectRoot: (dir: string) => boolean): string | null => {
|
|
9
25
|
let dir = dirname(path)
|
|
10
26
|
while (dir !== '.' && dir !== '/' && dir !== '') {
|
|
11
|
-
if (
|
|
27
|
+
if (isProjectRoot(dir)) return dir
|
|
12
28
|
dir = dirname(dir)
|
|
13
29
|
}
|
|
14
|
-
return
|
|
30
|
+
return isProjectRoot('.') ? '.' : null
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
export const packageRootsFor = (paths: readonly string[],
|
|
18
|
-
...new Set(paths.map((path) => packageRootOf(path,
|
|
33
|
+
export const packageRootsFor = (paths: readonly string[], isProjectRoot: (dir: string) => boolean): string[] => [
|
|
34
|
+
...new Set(paths.map((path) => packageRootOf(path, isProjectRoot)).filter((root): root is string => root !== null)),
|
|
19
35
|
]
|
|
20
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The repository paths of the files a parsed `.jackherd/lock.json` lists: copies whose tests run where the kit
|
|
39
|
+
* keeps their source. A missing or invalid lock lists none, so every changed source file needs its own test.
|
|
40
|
+
*/
|
|
41
|
+
export const kitCopyPaths = (manifest: unknown): string[] => {
|
|
42
|
+
const parsed = parseManifest(manifest)
|
|
43
|
+
return parsed.ok ? Object.keys(parsed.manifest.files).map((path) => `${CLAUDE_DIRECTORY}/${path}`) : []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether a test run from `root` reaches `path`; from `.` it reaches every path. */
|
|
47
|
+
export const isUnder = (path: string, root: string): boolean => root === '.' || path.startsWith(`${root}/`)
|
|
48
|
+
|
|
21
49
|
/**
|
|
22
50
|
* Directories with test files that belong to no package root, which would otherwise be tested by
|
|
23
|
-
* nobody. The
|
|
51
|
+
* nobody. The repository root `.` is excluded: a test run from there walks the whole repository.
|
|
24
52
|
*/
|
|
25
53
|
export const looseDirectories = (
|
|
26
54
|
paths: readonly string[],
|
|
@@ -29,7 +57,7 @@ export const looseDirectories = (
|
|
|
29
57
|
): string[] => [
|
|
30
58
|
...new Set(
|
|
31
59
|
paths
|
|
32
|
-
.filter((path) => !roots.some((root) => path
|
|
60
|
+
.filter((path) => !roots.some((root) => isUnder(path, root)))
|
|
33
61
|
.map((path) => dirname(path))
|
|
34
62
|
.filter((dir) => dir !== '.' && hasTests(dir)),
|
|
35
63
|
),
|
package/src/check/schema.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { BUN_COMMANDS } from '../config/detect'
|
|
3
2
|
import { tokenize } from './command-line'
|
|
3
|
+
import { CHECK_KIND } from './result'
|
|
4
4
|
|
|
5
5
|
const nonEmpty = z.string().min(1)
|
|
6
6
|
|
|
@@ -40,6 +40,11 @@ const projectCheck = z.strictObject({
|
|
|
40
40
|
command,
|
|
41
41
|
/** Without globs the check runs on every change; with them, only when a changed path matches. */
|
|
42
42
|
globs: globs.optional(),
|
|
43
|
+
/**
|
|
44
|
+
* `tests` for a check that runs the project's tests, `types` for its type check; without one, a plain project check.
|
|
45
|
+
* A test run counts towards "no green without a test", and both run with the slow checks.
|
|
46
|
+
*/
|
|
47
|
+
kind: z.enum([CHECK_KIND.tests, CHECK_KIND.types]).optional(),
|
|
43
48
|
})
|
|
44
49
|
|
|
45
50
|
const generatorOwned = z.strictObject({
|
|
@@ -67,9 +72,9 @@ const skillBudgets = z.record(glob, z.number().int().positive())
|
|
|
67
72
|
export const checkConfigSchema = z.strictObject({
|
|
68
73
|
/**
|
|
69
74
|
* Run once over the changed files, which are appended as arguments. Separate from `commands.lint`,
|
|
70
|
-
* which lints the whole project.
|
|
75
|
+
* which lints the whole project. Detected from the repository when left out; without one, no lint runs.
|
|
71
76
|
*/
|
|
72
|
-
lintFilesCommand: command.
|
|
77
|
+
lintFilesCommand: command.optional(),
|
|
73
78
|
/** Project checks, run alongside lint; add repository-specific scripts here. */
|
|
74
79
|
checks: z.array(projectCheck).default([]),
|
|
75
80
|
generatorOwned: z.array(generatorOwned).default([]),
|
package/src/check/settings.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { jackherdConfigSchema } from '../config'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
detectStack,
|
|
5
|
+
isDecided,
|
|
6
|
+
type RepositoryFacts,
|
|
7
|
+
type Stack,
|
|
8
|
+
type StackDetection,
|
|
9
|
+
withDetected,
|
|
10
|
+
} from '../config/detect'
|
|
4
11
|
import { PROJECT_CONFIG_PATH } from '../config/location'
|
|
5
12
|
import type { PlanLanguage } from '../vocabulary'
|
|
6
13
|
import type { PackageCommands } from './plan'
|
|
@@ -16,13 +23,21 @@ export type CheckSettings = {
|
|
|
16
23
|
planLanguage: PlanLanguage
|
|
17
24
|
/** The branch on the remote whose plans count as merged. */
|
|
18
25
|
baseBranch: string
|
|
26
|
+
/** Where the lane worktrees live, below the main checkout: a board's plans are read there too. */
|
|
27
|
+
worktreeDirectory: string
|
|
28
|
+
/** What the lock files (without one, the manifests) decide: one stack, none, or several (ambiguous); `stackOf` reads the stack from it. */
|
|
29
|
+
stack: StackDetection
|
|
19
30
|
notes: string[]
|
|
20
31
|
}
|
|
21
32
|
|
|
33
|
+
/** The stack a detection decided, by lock file or manifest; null for none or several, where the check reads every stack's markers. */
|
|
34
|
+
export const stackOf = (detection: StackDetection): Stack | null => (isDecided(detection) ? detection.stack : null)
|
|
35
|
+
|
|
22
36
|
const commandsSchema = jackherdConfigSchema.shape.commands
|
|
23
37
|
const planDirectorySchema = jackherdConfigSchema.shape.planDirectory
|
|
24
38
|
const planLanguageSchema = jackherdConfigSchema.shape.planLanguage
|
|
25
39
|
const baseBranchSchema = jackherdConfigSchema.shape.baseBranch
|
|
40
|
+
const worktreeDirectorySchema = jackherdConfigSchema.shape.worktreeDirectory
|
|
26
41
|
|
|
27
42
|
/**
|
|
28
43
|
* The check runner's view of `.jackherd/config.json`: the shared `commands` block, `planDirectory`, `planLanguage` and `baseBranch` plus its own
|
|
@@ -39,6 +54,8 @@ export const parseCheckSettings = (raw: unknown, facts: RepositoryFacts): CheckS
|
|
|
39
54
|
planDirectory: planDirectorySchema.parse(object.planDirectory),
|
|
40
55
|
planLanguage: planLanguageSchema.parse(object.planLanguage),
|
|
41
56
|
baseBranch: baseBranchSchema.parse(object.baseBranch),
|
|
57
|
+
worktreeDirectory: worktreeDirectorySchema.parse(object.worktreeDirectory),
|
|
58
|
+
stack: detectStack(facts.lockFiles, facts.manifests),
|
|
42
59
|
notes: detected.notes,
|
|
43
60
|
}
|
|
44
61
|
}
|
|
@@ -20,7 +20,8 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
20
20
|
'.claude/skills/grill/SKILL.md': 685,
|
|
21
21
|
'.claude/skills/implement/SKILL.md': 2190,
|
|
22
22
|
// P33: the paw in the header chip, panes by tab and title, the lane in the context line of a passed-on question.
|
|
23
|
-
|
|
23
|
+
// Plain words: no plan code without its words in a short question.
|
|
24
|
+
'.claude/skills/orchestrate/SKILL.md': 7920,
|
|
24
25
|
'.claude/skills/orchestrate/references/acceptance.md': 200,
|
|
25
26
|
'.claude/skills/orchestrate/references/close-lane.md': 250,
|
|
26
27
|
'.claude/skills/orchestrate/references/coordinator-handover.md': 551,
|
|
@@ -42,7 +43,7 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
42
43
|
'.claude/skills/start/SKILL.md': 607,
|
|
43
44
|
'.claude/skills/uncle-bob/SKILL.md': 466,
|
|
44
45
|
// P33: grows with the skill above.
|
|
45
|
-
'.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md':
|
|
46
|
+
'.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md': 8720,
|
|
46
47
|
'.claude/agents/devops.md': 104,
|
|
47
48
|
'.claude/agents/explore.md': 132,
|
|
48
49
|
'.claude/agents/implement.md': 303,
|
package/src/config/command.ts
CHANGED
|
@@ -2,35 +2,54 @@ import { homedir } from 'node:os'
|
|
|
2
2
|
import { mainCheckout, worktreeRoot } from '../plans/git'
|
|
3
3
|
import { CLI_NAME } from '../version/release'
|
|
4
4
|
import { readResolvedConfig } from './read-config'
|
|
5
|
-
import {
|
|
5
|
+
import { readRepositoryFacts } from './repository-facts'
|
|
6
|
+
import {
|
|
7
|
+
commandFindings,
|
|
8
|
+
formatRepositoryValues,
|
|
9
|
+
isRepositoryValue,
|
|
10
|
+
isRepositoryValueSet,
|
|
11
|
+
REPOSITORY_VALUE,
|
|
12
|
+
repositoryValuesOf,
|
|
13
|
+
valueFindings,
|
|
14
|
+
} from './repository-values'
|
|
6
15
|
|
|
7
|
-
const
|
|
16
|
+
export const CONFIG_EXIT = { printed: 0, failed: 1, usage: 2, unset: 3 } as const
|
|
8
17
|
|
|
9
|
-
const
|
|
18
|
+
const USAGE = `Usage: ${CLI_NAME} config [${Object.values(REPOSITORY_VALUE).join(' | ')}]
|
|
19
|
+
Exit codes: ${CONFIG_EXIT.printed} printed, ${CONFIG_EXIT.failed} failed, ${CONFIG_EXIT.usage} usage, ${CONFIG_EXIT.unset} the named value is not set (its finding goes to stderr).`
|
|
10
20
|
|
|
11
21
|
export type ConfigOutput = { log: (line: string) => void; error: (line: string) => void }
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Prints the values this repository decides, as the config sets them or detection finds them: all of them, or
|
|
15
|
-
* the one `key` names. Notes about values the repository did not decide
|
|
16
|
-
* the
|
|
25
|
+
* the one `key` names. Notes about values the repository did not decide, and a finding for each command or lock
|
|
26
|
+
* file that is missing or contradicts the repository (`commandFindings`), go to `error`, so a key's output stays
|
|
27
|
+
* the bare value; for a `key`, only the finding about that key (the notes for `baseBranch`). A `key` whose value
|
|
28
|
+
* is unset prints nothing and returns `CONFIG_EXIT.unset`; its missing finding says where to set it.
|
|
17
29
|
*/
|
|
18
30
|
export const printRepositoryValues = (args: string[], roots: string[], home: string, output: ConfigOutput): number => {
|
|
19
31
|
const [key, extra] = args
|
|
20
32
|
if (extra !== undefined) {
|
|
21
33
|
output.error(USAGE)
|
|
22
|
-
return
|
|
34
|
+
return CONFIG_EXIT.usage
|
|
23
35
|
}
|
|
24
36
|
if (key !== undefined && !isRepositoryValue(key)) {
|
|
25
37
|
output.error(`Unknown key: ${key}\n${USAGE}`)
|
|
26
|
-
return
|
|
38
|
+
return CONFIG_EXIT.usage
|
|
27
39
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
40
|
+
const facts = readRepositoryFacts(roots[0])
|
|
41
|
+
const { config, notes } = readResolvedConfig(roots, home, facts)
|
|
30
42
|
const values = repositoryValuesOf(config)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
if (key === undefined) {
|
|
44
|
+
for (const note of [...notes, ...commandFindings(config, facts)]) output.error(note)
|
|
45
|
+
for (const line of formatRepositoryValues(values)) output.log(line)
|
|
46
|
+
return CONFIG_EXIT.printed
|
|
47
|
+
}
|
|
48
|
+
const diagnostics = key === REPOSITORY_VALUE.baseBranch ? notes : valueFindings(config, facts, key)
|
|
49
|
+
for (const line of diagnostics) output.error(line)
|
|
50
|
+
if (!isRepositoryValueSet(config, key)) return CONFIG_EXIT.unset
|
|
51
|
+
output.log(values[key])
|
|
52
|
+
return CONFIG_EXIT.printed
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
export const run = (args: string[]): number => {
|
|
@@ -40,6 +59,6 @@ export const run = (args: string[]): number => {
|
|
|
40
59
|
return printRepositoryValues(args, [worktreeRoot(cwd), mainCheckout(cwd)], homedir(), output)
|
|
41
60
|
} catch (error) {
|
|
42
61
|
output.error(`${CLI_NAME} config failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
43
|
-
return
|
|
62
|
+
return CONFIG_EXIT.failed
|
|
44
63
|
}
|
|
45
64
|
}
|