mirror of
https://github.com/lukaszraczylo/harness-sync.git
synced 2026-10-12 00:45:22 +00:00
feat(canonical): validate profile name, apply env-var substitution before render
This commit is contained in:
+24
-14
@@ -23,6 +23,7 @@ func NewInit(reg *adapter.Registry) *cobra.Command {
|
||||
home string
|
||||
from []string
|
||||
noPrompt bool
|
||||
force bool
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "init",
|
||||
@@ -42,6 +43,10 @@ func NewInit(reg *adapter.Registry) *cobra.Command {
|
||||
if err = os.MkdirAll(r, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
// Issue 1: refuse overwrite without --force
|
||||
if _, statErr := os.Stat(filepath.Join(r, "config.yaml")); statErr == nil && !force {
|
||||
return fmt.Errorf("canonical root %s already initialised (use --force to re-import; existing skills/agents/mcp will be merged)", r)
|
||||
}
|
||||
candidates := reg.DetectedNames()
|
||||
if len(candidates) == 0 {
|
||||
return fmt.Errorf("no harnesses detected under %s", home)
|
||||
@@ -77,6 +82,7 @@ func NewInit(reg *adapter.Registry) *cobra.Command {
|
||||
cmd.Flags().StringVar(&home, "home", "", "home dir (default $HOME)")
|
||||
cmd.Flags().StringSliceVar(&from, "from", nil, "import from specific adapter(s), skip prompt")
|
||||
cmd.Flags().BoolVar(&noPrompt, "no-prompt", false, "import from all detected adapters without prompting")
|
||||
cmd.Flags().BoolVar(&force, "force", false, "re-import even if canonical root already initialised")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -153,6 +159,18 @@ func runImport(cmd *cobra.Command, reg *adapter.Registry, root, home string, pic
|
||||
return nil
|
||||
}
|
||||
|
||||
// importedProfileBody is the placeholder written to profiles/imported.yaml on first init.
|
||||
// Written using escape sequences to avoid literal newlines in string literals.
|
||||
const importedProfileBody = "# harness-sync profile \xe2\x80\x94 generated by init\n" +
|
||||
"# Edit the gateway block below to point at your LLM gateway before running\n" +
|
||||
"# `harness-sync apply`. The dummy token is fine for most local gateways.\n" +
|
||||
"name: imported\n" +
|
||||
"gateway:\n" +
|
||||
" url: \"\" # \xe2\x86\x90 set this\n" +
|
||||
" token: \"dummy\"\n" +
|
||||
" default_model: \"\" # \xe2\x86\x90 set this\n" +
|
||||
"models: []\n"
|
||||
|
||||
func writeCanonical(root string, b *canonical.Bundle, picked []string) error {
|
||||
for _, sub := range []string{"profiles", "skills", "agents", "instructions"} {
|
||||
if err := os.MkdirAll(filepath.Join(root, sub), 0o750); err != nil {
|
||||
@@ -171,20 +189,12 @@ func writeCanonical(root string, b *canonical.Bundle, picked []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
profBody, err := render.YAML(canonical.Profile{
|
||||
Name: "imported",
|
||||
Gateway: canonical.Gateway{
|
||||
URL: "https://gateway.local",
|
||||
Token: "dummy",
|
||||
DefaultModel: "claude-sonnet-4-6",
|
||||
},
|
||||
Models: []canonical.Model{{ID: "claude-sonnet-4-6", Alias: "sonnet"}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.WriteFile(filepath.Join(root, "profiles", "imported.yaml"), profBody, 0o600); err != nil {
|
||||
return err
|
||||
// Issue 2: write placeholder profile only if it doesn't exist yet (preserve user edits on re-init).
|
||||
profPath := filepath.Join(root, "profiles", "imported.yaml")
|
||||
if _, statErr := os.Stat(profPath); os.IsNotExist(statErr) {
|
||||
if err = os.WriteFile(profPath, []byte(importedProfileBody), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.MCP.Servers) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user