@nac3/forge-cli 0.2.0-alpha.1
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 +45 -0
- package/README.md +371 -0
- package/dist/bin/yf.d.ts +5 -0
- package/dist/bin/yf.d.ts.map +1 -0
- package/dist/bin/yf.js +86 -0
- package/dist/bin/yf.js.map +1 -0
- package/dist/chat/claude.d.ts +100 -0
- package/dist/chat/claude.d.ts.map +1 -0
- package/dist/chat/claude.js +228 -0
- package/dist/chat/claude.js.map +1 -0
- package/dist/chat/ingest_session.d.ts +97 -0
- package/dist/chat/ingest_session.d.ts.map +1 -0
- package/dist/chat/ingest_session.js +99 -0
- package/dist/chat/ingest_session.js.map +1 -0
- package/dist/chat/panel.d.ts +15 -0
- package/dist/chat/panel.d.ts.map +1 -0
- package/dist/chat/panel.js +1526 -0
- package/dist/chat/panel.js.map +1 -0
- package/dist/chat/persistence.d.ts +37 -0
- package/dist/chat/persistence.d.ts.map +1 -0
- package/dist/chat/persistence.js +91 -0
- package/dist/chat/persistence.js.map +1 -0
- package/dist/chat/server.d.ts +34 -0
- package/dist/chat/server.d.ts.map +1 -0
- package/dist/chat/server.js +1540 -0
- package/dist/chat/server.js.map +1 -0
- package/dist/chat/spec_extract.d.ts +35 -0
- package/dist/chat/spec_extract.d.ts.map +1 -0
- package/dist/chat/spec_extract.js +152 -0
- package/dist/chat/spec_extract.js.map +1 -0
- package/dist/chat/spec_plan.d.ts +65 -0
- package/dist/chat/spec_plan.d.ts.map +1 -0
- package/dist/chat/spec_plan.js +160 -0
- package/dist/chat/spec_plan.js.map +1 -0
- package/dist/chat/spec_scaffold.d.ts +95 -0
- package/dist/chat/spec_scaffold.d.ts.map +1 -0
- package/dist/chat/spec_scaffold.js +220 -0
- package/dist/chat/spec_scaffold.js.map +1 -0
- package/dist/chat/tools/git.d.ts +59 -0
- package/dist/chat/tools/git.d.ts.map +1 -0
- package/dist/chat/tools/git.js +313 -0
- package/dist/chat/tools/git.js.map +1 -0
- package/dist/chat/tools/github.d.ts +59 -0
- package/dist/chat/tools/github.d.ts.map +1 -0
- package/dist/chat/tools/github.js +310 -0
- package/dist/chat/tools/github.js.map +1 -0
- package/dist/chat/tools/lifecycle.d.ts +82 -0
- package/dist/chat/tools/lifecycle.d.ts.map +1 -0
- package/dist/chat/tools/lifecycle.js +295 -0
- package/dist/chat/tools/lifecycle.js.map +1 -0
- package/dist/chat/tools/manual.d.ts +26 -0
- package/dist/chat/tools/manual.d.ts.map +1 -0
- package/dist/chat/tools/manual.js +164 -0
- package/dist/chat/tools/manual.js.map +1 -0
- package/dist/chat/tools/reader.d.ts +80 -0
- package/dist/chat/tools/reader.d.ts.map +1 -0
- package/dist/chat/tools/reader.js +471 -0
- package/dist/chat/tools/reader.js.map +1 -0
- package/dist/chat/tools.d.ts +106 -0
- package/dist/chat/tools.d.ts.map +1 -0
- package/dist/chat/tools.js +587 -0
- package/dist/chat/tools.js.map +1 -0
- package/dist/codegen/e2e.d.ts +106 -0
- package/dist/codegen/e2e.d.ts.map +1 -0
- package/dist/codegen/e2e.js +931 -0
- package/dist/codegen/e2e.js.map +1 -0
- package/dist/codegen/v3_flow_emit.d.ts +70 -0
- package/dist/codegen/v3_flow_emit.d.ts.map +1 -0
- package/dist/codegen/v3_flow_emit.js +225 -0
- package/dist/codegen/v3_flow_emit.js.map +1 -0
- package/dist/commands/_stub.d.ts +2 -0
- package/dist/commands/_stub.d.ts.map +1 -0
- package/dist/commands/_stub.js +21 -0
- package/dist/commands/_stub.js.map +1 -0
- package/dist/commands/app.d.ts +31 -0
- package/dist/commands/app.d.ts.map +1 -0
- package/dist/commands/app.js +331 -0
- package/dist/commands/app.js.map +1 -0
- package/dist/commands/chat.d.ts +18 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +76 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/deploy.d.ts +21 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +121 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/doctor.d.ts +14 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +280 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/figma.d.ts +32 -0
- package/dist/commands/figma.d.ts.map +1 -0
- package/dist/commands/figma.js +141 -0
- package/dist/commands/figma.js.map +1 -0
- package/dist/commands/gen-flow-tests.d.ts +8 -0
- package/dist/commands/gen-flow-tests.d.ts.map +1 -0
- package/dist/commands/gen-flow-tests.js +78 -0
- package/dist/commands/gen-flow-tests.js.map +1 -0
- package/dist/commands/gen-tests.d.ts +9 -0
- package/dist/commands/gen-tests.d.ts.map +1 -0
- package/dist/commands/gen-tests.js +118 -0
- package/dist/commands/gen-tests.js.map +1 -0
- package/dist/commands/license.d.ts +14 -0
- package/dist/commands/license.d.ts.map +1 -0
- package/dist/commands/license.js +182 -0
- package/dist/commands/license.js.map +1 -0
- package/dist/commands/log.d.ts +19 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +101 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/migrate.d.ts +118 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +1410 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/mobile.d.ts +27 -0
- package/dist/commands/mobile.d.ts.map +1 -0
- package/dist/commands/mobile.js +90 -0
- package/dist/commands/mobile.js.map +1 -0
- package/dist/commands/new.d.ts +32 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +107 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/pilot.d.ts +8 -0
- package/dist/commands/pilot.d.ts.map +1 -0
- package/dist/commands/pilot.js +104 -0
- package/dist/commands/pilot.js.map +1 -0
- package/dist/commands/projects.d.ts +21 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +238 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/publish.d.ts +35 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +194 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/repo.d.ts +59 -0
- package/dist/commands/repo.d.ts.map +1 -0
- package/dist/commands/repo.js +178 -0
- package/dist/commands/repo.js.map +1 -0
- package/dist/commands/review-screens.d.ts +28 -0
- package/dist/commands/review-screens.d.ts.map +1 -0
- package/dist/commands/review-screens.js +345 -0
- package/dist/commands/review-screens.js.map +1 -0
- package/dist/commands/scenarios.d.ts +23 -0
- package/dist/commands/scenarios.d.ts.map +1 -0
- package/dist/commands/scenarios.js +304 -0
- package/dist/commands/scenarios.js.map +1 -0
- package/dist/commands/ship.d.ts +18 -0
- package/dist/commands/ship.d.ts.map +1 -0
- package/dist/commands/ship.js +41 -0
- package/dist/commands/ship.js.map +1 -0
- package/dist/commands/test.d.ts +29 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +62 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/tunnel.d.ts +22 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +77 -0
- package/dist/commands/tunnel.js.map +1 -0
- package/dist/commands/validate.d.ts +14 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +51 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/vault.d.ts +32 -0
- package/dist/commands/vault.d.ts.map +1 -0
- package/dist/commands/vault.js +489 -0
- package/dist/commands/vault.js.map +1 -0
- package/dist/commands/voice.d.ts +16 -0
- package/dist/commands/voice.d.ts.map +1 -0
- package/dist/commands/voice.js +69 -0
- package/dist/commands/voice.js.map +1 -0
- package/dist/core/cascade_router.d.ts +90 -0
- package/dist/core/cascade_router.d.ts.map +1 -0
- package/dist/core/cascade_router.js +131 -0
- package/dist/core/cascade_router.js.map +1 -0
- package/dist/core/cf_tunnel.d.ts +52 -0
- package/dist/core/cf_tunnel.d.ts.map +1 -0
- package/dist/core/cf_tunnel.js +134 -0
- package/dist/core/cf_tunnel.js.map +1 -0
- package/dist/core/gha_dispatcher.d.ts +48 -0
- package/dist/core/gha_dispatcher.d.ts.map +1 -0
- package/dist/core/gha_dispatcher.js +198 -0
- package/dist/core/gha_dispatcher.js.map +1 -0
- package/dist/core/logger.d.ts +89 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +245 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/mode.d.ts +26 -0
- package/dist/core/mode.d.ts.map +1 -0
- package/dist/core/mode.js +122 -0
- package/dist/core/mode.js.map +1 -0
- package/dist/core/pairing.d.ts +40 -0
- package/dist/core/pairing.d.ts.map +1 -0
- package/dist/core/pairing.js +145 -0
- package/dist/core/pairing.js.map +1 -0
- package/dist/core/pilot_setup.d.ts +29 -0
- package/dist/core/pilot_setup.d.ts.map +1 -0
- package/dist/core/pilot_setup.js +119 -0
- package/dist/core/pilot_setup.js.map +1 -0
- package/dist/core/polar.d.ts +81 -0
- package/dist/core/polar.d.ts.map +1 -0
- package/dist/core/polar.js +175 -0
- package/dist/core/polar.js.map +1 -0
- package/dist/core/project_picker.d.ts +56 -0
- package/dist/core/project_picker.d.ts.map +1 -0
- package/dist/core/project_picker.js +86 -0
- package/dist/core/project_picker.js.map +1 -0
- package/dist/core/projects.d.ts +58 -0
- package/dist/core/projects.d.ts.map +1 -0
- package/dist/core/projects.js +146 -0
- package/dist/core/projects.js.map +1 -0
- package/dist/core/projects_sync.d.ts +80 -0
- package/dist/core/projects_sync.d.ts.map +1 -0
- package/dist/core/projects_sync.js +278 -0
- package/dist/core/projects_sync.js.map +1 -0
- package/dist/core/remote_runner.d.ts +70 -0
- package/dist/core/remote_runner.d.ts.map +1 -0
- package/dist/core/remote_runner.js +133 -0
- package/dist/core/remote_runner.js.map +1 -0
- package/dist/core/repo_state.d.ts +24 -0
- package/dist/core/repo_state.d.ts.map +1 -0
- package/dist/core/repo_state.js +109 -0
- package/dist/core/repo_state.js.map +1 -0
- package/dist/core/target.d.ts +31 -0
- package/dist/core/target.d.ts.map +1 -0
- package/dist/core/target.js +121 -0
- package/dist/core/target.js.map +1 -0
- package/dist/deploy/aws.d.ts +43 -0
- package/dist/deploy/aws.d.ts.map +1 -0
- package/dist/deploy/aws.js +173 -0
- package/dist/deploy/aws.js.map +1 -0
- package/dist/figma/api.d.ts +35 -0
- package/dist/figma/api.d.ts.map +1 -0
- package/dist/figma/api.js +40 -0
- package/dist/figma/api.js.map +1 -0
- package/dist/figma/decorator.d.ts +74 -0
- package/dist/figma/decorator.d.ts.map +1 -0
- package/dist/figma/decorator.js +210 -0
- package/dist/figma/decorator.js.map +1 -0
- package/dist/figma/heuristics.d.ts +29 -0
- package/dist/figma/heuristics.d.ts.map +1 -0
- package/dist/figma/heuristics.js +110 -0
- package/dist/figma/heuristics.js.map +1 -0
- package/dist/figma/normalize.d.ts +33 -0
- package/dist/figma/normalize.d.ts.map +1 -0
- package/dist/figma/normalize.js +101 -0
- package/dist/figma/normalize.js.map +1 -0
- package/dist/figma/tokens.d.ts +23 -0
- package/dist/figma/tokens.d.ts.map +1 -0
- package/dist/figma/tokens.js +111 -0
- package/dist/figma/tokens.js.map +1 -0
- package/dist/figma/types.d.ts +118 -0
- package/dist/figma/types.d.ts.map +1 -0
- package/dist/figma/types.js +12 -0
- package/dist/figma/types.js.map +1 -0
- package/dist/i18n/index.d.ts +48 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +135 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +52 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +85 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lan/mdns_packet.d.ts +74 -0
- package/dist/lan/mdns_packet.d.ts.map +1 -0
- package/dist/lan/mdns_packet.js +247 -0
- package/dist/lan/mdns_packet.js.map +1 -0
- package/dist/lan/mdns_service.d.ts +102 -0
- package/dist/lan/mdns_service.d.ts.map +1 -0
- package/dist/lan/mdns_service.js +206 -0
- package/dist/lan/mdns_service.js.map +1 -0
- package/dist/license/activate.d.ts +33 -0
- package/dist/license/activate.d.ts.map +1 -0
- package/dist/license/activate.js +135 -0
- package/dist/license/activate.js.map +1 -0
- package/dist/license/fingerprint.d.ts +2 -0
- package/dist/license/fingerprint.d.ts.map +1 -0
- package/dist/license/fingerprint.js +29 -0
- package/dist/license/fingerprint.js.map +1 -0
- package/dist/license/hito4_client.d.ts +24 -0
- package/dist/license/hito4_client.d.ts.map +1 -0
- package/dist/license/hito4_client.js +103 -0
- package/dist/license/hito4_client.js.map +1 -0
- package/dist/license/index.d.ts +22 -0
- package/dist/license/index.d.ts.map +1 -0
- package/dist/license/index.js +125 -0
- package/dist/license/index.js.map +1 -0
- package/dist/license/types.d.ts +38 -0
- package/dist/license/types.d.ts.map +1 -0
- package/dist/license/types.js +9 -0
- package/dist/license/types.js.map +1 -0
- package/dist/migrate/ai-apply.d.ts +198 -0
- package/dist/migrate/ai-apply.d.ts.map +1 -0
- package/dist/migrate/ai-apply.js +833 -0
- package/dist/migrate/ai-apply.js.map +1 -0
- package/dist/migrate/ai-decorator.d.ts +87 -0
- package/dist/migrate/ai-decorator.d.ts.map +1 -0
- package/dist/migrate/ai-decorator.js +203 -0
- package/dist/migrate/ai-decorator.js.map +1 -0
- package/dist/migrate/apply.d.ts +28 -0
- package/dist/migrate/apply.d.ts.map +1 -0
- package/dist/migrate/apply.js +119 -0
- package/dist/migrate/apply.js.map +1 -0
- package/dist/migrate/audit.d.ts +9 -0
- package/dist/migrate/audit.d.ts.map +1 -0
- package/dist/migrate/audit.js +197 -0
- package/dist/migrate/audit.js.map +1 -0
- package/dist/migrate/diff.d.ts +28 -0
- package/dist/migrate/diff.d.ts.map +1 -0
- package/dist/migrate/diff.js +154 -0
- package/dist/migrate/diff.js.map +1 -0
- package/dist/migrate/html-orchestrator.d.ts +81 -0
- package/dist/migrate/html-orchestrator.d.ts.map +1 -0
- package/dist/migrate/html-orchestrator.js +233 -0
- package/dist/migrate/html-orchestrator.js.map +1 -0
- package/dist/migrate/html-walker.d.ts +93 -0
- package/dist/migrate/html-walker.d.ts.map +1 -0
- package/dist/migrate/html-walker.js +288 -0
- package/dist/migrate/html-walker.js.map +1 -0
- package/dist/migrate/js-template-walker.d.ts +118 -0
- package/dist/migrate/js-template-walker.d.ts.map +1 -0
- package/dist/migrate/js-template-walker.js +644 -0
- package/dist/migrate/js-template-walker.js.map +1 -0
- package/dist/migrate/manifest-validator.d.ts +30 -0
- package/dist/migrate/manifest-validator.d.ts.map +1 -0
- package/dist/migrate/manifest-validator.js +261 -0
- package/dist/migrate/manifest-validator.js.map +1 -0
- package/dist/migrate/overrides.d.ts +58 -0
- package/dist/migrate/overrides.d.ts.map +1 -0
- package/dist/migrate/overrides.js +193 -0
- package/dist/migrate/overrides.js.map +1 -0
- package/dist/migrate/plugin-scope.d.ts +42 -0
- package/dist/migrate/plugin-scope.d.ts.map +1 -0
- package/dist/migrate/plugin-scope.js +94 -0
- package/dist/migrate/plugin-scope.js.map +1 -0
- package/dist/migrate/types.d.ts +45 -0
- package/dist/migrate/types.d.ts.map +1 -0
- package/dist/migrate/types.js +9 -0
- package/dist/migrate/types.js.map +1 -0
- package/dist/migrate/verb-inference.d.ts +37 -0
- package/dist/migrate/verb-inference.d.ts.map +1 -0
- package/dist/migrate/verb-inference.js +274 -0
- package/dist/migrate/verb-inference.js.map +1 -0
- package/dist/nac3/attrs.d.ts +87 -0
- package/dist/nac3/attrs.d.ts.map +1 -0
- package/dist/nac3/attrs.js +134 -0
- package/dist/nac3/attrs.js.map +1 -0
- package/dist/nac3/scenario_dsl.d.ts +71 -0
- package/dist/nac3/scenario_dsl.d.ts.map +1 -0
- package/dist/nac3/scenario_dsl.js +191 -0
- package/dist/nac3/scenario_dsl.js.map +1 -0
- package/dist/nac3/tokens.d.ts +126 -0
- package/dist/nac3/tokens.d.ts.map +1 -0
- package/dist/nac3/tokens.js +138 -0
- package/dist/nac3/tokens.js.map +1 -0
- package/dist/reader/parsers/csv.d.ts +42 -0
- package/dist/reader/parsers/csv.d.ts.map +1 -0
- package/dist/reader/parsers/csv.js +221 -0
- package/dist/reader/parsers/csv.js.map +1 -0
- package/dist/reader/parsers/docx.d.ts +31 -0
- package/dist/reader/parsers/docx.d.ts.map +1 -0
- package/dist/reader/parsers/docx.js +51 -0
- package/dist/reader/parsers/docx.js.map +1 -0
- package/dist/reader/parsers/epub.d.ts +39 -0
- package/dist/reader/parsers/epub.d.ts.map +1 -0
- package/dist/reader/parsers/epub.js +265 -0
- package/dist/reader/parsers/epub.js.map +1 -0
- package/dist/reader/parsers/html.d.ts +40 -0
- package/dist/reader/parsers/html.d.ts.map +1 -0
- package/dist/reader/parsers/html.js +386 -0
- package/dist/reader/parsers/html.js.map +1 -0
- package/dist/reader/parsers/md.d.ts +30 -0
- package/dist/reader/parsers/md.d.ts.map +1 -0
- package/dist/reader/parsers/md.js +199 -0
- package/dist/reader/parsers/md.js.map +1 -0
- package/dist/reader/parsers/pdf.d.ts +39 -0
- package/dist/reader/parsers/pdf.d.ts.map +1 -0
- package/dist/reader/parsers/pdf.js +220 -0
- package/dist/reader/parsers/pdf.js.map +1 -0
- package/dist/reader/parsers/rtf.d.ts +37 -0
- package/dist/reader/parsers/rtf.d.ts.map +1 -0
- package/dist/reader/parsers/rtf.js +347 -0
- package/dist/reader/parsers/rtf.js.map +1 -0
- package/dist/reader/parsers/source.d.ts +32 -0
- package/dist/reader/parsers/source.d.ts.map +1 -0
- package/dist/reader/parsers/source.js +122 -0
- package/dist/reader/parsers/source.js.map +1 -0
- package/dist/reader/parsers/txt.d.ts +25 -0
- package/dist/reader/parsers/txt.d.ts.map +1 -0
- package/dist/reader/parsers/txt.js +56 -0
- package/dist/reader/parsers/txt.js.map +1 -0
- package/dist/reader/parsers/xlsx.d.ts +33 -0
- package/dist/reader/parsers/xlsx.d.ts.map +1 -0
- package/dist/reader/parsers/xlsx.js +143 -0
- package/dist/reader/parsers/xlsx.js.map +1 -0
- package/dist/reader/registry.d.ts +39 -0
- package/dist/reader/registry.d.ts.map +1 -0
- package/dist/reader/registry.js +172 -0
- package/dist/reader/registry.js.map +1 -0
- package/dist/reader/search.d.ts +27 -0
- package/dist/reader/search.d.ts.map +1 -0
- package/dist/reader/search.js +77 -0
- package/dist/reader/search.js.map +1 -0
- package/dist/reader/state.d.ts +56 -0
- package/dist/reader/state.d.ts.map +1 -0
- package/dist/reader/state.js +179 -0
- package/dist/reader/state.js.map +1 -0
- package/dist/reader/types.d.ts +119 -0
- package/dist/reader/types.d.ts.map +1 -0
- package/dist/reader/types.js +23 -0
- package/dist/reader/types.js.map +1 -0
- package/dist/ship/run.d.ts +26 -0
- package/dist/ship/run.d.ts.map +1 -0
- package/dist/ship/run.js +123 -0
- package/dist/ship/run.js.map +1 -0
- package/dist/template/index.d.ts +50 -0
- package/dist/template/index.d.ts.map +1 -0
- package/dist/template/index.js +140 -0
- package/dist/template/index.js.map +1 -0
- package/dist/ui/colors.d.ts +13 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +26 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/validate/index.d.ts +19 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +181 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/vault/catalog.d.ts +55 -0
- package/dist/vault/catalog.d.ts.map +1 -0
- package/dist/vault/catalog.js +424 -0
- package/dist/vault/catalog.js.map +1 -0
- package/dist/vault/crypto.d.ts +82 -0
- package/dist/vault/crypto.d.ts.map +1 -0
- package/dist/vault/crypto.js +173 -0
- package/dist/vault/crypto.js.map +1 -0
- package/dist/vault/git_askpass.d.ts +26 -0
- package/dist/vault/git_askpass.d.ts.map +1 -0
- package/dist/vault/git_askpass.js +104 -0
- package/dist/vault/git_askpass.js.map +1 -0
- package/dist/vault/migrator.d.ts +57 -0
- package/dist/vault/migrator.d.ts.map +1 -0
- package/dist/vault/migrator.js +204 -0
- package/dist/vault/migrator.js.map +1 -0
- package/dist/vault/redactor.d.ts +73 -0
- package/dist/vault/redactor.d.ts.map +1 -0
- package/dist/vault/redactor.js +182 -0
- package/dist/vault/redactor.js.map +1 -0
- package/dist/vault/store.d.ts +132 -0
- package/dist/vault/store.d.ts.map +1 -0
- package/dist/vault/store.js +335 -0
- package/dist/vault/store.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/dist/voice/chunker.d.ts +43 -0
- package/dist/voice/chunker.d.ts.map +1 -0
- package/dist/voice/chunker.js +133 -0
- package/dist/voice/chunker.js.map +1 -0
- package/dist/voice/config.d.ts +14 -0
- package/dist/voice/config.d.ts.map +1 -0
- package/dist/voice/config.js +51 -0
- package/dist/voice/config.js.map +1 -0
- package/dist/voice/intents.d.ts +71 -0
- package/dist/voice/intents.d.ts.map +1 -0
- package/dist/voice/intents.js +0 -0
- package/dist/voice/intents.js.map +1 -0
- package/dist/voice/providers/elevenlabs.d.ts +53 -0
- package/dist/voice/providers/elevenlabs.d.ts.map +1 -0
- package/dist/voice/providers/elevenlabs.js +159 -0
- package/dist/voice/providers/elevenlabs.js.map +1 -0
- package/dist/voice/providers/google.d.ts +56 -0
- package/dist/voice/providers/google.d.ts.map +1 -0
- package/dist/voice/providers/google.js +253 -0
- package/dist/voice/providers/google.js.map +1 -0
- package/dist/voice/providers/whisper.d.ts +44 -0
- package/dist/voice/providers/whisper.d.ts.map +1 -0
- package/dist/voice/providers/whisper.js +179 -0
- package/dist/voice/providers/whisper.js.map +1 -0
- package/dist/voice/registry.d.ts +35 -0
- package/dist/voice/registry.d.ts.map +1 -0
- package/dist/voice/registry.js +48 -0
- package/dist/voice/registry.js.map +1 -0
- package/dist/voice/router.d.ts +62 -0
- package/dist/voice/router.d.ts.map +1 -0
- package/dist/voice/router.js +175 -0
- package/dist/voice/router.js.map +1 -0
- package/dist/voice/types.d.ts +116 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +22 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/voiceprint/enrollment.d.ts +36 -0
- package/dist/voice/voiceprint/enrollment.d.ts.map +1 -0
- package/dist/voice/voiceprint/enrollment.js +71 -0
- package/dist/voice/voiceprint/enrollment.js.map +1 -0
- package/dist/voice/voiceprint/identify.d.ts +16 -0
- package/dist/voice/voiceprint/identify.d.ts.map +1 -0
- package/dist/voice/voiceprint/identify.js +20 -0
- package/dist/voice/voiceprint/identify.js.map +1 -0
- package/dist/voice/voiceprint/liveness.d.ts +90 -0
- package/dist/voice/voiceprint/liveness.d.ts.map +1 -0
- package/dist/voice/voiceprint/liveness.js +251 -0
- package/dist/voice/voiceprint/liveness.js.map +1 -0
- package/dist/voice/voiceprint/match.d.ts +54 -0
- package/dist/voice/voiceprint/match.d.ts.map +1 -0
- package/dist/voice/voiceprint/match.js +88 -0
- package/dist/voice/voiceprint/match.js.map +1 -0
- package/dist/voice/voiceprint/providers/local-mfcc-stub.d.ts +44 -0
- package/dist/voice/voiceprint/providers/local-mfcc-stub.d.ts.map +1 -0
- package/dist/voice/voiceprint/providers/local-mfcc-stub.js +92 -0
- package/dist/voice/voiceprint/providers/local-mfcc-stub.js.map +1 -0
- package/dist/voice/voiceprint/store.d.ts +60 -0
- package/dist/voice/voiceprint/store.d.ts.map +1 -0
- package/dist/voice/voiceprint/store.js +155 -0
- package/dist/voice/voiceprint/store.js.map +1 -0
- package/dist/voice/voiceprint/trust.d.ts +90 -0
- package/dist/voice/voiceprint/trust.d.ts.map +1 -0
- package/dist/voice/voiceprint/trust.js +150 -0
- package/dist/voice/voiceprint/trust.js.map +1 -0
- package/dist/voice/voiceprint/types.d.ts +100 -0
- package/dist/voice/voiceprint/types.d.ts.map +1 -0
- package/dist/voice/voiceprint/types.js +23 -0
- package/dist/voice/voiceprint/types.js.map +1 -0
- package/dist/voice/wake.d.ts +64 -0
- package/dist/voice/wake.d.ts.map +1 -0
- package/dist/voice/wake.js +143 -0
- package/dist/voice/wake.js.map +1 -0
- package/docs/manuals/manual.ar.html +91 -0
- package/docs/manuals/manual.de.html +100 -0
- package/docs/manuals/manual.en.html +118 -0
- package/docs/manuals/manual.es.html +120 -0
- package/docs/manuals/manual.fr.html +102 -0
- package/docs/manuals/manual.hi.html +93 -0
- package/docs/manuals/manual.it.html +93 -0
- package/docs/manuals/manual.ja.html +97 -0
- package/docs/manuals/manual.pt.html +103 -0
- package/docs/manuals/manual.zh.html +89 -0
- package/package.json +94 -0
- package/src/i18n/catalogs/ar.json +86 -0
- package/src/i18n/catalogs/de.json +86 -0
- package/src/i18n/catalogs/en.json +86 -0
- package/src/i18n/catalogs/es.json +86 -0
- package/src/i18n/catalogs/fr.json +86 -0
- package/src/i18n/catalogs/hi.json +86 -0
- package/src/i18n/catalogs/it.json +86 -0
- package/src/i18n/catalogs/ja.json +86 -0
- package/src/i18n/catalogs/pt.json +86 -0
- package/src/i18n/catalogs/zh.json +86 -0
- package/templates/react-app/README.md +43 -0
- package/templates/react-app/index.html +12 -0
- package/templates/react-app/package.json +35 -0
- package/templates/react-app/src/App.tsx +106 -0
- package/templates/react-app/src/main.tsx +21 -0
- package/templates/react-app/src/nac/manifest.ts +46 -0
- package/templates/react-app/src/styles.css +68 -0
- package/templates/react-app/tsconfig.json +19 -0
- package/templates/react-app/vite.config.ts +12 -0
- package/templates/react-app/yujin.forge.json +15 -0
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { c, header } from '../ui/colors.js';
|
|
5
|
+
import { auditProject } from '../migrate/audit.js';
|
|
6
|
+
import { applyMigration } from '../migrate/apply.js';
|
|
7
|
+
import { runHtmlSilent, prepareHtmlAssisted, finalizeHtmlAssisted, } from '../migrate/html-orchestrator.js';
|
|
8
|
+
import { aiDecorateFile } from '../migrate/ai-decorator.js';
|
|
9
|
+
import { aiApplyProject } from '../migrate/ai-apply.js';
|
|
10
|
+
import { readOverrides, writeOverrides, computeBaselineHash, applyOverrides, searchManifest, OVERRIDES_VERSION, } from '../migrate/overrides.js';
|
|
11
|
+
import { unifiedDiff } from '../migrate/diff.js';
|
|
12
|
+
import { loadLicense, isPaidSeat } from '../license/index.js';
|
|
13
|
+
export function registerMigrateCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('migrate <repo>')
|
|
16
|
+
.description('Audit or apply a NAC-3 migration to an existing project (React or HTML).')
|
|
17
|
+
.option('--audit', 'produce a migration report without mutating files (default)', true)
|
|
18
|
+
.option('--apply', 'execute the proposed AST migration on .tsx/.jsx (requires paid seat)')
|
|
19
|
+
.option('--html', 'silent HTML walker -- auto-apply on .html files (paid seat)')
|
|
20
|
+
.option('--assisted', 'HTML walker with CLI prompts for ambiguous elements (paid seat)')
|
|
21
|
+
.option('--ai-silent', 'AI-powered (Claude) silent decoration of an HTML file (paid seat)')
|
|
22
|
+
.option('--ai-assisted', 'AI-powered (Claude) decoration with CLI prompts on ambiguity (paid seat)')
|
|
23
|
+
.option('--ai-model <id>', 'Anthropic model id (default: claude-sonnet-4-6)')
|
|
24
|
+
.option('--ai-hint <text>', 'Page hint passed to Claude (e.g. "calculator app")')
|
|
25
|
+
.option('--ai-out <dir>', 'Output directory for decorated HTML + manifest (default: <repo>-ai-<mode>)')
|
|
26
|
+
.option('--with-pilot', 'inject Yujin Pilot bundle into the decorated output')
|
|
27
|
+
.option('--ai-search <query>', 'search the active manifest in <repo>/manifest.json for elements matching <query>')
|
|
28
|
+
.option('--ai-override <nacId>', 'interactively re-decorate one element (writes to overrides.json + patches index.html + manifest.json)')
|
|
29
|
+
.option('--no-overrides', 'skip applying overrides.json on a --ai-silent re-run')
|
|
30
|
+
.option('--ai-apply', 'AI-powered JSX decoration: walk JSX/TSX files in <repo>/<subdir>, decorate each, aggregate manifest (paid seat)')
|
|
31
|
+
.option('--ai-plugin-slug <s>', 'plugin slug hint for --ai-apply (default: model derives from project)')
|
|
32
|
+
.option('--ai-concurrency <n>', 'parallel LLM calls during --ai-apply (default 4, max 8)')
|
|
33
|
+
.option('--with-i18n-tags', 'also emit data-i18n="<key>" on user-facing text nodes + write i18n/<lang>.json catalogs (paid seat extra to --ai-apply)')
|
|
34
|
+
.option('--i18n-langs <list>', 'comma-separated locales for --with-i18n-tags (default: en,es,pt,fr,it,de,ja,zh,hi,ar)')
|
|
35
|
+
.option('--multi-page', 'with --ai-silent / --ai-assisted on a directory, decorate every .html file (not just index.html); shared plugin_slug across pages')
|
|
36
|
+
.option('--dry-run', 'with --apply / --html / --assisted / --ai-silent / --ai-assisted, compute edits without writing files (prints unified diff for ai modes)')
|
|
37
|
+
.option('-y, --yes', 'skip confirmation prompts (CI mode)')
|
|
38
|
+
.option('-o, --out <path>', 'write the audit report to a JSON file')
|
|
39
|
+
.option('--subdir <name>', 'subdirectory to scan under <repo> (default: src)', 'src')
|
|
40
|
+
.option('--threshold <num>', 'confidence threshold for silent auto-apply (default 0.6)')
|
|
41
|
+
.action(async (repo, opts) => {
|
|
42
|
+
if (opts.aiSearch) {
|
|
43
|
+
await runAiSearch(repo, opts);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (opts.aiOverride) {
|
|
47
|
+
await runAiOverride(repo, opts);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (opts.aiApply) {
|
|
51
|
+
await runAiApply(repo, opts);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (opts.aiAssisted) {
|
|
55
|
+
if (opts.multiPage)
|
|
56
|
+
await runAiHtmlMultiPage(repo, opts, 'assisted');
|
|
57
|
+
else
|
|
58
|
+
await runAiHtml(repo, opts, 'assisted');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (opts.aiSilent) {
|
|
62
|
+
if (opts.multiPage)
|
|
63
|
+
await runAiHtmlMultiPage(repo, opts, 'silent');
|
|
64
|
+
else
|
|
65
|
+
await runAiHtml(repo, opts, 'silent');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (opts.assisted) {
|
|
69
|
+
await runAssistedHtml(repo, opts);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (opts.html) {
|
|
73
|
+
await runSilentHtml(repo, opts);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (opts.apply) {
|
|
77
|
+
await runApply(repo, opts);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await runAudit(repo, opts);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function runAudit(repo, opts) {
|
|
84
|
+
header('Yujin Forge -- migration audit');
|
|
85
|
+
console.log('');
|
|
86
|
+
const projectRoot = path.resolve(process.cwd(), repo);
|
|
87
|
+
let report;
|
|
88
|
+
try {
|
|
89
|
+
report = await auditProject({ projectRoot, scanSubdir: opts.subdir });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.error(c.error(err instanceof Error ? err.message : String(err)));
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(' Project: ' + c.dim(projectRoot));
|
|
97
|
+
console.log(' Files scanned: ' + report.scanned_files);
|
|
98
|
+
console.log(' Candidates: ' + report.summary.total);
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(' ' + c.brand('action ') + ' ' + report.summary.actions);
|
|
101
|
+
console.log(' ' + c.brand('field ') + ' ' + report.summary.fields);
|
|
102
|
+
console.log(' ' + c.brand('region ') + ' ' + report.summary.regions);
|
|
103
|
+
console.log(' ' + c.dim('already tagged ') + report.summary.already_tagged);
|
|
104
|
+
console.log('');
|
|
105
|
+
if (opts.out) {
|
|
106
|
+
const outPath = path.resolve(process.cwd(), opts.out);
|
|
107
|
+
await fs.writeFile(outPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
108
|
+
console.log(c.success('Wrote report to ' + outPath));
|
|
109
|
+
}
|
|
110
|
+
else if (report.candidates.length > 0) {
|
|
111
|
+
const preview = report.candidates.slice(0, 10);
|
|
112
|
+
console.log(c.dim('First ' + preview.length + ' candidate(s):'));
|
|
113
|
+
for (const cand of preview) {
|
|
114
|
+
const tag = cand.already_tagged ? c.dim('[ok] ') : '';
|
|
115
|
+
console.log(' ' + tag + cand.file + ':' + cand.line + ' '
|
|
116
|
+
+ c.brand(cand.kind) + ' ' + c.dim(cand.element + ' -> ' + cand.proposed_id));
|
|
117
|
+
}
|
|
118
|
+
if (report.candidates.length > preview.length) {
|
|
119
|
+
console.log(c.dim(' ... + ' + (report.candidates.length - preview.length) + ' more (use --out to dump JSON)'));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (report.summary.total === 0) {
|
|
123
|
+
console.log(c.success('Nothing to migrate -- project already looks NAC-3 clean.'));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const todo = report.summary.total - report.summary.already_tagged;
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(c.dim(todo + ' element(s) need a data-nac-id. Run ')
|
|
129
|
+
+ c.code('yf migrate ' + repo + ' --apply')
|
|
130
|
+
+ c.dim(' (paid seat) to add them.'));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export async function runApply(repo, opts) {
|
|
134
|
+
header('Yujin Forge -- migration apply' + (opts.dryRun ? ' (dry run)' : ''));
|
|
135
|
+
console.log('');
|
|
136
|
+
const lic = await loadLicense();
|
|
137
|
+
if (!isPaidSeat(lic)) {
|
|
138
|
+
console.error(c.error('migrate --apply requires a paid seat.'));
|
|
139
|
+
console.error(' ' + c.dim('Trial mode produces the audit only. Run ')
|
|
140
|
+
+ c.code('yf license activate --key <jwt>')
|
|
141
|
+
+ c.dim(' to install a seat.'));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const projectRoot = path.resolve(process.cwd(), repo);
|
|
146
|
+
try {
|
|
147
|
+
const result = await applyMigration({
|
|
148
|
+
projectRoot,
|
|
149
|
+
scanSubdir: opts.subdir,
|
|
150
|
+
dryRun: !!opts.dryRun,
|
|
151
|
+
});
|
|
152
|
+
console.log(' Project: ' + c.dim(projectRoot));
|
|
153
|
+
console.log(' Files scanned: ' + result.report.scanned_files);
|
|
154
|
+
console.log(' Files edited: ' + result.edited_files.length);
|
|
155
|
+
console.log(' Skipped (already tagged): ' + result.skipped_already_tagged);
|
|
156
|
+
console.log('');
|
|
157
|
+
for (const e of result.edited_files) {
|
|
158
|
+
console.log(' ' + c.success('+ ') + e.file + c.dim(' (' + e.inserts + ' insert(s))'));
|
|
159
|
+
}
|
|
160
|
+
console.log('');
|
|
161
|
+
if (opts.dryRun) {
|
|
162
|
+
console.log(c.warn('Dry run -- no files were written.'));
|
|
163
|
+
console.log(c.dim('Re-run without --dry-run to apply.'));
|
|
164
|
+
}
|
|
165
|
+
else if (result.edited_files.length === 0) {
|
|
166
|
+
console.log(c.success('Nothing to do -- project is already NAC-3 clean.'));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.log(c.success('Migration applied. Run your test suite to verify.'));
|
|
170
|
+
}
|
|
171
|
+
if (opts.out) {
|
|
172
|
+
const outPath = path.resolve(process.cwd(), opts.out);
|
|
173
|
+
await fs.writeFile(outPath, JSON.stringify(result.report, null, 2), 'utf-8');
|
|
174
|
+
console.log(c.dim('Audit report written to ' + outPath));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.error(c.error(err instanceof Error ? err.message : String(err)));
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/* ============================================================
|
|
183
|
+
HTML walker -- silent + assisted modes (paso 4 of the V24-04
|
|
184
|
+
benchmark roadmap; eventual destination of the auto-decoration
|
|
185
|
+
product layer cited in SPEC.md sec 9.)
|
|
186
|
+
============================================================ */
|
|
187
|
+
export async function runSilentHtml(repo, opts) {
|
|
188
|
+
header('Yujin Forge -- HTML walker (silent)' + (opts.dryRun ? ' (dry run)' : ''));
|
|
189
|
+
console.log('');
|
|
190
|
+
const lic = await loadLicense();
|
|
191
|
+
if (!isPaidSeat(lic)) {
|
|
192
|
+
console.error(c.error('migrate --html requires a paid seat.'));
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const inputPath = path.resolve(process.cwd(), repo);
|
|
197
|
+
const threshold = opts.threshold ? Number(opts.threshold) : 0.6;
|
|
198
|
+
try {
|
|
199
|
+
const result = await runHtmlSilent({
|
|
200
|
+
inputPath,
|
|
201
|
+
confidenceThreshold: threshold,
|
|
202
|
+
dryRun: !!opts.dryRun,
|
|
203
|
+
});
|
|
204
|
+
console.log(' Input: ' + c.dim(inputPath));
|
|
205
|
+
console.log(' Files walked: ' + result.files.length);
|
|
206
|
+
console.log(' Total applied: ' + result.total_applied);
|
|
207
|
+
console.log(' Skipped low-conf: ' + result.total_skipped_low_conf);
|
|
208
|
+
console.log(' Already decorated: ' + result.total_already_decorated);
|
|
209
|
+
console.log(' Decisions persisted: ' + result.decisions_added);
|
|
210
|
+
console.log('');
|
|
211
|
+
for (const f of result.files) {
|
|
212
|
+
const rel = path.relative(process.cwd(), f.filePath);
|
|
213
|
+
const tag = f.wrote ? c.success('+ ') : (opts.dryRun ? c.warn('~ ') : c.dim('. '));
|
|
214
|
+
console.log(' ' + tag + rel + c.dim(' (' + f.applied.length + ' applied, '
|
|
215
|
+
+ f.skipped_low_confidence.length + ' skipped, '
|
|
216
|
+
+ f.skipped_already_decorated + ' already)'));
|
|
217
|
+
}
|
|
218
|
+
if (opts.dryRun) {
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(c.warn('Dry run -- no files were written.'));
|
|
221
|
+
}
|
|
222
|
+
else if (result.total_applied === 0) {
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(c.success('Nothing to apply.'));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(c.success('HTML decoration applied. Decisions saved to .yujin/nac-decisions.json'));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
console.error(c.error(err instanceof Error ? err.message : String(err)));
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export async function runAssistedHtml(repo, opts) {
|
|
237
|
+
header('Yujin Forge -- HTML walker (assisted)' + (opts.dryRun ? ' (dry run)' : ''));
|
|
238
|
+
console.log('');
|
|
239
|
+
const lic = await loadLicense();
|
|
240
|
+
if (!isPaidSeat(lic)) {
|
|
241
|
+
console.error(c.error('migrate --assisted requires a paid seat.'));
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const inputPath = path.resolve(process.cwd(), repo);
|
|
246
|
+
const threshold = opts.threshold ? Number(opts.threshold) : 0.85;
|
|
247
|
+
try {
|
|
248
|
+
const queue = await prepareHtmlAssisted({
|
|
249
|
+
inputPath,
|
|
250
|
+
confidenceThreshold: threshold,
|
|
251
|
+
dryRun: !!opts.dryRun,
|
|
252
|
+
});
|
|
253
|
+
console.log(' Input: ' + c.dim(inputPath));
|
|
254
|
+
console.log(' Files walked: ' + queue.walkResults.length);
|
|
255
|
+
console.log(' Auto-apply (>= ' + threshold + '): ' + queue.auto_apply.length);
|
|
256
|
+
console.log(' Need review: ' + queue.items.filter(i => !i.prior).length);
|
|
257
|
+
console.log(' Resuming prior: ' + queue.items.filter(i => !!i.prior).length);
|
|
258
|
+
console.log('');
|
|
259
|
+
/* Collect decisions: auto + prior + manual. */
|
|
260
|
+
const decisions = [];
|
|
261
|
+
for (const c of queue.auto_apply) {
|
|
262
|
+
decisions.push({
|
|
263
|
+
fingerprint: c.fingerprint,
|
|
264
|
+
nac_id: c.proposed_id,
|
|
265
|
+
role: c.role,
|
|
266
|
+
verb: c.proposed_verb,
|
|
267
|
+
plugin: c.proposed_plugin,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
for (const item of queue.items) {
|
|
271
|
+
if (item.prior) {
|
|
272
|
+
/* Reuse persisted decision without prompting. */
|
|
273
|
+
decisions.push({
|
|
274
|
+
fingerprint: item.prior.fingerprint,
|
|
275
|
+
nac_id: item.prior.nac_id,
|
|
276
|
+
role: item.prior.role,
|
|
277
|
+
verb: item.prior.verb,
|
|
278
|
+
plugin: item.prior.plugin,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const toReview = queue.items.filter(i => !i.prior);
|
|
283
|
+
if (toReview.length === 0) {
|
|
284
|
+
console.log(c.dim('No candidates need manual review.'));
|
|
285
|
+
}
|
|
286
|
+
else if (opts.yes) {
|
|
287
|
+
console.log(c.dim('--yes: accepting all proposed values as-is.'));
|
|
288
|
+
for (const item of toReview) {
|
|
289
|
+
decisions.push(proposedToDecision(item.candidate));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
for (let i = 0; i < toReview.length; i++) {
|
|
294
|
+
const item = toReview[i];
|
|
295
|
+
if (!item)
|
|
296
|
+
continue;
|
|
297
|
+
const cand = item.candidate;
|
|
298
|
+
console.log(c.brand('[' + (i + 1) + '/' + toReview.length + '] ')
|
|
299
|
+
+ path.relative(process.cwd(), item.filePath)
|
|
300
|
+
+ c.dim(' conf=' + cand.confidence.toFixed(2)));
|
|
301
|
+
console.log(c.dim(' text: ') + (cand.text || '(empty)'));
|
|
302
|
+
if (cand.aria_label)
|
|
303
|
+
console.log(c.dim(' aria-label: ') + cand.aria_label);
|
|
304
|
+
console.log(c.dim(' tag: ') + cand.tag);
|
|
305
|
+
console.log(c.dim(' proposed: ') + cand.proposed_id);
|
|
306
|
+
console.log(c.dim(' role: ') + cand.role + (cand.proposed_verb ? c.dim(' / verb=') + cand.proposed_verb : ''));
|
|
307
|
+
if (cand.verb_signal)
|
|
308
|
+
console.log(c.dim(' why verb: ') + cand.verb_signal);
|
|
309
|
+
const ans = await prompts([
|
|
310
|
+
{ type: 'select', name: 'action', message: 'Decide:',
|
|
311
|
+
choices: [
|
|
312
|
+
{ title: 'Accept (use proposed)', value: 'accept' },
|
|
313
|
+
{ title: 'Edit nac_id...', value: 'edit_id' },
|
|
314
|
+
{ title: 'Change role...', value: 'edit_role' },
|
|
315
|
+
{ title: 'Change verb...', value: 'edit_verb' },
|
|
316
|
+
{ title: 'Skip this one', value: 'skip' },
|
|
317
|
+
{ title: 'Abort assisted walk', value: 'abort' },
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
]);
|
|
321
|
+
if (ans.action === 'abort') {
|
|
322
|
+
console.log(c.warn('Aborted by user. Decisions made so far will not be persisted.'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (ans.action === 'skip')
|
|
326
|
+
continue;
|
|
327
|
+
let nac_id = cand.proposed_id;
|
|
328
|
+
let role = cand.role;
|
|
329
|
+
let verb = cand.proposed_verb;
|
|
330
|
+
if (ans.action === 'edit_id') {
|
|
331
|
+
const r = await prompts([{ type: 'text', name: 'v', message: 'nac_id:', initial: nac_id }]);
|
|
332
|
+
if (r.v)
|
|
333
|
+
nac_id = String(r.v);
|
|
334
|
+
}
|
|
335
|
+
if (ans.action === 'edit_role') {
|
|
336
|
+
const r = await prompts([{ type: 'select', name: 'v', message: 'role:',
|
|
337
|
+
choices: ['action', 'field', 'tab', 'region', 'option'].map(x => ({ title: x, value: x })) }]);
|
|
338
|
+
if (r.v)
|
|
339
|
+
role = r.v;
|
|
340
|
+
}
|
|
341
|
+
if (ans.action === 'edit_verb') {
|
|
342
|
+
const r = await prompts([{ type: 'text', name: 'v', message: 'verb (empty to clear):',
|
|
343
|
+
initial: verb || '' }]);
|
|
344
|
+
verb = r.v ? String(r.v) : null;
|
|
345
|
+
}
|
|
346
|
+
decisions.push({ fingerprint: cand.fingerprint, nac_id, role, verb, plugin: cand.proposed_plugin });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/* Apply */
|
|
350
|
+
const result = await finalizeHtmlAssisted({
|
|
351
|
+
projectRoot: queue.projectRoot,
|
|
352
|
+
walkResults: queue.walkResults,
|
|
353
|
+
decisions,
|
|
354
|
+
dryRun: !!opts.dryRun,
|
|
355
|
+
});
|
|
356
|
+
console.log('');
|
|
357
|
+
console.log(' Total applied: ' + result.total_applied);
|
|
358
|
+
console.log(' Decisions persisted: ' + result.decisions_added);
|
|
359
|
+
for (const f of result.files) {
|
|
360
|
+
if (f.applied.length === 0)
|
|
361
|
+
continue;
|
|
362
|
+
const rel = path.relative(process.cwd(), f.filePath);
|
|
363
|
+
const tag = f.wrote ? c.success('+ ') : c.warn('~ ');
|
|
364
|
+
console.log(' ' + tag + rel + c.dim(' (' + f.applied.length + ' applied)'));
|
|
365
|
+
}
|
|
366
|
+
console.log('');
|
|
367
|
+
if (opts.dryRun) {
|
|
368
|
+
console.log(c.warn('Dry run -- no files were written.'));
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.log(c.success('Assisted walk complete. Decisions saved to .yujin/nac-decisions.json'));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
console.error(c.error(err instanceof Error ? err.message : String(err)));
|
|
376
|
+
process.exitCode = 1;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function proposedToDecision(c) {
|
|
380
|
+
return {
|
|
381
|
+
fingerprint: c.fingerprint,
|
|
382
|
+
nac_id: c.proposed_id,
|
|
383
|
+
role: c.role,
|
|
384
|
+
verb: c.proposed_verb,
|
|
385
|
+
plugin: c.proposed_plugin,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/* ===== AI-powered decoration (Claude) =====
|
|
389
|
+
Two modes:
|
|
390
|
+
- silent: one-shot decoration. Take whatever the model returns.
|
|
391
|
+
- assisted: model can flag ambiguities; CLI prompts user; re-runs.
|
|
392
|
+
Input: one HTML file (--repo points to the file OR to a dir
|
|
393
|
+
containing an index.html).
|
|
394
|
+
Output: <repo>-ai-<mode>/ with the decorated HTML + manifest.json.
|
|
395
|
+
*/
|
|
396
|
+
export async function runAiHtml(repo, opts, mode) {
|
|
397
|
+
header('Yujin Forge -- AI-powered NAC-3 decoration (' + mode + ')');
|
|
398
|
+
console.log('');
|
|
399
|
+
const inputPath = path.resolve(process.cwd(), repo);
|
|
400
|
+
let htmlPath;
|
|
401
|
+
let baseDir;
|
|
402
|
+
let jsCompanion;
|
|
403
|
+
/* Resolve: file vs dir. */
|
|
404
|
+
const stat = await fs.stat(inputPath).catch(() => null);
|
|
405
|
+
if (!stat) {
|
|
406
|
+
console.error(c.error('Path does not exist: ' + inputPath));
|
|
407
|
+
process.exitCode = 1;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (stat.isDirectory()) {
|
|
411
|
+
htmlPath = path.join(inputPath, 'index.html');
|
|
412
|
+
baseDir = inputPath;
|
|
413
|
+
const stat2 = await fs.stat(htmlPath).catch(() => null);
|
|
414
|
+
if (!stat2) {
|
|
415
|
+
console.error(c.error('No index.html in directory: ' + inputPath));
|
|
416
|
+
process.exitCode = 1;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
/* Heuristic: look for a sibling *.js with similar basename. */
|
|
420
|
+
const candidates = ['calc.js', 'app.js', 'main.js', 'script.js'];
|
|
421
|
+
for (const name of candidates) {
|
|
422
|
+
const p = path.join(baseDir, name);
|
|
423
|
+
if (await fs.stat(p).then(() => true, () => false)) {
|
|
424
|
+
jsCompanion = p;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
htmlPath = inputPath;
|
|
431
|
+
baseDir = path.dirname(inputPath);
|
|
432
|
+
const stem = path.basename(inputPath, path.extname(inputPath));
|
|
433
|
+
const jsTry = path.join(baseDir, stem + '.js');
|
|
434
|
+
if (await fs.stat(jsTry).then(() => true, () => false))
|
|
435
|
+
jsCompanion = jsTry;
|
|
436
|
+
}
|
|
437
|
+
const outDir = opts.aiOut
|
|
438
|
+
? path.resolve(process.cwd(), opts.aiOut)
|
|
439
|
+
: baseDir + '-ai-' + mode;
|
|
440
|
+
const dryRun = !!opts.dryRun;
|
|
441
|
+
if (!dryRun) {
|
|
442
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
443
|
+
/* Copy companion CSS so the served output renders. */
|
|
444
|
+
for (const name of await fs.readdir(baseDir)) {
|
|
445
|
+
if (name.endsWith('.css')) {
|
|
446
|
+
await fs.copyFile(path.join(baseDir, name), path.join(outDir, name));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/* In silent mode the JS companion goes through unchanged; the runtime
|
|
450
|
+
reads attributes off the DOM. We still pass it to Claude so it knows
|
|
451
|
+
the verb hints, but the output is just decoration. */
|
|
452
|
+
if (jsCompanion) {
|
|
453
|
+
await fs.copyFile(jsCompanion, path.join(outDir, path.basename(jsCompanion)));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
console.log(' Input HTML: ' + c.dim(htmlPath));
|
|
457
|
+
if (jsCompanion)
|
|
458
|
+
console.log(' JS companion: ' + c.dim(jsCompanion));
|
|
459
|
+
console.log(' Output dir: ' + c.dim(outDir) + (dryRun ? c.warn(' (dry run -- nothing will be written)') : ''));
|
|
460
|
+
console.log(' Mode: ' + c.brand(mode));
|
|
461
|
+
console.log(' Model: ' + c.dim(opts.aiModel || 'claude-sonnet-4-6'));
|
|
462
|
+
if (opts.aiHint)
|
|
463
|
+
console.log(' Hint: ' + c.dim(opts.aiHint));
|
|
464
|
+
console.log('');
|
|
465
|
+
const outHtmlPath = path.join(outDir, 'index.html');
|
|
466
|
+
const outManifestPath = path.join(outDir, 'manifest.json');
|
|
467
|
+
let result;
|
|
468
|
+
try {
|
|
469
|
+
result = await aiDecorateFile({
|
|
470
|
+
htmlPath, jsCompanionPath: jsCompanion,
|
|
471
|
+
outHtmlPath, outManifestPath,
|
|
472
|
+
mode, pluginHint: opts.aiHint, model: opts.aiModel,
|
|
473
|
+
dryRun,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
console.error(c.error(err instanceof Error ? err.message : String(err)));
|
|
478
|
+
process.exitCode = 1;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (dryRun) {
|
|
482
|
+
/* Read the source HTML for diffing against the proposed decoration.
|
|
483
|
+
The diff is unified-format on stdout; manifest is printed as a
|
|
484
|
+
capped preview (it is a brand-new artifact, so a side-by-side
|
|
485
|
+
diff against an empty file would be noise). */
|
|
486
|
+
const originalHtml = await fs.readFile(htmlPath, 'utf-8');
|
|
487
|
+
const diff = unifiedDiff(originalHtml, result.decorated_html, htmlPath, outHtmlPath);
|
|
488
|
+
console.log(c.warn('Dry run -- no files were written.'));
|
|
489
|
+
console.log('');
|
|
490
|
+
console.log(' ' + c.dim('plugin: ') + result.plugin_slug);
|
|
491
|
+
console.log(' ' + c.dim('tokens: ') + result.meta.input_tokens + ' in / ' + result.meta.output_tokens + ' out');
|
|
492
|
+
console.log(' ' + c.dim('cost: ') + '$' + result.meta.cost_usd.toFixed(4));
|
|
493
|
+
console.log(' ' + c.dim('latency: ') + result.meta.latency_ms + 'ms');
|
|
494
|
+
console.log('');
|
|
495
|
+
if (!diff) {
|
|
496
|
+
console.log(c.dim('No HTML changes (decorator returned source unchanged).'));
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
console.log(c.brand('--- HTML diff ---'));
|
|
500
|
+
console.log(diff);
|
|
501
|
+
}
|
|
502
|
+
console.log('');
|
|
503
|
+
console.log(c.brand('--- Manifest preview (would write to ' + outManifestPath + ') ---'));
|
|
504
|
+
const manifestPreview = result.manifest_json.length > 4000
|
|
505
|
+
? result.manifest_json.slice(0, 4000) + '\n... + ' + (result.manifest_json.length - 4000) + ' bytes'
|
|
506
|
+
: result.manifest_json;
|
|
507
|
+
console.log(manifestPreview);
|
|
508
|
+
console.log('');
|
|
509
|
+
console.log(c.dim('Would also create in ' + outDir + ':'));
|
|
510
|
+
console.log(c.dim(' - nac-bridge.js (NAC3 contract bridge, ~2KB)'));
|
|
511
|
+
console.log(c.dim(' - nac.browser.js (local copy of NAC runtime when available; otherwise CDN tag injected)'));
|
|
512
|
+
if (jsCompanion)
|
|
513
|
+
console.log(c.dim(' - ' + path.basename(jsCompanion) + ' (companion JS copied unchanged)'));
|
|
514
|
+
if (opts.withPilot)
|
|
515
|
+
console.log(c.dim(' - pilot.bundle.js + pilot.css (Yujin Pilot cockpit)'));
|
|
516
|
+
console.log(c.dim('And inject the NAC3 runtime + bridge + register tags into the decorated HTML.'));
|
|
517
|
+
console.log('');
|
|
518
|
+
console.log(c.dim('Re-run without --dry-run to apply.'));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
console.log(c.success('AI decoration written:'));
|
|
522
|
+
console.log(' ' + c.dim('html: ') + outHtmlPath);
|
|
523
|
+
console.log(' ' + c.dim('manifest: ') + outManifestPath);
|
|
524
|
+
console.log(' ' + c.dim('plugin: ') + result.plugin_slug);
|
|
525
|
+
console.log(' ' + c.dim('tokens: ') + result.meta.input_tokens + ' in / ' + result.meta.output_tokens + ' out');
|
|
526
|
+
console.log(' ' + c.dim('cost: ') + '$' + result.meta.cost_usd.toFixed(4));
|
|
527
|
+
console.log(' ' + c.dim('latency: ') + result.meta.latency_ms + 'ms');
|
|
528
|
+
console.log('');
|
|
529
|
+
/* Assisted: handle ambiguities. The model flagged questions; we prompt
|
|
530
|
+
the user, then either re-issue (full second round) or just record
|
|
531
|
+
the answers as notes alongside the manifest. For MVP we record. */
|
|
532
|
+
if (mode === 'assisted' && result.ambiguities.length > 0) {
|
|
533
|
+
console.log(c.warn('Model flagged ' + result.ambiguities.length + ' ambiguous elements:'));
|
|
534
|
+
const answers = [];
|
|
535
|
+
for (const a of result.ambiguities) {
|
|
536
|
+
console.log('');
|
|
537
|
+
console.log(' ' + c.brand(a.kind.toUpperCase()) + ' ' + c.dim(a.id));
|
|
538
|
+
console.log(' Element: ' + c.dim(a.element_excerpt.slice(0, 100)));
|
|
539
|
+
console.log(' Question: ' + a.question);
|
|
540
|
+
const choices = a.options.map((o) => ({ title: o, value: o }));
|
|
541
|
+
choices.push({ title: '<custom...>', value: '__custom__' });
|
|
542
|
+
const pick = await prompts({
|
|
543
|
+
type: 'select',
|
|
544
|
+
name: 'choice',
|
|
545
|
+
message: 'Pick:',
|
|
546
|
+
choices,
|
|
547
|
+
});
|
|
548
|
+
let chosen = pick.choice;
|
|
549
|
+
if (chosen === '__custom__') {
|
|
550
|
+
const custom = await prompts({
|
|
551
|
+
type: 'text',
|
|
552
|
+
name: 'val',
|
|
553
|
+
message: 'Custom value:',
|
|
554
|
+
});
|
|
555
|
+
chosen = custom.val || a.options[0];
|
|
556
|
+
}
|
|
557
|
+
answers.push({ id: a.id, kind: a.kind, question: a.question, answer: chosen });
|
|
558
|
+
}
|
|
559
|
+
const notesPath = path.join(outDir, 'assisted-answers.json');
|
|
560
|
+
await fs.writeFile(notesPath, JSON.stringify(answers, null, 2), 'utf-8');
|
|
561
|
+
console.log('');
|
|
562
|
+
console.log(c.success('Assisted answers saved: ') + notesPath);
|
|
563
|
+
console.log(c.dim('(Re-run --ai-assisted to fold them into a second decoration pass.)'));
|
|
564
|
+
}
|
|
565
|
+
else if (mode === 'assisted') {
|
|
566
|
+
console.log(c.dim('Model did not flag any ambiguities -- output matches a silent run.'));
|
|
567
|
+
}
|
|
568
|
+
/* Wire the decorated HTML so it is drop-in NAC3-ready:
|
|
569
|
+
1. Ensure <body data-nac-plugin="<slug>"> (LLM may have forgotten).
|
|
570
|
+
2. Inject NAC runtime <script> in <head>.
|
|
571
|
+
3. Emit nac-bridge.js next to index.html.
|
|
572
|
+
4. Inject <script src="nac-bridge.js"> + inline NAC.register before </body>.
|
|
573
|
+
5. Optionally inject Pilot bundle when --with-pilot is set. */
|
|
574
|
+
await wireDecoratedOutput({
|
|
575
|
+
outDir, pluginSlug: result.plugin_slug,
|
|
576
|
+
manifestJson: result.manifest_json,
|
|
577
|
+
withPilot: !!opts.withPilot,
|
|
578
|
+
});
|
|
579
|
+
/* Apply any stored selective overrides on top of the fresh
|
|
580
|
+
Silent output. First run with no overrides.json yet -> writes
|
|
581
|
+
a fresh file with the baseline_hash. Subsequent runs merge. */
|
|
582
|
+
await applyOrSeedOverrides(outDir, opts);
|
|
583
|
+
console.log('');
|
|
584
|
+
console.log(c.success('Done. Open ') + outDir + c.success(' to inspect.'));
|
|
585
|
+
}
|
|
586
|
+
async function applyOrSeedOverrides(outDir, opts) {
|
|
587
|
+
const htmlPath = path.join(outDir, 'index.html');
|
|
588
|
+
const manifestPath = path.join(outDir, 'manifest.json');
|
|
589
|
+
const html = await fs.readFile(htmlPath, 'utf-8');
|
|
590
|
+
const manifestSrc = await fs.readFile(manifestPath, 'utf-8');
|
|
591
|
+
const baselineHash = computeBaselineHash(html, manifestSrc);
|
|
592
|
+
const existing = await readOverrides(outDir);
|
|
593
|
+
if (opts.noOverrides) {
|
|
594
|
+
/* Explicit opt-out. Reset overrides.json to a fresh empty file
|
|
595
|
+
so the user starts clean from this baseline. */
|
|
596
|
+
await writeOverrides(outDir, {
|
|
597
|
+
version: OVERRIDES_VERSION,
|
|
598
|
+
baseline_hash: baselineHash,
|
|
599
|
+
overrides: [],
|
|
600
|
+
});
|
|
601
|
+
console.log(c.dim('Overrides reset (--no-overrides). Fresh baseline_hash recorded.'));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!existing || existing.overrides.length === 0) {
|
|
605
|
+
/* First run -- seed the file. */
|
|
606
|
+
await writeOverrides(outDir, {
|
|
607
|
+
version: OVERRIDES_VERSION,
|
|
608
|
+
baseline_hash: baselineHash,
|
|
609
|
+
overrides: existing ? existing.overrides : [],
|
|
610
|
+
});
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
/* Apply stored overrides on top of fresh Silent output. */
|
|
614
|
+
const manifest = JSON.parse(manifestSrc);
|
|
615
|
+
const { html: patchedHtml, manifest: patchedManifest, result: applyResult } = applyOverrides(html, manifest, existing.overrides);
|
|
616
|
+
await fs.writeFile(htmlPath, patchedHtml, 'utf-8');
|
|
617
|
+
await fs.writeFile(manifestPath, JSON.stringify(patchedManifest, null, 2), 'utf-8');
|
|
618
|
+
/* Update the file's baseline_hash to the post-merge state so the
|
|
619
|
+
next run is internally consistent. */
|
|
620
|
+
await writeOverrides(outDir, {
|
|
621
|
+
version: OVERRIDES_VERSION,
|
|
622
|
+
baseline_hash: computeBaselineHash(patchedHtml, JSON.stringify(patchedManifest)),
|
|
623
|
+
overrides: existing.overrides,
|
|
624
|
+
});
|
|
625
|
+
if (applyResult.applied.length > 0) {
|
|
626
|
+
console.log(c.success('Applied ') + applyResult.applied.length + c.success(' override(s) on top of Silent output.'));
|
|
627
|
+
}
|
|
628
|
+
if (applyResult.orphaned.length > 0) {
|
|
629
|
+
console.log(c.warn('Orphaned ') + applyResult.orphaned.length + c.warn(' override(s) -- pre-image missing in fresh manifest:'));
|
|
630
|
+
for (const ov of applyResult.orphaned) {
|
|
631
|
+
console.log(' - ' + c.dim(ov.match_value) + ' (' + (ov.rationale || 'no rationale') + ')');
|
|
632
|
+
}
|
|
633
|
+
console.log(c.dim('Orphaned entries are kept in overrides.json; remove manually if no longer needed.'));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Build the plan of pages a multi-page run would touch. Pure +
|
|
638
|
+
* testable: no LLM calls, no writes. Exported so the test suite can
|
|
639
|
+
* exercise the directory-walk logic against a fixture.
|
|
640
|
+
*/
|
|
641
|
+
export async function planMultiPageHtml(baseDir, outDir) {
|
|
642
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
643
|
+
const items = [];
|
|
644
|
+
for (const e of entries) {
|
|
645
|
+
if (!e.isFile() || !e.name.toLowerCase().endsWith('.html'))
|
|
646
|
+
continue;
|
|
647
|
+
const basename = e.name;
|
|
648
|
+
items.push({
|
|
649
|
+
htmlPath: path.join(baseDir, basename),
|
|
650
|
+
basename,
|
|
651
|
+
outHtmlPath: path.join(outDir, basename),
|
|
652
|
+
/* Sidecar manifest pattern: <basename>.manifest.json. The single
|
|
653
|
+
"index.html" page gets index.html.manifest.json so the runtime
|
|
654
|
+
can find it via a deterministic pathname-based lookup. */
|
|
655
|
+
outManifestPath: path.join(outDir, basename + '.manifest.json'),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
items.sort((a, b) => {
|
|
659
|
+
/* Sort index.html first when present, then alphabetical. The
|
|
660
|
+
first-page slug becomes the canonical plugin_slug. */
|
|
661
|
+
if (a.basename === 'index.html')
|
|
662
|
+
return -1;
|
|
663
|
+
if (b.basename === 'index.html')
|
|
664
|
+
return 1;
|
|
665
|
+
return a.basename.localeCompare(b.basename);
|
|
666
|
+
});
|
|
667
|
+
return items;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Decorate every .html file in `baseDir`, share a plugin_slug across
|
|
671
|
+
* pages, write per-page outputs. Pure + DI-friendly so the call site
|
|
672
|
+
* (`runAiHtmlMultiPage`) can stay terse and the test suite can drive
|
|
673
|
+
* it without hitting the network.
|
|
674
|
+
*/
|
|
675
|
+
export async function decorateMultiPageHtml(opts) {
|
|
676
|
+
const plan = await planMultiPageHtml(opts.baseDir, opts.outDir);
|
|
677
|
+
if (plan.length === 0) {
|
|
678
|
+
throw new Error('No .html files found in ' + opts.baseDir + ' -- nothing to do with --multi-page.');
|
|
679
|
+
}
|
|
680
|
+
if (!opts.dryRun) {
|
|
681
|
+
await fs.mkdir(opts.outDir, { recursive: true });
|
|
682
|
+
/* Copy companion CSS once; shared across all pages. */
|
|
683
|
+
for (const name of await fs.readdir(opts.baseDir)) {
|
|
684
|
+
if (name.endsWith('.css')) {
|
|
685
|
+
await fs.copyFile(path.join(opts.baseDir, name), path.join(opts.outDir, name));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const pages = [];
|
|
690
|
+
let totalTokensIn = 0;
|
|
691
|
+
let totalTokensOut = 0;
|
|
692
|
+
let totalCost = 0;
|
|
693
|
+
let totalLatency = 0;
|
|
694
|
+
let canonicalSlug = opts.pluginHint || '';
|
|
695
|
+
for (const item of plan) {
|
|
696
|
+
/* Each page gets its own decoration. Subsequent pages receive the
|
|
697
|
+
slug from the first page as the pluginHint so the model uses
|
|
698
|
+
the same one. */
|
|
699
|
+
const hintForThisPage = canonicalSlug || opts.pluginHint;
|
|
700
|
+
const result = await aiDecorateFile({
|
|
701
|
+
htmlPath: item.htmlPath,
|
|
702
|
+
outHtmlPath: item.outHtmlPath,
|
|
703
|
+
outManifestPath: item.outManifestPath,
|
|
704
|
+
mode: opts.mode,
|
|
705
|
+
pluginHint: hintForThisPage,
|
|
706
|
+
model: opts.model,
|
|
707
|
+
dryRun: opts.dryRun,
|
|
708
|
+
...(opts.decorator ? { decorator: opts.decorator } : {}),
|
|
709
|
+
});
|
|
710
|
+
if (!canonicalSlug)
|
|
711
|
+
canonicalSlug = result.plugin_slug;
|
|
712
|
+
/* If the model picked a slug that disagrees with the canonical
|
|
713
|
+
(because the hint wasn't strong enough on the first call), force
|
|
714
|
+
the canonical slug into the manifest + into the decorated HTML's
|
|
715
|
+
body[data-nac-plugin] before persisting downstream. */
|
|
716
|
+
let manifestJson = result.manifest_json;
|
|
717
|
+
let decoratedHtml = result.decorated_html;
|
|
718
|
+
if (result.plugin_slug !== canonicalSlug) {
|
|
719
|
+
manifestJson = result.manifest_json.replace(new RegExp('"plugin_slug"\\s*:\\s*"' + escapeRegex(result.plugin_slug) + '"'), '"plugin_slug":"' + canonicalSlug + '"');
|
|
720
|
+
decoratedHtml = result.decorated_html.replace(new RegExp('data-nac-plugin\\s*=\\s*"' + escapeRegex(result.plugin_slug) + '"'), 'data-nac-plugin="' + canonicalSlug + '"');
|
|
721
|
+
}
|
|
722
|
+
/* Recompute the diff against the source page. */
|
|
723
|
+
const original = await fs.readFile(item.htmlPath, 'utf-8');
|
|
724
|
+
const diff = unifiedDiff(original, decoratedHtml, item.htmlPath, item.outHtmlPath);
|
|
725
|
+
if (!opts.dryRun) {
|
|
726
|
+
/* aiDecorateFile already wrote the raw output; if we mutated
|
|
727
|
+
slug, overwrite with the corrected versions. */
|
|
728
|
+
if (decoratedHtml !== result.decorated_html || manifestJson !== result.manifest_json) {
|
|
729
|
+
await fs.writeFile(item.outHtmlPath, decoratedHtml, 'utf-8');
|
|
730
|
+
await fs.writeFile(item.outManifestPath, manifestJson, 'utf-8');
|
|
731
|
+
}
|
|
732
|
+
await wireDecoratedOutput({
|
|
733
|
+
outDir: opts.outDir,
|
|
734
|
+
pluginSlug: canonicalSlug,
|
|
735
|
+
manifestJson,
|
|
736
|
+
withPilot: opts.withPilot,
|
|
737
|
+
htmlBasename: item.basename,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
pages.push({
|
|
741
|
+
item,
|
|
742
|
+
plugin_slug: canonicalSlug,
|
|
743
|
+
manifest_json: manifestJson,
|
|
744
|
+
decorated_html: decoratedHtml,
|
|
745
|
+
diff,
|
|
746
|
+
ambiguities_count: result.ambiguities.length,
|
|
747
|
+
tokens_in: result.meta.input_tokens,
|
|
748
|
+
tokens_out: result.meta.output_tokens,
|
|
749
|
+
cost_usd: result.meta.cost_usd,
|
|
750
|
+
latency_ms: result.meta.latency_ms,
|
|
751
|
+
});
|
|
752
|
+
totalTokensIn += result.meta.input_tokens;
|
|
753
|
+
totalTokensOut += result.meta.output_tokens;
|
|
754
|
+
totalCost += result.meta.cost_usd;
|
|
755
|
+
totalLatency += result.meta.latency_ms;
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
plan,
|
|
759
|
+
pages,
|
|
760
|
+
plugin_slug: canonicalSlug,
|
|
761
|
+
total_tokens_in: totalTokensIn,
|
|
762
|
+
total_tokens_out: totalTokensOut,
|
|
763
|
+
total_cost_usd: totalCost,
|
|
764
|
+
total_latency_ms: totalLatency,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function escapeRegex(s) {
|
|
768
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
769
|
+
}
|
|
770
|
+
export async function runAiHtmlMultiPage(repo, opts, mode) {
|
|
771
|
+
header('Yujin Forge -- AI-powered NAC-3 decoration (' + mode + ', multi-page)');
|
|
772
|
+
console.log('');
|
|
773
|
+
const inputPath = path.resolve(process.cwd(), repo);
|
|
774
|
+
const stat = await fs.stat(inputPath).catch(() => null);
|
|
775
|
+
if (!stat || !stat.isDirectory()) {
|
|
776
|
+
console.error(c.error('--multi-page requires a directory input: ' + inputPath));
|
|
777
|
+
process.exitCode = 1;
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const outDir = opts.aiOut
|
|
781
|
+
? path.resolve(process.cwd(), opts.aiOut)
|
|
782
|
+
: inputPath + '-ai-' + mode;
|
|
783
|
+
const dryRun = !!opts.dryRun;
|
|
784
|
+
console.log(' Input dir: ' + c.dim(inputPath));
|
|
785
|
+
console.log(' Output dir: ' + c.dim(outDir) + (dryRun ? c.warn(' (dry run -- nothing will be written)') : ''));
|
|
786
|
+
console.log(' Mode: ' + c.brand(mode));
|
|
787
|
+
console.log(' Model: ' + c.dim(opts.aiModel || 'claude-sonnet-4-6'));
|
|
788
|
+
if (opts.aiHint)
|
|
789
|
+
console.log(' Hint: ' + c.dim(opts.aiHint));
|
|
790
|
+
console.log('');
|
|
791
|
+
let result;
|
|
792
|
+
try {
|
|
793
|
+
result = await decorateMultiPageHtml({
|
|
794
|
+
baseDir: inputPath,
|
|
795
|
+
outDir,
|
|
796
|
+
mode,
|
|
797
|
+
pluginHint: opts.aiHint,
|
|
798
|
+
...(opts.aiModel ? { model: opts.aiModel } : {}),
|
|
799
|
+
dryRun,
|
|
800
|
+
withPilot: !!opts.withPilot,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
console.error(c.error(err instanceof Error ? err.message : String(err)));
|
|
805
|
+
process.exitCode = 1;
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
console.log(' Pages walked: ' + result.pages.length);
|
|
809
|
+
console.log(' Plugin slug: ' + c.brand(result.plugin_slug));
|
|
810
|
+
console.log(' Tokens in/out: ' + result.total_tokens_in + ' / ' + result.total_tokens_out);
|
|
811
|
+
console.log(' Total cost: ' + '$' + result.total_cost_usd.toFixed(4));
|
|
812
|
+
console.log(' Total latency: ' + result.total_latency_ms + 'ms');
|
|
813
|
+
console.log('');
|
|
814
|
+
if (dryRun) {
|
|
815
|
+
console.log(c.warn('Dry run -- no files were written.'));
|
|
816
|
+
console.log('');
|
|
817
|
+
for (const p of result.pages) {
|
|
818
|
+
console.log(c.brand('--- ' + p.item.basename + ' ---'));
|
|
819
|
+
if (!p.diff) {
|
|
820
|
+
console.log(c.dim('No HTML changes.'));
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
console.log(p.diff);
|
|
824
|
+
}
|
|
825
|
+
console.log(c.dim('Would write manifest to ' + p.item.outManifestPath +
|
|
826
|
+
' (' + p.manifest_json.length + ' bytes)'));
|
|
827
|
+
console.log('');
|
|
828
|
+
}
|
|
829
|
+
console.log(c.dim('Would also create in ' + outDir + ':'));
|
|
830
|
+
console.log(c.dim(' - nac-bridge.js (shared across pages)'));
|
|
831
|
+
console.log(c.dim(' - nac.browser.js (shared across pages)'));
|
|
832
|
+
if (opts.withPilot)
|
|
833
|
+
console.log(c.dim(' - pilot.bundle.js + pilot.css (shared across pages)'));
|
|
834
|
+
console.log(c.dim('And inject the NAC3 runtime + bridge + register tags into every decorated HTML.'));
|
|
835
|
+
console.log('');
|
|
836
|
+
console.log(c.dim('Re-run without --dry-run to apply.'));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
console.log(c.success('Multi-page decoration written:'));
|
|
840
|
+
for (const p of result.pages) {
|
|
841
|
+
console.log(' ' + c.success('+ ') + p.item.basename + c.dim(' -> ') + p.item.outHtmlPath
|
|
842
|
+
+ c.dim(' (manifest: ') + path.basename(p.item.outManifestPath) + c.dim(')'));
|
|
843
|
+
}
|
|
844
|
+
console.log('');
|
|
845
|
+
console.log(c.success('Done. Open ') + outDir + c.success(' to inspect.'));
|
|
846
|
+
}
|
|
847
|
+
/* ------------------------------------------------------------------
|
|
848
|
+
* Post-processing: turn raw Claude output into a drop-in NAC3 app.
|
|
849
|
+
* ------------------------------------------------------------------ */
|
|
850
|
+
const NAC_BRIDGE_SOURCE = `/* nac-bridge.js -- emitted by Yujin Forge.
|
|
851
|
+
*
|
|
852
|
+
* Every element decorated with data-nac-role="action" and data-nac-id
|
|
853
|
+
* gets its inline onclick handler routed through NAC.bindAction, so the
|
|
854
|
+
* runtime's nac:action:succeeded contract fires after the host handler
|
|
855
|
+
* runs. Without this bridge, NAC.click() times out after 5 s even
|
|
856
|
+
* though el.click() already executed the host's handler.
|
|
857
|
+
*
|
|
858
|
+
* SPA-aware: also watches DOM mutations and binds newly mounted action
|
|
859
|
+
* elements as routes change (React/Vue/Angular re-render dynamically).
|
|
860
|
+
*
|
|
861
|
+
* The plugin slug is read from <body data-nac-plugin="...">. The
|
|
862
|
+
* Forge-decorated HTML always sets that attribute.
|
|
863
|
+
*/
|
|
864
|
+
(function () {
|
|
865
|
+
function bindOne(el, slug) {
|
|
866
|
+
if (!el || el.getAttribute('data-nac-bound') === '1') return false;
|
|
867
|
+
var id = el.getAttribute('data-nac-id');
|
|
868
|
+
if (!id) return false;
|
|
869
|
+
var handler = el.onclick;
|
|
870
|
+
if (typeof handler !== 'function') {
|
|
871
|
+
/* No inline onclick (typical of React: handlers attach via the
|
|
872
|
+
synthetic event system, not as inline attributes). Bind a
|
|
873
|
+
no-op so the contract event fires when the host's
|
|
874
|
+
React-managed click runs in the same dispatch tick. */
|
|
875
|
+
handler = function () { /* host owns the click */ };
|
|
876
|
+
} else {
|
|
877
|
+
el.onclick = null;
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
window.NAC.bindAction(el, handler, { plugin: slug, action_id: id });
|
|
881
|
+
el.setAttribute('data-nac-bound', '1');
|
|
882
|
+
return true;
|
|
883
|
+
} catch (e) {
|
|
884
|
+
if (window.console) console.error('[nac-bridge] bindAction failed for ' + id, e);
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function bindAll(slug) {
|
|
890
|
+
/* Bind every clickable role (action, navigation, tab, option,
|
|
891
|
+
breadcrumb-item, step, pagination-item, confirm-button,
|
|
892
|
+
sort-control, filter-control). Skip role=region/field --
|
|
893
|
+
containers / value-holders, not click targets. NAC.click()
|
|
894
|
+
listens for nac:action:succeeded as fallback on non-action
|
|
895
|
+
roles, so bindAction's emission works universally. */
|
|
896
|
+
var els = document.querySelectorAll(
|
|
897
|
+
'[data-nac-role="action"][data-nac-id]:not([data-nac-bound]),' +
|
|
898
|
+
'[data-nac-role="navigation"][data-nac-id]:not([data-nac-bound]),' +
|
|
899
|
+
'[data-nac-role="tab"][data-nac-id]:not([data-nac-bound]),' +
|
|
900
|
+
'[data-nac-role="option"][data-nac-id]:not([data-nac-bound]),' +
|
|
901
|
+
'[data-nac-role="breadcrumb-item"][data-nac-id]:not([data-nac-bound]),' +
|
|
902
|
+
'[data-nac-role="step"][data-nac-id]:not([data-nac-bound]),' +
|
|
903
|
+
'[data-nac-role="pagination-item"][data-nac-id]:not([data-nac-bound]),' +
|
|
904
|
+
'[data-nac-role="confirm-button"][data-nac-id]:not([data-nac-bound]),' +
|
|
905
|
+
'[data-nac-role="sort-control"][data-nac-id]:not([data-nac-bound]),' +
|
|
906
|
+
'[data-nac-role="filter-control"][data-nac-id]:not([data-nac-bound])'
|
|
907
|
+
);
|
|
908
|
+
var bound = 0;
|
|
909
|
+
els.forEach(function (el) { if (bindOne(el, slug)) bound++; });
|
|
910
|
+
return bound;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function bridge() {
|
|
914
|
+
if (!window.NAC || typeof window.NAC.bindAction !== 'function') {
|
|
915
|
+
if (window.console) console.error('[nac-bridge] window.NAC.bindAction missing -- include nac.browser.js before this bridge.');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
var slug = document.body && document.body.getAttribute('data-nac-plugin');
|
|
919
|
+
if (!slug) {
|
|
920
|
+
if (window.console) console.error('[nac-bridge] <body data-nac-plugin="..."> missing -- cannot scope bindAction.');
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
var initialBound = bindAll(slug);
|
|
925
|
+
if (window.console && console.info) {
|
|
926
|
+
console.info('[nac-bridge] initial bind: ' + initialBound + ' action(s) for plugin ' + slug);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/* SPA support: observe DOM mutations and bind any new action
|
|
930
|
+
elements that appear (route changes, modal mounts, etc). */
|
|
931
|
+
var pendingFrame = 0;
|
|
932
|
+
var totalLateBound = 0;
|
|
933
|
+
var obs = new MutationObserver(function (mutations) {
|
|
934
|
+
var hasAdded = false;
|
|
935
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
936
|
+
if (mutations[i].addedNodes && mutations[i].addedNodes.length > 0) { hasAdded = true; break; }
|
|
937
|
+
}
|
|
938
|
+
if (!hasAdded) return;
|
|
939
|
+
if (pendingFrame) return;
|
|
940
|
+
pendingFrame = window.requestAnimationFrame(function () {
|
|
941
|
+
pendingFrame = 0;
|
|
942
|
+
var bound = bindAll(slug);
|
|
943
|
+
if (bound > 0) {
|
|
944
|
+
totalLateBound += bound;
|
|
945
|
+
if (window.console && console.debug) {
|
|
946
|
+
console.debug('[nac-bridge] late bind: ' + bound + ' action(s) (total late: ' + totalLateBound + ')');
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (document.readyState === 'loading') {
|
|
955
|
+
document.addEventListener('DOMContentLoaded', bridge);
|
|
956
|
+
} else {
|
|
957
|
+
bridge();
|
|
958
|
+
}
|
|
959
|
+
})();
|
|
960
|
+
`;
|
|
961
|
+
const NAC_RUNTIME_CDN = 'https://unpkg.com/nac-spec@2.3.1/dist/nac.browser.js';
|
|
962
|
+
async function resolveNacRuntime() {
|
|
963
|
+
/* Forge looks for the NAC3 browser bundle in this order:
|
|
964
|
+
1. node_modules of the current cwd (npm install nac-spec)
|
|
965
|
+
2. Sibling rpaforce checkout (../rpaforce/packages/nac/dist/...)
|
|
966
|
+
3. Sibling nac-spec checkout
|
|
967
|
+
4. None: fall back to the unpkg CDN tag.
|
|
968
|
+
Returning a local path lets the decorated app run offline + works
|
|
969
|
+
around Chromium ORB blocks on cross-origin script in some configs. */
|
|
970
|
+
const candidates = [
|
|
971
|
+
path.resolve(process.cwd(), 'node_modules', 'nac-spec', 'dist', 'nac.browser.js'),
|
|
972
|
+
path.resolve(process.cwd(), '..', 'rpaforce', 'packages', 'nac', 'dist', 'nac.browser.js'),
|
|
973
|
+
path.resolve(process.cwd(), '..', 'nac-spec', 'dist', 'nac.browser.js'),
|
|
974
|
+
path.resolve(process.cwd(), '..', 'nac-spec-yujinapp', 'runtime', 'js', 'nac.js'),
|
|
975
|
+
];
|
|
976
|
+
for (const p of candidates) {
|
|
977
|
+
if (await fs.stat(p).then(() => true, () => false))
|
|
978
|
+
return p;
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
async function wireDecoratedOutput(opts) {
|
|
983
|
+
const htmlBasename = opts.htmlBasename || 'index.html';
|
|
984
|
+
const htmlPath = path.join(opts.outDir, htmlBasename);
|
|
985
|
+
let html = await fs.readFile(htmlPath, 'utf-8');
|
|
986
|
+
/* 1. Ensure data-nac-plugin on <body>. */
|
|
987
|
+
html = html.replace(/<body([^>]*)>/i, (match, attrs) => {
|
|
988
|
+
if (/data-nac-plugin\s*=/.test(attrs))
|
|
989
|
+
return match;
|
|
990
|
+
return `<body${attrs} data-nac-plugin="${opts.pluginSlug}">`;
|
|
991
|
+
});
|
|
992
|
+
/* 2. Inject NAC runtime in <head>. Skip if already present.
|
|
993
|
+
Prefer a local copy of nac.browser.js for offline + ORB safety. */
|
|
994
|
+
const runtimeSrcPath = await resolveNacRuntime();
|
|
995
|
+
let runtimeSrcAttr;
|
|
996
|
+
if (runtimeSrcPath) {
|
|
997
|
+
await fs.copyFile(runtimeSrcPath, path.join(opts.outDir, 'nac.browser.js'));
|
|
998
|
+
runtimeSrcAttr = 'nac.browser.js';
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
runtimeSrcAttr = NAC_RUNTIME_CDN;
|
|
1002
|
+
}
|
|
1003
|
+
if (!/nac\.browser\.js|nac-spec.*nac\.browser|js\/nac\.js/.test(html)) {
|
|
1004
|
+
const runtimeTag = ` <script src="${runtimeSrcAttr}"></script>`;
|
|
1005
|
+
if (/<\/head>/i.test(html)) {
|
|
1006
|
+
html = html.replace(/<\/head>/i, `${runtimeTag}\n</head>`);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
/* No <head>? Prepend at top of body. */
|
|
1010
|
+
html = html.replace(/<body([^>]*)>/i, `<body$1>\n${runtimeTag}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/* 3. Emit nac-bridge.js next to index.html. */
|
|
1014
|
+
await fs.writeFile(path.join(opts.outDir, 'nac-bridge.js'), NAC_BRIDGE_SOURCE, 'utf-8');
|
|
1015
|
+
/* 4. Inject bridge + inline register before </body>. */
|
|
1016
|
+
const registerInline = `<script>
|
|
1017
|
+
(function () {
|
|
1018
|
+
var manifest = ${opts.manifestJson};
|
|
1019
|
+
function reg() {
|
|
1020
|
+
if (window.NAC && window.NAC.register) {
|
|
1021
|
+
try { window.NAC.register(manifest); }
|
|
1022
|
+
catch (e) { if (window.console) console.error('NAC.register failed', e); }
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', reg);
|
|
1026
|
+
else reg();
|
|
1027
|
+
})();
|
|
1028
|
+
</script>`;
|
|
1029
|
+
const tailInjection = [
|
|
1030
|
+
'<script src="nac-bridge.js"></script>',
|
|
1031
|
+
registerInline,
|
|
1032
|
+
].join('\n');
|
|
1033
|
+
if (/<\/body>/i.test(html)) {
|
|
1034
|
+
html = html.replace(/<\/body>/i, `${tailInjection}\n</body>`);
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
html += '\n' + tailInjection;
|
|
1038
|
+
}
|
|
1039
|
+
/* 5. Optional Pilot injection. */
|
|
1040
|
+
if (opts.withPilot) {
|
|
1041
|
+
await injectPilotBundle(opts.outDir, html, htmlBasename);
|
|
1042
|
+
/* injectPilotBundle re-reads and writes the html itself. */
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
await fs.writeFile(htmlPath, html, 'utf-8');
|
|
1046
|
+
}
|
|
1047
|
+
console.log(c.success('Wired NAC3 runtime + bridge into decorated output.'));
|
|
1048
|
+
if (opts.withPilot)
|
|
1049
|
+
console.log(c.success('Yujin Pilot bundle injected.'));
|
|
1050
|
+
}
|
|
1051
|
+
async function injectPilotBundle(outDir, html, htmlBasename = 'index.html') {
|
|
1052
|
+
/* Pilot bundle ships under yujin-pilot/dist/. Forge resolves it at install
|
|
1053
|
+
time. If the bundle is missing, fall back to a hint message. */
|
|
1054
|
+
const pilotSrcCandidates = [
|
|
1055
|
+
path.resolve(process.cwd(), '..', 'yujin-pilot', 'dist', 'pilot.bundle.js'),
|
|
1056
|
+
path.resolve(process.cwd(), 'node_modules', '@yujin', 'pilot', 'dist', 'pilot.bundle.js'),
|
|
1057
|
+
];
|
|
1058
|
+
const pilotCssCandidates = [
|
|
1059
|
+
path.resolve(process.cwd(), '..', 'yujin-pilot', 'dist', 'pilot.css'),
|
|
1060
|
+
path.resolve(process.cwd(), 'node_modules', '@yujin', 'pilot', 'dist', 'pilot.css'),
|
|
1061
|
+
];
|
|
1062
|
+
let pilotJsSrc = null;
|
|
1063
|
+
let pilotCssSrc = null;
|
|
1064
|
+
for (const p of pilotSrcCandidates) {
|
|
1065
|
+
if (await fs.stat(p).then(() => true, () => false)) {
|
|
1066
|
+
pilotJsSrc = p;
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
for (const p of pilotCssCandidates) {
|
|
1071
|
+
if (await fs.stat(p).then(() => true, () => false)) {
|
|
1072
|
+
pilotCssSrc = p;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (!pilotJsSrc) {
|
|
1077
|
+
console.log(c.warn('Pilot bundle not found at ../yujin-pilot/dist/ -- skipping injection.'));
|
|
1078
|
+
console.log(c.dim('Run from a sibling checkout of yujin-pilot or install @yujin/pilot.'));
|
|
1079
|
+
await fs.writeFile(path.join(outDir, htmlBasename), html, 'utf-8');
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
await fs.copyFile(pilotJsSrc, path.join(outDir, 'pilot.bundle.js'));
|
|
1083
|
+
if (pilotCssSrc)
|
|
1084
|
+
await fs.copyFile(pilotCssSrc, path.join(outDir, 'pilot.css'));
|
|
1085
|
+
const tags = [
|
|
1086
|
+
pilotCssSrc ? '<link rel="stylesheet" href="pilot.css">' : '',
|
|
1087
|
+
'<script src="pilot.bundle.js"></script>',
|
|
1088
|
+
].filter(Boolean).join('\n');
|
|
1089
|
+
if (/<\/body>/i.test(html)) {
|
|
1090
|
+
html = html.replace(/<\/body>/i, `${tags}\n</body>`);
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
html += '\n' + tags;
|
|
1094
|
+
}
|
|
1095
|
+
await fs.writeFile(path.join(outDir, htmlBasename), html, 'utf-8');
|
|
1096
|
+
}
|
|
1097
|
+
/* ------------------------------------------------------------------
|
|
1098
|
+
* Selective re-decoration: --ai-search + --ai-override
|
|
1099
|
+
* ------------------------------------------------------------------ */
|
|
1100
|
+
/**
|
|
1101
|
+
* `yf migrate <repo> --ai-search "<query>"` -- list manifest
|
|
1102
|
+
* elements matching the free-text query, ranked by relevance.
|
|
1103
|
+
* Useful before running --ai-override to find the right nac_id.
|
|
1104
|
+
*/
|
|
1105
|
+
export async function runAiSearch(repo, opts) {
|
|
1106
|
+
header('Yujin Forge -- manifest search');
|
|
1107
|
+
console.log('');
|
|
1108
|
+
const dir = path.resolve(process.cwd(), repo);
|
|
1109
|
+
const manifestPath = path.join(dir, 'manifest.json');
|
|
1110
|
+
const stat = await fs.stat(manifestPath).catch(() => null);
|
|
1111
|
+
if (!stat) {
|
|
1112
|
+
console.error(c.error('No manifest.json found in: ' + dir));
|
|
1113
|
+
console.error(c.dim('Run --ai-silent first to produce one.'));
|
|
1114
|
+
process.exitCode = 1;
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
1118
|
+
const hits = searchManifest(manifest, opts.aiSearch || '');
|
|
1119
|
+
if (hits.length === 0) {
|
|
1120
|
+
console.log(c.dim('No manifest elements match: ') + (opts.aiSearch || ''));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
console.log(c.success(hits.length + ' hit(s) for "') + (opts.aiSearch || '') + c.success('":'));
|
|
1124
|
+
console.log('');
|
|
1125
|
+
for (const h of hits.slice(0, 20)) {
|
|
1126
|
+
const label = h.label_es || h.label_en;
|
|
1127
|
+
const labelStr = label ? c.dim(' (') + label + c.dim(')') : '';
|
|
1128
|
+
const verbStr = h.verbs.length ? c.dim(' verb=[') + h.verbs.join(',') + c.dim(']') : '';
|
|
1129
|
+
console.log(' ' + c.brand(h.nac_id) + c.dim(' role=') + h.role + verbStr + labelStr);
|
|
1130
|
+
}
|
|
1131
|
+
if (hits.length > 20)
|
|
1132
|
+
console.log(c.dim(' ... and ' + (hits.length - 20) + ' more.'));
|
|
1133
|
+
console.log('');
|
|
1134
|
+
console.log(c.dim('To re-decorate one of these:'));
|
|
1135
|
+
console.log(' ' + c.brand('yf migrate ' + repo + ' --ai-override <nac-id>'));
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* `yf migrate <repo> --ai-override <nacId>` -- interactively
|
|
1139
|
+
* re-decorate one element. Writes the override to overrides.json AND
|
|
1140
|
+
* applies it immediately to index.html + manifest.json.
|
|
1141
|
+
*/
|
|
1142
|
+
export async function runAiOverride(repo, opts) {
|
|
1143
|
+
header('Yujin Forge -- selective override');
|
|
1144
|
+
console.log('');
|
|
1145
|
+
const dir = path.resolve(process.cwd(), repo);
|
|
1146
|
+
const manifestPath = path.join(dir, 'manifest.json');
|
|
1147
|
+
const htmlPath = path.join(dir, 'index.html');
|
|
1148
|
+
const ok = await Promise.all([
|
|
1149
|
+
fs.stat(manifestPath).then(() => true, () => false),
|
|
1150
|
+
fs.stat(htmlPath).then(() => true, () => false),
|
|
1151
|
+
]);
|
|
1152
|
+
if (!ok[0] || !ok[1]) {
|
|
1153
|
+
console.error(c.error('Expected ' + manifestPath + ' and ' + htmlPath + ' to exist.'));
|
|
1154
|
+
console.error(c.dim('Run --ai-silent first to produce them.'));
|
|
1155
|
+
process.exitCode = 1;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const nacId = (opts.aiOverride || '').trim();
|
|
1159
|
+
if (!nacId) {
|
|
1160
|
+
console.error(c.error('--ai-override requires a nac-id argument.'));
|
|
1161
|
+
process.exitCode = 1;
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
1165
|
+
const el = (manifest.elements || []).find((e) => e && e.id === nacId);
|
|
1166
|
+
if (!el) {
|
|
1167
|
+
console.error(c.error('No element with data-nac-id="' + nacId + '" in the manifest.'));
|
|
1168
|
+
console.error(c.dim('Use --ai-search to find candidates.'));
|
|
1169
|
+
process.exitCode = 1;
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const currentVerb = (el.actions && el.actions[0] && el.actions[0].verb) || '';
|
|
1173
|
+
const currentLabelEs = (el.label_i18n && el.label_i18n.es) ||
|
|
1174
|
+
(el.actions && el.actions[0] && el.actions[0].label_i18n && el.actions[0].label_i18n.es) || '';
|
|
1175
|
+
const currentLabelEn = (el.label_i18n && el.label_i18n.en) ||
|
|
1176
|
+
(el.actions && el.actions[0] && el.actions[0].label_i18n && el.actions[0].label_i18n.en) || '';
|
|
1177
|
+
console.log(c.dim('Element selected:'));
|
|
1178
|
+
console.log(' ' + c.brand(el.id) + c.dim(' role=') + el.role + (currentVerb ? c.dim(' verb=') + currentVerb : ''));
|
|
1179
|
+
if (currentLabelEs)
|
|
1180
|
+
console.log(' ' + c.dim('label.es = ') + currentLabelEs);
|
|
1181
|
+
if (currentLabelEn)
|
|
1182
|
+
console.log(' ' + c.dim('label.en = ') + currentLabelEn);
|
|
1183
|
+
console.log('');
|
|
1184
|
+
const answers = await prompts([
|
|
1185
|
+
{ type: 'text', name: 'nac_id', message: 'New nac_id (blank = keep)', initial: el.id },
|
|
1186
|
+
{
|
|
1187
|
+
type: 'select', name: 'role', message: 'Role',
|
|
1188
|
+
choices: ['action', 'field', 'option', 'tab', 'region', 'navigation']
|
|
1189
|
+
.map((r) => ({ title: r, value: r, selected: r === el.role })),
|
|
1190
|
+
initial: ['action', 'field', 'option', 'tab', 'region', 'navigation'].indexOf(el.role),
|
|
1191
|
+
},
|
|
1192
|
+
{ type: 'text', name: 'verb', message: 'Verb (blank = keep / not applicable)', initial: currentVerb },
|
|
1193
|
+
{ type: 'text', name: 'label_es', message: 'Label es (blank = keep)', initial: currentLabelEs },
|
|
1194
|
+
{ type: 'text', name: 'label_en', message: 'Label en (blank = keep)', initial: currentLabelEn },
|
|
1195
|
+
{ type: 'text', name: 'rationale', message: 'Rationale (one line, for the audit trail)' },
|
|
1196
|
+
]);
|
|
1197
|
+
if (!answers || typeof answers.role !== 'string') {
|
|
1198
|
+
console.log(c.dim('Cancelled.'));
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const patch = {};
|
|
1202
|
+
if (answers.nac_id && answers.nac_id !== el.id)
|
|
1203
|
+
patch.nac_id = answers.nac_id;
|
|
1204
|
+
if (answers.role && answers.role !== el.role)
|
|
1205
|
+
patch.role = answers.role;
|
|
1206
|
+
if (answers.verb && answers.verb !== currentVerb)
|
|
1207
|
+
patch.verb = answers.verb;
|
|
1208
|
+
if (answers.label_es || answers.label_en) {
|
|
1209
|
+
patch.label_i18n = {};
|
|
1210
|
+
if (answers.label_es)
|
|
1211
|
+
patch.label_i18n.es = answers.label_es;
|
|
1212
|
+
if (answers.label_en)
|
|
1213
|
+
patch.label_i18n.en = answers.label_en;
|
|
1214
|
+
}
|
|
1215
|
+
if (Object.keys(patch).length === 0) {
|
|
1216
|
+
console.log(c.dim('No changes -- nothing to write.'));
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const override = {
|
|
1220
|
+
match_by: 'nac_id',
|
|
1221
|
+
match_value: el.id,
|
|
1222
|
+
patch,
|
|
1223
|
+
rationale: answers.rationale || undefined,
|
|
1224
|
+
locked_at: new Date().toISOString(),
|
|
1225
|
+
locked_by: process.env.USER || process.env.USERNAME || 'cli',
|
|
1226
|
+
};
|
|
1227
|
+
/* Apply on top of current files (not just store -- want immediate effect). */
|
|
1228
|
+
const html = await fs.readFile(htmlPath, 'utf-8');
|
|
1229
|
+
const { html: patchedHtml, manifest: patchedManifest, result: applyResult } = applyOverrides(html, manifest, [override]);
|
|
1230
|
+
if (applyResult.applied.length === 0) {
|
|
1231
|
+
console.error(c.error('Override application failed (orphaned).'));
|
|
1232
|
+
process.exitCode = 1;
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
await fs.writeFile(htmlPath, patchedHtml, 'utf-8');
|
|
1236
|
+
await fs.writeFile(manifestPath, JSON.stringify(patchedManifest, null, 2), 'utf-8');
|
|
1237
|
+
/* Append to overrides.json. */
|
|
1238
|
+
const existing = await readOverrides(dir);
|
|
1239
|
+
const file = existing || {
|
|
1240
|
+
version: OVERRIDES_VERSION,
|
|
1241
|
+
baseline_hash: computeBaselineHash(html, JSON.stringify(manifest)),
|
|
1242
|
+
overrides: [],
|
|
1243
|
+
};
|
|
1244
|
+
/* Replace any prior override on the same nac_id; otherwise append. */
|
|
1245
|
+
const ix = file.overrides.findIndex((o) => o.match_value === el.id);
|
|
1246
|
+
if (ix >= 0)
|
|
1247
|
+
file.overrides[ix] = override;
|
|
1248
|
+
else
|
|
1249
|
+
file.overrides.push(override);
|
|
1250
|
+
await writeOverrides(dir, file);
|
|
1251
|
+
console.log('');
|
|
1252
|
+
console.log(c.success('Override applied + persisted.'));
|
|
1253
|
+
console.log(' ' + c.dim('overrides.json now carries ') + file.overrides.length + c.dim(' entry(ies).'));
|
|
1254
|
+
console.log(' ' + c.dim('Re-running --ai-silent will preserve this override.'));
|
|
1255
|
+
}
|
|
1256
|
+
/* ------------------------------------------------------------------
|
|
1257
|
+
* --ai-apply: Claude-powered JSX decoration of a React project
|
|
1258
|
+
* ------------------------------------------------------------------ */
|
|
1259
|
+
export async function runAiApply(repo, opts) {
|
|
1260
|
+
header('Yujin Forge -- AI-powered JSX decoration (--ai-apply)');
|
|
1261
|
+
console.log('');
|
|
1262
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
1263
|
+
console.error(c.error('ANTHROPIC_API_KEY env variable missing -- set it before --ai-apply.'));
|
|
1264
|
+
process.exitCode = 1;
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const lic = await loadLicense();
|
|
1268
|
+
if (!isPaidSeat(lic)) {
|
|
1269
|
+
console.error(c.error('--ai-apply requires a paid seat.'));
|
|
1270
|
+
console.error(' ' + c.dim('Trial mode produces the audit only. Run ')
|
|
1271
|
+
+ c.code('yf license activate --key <jwt>')
|
|
1272
|
+
+ c.dim(' to install a seat.'));
|
|
1273
|
+
process.exitCode = 1;
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const projectRoot = path.resolve(process.cwd(), repo);
|
|
1277
|
+
const subdir = opts.subdir || 'src';
|
|
1278
|
+
const outDir = opts.aiOut
|
|
1279
|
+
? path.resolve(process.cwd(), opts.aiOut)
|
|
1280
|
+
: undefined;
|
|
1281
|
+
const concurrency = opts.aiConcurrency
|
|
1282
|
+
? Math.max(1, Math.min(parseInt(opts.aiConcurrency, 10) || 4, 8))
|
|
1283
|
+
: 4;
|
|
1284
|
+
console.log(' Project: ' + c.dim(projectRoot));
|
|
1285
|
+
console.log(' Subdir: ' + c.dim(subdir));
|
|
1286
|
+
console.log(' Output: ' + c.dim(outDir || '(in-place)'));
|
|
1287
|
+
console.log(' Plugin hint: ' + c.dim(opts.aiPluginSlug || '(model derives)'));
|
|
1288
|
+
console.log(' Model: ' + c.dim(opts.aiModel || 'claude-sonnet-4-6'));
|
|
1289
|
+
console.log(' Concurrency: ' + c.dim(String(concurrency)));
|
|
1290
|
+
console.log(' Dry-run: ' + c.dim(opts.dryRun ? 'YES (no files written)' : 'no'));
|
|
1291
|
+
console.log('');
|
|
1292
|
+
let lastReport = 0;
|
|
1293
|
+
const i18nLangs = opts.i18nLangs
|
|
1294
|
+
? opts.i18nLangs.split(',').map(s => s.trim()).filter(Boolean)
|
|
1295
|
+
: undefined;
|
|
1296
|
+
const result = await aiApplyProject({
|
|
1297
|
+
projectRoot,
|
|
1298
|
+
subdir,
|
|
1299
|
+
outDir,
|
|
1300
|
+
pluginSlugHint: opts.aiPluginSlug,
|
|
1301
|
+
model: opts.aiModel,
|
|
1302
|
+
dryRun: opts.dryRun,
|
|
1303
|
+
concurrency,
|
|
1304
|
+
withI18n: !!opts.withI18nTags,
|
|
1305
|
+
i18nLangs,
|
|
1306
|
+
onProgress: (i, total, rel, r) => {
|
|
1307
|
+
/* Throttle progress to one line every ~5 files or last file. */
|
|
1308
|
+
if (i === total || i - lastReport >= 5) {
|
|
1309
|
+
const mark = r.decorated ? c.success('+') : c.dim('.');
|
|
1310
|
+
console.log(' ' + mark + ' [' + i + '/' + total + '] ' + rel +
|
|
1311
|
+
(r.decorated ? c.dim(' (' + r.elements.length + ' element' + (r.elements.length === 1 ? '' : 's') + ')') : ''));
|
|
1312
|
+
lastReport = i;
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
});
|
|
1316
|
+
/* Persist top-level manifest alongside the project (or the outDir). */
|
|
1317
|
+
const manifestTargetDir = outDir || projectRoot;
|
|
1318
|
+
const manifestPath = path.join(manifestTargetDir, 'manifest.json');
|
|
1319
|
+
if (!opts.dryRun) {
|
|
1320
|
+
await fs.mkdir(manifestTargetDir, { recursive: true });
|
|
1321
|
+
await fs.writeFile(manifestPath, JSON.stringify(result.manifest, null, 2), 'utf-8');
|
|
1322
|
+
}
|
|
1323
|
+
/* SPA wiring: if the project has an index.html at root (Vite / CRA /
|
|
1324
|
+
similar bundler patterns) and a public/ dir, copy nac.browser.js
|
|
1325
|
+
+ nac-bridge.js + manifest into public/ and inject the tags into
|
|
1326
|
+
index.html. Skip when the file is already wired (idempotent). */
|
|
1327
|
+
if (!opts.dryRun) {
|
|
1328
|
+
await wireSpaIndexHtml({
|
|
1329
|
+
projectRoot: manifestTargetDir,
|
|
1330
|
+
pluginSlug: result.manifest.plugin_slug,
|
|
1331
|
+
manifestJson: JSON.stringify(result.manifest),
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
console.log('');
|
|
1335
|
+
console.log(c.success('AI apply complete.'));
|
|
1336
|
+
console.log(' ' + c.dim('Files scanned: ') + result.files.length);
|
|
1337
|
+
console.log(' ' + c.dim('Files decorated: ') + result.files.filter((f) => f.decorated).length);
|
|
1338
|
+
console.log(' ' + c.dim('Manifest elements: ') + result.manifest.elements.length);
|
|
1339
|
+
console.log(' ' + c.dim('Plugin slug: ') + result.manifest.plugin_slug);
|
|
1340
|
+
/* H2: surface qualifier warnings (reusable components without an
|
|
1341
|
+
inferable qualifier). These are the "N DOM instances collide on
|
|
1342
|
+
the same id" cases that destroy agent disambiguation if hidden. */
|
|
1343
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
1344
|
+
console.log('');
|
|
1345
|
+
console.log(c.warn('Qualifier warnings -- ') + result.warnings.length + c.warn(' reusable component(s) without an inferable qualifier:'));
|
|
1346
|
+
for (const w of result.warnings) {
|
|
1347
|
+
console.log(' ' + c.warn('!') + ' ' + c.dim(w.file) + ' ' + c.brand(w.id));
|
|
1348
|
+
console.log(' ' + c.dim(w.warning));
|
|
1349
|
+
}
|
|
1350
|
+
console.log(' ' + c.dim('Each of these will collapse N DOM instances onto a single nac_id.'));
|
|
1351
|
+
console.log(' ' + c.dim('Disambiguate with: ') + c.code('yf migrate ' + repo + ' --ai-override <nac-id>'));
|
|
1352
|
+
}
|
|
1353
|
+
console.log(' ' + c.dim('Tokens in/out: ') + result.total_tokens_in + ' / ' + result.total_tokens_out);
|
|
1354
|
+
console.log(' ' + c.dim('Total cost: ') + '$' + result.total_cost_usd.toFixed(4));
|
|
1355
|
+
console.log(' ' + c.dim('Total LLM time: ') + Math.round(result.total_latency_ms / 1000) + 's wall (across all files)');
|
|
1356
|
+
if (!opts.dryRun) {
|
|
1357
|
+
console.log(' ' + c.dim('Manifest: ') + manifestPath);
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
console.log(' ' + c.warn('Dry-run -- no files written. Re-run without --dry-run to apply.'));
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async function wireSpaIndexHtml(opts) {
|
|
1364
|
+
const indexPath = path.join(opts.projectRoot, "index.html");
|
|
1365
|
+
const has = await fs.stat(indexPath).then(() => true, () => false);
|
|
1366
|
+
if (!has)
|
|
1367
|
+
return;
|
|
1368
|
+
let html = await fs.readFile(indexPath, "utf-8");
|
|
1369
|
+
if (/nac\.browser\.js/.test(html)) {
|
|
1370
|
+
/* Already wired (idempotent re-run). */
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
/* Resolve nac.browser.js source. */
|
|
1374
|
+
const runtimePath = await resolveNacRuntime();
|
|
1375
|
+
const publicDir = path.join(opts.projectRoot, "public");
|
|
1376
|
+
const hasPublic = await fs.stat(publicDir).then(() => true, () => false);
|
|
1377
|
+
if (!hasPublic) {
|
|
1378
|
+
await fs.mkdir(publicDir, { recursive: true });
|
|
1379
|
+
}
|
|
1380
|
+
if (runtimePath) {
|
|
1381
|
+
await fs.copyFile(runtimePath, path.join(publicDir, "nac.browser.js"));
|
|
1382
|
+
}
|
|
1383
|
+
await fs.writeFile(path.join(publicDir, "nac-bridge.js"), NAC_BRIDGE_SOURCE, "utf-8");
|
|
1384
|
+
await fs.writeFile(path.join(publicDir, "nac-manifest.json"), opts.manifestJson, "utf-8");
|
|
1385
|
+
const runtimeTag = "<script src=\"/nac.browser.js\"></script>";
|
|
1386
|
+
const bridgeTag = "<script src=\"/nac-bridge.js\"></script>";
|
|
1387
|
+
const registerInline = "<script>\n (function () {\n function reg() {\n fetch(\"/nac-manifest.json\").then(function (r) { return r.json(); }).then(function (m) {\n if (window.NAC && window.NAC.register) {\n try { window.NAC.register(m); }\n catch (e) { if (window.console) console.error(\"NAC.register failed\", e); }\n }\n });\n }\n if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", reg);\n else reg();\n })();\n</script>";
|
|
1388
|
+
/* 1. Inject runtime tag before </head>. */
|
|
1389
|
+
if (/<\/head>/i.test(html)) {
|
|
1390
|
+
html = html.replace(/<\/head>/i, runtimeTag + "\n</head>");
|
|
1391
|
+
}
|
|
1392
|
+
else {
|
|
1393
|
+
html = runtimeTag + "\n" + html;
|
|
1394
|
+
}
|
|
1395
|
+
/* 2. Add data-nac-plugin to body. */
|
|
1396
|
+
html = html.replace(/<body([^>]*)>/i, (m, attrs) => {
|
|
1397
|
+
if (/data-nac-plugin/.test(attrs))
|
|
1398
|
+
return m;
|
|
1399
|
+
return "<body" + attrs + " data-nac-plugin=\"" + opts.pluginSlug + "\">";
|
|
1400
|
+
});
|
|
1401
|
+
/* 3. Inject bridge + register before </body>. */
|
|
1402
|
+
if (/<\/body>/i.test(html)) {
|
|
1403
|
+
html = html.replace(/<\/body>/i, bridgeTag + "\n" + registerInline + "\n</body>");
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
html += "\n" + bridgeTag + "\n" + registerInline;
|
|
1407
|
+
}
|
|
1408
|
+
await fs.writeFile(indexPath, html, "utf-8");
|
|
1409
|
+
}
|
|
1410
|
+
//# sourceMappingURL=migrate.js.map
|