170 lines
6.4 KiB
Go
170 lines
6.4 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
package commands
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"code.vikunja.io/veans/internal/bootstrap"
|
|
"code.vikunja.io/veans/internal/config"
|
|
)
|
|
|
|
type initFlags struct {
|
|
server string
|
|
token string
|
|
username string
|
|
password string
|
|
totp string
|
|
usePassword bool
|
|
botUsername string
|
|
projectID int64
|
|
viewID int64
|
|
yesBuckets bool
|
|
skipBuckets bool
|
|
configPath string
|
|
installClaude bool
|
|
installOpenCode bool
|
|
noHooks bool
|
|
}
|
|
|
|
func newInitCmd() *cobra.Command {
|
|
f := &initFlags{}
|
|
cmd := &cobra.Command{
|
|
Use: "init",
|
|
Short: "Provision a Vikunja bot user and write .veans.yml",
|
|
Long: `Onboards veans into the current repository:
|
|
|
|
1. Authenticate as you (--token, or username/password)
|
|
2. Pick a Vikunja project and Kanban view
|
|
3. Bootstrap canonical buckets (Todo / In Progress / In Review / Done / Scrapped)
|
|
4. Create a 'bot-<repo>' user, share the project with it, mint its API token
|
|
5. Store the bot's token in your keychain (or ~/.config/veans/credentials.yml)
|
|
6. Write .veans.yml to the repository root
|
|
|
|
The token stored locally belongs to the bot, not to you — you can rotate or
|
|
revoke it at any time without affecting your own session.`,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
path := f.configPath
|
|
if path == "" {
|
|
root, err := config.RepoRoot(cmd.Context(), "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path = filepath.Join(root, config.Filename)
|
|
}
|
|
res, err := bootstrap.Init(cmd.Context(), &bootstrap.Options{
|
|
ConfigPath: path,
|
|
Server: f.server,
|
|
HumanToken: f.token,
|
|
HumanUsePassword: f.usePassword,
|
|
HumanUsername: f.username,
|
|
HumanPassword: f.password,
|
|
HumanTOTP: f.totp,
|
|
BotUsername: f.botUsername,
|
|
ProjectID: f.projectID,
|
|
ViewID: f.viewID,
|
|
AutoApproveBuckets: f.yesBuckets,
|
|
SkipBucketBootstrap: f.skipBuckets,
|
|
InstallClaudeCode: f.installClaude,
|
|
InstallOpenCode: f.installOpenCode,
|
|
ClaudeCodeFlagSet: cmd.Flags().Changed("install-claude"),
|
|
OpenCodeFlagSet: cmd.Flags().Changed("install-opencode"),
|
|
NoHooks: f.noHooks,
|
|
Out: os.Stderr,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printPostInitSummary(cmd.OutOrStdout(), res)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&f.server, "server", "", "Vikunja server URL")
|
|
cmd.Flags().StringVar(&f.token, "token", "", "JWT or personal API token (skips OAuth/password; useful for SSO/OIDC instances)")
|
|
cmd.Flags().BoolVar(&f.usePassword, "use-password", false, "use POST /login (username+password) instead of the default OAuth flow")
|
|
cmd.Flags().StringVar(&f.username, "username", "", "Vikunja username (implies --use-password)")
|
|
cmd.Flags().StringVar(&f.password, "password", "", "Vikunja password (implies --use-password; prompted if empty)")
|
|
cmd.Flags().StringVar(&f.totp, "totp", "", "TOTP code if your account requires 2FA")
|
|
cmd.Flags().StringVar(&f.botUsername, "bot-username", "", "override the bot-<repo> default")
|
|
cmd.Flags().Int64Var(&f.projectID, "project", 0, "skip the interactive project picker")
|
|
cmd.Flags().Int64Var(&f.viewID, "view", 0, "skip the interactive view picker")
|
|
cmd.Flags().BoolVar(&f.yesBuckets, "yes-buckets", false, "auto-approve canonical bucket bootstrap")
|
|
cmd.Flags().BoolVar(&f.skipBuckets, "skip-buckets", false, "do not prompt or create buckets (assumes they exist)")
|
|
cmd.Flags().StringVar(&f.configPath, "config", "", "where to write .veans.yml (defaults to the repo root)")
|
|
cmd.Flags().BoolVar(&f.installClaude, "install-claude", false, "wire `veans prime` into .claude/settings.json (skip prompt)")
|
|
cmd.Flags().BoolVar(&f.installOpenCode, "install-opencode", false, "wire `veans prime` into .opencode/plugin/veans-prime.ts (skip prompt)")
|
|
cmd.Flags().BoolVar(&f.noHooks, "no-hooks", false, "don't offer to install agent hooks; just print the snippets")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func printPostInitSummary(w io.Writer, res *bootstrap.Result) {
|
|
fmt.Fprintf(w, "\nveans is ready. Bot user: %s\n", res.BotUser.Username)
|
|
fmt.Fprintf(w, "Config: %s\n", res.Config.Path())
|
|
fmt.Fprintf(w, "Project: #%d %s\n", res.Config.ProjectID, identOrFallback(res.Config.ProjectIdentifier))
|
|
|
|
// Only fall back to printing the snippets when the user declined or
|
|
// skipped the install offer. When at least one hook was installed, the
|
|
// install routine already logged what it did to stderr.
|
|
if res.AgentChoices.ClaudeCode || res.AgentChoices.OpenCode {
|
|
return
|
|
}
|
|
// Snippets are sourced from the bootstrap package so manual installs
|
|
// stay byte-for-byte equivalent to what `installAgentHooks` would have
|
|
// written — if a new hook event is added there, it shows up here too.
|
|
fmt.Fprintf(w, `
|
|
To wire veans into your coding agent later, paste one of these snippets:
|
|
|
|
Claude Code (%s):
|
|
%s
|
|
|
|
OpenCode (%s):
|
|
%s`, bootstrap.ClaudeCodeSettingsRelPath, indent(bootstrap.ClaudeCodeHookSnippet(), " "),
|
|
bootstrap.OpenCodePluginRelPath, indent(bootstrap.OpenCodePluginSnippet, " "))
|
|
}
|
|
|
|
// indent prefixes every line of s with prefix. Used to inset the embedded
|
|
// snippets under their "Claude Code:" / "OpenCode:" headings without
|
|
// hard-coding the indent inside the bootstrap package's snippet strings.
|
|
func indent(s, prefix string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
|
|
for i, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
lines[i] = prefix + line
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func identOrFallback(s string) string {
|
|
if s == "" {
|
|
return "(no identifier — task IDs render as #NN)"
|
|
}
|
|
return s
|
|
}
|