aidp 0.44.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: d898c7dea670e05503b8777e0907e0d0f975d398fd3cf80d9ddc659e8806070c
4
- data.tar.gz: c25d8e50f56ce0e89f93c3e5fea9d4fb0ca6d540c19ac5fd3d2710d38be57290
3
+ metadata.gz: 5efad8256d463a17afb3037a0648f87575b1468b1a87cc97ca9498f6bfe58d30
4
+ data.tar.gz: 709c073b1fb9202e0c5b6af353677dce774dccd6f885d0b90021d769fff9aa56
5
5
  SHA512:
6
- metadata.gz: ec307da67468acc15043f72c0295f443a7650a4312d1767f12629fbf582d02354a76cf2476081a313cb434c48eb63f5ec06d6d7d90f39d00d972342725d0dbe5
7
- data.tar.gz: 7bbb9cbfff2543d753b56ca940c19745b0b4678d9e4825c418639c5ec2cbeeac99069de846e75e8c8ef05a8d83b825d13d7e99b056a462e07a89c1f56a2cb96a
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
- JSON.parse(File.read(backup_path))
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
- File.write(path, JSON.pretty_generate(config))
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
data/lib/aidp/config.rb CHANGED
@@ -215,10 +215,17 @@ module Aidp
215
215
  priority: "Priority",
216
216
  skills: "Skills",
217
217
  personas: "Personas",
218
- blocking: "Blocking"
218
+ blocking: "Blocking",
219
+ start_date: "Start Date",
220
+ target_date: "Target Date",
221
+ dependencies: "Dependencies",
222
+ critical_path: "Critical Path"
219
223
  },
220
224
  auto_create_fields: true,
221
225
  sync_interval: 60,
226
+ prd_path: nil,
227
+ auto_sync_gantt: false,
228
+ gantt_format: "auto",
222
229
  default_status_values: ["Backlog", "Todo", "In Progress", "In Review", "Done"],
223
230
  default_priority_values: ["Low", "Medium", "High", "Critical"]
224
231
  },
@@ -512,6 +519,11 @@ module Aidp
512
519
  merged[:security] = deep_merge_hash(merged[:security], symbolize_keys(security_section))
513
520
  end
514
521
 
522
+ if config[:watch] || config["watch"]
523
+ watch_section = config[:watch] || config["watch"]
524
+ merged[:watch] = deep_merge_hash(merged[:watch], symbolize_keys(watch_section))
525
+ end
526
+
515
527
  merged
516
528
  end
517
529
 
@@ -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
- @config = JSON.parse(content)
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
@@ -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
- File.write(devcontainer_path, JSON.pretty_generate(new_config))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.44.0"
4
+ VERSION = "0.46.0"
5
5
  end