@@ -1473,6 +1473,37 @@ var remoteDelCmd = &cobra.Command{
14731473 RunE: runRemoteDel,
14741474}
14751475
1476+ // ── Org management ─────────────────────────────────────────────────
1477+
1478+ var orgCmd = &cobra.Command{
1479+ Use: "org",
1480+ Short: "Manage organizations on the remote server",
1481+ Long: `Inspect and administer organizations on a remote Kailab server
1482+ (kaicontext.com by default). Requires an authenticated session
1483+ (run 'kai auth login' first).`,
1484+ }
1485+
1486+ var orgListCmd = &cobra.Command{
1487+ Use: "list",
1488+ Short: "List organizations you belong to",
1489+ RunE: runOrgList,
1490+ }
1491+
1492+ var orgDeleteYes bool
1493+
1494+ var orgDeleteCmd = &cobra.Command{
1495+ Use: "delete <slug>",
1496+ Short: "Delete an organization (hard delete, destructive)",
1497+ Long: `Hard-deletes an organization, every repo inside it, and all
1498+ dependent data (snapshots, refs, CI runs, webhooks, secrets, variables,
1499+ billing, memberships). Irreversible.
1500+
1501+ Only the org owner can delete an org. The CLI requires you to type the
1502+ slug to confirm; --yes skips the prompt (for scripts).`,
1503+ Args: cobra.ExactArgs(1),
1504+ RunE: runOrgDelete,
1505+ }
1506+
14761507var pushCmd = &cobra.Command{
14771508 Use: "push [remote] [target...]",
14781509 Short: "Push snapshots, changesets, and reviews to a remote server",
@@ -3361,6 +3392,10 @@ func init() {
33613392 remoteCmd.AddCommand(remoteListCmd)
33623393 remoteCmd.AddCommand(remoteDelCmd)
33633394
3395+ orgDeleteCmd.Flags().BoolVarP(&orgDeleteYes, "yes", "y", false, "Skip confirmation prompt (dangerous)")
3396+ orgCmd.AddCommand(orgListCmd)
3397+ orgCmd.AddCommand(orgDeleteCmd)
3398+
33643399 // Add ref subcommands
33653400 refCmd.AddCommand(refListCmd)
33663401 refCmd.AddCommand(refSetCmd)
@@ -3627,6 +3662,8 @@ func init() {
36273662 authCmd.GroupID = groupRemote
36283663 updateCmd.GroupID = groupRemote
36293664 rootCmd.AddCommand(remoteCmd)
3665+ orgCmd.GroupID = groupRemote
3666+ rootCmd.AddCommand(orgCmd)
36303667 rootCmd.AddCommand(pushCmd)
36313668 rootCmd.AddCommand(fetchCmd)
36323669 rootCmd.AddCommand(pullCmd)
@@ -13757,6 +13794,86 @@ func runRemoteDel(cmd *cobra.Command, args []string) error {
1375713794 return nil
1375813795}
1375913796
13797+ // ── kai org list / delete ──────────────────────────────────────────
13798+
13799+ // newControlClientAuthed returns a ControlClient pointed at KAI_SERVER
13800+ // (default kaicontext.com) with the current user's access token wired
13801+ // in. Returns a friendly error if the user is not logged in.
13802+ func newControlClientAuthed() (*remote.ControlClient, error) {
13803+ serverURL := os.Getenv("KAI_SERVER")
13804+ if serverURL == "" {
13805+ serverURL = remote.DefaultServer
13806+ }
13807+ token, err := remote.GetValidAccessToken()
13808+ if err != nil || token == "" {
13809+ return nil, fmt.Errorf("not logged in — run 'kai auth login' first")
13810+ }
13811+ c := remote.NewControlClient(serverURL)
13812+ c.AuthToken = token
13813+ return c, nil
13814+ }
13815+
13816+ func runOrgList(cmd *cobra.Command, args []string) error {
13817+ c, err := newControlClientAuthed()
13818+ if err != nil {
13819+ return err
13820+ }
13821+ orgs, err := c.ListOrgs()
13822+ if err != nil {
13823+ return fmt.Errorf("listing orgs: %w", err)
13824+ }
13825+ if len(orgs) == 0 {
13826+ fmt.Println("You don't belong to any organizations.")
13827+ return nil
13828+ }
13829+ for _, o := range orgs {
13830+ fmt.Printf(" %-20s %s\n", o.Slug, o.Name)
13831+ }
13832+ return nil
13833+ }
13834+
13835+ func runOrgDelete(cmd *cobra.Command, args []string) error {
13836+ slug := args[0]
13837+ c, err := newControlClientAuthed()
13838+ if err != nil {
13839+ return err
13840+ }
13841+
13842+ if !orgDeleteYes {
13843+ // List repos first so the user sees the blast radius.
13844+ repos, err := c.ListRepos(slug)
13845+ if err != nil {
13846+ // A 403/404 here usually means the user isn't a member;
13847+ // we still want to try the delete for the owner-only case,
13848+ // so just warn and continue to the confirmation prompt.
13849+ fmt.Fprintf(os.Stderr, "Note: couldn't list repos (%v); proceeding.\n\n", err)
13850+ } else {
13851+ fmt.Printf("About to delete organization %q and %d repo(s):\n", slug, len(repos))
13852+ for _, r := range repos {
13853+ fmt.Printf(" - %s/%s\n", slug, r.Name)
13854+ }
13855+ fmt.Println()
13856+ fmt.Println("This is irreversible. Every snapshot, ref, CI run, webhook,")
13857+ fmt.Println("secret, variable, and membership will be permanently removed.")
13858+ fmt.Println()
13859+ }
13860+ fmt.Printf("Type the org slug (%s) to confirm: ", slug)
13861+ reader := bufio.NewReader(os.Stdin)
13862+ typed, _ := reader.ReadString('\n')
13863+ typed = strings.TrimSpace(typed)
13864+ if typed != slug {
13865+ return fmt.Errorf("confirmation did not match; org NOT deleted")
13866+ }
13867+ }
13868+
13869+ reposDeleted, err := c.DeleteOrg(slug)
13870+ if err != nil {
13871+ return fmt.Errorf("deleting org: %w", err)
13872+ }
13873+ fmt.Printf("✓ Deleted organization %q (%d repo(s))\n", slug, reposDeleted)
13874+ return nil
13875+ }
13876+
1376013877// interactivePushOnboarding handles the case when a user tries to push without a remote configured.
1376113878// It walks them through authentication, org selection, repo creation, and remote setup.
1376213879func interactivePushOnboarding(remoteName string) (*remote.Client, error) {
0 commit comments