| 'use strict'; | ||
| const ui = require('../lib/ui.js'); | ||
| const fsx = require('../lib/fsx.js'); | ||
| const sections = require('../lib/sections.js'); | ||
| // `gatecraft prompt <name>` — print one prompt from the library. | ||
| // | ||
| // PROMPTS.md holds 62 independent prompts across ~28k tokens. Exactly one of them | ||
| // is relevant to any given task, and the whole point of a prompt is to be pasted, | ||
| // so the retrieval cost should be a command rather than a file read plus a search. | ||
| const FILE = 'PROMPTS.md'; | ||
| function help() { | ||
| ui.out(`${ui.color.bold('gatecraft prompt')} — print one prompt from the library | ||
| ${ui.color.bold('USAGE')} | ||
| gatecraft prompt # list all 62 | ||
| gatecraft prompt write-an-adr # print one | ||
| gatecraft prompt adr --md # raw markdown, for piping | ||
| gatecraft prompt --category backend # list one category | ||
| ${ui.color.bold('OPTIONS')}`); | ||
| ui.table([ | ||
| ['--md', 'Raw markdown with no terminal formatting'], | ||
| ['--category <name>', 'Restrict the listing to one category'], | ||
| ['--dir <path>', 'Read from a specific project install'], | ||
| ]); | ||
| ui.out(` | ||
| ${ui.color.bold('WHY A COMMAND')} | ||
| ${ui.color.dim('A prompt exists to be pasted. Reading 62 of them to reach one wastes the')} | ||
| ${ui.color.dim('context the prompt was going to be used in. Project prompts go in')} | ||
| ${ui.color.dim('.ai/prompts/ — this command reads the framework library.')} | ||
| `); | ||
| return 0; | ||
| } | ||
| /** Prompts are mostly fenced blocks meant to be copied; keep them verbatim. */ | ||
| function render(body) { | ||
| let fenced = false; | ||
| for (const line of fsx.lines(body)) { | ||
| if (/^\s*(```|~~~)/.test(line)) { | ||
| fenced = !fenced; | ||
| ui.out(` ${ui.color.dim(line)}`); | ||
| continue; | ||
| } | ||
| if (!fenced && /^\*\*.+\*\*/.test(line)) ui.out(` ${ui.color.bold(line.replace(/\*\*/g, ''))}`); | ||
| else ui.out(line ? ` ${line}` : ''); | ||
| } | ||
| } | ||
| function list(all, source, category) { | ||
| const shown = category | ||
| ? all.filter((p) => sections.slugify(String(p.category || '')).includes(sections.slugify(String(category)))) | ||
| : all; | ||
| if (!shown.length) { | ||
| ui.fail(`no category matching "${category}"`); | ||
| const names = [...new Set(all.map((p) => p.category).filter(Boolean))]; | ||
| ui.out(`\nCategories: ${names.map((n) => ui.color.cyan(n)).join(', ')}`); | ||
| return 1; | ||
| } | ||
| ui.step(`Prompts ${ui.color.dim(source === 'project' ? '(from this project)' : '(framework defaults)')}`); | ||
| // Width across every row, not per category, so the second column is a straight | ||
| // edge down the whole listing. Padding is applied to the raw slug — colour codes | ||
| // are invisible but not zero-length, and padding the coloured string shears it. | ||
| // | ||
| // Capped, because the longest slug is nearly twice the median: letting one | ||
| // outlier set the column would push every title past 80 characters and wrap the | ||
| // whole listing. Two names overflow the cap and take a single space instead. | ||
| const MAX_COL = 44; | ||
| const width = Math.min(MAX_COL, shown.reduce((m, p) => Math.max(m, p.slug.length), 0)); | ||
| let current = null; | ||
| for (const p of shown) { | ||
| if (p.category !== current) { | ||
| current = p.category; | ||
| ui.out(`\n ${ui.color.bold(current || 'Uncategorised')}`); | ||
| } | ||
| const pad = ' '.repeat(Math.max(0, width - p.slug.length)); | ||
| ui.out(` ${ui.color.cyan(p.slug)}${pad} ${ui.color.dim(p.title)}`); | ||
| } | ||
| ui.out(`\n ${ui.color.dim('gatecraft prompt <name> print one')}`); | ||
| ui.out(` ${ui.color.dim('gatecraft prompt <name> --md raw markdown')}\n`); | ||
| return 0; | ||
| } | ||
| async function run({ flags, args }) { | ||
| const { file, source } = sections.locate(flags, FILE); | ||
| const all = sections.parseEntries(fsx.read(file)); | ||
| if (!args[0]) return list(all, source, typeof flags.category === 'string' ? flags.category : null); | ||
| const { found, matches } = sections.resolve(all, args[0]); | ||
| if (!found) { | ||
| if (matches.length > 1) { | ||
| ui.fail(`"${args[0]}" matches ${matches.length} prompts`); | ||
| ui.out(''); | ||
| ui.table(matches.map((p) => [p.slug, `${p.title}${p.category ? ui.color.dim(` (${p.category})`) : ''}`])); | ||
| return 1; | ||
| } | ||
| ui.fail(`no prompt named "${args[0]}"`); | ||
| ui.out(`\nRun ${ui.color.cyan('gatecraft prompt')} to see all ${all.length}.`); | ||
| return 1; | ||
| } | ||
| if (flags.md) { | ||
| process.stdout.write(`### ${found.title}\n\n${found.body}\n`); | ||
| return 0; | ||
| } | ||
| ui.out(''); | ||
| ui.out(`${ui.color.bold(found.title)}${found.category ? ` ${ui.color.dim(found.category)}` : ''}`); | ||
| ui.out(''); | ||
| render(found.body); | ||
| ui.out(''); | ||
| ui.out(` ${ui.color.dim('Fill every {{placeholder}} before sending. An unfilled prompt gets a')}`); | ||
| ui.out(` ${ui.color.dim('generic answer, which is the failure this library exists to prevent.')}`); | ||
| ui.out(''); | ||
| return 0; | ||
| } | ||
| module.exports = { run, help }; |
| 'use strict'; | ||
| const ui = require('../lib/ui.js'); | ||
| const fsx = require('../lib/fsx.js'); | ||
| const sections = require('../lib/sections.js'); | ||
| // `gatecraft standard <topic>` — print one section of STANDARDS.md. | ||
| // | ||
| // STANDARDS.md is 25 sections and ~11k tokens. An agent about to write a database | ||
| // migration needs section 12, and reading the other 24 to find it is context spent | ||
| // on nothing. `--md` exists so the section can be piped straight into a prompt. | ||
| const FILE = 'STANDARDS.md'; | ||
| const SUFFIX = /\s+standards$/i; | ||
| function help() { | ||
| ui.out(`${ui.color.bold('gatecraft standard')} — print one section of the engineering standards | ||
| ${ui.color.bold('USAGE')} | ||
| gatecraft standard # list all 25 | ||
| gatecraft standard security # print one | ||
| gatecraft standard 12 # by number | ||
| gatecraft standard api --md # raw markdown, for piping | ||
| ${ui.color.bold('OPTIONS')}`); | ||
| ui.table([ | ||
| ['--md', 'Raw markdown with no terminal formatting'], | ||
| ['--dir <path>', 'Read from a specific project install'], | ||
| ]); | ||
| ui.out(` | ||
| ${ui.color.bold('WHY A COMMAND')} | ||
| ${ui.color.dim('The standards are a reference work, not a document you read front to back.')} | ||
| ${ui.color.dim('Reading all 25 sections to apply one costs context an agent needed for the')} | ||
| ${ui.color.dim('work itself. Project overrides go in .ai/standards/, never in this file.')} | ||
| `); | ||
| return 0; | ||
| } | ||
| /** Bold the `**Topic — MUST**` group headers; leave everything else as written. */ | ||
| function render(body) { | ||
| let fenced = false; | ||
| for (const line of fsx.lines(body)) { | ||
| if (/^\s*(```|~~~)/.test(line)) { | ||
| fenced = !fenced; | ||
| ui.out(` ${ui.color.dim(line)}`); | ||
| continue; | ||
| } | ||
| if (!fenced && /^\*\*.+\*\*\s*$/.test(line)) ui.out(` ${ui.color.bold(line.replace(/\*\*/g, ''))}`); | ||
| else ui.out(line ? ` ${line}` : ''); | ||
| } | ||
| } | ||
| async function run({ flags, args }) { | ||
| const { file, source } = sections.locate(flags, FILE); | ||
| const all = sections.parseNumbered(fsx.read(file), { stripSuffix: SUFFIX }); | ||
| if (!args[0]) { | ||
| ui.step(`Standards ${ui.color.dim(source === 'project' ? '(from this project)' : '(framework defaults)')}`); | ||
| ui.table(all.map((s) => [`${String(s.number).padStart(2)} ${s.slug}`, s.title])); | ||
| ui.out(`\n ${ui.color.dim('gatecraft standard <topic> print one')}`); | ||
| ui.out(` ${ui.color.dim('gatecraft standard <topic> --md raw markdown')}\n`); | ||
| return 0; | ||
| } | ||
| const { found, matches } = sections.resolve(all, args[0]); | ||
| if (!found) { | ||
| if (matches.length > 1) { | ||
| ui.fail(`"${args[0]}" matches ${matches.length} standards`); | ||
| ui.out(''); | ||
| ui.table(matches.map((s) => [s.slug, s.title])); | ||
| return 1; | ||
| } | ||
| ui.fail(`no standard named "${args[0]}"`); | ||
| ui.out(`\nRun ${ui.color.cyan('gatecraft standard')} to see all ${all.length}.`); | ||
| return 1; | ||
| } | ||
| if (flags.md) { | ||
| process.stdout.write(`## ${found.number}. ${found.title}\n\n${found.body}\n`); | ||
| return 0; | ||
| } | ||
| ui.out(''); | ||
| ui.out(`${ui.color.bold(`${found.number}. ${found.title}`)}`); | ||
| ui.out(''); | ||
| render(found.body); | ||
| ui.out(''); | ||
| ui.out(` ${ui.color.dim('MUST is not negotiable. Record a deliberate exception in')}`); | ||
| ui.out(` ${ui.color.dim('.ai/PROJECT_CONTEXT.md#12-overrides-and-exceptions with an owner and a reason.')}`); | ||
| ui.out(''); | ||
| return 0; | ||
| } | ||
| module.exports = { run, help }; |
| 'use strict'; | ||
| const path = require('path'); | ||
| const fsx = require('./fsx.js'); | ||
| const paths = require('./paths.js'); | ||
| const payload = require('./payload.js'); | ||
| const links = require('./links.js'); | ||
| // Addressing one section of a payload document from the command line. | ||
| // | ||
| // The framework is deliberately large, and every document in it is a reference | ||
| // work rather than something read front to back. STANDARDS.md is 976 lines and an | ||
| // agent implementing an endpoint needs section 13; PROMPTS.md is 2,804 lines and | ||
| // holds 62 independent prompts. Reading the whole file to reach one section costs | ||
| // thousands of tokens of context an agent could have spent on the actual work, | ||
| // and on a small context window it is the difference between fitting and not. | ||
| // | ||
| // So: one parser for the two shapes these documents use, and one resolver, shared | ||
| // by `checklist`, `standard`, and `prompt` so the three behave identically. The | ||
| // alternative — line offsets in the docs — rots the moment anything is inserted. | ||
| /** A heading turned into something typeable: `10. Security standards` -> `security`. */ | ||
| function slugify(title, stripSuffix) { | ||
| const base = stripSuffix ? title.replace(stripSuffix, '') : title; | ||
| return base | ||
| .trim() | ||
| .toLowerCase() | ||
| .replace(/[^\w]+/g, '-') | ||
| .replace(/^-+|-+$/g, ''); | ||
| } | ||
| /** Trailing `---` rules separate sections in the payload; they are not content. */ | ||
| function finish(section) { | ||
| section.body = section.lines.join('\n').replace(/\n*---\s*$/, '').trim(); | ||
| section.items = (section.body.match(/^- \[ \]/gm) || []).length; | ||
| delete section.lines; | ||
| return section; | ||
| } | ||
| /** | ||
| * Sections of the form `## N. Title` — the shape CHECKLISTS.md and STANDARDS.md | ||
| * use. Fenced code is skipped, because a `## ` inside a fence is sample output, | ||
| * not a heading. | ||
| */ | ||
| function parseNumbered(text, { stripSuffix = null } = {}) { | ||
| const out = []; | ||
| let current = null; | ||
| let fenced = false; | ||
| for (const line of fsx.lines(text)) { | ||
| if (/^\s*(```|~~~)/.test(line)) fenced = !fenced; | ||
| const m = !fenced && /^##\s+(\d+)\.\s+(.+?)\s*$/.exec(line); | ||
| if (m) { | ||
| current = { | ||
| number: Number(m[1]), | ||
| title: m[2], | ||
| slug: slugify(m[2], stripSuffix), | ||
| anchor: links.slug(`${m[1]}. ${m[2]}`), | ||
| lines: [], | ||
| }; | ||
| out.push(current); | ||
| continue; | ||
| } | ||
| if (current) current.lines.push(line); | ||
| } | ||
| return out.map(finish); | ||
| } | ||
| /** | ||
| * Entries of the form `### Name` grouped under `## N. Category` — the shape | ||
| * PROMPTS.md, TEMPLATES.md, and PLAYBOOKS.md use. Each entry carries its parent | ||
| * category so an ambiguous query can be disambiguated by where it lives. | ||
| */ | ||
| function parseEntries(text) { | ||
| const out = []; | ||
| let category = null; | ||
| let categoryNumber = 0; | ||
| let current = null; | ||
| let fenced = false; | ||
| for (const line of fsx.lines(text)) { | ||
| if (/^\s*(```|~~~)/.test(line)) fenced = !fenced; | ||
| if (!fenced) { | ||
| const cat = /^##\s+(?:(\d+)\.\s+)?(.+?)\s*$/.exec(line); | ||
| if (cat && !line.startsWith('###')) { | ||
| categoryNumber = cat[1] ? Number(cat[1]) : categoryNumber + 1; | ||
| category = cat[2]; | ||
| current = null; | ||
| continue; | ||
| } | ||
| const entry = /^###\s+(.+?)\s*$/.exec(line); | ||
| if (entry) { | ||
| current = { | ||
| number: out.length + 1, | ||
| title: entry[1], | ||
| category, | ||
| categoryNumber, | ||
| slug: slugify(entry[1]), | ||
| anchor: links.slug(entry[1]), | ||
| lines: [], | ||
| }; | ||
| out.push(current); | ||
| continue; | ||
| } | ||
| } | ||
| if (current) current.lines.push(line); | ||
| } | ||
| return out.map(finish); | ||
| } | ||
| /** | ||
| * Find the one section a query means. | ||
| * | ||
| * Exact slug beats prefix beats substring, and a query matching several returns | ||
| * them for the caller to print rather than guessing — silently picking the first | ||
| * of four security standards is worse than asking again. | ||
| */ | ||
| function resolve(all, raw) { | ||
| const query = String(raw || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, ''); | ||
| if (!query) return { query, found: null, matches: [] }; | ||
| if (/^\d+$/.test(query)) { | ||
| const byNumber = all.find((s) => s.number === Number(query)); | ||
| if (byNumber) return { query, found: byNumber, matches: [byNumber] }; | ||
| } | ||
| const exact = all.find((s) => s.slug === query); | ||
| if (exact) return { query, found: exact, matches: [exact] }; | ||
| const prefix = all.filter((s) => s.slug.startsWith(query)); | ||
| const pool = prefix.length | ||
| ? prefix | ||
| : all.filter((s) => s.slug.includes(query) || s.title.toLowerCase().includes(query.replace(/-/g, ' '))); | ||
| return { query, found: pool.length === 1 ? pool[0] : null, matches: pool }; | ||
| } | ||
| /** | ||
| * Prefer the copy installed in the project, since `.ai/standards/` overrides and | ||
| * a pinned framework version both live there. Fall back to the packaged payload | ||
| * so these commands still work outside an installed project. | ||
| */ | ||
| function locate(flags, file) { | ||
| const root = flags.dir ? path.resolve(flags.dir) : paths.findProjectRoot(); | ||
| const installed = path.join(paths.paths(root).ai, file); | ||
| if (fsx.exists(installed)) return { file: installed, source: 'project' }; | ||
| return { file: path.join(payload.source(), file), source: 'framework' }; | ||
| } | ||
| module.exports = { slugify, parseNumbered, parseEntries, resolve, locate }; |
+8
-5
| #!/bin/sh | ||
| # gatecraft installer — for projects without Node, or without a Node you want to use. | ||
| # | ||
| # curl -fsSL https://gatecraft.dev/install.sh | sh | ||
| # curl -fsSL https://gatecraft.dev/install.sh | sh -s -- --version 1.2.0 | ||
| # The short https://gatecraft.dev/install.sh URL is not live yet; use the raw | ||
| # GitHub URL until that domain is registered and serving this file. | ||
| # | ||
| # curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh | ||
| # curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh -s -- --version 1.2.0 | ||
| # | ||
| # What this does: downloads the gatecraft release tarball, extracts the framework | ||
@@ -50,4 +53,4 @@ # payload into ./.ai, adds `.ai/` to .gitignore, and writes an AGENTS.md bootstrap | ||
| ${B}USAGE${R} | ||
| curl -fsSL https://gatecraft.dev/install.sh | sh | ||
| curl -fsSL https://gatecraft.dev/install.sh | sh -s -- [options] | ||
| curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh | ||
| curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh -s -- [options] | ||
@@ -130,3 +133,3 @@ ${B}OPTIONS${R} | ||
| die "gatecraft is already installed here. | ||
| Upgrade with: curl -fsSL https://gatecraft.dev/install.sh | sh -s -- --force | ||
| Upgrade with: curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh -s -- --force | ||
| Or, with Node: npx gatecraft upgrade" | ||
@@ -133,0 +136,0 @@ fi |
+1
-1
| { | ||
| "name": "gatecraft", | ||
| "version": "1.0.1", | ||
| "version": "1.1.0", | ||
| "description": "The AI Engineering Operating System — install a complete engineering loop, quality gates, and 26 specialist roles into any repository. Hidden, gitignored, zero dependencies.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
+30
-0
@@ -29,2 +29,32 @@ # CHANGELOG.md — History of the Gatecraft | ||
| ## [1.1.0] — 2026-08-06 | ||
| Two retrieval commands, so an agent can pull one section instead of reading a whole | ||
| document. `PROMPTS.md` is 114 KB and `STANDARDS.md` is 46 KB; an agent that needed one | ||
| prompt had to load all 62 and search, which wastes context on small-window models and | ||
| risks truncation on any of them. `gatecraft checklist` already solved this for | ||
| checklists — this extends the same pattern to standards and prompts. | ||
| **What you must do.** Nothing is required; both commands are additive and no document, | ||
| section number, or anchor changed. To let your agent use them, run | ||
| `npx gatecraft@latest upgrade` to pick up the new `AGENTS.md` bootstrap section that | ||
| names them. If you customized `AGENTS.md` outside the managed marker block, your edits | ||
| are preserved. | ||
| ### Added | ||
| - `gatecraft standard <topic>` prints one section of `STANDARDS.md`. Run it bare to | ||
| list the 25 topics. `--md` emits raw markdown for piping into a prompt. | ||
| - `gatecraft prompt <name>` prints one prompt from `PROMPTS.md`. Run it bare to list | ||
| all 62 grouped by category, or `--category <name>` to list one category. | ||
| - Both resolve a query by section number, exact slug, unique prefix, then substring, | ||
| and read your installed `.ai/` copy before the packaged defaults — so local edits to | ||
| a standard are what you get back. An ambiguous query lists the matches and exits | ||
| non-zero rather than guessing. | ||
| - `AGENTS.md` gained a "Read one section, not the whole document" section listing all | ||
| three retrieval commands, with a fallback for agents that cannot run shell commands. | ||
| Previously none of them were mentioned in the bootstrap at all. | ||
| --- | ||
| ## [1.0.1] — 2026-08-05 | ||
@@ -31,0 +61,0 @@ |
| # AI Engineering Operating System (Gatecraft) | ||
| Version 1.0.1 — see [VERSION.md](VERSION.md) and [CHANGELOG.md](CHANGELOG.md). | ||
| Version 1.1.0 — see [VERSION.md](VERSION.md) and [CHANGELOG.md](CHANGELOG.md). | ||
@@ -5,0 +5,0 @@ The Gatecraft is a technology-agnostic operating system for AI coding agents and the |
| # VERSION.md — Versioning and Compatibility | ||
| **Current version: 1.0.1** | ||
| **Current version: 1.1.0** | ||
@@ -151,2 +151,3 @@ This file governs the version of the AI Engineering Operating System itself — the | ||
| | 1.0.1 | 15 | 12 | Unchanged | Installer fixes only. No document, directory, or kernel change; no override can be affected. | | ||
| | 1.1.0 | 15 | 12 | Unchanged | Retrieval commands (`standard`, `prompt`) and a bootstrap section pointing at them. No document, directory, section number, or anchor changed; no override can be affected. | | ||
@@ -153,0 +154,0 @@ Record each release here. The columns are the things an override can depend on, so a |
+42
-0
@@ -57,2 +57,25 @@ <div align="center"> | ||
| The loop is the part worth seeing. Solid arrows are the happy path; dotted arrows | ||
| are the gates sending work backwards, which is the behaviour an agent left to | ||
| itself does not have. | ||
| ```mermaid | ||
| flowchart LR | ||
| A(Understand) --> B(Research) --> C(Plan) --> D(Design) | ||
| D --> E(Implement) --> F{{Review}} --> G{{Critique}} | ||
| G --> H(Improve) --> I{{Validate}} --> J{{Test}} | ||
| J --> K(Document) --> L{{Evaluate}} | ||
| L --> M([Done]) | ||
| F -.->|defect found| E | ||
| I -.->|no evidence| H | ||
| J -.->|test fails| E | ||
| L -.->|score below 90| C | ||
| classDef gate fill:#fde68a,stroke:#b45309,color:#1c1917 | ||
| classDef done fill:#bbf7d0,stroke:#15803d,color:#1c1917 | ||
| class F,G,I,J,L gate | ||
| class M done | ||
| ``` | ||
| ## Install | ||
@@ -69,2 +92,6 @@ | ||
| <!-- Hidden until gatecraft.dev is registered and serving install.sh. Restore this | ||
| section once the domain is live; install.sh itself is unchanged and still ships | ||
| in the package. | ||
| **Without Node:** | ||
@@ -79,2 +106,4 @@ | ||
| --> | ||
| **Globally, if you install into projects often:** | ||
@@ -220,5 +249,18 @@ | ||
| | `gatecraft checklist [name]` | Print a quality gate. `--md` to pipe into a PR. | | ||
| | `gatecraft standard [topic]` | Print one of the 25 standards sections, not all 25. | | ||
| | `gatecraft prompt [name]` | Print one of the 62 prompts, ready to fill in and send. | | ||
| | `gatecraft eject` | Keep the files, drop the tooling. No lock-in. | | ||
| | `gatecraft uninstall` | Remove everything, including our `.gitignore` and `AGENTS.md` blocks. | | ||
| The three retrieval commands exist for the same reason: these documents are | ||
| reference works, and an agent that reads all of `STANDARDS.md` to apply one | ||
| section has spent ~11,000 tokens of its context to get ~1,200 tokens of answer. | ||
| `gatecraft standard security --md` returns the section alone. | ||
| ```sh | ||
| gatecraft standard api --md # one section, not the other 24 | ||
| gatecraft prompt write-an-adr --md # one prompt, not the other 61 | ||
| gatecraft prompt --category backend # what is available, before choosing | ||
| ``` | ||
| ## Upgrades will not eat your work | ||
@@ -225,0 +267,0 @@ |
+3
-1
@@ -12,2 +12,4 @@ 'use strict'; | ||
| checklist: { file: './commands/checklist.js', blurb: 'Print a quality gate checklist to run' }, | ||
| standard: { file: './commands/standard.js', blurb: 'Print one section of the engineering standards' }, | ||
| prompt: { file: './commands/prompt.js', blurb: 'Print one prompt from the library' }, | ||
| uninstall: { file: './commands/uninstall.js', blurb: 'Remove the framework and everything it added' }, | ||
@@ -30,3 +32,3 @@ eject: { file: './commands/eject.js', blurb: 'Stop managing the install; keep the files, commit them' }, | ||
| // `--dir` without this list would silently swallow the next argument. | ||
| const VALUED = new Set(['dir', 'name']); | ||
| const VALUED = new Set(['dir', 'name', 'category']); | ||
@@ -33,0 +35,0 @@ function parse(argv) { |
| 'use strict'; | ||
| const path = require('path'); | ||
| const ui = require('../lib/ui.js'); | ||
| const fsx = require('../lib/fsx.js'); | ||
| const paths = require('../lib/paths.js'); | ||
| const payload = require('../lib/payload.js'); | ||
| const links = require('../lib/links.js'); | ||
| const sections = require('../lib/sections.js'); | ||
@@ -16,2 +13,5 @@ // `gatecraft checklist <name>` exists so a gate can be run without opening a file. | ||
| const FILE = 'CHECKLISTS.md'; | ||
| const SUFFIX = /\s+checklist$/i; | ||
| function help() { | ||
@@ -39,47 +39,7 @@ ui.out(`${ui.color.bold('gatecraft checklist')} — print a quality gate checklist | ||
| /** Split CHECKLISTS.md into its `## N. Name checklist` sections. */ | ||
| function parse(text) { | ||
| const out = []; | ||
| const lines = fsx.lines(text); | ||
| let current = null; | ||
| let fenced = false; | ||
| for (const line of lines) { | ||
| if (/^\s*(```|~~~)/.test(line)) fenced = !fenced; | ||
| const m = !fenced && /^##\s+(\d+)\.\s+(.+?)\s*$/.exec(line); | ||
| if (m) { | ||
| current = { | ||
| number: Number(m[1]), | ||
| title: m[2], | ||
| slug: m[2].replace(/\s+checklist$/i, '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-|-$/g, ''), | ||
| anchor: links.slug(`${m[1]}. ${m[2]}`), | ||
| lines: [], | ||
| }; | ||
| out.push(current); | ||
| continue; | ||
| } | ||
| if (current) current.lines.push(line); | ||
| } | ||
| for (const c of out) { | ||
| c.body = c.lines.join('\n').replace(/\n*---\s*$/, '').trim(); | ||
| c.items = (c.body.match(/^- \[ \]/gm) || []).length; | ||
| delete c.lines; | ||
| } | ||
| return out; | ||
| } | ||
| function locate(flags) { | ||
| const root = flags.dir ? path.resolve(flags.dir) : paths.findProjectRoot(); | ||
| const installed = path.join(paths.paths(root).ai, 'CHECKLISTS.md'); | ||
| if (fsx.exists(installed)) return { file: installed, source: 'project' }; | ||
| return { file: path.join(payload.source(), 'CHECKLISTS.md'), source: 'framework' }; | ||
| } | ||
| async function run({ flags, args }) { | ||
| const { file, source } = locate(flags); | ||
| const all = parse(fsx.read(file)); | ||
| const query = (args[0] || '').toLowerCase().replace(/[^\w]+/g, '-'); | ||
| const { file, source } = sections.locate(flags, FILE); | ||
| const all = sections.parseNumbered(fsx.read(file), { stripSuffix: SUFFIX }); | ||
| if (!query) { | ||
| if (!args[0]) { | ||
| ui.step(`Checklists ${ui.color.dim(source === 'project' ? '(from this project)' : '(framework defaults)')}`); | ||
@@ -92,12 +52,9 @@ ui.table(all.map((c) => [`${String(c.number).padStart(2)} ${c.slug}`, `${c.items} items — ${c.title}`])); | ||
| const byNumber = /^\d+$/.test(query) ? all.find((c) => c.number === Number(query)) : null; | ||
| const exact = all.find((c) => c.slug === query); | ||
| const partial = all.filter((c) => c.slug.includes(query) || c.title.toLowerCase().includes(args[0].toLowerCase())); | ||
| const found = byNumber || exact || (partial.length === 1 ? partial[0] : null); | ||
| const { found, matches } = sections.resolve(all, args[0]); | ||
| if (!found) { | ||
| if (partial.length > 1) { | ||
| ui.fail(`"${args[0]}" matches ${partial.length} checklists`); | ||
| if (matches.length > 1) { | ||
| ui.fail(`"${args[0]}" matches ${matches.length} checklists`); | ||
| ui.out(''); | ||
| ui.table(partial.map((c) => [c.slug, c.title])); | ||
| ui.table(matches.map((c) => [c.slug, c.title])); | ||
| return 1; | ||
@@ -129,2 +86,2 @@ } | ||
| module.exports = { run, help, parse }; | ||
| module.exports = { run, help }; |
+14
-0
@@ -45,2 +45,16 @@ 'use strict'; | ||
| ### Read one section, not the whole document | ||
| \`STANDARDS.md\`, \`CHECKLISTS.md\`, and \`PROMPTS.md\` are reference works. Pull the | ||
| section you need instead of reading the file and searching it: | ||
| \`\`\`sh | ||
| gatecraft standard security --md # one of 25 sections | ||
| gatecraft checklist release --md # one of 20 gates | ||
| gatecraft prompt write-an-adr --md # one of 62 prompts | ||
| \`\`\` | ||
| Run any of the three with no argument to see what exists. If \`gatecraft\` is not on | ||
| PATH, use \`npx gatecraft\`, or read the file — but read only the section. | ||
| ### The loop is not optional | ||
@@ -47,0 +61,0 @@ |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
969471
1.72%63
5%2708
12.27%396
11.86%