aidp 0.45.0 → 0.46.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.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5efad8256d463a17afb3037a0648f87575b1468b1a87cc97ca9498f6bfe58d30
|
|
4
|
+
data.tar.gz: 709c073b1fb9202e0c5b6af353677dce774dccd6f885d0b90021d769fff9aa56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8bd69d797c40548a5b34ebb669ebb6f83504f4286144bef7c34380aa3d5be1bee3c14d3472a3a1b4477896ca330abdcbee318abb80cbfbf08846bca2ea65d17d
|
|
7
|
+
data.tar.gz: a5f2baa7b258fbf34810d4849171f47627fe2c3f30df747a134b0f60de8bc88f0a7ea7099c60909c770e698545853802671213e35fa3c7955bbfd5668b817b1b
|
|
@@ -58,6 +58,7 @@ module Aidp
|
|
|
58
58
|
|
|
59
59
|
parser = Aidp::Setup::Devcontainer::Parser.new(@project_dir)
|
|
60
60
|
existing_config = parser.devcontainer_exists? ? parser.parse : nil
|
|
61
|
+
existing_comments = existing_config ? parser.comments : {}
|
|
61
62
|
devcontainer_path = parser.detect || default_devcontainer_path
|
|
62
63
|
|
|
63
64
|
# Load configuration
|
|
@@ -112,7 +113,7 @@ module Aidp
|
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
# Write devcontainer.json
|
|
115
|
-
write_devcontainer(devcontainer_path, final_config)
|
|
116
|
+
write_devcontainer(devcontainer_path, final_config, comments: existing_comments)
|
|
116
117
|
|
|
117
118
|
display_message("✅ Devcontainer configuration applied", type: :success)
|
|
118
119
|
display_message(" #{devcontainer_path}", type: :muted)
|
|
@@ -191,7 +192,7 @@ module Aidp
|
|
|
191
192
|
target_path = parser.detect || default_devcontainer_path
|
|
192
193
|
|
|
193
194
|
# Show what will be restored
|
|
194
|
-
|
|
195
|
+
Aidp::Setup::Devcontainer::JsoncDocument.parse(File.read(backup_path))
|
|
195
196
|
display_message("📦 Restoring Backup", type: :highlight)
|
|
196
197
|
display_message(" From: #{File.basename(backup_path)}", type: :muted)
|
|
197
198
|
display_message(" To: #{target_path}", type: :muted)
|
|
@@ -473,9 +474,14 @@ module Aidp
|
|
|
473
474
|
end
|
|
474
475
|
end
|
|
475
476
|
|
|
476
|
-
def write_devcontainer(path, config)
|
|
477
|
+
def write_devcontainer(path, config, comments: {})
|
|
477
478
|
FileUtils.mkdir_p(File.dirname(path))
|
|
478
|
-
|
|
479
|
+
content = if comments.any?
|
|
480
|
+
Aidp::Setup::Devcontainer::JsoncDocument.dump(config, comments: comments)
|
|
481
|
+
else
|
|
482
|
+
JSON.pretty_generate(config)
|
|
483
|
+
end
|
|
484
|
+
File.write(path, content)
|
|
479
485
|
end
|
|
480
486
|
|
|
481
487
|
def default_devcontainer_path
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Setup
|
|
7
|
+
module Devcontainer
|
|
8
|
+
# Parses and serializes JSON with comments while preserving comment blocks.
|
|
9
|
+
class JsoncDocument
|
|
10
|
+
attr_reader :data, :comments
|
|
11
|
+
|
|
12
|
+
def self.parse(text)
|
|
13
|
+
new(text).parse
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.dump(data, comments: {})
|
|
17
|
+
new(nil).send(:dump, data, comments)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(text)
|
|
21
|
+
@text = text
|
|
22
|
+
@index = 0
|
|
23
|
+
@comments = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse
|
|
27
|
+
root_comments = consume_comments
|
|
28
|
+
store_comments([], root_comments, position: :leading) if root_comments.any?
|
|
29
|
+
|
|
30
|
+
value = parse_value([])
|
|
31
|
+
trailing_comments = consume_comments
|
|
32
|
+
store_comments([], trailing_comments, position: :trailing) if trailing_comments.any?
|
|
33
|
+
skip_whitespace
|
|
34
|
+
raise ParseError, "Unexpected trailing content" unless eof?
|
|
35
|
+
|
|
36
|
+
@data = value
|
|
37
|
+
self
|
|
38
|
+
rescue ParseError => e
|
|
39
|
+
raise e
|
|
40
|
+
rescue => e
|
|
41
|
+
raise ParseError, e.message
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
class ParseError < StandardError; end
|
|
47
|
+
|
|
48
|
+
def dump(data, comments)
|
|
49
|
+
writer = Writer.new(data, comments)
|
|
50
|
+
writer.dump
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_value(path)
|
|
54
|
+
skip_trivia
|
|
55
|
+
|
|
56
|
+
case current_char
|
|
57
|
+
when "{"
|
|
58
|
+
parse_object(path)
|
|
59
|
+
when "["
|
|
60
|
+
parse_array(path)
|
|
61
|
+
when '"'
|
|
62
|
+
parse_string
|
|
63
|
+
when "t", "f", "n"
|
|
64
|
+
parse_literal
|
|
65
|
+
else
|
|
66
|
+
parse_number
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_object(path)
|
|
71
|
+
expect("{")
|
|
72
|
+
advance while whitespace?(current_char)
|
|
73
|
+
|
|
74
|
+
result = {}
|
|
75
|
+
return result if consume_if("}")
|
|
76
|
+
|
|
77
|
+
loop do
|
|
78
|
+
leading_comments = consume_comments
|
|
79
|
+
break if consume_if("}")
|
|
80
|
+
|
|
81
|
+
key = parse_string
|
|
82
|
+
skip_trivia
|
|
83
|
+
expect(":")
|
|
84
|
+
|
|
85
|
+
value_path = path + [key]
|
|
86
|
+
value = parse_value(value_path)
|
|
87
|
+
inline_comments = consume_inline_comments
|
|
88
|
+
trailing_comments = consume_comments
|
|
89
|
+
store_comments(value_path, leading_comments, position: :leading, value: value)
|
|
90
|
+
store_comments(value_path, inline_comments, position: :inline, value: value)
|
|
91
|
+
store_comments(value_path, trailing_comments, position: :trailing, value: value)
|
|
92
|
+
result[key] = value
|
|
93
|
+
|
|
94
|
+
skip_trivia
|
|
95
|
+
break if consume_if("}")
|
|
96
|
+
expect(",")
|
|
97
|
+
skip_whitespace
|
|
98
|
+
break if consume_if("}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def parse_array(path)
|
|
105
|
+
expect("[")
|
|
106
|
+
advance while whitespace?(current_char)
|
|
107
|
+
|
|
108
|
+
result = []
|
|
109
|
+
return result if consume_if("]")
|
|
110
|
+
|
|
111
|
+
index = 0
|
|
112
|
+
loop do
|
|
113
|
+
leading_comments = consume_comments
|
|
114
|
+
break if consume_if("]")
|
|
115
|
+
|
|
116
|
+
value_path = path + [index]
|
|
117
|
+
value = parse_value(value_path)
|
|
118
|
+
inline_comments = consume_inline_comments
|
|
119
|
+
trailing_comments = consume_comments
|
|
120
|
+
store_comments(value_path, leading_comments, position: :leading, value: value)
|
|
121
|
+
store_comments(value_path, inline_comments, position: :inline, value: value)
|
|
122
|
+
store_comments(value_path, trailing_comments, position: :trailing, value: value)
|
|
123
|
+
result << value
|
|
124
|
+
|
|
125
|
+
index += 1
|
|
126
|
+
skip_trivia
|
|
127
|
+
break if consume_if("]")
|
|
128
|
+
expect(",")
|
|
129
|
+
skip_whitespace
|
|
130
|
+
break if consume_if("]")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
result
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def parse_string
|
|
137
|
+
expect('"')
|
|
138
|
+
chars = +""
|
|
139
|
+
|
|
140
|
+
until eof?
|
|
141
|
+
char = current_char
|
|
142
|
+
|
|
143
|
+
case char
|
|
144
|
+
when '"'
|
|
145
|
+
advance
|
|
146
|
+
return chars
|
|
147
|
+
when "\\"
|
|
148
|
+
advance
|
|
149
|
+
chars << parse_escape_sequence
|
|
150
|
+
else
|
|
151
|
+
raise ParseError, "Invalid control character in string" if control_character?(char)
|
|
152
|
+
|
|
153
|
+
chars << char
|
|
154
|
+
advance
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
raise ParseError, "Unterminated string"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def parse_escape_sequence
|
|
162
|
+
raise ParseError, "Unterminated escape sequence" if eof?
|
|
163
|
+
|
|
164
|
+
char = current_char
|
|
165
|
+
advance
|
|
166
|
+
|
|
167
|
+
case char
|
|
168
|
+
when '"', "\\", "/"
|
|
169
|
+
char
|
|
170
|
+
when "b" then "\b"
|
|
171
|
+
when "f" then "\f"
|
|
172
|
+
when "n" then "\n"
|
|
173
|
+
when "r" then "\r"
|
|
174
|
+
when "t" then "\t"
|
|
175
|
+
when "u"
|
|
176
|
+
parse_unicode_escape
|
|
177
|
+
else
|
|
178
|
+
raise ParseError, "Invalid escape sequence"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def parse_unicode_escape
|
|
183
|
+
codepoint = read_unicode_codepoint
|
|
184
|
+
raise ParseError, "Invalid Unicode escape sequence" if low_surrogate?(codepoint)
|
|
185
|
+
|
|
186
|
+
return [codepoint].pack("U") unless high_surrogate?(codepoint)
|
|
187
|
+
|
|
188
|
+
low_surrogate = read_low_surrogate
|
|
189
|
+
[combine_surrogate_pair(codepoint, low_surrogate)].pack("U")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def read_unicode_codepoint
|
|
193
|
+
hex = read_chars(4)
|
|
194
|
+
raise ParseError, "Invalid Unicode escape sequence" unless hex.match?(/\A\h{4}\z/)
|
|
195
|
+
|
|
196
|
+
hex.to_i(16)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def read_low_surrogate
|
|
200
|
+
raise ParseError, "Invalid Unicode escape sequence" unless consume_sequence("\\u")
|
|
201
|
+
|
|
202
|
+
codepoint = read_unicode_codepoint
|
|
203
|
+
raise ParseError, "Invalid Unicode escape sequence" unless low_surrogate?(codepoint)
|
|
204
|
+
|
|
205
|
+
codepoint
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def combine_surrogate_pair(high_surrogate, low_surrogate)
|
|
209
|
+
0x10000 + ((high_surrogate - 0xD800) << 10) + (low_surrogate - 0xDC00)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def parse_literal
|
|
213
|
+
return true if consume_sequence("true")
|
|
214
|
+
return false if consume_sequence("false")
|
|
215
|
+
return nil if consume_sequence("null")
|
|
216
|
+
|
|
217
|
+
raise ParseError, "Invalid literal"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def parse_number
|
|
221
|
+
match = remaining.match(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/)
|
|
222
|
+
raise ParseError, "Invalid number" unless match
|
|
223
|
+
|
|
224
|
+
number = match[0]
|
|
225
|
+
advance_by(number.length)
|
|
226
|
+
(number.include?(".") || number.match?(/[eE]/)) ? number.to_f : number.to_i
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def skip_trivia
|
|
230
|
+
loop do
|
|
231
|
+
skip_whitespace
|
|
232
|
+
|
|
233
|
+
if consume_sequence("//")
|
|
234
|
+
advance while !eof? && current_char != "\n"
|
|
235
|
+
elsif consume_sequence("/*")
|
|
236
|
+
until eof? || consume_sequence("*/")
|
|
237
|
+
advance
|
|
238
|
+
end
|
|
239
|
+
raise ParseError, "Unterminated block comment" if eof?
|
|
240
|
+
else
|
|
241
|
+
break
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def skip_whitespace
|
|
247
|
+
advance while whitespace?(current_char)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def consume_comments
|
|
251
|
+
comments = []
|
|
252
|
+
|
|
253
|
+
loop do
|
|
254
|
+
advance while whitespace?(current_char)
|
|
255
|
+
|
|
256
|
+
if consume_sequence("//")
|
|
257
|
+
comment = +"//"
|
|
258
|
+
until eof? || current_char == "\n"
|
|
259
|
+
comment << current_char
|
|
260
|
+
advance
|
|
261
|
+
end
|
|
262
|
+
comments << comment.rstrip
|
|
263
|
+
elsif consume_sequence("/*")
|
|
264
|
+
comment = +"/*"
|
|
265
|
+
until eof?
|
|
266
|
+
if consume_sequence("*/")
|
|
267
|
+
comment << "*/"
|
|
268
|
+
comments << comment.rstrip
|
|
269
|
+
break
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
comment << current_char
|
|
273
|
+
advance
|
|
274
|
+
end
|
|
275
|
+
raise ParseError, "Unterminated block comment" if eof? && !comments.last&.end_with?("*/")
|
|
276
|
+
else
|
|
277
|
+
break
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
comments
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def consume_inline_comments
|
|
285
|
+
start_index = @index
|
|
286
|
+
advance while horizontal_whitespace?(current_char)
|
|
287
|
+
|
|
288
|
+
return [] unless remaining.start_with?("//", "/*")
|
|
289
|
+
|
|
290
|
+
comments = consume_comments
|
|
291
|
+
return comments if comments.any?
|
|
292
|
+
|
|
293
|
+
@index = start_index
|
|
294
|
+
[]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def store_comments(path, comments, position:, value: nil)
|
|
298
|
+
return if comments.empty?
|
|
299
|
+
|
|
300
|
+
append_comments(comment_key(path, position), comments)
|
|
301
|
+
store_array_value_comments(path, value, comments, position)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def path_key(path)
|
|
305
|
+
path.map { |part| "#{path_prefix(part)}:#{part}" }.join("\u0000")
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def path_prefix(part)
|
|
309
|
+
part.instance_of?(Integer) ? "i" : "s"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def append_comments(key, comments)
|
|
313
|
+
@comments[key] ||= []
|
|
314
|
+
@comments[key].concat(comments)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def comment_key(path, position)
|
|
318
|
+
"#{path_key(path)}\u0000c:#{position}"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def store_array_value_comments(path, value, comments, position)
|
|
322
|
+
return unless path.last.instance_of?(Integer)
|
|
323
|
+
|
|
324
|
+
append_comments(array_value_key(path[0...-1], value, position), comments)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def array_value_key(path, value, position)
|
|
328
|
+
"#{path_key(path)}\u0000a:#{JSON.generate(value)}\u0000c:#{position}"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def consume_if(char)
|
|
332
|
+
return false unless current_char == char
|
|
333
|
+
|
|
334
|
+
advance
|
|
335
|
+
true
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def expect(char)
|
|
339
|
+
return if consume_if(char)
|
|
340
|
+
|
|
341
|
+
raise ParseError, "Expected #{char}"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def consume_sequence(sequence)
|
|
345
|
+
return false unless remaining.start_with?(sequence)
|
|
346
|
+
|
|
347
|
+
advance_by(sequence.length)
|
|
348
|
+
true
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def read_chars(count)
|
|
352
|
+
value = remaining[0, count]
|
|
353
|
+
raise ParseError, "Unexpected end of input" unless value && value.length == count
|
|
354
|
+
|
|
355
|
+
advance_by(count)
|
|
356
|
+
value
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def current_char
|
|
360
|
+
(@text && !eof?) ? @text[@index] : nil
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def remaining
|
|
364
|
+
return "" if @text.nil? || eof?
|
|
365
|
+
|
|
366
|
+
@text[@index..]
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def advance
|
|
370
|
+
@index += 1
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def advance_by(count)
|
|
374
|
+
@index += count
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def eof?
|
|
378
|
+
@text.nil? || @index >= @text.length
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def whitespace?(char)
|
|
382
|
+
char&.match?(/\s/)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def horizontal_whitespace?(char)
|
|
386
|
+
char == " " || char == "\t"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def control_character?(char)
|
|
390
|
+
char.ord < 0x20
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def high_surrogate?(codepoint)
|
|
394
|
+
codepoint.between?(0xD800, 0xDBFF)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def low_surrogate?(codepoint)
|
|
398
|
+
codepoint.between?(0xDC00, 0xDFFF)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Serializes JSON while re-inserting preserved comments before values.
|
|
402
|
+
class Writer
|
|
403
|
+
def initialize(data, comments)
|
|
404
|
+
@data = data
|
|
405
|
+
@comments = comments
|
|
406
|
+
@output = +""
|
|
407
|
+
@array_comment_offsets = Hash.new(0)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def dump
|
|
411
|
+
write_comments([], 0, position: :leading)
|
|
412
|
+
write_value(@data, [])
|
|
413
|
+
write_root_trailing_comments
|
|
414
|
+
@output
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private
|
|
418
|
+
|
|
419
|
+
def write_value(value, path, indent = 0)
|
|
420
|
+
case value
|
|
421
|
+
when Hash
|
|
422
|
+
write_object(value, path, indent)
|
|
423
|
+
when Array
|
|
424
|
+
write_array(value, path, indent)
|
|
425
|
+
else
|
|
426
|
+
@output << JSON.generate(value)
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def write_object(hash, path, indent)
|
|
431
|
+
if hash.empty?
|
|
432
|
+
@output << "{}"
|
|
433
|
+
return
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
@output << "{\n"
|
|
437
|
+
entries = hash.to_a
|
|
438
|
+
|
|
439
|
+
entries.each_with_index do |(key, value), index|
|
|
440
|
+
child_path = path + [key]
|
|
441
|
+
write_comments(child_path, indent + 2, position: :leading)
|
|
442
|
+
@output << (" " * (indent + 2))
|
|
443
|
+
@output << JSON.generate(key)
|
|
444
|
+
@output << ": "
|
|
445
|
+
write_value(value, child_path, indent + 2)
|
|
446
|
+
write_inline_comments(child_path)
|
|
447
|
+
@output << "," if index < entries.length - 1
|
|
448
|
+
@output << "\n"
|
|
449
|
+
write_comments(child_path, indent + 2, position: :trailing)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
@output << (" " * indent) << "}"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def write_array(array, path, indent)
|
|
456
|
+
if array.empty?
|
|
457
|
+
@output << "[]"
|
|
458
|
+
return
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
@output << "[\n"
|
|
462
|
+
array.each_with_index do |value, index|
|
|
463
|
+
child_path = path + [index]
|
|
464
|
+
write_comments(child_path, indent + 2, position: :leading)
|
|
465
|
+
@output << (" " * (indent + 2))
|
|
466
|
+
write_value(value, child_path, indent + 2)
|
|
467
|
+
write_inline_comments(child_path)
|
|
468
|
+
@output << "," if index < array.length - 1
|
|
469
|
+
@output << "\n"
|
|
470
|
+
write_comments(child_path, indent + 2, position: :trailing)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
@output << (" " * indent) << "]"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def write_comments(path, indent, position:)
|
|
477
|
+
comments_for(path, position).each do |comment|
|
|
478
|
+
comment.each_line do |line|
|
|
479
|
+
@output << (" " * indent) << line.rstrip << "\n"
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def write_inline_comments(path)
|
|
485
|
+
comments_for(path, :inline).each do |comment|
|
|
486
|
+
@output << " " << comment
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def write_root_trailing_comments
|
|
491
|
+
trailing_comments = comments_for([], :trailing)
|
|
492
|
+
return if trailing_comments.empty?
|
|
493
|
+
|
|
494
|
+
@output << "\n"
|
|
495
|
+
write_comments([], 0, position: :trailing)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def comments_for(path, position)
|
|
499
|
+
return array_comments_for(path, position) if path.last.instance_of?(Integer)
|
|
500
|
+
|
|
501
|
+
@comments[comment_key(path, position)] || []
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def path_key(path)
|
|
505
|
+
path.map { |part| "#{path_prefix(part)}:#{part}" }.join("\u0000")
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def path_prefix(part)
|
|
509
|
+
part.instance_of?(Integer) ? "i" : "s"
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def comment_key(path, position)
|
|
513
|
+
"#{path_key(path)}\u0000c:#{position}"
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def array_comments_for(path, position)
|
|
517
|
+
parent_path = path[0...-1]
|
|
518
|
+
array = @data.dig(*parent_path)
|
|
519
|
+
return [] unless array.is_a?(Array)
|
|
520
|
+
|
|
521
|
+
key = array_value_key(parent_path, array[path.last], position)
|
|
522
|
+
comments = @comments[key]
|
|
523
|
+
return [] if comments.nil? || comments.empty?
|
|
524
|
+
|
|
525
|
+
offset = @array_comment_offsets[key]
|
|
526
|
+
return [] if offset >= comments.length
|
|
527
|
+
|
|
528
|
+
@array_comment_offsets[key] += 1
|
|
529
|
+
[comments[offset]]
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def array_value_key(path, value, position)
|
|
533
|
+
"#{path_key(path)}\u0000a:#{JSON.generate(value)}\u0000c:#{position}"
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require_relative "jsonc_document"
|
|
5
6
|
|
|
6
7
|
module Aidp
|
|
7
8
|
module Setup
|
|
@@ -18,7 +19,7 @@ module Aidp
|
|
|
18
19
|
"devcontainer.json"
|
|
19
20
|
].freeze
|
|
20
21
|
|
|
21
|
-
attr_reader :project_dir, :devcontainer_path, :config
|
|
22
|
+
attr_reader :project_dir, :devcontainer_path, :config, :comments
|
|
22
23
|
|
|
23
24
|
def initialize(project_dir = Dir.pwd)
|
|
24
25
|
@project_dir = project_dir
|
|
@@ -61,12 +62,14 @@ module Aidp
|
|
|
61
62
|
|
|
62
63
|
begin
|
|
63
64
|
content = File.read(@devcontainer_path)
|
|
64
|
-
|
|
65
|
+
document = JsoncDocument.parse(content)
|
|
66
|
+
@config = document.data
|
|
67
|
+
@comments = document.comments
|
|
65
68
|
Aidp.log_debug("devcontainer_parser", "parsed devcontainer",
|
|
66
69
|
features_count: extract_features.size,
|
|
67
70
|
ports_count: extract_ports.size)
|
|
68
71
|
@config
|
|
69
|
-
rescue JSON::ParserError => e
|
|
72
|
+
rescue JsoncDocument::ParseError, JSON::ParserError => e
|
|
70
73
|
Aidp.log_error("devcontainer_parser", "invalid JSON", error: e.message, path: @devcontainer_path)
|
|
71
74
|
raise InvalidDevcontainerError, "Invalid JSON in #{@devcontainer_path}: #{e.message}"
|
|
72
75
|
rescue => e
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -2888,6 +2888,7 @@ module Aidp
|
|
|
2888
2888
|
|
|
2889
2889
|
parser = Devcontainer::Parser.new(project_dir)
|
|
2890
2890
|
existing = parser.devcontainer_exists? ? parser.parse : nil
|
|
2891
|
+
existing_comments = existing ? parser.comments : {}
|
|
2891
2892
|
|
|
2892
2893
|
generator = Devcontainer::Generator.new(project_dir, @config)
|
|
2893
2894
|
new_config = generator.generate(wizard_config, existing)
|
|
@@ -2907,7 +2908,12 @@ module Aidp
|
|
|
2907
2908
|
# Write devcontainer.json
|
|
2908
2909
|
devcontainer_path = File.join(project_dir, ".devcontainer", "devcontainer.json")
|
|
2909
2910
|
FileUtils.mkdir_p(File.dirname(devcontainer_path))
|
|
2910
|
-
|
|
2911
|
+
content = if existing_comments.any?
|
|
2912
|
+
Devcontainer::JsoncDocument.dump(new_config, comments: existing_comments)
|
|
2913
|
+
else
|
|
2914
|
+
JSON.pretty_generate(new_config)
|
|
2915
|
+
end
|
|
2916
|
+
File.write(devcontainer_path, content)
|
|
2911
2917
|
|
|
2912
2918
|
prompt.ok("✅ Generated #{devcontainer_path}")
|
|
2913
2919
|
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.complete",
|
data/lib/aidp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aidp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.46.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -551,6 +551,7 @@ files:
|
|
|
551
551
|
- lib/aidp/security/work_loop_adapter.rb
|
|
552
552
|
- lib/aidp/setup/devcontainer/backup_manager.rb
|
|
553
553
|
- lib/aidp/setup/devcontainer/generator.rb
|
|
554
|
+
- lib/aidp/setup/devcontainer/jsonc_document.rb
|
|
554
555
|
- lib/aidp/setup/devcontainer/parser.rb
|
|
555
556
|
- lib/aidp/setup/devcontainer/port_manager.rb
|
|
556
557
|
- lib/aidp/setup/in_memory_config_adapter.rb
|