diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 072eb4439..bb7dea8a8 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -520,6 +520,21 @@ jobs: - name: Plugin suite run: node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/**/*.test.js' + # A Windows commit's hooks (#948), less the three cli jobs that need cli/'s dependencies. + - name: Hooks - a commit's hooks pass on Windows + run: | + echo "# lefthook subject" > "$RUNNER_TEMP/msg" + status=0 + LEFTHOOK_EXCLUDE=cli-biome,cli-architecture,cli-typecheck \ + pnpm exec lefthook run pre-commit --all-files --no-auto-install --no-tty > "$RUNNER_TEMP/hooks.log" 2>&1 || status=1 + pnpm exec lefthook run prepare-commit-msg "$RUNNER_TEMP/msg" message --no-auto-install --no-tty >> "$RUNNER_TEMP/hooks.log" 2>&1 || status=1 + cat "$RUNNER_TEMP/hooks.log" + if grep -n "syntax error" "$RUNNER_TEMP/hooks.log"; then + echo "::error::lefthook handed sh a broken command line (#948)" + exit 1 + fi + exit $status + - run: cd cli && pnpm install --frozen-lockfile # A real global install, not `node dist/cli.js` by path: nothing else proves `aidd` # resolves on this platform's PATH. Built here, ahead of the suites below, so this is diff --git a/lefthook.yml b/lefthook.yml index 00d136edb..df11272be 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -7,7 +7,7 @@ pre-commit: - "cli/tests/fixtures/**" run: | if ! command -v node >/dev/null 2>&1; then - echo "❌ node not available; cannot validate JSON" + echo '❌ node not available; cannot validate JSON' exit 1 fi node scripts/validate-json.mjs {files} @@ -17,7 +17,7 @@ pre-commit: - "plugins/*/skills/*/assets/**" run: | if ! command -v node >/dev/null 2>&1; then - echo "❌ node not available; cannot validate YAML" + echo '❌ node not available; cannot validate YAML' exit 1 fi node scripts/validate-yaml.mjs {files} @@ -25,14 +25,14 @@ pre-commit: glob: "{scripts,plugins,cli/src}/**" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping scripts-tests" + echo 'ℹ️ node not available; skipping scripts-tests' exit 0 fi # `node --test ` fails on a directory, so a glob is required — and a glob # matching nothing exits 0 in silence, so a renamed folder must be caught by a count. found=$(ls scripts/__tests__/*.test.js 2>/dev/null | wc -l | tr -d ' ') - if [ "$found" -eq 0 ]; then - echo "❌ no test files under scripts/__tests__/; the runner is not wired" + if [ $found -eq 0 ]; then + echo '❌ no test files under scripts/__tests__/; the runner is not wired' exit 1 fi # Wrapped: a suite writing into this repository's own .git/hooks passes every @@ -43,13 +43,13 @@ pre-commit: glob: "plugins/*/skills/*/SKILL.md" run: | for f in {files}; do - [ -f "$f" ] || continue - if ! head -10 "$f" | grep -q "^name:"; then - echo "❌ missing 'name:' frontmatter: $f" + [ -f $f ] || continue + if ! head -10 $f | grep -q '^name:'; then + echo '❌ missing name: frontmatter:' $f exit 1 fi - if ! head -10 "$f" | grep -q "^description:"; then - echo "❌ missing 'description:' frontmatter: $f" + if ! head -10 $f | grep -q '^description:'; then + echo '❌ missing description: frontmatter:' $f exit 1 fi done @@ -57,7 +57,7 @@ pre-commit: glob: "plugins/*/skills/**" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping check-skill-argument-hints" + echo 'ℹ️ node not available; skipping check-skill-argument-hints' exit 0 fi node scripts/check-skill-argument-hints.mjs @@ -65,7 +65,7 @@ pre-commit: glob: "plugins/*/{skills,agents}/**/*.md" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping architecture-rules" + echo 'ℹ️ node not available; skipping architecture-rules' exit 0 fi node scripts/check-architecture-rules.js {files} @@ -76,7 +76,7 @@ pre-commit: - "**/{CLAUDE,AGENTS,copilot-instructions}.md" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping context-imports" + echo 'ℹ️ node not available; skipping context-imports' exit 0 fi node scripts/check-context-imports.js @@ -89,7 +89,7 @@ pre-commit: - "plugins/aidd-context/hooks/update_memory.js" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping context-reference-form" + echo 'ℹ️ node not available; skipping context-reference-form' exit 0 fi node scripts/check-context-reference-form.js @@ -97,7 +97,7 @@ pre-commit: glob: "{*.md,docs/**,aidd_docs/memory/**}" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping referenced-paths" + echo 'ℹ️ node not available; skipping referenced-paths' exit 0 fi node scripts/check-referenced-paths.js @@ -106,7 +106,7 @@ pre-commit: glob: "**/*.md" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping doc-duplication" + echo 'ℹ️ node not available; skipping doc-duplication' exit 0 fi node scripts/check-doc-duplication.js @@ -115,33 +115,18 @@ pre-commit: glob: "**/*.md" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping markdown-links" + echo 'ℹ️ node not available; skipping markdown-links' exit 0 fi # Both ignored trees hold deliberately unresolvable targets — fixture paths and # `@src/foo.ts` agent mentions — not links a reader is meant to follow. node scripts/check-markdown-links.js --ignore cli/tests/fixtures --ignore cli/aidd_docs/tasks summarize-plugin-catalogs: - run: | - [ -d plugins ] || exit 0 - if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping summarize-plugin-catalogs" - exit 0 - fi - for plugin in plugins/*/; do - name=$(basename "$plugin") - node scripts/summarize-markdown.js "$plugin" "${plugin}CATALOG.md" \ - --depth=4 \ - --fields=description \ - --title="$name catalog" \ - --tagline="Auto-generated index of skills, agents, references and assets shipped by the \`$name\` plugin." \ - >/dev/null - git add "${plugin}CATALOG.md" 2>/dev/null || true - done + run: sh scripts/summarize-plugin-catalogs.sh summarize-telemetry-prompts-doc: run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping summarize-telemetry-prompts-doc" + echo 'ℹ️ node not available; skipping summarize-telemetry-prompts-doc' exit 0 fi node scripts/summarize-markdown.js plugins/aidd-telemetry docs/prompts-documentation.md \ @@ -152,7 +137,7 @@ pre-commit: sync-readme-counts: run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping sync-readme-counts" + echo 'ℹ️ node not available; skipping sync-readme-counts' exit 0 fi node scripts/sync-readme-counts.mjs >/dev/null @@ -174,7 +159,7 @@ pre-commit: glob: "{cli/src/**,cli/tests/**}" run: | if ! command -v node >/dev/null 2>&1; then - echo "ℹ️ node not available; skipping cli-type-honesty" + echo 'ℹ️ node not available; skipping cli-type-honesty' exit 0 fi node scripts/check-cli-type-honesty.mjs @@ -195,9 +180,8 @@ commit-msg: # lefthook owns prepare-commit-msg here and regenerates it on install, so `aidd telemetry on`'s # delegate is called from a job instead. Absent delegate, nothing runs — opt-in per machine. +# `aidd telemetry` finds this job by the delegate name in its run line. prepare-commit-msg: commands: aidd-session-trailer: - run: | - delegate="$(git rev-parse --git-common-dir)/hooks/aidd-session-trailer.sh" - if [ -f "$delegate" ]; then sh "$delegate" {1} {2}; fi + run: sh scripts/session-trailer-delegate.sh aidd-session-trailer.sh {1} {2} diff --git a/scripts/__tests__/lefthook-runs-hold-no-double-quote.test.js b/scripts/__tests__/lefthook-runs-hold-no-double-quote.test.js new file mode 100644 index 000000000..4717bcf30 --- /dev/null +++ b/scripts/__tests__/lefthook-runs-hold-no-double-quote.test.js @@ -0,0 +1,24 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); +const yaml = require("js-yaml"); + +const root = path.resolve(__dirname, "../.."); + +// Lefthook on Windows passes a run to `sh -c` unescaped, so a `"` truncates it (#948). +test("no run in lefthook.yml holds a double quote", () => { + const config = yaml.load(fs.readFileSync(path.join(root, "lefthook.yml"), "utf8")); + const offenders = []; + let scanned = 0; + + for (const [hook, def] of Object.entries(config)) { + for (const [name, command] of Object.entries(def?.commands ?? {})) { + scanned++; + if (String(command.run ?? "").includes('"')) offenders.push(`${hook}/${name}`); + } + } + + assert.ok(scanned > 0, "no `commands:` run found in lefthook.yml"); + assert.deepEqual(offenders, []); +}); diff --git a/scripts/__tests__/skill-argument-hints.test.js b/scripts/__tests__/skill-argument-hints.test.js index 90902e92f..fd394701b 100644 --- a/scripts/__tests__/skill-argument-hints.test.js +++ b/scripts/__tests__/skill-argument-hints.test.js @@ -64,3 +64,12 @@ test("a skill whose actions take no input needs no argument-hint", () => { assert.equal(clean.status, 0, clean.output); }); + +test("a skill checked out with CRLF line endings still reads its argument-hint", () => { + const clean = runOn({ + [SKILL]: frontmatter(["argument-hint: a file path or a ticket"]).replaceAll("\n", "\r\n"), + "plugins/aidd-probe/skills/01-thing/actions/01-do.md": "## Input\r\n\r\nA path.", + }); + + assert.equal(clean.status, 0, clean.output); +}); diff --git a/scripts/__tests__/sync-readme-counts.test.js b/scripts/__tests__/sync-readme-counts.test.js new file mode 100644 index 000000000..a7068127e --- /dev/null +++ b/scripts/__tests__/sync-readme-counts.test.js @@ -0,0 +1,12 @@ +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); +const test = require("node:test"); + +const SCRIPT = path.resolve(__dirname, "..", "sync-readme-counts.mjs"); + +test("the README counts check finds the README and agrees with the tree", () => { + const result = spawnSync(process.execPath, [SCRIPT, "--check"], { encoding: "utf8" }); + + assert.equal(result.status, 0, `${result.stdout}${result.stderr}`); +}); diff --git a/scripts/check-skill-argument-hints.mjs b/scripts/check-skill-argument-hints.mjs index 258f1b136..cfc498c02 100644 --- a/scripts/check-skill-argument-hints.mjs +++ b/scripts/check-skill-argument-hints.mjs @@ -67,7 +67,7 @@ function argumentHint(content) { async function takesInput(actionFiles) { for (const file of actionFiles) { const body = await readFile(file, "utf8"); - if (/^## Input$/m.test(body)) return true; + if (/^## Input\r?$/m.test(body)) return true; } return actionFiles.length === 0; } @@ -75,8 +75,8 @@ async function takesInput(actionFiles) { const breaches = []; for (const dir of await skillDirs()) { const skillPath = path.join(dir, "SKILL.md"); - const content = await readFile(skillPath, "utf8").catch(() => null); - if (content === null) continue; + const content = (await readFile(skillPath, "utf8").catch(() => null))?.replaceAll("\r\n", "\n"); + if (content == null) continue; // Named the way the contract and every other guard name a path, whatever the host separator. const relative = path.relative(ROOT, skillPath).split(path.sep).join("/"); diff --git a/scripts/session-trailer-delegate.sh b/scripts/session-trailer-delegate.sh new file mode 100755 index 000000000..dd23394a7 --- /dev/null +++ b/scripts/session-trailer-delegate.sh @@ -0,0 +1,4 @@ +#!/bin/sh +delegate="$(git rev-parse --git-common-dir)/hooks/$1" +shift +if [ -f "$delegate" ]; then sh "$delegate" "$@"; fi diff --git a/scripts/summarize-plugin-catalogs.sh b/scripts/summarize-plugin-catalogs.sh new file mode 100755 index 000000000..0863bbb89 --- /dev/null +++ b/scripts/summarize-plugin-catalogs.sh @@ -0,0 +1,16 @@ +#!/bin/sh +[ -d plugins ] || exit 0 +if ! command -v node >/dev/null 2>&1; then + echo "ℹ️ node not available; skipping summarize-plugin-catalogs" + exit 0 +fi +for plugin in plugins/*/; do + name=$(basename "$plugin") + node scripts/summarize-markdown.js "$plugin" "${plugin}CATALOG.md" \ + --depth=4 \ + --fields=description \ + --title="$name catalog" \ + --tagline="Auto-generated index of skills, agents, references and assets shipped by the \`$name\` plugin." \ + >/dev/null + git add "${plugin}CATALOG.md" 2>/dev/null || true +done diff --git a/scripts/sync-readme-counts.mjs b/scripts/sync-readme-counts.mjs index d3165acf4..6c47ae300 100644 --- a/scripts/sync-readme-counts.mjs +++ b/scripts/sync-readme-counts.mjs @@ -4,8 +4,9 @@ // `N skill(s)` code-span after that plugin's heading. `--check` reports instead of writing. import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; -const ROOT = new URL("..", import.meta.url).pathname; +const ROOT = fileURLToPath(new URL("..", import.meta.url)); const README = ROOT + "README.md"; const PLUGINS = ROOT + "plugins";