parapet-rails 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +48 -0
- data/Rakefile +7 -0
- data/app/assets/config/parapet/grammar-registry.json +52 -0
- data/app/assets/images/parapet/icons/activities/component-inspector.svg +4 -0
- data/app/assets/images/parapet/icons/activities/coverage-heatmap.svg +3 -0
- data/app/assets/images/parapet/icons/activities/discovery-sidebar.svg +3 -0
- data/app/assets/images/parapet/icons/activities/environment-toggles.svg +4 -0
- data/app/assets/images/parapet/icons/activities/failure-inspector.svg +5 -0
- data/app/assets/images/parapet/icons/activities/flaky-history.svg +3 -0
- data/app/assets/images/parapet/icons/activities/flight-recorder.svg +5 -0
- data/app/assets/images/parapet/icons/activities/health-graph.svg +3 -0
- data/app/assets/images/parapet/icons/activities/injection-engine.svg +3 -0
- data/app/assets/images/parapet/icons/activities/live-sql-stream.svg +5 -0
- data/app/assets/images/parapet/icons/activities/live-web-preview.svg +4 -0
- data/app/assets/images/parapet/icons/activities/monaco-editor.svg +5 -0
- data/app/assets/images/parapet/icons/activities/progress-area.svg +3 -0
- data/app/assets/images/parapet/icons/activities/registry-metadata.svg +5 -0
- data/app/assets/images/parapet/icons/activities/seed-inspector.svg +4 -0
- data/app/assets/images/parapet/icons/activities/slow-test-bottlenecks.svg +4 -0
- data/app/assets/images/parapet/icons/activities/smart-search.svg +4 -0
- data/app/assets/images/parapet/icons/activities/snippet-library.svg +4 -0
- data/app/assets/images/parapet/icons/activities/terminal.svg +4 -0
- data/app/assets/images/parapet/icons/activities/unified-schema-view.svg +4 -0
- data/app/assets/images/parapet/icons/activities/visual-comparison.svg +3 -0
- data/app/assets/images/parapet/icons/activities/visual-summary-bar.svg +5 -0
- data/app/assets/images/parapet/icons/nav/ideas.svg +4 -0
- data/app/assets/images/parapet/icons/nav/languages.svg +8 -0
- data/app/assets/images/parapet/icons/nav/profile.svg +4 -0
- data/app/assets/images/parapet/icons/nav/pulse.svg +7 -0
- data/app/assets/images/parapet/icons/nav/settings.svg +4 -0
- data/app/assets/images/parapet/icons/nav/state.svg +5 -0
- data/app/assets/images/parapet/icons/nav/styles.svg +7 -0
- data/app/assets/images/parapet/icons/nav/tests.svg +4 -0
- data/app/assets/images/parapet/icons/nav/traffic.svg +12 -0
- data/app/assets/javascripts/parapet/activity-card.js +439 -0
- data/app/assets/javascripts/parapet/activity-cards.js +291 -0
- data/app/assets/javascripts/parapet/activity-registry.js +181 -0
- data/app/assets/javascripts/parapet/activity-toggles.js +76 -0
- data/app/assets/javascripts/parapet/application.js +37 -0
- data/app/assets/javascripts/parapet/card-failure-inspector.js +134 -0
- data/app/assets/javascripts/parapet/card-file-editor.js +1722 -0
- data/app/assets/javascripts/parapet/card-progress-area.js +297 -0
- data/app/assets/javascripts/parapet/card-smart-search.js +114 -0
- data/app/assets/javascripts/parapet/card-snippet-library.js +251 -0
- data/app/assets/javascripts/parapet/explorer-resizer.js +92 -0
- data/app/assets/javascripts/parapet/parapet_qss.js +342 -0
- data/app/assets/javascripts/parapet/proficiency-filter.js +53 -0
- data/app/assets/javascripts/parapet/sidebar-resizer.js +197 -0
- data/app/assets/javascripts/parapet/styles-workbench.js +819 -0
- data/app/assets/javascripts/parapet/tree-logic.js +320 -0
- data/app/assets/stylesheets/parapet/_qss_variables.scss +122 -0
- data/app/assets/stylesheets/parapet/parapet_qss.scss +69 -0
- data/app/assets/stylesheets/parapet/qss_application.css +260940 -0
- data/app/assets/stylesheets/parapet/qss_application.css.map +1 -0
- data/app/assets/stylesheets/parapet/qss_application.scss +5 -0
- data/app/assets/stylesheets/parapet/qss_workspace.scss +209 -0
- data/app/assets/stylesheets/parapet/sample.qss +22 -0
- data/app/controllers/parapet/application_controller.rb +54 -0
- data/app/controllers/parapet/base_controller.rb +18 -0
- data/app/controllers/parapet/build_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/css_suite/compiler_controller.rb +95 -0
- data/app/controllers/parapet/css_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/css_suite/preview_controller.rb +46 -0
- data/app/controllers/parapet/dashboard_controller.rb +8 -0
- data/app/controllers/parapet/drafts_controller.rb +30 -0
- data/app/controllers/parapet/explorers_controller.rb +53 -0
- data/app/controllers/parapet/files_controller.rb +208 -0
- data/app/controllers/parapet/footers_controller.rb +24 -0
- data/app/controllers/parapet/intentions_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/landing_controller.rb +14 -0
- data/app/controllers/parapet/monitor_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/pm_suite/publish_controller.rb +16 -0
- data/app/controllers/parapet/polyglot_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/runner_controller.rb +34 -0
- data/app/controllers/parapet/sessions_controller.rb +35 -0
- data/app/controllers/parapet/signal_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/test_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/widgets_controller.rb +33 -0
- data/app/helpers/parapet/application_helper.rb +23 -0
- data/app/helpers/parapet/icon_helper.rb +57 -0
- data/app/jobs/parapet/application_job.rb +4 -0
- data/app/mailers/parapet/application_mailer.rb +6 -0
- data/app/models/parapet/activity.rb +14 -0
- data/app/models/parapet/application_record.rb +5 -0
- data/app/models/parapet/current.rb +6 -0
- data/app/models/parapet/draft_recovery.rb +9 -0
- data/app/models/parapet/internal_record.rb +34 -0
- data/app/models/parapet/run_history.rb +22 -0
- data/app/models/parapet/runtime_setting.rb +8 -0
- data/app/models/parapet/session.rb +7 -0
- data/app/models/parapet/suite_assignment.rb +30 -0
- data/app/models/parapet/user.rb +12 -0
- data/app/services/parapet/activity_phrase_renderer.rb +35 -0
- data/app/services/parapet/activity_writer.rb +26 -0
- data/app/services/parapet/authorization.rb +198 -0
- data/app/services/parapet/consistency_auditor.rb +58 -0
- data/app/services/parapet/directory_scanner.rb +159 -0
- data/app/services/parapet/draft_recovery_store.rb +18 -0
- data/app/services/parapet/explorer_manager.rb +402 -0
- data/app/services/parapet/file_save_service.rb +21 -0
- data/app/services/parapet/git_side_ref_committer.rb +50 -0
- data/app/services/parapet/noun_discovery.rb +26 -0
- data/app/services/parapet/publish_gate.rb +83 -0
- data/app/services/parapet/test_runner.rb +178 -0
- data/app/views/layouts/parapet/application.html.erb +83 -0
- data/app/views/layouts/parapet/landing.html.erb +19 -0
- data/app/views/parapet/activities/_ai_error_explainer.html.erb +33 -0
- data/app/views/parapet/activities/_ai_explainer_widget.html.erb +20 -0
- data/app/views/parapet/activities/_calendar_widget.html.erb +31 -0
- data/app/views/parapet/activities/_code_snippets_widget.html.erb +22 -0
- data/app/views/parapet/activities/_coverage_explorer.html.erb +1 -0
- data/app/views/parapet/activities/_dropdown_test.html.erb +20 -0
- data/app/views/parapet/activities/_explorer_node.html.erb +102 -0
- data/app/views/parapet/activities/_explorer_tree.html.erb +16 -0
- data/app/views/parapet/activities/_file_editor.html.erb +183 -0
- data/app/views/parapet/activities/_file_explorer.html.erb +23 -0
- data/app/views/parapet/activities/_fix_suggestions_widget.html.erb +13 -0
- data/app/views/parapet/activities/_generic.html.erb +6 -0
- data/app/views/parapet/activities/_live_preview.html.erb +13 -0
- data/app/views/parapet/activities/_perspective_filters.html.erb +36 -0
- data/app/views/parapet/activities/_progress_area.html.erb +37 -0
- data/app/views/parapet/activities/_project_explorer.html.erb +1 -0
- data/app/views/parapet/activities/_registry_explorer.html.erb +1 -0
- data/app/views/parapet/activities/_run_history.html.erb +2 -0
- data/app/views/parapet/activities/_smart_search.html.erb +11 -0
- data/app/views/parapet/activities/file_explorer/_perspective_scripts.html.erb +58 -0
- data/app/views/parapet/activities/file_explorer/_perspectives.html.erb +0 -0
- data/app/views/parapet/activities/file_explorer/_viewport_scripts.html.erb +33 -0
- data/app/views/parapet/css_suite/_workbench.html.erb +51 -0
- data/app/views/parapet/dashboards/index.html.erb +113 -0
- data/app/views/parapet/debug_panel.html.erb +79 -0
- data/app/views/parapet/debug_panel_tailwind.html.erb +98 -0
- data/app/views/parapet/erb_patch_test.html.erb +5 -0
- data/app/views/parapet/explorers/show.html.erb +1 -0
- data/app/views/parapet/git_viewer.html.erb +62 -0
- data/app/views/parapet/git_viewer_after.html.erb +14 -0
- data/app/views/parapet/git_viewer_before.html.erb +14 -0
- data/app/views/parapet/git_viewer_governed.html.erb +88 -0
- data/app/views/parapet/git_viewer_governed_after.html.erb +36 -0
- data/app/views/parapet/git_viewer_governed_before.html.erb +36 -0
- data/app/views/parapet/git_viewer_tailwind.html.erb +88 -0
- data/app/views/parapet/git_viewer_thin_wrapper.html.erb +85 -0
- data/app/views/parapet/hierarchical_layout_test.html.erb +85 -0
- data/app/views/parapet/hybrid_stress_test.html.erb +25 -0
- data/app/views/parapet/index.html.erb +35 -0
- data/app/views/parapet/mutation_stress_test.html.erb +82 -0
- data/app/views/parapet/recursive_anchoring_test.html.erb +66 -0
- data/app/views/parapet/sessions/new.html.erb +28 -0
- data/app/views/parapet/shared/_cad_inspector.html.erb +92 -0
- data/app/views/parapet/shared/_discovery_sidebar.html.erb +48 -0
- data/app/views/parapet/shared/_explorer_bar.html.erb +71 -0
- data/app/views/parapet/shared/_footer.html.erb +10 -0
- data/app/views/parapet/shared/_footer_ideas.html.erb +36 -0
- data/app/views/parapet/shared/_footer_languages.html.erb +33 -0
- data/app/views/parapet/shared/_footer_pulse.html.erb +33 -0
- data/app/views/parapet/shared/_footer_state.html.erb +33 -0
- data/app/views/parapet/shared/_footer_styles.html.erb +37 -0
- data/app/views/parapet/shared/_footer_tests.html.erb +39 -0
- data/app/views/parapet/shared/_footer_traffic.html.erb +45 -0
- data/app/views/parapet/shared/_global_nav.html.erb +55 -0
- data/app/views/parapet/shared/_head.html.erb +4 -0
- data/app/views/parapet/shared/_header.html.erb +20 -0
- data/app/views/parapet/shared/_object_inspector.html.erb +76 -0
- data/app/views/parapet/shared/_ribbon_header.html.erb +3 -0
- data/app/views/parapet/shared/_root_selector_modal.html.erb +15 -0
- data/app/views/parapet/shared/_tab_bar.html.erb +62 -0
- data/app/views/parapet/shared/activities_ribbon/_agent_group.html.erb +15 -0
- data/app/views/parapet/shared/activities_ribbon/_analysis_group.html.erb +15 -0
- data/app/views/parapet/shared/activities_ribbon/_architect_group.html.erb +21 -0
- data/app/views/parapet/shared/activities_ribbon/_explorer_group.html.erb +12 -0
- data/app/views/parapet/shared/activities_ribbon/_inspector_group.html.erb +15 -0
- data/app/views/parapet/shared/activities_ribbon/_proficiency_filter.html.erb +24 -0
- data/app/views/parapet/shared/activities_ribbon/_runner_group.html.erb +21 -0
- data/app/views/parapet/shared/activities_ribbon/_spatial_inspector_group.html.erb +18 -0
- data/app/views/parapet/shared/activities_ribbon/_studio_group.html.erb +18 -0
- data/app/views/parapet/shared/activities_ribbon/_stylist_group.html.erb +18 -0
- data/app/views/parapet/shared/explorers/_base.html.erb +107 -0
- data/app/views/parapet/shared/explorers/_css.html.erb +1 -0
- data/app/views/parapet/shared/explorers/_intentions.html.erb +1 -0
- data/app/views/parapet/shared/explorers/_project.html.erb +1 -0
- data/app/views/parapet/shared/explorers/_tests.html.erb +1 -0
- data/app/views/parapet/shared/head/_meta.html.erb +3 -0
- data/app/views/parapet/shared/head/_styles_activities.html.erb +152 -0
- data/app/views/parapet/shared/head/_styles_shared.html.erb +52 -0
- data/app/views/parapet/shared/head/_styles_workspace.html.erb +67 -0
- data/app/views/parapet/shared/ribbon_header/_diagnostic_controls.html.erb +17 -0
- data/app/views/parapet/shared/ribbon_header/_execution_controls.html.erb +28 -0
- data/app/views/parapet/shared/ribbon_header/_focus_search.html.erb +5 -0
- data/app/views/parapet/shared/root_selector/_header.html.erb +10 -0
- data/app/views/parapet/shared/root_selector/_path_input.html.erb +13 -0
- data/app/views/parapet/shared/root_selector/_quick_select.html.erb +20 -0
- data/app/views/parapet/shared/root_selector/_scripts.html.erb +61 -0
- data/app/views/parapet/shared/root_selector/_warning.html.erb +11 -0
- data/app/views/parapet/shared/widgets/_base.html.erb +6 -0
- data/app/views/parapet/shared/widgets/_css.html.erb +79 -0
- data/app/views/parapet/widgets/_explainer.html.erb +17 -0
- data/app/views/parapet/widgets/_hotkeys.html.erb +20 -0
- data/app/views/parapet/widgets/_search.html.erb +68 -0
- data/app/views/parapet/widgets/_spatial_inspector.html.erb +54 -0
- data/app/views/parapet/widgets/_widget-explainer.html.erb +17 -0
- data/app/views/parapet/widgets/_widget-hotkeys.html.erb +20 -0
- data/app/views/parapet/widgets/_widget-search.html.erb +68 -0
- data/app/views/parapet/widgets/_widget-snippets.html.erb +18 -0
- data/app/views/parapet/widgets/_widget-suggestions.html.erb +15 -0
- data/app/views/parapet/widgets/show.html.erb +3 -0
- data/app/views/shared/_footer.html.erb +0 -0
- data/config/parapet/qss_config.yml +117 -0
- data/config/parapet/qss_registry.yml +14 -0
- data/config/routes.rb +40 -0
- data/db/migrate/20260916035300_create_run_histories.rb +16 -0
- data/db/migrate/20261002000000_expand_run_histories_for_activity_tracking.rb +7 -0
- data/db/parapet_migrate/20260929184200_create_parapet_users.rb +12 -0
- data/db/parapet_migrate/20260929184300_create_parapet_sessions.rb +9 -0
- data/db/parapet_migrate/20260930000000_create_parapet_runtime_settings.rb +12 -0
- data/db/parapet_migrate/20260930000100_create_parapet_suite_assignments.rb +14 -0
- data/db/parapet_migrate/20261002000000_create_parapet_activities.rb +16 -0
- data/db/parapet_migrate/20261002000100_create_parapet_draft_recoveries.rb +13 -0
- data/db/parapet_schema.rb +64 -0
- data/db/schema.rb +21 -0
- data/db/schema.released.rb +0 -0
- data/lib/parapet/engine.rb +11 -0
- data/lib/parapet/environment_guard.rb +14 -0
- data/lib/parapet/version.rb +3 -0
- data/lib/parapet-rails.rb +1 -0
- data/lib/parapet.rb +59 -0
- data/lib/tasks/parapet_qss.rake +26 -0
- data/lib/tasks/parapet_tasks.rake +25 -0
- data/lib/tasks/randomize_explorer_health.rb +101 -0
- metadata +423 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class FilesController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
# Use Engine.root to find the mock file stored within the engine
|
|
5
|
+
mock_path = Parapet::Engine.root.join("storage", "seeds-project-explorer.json")
|
|
6
|
+
|
|
7
|
+
if File.exist?(mock_path)
|
|
8
|
+
render json: File.read(mock_path)
|
|
9
|
+
else
|
|
10
|
+
# Fallback to real filesystem scanning
|
|
11
|
+
root_path = Parapet::Engine.root
|
|
12
|
+
render json: build_tree(root_path)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show
|
|
17
|
+
raw_path = params[:path].to_s
|
|
18
|
+
|
|
19
|
+
# Security: Get absolute root path
|
|
20
|
+
root_path = File.expand_path(Parapet::Engine.root.to_s)
|
|
21
|
+
|
|
22
|
+
# On Windows, a path may arrive as an absolute path with a drive letter
|
|
23
|
+
if Gem.win_platform? && raw_path.match?(/^[A-Z]:/i)
|
|
24
|
+
full_path = File.expand_path(raw_path)
|
|
25
|
+
else
|
|
26
|
+
# Strip leading slash to ensure it's relative to root_path
|
|
27
|
+
relative_path = raw_path.sub(/^\//, '')
|
|
28
|
+
full_path = File.expand_path(File.join(root_path, relative_path))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Fallback: Deduplication logic for cases like spec/spec/file.rb
|
|
32
|
+
if !File.exist?(full_path)
|
|
33
|
+
# Try to remove duplicated first segment if it exists
|
|
34
|
+
relative_path = raw_path.sub(/^\//, '')
|
|
35
|
+
parts = relative_path.split('/')
|
|
36
|
+
if parts.size > 1 && parts[0] == parts[1]
|
|
37
|
+
alternate_relative = parts[1..-1].join('/')
|
|
38
|
+
alternate_path = File.expand_path(File.join(root_path, alternate_relative))
|
|
39
|
+
if File.exist?(alternate_path)
|
|
40
|
+
Rails.logger.warn "Parapet Explorer: Found file via de-duplication: #{alternate_path}"
|
|
41
|
+
full_path = alternate_path
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Final security and existence check
|
|
47
|
+
if full_path.start_with?(root_path)
|
|
48
|
+
# Timing mitigation (Windows/AV suspected; the cause was never demonstrated): retry once
|
|
49
|
+
# if the file is not found. The retry is logged when it actually finds the file, so we
|
|
50
|
+
# learn whether it is ever needed. If that line never appears in the log, remove it.
|
|
51
|
+
exists = File.exist?(full_path)
|
|
52
|
+
if !exists && !File.directory?(full_path)
|
|
53
|
+
sleep 0.05
|
|
54
|
+
exists = File.exist?(full_path)
|
|
55
|
+
Rails.logger.warn "Parapet Files: #{full_path} was not found on the first check but was found on the retry (timing mitigation was needed)" if exists
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if exists && !File.directory?(full_path)
|
|
59
|
+
content = File.read(full_path)
|
|
60
|
+
render json: { content: content, path: params[:path] }
|
|
61
|
+
else
|
|
62
|
+
render json: {
|
|
63
|
+
content: "# Error: Access Denied or File Not Found\n# Target: #{full_path}\n# Root: #{root_path}",
|
|
64
|
+
path: params[:path]
|
|
65
|
+
}, status: :not_found
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
render json: { error: "Access Denied" }, status: :forbidden
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def update
|
|
74
|
+
relative_path = params[:path].to_s.sub(/^\//, '')
|
|
75
|
+
root_path = File.expand_path(Parapet::Engine.root.to_s)
|
|
76
|
+
full_path = File.expand_path(File.join(root_path, relative_path))
|
|
77
|
+
|
|
78
|
+
# Security check: Ensure we are within root
|
|
79
|
+
unless full_path.start_with?(root_path)
|
|
80
|
+
render json: { error: "Access Denied" }, status: :forbidden
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
Parapet::FileSaveService.new(
|
|
86
|
+
user: current_user,
|
|
87
|
+
full_path: full_path,
|
|
88
|
+
relative_path: relative_path,
|
|
89
|
+
content: params[:content]
|
|
90
|
+
).save!
|
|
91
|
+
render json: {
|
|
92
|
+
status: "success",
|
|
93
|
+
path: params[:path],
|
|
94
|
+
flash: { notice: "File persisted successfully to #{params[:path]}" }
|
|
95
|
+
}
|
|
96
|
+
rescue => e
|
|
97
|
+
render json: {
|
|
98
|
+
error: e.message,
|
|
99
|
+
flash: { alert: "Failed to persist file: #{e.message}" }
|
|
100
|
+
}, status: :internal_server_error
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def move
|
|
105
|
+
old_relative = params[:old_path].to_s.sub(/^\//, '')
|
|
106
|
+
new_relative = params[:new_path].to_s.sub(/^\//, '')
|
|
107
|
+
root_path = File.expand_path(Parapet::Engine.root.to_s)
|
|
108
|
+
|
|
109
|
+
old_full = File.expand_path(File.join(root_path, old_relative))
|
|
110
|
+
new_full = File.expand_path(File.join(root_path, new_relative))
|
|
111
|
+
|
|
112
|
+
# Security check
|
|
113
|
+
unless old_full.start_with?(root_path) && new_full.start_with?(root_path)
|
|
114
|
+
render json: { error: "Access Denied" }, status: :forbidden
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if File.exist?(old_full)
|
|
119
|
+
begin
|
|
120
|
+
# Ensure target directory exists
|
|
121
|
+
FileUtils.mkdir_p(File.dirname(new_full))
|
|
122
|
+
File.rename(old_full, new_full)
|
|
123
|
+
render json: {
|
|
124
|
+
status: "success",
|
|
125
|
+
old_path: params[:old_path],
|
|
126
|
+
new_path: params[:new_path],
|
|
127
|
+
flash: { notice: "Renamed: #{File.basename(old_full)} -> #{File.basename(new_full)}" }
|
|
128
|
+
}
|
|
129
|
+
rescue => e
|
|
130
|
+
render json: { error: e.message }, status: :internal_server_error
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
render json: { error: "File not found" }, status: :not_found
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def assets
|
|
138
|
+
root_path = Parapet::Engine.root.to_s
|
|
139
|
+
image_extensions = %w[svg png jpg jpeg webp gif]
|
|
140
|
+
|
|
141
|
+
assets = []
|
|
142
|
+
begin
|
|
143
|
+
Dir.glob(File.join(root_path, "app/assets/images/**/*.{#{image_extensions.join(',')}}")).each do |path|
|
|
144
|
+
rel_path = path.sub(root_path, '').sub(/^[\\\/]/, '').gsub('\\', '/')
|
|
145
|
+
assets << {
|
|
146
|
+
name: File.basename(path),
|
|
147
|
+
path: rel_path,
|
|
148
|
+
url: (view_context.asset_path("parapet/#{rel_path.sub('app/assets/images/parapet/', '')}") rescue rel_path)
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
rescue => e
|
|
152
|
+
Rails.logger.error "Parapet Assets Error: #{e.message}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
render json: assets
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def build_tree(path)
|
|
161
|
+
root_path = Parapet::Engine.root.to_s
|
|
162
|
+
name = File.basename(path)
|
|
163
|
+
return nil if name.start_with?('.') && name != '.'
|
|
164
|
+
|
|
165
|
+
# Basic ignore list for performance
|
|
166
|
+
ignored = %w[node_modules vendor .git tmp log .bundle .zencoder]
|
|
167
|
+
return nil if ignored.include?(name)
|
|
168
|
+
|
|
169
|
+
# Editable assets for the Style Workbench
|
|
170
|
+
allowed_extensions = %w[erb scss css rb]
|
|
171
|
+
|
|
172
|
+
# Calculate relative path without leading slash
|
|
173
|
+
rel_path = path.to_s.sub(root_path, '').sub(/^[\\\/]/, '').gsub('\\', '/')
|
|
174
|
+
|
|
175
|
+
if File.directory?(path)
|
|
176
|
+
children = Dir.children(path).sort.map { |child| build_tree(File.join(path, child)) }.compact
|
|
177
|
+
|
|
178
|
+
# Only return directory if it has children (prune empty branches)
|
|
179
|
+
return nil if children.empty?
|
|
180
|
+
|
|
181
|
+
{
|
|
182
|
+
name: name == '.' ? 'root' : name,
|
|
183
|
+
type: 'directory',
|
|
184
|
+
path: rel_path.empty? ? '/' : rel_path,
|
|
185
|
+
children: children
|
|
186
|
+
}
|
|
187
|
+
else
|
|
188
|
+
ext = File.extname(path).delete('.')
|
|
189
|
+
|
|
190
|
+
# Strict filtering for relevant file types
|
|
191
|
+
# Note: .html.erb has ext "erb"
|
|
192
|
+
return nil unless allowed_extensions.include?(ext)
|
|
193
|
+
|
|
194
|
+
# If it's .rb, ensure it's in a relevant directory (optional, but safer)
|
|
195
|
+
# For now, we'll keep all .rb (presenters/models) as they might contain logic
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
name: name,
|
|
199
|
+
type: 'file',
|
|
200
|
+
path: rel_path,
|
|
201
|
+
ext: ext
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
rescue Errno::EACCES
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class FootersController < ApplicationController
|
|
3
|
+
SUITE_MAP = {
|
|
4
|
+
"tests" => "parapet/shared/footer_tests",
|
|
5
|
+
"styles" => "parapet/shared/footer_styles",
|
|
6
|
+
"state" => "parapet/shared/footer_state",
|
|
7
|
+
"pulse" => "parapet/shared/footer_pulse",
|
|
8
|
+
"traffic" => "parapet/shared/footer_traffic",
|
|
9
|
+
"languages" => "parapet/shared/footer_languages",
|
|
10
|
+
"ideas" => "parapet/shared/footer_ideas"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
suite = params[:suite]
|
|
15
|
+
partial = SUITE_MAP[suite]
|
|
16
|
+
|
|
17
|
+
if partial
|
|
18
|
+
render partial: partial
|
|
19
|
+
else
|
|
20
|
+
render plain: "Footer not found", status: :not_found
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
module PmSuite
|
|
3
|
+
class PublishController < ApplicationController
|
|
4
|
+
def create
|
|
5
|
+
result = Parapet::PublishGate.new(user: current_user).run
|
|
6
|
+
|
|
7
|
+
render json: {
|
|
8
|
+
status: result.run_history.status,
|
|
9
|
+
exit_code: result.run_history.exit_code,
|
|
10
|
+
raw_output: result.run_history.raw_output,
|
|
11
|
+
run_history_id: result.run_history.id
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
module Parapet
|
|
4
|
+
class RunnerController < ApplicationController
|
|
5
|
+
def detect_frameworks
|
|
6
|
+
runner = Parapet::TestRunner.new
|
|
7
|
+
frameworks = runner.detect_frameworks
|
|
8
|
+
render json: { frameworks: frameworks }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute
|
|
12
|
+
runner = Parapet::TestRunner.new(user: current_user)
|
|
13
|
+
result = runner.execute(
|
|
14
|
+
command_type: params[:command],
|
|
15
|
+
path: params[:path]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
render json: {
|
|
19
|
+
status: "success",
|
|
20
|
+
command: result[:command],
|
|
21
|
+
output: result[:output],
|
|
22
|
+
summary: result[:summary],
|
|
23
|
+
exit_code: result[:exit_code]
|
|
24
|
+
}
|
|
25
|
+
rescue => e
|
|
26
|
+
render json: { status: "error", error: e.message }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def status
|
|
30
|
+
# Placeholder for long-running test status checks
|
|
31
|
+
render json: { status: "idle" }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class SessionsController < ApplicationController
|
|
3
|
+
layout "parapet/landing"
|
|
4
|
+
|
|
5
|
+
skip_before_action :require_authentication, only: %i[new create]
|
|
6
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to login_path, alert: "Try again later." }
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
user = User.authenticate_by(params.permit(:email_address, :password))
|
|
13
|
+
|
|
14
|
+
if user
|
|
15
|
+
Current.session&.destroy
|
|
16
|
+
Current.session = user.sessions.create!
|
|
17
|
+
cookies.encrypted.permanent[:parapet_session_id] = {
|
|
18
|
+
value: Current.session.id,
|
|
19
|
+
httponly: true,
|
|
20
|
+
same_site: :lax
|
|
21
|
+
}
|
|
22
|
+
redirect_to root_path
|
|
23
|
+
else
|
|
24
|
+
flash.now[:alert] = "Invalid email address or password."
|
|
25
|
+
render :new, status: :unprocessable_entity
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def destroy
|
|
30
|
+
Current.session&.destroy
|
|
31
|
+
cookies.delete(:parapet_session_id)
|
|
32
|
+
redirect_to login_path
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class WidgetsController < ApplicationController
|
|
3
|
+
def show
|
|
4
|
+
@widget_type = params[:type]
|
|
5
|
+
@audit_stats = Parapet::ExplorerManager.audit_data(@widget_type)
|
|
6
|
+
|
|
7
|
+
# Determine which partial to render based on suite type
|
|
8
|
+
# We look for a suite-specific widget partial first
|
|
9
|
+
suite_partial = "parapet/shared/widgets/#{@widget_type}"
|
|
10
|
+
|
|
11
|
+
if lookup_context.exists?(suite_partial, [], true)
|
|
12
|
+
render partial: suite_partial
|
|
13
|
+
else
|
|
14
|
+
# Fallback to legacy partial map or base widget
|
|
15
|
+
partial_map = {
|
|
16
|
+
"search" => "parapet/widgets/search",
|
|
17
|
+
"explainer" => "parapet/widgets/explainer",
|
|
18
|
+
"snippets" => "parapet/widgets/snippets",
|
|
19
|
+
"suggestions" => "parapet/widgets/suggestions",
|
|
20
|
+
"hotkeys" => "parapet/widgets/hotkeys"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@partial_path = partial_map[@widget_type]
|
|
24
|
+
|
|
25
|
+
if @partial_path && lookup_context.exists?(@partial_path, [], true)
|
|
26
|
+
render partial: @partial_path
|
|
27
|
+
else
|
|
28
|
+
render partial: "parapet/shared/widgets/base"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
include IconHelper
|
|
4
|
+
|
|
5
|
+
def storage_path(filename = nil)
|
|
6
|
+
ExplorerManager.storage_path(filename)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def explorer_summary(type = 'project')
|
|
10
|
+
ExplorerManager.summary(type)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def qss
|
|
14
|
+
if Rails.env.development?
|
|
15
|
+
# Force reload of QSS lib during development
|
|
16
|
+
qss_path = File.expand_path('../../../../lib/parapet/qss.rb', __FILE__)
|
|
17
|
+
load qss_path if File.exist?(qss_path)
|
|
18
|
+
::QSS.instance_variable_set(:@objects, nil)
|
|
19
|
+
end
|
|
20
|
+
::QSS
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
|
|
3
|
+
module Parapet
|
|
4
|
+
module IconHelper
|
|
5
|
+
def pp_icon(name, **options)
|
|
6
|
+
css_class = options.delete(:class)
|
|
7
|
+
|
|
8
|
+
# Try resolving custom SVG icon first
|
|
9
|
+
asset = resolve_icon_asset(name)
|
|
10
|
+
|
|
11
|
+
content = if asset
|
|
12
|
+
render_svg_icon(asset, name, options)
|
|
13
|
+
else
|
|
14
|
+
render_lucide_icon(name, options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
content_tag(:span, content, class: "qss-icon-wrap qss-w-4 qss-h-4 #{css_class}".strip, **options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def resolve_icon_asset(name)
|
|
23
|
+
paths = [
|
|
24
|
+
"parapet/icons/activities/#{name}.svg",
|
|
25
|
+
"parapet/icons/nav/#{name}.svg",
|
|
26
|
+
"parapet/icons/#{name}.svg"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
paths.each do |path|
|
|
30
|
+
# Propshaft resolution API
|
|
31
|
+
found = Rails.application.assets.load_path.find(path)
|
|
32
|
+
return found if found
|
|
33
|
+
end
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def render_svg_icon(asset, name, options)
|
|
38
|
+
raw_svg = File.read(asset.path)
|
|
39
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(raw_svg)
|
|
40
|
+
svg = doc.at_css('svg')
|
|
41
|
+
|
|
42
|
+
if svg
|
|
43
|
+
# Ensure structural sizing classes are present
|
|
44
|
+
svg['class'] = "qss-icon-raw qss-w-full qss-h-full #{svg['class']}".strip
|
|
45
|
+
svg.remove_attribute('width')
|
|
46
|
+
svg.remove_attribute('height')
|
|
47
|
+
svg['aria-hidden'] = 'true'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
doc.to_html.html_safe
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_lucide_icon(name, options)
|
|
54
|
+
content_tag(:i, nil, class: "lucide-#{name} qss-icon-lucide qss-w-full qss-h-full", aria: { hidden: true })
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class Activity < InternalRecord
|
|
3
|
+
self.table_name = "activities"
|
|
4
|
+
|
|
5
|
+
belongs_to :user, class_name: "Parapet::User", optional: true
|
|
6
|
+
belongs_to :trackable, polymorphic: true, optional: true
|
|
7
|
+
|
|
8
|
+
validates :activity_type, presence: true
|
|
9
|
+
validates :action, presence: true
|
|
10
|
+
validates :performed_via, inclusion: { in: %w[human ai_agent] }
|
|
11
|
+
|
|
12
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Parapet
|
|
4
|
+
class InternalRecord < ActiveRecord::Base
|
|
5
|
+
self.abstract_class = true
|
|
6
|
+
|
|
7
|
+
database_filename = Rails.env.test? ? "parapet-test.sqlite3" : "parapet.sqlite3"
|
|
8
|
+
database_path = Rails.root.join("storage", database_filename)
|
|
9
|
+
FileUtils.mkdir_p(database_path.dirname)
|
|
10
|
+
|
|
11
|
+
establish_connection(
|
|
12
|
+
adapter: "sqlite3",
|
|
13
|
+
database: database_path.to_s,
|
|
14
|
+
pool: ENV.fetch("RAILS_MAX_THREADS", 5),
|
|
15
|
+
timeout: 5000,
|
|
16
|
+
pragmas: { journal_mode: "wal" }
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def self.migrate!
|
|
20
|
+
database_tasks = ActiveRecord::Tasks::DatabaseTasks
|
|
21
|
+
original_migration_class = database_tasks.method(:migration_class)
|
|
22
|
+
database_tasks.define_singleton_method(:migration_class) { Parapet::InternalRecord }
|
|
23
|
+
|
|
24
|
+
migration_context = ActiveRecord::MigrationContext.new(
|
|
25
|
+
[ Parapet::Engine.root.join("db/parapet_migrate").to_s ],
|
|
26
|
+
connection_pool.schema_migration,
|
|
27
|
+
connection_pool.internal_metadata
|
|
28
|
+
)
|
|
29
|
+
migration_context.migrate
|
|
30
|
+
ensure
|
|
31
|
+
database_tasks.define_singleton_method(:migration_class, original_migration_class) if original_migration_class
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class RunHistory < ApplicationRecord
|
|
3
|
+
self.table_name = "run_history"
|
|
4
|
+
|
|
5
|
+
belongs_to :user, class_name: "Parapet::User", optional: true
|
|
6
|
+
|
|
7
|
+
validates :run_type, presence: true
|
|
8
|
+
validates :command, presence: true
|
|
9
|
+
validates :status, presence: true
|
|
10
|
+
validates :triggered_by_actor_type, presence: true
|
|
11
|
+
validates :performed_via, inclusion: { in: %w[human ai_agent] }
|
|
12
|
+
validates :started_at, presence: true
|
|
13
|
+
|
|
14
|
+
def passed?
|
|
15
|
+
status == "passed"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def failed?
|
|
19
|
+
status == "failed"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class SuiteAssignment < InternalRecord
|
|
3
|
+
self.table_name = "suite_assignments"
|
|
4
|
+
|
|
5
|
+
SUITES = %w[
|
|
6
|
+
authentication
|
|
7
|
+
authorization
|
|
8
|
+
licensing
|
|
9
|
+
activity_tracking
|
|
10
|
+
intentions_suite
|
|
11
|
+
style_suite
|
|
12
|
+
test_suite
|
|
13
|
+
build_suite
|
|
14
|
+
database_suite
|
|
15
|
+
signal_suite
|
|
16
|
+
process_monitor
|
|
17
|
+
polyglot_suite
|
|
18
|
+
pm_suite
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
TIERS = %w[novice intermediate advanced power].freeze
|
|
22
|
+
|
|
23
|
+
belongs_to :user, class_name: "Parapet::User"
|
|
24
|
+
|
|
25
|
+
validates :suite, inclusion: { in: SUITES }
|
|
26
|
+
validates :suite, uniqueness: { scope: :user_id }
|
|
27
|
+
validates :role, presence: true, allow_nil: true, length: { maximum: 64 }
|
|
28
|
+
validates :tier, inclusion: { in: TIERS }
|
|
29
|
+
end
|
|
30
|
+
end
|