99 "fmt"
1010 "path"
1111 "slices"
12+ "strings"
1213 "text/template"
1314)
1415
@@ -31,16 +32,27 @@ const (
3132)
3233
3334// The embedded templates rendered into the emitted files and the served skill
34- // documents.
35+ // documents. Templates under 'content/withoutlockfile' replace the same-named
36+ // template when lock-file-free mode is selected.
3537//
36- //go:embed content/*.tmpl
38+ //go:embed content/*.tmpl content/withoutlockfile/*.tmpl
3739var content embed.FS
3840
39- // templates holds all parsed templates, keyed by their base file name.
41+ // templates holds all parsed templates for azldev's default mode, keyed by their
42+ // base file name.
4043//
4144//nolint:gochecknoglobals // parsed templates are effectively constant and safe for concurrent use.
4245var templates = template .Must (template .ParseFS (content , "content/*.tmpl" ))
4346
47+ // withoutLockfileTemplates holds the same templates with the lock-file-free
48+ // variants layered on top: parsing a template whose base name already exists
49+ // replaces it, so only the differing documents need their own file.
50+ //
51+ //nolint:gochecknoglobals // parsed templates are effectively constant and safe for concurrent use.
52+ var withoutLockfileTemplates = template .Must (template .ParseFS (
53+ content , "content/*.tmpl" , "content/withoutlockfile/*.tmpl" ,
54+ ))
55+
4456// Skill describes a single emitted Agent Skill.
4557type Skill struct {
4658 // Name is the stable base identifier (lowercase, hyphen-delimited). It is the
@@ -156,14 +168,81 @@ var skills = []Skill{
156168 },
157169}
158170
159- // Skills returns the registered skills in emission order.
160- func Skills () []Skill {
161- return slices .Clone (skills )
171+ // withoutLockfileSkills replaces registry entries when lock-file-free mode is
172+ // selected, keyed by the name of the default-mode skill it replaces. Everything
173+ // else — order, remaining skills, and the emitted layout — is shared.
174+ //
175+ //nolint:gochecknoglobals // effectively a constant registry of the mode's skills.
176+ var withoutLockfileSkills = map [string ]Skill {
177+ "azldev" : {
178+ Name : "azldev" ,
179+ Description : "Read this before running azldev or editing azldev config, and whenever working " +
180+ "in a repo that contains an azldev.toml file; do not guess azldev's commands or config. " +
181+ "Explains how to use the azldev CLI to build a distro from TOML config, including the core " +
182+ "concepts (components, overlays, distros, rendered specs, upstream commit config), running " +
183+ "azldev (repo root or -C, plus the -q and -O json flags), the common commands, and where to " +
184+ "go for each workflow. Triggers include azldev, comp build, comp render, " +
185+ "comp refresh-upstream-commit, build a component, add a component, distro config." ,
186+ bodyTemplate : "azldev.md.tmpl" ,
187+ },
188+ "azldev-update-component" : {
189+ Name : "azldev-refresh-upstream-commit" ,
190+ Description : "Read this before finalizing a component change, changing source resolution, or " +
191+ "editing generated upstream commit TOML by hand. Explains how to refresh commits with " +
192+ "'azldev comp refresh-upstream-commit', covering when to refresh versus render, the " +
193+ "update/render/commit/re-render/amend workflow, and per-component versus -a refresh. " +
194+ "Triggers include comp refresh-upstream-commit, refresh upstream commit, bump pin, change " +
195+ "snapshot, upstream distro, commit drift, version bump, finalize component." ,
196+ bodyTemplate : "refresh-upstream-commit.md.tmpl" ,
197+ },
162198}
163199
164- // FindSkill returns the registered skill with the given name.
165- func FindSkill (name string ) (Skill , error ) {
166- for _ , skill := range skills {
200+ // updateComponentSkillName is the default mode's finalization skill; lock-file-free
201+ // mode replaces it with the refresh-upstream-commit skill.
202+ const updateComponentSkillName = "azldev-update-component"
203+
204+ // Catalog exposes the skills and instruction files for one of azldev's modes. The
205+ // registries are shared; only the documents and pointers that describe how resolved
206+ // component state is maintained differ.
207+ type Catalog struct {
208+ withoutLockfile bool
209+ }
210+
211+ // NewCatalog returns the catalog for the selected mode. Pass true to describe
212+ // lock-file-free mode, as selected by the global '--without-lockfile' flag.
213+ func NewCatalog (withoutLockfile bool ) Catalog {
214+ return Catalog {withoutLockfile : withoutLockfile }
215+ }
216+
217+ // templates returns the parsed template set for the catalog's mode.
218+ func (c Catalog ) templates () * template.Template {
219+ if c .withoutLockfile {
220+ return withoutLockfileTemplates
221+ }
222+
223+ return templates
224+ }
225+
226+ // Skills returns the catalog's skills in emission order.
227+ func (c Catalog ) Skills () []Skill {
228+ result := slices .Clone (skills )
229+
230+ if ! c .withoutLockfile {
231+ return result
232+ }
233+
234+ for idx := range result {
235+ if replacement , ok := withoutLockfileSkills [result [idx ].Name ]; ok {
236+ result [idx ] = replacement
237+ }
238+ }
239+
240+ return result
241+ }
242+
243+ // FindSkill returns the catalog's skill with the given name.
244+ func (c Catalog ) FindSkill (name string ) (Skill , error ) {
245+ for _ , skill := range c .Skills () {
167246 if skill .Name == name {
168247 return skill , nil
169248 }
@@ -172,6 +251,48 @@ func FindSkill(name string) (Skill, error) {
172251 return Skill {}, fmt .Errorf ("unknown skill %#q" , name )
173252}
174253
254+ // Instructions returns the catalog's instruction files in emission order.
255+ func (c Catalog ) Instructions () []Instruction {
256+ result := slices .Clone (instructions )
257+ for idx := range result {
258+ result [idx ].Skills = slices .Clone (result [idx ].Skills )
259+ }
260+
261+ if ! c .withoutLockfile {
262+ return result
263+ }
264+
265+ refresh := withoutLockfileSkills [updateComponentSkillName ]
266+
267+ for instIdx := range result {
268+ if replacement , ok := withoutLockfileInstructions [result [instIdx ].Name ]; ok {
269+ result [instIdx ].Description = replacement .Description
270+ }
271+
272+ for skillIdx := range result [instIdx ].Skills {
273+ pointer := & result [instIdx ].Skills [skillIdx ]
274+ if pointer .Skill != updateComponentSkillName {
275+ continue
276+ }
277+
278+ pointer .Skill = refresh .Name
279+ pointer .Purpose = strings .ReplaceAll (pointer .Purpose , "lock" , "upstream commit" )
280+ }
281+ }
282+
283+ return result
284+ }
285+
286+ // Skills returns the registered skills in emission order for azldev's default mode.
287+ func Skills () []Skill {
288+ return NewCatalog (false ).Skills ()
289+ }
290+
291+ // FindSkill returns the registered skill with the given name in azldev's default mode.
292+ func FindSkill (name string ) (Skill , error ) {
293+ return NewCatalog (false ).FindSkill (name )
294+ }
295+
175296// SkillPointer names a skill an instruction file points at, together with a short
176297// purpose describing when to read it ("read the `azldev-overlays` skill to add or change
177298// overlays").
@@ -268,14 +389,24 @@ var instructions = []Instruction{
268389 },
269390}
270391
271- // Instructions returns the registered instruction files in emission order.
272- func Instructions () []Instruction {
273- result := slices .Clone (instructions )
274- for i := range result {
275- result [i ].Skills = slices .Clone (result [i ].Skills )
276- }
392+ // withoutLockfileInstructions replaces instruction descriptions when lock-file-free
393+ // mode is selected, keyed by instruction name. Skill pointers are rewritten
394+ // automatically, so only the free-form trigger text lives here.
395+ //
396+ //nolint:gochecknoglobals // effectively a constant registry of the mode's instructions.
397+ var withoutLockfileInstructions = map [string ]Instruction {
398+ SkillName : {
399+ Description : "This repo is an azldev distro project (azldev.toml present). Before running azldev " +
400+ "or editing its config, load the azldev skill; do not guess azldev's commands or config. " +
401+ "Triggers include azldev, comp build, comp render, comp refresh-upstream-commit, build a " +
402+ "component, add a component, distro config." ,
403+ },
404+ }
277405
278- return result
406+ // Instructions returns the registered instruction files in emission order for
407+ // azldev's default mode.
408+ func Instructions () []Instruction {
409+ return NewCatalog (false ).Instructions ()
279410}
280411
281412// Layout controls where emitted skill files are written in a target repository.
@@ -322,8 +453,13 @@ type Command struct {
322453// accurate for a default project even with no configuration present.
323454type Bindings struct {
324455 // LockDir is the repo-relative directory holding per-component lock files.
456+ // Only meaningful in azldev's default mode.
325457 LockDir string
326458
459+ // UpstreamCommitsDir is the repo-relative directory holding the generated
460+ // per-component commit TOMLs. Only meaningful in lock-file-free mode.
461+ UpstreamCommitsDir string
462+
327463 // RenderedSpecsDir is the repo-relative directory holding rendered component specs.
328464 RenderedSpecsDir string
329465
@@ -356,7 +492,7 @@ type EmittedFile struct {
356492 Content string `json:"-"`
357493}
358494
359- func renderSkill (templateName string , skill Skill , params Params ) (string , error ) {
495+ func ( c Catalog ) renderSkill (templateName string , skill Skill , params Params ) (string , error ) {
360496 var buf bytes.Buffer
361497
362498 data := struct {
@@ -369,16 +505,16 @@ func renderSkill(templateName string, skill Skill, params Params) (string, error
369505 ShowSkillToolName : ShowSkillToolName ,
370506 }
371507
372- err := templates .ExecuteTemplate (& buf , templateName , data )
508+ err := c . templates () .ExecuteTemplate (& buf , templateName , data )
373509 if err != nil {
374510 return "" , fmt .Errorf ("failed to render agent skill template %#q:\n %w" , templateName , err )
375511 }
376512
377513 return buf .String (), nil
378514}
379515
380- func renderInstruction (inst Instruction , params Params ) (string , error ) {
381- if err := validateInstruction (inst ); err != nil {
516+ func ( c Catalog ) renderInstruction (inst Instruction , params Params ) (string , error ) {
517+ if err := c . validateInstruction (inst ); err != nil {
382518 return "" , err
383519 }
384520
@@ -398,21 +534,21 @@ func renderInstruction(inst Instruction, params Params) (string, error) {
398534 Instruction : inst ,
399535 }
400536
401- err = templates .ExecuteTemplate (& buf , "instruction-wrapper.md.tmpl" , data )
537+ err = c . templates () .ExecuteTemplate (& buf , "instruction-wrapper.md.tmpl" , data )
402538 if err != nil {
403539 return "" , fmt .Errorf ("failed to render instruction template for %#q:\n %w" , inst .Name , err )
404540 }
405541
406542 return buf .String (), nil
407543}
408544
409- func validateInstruction (inst Instruction ) error {
545+ func ( c Catalog ) validateInstruction (inst Instruction ) error {
410546 if len (inst .Skills ) == 0 {
411547 return fmt .Errorf ("instruction %#q must reference at least one skill" , inst .Name )
412548 }
413549
414550 for _ , pointer := range inst .Skills {
415- if _ , err := FindSkill (pointer .Skill ); err != nil {
551+ if _ , err := c . FindSkill (pointer .Skill ); err != nil {
416552 return fmt .Errorf ("instruction %#q references unknown skill %#q:\n %w" ,
417553 inst .Name , pointer .Skill , err )
418554 }
@@ -442,39 +578,46 @@ func renderInline(name, text string, params Params) (string, error) {
442578// SkillDocument renders the full document for the named skill. It is served
443579// verbatim by the read-only MCP tool and by 'azldev docs agent show'. The default
444580// layout is used since a served document has no on-disk directory.
445- func SkillDocument (name string , params Params ) (string , error ) {
446- skill , err := FindSkill (name )
581+ func ( c Catalog ) SkillDocument (name string , params Params ) (string , error ) {
582+ skill , err := c . FindSkill (name )
447583 if err != nil {
448584 return "" , err
449585 }
450586
451- return renderSkill (skill .bodyTemplate , skill , params )
587+ return c .renderSkill (skill .bodyTemplate , skill , params )
588+ }
589+
590+ // SkillDocument renders the named skill's document for azldev's default mode.
591+ func SkillDocument (name string , params Params ) (string , error ) {
592+ return NewCatalog (false ).SkillDocument (name , params )
452593}
453594
454595// Files renders the set of agent files to write into a target repository using the
455596// given layout. When full is true, each on-disk SKILL.md contains the complete
456597// skill document instead of a light MCP wrapper (useful when the azldev MCP server
457598// is not available in the target environment). Instruction files are always light
458599// wrappers that point at the relevant skills.
459- func Files (layout Layout , params Params , full bool ) ([]EmittedFile , error ) {
460- files := make ([]EmittedFile , 0 , len (skills )+ len (instructions ))
600+ func (c Catalog ) Files (layout Layout , params Params , full bool ) ([]EmittedFile , error ) {
601+ catalogSkills := c .Skills ()
602+ catalogInstructions := c .Instructions ()
603+ files := make ([]EmittedFile , 0 , len (catalogSkills )+ len (catalogInstructions ))
461604
462- for _ , skill := range skills {
605+ for _ , skill := range catalogSkills {
463606 templateName := "skill-wrapper.md.tmpl"
464607 if full {
465608 templateName = skill .bodyTemplate
466609 }
467610
468- rendered , err := renderSkill (templateName , skill , params )
611+ rendered , err := c . renderSkill (templateName , skill , params )
469612 if err != nil {
470613 return nil , err
471614 }
472615
473616 files = append (files , EmittedFile {RelPath : layout .SkillFile (skill ), Content : rendered })
474617 }
475618
476- for _ , inst := range instructions {
477- rendered , err := renderInstruction (inst , params )
619+ for _ , inst := range catalogInstructions {
620+ rendered , err := c . renderInstruction (inst , params )
478621 if err != nil {
479622 return nil , err
480623 }
@@ -484,3 +627,8 @@ func Files(layout Layout, params Params, full bool) ([]EmittedFile, error) {
484627
485628 return files , nil
486629}
630+
631+ // Files renders the agent files for azldev's default mode.
632+ func Files (layout Layout , params Params , full bool ) ([]EmittedFile , error ) {
633+ return NewCatalog (false ).Files (layout , params , full )
634+ }
0 commit comments