asciidoclint 1.0.0 → 1.2.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.
Files changed (90) hide show
  1. package/README.md +79 -16
  2. package/dist/api/lint.js +41 -4
  3. package/dist/api/rules.js +1 -1
  4. package/dist/cli/index.js +7 -7
  5. package/dist/cli/install-skill.d.ts +5 -0
  6. package/dist/cli/install-skill.js +131 -42
  7. package/dist/parsers/asciidoctor.d.ts +2 -1
  8. package/dist/parsers/asciidoctor.js +150 -1
  9. package/dist/parsers/tolerant.js +29 -5
  10. package/dist/rules/AD004.js +99 -0
  11. package/dist/rules/AD009.d.ts +2 -0
  12. package/dist/rules/AD009.js +80 -0
  13. package/dist/rules/AD010.js +17 -3
  14. package/dist/rules/AD011.js +20 -5
  15. package/dist/rules/AD012.js +14 -2
  16. package/dist/rules/AD013.js +2 -2
  17. package/dist/rules/AD016.js +16 -10
  18. package/dist/rules/AD017.js +4 -4
  19. package/dist/rules/AD020.js +41 -20
  20. package/dist/rules/AD025.js +2 -2
  21. package/dist/rules/AD028.js +77 -10
  22. package/dist/rules/AD029.js +2 -2
  23. package/dist/rules/AD039.js +2 -2
  24. package/dist/rules/AD046.d.ts +2 -0
  25. package/dist/rules/AD046.js +91 -0
  26. package/dist/rules/AD047.d.ts +2 -0
  27. package/dist/rules/AD047.js +47 -0
  28. package/dist/rules/AD048.d.ts +2 -0
  29. package/dist/rules/AD048.js +37 -0
  30. package/dist/rules/AD049.d.ts +2 -0
  31. package/dist/rules/AD049.js +46 -0
  32. package/dist/rules/AD050.d.ts +2 -0
  33. package/dist/rules/AD050.js +46 -0
  34. package/dist/rules/AD051.d.ts +2 -0
  35. package/dist/rules/AD051.js +52 -0
  36. package/dist/rules/AD052.d.ts +2 -0
  37. package/dist/rules/AD052.js +46 -0
  38. package/dist/rules/AD053.d.ts +2 -0
  39. package/dist/rules/AD053.js +46 -0
  40. package/dist/rules/AD054.d.ts +2 -0
  41. package/dist/rules/AD054.js +46 -0
  42. package/dist/rules/AD055.d.ts +2 -0
  43. package/dist/rules/AD055.js +41 -0
  44. package/dist/rules/AD056.d.ts +2 -0
  45. package/dist/rules/AD056.js +49 -0
  46. package/dist/rules/AD057.d.ts +2 -0
  47. package/dist/rules/AD057.js +79 -0
  48. package/dist/rules/AD058.d.ts +2 -0
  49. package/dist/rules/AD058.js +33 -0
  50. package/dist/rules/AD059.d.ts +2 -0
  51. package/dist/rules/AD059.js +99 -0
  52. package/dist/rules/builtin.js +30 -0
  53. package/dist/rules/specialSections.d.ts +13 -0
  54. package/dist/rules/specialSections.js +54 -0
  55. package/dist/types.d.ts +5 -0
  56. package/dist/version.d.ts +1 -1
  57. package/dist/version.js +1 -1
  58. package/docs/architecture.md +250 -77
  59. package/docs/rules/AD009.md +59 -0
  60. package/docs/rules/AD010.md +12 -3
  61. package/docs/rules/AD011.md +17 -9
  62. package/docs/rules/AD012.md +14 -3
  63. package/docs/rules/AD013.md +3 -3
  64. package/docs/rules/AD016.md +11 -10
  65. package/docs/rules/AD017.md +5 -4
  66. package/docs/rules/AD020.md +57 -11
  67. package/docs/rules/AD025.md +2 -2
  68. package/docs/rules/AD028.md +14 -9
  69. package/docs/rules/AD029.md +2 -2
  70. package/docs/rules/AD046.md +118 -0
  71. package/docs/rules/AD047.md +46 -0
  72. package/docs/rules/AD048.md +43 -0
  73. package/docs/rules/AD049.md +43 -0
  74. package/docs/rules/AD050.md +41 -0
  75. package/docs/rules/AD051.md +49 -0
  76. package/docs/rules/AD052.md +39 -0
  77. package/docs/rules/AD053.md +41 -0
  78. package/docs/rules/AD054.md +40 -0
  79. package/docs/rules/AD055.md +60 -0
  80. package/docs/rules/AD056.md +36 -0
  81. package/docs/rules/AD057.md +44 -0
  82. package/docs/rules/AD058.md +28 -0
  83. package/docs/rules/AD059.md +68 -0
  84. package/docs/rules/rule-necessity.md +22 -7
  85. package/package.json +1 -1
  86. package/skills/asciidoclint/SKILL.md +28 -6
  87. package/skills/asciidoclint/references/feedback.md +9 -6
  88. package/skills/asciidoclint/references/lint-summary.md +4 -4
  89. package/skills/asciidoclint/references/rule-create.md +19 -8
  90. package/skills/asciidoclint/references/rule-review.md +8 -1
package/README.md CHANGED
@@ -9,12 +9,39 @@ CLI, AI-agent, and editor workflows.
9
9
 
10
10
  ## Install the npm package
11
11
 
12
+ `asciidoclint` requires Node.js 20 or newer. The package does not install or
13
+ manage Node.js; use the Node.js version chosen by your shell, package manager, or
14
+ higher-level tooling.
15
+
16
+ Install globally when you want `asciidoclint` available as a shell command:
17
+
18
+ ```bash
19
+ npm install -g asciidoclint
20
+ ```
21
+
22
+ If the shell cannot find `asciidoclint` after install, npm's global bin
23
+ directory may not be on `PATH`. It is usually:
24
+
25
+ ```bash
26
+ $(npm prefix -g)/bin
27
+ ```
28
+
29
+ For documentation repositories that need a pinned project-local install without
30
+ creating a project-root `package.json`, install under `.asciidoclint`:
31
+
12
32
  ```bash
13
- npm install --save-dev asciidoclint
33
+ npm --prefix .asciidoclint install asciidoclint@<version>
14
34
  ```
15
35
 
16
36
  ## Use the CLI
17
37
 
38
+ The examples below use `npx asciidoclint` because that works even when npm's
39
+ global bin directory is not on `PATH`. If `asciidoclint` is recognized by your
40
+ shell, you can use `asciidoclint` instead.
41
+
42
+ If the package is installed with the dedicated project-local layout, use
43
+ `.asciidoclint/node_modules/.bin/asciidoclint`.
44
+
18
45
  Run lint:
19
46
 
20
47
  ```bash
@@ -60,26 +87,43 @@ reporting.
60
87
 
61
88
  ## Install the AI skill
62
89
 
63
- The repository ships an `asciidoclint` skill for AI agents. Install it from the
64
- npm package:
90
+ The npm package includes an `asciidoclint` skill for AI agents. Installing the
91
+ npm package does not automatically install the skill into agent roots; run
92
+ `install-skill` explicitly.
93
+
94
+ For a user-global skill install:
65
95
 
66
96
  ```bash
67
97
  npx asciidoclint install-skill
68
98
  ```
69
99
 
70
- Or install it directly from GitHub with the open skills CLI:
100
+ This copies the bundled skill to `~/.agents/skills/asciidoclint` and creates
101
+ `~/.claude/skills/asciidoclint` as a symbolic link to that copy.
102
+
103
+ For a pinned project-local skill install:
71
104
 
72
105
  ```bash
73
- npx skills add f33lgood/asciidoclint --skill asciidoclint -a codex -g
106
+ npm --prefix .asciidoclint install asciidoclint@<version>
107
+ .asciidoclint/node_modules/.bin/asciidoclint install-skill --project
74
108
  ```
75
109
 
76
- Remove the installed skill when you want to use `asciidoclint` without AI skill
77
- assistance:
110
+ This creates symbolic links at `.agents/skills/asciidoclint` and
111
+ `.claude/skills/asciidoclint`, pointing to the skill source bundled with the
112
+ package or checkout that ran `install-skill`. Add `--force` only when replacing
113
+ an existing project-local skill install.
114
+
115
+ Remove installed skills explicitly:
78
116
 
79
117
  ```bash
80
118
  npx asciidoclint uninstall-skill
119
+ npx asciidoclint uninstall-skill --project
81
120
  ```
82
121
 
122
+ Normal npm uninstall removes package files only. It does not remove
123
+ `~/.agents/skills/asciidoclint`, `~/.claude/skills/asciidoclint`,
124
+ `.agents/skills/asciidoclint`, or `.claude/skills/asciidoclint`; run
125
+ `uninstall-skill` for those.
126
+
83
127
  The public skill exposes these user-facing workflows:
84
128
 
85
129
  | Workflow | Purpose |
@@ -122,6 +166,7 @@ extension commands and settings.
122
166
  | `AD006/included-document-title` | Included AsciiDoc files should not introduce a level-0 title without level offset |
123
167
  | `AD007/heading-depth-limit` | Section headings should not exceed Asciidoctor's supported depth |
124
168
  | `AD008/blank-before-list` | Lists should be separated from preceding paragraph text |
169
+ | `AD009/blank-after-list` | Lists should be separated from following structural blocks |
125
170
  | `AD010/table-title` | Table blocks should have a title |
126
171
  | `AD011/image-title` | Block images should have a title |
127
172
  | `AD012/diagram-title` | Diagram blocks should have a title |
@@ -129,14 +174,14 @@ extension commands and settings.
129
174
  | `AD016/malformed-figure-caption` | Figure captions should use AsciiDoc title syntax |
130
175
  | `AD017/malformed-table-caption` | Table captions should use AsciiDoc title syntax |
131
176
  | `AD019/content-after-include` | Text should not be attached directly after include directives |
132
- | `AD020/appendix-section-level` | Appendices should be section-level blocks in article documents |
177
+ | `AD020/appendix-placement` | Appendix markers should apply to documented appendix section levels |
133
178
  | `AD022/circular-include` | Include trees must not contain cycles |
134
179
  | `AD023/empty-section` | Sections should contain body content or child sections |
135
180
  | `AD024/missing-include` | Include targets should exist after attribute substitution |
136
181
  | `AD025/missing-image` | Image targets should exist after attribute substitution |
137
182
  | `AD026/missing-xref` | Cross-reference targets should resolve to an anchor or file |
138
183
  | `AD027/missing-local-link` | Local link targets should resolve to existing files |
139
- | `AD028/image-alt-text` | Images should not explicitly set empty alt text |
184
+ | `AD028/image-alt-text` | Images should provide meaningful alt text |
140
185
  | `AD029/markdown-link-image-residue` | Markdown link and image residue should not render as text |
141
186
  | `AD030/markdown-table-residue` | Markdown pipe table residue should not render as text |
142
187
  | `AD031/no-nested-link-text` | Link text should not contain nested links or cross references |
@@ -152,6 +197,20 @@ extension commands and settings.
152
197
  | `AD043/section-title-start-left` | Section title syntax should start at the beginning of the line |
153
198
  | `AD044/local-adoc-link` | Local AsciiDoc files should be referenced with xref, not link |
154
199
  | `AD045/markdown-heading-mix` | Markdown-compatible headings should not be mixed with AsciiDoc headings |
200
+ | `AD046/preface-placement` | Preface sections should match documented book placement |
201
+ | `AD047/abstract-placement` | Abstract sections should match documented article placement |
202
+ | `AD048/bibliography-placement` | Bibliography markers should apply to documented bibliography sections |
203
+ | `AD049/glossary-placement` | Glossary markers should apply to documented glossary section levels |
204
+ | `AD050/index-placement` | Index markers should apply to documented index section levels |
205
+ | `AD051/partintro-placement` | Part introduction markers should apply to the introductory block of a book part |
206
+ | `AD052/acknowledgments-placement` | Acknowledgments markers should apply to documented book section levels |
207
+ | `AD053/dedication-placement` | Dedication markers should apply to documented book section levels |
208
+ | `AD054/colophon-placement` | Colophon markers should apply to documented book section levels |
209
+ | `AD055/docx-anchor-caption-residue` | DOCX anchor caption residue should be converted to AsciiDoc titles |
210
+ | `AD056/dangling-blank-list-continuation` | Blank list continuations should not capture following content |
211
+ | `AD057/semantic-anchor-target` | Semantic anchors should attach to the matching block type |
212
+ | `AD058/docx-bookmark-hash-residue` | DOCX bookmark/hash residue should be cleaned up |
213
+ | `AD059/docx-nested-table-structure` | DOCX-converted nested tables should use valid nested table separators and shallow nesting |
155
214
  | `ADW01/unknown-waiver-directive` | Waiver directive names should be known |
156
215
  | `ADW02/missing-waiver-rule-list` | Waiver directives should include a rule list |
157
216
  | `ADW03/malformed-waiver-rule-list` | Waiver rule lists should use comma-separated rule IDs |
@@ -168,26 +227,30 @@ rules. `ADW##` waiver diagnostics use tags for discovery, but remain always on.
168
227
 
169
228
  | Group | IDs |
170
229
  |---|---|
171
- | `blank_lines` | `AD008` |
230
+ | `anchor` | `AD057` |
231
+ | `blank_lines` | `AD008`, `AD009` |
172
232
  | `blocks` | `AD003`, `AD032`, `AD035` |
173
233
  | `accessibility` | `AD028`, `AD042` |
174
- | `cleanup` | `AD032`, `AD034`, `AD035`, `AD036`, `AD037`, `AD039`, `AD040`, `AD041` |
175
- | `conversion` | `AD029`, `AD030`, `AD031`, `AD036`, `AD037`, `AD039`, `AD040` |
234
+ | `cleanup` | `AD032`, `AD034`, `AD035`, `AD036`, `AD037`, `AD039`, `AD040`, `AD041`, `AD055`, `AD058`, `AD059` |
235
+ | `conversion` | `AD029`, `AD030`, `AD031`, `AD037`, `AD039`, `AD040`, `AD055`, `AD056`, `AD058`, `AD059` |
236
+ | `docx` | `AD016`, `AD017`, `AD028`, `AD055`, `AD056`, `AD058`, `AD059` |
176
237
  | `dependencies` | `AD024`, `AD025`, `AD026`, `AD027` |
177
238
  | `diagram` | `AD012` |
178
239
  | `format` | `AD041` |
179
240
  | `headings` | `AD001`, `AD002`, `AD005`, `AD006`, `AD007`, `AD043`, `AD045` |
180
- | `image` | `AD011`, `AD013`, `AD016`, `AD025`, `AD028` |
241
+ | `image` | `AD011`, `AD013`, `AD016`, `AD025`, `AD028`, `AD057` |
181
242
  | `include` | `AD006`, `AD019`, `AD022`, `AD024` |
182
243
  | `inline` | `AD041` |
183
- | `lists` | `AD008`, `AD036` |
244
+ | `list` | `AD056` |
245
+ | `lists` | `AD008`, `AD009`, `AD036` |
246
+ | `pandoc` | `AD059` |
184
247
  | `parser` | `AD000` |
185
- | `table` | `AD004`, `AD010`, `AD017`, `AD030` |
248
+ | `table` | `AD004`, `AD010`, `AD017`, `AD030`, `AD057`, `AD059` |
186
249
  | `waiver` | `ADW01`, `ADW02`, `ADW03`, `ADW04`, `ADW05`, `ADW06`, `ADW07`, `ADW08` |
187
250
  | `whitespace` | `AD034` |
188
251
  | `references` | `AD023` |
189
252
  | `links` | `AD027`, `AD031`, `AD042`, `AD044` |
190
- | `structure` | `AD043` |
253
+ | `structure` | `AD009`, `AD019`, `AD020`, `AD022`, `AD023`, `AD043`, `AD046`, `AD047`, `AD048`, `AD049`, `AD050`, `AD051`, `AD052`, `AD053`, `AD054`, `AD056`, `AD057` |
191
254
  | `markdown-compatibility` | `AD045` |
192
255
  | `maintainability` | `AD045` |
193
256
  | `xref` | `AD026`, `AD042`, `AD044` |
package/dist/api/lint.js CHANGED
@@ -23,6 +23,7 @@ async function lintFilesInternal(patterns, options, afterFix) {
23
23
  for (const parsedFile of document.files) {
24
24
  parsedFiles.set(path.resolve(parsedFile.file), parsedFile);
25
25
  }
26
+ mergeAsciidoctorSections(document, await collectParserSections(file));
26
27
  mergeAsciidoctorBlocks(document, await collectParserBlocks(file));
27
28
  mergeAsciidoctorReferenceTargets(document, await collectParserReferenceTargets(file));
28
29
  resolveDocumentXrefs(document);
@@ -69,24 +70,60 @@ async function collectParserBlocks(file) {
69
70
  const { collectAsciidoctorBlocks } = await import("../parsers/asciidoctor.js");
70
71
  return collectAsciidoctorBlocks(file);
71
72
  }
73
+ async function collectParserSections(file) {
74
+ const { collectAsciidoctorSections } = await import("../parsers/asciidoctor.js");
75
+ return collectAsciidoctorSections(file);
76
+ }
72
77
  async function collectParserReferenceTargets(file) {
73
78
  const { collectAsciidoctorReferenceTargets } = await import("../parsers/asciidoctor.js");
74
79
  return collectAsciidoctorReferenceTargets(file);
75
80
  }
81
+ function mergeAsciidoctorSections(document, sections) {
82
+ if (!sections.length) {
83
+ return;
84
+ }
85
+ for (const section of sections) {
86
+ const existing = document.sections.find((candidate) => (path.resolve(candidate.range.start.file) === path.resolve(section.range.start.file)
87
+ && candidate.range.start.line === section.range.start.line
88
+ && candidate.title === section.title));
89
+ if (!existing) {
90
+ continue;
91
+ }
92
+ existing.sectname = section.sectname;
93
+ existing.source = "asciidoctor";
94
+ if (!existing.style && section.style) {
95
+ existing.style = section.style;
96
+ }
97
+ }
98
+ }
76
99
  function mergeAsciidoctorBlocks(document, blocks) {
77
100
  if (!blocks.length) {
78
101
  return;
79
102
  }
80
- const authoritativeTypes = new Set(blocks.map((block) => block.type));
103
+ const authoritativeTypes = new Set(blocks
104
+ .filter((block) => ["table", "image", "diagram"].includes(block.type))
105
+ .map((block) => block.type));
81
106
  const authoritativeFiles = new Set(blocks.map((block) => path.resolve(block.range.start.file)));
82
- document.blocks = [
107
+ const merged = [
83
108
  ...document.blocks.filter((block) => (!authoritativeTypes.has(block.type)
84
109
  || !authoritativeFiles.has(path.resolve(block.range.start.file)))),
85
- ...blocks,
86
- ].sort((a, b) => (a.range.start.file.localeCompare(b.range.start.file)
110
+ ];
111
+ for (const block of blocks) {
112
+ if (!merged.some((candidate) => sameBlock(candidate, block))) {
113
+ merged.push(block);
114
+ }
115
+ }
116
+ document.blocks = merged.sort((a, b) => (a.range.start.file.localeCompare(b.range.start.file)
87
117
  || a.range.start.line - b.range.start.line
88
118
  || a.range.start.column - b.range.start.column));
89
119
  }
120
+ function sameBlock(a, b) {
121
+ return path.resolve(a.range.start.file) === path.resolve(b.range.start.file)
122
+ && a.range.start.line === b.range.start.line
123
+ && a.range.start.column === b.range.start.column
124
+ && a.type === b.type
125
+ && a.style === b.style;
126
+ }
90
127
  function mergeAsciidoctorReferenceTargets(document, targets) {
91
128
  const byKey = new Map();
92
129
  for (const target of document.referenceTargets) {
package/dist/api/rules.js CHANGED
@@ -90,7 +90,7 @@ function configSources(options) {
90
90
  return sources;
91
91
  }
92
92
  const project = findProjectConfig(options.cwd);
93
- if (project) {
93
+ if (project && !sources.some((source) => path.resolve(source.file) === path.resolve(project))) {
94
94
  sources.push({ kind: "project", file: project });
95
95
  }
96
96
  return sources;
package/dist/cli/index.js CHANGED
@@ -38,13 +38,13 @@ program
38
38
  .command("install-skill")
39
39
  .description("install the bundled AI-agent skill")
40
40
  .option("--dest <directory>", "skills root directory")
41
- .option("--agent <agent>", "target agent: codex, cursor, claude-code, or openclaw", "codex")
42
- .option("--project", "install into the target agent's project skills directory")
41
+ .option("--agent <agent>", "deprecated; fails with guidance instead of selecting a skill root")
42
+ .option("--project", "install into project .agents and .claude skill roots")
43
43
  .option("--force", "replace an existing installed skill")
44
44
  .action((options) => {
45
45
  try {
46
46
  const result = installSkill(options);
47
- console.log(`Installed asciidoclint skill to ${result.destination}`);
47
+ console.log(`Installed asciidoclint skill to ${result.destinations.join(", ")}`);
48
48
  }
49
49
  catch (error) {
50
50
  console.error(error instanceof Error ? error.message : String(error));
@@ -55,14 +55,14 @@ program
55
55
  .command("uninstall-skill")
56
56
  .description("uninstall the AI-agent skill")
57
57
  .option("--dest <directory>", "skills root directory")
58
- .option("--agent <agent>", "target agent: codex, cursor, claude-code, or openclaw", "codex")
59
- .option("--project", "uninstall from the target agent's project skills directory")
58
+ .option("--agent <agent>", "deprecated; fails with guidance instead of selecting a skill root")
59
+ .option("--project", "uninstall from project .agents and .claude skill roots")
60
60
  .action((options) => {
61
61
  try {
62
62
  const result = uninstallSkill(options);
63
63
  console.log(result.removed
64
- ? `Uninstalled asciidoclint skill from ${result.destination}`
65
- : `No asciidoclint skill installed at ${result.destination}`);
64
+ ? `Uninstalled asciidoclint skill from ${result.removedDestinations.join(", ")}`
65
+ : `No asciidoclint skill installed at ${result.destinations.join(", ")}`);
66
66
  }
67
67
  catch (error) {
68
68
  console.error(error instanceof Error ? error.message : String(error));
@@ -3,19 +3,24 @@ export interface InstallSkillOptions {
3
3
  project?: boolean;
4
4
  agent?: SupportedSkillAgent;
5
5
  force?: boolean;
6
+ homeDir?: string;
6
7
  }
7
8
  export interface InstallSkillResult {
8
9
  source: string;
9
10
  destination: string;
11
+ destinations: string[];
10
12
  }
11
13
  export interface UninstallSkillOptions {
12
14
  dest?: string;
13
15
  project?: boolean;
14
16
  agent?: SupportedSkillAgent;
17
+ homeDir?: string;
15
18
  }
16
19
  export interface UninstallSkillResult {
17
20
  destination: string;
21
+ destinations: string[];
18
22
  removed: boolean;
23
+ removedDestinations: string[];
19
24
  }
20
25
  export type SupportedSkillAgent = "codex" | "cursor" | "claude-code" | "openclaw";
21
26
  export declare function installSkill(options?: InstallSkillOptions): InstallSkillResult;
@@ -3,60 +3,149 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  const skillName = "asciidoclint";
6
- const defaultAgent = "codex";
7
- const skillAgentRoots = {
8
- codex: {
9
- project: path.join(".agents", "skills"),
10
- global: path.join(os.homedir(), ".codex", "skills"),
11
- },
12
- cursor: {
13
- project: path.join(".agents", "skills"),
14
- global: path.join(os.homedir(), ".cursor", "skills"),
15
- },
16
- "claude-code": {
17
- project: path.join(".claude", "skills"),
18
- global: path.join(os.homedir(), ".claude", "skills"),
19
- },
20
- openclaw: {
21
- project: "skills",
22
- global: path.join(os.homedir(), ".openclaw", "skills"),
23
- },
24
- };
6
+ const deprecatedAgentMessage = "--agent is deprecated and no longer selects a skill root. Use the command without --agent for the default global roots, add --project for project roots, or use --dest <directory> for a custom root.";
25
7
  export function installSkill(options = {}) {
8
+ rejectDeprecatedAgentOption(options.agent);
26
9
  const source = findBundledSkill();
27
- const root = resolveSkillsRoot(options);
28
- const destination = path.join(root, skillName);
10
+ if (options.dest) {
11
+ const destination = path.join(path.resolve(options.dest), skillName);
12
+ installCopy(source, destination, options.force);
13
+ return { source, destination, destinations: [destination] };
14
+ }
15
+ if (options.project) {
16
+ const destinations = projectSkillDestinations(process.cwd());
17
+ prepareDestinations(destinations, options.force);
18
+ installPreparedDestinations(destinations, (destination) => writeSymlink(source, destination));
19
+ return { source, destination: destinations[0], destinations };
20
+ }
21
+ const { common, claude } = globalSkillDestinations(options.homeDir);
22
+ prepareDestinations([common, claude], options.force);
23
+ installPreparedDestinations([common, claude], (destination) => {
24
+ if (destination === common) {
25
+ writeCopy(source, common);
26
+ return;
27
+ }
28
+ writeSymlink(common, claude);
29
+ });
30
+ return { source, destination: common, destinations: [common, claude] };
31
+ }
32
+ export function uninstallSkill(options = {}) {
33
+ rejectDeprecatedAgentOption(options.agent);
34
+ const destinations = uninstallDestinations(options);
35
+ const removedDestinations = [];
36
+ for (const destination of destinations) {
37
+ if (pathExists(destination)) {
38
+ removePath(destination);
39
+ removedDestinations.push(destination);
40
+ }
41
+ }
42
+ return {
43
+ // For global uninstall, destinations are ordered for removal safety:
44
+ // Claude symlink first, then the common copy.
45
+ destination: destinations[0],
46
+ destinations,
47
+ removed: removedDestinations.length > 0,
48
+ removedDestinations,
49
+ };
50
+ }
51
+ function rejectDeprecatedAgentOption(agent) {
52
+ if (agent) {
53
+ throw new Error(deprecatedAgentMessage);
54
+ }
55
+ }
56
+ function uninstallDestinations(options) {
57
+ if (options.dest) {
58
+ return [path.join(path.resolve(options.dest), skillName)];
59
+ }
60
+ if (options.project) {
61
+ return projectSkillDestinations(process.cwd());
62
+ }
63
+ const { common, claude } = globalSkillDestinations(options.homeDir);
64
+ return [claude, common];
65
+ }
66
+ function projectSkillDestinations(root) {
67
+ return [
68
+ path.resolve(root, ".agents", "skills", skillName),
69
+ path.resolve(root, ".claude", "skills", skillName),
70
+ ];
71
+ }
72
+ function globalSkillDestinations(homeDir = os.homedir()) {
73
+ return {
74
+ common: path.join(homeDir, ".agents", "skills", skillName),
75
+ claude: path.join(homeDir, ".claude", "skills", skillName),
76
+ };
77
+ }
78
+ function installCopy(source, destination, force = false) {
29
79
  if (path.resolve(source) === path.resolve(destination)) {
30
- return { source, destination };
80
+ return;
31
81
  }
32
- if (fs.existsSync(destination)) {
33
- if (!options.force) {
34
- throw new Error(`${destination} already exists; pass --force to replace it`);
82
+ prepareDestination(destination, force);
83
+ writeCopy(source, destination);
84
+ }
85
+ function prepareDestinations(destinations, force = false) {
86
+ for (const destination of destinations) {
87
+ prepareDestination(destination, force);
88
+ }
89
+ }
90
+ function installPreparedDestinations(destinations, write) {
91
+ const written = [];
92
+ try {
93
+ for (const destination of destinations) {
94
+ write(destination);
95
+ written.push(destination);
35
96
  }
36
- fs.rmSync(destination, { recursive: true, force: true });
37
97
  }
38
- fs.mkdirSync(root, { recursive: true });
98
+ catch (error) {
99
+ rollbackWrittenDestinations(written);
100
+ throw error;
101
+ }
102
+ }
103
+ function rollbackWrittenDestinations(destinations) {
104
+ for (const destination of destinations.slice().reverse()) {
105
+ if (pathExists(destination)) {
106
+ removePath(destination);
107
+ }
108
+ }
109
+ }
110
+ function writeCopy(source, destination) {
111
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
39
112
  fs.cpSync(source, destination, { recursive: true });
40
- return { source, destination };
41
113
  }
42
- export function uninstallSkill(options = {}) {
43
- const destination = path.join(resolveSkillsRoot(options), skillName);
44
- const removed = fs.existsSync(destination);
45
- if (removed) {
46
- fs.rmSync(destination, { recursive: true, force: true });
114
+ function writeSymlink(source, destination) {
115
+ if (path.resolve(source) === path.resolve(destination)) {
116
+ return;
47
117
  }
48
- return { destination, removed };
118
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
119
+ fs.symlinkSync(path.relative(path.dirname(destination), source), destination, "dir");
49
120
  }
50
- function resolveSkillsRoot(options) {
51
- if (options.dest) {
52
- return path.resolve(options.dest);
121
+ function prepareDestination(destination, force) {
122
+ if (!pathExists(destination)) {
123
+ return;
124
+ }
125
+ if (!force) {
126
+ throw new Error(`${destination} already exists; pass --force to replace it`);
127
+ }
128
+ removePath(destination);
129
+ }
130
+ function removePath(file) {
131
+ const stat = fs.lstatSync(file);
132
+ if (stat.isSymbolicLink()) {
133
+ fs.unlinkSync(file);
134
+ return;
135
+ }
136
+ fs.rmSync(file, { recursive: true, force: true });
137
+ }
138
+ function pathExists(file) {
139
+ try {
140
+ fs.lstatSync(file);
141
+ return true;
53
142
  }
54
- const agent = options.agent ?? defaultAgent;
55
- const roots = skillAgentRoots[agent];
56
- if (!roots) {
57
- throw new Error(`Unsupported skill agent: ${agent}`);
143
+ catch (error) {
144
+ if (error.code === "ENOENT") {
145
+ return false;
146
+ }
147
+ throw error;
58
148
  }
59
- return options.project ? path.resolve(process.cwd(), roots.project) : roots.global;
60
149
  }
61
150
  function findBundledSkill() {
62
151
  let current = path.dirname(fileURLToPath(import.meta.url));
@@ -1,4 +1,5 @@
1
- import type { BlockNode, LintFinding, ReferenceTarget } from "../types.js";
1
+ import type { BlockNode, LintFinding, ReferenceTarget, SectionNode } from "../types.js";
2
2
  export declare function collectAsciidoctorDiagnostics(file: string): LintFinding[];
3
3
  export declare function collectAsciidoctorBlocks(file: string): BlockNode[];
4
+ export declare function collectAsciidoctorSections(file: string): SectionNode[];
4
5
  export declare function collectAsciidoctorReferenceTargets(file: string): ReferenceTarget[];