javi-forge 1.24.0 → 1.25.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/dist/cli/dispatch/ci.js
CHANGED
|
@@ -70,9 +70,14 @@ export async function handleCi(cli, ctx) {
|
|
|
70
70
|
// Sub-command: javi-forge ci init → install git hooks
|
|
71
71
|
if (cli.input[1] === "init") {
|
|
72
72
|
const { installCIHooks } = await import("../../commands/ci.js");
|
|
73
|
-
const { installed, upgraded, backups, errors, states } = await installCIHooks(process.cwd(), {
|
|
73
|
+
const { installed, upgraded, backups, errors, states, notes } = await installCIHooks(process.cwd(), {
|
|
74
74
|
force: cli.flags.force === true,
|
|
75
75
|
});
|
|
76
|
+
// Migration notes (e.g. "legacy javi-forge hooksPath removed") come first:
|
|
77
|
+
// they explain a config change the operator did not explicitly ask for.
|
|
78
|
+
for (const note of notes) {
|
|
79
|
+
console.log(`ℹ ${note}`);
|
|
80
|
+
}
|
|
76
81
|
for (const backup of backups) {
|
|
77
82
|
console.log(`⚠ Backed up the previous hook → ${backup}`);
|
|
78
83
|
}
|
package/dist/commands/ci.d.ts
CHANGED
|
@@ -212,6 +212,12 @@ export interface InstallHooksResult {
|
|
|
212
212
|
backups: string[];
|
|
213
213
|
errors: string[];
|
|
214
214
|
states: HookStateReport[];
|
|
215
|
+
/**
|
|
216
|
+
* Informational messages from the hooksPath guard (D6) — e.g. the "legacy
|
|
217
|
+
* javi-forge hooksPath removed" note emitted when the migration path unsets
|
|
218
|
+
* a `ci-local/hooks` local value. NOT errors; surfaced to the operator.
|
|
219
|
+
*/
|
|
220
|
+
notes: string[];
|
|
215
221
|
}
|
|
216
222
|
/**
|
|
217
223
|
* Pure classification of hook CONTENT (D6 steps 1-5).
|
package/dist/commands/ci.js
CHANGED
|
@@ -1548,9 +1548,227 @@ async function repairHookMode(hookPath) {
|
|
|
1548
1548
|
async function readHookBody(hookName) {
|
|
1549
1549
|
return await fs.readFile(path.join(HOOK_ASSETS_DIR, hookName), "utf8");
|
|
1550
1550
|
}
|
|
1551
|
+
/** The exact — and ONLY — value javi-forge itself ever wrote (git.ts:83). */
|
|
1552
|
+
const LEGACY_HOOKSPATH = "ci-local/hooks";
|
|
1553
|
+
/**
|
|
1554
|
+
* Read a higher-scope `core.hooksPath` with `git config <scope> --get`.
|
|
1555
|
+
*
|
|
1556
|
+
* `--global`/`--system` reads return each scope's OWN value regardless of the
|
|
1557
|
+
* local value, so a shadowing higher-scope hooksPath is visible with ZERO
|
|
1558
|
+
* mutation. git exits 1 (no section / no config file) or 5 (key unset) for "no
|
|
1559
|
+
* value" → `{ value: "" }`. ANY other failure is surfaced as `failed` so the
|
|
1560
|
+
* guard can fail CLOSED: a blind spot on a possible shadow must never let the
|
|
1561
|
+
* migration unset the local value and half-migrate the repo.
|
|
1562
|
+
*/
|
|
1563
|
+
async function readScopedHooksPath(projectDir, scope) {
|
|
1564
|
+
try {
|
|
1565
|
+
const { stdout } = await execFileAsync("git", ["config", scope, "--get", "core.hooksPath"], { cwd: projectDir });
|
|
1566
|
+
return { value: stdout.trim() };
|
|
1567
|
+
}
|
|
1568
|
+
catch (e) {
|
|
1569
|
+
const code = e.code;
|
|
1570
|
+
if (code === 1 || code === 5) {
|
|
1571
|
+
return { value: "" };
|
|
1572
|
+
}
|
|
1573
|
+
return { value: "", failed: e instanceof Error ? e.message : String(e) };
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Read the LOCAL `core.hooksPath` with the SAME fail-closed discipline as
|
|
1578
|
+
* {@link readScopedHooksPath} (JDA-001).
|
|
1579
|
+
*
|
|
1580
|
+
* git exits 1 (no section / no config file) or 5 (key unset) for a genuine "no
|
|
1581
|
+
* value" → `{ value: "" }`, the safe fresh-install path. ANY OTHER failure
|
|
1582
|
+
* (config lock, I/O error, a corrupt `.git/config`, a git fault) is surfaced as
|
|
1583
|
+
* `failed` so the guard can REFUSE. Swallowing such a failure to `""` would be a
|
|
1584
|
+
* FAIL-OPEN: a local value genuinely = `ci-local/hooks` that transiently fails
|
|
1585
|
+
* to read would look like a fresh repo, so step 5 would never unset it and the
|
|
1586
|
+
* shims would be installed into `.git/hooks` while the still-present local value
|
|
1587
|
+
* keeps SHADOWING them — `installCIHooks` would report installed/upgraded while
|
|
1588
|
+
* git runs the OLD ci-local bodies. An unknown-state local read must never be
|
|
1589
|
+
* treated as a fresh install.
|
|
1590
|
+
*/
|
|
1591
|
+
async function readLocalHooksPath(projectDir) {
|
|
1592
|
+
try {
|
|
1593
|
+
const { stdout } = await execFileAsync("git", ["config", "--local", "--get", "core.hooksPath"], { cwd: projectDir });
|
|
1594
|
+
return { value: stdout.trim() };
|
|
1595
|
+
}
|
|
1596
|
+
catch (e) {
|
|
1597
|
+
const code = e.code;
|
|
1598
|
+
if (code === 1 || code === 5) {
|
|
1599
|
+
return { value: "" };
|
|
1600
|
+
}
|
|
1601
|
+
return { value: "", failed: e instanceof Error ? e.message : String(e) };
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Read a WORKTREE-scoped `core.hooksPath` (JDA-002), which — with
|
|
1606
|
+
* `extensions.worktreeConfig` enabled — ALSO shadows `.git/hooks`.
|
|
1607
|
+
*
|
|
1608
|
+
* The extension is checked FIRST for a load-bearing reason: when it is OFF, `git
|
|
1609
|
+
* config --worktree` silently behaves like `--local` (verified), so reading it
|
|
1610
|
+
* here would double-count the LOCAL value and refuse a valid legacy migration.
|
|
1611
|
+
* It is a DISTINCT scope only when the extension is enabled, and only then does
|
|
1612
|
+
* `--worktree --get` read from `$GIT_DIR/config.worktree` WITHOUT falling back to
|
|
1613
|
+
* local. Gating on the extension also handles the multiple-worktree + extension-off
|
|
1614
|
+
* case where `--worktree` errors out ("not enabled"): we never issue that read, so
|
|
1615
|
+
* that specific error can never surface as a spurious refuse.
|
|
1616
|
+
*
|
|
1617
|
+
* Same fail-closed discipline as the scoped reads: exit 1/5 → genuine no value;
|
|
1618
|
+
* any OTHER failure → `failed` so the guard REFUSES rather than fail open.
|
|
1619
|
+
*/
|
|
1620
|
+
async function readWorktreeHooksPath(projectDir) {
|
|
1621
|
+
// Determine whether the worktree config extension is enabled.
|
|
1622
|
+
let enabled = false;
|
|
1623
|
+
try {
|
|
1624
|
+
const { stdout } = await execFileAsync("git", ["config", "--bool", "--get", "extensions.worktreeConfig"], { cwd: projectDir });
|
|
1625
|
+
enabled = stdout.trim() === "true";
|
|
1626
|
+
}
|
|
1627
|
+
catch (e) {
|
|
1628
|
+
const code = e.code;
|
|
1629
|
+
if (code === 1 || code === 5) {
|
|
1630
|
+
// Extension unset/disabled → no DISTINCT worktree scope exists; the local
|
|
1631
|
+
// read (step 3) already covers the --worktree==--local fallback. Treat as
|
|
1632
|
+
// no worktree value, never a refuse.
|
|
1633
|
+
return { value: "" };
|
|
1634
|
+
}
|
|
1635
|
+
// Cannot determine the extension state → fail CLOSED.
|
|
1636
|
+
return { value: "", failed: e instanceof Error ? e.message : String(e) };
|
|
1637
|
+
}
|
|
1638
|
+
if (!enabled) {
|
|
1639
|
+
return { value: "" };
|
|
1640
|
+
}
|
|
1641
|
+
try {
|
|
1642
|
+
const { stdout } = await execFileAsync("git", ["config", "--worktree", "--get", "core.hooksPath"], { cwd: projectDir });
|
|
1643
|
+
return { value: stdout.trim() };
|
|
1644
|
+
}
|
|
1645
|
+
catch (e) {
|
|
1646
|
+
const code = e.code;
|
|
1647
|
+
if (code === 1 || code === 5) {
|
|
1648
|
+
return { value: "" };
|
|
1649
|
+
}
|
|
1650
|
+
return { value: "", failed: e instanceof Error ? e.message : String(e) };
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* The ATOMIC `core.hooksPath` guard (design D6). Runs BEFORE the install loop
|
|
1655
|
+
* mutates anything.
|
|
1656
|
+
*
|
|
1657
|
+
* DETECT-BEFORE-MUTATE — steps 1-4 are pure reads; only step 5 writes (unset
|
|
1658
|
+
* the legacy local value). EVERY refuse path returns `{ refuse }` having mutated
|
|
1659
|
+
* NOTHING, so the repo is left in its EXACT prior state and can never land
|
|
1660
|
+
* half-migrated (JDA-001 / JDA-008). The single write (`--local --unset`) and
|
|
1661
|
+
* the subsequent shim install happen ONLY on the fully-validated success/force
|
|
1662
|
+
* path.
|
|
1663
|
+
*/
|
|
1664
|
+
async function guardHooksPath(projectDir, hooksDir, manifest, force) {
|
|
1665
|
+
// ── Step 1: classify the managed slots (pure read), exactly as the install
|
|
1666
|
+
// loop will. A manifest problem is NOT classified here — it surfaces
|
|
1667
|
+
// per-hook in the loop; an unclassifiable slot is treated as ABSENT (never
|
|
1668
|
+
// fabricate a foreign refusal from a manifest error).
|
|
1669
|
+
const foreignSlots = [];
|
|
1670
|
+
for (const name of HOOK_NAMES) {
|
|
1671
|
+
const entry = manifest[name];
|
|
1672
|
+
try {
|
|
1673
|
+
assertHookManifestEntry(entry, name);
|
|
1674
|
+
}
|
|
1675
|
+
catch {
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
const state = await classifyHookPath(path.join(hooksDir, name), name, entry);
|
|
1679
|
+
if (state === HOOK_STATE.FOREIGN) {
|
|
1680
|
+
foreignSlots.push(name);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
// ── Step 2: DETECT a shadowing higher-scope hooksPath via SCOPED reads (ZERO
|
|
1684
|
+
// mutation). A global/system value shadows .git/hooks the moment git
|
|
1685
|
+
// consults hooksPath, so installing there would be INERT — refuse rather
|
|
1686
|
+
// than fail open. Detected WITHOUT unsetting local, so a
|
|
1687
|
+
// `local-unset + global-present` half-migration is impossible.
|
|
1688
|
+
for (const scope of ["--system", "--global"]) {
|
|
1689
|
+
const probe = await readScopedHooksPath(projectDir, scope);
|
|
1690
|
+
if (probe.failed !== undefined) {
|
|
1691
|
+
return {
|
|
1692
|
+
refuse: `could not read ${scope} git config core.hooksPath (${probe.failed}). Refusing to install: a higher-scope hooksPath could silently shadow .git/hooks and the hooks would not run. Resolve the git error and re-run.`,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
if (probe.value !== "") {
|
|
1696
|
+
const label = scope === "--global" ? "global" : "system";
|
|
1697
|
+
return {
|
|
1698
|
+
refuse: `a ${label} core.hooksPath='${probe.value}' would redirect git away from .git/hooks — the installed hooks would NOT run. Resolve it first (git config ${scope} --unset core.hooksPath) and re-run.`,
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
// ── Step 2b (JDA-002): a WORKTREE-scoped hooksPath (with
|
|
1703
|
+
// extensions.worktreeConfig enabled) ALSO shadows .git/hooks. Same
|
|
1704
|
+
// fail-closed handling: a non-1/5 read failure REFUSES rather than fail open.
|
|
1705
|
+
const worktree = await readWorktreeHooksPath(projectDir);
|
|
1706
|
+
if (worktree.failed !== undefined) {
|
|
1707
|
+
return {
|
|
1708
|
+
refuse: `could not read --worktree git config core.hooksPath (${worktree.failed}). Refusing to install: a worktree-scoped hooksPath could silently shadow .git/hooks and the hooks would not run. Resolve the git error and re-run.`,
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
if (worktree.value !== "") {
|
|
1712
|
+
return {
|
|
1713
|
+
refuse: `a worktree core.hooksPath='${worktree.value}' would redirect git away from .git/hooks — the installed hooks would NOT run. Resolve it first (git config --worktree --unset core.hooksPath) and re-run.`,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
// Shadow reads now cover --global, --system and --worktree. The includeIf
|
|
1717
|
+
// residual edge remains: a hooksPath injected only through a
|
|
1718
|
+
// [includeIf "gitdir:…"] conditional include is NOT returned by
|
|
1719
|
+
// --global/--system/--worktree --get and would surface only in an effective
|
|
1720
|
+
// read — a documented residual edge, not covered by these scoped reads.
|
|
1721
|
+
// ── Step 3: the LOCAL value. A foreign local manager (husky/lefthook/custom)
|
|
1722
|
+
// owns this repo's hooks; never hijack it. --force does NOT override (force
|
|
1723
|
+
// is consent to lose a file, not to seize another manager's config). The read
|
|
1724
|
+
// fails CLOSED (JDA-001): a non-1/5 failure REFUSES rather than mistake an
|
|
1725
|
+
// unreadable local value for a fresh repo and install over a shadowing value.
|
|
1726
|
+
const localProbe = await readLocalHooksPath(projectDir);
|
|
1727
|
+
if (localProbe.failed !== undefined) {
|
|
1728
|
+
return {
|
|
1729
|
+
refuse: `could not read --local git config core.hooksPath (${localProbe.failed}). Refusing to install: a local hooksPath could silently shadow .git/hooks and the installed hooks would not run. Resolve the git error and re-run.`,
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
const local = localProbe.value;
|
|
1733
|
+
if (local !== "" && local !== LEGACY_HOOKSPATH) {
|
|
1734
|
+
return {
|
|
1735
|
+
refuse: `core.hooksPath is set to '${local}' — another hook manager owns this repo's hooks. javi-forge refuses to install or change it. Unset it yourself (git config --local --unset core.hooksPath) if you want javi-forge hooks.`,
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
const legacy = local === LEGACY_HOOKSPATH;
|
|
1739
|
+
// ── Step 4: installability, ONLY on the migration path. Unsetting the legacy
|
|
1740
|
+
// value ACTIVATES whatever sits in .git/hooks; if a slot is FOREIGN without
|
|
1741
|
+
// --force, migrating now would activate a dormant foreign hook → ATOMIC
|
|
1742
|
+
// REFUSE, leave core.hooksPath SET, install NOTHING. When there is no legacy
|
|
1743
|
+
// value to unset, foreign slots are handled per-hook by the install loop as
|
|
1744
|
+
// before — nothing is being activated.
|
|
1745
|
+
if (legacy && !force && foreignSlots.length > 0) {
|
|
1746
|
+
const slot = foreignSlots[0];
|
|
1747
|
+
return {
|
|
1748
|
+
refuse: `core.hooksPath=${LEGACY_HOOKSPATH} would be removed, but .git/hooks/${slot} holds a foreign hook (a prior 'tdd init' or hand-written hook). Migrating now would activate it. Resolve it (delete/back it up) or re-run with --force.`,
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
// ── Step 5: ONLY NOW mutate. Every check passed (or --force). Unset the
|
|
1752
|
+
// legacy local value — the ONLY value javi-forge ever wrote, and only the
|
|
1753
|
+
// --local scope is ever touched — then let the caller install. The install
|
|
1754
|
+
// NEVER runs before steps 2 and 4.
|
|
1755
|
+
if (legacy) {
|
|
1756
|
+
await execFileAsync("git", ["config", "--local", "--unset", "core.hooksPath"], { cwd: projectDir });
|
|
1757
|
+
return {
|
|
1758
|
+
note: "legacy javi-forge hooksPath removed; hooks now live in .git/hooks",
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
return {};
|
|
1762
|
+
}
|
|
1551
1763
|
export async function installCIHooks(projectDir, options = {}) {
|
|
1552
1764
|
const force = options.force === true;
|
|
1553
|
-
const empty = {
|
|
1765
|
+
const empty = {
|
|
1766
|
+
installed: [],
|
|
1767
|
+
upgraded: [],
|
|
1768
|
+
backups: [],
|
|
1769
|
+
states: [],
|
|
1770
|
+
notes: [],
|
|
1771
|
+
};
|
|
1554
1772
|
const gitDir = path.join(projectDir, ".git");
|
|
1555
1773
|
if (!(await fs.pathExists(gitDir))) {
|
|
1556
1774
|
return {
|
|
@@ -1587,6 +1805,19 @@ export async function installCIHooks(projectDir, options = {}) {
|
|
|
1587
1805
|
const backups = [];
|
|
1588
1806
|
const errors = [];
|
|
1589
1807
|
const states = [];
|
|
1808
|
+
const notes = [];
|
|
1809
|
+
// The ATOMIC core.hooksPath guard (D6) is the SINGLE choke point shared by
|
|
1810
|
+
// `ci init` and `init`. It runs DETECT-BEFORE-MUTATE: on refuse it has
|
|
1811
|
+
// mutated nothing and we return without installing; on the success/force
|
|
1812
|
+
// path it has already unset any legacy `ci-local/hooks` value and we
|
|
1813
|
+
// proceed to install the shims.
|
|
1814
|
+
const guard = await guardHooksPath(projectDir, hooksDir, manifest, force);
|
|
1815
|
+
if ("refuse" in guard) {
|
|
1816
|
+
return { ...empty, errors: [guard.refuse] };
|
|
1817
|
+
}
|
|
1818
|
+
if (guard.note !== undefined) {
|
|
1819
|
+
notes.push(guard.note);
|
|
1820
|
+
}
|
|
1590
1821
|
for (const name of HOOK_NAMES) {
|
|
1591
1822
|
const hookPath = path.join(hooksDir, name);
|
|
1592
1823
|
const entry = manifest[name];
|
|
@@ -1631,6 +1862,6 @@ export async function installCIHooks(projectDir, options = {}) {
|
|
|
1631
1862
|
errors.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1632
1863
|
}
|
|
1633
1864
|
}
|
|
1634
|
-
return { installed, upgraded, backups, errors, states };
|
|
1865
|
+
return { installed, upgraded, backups, errors, states, notes };
|
|
1635
1866
|
}
|
|
1636
1867
|
//# sourceMappingURL=ci.js.map
|
|
@@ -11,15 +11,17 @@ import type { StepFn } from "../types.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const stepGitInit: StepFn;
|
|
13
13
|
/**
|
|
14
|
-
* Step 2:
|
|
14
|
+
* Step 2: Install managed git hooks (D7 reconciliation).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* Init no longer copies `ci-local/` hook bodies into the project nor flips
|
|
17
|
+
* `core.hooksPath` — both were the pre-consolidation mechanism. It now delegates
|
|
18
|
+
* to `installCIHooks`, the SINGLE writer of `.git/hooks` and the choke point for
|
|
19
|
+
* the ATOMIC `core.hooksPath` guard (D6). A foreign/global hooksPath, or a
|
|
20
|
+
* dormant foreign slot, is REFUSED there with zero mutation and surfaced as an
|
|
21
|
+
* error here — init never overrides the user's hook manager.
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
+
* - dry-run: reports "would install managed hooks", calls nothing.
|
|
24
|
+
* - Errors are swallowed and reported as status:"error" — never thrown.
|
|
23
25
|
*/
|
|
24
26
|
export declare const stepGitHooks: StepFn;
|
|
25
27
|
//# sourceMappingURL=git.d.ts.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
import { CI_LOCAL_DIR } from "../../../constants.js";
|
|
4
3
|
import { execFileAsync } from "../../../lib/exec.js";
|
|
5
4
|
import { report } from "../report.js";
|
|
6
5
|
/**
|
|
@@ -34,48 +33,46 @@ export const stepGitInit = async (ctx) => {
|
|
|
34
33
|
}
|
|
35
34
|
};
|
|
36
35
|
/**
|
|
37
|
-
* Step 2:
|
|
36
|
+
* Step 2: Install managed git hooks (D7 reconciliation).
|
|
38
37
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
38
|
+
* Init no longer copies `ci-local/` hook bodies into the project nor flips
|
|
39
|
+
* `core.hooksPath` — both were the pre-consolidation mechanism. It now delegates
|
|
40
|
+
* to `installCIHooks`, the SINGLE writer of `.git/hooks` and the choke point for
|
|
41
|
+
* the ATOMIC `core.hooksPath` guard (D6). A foreign/global hooksPath, or a
|
|
42
|
+
* dormant foreign slot, is REFUSED there with zero mutation and surfaced as an
|
|
43
|
+
* error here — init never overrides the user's hook manager.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
45
|
+
* - dry-run: reports "would install managed hooks", calls nothing.
|
|
46
|
+
* - Errors are swallowed and reported as status:"error" — never thrown.
|
|
46
47
|
*/
|
|
47
48
|
export const stepGitHooks = async (ctx) => {
|
|
48
49
|
const { projectDir, dryRun, onStep } = ctx;
|
|
49
50
|
const stepId = "git-hooks";
|
|
50
|
-
|
|
51
|
+
const label = "Install git hooks";
|
|
52
|
+
report(onStep, stepId, label, "running");
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
report(onStep, stepId, label, "done", "would install managed hooks");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
51
57
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
// Set core.hooksPath to ci-local/hooks
|
|
61
|
-
const hooksDir = path.join(ciLocalDest, "hooks");
|
|
62
|
-
if (await fs.pathExists(hooksDir)) {
|
|
63
|
-
// Ensure hooks are executable
|
|
64
|
-
const hookFiles = await fs.readdir(hooksDir);
|
|
65
|
-
for (const hook of hookFiles) {
|
|
66
|
-
await fs.chmod(path.join(hooksDir, hook), 0o755);
|
|
67
|
-
}
|
|
68
|
-
await execFileAsync("git", ["config", "core.hooksPath", "ci-local/hooks"], { cwd: projectDir });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
report(onStep, stepId, "Configure git hooks path", "done", "ci-local/hooks");
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
report(onStep, stepId, "Configure git hooks path", "skipped", "no ci-local dir");
|
|
58
|
+
// Lazy import: installCIHooks pulls in the CI command surface; keep it off
|
|
59
|
+
// the init cold-start path, exactly like the `ci init` dispatch branch.
|
|
60
|
+
const { installCIHooks } = await import("../../ci.js");
|
|
61
|
+
const { installed, upgraded, backups, errors, notes } = await installCIHooks(projectDir);
|
|
62
|
+
if (errors.length > 0) {
|
|
63
|
+
report(onStep, stepId, label, "error", errors.join("; "));
|
|
64
|
+
return;
|
|
75
65
|
}
|
|
66
|
+
const detail = [
|
|
67
|
+
...installed.map((h) => `installed ${h}`),
|
|
68
|
+
...upgraded.map((h) => `upgraded ${h}`),
|
|
69
|
+
...notes,
|
|
70
|
+
...backups.map((b) => `backup ${b}`),
|
|
71
|
+
].join("; ") || "managed hooks installed";
|
|
72
|
+
report(onStep, stepId, label, "done", detail);
|
|
76
73
|
}
|
|
77
74
|
catch (e) {
|
|
78
|
-
report(onStep, stepId,
|
|
75
|
+
report(onStep, stepId, label, "error", String(e));
|
|
79
76
|
}
|
|
80
77
|
};
|
|
81
78
|
//# sourceMappingURL=git.js.map
|