agents.yaml 0.2.0 → 0.2.1

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