dw-sdlc 0.16.1 → 0.18.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 CHANGED
@@ -35,6 +35,10 @@ lines, exit codes and document.
35
35
  }
36
36
  ```
37
37
 
38
+ A Step is a command string, or `{ "command": "…", "hint": "…" }`; a `gate.checks.run` entry takes a
39
+ `hint` beside `name` and `command`. When the Step or script fails, the Gate prints `hint: <text>` under
40
+ its failure line, and `--json` carries `hint` on every Step and Check, `null` where there is none.
41
+
38
42
  ```json
39
43
  { "scripts": { "check": "dw-sdlc gate" } }
40
44
  ```
@@ -42,6 +46,23 @@ lines, exit codes and document.
42
46
  Under `gate.checks`, a Check the package ships is `true` for its defaults or an object of its options.
43
47
  `comment-budget` holds each comment block in the JavaScript, TypeScript and shell (`.sh`) files under
44
48
  its `roots` to `maxProseLines` lines of prose, 3 by default.
49
+ `disable-directives` fails every Disable directive in a comment that gives no reason: `oxlint-disable*`,
50
+ `eslint-disable*`, `@ts-expect-error`, `@ts-ignore`, `rubocop:disable` and `noqa`. A reason follows `--`
51
+ on the directive's line, follows an `@ts-` directive, or is a comment on the line above.
52
+
53
+ `skill-sections` holds each README section a skill copies to its source. A part sits between two
54
+ markers, each on its own line, and `dw-sdlc check skill-sections --write` fills an empty pair:
55
+
56
+ ```md
57
+ <!-- README.md#errors -->
58
+ <!-- /README.md#errors -->
59
+ ```
60
+
61
+ The anchor is GitHub's, and the section runs from its heading to the next heading of its level or
62
+ above, in the `README.md` beside the `skills` directory that holds the file, or beside the file
63
+ outside one. The Check fails a part that differs from its section, a part whose anchor names no
64
+ heading of the README, a part from a section no Primitive in `.ai/primitives.json` names among its
65
+ docs, a marker without its pair, and a code fence that never closes above a marker, which hides it.
45
66
 
46
67
  ## The Ship script
47
68
 
@@ -87,7 +108,7 @@ buys one more Review past it.
87
108
  | ------------------ | ------------------------------------------------------------------------------------------ |
88
109
  | `gate` | the Steps of `gate.steps` in order, then every Check under `gate.checks` |
89
110
  | `check <name>` | one Check of the Gate by its name, without the Steps and the other Checks |
90
- | `primitives` | the Primitives that own each path, with their summaries and documents |
111
+ | `primitives` | the Primitives that own each path, or that a change removes, adds or extends |
91
112
  | `setup` | this machine's local config layer, the git excludes and the permission rules |
92
113
  | `doctor` | one PASS / WARN / FAIL line per machine check the guardrails rely on |
93
114
  | `config` | the resolved agentic config, or one file validated against the schema |
@@ -42,6 +42,11 @@
42
42
  "description": "`deny` refuses a `git commit`, `merge`, `cherry-pick` or `revert` on a branch listed in `protectedBranches`, so every change starts on a branch of its own. `allow` lets it through, for a repository that commits small fixes on its protected branch and pushes them from a person's terminal. Default `allow`. Strictest first: deny, allow.",
43
43
  "enum": ["deny", "allow"]
44
44
  },
45
+ "filteredOutput": {
46
+ "description": "`deny` refuses a check command whose output a pipe feeds to `head`, `tail`, `grep`, `egrep`, `fgrep`, `rg`, `sed`, `awk`, `less` or `more`: the pipe hides the failure and, with `pipefail` off, the exit code. A check command is the `gate.command`, a Step, a `gate.checks.run` command, `dw-sdlc gate`, `dw-sdlc check`, or a common runner such as `vitest`, `tsc` or `rspec`. `tee` and a redirect pass. `allow` turns the guard off, for a repository whose workflow pipes check output. Default `deny`.",
47
+ "enum": ["deny", "allow"],
48
+ "default": "deny"
49
+ },
45
50
  "envFiles": {
46
51
  "description": "Which env files (`.env`, `.env.*`, `.envrc`) an agent may read, edit or write. A candidate is judged in this order: listed in `deny` → refused; listed in `allow` → readable; `policy` `deny` → refused; `policy` `git` → readable when git tracks it, refused when git ignores it or does not know it. Entries are globs against the path relative to the repository root: `*` and `?` as in a shell pattern (`*` crosses `/`), a leading `**/` means at any depth, so `.env` is the one in the root and `**/.env.example` every file of that name. No brace groups. A path outside the repository root, or outside any repository, is judged by its basename against the `**/` entries alone. Defaults: policy `git`, allow the `.env.example`, `.env.sample` and `.env.template` names anywhere, deny nothing. The local override may drop `allow` entries and add `deny` entries, never the reverse.",
47
52
  "type": "object",
@@ -114,11 +119,25 @@
114
119
  "minLength": 1
115
120
  },
116
121
  "steps": {
117
- "description": "The Steps: toolchain commands run in order from the repository root through `sh -c`; the first that does not pass ends the Gate and no Check runs. Each one's output is captured, and its last 50 lines are printed when it does not pass; `--verbose` passes the output through instead. A command the shell cannot find (exit 127) or execute (126) means the Gate cannot run.",
122
+ "description": "The Steps: toolchain commands run in order from the repository root through `sh -c`; the first that does not pass ends the Gate and no Check runs. A Step is a command string, or an object with its `command` and a `hint`. Each one's output is captured, and its last 50 lines are printed when it does not pass, with the `hint` under them; `--verbose` passes the output through instead. A command the shell cannot find (exit 127) or execute (126) means the Gate cannot run.",
118
123
  "type": "array",
119
124
  "items": {
120
- "type": "string",
121
- "minLength": 1
125
+ "type": ["string", "object"],
126
+ "minLength": 1,
127
+ "properties": {
128
+ "command": {
129
+ "description": "The command the Step runs.",
130
+ "type": "string",
131
+ "minLength": 1
132
+ },
133
+ "hint": {
134
+ "description": "What to try when the Step fails: one line the Gate prints as `hint: <text>` under the failure. A passing Step prints nothing.",
135
+ "type": "string",
136
+ "minLength": 1
137
+ }
138
+ },
139
+ "required": ["command"],
140
+ "additionalProperties": false
122
141
  }
123
142
  },
124
143
  "drift": {
@@ -144,6 +163,11 @@
144
163
  "command": {
145
164
  "type": "string",
146
165
  "minLength": 1
166
+ },
167
+ "hint": {
168
+ "description": "What to try when the script fails: one line the Gate prints as `hint: <text>` under the failure. A passing script prints nothing.",
169
+ "type": "string",
170
+ "minLength": 1
147
171
  }
148
172
  },
149
173
  "required": ["name", "command"],
@@ -379,6 +403,47 @@
379
403
  }
380
404
  },
381
405
  "additionalProperties": false
406
+ },
407
+ "disable-directives": {
408
+ "description": "Every Disable directive in a comment gives a reason: `oxlint-disable*` and `eslint-disable*` in every form, `@ts-expect-error` and `@ts-ignore` in JavaScript and TypeScript, `rubocop:disable` in Ruby and `noqa` in Python. A reason is text after `--` on the directive's line; for `@ts-expect-error` and `@ts-ignore`, text after the directive, with or without `:`; or a comment on the line directly above. A directive inside a string or a template is none, and a rule set to `off` in a lint config is not judged. A hard fail, with no Ratchet; the Gate runs it, never a session hook.",
409
+ "type": ["boolean", "object"],
410
+ "properties": {
411
+ "files": {
412
+ "description": "Globs of the files to read, against the files git tracks or would track; `*` and `**` skip a name that starts with a dot. Default every JavaScript, TypeScript, Ruby (`.rb`, `.rake`, `.gemspec`, `.ru`, `Rakefile`, `Gemfile`) and Python file.",
413
+ "type": "array",
414
+ "items": {
415
+ "type": "string",
416
+ "minLength": 1
417
+ },
418
+ "uniqueItems": true
419
+ },
420
+ "exempt": {
421
+ "description": "Globs of the files left out. Default `references/**`, `**/vendor/**` and `**/node_modules/**`; a list replaces the default.",
422
+ "type": "array",
423
+ "items": {
424
+ "type": "string",
425
+ "minLength": 1
426
+ },
427
+ "uniqueItems": true
428
+ }
429
+ },
430
+ "additionalProperties": false
431
+ },
432
+ "skill-sections": {
433
+ "description": "Every README section a skill copies matches its source. A part is the section of the README under the heading with GitHub's anchor `<anchor>`, up to the next heading of its level or above, copied whole between `<!-- README.md#<anchor> -->` and `<!-- /README.md#<anchor> -->`, each on its own line, with a blank line inside each. The README is the `README.md` of the directory that holds the `skills` directory the file is in, or of the file's own directory outside one. A part fails when it differs from its section, when the README has no heading with its anchor, and when no Primitive in the catalogue `primitives` reads names `<README>#<anchor>` among its docs; a marker without its pair fails too. A marker inside code is none, and a code fence that never closes above a marker fails, since it hides that marker. `dw-sdlc check skill-sections --write` writes each part from its section. A repository with no part passes without reading the catalogue; one with a part and no valid catalogue cannot run.",
434
+ "type": ["boolean", "object"],
435
+ "properties": {
436
+ "files": {
437
+ "description": "Globs of the skill files, against the files git tracks or would track; `*` and `**` skip a name that starts with a dot. Default `**/skills/**/*.md`.",
438
+ "type": "array",
439
+ "items": {
440
+ "type": "string",
441
+ "minLength": 1
442
+ },
443
+ "uniqueItems": true
444
+ }
445
+ },
446
+ "additionalProperties": false
382
447
  }
383
448
  },
384
449
  "additionalProperties": false
@@ -54,11 +54,21 @@ export type Guardrails = {
54
54
  export type GuardrailsLayer = Partial<Omit<Guardrails, "envFiles">> & {
55
55
  envFiles?: Partial<EnvFiles>;
56
56
  };
57
- /** A repository's own script under `gate.checks.run`, reported as a Check under `name`. */
57
+ /** A repository's own script under `gate.checks.run`, reported as a Check under `name`; `hint` is what to try when it fails. */
58
58
  export type RunCheck = {
59
59
  name: string;
60
60
  command: string;
61
+ hint?: string;
61
62
  };
63
+ /** A Step of `gate.steps`: its command, or the command with a `hint` on what to try when it fails. */
64
+ export type Step = string | {
65
+ command: string;
66
+ hint?: string;
67
+ };
68
+ /** The command a Step runs. */
69
+ export declare function stepCommand(step: Step): string;
70
+ /** The Hint a Step carries, null where it has none. */
71
+ export declare function stepHint(step: Step): string | null;
62
72
  /**
63
73
  * `run`, and under each other key a Check the package ships, as the schema takes it: `true`, `false` or
64
74
  * its options. `src/checks/` reads the setting of each.
@@ -72,14 +82,14 @@ export type DriftSetting = "fail" | "allow";
72
82
  /** The Gate: its command, the steps run in order, whether they may write the tree, the Checks run after them. */
73
83
  export type Gate = {
74
84
  command?: string;
75
- steps: string[];
85
+ steps: Step[];
76
86
  drift: DriftSetting;
77
87
  checks: GateChecks;
78
88
  };
79
89
  /** `gate` as one layer writes it: every key optional, `checks` Check by Check. */
80
90
  export type GateLayer = {
81
91
  command?: string;
82
- steps?: string[];
92
+ steps?: Step[];
83
93
  drift?: DriftSetting;
84
94
  checks?: Partial<GateChecks>;
85
95
  };
@@ -15,6 +15,14 @@ export const COMMIT_CONVENTIONS = ["ticket-key", "conventional"];
15
15
  export const REVIEW_DEPTHS = ["full", "light", "off"];
16
16
  /** Claude Code's effort levels, lowest first: what `worker.effort` takes. */
17
17
  export const WORKER_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
18
+ /** The command a Step runs. */
19
+ export function stepCommand(step) {
20
+ return typeof step === "string" ? step : step.command;
21
+ }
22
+ /** The Hint a Step carries, null where it has none. */
23
+ export function stepHint(step) {
24
+ return typeof step === "string" ? null : (step.hint ?? null);
25
+ }
18
26
  /** Strictest value first; the local override may only move towards the front. */
19
27
  export const STRICTNESS = {
20
28
  forcePush: ["deny", "ask", "lease"],
@@ -27,6 +27,10 @@ export type CatalogueRead = {
27
27
  catalogue: Catalogue;
28
28
  text: string;
29
29
  };
30
+ /** The catalogue `text` holds, or why it is not one. */
31
+ export declare function parseCatalogue(text: string): Exclude<CatalogueRead, {
32
+ state: "missing";
33
+ }>;
30
34
  /** The catalogue at `file` under `root`, or why there is none to read. */
31
35
  export declare function readCatalogue(root: string, file: string): CatalogueRead;
32
36
  /**
package/dist/catalogue.js CHANGED
@@ -34,13 +34,8 @@ const SCHEMA = {
34
34
  },
35
35
  additionalProperties: false
36
36
  };
37
- /** The catalogue at `file` under `root`, or why there is none to read. */
38
- export function readCatalogue(root, file) {
39
- const path = join(root, file);
40
- if (!existsSync(path)) {
41
- return { state: "missing" };
42
- }
43
- const text = readFileSync(path, "utf8");
37
+ /** The catalogue `text` holds, or why it is not one. */
38
+ export function parseCatalogue(text) {
44
39
  let parsed;
45
40
  try {
46
41
  parsed = JSON.parse(text);
@@ -55,6 +50,11 @@ export function readCatalogue(root, file) {
55
50
  // The schema above is the runtime twin of `Catalogue`: a document it accepts has this shape.
56
51
  return { state: "valid", catalogue: parsed, text };
57
52
  }
53
+ /** The catalogue at `file` under `root`, or why there is none to read. */
54
+ export function readCatalogue(root, file) {
55
+ const path = join(root, file);
56
+ return existsSync(path) ? parseCatalogue(readFileSync(path, "utf8")) : { state: "missing" };
57
+ }
58
58
  /**
59
59
  * How near the `code` entry `entry`, a file or a directory, names `path`: the length of the entry when
60
60
  * it is the path or a directory above it, -1 when it is neither.
@@ -22,6 +22,8 @@ export type Check<Options extends object> = {
22
22
  run: (root: string, options: Options) => Finding[];
23
23
  /** Present on a Ratchet. */
24
24
  baseline?: Baseline<Options>;
25
+ /** Present on a Check that derives files from their sources: writes them, says what it wrote. */
26
+ write?: (root: string, options: Options) => string;
25
27
  };
26
28
  /** The options `setting` turns the Check on with: the defaults under `true`, an object over them, else off. */
27
29
  export declare function optionsOf<Options extends object>(check: Check<Options>, setting: unknown): Options | undefined;
@@ -2,262 +2,11 @@
2
2
  // of `//` or shell `#` lines alone on theirs, carries at most `maxProseLines` lines with a letter or a digit.
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join, matchesGlob } from "node:path";
5
+ import { commentsOf, shellCommentsOf } from "./comments.js";
5
6
  import { matching, treeFiles } from "./tree.js";
6
7
  const SOURCE = ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"];
7
8
  const SHELL = "**/*.sh";
8
9
  const REMEDY = "cut it to what the code cannot say, or move the reasoning into a doc and point at it";
9
- /** The characters after which a `/` opens a regular expression, not a division. */
10
- const BEFORE_REGEX = new Set("(,=:[!&|?{};+-*%<>~^");
11
- const KEYWORD_BEFORE_REGEX = /(^|[^\w$])(return|typeof|instanceof|in|of|new|delete|void|throw|case|do|else|yield|await)$/;
12
- /** Where the regular expression whose `/` is at `start` ends, or undefined when the line ends first. */
13
- function regexEnd(text, start) {
14
- let inClass = false;
15
- for (let index = start + 1; index < text.length; index += 1) {
16
- const char = text[index];
17
- if (char === "\\") {
18
- index += 1;
19
- }
20
- else if (char === "\n") {
21
- return undefined;
22
- }
23
- else if (char === "[" || char === "]") {
24
- inClass = char === "[";
25
- }
26
- else if (char === "/" && !inClass) {
27
- return index + 1;
28
- }
29
- }
30
- return undefined;
31
- }
32
- /** Whether the `/` at `index` opens a regular expression: the code before it ends where a value starts. */
33
- function opensRegex(text, index) {
34
- const before = text.slice(0, index).trimEnd();
35
- const last = before.at(-1);
36
- return last === undefined || BEFORE_REGEX.has(last) || KEYWORD_BEFORE_REGEX.test(before);
37
- }
38
- /** Every comment in JavaScript or TypeScript `text`, skipping strings, template literals and regular expressions. */
39
- function commentsOf(text) {
40
- const comments = [];
41
- // The open quotes, and a `{` for each brace opened inside a template literal's `${…}`.
42
- const stack = [];
43
- let line = 1;
44
- let index = 0;
45
- while (index < text.length) {
46
- const char = text[index];
47
- const next = text[index + 1];
48
- const top = stack.at(-1);
49
- if (top === "'" || top === '"' || top === "`") {
50
- if (char === "\\") {
51
- line += next === "\n" ? 1 : 0;
52
- index += 2;
53
- continue;
54
- }
55
- if (top === "`" && char === "$" && next === "{") {
56
- stack.push("{");
57
- index += 2;
58
- continue;
59
- }
60
- if (char === top) {
61
- stack.pop();
62
- }
63
- line += char === "\n" ? 1 : 0;
64
- index += 1;
65
- continue;
66
- }
67
- if (char === "'" || char === '"' || char === "`" || (char === "{" && top === "{")) {
68
- stack.push(char);
69
- }
70
- else if (char === "}" && top === "{") {
71
- stack.pop();
72
- }
73
- else if (char === "/" && next === "/") {
74
- const stop = text.indexOf("\n", index) === -1 ? text.length : text.indexOf("\n", index);
75
- const alone = text.slice(text.lastIndexOf("\n", index - 1) + 1, index).trim() === "";
76
- comments.push({ kind: alone ? "line" : "trailing", start: line, end: line, text: text.slice(index, stop) });
77
- index = stop;
78
- continue;
79
- }
80
- else if (char === "/" && next === "*") {
81
- const close = text.indexOf("*/", index + 2);
82
- const body = text.slice(index, close === -1 ? text.length : close + 2);
83
- const start = line;
84
- line += body.split("\n").length - 1;
85
- comments.push({ kind: "block", start, end: line, text: body });
86
- index += body.length;
87
- continue;
88
- }
89
- else if (char === "/" && opensRegex(text, index)) {
90
- index = regexEnd(text, index) ?? index + 1;
91
- continue;
92
- }
93
- line += char === "\n" ? 1 : 0;
94
- index += 1;
95
- }
96
- return comments;
97
- }
98
- /** The characters that end a shell word, so a `#` after one opens a comment. */
99
- const WORD_BREAK = new Set(" \t\n;&|()<>");
100
- /** Where the line that holds `index` ends: at its newline, or at the end of `text`. */
101
- function lineEnd(text, index) {
102
- const newline = text.indexOf("\n", index);
103
- return newline === -1 ? text.length : newline;
104
- }
105
- /** Where a span that `closer` ends, read from `start`, stops, past the closer; `\` escapes a character when `escapes`. */
106
- function spanEnd(text, start, closer, escapes) {
107
- for (let index = start; index < text.length; index += 1) {
108
- if (escapes && text.charAt(index) === "\\") {
109
- index += 1;
110
- }
111
- else if (text.charAt(index) === closer) {
112
- return index + 1;
113
- }
114
- }
115
- return text.length;
116
- }
117
- /** The heredoc whose `<<` is at `start`, its delimiter's quotes off, and where the delimiter's word ends. */
118
- function heredocAt(text, start) {
119
- const strip = text.charAt(start + 2) === "-";
120
- let index = start + (strip ? 3 : 2);
121
- while (text.charAt(index) === " " || text.charAt(index) === "\t") {
122
- index += 1;
123
- }
124
- let end = index;
125
- while (end < text.length && !WORD_BREAK.has(text.charAt(end))) {
126
- const char = text.charAt(end);
127
- end = char === "'" || char === '"' ? spanEnd(text, end + 1, char, false) : end + (char === "\\" ? 2 : 1);
128
- }
129
- return { delimiter: text.slice(index, end).replace(/["'\\]/g, ""), strip, end };
130
- }
131
- /** Where the heredoc body that starts at `start` ends: past the line that is its delimiter. */
132
- function heredocEnd(text, start, { delimiter, strip }) {
133
- for (let index = start; index < text.length;) {
134
- const end = lineEnd(text, index);
135
- const line = text.slice(index, end);
136
- index = end + 1;
137
- if ((strip ? line.replace(/^\t+/, "") : line) === delimiter) {
138
- return index;
139
- }
140
- }
141
- return text.length;
142
- }
143
- /** The character that opens a nesting inside each frame and the one that closes it; a double quote has none. */
144
- const NESTING = {
145
- code: ["(", ")"],
146
- arithmetic: ["(", ")"],
147
- expansion: ["{", "}"],
148
- double: []
149
- };
150
- /**
151
- * Every `#` comment in shell `text`: a `#` that starts a word of code, never one in a quote, an
152
- * expansion, an arithmetic `((…))` or a heredoc body, nor the shebang on line 1.
153
- */
154
- function shellCommentsOf(text) {
155
- const comments = [];
156
- const newlines = [...text.matchAll(/\n/g)].map((match) => match.index);
157
- const outer = [];
158
- let frame = { kind: "code", depth: 0 };
159
- const heredocs = [];
160
- let wordStart = true;
161
- let index = text.startsWith("#!") ? lineEnd(text, 0) : 0;
162
- const open = (kind, length) => {
163
- outer.push(frame);
164
- frame = { kind, depth: 0 };
165
- index += length;
166
- wordStart = true;
167
- };
168
- const close = (length) => {
169
- frame = outer.pop() ?? frame;
170
- index += length;
171
- wordStart = false;
172
- };
173
- const skipTo = (end) => {
174
- index = end;
175
- wordStart = false;
176
- };
177
- while (index < text.length) {
178
- const char = text.charAt(index);
179
- const next = text.charAt(index + 1);
180
- const { kind } = frame;
181
- const [opener, closer] = NESTING[kind];
182
- if (char === "\\" && next === "\n") {
183
- index += 2;
184
- }
185
- else if (char === "\\") {
186
- skipTo(index + 2);
187
- }
188
- else if (kind === "double" && char === '"') {
189
- close(1);
190
- }
191
- else if (char === "$" && text.startsWith("((", index + 1)) {
192
- open("arithmetic", 3);
193
- }
194
- else if (char === "$" && next === "(") {
195
- open("code", 2);
196
- }
197
- else if (char === "$" && next === "{") {
198
- open("expansion", 2);
199
- }
200
- else if (char === "`") {
201
- skipTo(spanEnd(text, index + 1, "`", true));
202
- }
203
- else if (kind === "double") {
204
- index += 1;
205
- }
206
- else if (char === "$" && next === "'") {
207
- skipTo(spanEnd(text, index + 2, "'", true));
208
- }
209
- else if (char === "'") {
210
- skipTo(spanEnd(text, index + 1, "'", false));
211
- }
212
- else if (char === '"') {
213
- open("double", 1);
214
- }
215
- else if (kind === "code" && char === "#" && wordStart) {
216
- const end = lineEnd(text, index);
217
- const line = newlines.filter((newline) => newline < index).length + 1;
218
- const alone = text.slice(text.lastIndexOf("\n", index - 1) + 1, index).trim() === "";
219
- comments.push({ kind: alone ? "line" : "trailing", start: line, end: line, text: text.slice(index, end) });
220
- index = end;
221
- }
222
- else if (kind === "code" && char === "\n") {
223
- index += 1;
224
- for (const heredoc of heredocs.splice(0)) {
225
- index = heredocEnd(text, index, heredoc);
226
- }
227
- wordStart = true;
228
- }
229
- else if (kind === "code" && text.startsWith("<<<", index)) {
230
- index += 3;
231
- wordStart = true;
232
- }
233
- else if (kind === "code" && text.startsWith("<<", index)) {
234
- const { end, ...heredoc } = heredocAt(text, index);
235
- heredocs.push(heredoc);
236
- skipTo(end);
237
- }
238
- else if (kind === "code" && text.startsWith("((", index)) {
239
- open("arithmetic", 2);
240
- }
241
- else if (char === opener) {
242
- frame.depth += 1;
243
- index += 1;
244
- wordStart = true;
245
- }
246
- else if (char === closer && frame.depth > 0) {
247
- frame.depth -= 1;
248
- index += 1;
249
- wordStart = true;
250
- }
251
- else if (char === closer && outer.length > 0) {
252
- close(kind === "arithmetic" && next === ")" ? 2 : 1);
253
- }
254
- else {
255
- wordStart = WORD_BREAK.has(char);
256
- index += 1;
257
- }
258
- }
259
- return comments;
260
- }
261
10
  /** The comments as blocks: a run of `//` or `#` lines alone on consecutive lines is one; a comment after code is none. */
262
11
  function blocksOf(comments) {
263
12
  const blocks = [];
@@ -0,0 +1,29 @@
1
+ /** A `//` or `#` comment alone on its line, one after code, or a block comment, by its lines and text. */
2
+ export type Comment = {
3
+ kind: "line" | "trailing" | "block";
4
+ start: number;
5
+ end: number;
6
+ text: string;
7
+ };
8
+ /** Every comment in JavaScript or TypeScript `text`, skipping strings, template literals and regular expressions. */
9
+ export declare function commentsOf(text: string): Comment[];
10
+ /** Where the line that holds `index` ends: at its newline, or at the end of `text`. */
11
+ export declare function lineEnd(text: string, index: number): number;
12
+ /** Where a span that `closer` ends, read from `start`, stops, past the closer; `\` escapes a character when `escapes`. */
13
+ export declare function spanEnd(text: string, start: number, closer: string, escapes: boolean): number;
14
+ /** A heredoc whose body starts on the next line: its delimiter, and whether `<<-` strips the body's leading tabs. */
15
+ export type Heredoc = {
16
+ delimiter: string;
17
+ strip: boolean;
18
+ };
19
+ /** The heredoc whose `<<` is at `start`, its delimiter's quotes off, and where the delimiter's word ends. */
20
+ export declare function heredocAt(text: string, start: number): Heredoc & {
21
+ end: number;
22
+ };
23
+ /** Where the heredoc body that starts at `start` ends: past the line that is its delimiter. */
24
+ export declare function heredocEnd(text: string, start: number, { delimiter, strip }: Heredoc): number;
25
+ /**
26
+ * Every `#` comment in shell `text`: a `#` that starts a word of code, never one in a quote, an
27
+ * expansion, an arithmetic `((…))` or a heredoc body, nor the shebang on line 1.
28
+ */
29
+ export declare function shellCommentsOf(text: string): Comment[];