@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.
- package/.github/workflows/release.yml +44 -0
- package/.goreleaser.yml +44 -0
- package/AGENTS.md +60 -0
- package/CLAUDE.md +192 -0
- package/README.md +2 -0
- package/docs/architecture/awiki-command-v2.md +955 -0
- package/docs/architecture/awiki-skill-architecture.md +475 -0
- package/docs/architecture/awiki-v2-architecture.md +1063 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
- package/docs/harness/review-spec.md +474 -0
- package/docs/installation.md +372 -0
- package/docs/plan/awiki-v2-implementation-plan.md +903 -0
- package/docs/plan/phase-0/adr-index.md +56 -0
- package/docs/plan/phase-0/audit-findings.md +251 -0
- package/docs/plan/phase-0/capability-mapping.md +108 -0
- package/docs/plan/phase-0/implementation-constraints.md +363 -0
- package/docs/publish.md +169 -0
- package/go.mod +29 -0
- package/go.sum +73 -0
- package/internal/anpsdk/registry.go +63 -0
- package/internal/authsdk/session.go +351 -0
- package/internal/buildinfo/buildinfo.go +34 -0
- package/internal/cli/app.go +136 -0
- package/internal/cli/app_test.go +88 -0
- package/internal/cli/debug.go +104 -0
- package/internal/cli/group.go +263 -0
- package/internal/cli/id.go +473 -0
- package/internal/cli/init.go +134 -0
- package/internal/cli/msg.go +228 -0
- package/internal/cli/page.go +267 -0
- package/internal/cli/root.go +499 -0
- package/internal/cli/runtime.go +232 -0
- package/internal/cli/upgrade.go +60 -0
- package/internal/cmdmeta/catalog.go +203 -0
- package/internal/cmdmeta/catalog_test.go +21 -0
- package/internal/config/config.go +399 -0
- package/internal/config/config_test.go +104 -0
- package/internal/config/write.go +37 -0
- package/internal/content/service.go +314 -0
- package/internal/content/service_test.go +165 -0
- package/internal/content/types.go +44 -0
- package/internal/docs/topics.go +110 -0
- package/internal/doctor/doctor.go +306 -0
- package/internal/identity/client.go +267 -0
- package/internal/identity/did.go +85 -0
- package/internal/identity/did_test.go +50 -0
- package/internal/identity/layout.go +206 -0
- package/internal/identity/legacy.go +378 -0
- package/internal/identity/public.go +70 -0
- package/internal/identity/public_test.go +73 -0
- package/internal/identity/readiness.go +74 -0
- package/internal/identity/service.go +826 -0
- package/internal/identity/store.go +385 -0
- package/internal/identity/store_test.go +180 -0
- package/internal/identity/types.go +204 -0
- package/internal/message/auth.go +167 -0
- package/internal/message/group_service.go +838 -0
- package/internal/message/group_wire.go +350 -0
- package/internal/message/group_wire_test.go +67 -0
- package/internal/message/helpers.go +61 -0
- package/internal/message/http_client.go +334 -0
- package/internal/message/proof.go +156 -0
- package/internal/message/proof_test.go +61 -0
- package/internal/message/service.go +696 -0
- package/internal/message/service_test.go +97 -0
- package/internal/message/types.go +155 -0
- package/internal/message/wire.go +100 -0
- package/internal/message/wire_test.go +49 -0
- package/internal/message/ws_proxy_client.go +151 -0
- package/internal/output/output.go +350 -0
- package/internal/output/output_test.go +48 -0
- package/internal/runtime/config.go +117 -0
- package/internal/runtime/config_test.go +46 -0
- package/internal/runtime/listener/files.go +65 -0
- package/internal/runtime/listener/manager.go +142 -0
- package/internal/runtime/listener/server.go +983 -0
- package/internal/runtime/listener/server_test.go +319 -0
- package/internal/runtime/listener/sysproc_unix.go +17 -0
- package/internal/runtime/listener/sysproc_windows.go +13 -0
- package/internal/runtime/listener/types.go +21 -0
- package/internal/runtime/listener/wsclient.go +299 -0
- package/internal/runtime/listener/wsclient_test.go +41 -0
- package/internal/store/dao.go +632 -0
- package/internal/store/dao_test.go +87 -0
- package/internal/store/helpers.go +197 -0
- package/internal/store/import.go +499 -0
- package/internal/store/import_test.go +103 -0
- package/internal/store/open.go +71 -0
- package/internal/store/query.go +151 -0
- package/internal/store/schema.go +277 -0
- package/internal/store/schema_test.go +56 -0
- package/internal/store/types.go +177 -0
- package/internal/update/update.go +368 -0
- package/package.json +17 -0
- package/scripts/install.js +171 -0
- package/scripts/release/release-prerelease.sh +86 -0
- package/scripts/release/tag-release.sh +66 -0
- package/scripts/release/withdraw-release.sh +78 -0
- package/scripts/run.js +69 -0
- package/skills/README.md +32 -0
- package/skills/awiki-bundle/SKILL.md +76 -0
- package/skills/awiki-debug/SKILL.md +80 -0
- package/skills/awiki-group/SKILL.md +111 -0
- package/skills/awiki-id/SKILL.md +123 -0
- package/skills/awiki-msg/SKILL.md +131 -0
- package/skills/awiki-page/SKILL.md +93 -0
- package/skills/awiki-people/SKILL.md +66 -0
- package/skills/awiki-runtime/SKILL.md +137 -0
- package/skills/awiki-shared/SKILL.md +124 -0
- package/skills/awiki-workflow-discovery/SKILL.md +93 -0
- package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
- package/skills/manifests/skills.yaml +260 -0
- package/skills/templates/bundle-skill-template.md +42 -0
- package/skills/templates/debug-skill-template.md +44 -0
- package/skills/templates/domain-skill-template.md +56 -0
- package/skills/templates/shared-skill-template.md +46 -0
- 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
|
+
}
|