waymark-docs 0.1.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +29 -0
- package/dist/commands/find.d.ts +2 -0
- package/dist/commands/find.js +176 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +11 -0
- package/dist/commands/ls.d.ts +2 -0
- package/dist/commands/ls.js +39 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +71 -0
- package/dist/configuration/index.d.ts +3 -0
- package/dist/configuration/index.js +2 -0
- package/dist/configuration/initialize.d.ts +3 -0
- package/dist/configuration/initialize.js +34 -0
- package/dist/configuration/load.d.ts +27 -0
- package/dist/configuration/load.js +174 -0
- package/dist/diagnostics.d.ts +7 -0
- package/dist/diagnostics.js +13 -0
- package/dist/documents/classify.d.ts +29 -0
- package/dist/documents/classify.js +203 -0
- package/dist/documents/filter.d.ts +24 -0
- package/dist/documents/filter.js +212 -0
- package/dist/documents/index.d.ts +3 -0
- package/dist/documents/index.js +2 -0
- package/dist/documents/scan.d.ts +33 -0
- package/dist/documents/scan.js +134 -0
- package/dist/filesystem.d.ts +2 -0
- package/dist/filesystem.js +15 -0
- package/package.json +55 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Aran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Waymark 🪧
|
|
2
|
+
|
|
3
|
+
Find the right repository docs for your coding agent.
|
|
4
|
+
|
|
5
|
+
Waymark is a small, offline-first CLI built for AI agents. Add structured
|
|
6
|
+
frontmatter to Markdown or MDX files, then let the agent select the kinds and
|
|
7
|
+
tags relevant to its task. Waymark returns deterministic matches without
|
|
8
|
+
ranking results or maintaining an index.
|
|
9
|
+
|
|
10
|
+
This makes documentation discovery token-efficient: agents spend less time
|
|
11
|
+
searching unrelated files and more time working with the context they need.
|
|
12
|
+
|
|
13
|
+
## Table of contents
|
|
14
|
+
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Commands](#commands)
|
|
18
|
+
- [`waymark init`](#waymark-init)
|
|
19
|
+
- [`waymark status`](#waymark-status)
|
|
20
|
+
- [`waymark find`](#waymark-find)
|
|
21
|
+
- [`waymark ls`](#waymark-ls)
|
|
22
|
+
- [`waymark help`](#waymark-help)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install the `waymark-docs` package as a development dependency:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pnpm add -D waymark-docs
|
|
31
|
+
yarn add -D waymark-docs
|
|
32
|
+
bun add -d waymark-docs
|
|
33
|
+
npm install --save-dev waymark-docs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
1. Create `waymark.yaml` in the repository root:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx waymark init
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
2. Define the document kinds and tags that agents can search:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
# When true, document metadata must be nested under a `waymark` frontmatter key.
|
|
48
|
+
require-namespace: false
|
|
49
|
+
|
|
50
|
+
kinds:
|
|
51
|
+
adr: Read to understand past architectural decisions and their constraints
|
|
52
|
+
convention: Read before changing code to follow required repository practices
|
|
53
|
+
|
|
54
|
+
tags:
|
|
55
|
+
architecture: System boundaries, component relationships, and dependencies
|
|
56
|
+
typescript: TypeScript-related documentation
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
3. Register a Markdown or MDX file by adding Waymark metadata. For example,
|
|
60
|
+
save this as `docs/conventions/typescript.md`:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
---
|
|
64
|
+
kind: convention
|
|
65
|
+
description: TypeScript conventions for this repository
|
|
66
|
+
tags: [typescript]
|
|
67
|
+
---
|
|
68
|
+
# TypeScript conventions
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
4. Validate the repository:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx waymark status
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
Root: /path/to/repository
|
|
79
|
+
Status: valid
|
|
80
|
+
Waymark Documents: 1
|
|
81
|
+
Unregistered Documents: 1
|
|
82
|
+
Kinds: 2
|
|
83
|
+
Tags: 2
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
5. Discover the document:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npx waymark find --kinds convention --tags typescript --show description
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
docs/conventions/typescript.md — TypeScript conventions for this repository
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Waymark looks for `waymark.yaml` in the current directory and its ancestors, so
|
|
97
|
+
commands can also be run from a nested repository directory.
|
|
98
|
+
|
|
99
|
+
## Commands
|
|
100
|
+
|
|
101
|
+
| Command | Purpose |
|
|
102
|
+
| ---------------- | --------------------------------------------------------- |
|
|
103
|
+
| `waymark init` | Create a starter configuration |
|
|
104
|
+
| `waymark status` | Validate and summarize the repository |
|
|
105
|
+
| `waymark find` | Find registered documents across the repository |
|
|
106
|
+
| `waymark ls` | Audit registered or unregistered documents in a directory |
|
|
107
|
+
| `waymark help` | Show CLI or command-specific help |
|
|
108
|
+
|
|
109
|
+
### `waymark init`
|
|
110
|
+
|
|
111
|
+
Create a starter `waymark.yaml` in the current directory.
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
Usage: waymark init [options]
|
|
115
|
+
|
|
116
|
+
Options:
|
|
117
|
+
-h, --help Display help for the command
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
npx waymark init
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The generated file explains metadata namespacing and includes declarations to
|
|
125
|
+
replace with your own kind and tag:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
# When true, document metadata must be nested under a `waymark` frontmatter key.
|
|
129
|
+
require-namespace: false
|
|
130
|
+
kinds:
|
|
131
|
+
example-kind: Explain when agents should read this kind of document
|
|
132
|
+
tags:
|
|
133
|
+
example-tag: Explain the topic represented by this tag
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`init` never overwrites an existing configuration and does not allow a nested
|
|
137
|
+
configuration beneath another Waymark root.
|
|
138
|
+
|
|
139
|
+
### `waymark status`
|
|
140
|
+
|
|
141
|
+
Validate `waymark.yaml` and all discovered Waymark Documents, then print the
|
|
142
|
+
repository root and counts for registered documents, unregistered documents,
|
|
143
|
+
kinds, and tags. Invalid repositories produce diagnostics and a non-zero exit
|
|
144
|
+
code, which makes this command suitable for CI.
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
Usage: waymark status [options]
|
|
148
|
+
|
|
149
|
+
Options:
|
|
150
|
+
-s, --show <fields> Show declared kind and tag details (kind,tags)
|
|
151
|
+
-h, --help Display help for the command
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Validate the repository:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
npx waymark status
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
List every declared kind and tag with its description and usage count:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
npx waymark status --show kind,tags
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Show only kind details:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
npx waymark status --show kind
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `waymark find`
|
|
173
|
+
|
|
174
|
+
Find registered Waymark Documents across the repository. With no filters,
|
|
175
|
+
`find` returns every registered document in deterministic path order.
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
Usage: waymark find [options]
|
|
179
|
+
|
|
180
|
+
Options:
|
|
181
|
+
-k, --kinds <identifiers> Match any kind (comma-separated, repeatable)
|
|
182
|
+
-t, --tags <identifiers> Match any tag (comma-separated, repeatable)
|
|
183
|
+
-r, --require-tags <identifiers> Require every tag (comma-separated, repeatable)
|
|
184
|
+
-f, --filter <expression> Match a Boolean metadata filter
|
|
185
|
+
-q, --query <text> Match a literal content query
|
|
186
|
+
-s, --show <fields> Show kind, tags, and description
|
|
187
|
+
--json Return a flat JSON array
|
|
188
|
+
--tree Return a directory tree
|
|
189
|
+
-h, --help Display help for the command
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Simple filter values use OR within an option. Different options combine with
|
|
193
|
+
AND:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
# Kind is adr OR convention, and at least one tag is typescript OR architecture
|
|
197
|
+
npx waymark find --kinds adr,convention --tags typescript,architecture
|
|
198
|
+
|
|
199
|
+
# Kind is adr, and both architecture AND typescript tags are required
|
|
200
|
+
npx waymark find --kinds adr --require-tags architecture,typescript
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The three simple metadata filters are repeatable. Repeating an option is
|
|
204
|
+
equivalent to passing a comma-separated list:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
npx waymark find --kinds adr --kinds convention
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use `--query` for a case-insensitive literal search of document bodies. It can
|
|
211
|
+
be combined with either simple or Boolean metadata filters:
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
npx waymark find --kinds convention --query "dependency injection"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Use `--filter` for advanced metadata expressions with `kind:`, `tag:`, `NOT`,
|
|
218
|
+
`AND`, `OR`, and parentheses:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
npx waymark find --filter '(kind:adr OR kind:convention) AND tag:typescript AND NOT tag:architecture'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`--filter` cannot be combined with `--kinds`, `--tags`, or `--require-tags`.
|
|
225
|
+
|
|
226
|
+
Add metadata fields to the default line-oriented output with `--show`:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
npx waymark find --kinds convention --show kind,tags,description
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Return structured output for scripts and agents:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
npx waymark find --tags typescript --show kind,description --json
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Or visualize matching documents by directory:
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
npx waymark find --kinds adr,convention --show kind --tree
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`--json` and `--tree` are mutually exclusive. A search with no matches succeeds
|
|
245
|
+
and returns an empty result.
|
|
246
|
+
|
|
247
|
+
### `waymark ls`
|
|
248
|
+
|
|
249
|
+
Inventory Markdown and MDX registration within a directory. By default, `ls`
|
|
250
|
+
inspects only the current directory and lists registered Waymark Documents.
|
|
251
|
+
Paths are returned relative to the repository root in deterministic order.
|
|
252
|
+
|
|
253
|
+
```text
|
|
254
|
+
Usage: waymark ls [options] [directory]
|
|
255
|
+
|
|
256
|
+
Arguments:
|
|
257
|
+
directory Directory to inspect (defaults to the current directory)
|
|
258
|
+
|
|
259
|
+
Options:
|
|
260
|
+
-R, --recursive Inspect directories recursively
|
|
261
|
+
-u, --unregistered List only unregistered documents
|
|
262
|
+
-h, --help Display help for the command
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
List registered documents directly inside `docs`:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
npx waymark ls docs
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Include all nested directories:
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
npx waymark ls --recursive docs
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Find Markdown and MDX files that are missing Waymark metadata:
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
npx waymark ls -R --unregistered docs
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`ls` respects `.gitignore`, Waymark exclusions, and Git directory boundaries.
|
|
284
|
+
The selected directory must be inside the repository root.
|
|
285
|
+
|
|
286
|
+
### `waymark help`
|
|
287
|
+
|
|
288
|
+
Show the command list or detailed help for one command:
|
|
289
|
+
|
|
290
|
+
```text
|
|
291
|
+
Usage: waymark help [command]
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
npx waymark --help
|
|
296
|
+
npx waymark help find
|
|
297
|
+
npx waymark find --help
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Contributing
|
|
301
|
+
|
|
302
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CommanderError, Command } from "commander";
|
|
3
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
4
|
+
import { createFindCommand } from "./commands/find.js";
|
|
5
|
+
import { createInitCommand } from "./commands/init.js";
|
|
6
|
+
import { createLsCommand } from "./commands/ls.js";
|
|
7
|
+
import { createStatusCommand } from "./commands/status.js";
|
|
8
|
+
const program = new Command()
|
|
9
|
+
.name("waymark")
|
|
10
|
+
.description("Discover repository documentation deterministically")
|
|
11
|
+
.version(packageJson.version)
|
|
12
|
+
.exitOverride();
|
|
13
|
+
program.addCommand(createInitCommand());
|
|
14
|
+
program.addCommand(createStatusCommand());
|
|
15
|
+
program.addCommand(createFindCommand());
|
|
16
|
+
program.addCommand(createLsCommand());
|
|
17
|
+
try {
|
|
18
|
+
await program.parseAsync();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof CommanderError) {
|
|
22
|
+
process.exitCode = error.exitCode;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
process.stderr.write(`error: ${message}\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { loadConfiguration } from "../configuration/index.js";
|
|
3
|
+
import { compareDiagnostics, throwDiagnostics } from "../diagnostics.js";
|
|
4
|
+
import { filterDocuments, scanDocuments, } from "../documents/index.js";
|
|
5
|
+
export function createFindCommand() {
|
|
6
|
+
return new Command("find")
|
|
7
|
+
.description("Discover Waymark Documents")
|
|
8
|
+
.option("-k, --kinds <identifiers>", "Match any Document Kind (comma-separated, repeatable)", collectOptionValue, [])
|
|
9
|
+
.option("-t, --tags <identifiers>", "Match any Document Tag (comma-separated, repeatable)", collectOptionValue, [])
|
|
10
|
+
.option("-r, --require-tags <identifiers>", "Require every Document Tag (comma-separated, repeatable)", collectOptionValue, [])
|
|
11
|
+
.option("-f, --filter <expression>", "Match a Boolean Metadata Filter")
|
|
12
|
+
.option("-q, --query <text>", "Match a literal Content Query")
|
|
13
|
+
.option("-s, --show <fields>", "Show kind, tags, and description (comma-separated)")
|
|
14
|
+
.option("--json", "Return a flat JSON array")
|
|
15
|
+
.option("--tree", "Return a directory-tree presentation")
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
if (options.filter !== undefined &&
|
|
18
|
+
(options.kinds.length > 0 ||
|
|
19
|
+
options.tags.length > 0 ||
|
|
20
|
+
options.requireTags.length > 0)) {
|
|
21
|
+
throw new Error("--filter cannot be combined with --kinds, --tags, or --require-tags.");
|
|
22
|
+
}
|
|
23
|
+
if (options.json && options.tree) {
|
|
24
|
+
throw new Error("--json cannot be combined with --tree.");
|
|
25
|
+
}
|
|
26
|
+
const shownFields = parseShownFields(options.show);
|
|
27
|
+
const loadedConfiguration = await loadConfiguration(process.cwd());
|
|
28
|
+
if (loadedConfiguration.kind === "malformed") {
|
|
29
|
+
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
30
|
+
}
|
|
31
|
+
const { configuration, rootPath } = loadedConfiguration;
|
|
32
|
+
const documentScan = await scanDocuments({ rootPath, configuration });
|
|
33
|
+
const diagnostics = [
|
|
34
|
+
...loadedConfiguration.diagnostics,
|
|
35
|
+
...(documentScan.kind === "invalid" ? documentScan.diagnostics : []),
|
|
36
|
+
].sort(compareDiagnostics);
|
|
37
|
+
if (loadedConfiguration.diagnostics.length > 0 ||
|
|
38
|
+
documentScan.kind === "invalid") {
|
|
39
|
+
throwDiagnostics(diagnostics);
|
|
40
|
+
}
|
|
41
|
+
const matchingDocuments = filterDocuments({
|
|
42
|
+
documents: documentScan.documents,
|
|
43
|
+
configuration,
|
|
44
|
+
criteria: {
|
|
45
|
+
kinds: options.kinds,
|
|
46
|
+
tags: options.tags,
|
|
47
|
+
requiredTags: options.requireTags,
|
|
48
|
+
filter: options.filter,
|
|
49
|
+
query: options.query,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
if (options.json) {
|
|
53
|
+
process.stdout.write(`${JSON.stringify(matchingDocuments.map((document) => projectDocument(document, shownFields)), undefined, 2)}\n`);
|
|
54
|
+
}
|
|
55
|
+
else if (options.tree) {
|
|
56
|
+
process.stdout.write(renderDocumentTree(matchingDocuments, shownFields));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
process.stdout.write(matchingDocuments
|
|
60
|
+
.map((document) => renderDocumentLine(document, shownFields))
|
|
61
|
+
.join("\n") + (matchingDocuments.length > 0 ? "\n" : ""));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function projectDocument(document, shownFields) {
|
|
66
|
+
const projection = { path: document.path };
|
|
67
|
+
if (shownFields.has("kind"))
|
|
68
|
+
projection.kind = document.kind;
|
|
69
|
+
if (shownFields.has("tags"))
|
|
70
|
+
projection.tags = document.tags;
|
|
71
|
+
if (shownFields.has("description")) {
|
|
72
|
+
projection.description = document.description;
|
|
73
|
+
}
|
|
74
|
+
return projection;
|
|
75
|
+
}
|
|
76
|
+
function parseShownFields(value) {
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
return new Set();
|
|
79
|
+
const shownFields = new Set();
|
|
80
|
+
for (const field of value.split(",")) {
|
|
81
|
+
if (field !== "kind" && field !== "tags" && field !== "description") {
|
|
82
|
+
throw new Error(`Unknown find field "${field}". Expected kind, tags, or description.`);
|
|
83
|
+
}
|
|
84
|
+
if (shownFields.has(field)) {
|
|
85
|
+
throw new Error(`Duplicate find field "${field}".`);
|
|
86
|
+
}
|
|
87
|
+
shownFields.add(field);
|
|
88
|
+
}
|
|
89
|
+
return shownFields;
|
|
90
|
+
}
|
|
91
|
+
function renderDocumentLine(document, shownFields, displayedPath = document.path) {
|
|
92
|
+
let line = displayedPath;
|
|
93
|
+
if (shownFields.has("kind"))
|
|
94
|
+
line += ` [${document.kind}]`;
|
|
95
|
+
if (shownFields.has("tags"))
|
|
96
|
+
line += ` [${document.tags.join(",")}]`;
|
|
97
|
+
if (shownFields.has("description")) {
|
|
98
|
+
const description = document.description.replaceAll(/\s+/g, " ").trim();
|
|
99
|
+
line += ` — ${description}`;
|
|
100
|
+
}
|
|
101
|
+
return line;
|
|
102
|
+
}
|
|
103
|
+
function renderDocumentTree(documents, shownFields) {
|
|
104
|
+
const root = createTreeDirectory();
|
|
105
|
+
for (const document of documents) {
|
|
106
|
+
const pathParts = document.path.split("/");
|
|
107
|
+
const fileName = pathParts.pop();
|
|
108
|
+
if (!fileName)
|
|
109
|
+
continue;
|
|
110
|
+
let directory = root;
|
|
111
|
+
for (const pathPart of pathParts) {
|
|
112
|
+
let childDirectory = directory.directories.get(pathPart);
|
|
113
|
+
if (!childDirectory) {
|
|
114
|
+
childDirectory = createTreeDirectory();
|
|
115
|
+
directory.directories.set(pathPart, childDirectory);
|
|
116
|
+
}
|
|
117
|
+
directory = childDirectory;
|
|
118
|
+
}
|
|
119
|
+
directory.documents.set(fileName, document);
|
|
120
|
+
}
|
|
121
|
+
let output = "";
|
|
122
|
+
for (const entry of sortedTreeEntries(root)) {
|
|
123
|
+
if (entry.kind === "document") {
|
|
124
|
+
output += `${renderDocumentLine(entry.document, shownFields, entry.name)}\n`;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
output += `${entry.name}/\n`;
|
|
128
|
+
output += renderTreeDirectory(entry.directory, shownFields, "");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return output;
|
|
132
|
+
}
|
|
133
|
+
function renderTreeDirectory(directory, shownFields, prefix) {
|
|
134
|
+
const entries = sortedTreeEntries(directory);
|
|
135
|
+
let output = "";
|
|
136
|
+
for (const [index, entry] of entries.entries()) {
|
|
137
|
+
const isLast = index === entries.length - 1;
|
|
138
|
+
const connector = isLast ? "└── " : "├── ";
|
|
139
|
+
if (entry.kind === "document") {
|
|
140
|
+
output +=
|
|
141
|
+
`${prefix}${connector}` +
|
|
142
|
+
`${renderDocumentLine(entry.document, shownFields, entry.name)}\n`;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
output += `${prefix}${connector}${entry.name}/\n`;
|
|
146
|
+
output += renderTreeDirectory(entry.directory, shownFields, `${prefix}${isLast ? " " : "│ "}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return output;
|
|
150
|
+
}
|
|
151
|
+
function sortedTreeEntries(directory) {
|
|
152
|
+
return [
|
|
153
|
+
...[...directory.directories].map(([name, childDirectory]) => ({
|
|
154
|
+
kind: "directory",
|
|
155
|
+
name,
|
|
156
|
+
directory: childDirectory,
|
|
157
|
+
})),
|
|
158
|
+
...[...directory.documents].map(([name, document]) => ({
|
|
159
|
+
kind: "document",
|
|
160
|
+
name,
|
|
161
|
+
document,
|
|
162
|
+
})),
|
|
163
|
+
].sort((left, right) => compareText(left.kind === "directory" ? `${left.name}/` : left.name, right.kind === "directory" ? `${right.name}/` : right.name));
|
|
164
|
+
}
|
|
165
|
+
function createTreeDirectory() {
|
|
166
|
+
return {
|
|
167
|
+
directories: new Map(),
|
|
168
|
+
documents: new Map(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function collectOptionValue(value, previous) {
|
|
172
|
+
return [...previous, value];
|
|
173
|
+
}
|
|
174
|
+
function compareText(left, right) {
|
|
175
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
176
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { initializeConfiguration } from "../configuration/index.js";
|
|
4
|
+
export function createInitCommand() {
|
|
5
|
+
return new Command("init")
|
|
6
|
+
.description("Create a starter Waymark configuration")
|
|
7
|
+
.action(async () => {
|
|
8
|
+
const { configurationPath } = await initializeConfiguration(process.cwd());
|
|
9
|
+
process.stdout.write(`Created ${basename(configurationPath)}\n`);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { loadConfiguration } from "../configuration/index.js";
|
|
4
|
+
import { throwDiagnostics } from "../diagnostics.js";
|
|
5
|
+
import { scanDocuments } from "../documents/index.js";
|
|
6
|
+
export function createLsCommand() {
|
|
7
|
+
return new Command("ls")
|
|
8
|
+
.description("Inventory document registration in a directory")
|
|
9
|
+
.argument("[directory]", "Directory to inspect")
|
|
10
|
+
.option("-R, --recursive", "Inspect directories recursively")
|
|
11
|
+
.option("-u, --unregistered", "List only Unregistered Documents")
|
|
12
|
+
.action(async (directory, options) => {
|
|
13
|
+
const loadedConfiguration = await loadConfiguration(process.cwd());
|
|
14
|
+
if (loadedConfiguration.kind === "malformed") {
|
|
15
|
+
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
16
|
+
}
|
|
17
|
+
if (loadedConfiguration.diagnostics.length > 0) {
|
|
18
|
+
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
19
|
+
}
|
|
20
|
+
const rootPath = loadedConfiguration.rootPath;
|
|
21
|
+
const documentScan = await scanDocuments({
|
|
22
|
+
rootPath,
|
|
23
|
+
configuration: loadedConfiguration.configuration,
|
|
24
|
+
scope: {
|
|
25
|
+
directoryPath: resolve(process.cwd(), directory ?? "."),
|
|
26
|
+
recursive: options.recursive === true,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
if (documentScan.kind === "invalid") {
|
|
30
|
+
throwDiagnostics(documentScan.diagnostics);
|
|
31
|
+
}
|
|
32
|
+
const paths = options.unregistered
|
|
33
|
+
? documentScan.unregisteredDocuments
|
|
34
|
+
: documentScan.documents.map((document) => document.path);
|
|
35
|
+
const output = paths.join("\n");
|
|
36
|
+
if (output !== "")
|
|
37
|
+
process.stdout.write(`${output}\n`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { loadConfiguration, } from "../configuration/index.js";
|
|
3
|
+
import { compareDiagnostics, throwDiagnostics } from "../diagnostics.js";
|
|
4
|
+
import { scanDocuments } from "../documents/index.js";
|
|
5
|
+
export function createStatusCommand() {
|
|
6
|
+
return new Command("status")
|
|
7
|
+
.description("Validate and summarize the Waymark repository")
|
|
8
|
+
.option("-s, --show <fields>", "Show declared kind and tag details (kind,tags)")
|
|
9
|
+
.action(async (options) => {
|
|
10
|
+
const shownFields = parseShownFields(options.show);
|
|
11
|
+
const loadedConfiguration = await loadConfiguration(process.cwd());
|
|
12
|
+
if (loadedConfiguration.kind === "malformed") {
|
|
13
|
+
process.stdout.write(`Root: ${loadedConfiguration.rootPath}\n` + "Status: invalid\n");
|
|
14
|
+
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
15
|
+
}
|
|
16
|
+
const { configuration, rootPath } = loadedConfiguration;
|
|
17
|
+
const documentScan = await scanDocuments({ rootPath, configuration });
|
|
18
|
+
const diagnostics = [
|
|
19
|
+
...loadedConfiguration.diagnostics,
|
|
20
|
+
...(documentScan.kind === "invalid" ? documentScan.diagnostics : []),
|
|
21
|
+
].sort(compareDiagnostics);
|
|
22
|
+
if (loadedConfiguration.diagnostics.length > 0 ||
|
|
23
|
+
documentScan.kind === "invalid") {
|
|
24
|
+
process.stdout.write(`Root: ${rootPath}\n` + "Status: invalid\n");
|
|
25
|
+
throwDiagnostics(diagnostics);
|
|
26
|
+
}
|
|
27
|
+
let output = `Root: ${rootPath}\n` +
|
|
28
|
+
"Status: valid\n" +
|
|
29
|
+
`Waymark Documents: ${documentScan.documents.length}\n` +
|
|
30
|
+
`Unregistered Documents: ${documentScan.unregisteredDocuments.length}\n` +
|
|
31
|
+
`Kinds: ${configuration.kinds.size}\n` +
|
|
32
|
+
`Tags: ${configuration.tags.size}\n`;
|
|
33
|
+
if (shownFields.size > 0)
|
|
34
|
+
output += "\n";
|
|
35
|
+
if (shownFields.has("kind")) {
|
|
36
|
+
output += renderDeclaredValues("Kinds", configuration.kinds, documentScan.kindUsageCounts);
|
|
37
|
+
}
|
|
38
|
+
if (shownFields.has("tags")) {
|
|
39
|
+
output += renderDeclaredValues("Tags", configuration.tags, documentScan.tagUsageCounts);
|
|
40
|
+
}
|
|
41
|
+
process.stdout.write(output);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function parseShownFields(value) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return new Set();
|
|
47
|
+
const fields = value.split(",");
|
|
48
|
+
const shownFields = new Set();
|
|
49
|
+
for (const field of fields) {
|
|
50
|
+
if (field !== "kind" && field !== "tags") {
|
|
51
|
+
throw new Error(`Unknown status field "${field}". Expected kind or tags.`);
|
|
52
|
+
}
|
|
53
|
+
if (shownFields.has(field)) {
|
|
54
|
+
throw new Error(`Duplicate status field "${field}".`);
|
|
55
|
+
}
|
|
56
|
+
shownFields.add(field);
|
|
57
|
+
}
|
|
58
|
+
return shownFields;
|
|
59
|
+
}
|
|
60
|
+
function renderDeclaredValues(heading, values, usageCounts) {
|
|
61
|
+
let output = `${heading}:\n`;
|
|
62
|
+
const sortedValues = [...values.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
63
|
+
for (const [identifier, value] of sortedValues) {
|
|
64
|
+
const documentCount = usageCounts.get(identifier) ?? 0;
|
|
65
|
+
const noun = documentCount === 1 ? "document" : "documents";
|
|
66
|
+
const description = value.description.replaceAll(/\s+/g, " ").trim();
|
|
67
|
+
output +=
|
|
68
|
+
` ${identifier} — ${description} ` + `(${documentCount} ${noun})\n`;
|
|
69
|
+
}
|
|
70
|
+
return output;
|
|
71
|
+
}
|