Loading AGENTS.md +23 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,29 @@ Maintain `climax` as a stable CLI app, while preserving behavior and output expe - On cache miss, show network activity feedback (spinner/progress) on interactive terminals. - Keep non-interactive output clean (no noisy progress output when not on a TTY). ## Vocabulary Bundle Behavior - A vocabulary bundle is detected from the installed bundle contents, not only from `available-bundles.xml`. - Detection requires: - `expath-pkg.xml` - an EXPath `<xquery>` namespace - an EXPath-listed `<file>` - a function named `xml-to-html` declared in that namespace. - XQuery namespace declarations can use either: - `declare namespace prefix = "...";` - `module namespace prefix = "...";` - If an EXPath `<file>` value is only a basename, the detector falls back to a recursive basename search inside the bundle. Keep path traversal protections intact when changing this logic. - When `bundles-add` adds a vocabulary bundle, it becomes the configured `vocabulary-bundle`; the previous vocabulary bundle is removed from active bundles in `config.xml` and sync is run again so the old bundle is ignored like a removed bundle. - Only one vocabulary bundle should be active. Do not reintroduce behavior where every bundle containing `expath-pkg.xml` is treated as a vocabulary bundle. - `bundles-list` should keep showing whether each row is a vocabulary bundle. ## Storage and Project Registry - Use JSON storage in `~/.climax/projects.json`. Loading cli_commands_test.go +3 −0 Original line number Diff line number Diff line Loading @@ -229,6 +229,9 @@ func TestBundlesAddHelpDocumentsInputKinds(t *testing.T) { "MaX-provided bundle", "local zip bundle archive", "HTTPS URL to a remote zip bundle archive", "project's vocabulary bundle", "previous vocabulary bundle is removed from active bundles", "sync runs again", "climax bundles-add max-dev", "climax bundles-add ./local-bundle.zip", "climax bundles-add https://example.org/local-bundle.zip --yes", Loading commandes.md +12 −0 Original line number Diff line number Diff line Loading @@ -190,6 +190,7 @@ Options : Liste les bundles disponibles et configurés. Un bundle est un paquet d'extension MaX qui peut ajouter des templates, des fichiers statiques, des scripts ou d'autres ressources au projet. Le tableau indique aussi si chaque bundle est un bundle de vocabulaire. - Liste les bundles du projet courant : Loading @@ -211,6 +212,17 @@ Options : Ajoute un bundle à la configuration du projet, puis synchronise le projet. Si le bundle ajouté est détecté comme bundle de vocabulaire, il devient le bundle de vocabulaire configuré du projet. Le précédent bundle de vocabulaire est retiré des bundles actifs dans `config.xml`, puis la synchronisation est relancée pour l'ignorer comme un bundle supprimé. La détection d'un bundle de vocabulaire s'appuie sur le contenu du bundle installé : `expath-pkg.xml` doit lister un namespace XQuery et un fichier, et ce module doit déclarer une fonction `xml-to-html` dans le namespace du paquet. Les fichiers listés peuvent se trouver dans des sous-dossiers ; `bundles-add` les résout après extraction. - Ajoute le bundle `max-dev` au projet courant : ```sh Loading commands.go +65 −28 Original line number Diff line number Diff line Loading @@ -160,7 +160,7 @@ func runInfo(directory string, jsonOut bool) error { } func runBundlesList(directory string) error { _, config, err := prepareProject(directory) abs, config, err := prepareProject(directory) if err != nil { return err } Loading @@ -170,13 +170,18 @@ func runBundlesList(directory string) error { } t := newStyledTable() t.AppendHeader(table.Row{T("table.bundles.name"), T("table.bundles.installed"), T("table.bundles.description")}) t.AppendHeader(table.Row{T("table.bundles.name"), T("table.bundles.installed"), T("table.bundles.vocabulary"), T("table.bundles.description")}) done := map[string]struct{}{} for _, name := range sortedMapKeys(available) { b := available[name] isVocabulary, err := bundleVocabularyStatus(abs, b.Name, b.Vocabulary) if err != nil { return err } t.AppendRow(table.Row{ b.Name, coloredYesNo(b.Active), coloredYesNo(isVocabulary), b.Description, }) done[name] = struct{}{} Loading @@ -188,12 +193,28 @@ func runBundlesList(directory string) error { } } for _, name := range localNames { t.AppendRow(table.Row{name, coloredYesNo(true), T("msg.bundle.local_no_desc")}) isVocabulary, err := bundleVocabularyStatus(abs, name, false) if err != nil { return err } t.AppendRow(table.Row{name, coloredYesNo(true), coloredYesNo(isVocabulary), T("msg.bundle.local_no_desc")}) } t.Render() return nil } func bundleVocabularyStatus(rootDirectory, bundleName string, fallback bool) (bool, error) { bundleDir := filepath.Join(rootDirectory, ".max", "basex", "webapp", "max", "bundles", bundleName) if !dirExists(bundleDir) { return fallback, nil } isVocabulary, err := bundleIsVocabularyBundle(bundleDir) if err != nil { return false, err } return isVocabulary, nil } func runBundlesAdd(bundleName, directory string, yes bool) error { abs, config, err := prepareProject(directory) if err != nil { Loading Loading @@ -221,14 +242,7 @@ func runBundlesAdd(bundleName, directory string, yes bool) error { } } current[remoteName] = bundleConfig{Name: remoteName, URL: remoteURL} config.setBundlesMap(current) if err := config.write(); err != nil { return err } if err := syncMaxInstance(abs, false); err != nil { return err } return runBundlesList(abs) return writeSyncAndListAfterBundleAdd(abs, config, current, remoteName) } if fromArchive != "" { Loading @@ -246,39 +260,62 @@ func runBundlesAdd(bundleName, directory string, yes bool) error { return err } current[bundleName] = bundleConfig{Name: bundleName, URL: "local://" + filepath.Base(localDest)} config.setBundlesMap(current) if fileExists(filepath.Join(localDest, "expath-pkg.xml")) { config.Cfg.VocabularyBundle = bundleName return writeSyncAndListAfterBundleAdd(abs, config, current, bundleName) } available, err := config.availableBundles() if err != nil { return err } selected, ok := available[bundleName] if !ok { return fmt.Errorf(T("err.bundle_not_available"), bundleName) } current[bundleName] = bundleConfig{Name: bundleName, URL: selected.URL} return writeSyncAndListAfterBundleAdd(abs, config, current, bundleName) } func writeSyncAndListAfterBundleAdd(rootDirectory string, config *projectConfig, bundles map[string]bundleConfig, bundleName string) error { config.setBundlesMap(bundles) if err := config.write(); err != nil { return err } if err := syncMaxInstance(abs, false); err != nil { if err := syncMaxInstance(rootDirectory, false); err != nil { return err } return runBundlesList(abs) if err := updateVocabularyBundleAfterAdd(rootDirectory, bundleName); err != nil { return err } return runBundlesList(rootDirectory) } available, err := config.availableBundles() func updateVocabularyBundleAfterAdd(rootDirectory, bundleName string) error { bundleDir := filepath.Join(rootDirectory, ".max", "basex", "webapp", "max", "bundles", bundleName) isVocabulary, err := bundleIsVocabularyBundle(bundleDir) if err != nil { return err } selected, ok := available[bundleName] if !ok { return fmt.Errorf(T("err.bundle_not_available"), bundleName) if !isVocabulary { return nil } if selected.Vocabulary { config.Cfg.VocabularyBundle = bundleName config, err := loadProjectConfig(filepath.Join(rootDirectory, maxConfigFile)) if err != nil { return err } current[bundleName] = bundleConfig{Name: bundleName, URL: selected.URL} current := config.bundlesMap() previous := strings.TrimSpace(config.Cfg.VocabularyBundle) if previous != "" && previous != bundleName { delete(current, previous) } if _, ok := current[bundleName]; !ok { current[bundleName] = bundleConfig{Name: bundleName} } config.Cfg.VocabularyBundle = bundleName config.setBundlesMap(current) if err := config.write(); err != nil { return err } if err := syncMaxInstance(abs, false); err != nil { return err } return runBundlesList(abs) return syncMaxInstance(rootDirectory, false) } func remoteBundleArchive(input string) (string, string, bool, error) { Loading commands.md +11 −0 Original line number Diff line number Diff line Loading @@ -186,6 +186,7 @@ Options: List available and configured bundles. Output keeps the existing table style. A bundle is a MaX extension package that can add templates, static assets, scripts, or other project resources. The table also indicates whether each bundle is a vocabulary bundle. - List bundles for the current project: Loading @@ -207,6 +208,16 @@ Options: Add a bundle to the project configuration, then sync the project. If the added bundle is detected as a vocabulary bundle, it becomes the project's configured vocabulary bundle. The previously configured vocabulary bundle is removed from active bundles in `config.xml`, then sync runs again so the previous bundle is ignored in the same way as a removed bundle. Vocabulary bundle detection is based on the installed bundle contents: `expath-pkg.xml` must list an XQuery namespace and file, and that listed module must declare an `xml-to-html` function in the package namespace. Listed files may be stored in subdirectories; `bundles-add` resolves them after extraction. - Add the `max-dev` bundle to the current project: ```sh Loading Loading
AGENTS.md +23 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,29 @@ Maintain `climax` as a stable CLI app, while preserving behavior and output expe - On cache miss, show network activity feedback (spinner/progress) on interactive terminals. - Keep non-interactive output clean (no noisy progress output when not on a TTY). ## Vocabulary Bundle Behavior - A vocabulary bundle is detected from the installed bundle contents, not only from `available-bundles.xml`. - Detection requires: - `expath-pkg.xml` - an EXPath `<xquery>` namespace - an EXPath-listed `<file>` - a function named `xml-to-html` declared in that namespace. - XQuery namespace declarations can use either: - `declare namespace prefix = "...";` - `module namespace prefix = "...";` - If an EXPath `<file>` value is only a basename, the detector falls back to a recursive basename search inside the bundle. Keep path traversal protections intact when changing this logic. - When `bundles-add` adds a vocabulary bundle, it becomes the configured `vocabulary-bundle`; the previous vocabulary bundle is removed from active bundles in `config.xml` and sync is run again so the old bundle is ignored like a removed bundle. - Only one vocabulary bundle should be active. Do not reintroduce behavior where every bundle containing `expath-pkg.xml` is treated as a vocabulary bundle. - `bundles-list` should keep showing whether each row is a vocabulary bundle. ## Storage and Project Registry - Use JSON storage in `~/.climax/projects.json`. Loading
cli_commands_test.go +3 −0 Original line number Diff line number Diff line Loading @@ -229,6 +229,9 @@ func TestBundlesAddHelpDocumentsInputKinds(t *testing.T) { "MaX-provided bundle", "local zip bundle archive", "HTTPS URL to a remote zip bundle archive", "project's vocabulary bundle", "previous vocabulary bundle is removed from active bundles", "sync runs again", "climax bundles-add max-dev", "climax bundles-add ./local-bundle.zip", "climax bundles-add https://example.org/local-bundle.zip --yes", Loading
commandes.md +12 −0 Original line number Diff line number Diff line Loading @@ -190,6 +190,7 @@ Options : Liste les bundles disponibles et configurés. Un bundle est un paquet d'extension MaX qui peut ajouter des templates, des fichiers statiques, des scripts ou d'autres ressources au projet. Le tableau indique aussi si chaque bundle est un bundle de vocabulaire. - Liste les bundles du projet courant : Loading @@ -211,6 +212,17 @@ Options : Ajoute un bundle à la configuration du projet, puis synchronise le projet. Si le bundle ajouté est détecté comme bundle de vocabulaire, il devient le bundle de vocabulaire configuré du projet. Le précédent bundle de vocabulaire est retiré des bundles actifs dans `config.xml`, puis la synchronisation est relancée pour l'ignorer comme un bundle supprimé. La détection d'un bundle de vocabulaire s'appuie sur le contenu du bundle installé : `expath-pkg.xml` doit lister un namespace XQuery et un fichier, et ce module doit déclarer une fonction `xml-to-html` dans le namespace du paquet. Les fichiers listés peuvent se trouver dans des sous-dossiers ; `bundles-add` les résout après extraction. - Ajoute le bundle `max-dev` au projet courant : ```sh Loading
commands.go +65 −28 Original line number Diff line number Diff line Loading @@ -160,7 +160,7 @@ func runInfo(directory string, jsonOut bool) error { } func runBundlesList(directory string) error { _, config, err := prepareProject(directory) abs, config, err := prepareProject(directory) if err != nil { return err } Loading @@ -170,13 +170,18 @@ func runBundlesList(directory string) error { } t := newStyledTable() t.AppendHeader(table.Row{T("table.bundles.name"), T("table.bundles.installed"), T("table.bundles.description")}) t.AppendHeader(table.Row{T("table.bundles.name"), T("table.bundles.installed"), T("table.bundles.vocabulary"), T("table.bundles.description")}) done := map[string]struct{}{} for _, name := range sortedMapKeys(available) { b := available[name] isVocabulary, err := bundleVocabularyStatus(abs, b.Name, b.Vocabulary) if err != nil { return err } t.AppendRow(table.Row{ b.Name, coloredYesNo(b.Active), coloredYesNo(isVocabulary), b.Description, }) done[name] = struct{}{} Loading @@ -188,12 +193,28 @@ func runBundlesList(directory string) error { } } for _, name := range localNames { t.AppendRow(table.Row{name, coloredYesNo(true), T("msg.bundle.local_no_desc")}) isVocabulary, err := bundleVocabularyStatus(abs, name, false) if err != nil { return err } t.AppendRow(table.Row{name, coloredYesNo(true), coloredYesNo(isVocabulary), T("msg.bundle.local_no_desc")}) } t.Render() return nil } func bundleVocabularyStatus(rootDirectory, bundleName string, fallback bool) (bool, error) { bundleDir := filepath.Join(rootDirectory, ".max", "basex", "webapp", "max", "bundles", bundleName) if !dirExists(bundleDir) { return fallback, nil } isVocabulary, err := bundleIsVocabularyBundle(bundleDir) if err != nil { return false, err } return isVocabulary, nil } func runBundlesAdd(bundleName, directory string, yes bool) error { abs, config, err := prepareProject(directory) if err != nil { Loading Loading @@ -221,14 +242,7 @@ func runBundlesAdd(bundleName, directory string, yes bool) error { } } current[remoteName] = bundleConfig{Name: remoteName, URL: remoteURL} config.setBundlesMap(current) if err := config.write(); err != nil { return err } if err := syncMaxInstance(abs, false); err != nil { return err } return runBundlesList(abs) return writeSyncAndListAfterBundleAdd(abs, config, current, remoteName) } if fromArchive != "" { Loading @@ -246,39 +260,62 @@ func runBundlesAdd(bundleName, directory string, yes bool) error { return err } current[bundleName] = bundleConfig{Name: bundleName, URL: "local://" + filepath.Base(localDest)} config.setBundlesMap(current) if fileExists(filepath.Join(localDest, "expath-pkg.xml")) { config.Cfg.VocabularyBundle = bundleName return writeSyncAndListAfterBundleAdd(abs, config, current, bundleName) } available, err := config.availableBundles() if err != nil { return err } selected, ok := available[bundleName] if !ok { return fmt.Errorf(T("err.bundle_not_available"), bundleName) } current[bundleName] = bundleConfig{Name: bundleName, URL: selected.URL} return writeSyncAndListAfterBundleAdd(abs, config, current, bundleName) } func writeSyncAndListAfterBundleAdd(rootDirectory string, config *projectConfig, bundles map[string]bundleConfig, bundleName string) error { config.setBundlesMap(bundles) if err := config.write(); err != nil { return err } if err := syncMaxInstance(abs, false); err != nil { if err := syncMaxInstance(rootDirectory, false); err != nil { return err } return runBundlesList(abs) if err := updateVocabularyBundleAfterAdd(rootDirectory, bundleName); err != nil { return err } return runBundlesList(rootDirectory) } available, err := config.availableBundles() func updateVocabularyBundleAfterAdd(rootDirectory, bundleName string) error { bundleDir := filepath.Join(rootDirectory, ".max", "basex", "webapp", "max", "bundles", bundleName) isVocabulary, err := bundleIsVocabularyBundle(bundleDir) if err != nil { return err } selected, ok := available[bundleName] if !ok { return fmt.Errorf(T("err.bundle_not_available"), bundleName) if !isVocabulary { return nil } if selected.Vocabulary { config.Cfg.VocabularyBundle = bundleName config, err := loadProjectConfig(filepath.Join(rootDirectory, maxConfigFile)) if err != nil { return err } current[bundleName] = bundleConfig{Name: bundleName, URL: selected.URL} current := config.bundlesMap() previous := strings.TrimSpace(config.Cfg.VocabularyBundle) if previous != "" && previous != bundleName { delete(current, previous) } if _, ok := current[bundleName]; !ok { current[bundleName] = bundleConfig{Name: bundleName} } config.Cfg.VocabularyBundle = bundleName config.setBundlesMap(current) if err := config.write(); err != nil { return err } if err := syncMaxInstance(abs, false); err != nil { return err } return runBundlesList(abs) return syncMaxInstance(rootDirectory, false) } func remoteBundleArchive(input string) (string, string, bool, error) { Loading
commands.md +11 −0 Original line number Diff line number Diff line Loading @@ -186,6 +186,7 @@ Options: List available and configured bundles. Output keeps the existing table style. A bundle is a MaX extension package that can add templates, static assets, scripts, or other project resources. The table also indicates whether each bundle is a vocabulary bundle. - List bundles for the current project: Loading @@ -207,6 +208,16 @@ Options: Add a bundle to the project configuration, then sync the project. If the added bundle is detected as a vocabulary bundle, it becomes the project's configured vocabulary bundle. The previously configured vocabulary bundle is removed from active bundles in `config.xml`, then sync runs again so the previous bundle is ignored in the same way as a removed bundle. Vocabulary bundle detection is based on the installed bundle contents: `expath-pkg.xml` must list an XQuery namespace and file, and that listed module must declare an `xml-to-html` function in the package namespace. Listed files may be stored in subdirectories; `bundles-add` resolves them after extraction. - Add the `max-dev` bundle to the current project: ```sh Loading