@prmichaelsen/acp-mcp 0.1.0

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 (94) hide show
  1. package/.env.example +5 -0
  2. package/AGENT.md +1279 -0
  3. package/README.md +78 -0
  4. package/agent/commands/acp.command-create.md +372 -0
  5. package/agent/commands/acp.design-create.md +224 -0
  6. package/agent/commands/acp.init.md +410 -0
  7. package/agent/commands/acp.package-create.md +894 -0
  8. package/agent/commands/acp.package-info.md +211 -0
  9. package/agent/commands/acp.package-install.md +461 -0
  10. package/agent/commands/acp.package-list.md +279 -0
  11. package/agent/commands/acp.package-publish.md +540 -0
  12. package/agent/commands/acp.package-remove.md +292 -0
  13. package/agent/commands/acp.package-search.md +306 -0
  14. package/agent/commands/acp.package-update.md +310 -0
  15. package/agent/commands/acp.package-validate.md +535 -0
  16. package/agent/commands/acp.pattern-create.md +326 -0
  17. package/agent/commands/acp.plan.md +552 -0
  18. package/agent/commands/acp.proceed.md +336 -0
  19. package/agent/commands/acp.project-create.md +672 -0
  20. package/agent/commands/acp.report.md +394 -0
  21. package/agent/commands/acp.resume.md +237 -0
  22. package/agent/commands/acp.status.md +280 -0
  23. package/agent/commands/acp.sync.md +363 -0
  24. package/agent/commands/acp.task-create.md +390 -0
  25. package/agent/commands/acp.update.md +301 -0
  26. package/agent/commands/acp.validate.md +436 -0
  27. package/agent/commands/acp.version-check-for-updates.md +275 -0
  28. package/agent/commands/acp.version-check.md +190 -0
  29. package/agent/commands/acp.version-update.md +288 -0
  30. package/agent/commands/command.template.md +316 -0
  31. package/agent/commands/git.commit.md +513 -0
  32. package/agent/commands/git.init.md +513 -0
  33. package/agent/commands/mcp-server-starter.add-tool.md +677 -0
  34. package/agent/commands/mcp-server-starter.init.md +894 -0
  35. package/agent/design/.gitkeep +0 -0
  36. package/agent/design/design.template.md +136 -0
  37. package/agent/design/remember-mcp-analysis.md +987 -0
  38. package/agent/design/requirements.template.md +387 -0
  39. package/agent/manifest.template.yaml +13 -0
  40. package/agent/manifest.yaml +109 -0
  41. package/agent/milestones/.gitkeep +0 -0
  42. package/agent/milestones/milestone-1-{title}.template.md +206 -0
  43. package/agent/package.template.yaml +36 -0
  44. package/agent/patterns/.gitkeep +0 -0
  45. package/agent/patterns/bootstrap.template.md +1237 -0
  46. package/agent/patterns/mcp-server-starter.bootstrap.md +597 -0
  47. package/agent/patterns/mcp-server-starter.build-config.md +554 -0
  48. package/agent/patterns/mcp-server-starter.config-management.md +525 -0
  49. package/agent/patterns/mcp-server-starter.server-factory.md +616 -0
  50. package/agent/patterns/mcp-server-starter.server-standalone.md +642 -0
  51. package/agent/patterns/mcp-server-starter.test-config.md +558 -0
  52. package/agent/patterns/mcp-server-starter.tool-creation.md +653 -0
  53. package/agent/patterns/pattern.template.md +364 -0
  54. package/agent/progress.template.yaml +161 -0
  55. package/agent/progress.yaml +33 -0
  56. package/agent/schemas/package.schema.yaml +161 -0
  57. package/agent/scripts/acp.common.sh +1362 -0
  58. package/agent/scripts/acp.install.sh +213 -0
  59. package/agent/scripts/acp.package-create.sh +925 -0
  60. package/agent/scripts/acp.package-info.sh +270 -0
  61. package/agent/scripts/acp.package-install.sh +550 -0
  62. package/agent/scripts/acp.package-list.sh +263 -0
  63. package/agent/scripts/acp.package-publish.sh +420 -0
  64. package/agent/scripts/acp.package-remove.sh +272 -0
  65. package/agent/scripts/acp.package-search.sh +156 -0
  66. package/agent/scripts/acp.package-update.sh +356 -0
  67. package/agent/scripts/acp.package-validate.sh +766 -0
  68. package/agent/scripts/acp.uninstall.sh +85 -0
  69. package/agent/scripts/acp.version-check-for-updates.sh +98 -0
  70. package/agent/scripts/acp.version-check.sh +47 -0
  71. package/agent/scripts/acp.version-update.sh +158 -0
  72. package/agent/scripts/acp.yaml-parser.sh +736 -0
  73. package/agent/scripts/acp.yaml-validate.sh +205 -0
  74. package/agent/tasks/.gitkeep +0 -0
  75. package/agent/tasks/task-1-{title}.template.md +225 -0
  76. package/dist/config.d.ts +4 -0
  77. package/dist/server-factory.d.ts +9 -0
  78. package/dist/server-factory.js +99 -0
  79. package/dist/server-factory.js.map +7 -0
  80. package/dist/server.d.ts +2 -0
  81. package/dist/server.js +106 -0
  82. package/dist/server.js.map +7 -0
  83. package/dist/tools/acp-remote-list-files.d.ts +15 -0
  84. package/dist/types/ssh-config.d.ts +16 -0
  85. package/esbuild.build.js +34 -0
  86. package/esbuild.watch.js +31 -0
  87. package/jest.config.js +31 -0
  88. package/package.json +54 -0
  89. package/src/config.ts +16 -0
  90. package/src/server-factory.ts +43 -0
  91. package/src/server.ts +46 -0
  92. package/src/tools/acp-remote-list-files.ts +89 -0
  93. package/src/types/ssh-config.ts +17 -0
  94. package/tsconfig.json +22 -0
@@ -0,0 +1,736 @@
1
+ #!/bin/bash
2
+ # Generic YAML Parser with AST
3
+ # Pure POSIX shell implementation
4
+ # Version: 1.0.0
5
+ # Created: 2026-02-21
6
+
7
+ # ============================================================================
8
+ # GLOBAL STATE
9
+ # ============================================================================
10
+
11
+ AST_FILE=""
12
+ AST_ROOT_ID=0
13
+ YAML_CURRENT_FILE=""
14
+
15
+ # ============================================================================
16
+ # UTILITY FUNCTIONS
17
+ # ============================================================================
18
+
19
+ init_ast() {
20
+ AST_FILE=$(mktemp)
21
+ echo "0|map||root|-1|" > "$AST_FILE"
22
+ AST_ROOT_ID=0
23
+ }
24
+
25
+ cleanup_ast() {
26
+ if [ -n "$AST_FILE" ] && [ -f "$AST_FILE" ]; then
27
+ rm -f "$AST_FILE"
28
+ fi
29
+ }
30
+
31
+ get_next_node_id() {
32
+ wc -l < "$AST_FILE"
33
+ }
34
+
35
+ create_node() {
36
+ local type="$1"
37
+ local key="$2"
38
+ local value="$3"
39
+ local parent_id="$4"
40
+
41
+ key=$(echo "$key" | sed 's/|/\\|/g')
42
+ value=$(echo "$value" | sed 's/|/\\|/g')
43
+
44
+ local node_id
45
+ node_id=$(get_next_node_id)
46
+
47
+ echo "$node_id|$type|$key|$value|$parent_id|" >> "$AST_FILE"
48
+ echo "$node_id"
49
+ }
50
+
51
+ get_node() {
52
+ local node_id="$1"
53
+ sed -n "$((node_id + 1))p" "$AST_FILE"
54
+ }
55
+
56
+ get_node_field() {
57
+ local node_id="$1"
58
+ local field_num="$2"
59
+ get_node "$node_id" | cut -d'|' -f"$field_num"
60
+ }
61
+
62
+ add_child() {
63
+ local parent_id="$1"
64
+ local child_id="$2"
65
+
66
+ local node
67
+ node=$(get_node "$parent_id")
68
+
69
+ local id type key value parent children
70
+ id=$(echo "$node" | cut -d'|' -f1)
71
+ type=$(echo "$node" | cut -d'|' -f2)
72
+ key=$(echo "$node" | cut -d'|' -f3)
73
+ value=$(echo "$node" | cut -d'|' -f4)
74
+ parent=$(echo "$node" | cut -d'|' -f5)
75
+ children=$(echo "$node" | cut -d'|' -f6)
76
+
77
+ if [ -z "$children" ]; then
78
+ children="$child_id"
79
+ else
80
+ children="$children,$child_id"
81
+ fi
82
+
83
+ local updated="$id|$type|$key|$value|$parent|$children"
84
+ sed -i "$((parent_id + 1))s@.*@$updated@" "$AST_FILE"
85
+ }
86
+
87
+ update_node_type() {
88
+ local node_id="$1"
89
+ local new_type="$2"
90
+
91
+ local node
92
+ node=$(get_node "$node_id")
93
+
94
+ local id type key value parent children
95
+ id=$(echo "$node" | cut -d'|' -f1)
96
+ key=$(echo "$node" | cut -d'|' -f3)
97
+ value=$(echo "$node" | cut -d'|' -f4)
98
+ parent=$(echo "$node" | cut -d'|' -f5)
99
+ children=$(echo "$node" | cut -d'|' -f6)
100
+
101
+ local updated="$id|$new_type|$key|$value|$parent|$children"
102
+ sed -i "$((node_id + 1))s@.*@$updated@" "$AST_FILE"
103
+ }
104
+
105
+ # ============================================================================
106
+ # PARSER
107
+ # ============================================================================
108
+
109
+ yaml_parse() {
110
+ local file="$1"
111
+
112
+ if [ ! -f "$file" ]; then
113
+ echo "Error: File not found: $file" >&2
114
+ return 1
115
+ fi
116
+
117
+ cleanup_ast
118
+ init_ast
119
+ YAML_CURRENT_FILE="$file"
120
+
121
+ # State tracking
122
+ local parent_stack="0"
123
+ local indent_stack="-1"
124
+ local current_parent=0
125
+ local prev_indent=-1
126
+ local last_key_node=-1
127
+
128
+ while IFS= read -r line || [ -n "$line" ]; do
129
+ # Skip empty lines
130
+ [ -z "$line" ] && continue
131
+
132
+ # Skip comment lines
133
+ case "$line" in \#*) continue ;; esac
134
+
135
+ # Strip inline comments
136
+ line=$(echo "$line" | sed 's/#.*$//')
137
+
138
+ # Calculate indentation
139
+ local indent=0
140
+ local trimmed="$line"
141
+ while [ "$trimmed" != "${trimmed# }" ]; do
142
+ indent=$((indent + 1))
143
+ trimmed="${trimmed# }"
144
+ done
145
+
146
+ # Skip empty after trim
147
+ [ -z "$trimmed" ] && continue
148
+
149
+ # Handle dedent - pop stack
150
+ while [ "$prev_indent" -ge 0 ] && [ "$indent" -le "$prev_indent" ]; do
151
+ # Pop one level
152
+ parent_stack=$(echo "$parent_stack" | sed 's/,[^,]*$//')
153
+ indent_stack=$(echo "$indent_stack" | sed 's/,[^,]*$//')
154
+
155
+ # Get new current parent
156
+ current_parent=$(echo "$parent_stack" | awk -F',' '{print $NF}')
157
+ prev_indent=$(echo "$indent_stack" | awk -F',' '{print $NF}')
158
+
159
+ # Handle empty stack
160
+ [ -z "$current_parent" ] && current_parent=0
161
+ [ -z "$prev_indent" ] && prev_indent=-1
162
+
163
+ last_key_node=-1
164
+ done
165
+
166
+ # Parse line content
167
+ if echo "$trimmed" | grep -q '^-[[:space:]]'; then
168
+ # Array item
169
+ local item_content
170
+ item_content=$(echo "$trimmed" | sed 's/^-[[:space:]]*//')
171
+
172
+ # Convert last key node to array if needed
173
+ if [ "$last_key_node" -ge 0 ]; then
174
+ update_node_type "$last_key_node" "array"
175
+ current_parent="$last_key_node"
176
+ last_key_node=-1
177
+ fi
178
+
179
+ # Check if inline object (has colon on same line)
180
+ if echo "$item_content" | grep -q ':'; then
181
+ # Inline object: - name: value
182
+ local obj_node
183
+ obj_node=$(create_node "map" "" "" "$current_parent")
184
+ add_child "$current_parent" "$obj_node"
185
+
186
+ # Parse first field
187
+ local key value
188
+ key=$(echo "$item_content" | cut -d':' -f1 | sed 's/[[:space:]]*$//')
189
+ value=$(echo "$item_content" | cut -d':' -f2- | sed 's/^[[:space:]]*//')
190
+
191
+ local field_node
192
+ field_node=$(create_node "scalar" "$key" "$value" "$obj_node")
193
+ add_child "$obj_node" "$field_node"
194
+
195
+ # Push object onto stack for potential additional fields
196
+ parent_stack="$parent_stack,$obj_node"
197
+ indent_stack="$indent_stack,$indent"
198
+ current_parent="$obj_node"
199
+ prev_indent="$indent"
200
+ else
201
+ # Simple array item: - value
202
+ local item_node
203
+ item_node=$(create_node "scalar" "" "$item_content" "$current_parent")
204
+ add_child "$current_parent" "$item_node"
205
+ fi
206
+ elif echo "$trimmed" | grep -q ':'; then
207
+ # Key-value pair
208
+ local key value
209
+ key=$(echo "$trimmed" | cut -d':' -f1 | sed 's/[[:space:]]*$//')
210
+ value=$(echo "$trimmed" | cut -d':' -f2- | sed 's/^[[:space:]]*//')
211
+
212
+ if [ -z "$value" ]; then
213
+ # Key with no value - map or array follows
214
+ local node_id
215
+ node_id=$(create_node "map" "$key" "" "$current_parent")
216
+ add_child "$current_parent" "$node_id"
217
+
218
+ # Push onto stack
219
+ parent_stack="$parent_stack,$node_id"
220
+ indent_stack="$indent_stack,$indent"
221
+ current_parent="$node_id"
222
+ prev_indent="$indent"
223
+ last_key_node="$node_id"
224
+ else
225
+ # Scalar value
226
+ local node_id
227
+ node_id=$(create_node "scalar" "$key" "$value" "$current_parent")
228
+ add_child "$current_parent" "$node_id"
229
+ fi
230
+ fi
231
+ done < "$file"
232
+
233
+ return 0
234
+ }
235
+
236
+ # ============================================================================
237
+ # QUERY ENGINE
238
+ # ============================================================================
239
+
240
+ find_child_by_key() {
241
+ local parent_id="$1"
242
+ local key="$2"
243
+
244
+ local children
245
+ children=$(get_node_field "$parent_id" 6)
246
+
247
+ [ -z "$children" ] && return 1
248
+
249
+ local IFS=','
250
+ for child_id in $children; do
251
+ local child_key
252
+ child_key=$(get_node_field "$child_id" 3)
253
+
254
+ if [ "$child_key" = "$key" ]; then
255
+ echo "$child_id"
256
+ return 0
257
+ fi
258
+ done
259
+
260
+ return 1
261
+ }
262
+
263
+ find_child_by_index() {
264
+ local parent_id="$1"
265
+ local index="$2"
266
+
267
+ local children
268
+ children=$(get_node_field "$parent_id" 6)
269
+
270
+ [ -z "$children" ] && return 1
271
+
272
+ local child_id
273
+ child_id=$(echo "$children" | tr ',' '\n' | sed -n "$((index + 1))p")
274
+
275
+ if [ -n "$child_id" ]; then
276
+ echo "$child_id"
277
+ return 0
278
+ fi
279
+
280
+ return 1
281
+ }
282
+
283
+ yaml_query() {
284
+ local path="$1"
285
+
286
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
287
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
288
+ return 1
289
+ fi
290
+
291
+ path=$(echo "$path" | sed 's/^\.//')
292
+
293
+ local current_node="$AST_ROOT_ID"
294
+
295
+ local IFS='.'
296
+ for segment in $path; do
297
+ if echo "$segment" | grep -q '\['; then
298
+ local key index
299
+ key=$(echo "$segment" | sed 's/\[.*//')
300
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
301
+
302
+ current_node=$(find_child_by_key "$current_node" "$key")
303
+ [ -z "$current_node" ] && return 1
304
+
305
+ current_node=$(find_child_by_index "$current_node" "$index")
306
+ [ -z "$current_node" ] && return 1
307
+ else
308
+ current_node=$(find_child_by_key "$current_node" "$segment")
309
+ [ -z "$current_node" ] && return 1
310
+ fi
311
+ done
312
+
313
+ get_node_field "$current_node" 4
314
+ }
315
+
316
+ yaml_set() {
317
+ local path="$1"
318
+ local new_value="$2"
319
+
320
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
321
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
322
+ return 1
323
+ fi
324
+
325
+ path=$(echo "$path" | sed 's/^\.//')
326
+
327
+ local current_node="$AST_ROOT_ID"
328
+ local IFS='.'
329
+ for segment in $path; do
330
+ if echo "$segment" | grep -q '\['; then
331
+ local key index
332
+ key=$(echo "$segment" | sed 's/\[.*//')
333
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
334
+
335
+ current_node=$(find_child_by_key "$current_node" "$key")
336
+ [ -z "$current_node" ] && return 1
337
+
338
+ current_node=$(find_child_by_index "$current_node" "$index")
339
+ [ -z "$current_node" ] && return 1
340
+ else
341
+ current_node=$(find_child_by_key "$current_node" "$segment")
342
+ [ -z "$current_node" ] && return 1
343
+ fi
344
+ done
345
+
346
+ local node
347
+ node=$(get_node "$current_node")
348
+
349
+ local id type key value parent children
350
+ id=$(echo "$node" | cut -d'|' -f1)
351
+ type=$(echo "$node" | cut -d'|' -f2)
352
+ key=$(echo "$node" | cut -d'|' -f3)
353
+ parent=$(echo "$node" | cut -d'|' -f5)
354
+ children=$(echo "$node" | cut -d'|' -f6)
355
+
356
+ new_value=$(echo "$new_value" | sed 's/|/\\|/g')
357
+
358
+ local updated="$id|$type|$key|$new_value|$parent|$children"
359
+ sed -i "$((current_node + 1))s@.*@$updated@" "$AST_FILE"
360
+ }
361
+
362
+ yaml_write() {
363
+ local output_file="$1"
364
+
365
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
366
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
367
+ return 1
368
+ fi
369
+
370
+ serialize_node "$AST_ROOT_ID" 0 > "$output_file"
371
+ }
372
+
373
+ serialize_node() {
374
+ local node_id="$1"
375
+ local indent_level="$2"
376
+ local parent_type="${3:-}"
377
+
378
+ local node
379
+ node=$(get_node "$node_id")
380
+
381
+ local type key value children parent_id
382
+ type=$(echo "$node" | cut -d'|' -f2)
383
+ key=$(echo "$node" | cut -d'|' -f3)
384
+ value=$(echo "$node" | cut -d'|' -f4)
385
+ parent_id=$(echo "$node" | cut -d'|' -f5)
386
+ children=$(echo "$node" | cut -d'|' -f6)
387
+
388
+ # Determine parent type if not provided
389
+ if [ -z "$parent_type" ] && [ "$parent_id" -ge 0 ]; then
390
+ parent_type=$(get_node_field "$parent_id" 2)
391
+ fi
392
+
393
+ local indent=""
394
+ local i=0
395
+ while [ "$i" -lt "$indent_level" ]; do
396
+ indent="$indent "
397
+ i=$((i + 1))
398
+ done
399
+
400
+ case "$type" in
401
+ scalar)
402
+ if [ -n "$key" ]; then
403
+ echo "$indent$key: $value"
404
+ else
405
+ echo "$indent- $value"
406
+ fi
407
+ ;;
408
+
409
+ map)
410
+ # If this map is in an array, first child gets dash prefix
411
+ local is_first_child=true
412
+
413
+ if [ "$node_id" -ne 0 ] && [ -n "$key" ]; then
414
+ echo "$indent$key:"
415
+ fi
416
+
417
+ if [ -n "$children" ]; then
418
+ local IFS=','
419
+ local next_indent
420
+ # Root node (id=0) doesn't add indentation
421
+ if [ "$node_id" -eq 0 ]; then
422
+ next_indent="$indent_level"
423
+ else
424
+ next_indent="$((indent_level + 1))"
425
+ fi
426
+
427
+ for child_id in $children; do
428
+ # If parent is array and this is first child, use dash
429
+ if [ "$parent_type" = "array" ] && [ "$is_first_child" = true ]; then
430
+ # Serialize first field with dash
431
+ local child_node
432
+ child_node=$(get_node "$child_id")
433
+ local child_type child_key child_value
434
+ child_type=$(echo "$child_node" | cut -d'|' -f2)
435
+ child_key=$(echo "$child_node" | cut -d'|' -f3)
436
+ child_value=$(echo "$child_node" | cut -d'|' -f4)
437
+
438
+ if [ "$child_type" = "scalar" ] && [ -n "$child_key" ]; then
439
+ echo "$indent- $child_key: $child_value"
440
+ fi
441
+ is_first_child=false
442
+ else
443
+ serialize_node "$child_id" "$next_indent" "$type"
444
+ fi
445
+ done
446
+ fi
447
+ ;;
448
+
449
+ array)
450
+ if [ -n "$key" ]; then
451
+ echo "$indent$key:"
452
+ fi
453
+
454
+ if [ -n "$children" ]; then
455
+ local IFS=','
456
+ # Array children need to be indented
457
+ for child_id in $children; do
458
+ serialize_node "$child_id" "$((indent_level + 1))" "array"
459
+ done
460
+ fi
461
+ ;;
462
+ esac
463
+ }
464
+
465
+ # ============================================================================
466
+ # BACKWARD COMPATIBILITY
467
+ # ============================================================================
468
+
469
+ yaml_get() {
470
+ local file="$1"
471
+ local key="$2"
472
+
473
+ if [ "$YAML_CURRENT_FILE" != "$file" ]; then
474
+ yaml_parse "$file" || return 1
475
+ fi
476
+
477
+ yaml_query ".$key"
478
+ }
479
+
480
+ yaml_get_nested() {
481
+ local file="$1"
482
+ local path="$2"
483
+
484
+ if [ "$YAML_CURRENT_FILE" != "$file" ]; then
485
+ yaml_parse "$file" || return 1
486
+ fi
487
+
488
+ yaml_query ".$path"
489
+ }
490
+
491
+ # Check if key exists (checks if node exists, not if it has a value)
492
+ yaml_has_key() {
493
+ local file="$1"
494
+ local key="$2"
495
+
496
+ if [ "$YAML_CURRENT_FILE" != "$file" ]; then
497
+ yaml_parse "$file" || return 1
498
+ fi
499
+
500
+ # Try to find the node (returns empty string on failure, but exit code tells us)
501
+ path=$(echo "$key" | sed 's/^\.//')
502
+ local current_node="$AST_ROOT_ID"
503
+
504
+ local IFS='.'
505
+ for segment in $path; do
506
+ if echo "$segment" | grep -q '\['; then
507
+ local k index
508
+ k=$(echo "$segment" | sed 's/\[.*//')
509
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
510
+
511
+ current_node=$(find_child_by_key "$current_node" "$k" 2>/dev/null)
512
+ [ -z "$current_node" ] && return 1
513
+
514
+ current_node=$(find_child_by_index "$current_node" "$index" 2>/dev/null)
515
+ [ -z "$current_node" ] && return 1
516
+ else
517
+ current_node=$(find_child_by_key "$current_node" "$segment" 2>/dev/null)
518
+ [ -z "$current_node" ] && return 1
519
+ fi
520
+ done
521
+
522
+ # Node exists
523
+ return 0
524
+ }
525
+
526
+ # Get array count (for object arrays)
527
+ # Usage: yaml_get_array file.yaml "contents.commands"
528
+ # Returns: count of array elements
529
+ yaml_get_array() {
530
+ local file="$1"
531
+ local path="$2"
532
+
533
+ if [ "$YAML_CURRENT_FILE" != "$file" ]; then
534
+ yaml_parse "$file" || return 1
535
+ fi
536
+
537
+ # Find the array node
538
+ path=$(echo "$path" | sed 's/^\.//')
539
+ local current_node="$AST_ROOT_ID"
540
+
541
+ local IFS='.'
542
+ for segment in $path; do
543
+ current_node=$(find_child_by_key "$current_node" "$segment")
544
+ [ -z "$current_node" ] && return 1
545
+ done
546
+
547
+ # Get children count
548
+ local children
549
+ children=$(get_node_field "$current_node" 6)
550
+
551
+ if [ -z "$children" ]; then
552
+ echo "0"
553
+ else
554
+ echo "$children" | tr ',' '\n' | wc -l
555
+ fi
556
+ }
557
+
558
+ # Append scalar item to array
559
+ # Usage: yaml_array_append ".path.to.array" "value"
560
+ # Returns: node_id of new item
561
+ yaml_array_append() {
562
+ local path="$1"
563
+ local value="$2"
564
+
565
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
566
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
567
+ return 1
568
+ fi
569
+
570
+ # Find the array node
571
+ path=$(echo "$path" | sed 's/^\.//')
572
+ local current_node="$AST_ROOT_ID"
573
+
574
+ local IFS='.'
575
+ for segment in $path; do
576
+ if echo "$segment" | grep -q '\['; then
577
+ local key index
578
+ key=$(echo "$segment" | sed 's/\[.*//')
579
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
580
+
581
+ current_node=$(find_child_by_key "$current_node" "$key")
582
+ [ -z "$current_node" ] && return 1
583
+
584
+ current_node=$(find_child_by_index "$current_node" "$index")
585
+ [ -z "$current_node" ] && return 1
586
+ else
587
+ current_node=$(find_child_by_key "$current_node" "$segment")
588
+ [ -z "$current_node" ] && return 1
589
+ fi
590
+ done
591
+
592
+ # Verify it's an array
593
+ local node_type
594
+ node_type=$(get_node_field "$current_node" 2)
595
+
596
+ if [ "$node_type" != "array" ]; then
597
+ echo "Error: Path does not point to an array" >&2
598
+ return 1
599
+ fi
600
+
601
+ # Create new scalar node
602
+ local new_node
603
+ new_node=$(create_node "scalar" "" "$value" "$current_node")
604
+
605
+ # Add as child
606
+ add_child "$current_node" "$new_node"
607
+
608
+ echo "$new_node"
609
+ }
610
+
611
+ # Append object to array
612
+ # Usage: yaml_array_append_object ".path.to.array"
613
+ # Returns: node_id of new object (use yaml_object_set to add fields)
614
+ yaml_array_append_object() {
615
+ local path="$1"
616
+
617
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
618
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
619
+ return 1
620
+ fi
621
+
622
+ # Find the node
623
+ path=$(echo "$path" | sed 's/^\.//')
624
+ local current_node="$AST_ROOT_ID"
625
+
626
+ local IFS='.'
627
+ for segment in $path; do
628
+ if echo "$segment" | grep -q '\['; then
629
+ local key index
630
+ key=$(echo "$segment" | sed 's/\[.*//')
631
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
632
+
633
+ current_node=$(find_child_by_key "$current_node" "$key")
634
+ [ -z "$current_node" ] && return 1
635
+
636
+ current_node=$(find_child_by_index "$current_node" "$index")
637
+ [ -z "$current_node" ] && return 1
638
+ else
639
+ current_node=$(find_child_by_key "$current_node" "$segment")
640
+ [ -z "$current_node" ] && return 1
641
+ fi
642
+ done
643
+
644
+ # Check node type
645
+ local node_type
646
+ node_type=$(get_node_field "$current_node" 2)
647
+
648
+ # If it's a map with no children, convert to array
649
+ if [ "$node_type" = "map" ]; then
650
+ local children
651
+ children=$(get_node_field "$current_node" 6)
652
+ if [ -z "$children" ]; then
653
+ # Empty map - convert to array
654
+ update_node_type "$current_node" "array"
655
+ else
656
+ echo "Error: Path points to non-empty map, not array" >&2
657
+ return 1
658
+ fi
659
+ elif [ "$node_type" != "array" ]; then
660
+ echo "Error: Path does not point to an array" >&2
661
+ return 1
662
+ fi
663
+
664
+ # Create new map node (object)
665
+ local new_node
666
+ new_node=$(create_node "map" "" "" "$current_node")
667
+
668
+ # Add as child
669
+ add_child "$current_node" "$new_node"
670
+
671
+ echo "$new_node"
672
+ }
673
+
674
+ # Set field on object (for building objects in arrays)
675
+ # Usage: yaml_object_set node_id "field_name" "value"
676
+ yaml_object_set() {
677
+ local object_node="$1"
678
+ local field_name="$2"
679
+ local field_value="$3"
680
+
681
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
682
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
683
+ return 1
684
+ fi
685
+
686
+ # Create scalar field
687
+ local field_node
688
+ field_node=$(create_node "scalar" "$field_name" "$field_value" "$object_node")
689
+
690
+ # Add as child
691
+ add_child "$object_node" "$field_node"
692
+
693
+ echo "$field_node"
694
+ }
695
+
696
+ # ============================================================================
697
+ # MAIN
698
+ # ============================================================================
699
+
700
+ trap cleanup_ast EXIT INT TERM
701
+
702
+ # Only run main if script is executed directly (not sourced)
703
+ if [ -n "$1" ] && [ "$1" != "-" ] && [ "${BASH_SOURCE[0]}" = "${0}" ]; then
704
+ case "$1" in
705
+ parse)
706
+ yaml_parse "$2"
707
+ echo "✓ Parsed $2 ($(get_next_node_id) nodes)"
708
+ ;;
709
+ query)
710
+ yaml_parse "$2"
711
+ yaml_query "$3"
712
+ ;;
713
+ set)
714
+ yaml_parse "$2"
715
+ yaml_set "$3" "$4"
716
+ yaml_write "$2"
717
+ echo "✓ Updated $2"
718
+ ;;
719
+ debug)
720
+ yaml_parse "$2"
721
+ echo "AST Contents:"
722
+ cat "$AST_FILE"
723
+ ;;
724
+ *)
725
+ echo "Usage: $0 {parse|query|set|debug} file.yaml [path] [value]"
726
+ echo ""
727
+ echo "Examples:"
728
+ echo " $0 parse file.yaml"
729
+ echo " $0 query file.yaml .name"
730
+ echo " $0 query file.yaml .tags[0]"
731
+ echo " $0 set file.yaml .version 2.0.0"
732
+ echo " $0 debug file.yaml"
733
+ exit 1
734
+ ;;
735
+ esac
736
+ fi