@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,473 @@
1
+ package cli
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "strings"
7
+
8
+ "github.com/agentconnect/awiki-cli/internal/identity"
9
+ "github.com/agentconnect/awiki-cli/internal/output"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ func (a *App) identityService() (*identity.Service, output.Format, error) {
14
+ resolved, err := a.resolveConfig()
15
+ if err != nil {
16
+ return nil, output.FormatJSON, err
17
+ }
18
+ format := normalizedFormat(resolved.OutputFormat)
19
+ service, err := identity.NewService(resolved)
20
+ if err != nil {
21
+ return nil, format, err
22
+ }
23
+ return service, format, nil
24
+ }
25
+
26
+ func (a *App) renderIdentityResult(cmd *cobra.Command, format output.Format, result *identity.CommandResult) error {
27
+ if result == nil {
28
+ return nil
29
+ }
30
+ meta := a.identityMeta()
31
+ if meta == nil {
32
+ meta = identityMetaFromData(result.Data)
33
+ }
34
+ return a.renderSuccess(
35
+ cmd.CommandPath(),
36
+ format,
37
+ a.globals.JQ,
38
+ identity.PublicData(result.Data),
39
+ result.Summary,
40
+ result.Warnings,
41
+ meta,
42
+ )
43
+ }
44
+
45
+ func (a *App) identityExit(err error, fallbackHint string) error {
46
+ if err == nil {
47
+ return nil
48
+ }
49
+ var serviceErr *identity.ServiceError
50
+ if errors.As(err, &serviceErr) {
51
+ switch {
52
+ case serviceErr.StatusCode == 400:
53
+ return output.NewExitError("invalid_argument", 2, serviceErr.Error(), fallbackHint)
54
+ case serviceErr.StatusCode == 401:
55
+ return output.NewExitError("auth_required", 3, serviceErr.Error(), "Use an identity with a valid JWT, or run `awiki-cli id register` / `awiki-cli id recover` first.")
56
+ case serviceErr.StatusCode == 404:
57
+ return output.NewExitError("not_found", 5, serviceErr.Error(), fallbackHint)
58
+ case serviceErr.StatusCode == 409:
59
+ return output.NewExitError("conflict", 1, serviceErr.Error(), fallbackHint)
60
+ case serviceErr.RPCCode != 0:
61
+ code := "internal_error"
62
+ exitCode := 1
63
+ switch serviceErr.RPCCode {
64
+ case -32602:
65
+ code, exitCode = "invalid_argument", 2
66
+ case -32000:
67
+ code, exitCode = "auth_required", 3
68
+ case -32002:
69
+ code, exitCode = "not_found", 5
70
+ case -32003, -32004:
71
+ code = "conflict"
72
+ }
73
+ return output.NewExitError(code, exitCode, serviceErr.Error(), fallbackHint)
74
+ }
75
+ }
76
+ switch {
77
+ case errors.Is(err, identity.ErrInvalidInput):
78
+ return output.NewExitError("invalid_argument", 2, err.Error(), fallbackHint)
79
+ case errors.Is(err, identity.ErrIdentityNotFound), errors.Is(err, identity.ErrLegacyNotFound), errors.Is(err, identity.ErrNoDefaultIdentity):
80
+ return output.NewExitError("not_found", 5, err.Error(), fallbackHint)
81
+ case errors.Is(err, identity.ErrIdentityConflict):
82
+ return output.NewExitError("conflict", 1, err.Error(), fallbackHint)
83
+ case errors.Is(err, identity.ErrAuthRequired):
84
+ 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.")
85
+ default:
86
+ return output.NewExitError("internal_error", 1, err.Error(), fallbackHint)
87
+ }
88
+ }
89
+
90
+ func (a *App) runIDStatus(cmd *cobra.Command, args []string) error {
91
+ service, format, err := a.identityService()
92
+ if err != nil {
93
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the local identity store.")
94
+ }
95
+ result, err := service.Status()
96
+ if err != nil {
97
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the local identity store.")
98
+ }
99
+ return a.renderIdentityResult(cmd, format, result)
100
+ }
101
+
102
+ func (a *App) runIDList(cmd *cobra.Command, args []string) error {
103
+ service, format, err := a.identityService()
104
+ if err != nil {
105
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the local identity store.")
106
+ }
107
+ result, err := service.List()
108
+ if err != nil {
109
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the local identity store.")
110
+ }
111
+ return a.renderIdentityResult(cmd, format, result)
112
+ }
113
+
114
+ func (a *App) runIDCurrent(cmd *cobra.Command, args []string) error {
115
+ service, format, err := a.identityService()
116
+ if err != nil {
117
+ return a.identityExit(err, "Run `awiki-cli id list` to inspect available identities.")
118
+ }
119
+ result, err := service.Current()
120
+ if err != nil {
121
+ return a.identityExit(err, "Run `awiki-cli id list` to inspect available identities.")
122
+ }
123
+ return a.renderIdentityResult(cmd, format, result)
124
+ }
125
+
126
+ func (a *App) runIDUse(cmd *cobra.Command, args []string) error {
127
+ if len(args) != 1 {
128
+ return output.NewExitError("invalid_argument", 2, "id use requires exactly one identity name.", "Usage: awiki-cli id use <identity>")
129
+ }
130
+ service, format, err := a.identityService()
131
+ if err != nil {
132
+ return a.identityExit(err, "Run `awiki-cli id list` to inspect available identities.")
133
+ }
134
+ if a.globals.DryRun {
135
+ result := &identity.CommandResult{
136
+ Data: map[string]any{
137
+ "plan": map[string]any{
138
+ "action": "set_default_identity",
139
+ "identity_name": args[0],
140
+ "writes": []string{"index.json"},
141
+ "side_effect": true,
142
+ "previous_source": "identity_index",
143
+ },
144
+ },
145
+ Summary: "Dry run: default identity switch planned",
146
+ }
147
+ return a.renderIdentityResult(cmd, format, result)
148
+ }
149
+ result, err := service.Use(args[0])
150
+ if err != nil {
151
+ return a.identityExit(err, "Run `awiki-cli id list` to inspect available identities.")
152
+ }
153
+ return a.renderIdentityResult(cmd, format, result)
154
+ }
155
+
156
+ func (a *App) runIDCreate(cmd *cobra.Command, args []string) error {
157
+ name, _ := cmd.Flags().GetString("name")
158
+ identityName, _ := cmd.Flags().GetString("identity")
159
+ if strings.TrimSpace(name) == "" {
160
+ return output.NewExitError("invalid_argument", 2, "id create requires --name.", "Usage: awiki-cli id create --name \"Alice\" [--identity alice]")
161
+ }
162
+ service, format, err := a.identityService()
163
+ if err != nil {
164
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect configuration and storage paths.")
165
+ }
166
+ if a.globals.DryRun {
167
+ existing, _ := service.Manager().List()
168
+ alias := identity.PreviewDefaultIdentityName(identityName, existing, name)
169
+ result := &identity.CommandResult{
170
+ Data: map[string]any{
171
+ "plan": map[string]any{
172
+ "action": "create_identity",
173
+ "identity_name": alias,
174
+ "display_name": name,
175
+ "writes": []string{
176
+ "index.json",
177
+ "identity.json",
178
+ "auth.json",
179
+ "did_document.json",
180
+ "key-1-private.pem",
181
+ "key-1-public.pem",
182
+ "e2ee-signing-private.pem",
183
+ "e2ee-agreement-private.pem",
184
+ },
185
+ },
186
+ },
187
+ Summary: "Dry run: local DID identity creation planned",
188
+ }
189
+ return a.renderIdentityResult(cmd, format, result)
190
+ }
191
+ result, err := service.Create(name, identityName)
192
+ if err != nil {
193
+ return a.identityExit(err, "Use a different --identity value if the alias is already occupied.")
194
+ }
195
+ return a.renderIdentityResult(cmd, format, result)
196
+ }
197
+
198
+ func (a *App) runIDRegister(cmd *cobra.Command, args []string) error {
199
+ handle, _ := cmd.Flags().GetString("handle")
200
+ phone, _ := cmd.Flags().GetString("phone")
201
+ email, _ := cmd.Flags().GetString("email")
202
+ otp, _ := cmd.Flags().GetString("otp")
203
+ inviteCode, _ := cmd.Flags().GetString("invite-code")
204
+ wait, _ := cmd.Flags().GetBool("wait")
205
+ service, format, err := a.identityService()
206
+ if err != nil {
207
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect configuration and storage paths.")
208
+ }
209
+ params := identity.RegisterParams{
210
+ IdentityName: a.globals.Identity,
211
+ Handle: handle,
212
+ Phone: phone,
213
+ Email: email,
214
+ OTP: otp,
215
+ InviteCode: inviteCode,
216
+ Wait: wait,
217
+ VerificationTimeout: identity.DefaultEmailVerificationSecs,
218
+ PollIntervalSeconds: identity.DefaultEmailPollIntervalSecs,
219
+ }
220
+ if a.globals.DryRun {
221
+ existing, _ := service.Manager().List()
222
+ alias := identity.PreviewNamedIdentity(a.globals.Identity, existing, handle)
223
+ action := "register_handle"
224
+ remoteCalls := []string{"did-auth.register"}
225
+ if phone != "" && strings.TrimSpace(otp) == "" {
226
+ action = "send_handle_otp"
227
+ remoteCalls = []string{"handle.send_otp"}
228
+ }
229
+ if email != "" && !wait {
230
+ action = "send_registration_email"
231
+ remoteCalls = []string{"POST /user-service/auth/email-send"}
232
+ }
233
+ if email != "" && wait {
234
+ remoteCalls = []string{"GET /user-service/auth/email-status", "POST /user-service/auth/email-send", "did-auth.register"}
235
+ }
236
+ result := &identity.CommandResult{
237
+ Data: map[string]any{
238
+ "plan": map[string]any{
239
+ "action": action,
240
+ "identity_name": alias,
241
+ "handle": handle,
242
+ "phone": phone,
243
+ "email": email,
244
+ "remote_calls": remoteCalls,
245
+ },
246
+ },
247
+ Summary: "Dry run: handle registration flow planned",
248
+ }
249
+ return a.renderIdentityResult(cmd, format, result)
250
+ }
251
+ result, err := service.Register(context.Background(), params)
252
+ if err != nil {
253
+ return a.identityExit(err, "Ensure the handle, verification method, and local alias are valid.")
254
+ }
255
+ return a.renderIdentityResult(cmd, format, result)
256
+ }
257
+
258
+ func (a *App) runIDBind(cmd *cobra.Command, args []string) error {
259
+ phone, _ := cmd.Flags().GetString("phone")
260
+ email, _ := cmd.Flags().GetString("email")
261
+ otp, _ := cmd.Flags().GetString("otp")
262
+ wait, _ := cmd.Flags().GetBool("wait")
263
+ service, format, err := a.identityService()
264
+ if err != nil {
265
+ return a.identityExit(err, "Run `awiki-cli id current` to confirm the active identity.")
266
+ }
267
+ params := identity.BindParams{
268
+ Phone: phone,
269
+ Email: email,
270
+ OTP: otp,
271
+ Wait: wait,
272
+ VerificationTimeout: identity.DefaultEmailVerificationSecs,
273
+ PollIntervalSeconds: identity.DefaultEmailPollIntervalSecs,
274
+ }
275
+ if a.globals.DryRun {
276
+ action := "bind_contact"
277
+ remoteCalls := []string{}
278
+ if phone != "" && strings.TrimSpace(otp) == "" {
279
+ action = "send_bind_phone_otp"
280
+ remoteCalls = []string{"POST /user-service/auth/phone-bind-send"}
281
+ } else if phone != "" {
282
+ action = "bind_phone"
283
+ remoteCalls = []string{"POST /user-service/auth/phone-bind-verify"}
284
+ } else if email != "" && !wait {
285
+ action = "send_bind_email"
286
+ remoteCalls = []string{"POST /user-service/auth/email-send"}
287
+ } else if email != "" {
288
+ action = "bind_email"
289
+ remoteCalls = []string{"GET /user-service/auth/email-status", "POST /user-service/auth/email-send"}
290
+ }
291
+ result := &identity.CommandResult{
292
+ Data: map[string]any{
293
+ "plan": map[string]any{
294
+ "action": action,
295
+ "phone": phone,
296
+ "email": email,
297
+ "remote_calls": remoteCalls,
298
+ },
299
+ },
300
+ Summary: "Dry run: contact binding flow planned",
301
+ }
302
+ return a.renderIdentityResult(cmd, format, result)
303
+ }
304
+ result, err := service.Bind(context.Background(), params)
305
+ if err != nil {
306
+ return a.identityExit(err, "Use an identity that already has a valid JWT.")
307
+ }
308
+ return a.renderIdentityResult(cmd, format, result)
309
+ }
310
+
311
+ func (a *App) runIDResolve(cmd *cobra.Command, args []string) error {
312
+ handle, _ := cmd.Flags().GetString("handle")
313
+ did, _ := cmd.Flags().GetString("did")
314
+ service, format, err := a.identityService()
315
+ if err != nil {
316
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the configured service endpoints.")
317
+ }
318
+ result, err := service.Resolve(context.Background(), handle, did)
319
+ if err != nil {
320
+ return a.identityExit(err, "Provide either --handle or --did, and make sure the target exists.")
321
+ }
322
+ return a.renderIdentityResult(cmd, format, result)
323
+ }
324
+
325
+ func (a *App) runIDRecover(cmd *cobra.Command, args []string) error {
326
+ handle, _ := cmd.Flags().GetString("handle")
327
+ phone, _ := cmd.Flags().GetString("phone")
328
+ otp, _ := cmd.Flags().GetString("otp")
329
+ service, format, err := a.identityService()
330
+ if err != nil {
331
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the configured service endpoints.")
332
+ }
333
+ params := identity.RecoverParams{
334
+ IdentityName: a.globals.Identity,
335
+ Handle: handle,
336
+ Phone: phone,
337
+ OTP: otp,
338
+ }
339
+ if a.globals.DryRun {
340
+ existing, _ := service.Manager().List()
341
+ alias := identity.PreviewNamedIdentity(a.globals.Identity, existing, handle)
342
+ result := &identity.CommandResult{
343
+ Data: map[string]any{
344
+ "plan": map[string]any{
345
+ "action": "recover_handle",
346
+ "identity_name": alias,
347
+ "handle": handle,
348
+ "phone": phone,
349
+ "remote_calls": []string{"did-auth.recover_handle"},
350
+ },
351
+ },
352
+ Summary: "Dry run: handle recovery planned",
353
+ }
354
+ return a.renderIdentityResult(cmd, format, result)
355
+ }
356
+ result, err := service.Recover(context.Background(), params)
357
+ if err != nil {
358
+ return a.identityExit(err, "Make sure the handle exists and the recovery OTP is valid.")
359
+ }
360
+ return a.renderIdentityResult(cmd, format, result)
361
+ }
362
+
363
+ func (a *App) runIDProfileGet(cmd *cobra.Command, args []string) error {
364
+ self, _ := cmd.Flags().GetBool("self")
365
+ handle, _ := cmd.Flags().GetString("handle")
366
+ did, _ := cmd.Flags().GetString("did")
367
+ service, format, err := a.identityService()
368
+ if err != nil {
369
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the configured service endpoints.")
370
+ }
371
+ result, err := service.GetProfile(context.Background(), self, handle, did)
372
+ if err != nil {
373
+ return a.identityExit(err, "Use `--self`, `--handle`, or `--did` to select one profile target.")
374
+ }
375
+ return a.renderIdentityResult(cmd, format, result)
376
+ }
377
+
378
+ func (a *App) runIDProfileSet(cmd *cobra.Command, args []string) error {
379
+ displayName, _ := cmd.Flags().GetString("display-name")
380
+ bio, _ := cmd.Flags().GetString("bio")
381
+ tags, _ := cmd.Flags().GetString("tags")
382
+ markdown, _ := cmd.Flags().GetString("markdown")
383
+ markdownFile, _ := cmd.Flags().GetString("markdown-file")
384
+ if strings.TrimSpace(markdown) != "" && strings.TrimSpace(markdownFile) != "" {
385
+ return output.NewExitError("invalid_argument", 2, "Use either --markdown or --markdown-file, not both.", "Choose one profile body source.")
386
+ }
387
+ service, format, err := a.identityService()
388
+ if err != nil {
389
+ return a.identityExit(err, "Run `awiki-cli id current` to confirm the active identity.")
390
+ }
391
+ params := identity.UpdateProfileParams{
392
+ DisplayName: displayName,
393
+ Bio: bio,
394
+ TagsCSV: tags,
395
+ Markdown: markdown,
396
+ MarkdownFile: markdownFile,
397
+ }
398
+ if a.globals.DryRun {
399
+ result := &identity.CommandResult{
400
+ Data: map[string]any{
401
+ "plan": map[string]any{
402
+ "action": "update_profile",
403
+ "display_name": displayName,
404
+ "bio": bio,
405
+ "tags": tags,
406
+ "markdown": markdown,
407
+ "markdown_file": markdownFile,
408
+ "remote_calls": []string{"did.profile.update_me"},
409
+ "local_writes": []string{"identity.json", "index.json"},
410
+ },
411
+ },
412
+ Summary: "Dry run: profile update planned",
413
+ }
414
+ return a.renderIdentityResult(cmd, format, result)
415
+ }
416
+ result, err := service.SetProfile(context.Background(), params)
417
+ if err != nil {
418
+ return a.identityExit(err, "Use an identity that already has a valid DID JWT.")
419
+ }
420
+ return a.renderIdentityResult(cmd, format, result)
421
+ }
422
+
423
+ func (a *App) runIDImportV1(cmd *cobra.Command, args []string) error {
424
+ name, _ := cmd.Flags().GetString("name")
425
+ importAll, _ := cmd.Flags().GetBool("all")
426
+ service, format, err := a.identityService()
427
+ if err != nil {
428
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the legacy credential paths.")
429
+ }
430
+ if a.globals.DryRun {
431
+ result := &identity.CommandResult{
432
+ Data: map[string]any{
433
+ "plan": map[string]any{
434
+ "action": "import_v1_identities",
435
+ "name": name,
436
+ "all": importAll,
437
+ },
438
+ },
439
+ Summary: "Dry run: v1 credential import planned",
440
+ }
441
+ return a.renderIdentityResult(cmd, format, result)
442
+ }
443
+ result, err := service.ImportV1(name, importAll)
444
+ if err != nil {
445
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the detected v1 credential layout.")
446
+ }
447
+ return a.renderIdentityResult(cmd, format, result)
448
+ }
449
+
450
+ func identityMetaFromData(data map[string]any) *output.IdentityMeta {
451
+ if data == nil {
452
+ return nil
453
+ }
454
+ for _, key := range []string{"identity", "active_identity", "default_identity"} {
455
+ value, ok := data[key]
456
+ if !ok || value == nil {
457
+ continue
458
+ }
459
+ switch typed := value.(type) {
460
+ case *identity.IdentitySummary:
461
+ return &output.IdentityMeta{Name: typed.IdentityName, DID: typed.DID}
462
+ case identity.IdentitySummary:
463
+ return &output.IdentityMeta{Name: typed.IdentityName, DID: typed.DID}
464
+ case map[string]any:
465
+ name, _ := typed["identity_name"].(string)
466
+ did, _ := typed["did"].(string)
467
+ if name != "" || did != "" {
468
+ return &output.IdentityMeta{Name: name, DID: did}
469
+ }
470
+ }
471
+ }
472
+ return nil
473
+ }
@@ -0,0 +1,134 @@
1
+ package cli
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+
8
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
9
+ "github.com/agentconnect/awiki-cli/internal/output"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ // runInit initializes the awiki-cli workdir and a minimal config.json.
14
+ //
15
+ // It is the primary entrypoint for users to discover and prepare the workdir:
16
+ // it uses the same root resolution as other commands (AWIKI_HOME override or
17
+ // the built-in default root) and ensures the directory layout and config.json
18
+ // are in place.
19
+ func (a *App) runInit(cmd *cobra.Command, args []string) error {
20
+ resolved, err := a.resolveConfig()
21
+ if err != nil {
22
+ return output.NewExitError(
23
+ "internal_error",
24
+ 1,
25
+ err.Error(),
26
+ "Run `awiki-cli doctor` to inspect configuration and environment.",
27
+ )
28
+ }
29
+
30
+ format := normalizedFormat(resolved.OutputFormat)
31
+ rootSource := resolved.Sources["root_dir"]
32
+
33
+ dirs := []string{
34
+ resolved.Paths.RootDir,
35
+ filepath.Dir(resolved.Paths.ConfigFile),
36
+ resolved.Paths.DataDir,
37
+ resolved.Paths.StateDir,
38
+ resolved.Paths.CacheDir,
39
+ resolved.Paths.IdentityDir,
40
+ resolved.Paths.LogsDir,
41
+ }
42
+
43
+ if a.globals.DryRun {
44
+ data := map[string]any{
45
+ "plan": map[string]any{
46
+ "action": "init_workdir",
47
+ "root_dir": resolved.Paths.RootDir,
48
+ "root_source": rootSource,
49
+ "directories": dirs,
50
+ "config_file": resolved.Paths.ConfigFile,
51
+ "config_exists": resolved.ConfigExists,
52
+ "config_error": resolved.ConfigError,
53
+ },
54
+ }
55
+ return a.renderSuccess(
56
+ cmd.CommandPath(),
57
+ format,
58
+ a.globals.JQ,
59
+ data,
60
+ "Dry run: workdir initialization planned",
61
+ nil,
62
+ identityMetaFromResolved(resolved),
63
+ )
64
+ }
65
+
66
+ // Avoid silently overwriting a broken config file – require the user to
67
+ // fix or remove it first.
68
+ if resolved.ConfigError != "" {
69
+ return output.NewExitError(
70
+ "invalid_argument",
71
+ 2,
72
+ "config.json exists but failed to parse; fix or remove it before running init.",
73
+ "Run `awiki-cli config show` to inspect the parse error, then correct the JSON syntax.",
74
+ )
75
+ }
76
+
77
+ for _, dir := range dirs {
78
+ if strings.TrimSpace(dir) == "" {
79
+ continue
80
+ }
81
+ if err := os.MkdirAll(dir, 0o700); err != nil {
82
+ return output.NewExitError(
83
+ "internal_error",
84
+ 1,
85
+ err.Error(),
86
+ "Check directory permissions for the awiki-cli work directory.",
87
+ )
88
+ }
89
+ }
90
+
91
+ // If there is no config.json yet, create a minimal one based on the
92
+ // currently resolved defaults so future reads remain consistent.
93
+ if !resolved.ConfigExists {
94
+ cfg := appconfig.FileConfig{}
95
+ cfg.Services.Domain = resolved.DIDDomain
96
+ cfg.Identity.Active = resolved.ActiveIdentity
97
+ cfg.Runtime.Mode = resolved.RuntimeMode
98
+ cfg.Output.Format = resolved.OutputFormat
99
+ noColor := resolved.NoColor
100
+ cfg.Output.NoColor = &noColor
101
+ cfg.Update.DisableStrictVersion = resolved.UpdateDisableStrictVersion
102
+ cfg.Update.MetadataCacheTTLSeconds = resolved.UpdateMetadataCacheTTLSeconds
103
+
104
+ if err := appconfig.WriteFileConfig(resolved.Paths.ConfigFile, cfg); err != nil {
105
+ return output.NewExitError(
106
+ "internal_error",
107
+ 1,
108
+ err.Error(),
109
+ "Check write permissions for config.json under the awiki-cli work directory.",
110
+ )
111
+ }
112
+ resolved.ConfigExists = true
113
+ }
114
+
115
+ result := map[string]any{
116
+ "workdir": map[string]any{
117
+ "root_dir": resolved.Paths.RootDir,
118
+ "root_source": rootSource,
119
+ "paths": resolved.Paths,
120
+ "config_file": resolved.Paths.ConfigFile,
121
+ "config_exists": resolved.ConfigExists,
122
+ },
123
+ }
124
+
125
+ return a.renderSuccess(
126
+ cmd.CommandPath(),
127
+ format,
128
+ a.globals.JQ,
129
+ result,
130
+ "Workdir initialized",
131
+ nil,
132
+ identityMetaFromResolved(resolved),
133
+ )
134
+ }