agents.yaml 0.2.0 → 0.2.2

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/src/run.ts CHANGED
@@ -1,20 +1,35 @@
1
- import * as clack from '@clack/prompts'
1
+ import { MultiSelectPrompt } from "@clack/core"
2
+ import * as clack from "@clack/prompts"
3
+ import { styleText } from "node:util"
2
4
  import {
3
- addDocuments,
4
- initProject,
5
- loadAgentsFile,
6
- removeDocuments,
7
- validateAgentsFile,
8
- } from './agents-file.ts'
9
- import { describeAgentDocument, discoverAgentDocuments } from './discover.ts'
10
- import { cwd, formatProjectPath, resolveFromRoot } from './paths.ts'
11
-
12
- type Command = 'add' | 'discover' | 'help' | 'init' | 'remove' | 'validate' | 'version'
5
+ addDocuments,
6
+ initProject,
7
+ loadAgentsFile,
8
+ removeDocuments,
9
+ validateAgentsFile,
10
+ } from "./agents-file.ts"
11
+ import { describeAgentDocument, discoverAgentDocuments } from "./discover.ts"
12
+ import { cwd, formatProjectPath, resolveFromRoot } from "./paths.ts"
13
+
14
+ type Command =
15
+ | "add"
16
+ | "discover"
17
+ | "help"
18
+ | "init"
19
+ | "remove"
20
+ | "validate"
21
+ | "version"
13
22
 
14
23
  type ParsedArgs = {
15
- command: Command | undefined
16
- values: string[]
17
- flags: Map<string, string | boolean>
24
+ command: Command | undefined
25
+ values: string[]
26
+ flags: Map<string, string | boolean>
27
+ }
28
+
29
+ type DocumentOption = {
30
+ value: string
31
+ label: string
32
+ disabled?: boolean
18
33
  }
19
34
 
20
35
  const helpText = `agents
@@ -22,7 +37,7 @@ const helpText = `agents
22
37
  Usage:
23
38
  agents
24
39
  agents init [--force]
25
- agents discover [--json]
40
+ agents discover [--json] [--include-dot-directories]
26
41
  agents add <path...>
27
42
  agents remove <path...>
28
43
  agents validate [--json]
@@ -30,214 +45,297 @@ Usage:
30
45
  agents.yaml is a curated table of contents for promoted AGENTS.md guidance.`
31
46
 
32
47
  export async function run(argv: string[]): Promise<void> {
33
- const parsed = parseArgs(argv)
34
- const root = cwd()
35
-
36
- switch (parsed.command) {
37
- case undefined:
38
- await interactive(root)
39
- return
40
- case 'help':
41
- console.log(helpText)
42
- return
43
- case 'version':
44
- console.log('0.1.0')
45
- return
46
- case 'init':
47
- await commandInit(root, parsed.flags.get('force') === true)
48
- return
49
- case 'discover':
50
- await commandDiscover(root, parsed.flags.get('json') === true)
51
- return
52
- case 'add':
53
- await commandAdd(root, parsed.values)
54
- return
55
- case 'remove':
56
- await commandRemove(root, parsed.values)
57
- return
58
- case 'validate':
59
- await commandValidate(root, parsed.flags.get('json') === true)
60
- return
61
- }
48
+ const parsed = parseArgs(argv)
49
+ const root = cwd()
50
+
51
+ switch (parsed.command) {
52
+ case undefined:
53
+ await interactive(root)
54
+ return
55
+ case "help":
56
+ console.log(helpText)
57
+ return
58
+ case "version":
59
+ console.log("0.1.0")
60
+ return
61
+ case "init":
62
+ await commandInit(root, parsed.flags.get("force") === true)
63
+ return
64
+ case "discover":
65
+ await commandDiscover(root, {
66
+ json: parsed.flags.get("json") === true,
67
+ includeDotDirectories:
68
+ parsed.flags.get("include-dot-directories") === true,
69
+ })
70
+ return
71
+ case "add":
72
+ await commandAdd(root, parsed.values)
73
+ return
74
+ case "remove":
75
+ await commandRemove(root, parsed.values)
76
+ return
77
+ case "validate":
78
+ await commandValidate(root, parsed.flags.get("json") === true)
79
+ return
80
+ }
62
81
  }
63
82
 
64
83
  function parseArgs(argv: string[]): ParsedArgs {
65
- const flags = new Map<string, string | boolean>()
66
- const values: string[] = []
67
- let command: Command | undefined
68
-
69
- for (let index = 0; index < argv.length; index += 1) {
70
- const arg = argv[index]
71
- if (!arg) continue
72
-
73
- if (arg === '--help' || arg === '-h') {
74
- command = 'help'
75
- continue
76
- }
77
-
78
- if (arg === '--version' || arg === '-v') {
79
- command = 'version'
80
- continue
81
- }
82
-
83
- if (arg.startsWith('--')) {
84
- const [rawName, inlineValue] = arg.slice(2).split('=', 2)
85
- if (!rawName) continue
86
- if (inlineValue !== undefined) {
87
- flags.set(rawName, inlineValue)
88
- continue
89
- }
90
-
91
- flags.set(rawName, true)
92
- continue
93
- }
94
-
95
- if (!command && isCommand(arg)) {
96
- command = arg
97
- continue
98
- }
99
-
100
- values.push(arg)
101
- }
102
-
103
- return { command, values, flags }
84
+ const flags = new Map<string, string | boolean>()
85
+ const values: string[] = []
86
+ let command: Command | undefined
87
+
88
+ for (let index = 0; index < argv.length; index += 1) {
89
+ const arg = argv[index]
90
+ if (!arg) continue
91
+
92
+ if (arg === "--help" || arg === "-h") {
93
+ command = "help"
94
+ continue
95
+ }
96
+
97
+ if (arg === "--version" || arg === "-v") {
98
+ command = "version"
99
+ continue
100
+ }
101
+
102
+ if (arg.startsWith("--")) {
103
+ const [rawName, inlineValue] = arg.slice(2).split("=", 2)
104
+ if (!rawName) continue
105
+ if (inlineValue !== undefined) {
106
+ flags.set(rawName, inlineValue)
107
+ continue
108
+ }
109
+
110
+ flags.set(rawName, true)
111
+ continue
112
+ }
113
+
114
+ if (!command && isCommand(arg)) {
115
+ command = arg
116
+ continue
117
+ }
118
+
119
+ values.push(arg)
120
+ }
121
+
122
+ return { command, values, flags }
104
123
  }
105
124
 
106
125
  function isCommand(value: string): value is Command {
107
- return ['add', 'discover', 'help', 'init', 'remove', 'validate', 'version'].includes(value)
126
+ return [
127
+ "add",
128
+ "discover",
129
+ "help",
130
+ "init",
131
+ "remove",
132
+ "validate",
133
+ "version",
134
+ ].includes(value)
108
135
  }
109
136
 
110
137
  async function commandInit(root: string, force: boolean): Promise<void> {
111
- clack.intro('agents init')
112
- const result = await initProject(root, { force })
113
- clack.note(result.messages.join('\n'), 'Updated')
114
- clack.outro('Project breadcrumb is ready.')
138
+ clack.intro("agents init")
139
+ const result = await initProject(root, { force })
140
+ clack.note(result.messages.join("\n"), "Updated")
141
+ clack.outro("Project breadcrumb is ready.")
115
142
  }
116
143
 
117
- async function commandDiscover(root: string, json: boolean): Promise<void> {
118
- const documents = await discoverAgentDocuments(root)
119
- if (json) {
120
- console.log(JSON.stringify(documents, null, 2))
121
- return
122
- }
123
-
124
- clack.intro('agents discover')
125
- if (documents.length === 0) {
126
- clack.outro('No supplemental AGENTS.md files found.')
127
- return
128
- }
129
-
130
- clack.note(formatDocumentList(documents), `Found ${documents.length}`)
131
- clack.outro('Use agents add <path> to enable one.')
144
+ async function commandDiscover(
145
+ root: string,
146
+ options: { json: boolean; includeDotDirectories: boolean },
147
+ ): Promise<void> {
148
+ const documents = await discoverAgentDocuments(root, {
149
+ includeDotDirectories: options.includeDotDirectories,
150
+ })
151
+ if (options.json) {
152
+ console.log(JSON.stringify(documents, null, 2))
153
+ return
154
+ }
155
+
156
+ clack.intro("agents discover")
157
+ if (documents.length === 0) {
158
+ clack.outro("No supplemental AGENTS.md files found.")
159
+ return
160
+ }
161
+
162
+ clack.note(formatDocumentList(documents), `Found ${documents.length}`)
163
+ clack.outro("Use agents add <path> to enable one.")
132
164
  }
133
165
 
134
166
  async function commandAdd(root: string, paths: string[]): Promise<void> {
135
- if (paths.length === 0) {
136
- throw new Error('add requires at least one AGENTS.md path')
137
- }
138
-
139
- const documents = await Promise.all(
140
- paths.map((path) =>
141
- describeAgentDocument(root, formatProjectPath(root, resolveFromRoot(root, path))),
142
- ),
143
- )
144
-
145
- const file = await addDocuments(root, documents)
146
- clack.intro('agents add')
147
- clack.note(formatDocumentList(file.documents), 'Promoted documents')
148
- clack.outro(`Added ${documents.length} document${documents.length === 1 ? '' : 's'}.`)
167
+ if (paths.length === 0) {
168
+ throw new Error("add requires at least one AGENTS.md path")
169
+ }
170
+
171
+ const documents = await Promise.all(
172
+ paths.map((path) =>
173
+ describeAgentDocument(
174
+ root,
175
+ formatProjectPath(root, resolveFromRoot(root, path)),
176
+ ),
177
+ ),
178
+ )
179
+
180
+ const file = await addDocuments(root, documents)
181
+ clack.intro("agents add")
182
+ clack.note(formatDocumentList(file.documents), "Promoted documents")
183
+ clack.outro(
184
+ `Added ${documents.length} document${documents.length === 1 ? "" : "s"}.`,
185
+ )
149
186
  }
150
187
 
151
188
  function formatDocumentList(
152
- documents: { path: string; description?: string | undefined }[],
189
+ documents: { path: string; description?: string | undefined }[],
153
190
  ): string {
154
- return documents
155
- .map((doc) => (doc.description ? `${doc.path}\n ${doc.description}` : doc.path))
156
- .join('\n')
191
+ return documents
192
+ .map((doc) =>
193
+ doc.description ? `${doc.path}\n ${doc.description}` : doc.path,
194
+ )
195
+ .join("\n")
157
196
  }
158
197
 
159
198
  async function commandRemove(root: string, paths: string[]): Promise<void> {
160
- if (paths.length === 0) {
161
- throw new Error('remove requires at least one path')
162
- }
163
-
164
- const normalizedPaths = paths.map((path) => formatProjectPath(root, resolveFromRoot(root, path)))
165
- const result = await removeDocuments(root, normalizedPaths)
166
- clack.intro('agents remove')
167
- clack.note(result.removed.join('\n') || 'No matching documents were listed.', 'Removed')
168
- clack.outro(
169
- `agents.yaml now has ${result.file.documents.length} promoted document${result.file.documents.length === 1 ? '' : 's'}.`,
170
- )
199
+ if (paths.length === 0) {
200
+ throw new Error("remove requires at least one path")
201
+ }
202
+
203
+ const normalizedPaths = paths.map((path) =>
204
+ formatProjectPath(root, resolveFromRoot(root, path)),
205
+ )
206
+ const result = await removeDocuments(root, normalizedPaths)
207
+ clack.intro("agents remove")
208
+ clack.note(
209
+ result.removed.join("\n") || "No matching documents were listed.",
210
+ "Removed",
211
+ )
212
+ clack.outro(
213
+ `agents.yaml now has ${result.file.documents.length} promoted document${result.file.documents.length === 1 ? "" : "s"}.`,
214
+ )
171
215
  }
172
216
 
173
217
  async function commandValidate(root: string, json: boolean): Promise<void> {
174
- const result = await validateAgentsFile(root)
175
- if (json) {
176
- console.log(JSON.stringify(result, null, 2))
177
- return
178
- }
179
-
180
- clack.intro('agents validate')
181
- if (result.errors.length > 0) {
182
- clack.note(result.errors.join('\n'), 'Errors')
183
- }
184
- if (result.warnings.length > 0) {
185
- clack.note(result.warnings.join('\n'), 'Warnings')
186
- }
187
-
188
- clack.outro(result.ok ? 'agents.yaml is valid.' : 'agents.yaml needs attention.')
189
- if (!result.ok) {
190
- process.exitCode = 1
191
- }
218
+ const result = await validateAgentsFile(root)
219
+ if (json) {
220
+ console.log(JSON.stringify(result, null, 2))
221
+ return
222
+ }
223
+
224
+ clack.intro("agents validate")
225
+ if (result.errors.length > 0) {
226
+ clack.note(result.errors.join("\n"), "Errors")
227
+ }
228
+ if (result.warnings.length > 0) {
229
+ clack.note(result.warnings.join("\n"), "Warnings")
230
+ }
231
+
232
+ clack.outro(
233
+ result.ok ? "agents.yaml is valid." : "agents.yaml needs attention.",
234
+ )
235
+ if (!result.ok) {
236
+ process.exitCode = 1
237
+ }
192
238
  }
193
239
 
194
240
  async function interactive(root: string): Promise<void> {
195
- clack.intro('agents')
196
- const action = await clack.select({
197
- message: 'What would you like to do?',
198
- options: [
199
- { value: 'discover', label: 'Discover and enable AGENTS.md files' },
200
- { value: 'validate', label: 'Validate agents.yaml' },
201
- { value: 'init', label: 'Initialize breadcrumb files' },
202
- ],
203
- })
204
-
205
- if (clack.isCancel(action)) {
206
- clack.cancel('Cancelled.')
207
- return
208
- }
209
-
210
- if (action === 'init') {
211
- await commandInit(root, false)
212
- return
213
- }
214
-
215
- if (action === 'validate') {
216
- await commandValidate(root, false)
217
- return
218
- }
219
-
220
- const existing = await loadAgentsFile(root)
221
- const discovered = await discoverAgentDocuments(root)
222
- const candidates = discovered.filter(
223
- (doc) => !existing.documents.some((active) => active.path === doc.path),
224
- )
225
-
226
- if (candidates.length === 0) {
227
- clack.outro('No unlisted supplemental AGENTS.md files found.')
228
- return
229
- }
230
-
231
- const selected = await clack.multiselect({
232
- message: 'Choose documents to enable',
233
- options: candidates.map((doc) => ({ value: doc.path, label: doc.path })),
234
- required: false,
235
- })
236
-
237
- if (clack.isCancel(selected) || selected.length === 0) {
238
- clack.cancel('No documents selected.')
239
- return
240
- }
241
-
242
- await commandAdd(root, selected)
241
+ clack.intro("agents")
242
+ const action = await clack.select({
243
+ message: "What would you like to do?",
244
+ options: [
245
+ { value: "discover", label: "Discover and enable AGENTS.md files" },
246
+ { value: "validate", label: "Validate agents.yaml" },
247
+ { value: "init", label: "Initialize breadcrumb files" },
248
+ ],
249
+ })
250
+
251
+ if (clack.isCancel(action)) {
252
+ clack.cancel("Cancelled.")
253
+ return
254
+ }
255
+
256
+ if (action === "init") {
257
+ await commandInit(root, false)
258
+ return
259
+ }
260
+
261
+ if (action === "validate") {
262
+ await commandValidate(root, false)
263
+ return
264
+ }
265
+
266
+ const existing = await loadAgentsFile(root)
267
+ const discovered = await discoverAgentDocuments(root)
268
+ const candidates = discovered.filter(
269
+ (doc) => !existing.documents.some((active) => active.path === doc.path),
270
+ )
271
+
272
+ if (candidates.length === 0) {
273
+ clack.outro("No unlisted supplemental AGENTS.md files found.")
274
+ return
275
+ }
276
+
277
+ const selected = await chooseDocumentsToEnable(
278
+ candidates.map((doc) => ({ value: doc.path, label: doc.path })),
279
+ )
280
+
281
+ if (
282
+ clack.isCancel(selected) ||
283
+ selected === undefined ||
284
+ selected.length === 0
285
+ ) {
286
+ clack.cancel("No documents selected.")
287
+ return
288
+ }
289
+
290
+ await commandAdd(root, selected)
291
+ }
292
+
293
+ function chooseDocumentsToEnable(
294
+ options: DocumentOption[],
295
+ ): Promise<string[] | symbol | undefined> {
296
+ return new MultiSelectPrompt<DocumentOption>({
297
+ options,
298
+ required: false,
299
+ render() {
300
+ const prefix = `${styleText("cyan", clack.S_BAR)} `
301
+ const selected = this.value ?? []
302
+
303
+ return `${styleText("gray", clack.S_BAR)}
304
+ ${clack.symbol(this.state)} Choose documents to enable
305
+ ${prefix}${clack.limitOptions({
306
+ options: this.options,
307
+ cursor: this.cursor,
308
+ columnPadding: prefix.length,
309
+ style: (option, active) =>
310
+ styleDocumentOption(option, {
311
+ active,
312
+ selected: selected.includes(option.value),
313
+ }),
314
+ }).join(`
315
+ ${prefix}`)}
316
+ ${styleText("cyan", clack.S_BAR_END)}
317
+ `
318
+ },
319
+ }).prompt()
320
+ }
321
+
322
+ function styleDocumentOption(
323
+ option: DocumentOption,
324
+ state: { active: boolean; selected: boolean },
325
+ ): string {
326
+ if (option.disabled) {
327
+ return `${styleText("gray", clack.S_CHECKBOX_INACTIVE)} ${styleText(
328
+ ["strikethrough", "gray"],
329
+ option.label,
330
+ )}`
331
+ }
332
+
333
+ const checkbox = state.selected
334
+ ? styleText("green", clack.S_CHECKBOX_SELECTED)
335
+ : state.active
336
+ ? styleText("cyan", clack.S_CHECKBOX_ACTIVE)
337
+ : styleText("dim", clack.S_CHECKBOX_INACTIVE)
338
+ const cursor = state.active ? styleText("cyan", ">") : " "
339
+ const label = state.active ? option.label : styleText("dim", option.label)
340
+ return `${cursor} ${checkbox} ${label}`
243
341
  }