@awiki/cli 0.0.1-beta.2

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 (119) hide show
  1. package/.github/workflows/release.yml +44 -0
  2. package/.goreleaser.yml +44 -0
  3. package/AGENTS.md +60 -0
  4. package/CLAUDE.md +192 -0
  5. package/README.md +2 -0
  6. package/docs/architecture/awiki-command-v2.md +955 -0
  7. package/docs/architecture/awiki-skill-architecture.md +475 -0
  8. package/docs/architecture/awiki-v2-architecture.md +1063 -0
  9. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
  10. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
  11. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
  12. package/docs/harness/review-spec.md +474 -0
  13. package/docs/installation.md +372 -0
  14. package/docs/plan/awiki-v2-implementation-plan.md +903 -0
  15. package/docs/plan/phase-0/adr-index.md +56 -0
  16. package/docs/plan/phase-0/audit-findings.md +251 -0
  17. package/docs/plan/phase-0/capability-mapping.md +108 -0
  18. package/docs/plan/phase-0/implementation-constraints.md +363 -0
  19. package/docs/publish.md +169 -0
  20. package/go.mod +29 -0
  21. package/go.sum +73 -0
  22. package/internal/anpsdk/registry.go +63 -0
  23. package/internal/authsdk/session.go +351 -0
  24. package/internal/buildinfo/buildinfo.go +34 -0
  25. package/internal/cli/app.go +136 -0
  26. package/internal/cli/app_test.go +88 -0
  27. package/internal/cli/debug.go +104 -0
  28. package/internal/cli/group.go +263 -0
  29. package/internal/cli/id.go +473 -0
  30. package/internal/cli/init.go +134 -0
  31. package/internal/cli/msg.go +228 -0
  32. package/internal/cli/page.go +267 -0
  33. package/internal/cli/root.go +499 -0
  34. package/internal/cli/runtime.go +232 -0
  35. package/internal/cli/upgrade.go +60 -0
  36. package/internal/cmdmeta/catalog.go +203 -0
  37. package/internal/cmdmeta/catalog_test.go +21 -0
  38. package/internal/config/config.go +399 -0
  39. package/internal/config/config_test.go +104 -0
  40. package/internal/config/write.go +37 -0
  41. package/internal/content/service.go +314 -0
  42. package/internal/content/service_test.go +165 -0
  43. package/internal/content/types.go +44 -0
  44. package/internal/docs/topics.go +110 -0
  45. package/internal/doctor/doctor.go +306 -0
  46. package/internal/identity/client.go +267 -0
  47. package/internal/identity/did.go +85 -0
  48. package/internal/identity/did_test.go +50 -0
  49. package/internal/identity/layout.go +206 -0
  50. package/internal/identity/legacy.go +378 -0
  51. package/internal/identity/public.go +70 -0
  52. package/internal/identity/public_test.go +73 -0
  53. package/internal/identity/readiness.go +74 -0
  54. package/internal/identity/service.go +826 -0
  55. package/internal/identity/store.go +385 -0
  56. package/internal/identity/store_test.go +180 -0
  57. package/internal/identity/types.go +204 -0
  58. package/internal/message/auth.go +167 -0
  59. package/internal/message/group_service.go +838 -0
  60. package/internal/message/group_wire.go +350 -0
  61. package/internal/message/group_wire_test.go +67 -0
  62. package/internal/message/helpers.go +61 -0
  63. package/internal/message/http_client.go +334 -0
  64. package/internal/message/proof.go +156 -0
  65. package/internal/message/proof_test.go +61 -0
  66. package/internal/message/service.go +696 -0
  67. package/internal/message/service_test.go +97 -0
  68. package/internal/message/types.go +155 -0
  69. package/internal/message/wire.go +100 -0
  70. package/internal/message/wire_test.go +49 -0
  71. package/internal/message/ws_proxy_client.go +151 -0
  72. package/internal/output/output.go +350 -0
  73. package/internal/output/output_test.go +48 -0
  74. package/internal/runtime/config.go +117 -0
  75. package/internal/runtime/config_test.go +46 -0
  76. package/internal/runtime/listener/files.go +65 -0
  77. package/internal/runtime/listener/manager.go +142 -0
  78. package/internal/runtime/listener/server.go +983 -0
  79. package/internal/runtime/listener/server_test.go +319 -0
  80. package/internal/runtime/listener/sysproc_unix.go +17 -0
  81. package/internal/runtime/listener/sysproc_windows.go +13 -0
  82. package/internal/runtime/listener/types.go +21 -0
  83. package/internal/runtime/listener/wsclient.go +299 -0
  84. package/internal/runtime/listener/wsclient_test.go +41 -0
  85. package/internal/store/dao.go +632 -0
  86. package/internal/store/dao_test.go +87 -0
  87. package/internal/store/helpers.go +197 -0
  88. package/internal/store/import.go +499 -0
  89. package/internal/store/import_test.go +103 -0
  90. package/internal/store/open.go +71 -0
  91. package/internal/store/query.go +151 -0
  92. package/internal/store/schema.go +277 -0
  93. package/internal/store/schema_test.go +56 -0
  94. package/internal/store/types.go +177 -0
  95. package/internal/update/update.go +368 -0
  96. package/package.json +17 -0
  97. package/scripts/install.js +171 -0
  98. package/scripts/release/release-prerelease.sh +86 -0
  99. package/scripts/release/tag-release.sh +66 -0
  100. package/scripts/release/withdraw-release.sh +78 -0
  101. package/scripts/run.js +69 -0
  102. package/skills/README.md +32 -0
  103. package/skills/awiki-bundle/SKILL.md +76 -0
  104. package/skills/awiki-debug/SKILL.md +80 -0
  105. package/skills/awiki-group/SKILL.md +111 -0
  106. package/skills/awiki-id/SKILL.md +123 -0
  107. package/skills/awiki-msg/SKILL.md +131 -0
  108. package/skills/awiki-page/SKILL.md +93 -0
  109. package/skills/awiki-people/SKILL.md +66 -0
  110. package/skills/awiki-runtime/SKILL.md +137 -0
  111. package/skills/awiki-shared/SKILL.md +124 -0
  112. package/skills/awiki-workflow-discovery/SKILL.md +93 -0
  113. package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
  114. package/skills/manifests/skills.yaml +260 -0
  115. package/skills/templates/bundle-skill-template.md +42 -0
  116. package/skills/templates/debug-skill-template.md +44 -0
  117. package/skills/templates/domain-skill-template.md +56 -0
  118. package/skills/templates/shared-skill-template.md +46 -0
  119. package/skills/templates/workflow-skill-template.md +46 -0
@@ -0,0 +1,228 @@
1
+ package cli
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "os"
7
+ "strings"
8
+
9
+ "github.com/agentconnect/awiki-cli/internal/identity"
10
+ "github.com/agentconnect/awiki-cli/internal/message"
11
+ "github.com/agentconnect/awiki-cli/internal/output"
12
+ "github.com/spf13/cobra"
13
+ )
14
+
15
+ func (a *App) messageService() (*message.Service, output.Format, error) {
16
+ resolved, err := a.resolveConfig()
17
+ if err != nil {
18
+ return nil, output.FormatJSON, err
19
+ }
20
+ format := normalizedFormat(resolved.OutputFormat)
21
+ service, err := message.NewService(resolved)
22
+ if err != nil {
23
+ return nil, format, err
24
+ }
25
+ return service, format, nil
26
+ }
27
+
28
+ func (a *App) messageExit(err error, hint string) error {
29
+ if err == nil {
30
+ return nil
31
+ }
32
+ var serviceErr *message.ServiceError
33
+ if errors.As(err, &serviceErr) {
34
+ switch {
35
+ case serviceErr.StatusCode == 400 || serviceErr.RPCCode == -32602:
36
+ return output.NewExitError("invalid_argument", 2, err.Error(), hint)
37
+ case serviceErr.StatusCode == 401 || serviceErr.RPCCode == -32000:
38
+ return output.NewExitError("auth_required", 3, err.Error(), "Use an identity with a valid JWT or DID WBA auth material.")
39
+ case serviceErr.StatusCode == 404 || serviceErr.RPCCode == -32002:
40
+ return output.NewExitError("not_found", 5, err.Error(), hint)
41
+ case serviceErr.StatusCode == 409 || serviceErr.RPCCode == -32003 || serviceErr.RPCCode == -32004:
42
+ return output.NewExitError("conflict", 1, err.Error(), hint)
43
+ }
44
+ }
45
+ switch {
46
+ case errors.Is(err, message.ErrTargetRequired), errors.Is(err, message.ErrGroupRequired), errors.Is(err, message.ErrMemberRequired), errors.Is(err, message.ErrTextRequired), errors.Is(err, message.ErrMessageNotFound):
47
+ return output.NewExitError("invalid_argument", 2, err.Error(), hint)
48
+ case errors.Is(err, identity.ErrUserRegistrationRequired):
49
+ return output.NewExitError("identity_required", 3, err.Error(), "Complete user setup with `awiki-cli id register --handle <handle> ...` or recover an existing handle before using msg commands.")
50
+ case errors.Is(err, message.ErrSecureNotSupported):
51
+ return output.NewExitError("unsupported_mode", 1, err.Error(), "Direct secure messaging is planned for Phase 5.")
52
+ case errors.Is(err, message.ErrTransportUnavailable):
53
+ return output.NewExitError("transport_unavailable", 1, err.Error(), "Start the websocket listener/daemon or switch runtime.mode back to http.")
54
+ default:
55
+ type rpcCoder interface{ Error() string }
56
+ var _ rpcCoder = err
57
+ return output.NewExitError("internal_error", 1, err.Error(), hint)
58
+ }
59
+ }
60
+
61
+ func (a *App) runMsgSend(cmd *cobra.Command, args []string) error {
62
+ to, _ := cmd.Flags().GetString("to")
63
+ group, _ := cmd.Flags().GetString("group")
64
+ text, _ := cmd.Flags().GetString("text")
65
+ textFile, _ := cmd.Flags().GetString("text-file")
66
+ messageType, _ := cmd.Flags().GetString("type")
67
+ secure, _ := cmd.Flags().GetString("secure")
68
+ if strings.TrimSpace(group) == "" && strings.TrimSpace(to) == "" {
69
+ return output.NewExitError("invalid_argument", 2, "msg send requires either --to or --group.", "Usage: awiki-cli msg send --to <handle|did> --text \"Hello\" or awiki-cli msg send --group <group_did> --text \"Hello group\"")
70
+ }
71
+ if strings.TrimSpace(group) != "" && strings.TrimSpace(to) != "" {
72
+ return output.NewExitError("invalid_argument", 2, "msg send accepts either --to or --group, but not both.", "Choose direct messaging with --to or group messaging with --group.")
73
+ }
74
+ if strings.TrimSpace(text) == "" && strings.TrimSpace(textFile) != "" {
75
+ raw, err := os.ReadFile(textFile)
76
+ if err != nil {
77
+ return output.NewExitError("invalid_argument", 2, err.Error(), "Make sure --text-file points to a readable file.")
78
+ }
79
+ text = string(raw)
80
+ }
81
+ if strings.TrimSpace(text) == "" {
82
+ return output.NewExitError("invalid_argument", 2, "msg send requires --text or --text-file.", "Provide the message body via --text or --text-file.")
83
+ }
84
+ service, format, err := a.messageService()
85
+ if err != nil {
86
+ return a.messageExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
87
+ }
88
+ request := message.SendRequest{
89
+ IdentityName: a.globals.Identity,
90
+ Target: to,
91
+ Group: group,
92
+ Text: text,
93
+ MessageType: messageType,
94
+ SecureMode: secure,
95
+ }
96
+ if a.globals.DryRun {
97
+ action := "direct.send"
98
+ target := map[string]any{"did": to, "kind": "direct"}
99
+ if strings.TrimSpace(group) != "" {
100
+ action = "group.send"
101
+ target = map[string]any{"did": group, "kind": "group"}
102
+ }
103
+ data := map[string]any{
104
+ "plan": map[string]any{
105
+ "action": action,
106
+ "identity": a.globals.Identity,
107
+ "target": target,
108
+ "message_type": defaultString(messageType, "text"),
109
+ "runtime_mode": service.Config().RuntimeMode,
110
+ "transport": service.Config().RuntimeMode,
111
+ "local_writes": []string{"messages"},
112
+ },
113
+ }
114
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, "Dry run: message send planned", nil, a.identityMeta())
115
+ }
116
+ result, err := service.Send(context.Background(), request)
117
+ if err != nil {
118
+ return a.messageExit(err, "Ensure the target exists, the active identity is valid, and runtime mode is configured correctly.")
119
+ }
120
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
121
+ }
122
+
123
+ func (a *App) runMsgInbox(cmd *cobra.Command, args []string) error {
124
+ scope, _ := cmd.Flags().GetString("scope")
125
+ with, _ := cmd.Flags().GetString("with")
126
+ group, _ := cmd.Flags().GetString("group")
127
+ unread, _ := cmd.Flags().GetBool("unread")
128
+ limit, _ := cmd.Flags().GetInt("limit")
129
+ markRead, _ := cmd.Flags().GetBool("mark-read")
130
+ service, format, err := a.messageService()
131
+ if err != nil {
132
+ return a.messageExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
133
+ }
134
+ request := message.InboxRequest{
135
+ IdentityName: a.globals.Identity,
136
+ Scope: scope,
137
+ With: with,
138
+ Group: group,
139
+ Limit: limit,
140
+ UnreadOnly: unread,
141
+ MarkRead: markRead,
142
+ }
143
+ if a.globals.DryRun {
144
+ data := map[string]any{"plan": map[string]any{
145
+ "action": "inbox.get",
146
+ "identity": a.globals.Identity,
147
+ "runtime_mode": service.Config().RuntimeMode,
148
+ "scope": scope,
149
+ "with": with,
150
+ "group": group,
151
+ "limit": limit,
152
+ "mark_read": markRead,
153
+ }}
154
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, "Dry run: inbox read planned", nil, a.identityMeta())
155
+ }
156
+ result, err := service.Inbox(context.Background(), request)
157
+ if err != nil {
158
+ return a.messageExit(err, "Make sure the active identity is valid and runtime mode is available.")
159
+ }
160
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
161
+ }
162
+
163
+ func (a *App) runMsgHistory(cmd *cobra.Command, args []string) error {
164
+ with, _ := cmd.Flags().GetString("with")
165
+ limit, _ := cmd.Flags().GetInt("limit")
166
+ cursor, _ := cmd.Flags().GetString("cursor")
167
+ service, format, err := a.messageService()
168
+ if err != nil {
169
+ return a.messageExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
170
+ }
171
+ request := message.HistoryRequest{
172
+ IdentityName: a.globals.Identity,
173
+ With: with,
174
+ Limit: limit,
175
+ Cursor: cursor,
176
+ }
177
+ if a.globals.DryRun {
178
+ data := map[string]any{"plan": map[string]any{
179
+ "action": "direct.get_history",
180
+ "identity": a.globals.Identity,
181
+ "runtime_mode": service.Config().RuntimeMode,
182
+ "with": with,
183
+ "limit": limit,
184
+ "cursor": cursor,
185
+ }}
186
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, "Dry run: direct history read planned", nil, a.identityMeta())
187
+ }
188
+ result, err := service.History(context.Background(), request)
189
+ if err != nil {
190
+ return a.messageExit(err, "Make sure the peer exists and runtime mode is available.")
191
+ }
192
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
193
+ }
194
+
195
+ func (a *App) runMsgMarkRead(cmd *cobra.Command, args []string) error {
196
+ if len(args) == 0 {
197
+ return output.NewExitError("invalid_argument", 2, "msg mark-read requires at least one message id.", "Usage: awiki-cli msg mark-read <MESSAGE_ID...>")
198
+ }
199
+ service, format, err := a.messageService()
200
+ if err != nil {
201
+ return a.messageExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
202
+ }
203
+ request := message.MarkReadRequest{
204
+ IdentityName: a.globals.Identity,
205
+ MessageIDs: args,
206
+ }
207
+ if a.globals.DryRun {
208
+ data := map[string]any{"plan": map[string]any{
209
+ "action": "inbox.mark_read",
210
+ "identity": a.globals.Identity,
211
+ "runtime_mode": service.Config().RuntimeMode,
212
+ "message_ids": args,
213
+ }}
214
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, "Dry run: mark-read planned", nil, a.identityMeta())
215
+ }
216
+ result, err := service.MarkRead(context.Background(), request)
217
+ if err != nil {
218
+ return a.messageExit(err, "Make sure the message ids are valid and runtime mode is available.")
219
+ }
220
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
221
+ }
222
+
223
+ func defaultString(value string, fallback string) string {
224
+ if strings.TrimSpace(value) == "" {
225
+ return fallback
226
+ }
227
+ return value
228
+ }
@@ -0,0 +1,267 @@
1
+ package cli
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "os"
7
+ "strings"
8
+
9
+ "github.com/agentconnect/awiki-cli/internal/content"
10
+ "github.com/agentconnect/awiki-cli/internal/identity"
11
+ "github.com/agentconnect/awiki-cli/internal/output"
12
+ "github.com/spf13/cobra"
13
+ )
14
+
15
+ func (a *App) contentService() (*content.Service, output.Format, error) {
16
+ resolved, err := a.resolveConfig()
17
+ if err != nil {
18
+ return nil, output.FormatJSON, err
19
+ }
20
+ format := normalizedFormat(resolved.OutputFormat)
21
+ service, err := content.NewService(resolved)
22
+ if err != nil {
23
+ return nil, format, err
24
+ }
25
+ return service, format, nil
26
+ }
27
+
28
+ func (a *App) contentExit(err error, hint string) error {
29
+ if err == nil {
30
+ return nil
31
+ }
32
+ var serviceErr *content.ServiceError
33
+ if errors.As(err, &serviceErr) {
34
+ switch {
35
+ case serviceErr.StatusCode == 400 || serviceErr.RPCCode == -32602:
36
+ return output.NewExitError("invalid_argument", 2, err.Error(), hint)
37
+ case serviceErr.StatusCode == 401 || serviceErr.RPCCode == -32000:
38
+ return output.NewExitError("auth_required", 3, err.Error(), "Use an identity with a valid JWT or DID WBA auth material.")
39
+ case serviceErr.StatusCode == 404 || serviceErr.RPCCode == -32002:
40
+ return output.NewExitError("not_found", 5, err.Error(), hint)
41
+ case serviceErr.StatusCode == 409 || serviceErr.RPCCode == -32003 || serviceErr.RPCCode == -32004:
42
+ return output.NewExitError("conflict", 1, err.Error(), hint)
43
+ }
44
+ }
45
+ switch {
46
+ case errors.Is(err, content.ErrSlugRequired), errors.Is(err, content.ErrTitleRequired), errors.Is(err, content.ErrNoUpdateFields), errors.Is(err, content.ErrVisibilityInvalid):
47
+ return output.NewExitError("invalid_argument", 2, err.Error(), hint)
48
+ case errors.Is(err, identity.ErrIdentityNotFound), errors.Is(err, identity.ErrNoDefaultIdentity):
49
+ return output.NewExitError("not_found", 5, err.Error(), "Run `awiki-cli id list` to inspect available identities.")
50
+ case errors.Is(err, identity.ErrAuthRequired):
51
+ return output.NewExitError("auth_required", 3, err.Error(), "Use an identity with a valid JWT, or run `awiki-cli id register` / `awiki-cli id recover` first.")
52
+ default:
53
+ return output.NewExitError("internal_error", 1, err.Error(), hint)
54
+ }
55
+ }
56
+
57
+ func (a *App) runPageCreate(cmd *cobra.Command, args []string) error {
58
+ slug, _ := cmd.Flags().GetString("slug")
59
+ title, _ := cmd.Flags().GetString("title")
60
+ markdown, _ := cmd.Flags().GetString("markdown")
61
+ markdownFile, _ := cmd.Flags().GetString("markdown-file")
62
+ visibility, _ := cmd.Flags().GetString("visibility")
63
+ body, err := resolveMarkdownBody(markdown, cmd.Flags().Changed("markdown"), markdownFile, cmd.Flags().Changed("markdown-file"))
64
+ if err != nil {
65
+ return output.NewExitError("invalid_argument", 2, err.Error(), "Choose one content body source and make sure the file is readable.")
66
+ }
67
+ service, format, err := a.contentService()
68
+ if err != nil {
69
+ return a.contentExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
70
+ }
71
+ request := content.CreatePageParams{Slug: slug, Title: title, Body: body, Visibility: visibility}
72
+ if a.globals.DryRun {
73
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, map[string]any{"plan": map[string]any{
74
+ "action": "page.create",
75
+ "identity": a.globals.Identity,
76
+ "rpc_endpoint": "/content/rpc",
77
+ "rpc_method": "create",
78
+ "request": map[string]any{
79
+ "slug": strings.TrimSpace(slug),
80
+ "title": strings.TrimSpace(title),
81
+ "body_bytes": len(body),
82
+ "visibility": defaultString(visibility, "public"),
83
+ },
84
+ }}, "Dry run: page create planned", nil, a.identityMeta())
85
+ }
86
+ result, err := service.CreatePage(context.Background(), request)
87
+ if err != nil {
88
+ return a.contentExit(err, "Make sure the active identity has a handle and the page slug is valid.")
89
+ }
90
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
91
+ }
92
+
93
+ func (a *App) runPageList(cmd *cobra.Command, args []string) error {
94
+ service, format, err := a.contentService()
95
+ if err != nil {
96
+ return a.contentExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
97
+ }
98
+ if a.globals.DryRun {
99
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, map[string]any{"plan": map[string]any{
100
+ "action": "page.list",
101
+ "identity": a.globals.Identity,
102
+ "rpc_endpoint": "/content/rpc",
103
+ "rpc_method": "list",
104
+ }}, "Dry run: page list planned", nil, a.identityMeta())
105
+ }
106
+ result, err := service.ListPages(context.Background())
107
+ if err != nil {
108
+ return a.contentExit(err, "Make sure the active identity has a handle and can access content pages.")
109
+ }
110
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
111
+ }
112
+
113
+ func (a *App) runPageGet(cmd *cobra.Command, args []string) error {
114
+ slug, _ := cmd.Flags().GetString("slug")
115
+ service, format, err := a.contentService()
116
+ if err != nil {
117
+ return a.contentExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
118
+ }
119
+ if a.globals.DryRun {
120
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, map[string]any{"plan": map[string]any{
121
+ "action": "page.get",
122
+ "identity": a.globals.Identity,
123
+ "rpc_endpoint": "/content/rpc",
124
+ "rpc_method": "get",
125
+ "request": map[string]any{"slug": strings.TrimSpace(slug)},
126
+ }}, "Dry run: page get planned", nil, a.identityMeta())
127
+ }
128
+ result, err := service.GetPage(context.Background(), slug)
129
+ if err != nil {
130
+ return a.contentExit(err, "Make sure the page exists and the active identity can access it.")
131
+ }
132
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
133
+ }
134
+
135
+ func (a *App) runPageUpdate(cmd *cobra.Command, args []string) error {
136
+ slug, _ := cmd.Flags().GetString("slug")
137
+ title, _ := cmd.Flags().GetString("title")
138
+ markdown, _ := cmd.Flags().GetString("markdown")
139
+ markdownFile, _ := cmd.Flags().GetString("markdown-file")
140
+ visibilityChanged := cmd.Flags().Changed("visibility")
141
+ visibility, _ := cmd.Flags().GetString("visibility")
142
+ body, err := resolveOptionalMarkdownBody(markdown, cmd.Flags().Changed("markdown"), markdownFile, cmd.Flags().Changed("markdown-file"))
143
+ if err != nil {
144
+ return output.NewExitError("invalid_argument", 2, err.Error(), "Choose one content body source and make sure the file is readable.")
145
+ }
146
+ service, format, err := a.contentService()
147
+ if err != nil {
148
+ return a.contentExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
149
+ }
150
+ request := content.UpdatePageParams{Slug: slug, Title: title, Body: body}
151
+ if visibilityChanged {
152
+ request.Visibility = &visibility
153
+ }
154
+ if a.globals.DryRun {
155
+ changedFields := make([]string, 0, 3)
156
+ if strings.TrimSpace(title) != "" {
157
+ changedFields = append(changedFields, "title")
158
+ }
159
+ if body != nil {
160
+ changedFields = append(changedFields, "body")
161
+ }
162
+ if visibilityChanged {
163
+ changedFields = append(changedFields, "visibility")
164
+ }
165
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, map[string]any{"plan": map[string]any{
166
+ "action": "page.update",
167
+ "identity": a.globals.Identity,
168
+ "rpc_endpoint": "/content/rpc",
169
+ "rpc_method": "update",
170
+ "changed_fields": changedFields,
171
+ "request": map[string]any{
172
+ "slug": strings.TrimSpace(slug),
173
+ "title": strings.TrimSpace(title),
174
+ "body_bytes": bodySize(body),
175
+ "visibility": visibility,
176
+ },
177
+ }}, "Dry run: page update planned", nil, a.identityMeta())
178
+ }
179
+ result, err := service.UpdatePage(context.Background(), request)
180
+ if err != nil {
181
+ return a.contentExit(err, "Make sure the page exists and the updated fields are valid.")
182
+ }
183
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
184
+ }
185
+
186
+ func (a *App) runPageRename(cmd *cobra.Command, args []string) error {
187
+ slug, _ := cmd.Flags().GetString("slug")
188
+ target, _ := cmd.Flags().GetString("to")
189
+ service, format, err := a.contentService()
190
+ if err != nil {
191
+ return a.contentExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
192
+ }
193
+ request := content.RenamePageParams{Slug: slug, To: target}
194
+ if a.globals.DryRun {
195
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, map[string]any{"plan": map[string]any{
196
+ "action": "page.rename",
197
+ "identity": a.globals.Identity,
198
+ "rpc_endpoint": "/content/rpc",
199
+ "rpc_method": "rename",
200
+ "request": map[string]any{"old_slug": strings.TrimSpace(slug), "new_slug": strings.TrimSpace(target)},
201
+ }}, "Dry run: page rename planned", nil, a.identityMeta())
202
+ }
203
+ result, err := service.RenamePage(context.Background(), request)
204
+ if err != nil {
205
+ return a.contentExit(err, "Make sure the source page exists and the target slug is available.")
206
+ }
207
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
208
+ }
209
+
210
+ func (a *App) runPageDelete(cmd *cobra.Command, args []string) error {
211
+ slug, _ := cmd.Flags().GetString("slug")
212
+ service, format, err := a.contentService()
213
+ if err != nil {
214
+ return a.contentExit(err, "Run `awiki-cli doctor` to inspect configuration and identity state.")
215
+ }
216
+ if a.globals.DryRun {
217
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, map[string]any{"plan": map[string]any{
218
+ "action": "page.delete",
219
+ "identity": a.globals.Identity,
220
+ "rpc_endpoint": "/content/rpc",
221
+ "rpc_method": "delete",
222
+ "request": map[string]any{"slug": strings.TrimSpace(slug)},
223
+ }}, "Dry run: page delete planned", nil, a.identityMeta())
224
+ }
225
+ result, err := service.DeletePage(context.Background(), slug)
226
+ if err != nil {
227
+ return a.contentExit(err, "Make sure the page exists and the active identity can delete it.")
228
+ }
229
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, result.Data, result.Summary, result.Warnings, a.identityMeta())
230
+ }
231
+
232
+ func resolveMarkdownBody(markdown string, markdownChanged bool, markdownFile string, markdownFileChanged bool) (string, error) {
233
+ body, err := resolveOptionalMarkdownBody(markdown, markdownChanged, markdownFile, markdownFileChanged)
234
+ if err != nil {
235
+ return "", err
236
+ }
237
+ if body == nil {
238
+ return "", nil
239
+ }
240
+ return *body, nil
241
+ }
242
+
243
+ func resolveOptionalMarkdownBody(markdown string, markdownChanged bool, markdownFile string, markdownFileChanged bool) (*string, error) {
244
+ if markdownChanged && markdownFileChanged {
245
+ return nil, content.ErrBodySourceConflict
246
+ }
247
+ if markdownFileChanged {
248
+ raw, err := os.ReadFile(markdownFile)
249
+ if err != nil {
250
+ return nil, err
251
+ }
252
+ text := string(raw)
253
+ return &text, nil
254
+ }
255
+ if !markdownChanged {
256
+ return nil, nil
257
+ }
258
+ text := markdown
259
+ return &text, nil
260
+ }
261
+
262
+ func bodySize(body *string) int {
263
+ if body == nil {
264
+ return 0
265
+ }
266
+ return len(*body)
267
+ }