@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.
- package/.env.example +5 -0
- package/AGENT.md +1279 -0
- package/README.md +78 -0
- package/agent/commands/acp.command-create.md +372 -0
- package/agent/commands/acp.design-create.md +224 -0
- package/agent/commands/acp.init.md +410 -0
- package/agent/commands/acp.package-create.md +894 -0
- package/agent/commands/acp.package-info.md +211 -0
- package/agent/commands/acp.package-install.md +461 -0
- package/agent/commands/acp.package-list.md +279 -0
- package/agent/commands/acp.package-publish.md +540 -0
- package/agent/commands/acp.package-remove.md +292 -0
- package/agent/commands/acp.package-search.md +306 -0
- package/agent/commands/acp.package-update.md +310 -0
- package/agent/commands/acp.package-validate.md +535 -0
- package/agent/commands/acp.pattern-create.md +326 -0
- package/agent/commands/acp.plan.md +552 -0
- package/agent/commands/acp.proceed.md +336 -0
- package/agent/commands/acp.project-create.md +672 -0
- package/agent/commands/acp.report.md +394 -0
- package/agent/commands/acp.resume.md +237 -0
- package/agent/commands/acp.status.md +280 -0
- package/agent/commands/acp.sync.md +363 -0
- package/agent/commands/acp.task-create.md +390 -0
- package/agent/commands/acp.update.md +301 -0
- package/agent/commands/acp.validate.md +436 -0
- package/agent/commands/acp.version-check-for-updates.md +275 -0
- package/agent/commands/acp.version-check.md +190 -0
- package/agent/commands/acp.version-update.md +288 -0
- package/agent/commands/command.template.md +316 -0
- package/agent/commands/git.commit.md +513 -0
- package/agent/commands/git.init.md +513 -0
- package/agent/commands/mcp-server-starter.add-tool.md +677 -0
- package/agent/commands/mcp-server-starter.init.md +894 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +136 -0
- package/agent/design/remember-mcp-analysis.md +987 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +109 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/package.template.yaml +36 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/mcp-server-starter.bootstrap.md +597 -0
- package/agent/patterns/mcp-server-starter.build-config.md +554 -0
- package/agent/patterns/mcp-server-starter.config-management.md +525 -0
- package/agent/patterns/mcp-server-starter.server-factory.md +616 -0
- package/agent/patterns/mcp-server-starter.server-standalone.md +642 -0
- package/agent/patterns/mcp-server-starter.test-config.md +558 -0
- package/agent/patterns/mcp-server-starter.tool-creation.md +653 -0
- package/agent/patterns/pattern.template.md +364 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +33 -0
- package/agent/schemas/package.schema.yaml +161 -0
- package/agent/scripts/acp.common.sh +1362 -0
- package/agent/scripts/acp.install.sh +213 -0
- package/agent/scripts/acp.package-create.sh +925 -0
- package/agent/scripts/acp.package-info.sh +270 -0
- package/agent/scripts/acp.package-install.sh +550 -0
- package/agent/scripts/acp.package-list.sh +263 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +272 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +356 -0
- package/agent/scripts/acp.package-validate.sh +766 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +158 -0
- package/agent/scripts/acp.yaml-parser.sh +736 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/task-1-{title}.template.md +225 -0
- package/dist/config.d.ts +4 -0
- package/dist/server-factory.d.ts +9 -0
- package/dist/server-factory.js +99 -0
- package/dist/server-factory.js.map +7 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +106 -0
- package/dist/server.js.map +7 -0
- package/dist/tools/acp-remote-list-files.d.ts +15 -0
- package/dist/types/ssh-config.d.ts +16 -0
- package/esbuild.build.js +34 -0
- package/esbuild.watch.js +31 -0
- package/jest.config.js +31 -0
- package/package.json +54 -0
- package/src/config.ts +16 -0
- package/src/server-factory.ts +43 -0
- package/src/server.ts +46 -0
- package/src/tools/acp-remote-list-files.ts +89 -0
- package/src/types/ssh-config.ts +17 -0
- 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
|