@erclx/canon 5.44.0 → 5.46.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/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/REQUIREMENT.md +15 -31
- package/claude/skills/auto-ship/REQUIREMENT.md +14 -25
- package/claude/skills/backlog-triage/REQUIREMENT.md +12 -23
- package/claude/skills/build-in-slices/REQUIREMENT.md +15 -25
- package/claude/skills/canon-cli/REQUIREMENT.md +15 -27
- package/claude/skills/canon-cli/SKILL.md +0 -1
- package/claude/skills/canon-feedback/REQUIREMENT.md +10 -18
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +10 -15
- package/claude/skills/canvas-board/REQUIREMENT.md +14 -28
- package/claude/skills/ci-workflow/REQUIREMENT.md +12 -21
- package/claude/skills/code-craft/REQUIREMENT.md +14 -28
- package/claude/skills/codebase-layout/REQUIREMENT.md +14 -31
- package/claude/skills/context-fold/REQUIREMENT.md +15 -39
- package/claude/skills/context-fold/references/context.md +3 -3
- package/claude/skills/create-rule/REQUIREMENT.md +10 -24
- package/claude/skills/create-rule/SKILL.md +3 -0
- package/claude/skills/create-skill/REQUIREMENT.md +10 -21
- package/claude/skills/decision-escalate/REQUIREMENT.md +12 -24
- package/claude/skills/deploy-app/REQUIREMENT.md +12 -22
- package/claude/skills/deprecation-migration/REQUIREMENT.md +14 -42
- package/claude/skills/design-identity/REQUIREMENT.md +14 -20
- package/claude/skills/design-taste/REQUIREMENT.md +16 -57
- package/claude/skills/docs-sync/REQUIREMENT.md +14 -23
- package/claude/skills/document-health/REQUIREMENT.md +15 -29
- package/claude/skills/document-health/SKILL.md +2 -2
- package/claude/skills/document-repair/REQUIREMENT.md +32 -0
- package/claude/skills/document-repair/SKILL.md +76 -0
- package/claude/skills/document-repair/references/batches.md +44 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -3
- package/claude/skills/draft-doc/REQUIREMENT.md +4 -3
- package/claude/skills/draft-figure/REQUIREMENT.md +4 -3
- package/claude/skills/draft-identity/REQUIREMENT.md +4 -3
- package/claude/skills/draft-ready/REQUIREMENT.md +4 -3
- package/claude/skills/draft-screencast/REQUIREMENT.md +4 -3
- package/claude/skills/draft-slides/REQUIREMENT.md +4 -3
- package/claude/skills/draw-figure/REQUIREMENT.md +15 -39
- package/claude/skills/git-branch/REQUIREMENT.md +12 -26
- package/claude/skills/git-commit/REQUIREMENT.md +11 -15
- package/claude/skills/git-followup/REQUIREMENT.md +11 -22
- package/claude/skills/git-issue/REQUIREMENT.md +9 -15
- package/claude/skills/git-pr/REQUIREMENT.md +13 -32
- package/claude/skills/git-pr/SKILL.md +10 -2
- package/claude/skills/git-pr/references/closing-issues.md +47 -0
- package/claude/skills/git-pr/references/pr.md +4 -0
- package/claude/skills/git-ship/REQUIREMENT.md +13 -26
- package/claude/skills/git-split/REQUIREMENT.md +10 -17
- package/claude/skills/git-stage/REQUIREMENT.md +12 -18
- package/claude/skills/git-worktree/REQUIREMENT.md +15 -19
- package/claude/skills/make-deck/REQUIREMENT.md +13 -21
- package/claude/skills/markdown-craft/REQUIREMENT.md +9 -3
- package/claude/skills/pick-by-eye/REQUIREMENT.md +17 -67
- package/claude/skills/plan-feature/REQUIREMENT.md +15 -24
- package/claude/skills/plan-groundwork/REQUIREMENT.md +12 -28
- package/claude/skills/plan-groundwork/SKILL.md +5 -2
- package/claude/skills/plan-groundwork/references/output.md +47 -1
- package/claude/skills/plan-intake/REQUIREMENT.md +11 -26
- package/claude/skills/plan-intake/references/intake.md +2 -2
- package/claude/skills/plan-intake-answer/REQUIREMENT.md +14 -29
- package/claude/skills/plan-intake-answer/SKILL.md +7 -1
- package/claude/skills/plan-ready/REQUIREMENT.md +17 -36
- package/claude/skills/pr-evidence/REQUIREMENT.md +11 -17
- package/claude/skills/read-frames/REQUIREMENT.md +3 -8
- package/claude/skills/record-screencast/REQUIREMENT.md +14 -17
- package/claude/skills/review-address/REQUIREMENT.md +14 -38
- package/claude/skills/review-craft/REQUIREMENT.md +14 -32
- package/claude/skills/review-pr/REQUIREMENT.md +15 -34
- package/claude/skills/review-ui/REQUIREMENT.md +14 -39
- package/claude/skills/role-orchestrator/REQUIREMENT.md +16 -99
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +5 -0
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +9 -0
- package/claude/skills/role-planner/REQUIREMENT.md +15 -73
- package/claude/skills/role-reviewer/REQUIREMENT.md +15 -65
- package/claude/skills/role-reviewer/SKILL.md +5 -1
- package/claude/skills/role-worker/REQUIREMENT.md +13 -41
- package/claude/skills/role-worker/SKILL.md +2 -0
- package/claude/skills/script-screencast/REQUIREMENT.md +10 -16
- package/claude/skills/search-craft/REQUIREMENT.md +12 -36
- package/claude/skills/search-craft/SKILL.md +1 -1
- package/claude/skills/search-craft/references/adopted.md +2 -2
- package/claude/skills/session-compact/REQUIREMENT.md +13 -23
- package/claude/skills/session-map/REQUIREMENT.md +13 -29
- package/claude/skills/session-relay/REQUIREMENT.md +14 -58
- package/claude/skills/session-relay/SKILL.md +6 -0
- package/claude/skills/session-resume/REQUIREMENT.md +13 -27
- package/claude/skills/session-worktree/REQUIREMENT.md +14 -43
- package/claude/skills/systematic-debugging/REQUIREMENT.md +13 -34
- package/claude/skills/target-check/REQUIREMENT.md +16 -23
- package/claude/skills/target-setup/REQUIREMENT.md +11 -34
- package/claude/skills/target-setup/SKILL.md +2 -2
- package/claude/skills/target-setup/references/detect.md +0 -1
- package/claude/skills/target-setup/references/verify.md +2 -2
- package/claude/skills/task-board/REQUIREMENT.md +13 -22
- package/claude/skills/task-board/SKILL.md +7 -0
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/REQUIREMENT.md +11 -24
- package/claude/skills/task-complete/references/archive.md +1 -0
- package/claude/skills/teach-workspace/REQUIREMENT.md +14 -45
- package/claude/skills/teach-workspace/references/promotion.md +5 -5
- package/claude/skills/test-craft/REQUIREMENT.md +15 -46
- package/claude/skills/test-first/REQUIREMENT.md +14 -20
- package/claude/skills/ui-checklist/REQUIREMENT.md +14 -29
- package/claude/skills/ux-audit/REQUIREMENT.md +3 -7
- package/claude/skills/ux-measure/REQUIREMENT.md +14 -28
- package/claude/skills/video-craft/REQUIREMENT.md +14 -27
- package/claude/skills/write-doc/REQUIREMENT.md +14 -24
- package/claude/skills/write-human/REQUIREMENT.md +15 -27
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +9 -15
- package/docs/agents/context-audit.md +19 -12
- package/docs/agents/install-and-sync.md +2 -2
- package/docs/agents/markdown-audit.md +15 -10
- package/docs/agents/output-shape.md +1 -2
- package/docs/target/migrations.md +18 -0
- package/docs/target/projects.md +3 -2
- package/docs/target/scenarios.md +4 -1
- package/docs/workflow/skill-map-on-request.md +1 -1
- package/docs/workflow/skill-map.md +6 -5
- package/governance/detect.toml +4 -0
- package/governance/ledger/lang/go.md +61 -0
- package/governance/ledger/lang/php.md +50 -0
- package/governance/ledger/lang/python.md +68 -0
- package/governance/ledger/lang/typescript.md +64 -0
- package/governance/ledger/lib/database.md +65 -0
- package/governance/ledger/lib/e2e-reliability.md +57 -0
- package/governance/ledger/lib/pydantic.md +61 -0
- package/governance/ledger/lib/security-server.md +62 -0
- package/governance/ledger/lib/security-web.md +51 -0
- package/governance/ledger/lib/tanstack-query.md +57 -0
- package/governance/ledger/lib/test-scope.md +43 -0
- package/governance/ledger/lib/testing-go.md +57 -0
- package/governance/ledger/lib/testing-php.md +51 -0
- package/governance/ledger/lib/testing-py.md +67 -0
- package/governance/ledger/lib/testing-ts.md +63 -0
- package/governance/ledger/lib/zod.md +47 -0
- package/governance/ledger/tooling/ci-workflow.md +33 -0
- package/governance/ledger/tooling/code-comments.md +48 -0
- package/governance/ledger/tooling/config-comments.md +27 -0
- package/governance/ledger/tooling/dependencies.md +38 -0
- package/governance/ledger/ui/a11y.md +55 -0
- package/governance/ledger/ui/copy.md +49 -0
- package/governance/ledger/ui/design-reference.md +42 -0
- package/governance/ledger/ui/design-taste.md +19 -0
- package/governance/ledger/ui/forms.md +71 -0
- package/governance/ledger/ui/interface-casing.md +47 -0
- package/governance/ledger/ui/keyboard.md +68 -0
- package/governance/ledger/ui/link-behavior.md +29 -0
- package/governance/ledger/ui/motion.md +33 -0
- package/governance/ledger/ui/surface-capture.md +69 -0
- package/governance/ledger/ui/ux-completeness.md +79 -0
- package/governance/renames.toml +0 -2
- package/governance/rules/claude/output.md +5 -1
- package/governance/rules/code/routing.md +5 -1
- package/governance/rules/lang/go.md +7 -16
- package/governance/rules/lang/php.md +3 -3
- package/governance/rules/lang/python.md +9 -18
- package/governance/rules/lang/typescript.md +9 -19
- package/governance/rules/lib/database.md +5 -5
- package/governance/rules/lib/e2e-reliability.md +4 -6
- package/governance/rules/lib/pydantic.md +14 -16
- package/governance/rules/lib/security-server.md +4 -4
- package/governance/rules/lib/security-web.md +6 -10
- package/governance/rules/lib/tanstack-query.md +10 -12
- package/governance/rules/lib/test-scope.md +4 -4
- package/governance/rules/lib/testing-go.md +1 -2
- package/governance/rules/lib/testing-php.md +4 -5
- package/governance/rules/lib/testing-py.md +11 -19
- package/governance/rules/lib/testing-ts.md +7 -16
- package/governance/rules/lib/zod.md +6 -8
- package/governance/rules/tooling/code-comments.md +2 -4
- package/governance/rules/tooling/config-comments.md +1 -4
- package/governance/rules/tooling/dependencies.md +1 -1
- package/governance/rules/ui/a11y.md +16 -45
- package/governance/rules/ui/copy.md +6 -9
- package/governance/rules/ui/forms.md +13 -19
- package/governance/rules/ui/interface-casing.md +5 -7
- package/governance/rules/ui/keyboard.md +38 -0
- package/governance/rules/ui/link-behavior.md +3 -3
- package/governance/rules/ui/motion.md +3 -3
- package/governance/rules/ui/surface-capture.md +15 -27
- package/governance/rules/ui/ux-completeness.md +16 -39
- package/governance/stacks/base.toml +0 -1
- package/package.json +1 -1
- package/scripts/core/install-check.sh +1 -1
- package/src/claude/cases/workflow.ts +12 -0
- package/src/cli/help.ts +0 -1
- package/src/cli.ts +0 -2
- package/src/commands/context.ts +57 -3
- package/src/commands/gov.ts +4 -0
- package/src/commands/init.ts +2 -2
- package/src/commands/labels.ts +83 -3
- package/src/commands/markdown.ts +188 -21
- package/src/commands/sessions.ts +36 -3
- package/src/commands/tasks.ts +180 -0
- package/src/commands/tooling.ts +18 -5
- package/src/commands/worktrees.ts +2 -0
- package/src/context/coverage.ts +210 -0
- package/src/context/gate.ts +12 -3
- package/src/design/components.ts +1 -1
- package/src/gate/stages.ts +1 -1
- package/src/gov/audit.ts +22 -2
- package/src/init/plan.ts +1 -8
- package/src/init/steps.ts +44 -7
- package/src/intake/items.ts +14 -0
- package/src/inventory/subjects.ts +1 -1
- package/src/labels/closing.ts +46 -0
- package/src/markdown/gated.ts +250 -0
- package/src/markdown/scope.ts +119 -2
- package/src/migrate/rename.ts +6 -0
- package/src/migrate/scratch-evidence.ts +12 -4
- package/src/records/backup.ts +3 -3
- package/src/records/validate.ts +3 -3
- package/src/roots/record.ts +17 -8
- package/src/sessions/claim.ts +7 -0
- package/src/sessions/live.ts +53 -6
- package/src/sessions/registry.ts +11 -0
- package/src/sessions/resolve.ts +32 -5
- package/src/tasks/closes.ts +136 -0
- package/src/tasks/record.ts +1 -1
- package/src/tooling/inject.ts +26 -7
- package/src/tooling/package.ts +32 -0
- package/src/tooling/read.ts +46 -3
- package/src/tooling/scan.ts +8 -2
- package/src/worktrees/reclaim.ts +16 -1
- package/tooling/astro/reference.md +64 -93
- package/tooling/base/configs/.github/workflows/phase-label-gate.yml +1 -1
- package/tooling/base/configs/scripts/verify.ts +2 -2
- package/tooling/base/reference.md +1 -9
- package/tooling/base/seeds/canon/context/{ci.md → development/ci.md} +1 -1
- package/tooling/base/seeds/canon/context/development/index.md +11 -0
- package/tooling/base/seeds/canon/context/{development.md → development/setup.md} +8 -12
- package/tooling/base/seeds/canon/context/index.md +1 -2
- package/tooling/claude/manifest.toml +1 -5
- package/tooling/claude/reference.md +36 -59
- package/tooling/claude/seeds/.claude/hooks/board-validate.sh +1 -1
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +2 -7
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -7
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +3 -10
- package/tooling/claude/seeds/.claude/hooks/search-reminder.sh +2 -7
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +2 -4
- package/tooling/claude/seeds/CLAUDE.md +2 -2
- package/tooling/nextjs/reference.md +0 -6
- package/tooling/php/reference.md +0 -4
- package/tooling/web/configs/.github/workflows/verify.yml +28 -0
- package/tooling/web/configs/.oxlintrc.json +7 -0
- package/tooling/web/configs/e2e/capture-types.ts +35 -0
- package/tooling/web/configs/e2e/screenshot.ts +23 -72
- package/tooling/web/manifest.toml +1 -1
- package/tooling/web/reference.md +77 -124
- package/tooling/web/seeds/e2e/capture/cases.ts +47 -0
- package/governance/rules/canon/task-scope.md +0 -12
- package/src/commands/wiki.ts +0 -100
- package/src/wiki/init.ts +0 -85
package/src/records/backup.ts
CHANGED
|
@@ -107,9 +107,9 @@ export const EXCLUDED_ENTRIES = [
|
|
|
107
107
|
/**
|
|
108
108
|
* Whether a top-level name stays out of every push, at either root.
|
|
109
109
|
*
|
|
110
|
-
* `SCRATCH` is the
|
|
111
|
-
*
|
|
112
|
-
* spelling only.
|
|
110
|
+
* `SCRATCH` is the retired `.tmp` spelling. It is a guard rather than a lookup,
|
|
111
|
+
* keeping a stray `.tmp` out of a push, and `EXCLUDED_ENTRIES` names the
|
|
112
|
+
* `.canon` spelling only.
|
|
113
113
|
*/
|
|
114
114
|
function isExcluded(name: string): boolean {
|
|
115
115
|
return (
|
package/src/records/validate.ts
CHANGED
|
@@ -228,9 +228,9 @@ const PLAN_REQUIRED: readonly PlanSection[] = [
|
|
|
228
228
|
]
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
|
-
* The files whose edit changes what a person sees, the set the `ui/a11y`
|
|
232
|
-
*
|
|
233
|
-
* one owes a `Design references` section, so a set too wide forces it on every
|
|
231
|
+
* The files whose edit changes what a person sees, the set the `ui/a11y` and
|
|
232
|
+
* `ui/keyboard` rules glob plus the other markup and style extensions a plan
|
|
233
|
+
* names. A plan touching one owes a `Design references` section, so a set too wide forces it on every
|
|
234
234
|
* plan and one too narrow lets a UI plan skip it.
|
|
235
235
|
*/
|
|
236
236
|
const RENDERED_SURFACE = /\.(?:tsx|jsx|astro|vue|svelte|html|css|scss)$/
|
package/src/roots/record.ts
CHANGED
|
@@ -29,17 +29,22 @@ export type RecordRoot = (typeof RECORD_ROOTS)[number]
|
|
|
29
29
|
export const CREATION_ROOT: RecordRoot = '.canon'
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* The deletable scratch folder, named at the spelling `.claude/`
|
|
32
|
+
* The deletable scratch folder, named at the spelling `.claude/` gave it.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
39
|
-
*
|
|
34
|
+
* Scratch resolves at `.canon/tmp` only. The `.claude/.tmp` spelling is a
|
|
35
|
+
* migration source, named through `LEGACY_SCRATCH_REL` by the verbs that move
|
|
36
|
+
* it, and no lookup falls back to it. Callers still name the folder at this
|
|
37
|
+
* spelling because `spell` is the only place the variant is decided. Inside a
|
|
38
|
+
* dotted root the leading dot hides nothing already hidden and costs a bare
|
|
39
|
+
* `ls` that omits the folder, so the move dropped it. Every other record
|
|
40
|
+
* folder keeps its name, `.records.git` included, where the dot marks the
|
|
41
|
+
* mechanism apart from a payload rather than hiding it.
|
|
40
42
|
*/
|
|
41
43
|
export const SCRATCH = '.tmp'
|
|
42
44
|
|
|
45
|
+
/** The retired scratch location, read only as the source of a migration. */
|
|
46
|
+
export const LEGACY_SCRATCH_REL = '.claude/.tmp' // canon-keep-record-root
|
|
47
|
+
|
|
43
48
|
/** The scratch folder's name under `.canon/`. */
|
|
44
49
|
const CANON_SCRATCH = 'tmp'
|
|
45
50
|
|
|
@@ -109,6 +114,8 @@ export function spell(root: RecordRoot, folder: string): string {
|
|
|
109
114
|
* records it belongs to rather than at the creation default.
|
|
110
115
|
*/
|
|
111
116
|
function rootOf(root: string, folder: string): RecordRoot {
|
|
117
|
+
if (folder === SCRATCH) return CREATION_ROOT
|
|
118
|
+
|
|
112
119
|
return (
|
|
113
120
|
RECORD_ROOTS.find((candidate) =>
|
|
114
121
|
existsSync(join(root, candidate, spell(candidate, folder))),
|
|
@@ -147,7 +154,9 @@ export function recordDirs(
|
|
|
147
154
|
folder: string,
|
|
148
155
|
...rest: string[]
|
|
149
156
|
): string[] {
|
|
150
|
-
|
|
157
|
+
const candidates = folder === SCRATCH ? [CREATION_ROOT] : RECORD_ROOTS
|
|
158
|
+
|
|
159
|
+
return candidates.map((candidate) =>
|
|
151
160
|
join(root, candidate, spell(candidate, folder), ...rest),
|
|
152
161
|
)
|
|
153
162
|
}
|
package/src/sessions/claim.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface ClaimReport {
|
|
|
19
19
|
readonly refs: readonly string[]
|
|
20
20
|
/** False when the session roster could not be read, so `claimed` covers the readings around it alone and cannot be trusted as a clean "unclaimed". */
|
|
21
21
|
readonly sessionsReadable: boolean
|
|
22
|
+
/**
|
|
23
|
+
* Foreign-namespace records whose socket is gone. They read dead, so a branch
|
|
24
|
+
* one of them holds can read unclaimed, which a caller with a count above
|
|
25
|
+
* zero should treat as unverified.
|
|
26
|
+
*/
|
|
27
|
+
readonly foreignDead: number
|
|
22
28
|
/** False when the ref read failed, which is the same false clean under a different reading. */
|
|
23
29
|
readonly refsReadable: boolean
|
|
24
30
|
}
|
|
@@ -80,6 +86,7 @@ export async function checkClaim(
|
|
|
80
86
|
sessions,
|
|
81
87
|
refs: refs.refs,
|
|
82
88
|
sessionsReadable: report.kind === 'resolved',
|
|
89
|
+
foreignDead: report.kind === 'resolved' ? report.foreignDead : 0,
|
|
83
90
|
refsReadable: refs.readable,
|
|
84
91
|
}
|
|
85
92
|
}
|
package/src/sessions/live.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readFileSync, readlinkSync } from 'node:fs'
|
|
2
2
|
import type { SessionRecord } from '@/sessions/registry'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -7,13 +7,19 @@ import type { SessionRecord } from '@/sessions/registry'
|
|
|
7
7
|
* `confirmed` means the running process was matched against the start time the
|
|
8
8
|
* record stamped, so the pid belongs to the session that wrote the file.
|
|
9
9
|
* `unverified` means the pid answers a signal probe and nothing more, which
|
|
10
|
-
* cannot separate the original session from whatever inherited its pid
|
|
10
|
+
* cannot separate the original session from whatever inherited its pid, or,
|
|
11
|
+
* for a `foreign` row, that its messaging socket still exists.
|
|
11
12
|
*/
|
|
12
13
|
export type Confidence = 'confirmed' | 'unverified'
|
|
13
14
|
|
|
14
15
|
export interface Liveness {
|
|
15
16
|
readonly alive: boolean
|
|
16
17
|
readonly confidence: Confidence
|
|
18
|
+
/**
|
|
19
|
+
* Set only when the record belongs to another pid namespace, so the pid was
|
|
20
|
+
* never probed. Absent rather than false on every other path.
|
|
21
|
+
*/
|
|
22
|
+
readonly foreign?: true
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export interface LivenessProbes {
|
|
@@ -21,6 +27,14 @@ export interface LivenessProbes {
|
|
|
21
27
|
readonly procStartOf: (pid: number) => string | null
|
|
22
28
|
/** Whether the pid answers at all, ignoring which process holds it. */
|
|
23
29
|
readonly responds: (pid: number) => boolean
|
|
30
|
+
/**
|
|
31
|
+
* The reader's own pid namespace in `pid:[inode]` form, or null where it
|
|
32
|
+
* cannot be read. Optional so a caller with no use for the foreign branch,
|
|
33
|
+
* such as the gate's lock, states nothing and takes the pid path.
|
|
34
|
+
*/
|
|
35
|
+
readonly ownDomain?: () => string | null
|
|
36
|
+
/** Whether the messaging socket a foreign record names still exists. */
|
|
37
|
+
readonly socketExists?: (path: string) => boolean
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
/**
|
|
@@ -51,7 +65,21 @@ function responds(pid: number): boolean {
|
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
/** The link target reads `pid:[inode]`, which a record's `pidDomain` ends with. */
|
|
69
|
+
function ownDomain(): string | null {
|
|
70
|
+
try {
|
|
71
|
+
return readlinkSync('/proc/self/ns/pid')
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const SYSTEM_PROBES: LivenessProbes = {
|
|
78
|
+
procStartOf,
|
|
79
|
+
responds,
|
|
80
|
+
ownDomain,
|
|
81
|
+
socketExists: existsSync,
|
|
82
|
+
}
|
|
55
83
|
|
|
56
84
|
/**
|
|
57
85
|
* Decides whether a record describes a session that is still running.
|
|
@@ -62,13 +90,32 @@ export const SYSTEM_PROBES: LivenessProbes = { procStartOf, responds }
|
|
|
62
90
|
* probe alone stands, and every surviving row is marked so a caller never reads
|
|
63
91
|
* a recycled pid as a confirmed identity.
|
|
64
92
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
93
|
+
* A record from another pid namespace is judged first, on its messaging
|
|
94
|
+
* socket, because its pid names nothing in this namespace and either probe
|
|
95
|
+
* would read an unrelated process or none. That answer is always `unverified`,
|
|
96
|
+
* since a crashed session leaves its socket behind.
|
|
97
|
+
*
|
|
98
|
+
* The pid and start time are the only fields the rest needs, so a record that
|
|
99
|
+
* is not a session, such as the gate's machine lock, carries the same proof and
|
|
100
|
+
* takes the pid path for want of a `pidDomain`.
|
|
67
101
|
*/
|
|
68
102
|
export function liveness(
|
|
69
|
-
record: Pick<SessionRecord, 'pid' | 'procStart'
|
|
103
|
+
record: Pick<SessionRecord, 'pid' | 'procStart'> &
|
|
104
|
+
Partial<Pick<SessionRecord, 'pidDomain' | 'messagingSocketPath'>>,
|
|
70
105
|
probes: LivenessProbes = SYSTEM_PROBES,
|
|
71
106
|
): Liveness {
|
|
107
|
+
const own = probes.ownDomain?.() ?? null
|
|
108
|
+
if (
|
|
109
|
+
own !== null &&
|
|
110
|
+
record.pidDomain !== undefined &&
|
|
111
|
+
!record.pidDomain.endsWith(own)
|
|
112
|
+
) {
|
|
113
|
+
const socket = record.messagingSocketPath
|
|
114
|
+
const alive =
|
|
115
|
+
socket !== undefined && (probes.socketExists?.(socket) ?? false)
|
|
116
|
+
return { alive, confidence: 'unverified', foreign: true }
|
|
117
|
+
}
|
|
118
|
+
|
|
72
119
|
const started = record.procStart
|
|
73
120
|
|
|
74
121
|
if (started !== undefined && started.length > 0) {
|
package/src/sessions/registry.ts
CHANGED
|
@@ -45,6 +45,17 @@ export interface SessionRecord {
|
|
|
45
45
|
* been handed to something else.
|
|
46
46
|
*/
|
|
47
47
|
readonly procStart: string | undefined
|
|
48
|
+
/**
|
|
49
|
+
* The pid namespace the client stamped, ending in the `pid:[inode]` form that
|
|
50
|
+
* `/proc/self/ns/pid` reads as. A reader in another namespace cannot see the
|
|
51
|
+
* record's pid, so this is what tells a foreign row from a dead one.
|
|
52
|
+
*/
|
|
53
|
+
readonly pidDomain: string | undefined
|
|
54
|
+
/**
|
|
55
|
+
* The socket the session listens on for messages. It is the only evidence of
|
|
56
|
+
* life a foreign-namespace reader can reach, since the pid is out of sight.
|
|
57
|
+
*/
|
|
58
|
+
readonly messagingSocketPath: string | undefined
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
/**
|
package/src/sessions/resolve.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface ResolvedSession {
|
|
|
22
22
|
readonly cwd: string
|
|
23
23
|
readonly kind: string
|
|
24
24
|
readonly status: string
|
|
25
|
+
/**
|
|
26
|
+
* What decided the row was live. `socket` means the record belongs to another
|
|
27
|
+
* pid namespace and only its messaging socket answered, so the row can be a
|
|
28
|
+
* crashed session's leftover and no start time backs it.
|
|
29
|
+
*/
|
|
30
|
+
readonly via: 'pid' | 'socket'
|
|
25
31
|
readonly startedAt: string | null
|
|
26
32
|
/**
|
|
27
33
|
* The stamp the record carries beside `status`, kept alongside the dwell it
|
|
@@ -64,6 +70,13 @@ export type SessionReport =
|
|
|
64
70
|
* tells a caller whether the roster can be trusted as an identity.
|
|
65
71
|
*/
|
|
66
72
|
readonly confidence: Confidence
|
|
73
|
+
/**
|
|
74
|
+
* Foreign-namespace records whose socket is gone. They read dead, but the
|
|
75
|
+
* reader could not check their pid, so an empty roster beside a count
|
|
76
|
+
* above zero is a roster that could not be checked rather than one that
|
|
77
|
+
* ended.
|
|
78
|
+
*/
|
|
79
|
+
readonly foreignDead: number
|
|
67
80
|
readonly sessions: readonly ResolvedSession[]
|
|
68
81
|
}
|
|
69
82
|
|
|
@@ -167,6 +180,7 @@ function present(
|
|
|
167
180
|
record: SessionRecord,
|
|
168
181
|
located: Located,
|
|
169
182
|
now: number,
|
|
183
|
+
via: ResolvedSession['via'],
|
|
170
184
|
): ResolvedSession {
|
|
171
185
|
return {
|
|
172
186
|
name: record.name,
|
|
@@ -175,6 +189,7 @@ function present(
|
|
|
175
189
|
cwd: record.cwd,
|
|
176
190
|
kind: record.kind ?? 'unknown',
|
|
177
191
|
status: record.status ?? 'unknown',
|
|
192
|
+
via,
|
|
178
193
|
startedAt:
|
|
179
194
|
record.startedAt === undefined
|
|
180
195
|
? null
|
|
@@ -213,21 +228,33 @@ export async function resolveSessions(
|
|
|
213
228
|
|
|
214
229
|
if (registry.kind === 'absent') return { kind: 'absent', dir: registry.dir }
|
|
215
230
|
|
|
216
|
-
const live: SessionRecord[] = []
|
|
231
|
+
const live: { record: SessionRecord; via: ResolvedSession['via'] }[] = []
|
|
217
232
|
let confidence: Confidence = 'confirmed'
|
|
233
|
+
let foreignDead = 0
|
|
218
234
|
|
|
219
235
|
for (const record of registry.records) {
|
|
220
236
|
const state = liveness(record, probes)
|
|
221
|
-
if (!state.alive)
|
|
237
|
+
if (!state.alive) {
|
|
238
|
+
if (state.foreign) foreignDead += 1
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
222
241
|
if (state.confidence === 'unverified') confidence = 'unverified'
|
|
223
|
-
live.push(record)
|
|
242
|
+
live.push({ record, via: state.foreign ? 'socket' : 'pid' })
|
|
224
243
|
}
|
|
225
244
|
|
|
226
245
|
const sessions = await Promise.all(
|
|
227
|
-
live.map(async (
|
|
246
|
+
live.map(async ({ record, via }) =>
|
|
247
|
+
present(record, await find(record.cwd), now, via),
|
|
248
|
+
),
|
|
228
249
|
)
|
|
229
250
|
|
|
230
|
-
return {
|
|
251
|
+
return {
|
|
252
|
+
kind: 'resolved',
|
|
253
|
+
dir: registry.dir,
|
|
254
|
+
confidence,
|
|
255
|
+
foreignDead,
|
|
256
|
+
sessions,
|
|
257
|
+
}
|
|
231
258
|
}
|
|
232
259
|
|
|
233
260
|
/** The environment slice the caller's own identity is read from. */
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import {
|
|
3
|
+
fenceMask,
|
|
4
|
+
readOutcomes,
|
|
5
|
+
readPendingBranches,
|
|
6
|
+
readPullRequest,
|
|
7
|
+
} from '@/tasks/archive'
|
|
8
|
+
import {
|
|
9
|
+
openTask,
|
|
10
|
+
type RecordRefused,
|
|
11
|
+
type RecordSelector,
|
|
12
|
+
} from '@/tasks/record'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Why a task does or does not close its issues. `last-slice` is the only
|
|
16
|
+
* reason that closes. The rest mirror the conditions `archiveTask` applies at
|
|
17
|
+
* merge time, so an issue closes exactly when the task archives.
|
|
18
|
+
*/
|
|
19
|
+
export type ClosingReason =
|
|
20
|
+
| 'earlier-slice'
|
|
21
|
+
| 'last-slice'
|
|
22
|
+
| 'lookup-failed'
|
|
23
|
+
| 'no-issues'
|
|
24
|
+
| 'no-outcomes'
|
|
25
|
+
| 'open-outcomes'
|
|
26
|
+
| 'pending-branch'
|
|
27
|
+
|
|
28
|
+
export interface Closure {
|
|
29
|
+
readonly issues: readonly number[]
|
|
30
|
+
readonly closing: boolean
|
|
31
|
+
readonly reason: ClosingReason
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TaskClosure extends Closure {
|
|
35
|
+
readonly ok: true
|
|
36
|
+
readonly stem: string
|
|
37
|
+
readonly path: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ClosureOutcome = TaskClosure | RecordRefused
|
|
41
|
+
|
|
42
|
+
const ISSUE_LINE = /^Issue:[ \t]*(#\d+(?:[ \t]*,[ \t]*#\d+)*)[ \t]*$/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads every `Issue:` line outside a fence, each a bare `#NNN` or a comma
|
|
46
|
+
* list of them. A line carrying anything else, such as the older bare-path
|
|
47
|
+
* form, names no issue rather than the numbers that happened to parse, since a
|
|
48
|
+
* wrong number would close an unrelated issue.
|
|
49
|
+
*/
|
|
50
|
+
export function readIssues(text: string): readonly number[] {
|
|
51
|
+
const lines = text.split('\n')
|
|
52
|
+
const fenced = fenceMask(lines)
|
|
53
|
+
const issues = new Set<number>()
|
|
54
|
+
|
|
55
|
+
for (const [index, line] of lines.entries()) {
|
|
56
|
+
if (fenced[index]) continue
|
|
57
|
+
|
|
58
|
+
const match = ISSUE_LINE.exec(line)
|
|
59
|
+
if (!match) continue
|
|
60
|
+
|
|
61
|
+
for (const entry of match[1].split(',')) {
|
|
62
|
+
issues.add(Number(entry.trim().slice(1)))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [...issues]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decides whether the pull request for `branches` closes the task's issues.
|
|
71
|
+
* `branches` is the working branch and every name it carried, since a rename
|
|
72
|
+
* leaves the `Pending branch:` marker under the old one. Any other pending
|
|
73
|
+
* branch is a slice still to ship, so the task stays open behind it.
|
|
74
|
+
*/
|
|
75
|
+
export function decideClosing(
|
|
76
|
+
text: string,
|
|
77
|
+
branches: readonly string[],
|
|
78
|
+
pulls: readonly number[] | null = [],
|
|
79
|
+
): Closure {
|
|
80
|
+
const issues = readIssues(text)
|
|
81
|
+
if (issues.length === 0) {
|
|
82
|
+
return { issues, closing: false, reason: 'no-issues' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const listed = readPullRequest(text)
|
|
86
|
+
const last = listed.at(-1)
|
|
87
|
+
|
|
88
|
+
// A failed lookup cannot rule out that this branch is an earlier slice, and
|
|
89
|
+
// closing early is the unsafe direction. With nothing recorded yet there is
|
|
90
|
+
// no earlier slice to confuse it with, so that case still closes.
|
|
91
|
+
if (pulls === null && listed.length > 0) {
|
|
92
|
+
return { issues, closing: false, reason: 'lookup-failed' }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
last !== undefined &&
|
|
97
|
+
pulls !== null &&
|
|
98
|
+
!pulls.includes(last) &&
|
|
99
|
+
pulls.some((number) => listed.includes(number))
|
|
100
|
+
) {
|
|
101
|
+
return { issues, closing: false, reason: 'earlier-slice' }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const others = readPendingBranches(text).filter(
|
|
105
|
+
(branch) => !branches.includes(branch),
|
|
106
|
+
)
|
|
107
|
+
if (others.length > 0) {
|
|
108
|
+
return { issues, closing: false, reason: 'pending-branch' }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { open, closed, cut } = readOutcomes(text)
|
|
112
|
+
if (open.length > 0) {
|
|
113
|
+
return { issues, closing: false, reason: 'open-outcomes' }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (closed.length === 0 && cut.length === 0) {
|
|
117
|
+
return { issues, closing: false, reason: 'no-outcomes' }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { issues, closing: true, reason: 'last-slice' }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Resolves the task through the same selector `pull-request` takes. */
|
|
124
|
+
export async function readClosure(
|
|
125
|
+
root: string,
|
|
126
|
+
selector: RecordSelector,
|
|
127
|
+
branches: readonly string[],
|
|
128
|
+
pulls: readonly number[] | null = [],
|
|
129
|
+
): Promise<ClosureOutcome> {
|
|
130
|
+
const opened = await openTask(root, selector)
|
|
131
|
+
if ('ok' in opened) return opened
|
|
132
|
+
|
|
133
|
+
const text = await readFile(opened.path, 'utf8')
|
|
134
|
+
|
|
135
|
+
return { ok: true, ...opened, ...decideClosing(text, branches, pulls) }
|
|
136
|
+
}
|
package/src/tasks/record.ts
CHANGED
|
@@ -400,7 +400,7 @@ async function resolveStem(
|
|
|
400
400
|
return matched[0]
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
async function openTask(
|
|
403
|
+
export async function openTask(
|
|
404
404
|
root: string,
|
|
405
405
|
selector: RecordSelector,
|
|
406
406
|
): Promise<{ readonly stem: string; readonly path: string } | RecordRefused> {
|
package/src/tooling/inject.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
applyScripts,
|
|
18
18
|
collectDeps,
|
|
19
|
+
declaresInstalls,
|
|
20
|
+
minimalPackage,
|
|
19
21
|
readPackage,
|
|
20
22
|
serializePackage,
|
|
21
23
|
} from '@/tooling/package'
|
|
@@ -156,15 +158,22 @@ export async function pruneGitignore(
|
|
|
156
158
|
|
|
157
159
|
/**
|
|
158
160
|
* Installs missing dev dependencies, fills package scripts, and merges
|
|
159
|
-
* gitignore entries. A target with no package.json gets
|
|
160
|
-
*
|
|
161
|
-
*
|
|
161
|
+
* gitignore entries. A target with no package.json gets a minimal one first
|
|
162
|
+
* when the chain installs anything. A package.json that exists but cannot be
|
|
163
|
+
* parsed is never rewritten, and the install is skipped with a warning.
|
|
162
164
|
*/
|
|
163
165
|
export async function injectManifest(
|
|
164
166
|
chain: readonly Manifest[],
|
|
165
167
|
target: string,
|
|
166
168
|
): Promise<void> {
|
|
167
169
|
const packagePath = join(target, 'package.json')
|
|
170
|
+
|
|
171
|
+
if (!existsSync(packagePath) && declaresInstalls(chain)) {
|
|
172
|
+
logStep('Writing package.json')
|
|
173
|
+
await writeFile(packagePath, serializePackage(minimalPackage(target)))
|
|
174
|
+
logAdd('package.json')
|
|
175
|
+
}
|
|
176
|
+
|
|
168
177
|
const pkg = readPackage(packagePath)
|
|
169
178
|
|
|
170
179
|
if (pkg) {
|
|
@@ -192,14 +201,22 @@ export async function injectManifest(
|
|
|
192
201
|
|
|
193
202
|
await injectGitignore(chain, target)
|
|
194
203
|
|
|
195
|
-
if (!pkg)
|
|
204
|
+
if (!pkg) warnUnreadablePackage(chain, target)
|
|
196
205
|
}
|
|
197
206
|
|
|
198
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Only an existing package.json that fails to parse reaches here with installs
|
|
209
|
+
* pending, since an absent one was written above. It is left untouched.
|
|
210
|
+
*/
|
|
211
|
+
function warnUnreadablePackage(
|
|
212
|
+
chain: readonly Manifest[],
|
|
213
|
+
target: string,
|
|
214
|
+
): void {
|
|
215
|
+
if (!declaresInstalls(chain)) return
|
|
216
|
+
|
|
199
217
|
const deps = collectDeps(chain, {}).map((dep) => dep.spec)
|
|
200
218
|
const skipped = applyScripts({}, chain)
|
|
201
219
|
const scripts = [...new Set([...skipped.added, ...skipped.overridden])]
|
|
202
|
-
if (deps.length === 0 && scripts.length === 0) return
|
|
203
220
|
|
|
204
221
|
logStep('Skipped installs')
|
|
205
222
|
if (deps.length > 0) {
|
|
@@ -208,5 +225,7 @@ function warnSkippedInstalls(chain: readonly Manifest[], target: string): void {
|
|
|
208
225
|
if (scripts.length > 0) {
|
|
209
226
|
logWarn(`Scripts not added: ${scripts.join(', ')}`)
|
|
210
227
|
}
|
|
211
|
-
logWarn(
|
|
228
|
+
logWarn(
|
|
229
|
+
`package.json is not valid JSON: fix it in ${target}, then sync again`,
|
|
230
|
+
)
|
|
212
231
|
}
|
package/src/tooling/package.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { basename } from 'node:path'
|
|
2
3
|
import type { Manifest } from '@/tooling/manifest'
|
|
3
4
|
|
|
4
5
|
export interface PackageJson {
|
|
@@ -28,6 +29,37 @@ export function readPackage(path: string): PackageJson | undefined {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Whether the chain asks for any dev dependency or script, which is what makes
|
|
34
|
+
* a missing package.json worth writing. A stack that installs nothing, such as
|
|
35
|
+
* the one `canon claude` drives, leaves a project without a manifest alone.
|
|
36
|
+
*/
|
|
37
|
+
export function declaresInstalls(chain: readonly Manifest[]): boolean {
|
|
38
|
+
if (collectDeps(chain, {}).length > 0) return true
|
|
39
|
+
|
|
40
|
+
const scripts = applyScripts({}, chain)
|
|
41
|
+
return scripts.added.length > 0 || scripts.overridden.length > 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The manifest a sync writes into a folder that has none. The name is the
|
|
46
|
+
* folder's, reduced to the characters npm accepts, so it is never empty or
|
|
47
|
+
* invalid.
|
|
48
|
+
*/
|
|
49
|
+
export function minimalPackage(target: string): PackageJson {
|
|
50
|
+
const name = basename(target)
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/[^a-z0-9-._]/g, '-')
|
|
53
|
+
.replace(/^[._]+/, '')
|
|
54
|
+
.slice(0, 214)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: name === '' ? 'project' : name,
|
|
58
|
+
version: '0.0.0',
|
|
59
|
+
type: 'module',
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
export function serializePackage(pkg: PackageJson): string {
|
|
32
64
|
return `${JSON.stringify(pkg, null, 2)}\n`
|
|
33
65
|
}
|
package/src/tooling/read.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { PROJECT_ROOT } from '@/roots/project'
|
|
4
|
+
import { configFiles, type Manifest, seedFiles } from '@/tooling/manifest'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Spells the package root in a report where every other root spells a
|
|
@@ -11,6 +12,8 @@ import { PROJECT_ROOT } from '@/roots/project'
|
|
|
11
12
|
const PACKAGE_LABEL = '<canon>'
|
|
12
13
|
|
|
13
14
|
export interface ReferenceRoot {
|
|
15
|
+
/** The root whose `tooling/` this searches, as the manifest loader takes it. */
|
|
16
|
+
readonly root: string
|
|
14
17
|
/** Absolute `tooling/` directory to search. */
|
|
15
18
|
readonly dir: string
|
|
16
19
|
/** How a report spells a copy found under this root. */
|
|
@@ -19,6 +22,8 @@ export interface ReferenceRoot {
|
|
|
19
22
|
|
|
20
23
|
export interface ResolvedReference {
|
|
21
24
|
readonly path: string
|
|
25
|
+
/** The root that won, so the manifest is read from the same copy. */
|
|
26
|
+
readonly root: string
|
|
22
27
|
/** The label of the root that won, joined to the stack and filename. */
|
|
23
28
|
readonly source: string
|
|
24
29
|
}
|
|
@@ -36,8 +41,9 @@ function toolingSourceDir(root: string): string {
|
|
|
36
41
|
*/
|
|
37
42
|
export function referenceRoots(root: string): ReferenceRoot[] {
|
|
38
43
|
return [
|
|
39
|
-
{ dir: toolingSourceDir(root), label: 'tooling' },
|
|
44
|
+
{ root, dir: toolingSourceDir(root), label: 'tooling' },
|
|
40
45
|
{
|
|
46
|
+
root: PROJECT_ROOT,
|
|
41
47
|
dir: toolingSourceDir(PROJECT_ROOT),
|
|
42
48
|
label: join(PACKAGE_LABEL, 'tooling'),
|
|
43
49
|
},
|
|
@@ -49,10 +55,10 @@ export function resolveReference(
|
|
|
49
55
|
root: string,
|
|
50
56
|
stack: string,
|
|
51
57
|
): ResolvedReference | undefined {
|
|
52
|
-
for (const { dir, label } of referenceRoots(root)) {
|
|
58
|
+
for (const { root: owner, dir, label } of referenceRoots(root)) {
|
|
53
59
|
const path = join(dir, stack, 'reference.md')
|
|
54
60
|
if (existsSync(path)) {
|
|
55
|
-
return { path, source: join(label, stack, 'reference.md') }
|
|
61
|
+
return { path, root: owner, source: join(label, stack, 'reference.md') }
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -62,3 +68,40 @@ export function resolveReference(
|
|
|
62
68
|
export function readReference(reference: ResolvedReference): string {
|
|
63
69
|
return readFileSync(reference.path, 'utf8')
|
|
64
70
|
}
|
|
71
|
+
|
|
72
|
+
function codeList(items: readonly string[]): string {
|
|
73
|
+
return items.map((item) => `- \`${item}\``).join('\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The sections the verb prints after a stack's reference, read from its
|
|
78
|
+
* manifest so no reference keeps a hand list that drifts from it. A stack lists
|
|
79
|
+
* its own files and groups alone, since a parent's print under the parent's
|
|
80
|
+
* reference, and a section with nothing to list prints no heading.
|
|
81
|
+
*/
|
|
82
|
+
export function renderStackSections(manifest: Manifest): string {
|
|
83
|
+
const sections: string[] = []
|
|
84
|
+
const configs = configFiles(manifest)
|
|
85
|
+
const seeds = seedFiles(manifest)
|
|
86
|
+
|
|
87
|
+
if (configs.length > 0 || seeds.length > 0) {
|
|
88
|
+
const sync = ['## Sync']
|
|
89
|
+
if (configs.length > 0) {
|
|
90
|
+
sync.push('Sync keeps these current, from `configs/`:', codeList(configs))
|
|
91
|
+
}
|
|
92
|
+
if (seeds.length > 0) {
|
|
93
|
+
sync.push('Sync copies these once, from `seeds/`:', codeList(seeds))
|
|
94
|
+
}
|
|
95
|
+
sections.push(sync.join('\n\n'))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (manifest.gitignore.length > 0) {
|
|
99
|
+
const groups = manifest.gitignore.map(
|
|
100
|
+
({ header, entries }) =>
|
|
101
|
+
`- \`${header}\`: ${entries.map((entry) => `\`${entry}\``).join(', ')}`,
|
|
102
|
+
)
|
|
103
|
+
sections.push(`## Gitignore\n\n${groups.join('\n')}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return sections.map((section) => `\n${section}\n`).join('')
|
|
107
|
+
}
|