@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,399 @@
1
+ package config
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "runtime"
10
+ "strings"
11
+ )
12
+
13
+ const (
14
+ appName = "awiki-cli"
15
+ legacySkillName = "awiki-agent-id-message"
16
+ defaultDomain = "awiki.ai"
17
+ defaultService = "https://" + defaultDomain
18
+ defaultDIDDomain = defaultDomain
19
+ )
20
+
21
+ type Overrides struct {
22
+ Identity string
23
+ IdentityChanged bool
24
+ Format string
25
+ FormatChanged bool
26
+ }
27
+
28
+ // Paths describes all filesystem locations used by awiki-cli.
29
+ // After the config/workdir refactor, everything is derived from a single
30
+ // root directory:
31
+ //
32
+ // rootDir/config.json
33
+ // rootDir/db/awiki-cli.db
34
+ // rootDir/identities/...
35
+ // rootDir/logs/...
36
+ // rootDir/cache/...
37
+ // rootDir/tmp/...
38
+ //
39
+ // Legacy v1 artefacts still live under ~/.openclaw/... and are exposed via
40
+ // LegacyCredentialsDir / LegacyDataDir for import/doctor commands.
41
+ type Paths struct {
42
+ RootDir string `json:"root_dir"`
43
+ ConfigDir string `json:"config_dir"`
44
+ DataDir string `json:"data_dir"`
45
+ StateDir string `json:"state_dir"`
46
+ CacheDir string `json:"cache_dir"`
47
+ ConfigFile string `json:"config_file"`
48
+ IdentityDir string `json:"identity_dir"`
49
+ DatabaseFile string `json:"database_file"`
50
+ LogsDir string `json:"logs_dir"`
51
+ LegacyCredentialsDir string `json:"legacy_credentials_dir"`
52
+ LegacyDataDir string `json:"legacy_data_dir"`
53
+ }
54
+
55
+ // FileConfig mirrors the on-disk JSON config structure under <AWIKI_HOME>/config.json.
56
+ type FileConfig struct {
57
+ Services struct {
58
+ Domain string `json:"domain"`
59
+ } `json:"services"`
60
+ Identity struct {
61
+ Active string `json:"active"`
62
+ } `json:"identity"`
63
+ Runtime struct {
64
+ Mode string `json:"mode"`
65
+ } `json:"runtime"`
66
+ Output struct {
67
+ Format string `json:"format"`
68
+ NoColor *bool `json:"no_color"`
69
+ } `json:"output"`
70
+ Update struct {
71
+ DisableStrictVersion bool `json:"disable_strict_version"`
72
+ MetadataCacheTTLSeconds int `json:"metadata_cache_ttl_seconds"`
73
+ } `json:"update"`
74
+ }
75
+
76
+ type EnvHit struct {
77
+ Key string `json:"key"`
78
+ Value string `json:"value"`
79
+ Tier string `json:"tier"`
80
+ Target string `json:"target"`
81
+ }
82
+
83
+ type ValueSource struct {
84
+ Source string `json:"source"`
85
+ Key string `json:"key,omitempty"`
86
+ Value string `json:"value,omitempty"`
87
+ }
88
+
89
+ type Resolved struct {
90
+ Paths Paths `json:"paths"`
91
+ ActiveIdentity string `json:"active_identity,omitempty"`
92
+ RuntimeMode string `json:"runtime_mode"`
93
+ RuntimeSocketPath string `json:"runtime_socket_path,omitempty"`
94
+ OutputFormat string `json:"output_format"`
95
+ NoColor bool `json:"no_color"`
96
+ UserServiceURL string `json:"user_service_url"`
97
+ MessageServiceURL string `json:"message_service_url"`
98
+ MessageServiceWSURL string `json:"message_service_ws_url,omitempty"`
99
+ DIDDomain string `json:"did_domain"`
100
+ CABundle string `json:"ca_bundle,omitempty"`
101
+
102
+ UpdateDisableStrictVersion bool `json:"update_disable_strict_version"`
103
+ UpdateMetadataCacheTTLSeconds int `json:"update_metadata_cache_ttl_seconds"`
104
+
105
+ ConfigExists bool `json:"config_exists"`
106
+ ConfigError string `json:"config_error,omitempty"`
107
+ EnvHits []EnvHit `json:"env_hits,omitempty"`
108
+ Sources map[string]ValueSource `json:"sources"`
109
+ }
110
+
111
+ func Resolve(overrides Overrides) (*Resolved, error) {
112
+ home, err := os.UserHomeDir()
113
+ if err != nil {
114
+ return nil, fmt.Errorf("resolve user home: %w", err)
115
+ }
116
+
117
+ rootDir, rootSource := resolveRootDir(home)
118
+ paths, err := buildPaths(home, rootDir)
119
+ if err != nil {
120
+ return nil, err
121
+ }
122
+
123
+ resolved := &Resolved{
124
+ Paths: paths,
125
+ RuntimeMode: "http",
126
+ OutputFormat: "json",
127
+ UserServiceURL: defaultService,
128
+ MessageServiceURL: defaultService,
129
+ DIDDomain: defaultDIDDomain,
130
+ CABundle: "",
131
+ Sources: map[string]ValueSource{
132
+ "root_dir": rootSource,
133
+ },
134
+ }
135
+ resolved.EnvHits = collectEnvHits()
136
+
137
+ fileConfig, configExists, configError := loadFileConfig(paths.ConfigFile)
138
+ resolved.ConfigExists = configExists
139
+ if configError != nil {
140
+ resolved.ConfigError = configError.Error()
141
+ }
142
+
143
+ // Identity
144
+ resolved.ActiveIdentity, resolved.Sources["active_identity"] = resolveString(
145
+ overrides.Identity,
146
+ overrides.IdentityChanged,
147
+ "AWIKI_IDENTITY",
148
+ fileConfig.Identity.Active,
149
+ "",
150
+ )
151
+
152
+ // Runtime mode
153
+ resolved.RuntimeMode, resolved.Sources["runtime_mode"] = resolveString(
154
+ "",
155
+ false,
156
+ "AWIKI_RUNTIME_MODE",
157
+ fileConfig.Runtime.Mode,
158
+ "http",
159
+ )
160
+
161
+ // Runtime socket path: derived from state dir, not user-configurable for now.
162
+ defaultSocket := filepath.Join(paths.StateDir, "runtime", "message-daemon.sock")
163
+ resolved.RuntimeSocketPath = defaultSocket
164
+ resolved.Sources["runtime_socket_path"] = ValueSource{
165
+ Source: "derived",
166
+ Value: defaultSocket,
167
+ }
168
+
169
+ // Output format
170
+ resolved.OutputFormat, resolved.Sources["output_format"] = resolveString(
171
+ overrides.Format,
172
+ overrides.FormatChanged,
173
+ "AWIKI_FORMAT",
174
+ fileConfig.Output.Format,
175
+ "json",
176
+ )
177
+ if strings.TrimSpace(resolved.OutputFormat) == "" {
178
+ resolved.OutputFormat = "json"
179
+ }
180
+
181
+ // NoColor bool with env > config > default precedence.
182
+ resolved.NoColor, resolved.Sources["no_color"] = resolveBool(
183
+ "AWIKI_NO_COLOR",
184
+ fileConfig.Output.NoColor,
185
+ false,
186
+ )
187
+
188
+ // Services domain and derived URLs.
189
+ domain, domainSource := resolveString(
190
+ "",
191
+ false,
192
+ "",
193
+ fileConfig.Services.Domain,
194
+ defaultDomain,
195
+ )
196
+ domain = strings.TrimSpace(domain)
197
+ resolved.Sources["services_domain"] = domainSource
198
+
199
+ resolved.DIDDomain = domain
200
+ resolved.Sources["did_domain"] = ValueSource{
201
+ Source: domainSource.Source,
202
+ Value: domain,
203
+ }
204
+
205
+ resolved.UserServiceURL = "https://" + domain
206
+ resolved.Sources["user_service_url"] = ValueSource{
207
+ Source: "derived",
208
+ Value: resolved.UserServiceURL,
209
+ }
210
+
211
+ resolved.MessageServiceURL = "https://" + domain + "/message-service"
212
+ resolved.Sources["message_service_url"] = ValueSource{
213
+ Source: "derived",
214
+ Value: resolved.MessageServiceURL,
215
+ }
216
+
217
+ resolved.MessageServiceWSURL = "wss://" + domain + "/message-service/ws"
218
+ resolved.Sources["message_service_ws_url"] = ValueSource{
219
+ Source: "derived",
220
+ Value: resolved.MessageServiceWSURL,
221
+ }
222
+
223
+ // Update-related knobs (no env overrides for now).
224
+ resolved.UpdateDisableStrictVersion = fileConfig.Update.DisableStrictVersion
225
+ resolved.Sources["update_disable_strict_version"] = ValueSource{
226
+ Source: "config_file_or_default",
227
+ Value: fmt.Sprintf("%t", resolved.UpdateDisableStrictVersion),
228
+ }
229
+
230
+ resolved.UpdateMetadataCacheTTLSeconds = fileConfig.Update.MetadataCacheTTLSeconds
231
+ resolved.Sources["update_metadata_cache_ttl_seconds"] = ValueSource{
232
+ Source: "config_file_or_default",
233
+ Value: fmt.Sprintf("%d", resolved.UpdateMetadataCacheTTLSeconds),
234
+ }
235
+
236
+ return resolved, nil
237
+ }
238
+
239
+ func Snapshot(resolved *Resolved) map[string]any {
240
+ if resolved == nil {
241
+ return map[string]any{}
242
+ }
243
+ return map[string]any{
244
+ "paths": resolved.Paths,
245
+ "active_identity": resolved.ActiveIdentity,
246
+ "runtime_mode": resolved.RuntimeMode,
247
+ "runtime_socket_path": resolved.RuntimeSocketPath,
248
+ "output_format": resolved.OutputFormat,
249
+ "no_color": resolved.NoColor,
250
+ "user_service_url": resolved.UserServiceURL,
251
+ "message_service_url": resolved.MessageServiceURL,
252
+ "message_service_ws_url": resolved.MessageServiceWSURL,
253
+ "did_domain": resolved.DIDDomain,
254
+ "ca_bundle": resolved.CABundle,
255
+ "update_disable_strict_version": resolved.UpdateDisableStrictVersion,
256
+ "update_metadata_cache_ttl_seconds": resolved.UpdateMetadataCacheTTLSeconds,
257
+ "config_exists": resolved.ConfigExists,
258
+ "config_error": resolved.ConfigError,
259
+ "env_hits": resolved.EnvHits,
260
+ "sources": resolved.Sources,
261
+ }
262
+ }
263
+
264
+ // DefaultRootDir returns the built-in default workdir root for the current
265
+ // platform without considering AWIKI_HOME.
266
+ func DefaultRootDir(home string) string {
267
+ if runtime.GOOS == "windows" {
268
+ base := strings.TrimSpace(os.Getenv("LOCALAPPDATA"))
269
+ if base == "" {
270
+ base = filepath.Join(home, "AppData", "Local")
271
+ }
272
+ return filepath.Join(base, "AwikiCli")
273
+ }
274
+ return filepath.Join(home, "."+appName)
275
+ }
276
+
277
+ func resolveRootDir(home string) (string, ValueSource) {
278
+ // 1. Explicit AWIKI_HOME override (advanced / CI use).
279
+ raw := strings.TrimSpace(os.Getenv("AWIKI_HOME"))
280
+ if raw != "" {
281
+ root := ExpandHome(home, raw)
282
+ return root, ValueSource{
283
+ Source: "canonical_env",
284
+ Key: "AWIKI_HOME",
285
+ Value: root,
286
+ }
287
+ }
288
+
289
+ // 2. Fallback to the built-in default root.
290
+ root := DefaultRootDir(home)
291
+ return root, ValueSource{
292
+ Source: "default",
293
+ Value: root,
294
+ }
295
+ }
296
+
297
+ func buildPaths(home, root string) (Paths, error) {
298
+ // 尽早为用户创建工作目录根(AWIKI_HOME),便于发现 config.json 等文件。
299
+ if err := os.MkdirAll(root, 0o700); err != nil {
300
+ return Paths{}, fmt.Errorf("create workdir root %s: %w", root, err)
301
+ }
302
+
303
+ configDir := root
304
+ dataDir := filepath.Join(root, "db")
305
+ stateDir := filepath.Join(root, "tmp")
306
+ cacheDir := filepath.Join(root, "cache")
307
+ identityDir := filepath.Join(root, "identities")
308
+ logsDir := filepath.Join(root, "logs")
309
+
310
+ legacyCredentialsDir := filepath.Join(home, ".openclaw", "credentials", legacySkillName)
311
+ legacyDataDir := filepath.Join(home, ".openclaw", "workspace", "data", legacySkillName)
312
+
313
+ return Paths{
314
+ RootDir: root,
315
+ ConfigDir: configDir,
316
+ DataDir: dataDir,
317
+ StateDir: stateDir,
318
+ CacheDir: cacheDir,
319
+ ConfigFile: filepath.Join(configDir, "config.json"),
320
+ IdentityDir: identityDir,
321
+ DatabaseFile: filepath.Join(dataDir, appName+".db"),
322
+ LogsDir: logsDir,
323
+ LegacyCredentialsDir: legacyCredentialsDir,
324
+ LegacyDataDir: legacyDataDir,
325
+ }, nil
326
+ }
327
+
328
+ func loadFileConfig(path string) (FileConfig, bool, error) {
329
+ var config FileConfig
330
+ raw, err := os.ReadFile(path)
331
+ if err != nil {
332
+ if errors.Is(err, os.ErrNotExist) {
333
+ return config, false, nil
334
+ }
335
+ return config, false, err
336
+ }
337
+ if err := json.Unmarshal(raw, &config); err != nil {
338
+ return config, true, err
339
+ }
340
+ return config, true, nil
341
+ }
342
+
343
+ func resolveString(flagValue string, flagChanged bool, envKey string, fileValue string, defaultValue string) (string, ValueSource) {
344
+ if flagChanged && strings.TrimSpace(flagValue) != "" {
345
+ value := strings.TrimSpace(flagValue)
346
+ return value, ValueSource{Source: "flag", Value: value}
347
+ }
348
+ if strings.TrimSpace(envKey) != "" {
349
+ if value := strings.TrimSpace(os.Getenv(envKey)); value != "" {
350
+ return value, ValueSource{Source: "canonical_env", Key: envKey, Value: value}
351
+ }
352
+ }
353
+ if strings.TrimSpace(fileValue) != "" {
354
+ value := strings.TrimSpace(fileValue)
355
+ return value, ValueSource{Source: "config_file", Value: value}
356
+ }
357
+ return defaultValue, ValueSource{Source: "default", Value: defaultValue}
358
+ }
359
+
360
+ func resolveBool(envKey string, fileValue *bool, defaultValue bool) (bool, ValueSource) {
361
+ if strings.TrimSpace(envKey) != "" {
362
+ if value := strings.TrimSpace(os.Getenv(envKey)); value != "" {
363
+ parsed := strings.EqualFold(value, "1") ||
364
+ strings.EqualFold(value, "true") ||
365
+ strings.EqualFold(value, "yes")
366
+ return parsed, ValueSource{Source: "canonical_env", Key: envKey, Value: value}
367
+ }
368
+ }
369
+ if fileValue != nil {
370
+ return *fileValue, ValueSource{Source: "config_file", Value: fmt.Sprintf("%t", *fileValue)}
371
+ }
372
+ return defaultValue, ValueSource{Source: "default", Value: fmt.Sprintf("%t", defaultValue)}
373
+ }
374
+
375
+ // ExpandHome resolves "~/..." style paths against the provided home directory.
376
+ func ExpandHome(home string, value string) string {
377
+ if strings.HasPrefix(value, "~/") {
378
+ return filepath.Join(home, strings.TrimPrefix(value, "~/"))
379
+ }
380
+ return value
381
+ }
382
+
383
+ func collectEnvHits() []EnvHit {
384
+ definitions := []EnvHit{
385
+ {Key: "AWIKI_HOME", Tier: "canonical_env", Target: "root_dir"},
386
+ {Key: "AWIKI_IDENTITY", Tier: "canonical_env", Target: "active_identity"},
387
+ {Key: "AWIKI_RUNTIME_MODE", Tier: "canonical_env", Target: "runtime_mode"},
388
+ {Key: "AWIKI_FORMAT", Tier: "canonical_env", Target: "output_format"},
389
+ {Key: "AWIKI_NO_COLOR", Tier: "canonical_env", Target: "no_color"},
390
+ }
391
+ hits := make([]EnvHit, 0, len(definitions))
392
+ for _, definition := range definitions {
393
+ if value := strings.TrimSpace(os.Getenv(definition.Key)); value != "" {
394
+ definition.Value = value
395
+ hits = append(hits, definition)
396
+ }
397
+ }
398
+ return hits
399
+ }
@@ -0,0 +1,104 @@
1
+ package config
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ func writeTestConfig(t *testing.T, root string, cfg FileConfig) {
11
+ t.Helper()
12
+ if err := os.MkdirAll(root, 0o700); err != nil {
13
+ t.Fatalf("os.MkdirAll() error = %v", err)
14
+ }
15
+ raw, err := json.Marshal(cfg)
16
+ if err != nil {
17
+ t.Fatalf("json.Marshal() error = %v", err)
18
+ }
19
+ if err := os.WriteFile(filepath.Join(root, "config.json"), raw, 0o600); err != nil {
20
+ t.Fatalf("os.WriteFile() error = %v", err)
21
+ }
22
+ }
23
+
24
+ // Env should override config for no_color when AWIKI_NO_COLOR is set.
25
+ func TestResolve_NoColorEnvOverridesConfig(t *testing.T) {
26
+ root := t.TempDir()
27
+ cfg := FileConfig{}
28
+ falseVal := false
29
+ cfg.Output.NoColor = &falseVal
30
+ writeTestConfig(t, root, cfg)
31
+
32
+ t.Setenv("AWIKI_HOME", root)
33
+ t.Setenv("AWIKI_NO_COLOR", "1")
34
+
35
+ resolved, err := Resolve(Overrides{})
36
+ if err != nil {
37
+ t.Fatalf("Resolve() error = %v", err)
38
+ }
39
+ if !resolved.NoColor {
40
+ t.Fatalf("resolved.NoColor = false, want true")
41
+ }
42
+ source := resolved.Sources["no_color"]
43
+ if source.Source != "canonical_env" {
44
+ t.Fatalf("resolved.Sources[no_color].Source = %q, want %q", source.Source, "canonical_env")
45
+ }
46
+ if source.Key != "AWIKI_NO_COLOR" {
47
+ t.Fatalf("resolved.Sources[no_color].Key = %q, want %q", source.Key, "AWIKI_NO_COLOR")
48
+ }
49
+ }
50
+
51
+ // When AWIKI_NO_COLOR is not set, config value should be used.
52
+ func TestResolve_NoColorFromConfigWhenNoEnv(t *testing.T) {
53
+ root := t.TempDir()
54
+ cfg := FileConfig{}
55
+ falseVal := false
56
+ cfg.Output.NoColor = &falseVal
57
+ writeTestConfig(t, root, cfg)
58
+
59
+ t.Setenv("AWIKI_HOME", root)
60
+
61
+ resolved, err := Resolve(Overrides{})
62
+ if err != nil {
63
+ t.Fatalf("Resolve() error = %v", err)
64
+ }
65
+ if resolved.NoColor {
66
+ t.Fatalf("resolved.NoColor = true, want false")
67
+ }
68
+ source := resolved.Sources["no_color"]
69
+ if source.Source != "config_file" {
70
+ t.Fatalf("resolved.Sources[no_color].Source = %q, want %q", source.Source, "config_file")
71
+ }
72
+ if source.Value != "false" {
73
+ t.Fatalf("resolved.Sources[no_color].Value = %q, want %q", source.Value, "false")
74
+ }
75
+ }
76
+
77
+ func TestResolveRootDir_PrefersEnvOverDefaultRoot(t *testing.T) {
78
+ home := t.TempDir()
79
+ envTarget := filepath.Join(home, "env-root")
80
+
81
+ t.Setenv("AWIKI_HOME", envTarget)
82
+
83
+ root, source := resolveRootDir(home)
84
+ if root != envTarget {
85
+ t.Fatalf("resolveRootDir() root = %q, want %q from AWIKI_HOME", root, envTarget)
86
+ }
87
+ if source.Source != "canonical_env" || source.Key != "AWIKI_HOME" {
88
+ t.Fatalf("resolveRootDir() source = %+v, want canonical_env AWIKI_HOME", source)
89
+ }
90
+ }
91
+
92
+ func TestResolveRootDir_UsesDefaultRootWhenNoEnv(t *testing.T) {
93
+ home := t.TempDir()
94
+ t.Setenv("AWIKI_HOME", "")
95
+
96
+ root, source := resolveRootDir(home)
97
+ expected := DefaultRootDir(home)
98
+ if root != expected {
99
+ t.Fatalf("resolveRootDir() root = %q, want %q from default root", root, expected)
100
+ }
101
+ if source.Source != "default" || source.Key != "" {
102
+ t.Fatalf("resolveRootDir() source = %+v, want default with empty key", source)
103
+ }
104
+ }
@@ -0,0 +1,37 @@
1
+ package config
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ )
9
+
10
+ // UpdateRuntimeSettings updates the runtime.mode in config.json and ensures the
11
+ // config directory exists. The socketPath parameter is currently ignored and
12
+ // kept only for forward-compatibility with potential future extensions.
13
+ func UpdateRuntimeSettings(paths Paths, mode string, socketPath string) error {
14
+ configPath := paths.ConfigFile
15
+ if err := os.MkdirAll(filepath.Dir(configPath), 0o700); err != nil {
16
+ return fmt.Errorf("create config dir: %w", err)
17
+ }
18
+ fileConfig, _, err := loadFileConfig(configPath)
19
+ if err != nil {
20
+ return err
21
+ }
22
+ fileConfig.Runtime.Mode = mode
23
+ return WriteFileConfig(configPath, fileConfig)
24
+ }
25
+
26
+ // WriteFileConfig writes the given FileConfig to the target path using
27
+ // indented JSON and 0600 permissions.
28
+ func WriteFileConfig(path string, fileConfig FileConfig) error {
29
+ raw, err := json.MarshalIndent(fileConfig, "", " ")
30
+ if err != nil {
31
+ return fmt.Errorf("marshal config json: %w", err)
32
+ }
33
+ if err := os.WriteFile(path, raw, 0o600); err != nil {
34
+ return fmt.Errorf("write config json: %w", err)
35
+ }
36
+ return nil
37
+ }