Loading LISEZMOI.md +2 −0 Changes for LISEZMOI.md: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,8 @@ Ce dossier contient l'implémentation de `climax`, la CLI utilisée pour gérer - exporter une copie statique du site (`freeze`) - inspecter les ressources de bundles (`templates-list`, `static-list`) Voir [`commands.md`](./commands.md) pour la référence complète des commandes avec exemples. ## Périmètre et notes - Les données de registre des projets sont stockées dans `~/.climax/projects.json`. Loading README.md +2 −0 Changes for README.md: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,8 @@ This directory contains the implementation of `climax`, the CLI used to manage [ - export a static site snapshot (`freeze`) - inspect bundle assets (`templates-list`, `static-list`) See [`commands.md`](./commands.md) for the full command reference with examples. ## Scope and Notes Loading cli.go +67 −0 Changes for cli.go: 67 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "errors" "fmt" "strconv" "strings" Loading Loading @@ -34,6 +35,7 @@ func newRootCmd() *cobra.Command { cmd.AddCommand(newBundlesRemoveCmd()) cmd.AddCommand(newFeedCmd()) cmd.AddCommand(newSyncCmd()) cmd.AddCommand(newRunCmd()) cmd.AddCommand(newBasexCmd()) cmd.AddCommand(newBasexGUICmd()) cmd.AddCommand(newTemplatesListCmd()) Loading Loading @@ -223,6 +225,71 @@ func newSyncCmd() *cobra.Command { return cmd } // newRunCmd uses custom parsing because everything after <bundle>/<script> is // intended for the XQuery script, not for Cobra. Only wrapper options before // the target are consumed here; script flags are converted to BaseX bindings // when the command runs. func newRunCmd() *cobra.Command { directory := cwd() cmd := &cobra.Command{ Use: "run <bundle>/<script> [script args...]", Short: T("cmd.run.short"), DisableFlagParsing: true, RunE: func(cmd *cobra.Command, args []string) error { opts, err := parseRunArgs(args, directory) if err != nil { return err } if opts.help { renderHelp(cmd, []string{}) return nil } return runBundleScript(opts.target, opts.directory, opts.scriptArgs) }, } cmd.Flags().StringVar(&directory, "directory", directory, T("flag.common.directory")) return cmd } type runArgs struct { directory string target string help bool scriptArgs []string } func parseRunArgs(args []string, directory string) (runArgs, error) { opts := runArgs{directory: directory} for i := 0; i < len(args); i++ { arg := args[i] switch { case arg == "--": if i+1 >= len(args) { return opts, errors.New(T("err.run_target_required")) } opts.target = args[i+1] opts.scriptArgs = args[i+2:] return opts, nil case arg == "--help" || arg == "-h": opts.help = true return opts, nil case arg == "--directory": if i+1 >= len(args) { return opts, fmt.Errorf(T("err.flag_needs_argument"), "--directory") } i++ opts.directory = args[i] case strings.HasPrefix(arg, "--directory="): opts.directory = strings.TrimPrefix(arg, "--directory=") default: opts.target = arg opts.scriptArgs = args[i+1:] return opts, nil } } return opts, errors.New(T("err.run_target_required")) } // newBasexCmd is intentionally wired differently from the other climax // commands: BaseX has its own CLI flags, so Cobra must not parse or reject // unknown options before Java sees them. Only climax-owned options are handled Loading cli_commands_test.go +3 −0 Changes for cli_commands_test.go: 3 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ func TestRootRegistersAllCLICommands(t *testing.T) { "info", "new", "projects", "run", "start", "static-list", "stop", Loading Loading @@ -64,6 +65,7 @@ func TestCLICommandsExposeExpectedFlags(t *testing.T) { "bundles-remove": {"directory"}, "feed": {"directory", "verbose", "yes"}, "sync": {"directory"}, "run": {"directory"}, "basex": {"directory", "verbose"}, "basex-gui": {"directory", "verbose"}, "templates-list": {"directory"}, Loading Loading @@ -128,6 +130,7 @@ func TestHelpRunsForAllCLICommands(t *testing.T) { {"bundles-remove", "dummy", "--help"}, {"feed", "dummy", "--help"}, {"sync", "--help"}, {"run", "--help"}, {"help", "basex"}, {"basex-gui", "--help"}, {"templates-list", "--help"}, Loading commands.go +97 −0 Changes for commands.go: 97 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -408,6 +408,103 @@ func runSync(directory string) error { return errors.New(T("err.no_max_use_new")) } func runBundleScript(target, directory string, scriptArgs []string) error { abs, _, err := loadProject(directory) if err != nil { return err } bundleName, scriptName, err := parseBundleScriptTarget(target) if err != nil { return err } bundleDir := filepath.Join(abs, ".max", "basex", "webapp", "max", "bundles", bundleName) if !dirExists(bundleDir) || fileExists(filepath.Join(bundleDir, ".ignore")) { return fmt.Errorf(T("err.bundle_not_active"), bundleName) } scriptPath, err := bundleScriptPath(bundleDir, scriptName) if err != nil { return err } if !fileExists(scriptPath) { return fmt.Errorf(T("err.run_script_not_found"), filepath.ToSlash(filepath.Join(bundleName, scriptName))) } bindings, err := runScriptBindings(scriptArgs) if err != nil { return err } cmdArgs := []string{ "-u", "-b", fmt.Sprintf("maxPath=\"%s\"", abs), } cmdArgs = append(cmdArgs, bindings...) cmdArgs = append(cmdArgs, scriptPath) cmd, err := newBaseXCommand(abs, "org.basex.BaseX", cmdArgs...) if err != nil { return err } return cmd.Run() } func parseBundleScriptTarget(target string) (string, string, error) { bundleName, scriptName, ok := strings.Cut(strings.TrimSpace(target), "/") if !ok || strings.TrimSpace(bundleName) == "" || strings.TrimSpace(scriptName) == "" { return "", "", fmt.Errorf(T("err.run_target_format"), target) } if bundleName != filepath.Base(bundleName) || bundleName == "." || bundleName == ".." { return "", "", fmt.Errorf(T("err.run_target_format"), target) } return bundleName, scriptName, nil } func bundleScriptPath(bundleDir, scriptName string) (string, error) { scriptsDir := filepath.Join(bundleDir, "scripts") scriptRel := filepath.Clean(filepath.FromSlash(strings.TrimSpace(scriptName))) if scriptRel == "." || scriptRel == ".." || filepath.IsAbs(scriptRel) || strings.HasPrefix(scriptRel, ".."+string(os.PathSeparator)) { return "", fmt.Errorf(T("err.run_target_format"), scriptName) } if !strings.HasSuffix(strings.ToLower(scriptRel), ".xq") { scriptRel += ".xq" } scriptPath := filepath.Join(scriptsDir, scriptRel) rel, err := filepath.Rel(scriptsDir, scriptPath) if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { return "", fmt.Errorf(T("err.run_target_format"), scriptName) } return scriptPath, nil } func runScriptBindings(args []string) ([]string, error) { bindings := make([]string, 0, len(args)*2) for i := 0; i < len(args); i++ { arg := args[i] if !strings.HasPrefix(arg, "-") || arg == "-" { return nil, fmt.Errorf(T("err.run_param_format"), arg) } nameValue := strings.TrimLeft(arg, "-") name, value, hasValue := strings.Cut(nameValue, "=") name = strings.TrimSpace(name) if name == "" { return nil, fmt.Errorf(T("err.run_param_format"), arg) } if !hasValue { if i+1 < len(args) && !looksLikeRunScriptFlag(args[i+1]) { i++ value = args[i] } else { value = "true" } } bindings = append(bindings, "-b", fmt.Sprintf("%s=%s", name, value)) } return bindings, nil } func looksLikeRunScriptFlag(arg string) bool { return strings.HasPrefix(arg, "-") && arg != "-" } func runBasex(directory string, verbose bool, args []string) error { abs, _, err := loadProject(directory) if err != nil { Loading Loading
LISEZMOI.md +2 −0 Changes for LISEZMOI.md: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,8 @@ Ce dossier contient l'implémentation de `climax`, la CLI utilisée pour gérer - exporter une copie statique du site (`freeze`) - inspecter les ressources de bundles (`templates-list`, `static-list`) Voir [`commands.md`](./commands.md) pour la référence complète des commandes avec exemples. ## Périmètre et notes - Les données de registre des projets sont stockées dans `~/.climax/projects.json`. Loading
README.md +2 −0 Changes for README.md: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,8 @@ This directory contains the implementation of `climax`, the CLI used to manage [ - export a static site snapshot (`freeze`) - inspect bundle assets (`templates-list`, `static-list`) See [`commands.md`](./commands.md) for the full command reference with examples. ## Scope and Notes Loading
cli.go +67 −0 Changes for cli.go: 67 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "errors" "fmt" "strconv" "strings" Loading Loading @@ -34,6 +35,7 @@ func newRootCmd() *cobra.Command { cmd.AddCommand(newBundlesRemoveCmd()) cmd.AddCommand(newFeedCmd()) cmd.AddCommand(newSyncCmd()) cmd.AddCommand(newRunCmd()) cmd.AddCommand(newBasexCmd()) cmd.AddCommand(newBasexGUICmd()) cmd.AddCommand(newTemplatesListCmd()) Loading Loading @@ -223,6 +225,71 @@ func newSyncCmd() *cobra.Command { return cmd } // newRunCmd uses custom parsing because everything after <bundle>/<script> is // intended for the XQuery script, not for Cobra. Only wrapper options before // the target are consumed here; script flags are converted to BaseX bindings // when the command runs. func newRunCmd() *cobra.Command { directory := cwd() cmd := &cobra.Command{ Use: "run <bundle>/<script> [script args...]", Short: T("cmd.run.short"), DisableFlagParsing: true, RunE: func(cmd *cobra.Command, args []string) error { opts, err := parseRunArgs(args, directory) if err != nil { return err } if opts.help { renderHelp(cmd, []string{}) return nil } return runBundleScript(opts.target, opts.directory, opts.scriptArgs) }, } cmd.Flags().StringVar(&directory, "directory", directory, T("flag.common.directory")) return cmd } type runArgs struct { directory string target string help bool scriptArgs []string } func parseRunArgs(args []string, directory string) (runArgs, error) { opts := runArgs{directory: directory} for i := 0; i < len(args); i++ { arg := args[i] switch { case arg == "--": if i+1 >= len(args) { return opts, errors.New(T("err.run_target_required")) } opts.target = args[i+1] opts.scriptArgs = args[i+2:] return opts, nil case arg == "--help" || arg == "-h": opts.help = true return opts, nil case arg == "--directory": if i+1 >= len(args) { return opts, fmt.Errorf(T("err.flag_needs_argument"), "--directory") } i++ opts.directory = args[i] case strings.HasPrefix(arg, "--directory="): opts.directory = strings.TrimPrefix(arg, "--directory=") default: opts.target = arg opts.scriptArgs = args[i+1:] return opts, nil } } return opts, errors.New(T("err.run_target_required")) } // newBasexCmd is intentionally wired differently from the other climax // commands: BaseX has its own CLI flags, so Cobra must not parse or reject // unknown options before Java sees them. Only climax-owned options are handled Loading
cli_commands_test.go +3 −0 Changes for cli_commands_test.go: 3 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ func TestRootRegistersAllCLICommands(t *testing.T) { "info", "new", "projects", "run", "start", "static-list", "stop", Loading Loading @@ -64,6 +65,7 @@ func TestCLICommandsExposeExpectedFlags(t *testing.T) { "bundles-remove": {"directory"}, "feed": {"directory", "verbose", "yes"}, "sync": {"directory"}, "run": {"directory"}, "basex": {"directory", "verbose"}, "basex-gui": {"directory", "verbose"}, "templates-list": {"directory"}, Loading Loading @@ -128,6 +130,7 @@ func TestHelpRunsForAllCLICommands(t *testing.T) { {"bundles-remove", "dummy", "--help"}, {"feed", "dummy", "--help"}, {"sync", "--help"}, {"run", "--help"}, {"help", "basex"}, {"basex-gui", "--help"}, {"templates-list", "--help"}, Loading
commands.go +97 −0 Changes for commands.go: 97 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -408,6 +408,103 @@ func runSync(directory string) error { return errors.New(T("err.no_max_use_new")) } func runBundleScript(target, directory string, scriptArgs []string) error { abs, _, err := loadProject(directory) if err != nil { return err } bundleName, scriptName, err := parseBundleScriptTarget(target) if err != nil { return err } bundleDir := filepath.Join(abs, ".max", "basex", "webapp", "max", "bundles", bundleName) if !dirExists(bundleDir) || fileExists(filepath.Join(bundleDir, ".ignore")) { return fmt.Errorf(T("err.bundle_not_active"), bundleName) } scriptPath, err := bundleScriptPath(bundleDir, scriptName) if err != nil { return err } if !fileExists(scriptPath) { return fmt.Errorf(T("err.run_script_not_found"), filepath.ToSlash(filepath.Join(bundleName, scriptName))) } bindings, err := runScriptBindings(scriptArgs) if err != nil { return err } cmdArgs := []string{ "-u", "-b", fmt.Sprintf("maxPath=\"%s\"", abs), } cmdArgs = append(cmdArgs, bindings...) cmdArgs = append(cmdArgs, scriptPath) cmd, err := newBaseXCommand(abs, "org.basex.BaseX", cmdArgs...) if err != nil { return err } return cmd.Run() } func parseBundleScriptTarget(target string) (string, string, error) { bundleName, scriptName, ok := strings.Cut(strings.TrimSpace(target), "/") if !ok || strings.TrimSpace(bundleName) == "" || strings.TrimSpace(scriptName) == "" { return "", "", fmt.Errorf(T("err.run_target_format"), target) } if bundleName != filepath.Base(bundleName) || bundleName == "." || bundleName == ".." { return "", "", fmt.Errorf(T("err.run_target_format"), target) } return bundleName, scriptName, nil } func bundleScriptPath(bundleDir, scriptName string) (string, error) { scriptsDir := filepath.Join(bundleDir, "scripts") scriptRel := filepath.Clean(filepath.FromSlash(strings.TrimSpace(scriptName))) if scriptRel == "." || scriptRel == ".." || filepath.IsAbs(scriptRel) || strings.HasPrefix(scriptRel, ".."+string(os.PathSeparator)) { return "", fmt.Errorf(T("err.run_target_format"), scriptName) } if !strings.HasSuffix(strings.ToLower(scriptRel), ".xq") { scriptRel += ".xq" } scriptPath := filepath.Join(scriptsDir, scriptRel) rel, err := filepath.Rel(scriptsDir, scriptPath) if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { return "", fmt.Errorf(T("err.run_target_format"), scriptName) } return scriptPath, nil } func runScriptBindings(args []string) ([]string, error) { bindings := make([]string, 0, len(args)*2) for i := 0; i < len(args); i++ { arg := args[i] if !strings.HasPrefix(arg, "-") || arg == "-" { return nil, fmt.Errorf(T("err.run_param_format"), arg) } nameValue := strings.TrimLeft(arg, "-") name, value, hasValue := strings.Cut(nameValue, "=") name = strings.TrimSpace(name) if name == "" { return nil, fmt.Errorf(T("err.run_param_format"), arg) } if !hasValue { if i+1 < len(args) && !looksLikeRunScriptFlag(args[i+1]) { i++ value = args[i] } else { value = "true" } } bindings = append(bindings, "-b", fmt.Sprintf("%s=%s", name, value)) } return bindings, nil } func looksLikeRunScriptFlag(arg string) bool { return strings.HasPrefix(arg, "-") && arg != "-" } func runBasex(directory string, verbose bool, args []string) error { abs, _, err := loadProject(directory) if err != nil { Loading