feat: teai role
This commit is contained in:
+4
-4
@@ -75,9 +75,9 @@ type command struct {
|
||||
type Globals struct {
|
||||
// URL 是 Gitea 站點網址。
|
||||
URL string
|
||||
// Token 來自 --token 旗標;空字串表示未提供(依序退回環境變數與 tea 組態)。
|
||||
// Token 來自 --token 旗標;空字串表示未提供(依序退回環境變數與 teai 組態)。
|
||||
Token string
|
||||
// ConfigPath 是 tea 組態路徑(--config,等同 TEA_CONFIG)。
|
||||
// ConfigPath 是 teai 組態路徑(--config,等同 TEAI_CONFIG)。
|
||||
ConfigPath string
|
||||
// Output 是輸出格式。
|
||||
Output gitea.Format
|
||||
@@ -464,8 +464,8 @@ func printUsage(w io.Writer) {
|
||||
fmt.Fprintf(w, "用法:\n teai [全域選項] <命令> [參數]\n\n")
|
||||
fmt.Fprintf(w, "全域選項:\n")
|
||||
fmt.Fprintf(w, " --url <URL> Gitea 站點(預設 https://gitea.alterminal.com)\n")
|
||||
fmt.Fprintf(w, " --token <TOKEN> API token;未給則依序嘗試 TEAI_TOKEN、tea 登入組態\n")
|
||||
fmt.Fprintf(w, " --config <path> tea 組態檔路徑,等同 TEA_CONFIG\n")
|
||||
fmt.Fprintf(w, " --token <TOKEN> API token;未給則依序嘗試 TEAI_TOKEN、teai 登入組態\n")
|
||||
fmt.Fprintf(w, " --config <path> teai 組態檔路徑,等同 TEAI_CONFIG\n")
|
||||
fmt.Fprintf(w, " --output <format> 輸出格式 json|table(預設 json)\n")
|
||||
fmt.Fprintf(w, " --timeout <dur> HTTP 逾時(預設 30s)\n\n")
|
||||
fmt.Fprintf(w, "命令:\n")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// login.go — teai login 指令:管理 tea 相容的登入組態。
|
||||
// login.go — teai login 指令:管理 teai 自己的登入組態。
|
||||
//
|
||||
// teai 與 tea 共用同一份組態檔(README 設計原則 5),login 的職責是:
|
||||
// 組態由 teai 自行讀寫(README 設計原則 5),不依賴 tea。login 的職責是:
|
||||
// 以 token 向站點驗證(GET /user),成功後才寫入組態並記下帳號。
|
||||
// token 只走記憶體與組態檔,不進輸出、日誌或錯誤訊息。
|
||||
//
|
||||
@@ -25,20 +25,20 @@ import (
|
||||
// loginStdin 是讀 token 與確認的輸入(測試可替換)。
|
||||
var loginStdin io.Reader = os.Stdin
|
||||
|
||||
// loginConfigPath 回傳組態檔路徑:--config/TEA_CONFIG 優先,其次
|
||||
// ~/.config/tea/config.yml。找不到可用路徑回空字串。
|
||||
// loginConfigPath 回傳組態檔路徑:--config/TEAI_CONFIG 優先,其次
|
||||
// ~/.config/teai/config.yml。找不到可用路徑回空字串。
|
||||
func loginConfigPath(g Globals) string {
|
||||
if p := strings.TrimSpace(g.ConfigPath); p != "" {
|
||||
return expandHome(p)
|
||||
}
|
||||
if p := os.Getenv("TEA_CONFIG"); strings.TrimSpace(p) != "" {
|
||||
if p := os.Getenv("TEAI_CONFIG"); strings.TrimSpace(p) != "" {
|
||||
return expandHome(strings.TrimSpace(p))
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".config", "tea", "config.yml")
|
||||
return filepath.Join(home, ".config", "teai", "config.yml")
|
||||
}
|
||||
|
||||
// expandHome 展開路徑開頭的 ~。
|
||||
@@ -178,7 +178,7 @@ func runLoginAdd(env *Env, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gitea.SaveTeaConfigFile(path, out); err != nil {
|
||||
if err := gitea.SaveConfigFile(path, out); err != nil {
|
||||
return err
|
||||
}
|
||||
p := gitea.Printer{Format: env.Globals.Output, Out: env.Out}
|
||||
@@ -329,7 +329,7 @@ func saveLoginConfig(env *Env, data []byte) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("cannot determine config path")
|
||||
}
|
||||
return gitea.SaveTeaConfigFile(path, data)
|
||||
return gitea.SaveConfigFile(path, data)
|
||||
}
|
||||
|
||||
// findLoginByHost 在組態內容中找 URL 主機與 siteURL 相同的登入。
|
||||
@@ -352,7 +352,7 @@ func findLogin(data []byte, name string) (gitea.LoginEntry, bool) {
|
||||
return gitea.LoginEntry{}, false
|
||||
}
|
||||
|
||||
// defaultLoginName 以 URL 主機作為登入名稱(同 tea 的習慣)。
|
||||
// defaultLoginName 以 URL 主機作為登入名稱。
|
||||
func defaultLoginName(rawURL string) string {
|
||||
s := rawURL
|
||||
if !strings.Contains(s, "://") {
|
||||
|
||||
@@ -25,7 +25,7 @@ func loginTestEnv(t *testing.T, configContent string) (args []string, configPath
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Setenv("TEA_CONFIG", configPath)
|
||||
t.Setenv("TEAI_CONFIG", configPath)
|
||||
return []string{"--config", configPath}, configPath
|
||||
}
|
||||
|
||||
@@ -117,6 +117,13 @@ func TestLoginAddVerifiesThenWrites(t *testing.T) {
|
||||
if info.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("perm = %v", info.Mode().Perm())
|
||||
}
|
||||
// 新項目只寫 teai 欄位,不含 tea 的 ssh_*/preferences。
|
||||
s := string(data)
|
||||
for _, ban := range []string{"ssh_", "insecure:", "version_check:", "preferences:"} {
|
||||
if strings.Contains(s, ban) {
|
||||
t.Fatalf("tea field leaked %q:\n%s", ban, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginAddTokenFromStdin(t *testing.T) {
|
||||
@@ -187,6 +194,7 @@ preferences:
|
||||
if strings.Contains(s, "old-tok") || !strings.Contains(s, "token: new-tok") {
|
||||
t.Fatalf("token not replaced:\n%s", s)
|
||||
}
|
||||
// 未知欄位與其他區段逐字保留(行級編輯,不整檔覆寫)。
|
||||
if !strings.Contains(s, `ssh_key: "keep-me"`) || !strings.Contains(s, "editor: false") {
|
||||
t.Fatalf("unrelated content lost:\n%s", s)
|
||||
}
|
||||
@@ -290,3 +298,26 @@ func TestLoginRegisteredInUsage(t *testing.T) {
|
||||
t.Fatalf("usage should list login: %s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginConfigPathIndependentOfTea(t *testing.T) {
|
||||
t.Setenv("TEA_CONFIG", "/from-tea.yml")
|
||||
t.Setenv("TEAI_CONFIG", "")
|
||||
g := defaultGlobals()
|
||||
got := loginConfigPath(g)
|
||||
if got == "/from-tea.yml" {
|
||||
t.Fatal("TEA_CONFIG must not be used")
|
||||
}
|
||||
if !strings.HasSuffix(filepath.ToSlash(got), ".config/teai/config.yml") {
|
||||
t.Fatalf("default path want ~/.config/teai/config.yml, got %q", got)
|
||||
}
|
||||
|
||||
t.Setenv("TEAI_CONFIG", "/from-teai.yml")
|
||||
if got := loginConfigPath(g); got != "/from-teai.yml" {
|
||||
t.Fatalf("TEAI_CONFIG: got %q", got)
|
||||
}
|
||||
|
||||
g.ConfigPath = "/explicit.yml"
|
||||
if got := loginConfigPath(g); got != "/explicit.yml" {
|
||||
t.Fatalf("--config: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// workflow_commands.go — gitea.py 工作流命令的 CLI 接線(#6):
|
||||
// whoami、orgs、members [--has-work]、mine、pulls [--mine|--reviewer] [--repo]、next。
|
||||
// whoami、role、orgs、members [--has-work]、mine、pulls [--mine|--reviewer] [--repo]、next。
|
||||
//
|
||||
// 輸出走 internal/gitea 的 Printer(--output json|table);錯誤經 cli.exitCodeFor
|
||||
// 映射結束碼(用法 2/API 3)。token 解析選序見 internal/gitea/auth.go。
|
||||
@@ -20,6 +20,7 @@ func init() {
|
||||
commands[c.name] = c
|
||||
}
|
||||
register(&command{name: "whoami", usage: "目前帳號", run: runWhoami})
|
||||
register(&command{name: "role", usage: "目前角色(Supervisor→supervisor,Agents→worker)", run: runRole})
|
||||
register(&command{name: "orgs", usage: "我所屬的組織", run: runOrgs})
|
||||
register(&command{name: "members", usage: "Agents 團隊成員;--has-work 只列有未完成工作者", run: runMembers})
|
||||
register(&command{name: "mine", usage: "分派給我、待跟進的 issues", run: runMine})
|
||||
@@ -77,6 +78,29 @@ func runWhoami(env *Env, args []string) error {
|
||||
)
|
||||
}
|
||||
|
||||
// runRole:teai role。不在 Supervisor/Agents 團隊時輸出 null(exit 0)。
|
||||
func runRole(env *Env, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return &ErrUsage{Msg: "role 不接受參數"}
|
||||
}
|
||||
s, err := newScannerFunc(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
role, err := s.Role()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer(env)
|
||||
if role == "" {
|
||||
return p.PrintNull()
|
||||
}
|
||||
return p.PrintOne(
|
||||
[]gitea.Field{{Name: "role"}},
|
||||
gitea.Values{role},
|
||||
)
|
||||
}
|
||||
|
||||
// runOrgs:teai orgs。
|
||||
func runOrgs(env *Env, args []string) error {
|
||||
if len(args) != 0 {
|
||||
|
||||
@@ -224,6 +224,71 @@ func TestWiringWhoamiOrgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWiringRoleWorker 驗證 Agents 成員的 role 輸出 worker。
|
||||
func TestWiringRoleWorker(t *testing.T) {
|
||||
newStubEnv(t, newStubData())
|
||||
stdout, _, code := run("role")
|
||||
if code != 0 {
|
||||
t.Fatalf("role 應回 0,得到 %d(stdout=%q)", code, stdout)
|
||||
}
|
||||
if strings.TrimSpace(stdout) != `{"role":"worker"}` {
|
||||
t.Fatalf("Agents 成員 role 應輸出 worker,得到 %q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWiringRoleSupervisor 驗證 Supervisor 成員的 role 輸出 supervisor。
|
||||
func TestWiringRoleSupervisor(t *testing.T) {
|
||||
c := newStubData()
|
||||
c.teams["alterminal"] = []workflow.Team{
|
||||
{ID: 1, Name: "Agents"},
|
||||
{ID: 2, Name: "Supervisor"},
|
||||
}
|
||||
c.members[2] = []string{"alex"}
|
||||
newStubEnv(t, c)
|
||||
stdout, _, code := run("role")
|
||||
if code != 0 {
|
||||
t.Fatalf("role 應回 0,得到 %d(stdout=%q)", code, stdout)
|
||||
}
|
||||
if strings.TrimSpace(stdout) != `{"role":"supervisor"}` {
|
||||
t.Fatalf("Supervisor 成員 role 應輸出 supervisor,得到 %q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWiringRoleNone 驗證不在任一角色團隊時輸出 null、exit 0。
|
||||
func TestWiringRoleNone(t *testing.T) {
|
||||
c := newStubData()
|
||||
c.currentUser = "outsider"
|
||||
newStubEnv(t, c)
|
||||
stdout, _, code := run("role")
|
||||
if code != 0 {
|
||||
t.Fatalf("role 無角色應回 0,得到 %d", code)
|
||||
}
|
||||
if strings.TrimSpace(stdout) != "null" {
|
||||
t.Fatalf("role 無角色應輸出 null,得到 %q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWiringRoleRejectsArgs 驗證 role 不接受參數。
|
||||
func TestWiringRoleRejectsArgs(t *testing.T) {
|
||||
newStubEnv(t, newStubData())
|
||||
_, _, code := run("role", "extra")
|
||||
if code != int(ExitUsage) {
|
||||
t.Fatalf("role extra 應回 ExitUsage(%d),得到 %d", int(ExitUsage), code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWiringRoleTable 驗證 teai role --output table。
|
||||
func TestWiringRoleTable(t *testing.T) {
|
||||
newStubEnv(t, newStubData())
|
||||
stdout, _, code := run("role", "--output", "table")
|
||||
if code != 0 {
|
||||
t.Fatalf("role --output table 應回 0,得到 %d", code)
|
||||
}
|
||||
if !strings.Contains(stdout, "worker") {
|
||||
t.Fatalf("role --output table 應含 worker,得到 %q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWiringVersionRejectsArgs 驗證 version 不接受參數(新剖析語義下仍 exit 2)。
|
||||
func TestWiringVersionRejectsArgs(t *testing.T) {
|
||||
_, _, code := run("version", "--wat")
|
||||
|
||||
Reference in New Issue
Block a user