Files
member-console/cmd/init.go
T
cgalo5758 88db730fcc Add dual licensing and SPDX headers
Introduce a commercial license option alongside AGPL-3.0-only, require a
CLA for contributors, and document the terms in COMMERCIAL.md and
NOTICE. Add a script to stamp SPDX headers on Go files and apply it
across the tree.
2026-09-06 02:29:42 -05:00

128 lines
4.5 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
/*
Copyright © 2025-2026 Christian Galo
This program is free software: you can redistribute it and/or modify
it under the terms of version 3 of the GNU Affero General Public License as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
A separate commercial license is available from the copyright holder; see
COMMERCIAL.md.
*/
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"git.coopcloud.tech/wiki-cafe/member-console/internal/config"
"git.coopcloud.tech/wiki-cafe/member-console/internal/embeds"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations"
"github.com/spf13/cobra"
)
// initCmd represents the init command
var initCmd = &cobra.Command{
Use: "init [path]",
Short: "Create the directory structure needed for the member-console",
Long: `The init command creates the directory structure and files needed for the
member-console web application in the current directory.
A different directory can be specified using the --dir flag.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Create the directories and files needed for the member-console web application
targetDir := args[0]
// Create the target directory if it doesn't exist
if err := os.MkdirAll(targetDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating target directory: %v\n", err)
os.Exit(1)
}
// Create the templates, static, and assets directories
directories := []string{"templates", "static", "assets"}
for _, dir := range directories {
dirPath := filepath.Join(targetDir, dir)
if err := os.MkdirAll(dirPath, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating %s directory: %v\n", dir, err)
os.Exit(1)
}
}
// Write the embedded starter config template plus one generated
// section per installed integration that declares configuration
// (console-init spec: integration sections are generated from each
// ConfigSpec, never hand-maintained).
configFilePath := filepath.Join(targetDir, "mc-config.yaml")
configTemplate, err := embeds.Config.ReadFile("mc-config.yaml")
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading embedded config template: %v\n", err)
os.Exit(1)
}
scaffold := append(configTemplate, []byte(integrationConfigSections())...)
if err := os.WriteFile(configFilePath, scaffold, 0o644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing config file: %v\n", err)
os.Exit(1)
}
fmt.Printf("Created config file at %s (edit it, then run `member-console start`)\n", configFilePath)
fmt.Printf("Project initialized at %s\n", targetDir)
},
}
// integrationConfigSections renders one commented scaffold section per
// registered integration that declares configuration, from its ConfigSpec:
// every key with its usage text, its declared default when one exists, and a
// "-file" variant note for secrets. Comment lines only, so the scaffold stays
// valid YAML regardless of key types, and a new integration appears here with
// no change to this file or the embedded template.
func integrationConfigSections() string {
var b strings.Builder
for _, integ := range integrations.All() {
cp, ok := integ.(config.ConfigProvider)
if !ok {
continue
}
displayName := integ.Provider().ProviderManifest().DisplayName
fmt.Fprintf(&b, "\n# --- Optional: %s integration (uncomment and fill in to enable) ---\n", displayName)
for _, key := range cp.ConfigSpec() {
fmt.Fprintf(&b, "# %s\n", key.Usage)
switch def := key.Default.(type) {
case nil:
fmt.Fprintf(&b, "# %s: \"\"\n", key.Name)
case []string:
fmt.Fprintf(&b, "# %s: []\n", key.Name)
case bool:
fmt.Fprintf(&b, "# %s: %v\n", key.Name, def)
case string:
fmt.Fprintf(&b, "# %s: %q\n", key.Name, def)
default:
// Durations and any future scalar render via %v inside
// quotes; viper parses the string form back.
fmt.Fprintf(&b, "# %s: \"%v\"\n", key.Name, def)
}
if key.Secret {
fmt.Fprintf(&b, "# %s-file: \"\" # alternative: read the secret from a file path\n", key.Name)
}
}
}
return b.String()
}
func init() {
rootCmd.AddCommand(initCmd)
}