@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,350 @@
1
+ package output
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "io"
8
+ "sort"
9
+ "strings"
10
+ "text/tabwriter"
11
+
12
+ "github.com/itchyny/gojq"
13
+ )
14
+
15
+ type Format string
16
+
17
+ const (
18
+ FormatJSON Format = "json"
19
+ FormatPretty Format = "pretty"
20
+ FormatNDJSON Format = "ndjson"
21
+ FormatTable Format = "table"
22
+ )
23
+
24
+ type IdentityMeta struct {
25
+ Name string `json:"name,omitempty"`
26
+ DID string `json:"did,omitempty"`
27
+ }
28
+
29
+ type Meta struct {
30
+ Version string `json:"version"`
31
+ Identity *IdentityMeta `json:"identity,omitempty"`
32
+ DryRun bool `json:"dry_run"`
33
+ Format string `json:"format"`
34
+ }
35
+
36
+ type SuccessEnvelope struct {
37
+ OK bool `json:"ok"`
38
+ Command string `json:"command"`
39
+ Data any `json:"data,omitempty"`
40
+ Warnings []string `json:"warnings,omitempty"`
41
+ Summary string `json:"summary,omitempty"`
42
+ Notice map[string]any `json:"_notice,omitempty"`
43
+ Meta Meta `json:"meta"`
44
+ }
45
+
46
+ type ErrorDetail struct {
47
+ Code string `json:"code"`
48
+ Message string `json:"message"`
49
+ Hint string `json:"hint,omitempty"`
50
+ Retryable bool `json:"retryable"`
51
+ Details any `json:"details,omitempty"`
52
+ }
53
+
54
+ type ErrorEnvelope struct {
55
+ OK bool `json:"ok"`
56
+ Error ErrorDetail `json:"error"`
57
+ Notice map[string]any `json:"_notice,omitempty"`
58
+ Meta Meta `json:"meta"`
59
+ }
60
+
61
+ type ExitError struct {
62
+ Code int
63
+ Detail ErrorDetail
64
+ }
65
+
66
+ func (e *ExitError) Error() string {
67
+ if e == nil {
68
+ return ""
69
+ }
70
+ return e.Detail.Message
71
+ }
72
+
73
+ func NewExitError(code string, exitCode int, message string, hint string) *ExitError {
74
+ return &ExitError{
75
+ Code: exitCode,
76
+ Detail: ErrorDetail{
77
+ Code: code,
78
+ Message: message,
79
+ Hint: hint,
80
+ Retryable: false,
81
+ },
82
+ }
83
+ }
84
+
85
+ func NormalizeFormat(raw string) (Format, error) {
86
+ normalized := Format(strings.ToLower(strings.TrimSpace(raw)))
87
+ switch normalized {
88
+ case FormatJSON, FormatPretty, FormatNDJSON, FormatTable:
89
+ return normalized, nil
90
+ default:
91
+ return "", fmt.Errorf("unsupported format %q", raw)
92
+ }
93
+ }
94
+
95
+ func RenderSuccess(w io.Writer, format Format, jqExpr string, envelope SuccessEnvelope) error {
96
+ return render(w, format, jqExpr, envelope)
97
+ }
98
+
99
+ func RenderError(w io.Writer, format Format, jqExpr string, envelope ErrorEnvelope) error {
100
+ return render(w, format, jqExpr, envelope)
101
+ }
102
+
103
+ func render(w io.Writer, format Format, jqExpr string, envelope any) error {
104
+ value, err := toGeneric(envelope)
105
+ if err != nil {
106
+ return err
107
+ }
108
+ if strings.TrimSpace(jqExpr) != "" {
109
+ value, err = applyJQ(value, jqExpr)
110
+ if err != nil {
111
+ return err
112
+ }
113
+ }
114
+ return writeValue(w, format, value)
115
+ }
116
+
117
+ func toGeneric(value any) (any, error) {
118
+ raw, err := json.Marshal(value)
119
+ if err != nil {
120
+ return nil, err
121
+ }
122
+ var decoded any
123
+ if err := json.Unmarshal(raw, &decoded); err != nil {
124
+ return nil, err
125
+ }
126
+ return decoded, nil
127
+ }
128
+
129
+ func applyJQ(value any, expr string) (any, error) {
130
+ query, err := gojq.Parse(expr)
131
+ if err != nil {
132
+ return nil, fmt.Errorf("invalid jq expression: %w", err)
133
+ }
134
+ iter := query.Run(value)
135
+ results := make([]any, 0)
136
+ for {
137
+ item, ok := iter.Next()
138
+ if !ok {
139
+ break
140
+ }
141
+ if jqErr, ok := item.(error); ok {
142
+ return nil, fmt.Errorf("jq execution failed: %w", jqErr)
143
+ }
144
+ results = append(results, item)
145
+ }
146
+ if len(results) == 0 {
147
+ return nil, nil
148
+ }
149
+ if len(results) == 1 {
150
+ return results[0], nil
151
+ }
152
+ return results, nil
153
+ }
154
+
155
+ func writeValue(w io.Writer, format Format, value any) error {
156
+ switch format {
157
+ case FormatJSON:
158
+ return writeJSON(w, value, true)
159
+ case FormatPretty:
160
+ return writeJSON(w, value, true)
161
+ case FormatNDJSON:
162
+ return writeNDJSON(w, value)
163
+ case FormatTable:
164
+ return writeTable(w, value)
165
+ default:
166
+ return fmt.Errorf("unsupported format %q", format)
167
+ }
168
+ }
169
+
170
+ func writeJSON(w io.Writer, value any, indent bool) error {
171
+ var (
172
+ raw []byte
173
+ err error
174
+ )
175
+ if indent {
176
+ raw, err = json.MarshalIndent(value, "", " ")
177
+ } else {
178
+ raw, err = json.Marshal(value)
179
+ }
180
+ if err != nil {
181
+ return err
182
+ }
183
+ _, err = fmt.Fprintln(w, string(raw))
184
+ return err
185
+ }
186
+
187
+ func writeNDJSON(w io.Writer, value any) error {
188
+ if rows, ok := value.([]any); ok {
189
+ for _, row := range rows {
190
+ if err := writeJSON(w, row, false); err != nil {
191
+ return err
192
+ }
193
+ }
194
+ return nil
195
+ }
196
+ return writeJSON(w, value, false)
197
+ }
198
+
199
+ func writeTable(w io.Writer, value any) error {
200
+ value = tableViewValue(value)
201
+ if rows, ok := value.([]any); ok {
202
+ return writeTableRows(w, rows)
203
+ }
204
+ if object, ok := value.(map[string]any); ok {
205
+ return writeTableObject(w, object)
206
+ }
207
+ return writeJSON(w, value, true)
208
+ }
209
+
210
+ func tableViewValue(value any) any {
211
+ value = unwrapTableEnvelope(value)
212
+ if object, ok := value.(map[string]any); ok {
213
+ if _, hasCommand := object["command"]; hasCommand {
214
+ return value
215
+ }
216
+ }
217
+ if rows, ok := preferredTableRows(value); ok {
218
+ return rows
219
+ }
220
+ return value
221
+ }
222
+
223
+ func unwrapTableEnvelope(value any) any {
224
+ object, ok := value.(map[string]any)
225
+ if !ok {
226
+ return value
227
+ }
228
+ okValue, hasOK := object["ok"].(bool)
229
+ if !hasOK {
230
+ return value
231
+ }
232
+ if okValue {
233
+ if data, exists := object["data"]; exists {
234
+ return data
235
+ }
236
+ return value
237
+ }
238
+ if detail, exists := object["error"]; exists {
239
+ return detail
240
+ }
241
+ return value
242
+ }
243
+
244
+ func preferredTableRows(value any) ([]any, bool) {
245
+ object, ok := value.(map[string]any)
246
+ if !ok {
247
+ return nil, false
248
+ }
249
+ for _, key := range []string{"rows", "items", "messages", "members", "pages", "identities", "groups", "followers", "following", "checks", "commands"} {
250
+ if rows, ok := tableRows(object[key]); ok {
251
+ return rows, true
252
+ }
253
+ }
254
+ sliceKeys := make([]string, 0, len(object))
255
+ for key, item := range object {
256
+ if _, ok := tableRows(item); ok {
257
+ sliceKeys = append(sliceKeys, key)
258
+ }
259
+ }
260
+ if len(sliceKeys) != 1 {
261
+ return nil, false
262
+ }
263
+ return tableRows(object[sliceKeys[0]])
264
+ }
265
+
266
+ func tableRows(value any) ([]any, bool) {
267
+ rows, ok := value.([]any)
268
+ return rows, ok
269
+ }
270
+
271
+ func writeTableObject(w io.Writer, value map[string]any) error {
272
+ tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
273
+ keys := make([]string, 0, len(value))
274
+ for key := range value {
275
+ keys = append(keys, key)
276
+ }
277
+ sort.Strings(keys)
278
+ for _, key := range keys {
279
+ cell, err := tableCell(value[key])
280
+ if err != nil {
281
+ return err
282
+ }
283
+ if _, err := fmt.Fprintf(tw, "%s\t%s\n", key, cell); err != nil {
284
+ return err
285
+ }
286
+ }
287
+ return tw.Flush()
288
+ }
289
+
290
+ func writeTableRows(w io.Writer, rows []any) error {
291
+ if len(rows) == 0 {
292
+ _, err := fmt.Fprintln(w, "No rows")
293
+ return err
294
+ }
295
+
296
+ objects := make([]map[string]any, 0, len(rows))
297
+ columnsSet := map[string]struct{}{}
298
+ for _, row := range rows {
299
+ object, ok := row.(map[string]any)
300
+ if !ok {
301
+ return writeJSON(w, rows, true)
302
+ }
303
+ objects = append(objects, object)
304
+ for key := range object {
305
+ columnsSet[key] = struct{}{}
306
+ }
307
+ }
308
+
309
+ columns := make([]string, 0, len(columnsSet))
310
+ for column := range columnsSet {
311
+ columns = append(columns, column)
312
+ }
313
+ sort.Strings(columns)
314
+
315
+ tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
316
+ if _, err := fmt.Fprintln(tw, strings.Join(columns, "\t")); err != nil {
317
+ return err
318
+ }
319
+ for _, row := range objects {
320
+ cells := make([]string, 0, len(columns))
321
+ for _, column := range columns {
322
+ cell, err := tableCell(row[column])
323
+ if err != nil {
324
+ return err
325
+ }
326
+ cells = append(cells, cell)
327
+ }
328
+ if _, err := fmt.Fprintln(tw, strings.Join(cells, "\t")); err != nil {
329
+ return err
330
+ }
331
+ }
332
+ return tw.Flush()
333
+ }
334
+
335
+ func tableCell(value any) (string, error) {
336
+ switch typed := value.(type) {
337
+ case nil:
338
+ return "", nil
339
+ case string:
340
+ return typed, nil
341
+ case bool, float64, float32, int, int32, int64, uint, uint32, uint64:
342
+ return fmt.Sprint(typed), nil
343
+ default:
344
+ buffer := bytes.NewBuffer(nil)
345
+ if err := writeJSON(buffer, typed, false); err != nil {
346
+ return "", err
347
+ }
348
+ return strings.TrimSpace(buffer.String()), nil
349
+ }
350
+ }
@@ -0,0 +1,48 @@
1
+ package output
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ func TestRenderSuccessTableUsesCommandDataRows(t *testing.T) {
10
+ t.Parallel()
11
+
12
+ envelope := SuccessEnvelope{
13
+ OK: true,
14
+ Command: "awiki-cli msg inbox",
15
+ Data: map[string]any{
16
+ "messages": []map[string]any{
17
+ {
18
+ "id": "msg-1",
19
+ "text": "hello",
20
+ },
21
+ },
22
+ "total": 1,
23
+ "source": "http",
24
+ },
25
+ Summary: "Loaded inbox",
26
+ Meta: Meta{
27
+ Version: "test",
28
+ Format: string(FormatTable),
29
+ },
30
+ }
31
+
32
+ var buffer bytes.Buffer
33
+ if err := RenderSuccess(&buffer, FormatTable, "", envelope); err != nil {
34
+ t.Fatalf("RenderSuccess() error = %v", err)
35
+ }
36
+
37
+ rendered := buffer.String()
38
+ for _, forbidden := range []string{"command", "meta", "data", "ok", "awiki-cli msg inbox"} {
39
+ if strings.Contains(rendered, forbidden) {
40
+ t.Fatalf("table output %q unexpectedly contains envelope field %q", rendered, forbidden)
41
+ }
42
+ }
43
+ for _, want := range []string{"msg-1", "hello"} {
44
+ if !strings.Contains(rendered, want) {
45
+ t.Fatalf("table output %q does not contain row value %q", rendered, want)
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,117 @@
1
+ package runtime
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "fmt"
8
+ "net"
9
+ "os"
10
+ "path/filepath"
11
+ "runtime"
12
+ "strings"
13
+
14
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
15
+ )
16
+
17
+ const (
18
+ ModeHTTP = "http"
19
+ ModeWebSocket = "websocket"
20
+ maxUnixSocketPathBytes = 100
21
+ )
22
+
23
+ type Resolved struct {
24
+ Mode string `json:"mode"`
25
+ SocketPath string `json:"socket_path,omitempty"`
26
+ }
27
+
28
+ func Resolve(resolved *appconfig.Resolved) Resolved {
29
+ if resolved == nil {
30
+ return Resolved{Mode: ModeHTTP}
31
+ }
32
+ mode := strings.ToLower(strings.TrimSpace(resolved.RuntimeMode))
33
+ if mode != ModeWebSocket {
34
+ mode = ModeHTTP
35
+ }
36
+ socketPath := strings.TrimSpace(resolved.RuntimeSocketPath)
37
+ if socketPath == "" {
38
+ socketPath = filepath.Join(resolved.Paths.StateDir, "runtime", "message-daemon.sock")
39
+ }
40
+ socketPath = normalizeSocketPath(socketPath)
41
+ return Resolved{
42
+ Mode: mode,
43
+ SocketPath: socketPath,
44
+ }
45
+ }
46
+
47
+ func IsWebSocketMode(resolved *appconfig.Resolved) bool {
48
+ return Resolve(resolved).Mode == ModeWebSocket
49
+ }
50
+
51
+ type BridgeRequest struct {
52
+ Method string `json:"method"`
53
+ Params map[string]any `json:"params"`
54
+ IdentityName string `json:"identity_name"`
55
+ }
56
+
57
+ type BridgeResponse struct {
58
+ OK bool `json:"ok"`
59
+ Result map[string]any `json:"result,omitempty"`
60
+ Error *BridgeError `json:"error,omitempty"`
61
+ }
62
+
63
+ type BridgeError struct {
64
+ Code string `json:"code,omitempty"`
65
+ Message string `json:"message"`
66
+ }
67
+
68
+ func CallLocalBridge(request BridgeRequest, resolved *appconfig.Resolved) (map[string]any, error) {
69
+ bridge := Resolve(resolved)
70
+ if bridge.Mode != ModeWebSocket {
71
+ return nil, fmt.Errorf("runtime mode %s does not use the local websocket bridge", bridge.Mode)
72
+ }
73
+ if strings.TrimSpace(bridge.SocketPath) == "" {
74
+ return nil, fmt.Errorf("runtime websocket bridge socket is not configured")
75
+ }
76
+ if runtime.GOOS == "windows" {
77
+ return nil, fmt.Errorf("websocket bridge is not implemented on windows yet")
78
+ }
79
+ if err := os.MkdirAll(filepath.Dir(bridge.SocketPath), 0o700); err != nil {
80
+ return nil, fmt.Errorf("prepare websocket bridge socket dir: %w", err)
81
+ }
82
+ conn, err := net.Dial("unix", bridge.SocketPath)
83
+ if err != nil {
84
+ return nil, fmt.Errorf("local websocket bridge unavailable: %w", err)
85
+ }
86
+ defer conn.Close()
87
+ payload, err := json.Marshal(request)
88
+ if err != nil {
89
+ return nil, err
90
+ }
91
+ if _, err := conn.Write(append(payload, '\n')); err != nil {
92
+ return nil, fmt.Errorf("write websocket bridge request: %w", err)
93
+ }
94
+ decoder := json.NewDecoder(conn)
95
+ var response BridgeResponse
96
+ if err := decoder.Decode(&response); err != nil {
97
+ return nil, fmt.Errorf("decode websocket bridge response: %w", err)
98
+ }
99
+ if !response.OK {
100
+ if response.Error == nil {
101
+ return nil, fmt.Errorf("local websocket bridge request failed")
102
+ }
103
+ return nil, fmt.Errorf("local websocket bridge request failed: %s", response.Error.Message)
104
+ }
105
+ if response.Result == nil {
106
+ return map[string]any{}, nil
107
+ }
108
+ return response.Result, nil
109
+ }
110
+
111
+ func normalizeSocketPath(path string) string {
112
+ if len(path) <= maxUnixSocketPathBytes {
113
+ return path
114
+ }
115
+ sum := sha256.Sum256([]byte(path))
116
+ return filepath.Join(os.TempDir(), "awiki-cli-"+hex.EncodeToString(sum[:8])+".sock")
117
+ }
@@ -0,0 +1,46 @@
1
+ package runtime
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
10
+ )
11
+
12
+ func TestResolveShortensLongSocketPath(t *testing.T) {
13
+ t.Parallel()
14
+
15
+ longStateDir := filepath.Join("/tmp", strings.Repeat("very-long-runtime-dir-", 10))
16
+ resolved := Resolve(&appconfig.Resolved{
17
+ RuntimeMode: "websocket",
18
+ Paths: appconfig.Paths{
19
+ StateDir: longStateDir,
20
+ },
21
+ })
22
+ if resolved.Mode != ModeWebSocket {
23
+ t.Fatalf("resolved.Mode = %q, want %q", resolved.Mode, ModeWebSocket)
24
+ }
25
+ if len(resolved.SocketPath) > maxUnixSocketPathBytes {
26
+ t.Fatalf("len(resolved.SocketPath) = %d, want <= %d", len(resolved.SocketPath), maxUnixSocketPathBytes)
27
+ }
28
+ if !strings.HasPrefix(resolved.SocketPath, filepath.Join(os.TempDir(), "awiki-cli-")) {
29
+ t.Fatalf("resolved.SocketPath = %q, want shortened temp-dir path", resolved.SocketPath)
30
+ }
31
+ }
32
+
33
+ func TestResolveKeepsShortSocketPath(t *testing.T) {
34
+ t.Parallel()
35
+
36
+ resolved := Resolve(&appconfig.Resolved{
37
+ RuntimeMode: "websocket",
38
+ RuntimeSocketPath: "/tmp/custom-awiki.sock",
39
+ Paths: appconfig.Paths{
40
+ StateDir: "/tmp/state",
41
+ },
42
+ })
43
+ if resolved.SocketPath != "/tmp/custom-awiki.sock" {
44
+ t.Fatalf("resolved.SocketPath = %q, want /tmp/custom-awiki.sock", resolved.SocketPath)
45
+ }
46
+ }
@@ -0,0 +1,65 @@
1
+ package listener
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "strconv"
9
+ "strings"
10
+
11
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
12
+ "github.com/agentconnect/awiki-cli/internal/runtime"
13
+ )
14
+
15
+ func paths(resolved *appconfig.Resolved) (pidFile string, logFile string, statusFile string, socketPath string, err error) {
16
+ bridge := runtime.Resolve(resolved)
17
+ stateRoot := filepath.Join(resolved.Paths.StateDir, "runtime")
18
+ if err := os.MkdirAll(stateRoot, 0o700); err != nil {
19
+ return "", "", "", "", fmt.Errorf("create runtime state dir: %w", err)
20
+ }
21
+ logDir := strings.TrimSpace(resolved.Paths.LogsDir)
22
+ if logDir == "" {
23
+ logDir = stateRoot
24
+ }
25
+ if err := os.MkdirAll(logDir, 0o700); err != nil {
26
+ return "", "", "", "", fmt.Errorf("create runtime log dir: %w", err)
27
+ }
28
+ return filepath.Join(stateRoot, "listener.pid"),
29
+ filepath.Join(logDir, "listener.log"),
30
+ filepath.Join(stateRoot, "listener.status.json"),
31
+ bridge.SocketPath,
32
+ nil
33
+ }
34
+
35
+ func writePID(path string, pid int) error {
36
+ return os.WriteFile(path, []byte(strconv.Itoa(pid)), 0o600)
37
+ }
38
+
39
+ func readPID(path string) (int, error) {
40
+ raw, err := os.ReadFile(path)
41
+ if err != nil {
42
+ return 0, err
43
+ }
44
+ return strconv.Atoi(strings.TrimSpace(string(raw)))
45
+ }
46
+
47
+ func writeStatus(path string, status Status) error {
48
+ raw, err := json.MarshalIndent(status, "", " ")
49
+ if err != nil {
50
+ return err
51
+ }
52
+ return os.WriteFile(path, raw, 0o600)
53
+ }
54
+
55
+ func readStatus(path string) (Status, error) {
56
+ raw, err := os.ReadFile(path)
57
+ if err != nil {
58
+ return Status{}, err
59
+ }
60
+ var status Status
61
+ if err := json.Unmarshal(raw, &status); err != nil {
62
+ return Status{}, err
63
+ }
64
+ return status, nil
65
+ }