@supportpages.io/wtfm 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.md +392 -2
- package/dist/actions.d.ts +98 -0
- package/dist/actions.js +86 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent-settings.d.ts +22 -0
- package/dist/agent-settings.js +35 -0
- package/dist/agent-settings.js.map +1 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.js +159 -0
- package/dist/api.js.map +1 -0
- package/dist/article-link.d.ts +16 -0
- package/dist/article-link.js +25 -0
- package/dist/article-link.js.map +1 -0
- package/dist/artifacts.d.ts +45 -0
- package/dist/artifacts.js +144 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/brand.d.ts +4 -0
- package/dist/brand.js +9 -0
- package/dist/brand.js.map +1 -0
- package/dist/bridge.d.ts +2964 -0
- package/dist/bridge.js +1036 -0
- package/dist/bridge.js.map +1 -0
- package/dist/capacity.d.ts +5 -0
- package/dist/capacity.js +11 -0
- package/dist/capacity.js.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +88 -0
- package/dist/credentials.js.map +1 -0
- package/dist/development-tls.d.ts +8 -0
- package/dist/development-tls.js +38 -0
- package/dist/development-tls.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/export.d.ts +11 -0
- package/dist/export.js +91 -0
- package/dist/export.js.map +1 -0
- package/dist/hosted-operations.d.ts +108 -0
- package/dist/hosted-operations.js +120 -0
- package/dist/hosted-operations.js.map +1 -0
- package/dist/hosting-benefits.d.ts +66 -0
- package/dist/hosting-benefits.js +67 -0
- package/dist/hosting-benefits.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/local-inventory.d.ts +22 -0
- package/dist/local-inventory.js +59 -0
- package/dist/local-inventory.js.map +1 -0
- package/dist/local-setup.d.ts +208 -0
- package/dist/local-setup.js +140 -0
- package/dist/local-setup.js.map +1 -0
- package/dist/pairing.d.ts +71 -0
- package/dist/pairing.js +235 -0
- package/dist/pairing.js.map +1 -0
- package/dist/preferences.d.ts +18 -0
- package/dist/preferences.js +44 -0
- package/dist/preferences.js.map +1 -0
- package/dist/progress.d.ts +205 -0
- package/dist/progress.js +224 -0
- package/dist/progress.js.map +1 -0
- package/dist/reminders.d.ts +31 -0
- package/dist/reminders.js +73 -0
- package/dist/reminders.js.map +1 -0
- package/dist/replace-connection.d.ts +6 -0
- package/dist/replace-connection.js +77 -0
- package/dist/replace-connection.js.map +1 -0
- package/dist/repository-actions.d.ts +18 -0
- package/dist/repository-actions.js +9 -0
- package/dist/repository-actions.js.map +1 -0
- package/dist/repository-benefits.d.ts +96 -0
- package/dist/repository-benefits.js +64 -0
- package/dist/repository-benefits.js.map +1 -0
- package/dist/run-update.d.ts +17 -0
- package/dist/run-update.js +27 -0
- package/dist/run-update.js.map +1 -0
- package/dist/runs.d.ts +541 -0
- package/dist/runs.js +146 -0
- package/dist/runs.js.map +1 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +58 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +277 -0
- package/dist/schema.js +66 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +298 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +1714 -0
- package/dist/session.js +619 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.d.ts +9 -0
- package/dist/settings.js +21 -0
- package/dist/settings.js.map +1 -0
- package/dist/sync.d.ts +495 -0
- package/dist/sync.js +191 -0
- package/dist/sync.js.map +1 -0
- package/dist/telemetry-scrub.d.ts +13 -0
- package/dist/telemetry-scrub.js +51 -0
- package/dist/telemetry-scrub.js.map +1 -0
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.js +173 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/walkthroughs.d.ts +224 -0
- package/dist/walkthroughs.js +109 -0
- package/dist/walkthroughs.js.map +1 -0
- package/dist/workspace.d.ts +14 -0
- package/dist/workspace.js +127 -0
- package/dist/workspace.js.map +1 -0
- package/dist/writer-agent.d.ts +21 -0
- package/dist/writer-agent.js +27 -0
- package/dist/writer-agent.js.map +1 -0
- package/dist/writer-entry.d.ts +171 -0
- package/dist/writer-entry.js +233 -0
- package/dist/writer-entry.js.map +1 -0
- package/dist/writing-style.d.ts +7 -0
- package/dist/writing-style.js +52 -0
- package/dist/writing-style.js.map +1 -0
- package/engine/SYNC.json +4 -0
- package/engine/VERSION +1 -0
- package/engine/detect-project/README.md +141 -0
- package/engine/detect-project/SKILL.md +1421 -0
- package/engine/detect-project/assets/desktop/desktop-frame.css +428 -0
- package/engine/detect-project/assets/game/game-frame.css +132 -0
- package/engine/detect-project/assets/macosui/LICENSE-puppertino.txt +21 -0
- package/engine/detect-project/assets/macosui/VERSIONS.txt +1 -0
- package/engine/detect-project/assets/macosui/fonts.css +15 -0
- package/engine/detect-project/assets/macosui/macos-frame.css +481 -0
- package/engine/detect-project/assets/macosui/puppertino.css +2153 -0
- package/engine/detect-project/assets/mobileui/LICENSE-fonts.txt +13 -0
- package/engine/detect-project/assets/mobileui/LICENSE-framework7.txt +52 -0
- package/engine/detect-project/assets/mobileui/VERSIONS.txt +6 -0
- package/engine/detect-project/assets/mobileui/device-frame.css +316 -0
- package/engine/detect-project/assets/mobileui/f7-color-theme.mjs +1345 -0
- package/engine/detect-project/assets/mobileui/f7-icons-names.json +1254 -0
- package/engine/detect-project/assets/mobileui/fonts.css +16 -0
- package/engine/detect-project/assets/mobileui/framework7-components.css +39 -0
- package/engine/detect-project/assets/mobileui/framework7-core.css +5245 -0
- package/engine/detect-project/assets/mobileui/icons.css +31 -0
- package/engine/detect-project/assets/mobileui/md3-defaults.css +89 -0
- package/engine/detect-project/assets/mobileui/platforms.json +46 -0
- package/engine/detect-project/assets/tailwind-fallback.css +1729 -0
- package/engine/detect-project/assets/webtui/LICENSE-webtui.txt +28 -0
- package/engine/detect-project/assets/webtui/VERSIONS.txt +7 -0
- package/engine/detect-project/assets/webtui/terminal-frame.css +195 -0
- package/engine/detect-project/assets/webtui/theme-catppuccin.css +1 -0
- package/engine/detect-project/assets/webtui/theme-everforest.css +1 -0
- package/engine/detect-project/assets/webtui/theme-gruvbox.css +1 -0
- package/engine/detect-project/assets/webtui/theme-nord.css +1 -0
- package/engine/detect-project/assets/webtui/theme-vitesse.css +1 -0
- package/engine/detect-project/assets/webtui/themes.json +37 -0
- package/engine/detect-project/assets/webtui/webtui-core.css +1 -0
- package/engine/detect-project/assets/win32ui/7css.css +2 -0
- package/engine/detect-project/assets/win32ui/LICENSE-7css.txt +21 -0
- package/engine/detect-project/assets/win32ui/VERSIONS.txt +1 -0
- package/engine/detect-project/assets/win32ui/win32-frame.css +278 -0
- package/engine/detect-project/package-lock.json +12 -0
- package/engine/detect-project/package.json +10 -0
- package/engine/detect-project/scripts/apply_runtime_profiles.js +313 -0
- package/engine/detect-project/scripts/check_css_health.js +412 -0
- package/engine/detect-project/scripts/check_project_map.js +150 -0
- package/engine/detect-project/scripts/check_runtime_coverage.js +311 -0
- package/engine/detect-project/scripts/check_runtime_recipe_quality.js +184 -0
- package/engine/detect-project/scripts/classify_app_type.sh +246 -0
- package/engine/detect-project/scripts/classify_surface.sh +95 -0
- package/engine/detect-project/scripts/classify_workspace.js +39 -0
- package/engine/detect-project/scripts/compile_css.sh +447 -0
- package/engine/detect-project/scripts/detect_static.js +645 -0
- package/engine/detect-project/scripts/detect_structure.js +187 -0
- package/engine/detect-project/scripts/include_census.js +451 -0
- package/engine/detect-project/scripts/json_get.js +142 -0
- package/engine/detect-project/scripts/merge_json.js +52 -0
- package/engine/detect-project/scripts/recommend_model_tier.js +252 -0
- package/engine/detect-project/scripts/resolve_route_chains.js +135 -0
- package/engine/detect-project/scripts/run_css_build.sh +40 -0
- package/engine/detect-project/scripts/sanitize_css.js +83 -0
- package/engine/detect-project/scripts/test_classify_surface.js +108 -0
- package/engine/detect-project/scripts/test_node_helpers.js +142 -0
- package/engine/detect-project/scripts/test_recommend_model_tier.js +119 -0
- package/engine/detect-project/scripts/test_resolve_route_chains.js +182 -0
- package/engine/detect-project/scripts/test_runtime_coverage.js +323 -0
- package/engine/detect-project/scripts/test_runtime_recipe_quality.js +187 -0
- package/engine/detect-project/scripts/theme_overrides.js +169 -0
- package/engine/detect-project/scripts/write_branding.js +129 -0
- package/engine/generate-illustrated-article/SKILL.md +461 -0
- package/engine/generate-illustrated-article/contracts/desktop.md +90 -0
- package/engine/generate-illustrated-article/contracts/game.md +38 -0
- package/engine/generate-illustrated-article/contracts/label-evidence.md +38 -0
- package/engine/generate-illustrated-article/contracts/macos.md +79 -0
- package/engine/generate-illustrated-article/contracts/mobile.md +40 -0
- package/engine/generate-illustrated-article/contracts/terminal.md +29 -0
- package/engine/generate-illustrated-article/contracts/win32.md +43 -0
- package/engine/generate-illustrated-article/package-lock.json +366 -0
- package/engine/generate-illustrated-article/package.json +15 -0
- package/engine/generate-illustrated-article/scripts/article_blocks.js +34 -0
- package/engine/generate-illustrated-article/scripts/check_article_json.js +105 -0
- package/engine/generate-illustrated-article/scripts/emit_walkthrough_signals.js +100 -0
- package/engine/generate-illustrated-article/scripts/extract_images.js +187 -0
- package/engine/generate-illustrated-article/scripts/generate_content_images.js +376 -0
- package/engine/generate-illustrated-article/scripts/include_census.js +451 -0
- package/engine/generate-illustrated-article/scripts/inject_assets.js +1691 -0
- package/engine/generate-illustrated-article/scripts/jit_mockup_css.js +191 -0
- package/engine/generate-illustrated-article/scripts/label_evidence.js +87 -0
- package/engine/generate-illustrated-article/scripts/lint_article_copy.js +252 -0
- package/engine/generate-illustrated-article/scripts/lint_mockup_fidelity.js +2403 -0
- package/engine/generate-illustrated-article/scripts/polish_tickets.js +1148 -0
- package/engine/generate-illustrated-article/scripts/related_repos.sh +52 -0
- package/engine/generate-illustrated-article/scripts/render_all.js +177 -0
- package/engine/generate-illustrated-article/scripts/render_mockup.js +665 -0
- package/engine/generate-illustrated-article/scripts/render_ready.js +164 -0
- package/engine/generate-illustrated-article/scripts/resolve_workspace.sh +86 -0
- package/engine/generate-illustrated-article/scripts/runtime_region_geometry.js +55 -0
- package/engine/generate-illustrated-article/scripts/source_paths.js +49 -0
- package/engine/generate-illustrated-article/scripts/test_control_visibility.js +30 -0
- package/engine/generate-illustrated-article/scripts/test_emit_walkthrough_signals.js +157 -0
- package/engine/generate-illustrated-article/scripts/test_generate_content_images.js +168 -0
- package/engine/generate-illustrated-article/scripts/test_include_census.js +126 -0
- package/engine/generate-illustrated-article/scripts/test_label_evidence.js +49 -0
- package/engine/generate-illustrated-article/scripts/test_lint_article_copy.js +138 -0
- package/engine/generate-illustrated-article/scripts/test_lint_mockup_fidelity.js +653 -0
- package/engine/generate-illustrated-article/scripts/test_node_ports.js +152 -0
- package/engine/generate-illustrated-article/scripts/test_polish_tickets.js +481 -0
- package/engine/generate-illustrated-article/scripts/test_related_repos.js +64 -0
- package/engine/generate-illustrated-article/scripts/test_render_ready.js +85 -0
- package/engine/generate-illustrated-article/scripts/test_source_paths.js +68 -0
- package/engine/generate-illustrated-article/scripts/trace_hook.js +79 -0
- package/engine/generate-illustrated-article/scripts/trace_hook.sh +4 -0
- package/engine/generate-illustrated-article/scripts/validate_html.js +114 -0
- package/engine/generate-illustrated-article/scripts/watermark.js +69 -0
- package/install.sh +14 -0
- package/package.json +57 -4
- package/scripts/auth.mjs +33 -0
- package/scripts/auto-update.mjs +7 -0
- package/scripts/build-plugin.mjs +58 -0
- package/scripts/build-release.mjs +94 -0
- package/scripts/check-release.mjs +40 -0
- package/scripts/check-runtime.mjs +10 -0
- package/scripts/cli.mjs +8 -0
- package/scripts/install.mjs +64 -0
- package/scripts/lib/agent-runner.mjs +181 -0
- package/scripts/lib/agent-settings.mjs +215 -0
- package/scripts/lib/article-skills.mjs +82 -0
- package/scripts/lib/auto-update.mjs +50 -0
- package/scripts/lib/brand.mjs +9 -0
- package/scripts/lib/browser.mjs +12 -0
- package/scripts/lib/claude-connection.mjs +33 -0
- package/scripts/lib/claude-permissions.mjs +47 -0
- package/scripts/lib/claude-plugin.mjs +15 -0
- package/scripts/lib/claude-writer.mjs +10 -0
- package/scripts/lib/cli-main.mjs +85 -0
- package/scripts/lib/cli.mjs +888 -0
- package/scripts/lib/codex-config.mjs +51 -0
- package/scripts/lib/codex-integration.mjs +61 -0
- package/scripts/lib/codex-skill.mjs +34 -0
- package/scripts/lib/harness-models.mjs +109 -0
- package/scripts/lib/install.mjs +269 -0
- package/scripts/lib/managed-writer.mjs +38 -0
- package/scripts/lib/planning.mjs +263 -0
- package/scripts/lib/prepare-update.mjs +85 -0
- package/scripts/lib/refresh-writers.mjs +12 -0
- package/scripts/lib/remove.mjs +167 -0
- package/scripts/lib/renderer.mjs +34 -0
- package/scripts/lib/terminal.mjs +252 -0
- package/scripts/lib/uninit.mjs +72 -0
- package/scripts/lib/update.mjs +55 -0
- package/scripts/lib/writer-recovery.mjs +35 -0
- package/scripts/lib/yolo.mjs +362 -0
- package/scripts/plugin-session.mjs +65 -0
- package/scripts/prepare-update.mjs +12 -0
- package/scripts/publish-release.mjs +92 -0
- package/scripts/skills/supportpages/SKILL.md +120 -0
- package/scripts/smoke-release.mjs +101 -0
- package/server.json +27 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* detect_structure.js — emit the deterministic skeleton of project_map.json.
|
|
4
|
+
*
|
|
5
|
+
* The article/walkthrough skills re-derive the same project structure on every
|
|
6
|
+
* run (where routes/controllers/views live, which layouts exist, the shared
|
|
7
|
+
* chrome). This script captures that once so generators can read it instead of
|
|
8
|
+
* re-running the grep/ls/find discovery flurry (see tools/analyze_traces.py).
|
|
9
|
+
*
|
|
10
|
+
* It fills the cheap, regex/fs-derivable fields:
|
|
11
|
+
* framework, dir_map, layouts[] (+ body_class), global_chrome[]
|
|
12
|
+
* It leaves default_user_assumptions[] empty and route_index{} empty — those
|
|
13
|
+
* are filled lazily by the generator skills (detect-project leaves them empty)
|
|
14
|
+
* respectively.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node detect_structure.js <out_json> <codebase_dir> [framework] [app_type] [app_type_source]
|
|
18
|
+
*
|
|
19
|
+
* app_type/app_type_source, when given, are stamped verbatim into the map
|
|
20
|
+
* (source ∈ hint|auto|default — how STEP 0 decided the type). Stamping here
|
|
21
|
+
* rather than in the model-executed enrichment keeps the record deterministic.
|
|
22
|
+
*
|
|
23
|
+
* Node built-ins only (matches detect_static.js). No deps.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { execSync } = require('child_process');
|
|
29
|
+
|
|
30
|
+
const TEMPLATE_EXT = /\.(html\.(erb|haml|slim)|heex|html|blade\.php|jsx|tsx|vue)$/;
|
|
31
|
+
const BODY_CLASS_RE = /<body[^>]*\sclass=["']([^"']*)["']/i;
|
|
32
|
+
|
|
33
|
+
// Per-framework structural conventions. layoutsDir/chrome use directory reads
|
|
34
|
+
// (not full globbing) so this stays built-in-only and fast.
|
|
35
|
+
const FRAMEWORKS = {
|
|
36
|
+
rails: {
|
|
37
|
+
dir_map: { routes: 'config/routes.rb', controllers: 'app/controllers', views: 'app/views' },
|
|
38
|
+
layoutsDir: 'app/views/layouts',
|
|
39
|
+
leafRe: /\.html\.(erb|haml|slim)$/,
|
|
40
|
+
},
|
|
41
|
+
phoenix: {
|
|
42
|
+
dir_map: { routes: 'lib', controllers: 'lib', views: 'lib' },
|
|
43
|
+
layoutsDir: null, // heex layouts vary by app name; resolved by glob below
|
|
44
|
+
leafRe: /\.heex$/,
|
|
45
|
+
},
|
|
46
|
+
django: {
|
|
47
|
+
dir_map: { routes: '.', controllers: '.', views: '.' },
|
|
48
|
+
layoutsDir: null,
|
|
49
|
+
leafRe: /\.html$/,
|
|
50
|
+
},
|
|
51
|
+
laravel: {
|
|
52
|
+
dir_map: { routes: 'routes/web.php', controllers: 'app/Http/Controllers', views: 'resources/views' },
|
|
53
|
+
layoutsDir: 'resources/views/layouts',
|
|
54
|
+
leafRe: /\.blade\.php$/,
|
|
55
|
+
},
|
|
56
|
+
'next-app': {
|
|
57
|
+
dir_map: { routes: 'app', controllers: 'app', views: 'app' },
|
|
58
|
+
layoutsDir: 'app',
|
|
59
|
+
leafRe: /^layout\.(jsx|tsx)$/,
|
|
60
|
+
},
|
|
61
|
+
'next-pages': {
|
|
62
|
+
dir_map: { routes: 'pages', controllers: 'pages/api', views: 'pages' },
|
|
63
|
+
layoutsDir: 'pages',
|
|
64
|
+
leafRe: /^_app\.(jsx|tsx)$/,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function exists(base, rel) {
|
|
69
|
+
try { fs.accessSync(path.join(base, rel)); return true; } catch { return false; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectFramework(base) {
|
|
73
|
+
const has = (f) => exists(base, f);
|
|
74
|
+
if (has('Gemfile') && has('config/routes.rb')) return 'rails';
|
|
75
|
+
if (has('mix.exs')) return 'phoenix';
|
|
76
|
+
if (has('artisan')) return 'laravel';
|
|
77
|
+
if (has('manage.py')) return 'django';
|
|
78
|
+
if (has('next.config.js') || has('next.config.mjs') || has('next.config.ts')) {
|
|
79
|
+
return has('app') ? 'next-app' : 'next-pages';
|
|
80
|
+
}
|
|
81
|
+
return 'unknown';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readBodyClass(file) {
|
|
85
|
+
try {
|
|
86
|
+
const txt = fs.readFileSync(file, 'utf8').slice(0, 8000);
|
|
87
|
+
const m = txt.match(BODY_CLASS_RE);
|
|
88
|
+
return m ? m[1].trim() : null;
|
|
89
|
+
} catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Find layout files + chrome partials in a known layouts dir.
|
|
93
|
+
function scanLayoutsDir(base, dirRel, leafRe) {
|
|
94
|
+
const layouts = [];
|
|
95
|
+
const chrome = [];
|
|
96
|
+
const abs = path.join(base, dirRel);
|
|
97
|
+
let entries;
|
|
98
|
+
try { entries = fs.readdirSync(abs); } catch { return { layouts, chrome }; }
|
|
99
|
+
for (const name of entries) {
|
|
100
|
+
if (!leafRe.test(name) && !name.endsWith('.erb') && !name.endsWith('.haml')
|
|
101
|
+
&& !name.endsWith('.slim') && !name.endsWith('.heex')) continue;
|
|
102
|
+
const rel = path.join(dirRel, name);
|
|
103
|
+
if (name.startsWith('_')) {
|
|
104
|
+
chrome.push(rel);
|
|
105
|
+
} else if (leafRe.test(name)) {
|
|
106
|
+
layouts.push(rel);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { layouts, chrome };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function gitSha(base) {
|
|
113
|
+
try {
|
|
114
|
+
return execSync('git rev-parse HEAD', { cwd: base, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
115
|
+
.toString().trim();
|
|
116
|
+
} catch { return null; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Stamp the running skills version + this file's content hash to stderr, so a
|
|
120
|
+
// debug bundle reveals exactly which code ran (vendored copies of the skills can
|
|
121
|
+
// drift behind a release). Hash is authoritative.
|
|
122
|
+
function logSkillsVersion() {
|
|
123
|
+
const here = path.dirname(fs.realpathSync(__filename));
|
|
124
|
+
let version = 'unknown';
|
|
125
|
+
for (const c of [path.join(here, '..', '..', 'VERSION'), path.join(here, '..', 'VERSION')]) {
|
|
126
|
+
try { const v = fs.readFileSync(c, 'utf8').trim(); if (v) { version = v; break; } } catch { /* next */ }
|
|
127
|
+
}
|
|
128
|
+
let sha = '?';
|
|
129
|
+
try { sha = require('crypto').createHash('sha256').update(fs.readFileSync(fs.realpathSync(__filename))).digest('hex').slice(0, 8); } catch { /* ignore */ }
|
|
130
|
+
console.error(`rtfm-skills detect_structure.js — v${version} (sha ${sha})`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function main() {
|
|
134
|
+
logSkillsVersion();
|
|
135
|
+
const [outJson, codebaseArg, frameworkArg, appTypeArg, appTypeSourceArg] = process.argv.slice(2);
|
|
136
|
+
if (!outJson || !codebaseArg) {
|
|
137
|
+
console.error('Usage: detect_structure.js <out_json> <codebase_dir> [framework] [app_type] [app_type_source]');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
const base = path.resolve(codebaseArg);
|
|
141
|
+
let framework = frameworkArg && frameworkArg !== 'unknown' ? frameworkArg : detectFramework(base);
|
|
142
|
+
|
|
143
|
+
const fw = FRAMEWORKS[framework] || null;
|
|
144
|
+
const dir_map = fw ? { ...fw.dir_map } : {};
|
|
145
|
+
// Verify each mapped path actually exists; drop the ones that don't.
|
|
146
|
+
for (const [k, v] of Object.entries(dir_map)) {
|
|
147
|
+
if (!exists(base, v)) delete dir_map[k];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let layouts = [];
|
|
151
|
+
let chrome = [];
|
|
152
|
+
if (fw && fw.layoutsDir && exists(base, fw.layoutsDir)) {
|
|
153
|
+
const scan = scanLayoutsDir(base, fw.layoutsDir, fw.leafRe);
|
|
154
|
+
chrome = scan.chrome;
|
|
155
|
+
layouts = scan.layouts.map((rel) => {
|
|
156
|
+
const isDefault = /application|app|layout/i.test(path.basename(rel));
|
|
157
|
+
const out = { file: rel, body_class: readBodyClass(path.join(base, rel)) };
|
|
158
|
+
// area = filename without the extension chain, when it isn't the default
|
|
159
|
+
const stem = path.basename(rel).replace(/\..*$/, '');
|
|
160
|
+
if (!/^application$|^layout$|^app$/i.test(stem)) out.area = stem;
|
|
161
|
+
if (isDefault && /application|^app\./i.test(path.basename(rel))) out.default = true;
|
|
162
|
+
return out;
|
|
163
|
+
});
|
|
164
|
+
// Guarantee exactly one default if we found any layouts.
|
|
165
|
+
if (layouts.length && !layouts.some((l) => l.default)) layouts[0].default = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const map = {
|
|
169
|
+
schema_version: 5,
|
|
170
|
+
framework,
|
|
171
|
+
...(appTypeArg ? { app_type: appTypeArg, app_type_source: appTypeSourceArg || 'default' } : {}),
|
|
172
|
+
git_sha: gitSha(base),
|
|
173
|
+
generated_at: new Date().toISOString(),
|
|
174
|
+
dir_map,
|
|
175
|
+
layouts,
|
|
176
|
+
global_chrome: chrome,
|
|
177
|
+
default_user_assumptions: [], // filled lazily by the generator skills
|
|
178
|
+
route_index: {}, // accumulated by the generators
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
fs.mkdirSync(path.dirname(outJson), { recursive: true });
|
|
182
|
+
fs.writeFileSync(outJson, JSON.stringify(map, null, 2));
|
|
183
|
+
console.log(`project_map: framework=${framework}, ${layouts.length} layout(s), ` +
|
|
184
|
+
`${chrome.length} chrome partial(s) -> ${outJson}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main();
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* include_census.js — what a view CONTRIBUTES to the screen, framework-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* A view file is a tree: its own inline sections plus the files it includes
|
|
5
|
+
* (Rails partials / ViewComponents, JSX/Vue/Svelte components, Blade/Livewire,
|
|
6
|
+
* Django/Jinja/Twig/Liquid includes, Razor partials, Phoenix/Angular custom
|
|
7
|
+
* elements, plain PHP requires). For the mockup contract what matters is not the
|
|
8
|
+
* syntax but the RENDER CONDITION of each piece:
|
|
9
|
+
*
|
|
10
|
+
* unconditional — rendered on every load of the view: part of the screen
|
|
11
|
+
* overlay — in the DOM but hidden until an action (modal/dialog/drawer/
|
|
12
|
+
* popover/menu/toast): the host screen plus the overlay when a
|
|
13
|
+
* step opens it; never a screen on its own
|
|
14
|
+
* conditional — behind a guard (if/unless/&&/ternary/@if/{% if %}/v-if/…):
|
|
15
|
+
* resolved for the default user (default_user_assumptions)
|
|
16
|
+
*
|
|
17
|
+
* The census is regex-only and bounded; it answers "which pieces must leave a
|
|
18
|
+
* trace in the mockup" and hands back per-piece marker strings (headings, labels,
|
|
19
|
+
* button text) that the lint and the polish tickets check for. It never fails a
|
|
20
|
+
* run on its own — a piece with no usable markers is simply not checkable.
|
|
21
|
+
*/
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const OVERLAY_NAME_RE = /modal|dialog|drawer|sheet|toast|tooltip|popover|snackbar|lightbox|flash|offcanvas|dropdown-menu|context-menu/i;
|
|
28
|
+
const OVERLAY_ATTR_RE = /\b(?:class\s*=\s*["'][^"']*\b(?:modal|dialog|drawer|offcanvas|popover|dropdown-menu|toast)\b|role\s*=\s*["'](?:dialog|alertdialog|menu|tooltip)["']|hidden\b|display\s*:\s*none|x-show\s*=|v-show\s*=|aria-hidden\s*=\s*["']true)/i;
|
|
29
|
+
// A template guard OPENING near the piece (same line or the few lines above),
|
|
30
|
+
// and the closers that cancel it. `guardOpen(ctx)` walks the window in order so
|
|
31
|
+
// an `<% end %>` / `{% endif %}` / `@endif` above the piece does not count as a
|
|
32
|
+
// guard, while an unclosed `if` does.
|
|
33
|
+
const GUARD_RE = /(?:<%-?\s*(?:if|unless|elsif|else)\b|\{%-?\s*(?:if|elif|else|unless)\b|@(?:if|elseif|else|unless|auth|guest|can|isset|empty)\b|\bv-(?:if|else-if|else)\b|\*ngIf\b|@if\s*\(|\?\s*\(?<|&&\s*\(?<|\bif\s+[^:]*:\s*$|\bunless\s+[^:]*:\s*$|<%=?\s*if\b)/;
|
|
34
|
+
const GUARD_CLOSE_RE = /(?:<%-?\s*end\b|\{%-?\s*end(?:if|unless)\b|@end(?:if|unless|auth|guest|can|isset|empty)\b)/;
|
|
35
|
+
function guardOpen(ctx) {
|
|
36
|
+
let depth = 0;
|
|
37
|
+
for (const line of ctx.split('\n')) {
|
|
38
|
+
if (GUARD_CLOSE_RE.test(line)) depth = Math.max(0, depth - 1);
|
|
39
|
+
if (GUARD_RE.test(line)) depth += 1;
|
|
40
|
+
}
|
|
41
|
+
return depth > 0;
|
|
42
|
+
}
|
|
43
|
+
const SKIP_EXT_RE = /\.(?:json|ya?ml|arb|po|css|scss|sass|less|svg|png|jpe?g|gif|webp|ico|woff2?|ttf|md|txt|lock)$/i;
|
|
44
|
+
const READ_CAP = 400 * 1024;
|
|
45
|
+
|
|
46
|
+
function readText(p) {
|
|
47
|
+
try {
|
|
48
|
+
const st = fs.statSync(p);
|
|
49
|
+
if (!st.isFile()) return '';
|
|
50
|
+
const fd = fs.openSync(p, 'r');
|
|
51
|
+
try {
|
|
52
|
+
const len = Math.min(st.size, READ_CAP);
|
|
53
|
+
const buf = Buffer.alloc(len);
|
|
54
|
+
fs.readSync(fd, buf, 0, len, 0);
|
|
55
|
+
return buf.toString('utf8');
|
|
56
|
+
} finally { fs.closeSync(fd); }
|
|
57
|
+
} catch { return ''; }
|
|
58
|
+
}
|
|
59
|
+
function isFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
|
|
60
|
+
function norm(s) { return String(s).replace(/\s+/g, ' ').trim(); }
|
|
61
|
+
function uniq(a) { return [...new Set(a)]; }
|
|
62
|
+
|
|
63
|
+
// ─── markers: short visible strings a piece paints ───────────────────────────
|
|
64
|
+
const TEMPLATE_NOISE_RE = /[{}<>]|<%|\$\{|#\{|%}|=>|\bt\(|\bI18n\b|__\(|\bthis\.|\bprops\.|\bstate\.|%\d*\$?[sd]|&#|\{\{|\$\w/;
|
|
65
|
+
function cleanMarker(s) {
|
|
66
|
+
const t = norm(String(s).replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&'));
|
|
67
|
+
if (t.length < 3 || t.length > 60) return null;
|
|
68
|
+
if (TEMPLATE_NOISE_RE.test(t)) return null;
|
|
69
|
+
if (!/[A-Za-z]/.test(t)) return null;
|
|
70
|
+
if (/^[a-z][A-Za-z0-9_]*$/.test(t) && /[A-Z_]/.test(t)) return null;
|
|
71
|
+
if (/[.!?]$/.test(t) || t.split(' ').length > 6) return null; // sentences are messages, not labels
|
|
72
|
+
return t;
|
|
73
|
+
}
|
|
74
|
+
/** Visible-text markers in a chunk of template: headings, legends, buttons, labels, th, link text, placeholders. */
|
|
75
|
+
function extractMarkers(text) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const push = s => { const c = cleanMarker(s); if (c) out.push(c); };
|
|
78
|
+
for (const m of text.matchAll(/<(h[1-6]|legend|summary|th|button|label|a|option)\b[^>]*>([\s\S]{0,300}?)<\/\1>/gi)) push(m[2]);
|
|
79
|
+
for (const m of text.matchAll(/<([A-Z][A-Za-z0-9.]*)\b[^>]*>([^<{}]{3,60})<\/\1>/g)) push(m[2]);
|
|
80
|
+
for (const m of text.matchAll(/\bplaceholder\s*=\s*["']([^"'{}<>]{3,60})["']/gi)) push(m[1]);
|
|
81
|
+
for (const m of text.matchAll(/\b(?:link_to|button_to|submit_tag|button_tag|label_tag)\s*\(?\s*["']([^"'{}<>#]{3,60})["']/g)) push(m[1]);
|
|
82
|
+
for (const m of text.matchAll(/\b\w+\.(?:submit|button|label)\s*\(?\s*(?::\w+\s*,\s*)?["']([^"'{}<>#]{3,60})["']/g)) push(m[1]);
|
|
83
|
+
for (const m of text.matchAll(/\b(?:__|_e|esc_html__|esc_html_e|esc_attr__|esc_attr_e|_x)\(\s*['"]([^'"]{3,60})['"]/g)) push(m[1]);
|
|
84
|
+
return uniq(out);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── include references with their call-site context ─────────────────────────
|
|
88
|
+
function findUnder(root, relDirs, tail) {
|
|
89
|
+
for (const d of relDirs) { const p = path.join(root, d, tail); if (isFile(p)) return p; }
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// ─── module resolution (aliases, workspaces) ─────────────────────────────────
|
|
93
|
+
// Component-tree apps import through aliases (`@/`, `~/`, `$lib`) declared per
|
|
94
|
+
// PACKAGE (tsconfig `paths`, svelte/vite `alias`) and through workspace packages
|
|
95
|
+
// (`@calcom/ui/...`, `twenty-ui/...`). Resolving against the git root alone
|
|
96
|
+
// leaves every such import unresolved, so the chain walk never leaves the
|
|
97
|
+
// primary view. Everything here is bounded and cached per root; a miss is
|
|
98
|
+
// reported as `file: null`, never guessed.
|
|
99
|
+
const _rootCache = new Map();
|
|
100
|
+
function rootState(root) {
|
|
101
|
+
let s = _rootCache.get(root);
|
|
102
|
+
if (!s) { s = { workspaces: null, pkgDirs: new Map(), aliasConf: new Map() }; _rootCache.set(root, s); }
|
|
103
|
+
return s;
|
|
104
|
+
}
|
|
105
|
+
// tsconfig/jsconfig are JSONC: comments and trailing commas. A regex stripper
|
|
106
|
+
// is unsafe here because alias keys like "~/*" contain comment openers, so the
|
|
107
|
+
// scanner tracks string state.
|
|
108
|
+
function stripJsonc(s) {
|
|
109
|
+
let out = '', q = false;
|
|
110
|
+
for (let i = 0; i < s.length; i++) {
|
|
111
|
+
const ch = s[i], nx = s[i + 1];
|
|
112
|
+
if (q) { out += ch; if (ch === '\\') { out += nx || ''; i++; } else if (ch === '"') q = false; continue; }
|
|
113
|
+
if (ch === '"') { q = true; out += ch; continue; }
|
|
114
|
+
if (ch === '/' && nx === '/') { while (i < s.length && s[i] !== '\n') i++; out += '\n'; continue; }
|
|
115
|
+
if (ch === '/' && nx === '*') { const e = s.indexOf('*/', i + 2); i = e < 0 ? s.length : e + 1; continue; }
|
|
116
|
+
out += ch;
|
|
117
|
+
}
|
|
118
|
+
return out.replace(/,(\s*[}\]])/g, '$1');
|
|
119
|
+
}
|
|
120
|
+
function readJsonLoose(p) {
|
|
121
|
+
const t = readText(p);
|
|
122
|
+
if (!t) return null;
|
|
123
|
+
try { return JSON.parse(t); } catch { /* jsonc */ }
|
|
124
|
+
try { return JSON.parse(stripJsonc(t)); } catch { return null; }
|
|
125
|
+
}
|
|
126
|
+
function listDirs(p) { try { return fs.readdirSync(p, { withFileTypes: true }).filter(d => d.isDirectory() && !d.name.startsWith('.') && d.name !== 'node_modules').map(d => path.join(p, d.name)); } catch { return []; } }
|
|
127
|
+
function expandWorkspaceGlob(root, pattern) {
|
|
128
|
+
// "packages/*", "packages/**", "apps/api/*", "web" — at most two wildcard levels.
|
|
129
|
+
const parts = pattern.replace(/\/+$/, '').split('/');
|
|
130
|
+
let dirs = [root];
|
|
131
|
+
for (const part of parts) {
|
|
132
|
+
if (part === '*' || part === '**') {
|
|
133
|
+
const next = [];
|
|
134
|
+
for (const d of dirs) { const kids = listDirs(d); next.push(...kids); if (part === '**') for (const k of kids) next.push(...listDirs(k)); }
|
|
135
|
+
dirs = next;
|
|
136
|
+
} else dirs = dirs.map(d => path.join(d, part));
|
|
137
|
+
if (dirs.length > 400) break;
|
|
138
|
+
}
|
|
139
|
+
return dirs;
|
|
140
|
+
}
|
|
141
|
+
function workspacePackages(root) {
|
|
142
|
+
const s = rootState(root);
|
|
143
|
+
if (s.workspaces) return s.workspaces;
|
|
144
|
+
const map = new Map();
|
|
145
|
+
const patterns = [];
|
|
146
|
+
const pj = readJsonLoose(path.join(root, 'package.json'));
|
|
147
|
+
if (pj && pj.workspaces) patterns.push(...(Array.isArray(pj.workspaces) ? pj.workspaces : (pj.workspaces.packages || [])));
|
|
148
|
+
const pw = readText(path.join(root, 'pnpm-workspace.yaml'));
|
|
149
|
+
if (pw) {
|
|
150
|
+
const m = pw.match(/^packages:\s*\n((?:\s+-\s+.*\n?)+)/m);
|
|
151
|
+
if (m) for (const l of m[1].split('\n')) { const mm = l.match(/^\s+-\s+['"]?([^'"#\s]+)/); if (mm && !mm[1].startsWith('!')) patterns.push(mm[1]); }
|
|
152
|
+
}
|
|
153
|
+
for (const pat of patterns.slice(0, 40)) for (const d of expandWorkspaceGlob(root, pat)) {
|
|
154
|
+
const p = readJsonLoose(path.join(d, 'package.json'));
|
|
155
|
+
if (p && typeof p.name === 'string' && !map.has(p.name)) map.set(p.name, d);
|
|
156
|
+
}
|
|
157
|
+
s.workspaces = map;
|
|
158
|
+
return map;
|
|
159
|
+
}
|
|
160
|
+
function nearestPackageDir(fromDir, root) {
|
|
161
|
+
for (let d = fromDir; d.startsWith(root); d = path.dirname(d)) {
|
|
162
|
+
if (isFile(path.join(d, 'package.json'))) return d;
|
|
163
|
+
if (d === root) break;
|
|
164
|
+
}
|
|
165
|
+
return root;
|
|
166
|
+
}
|
|
167
|
+
// Alias table for the package that owns `fromDir`: [{prefix, targets:[abs dir/file base]}].
|
|
168
|
+
function aliasConfig(pkgDir, root) {
|
|
169
|
+
const s = rootState(root);
|
|
170
|
+
if (s.aliasConf.has(pkgDir)) return s.aliasConf.get(pkgDir);
|
|
171
|
+
const aliases = [];
|
|
172
|
+
const addAlias = (key, targets, base) => {
|
|
173
|
+
const prefix = key.replace(/\/?\*$/, '');
|
|
174
|
+
if (!prefix) return;
|
|
175
|
+
const ts = (Array.isArray(targets) ? targets : [targets]).filter(t => typeof t === 'string').map(t => path.resolve(base, t.replace(/\/?\*$/, '')));
|
|
176
|
+
if (ts.length) aliases.push({ prefix, targets: ts, exact: !/\*$/.test(key) });
|
|
177
|
+
};
|
|
178
|
+
// tsconfig / jsconfig `paths` — follow `extends` up to 3 hops for paths/baseUrl.
|
|
179
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
180
|
+
let file = path.join(pkgDir, name), seen = 0, baseDir = pkgDir, baseUrl = null;
|
|
181
|
+
while (file && isFile(file) && seen++ < 4) {
|
|
182
|
+
const cfg = readJsonLoose(file); if (!cfg) break;
|
|
183
|
+
const co = cfg.compilerOptions || {};
|
|
184
|
+
if (co.baseUrl && !baseUrl) baseUrl = path.resolve(path.dirname(file), co.baseUrl);
|
|
185
|
+
if (co.paths) { const base = baseUrl || path.dirname(file); for (const [k, v] of Object.entries(co.paths)) addAlias(k, v, base); break; }
|
|
186
|
+
file = typeof cfg.extends === 'string' && cfg.extends.startsWith('.') ? path.resolve(path.dirname(file), cfg.extends) : null;
|
|
187
|
+
if (file && !isFile(file) && isFile(file + '.json')) file += '.json';
|
|
188
|
+
}
|
|
189
|
+
if (aliases.length) break;
|
|
190
|
+
}
|
|
191
|
+
// svelte.config.js / vite.config.* `alias: { $lib: 'src/lib', '@': path.resolve(__dirname, 'src') }`
|
|
192
|
+
for (const name of ['svelte.config.js', 'svelte.config.ts', 'vite.config.ts', 'vite.config.js', 'vite.config.mts', 'nuxt.config.ts']) {
|
|
193
|
+
const t = readText(path.join(pkgDir, name)); if (!t) continue;
|
|
194
|
+
const m = t.match(/\balias\s*:\s*\{([\s\S]*?)\n\s*\}/); if (!m) continue;
|
|
195
|
+
for (const mm of m[1].matchAll(/['"]?([$@~][\w/-]*|[\w-]+)['"]?\s*:\s*(?:['"]([^'"]+)['"]|(?:path\.)?resolve\([^)]*?['"]([^'"]+)['"]\s*\)|fileURLToPath\(new URL\(['"]([^'"]+)['"])/g)) {
|
|
196
|
+
const target = mm[2] || mm[3] || mm[4]; if (!target) continue;
|
|
197
|
+
addAlias(mm[1], target.replace(/^\.\//, ''), pkgDir);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
s.aliasConf.set(pkgDir, aliases);
|
|
201
|
+
return aliases;
|
|
202
|
+
}
|
|
203
|
+
const RESOLVE_EXTS = ['', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.vue', '.svelte', '.astro'];
|
|
204
|
+
function tryBases(bases) {
|
|
205
|
+
for (const b of bases) {
|
|
206
|
+
for (const e of RESOLVE_EXTS) if (isFile(b + e)) return b + e;
|
|
207
|
+
for (const e of RESOLVE_EXTS.slice(1)) if (isFile(path.join(b, 'index' + e))) return path.join(b, 'index' + e);
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function resolveModule(src, fromDir, root) {
|
|
212
|
+
if (!src || /^(?:node:|https?:)/.test(src)) return null;
|
|
213
|
+
if (src.startsWith('.')) return tryBases([path.resolve(fromDir, src)]);
|
|
214
|
+
if (src.startsWith('/')) return tryBases([path.join(root, src)]);
|
|
215
|
+
const pkgDir = nearestPackageDir(fromDir, root);
|
|
216
|
+
// 1. declared aliases of the owning package (longest prefix first)
|
|
217
|
+
const aliases = aliasConfig(pkgDir, root).slice().sort((a, b) => b.prefix.length - a.prefix.length);
|
|
218
|
+
for (const a of aliases) {
|
|
219
|
+
if (a.exact ? src === a.prefix : (src === a.prefix || src.startsWith(a.prefix + '/') || (a.prefix.endsWith('/') && src.startsWith(a.prefix)))) {
|
|
220
|
+
const rest = src === a.prefix ? '' : src.slice(a.prefix.length).replace(/^\//, '');
|
|
221
|
+
const hit = tryBases(a.targets.map(t => rest ? path.join(t, rest) : t));
|
|
222
|
+
if (hit) return hit;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 2. conventional aliases relative to the owning package
|
|
226
|
+
if (src.startsWith('@/') || src.startsWith('~/')) {
|
|
227
|
+
const rest = src.slice(2);
|
|
228
|
+
const hit = tryBases(['src', 'app', 'resources/js', ''].map(pre => path.join(pkgDir, pre, rest)));
|
|
229
|
+
if (hit) return hit;
|
|
230
|
+
}
|
|
231
|
+
if (src.startsWith('$lib/') || src === '$lib') return tryBases([path.join(pkgDir, 'src/lib', src.slice(4).replace(/^\//, ''))]);
|
|
232
|
+
// 3. workspace packages: "@scope/pkg/sub" / "pkg/sub" → the package dir (honouring
|
|
233
|
+
// an exact `exports` entry), then <pkg>/sub and <pkg>/src/sub.
|
|
234
|
+
const ws = workspacePackages(root);
|
|
235
|
+
if (ws.size) {
|
|
236
|
+
const segs = src.split('/');
|
|
237
|
+
const name = src.startsWith('@') ? segs.slice(0, 2).join('/') : segs[0];
|
|
238
|
+
const dir = ws.get(name);
|
|
239
|
+
if (dir) {
|
|
240
|
+
const sub = src.slice(name.length).replace(/^\//, '');
|
|
241
|
+
const pj = readJsonLoose(path.join(dir, 'package.json')) || {};
|
|
242
|
+
const ex = pj.exports && pj.exports['.' + (sub ? '/' + sub : '')];
|
|
243
|
+
const exTarget = typeof ex === 'string' ? ex : (ex && typeof ex === 'object' ? (ex.import || ex.default || ex.types) : null);
|
|
244
|
+
if (typeof exTarget === 'string' && !/\/dist\//.test(exTarget) && isFile(path.join(dir, exTarget))) return path.join(dir, exTarget);
|
|
245
|
+
const hit = tryBases(sub ? [path.join(dir, sub), path.join(dir, 'src', sub)] : [path.join(dir, 'src'), dir]);
|
|
246
|
+
if (hit) return hit;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
// A barrel (`index.ts` of re-exports) is where an alias/workspace import lands;
|
|
252
|
+
// the piece that renders is the module it re-exports. Follow `export { Name }
|
|
253
|
+
// from './x'` and `export * from './x'` for the imported name, two hops at most.
|
|
254
|
+
function followBarrel(file, name, root, hop = 0) {
|
|
255
|
+
if (!file || !name || hop > 2) return file;
|
|
256
|
+
const text = readText(file);
|
|
257
|
+
if (!text || !/\bexport\s+(?:\{|\*)/.test(text)) return file;
|
|
258
|
+
// The module defines the name itself: it is the piece.
|
|
259
|
+
if (new RegExp('(?:^|\\n)\\s*export\\s+(?:default\\s+)?(?:const|let|var|function|class|async function)\\s+' + name + '\\b').test(text)) return file;
|
|
260
|
+
const dir = path.dirname(file);
|
|
261
|
+
for (const m of text.matchAll(/export\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
262
|
+
const names = m[1].split(',').map(x => x.trim().split(/\s+as\s+/).pop().trim());
|
|
263
|
+
if (names.includes(name)) { const t = resolveModule(m[2], dir, root); return t ? followBarrel(t, name, root, hop + 1) : file; }
|
|
264
|
+
}
|
|
265
|
+
let seen = 0;
|
|
266
|
+
for (const m of text.matchAll(/export\s*\*\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
267
|
+
if (seen++ > 12) break;
|
|
268
|
+
const t = resolveModule(m[1], dir, root);
|
|
269
|
+
if (!t) continue;
|
|
270
|
+
const tt = readText(t) || '';
|
|
271
|
+
if (new RegExp('\\b' + name + '\\b').test(tt)) return followBarrel(t, name, root, hop + 1);
|
|
272
|
+
}
|
|
273
|
+
return file;
|
|
274
|
+
}
|
|
275
|
+
// Angular: `<pngx-widget>` → the component whose decorator declares that selector.
|
|
276
|
+
// One bounded walk per package (component .ts files under src/), cached.
|
|
277
|
+
function angularSelectorMap(pkgDir, root) {
|
|
278
|
+
const s = rootState(root);
|
|
279
|
+
if (!s.selectors) s.selectors = new Map();
|
|
280
|
+
if (s.selectors.has(pkgDir)) return s.selectors.get(pkgDir);
|
|
281
|
+
const map = new Map();
|
|
282
|
+
const start = isFile(path.join(pkgDir, 'src', 'main.ts')) || fs.existsSync(path.join(pkgDir, 'src', 'app')) ? path.join(pkgDir, 'src') : pkgDir;
|
|
283
|
+
let budget = 4000;
|
|
284
|
+
const walk = d => {
|
|
285
|
+
if (budget <= 0) return;
|
|
286
|
+
let ents = []; try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
287
|
+
for (const e of ents) {
|
|
288
|
+
if (budget-- <= 0) return;
|
|
289
|
+
const p = path.join(d, e.name);
|
|
290
|
+
if (e.isDirectory()) { if (!/^(?:node_modules|dist|\.angular|\.git|coverage)$/.test(e.name)) walk(p); continue; }
|
|
291
|
+
if (!/\.component\.ts$/.test(e.name)) continue;
|
|
292
|
+
const t = readText(p); if (!t) continue;
|
|
293
|
+
const m = t.match(/selector\s*:\s*['"`]([^'"`]+)['"`]/); if (!m) continue;
|
|
294
|
+
const tu = t.match(/templateUrl\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
295
|
+
const html = tu ? path.resolve(path.dirname(p), tu[1]) : p.replace(/\.ts$/, '.html');
|
|
296
|
+
for (const sel of m[1].split(',')) map.set(sel.trim().replace(/^\[|\]$/g, ''), isFile(html) ? html : p);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
walk(start);
|
|
300
|
+
s.selectors.set(pkgDir, map);
|
|
301
|
+
return map;
|
|
302
|
+
}
|
|
303
|
+
const UI_PRIMITIVES = new Set(('Button Icon Tooltip Dialog Link Input Select Form Label Badge Avatar Skeleton Trans Head Meta Fragment Suspense ' +
|
|
304
|
+
'Provider Image Img Text Box Flex Grid Stack Card Container Divider Spinner Loader Portal Popover Menu MenuItem DropdownMenu Checkbox Switch ' +
|
|
305
|
+
'Radio Textarea TextField Alert Toast Modal Sheet Tabs Tab Table Th Td Tr Slot Transition Motion ErrorBoundary Script Layout Component Title ' +
|
|
306
|
+
'Description Separator Toaster Helmet Router Routes Route Outlet NavLink Anchor Heading Paragraph Span Center Group Row Col Column Section Main ' +
|
|
307
|
+
'Header Footer Nav Aside Article Wrapper Root Content Trigger Item List ListItem Field FormField FormItem FormLabel FormControl FormMessage Kbd Code Pre Strong Em Small').split(' '));
|
|
308
|
+
const PRIMITIVE_SUFFIX_RE = /(?:Provider|Context|Boundary|Icon|Trigger|Portal|Props|Params|Button|Link)$/;
|
|
309
|
+
const ICON_PREFIX_RE = /^(?:Icon|Lucide|Tabler|Fa|Md|Hi|Bi|Ri|Io)[A-Z]/;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Every include reference in `relFile` with {name, file (repo-relative or null),
|
|
313
|
+
* line, guarded, overlay}. `guarded` = a template guard on the same line or within
|
|
314
|
+
* the 3 lines above; `overlay` = the include's name/file or its surrounding element
|
|
315
|
+
* reads as an overlay.
|
|
316
|
+
*/
|
|
317
|
+
function includeRefs(root, relFile) {
|
|
318
|
+
const abs = path.join(root, relFile);
|
|
319
|
+
const text = readText(abs);
|
|
320
|
+
if (!text) return [];
|
|
321
|
+
const dir = path.dirname(abs);
|
|
322
|
+
const rel = p => path.relative(root, p).split(path.sep).join('/');
|
|
323
|
+
const lines = text.split('\n');
|
|
324
|
+
const lineOf = idx => text.slice(0, idx).split('\n').length;
|
|
325
|
+
const refs = new Map();
|
|
326
|
+
const add = (name, file, idx) => {
|
|
327
|
+
if (refs.has(name)) return;
|
|
328
|
+
const ln = lineOf(idx);
|
|
329
|
+
const ctx = lines.slice(Math.max(0, ln - 4), ln).join('\n');
|
|
330
|
+
// Overlay-ness from the include's own line and the line above (an opening
|
|
331
|
+
// wrapper like <div class="modal"> / role="dialog"); no further, or a
|
|
332
|
+
// neighbouring overlay would taint an unrelated include.
|
|
333
|
+
const around = lines.slice(Math.max(0, ln - 2), ln).join('\n');
|
|
334
|
+
refs.set(name, {
|
|
335
|
+
name, file: file ? rel(file) : null, line: ln,
|
|
336
|
+
guarded: guardOpen(ctx),
|
|
337
|
+
overlay: OVERLAY_NAME_RE.test(name) || (file && OVERLAY_NAME_RE.test(path.basename(file))) || OVERLAY_ATTR_RE.test(around),
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
const ext = path.extname(relFile).toLowerCase();
|
|
341
|
+
if (['.tsx', '.jsx', '.js', '.ts', '.mjs', '.vue', '.svelte', '.astro'].includes(ext)) {
|
|
342
|
+
const imports = new Map();
|
|
343
|
+
for (const m of text.matchAll(/import\s+(?:type\s+)?(?:(\w+)|\{([^}]*)\}|(\w+)\s*,\s*\{([^}]*)\})\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
344
|
+
const names = [];
|
|
345
|
+
if (m[1]) names.push(m[1]);
|
|
346
|
+
if (m[3]) names.push(m[3]);
|
|
347
|
+
for (const g of [m[2], m[4]]) if (g) for (const part of g.split(',')) { const n = part.trim().split(/\s+as\s+/).pop().trim(); if (n) names.push(n); }
|
|
348
|
+
for (const n of names) imports.set(n, m[5]);
|
|
349
|
+
}
|
|
350
|
+
for (const m of text.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*(?:React\.)?lazy\(\s*(?:async\s*)?\(\)\s*=>\s*import\(\s*['"]([^'"]+)['"]/g)) if (!imports.has(m[1])) imports.set(m[1], m[2]);
|
|
351
|
+
for (const m of text.matchAll(/(?<![A-Za-z0-9_$\])])<([A-Z][A-Za-z0-9]*)(?:\.[A-Z][A-Za-z0-9]*)*(?=[\s/>])/g)) {
|
|
352
|
+
const name = m[1];
|
|
353
|
+
if (UI_PRIMITIVES.has(name) || PRIMITIVE_SUFFIX_RE.test(name) || ICON_PREFIX_RE.test(name)) continue;
|
|
354
|
+
const src = imports.get(name);
|
|
355
|
+
// Defined in this very file (a local sub-component): not an include.
|
|
356
|
+
if (!src && new RegExp('(?:^|\\n)\\s*(?:export\\s+)?(?:const|let|var|function|class)\\s+' + name + '\\b').test(text)) continue;
|
|
357
|
+
add(name, src ? followBarrel(resolveModule(src, dir, root), name, root) : null, m.index);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (/\.(?:erb|haml|slim)$/.test(ext) || ext === '.rb') {
|
|
361
|
+
for (const m of text.matchAll(/render\s*\(?\s*(?:partial:\s*)?['"]([\w\/.-]+)['"]/g)) {
|
|
362
|
+
const name = m[1]; let file = null;
|
|
363
|
+
const base = name.split('/').pop();
|
|
364
|
+
const viewsRoot = abs.includes('/app/views/') ? abs.slice(0, abs.indexOf('/app/views/') + '/app/views/'.length) : null;
|
|
365
|
+
// "shared/x" resolves against the views root when known, else against the
|
|
366
|
+
// view's dir and its parent (a bare "x" is a sibling partial).
|
|
367
|
+
const dirs = name.includes('/')
|
|
368
|
+
? [viewsRoot && path.join(viewsRoot, path.dirname(name)), path.join(dir, path.dirname(name)), path.join(path.dirname(dir), path.dirname(name))].filter(Boolean)
|
|
369
|
+
: [dir];
|
|
370
|
+
outer: for (const d of dirs) for (const e of ['.html.erb', '.erb', '.html.haml', '.html.slim', '.turbo_stream.erb']) {
|
|
371
|
+
const p = path.join(d, `_${base}${e}`); if (isFile(p)) { file = p; break outer; }
|
|
372
|
+
}
|
|
373
|
+
add(name, file, m.index);
|
|
374
|
+
}
|
|
375
|
+
for (const m of text.matchAll(/render\s*\(?\s*([A-Z][A-Za-z0-9:]*Component)\.new/g)) {
|
|
376
|
+
const snake = m[1].replace(/::/g, '/').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
377
|
+
add(m[1], findUnder(root, ['app/components'], `${snake}.html.erb`) || findUnder(root, ['app/components'], `${snake}.rb`), m.index);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (ext === '.php' && /\.blade\.php$/.test(relFile)) {
|
|
381
|
+
for (const m of text.matchAll(/@include(?:If|When|Unless)?\(\s*(?:[^,]+,\s*)?['"]([\w.-]+)['"]/g)) add(m[1], findUnder(root, ['resources/views'], m[1].replace(/\./g, '/') + '.blade.php'), m.index);
|
|
382
|
+
for (const m of text.matchAll(/<x-([\w.-]+)[\s/>]/g)) add(`x-${m[1]}`, findUnder(root, ['resources/views/components'], m[1].replace(/\./g, '/') + '.blade.php'), m.index);
|
|
383
|
+
for (const m of text.matchAll(/<livewire:([\w.-]+)[\s/>]/g)) add(`livewire:${m[1]}`, findUnder(root, ['resources/views/livewire'], m[1].replace(/\./g, '/') + '.blade.php'), m.index);
|
|
384
|
+
}
|
|
385
|
+
if (ext === '.php' && !/\.blade\.php$/.test(relFile)) {
|
|
386
|
+
for (const m of text.matchAll(/\b(?:require|include)(?:_once)?\s*\(?\s*(?:ABSPATH\s*\.\s*)?['"]([\w\/.-]+\.php)['"]/g)) {
|
|
387
|
+
const p = isFile(path.join(root, m[1])) ? path.join(root, m[1]) : (isFile(path.join(dir, m[1])) ? path.join(dir, m[1]) : null);
|
|
388
|
+
add(m[1], p, m.index);
|
|
389
|
+
}
|
|
390
|
+
for (const m of text.matchAll(/\bget_template_part\(\s*['"]([\w\/.-]+)['"]/g)) add(m[1], null, m.index);
|
|
391
|
+
}
|
|
392
|
+
for (const m of text.matchAll(/\{%-?\s*(?:include|embed)\s+['"]([\w\/.-]+)['"]/g)) {
|
|
393
|
+
let p = null;
|
|
394
|
+
for (let d = dir; d.startsWith(root) && !p; d = path.dirname(d)) {
|
|
395
|
+
if (isFile(path.join(d, m[1]))) p = path.join(d, m[1]);
|
|
396
|
+
else if (isFile(path.join(d, 'templates', m[1]))) p = path.join(d, 'templates', m[1]);
|
|
397
|
+
if (d === root) break;
|
|
398
|
+
}
|
|
399
|
+
add(m[1], p, m.index);
|
|
400
|
+
}
|
|
401
|
+
if (ext === '.cshtml') {
|
|
402
|
+
for (const m of text.matchAll(/(?:<partial\s+name\s*=\s*["']([\w.-]+)["']|Partial(?:Async)?\(\s*["']([\w.-]+)["'])/g)) {
|
|
403
|
+
const name = m[1] || m[2];
|
|
404
|
+
add(name, findUnder(root, [rel(dir), 'Views/Shared', 'Areas/Admin/Views/Shared', 'Pages/Shared'], `${name}.cshtml`), m.index);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (ext === '.html' || ext === '.heex' || ext === '.eex') {
|
|
408
|
+
const selectors = ext === '.html' ? angularSelectorMap(nearestPackageDir(dir, root), root) : new Map();
|
|
409
|
+
for (const m of text.matchAll(/<(app-[a-z0-9-]+|[a-z][a-z0-9]*-[a-z0-9-]+)[\s/>]/g)) {
|
|
410
|
+
if (/^(?:ng-|router-|mat-|cdk-|nz-|p-|i-bs|ion-)/.test(m[1]) && !selectors.has(m[1])) continue;
|
|
411
|
+
add(m[1], selectors.get(m[1]) || null, m.index);
|
|
412
|
+
}
|
|
413
|
+
for (const m of text.matchAll(/<\.([a-z_][a-z0-9_]*)[\s/>]/g)) add(`.${m[1]}`, null, m.index);
|
|
414
|
+
for (const m of text.matchAll(/<([A-Z][\w.]*\.[a-z_][a-z0-9_]*)[\s/>]/g)) add(m[1], null, m.index);
|
|
415
|
+
}
|
|
416
|
+
return [...refs.values()];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The census of a primary view: its own inline sections plus each include with
|
|
421
|
+
* its render condition and marker strings.
|
|
422
|
+
* {
|
|
423
|
+
* inline_sections: [{heading, markers}], // h1–h4 / legend blocks written directly in the view
|
|
424
|
+
* includes: [{name, file, line, kind: 'unconditional'|'overlay'|'conditional', markers}],
|
|
425
|
+
* }
|
|
426
|
+
* `markers` are only taken from resolvable files; an include with no file has
|
|
427
|
+
* `markers: []` and is reported but not checkable.
|
|
428
|
+
*/
|
|
429
|
+
function census(root, primaryView, opts = {}) {
|
|
430
|
+
const text = readText(path.join(root, primaryView));
|
|
431
|
+
const out = { inline_sections: [], includes: [] };
|
|
432
|
+
if (!text) return out;
|
|
433
|
+
// Inline sections: headings the view itself paints, with the markers that follow each until the next heading.
|
|
434
|
+
const heads = [...text.matchAll(/<(h[1-4]|legend)\b[^>]*>([\s\S]{0,200}?)<\/\1>/gi)];
|
|
435
|
+
for (let i = 0; i < heads.length; i++) {
|
|
436
|
+
const heading = cleanMarker(heads[i][2]);
|
|
437
|
+
if (!heading) continue;
|
|
438
|
+
const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : Math.min(text.length, start + 6000);
|
|
439
|
+
const before = text.slice(Math.max(0, start - 400), start);
|
|
440
|
+
out.inline_sections.push({ heading, guarded: guardOpen(before.split('\n').slice(-4).join('\n')), markers: extractMarkers(text.slice(start, end)).slice(0, 12) });
|
|
441
|
+
}
|
|
442
|
+
for (const ref of includeRefs(root, primaryView)) {
|
|
443
|
+
if (ref.file && SKIP_EXT_RE.test(ref.file)) continue;
|
|
444
|
+
const kind = ref.overlay ? 'overlay' : (ref.guarded ? 'conditional' : 'unconditional');
|
|
445
|
+
const markers = ref.file ? extractMarkers(readText(path.join(root, ref.file))).slice(0, 12) : [];
|
|
446
|
+
out.includes.push({ name: ref.name, file: ref.file, line: ref.line, kind, markers });
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
module.exports = { census, includeRefs, extractMarkers, cleanMarker, guardOpen, resolveModule, followBarrel, workspacePackages, OVERLAY_NAME_RE, GUARD_RE };
|