@savvy-web/silk-effects 7.5.3 → 8.0.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.
@@ -1,103 +0,0 @@
1
- import { Region } from "./region.js";
2
-
3
- //#region src/pr-body/markers.ts
4
- /**
5
- * The frozen `silk-release` marker vocabulary — the wire format of the shared
6
- * PR-body contract.
7
- *
8
- * @remarks
9
- * **The `silk-release:` token is frozen and names the CONTRACT, not the
10
- * emitting action.** `silk-update-action` PRs carry the same markers as
11
- * release PRs, deliberately: every live document, the `pr-body` plugin skill,
12
- * and every agent that edits a managed PR description key on these exact
13
- * byte sequences. Do not parameterize the token per action and do not rename
14
- * it — either forks the wire format for zero gain and orphans every open PR
15
- * (ruled in savvy-web/systems#419).
16
- *
17
- * These constants are the single source of truth for the marker grammar.
18
- * The agent-facing documentation in the silk plugin (`pr-body` and
19
- * `commit-create` skills) duplicates the literals for readability; a drift
20
- * lint in this package's test suite asserts the copies stay in sync.
21
- */
22
- /**
23
- * The marker constants of the `silk-release` PR-body contract.
24
- *
25
- * @public
26
- */
27
- var Markers = class {
28
- constructor() {}
29
- /**
30
- * Opening marker of the whole managed region.
31
- *
32
- * @remarks
33
- * Everything between this and {@link Markers.MANAGED_END} that is not
34
- * inside the summary or references region is regenerated wholesale on
35
- * every run; everything outside the pair is human territory and survives
36
- * every regeneration.
37
- *
38
- * @public
39
- */
40
- static MANAGED_START = Region.start("silk-release");
41
- /**
42
- * Closing marker of the whole managed region.
43
- *
44
- * @public
45
- */
46
- static MANAGED_END = Region.end("silk-release");
47
- /**
48
- * Opening marker of the region an AI summariser owns.
49
- *
50
- * @remarks
51
- * The generating action never writes into this region — it only reserves
52
- * it and carries its content through on regeneration.
53
- *
54
- * @public
55
- */
56
- static SUMMARY_START = Region.start("silk-release:summary");
57
- /**
58
- * Closing marker of the summariser's region.
59
- *
60
- * @public
61
- */
62
- static SUMMARY_END = Region.end("silk-release:summary");
63
- /**
64
- * The PLAIN opening marker of the closing-reference region — the form an
65
- * author writes by hand.
66
- *
67
- * @remarks
68
- * A generating run emits the ATTRIBUTED form instead (the plain prefix
69
- * plus an `owned="…"` attribute). Never locate the region by matching
70
- * this constant — match {@link Markers.REFERENCES_START_PREFIX}, or a
71
- * region a run wrote will not be found.
72
- *
73
- * @public
74
- */
75
- static REFERENCES_START = Region.start("silk-release:references");
76
- /**
77
- * Closing marker of the closing-reference region.
78
- *
79
- * @public
80
- */
81
- static REFERENCES_END = Region.end("silk-release:references");
82
- /**
83
- * The references opening marker up to its attributes, for locating a
84
- * region whose `owned` list is unknown.
85
- *
86
- * @public
87
- */
88
- static REFERENCES_START_PREFIX = "<!-- silk-release:references:start";
89
- /**
90
- * The fence language for the proposed squash-commit block.
91
- *
92
- * @remarks
93
- * Not a GFM language and apparently undocumented, but GitHub renders it.
94
- * It is a target for AI integrations to read and rewrite into the
95
- * eventual squash-commit message. **Do not "correct" it to `text`.**
96
- *
97
- * @public
98
- */
99
- static SQUASH_FENCE_LANGUAGE = "proposed-squash-commit";
100
- };
101
-
102
- //#endregion
103
- export { Markers };
@@ -1,157 +0,0 @@
1
- import { Markers } from "./markers.js";
2
- import { LinkedIssueRef } from "./linked-issue.js";
3
- import { Schema } from "effect";
4
- import { parseBareLines } from "@effected/github-references";
5
-
6
- //#region src/pr-body/references.ts
7
- /**
8
- * An ordered list of issue ids destined for closing references, with the two
9
- * renderers whose difference is the whole point of this module.
10
- *
11
- * @remarks
12
- * The same issues appear twice in a managed PR body, spelled differently, and
13
- * **neither consumer accepts the other's spelling**:
14
- *
15
- * - commitlint reads ONE comma-joined trailer (`Closes #1, #2`) inside the
16
- * proposed-squash-commit fence — {@link ClosingReferences.renderTrailer};
17
- * - GitHub's linker reads one bare `Closes #N` line each, OUTSIDE every
18
- * fence — {@link ClosingReferences.renderBareLines}. A reference inside a
19
- * fenced block is inert to GitHub.
20
- *
21
- * The duplication is load-bearing. Never "simplify" a body by emitting one
22
- * form in both places: comma-joined bare lines link nothing (verified by hand
23
- * against live pull requests — `savvy-web/silk-integration` #242/#232 with no
24
- * bare line reported `closingIssuesReferences: []`, #243 with one reported
25
- * `[168]`), and per-line trailers inside the fence break the commit contract.
26
- *
27
- * Ids are stored exactly as given — construction neither deduplicates nor
28
- * sorts. Call {@link ClosingReferences.dedupe} where uniqueness is wanted;
29
- * the split exists because the squash trailer historically renders duplicates
30
- * as-given while the references region deduplicates, and byte-compatibility
31
- * with live PR bodies pins that behavior.
32
- *
33
- * @public
34
- */
35
- var ClosingReferences = class ClosingReferences extends Schema.Class("ClosingReferences")({ ids: Schema.Array(Schema.Number.check(Schema.isInt())) }) {
36
- /**
37
- * The open issues' ids, in input order, duplicates preserved.
38
- *
39
- * @remarks
40
- * Closedness is decided by `LinkedIssueRef.isClosed` — the only
41
- * sanctioned test, case-insensitive so REST (`closed`) and GraphQL
42
- * (`CLOSED`) payloads classify identically.
43
- *
44
- * @public
45
- */
46
- static fromIssues(issues) {
47
- return ClosingReferences.make({ ids: issues.filter((issue) => !LinkedIssueRef.isClosed(issue)).map((issue) => issue.number) });
48
- }
49
- /**
50
- * Issue ids carried by a region's bare closing lines.
51
- *
52
- * @remarks
53
- * The region is read with `@effected/github-references`' `parseBareLines`
54
- * — per line, the whole line, after trimming, must be
55
- * `<keyword>[:] #<number>`, so a number mentioned in passing is never
56
- * mistaken for a closing reference. Every keyword GitHub accepts counts,
57
- * not just the present-tense plural this contract emits, because a
58
- * reference the parser fails to recognise is one the next regeneration
59
- * silently deletes.
60
- *
61
- * @public
62
- */
63
- static parseBare(region) {
64
- return parseBareLines(region).map((ref) => ref.issueNumber);
65
- }
66
- /**
67
- * A copy with duplicate ids removed, first occurrence winning.
68
- *
69
- * @public
70
- */
71
- dedupe() {
72
- return ClosingReferences.make({ ids: [...new Set(this.ids)] });
73
- }
74
- /**
75
- * The comma-joined trailer the squash-commit message carries, or `""`
76
- * when there is nothing to close.
77
- *
78
- * @remarks
79
- * `Closes #1, #2` on ONE line — the spelling commitlint reads and
80
- * GitHub's linker ignores. See the class remarks before changing either
81
- * renderer.
82
- *
83
- * @public
84
- */
85
- renderTrailer() {
86
- if (this.ids.length === 0) return "";
87
- return `Closes ${this.ids.map((id) => `#${id}`).join(", ")}`;
88
- }
89
- /**
90
- * One bare `Closes #N` line per id, or `""` when empty.
91
- *
92
- * @remarks
93
- * The spelling GitHub's linker reads — each line must sit OUTSIDE every
94
- * fenced block to link. See the class remarks before changing either
95
- * renderer.
96
- *
97
- * @public
98
- */
99
- renderBareLines() {
100
- return this.ids.map((id) => `Closes #${id}`).join("\n");
101
- }
102
- };
103
- /**
104
- * The `owned="…"` attribute on the references region's opening marker.
105
- *
106
- * @remarks
107
- * Records the issue ids a generating run emitted itself, so the next run can
108
- * tell its own references from ones an agent or human added. "Not in this
109
- * run's linked set" is NOT enough: a reference the previous run emitted also
110
- * disappears from the linked set when the release stops tracking that issue,
111
- * and treating it as agent-authored would preserve it forever — re-linking,
112
- * and on merge auto-closing, an issue the release deliberately dropped.
113
- *
114
- * **Never hand-edit the attribute.** A wrong value makes the next run delete
115
- * a real reference or resurrect a dropped one.
116
- *
117
- * @public
118
- */
119
- var OwnedAttribute = class {
120
- constructor() {}
121
- /**
122
- * The attribute as emitted on the opening marker.
123
- *
124
- * @public
125
- */
126
- static render(ids) {
127
- return `owned="${ids.join(",")}"`;
128
- }
129
- /**
130
- * The ids the prior body's opening marker claims as the previous run's
131
- * own.
132
- *
133
- * @remarks
134
- * An absent or malformed attribute reads as "none", which degrades to
135
- * treating every reference in the region as agent-authored. That
136
- * preserves too much rather than deleting someone's work — the safe
137
- * direction to fail. The match is anchored to an attribute boundary: an
138
- * unanchored match also finds `data-owned="…"` and `unowned="…"`, which
139
- * would let an unrelated attribute claim an agent's reference and get it
140
- * dropped on the next run.
141
- *
142
- * @public
143
- */
144
- static parse(priorBody) {
145
- const from = priorBody.indexOf(Markers.REFERENCES_START_PREFIX);
146
- if (from === -1) return /* @__PURE__ */ new Set();
147
- const openEnd = priorBody.indexOf("-->", from);
148
- if (openEnd === -1) return /* @__PURE__ */ new Set();
149
- const attributes = priorBody.slice(from + Markers.REFERENCES_START_PREFIX.length, openEnd);
150
- const owned = /(?:^|\s)owned="([\d,\s]*)"/.exec(attributes)?.[1];
151
- if (owned === void 0) return /* @__PURE__ */ new Set();
152
- return new Set(owned.split(",").map((part) => part.trim()).filter((part) => part !== "").map(Number));
153
- }
154
- };
155
-
156
- //#endregion
157
- export { ClosingReferences, OwnedAttribute };
package/pr-body/region.js DELETED
@@ -1,105 +0,0 @@
1
- //#region src/pr-body/region.ts
2
- /**
3
- * The generic marker-delimited region grammar every silk-managed document
4
- * uses: `<!-- token:start -->` … `<!-- token:end -->`.
5
- *
6
- * @remarks
7
- * Extracted from `silk-release-action` (its `pr-body.ts` and
8
- * `managed-sections.ts` both carried a private copy) so the grammar has one
9
- * owner. **Every marker is a pair.** A lone opening marker can only be located
10
- * by scanning forward to whatever happens to follow it, which makes the
11
- * region's extent a function of its neighbours rather than of itself — moving
12
- * anything nearby silently redefines it. The token is free-form; `:start` and
13
- * `:end` are the whole contract, so pairs nest and a region can contain
14
- * sub-regions without either needing to know about the other.
15
- */
16
- /**
17
- * Pure helpers over the `<!-- token:start -->` / `<!-- token:end -->` region
18
- * grammar.
19
- *
20
- * @remarks
21
- * Every operation is total: a body with no region (or a broken pair) degrades
22
- * to the documented fail-safe result rather than failing, because the callers
23
- * are regenerating actions that must still produce a body when the prior one
24
- * is malformed. Use `PrBodyDiagnostic.scan` when a caller wants to be told
25
- * about a broken pair instead of silently tolerating it.
26
- *
27
- * @public
28
- */
29
- var Region = class Region {
30
- constructor() {}
31
- /**
32
- * The opening delimiter for a named region.
33
- *
34
- * @public
35
- */
36
- static start(token) {
37
- return `<!-- ${token}:start -->`;
38
- }
39
- /**
40
- * The closing delimiter for a named region.
41
- *
42
- * @public
43
- */
44
- static end(token) {
45
- return `<!-- ${token}:end -->`;
46
- }
47
- /**
48
- * The content between a region's delimiters, or `undefined` when absent.
49
- *
50
- * @remarks
51
- * Finds the FIRST opening marker and the matching close after it, so a
52
- * nested region of a different token is returned as part of the content
53
- * rather than truncating it.
54
- *
55
- * @public
56
- */
57
- static read(body, token) {
58
- const from = body.indexOf(Region.start(token));
59
- const to = body.indexOf(Region.end(token), from === -1 ? 0 : from);
60
- if (from === -1 || to === -1 || to < from) return void 0;
61
- return body.slice(from + Region.start(token).length, to);
62
- }
63
- /**
64
- * Everything outside a region, with the region and its delimiters removed.
65
- *
66
- * @remarks
67
- * A body without the region comes back unchanged — removal of an absent
68
- * region is a no-op, not an error.
69
- *
70
- * @public
71
- */
72
- static strip(body, token) {
73
- const from = body.indexOf(Region.start(token));
74
- const to = body.indexOf(Region.end(token), from === -1 ? 0 : from);
75
- if (from === -1 || to === -1 || to < from) return body;
76
- return `${body.slice(0, from)}${body.slice(to + Region.end(token).length)}`;
77
- }
78
- /**
79
- * Put `rendered` (a fully rendered region, its own markers included) into
80
- * `body`, replacing a previous region of the same token and leaving
81
- * everything else alone.
82
- *
83
- * @remarks
84
- * **Human edits outside the markers survive.** A predecessor spliced on a
85
- * markdown heading, which silently ate any content a human happened to put
86
- * under a heading of that name and could not tell generated text from
87
- * theirs. An explicit marker pair can.
88
- *
89
- * A body with no markers keeps its content and gains the region **below**
90
- * it, so an existing hand-written document is not displaced. The result is
91
- * trimmed.
92
- *
93
- * @public
94
- */
95
- static upsert(body, token, rendered) {
96
- const start = body.indexOf(Region.start(token));
97
- const end = body.indexOf(Region.end(token));
98
- if (start !== -1 && end !== -1 && end > start) return `${body.slice(0, start)}${rendered}${body.slice(end + Region.end(token).length)}`.trim();
99
- const trimmed = body.trim();
100
- return trimmed === "" ? rendered : `${trimmed}\n\n${rendered}`;
101
- }
102
- };
103
-
104
- //#endregion
105
- export { Region };
@@ -1,232 +0,0 @@
1
- import { CommentStyle, SectionId } from "@effected/templates";
2
-
3
- //#region src/schemas/SavvyInstallSection.ts
4
- /**
5
- * Section identity for the dependency auto-install block.
6
- *
7
- * `toolName` is `"savvy-install"`; pair with {@link savvyInstallDeps}.
8
- *
9
- * @since 7.5.0
10
- * @public
11
- */
12
- const SavvyInstallSection = SectionId.make({
13
- key: "SAVVY-INSTALL",
14
- commentStyle: CommentStyle.hash
15
- });
16
- /**
17
- * Pathspecs whose change across a checkout or merge means `node_modules` is stale.
18
- *
19
- * @remarks
20
- * Git's default pathspec globbing does not stop a wildcard at a slash, so the
21
- * one nested-manifest entry below covers every workspace manifest at any depth
22
- * without needing a recursive spelling. Manifests are listed alongside lockfiles
23
- * because a dependency edit that has not been installed yet moves the manifest
24
- * first and the lockfile not at all.
25
- */
26
- const DEPENDENCY_PATHS = [
27
- "package.json",
28
- "*/package.json",
29
- "pnpm-lock.yaml",
30
- "pnpm-workspace.yaml",
31
- "package-lock.json",
32
- "npm-shrinkwrap.json",
33
- "yarn.lock",
34
- "bun.lock",
35
- "bun.lockb"
36
- ].map((path) => `'${path}'`).join(" ");
37
- /**
38
- * Dependency auto-install shared across Silk Suite hook files.
39
- *
40
- * @remarks
41
- * Brings `node_modules` back in line after a branch switch or a pull, so a batch
42
- * of repos pulled to align their dependencies does not each need a manual
43
- * install. Installs with lifecycle scripts skipped — `--ignore-scripts`, or
44
- * `--mode=skip-build` for Yarn Berry, which dropped the former — because the goal
45
- * is to get the dependency tree on disk, not to run a full postinstall.
46
- *
47
- * **That flag has a cost, and one shape of repo cannot pay it.** Where a package
48
- * publishes through a built link directory — `publishConfig.directory` with
49
- * `linkDirectory: true`, which is how this monorepo wires `dist/dev/pkg` — the
50
- * workspace resolves its own dependencies through a directory that a `prepare`
51
- * script has to build, and skipping scripts yields a populated `node_modules`
52
- * pointing at nothing.
53
- *
54
- * Those repos ask for a full install through the LOCAL git config key
55
- * `savvy.installLifecycleScripts`, which `savvy init` sets when it finds the
56
- * shape. The decision deliberately does NOT read the checked-out tree. Doing so
57
- * failed in both directions at once: a branch that merely declared the shape
58
- * could turn lifecycle scripts back on just by being checked out, making
59
- * `git checkout` of an untrusted revision a code-execution path; and the `jq`
60
- * the scan needed is absent on stock macOS and Ubuntu, where the missing answer
61
- * silently skipped scripts in precisely the repos that cannot survive it.
62
- * `.git/config` is neither checked out nor parsed with `jq`, so it has neither
63
- * failure mode.
64
- *
65
- * The package managers gate dependency scripts themselves on top of this —
66
- * pnpm's `strictDepBuilds` with an `allowBuilds` allowlist being the case in
67
- * point — but workspace and root lifecycle scripts still run, which is why the
68
- * opt-in is local rather than inferred. With the flag on, the hook says on the
69
- * way out that scripts were skipped rather than leaving it to be discovered.
70
- *
71
- * Deliberately self-contained, like `savvyToolchainCheck`: its homes carry
72
- * `SavvyHooksSection` but no `SavvyBaseSection`, so it defines its own root, CI
73
- * and package-manager lookups rather than depending on `ROOT`, `in_ci` or `PM`.
74
- *
75
- * Four guards keep it from firing on the many checkouts that are not dependency
76
- * events, each a silent no-op:
77
- *
78
- * - CI, where the runtime action owns installation by construction.
79
- * - `SAVVY_SKIP_INSTALL`, the escape hatch for a bisect or a scripted sweep that
80
- * does not want an install between steps.
81
- * - `post-checkout` only: a branch-flag of `0`, which is `git checkout -- <file>`
82
- * and not a move between commits.
83
- * - **The gate that makes this affordable:** nothing dependency-related actually
84
- * changed across the move. Without it every branch switch would pay for a full
85
- * install. A missing `node_modules` overrides it, since there is nothing to be
86
- * stale.
87
- *
88
- * A fresh clone is not among the cases it handles, and cannot be: husky sets
89
- * `core.hooksPath` from its own `prepare` script, so until the first manual
90
- * install has run there is no hook installed to fire.
91
- *
92
- * The install's exit status is swallowed and its output goes to stderr. Git
93
- * ignores what `post-checkout` and `post-merge` return, so a failure here must
94
- * not look like a failed checkout; the hint names the escape hatch instead.
95
- *
96
- * Every probe that can legitimately fail — no `ORIG_HEAD` to compare against, an
97
- * unresolvable range — is neutralised with `|| true` rather than left to its own
98
- * status. These blocks are co-owned and consumer content can sit below them, so
99
- * under a hook running `set -e` a bare failing substitution would abort the whole
100
- * file and take those later sections with it.
101
- *
102
- * @param hook - Which hook the block is being generated for; decides the
103
- * argument guard and how the comparison range is recovered.
104
- * @returns The install shell, with no surrounding markers or trailing newline.
105
- *
106
- * @since 7.5.0
107
- * @public
108
- */
109
- function savvyInstallDeps(hook) {
110
- const skip = `! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ] || [ -n "$SAVVY_SKIP_INSTALL" ]; }`;
111
- return `if ${hook === "post-checkout" ? `${skip} && [ "$3" = "1" ]` : skip}; then
112
- install_root=$(git rev-parse --show-toplevel 2>/dev/null) || true
113
- ${hook === "post-checkout" ? ` install_from="$1"
114
- install_to="$2"` : ` install_from=$(git rev-parse --verify --quiet ORIG_HEAD 2>/dev/null) || true
115
- install_to="HEAD"`}
116
- install_pm=""
117
- if [ -n "$install_root" ] && [ -f "$install_root/package.json" ]; then
118
- if command -v jq >/dev/null 2>&1; then
119
- install_pm=$(jq -r '.packageManager // empty' "$install_root/package.json" 2>/dev/null | cut -d'@' -f1)
120
- fi
121
- if [ -z "$install_pm" ]; then
122
- if [ -f "$install_root/pnpm-lock.yaml" ]; then install_pm="pnpm"
123
- elif [ -f "$install_root/yarn.lock" ]; then install_pm="yarn"
124
- elif [ -f "$install_root/bun.lock" ] || [ -f "$install_root/bun.lockb" ]; then install_pm="bun"
125
- else install_pm="npm"; fi
126
- fi
127
- fi
128
- # packageManager comes from the checked-out revision, so it names an executable
129
- # the tree controls. 'command -v' proves a binary exists, not that it is a
130
- # package manager, so the name is checked against the supported four first.
131
- case "$install_pm" in
132
- npm|pnpm|yarn|bun) ;;
133
- *) install_pm="" ;;
134
- esac
135
- # Nothing to bring up to date unless a manifest or lockfile actually moved.
136
- # A missing node_modules skips the diff outright: there is no tree to be stale.
137
- install_stale=""
138
- if [ -n "$install_pm" ]; then
139
- if [ ! -d "$install_root/node_modules" ]; then
140
- install_stale=1
141
- elif [ -n "$install_from" ] && [ -n "$install_to" ]; then
142
- install_stale=$(git diff --name-only "$install_from" "$install_to" -- ${DEPENDENCY_PATHS} 2>/dev/null | head -n 1) || true
143
- fi
144
- fi
145
- if [ -n "$install_stale" ] && command -v "$install_pm" >/dev/null 2>&1; then
146
- # Whether lifecycle scripts run is a LOCAL decision, read from .git/config,
147
- # which no checkout can rewrite. Default off. 'savvy init' turns it on for a
148
- # workspace that publishes through built link directories, where the links
149
- # point at directories a prepare script has to produce.
150
- install_scripts=$(git -C "$install_root" config --bool --get savvy.installLifecycleScripts 2>/dev/null) || true
151
- if [ "$install_scripts" = "true" ]; then
152
- install_flag=""
153
- else
154
- # Berry dropped --ignore-scripts for the install mode; Classic never knew the
155
- # mode. An unreadable major reads as Berry, the likelier of the two today.
156
- install_flag="--ignore-scripts"
157
- if [ "$install_pm" = "yarn" ]; then
158
- case "$(yarn --version 2>/dev/null | cut -d. -f1)" in
159
- 1) ;;
160
- *) install_flag="--mode=skip-build" ;;
161
- esac
162
- fi
163
- fi
164
- # ':+' so a full install announces itself as "pnpm install", not "pnpm install ".
165
- printf '↻ dependencies changed, running %s install%s\\n' "$install_pm" "\${install_flag:+ $install_flag}" >&2
166
- # Unquoted on purpose: empty must expand to NO argument rather than an empty
167
- # one. Safe because every value is a static flag literal from this generator.
168
- if ( cd "$install_root" && "$install_pm" install $install_flag ) >&2; then
169
- if [ -n "$install_flag" ]; then
170
- printf ' Lifecycle scripts were skipped. Run a full install if you need build outputs.\\n' >&2
171
- fi
172
- else
173
- printf '⚠ %s install failed; run it yourself to see why.\\n' "$install_pm" >&2
174
- printf ' Set SAVVY_SKIP_INSTALL=1 to stop this hook from trying.\\n' >&2
175
- fi
176
- fi
177
- unset install_root install_from install_to install_pm install_stale install_scripts install_flag
178
- fi`;
179
- }
180
- /**
181
- * The local git config key that authorizes lifecycle scripts during a hook install.
182
- *
183
- * @remarks
184
- * Local scope only. It lives in `.git/config`, which is never checked out, so no
185
- * incoming revision can set it — that is the whole point of reading the decision
186
- * from here rather than from a manifest in the tree.
187
- *
188
- * @since 7.5.0
189
- * @public
190
- */
191
- const LIFECYCLE_SCRIPTS_CONFIG_KEY = "savvy.installLifecycleScripts";
192
- /**
193
- * Whether `manifest` publishes through a built link directory.
194
- *
195
- * @remarks
196
- * `publishConfig.directory` with `linkDirectory: true` means consumers of this
197
- * package resolve it through a directory that a `prepare` script has to produce,
198
- * so an install that skips lifecycle scripts leaves the link pointing at nothing.
199
- * A workspace containing any such package is one whose owner probably wants
200
- * {@link LIFECYCLE_SCRIPTS_CONFIG_KEY} set.
201
- *
202
- * Reporting the shape is deliberately separate from acting on it: this answers
203
- * "does this repo need scripts", and a human still decides whether hook-time
204
- * installs may run them.
205
- *
206
- * @param manifest - A parsed `package.json`; any non-object reads as `false`.
207
- *
208
- * @since 7.5.0
209
- * @public
210
- */
211
- function publishesBuiltLinkDirectory(manifest) {
212
- if (typeof manifest !== "object" || manifest === null) return false;
213
- const { publishConfig } = manifest;
214
- if (typeof publishConfig !== "object" || publishConfig === null) return false;
215
- const { directory, linkDirectory } = publishConfig;
216
- return typeof directory === "string" && directory.length > 0 && linkDirectory === true;
217
- }
218
- /**
219
- * Build the dependency auto-install block for `hook`.
220
- *
221
- * @param hook - Which hook the section is destined for.
222
- * @returns A shell `Section` (`commentStyle: hash`) keyed `SAVVY-INSTALL`.
223
- *
224
- * @since 7.5.0
225
- * @public
226
- */
227
- function savvyInstallBlock(hook) {
228
- return SavvyInstallSection.section(savvyInstallDeps(hook));
229
- }
230
-
231
- //#endregion
232
- export { LIFECYCLE_SCRIPTS_CONFIG_KEY, SavvyInstallSection, publishesBuiltLinkDirectory, savvyInstallBlock, savvyInstallDeps };