@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,499 @@
1
+ package cli
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+ "sort"
8
+ "strconv"
9
+ "strings"
10
+
11
+ "github.com/agentconnect/awiki-cli/internal/buildinfo"
12
+ "github.com/agentconnect/awiki-cli/internal/cmdmeta"
13
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
14
+ doccheck "github.com/agentconnect/awiki-cli/internal/doctor"
15
+ "github.com/agentconnect/awiki-cli/internal/output"
16
+ "github.com/agentconnect/awiki-cli/internal/store"
17
+ "github.com/agentconnect/awiki-cli/internal/update"
18
+ "github.com/spf13/cobra"
19
+ )
20
+
21
+ const rootLong = `awiki-cli — Agent-native identity and messaging CLI.
22
+
23
+ Phase 1 currently provides the pure-Go CLI shell, global output contract,
24
+ static schema introspection, built-in docs, and baseline environment checks.
25
+
26
+ Use "awiki-cli schema" to inspect the frozen command contract and
27
+ "awiki-cli doctor" to inspect paths, env compatibility, and migration hints.`
28
+
29
+ func newRootCommand(app *App) *cobra.Command {
30
+ rootCmd := &cobra.Command{
31
+ Use: "awiki-cli",
32
+ Short: "awiki CLI phase-1 shell",
33
+ Long: rootLong,
34
+ SilenceErrors: true,
35
+ SilenceUsage: true,
36
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
37
+ app.globals.FormatChanged = cmd.Flags().Changed("format")
38
+ app.globals.IdentityChanged = cmd.Flags().Changed("identity")
39
+ _, err := output.NormalizeFormat(app.globals.Format)
40
+ if err != nil {
41
+ return output.NewExitError("invalid_argument", 2, err.Error(), "Use --format json, pretty, ndjson, or table.")
42
+ }
43
+ return app.maybeCheckForUpdates(cmd)
44
+ },
45
+ }
46
+ rootCmd.PersistentFlags().StringVar(&app.globals.Format, "format", string(output.FormatJSON), "Output format: json | pretty | ndjson | table")
47
+ rootCmd.PersistentFlags().StringVar(&app.globals.JQ, "jq", "", "Apply a jq expression to the JSON envelope")
48
+ rootCmd.PersistentFlags().BoolVar(&app.globals.DryRun, "dry-run", false, "Render the execution plan without mutating state")
49
+ rootCmd.PersistentFlags().StringVar(&app.globals.Identity, "identity", "", "Select the active identity")
50
+ rootCmd.PersistentFlags().BoolVar(&app.globals.Verbose, "verbose", false, "Enable verbose output")
51
+
52
+ commandsByName := map[string]*cobra.Command{"": rootCmd}
53
+ specs := app.catalog.Specs()
54
+ sort.Slice(specs, func(i, j int) bool {
55
+ leftDepth := strings.Count(specs[i].Name, ".")
56
+ rightDepth := strings.Count(specs[j].Name, ".")
57
+ if leftDepth == rightDepth {
58
+ return specs[i].Name < specs[j].Name
59
+ }
60
+ return leftDepth < rightDepth
61
+ })
62
+
63
+ for _, spec := range specs {
64
+ command := app.commandFromSpec(spec)
65
+ parent := commandsByName[parentName(spec.Name)]
66
+ if parent == nil {
67
+ panic(fmt.Sprintf("missing parent command for %s", spec.Name))
68
+ }
69
+ parent.AddCommand(command)
70
+ commandsByName[strings.ToLower(spec.Name)] = command
71
+ }
72
+ if runtimeListener := commandsByName["runtime.listener"]; runtimeListener != nil {
73
+ runtimeListener.AddCommand(&cobra.Command{
74
+ Use: "run",
75
+ Short: "Run the websocket listener in the foreground",
76
+ Hidden: true,
77
+ RunE: app.runRuntimeListenerRun,
78
+ })
79
+ }
80
+ return rootCmd
81
+ }
82
+
83
+ func parentName(name string) string {
84
+ trimmed := strings.ToLower(strings.TrimSpace(name))
85
+ index := strings.LastIndex(trimmed, ".")
86
+ if index < 0 {
87
+ return ""
88
+ }
89
+ return trimmed[:index]
90
+ }
91
+
92
+ func (a *App) commandFromSpec(spec cmdmeta.CommandSpec) *cobra.Command {
93
+ command := &cobra.Command{
94
+ Use: spec.Use,
95
+ Short: spec.Short,
96
+ Long: spec.Long,
97
+ Aliases: spec.Aliases,
98
+ Hidden: spec.Hidden,
99
+ }
100
+ for _, flag := range spec.Flags {
101
+ switch flag.Type {
102
+ case "string":
103
+ command.Flags().String(flag.Name, flag.Default, flag.Usage)
104
+ case "bool":
105
+ defaultValue := strings.EqualFold(flag.Default, "true")
106
+ command.Flags().Bool(flag.Name, defaultValue, flag.Usage)
107
+ case "int":
108
+ defaultValue := 0
109
+ if strings.TrimSpace(flag.Default) != "" {
110
+ if parsed, err := strconv.Atoi(flag.Default); err == nil {
111
+ defaultValue = parsed
112
+ }
113
+ }
114
+ command.Flags().Int(flag.Name, defaultValue, flag.Usage)
115
+ default:
116
+ command.Flags().String(flag.Name, flag.Default, flag.Usage)
117
+ }
118
+ if flag.Required {
119
+ _ = command.MarkFlagRequired(flag.Name)
120
+ }
121
+ }
122
+ if handler := a.handlerFor(spec); handler != nil {
123
+ command.RunE = handler
124
+ }
125
+ return command
126
+ }
127
+
128
+ func (a *App) handlerFor(spec cmdmeta.CommandSpec) func(*cobra.Command, []string) error {
129
+ switch spec.Handler {
130
+ case "init":
131
+ return a.runInit
132
+ case "status":
133
+ return a.runStatus
134
+ case "docs":
135
+ return a.runDocs
136
+ case "schema":
137
+ return a.runSchema
138
+ case "doctor":
139
+ return a.runDoctor
140
+ case "version":
141
+ return a.runVersion
142
+ case "upgrade":
143
+ return a.runUpgrade
144
+ case "config.show":
145
+ return a.runConfigShow
146
+ case "id.status":
147
+ return a.runIDStatus
148
+ case "id.create":
149
+ return a.runIDCreate
150
+ case "id.register":
151
+ return a.runIDRegister
152
+ case "id.bind":
153
+ return a.runIDBind
154
+ case "id.resolve":
155
+ return a.runIDResolve
156
+ case "id.recover":
157
+ return a.runIDRecover
158
+ case "id.list":
159
+ return a.runIDList
160
+ case "id.current":
161
+ return a.runIDCurrent
162
+ case "id.use":
163
+ return a.runIDUse
164
+ case "id.profile.get":
165
+ return a.runIDProfileGet
166
+ case "id.profile.set":
167
+ return a.runIDProfileSet
168
+ case "id.import-v1":
169
+ return a.runIDImportV1
170
+ case "msg.send":
171
+ return a.runMsgSend
172
+ case "msg.inbox":
173
+ return a.runMsgInbox
174
+ case "msg.history":
175
+ return a.runMsgHistory
176
+ case "msg.mark-read":
177
+ return a.runMsgMarkRead
178
+ case "group.create":
179
+ return a.runGroupCreate
180
+ case "group.show":
181
+ return a.runGroupShow
182
+ case "group.get":
183
+ return a.runGroupShow
184
+ case "group.join":
185
+ return a.runGroupJoin
186
+ case "group.add":
187
+ return a.runGroupAdd
188
+ case "group.kick":
189
+ return a.runGroupKick
190
+ case "group.remove":
191
+ return a.runGroupKick
192
+ case "group.leave":
193
+ return a.runGroupLeave
194
+ case "group.update":
195
+ return a.runGroupUpdate
196
+ case "group.members":
197
+ return a.runGroupMembers
198
+ case "group.messages":
199
+ return a.runGroupMessages
200
+ case "page.create":
201
+ return a.runPageCreate
202
+ case "page.list":
203
+ return a.runPageList
204
+ case "page.get":
205
+ return a.runPageGet
206
+ case "page.update":
207
+ return a.runPageUpdate
208
+ case "page.rename":
209
+ return a.runPageRename
210
+ case "page.delete":
211
+ return a.runPageDelete
212
+ case "runtime.status":
213
+ return a.runRuntimeStatus
214
+ case "runtime.setup":
215
+ return a.runRuntimeSetup
216
+ case "runtime.mode.get":
217
+ return a.runRuntimeModeGet
218
+ case "runtime.mode.set":
219
+ return a.runRuntimeModeSet
220
+ case "runtime.listener.status":
221
+ return a.runRuntimeListenerStatus
222
+ case "runtime.listener.install":
223
+ return a.runRuntimeListenerInstall
224
+ case "runtime.listener.start":
225
+ return a.runRuntimeListenerStart
226
+ case "runtime.listener.stop":
227
+ return a.runRuntimeListenerStop
228
+ case "runtime.listener.restart":
229
+ return a.runRuntimeListenerRestart
230
+ case "runtime.listener.uninstall":
231
+ return a.runRuntimeListenerUninstall
232
+ case "debug.db.query":
233
+ return a.runDebugDBQuery
234
+ case "debug.db.import-v1":
235
+ return a.runDebugDBImportV1
236
+ case "completion.bash":
237
+ return func(cmd *cobra.Command, args []string) error { return cmd.Root().GenBashCompletion(cmd.OutOrStdout()) }
238
+ case "completion.zsh":
239
+ return func(cmd *cobra.Command, args []string) error { return cmd.Root().GenZshCompletion(cmd.OutOrStdout()) }
240
+ case "completion.fish":
241
+ return func(cmd *cobra.Command, args []string) error {
242
+ return cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true)
243
+ }
244
+ case "completion.powershell":
245
+ return func(cmd *cobra.Command, args []string) error {
246
+ return cmd.Root().GenPowerShellCompletionWithDesc(cmd.OutOrStdout())
247
+ }
248
+ case "stub":
249
+ return a.runStub
250
+ default:
251
+ return nil
252
+ }
253
+ }
254
+
255
+ func (a *App) runStatus(cmd *cobra.Command, args []string) error {
256
+ service, format, err := a.identityService()
257
+ if err != nil {
258
+ return output.NewExitError("internal_error", 1, err.Error(), "Check your local configuration and environment variables.")
259
+ }
260
+ resolved := service.Config()
261
+ result, err := service.Status()
262
+ if err != nil {
263
+ return a.identityExit(err, "Run `awiki-cli doctor` to inspect the local identity store.")
264
+ }
265
+ data := map[string]any{
266
+ "cli": map[string]any{
267
+ "phase": "phase1-shell",
268
+ "version": buildinfo.Current(),
269
+ },
270
+ "paths": resolved.Paths,
271
+ "state": result.Data,
272
+ "config": map[string]any{
273
+ "config_exists": resolved.ConfigExists,
274
+ "config_error": resolved.ConfigError,
275
+ "env_hits": resolved.EnvHits,
276
+ "sources": resolved.Sources,
277
+ },
278
+ }
279
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, result.Summary, result.Warnings, identityMetaFromResolved(resolved))
280
+ }
281
+
282
+ func (a *App) runDocs(cmd *cobra.Command, args []string) error {
283
+ resolved, err := a.resolveConfig()
284
+ if err != nil {
285
+ return output.NewExitError("internal_error", 1, err.Error(), "Check your local configuration and environment variables.")
286
+ }
287
+ format := normalizedFormat(resolved.OutputFormat)
288
+ if len(args) == 0 {
289
+ data := map[string]any{"topics": a.docs.All()}
290
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, "Available documentation topics", nil, identityMetaFromResolved(resolved))
291
+ }
292
+ if len(args) > 1 {
293
+ return output.NewExitError("invalid_argument", 2, "docs accepts at most one topic.", "Run `awiki-cli docs` without arguments to list topics.")
294
+ }
295
+ topic, ok := a.docs.Lookup(args[0])
296
+ if !ok {
297
+ return output.NewExitError("not_found", 5, fmt.Sprintf("Unknown docs topic %q", args[0]), "Run `awiki-cli docs` to list available topics.")
298
+ }
299
+ data := map[string]any{"topic": topic}
300
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, fmt.Sprintf("Documentation topic %s", topic.Name), nil, identityMetaFromResolved(resolved))
301
+ }
302
+
303
+ func (a *App) runSchema(cmd *cobra.Command, args []string) error {
304
+ resolved, err := a.resolveConfig()
305
+ if err != nil {
306
+ return output.NewExitError("internal_error", 1, err.Error(), "Check your local configuration and environment variables.")
307
+ }
308
+ format := normalizedFormat(resolved.OutputFormat)
309
+ if len(args) == 0 {
310
+ data := map[string]any{
311
+ "commands": a.catalog.Specs(),
312
+ "phase": "phase1-shell",
313
+ }
314
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, "Static command contract", nil, identityMetaFromResolved(resolved))
315
+ }
316
+ lookupTarget := strings.Join(args, " ")
317
+ spec, ok := a.catalog.Lookup(lookupTarget)
318
+ if !ok {
319
+ return output.NewExitError("not_found", 5, fmt.Sprintf("Unknown command schema target %q", lookupTarget), "Use `awiki-cli schema` to list command contracts.")
320
+ }
321
+ data := map[string]any{
322
+ "command": spec,
323
+ "children": a.catalog.ChildrenOf(spec.Name),
324
+ }
325
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, fmt.Sprintf("Static contract for %s", spec.Name), nil, identityMetaFromResolved(resolved))
326
+ }
327
+
328
+ func (a *App) runDoctor(cmd *cobra.Command, args []string) error {
329
+ resolved, err := a.resolveConfig()
330
+ if err != nil {
331
+ return output.NewExitError("internal_error", 1, err.Error(), "Check your local configuration and environment variables.")
332
+ }
333
+ format := normalizedFormat(resolved.OutputFormat)
334
+ report := doccheck.Run(resolved)
335
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, report, report.Summary, nil, identityMetaFromResolved(resolved))
336
+ }
337
+
338
+ func (a *App) runVersion(cmd *cobra.Command, args []string) error {
339
+ resolved, err := a.resolveConfig()
340
+ if err != nil {
341
+ return output.NewExitError("internal_error", 1, err.Error(), "Check your local configuration and environment variables.")
342
+ }
343
+ format := normalizedFormat(resolved.OutputFormat)
344
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, buildinfo.Current(), "Build information", nil, identityMetaFromResolved(resolved))
345
+ }
346
+
347
+ func (a *App) runConfigShow(cmd *cobra.Command, args []string) error {
348
+ service, format, err := a.identityService()
349
+ if err != nil {
350
+ return output.NewExitError("internal_error", 1, err.Error(), "Check your local configuration and environment variables.")
351
+ }
352
+ resolved := service.Config()
353
+ current, _ := service.Manager().Current()
354
+ legacy, _ := service.Manager().ScanLegacy()
355
+ data := appconfig.Snapshot(resolved)
356
+ database := map[string]any{
357
+ "database_file": resolved.Paths.DatabaseFile,
358
+ "exists": fileExists(resolved.Paths.DatabaseFile),
359
+ }
360
+ if database["exists"] == true {
361
+ if db, err := store.OpenReadOnly(resolved.Paths.DatabaseFile); err == nil {
362
+ defer db.Close()
363
+ if version, err := store.CurrentSchemaVersion(db); err == nil {
364
+ database["schema_version"] = version
365
+ database["target_schema_version"] = store.SchemaVersion
366
+ } else {
367
+ database["schema_error"] = err.Error()
368
+ }
369
+ } else {
370
+ database["open_error"] = err.Error()
371
+ }
372
+ }
373
+ data["identity_store"] = map[string]any{
374
+ "identity_dir": resolved.Paths.IdentityDir,
375
+ "index_file": filepathJoin(resolved.Paths.IdentityDir, "index.json"),
376
+ "default_identity": current,
377
+ "legacy_scan": legacy,
378
+ }
379
+ data["database"] = database
380
+ return a.renderSuccess(cmd.CommandPath(), format, a.globals.JQ, data, "Resolved configuration", nil, identityMetaFromResolved(resolved))
381
+ }
382
+
383
+ func (a *App) runStub(cmd *cobra.Command, args []string) error {
384
+ spec, ok := a.catalog.Lookup(strings.TrimPrefix(cmd.CommandPath(), "awiki-cli "))
385
+ if !ok {
386
+ return output.NewExitError("internal_error", 1, "Command metadata is missing.", "Run `awiki-cli schema` to inspect the current catalog.")
387
+ }
388
+ hint := fmt.Sprintf("%s is planned for %s. Use `awiki-cli schema %s` to inspect the frozen contract.", cmd.CommandPath(), strings.ToUpper(spec.Phase), spec.Name)
389
+ return output.NewExitError("internal_error", 1, fmt.Sprintf("%s is not implemented yet.", cmd.CommandPath()), hint)
390
+ }
391
+
392
+ func identityMetaFromResolved(resolved *appconfig.Resolved) *output.IdentityMeta {
393
+ if resolved == nil || strings.TrimSpace(resolved.ActiveIdentity) == "" {
394
+ return nil
395
+ }
396
+ return &output.IdentityMeta{Name: resolved.ActiveIdentity}
397
+ }
398
+
399
+ func fileExists(path string) bool {
400
+ _, err := os.Stat(path)
401
+ return err == nil
402
+ }
403
+
404
+ func filepathJoin(parts ...string) string {
405
+ return filepath.Join(parts...)
406
+ }
407
+
408
+ func normalizedFormat(raw string) output.Format {
409
+ format, err := output.NormalizeFormat(raw)
410
+ if err != nil {
411
+ return output.FormatJSON
412
+ }
413
+ return format
414
+ }
415
+
416
+ // maybeCheckForUpdates performs a best-effort version policy check before
417
+ // running most commands. It is intentionally soft-fail: network / metadata
418
+ // errors do not break the CLI, but when we have a clear "below minSupported"
419
+ // signal we block remote-affecting commands.
420
+ func (a *App) maybeCheckForUpdates(cmd *cobra.Command) error {
421
+ // Some commands must always be available regardless of version policy.
422
+ if isUpdateExemptCommand(cmd) {
423
+ return nil
424
+ }
425
+
426
+ // Resolve config to get update-related knobs and cache paths.
427
+ resolved, err := a.resolveConfig()
428
+ if err != nil {
429
+ // Config errors are surfaced by individual commands; do not double-fail here.
430
+ return nil
431
+ }
432
+
433
+ decision, err := update.Check(resolved)
434
+ if err != nil {
435
+ // Best-effort: log to stderr in verbose mode, but never break the command.
436
+ if a.globals.Verbose {
437
+ fmt.Fprintf(os.Stderr, "[awiki-cli] update check failed: %v\n", err)
438
+ }
439
+ return nil
440
+ }
441
+
442
+ if decision.Blocked {
443
+ summary := fmt.Sprintf(
444
+ "awiki-cli %s is no longer supported (minimum supported version is %s).",
445
+ decision.CurrentVersion,
446
+ decision.MinSupportedVersion,
447
+ )
448
+ hint := "Please upgrade awiki-cli before running this command. Run `awiki-cli upgrade` or `npm install -g @awiki/cli@latest`."
449
+ return output.NewExitError("version_unsupported", 3, summary, hint)
450
+ }
451
+
452
+ if decision.HasNewerVersion && !decision.StrictDisabled && !decision.DevBuild {
453
+ a.updateWarning = fmt.Sprintf(
454
+ "A newer awiki-cli version (%s) is available; you are running %s. Run `awiki-cli upgrade` for details.",
455
+ decision.LatestVersion,
456
+ decision.CurrentVersion,
457
+ )
458
+ }
459
+ return nil
460
+ }
461
+
462
+ func isUpdateExemptCommand(cmd *cobra.Command) bool {
463
+ if cmd == nil {
464
+ return false
465
+ }
466
+ path := strings.TrimSpace(cmd.CommandPath())
467
+ if path == "" {
468
+ return false
469
+ }
470
+
471
+ // Allow basic local inspection / help commands even when the binary is old.
472
+ exempt := []string{
473
+ "awiki-cli help",
474
+ "awiki-cli version",
475
+ "awiki-cli upgrade",
476
+ "awiki-cli init",
477
+ "awiki-cli docs",
478
+ "awiki-cli schema",
479
+ "awiki-cli config show",
480
+ "awiki-cli doctor",
481
+ "awiki-cli completion",
482
+ "awiki-cli completion bash",
483
+ "awiki-cli completion zsh",
484
+ "awiki-cli completion fish",
485
+ "awiki-cli completion powershell",
486
+ }
487
+ for _, allowed := range exempt {
488
+ if strings.EqualFold(path, allowed) {
489
+ return true
490
+ }
491
+ }
492
+
493
+ // Cobra may construct aliases / nested paths; treat any "help ..." as exempt.
494
+ if strings.HasPrefix(strings.ToLower(path), "awiki-cli help ") {
495
+ return true
496
+ }
497
+
498
+ return false
499
+ }