hap-cli 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. hap_cli/README.md +194 -0
  2. hap_cli/README_CN.md +601 -0
  3. hap_cli/__init__.py +3 -0
  4. hap_cli/commands/__init__.py +1 -0
  5. hap_cli/commands/ai_cmd.py +224 -0
  6. hap_cli/commands/app_cmd.py +308 -0
  7. hap_cli/commands/calendar_cmd.py +138 -0
  8. hap_cli/commands/chat_cmd.py +101 -0
  9. hap_cli/commands/config_cmd.py +169 -0
  10. hap_cli/commands/contact_cmd.py +125 -0
  11. hap_cli/commands/department_cmd.py +168 -0
  12. hap_cli/commands/group_cmd.py +128 -0
  13. hap_cli/commands/instance_cmd.py +310 -0
  14. hap_cli/commands/node_cmd.py +538 -0
  15. hap_cli/commands/optionset_cmd.py +99 -0
  16. hap_cli/commands/page_cmd.py +102 -0
  17. hap_cli/commands/plugin_cmd.py +133 -0
  18. hap_cli/commands/post_cmd.py +155 -0
  19. hap_cli/commands/record_cmd.py +228 -0
  20. hap_cli/commands/role_cmd.py +221 -0
  21. hap_cli/commands/workflow_cmd.py +284 -0
  22. hap_cli/commands/worksheet_cmd.py +342 -0
  23. hap_cli/context.py +43 -0
  24. hap_cli/core/__init__.py +1 -0
  25. hap_cli/core/ai.py +133 -0
  26. hap_cli/core/app.py +307 -0
  27. hap_cli/core/auth.py +219 -0
  28. hap_cli/core/calendar_mod.py +114 -0
  29. hap_cli/core/chat.py +73 -0
  30. hap_cli/core/contact.py +85 -0
  31. hap_cli/core/department.py +131 -0
  32. hap_cli/core/flow_node.py +1001 -0
  33. hap_cli/core/group.py +99 -0
  34. hap_cli/core/instance.py +572 -0
  35. hap_cli/core/optionset.py +112 -0
  36. hap_cli/core/page.py +138 -0
  37. hap_cli/core/plugin.py +87 -0
  38. hap_cli/core/post.py +118 -0
  39. hap_cli/core/record.py +268 -0
  40. hap_cli/core/role.py +227 -0
  41. hap_cli/core/session.py +348 -0
  42. hap_cli/core/workflow.py +556 -0
  43. hap_cli/core/worksheet.py +403 -0
  44. hap_cli/hap_cli.py +105 -0
  45. hap_cli/skills/SKILL.md +383 -0
  46. hap_cli/skills/__init__.py +0 -0
  47. hap_cli/tests/__init__.py +1 -0
  48. hap_cli/tests/test_core.py +1824 -0
  49. hap_cli/tests/test_full_e2e.py +136 -0
  50. hap_cli/tests/test_integration.py +805 -0
  51. hap_cli/utils/__init__.py +1 -0
  52. hap_cli/utils/formatting.py +111 -0
  53. hap_cli/utils/options.py +10 -0
  54. hap_cli-0.5.0.dist-info/METADATA +223 -0
  55. hap_cli-0.5.0.dist-info/RECORD +58 -0
  56. hap_cli-0.5.0.dist-info/WHEEL +5 -0
  57. hap_cli-0.5.0.dist-info/entry_points.txt +2 -0
  58. hap_cli-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,101 @@
1
+ """CLI commands for chat and messaging management."""
2
+
3
+ import json
4
+
5
+ import click
6
+
7
+ from hap_cli.context import pass_context
8
+ from hap_cli.core import chat as chat_mod
9
+ from hap_cli.utils.formatting import output_json, output_kv
10
+
11
+
12
+ @click.group()
13
+ def chat():
14
+ """Chat and messaging management."""
15
+ pass
16
+
17
+
18
+ @chat.command("list")
19
+ @click.option("--page-size", "-n", default=20, help="Items per page")
20
+ @click.option("--page", "-p", default=1, help="Page number")
21
+ @pass_context
22
+ def chat_list(ctx, page_size, page):
23
+ """List recent chat sessions."""
24
+ try:
25
+ session = ctx.get_session()
26
+ data = chat_mod.get_chat_list(session, page_index=page, page_size=page_size)
27
+ ctx.output(data, lambda d: output_json(d))
28
+ except Exception as e:
29
+ ctx.handle_error(e)
30
+
31
+
32
+ @chat.command("send")
33
+ @click.option("--to", "-t", multiple=True, required=True, help="Target user account ID (can repeat)")
34
+ @click.option("--message", "-m", required=True, help="Message content")
35
+ @pass_context
36
+ def chat_send(ctx, to, message):
37
+ """Send a message to one or more users."""
38
+ try:
39
+ session = ctx.get_session()
40
+ data = chat_mod.send_message(session, list(to), message)
41
+ ctx.output(data, lambda d: click.echo("Message sent."))
42
+ except Exception as e:
43
+ ctx.handle_error(e)
44
+
45
+
46
+ @chat.command("send-file")
47
+ @click.argument("chat_id")
48
+ @click.option("--file-url", required=True, help="File URL to send")
49
+ @pass_context
50
+ def chat_send_file(ctx, chat_id, file_url):
51
+ """Send a file to a chat."""
52
+ try:
53
+ session = ctx.get_session()
54
+ data = chat_mod.send_file(session, chat_id, file_url)
55
+ ctx.output(data, lambda d: click.echo("File sent."))
56
+ except Exception as e:
57
+ ctx.handle_error(e)
58
+
59
+
60
+ @chat.command("send-card")
61
+ @click.argument("chat_id")
62
+ @click.option("--data", "card_data", required=True, help="Card data as JSON string")
63
+ @pass_context
64
+ def chat_send_card(ctx, chat_id, card_data):
65
+ """Send a card to a chat."""
66
+ try:
67
+ session = ctx.get_session()
68
+ data = chat_mod.send_card(session, chat_id, json.loads(card_data))
69
+ ctx.output(data, lambda d: click.echo("Card sent."))
70
+ except json.JSONDecodeError as e:
71
+ ctx.handle_error(click.UsageError(f"Invalid JSON: {e}"))
72
+ except Exception as e:
73
+ ctx.handle_error(e)
74
+
75
+
76
+ @chat.command("group-info")
77
+ @click.argument("group_id")
78
+ @pass_context
79
+ def chat_group_info(ctx, group_id):
80
+ """Get chat group information."""
81
+ try:
82
+ session = ctx.get_session()
83
+ data = chat_mod.get_group_info(session, group_id)
84
+ ctx.output(data, lambda d: output_kv(d))
85
+ except Exception as e:
86
+ ctx.handle_error(e)
87
+
88
+
89
+ @chat.command("group-files")
90
+ @click.argument("group_id")
91
+ @click.option("--page-size", "-n", default=20, help="Items per page")
92
+ @click.option("--page", "-p", default=1, help="Page number")
93
+ @pass_context
94
+ def chat_group_files(ctx, group_id, page_size, page):
95
+ """List files in a chat group."""
96
+ try:
97
+ session = ctx.get_session()
98
+ data = chat_mod.get_group_files(session, group_id, page_index=page, page_size=page_size)
99
+ ctx.output(data, lambda d: output_json(d))
100
+ except Exception as e:
101
+ ctx.handle_error(e)
@@ -0,0 +1,169 @@
1
+ """CLI commands for server configuration and authentication."""
2
+
3
+ import click
4
+
5
+ from hap_cli.context import pass_context
6
+ from hap_cli.core.session import Session, SessionError
7
+ from hap_cli.core import auth as auth_mod
8
+ from hap_cli.utils.formatting import output_json, output_kv
9
+
10
+
11
+ SERVER_HELP = (
12
+ "Server: 'mingdao', 'nocoly', or self-hosted URL "
13
+ "(e.g. https://hap.example.com)"
14
+ )
15
+
16
+
17
+ @click.group()
18
+ def config():
19
+ """Manage server connection and authentication."""
20
+ pass
21
+
22
+
23
+ @config.command("set")
24
+ @click.option("--server", "-s", required=True, help="MingDAO server URL")
25
+ @click.option("--token", "-t", required=True, help="Authentication token (md_pss_id)")
26
+ @click.option("--app-id", "-a", default="", help="Default application ID")
27
+ @click.option("--project-id", "-p", default="", help="Default project/org ID")
28
+ @pass_context
29
+ def config_set(ctx, server, token, app_id, project_id):
30
+ """Set server URL and authentication token."""
31
+ session = Session(server_url=server, auth_token=token)
32
+ session.default_app_id = app_id
33
+ session.default_project_id = project_id
34
+ session.save()
35
+ ctx.output(
36
+ {"status": "ok", "server": server},
37
+ lambda d: click.echo(f"Config saved. Server: {server}"),
38
+ )
39
+
40
+
41
+ @config.command("show")
42
+ @pass_context
43
+ def config_show(ctx):
44
+ """Show current configuration."""
45
+ session = ctx.get_session()
46
+ data = {
47
+ "server_url": session.server_url,
48
+ "auth_token": session.auth_token[:8] + "..." if session.auth_token else "",
49
+ "default_app_id": session.default_app_id,
50
+ "default_project_id": session.default_project_id,
51
+ "configured": session.is_configured(),
52
+ }
53
+ ctx.output(
54
+ data,
55
+ lambda d: output_kv(d, labels={
56
+ "server_url": "Server URL",
57
+ "auth_token": "Auth Token",
58
+ "default_app_id": "Default App ID",
59
+ "default_project_id": "Default Project ID",
60
+ "configured": "Configured",
61
+ }),
62
+ )
63
+
64
+
65
+ @config.command("login")
66
+ @click.argument("server", default="mingdao")
67
+ @click.option("--timeout", "-t", default=300, help="Login timeout in seconds")
68
+ @pass_context
69
+ def config_login(ctx, server, timeout):
70
+ """Login via browser. Opens HAP login page and captures token.
71
+
72
+ SERVER can be 'mingdao', 'nocoly', or a self-hosted URL.
73
+ Default is 'mingdao'.
74
+ """
75
+ try:
76
+ api_host, webui = auth_mod.resolve_server(server)
77
+ port = auth_mod.get_available_port()
78
+ auth_url = auth_mod.build_auth_url(webui, port)
79
+
80
+ if not ctx.json_mode:
81
+ click.echo(f"Opening browser for login...")
82
+ click.echo(f" Server: {webui}")
83
+ click.echo(f" If browser doesn't open, visit:")
84
+ click.echo(f" {auth_url}")
85
+ click.echo(f" Waiting for login (timeout: {timeout}s)...")
86
+
87
+ token, api_host, user_info = auth_mod.login(server, timeout=timeout)
88
+
89
+ # Save to config
90
+ session = ctx.get_session()
91
+ session.server_url = api_host
92
+ session.auth_token = token
93
+ session.save()
94
+
95
+ data = {
96
+ "status": "ok",
97
+ "server_url": api_host,
98
+ "user": user_info,
99
+ }
100
+ ctx.output(
101
+ data,
102
+ lambda d: click.echo(
103
+ f"Logged in as: {user_info.get('name', '')} "
104
+ f"({user_info.get('email', '')})\n"
105
+ f"Server: {api_host}"
106
+ ),
107
+ )
108
+ except TimeoutError:
109
+ ctx.handle_error(SessionError("Login timed out. Please try again."))
110
+ except ImportError as e:
111
+ ctx.handle_error(SessionError(str(e)))
112
+ except Exception as e:
113
+ ctx.handle_error(e)
114
+
115
+
116
+ @config.command("logout")
117
+ @pass_context
118
+ def config_logout(ctx):
119
+ """Clear saved authentication token."""
120
+ session = ctx.get_session()
121
+ old_server = session.server_url
122
+ session.auth_token = ""
123
+ session.save()
124
+ ctx.output(
125
+ {"status": "ok", "server_url": old_server},
126
+ lambda d: click.echo(f"Logged out. Token cleared."),
127
+ )
128
+
129
+
130
+ @config.command("orgs")
131
+ @pass_context
132
+ def config_orgs(ctx):
133
+ """List all organizations the current user belongs to."""
134
+ try:
135
+ session = ctx.get_session()
136
+ data = session.api_call("Account", "GetProjectList", {})
137
+ def _table(d):
138
+ projects = d if isinstance(d, list) else d.get("list", [d])
139
+ for p in projects:
140
+ pid = p.get("projectId", "")
141
+ name = p.get("companyName", p.get("name", ""))
142
+ role = p.get("roleName", "")
143
+ click.echo(f"{pid} {name} {role}")
144
+ ctx.output(data, _table)
145
+ except Exception as e:
146
+ ctx.handle_error(e)
147
+
148
+
149
+ @config.command("whoami")
150
+ @pass_context
151
+ def config_whoami(ctx):
152
+ """Show current logged-in user info."""
153
+ try:
154
+ session = ctx.get_session()
155
+ if not session.is_configured():
156
+ raise SessionError("Not logged in. Run 'config login' first.")
157
+
158
+ user_info = auth_mod.get_user_info(session.server_url, session.auth_token)
159
+ ctx.output(
160
+ user_info,
161
+ lambda d: output_kv(d, labels={
162
+ "id": "Account ID",
163
+ "name": "Name",
164
+ "email": "Email",
165
+ "lang": "Language",
166
+ }),
167
+ )
168
+ except Exception as e:
169
+ ctx.handle_error(e)
@@ -0,0 +1,125 @@
1
+ """CLI commands for address book and contact management."""
2
+
3
+ import click
4
+
5
+ from hap_cli.context import pass_context
6
+ from hap_cli.core import contact as contact_mod
7
+ from hap_cli.utils.formatting import output_json, output_table, output_kv
8
+
9
+
10
+ @click.group()
11
+ def contact():
12
+ """Address book and contact management."""
13
+ pass
14
+
15
+
16
+ @contact.command("search")
17
+ @click.argument("keywords")
18
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
19
+ @pass_context
20
+ def contact_search(ctx, keywords, project_id):
21
+ """Search contacts and departments."""
22
+ try:
23
+ session = ctx.get_session()
24
+ pid = project_id or session.default_project_id
25
+ data = contact_mod.search_contacts(session, keywords, project_id=pid)
26
+ ctx.output(data, lambda d: output_json(d))
27
+ except Exception as e:
28
+ ctx.handle_error(e)
29
+
30
+
31
+ @contact.command("get")
32
+ @click.argument("account_id")
33
+ @pass_context
34
+ def contact_get(ctx, account_id):
35
+ """Get user account details."""
36
+ try:
37
+ session = ctx.get_session()
38
+ data = contact_mod.get_account_detail(session, account_id)
39
+ ctx.output(data, lambda d: output_kv(d))
40
+ except Exception as e:
41
+ ctx.handle_error(e)
42
+
43
+
44
+ @contact.command("friends")
45
+ @click.option("--page-size", "-n", default=20, help="Items per page")
46
+ @click.option("--page", "-p", default=1, help="Page number")
47
+ @pass_context
48
+ def contact_friends(ctx, page_size, page):
49
+ """List friends/contacts in address book."""
50
+ try:
51
+ session = ctx.get_session()
52
+ data = contact_mod.get_friends(session, page_index=page, page_size=page_size)
53
+ ctx.output(data, lambda d: output_json(d))
54
+ except Exception as e:
55
+ ctx.handle_error(e)
56
+
57
+
58
+ @contact.command("add-friend")
59
+ @click.argument("account_id")
60
+ @click.option("--message", "-m", default="", help="Request message")
61
+ @pass_context
62
+ def contact_add_friend(ctx, account_id, message):
63
+ """Send a friend request."""
64
+ try:
65
+ session = ctx.get_session()
66
+ data = contact_mod.add_friend(session, account_id, message=message)
67
+ ctx.output(data, lambda d: click.echo("Friend request sent."))
68
+ except Exception as e:
69
+ ctx.handle_error(e)
70
+
71
+
72
+ @contact.command("remove-friend")
73
+ @click.argument("account_id")
74
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
75
+ @pass_context
76
+ def contact_remove_friend(ctx, account_id, yes):
77
+ """Remove a friend from address book."""
78
+ try:
79
+ if not yes:
80
+ click.confirm(f"Remove friend {account_id}?", abort=True)
81
+ session = ctx.get_session()
82
+ data = contact_mod.remove_friend(session, account_id)
83
+ ctx.output(data, lambda d: click.echo("Friend removed."))
84
+ except Exception as e:
85
+ ctx.handle_error(e)
86
+
87
+
88
+ @contact.command("friend-requests")
89
+ @click.option("--page-size", "-n", default=20, help="Items per page")
90
+ @click.option("--page", "-p", default=1, help="Page number")
91
+ @pass_context
92
+ def contact_friend_requests(ctx, page_size, page):
93
+ """List pending friend requests."""
94
+ try:
95
+ session = ctx.get_session()
96
+ data = contact_mod.get_friend_requests(session, page_index=page, page_size=page_size)
97
+ ctx.output(data, lambda d: output_json(d))
98
+ except Exception as e:
99
+ ctx.handle_error(e)
100
+
101
+
102
+ @contact.command("accept-friend")
103
+ @click.argument("account_id")
104
+ @pass_context
105
+ def contact_accept_friend(ctx, account_id):
106
+ """Accept a friend request."""
107
+ try:
108
+ session = ctx.get_session()
109
+ data = contact_mod.accept_friend(session, account_id)
110
+ ctx.output(data, lambda d: click.echo("Friend request accepted."))
111
+ except Exception as e:
112
+ ctx.handle_error(e)
113
+
114
+
115
+ @contact.command("reject-friend")
116
+ @click.argument("account_id")
117
+ @pass_context
118
+ def contact_reject_friend(ctx, account_id):
119
+ """Reject a friend request."""
120
+ try:
121
+ session = ctx.get_session()
122
+ data = contact_mod.reject_friend(session, account_id)
123
+ ctx.output(data, lambda d: click.echo("Friend request rejected."))
124
+ except Exception as e:
125
+ ctx.handle_error(e)
@@ -0,0 +1,168 @@
1
+ """CLI commands for department management."""
2
+
3
+ import click
4
+
5
+ from hap_cli.context import pass_context
6
+ from hap_cli.core import department as dept_mod
7
+ from hap_cli.utils.formatting import output_json, output_table, output_kv
8
+
9
+
10
+ @click.group()
11
+ def department():
12
+ """Department management."""
13
+ pass
14
+
15
+
16
+ @department.command("list")
17
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
18
+ @click.option("--parent-id", default="", help="Parent department ID")
19
+ @click.option("--page-size", "-n", default=50, help="Items per page")
20
+ @click.option("--page", default=1, help="Page number")
21
+ @pass_context
22
+ def department_list(ctx, project_id, parent_id, page_size, page):
23
+ """List sub-departments."""
24
+ try:
25
+ session = ctx.get_session()
26
+ pid = project_id or session.default_project_id
27
+ if not pid:
28
+ raise click.UsageError("Project ID required.")
29
+ data = dept_mod.list_departments(session, pid, parent_id=parent_id, page_index=page, page_size=page_size)
30
+ ctx.output(data, lambda d: output_json(d))
31
+ except Exception as e:
32
+ ctx.handle_error(e)
33
+
34
+
35
+ @department.command("tree")
36
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
37
+ @click.option("--department-id", "-d", default="", help="Root department ID")
38
+ @pass_context
39
+ def department_tree(ctx, project_id, department_id):
40
+ """Get the full department tree."""
41
+ try:
42
+ session = ctx.get_session()
43
+ pid = project_id or session.default_project_id
44
+ if not pid:
45
+ raise click.UsageError("Project ID required.")
46
+ data = dept_mod.get_department_tree(session, pid, department_id=department_id)
47
+ ctx.output(data, lambda d: output_json(d))
48
+ except Exception as e:
49
+ ctx.handle_error(e)
50
+
51
+
52
+ @department.command("info")
53
+ @click.argument("department_id")
54
+ @pass_context
55
+ def department_info(ctx, department_id):
56
+ """Get department details."""
57
+ try:
58
+ session = ctx.get_session()
59
+ data = dept_mod.get_department_info(session, department_id)
60
+ ctx.output(data, lambda d: output_kv(d))
61
+ except Exception as e:
62
+ ctx.handle_error(e)
63
+
64
+
65
+ @department.command("create")
66
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
67
+ @click.option("--name", "-n", required=True, help="Department name")
68
+ @click.option("--parent-id", default="", help="Parent department ID")
69
+ @pass_context
70
+ def department_create(ctx, project_id, name, parent_id):
71
+ """Create a new department."""
72
+ try:
73
+ session = ctx.get_session()
74
+ pid = project_id or session.default_project_id
75
+ if not pid:
76
+ raise click.UsageError("Project ID required.")
77
+ data = dept_mod.create_department(session, pid, name, parent_id=parent_id)
78
+ ctx.output(data, lambda d: click.echo(f"Department created: {d}"))
79
+ except Exception as e:
80
+ ctx.handle_error(e)
81
+
82
+
83
+ @department.command("update")
84
+ @click.argument("department_id")
85
+ @click.option("--name", "-n", required=True, help="New department name")
86
+ @pass_context
87
+ def department_update(ctx, department_id, name):
88
+ """Update department name."""
89
+ try:
90
+ session = ctx.get_session()
91
+ data = dept_mod.update_department(session, department_id, name)
92
+ ctx.output(data, lambda d: click.echo("Department updated."))
93
+ except Exception as e:
94
+ ctx.handle_error(e)
95
+
96
+
97
+ @department.command("delete")
98
+ @click.argument("department_ids", nargs=-1, required=True)
99
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
100
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
101
+ @pass_context
102
+ def department_delete(ctx, department_ids, project_id, yes):
103
+ """Delete one or more departments."""
104
+ try:
105
+ if not yes:
106
+ click.confirm(f"Delete {len(department_ids)} department(s)?", abort=True)
107
+ session = ctx.get_session()
108
+ pid = project_id or session.default_project_id
109
+ if not pid:
110
+ raise click.UsageError("Project ID required.")
111
+ data = dept_mod.delete_departments(session, pid, list(department_ids))
112
+ ctx.output(data, lambda d: click.echo(f"Deleted {len(department_ids)} department(s)."))
113
+ except Exception as e:
114
+ ctx.handle_error(e)
115
+
116
+
117
+ @department.command("members")
118
+ @click.argument("department_id")
119
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
120
+ @click.option("--page-size", "-n", default=50, help="Items per page")
121
+ @click.option("--page", default=1, help="Page number")
122
+ @pass_context
123
+ def department_members(ctx, department_id, project_id, page_size, page):
124
+ """Get members of a department."""
125
+ try:
126
+ session = ctx.get_session()
127
+ pid = project_id or session.default_project_id
128
+ if not pid:
129
+ raise click.UsageError("Project ID required.")
130
+ data = dept_mod.get_department_users(session, pid, department_id, page_index=page, page_size=page_size)
131
+ ctx.output(data, lambda d: output_json(d))
132
+ except Exception as e:
133
+ ctx.handle_error(e)
134
+
135
+
136
+ @department.command("search")
137
+ @click.argument("keywords")
138
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
139
+ @pass_context
140
+ def department_search(ctx, keywords, project_id):
141
+ """Search departments and users."""
142
+ try:
143
+ session = ctx.get_session()
144
+ pid = project_id or session.default_project_id
145
+ if not pid:
146
+ raise click.UsageError("Project ID required.")
147
+ data = dept_mod.search_dept_and_users(session, pid, keywords)
148
+ ctx.output(data, lambda d: output_json(d))
149
+ except Exception as e:
150
+ ctx.handle_error(e)
151
+
152
+
153
+ @department.command("move")
154
+ @click.argument("department_id")
155
+ @click.argument("target_dept_id")
156
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
157
+ @pass_context
158
+ def department_move(ctx, department_id, target_dept_id, project_id):
159
+ """Move a department under a new parent."""
160
+ try:
161
+ session = ctx.get_session()
162
+ pid = project_id or session.default_project_id
163
+ if not pid:
164
+ raise click.UsageError("Project ID required.")
165
+ data = dept_mod.move_department(session, pid, department_id, target_dept_id)
166
+ ctx.output(data, lambda d: click.echo("Department moved."))
167
+ except Exception as e:
168
+ ctx.handle_error(e)
@@ -0,0 +1,128 @@
1
+ """CLI commands for group management."""
2
+
3
+ import click
4
+
5
+ from hap_cli.context import pass_context
6
+ from hap_cli.core import group as group_mod
7
+ from hap_cli.utils.formatting import output_json, output_kv
8
+
9
+
10
+ @click.group()
11
+ def group():
12
+ """Group management."""
13
+ pass
14
+
15
+
16
+ @group.command("list")
17
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
18
+ @click.option("--page-size", "-n", default=50, help="Items per page")
19
+ @click.option("--page", default=1, help="Page number")
20
+ @pass_context
21
+ def group_list(ctx, project_id, page_size, page):
22
+ """List groups."""
23
+ try:
24
+ session = ctx.get_session()
25
+ pid = project_id or session.default_project_id
26
+ if not pid:
27
+ raise click.UsageError("Project ID required.")
28
+ data = group_mod.get_groups(session, pid, page_index=page, page_size=page_size)
29
+ ctx.output(data, lambda d: output_json(d))
30
+ except Exception as e:
31
+ ctx.handle_error(e)
32
+
33
+
34
+ @group.command("info")
35
+ @click.argument("group_id")
36
+ @pass_context
37
+ def group_info(ctx, group_id):
38
+ """Get group information."""
39
+ try:
40
+ session = ctx.get_session()
41
+ data = group_mod.get_group_info(session, group_id)
42
+ ctx.output(data, lambda d: output_kv(d))
43
+ except Exception as e:
44
+ ctx.handle_error(e)
45
+
46
+
47
+ @group.command("create")
48
+ @click.option("--project-id", "-p", default="", help="Project/org ID")
49
+ @click.option("--name", "-n", required=True, help="Group name")
50
+ @click.option("--about", "-a", default="", help="Group announcement")
51
+ @click.option("--member", "-m", multiple=True, help="Initial member account IDs")
52
+ @pass_context
53
+ def group_create(ctx, project_id, name, about, member):
54
+ """Create a new group."""
55
+ try:
56
+ session = ctx.get_session()
57
+ pid = project_id or session.default_project_id
58
+ if not pid:
59
+ raise click.UsageError("Project ID required.")
60
+ data = group_mod.add_group(session, pid, name, about=about, member_ids=list(member) or None)
61
+ ctx.output(data, lambda d: click.echo(f"Group created: {d}"))
62
+ except Exception as e:
63
+ ctx.handle_error(e)
64
+
65
+
66
+ @group.command("delete")
67
+ @click.argument("group_id")
68
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
69
+ @pass_context
70
+ def group_delete(ctx, group_id, yes):
71
+ """Delete/disband a group."""
72
+ try:
73
+ if not yes:
74
+ click.confirm(f"Delete group {group_id}?", abort=True)
75
+ session = ctx.get_session()
76
+ data = group_mod.remove_group(session, group_id)
77
+ ctx.output(data, lambda d: click.echo("Group deleted."))
78
+ except Exception as e:
79
+ ctx.handle_error(e)
80
+
81
+
82
+ @group.command("members")
83
+ @click.argument("group_id")
84
+ @click.option("--page-size", "-n", default=50, help="Items per page")
85
+ @click.option("--page", default=1, help="Page number")
86
+ @pass_context
87
+ def group_members(ctx, group_id, page_size, page):
88
+ """List group members."""
89
+ try:
90
+ session = ctx.get_session()
91
+ data = group_mod.get_group_users(session, group_id, page_index=page, page_size=page_size)
92
+ ctx.output(data, lambda d: output_json(d))
93
+ except Exception as e:
94
+ ctx.handle_error(e)
95
+
96
+
97
+ @group.command("remove-member")
98
+ @click.argument("group_id")
99
+ @click.argument("account_id")
100
+ @pass_context
101
+ def group_remove_member(ctx, group_id, account_id):
102
+ """Remove a member from a group."""
103
+ try:
104
+ session = ctx.get_session()
105
+ data = group_mod.remove_user(session, group_id, account_id)
106
+ ctx.output(data, lambda d: click.echo("Member removed."))
107
+ except Exception as e:
108
+ ctx.handle_error(e)
109
+
110
+
111
+ @group.command("update")
112
+ @click.argument("group_id")
113
+ @click.option("--name", "-n", default="", help="New group name")
114
+ @click.option("--about", "-a", default="", help="New group announcement")
115
+ @pass_context
116
+ def group_update(ctx, group_id, name, about):
117
+ """Update group name and/or announcement."""
118
+ try:
119
+ session = ctx.get_session()
120
+ if name:
121
+ data = group_mod.update_group_name(session, group_id, name)
122
+ if about:
123
+ data = group_mod.update_group_about(session, group_id, about)
124
+ if not name and not about:
125
+ raise click.UsageError("Provide --name or --about to update.")
126
+ ctx.output(data, lambda d: click.echo("Group updated."))
127
+ except Exception as e:
128
+ ctx.handle_error(e)