@sun-asterisk/sungen 3.2.31 → 3.2.33
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/dist/cli/commands/audit.d.ts +1 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +13 -2
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +46 -17
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +39 -18
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +14 -7
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts.map +1 -1
- package/dist/cli/commands/results-merge.js +2 -12
- package/dist/cli/commands/results-merge.js.map +1 -1
- package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
- package/dist/cli/commands/results-sanitize.js +2 -13
- package/dist/cli/commands/results-sanitize.js.map +1 -1
- package/dist/cli/commands/setup-mobile-env.d.ts +15 -0
- package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
- package/dist/cli/commands/setup-mobile-env.js +65 -0
- package/dist/cli/commands/setup-mobile-env.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +6 -10
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +21 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +32 -1
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/locale-variants.d.ts +7 -1
- package/dist/exporters/locale-variants.d.ts.map +1 -1
- package/dist/exporters/locale-variants.js +23 -16
- package/dist/exporters/locale-variants.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +13 -7
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
- package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
- package/dist/exporters/matrix/carry-manual-results.js +232 -0
- package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +2 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +7 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +7 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +27 -1
- package/dist/exporters/matrix/wording.js.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +2 -0
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +7 -2
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
- package/dist/generators/test-generator/code-generator.d.ts +2 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +122 -15
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +70 -4
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +69 -3
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +13 -3
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +16 -10
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +15 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +81 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +187 -13
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +18 -0
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +39 -8
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
- package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +84 -4
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +229 -82
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +35 -8
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability.d.ts +7 -1
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +15 -12
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
- package/dist/harness/config-file.d.ts +17 -0
- package/dist/harness/config-file.d.ts.map +1 -0
- package/dist/harness/config-file.js +100 -0
- package/dist/harness/config-file.js.map +1 -0
- package/dist/harness/depth-lint.d.ts.map +1 -1
- package/dist/harness/depth-lint.js +2 -1
- package/dist/harness/depth-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +6 -2
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/keyword-match.d.ts +59 -0
- package/dist/harness/keyword-match.d.ts.map +1 -0
- package/dist/harness/keyword-match.js +86 -0
- package/dist/harness/keyword-match.js.map +1 -0
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +2 -1
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +42 -7
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/perf.d.ts +7 -1
- package/dist/harness/perf.d.ts.map +1 -1
- package/dist/harness/perf.js +13 -10
- package/dist/harness/perf.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +9 -2
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +16 -4
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -1
- package/dist/harness/read-text.d.ts +6 -0
- package/dist/harness/read-text.d.ts.map +1 -1
- package/dist/harness/read-text.js +7 -1
- package/dist/harness/read-text.js.map +1 -1
- package/dist/harness/secret-scan.d.ts +27 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +57 -4
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/sensors.d.ts +12 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +41 -18
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-branches.d.ts +7 -20
- package/dist/harness/spec-branches.d.ts.map +1 -1
- package/dist/harness/spec-branches.js +85 -13
- package/dist/harness/spec-branches.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +16 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +131 -12
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +2 -1
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/harness/write-atomic.d.ts +2 -0
- package/dist/harness/write-atomic.d.ts.map +1 -0
- package/dist/harness/write-atomic.js +62 -0
- package/dist/harness/write-atomic.js.map +1 -0
- package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
- package/dist/orchestrator/ai-skills/writer.js +15 -7
- package/dist/orchestrator/ai-skills/writer.js.map +1 -1
- package/dist/orchestrator/appium-environment-setup.d.ts +24 -0
- package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
- package/dist/orchestrator/appium-environment-setup.js +157 -0
- package/dist/orchestrator/appium-environment-setup.js.map +1 -0
- package/dist/orchestrator/assets-drift.d.ts.map +1 -1
- package/dist/orchestrator/assets-drift.js +11 -6
- package/dist/orchestrator/assets-drift.js.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +2 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
- package/dist/orchestrator/project-deps-sync.js +22 -1
- package/dist/orchestrator/project-deps-sync.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +9 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +63 -7
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
- package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
- package/dist/orchestrator/security-overrides-catalog.js +63 -0
- package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
- package/dist/orchestrator/security-overrides.d.ts +51 -0
- package/dist/orchestrator/security-overrides.d.ts.map +1 -0
- package/dist/orchestrator/security-overrides.js +165 -0
- package/dist/orchestrator/security-overrides.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
- package/dist/orchestrator/templates/appium-environment/README.md +83 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
- package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
- package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
- package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
- package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
- package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
- package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-checked-state.js +26 -3
- package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
- package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
- package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
- package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-mock.js +10 -3
- package/dist/orchestrator/templates/specs-mock.js.map +1 -1
- package/dist/orchestrator/templates/specs-mock.ts +12 -4
- package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
- package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.js +14 -2
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.ts +24 -2
- package/dist/orchestrator/templates/wdio.conf.ts +24 -1
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +12 -3
- package/src/cli/commands/capability.ts +63 -19
- package/src/cli/commands/delivery.ts +38 -16
- package/src/cli/commands/generate.ts +15 -7
- package/src/cli/commands/init.ts +3 -0
- package/src/cli/commands/results-merge.ts +2 -8
- package/src/cli/commands/results-sanitize.ts +2 -9
- package/src/cli/commands/setup-mobile-env.ts +64 -0
- package/src/cli/index.ts +2 -0
- package/src/dashboard/snapshot-builder.ts +6 -8
- package/src/exporters/feature-parser.ts +32 -2
- package/src/exporters/locale-variants.ts +23 -17
- package/src/exporters/matrix/build.ts +14 -8
- package/src/exporters/matrix/carry-manual-results.ts +229 -0
- package/src/exporters/matrix/export.ts +10 -4
- package/src/exporters/matrix/wording.ts +26 -1
- package/src/exporters/xlsx-exporter.ts +7 -2
- package/src/generators/test-generator/adapters/adapter-interface.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
- package/src/generators/test-generator/code-generator.ts +119 -19
- package/src/generators/test-generator/diagnostics.ts +70 -4
- package/src/generators/test-generator/patterns/index.ts +16 -9
- package/src/generators/test-generator/patterns/types.ts +15 -0
- package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
- package/src/generators/test-generator/step-mapper.ts +196 -13
- package/src/generators/test-generator/template-engine.ts +18 -0
- package/src/generators/test-generator/utils/data-resolver.ts +41 -12
- package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +256 -85
- package/src/harness/audit.ts +41 -12
- package/src/harness/capability.ts +23 -12
- package/src/harness/catalog/universal-viewpoints.yaml +5 -5
- package/src/harness/config-file.ts +66 -0
- package/src/harness/depth-lint.ts +2 -1
- package/src/harness/flow-check.ts +6 -2
- package/src/harness/keyword-match.ts +85 -0
- package/src/harness/next-step.ts +2 -1
- package/src/harness/parse.ts +42 -7
- package/src/harness/perf.ts +15 -7
- package/src/harness/quality-gates.ts +16 -4
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/read-text.ts +7 -1
- package/src/harness/secret-scan.ts +60 -4
- package/src/harness/sensors.ts +50 -19
- package/src/harness/spec-branches.ts +72 -14
- package/src/harness/spec-coverage.ts +133 -13
- package/src/harness/viewpoint-ledger.ts +2 -1
- package/src/harness/write-atomic.ts +22 -0
- package/src/orchestrator/ai-skills/writer.ts +18 -3
- package/src/orchestrator/appium-environment-setup.ts +127 -0
- package/src/orchestrator/assets-drift.ts +10 -5
- package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
- package/src/orchestrator/project-deps-sync.ts +23 -1
- package/src/orchestrator/project-initializer.ts +69 -7
- package/src/orchestrator/security-overrides-catalog.ts +69 -0
- package/src/orchestrator/security-overrides.ts +149 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
- package/src/orchestrator/templates/appium-environment/README.md +83 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
- package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
- package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
- package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
- package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
- package/src/orchestrator/templates/specs-checked-state.ts +31 -6
- package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/src/orchestrator/templates/specs-mock.ts +12 -4
- package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/src/orchestrator/templates/specs-url-assert.ts +24 -2
- package/src/orchestrator/templates/wdio.conf.ts +24 -1
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* explicit — a named heading, a table that declares conditions and outcomes, or an if/else
|
|
17
17
|
* cascade — because a false "you missed a branch" costs more trust than it buys coverage.
|
|
18
18
|
*/
|
|
19
|
+
import { keywordHit } from './keyword-match';
|
|
19
20
|
import { ScenarioInfo } from './parse';
|
|
20
21
|
|
|
21
22
|
export interface SpecBranch {
|
|
@@ -172,7 +173,9 @@ export function unenumeratedBranches(branches: SpecBranch[], featureText: string
|
|
|
172
173
|
const words = [...new Set((b.label.toLowerCase().match(/[a-z][a-z-]{3,}/g) ?? []))]
|
|
173
174
|
.filter((w) => !GENERIC_WORD.has(w));
|
|
174
175
|
if (words.length === 0) return !hay.includes(b.label.toLowerCase());
|
|
175
|
-
|
|
176
|
+
// Word-aware (#779): `in·active` and `check·list` are not mentions of `active` and `list`,
|
|
177
|
+
// and two of them credited a branch no scenario cites.
|
|
178
|
+
const hits = words.filter((w) => keywordHit(hay, w)).length;
|
|
176
179
|
return hits < Math.min(2, words.length);
|
|
177
180
|
});
|
|
178
181
|
}
|
|
@@ -204,38 +207,93 @@ export function sameOracleClusters(scenarios: ScenarioInfo[]): Array<{ oracle: s
|
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
/**
|
|
207
|
-
* A "
|
|
210
|
+
* A "hidden" scenario with no "shown" counterpart (#630, widened in #679).
|
|
208
211
|
*
|
|
209
212
|
* Two security cases proved that chat content is hidden when `MESSAGE_READ` is denied. If the code
|
|
210
213
|
* hid it unconditionally — ignoring the permission entirely — both still pass. The deny side alone
|
|
211
214
|
* cannot distinguish "the guard works" from "the feature is broken for everyone", so the pair is
|
|
212
215
|
* the test, exactly as `min-1 / min / max / max+1` is the test for a boundary.
|
|
213
216
|
*
|
|
214
|
-
* The
|
|
217
|
+
* The first version gated on PERMISSION vocabulary, and "show A or B, never both" is far broader
|
|
218
|
+
* than permission: a conditional button, an empty-vs-populated state, an active tab. A field report
|
|
219
|
+
* lost exactly that case — "the score button is hidden once a score exists" was written, "it is
|
|
220
|
+
* shown while no score is set" was not, and nothing asked for it. So the trigger is now the
|
|
221
|
+
* ELEMENT the assertion names: an element this suite proves ABSENT and never proves PRESENT is
|
|
222
|
+
* unpaired, whatever the condition is called. That also catches the vacuous form — a selector that
|
|
223
|
+
* matches nothing passes `is hidden` forever.
|
|
224
|
+
*
|
|
225
|
+
* Permission stays its own `kind`, because the repair advice differs and its wording is what the
|
|
226
|
+
* skill teaches.
|
|
215
227
|
*/
|
|
216
228
|
const DENY_SHAPE = /\b(?:denied|deny|denies|forbidden|not authoriz\w*|unauthoriz\w*|no permission|without (?:the )?(?:permission|right|access|flag)|permission (?:is )?off|flag (?:is )?off|revoked|restricted)\b/i;
|
|
217
229
|
const HIDDEN_SHAPE = /\b(?:is hidden|are hidden|not (?:visible|shown|displayed)|blocked|masked|redacted|does not (?:show|render|appear))\b/i;
|
|
218
230
|
const ALLOW_SHAPE = /\b(?:granted|allowed|permitted|authoriz\w*|has (?:the )?(?:permission|right|access)|permission (?:is )?on|flag (?:is )?on|with (?:the )?(?:permission|right|access))\b/i;
|
|
219
231
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
/** Absence in sungen's own assertion grammar — `is hidden`, `does not exist`, `not exists`. */
|
|
233
|
+
const ABSENT_ASSERTION = /\bis hidden\b|\bare hidden\b|\bnot (?:visible|shown|displayed)\b|\bdoes not (?:exist|show|appear|render)\b|\bnot exists?\b/i;
|
|
234
|
+
|
|
235
|
+
export interface VisibilityPairGap {
|
|
236
|
+
scenario: string;
|
|
237
|
+
subject: string[];
|
|
238
|
+
/** The element reference the pair is about, when the assertion names one. */
|
|
239
|
+
element?: string;
|
|
240
|
+
/** `permission` keeps the deny/allow repair advice; `condition` is any other state. */
|
|
241
|
+
kind: 'permission' | 'condition';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** What a scenario ASSERTS — the Then side. Hand-built fixtures carry no buckets; use their steps. */
|
|
245
|
+
function thenLines(s: ScenarioInfo): string[] {
|
|
246
|
+
const thens = (s.steps ?? []).filter((st) => st.bucket === 'then').map((st) => st.text.toLowerCase());
|
|
247
|
+
if (thens.length) return thens;
|
|
248
|
+
return s.stepsText ? [s.stepsText] : [];
|
|
249
|
+
}
|
|
250
|
+
/** Every `[ref]` of a line. */
|
|
251
|
+
function refsIn(line: string): string[] {
|
|
252
|
+
return [...line.matchAll(/\[([^\]]{1,60})\]/g)].map((m) => m[1].trim().toLowerCase());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function visibilityPairGaps(scenarios: ScenarioInfo[]): VisibilityPairGap[] {
|
|
256
|
+
// Every element the suite proves PRESENT, anywhere. A positive assertion names its element and
|
|
257
|
+
// any element it is scoped INSIDE (`[Row] row in [Table] table`) — both are demonstrably there.
|
|
258
|
+
const shown = new Set<string>();
|
|
259
|
+
for (const s of scenarios) {
|
|
260
|
+
for (const l of thenLines(s)) {
|
|
261
|
+
if (ABSENT_ASSERTION.test(l)) continue;
|
|
262
|
+
for (const ref of refsIn(l)) shown.add(ref);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
226
265
|
// Words only — the scenario ID is stripped first, because `[a-z][a-z-]{4,}` happily matches
|
|
227
266
|
// `fl-er-` and an id fragment can never appear in the counterpart's title, so demanding it made
|
|
228
267
|
// the pair impossible to find.
|
|
229
268
|
const distinctive = (s: ScenarioInfo): string[] =>
|
|
230
269
|
[...new Set((s.name.replace(/^\S+\s*/, '').toLowerCase().match(/[a-z]{5,}/g) ?? []))]
|
|
231
270
|
.filter((w) => !GENERIC_WORD.has(w) && !/denied|denies|hidden|permission|scenario/.test(w));
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
271
|
+
const allows = scenarios.filter((s) => ALLOW_SHAPE.test(s.haystack) && !HIDDEN_SHAPE.test(s.stepsText));
|
|
272
|
+
const out: VisibilityPairGap[] = [];
|
|
273
|
+
const reported = new Set<string>();
|
|
274
|
+
for (const s of scenarios) {
|
|
275
|
+
const absent = thenLines(s).filter((l) => ABSENT_ASSERTION.test(l));
|
|
276
|
+
if (!absent.length && !(DENY_SHAPE.test(s.haystack) && HIDDEN_SHAPE.test(s.haystack))) continue;
|
|
277
|
+
const kind = DENY_SHAPE.test(s.haystack) ? 'permission' : 'condition';
|
|
278
|
+
// Only the FIRST ref of an absence line is its subject: in `see [Row] row in [Table] table … is
|
|
279
|
+
// hidden` the table is the scope, the row is what went away.
|
|
280
|
+
const subjects = [...new Set(absent.map((l) => refsIn(l)[0]).filter((r): r is string => !!r))];
|
|
281
|
+
if (subjects.length) {
|
|
282
|
+
for (const el of subjects) {
|
|
283
|
+
if (shown.has(el) || reported.has(el)) continue;
|
|
284
|
+
reported.add(el);
|
|
285
|
+
out.push({ scenario: s.name, subject: distinctive(s).slice(0, 4), element: el, kind });
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// The assertion names no element — the permission vocabulary is then the only safe trigger,
|
|
290
|
+
// paired on the scenario's distinctive title words as before.
|
|
291
|
+
if (kind !== 'permission' || !HIDDEN_SHAPE.test(s.haystack)) continue;
|
|
292
|
+
const subject = distinctive(s);
|
|
235
293
|
// The counterpart has to be about the SAME thing — two shared distinctive words, the same
|
|
236
294
|
// bar the ledger's prose matching uses.
|
|
237
|
-
const paired = allows.some((a) => subject.filter((w) => a.haystack
|
|
238
|
-
if (!paired) out.push({ scenario:
|
|
295
|
+
const paired = allows.some((a) => subject.filter((w) => keywordHit(a.haystack, w)).length >= Math.min(2, subject.length));
|
|
296
|
+
if (!paired) out.push({ scenario: s.name, subject: subject.slice(0, 4), kind: 'permission' });
|
|
239
297
|
}
|
|
240
298
|
return out;
|
|
241
299
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Generic: no project data.
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
|
+
import { keywordHit } from './keyword-match';
|
|
12
13
|
import { ScenarioInfo } from './parse';
|
|
13
14
|
import { readTextFile } from './read-text';
|
|
14
15
|
|
|
@@ -16,6 +17,12 @@ export type Modality = 'MUST' | 'SHOULD' | 'MAY';
|
|
|
16
17
|
|
|
17
18
|
export interface TriggerGap { constraint: string; code: string; required: string[]; found: string[]; missing: string[] }
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* One DIRECTION of a requirement that states both — `MUST be shown … MUST NOT be shown …` (#679).
|
|
22
|
+
* The id is cited, so the requirement looks covered; this names the half nothing asserts.
|
|
23
|
+
*/
|
|
24
|
+
export interface ClauseGap { id: string; polarity: 'affirm' | 'negate'; text: string }
|
|
25
|
+
|
|
19
26
|
export interface SpecCoverageResult {
|
|
20
27
|
hasSpec: boolean;
|
|
21
28
|
frTotal: number;
|
|
@@ -27,10 +34,13 @@ export interface SpecCoverageResult {
|
|
|
27
34
|
* requirement table and any later refactor cannot follow it. */
|
|
28
35
|
inferredOnly: string[];
|
|
29
36
|
triggerGaps: TriggerGap[]; // per-constraint trigger matrix gaps
|
|
37
|
+
/** Requirements whose two opposing clauses are not BOTH proved (#679). */
|
|
38
|
+
clauseGaps: ClauseGap[];
|
|
30
39
|
verdict: 'pass' | 'warn' | 'fail';
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
interface
|
|
42
|
+
export interface FrDirection { polarity: 'affirm' | 'negate'; text: string }
|
|
43
|
+
interface FrClause { id: string; text: string; modality: Modality; directions?: FrDirection[] }
|
|
34
44
|
interface ValRow { constraint: string; code: string; triggers: string[] }
|
|
35
45
|
|
|
36
46
|
// Parsing the spec's Trigger CELL: loose word match ("blur, submit").
|
|
@@ -55,6 +65,90 @@ function modalityOf(text: string): Modality {
|
|
|
55
65
|
return 'MAY';
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
/**
|
|
69
|
+
* A requirement that states BOTH directions in one sentence — "`X` MUST be shown when no score is
|
|
70
|
+
* set; MUST NOT be shown once a score exists" (#679).
|
|
71
|
+
*
|
|
72
|
+
* One `FrClause` per LINE carried one modality for the whole sentence, and coverage was a binary
|
|
73
|
+
* check per id: ONE scenario tagged `@spec:FR-013` closed both halves. The half that went missing
|
|
74
|
+
* in the field was the MUST NOT one — the classic "both buttons rendered at once" defect, reported
|
|
75
|
+
* as covered 100% on the first generation.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately narrow, three ways at once, because the finding is GATE-BLOCKING:
|
|
78
|
+
* - only a HARD prohibition paired with a hard obligation ("MUST be shown and enabled" is one
|
|
79
|
+
* direction and stays one unit);
|
|
80
|
+
* - only a requirement that NAMES its element, backticked or bracketed — the direction evidence
|
|
81
|
+
* is an assertion ABOUT something, and without a subject there is nothing to look for;
|
|
82
|
+
* - only a VISIBILITY/enabledness requirement, because that is the vocabulary the evidence test
|
|
83
|
+
* reads. "MUST validate the email format on submit. MUST NOT allow submission when it is
|
|
84
|
+
* invalid." is an opposing pair in prose and NOT one here: nothing in a correct suite for it
|
|
85
|
+
* asserts absence, so judging it would fail a suite with no gap in it.
|
|
86
|
+
*/
|
|
87
|
+
const VISIBILITY_CLAUSE = /\b(?:shown|shows?|displayed|displays?|visible|hidden|hides?|rendered|renders?|appears?|enabled|disabled)\b|表示|hiển thị/i;
|
|
88
|
+
const PROHIBITION = /\b(?:must not|must never|shall not|shall never)\b|KHÔNG ĐƯỢC|してはならない/i;
|
|
89
|
+
const OBLIGATION = /\b(?:must|shall)\b|\bPHẢI\b/i;
|
|
90
|
+
|
|
91
|
+
function splitDirections(text: string): FrDirection[] | undefined {
|
|
92
|
+
if (!VISIBILITY_CLAUSE.test(text) || elementRefs(text).length === 0) return undefined;
|
|
93
|
+
const parts = text.split(/\s*(?:;|;|。|\.\s+|,?\s+but\s+)\s*/i).map((p) => p.trim()).filter(Boolean);
|
|
94
|
+
if (parts.length < 2) return undefined;
|
|
95
|
+
const affirm: string[] = [];
|
|
96
|
+
const negate: string[] = [];
|
|
97
|
+
for (const part of parts) {
|
|
98
|
+
if (PROHIBITION.test(part)) negate.push(part);
|
|
99
|
+
else if (OBLIGATION.test(part)) affirm.push(part);
|
|
100
|
+
}
|
|
101
|
+
if (!affirm.length || !negate.length) return undefined;
|
|
102
|
+
return [
|
|
103
|
+
{ polarity: 'affirm', text: affirm.join('; ') },
|
|
104
|
+
{ polarity: 'negate', text: negate.join('; ') },
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What the clause is ABOUT: a backticked or bracketed element name is the one distinctive token. */
|
|
109
|
+
function elementRefs(text: string): string[] {
|
|
110
|
+
return [...new Set(
|
|
111
|
+
[...text.matchAll(/`([^`]+)`|\[([^\]]+)\]/g)]
|
|
112
|
+
.map((m) => (m[1] ?? m[2]).trim().toLowerCase())
|
|
113
|
+
.filter((x) => x.length >= 2 && x.length <= 60),
|
|
114
|
+
)];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Absence, in sungen's own assertion grammar (`is hidden`, `does not exist`) plus the prose a spec
|
|
119
|
+
* restatement uses. Positive visibility has no marker — `User see [T]` never says "is visible" —
|
|
120
|
+
* so the affirm side is "an assertion that is not a denial", which is what the suite actually writes.
|
|
121
|
+
*/
|
|
122
|
+
const NEGATIVE_ASSERTION = /\bis hidden\b|\bare hidden\b|\bnot (?:visible|shown|displayed|present)\b|\bdoes not (?:exist|show|appear|render)\b|\bnot exists?\b|\bno longer\b|表示されない|không hiển thị/i;
|
|
123
|
+
|
|
124
|
+
/** Assertion lines of a scenario block — Then plus the And/But that inherit its bucket. */
|
|
125
|
+
function assertionLines(block: string): string[] {
|
|
126
|
+
const out: string[] = [];
|
|
127
|
+
let bucket = '';
|
|
128
|
+
for (const raw of block.split('\n')) {
|
|
129
|
+
const kw = raw.trim().match(/^(given|when|then|and|but)\s+/);
|
|
130
|
+
if (!kw) continue;
|
|
131
|
+
if (kw[1] !== 'and' && kw[1] !== 'but') bucket = kw[1];
|
|
132
|
+
if (bucket === 'then') out.push(raw.trim());
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Does this scenario block assert THIS direction of the requirement? */
|
|
138
|
+
function directionCovered(block: string, dir: FrDirection, subject: string[]): boolean {
|
|
139
|
+
const lines = assertionLines(block);
|
|
140
|
+
const pool = lines.length ? lines : [block];
|
|
141
|
+
// Narrow to the lines about the clause's element when the suite names it the same way the spec
|
|
142
|
+
// does; a spec in one language and selector keys in another must not read as "no evidence".
|
|
143
|
+
// Word-aware (#779), or the narrowing lets in a line about something else and then trusts it:
|
|
144
|
+
// `under·score` is not a mention of `Score`, but it selected the only absence assertion in the
|
|
145
|
+
// block and so proved a MUST NOT clause about the Score. (`keywordHit` keeps substring semantics
|
|
146
|
+
// for a non-ASCII element name, which is what the cross-language case above depends on.)
|
|
147
|
+
const about = subject.length ? pool.filter((l) => subject.some((x) => keywordHit(l, x))) : [];
|
|
148
|
+
const scan = about.length ? about : pool;
|
|
149
|
+
return scan.some((l) => NEGATIVE_ASSERTION.test(l) === (dir.polarity === 'negate'));
|
|
150
|
+
}
|
|
151
|
+
|
|
58
152
|
function specTriggersIn(text: string): string[] {
|
|
59
153
|
return SPEC_TRIGGER.filter((t) => t.re.test(text)).map((t) => t.trigger);
|
|
60
154
|
}
|
|
@@ -86,7 +180,10 @@ export function parseSpecClauses(specPath: string): { frs: FrClause[]; valRows:
|
|
|
86
180
|
const id = m[1].toUpperCase();
|
|
87
181
|
if (seen.has(id)) continue; // first declaration wins
|
|
88
182
|
seen.add(id);
|
|
89
|
-
|
|
183
|
+
const text = m[2].replace(/\*\*/g, '').trim();
|
|
184
|
+
const modality = modalityOf(text);
|
|
185
|
+
// A MUST/MUST NOT pair is TWO requirements wearing one id (#679) — carry both directions.
|
|
186
|
+
frs.push({ id, text, modality, directions: modality === 'MUST' ? splitDirections(text) : undefined });
|
|
90
187
|
}
|
|
91
188
|
|
|
92
189
|
// Validation Rules table: a row carries a Constraint, a Trigger cell, and (often) a code.
|
|
@@ -156,40 +253,63 @@ export function citedIds(featureText: string): Set<string> {
|
|
|
156
253
|
export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featureText: string): SpecCoverageResult {
|
|
157
254
|
const { frs, valRows } = parseSpecClauses(specPath);
|
|
158
255
|
if (!fs.existsSync(specPath) || (frs.length === 0 && valRows.length === 0)) {
|
|
159
|
-
return { hasSpec: fs.existsSync(specPath), frTotal: 0, frCovered: 0, uncoveredMust: [], inferredOnly: [], triggerGaps: [], verdict: 'pass' };
|
|
256
|
+
return { hasSpec: fs.existsSync(specPath), frTotal: 0, frCovered: 0, uncoveredMust: [], inferredOnly: [], triggerGaps: [], clauseGaps: [], verdict: 'pass' };
|
|
160
257
|
}
|
|
161
258
|
const featLower = featureText.toLowerCase();
|
|
162
259
|
const cites = citedIds(featureText);
|
|
260
|
+
const blocks = scenarioBlocks(featureText);
|
|
163
261
|
|
|
164
262
|
// FR coverage: explicit @spec:FR / literal FR-id citation, else keyword fallback.
|
|
165
263
|
const uncoveredMust: { id: string; text: string }[] = [];
|
|
264
|
+
const clauseGaps: ClauseGap[] = [];
|
|
166
265
|
const inferredOnly: string[] = [];
|
|
266
|
+
let frTotal = 0;
|
|
167
267
|
let frCovered = 0;
|
|
168
268
|
for (const fr of frs) {
|
|
269
|
+
// A requirement stating both directions is TWO units — counting it as one is what let a suite
|
|
270
|
+
// holding only the MUST half read 100% covered (#679).
|
|
271
|
+
const units = fr.directions ? fr.directions.length : 1;
|
|
272
|
+
frTotal += units;
|
|
169
273
|
const idLower = fr.id.toLowerCase();
|
|
170
274
|
const cited = featLower.includes(idLower) || cites.has(fr.id.toUpperCase());
|
|
171
275
|
const words = [...new Set((fr.text.toLowerCase().match(/[a-z][a-z-]{4,}/g) || []))]
|
|
172
276
|
.filter((w) => !/must|should|system|screen|users?|value|input|field/.test(w));
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
277
|
+
// Word-aware, not substring (#779). `haystack.includes('format')` is satisfied by
|
|
278
|
+
// in·format·ion and `includes('length')` by wave·length, and two such collisions clear the
|
|
279
|
+
// `>= 2 words` bar as easily as one — so a MUST clause nobody tested read as covered. The
|
|
280
|
+
// threshold is a separate judgement (is one word enough evidence?) and is unchanged; see
|
|
281
|
+
// keyword-match.ts for why the boundary is leading-only.
|
|
282
|
+
const kwHit = words.length > 0 && scenarios.some((s) => words.filter((w) => keywordHit(s.haystack, w)).length >= Math.min(2, words.length));
|
|
283
|
+
if (!cited && !kwHit) {
|
|
284
|
+
if (fr.modality === 'MUST') uncoveredMust.push({ id: fr.id, text: fr.text.slice(0, 90) });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (!cited) inferredOnly.push(fr.id); // covered, but nothing links it explicitly
|
|
288
|
+
if (!fr.directions) { frCovered++; continue; }
|
|
289
|
+
// Which scenarios speak for this requirement — by its id (tags and comments sit in the block)
|
|
290
|
+
// or by the same keyword rule used above.
|
|
291
|
+
const owning = blocks.filter((b) => b.includes(idLower)
|
|
292
|
+
|| (words.length > 0 && words.filter((w) => keywordHit(b, w)).length >= Math.min(2, words.length)));
|
|
293
|
+
// Cited from outside any scenario (a header comment): nothing to judge a direction against, so
|
|
294
|
+
// report nothing rather than invent a gap.
|
|
295
|
+
if (!owning.length) { frCovered += units; continue; }
|
|
296
|
+
const subject = elementRefs(fr.text);
|
|
297
|
+
for (const dir of fr.directions) {
|
|
298
|
+
if (owning.some((b) => directionCovered(b, dir, subject))) frCovered++;
|
|
299
|
+
else if (fr.modality === 'MUST') clauseGaps.push({ id: fr.id, polarity: dir.polarity, text: dir.text.slice(0, 90) });
|
|
179
300
|
}
|
|
180
301
|
}
|
|
181
302
|
|
|
182
303
|
// Per-constraint trigger coverage — the matrix-collapse catch.
|
|
183
304
|
// generic words that don't identify a constraint (would over-match happy-path scenarios)
|
|
184
305
|
const GENERIC = new Set(['valid', 'local', 'part', 'domain', 'email', 'address', 'must', 'exist', 'store', 'field', 'input', 'value', 'formed', 'well', 'character', 'characters']);
|
|
185
|
-
const blocks = scenarioBlocks(featureText);
|
|
186
306
|
const triggerGaps: TriggerGap[] = [];
|
|
187
307
|
for (const row of valRows) {
|
|
188
308
|
const kw = (row.constraint.toLowerCase().match(/[a-z]{4,}/g) || []).filter((w) => !GENERIC.has(w));
|
|
189
309
|
// blocks that belong to this constraint: cite its msg code (primary), or mention a
|
|
190
310
|
// DISTINCTIVE constraint word (generic words filtered to avoid matching happy paths).
|
|
191
311
|
const own = blocks.filter((b) =>
|
|
192
|
-
(row.code && b.includes(row.code.toLowerCase())) || kw.some((w) => b
|
|
312
|
+
(row.code && b.includes(row.code.toLowerCase())) || kw.some((w) => keywordHit(b, w)),
|
|
193
313
|
);
|
|
194
314
|
if (!own.length) continue; // constraint has no scenarios at all — FR/viewpoint gate covers that
|
|
195
315
|
const found = [...new Set(own.flatMap((b) => actionTriggersIn(b)))];
|
|
@@ -198,9 +318,9 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
|
|
|
198
318
|
}
|
|
199
319
|
|
|
200
320
|
const verdict: SpecCoverageResult['verdict'] =
|
|
201
|
-
uncoveredMust.length > 0 || triggerGaps.length > 0 ? 'fail' : 'pass';
|
|
321
|
+
uncoveredMust.length > 0 || triggerGaps.length > 0 || clauseGaps.length > 0 ? 'fail' : 'pass';
|
|
202
322
|
|
|
203
|
-
return { hasSpec: true, frTotal
|
|
323
|
+
return { hasSpec: true, frTotal, frCovered, uncoveredMust, inferredOnly, triggerGaps, clauseGaps, verdict };
|
|
204
324
|
}
|
|
205
325
|
|
|
206
326
|
/**
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import { ScenarioInfo } from './parse';
|
|
13
13
|
import { readTextFile } from './read-text';
|
|
14
|
+
import { keywordHit } from './keyword-match';
|
|
14
15
|
|
|
15
16
|
export interface LedgerItem { id?: string; text: string; covered: boolean }
|
|
16
17
|
|
|
@@ -198,7 +199,7 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
|
|
|
198
199
|
} else {
|
|
199
200
|
const words = [...new Set((item.text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w)))];
|
|
200
201
|
const need = Math.min(2, words.length);
|
|
201
|
-
isCovered = words.length > 0 && scenarios.some((s) => words.filter((w) => s.haystack
|
|
202
|
+
isCovered = words.length > 0 && scenarios.some((s) => words.filter((w) => keywordHit(s.haystack, w)).length >= need);
|
|
202
203
|
}
|
|
203
204
|
if (isCovered) covered++;
|
|
204
205
|
else missing.push({ id: item.id, text: item.text });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a file so that a crash mid-write can never leave a truncated one behind: write a sibling
|
|
3
|
+
* tmp, then rename. Same directory = same device, so the rename is atomic; a concurrent writer is
|
|
4
|
+
* last-rename-wins. On failure the tmp is removed and the error rethrown — the caller still learns.
|
|
5
|
+
*
|
|
6
|
+
* `results merge` and `results sanitize` each grew this block inline, for the same reason and with
|
|
7
|
+
* the same comment. The third caller (`ai-skills/writer.ts`) did not, and its truncated manifest is
|
|
8
|
+
* precisely the corrupt input that #782's readers then absorbed in silence — so the two halves of
|
|
9
|
+
* that defect are one file apart.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
|
|
13
|
+
export function writeFileAtomic(file: string, data: string): void {
|
|
14
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
15
|
+
try {
|
|
16
|
+
fs.writeFileSync(tmp, data);
|
|
17
|
+
fs.renameSync(tmp, file);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup — the real error is below */ }
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -16,6 +16,8 @@ import * as path from 'path';
|
|
|
16
16
|
import * as crypto from 'crypto';
|
|
17
17
|
import { BuiltFile } from './build';
|
|
18
18
|
import { AdapterKey } from './adapters';
|
|
19
|
+
import { readJsonConfig, warnUnreadableConfig } from '../../harness/config-file';
|
|
20
|
+
import { writeFileAtomic } from '../../harness/write-atomic';
|
|
19
21
|
|
|
20
22
|
export interface ApplyOptions { force?: boolean; dryRun?: boolean; version: string }
|
|
21
23
|
export interface ApplyReport {
|
|
@@ -42,10 +44,21 @@ function adapterOf(relPath: string): AdapterKey | null {
|
|
|
42
44
|
return null;
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The manifest is what makes orphan cleanup possible: it is the only record of which files a
|
|
49
|
+
* PREVIOUS version managed. An unreadable one used to read as an empty one — so cleanup found
|
|
50
|
+
* nothing to remove, and the stale files it failed to remove are absent from the NEW manifest too.
|
|
51
|
+
* No later run can ever find them again. Say it, then carry on with what can still be done.
|
|
52
|
+
*/
|
|
45
53
|
function readManifest(cwd: string): Manifest {
|
|
46
54
|
const p = path.join(cwd, MANIFEST_REL);
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
const read = readJsonConfig<Manifest>(p);
|
|
56
|
+
if (read.state === 'unreadable') {
|
|
57
|
+
warnUnreadableConfig(p, read.error,
|
|
58
|
+
'orphan cleanup is skipped this run, so files managed by an older version stay in place');
|
|
59
|
+
return { version: '', managed: {} };
|
|
60
|
+
}
|
|
61
|
+
return read.state === 'ok' ? read.value : { version: '', managed: {} };
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
export function applyOutputs(cwd: string, outputs: BuiltFile[], opts: ApplyOptions): ApplyReport {
|
|
@@ -121,7 +134,9 @@ export function applyOutputs(cwd: string, outputs: BuiltFile[], opts: ApplyOptio
|
|
|
121
134
|
Object.assign(mergedManaged, newManaged);
|
|
122
135
|
const mPath = path.join(cwd, MANIFEST_REL);
|
|
123
136
|
fs.mkdirSync(path.dirname(mPath), { recursive: true });
|
|
124
|
-
|
|
137
|
+
// Atomic: an interrupted `sungen init`/`update` must not leave a half-written manifest, because
|
|
138
|
+
// the reader above can only report that damage — it cannot undo it.
|
|
139
|
+
writeFileAtomic(mPath, JSON.stringify({ version: opts.version, managed: mergedManaged }, null, 2));
|
|
125
140
|
}
|
|
126
141
|
return r;
|
|
127
142
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appium environment bundle — emit `appium-environment/` into a mobile project and run its entry point.
|
|
3
|
+
*
|
|
4
|
+
* The bundle (bash for macOS, PowerShell for Windows) provisions the OS layer BELOW Appium: JDK, Android
|
|
5
|
+
* SDK + env vars, Xcode CLT, applesimutils, then the project-local Appium platform drivers, and ends with
|
|
6
|
+
* `appium driver doctor`. Emitted by `sungen init --mobile` / `sungen capability add mobile` and run once
|
|
7
|
+
* right after the driver install (spec: docs/spec/sungen_mobile_environment_setup_spec.md, issue #718).
|
|
8
|
+
*
|
|
9
|
+
* Core ships the scripts as templates only — no runtime is embedded, nothing is installed globally.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { spawnSync } from 'child_process';
|
|
14
|
+
|
|
15
|
+
export const APPIUM_ENV_DIR = 'appium-environment';
|
|
16
|
+
|
|
17
|
+
export interface EmitResult {
|
|
18
|
+
created: string[];
|
|
19
|
+
skipped: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type EnvSetupOutcome = 'ran' | 'failed' | 'skipped' | 'unsupported';
|
|
23
|
+
|
|
24
|
+
/** Bundled template root — resolves from src/orchestrator (tsx) and dist/orchestrator (copy-templates). */
|
|
25
|
+
const TEMPLATE_ROOT = path.join(__dirname, 'templates', APPIUM_ENV_DIR);
|
|
26
|
+
|
|
27
|
+
/** Files under `dir`, relative and posix-separated, sorted for a deterministic emit order. */
|
|
28
|
+
function listTemplateFiles(dir: string, prefix = ''): string[] {
|
|
29
|
+
const out: string[] = [];
|
|
30
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
33
|
+
if (entry.isDirectory()) out.push(...listTemplateFiles(path.join(dir, entry.name), rel));
|
|
34
|
+
else out.push(rel);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Copy the bundle into `<cwd>/appium-environment/`, skipping every file that already exists (a QA may
|
|
41
|
+
* have edited the pinned cmdline-tools build or a hint — same idempotency as the other emitted files).
|
|
42
|
+
* `*.sh` get the executable bit explicitly: npm and git do not reliably preserve it.
|
|
43
|
+
*/
|
|
44
|
+
export function emitAppiumEnvironment(cwd: string): EmitResult {
|
|
45
|
+
const created: string[] = [];
|
|
46
|
+
const skipped: string[] = [];
|
|
47
|
+
// Non-fatal like the other provisioning steps: a broken install (templates missing) must not abort
|
|
48
|
+
// `init` / `capability add` — the harness scaffold is still valid without the bundle.
|
|
49
|
+
try {
|
|
50
|
+
for (const rel of listTemplateFiles(TEMPLATE_ROOT)) {
|
|
51
|
+
const dest = path.join(cwd, APPIUM_ENV_DIR, rel);
|
|
52
|
+
const label = `${APPIUM_ENV_DIR}/${rel}`;
|
|
53
|
+
if (fs.existsSync(dest)) { skipped.push(label); continue; }
|
|
54
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
55
|
+
fs.copyFileSync(path.join(TEMPLATE_ROOT, rel), dest);
|
|
56
|
+
if (rel.endsWith('.sh')) fs.chmodSync(dest, 0o755);
|
|
57
|
+
created.push(label);
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.warn(`⚠️ Could not emit ${APPIUM_ENV_DIR}/ (${e instanceof Error ? e.message : e}) — reinstall @sun-asterisk/sungen and re-run: sungen capability add mobile`);
|
|
61
|
+
}
|
|
62
|
+
return { created, skipped };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The command a QA re-runs by hand, for hints and the init summary. */
|
|
66
|
+
export function appiumEnvironmentRerunCommand(): string {
|
|
67
|
+
return process.platform === 'win32'
|
|
68
|
+
? `powershell -NoProfile -ExecutionPolicy Bypass -File .\\${APPIUM_ENV_DIR}\\setup.ps1`
|
|
69
|
+
: `./${APPIUM_ENV_DIR}/setup.sh`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Translate the bundle's POSIX flags to their PowerShell switches. `sungen setup-mobile-env` speaks
|
|
74
|
+
* one flag vocabulary on both OSes, so the user does not have to know which script is underneath.
|
|
75
|
+
*/
|
|
76
|
+
const PS_SWITCH: Record<string, string> = {
|
|
77
|
+
'--check': '-Check',
|
|
78
|
+
'--skip-android': '-SkipAndroid',
|
|
79
|
+
'--no-profile': '-NoProfile',
|
|
80
|
+
// `--skip-ios` has no Windows counterpart: setup.ps1 has no iOS steps to skip.
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function entryCommand(cwd: string, flags: string[] = []): { file: string; args: string[] } | null {
|
|
84
|
+
const bundle = path.join(cwd, APPIUM_ENV_DIR);
|
|
85
|
+
if (process.platform === 'win32') {
|
|
86
|
+
const switches = flags.map(f => PS_SWITCH[f]).filter((s): s is string => !!s);
|
|
87
|
+
return {
|
|
88
|
+
file: 'powershell',
|
|
89
|
+
// -NoProfile here is powershell.exe's own "skip the user profile" flag, unrelated to the
|
|
90
|
+
// bundle's -NoProfile switch; the latter arrives through `switches` after -File.
|
|
91
|
+
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', path.join(bundle, 'setup.ps1'), ...switches],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// setup.sh handles Darwin; on Linux it prints the manual-path notice and exits 0.
|
|
95
|
+
if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
96
|
+
return { file: 'bash', args: [path.join(bundle, 'setup.sh'), ...flags] };
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run the emitted entry point for this OS with inherited stdio — its per-step output IS the UX, and any
|
|
103
|
+
* Homebrew / sudo prompt must reach the user. Non-fatal by contract: problems are reported with the re-run
|
|
104
|
+
* command and init continues, because the scaffold itself is already complete.
|
|
105
|
+
*/
|
|
106
|
+
export function runAppiumEnvironmentSetup(
|
|
107
|
+
cwd: string,
|
|
108
|
+
opts: { skip?: boolean; flags?: string[] } = {},
|
|
109
|
+
): EnvSetupOutcome {
|
|
110
|
+
if (opts.skip) {
|
|
111
|
+
console.log(`– skipped environment setup (--skip-env-setup) — run it later: sungen setup-mobile-env`);
|
|
112
|
+
return 'skipped';
|
|
113
|
+
}
|
|
114
|
+
const cmd = entryCommand(cwd, opts.flags);
|
|
115
|
+
if (!cmd) {
|
|
116
|
+
console.log(`ℹ️ environment setup ships for macOS (setup.sh) and Windows (setup.ps1) — on ${process.platform} follow ${APPIUM_ENV_DIR}/README.md → Manual fallback`);
|
|
117
|
+
return 'unsupported';
|
|
118
|
+
}
|
|
119
|
+
console.log(`\n🔧 Provisioning the Appium environment (${APPIUM_ENV_DIR}/) — idempotent, re-run any time...`);
|
|
120
|
+
const result = spawnSync(cmd.file, cmd.args, { cwd, stdio: 'inherit' });
|
|
121
|
+
if (result.status === 0) return 'ran';
|
|
122
|
+
// status null = the shell itself could not start (ENOENT) or was killed — name the real cause.
|
|
123
|
+
const exit = result.error ? `could not start ${cmd.file}: ${result.error.message}`
|
|
124
|
+
: result.status === null ? `signal ${result.signal ?? '?'}` : `exit ${result.status}`;
|
|
125
|
+
console.warn(`⚠️ environment setup reported problems (${exit}) — fix the ✗ items above, then re-run: sungen setup-mobile-env (or ${appiumEnvironmentRerunCommand()} directly)`);
|
|
126
|
+
return 'failed';
|
|
127
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { getPackageVersion } from '../exporters/package-info';
|
|
4
|
+
import { readJsonConfig, warnUnreadableConfig } from '../harness/config-file';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Are this project's AI assets (commands + skills under `.claude/` etc.) the ones
|
|
@@ -27,12 +28,16 @@ export function checkAssetsDrift(cwd: string): AssetsDrift | null {
|
|
|
27
28
|
const manifestPath = path.join(cwd, '.sungen', 'manifest.json');
|
|
28
29
|
if (!fs.existsSync(manifestPath)) return null; // not a sungen project (yet)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const read = readJsonConfig<{ version?: string; managed?: Record<string, string> }>(manifestPath);
|
|
32
|
+
if (read.state === 'unreadable') {
|
|
33
|
+
// This function IS the drift alarm. Staying silent on a broken manifest disables the only
|
|
34
|
+
// warning that a project's .claude/ assets have gone stale — and a truncated manifest is the
|
|
35
|
+
// likeliest way to get one (it is written non-atomically by `ai-skills/writer.ts`).
|
|
36
|
+
warnUnreadableConfig(manifestPath, read.error, 'stale-assets drift cannot be checked — re-run `sungen init` to rewrite it');
|
|
37
|
+
return null;
|
|
35
38
|
}
|
|
39
|
+
if (read.state === 'absent') return null;
|
|
40
|
+
const manifest = read.value;
|
|
36
41
|
const recordedVersion = manifest.version ?? '';
|
|
37
42
|
const runningVersion = getPackageVersion();
|
|
38
43
|
const managed = manifest.managed ?? {};
|
|
@@ -22,6 +22,8 @@ export interface MobileInitOptions {
|
|
|
22
22
|
iosBundleId?: string;
|
|
23
23
|
iosDevice?: string;
|
|
24
24
|
iosVersion?: string;
|
|
25
|
+
/** Emit `appium-environment/` but do not run it now (`--skip-env-setup`); QA re-runs it by hand. */
|
|
26
|
+
skipEnvSetup?: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface ScaffoldResult {
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from 'child_process';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { resolveDriverVersion } from './lockstep-version';
|
|
5
|
+
import { ensureSecurityOverrides, reportSecurityOverrides } from './security-overrides';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Project-local sync of the sungen package family after a global `sungen update`.
|
|
@@ -143,8 +144,29 @@ export function syncProjectSungenDeps(cwd: string, core: string, dryRun: boolean
|
|
|
143
144
|
}
|
|
144
145
|
for (const s of plan.skipped) console.log(` – skipped ${s.pkg} — ${s.why}`);
|
|
145
146
|
|
|
147
|
+
// A project scaffolded before #764 carries no security overrides, and npm will never pick them up
|
|
148
|
+
// from sungen's own manifest — `sungen update` is the one pass that can put them there. Write them
|
|
149
|
+
// before any install below, since npm reads `overrides` when it resolves.
|
|
150
|
+
const overridePlan = ensureSecurityOverrides(path.join(cwd, 'package.json'), dryRun);
|
|
151
|
+
reportSecurityOverrides(overridePlan, dryRun);
|
|
152
|
+
|
|
146
153
|
if (!plan.specs.length) {
|
|
147
|
-
console.log(` ✓ already on ${core}: ${plan.found.map((f) => f.pkg).join(', ')}
|
|
154
|
+
console.log(` ✓ already on ${core}: ${plan.found.map((f) => f.pkg).join(', ')}`);
|
|
155
|
+
// Family already in lockstep, so nothing would re-resolve — but freshly written overrides only
|
|
156
|
+
// reach the tree through a resolution pass. Run one.
|
|
157
|
+
if (overridePlan?.add.length) {
|
|
158
|
+
if (dryRun) {
|
|
159
|
+
console.log(' [dry-run] would run: npm install (re-resolve under the new security overrides)\n');
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
console.log(' 📦 npm install (re-resolving under the new security overrides)');
|
|
163
|
+
const r = spawnSync('npm', ['install'], { cwd, stdio: 'inherit', shell: true });
|
|
164
|
+
if (r.status !== 0) {
|
|
165
|
+
console.warn('⚠️ Overrides were written to package.json but `npm install` failed — run it by hand.\n');
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
console.log('');
|
|
148
170
|
return true;
|
|
149
171
|
}
|
|
150
172
|
|