ro 4.2.0 → 5.0.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 +4 -4
- data/Gemfile.lock +57 -10
- data/LICENSE +1 -1
- data/MIGRATION.md +320 -0
- data/README.md +286 -111
- data/Rakefile +2 -2
- data/a.yml +60 -0
- data/bin/ro +10 -0
- data/lib/ro/_lib.rb +18 -6
- data/lib/ro/asset.rb +67 -16
- data/lib/ro/collection.rb +91 -10
- data/lib/ro/config.rb +4 -0
- data/lib/ro/error.rb +5 -2
- data/lib/ro/html.rb +23 -0
- data/lib/ro/html_safe.rb +143 -0
- data/lib/ro/methods.rb +95 -38
- data/lib/ro/migrator.rb +285 -0
- data/lib/ro/node.rb +128 -45
- data/lib/ro/path.rb +4 -0
- data/lib/ro/root.rb +75 -1
- data/lib/ro/script/migrate.rb +204 -0
- data/lib/ro/script/server.rb +1 -1
- data/lib/ro/template.rb +62 -22
- data/lib/ro/text.rb +120 -0
- data/lib/ro.rb +5 -0
- data/public/api/ro/index-1.json +997 -79
- data/public/api/ro/index.json +997 -79
- data/public/api/ro/nerd/fastest-possible-embeddings/index.json +90 -0
- data/public/api/ro/nerd/ima/index.json +49 -0
- data/public/api/ro/nerd/index/index.json +74 -0
- data/public/api/ro/nerd/index-1.json +204 -0
- data/public/api/ro/nerd/index.json +194 -0
- data/public/api/ro/pages/about/index.json +60 -0
- data/public/api/ro/pages/contact/index.json +50 -0
- data/public/api/ro/pages/cv/index.json +49 -0
- data/public/api/ro/pages/disco/index.json +117 -0
- data/public/api/ro/pages/index/index.json +30 -0
- data/public/api/ro/pages/index-1.json +366 -0
- data/public/api/ro/pages/index.json +356 -0
- data/public/api/ro/pages/jess/index.json +62 -0
- data/public/api/ro/pages/now/index.json +43 -0
- data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +265 -0
- data/public/api/ro/posts/facebook-and-global-extremism/index.json +90 -0
- data/public/api/ro/posts/index-1.json +461 -79
- data/public/api/ro/posts/index.json +461 -79
- data/public/api/ro/posts/lemmings-considered-harmful/index.json +49 -0
- data/public/api/ro/posts/lost-in-the-desert/index.json +49 -0
- data/public/api/ro/posts/mission/index.json +49 -0
- data/public/api/ro/posts/return-your-laptop/index.json +61 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/giraffe.jpeg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/let-me-in.jpg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.js +70 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.rs +68 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/terminal.jpg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/body.md +266 -0
- data/public/ro/nerd/fastest-possible-embeddings.yml +7 -0
- data/public/ro/nerd/ima/assets/og.jpeg +0 -0
- data/public/ro/nerd/ima/body.md +22 -0
- data/public/ro/nerd/ima.yml +8 -0
- data/public/ro/nerd/index/assets/giraffe.jpeg +0 -0
- data/public/ro/nerd/index/assets/let-me-in.jpg +0 -0
- data/public/ro/nerd/index/assets/terminal.jpg +0 -0
- data/public/ro/nerd/index/body.md +130 -0
- data/public/ro/nerd/index.yml +7 -0
- data/public/ro/pages/about/assets/og.jpeg +0 -0
- data/public/ro/pages/about/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/about/body.md +40 -0
- data/public/ro/pages/contact/assets/giraffe.jpeg +0 -0
- data/public/ro/pages/contact/body.md +9 -0
- data/public/ro/pages/contact.yml +7 -0
- data/public/ro/pages/cv/assets/ara.jpg +0 -0
- data/public/ro/pages/cv/body.md +122 -0
- data/public/ro/pages/cv.yml +6 -0
- data/public/ro/pages/disco/assets/disco.jpg +0 -0
- data/public/ro/pages/disco/assets/disco.png +0 -0
- data/public/ro/pages/disco/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/disco/assets/src/environment.md +2354 -0
- data/public/ro/pages/disco/assets/src/fortune-500.md +2518 -0
- data/public/ro/pages/disco/assets/src/greed.md +2703 -0
- data/public/ro/pages/disco/assets/src/up-at-night.md +2337 -0
- data/public/ro/pages/disco/body.md +99 -0
- data/public/ro/pages/disco/samples/environment.md +2354 -0
- data/public/ro/pages/disco/samples/fortune-500.md +2518 -0
- data/public/ro/pages/disco/samples/greed.md +2703 -0
- data/public/ro/pages/disco/samples/up-at-night.md +2337 -0
- data/public/ro/pages/disco.yml +9 -0
- data/public/ro/pages/index/body.md +15 -0
- data/public/ro/pages/index.yml +1 -0
- data/public/ro/pages/jess/assets/og.jpg +0 -0
- data/public/ro/pages/jess/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/jess/body.md +3 -0
- data/public/ro/pages/jess.yml +7 -0
- data/public/ro/pages/now/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/now/body.md +24 -0
- data/public/ro/pages/now.yml +1 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image1.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image10.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image11.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image12.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image13.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image14.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image15.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image2.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image3.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image4.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image5.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image6.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image7.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image8.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image9.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/josh-pointing.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/levi-rawr.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/og.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/purple-heart.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/body.md +419 -0
- data/public/ro/posts/almost-died-in-an-ice-cave.yml +6 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/background.html +125 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/background.md +95 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/og.jpg +0 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/prompt.txt +122 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/results.md +183 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/survey.txt +190 -0
- data/public/ro/posts/facebook-and-global-extremism/body.md +393 -0
- data/public/ro/posts/facebook-and-global-extremism.yml +7 -0
- data/public/ro/posts/lemmings-considered-harmful/assets/lemming.jpeg +0 -0
- data/public/ro/posts/lemmings-considered-harmful/body.md +43 -0
- data/public/ro/posts/lemmings-considered-harmful.yml +6 -0
- data/public/ro/posts/lost-in-the-desert/assets/og.jpg +0 -0
- data/public/ro/posts/lost-in-the-desert/body.md +7 -0
- data/public/ro/posts/lost-in-the-desert.yml +6 -0
- data/public/ro/posts/mission/assets/og.jpg +0 -0
- data/public/ro/posts/mission/body.md +4 -0
- data/public/ro/posts/mission.yml +6 -0
- data/public/ro/posts/return-your-laptop/assets/og.jpg +0 -0
- data/public/ro/posts/return-your-laptop/assets/return-your-laptop.png +0 -0
- data/public/ro/posts/return-your-laptop/body.md +58 -0
- data/public/ro/posts/return-your-laptop.yml +6 -0
- data/ro.gemspec +369 -49
- data/scripts/speedtest.rb +324 -0
- data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
- data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
- data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
- data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
- data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
- data/specs/001-simplify-asset-structure/data-model.md +381 -0
- data/specs/001-simplify-asset-structure/plan.md +90 -0
- data/specs/001-simplify-asset-structure/quickstart.md +575 -0
- data/specs/001-simplify-asset-structure/research.md +333 -0
- data/specs/001-simplify-asset-structure/spec.md +127 -0
- data/specs/001-simplify-asset-structure/tasks.md +349 -0
- data/test/fixtures/new_structure/mixed/test-json.json +5 -0
- data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
- data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
- data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
- data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
- data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
- data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
- data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
- data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
- data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
- data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
- data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
- data/test/integration/ro_integration_test.rb +165 -0
- data/test/test_helper.rb +149 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
- data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
- data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
- data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
- data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
- data/test/unit/asset_test.rb +90 -0
- data/test/unit/collection_test.rb +127 -0
- data/test/unit/migrator_test.rb +209 -0
- data/test/unit/node_test.rb +138 -0
- data/tmp/gem-details.oe +0 -0
- metadata +250 -33
- data/public/api/ro/posts/first_post/index.json +0 -52
- data/public/api/ro/posts/second_post/index.json +0 -51
- data/public/api/ro/posts/third_post/index.json +0 -51
- data/public/ro/posts/first_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/first_post/assets/foo.jpg +0 -0
- data/public/ro/posts/first_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/first_post/attributes.yml +0 -2
- data/public/ro/posts/first_post/blurb.erb.md +0 -7
- data/public/ro/posts/first_post/body.md +0 -16
- data/public/ro/posts/first_post/testing.txt +0 -3
- data/public/ro/posts/second_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/second_post/assets/foo.jpg +0 -0
- data/public/ro/posts/second_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/second_post/attributes.yml +0 -2
- data/public/ro/posts/second_post/blurb.erb.md +0 -5
- data/public/ro/posts/second_post/body.md +0 -16
- data/public/ro/posts/third_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/third_post/assets/foo.jpg +0 -0
- data/public/ro/posts/third_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/third_post/attributes.yml +0 -2
- data/public/ro/posts/third_post/blurb.erb.md +0 -5
- data/public/ro/posts/third_post/body.md +0 -16
data/lib/ro/migrator.rb
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
module Ro
|
|
2
|
+
class Migrator
|
|
3
|
+
attr_reader :root_path, :options
|
|
4
|
+
|
|
5
|
+
def initialize(root_path, options = {})
|
|
6
|
+
@root_path = Path.for(root_path)
|
|
7
|
+
@options = {
|
|
8
|
+
dry_run: false,
|
|
9
|
+
backup: false,
|
|
10
|
+
verbose: false,
|
|
11
|
+
force: false
|
|
12
|
+
}.merge(options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def dry_run?
|
|
16
|
+
@options[:dry_run]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def backup?
|
|
20
|
+
@options[:backup]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def verbose?
|
|
24
|
+
@options[:verbose]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def force?
|
|
28
|
+
@options[:force]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Validate the structure and return analysis
|
|
32
|
+
def validate
|
|
33
|
+
result = {
|
|
34
|
+
has_old_structure: false,
|
|
35
|
+
has_new_structure: false,
|
|
36
|
+
old_nodes: [],
|
|
37
|
+
new_nodes: [],
|
|
38
|
+
collections: []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
root = Root.for(@root_path)
|
|
42
|
+
|
|
43
|
+
root.collections.each do |collection|
|
|
44
|
+
collection_name = collection.name
|
|
45
|
+
result[:collections] << collection_name
|
|
46
|
+
|
|
47
|
+
# Check for new structure (metadata files at collection level)
|
|
48
|
+
collection.metadata_files.each do |metadata_file|
|
|
49
|
+
node_id = metadata_file.basename.to_s.sub(/\.(yml|yaml|json|toml)$/, '')
|
|
50
|
+
result[:new_nodes] << {
|
|
51
|
+
collection: collection_name,
|
|
52
|
+
node_id: node_id,
|
|
53
|
+
metadata_file: metadata_file
|
|
54
|
+
}
|
|
55
|
+
result[:has_new_structure] = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check for old structure (ALL subdirectories, with or without attributes)
|
|
59
|
+
# We need to migrate ALL nodes, even those without attributes.yml
|
|
60
|
+
collection.subdirectories.each do |subdir|
|
|
61
|
+
node_id = subdir.basename.to_s
|
|
62
|
+
|
|
63
|
+
# Skip if this node already has new-structure metadata
|
|
64
|
+
already_migrated = result[:new_nodes].any? { |n|
|
|
65
|
+
n[:collection] == collection_name && n[:node_id] == node_id
|
|
66
|
+
}
|
|
67
|
+
next if already_migrated
|
|
68
|
+
|
|
69
|
+
# Check if there's an attributes file (any format)
|
|
70
|
+
attributes_file = nil
|
|
71
|
+
has_attributes = false
|
|
72
|
+
%w[yml yaml json toml].each do |ext|
|
|
73
|
+
candidate = subdir.join("attributes.#{ext}")
|
|
74
|
+
if candidate.exist?
|
|
75
|
+
attributes_file = candidate
|
|
76
|
+
has_attributes = true
|
|
77
|
+
break
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
result[:old_nodes] << {
|
|
82
|
+
collection: collection_name,
|
|
83
|
+
node_id: node_id,
|
|
84
|
+
old_path: subdir,
|
|
85
|
+
attributes_file: attributes_file,
|
|
86
|
+
has_attributes: has_attributes
|
|
87
|
+
}
|
|
88
|
+
result[:has_old_structure] = true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
result
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Preview migration without making changes
|
|
96
|
+
def preview
|
|
97
|
+
validation = validate
|
|
98
|
+
plan = []
|
|
99
|
+
|
|
100
|
+
validation[:old_nodes].each do |old_node|
|
|
101
|
+
collection_name = old_node[:collection]
|
|
102
|
+
node_id = old_node[:node_id]
|
|
103
|
+
old_path = old_node[:old_path]
|
|
104
|
+
has_attributes = old_node[:has_attributes]
|
|
105
|
+
attributes_file = old_node[:attributes_file]
|
|
106
|
+
|
|
107
|
+
collection_path = @root_path.join(collection_name)
|
|
108
|
+
new_metadata_file = collection_path.join("#{node_id}.yml")
|
|
109
|
+
new_asset_dir = collection_path.join(node_id)
|
|
110
|
+
|
|
111
|
+
actions = []
|
|
112
|
+
if has_attributes
|
|
113
|
+
actions << "Move #{attributes_file} → #{new_metadata_file}"
|
|
114
|
+
else
|
|
115
|
+
actions << "Create empty #{new_metadata_file} (node has no attributes)"
|
|
116
|
+
end
|
|
117
|
+
actions << "Assets remain in #{old_path}/assets/ (no change needed)"
|
|
118
|
+
|
|
119
|
+
plan << {
|
|
120
|
+
node_id: node_id,
|
|
121
|
+
collection: collection_name,
|
|
122
|
+
old_path: old_path,
|
|
123
|
+
new_metadata_file: new_metadata_file,
|
|
124
|
+
new_asset_dir: new_asset_dir,
|
|
125
|
+
has_attributes: has_attributes,
|
|
126
|
+
actions: actions
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
plan
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Migrate a single node
|
|
134
|
+
def migrate_node(collection_name, node_id)
|
|
135
|
+
log "Migrating #{collection_name}/#{node_id}..."
|
|
136
|
+
|
|
137
|
+
collection_path = @root_path.join(collection_name)
|
|
138
|
+
old_node_path = collection_path.join(node_id)
|
|
139
|
+
|
|
140
|
+
unless old_node_path.directory?
|
|
141
|
+
return { success: false, error: "Node directory not found: #{old_node_path}" }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
new_metadata_file = collection_path.join("#{node_id}.yml")
|
|
145
|
+
|
|
146
|
+
# Look for attributes file in any format
|
|
147
|
+
old_attributes_file = nil
|
|
148
|
+
%w[yml yaml json toml].each do |ext|
|
|
149
|
+
candidate = old_node_path.join("attributes.#{ext}")
|
|
150
|
+
if candidate.exist?
|
|
151
|
+
old_attributes_file = candidate
|
|
152
|
+
break
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
unless dry_run?
|
|
157
|
+
if old_attributes_file
|
|
158
|
+
# Move existing attributes file to collection level
|
|
159
|
+
log " Moving #{old_attributes_file} → #{new_metadata_file}"
|
|
160
|
+
FileUtils.mv(old_attributes_file.to_s, new_metadata_file.to_s)
|
|
161
|
+
else
|
|
162
|
+
# Create empty metadata file for nodes without attributes
|
|
163
|
+
log " Creating empty metadata #{new_metadata_file} (node had no attributes)"
|
|
164
|
+
File.write(new_metadata_file.to_s, {}.to_yaml)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Assets stay in assets/ subdirectory - no moving needed
|
|
169
|
+
# Old: collection/identifier/assets/foo.png
|
|
170
|
+
# New: collection/identifier/assets/foo.png (same location)
|
|
171
|
+
|
|
172
|
+
{ success: true, node_id: node_id, had_attributes: !old_attributes_file.nil? }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Migrate an entire collection
|
|
176
|
+
def migrate_collection(collection_name)
|
|
177
|
+
log "Migrating collection: #{collection_name}"
|
|
178
|
+
|
|
179
|
+
validation = validate
|
|
180
|
+
collection_nodes = validation[:old_nodes].select { |n| n[:collection] == collection_name }
|
|
181
|
+
|
|
182
|
+
migrated_count = 0
|
|
183
|
+
errors = []
|
|
184
|
+
|
|
185
|
+
collection_nodes.each do |old_node|
|
|
186
|
+
result = migrate_node(collection_name, old_node[:node_id])
|
|
187
|
+
if result[:success]
|
|
188
|
+
migrated_count += 1
|
|
189
|
+
else
|
|
190
|
+
errors << result[:error]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
{
|
|
195
|
+
success: errors.empty?,
|
|
196
|
+
migrated_count: migrated_count,
|
|
197
|
+
errors: errors
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Migrate entire root
|
|
202
|
+
def migrate
|
|
203
|
+
log "Starting full migration of #{@root_path}"
|
|
204
|
+
|
|
205
|
+
if backup?
|
|
206
|
+
backup_path = backup
|
|
207
|
+
log "Created backup at #{backup_path}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
validation = validate
|
|
211
|
+
|
|
212
|
+
if validation[:old_nodes].empty?
|
|
213
|
+
log "No old structure nodes found to migrate"
|
|
214
|
+
return { success: true, nodes_migrated: 0, collections_migrated: 0 }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
collections = validation[:old_nodes].map { |n| n[:collection] }.uniq
|
|
218
|
+
total_migrated = 0
|
|
219
|
+
collections_migrated = 0
|
|
220
|
+
|
|
221
|
+
collections.each do |collection_name|
|
|
222
|
+
result = migrate_collection(collection_name)
|
|
223
|
+
if result[:success]
|
|
224
|
+
collections_migrated += 1
|
|
225
|
+
total_migrated += result[:migrated_count]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
log "Migration complete! Migrated #{total_migrated} nodes across #{collections_migrated} collections"
|
|
230
|
+
|
|
231
|
+
{
|
|
232
|
+
success: true,
|
|
233
|
+
nodes_migrated: total_migrated,
|
|
234
|
+
collections_migrated: collections_migrated
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Create backup
|
|
239
|
+
def backup
|
|
240
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
|
241
|
+
backup_name = "#{@root_path.basename}.backup.#{timestamp}"
|
|
242
|
+
backup_path = @root_path.parent.join(backup_name)
|
|
243
|
+
|
|
244
|
+
log "Creating backup: #{backup_path}"
|
|
245
|
+
|
|
246
|
+
unless dry_run?
|
|
247
|
+
FileUtils.cp_r(@root_path.to_s, backup_path.to_s)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
backup_path
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Rollback from backup
|
|
254
|
+
def rollback
|
|
255
|
+
# Find most recent backup
|
|
256
|
+
backup_pattern = "#{@root_path.basename}.backup.*"
|
|
257
|
+
backups = @root_path.parent.glob(backup_pattern).sort.reverse
|
|
258
|
+
|
|
259
|
+
if backups.empty?
|
|
260
|
+
return { success: false, error: "No backups found" }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
backup_path = backups.first
|
|
264
|
+
log "Rolling back from #{backup_path}"
|
|
265
|
+
|
|
266
|
+
unless dry_run?
|
|
267
|
+
# Remove current root
|
|
268
|
+
FileUtils.rm_rf(@root_path.to_s)
|
|
269
|
+
# Restore from backup
|
|
270
|
+
FileUtils.cp_r(backup_path.to_s, @root_path.to_s)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
{
|
|
274
|
+
success: true,
|
|
275
|
+
restored_from: backup_path
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
|
|
281
|
+
def log(message)
|
|
282
|
+
puts message if verbose? || dry_run?
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
data/lib/ro/node.rb
CHANGED
|
@@ -2,16 +2,45 @@ module Ro
|
|
|
2
2
|
class Node
|
|
3
3
|
include Klass
|
|
4
4
|
|
|
5
|
-
attr_reader :path, :root
|
|
5
|
+
attr_reader :path, :root, :metadata_file
|
|
6
|
+
|
|
7
|
+
# T023: Updated to accept (collection, metadata_file) for new structure
|
|
8
|
+
def initialize(collection_or_path, metadata_file = nil)
|
|
9
|
+
if metadata_file
|
|
10
|
+
# New structure: collection + metadata_file
|
|
11
|
+
@collection = collection_or_path
|
|
12
|
+
@metadata_file = Path.for(metadata_file)
|
|
13
|
+
|
|
14
|
+
# Raise error if metadata file doesn't exist
|
|
15
|
+
unless @metadata_file.exist?
|
|
16
|
+
raise Errno::ENOENT, "No such file or directory - #{@metadata_file}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@root = @collection.root
|
|
20
|
+
|
|
21
|
+
# Derive node ID from metadata filename (without extension)
|
|
22
|
+
# T025: ID derived from metadata filename
|
|
23
|
+
node_id = @metadata_file.basename.to_s.sub(/\.(yml|yaml|json|toml)$/, '')
|
|
24
|
+
|
|
25
|
+
# Path is the node directory (sibling to metadata file)
|
|
26
|
+
@path = @collection.path.join(node_id)
|
|
27
|
+
else
|
|
28
|
+
# Old structure compatibility: just a path
|
|
29
|
+
@path = Path.for(collection_or_path)
|
|
30
|
+
@root = Root.for(@path.parent.parent)
|
|
31
|
+
@metadata_file = nil
|
|
32
|
+
end
|
|
6
33
|
|
|
7
|
-
def initialize(path)
|
|
8
|
-
@path = Path.for(path)
|
|
9
|
-
@root = Root.for(@path.parent.parent)
|
|
10
34
|
@attributes = :lazyload
|
|
11
35
|
end
|
|
12
36
|
|
|
13
37
|
def name
|
|
14
|
-
@
|
|
38
|
+
if @metadata_file
|
|
39
|
+
# T025: For new structure, name comes from metadata filename
|
|
40
|
+
@metadata_file.basename.to_s.sub(/\.(yml|yaml|json|toml)$/, '')
|
|
41
|
+
else
|
|
42
|
+
@path.name
|
|
43
|
+
end
|
|
15
44
|
end
|
|
16
45
|
|
|
17
46
|
def id
|
|
@@ -31,7 +60,7 @@ module Ro
|
|
|
31
60
|
end
|
|
32
61
|
|
|
33
62
|
def collection
|
|
34
|
-
@root.collection_for(type)
|
|
63
|
+
@collection || @root.collection_for(type)
|
|
35
64
|
end
|
|
36
65
|
|
|
37
66
|
def attributes
|
|
@@ -47,40 +76,42 @@ module Ro
|
|
|
47
76
|
@attributes = Map.new
|
|
48
77
|
|
|
49
78
|
_load_base_attributes
|
|
79
|
+
_load_file_attributes
|
|
50
80
|
_load_asset_attributes
|
|
51
81
|
_load_meta_attributes
|
|
52
|
-
_load_file_attributes
|
|
53
82
|
|
|
54
83
|
@attributes
|
|
55
84
|
end
|
|
56
85
|
|
|
86
|
+
# T026: Modified to load from external metadata_file (new structure)
|
|
57
87
|
def _load_base_attributes
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"attributes.{yml,yaml,json}"
|
|
66
|
-
|
|
67
|
-
@path.glob(glob) do |file|
|
|
68
|
-
attrs = _render(file)
|
|
88
|
+
if @metadata_file && @metadata_file.exist?
|
|
89
|
+
# New structure: load from explicit metadata file
|
|
90
|
+
attrs = _render(@metadata_file)
|
|
91
|
+
update_attributes!(attrs, file: @metadata_file)
|
|
92
|
+
else
|
|
93
|
+
# Old structure: search for attributes.yml in node directory
|
|
94
|
+
glob = "attributes.{yml,yaml,json}"
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
@path.glob(glob) do |file|
|
|
97
|
+
attrs = _render(file)
|
|
98
|
+
update_attributes!(attrs, file:)
|
|
72
99
|
end
|
|
73
|
-
|
|
74
|
-
@attributes.update(attrs)
|
|
75
100
|
end
|
|
76
101
|
end
|
|
77
102
|
|
|
78
|
-
|
|
79
103
|
def _load_asset_attributes
|
|
80
104
|
{}.tap do |hash|
|
|
81
105
|
assets.each do |asset|
|
|
82
106
|
key = asset.name
|
|
83
|
-
|
|
107
|
+
url = asset.url
|
|
108
|
+
path = asset.path.relative_to(@root)
|
|
109
|
+
src = asset.src
|
|
110
|
+
img = asset.img
|
|
111
|
+
size = asset.size
|
|
112
|
+
|
|
113
|
+
value = { url:, path:, size:, img:, src: }
|
|
114
|
+
|
|
84
115
|
hash[key] = value
|
|
85
116
|
end
|
|
86
117
|
|
|
@@ -94,7 +125,9 @@ module Ro
|
|
|
94
125
|
identifier:,
|
|
95
126
|
type:,
|
|
96
127
|
id:,
|
|
97
|
-
urls
|
|
128
|
+
urls:,
|
|
129
|
+
created_at:,
|
|
130
|
+
updated_at:,
|
|
98
131
|
)
|
|
99
132
|
|
|
100
133
|
@attributes.set(_meta: hash)
|
|
@@ -114,17 +147,49 @@ module Ro
|
|
|
114
147
|
base = basename.split('.', 2).first
|
|
115
148
|
key.push(base)
|
|
116
149
|
|
|
117
|
-
|
|
118
|
-
|
|
150
|
+
value = _render(file)
|
|
151
|
+
|
|
152
|
+
if value.is_a?(HTML)
|
|
153
|
+
attrs = value.front_matter
|
|
154
|
+
update_attributes!(attrs, file:)
|
|
119
155
|
end
|
|
120
156
|
|
|
121
|
-
|
|
157
|
+
if @attributes.has?(key)
|
|
158
|
+
raise Error.new("path=#{ @path.inspect } masks #{ key.inspect } in #{ @attributes.inspect }!")
|
|
159
|
+
end
|
|
122
160
|
|
|
123
161
|
@attributes.set(key => value)
|
|
124
162
|
end
|
|
125
163
|
end
|
|
126
164
|
|
|
165
|
+
def update_attributes!(attrs = {}, **context)
|
|
166
|
+
attrs = Map.for(attrs)
|
|
167
|
+
|
|
168
|
+
blacklist = %w[
|
|
169
|
+
assets
|
|
170
|
+
_meta
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
blacklist.each do |key|
|
|
174
|
+
if attrs.has_key?(key)
|
|
175
|
+
Ro.error!("#{ key } is blacklisted!", **context)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
keys = @attributes.depth_first_keys
|
|
180
|
+
|
|
181
|
+
attrs.depth_first_keys.each do |key|
|
|
182
|
+
if keys.include?(key)
|
|
183
|
+
Ro.error!("#{ attrs.inspect } clobbers #{ @attributes.inspect }!", **context)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
@attributes.update(attrs)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# T028: Updated ignore patterns for new structure
|
|
127
191
|
def _ignored_files
|
|
192
|
+
# Both old and new structure: ignore attributes files and assets/ subdirectory
|
|
128
193
|
ignored_files =
|
|
129
194
|
%w[
|
|
130
195
|
attributes.yml
|
|
@@ -133,17 +198,23 @@ module Ro
|
|
|
133
198
|
./assets/**/**
|
|
134
199
|
].map do |glob|
|
|
135
200
|
@path.glob(glob).select(&:file?)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
ignored_files.flatten
|
|
201
|
+
end.flatten
|
|
139
202
|
end
|
|
140
203
|
|
|
141
204
|
def _render(file)
|
|
205
|
+
node = self
|
|
206
|
+
|
|
142
207
|
value = Ro.render(file, _render_context)
|
|
143
208
|
|
|
144
|
-
if value.is_a?(
|
|
145
|
-
|
|
146
|
-
|
|
209
|
+
if value.is_a?(HTML)
|
|
210
|
+
front_matter = value.front_matter
|
|
211
|
+
html = Ro.expand_asset_urls(value, node)
|
|
212
|
+
value = HTML.new(html, front_matter:)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if value.is_a?(Hash)
|
|
216
|
+
attributes = value
|
|
217
|
+
value = Ro.expand_asset_values(attributes, node)
|
|
147
218
|
end
|
|
148
219
|
|
|
149
220
|
value
|
|
@@ -156,6 +227,10 @@ module Ro
|
|
|
156
227
|
end
|
|
157
228
|
end
|
|
158
229
|
|
|
230
|
+
def fetch(*args)
|
|
231
|
+
attributes.fetch(*args)
|
|
232
|
+
end
|
|
233
|
+
|
|
159
234
|
def get(*args)
|
|
160
235
|
attributes.get(*args)
|
|
161
236
|
end
|
|
@@ -168,7 +243,10 @@ module Ro
|
|
|
168
243
|
path.relative_to(root)
|
|
169
244
|
end
|
|
170
245
|
|
|
246
|
+
# T027: Updated to return assets/ subdirectory in both old and new structure
|
|
171
247
|
def asset_dir
|
|
248
|
+
# Both old and new structure use assets/ subdirectory
|
|
249
|
+
# This prevents files from being rendered as templates
|
|
172
250
|
path.join('assets')
|
|
173
251
|
end
|
|
174
252
|
|
|
@@ -219,14 +297,13 @@ module Ro
|
|
|
219
297
|
end
|
|
220
298
|
|
|
221
299
|
def url_for(relative_path, options = {})
|
|
222
|
-
raise ArgumentError, relative_path if Path.absolute?(relative_path)
|
|
223
|
-
|
|
224
|
-
fullpath = Path.for(path, relative_path).expand
|
|
225
|
-
raise ArgumentError, "#{relative_path.inspect} -- DOES NOT EXIST" unless fullpath.exist?
|
|
226
|
-
|
|
227
300
|
Ro.url_for(self.relative_path, relative_path, options)
|
|
228
301
|
end
|
|
229
302
|
|
|
303
|
+
def path_for(...)
|
|
304
|
+
@path.join(...)
|
|
305
|
+
end
|
|
306
|
+
|
|
230
307
|
def src_for(*args)
|
|
231
308
|
key = Path.relative(:assets, :src, args).split('/')
|
|
232
309
|
get(key)
|
|
@@ -250,6 +327,10 @@ module Ro
|
|
|
250
327
|
to_json(...)
|
|
251
328
|
end
|
|
252
329
|
|
|
330
|
+
def to_str(...)
|
|
331
|
+
to_json(...)
|
|
332
|
+
end
|
|
333
|
+
|
|
253
334
|
def to_json(...)
|
|
254
335
|
JSON.pretty_generate(to_hash, ...)
|
|
255
336
|
end
|
|
@@ -262,12 +343,6 @@ module Ro
|
|
|
262
343
|
to_hash.to_yaml(...)
|
|
263
344
|
end
|
|
264
345
|
|
|
265
|
-
def _mapify(data)
|
|
266
|
-
converted = 'this_recursively_converts_nested_hashes_into_maps'
|
|
267
|
-
|
|
268
|
-
Map.for(converted => data)[converted]
|
|
269
|
-
end
|
|
270
|
-
|
|
271
346
|
def files
|
|
272
347
|
path.glob('**/**').select { |entry| entry.file? }.sort
|
|
273
348
|
end
|
|
@@ -291,5 +366,13 @@ module Ro
|
|
|
291
366
|
|
|
292
367
|
[position, published_at, created_at, name]
|
|
293
368
|
end
|
|
369
|
+
|
|
370
|
+
def created_at
|
|
371
|
+
files.map{|file| File.stat(file).ctime}.min
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def updated_at
|
|
375
|
+
files.map{|file| File.stat(file).mtime}.max
|
|
376
|
+
end
|
|
294
377
|
end
|
|
295
378
|
end
|
data/lib/ro/path.rb
CHANGED
data/lib/ro/root.rb
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
module Ro
|
|
2
2
|
class Root < Path
|
|
3
|
+
@@warned_paths = {}
|
|
4
|
+
|
|
3
5
|
def identifier
|
|
4
6
|
self
|
|
5
7
|
end
|
|
6
8
|
|
|
9
|
+
def initialize(*)
|
|
10
|
+
super
|
|
11
|
+
check_for_unmigrated_structure!
|
|
12
|
+
end
|
|
13
|
+
|
|
7
14
|
def collections(&block)
|
|
8
|
-
accum = Collection::List.for(self)
|
|
15
|
+
accum = Collection::List.for(self)
|
|
9
16
|
|
|
10
17
|
subdirectories do |subdirectory|
|
|
11
18
|
collection = collection_for(subdirectory)
|
|
@@ -69,5 +76,72 @@ module Ro
|
|
|
69
76
|
def method_missing(name, *args, **kws, &block)
|
|
70
77
|
get(name) || super
|
|
71
78
|
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def check_for_unmigrated_structure!
|
|
83
|
+
# Use absolute path for deduplication
|
|
84
|
+
begin
|
|
85
|
+
path_key = File.expand_path(self.to_s)
|
|
86
|
+
rescue
|
|
87
|
+
path_key = self.to_s
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Skip if we've already warned for this path
|
|
91
|
+
return if @@warned_paths[path_key]
|
|
92
|
+
|
|
93
|
+
# Mark as checked (even if no warning needed)
|
|
94
|
+
@@warned_paths[path_key] = true
|
|
95
|
+
|
|
96
|
+
# Quick check: look for old structure (subdirs with attributes.yml)
|
|
97
|
+
# but no new structure (metadata files at collection level)
|
|
98
|
+
has_old = false
|
|
99
|
+
has_new = false
|
|
100
|
+
|
|
101
|
+
subdirectories.each do |subdir|
|
|
102
|
+
# Check for new structure (metadata files)
|
|
103
|
+
has_new = true if subdir.glob('*.{yml,yaml,json,toml}').any? { |f| f.file? }
|
|
104
|
+
|
|
105
|
+
# Check for old structure (nested attributes.yml)
|
|
106
|
+
subdir.subdirectories.each do |node_dir|
|
|
107
|
+
if (node_dir.join('attributes.yml').exist? ||
|
|
108
|
+
node_dir.join('attributes.yaml').exist? ||
|
|
109
|
+
node_dir.join('attributes.json').exist?)
|
|
110
|
+
has_old = true
|
|
111
|
+
break
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
break if has_old && has_new
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if has_old && !has_new
|
|
119
|
+
warn_unmigrated_structure!
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def warn_unmigrated_structure!
|
|
124
|
+
$stderr.puts ""
|
|
125
|
+
$stderr.puts "=" * 70
|
|
126
|
+
$stderr.puts "⚠️ WARNING: Old Ro asset structure detected!"
|
|
127
|
+
$stderr.puts "=" * 70
|
|
128
|
+
$stderr.puts ""
|
|
129
|
+
$stderr.puts "This Ro root contains assets in the OLD structure format:"
|
|
130
|
+
$stderr.puts " • identifier/attributes.yml"
|
|
131
|
+
$stderr.puts " • identifier/assets/"
|
|
132
|
+
$stderr.puts ""
|
|
133
|
+
$stderr.puts "Ro v5.0 uses a simplified NEW structure:"
|
|
134
|
+
$stderr.puts " • identifier.yml"
|
|
135
|
+
$stderr.puts " • identifier/"
|
|
136
|
+
$stderr.puts ""
|
|
137
|
+
$stderr.puts "Collections will NOT automatically discover old-structure nodes."
|
|
138
|
+
$stderr.puts ""
|
|
139
|
+
$stderr.puts "To migrate your data, run:"
|
|
140
|
+
$stderr.puts " #{$0.include?('bin/') ? './bin/ro' : 'ro'} migrate #{self}"
|
|
141
|
+
$stderr.puts ""
|
|
142
|
+
$stderr.puts "Or see MIGRATION.md for details."
|
|
143
|
+
$stderr.puts "=" * 70
|
|
144
|
+
$stderr.puts ""
|
|
145
|
+
end
|
|
72
146
|
end
|
|
73
147
|
end
|