@thebackstoryis/engineering-with-ai 0.2.9
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/Docs/README.md +50 -0
- package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
- package/Docs/adoption/non-technical-team-guide.md +126 -0
- package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
- package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
- package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
- package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
- package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
- package/Docs/cli-reference.md +113 -0
- package/Docs/completed-phase-evidence-amendments.md +74 -0
- package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
- package/Docs/context-aware-delivery-companion-guide.md +198 -0
- package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
- package/Docs/context-management-and-token-efficiency.md +113 -0
- package/Docs/design-systems/design-system-implementation-guide.md +85 -0
- package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
- package/Docs/design-systems/design-system-review-guide.md +51 -0
- package/Docs/design-systems/design-system-user-guide.md +96 -0
- package/Docs/design-systems/product-owner-guide.md +49 -0
- package/Docs/designing-organisation-blueprint-packs.md +384 -0
- package/Docs/developer-delivery-guide.md +224 -0
- package/Docs/error-reporting-guide.md +110 -0
- package/Docs/error-reporting-provider-guide.md +49 -0
- package/Docs/examples/error-report-adapter.md +70 -0
- package/Docs/examples/meeting-review.md +76 -0
- package/Docs/examples/minimal-design-system.md +67 -0
- package/Docs/examples/prototype-review-inputs.md +175 -0
- package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
- package/Docs/examples/test-scenario-input.md +68 -0
- package/Docs/examples/worked-examples.md +147 -0
- package/Docs/existing-project-onboarding-guide.md +214 -0
- package/Docs/explanation/core-concepts.md +26 -0
- package/Docs/explanation/delivery-workflow.md +48 -0
- package/Docs/governance/governance-team-guide.md +139 -0
- package/Docs/governed-starter-project-materialisation-guide.md +284 -0
- package/Docs/guide-catalogue.md +117 -0
- package/Docs/guided-discovery-facilitator-guide.md +172 -0
- package/Docs/guided-intent-workspace-guide.md +109 -0
- package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
- package/Docs/human-approval-and-assurance-guide.md +146 -0
- package/Docs/knowledge-proposals-implementer-guide.md +106 -0
- package/Docs/knowledge-proposals-user-guide.md +247 -0
- package/Docs/maintainers/context-benchmarks.md +29 -0
- package/Docs/maintainers/contributing.md +58 -0
- package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
- package/Docs/maintainers/verification-walkthroughs.md +104 -0
- package/Docs/meeting-evidence-implementer-guide.md +132 -0
- package/Docs/meeting-evidence-user-guide.md +200 -0
- package/Docs/operations/dashboard-and-delivery-state.md +153 -0
- package/Docs/operations/dashboard-configuration.md +87 -0
- package/Docs/operations/installation-updating-and-entitlements.md +135 -0
- package/Docs/operations/premium-personas-setup.md +76 -0
- package/Docs/operations/troubleshooting-and-recovery.md +205 -0
- package/Docs/organisation-rollout-guide.md +142 -0
- package/Docs/persona-entitlement-provider-guide.md +199 -0
- package/Docs/persona-guided-prototype-iteration.md +129 -0
- package/Docs/personas/organisation-specific-personas.md +103 -0
- package/Docs/personas/persona-authoring-cookbook.md +176 -0
- package/Docs/personas/persona-engagement-ui.md +133 -0
- package/Docs/personas/persona-governance.md +118 -0
- package/Docs/platform-export-analysis-guide.md +336 -0
- package/Docs/policies/governance-owner-guide.md +36 -0
- package/Docs/policies/implementation-guide.md +42 -0
- package/Docs/policies/organisation-policy-design-gates.md +58 -0
- package/Docs/policies/policy-pack-authoring-guide.md +108 -0
- package/Docs/policies/product-owner-guide.md +43 -0
- package/Docs/policies/technical-owner-guide.md +37 -0
- package/Docs/product-owner-guide.md +327 -0
- package/Docs/project-portfolio-orchestration-guide.md +199 -0
- package/Docs/quality/manual-qa-and-acceptance.md +162 -0
- package/Docs/quality/persona-driven-test-scenarios.md +172 -0
- package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
- package/Docs/reference/capabilities-and-project-layout.md +678 -0
- package/Docs/reference/cli-and-configuration.md +398 -0
- package/Docs/reference/contributions-api.md +23 -0
- package/Docs/reference/security-adapter-authoring.md +81 -0
- package/Docs/reference/starter-adapter-authoring.md +74 -0
- package/Docs/repository-source-map-guide.md +381 -0
- package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
- package/Docs/screen-prototype-creation-guide.md +324 -0
- package/Docs/security-validation-guide.md +353 -0
- package/Docs/solution-readiness-review-guide.md +123 -0
- package/Docs/standards/project-standards-authoring.md +157 -0
- package/Docs/team-hub-guide.md +162 -0
- package/Docs/team-hub-resource-registry-guide.md +167 -0
- package/Docs/tutorials/first-delivery.md +83 -0
- package/Docs/tutorials/first-session.md +62 -0
- package/Docs/using-lifecycle-hooks.md +381 -0
- package/Docs/working-with-personas.md +274 -0
- package/LICENSE +165 -0
- package/README.md +96 -0
- package/agents-src/claude/ewai-security-reviewer.md +15 -0
- package/bin/ewai +5 -0
- package/config/archaeology-record-families.yaml +59 -0
- package/config/delivery-artifacts.yaml +121 -0
- package/config/delivery-stages.yaml +77 -0
- package/config/design-system.schema.json +46 -0
- package/config/error-reporting.schema.json +79 -0
- package/config/evidence-depth.schema.json +53 -0
- package/config/intent.schema.json +90 -0
- package/config/knowledge-proposals-proposal.schema.json +34 -0
- package/config/lifecycle-event.schema.json +68 -0
- package/config/lifecycle-handler.schema.json +45 -0
- package/config/lifecycle-hook-ack.schema.json +19 -0
- package/config/meeting-evidence-candidate.schema.json +102 -0
- package/config/organisation-policy.schema.json +137 -0
- package/config/pack.schema.json +250 -0
- package/config/persona-pack.schema.json +21 -0
- package/config/persona.schema.json +17 -0
- package/config/policy-evaluation.schema.json +77 -0
- package/config/policy-facts.schema.json +140 -0
- package/config/portfolio.schema.json +68 -0
- package/config/project.schema.json +313 -0
- package/config/prototype-iteration.schema.json +128 -0
- package/config/rollout.schema.json +87 -0
- package/config/security-adapter.schema.json +31 -0
- package/config/security-scan-request.schema.json +64 -0
- package/config/security-scan-response.schema.json +52 -0
- package/config/security-validation-policy.schema.json +74 -0
- package/config/starter-source-acknowledgement.schema.json +13 -0
- package/config/starter-source-adapter.schema.json +38 -0
- package/config/starter-source-request.schema.json +61 -0
- package/package.json +77 -0
- package/packs/core/pack.yaml +7 -0
- package/packs/design-systems/default/experience-promise.md +9 -0
- package/packs/design-systems/default/intentional-review.md +10 -0
- package/packs/design-systems/default/interaction-and-entry.md +9 -0
- package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
- package/packs/design-systems/default/pack.yaml +44 -0
- package/packs/design-systems/default/principles.md +10 -0
- package/packs/personas/core/pack.yaml +7 -0
- package/packs/personas/core/personas/archaeologist.md +37 -0
- package/packs/personas/core/personas/end-user.md +17 -0
- package/packs/personas/core/personas/maintainer.md +17 -0
- package/packs/personas/core/personas/operator.md +17 -0
- package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
- package/packs/technologies/laravel/pack.yaml +30 -0
- package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
- package/packs/technologies/nuxt/pack.yaml +30 -0
- package/packs/technologies/power-platform/pack.yaml +31 -0
- package/packs/technologies/salesforce/pack.yaml +25 -0
- package/public/app.js +4896 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/backstory-icon.png +0 -0
- package/public/dashboard-navigation.js +98 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +789 -0
- package/public/styles.css +2693 -0
- package/public/team-hub/app.js +202 -0
- package/public/team-hub/index.html +79 -0
- package/public/team-hub/styles.css +90 -0
- package/scripts/publication-check.mjs +140 -0
- package/scripts/setup.mjs +21 -0
- package/skills-src/ewai-archaeology/SKILL.md +334 -0
- package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
- package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
- package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
- package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
- package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
- package/skills-src/ewai-architecture/SKILL.md +108 -0
- package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
- package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
- package/skills-src/ewai-context/SKILL.md +68 -0
- package/skills-src/ewai-context/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/SKILL.md +118 -0
- package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
- package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
- package/skills-src/ewai-deliver/SKILL.md +122 -0
- package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
- package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
- package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
- package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
- package/skills-src/ewai-design-system-author/SKILL.md +28 -0
- package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
- package/skills-src/ewai-design-system-review/SKILL.md +26 -0
- package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
- package/skills-src/ewai-error-reporting/SKILL.md +46 -0
- package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
- package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
- package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
- package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
- package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
- package/skills-src/ewai-intent/SKILL.md +68 -0
- package/skills-src/ewai-intent/agents/openai.yaml +4 -0
- package/skills-src/ewai-intent/references/intent-contract.md +42 -0
- package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
- package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
- package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
- package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
- package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
- package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
- package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
- package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
- package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
- package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
- package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
- package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
- package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
- package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/SKILL.md +130 -0
- package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/references/cli.md +86 -0
- package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
- package/skills-src/ewai-portfolio/SKILL.md +70 -0
- package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
- package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
- package/skills-src/ewai-project-discovery/SKILL.md +95 -0
- package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
- package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
- package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
- package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
- package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
- package/skills-src/ewai-retro/SKILL.md +48 -0
- package/skills-src/ewai-retro/agents/openai.yaml +4 -0
- package/skills-src/ewai-retro/references/asset-routing.md +14 -0
- package/skills-src/ewai-rollout/SKILL.md +74 -0
- package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
- package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
- package/skills-src/ewai-shape-intents/SKILL.md +84 -0
- package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
- package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
- package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
- package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/SKILL.md +93 -0
- package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
- package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
- package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
- package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
- package/src/afk-worker.mjs +16 -0
- package/src/archaeology.mjs +1333 -0
- package/src/checkin.mjs +261 -0
- package/src/cli.mjs +2427 -0
- package/src/companion-guidance.mjs +257 -0
- package/src/companion-opening.mjs +62 -0
- package/src/companion.mjs +256 -0
- package/src/context.mjs +210 -0
- package/src/dashboard-preferences.mjs +80 -0
- package/src/delivery-artifacts.mjs +204 -0
- package/src/delivery-documents.mjs +248 -0
- package/src/delivery-gates.mjs +317 -0
- package/src/delivery.mjs +1433 -0
- package/src/design-system-application.mjs +291 -0
- package/src/design-system-authoring.mjs +101 -0
- package/src/design-systems.mjs +466 -0
- package/src/discovery.mjs +1314 -0
- package/src/error-reporting.mjs +323 -0
- package/src/evidence-depth.mjs +543 -0
- package/src/execution-state.mjs +243 -0
- package/src/install.mjs +166 -0
- package/src/intent-dependencies.mjs +117 -0
- package/src/intent-maps.mjs +402 -0
- package/src/intents.mjs +747 -0
- package/src/knowledge-proposals.mjs +717 -0
- package/src/launcher.mjs +51 -0
- package/src/meeting-evidence.mjs +703 -0
- package/src/network-rollout.mjs +386 -0
- package/src/organisation-blueprints.mjs +438 -0
- package/src/organisation-policies.mjs +448 -0
- package/src/packs.mjs +44 -0
- package/src/paths.mjs +62 -0
- package/src/persona-entitlements.mjs +438 -0
- package/src/persona-licence-config.mjs +98 -0
- package/src/persona-website-provider.mjs +134 -0
- package/src/persona-zip.mjs +87 -0
- package/src/personas.mjs +159 -0
- package/src/platform-metadata-analysis.mjs +314 -0
- package/src/policy-design-gates.mjs +623 -0
- package/src/policy-gate-integration.mjs +318 -0
- package/src/portfolio.mjs +509 -0
- package/src/power-platform-source-map.mjs +190 -0
- package/src/project.mjs +449 -0
- package/src/prototype-iterations.mjs +730 -0
- package/src/repository-source-map.mjs +603 -0
- package/src/runtime/afk-conductor.mjs +973 -0
- package/src/runtime/context-assembly.mjs +457 -0
- package/src/runtime/context-benchmarks.mjs +115 -0
- package/src/runtime/dashboard-actions.mjs +109 -0
- package/src/runtime/dashboard-handoffs.mjs +149 -0
- package/src/runtime/dashboard-server.mjs +1272 -0
- package/src/runtime/dashboard.mjs +197 -0
- package/src/runtime/database.mjs +789 -0
- package/src/runtime/error-reporting.mjs +581 -0
- package/src/runtime/evidence-depth-workspace.mjs +412 -0
- package/src/runtime/execution-leases.mjs +299 -0
- package/src/runtime/guided-discovery.mjs +350 -0
- package/src/runtime/guided-intents.mjs +517 -0
- package/src/runtime/impact-analysis.mjs +535 -0
- package/src/runtime/intents.mjs +222 -0
- package/src/runtime/knowledge.mjs +86 -0
- package/src/runtime/lifecycle-hooks.mjs +1239 -0
- package/src/runtime/mcp-config.mjs +110 -0
- package/src/runtime/mcp-server.mjs +1885 -0
- package/src/runtime/palace.mjs +362 -0
- package/src/runtime/paths.mjs +58 -0
- package/src/runtime/persona-engagement.mjs +255 -0
- package/src/runtime/phase-contributions.mjs +594 -0
- package/src/runtime/policy-workspace.mjs +170 -0
- package/src/runtime/prototype-iterations.mjs +235 -0
- package/src/runtime/provider-adapters.mjs +163 -0
- package/src/runtime/repository-index.mjs +838 -0
- package/src/runtime/runs.mjs +185 -0
- package/src/runtime/security-validation.mjs +1230 -0
- package/src/runtime/starter-materialisation.mjs +1155 -0
- package/src/runtime/team-hub-client.mjs +479 -0
- package/src/runtime/team-hub-database.mjs +288 -0
- package/src/runtime/team-hub-server.mjs +191 -0
- package/src/runtime/team-hub.mjs +110 -0
- package/src/runtime/tree-sitter-index.mjs +390 -0
- package/src/runtime/version.mjs +1 -0
- package/src/runtime/work.mjs +633 -0
- package/src/salesforce-source-map.mjs +212 -0
- package/src/security-validation-config.mjs +224 -0
- package/src/solution-readiness.mjs +620 -0
- package/src/starter-materialisation-contract.mjs +407 -0
- package/src/task-graph.mjs +544 -0
- package/src/team-hub-resources.mjs +239 -0
- package/src/team-hub.mjs +242 -0
- package/src/test-scenarios.mjs +622 -0
- package/src/validation-config.mjs +289 -0
- package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
- package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
- package/templates/SPECS/6.Build/_tracker-template.md +16 -0
- package/templates/SPECS/pipeline.yaml +62 -0
- package/templates/discovery-answers.yaml +86 -0
- package/templates/intent-body.md +21 -0
- package/tests/fixtures/context-benchmarks.json +9 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
accessSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
existsSync,
|
|
8
|
+
fstatSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
16
|
+
import { loadProjectConfig } from './project.mjs';
|
|
17
|
+
|
|
18
|
+
export const STARTER_MATERIALISATION_NOTICE = 'Governed Starter Pack materialisation runs explicitly trusted local adapter code with the invoking user\'s permissions; it is not OS-sandboxed. EWAI independently verifies staged content, permits additive-only publication, and requires named human approval. Results are evidence, not security certification, code-quality approval, licence approval or release approval.';
|
|
19
|
+
export const STARTER_ADAPTER_TRUST_NOTICE = 'A registered starter source adapter is trusted local code. EWAI does not OS-sandbox it, provide credentials, or authorise its output without independent verification.';
|
|
20
|
+
export const STARTER_SOURCE_CLASSES = Object.freeze(['https', 'git', 'archive', 'directory', 'package', 'opaque']);
|
|
21
|
+
|
|
22
|
+
const sourceClassSet = new Set(STARTER_SOURCE_CLASSES);
|
|
23
|
+
const slugPattern = /^[a-z][a-z0-9.-]{1,79}$/;
|
|
24
|
+
const rolePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
25
|
+
const versionPattern = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
26
|
+
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
|
27
|
+
const windowsReserved = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
28
|
+
const credentialPattern = /(?:[a-z][a-z0-9+.-]*:\/\/[^/@\s]+:[^/@\s]+@|Bearer\s+[A-Za-z0-9._~+\/-]{12,}|sk-[A-Za-z0-9_-]{16,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/i;
|
|
29
|
+
const protectedRoots = new Set(['.git', '.ewai-pipeline', '.codex', '.claude', '.agents']);
|
|
30
|
+
const protectedFiles = new Set(['agents.md', 'claude.md', '.mcp.json', '.env', '.npmrc', '.netrc']);
|
|
31
|
+
const defaultTreeLimits = Object.freeze({
|
|
32
|
+
maximumDepth: 24,
|
|
33
|
+
maximumPathBytes: 512,
|
|
34
|
+
maximumFiles: 5_000,
|
|
35
|
+
maximumFileBytes: 5 * 1024 * 1024,
|
|
36
|
+
maximumTotalBytes: 100 * 1024 * 1024,
|
|
37
|
+
});
|
|
38
|
+
const defaultAdapterLimits = Object.freeze({
|
|
39
|
+
maximumFiles: 128,
|
|
40
|
+
maximumBytes: 10 * 1024 * 1024,
|
|
41
|
+
maximumDepth: 12,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function sha256(value) {
|
|
45
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function strictObject(value, allowed, label) {
|
|
49
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
50
|
+
for (const key of Object.keys(value)) {
|
|
51
|
+
if (!allowed.has(key)) throw new Error(`${label} contains unknown field: ${key}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function safeText(value, label, maximum = 240) {
|
|
56
|
+
const text = String(value ?? '').trim();
|
|
57
|
+
if (!text || text.length > maximum || /[\u0000-\u001f\u007f]/.test(text)) throw new Error(`${label} is unsafe.`);
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function within(root, candidate) {
|
|
62
|
+
const path = relative(root, candidate);
|
|
63
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normaliseRelative(value, label, options = {}) {
|
|
67
|
+
const original = String(value ?? '').trim();
|
|
68
|
+
if (!original || original.includes('\0') || /[\u0000-\u001f\u007f]/.test(original)) throw new Error(`${label} is unsafe.`);
|
|
69
|
+
const text = original.replaceAll('\\', '/');
|
|
70
|
+
if (text !== text.normalize('NFC')) throw new Error(`${label} must be Unicode NFC.`);
|
|
71
|
+
if (isAbsolute(text) || /^[A-Za-z]:/.test(text) || text.startsWith('//')) throw new Error(`${label} must be relative.`);
|
|
72
|
+
if (text === '.' && options.allowDot !== false) return '.';
|
|
73
|
+
const segments = text.split('/');
|
|
74
|
+
if (segments.some((segment) => !segment || segment === '.' || segment === '..')) throw new Error(`${label} must be a bounded relative path.`);
|
|
75
|
+
for (const segment of segments) {
|
|
76
|
+
if (Buffer.byteLength(segment) > 255 || segment.endsWith('.') || segment.endsWith(' ') || windowsReserved.test(segment)) {
|
|
77
|
+
throw new Error(`${label} contains an unsafe path segment: ${segment}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return segments.join('/');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function safeSource(source) {
|
|
84
|
+
const text = safeText(source, 'Starter source', 2_048);
|
|
85
|
+
if (credentialPattern.test(text)) throw new Error('Starter source is credential-shaped or contains URL user information.');
|
|
86
|
+
try {
|
|
87
|
+
const url = new URL(text);
|
|
88
|
+
if (url.username || url.password) throw new Error('Starter source contains URL user information.');
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (/user information/.test(error.message)) throw error;
|
|
91
|
+
}
|
|
92
|
+
return text;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function classifyStarterSource(source) {
|
|
96
|
+
const text = safeSource(source);
|
|
97
|
+
if (/^git\+https?:/i.test(text) || /\.git(?:#|$)/i.test(text)) return 'git';
|
|
98
|
+
if (/^https:/i.test(text)) return 'https';
|
|
99
|
+
if (/^(?:file:|\.?\.?[\\/])/i.test(text)) return 'directory';
|
|
100
|
+
if (/\.(?:zip|tar|tgz|tar\.gz)(?:[?#]|$)/i.test(text)) return 'archive';
|
|
101
|
+
if (/^(?:npm|pkg|package):/i.test(text)) return 'package';
|
|
102
|
+
return 'opaque';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validatePathSegment(path, label) {
|
|
106
|
+
const normalised = normaliseRelative(path, label, { allowDot: false });
|
|
107
|
+
if (Buffer.byteLength(normalised) > defaultTreeLimits.maximumPathBytes) throw new Error(`${label} exceeds the path-byte limit.`);
|
|
108
|
+
return normalised;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function walkPackage(root, current, limits, entries, depth = 0) {
|
|
112
|
+
if (depth > limits.maximumDepth) throw new Error('Starter adapter package exceeds its maximum depth.');
|
|
113
|
+
for (const entry of readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
114
|
+
const absolute = resolve(current, entry.name);
|
|
115
|
+
const metadata = lstatSync(absolute);
|
|
116
|
+
if (metadata.isSymbolicLink()) throw new Error(`Starter adapter package may not contain symbolic links: ${entry.name}`);
|
|
117
|
+
const path = validatePathSegment(relative(root, absolute).replaceAll('\\', '/'), 'Starter adapter package path');
|
|
118
|
+
if (metadata.isDirectory()) {
|
|
119
|
+
walkPackage(root, absolute, limits, entries, depth + 1);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!metadata.isFile()) throw new Error(`Starter adapter package contains unsupported content: ${path}`);
|
|
123
|
+
if (metadata.nlink > 1) throw new Error(`Starter adapter package contains a hard-link alias: ${path}`);
|
|
124
|
+
const realFile = realpathSync(absolute);
|
|
125
|
+
if (!within(root, realFile)) throw new Error(`Starter adapter package content escapes its root: ${path}`);
|
|
126
|
+
entries.push({ path, bytes: readFileSync(absolute) });
|
|
127
|
+
if (entries.length > limits.maximumFiles) throw new Error('Starter adapter package exceeds its file-count limit.');
|
|
128
|
+
const total = entries.reduce((sum, item) => sum + item.bytes.length, 0);
|
|
129
|
+
if (total > limits.maximumBytes) throw new Error('Starter adapter package exceeds its byte limit.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function validateStarterAdapterPackage(folder, options = {}) {
|
|
134
|
+
const requestedRoot = resolve(folder);
|
|
135
|
+
if (!existsSync(requestedRoot)) throw new Error(`Starter adapter package does not exist: ${folder}`);
|
|
136
|
+
if (lstatSync(requestedRoot).isSymbolicLink() || !lstatSync(requestedRoot).isDirectory()) {
|
|
137
|
+
throw new Error('Starter adapter package root must be a non-symbolic directory.');
|
|
138
|
+
}
|
|
139
|
+
const root = realpathSync(requestedRoot);
|
|
140
|
+
const manifestPath = resolve(root, 'starter-adapter.json');
|
|
141
|
+
if (!existsSync(manifestPath) || lstatSync(manifestPath).isSymbolicLink() || !lstatSync(manifestPath).isFile()) {
|
|
142
|
+
throw new Error('Starter adapter package requires a regular starter-adapter.json manifest.');
|
|
143
|
+
}
|
|
144
|
+
if (lstatSync(manifestPath).size > 64 * 1024) throw new Error('Starter adapter manifest exceeds its byte limit.');
|
|
145
|
+
let manifest;
|
|
146
|
+
try {
|
|
147
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new Error(`Starter adapter manifest is invalid JSON: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
strictObject(manifest, new Set(['schema', 'id', 'name', 'publisher', 'version', 'protocolVersion', 'sourceClasses', 'entrypoint']), 'Starter adapter manifest');
|
|
152
|
+
if (manifest.schema !== 'ewai.starter-source-adapter/v1') throw new Error('Unsupported starter adapter schema.');
|
|
153
|
+
const id = safeText(manifest.id, 'Starter adapter ID', 80);
|
|
154
|
+
if (!slugPattern.test(id)) throw new Error('Starter adapter ID is invalid.');
|
|
155
|
+
const name = safeText(manifest.name, 'Starter adapter name');
|
|
156
|
+
strictObject(manifest.publisher, new Set(['id', 'name']), 'Starter adapter publisher');
|
|
157
|
+
const publisher = {
|
|
158
|
+
id: safeText(manifest.publisher.id, 'Starter adapter publisher ID', 80),
|
|
159
|
+
name: safeText(manifest.publisher.name, 'Starter adapter publisher name'),
|
|
160
|
+
};
|
|
161
|
+
if (!rolePattern.test(publisher.id)) throw new Error('Starter adapter publisher ID is invalid.');
|
|
162
|
+
const version = safeText(manifest.version, 'Starter adapter version', 80);
|
|
163
|
+
if (!versionPattern.test(version)) throw new Error('Starter adapter version is invalid.');
|
|
164
|
+
if (String(manifest.protocolVersion) !== '1') throw new Error('Unsupported starter adapter protocol version.');
|
|
165
|
+
if (!Array.isArray(manifest.sourceClasses) || !manifest.sourceClasses.length || manifest.sourceClasses.length > STARTER_SOURCE_CLASSES.length) {
|
|
166
|
+
throw new Error('Starter adapter source classes must be a non-empty bounded array.');
|
|
167
|
+
}
|
|
168
|
+
const sourceClasses = manifest.sourceClasses.map((item) => safeText(item, 'Starter adapter source class', 40));
|
|
169
|
+
if (new Set(sourceClasses).size !== sourceClasses.length || sourceClasses.some((item) => !sourceClassSet.has(item))) {
|
|
170
|
+
throw new Error('Starter adapter source classes are invalid or duplicated.');
|
|
171
|
+
}
|
|
172
|
+
const entrypoint = normaliseRelative(manifest.entrypoint, 'Starter adapter entrypoint', { allowDot: false });
|
|
173
|
+
const entrypointPath = resolve(root, entrypoint);
|
|
174
|
+
if (!within(root, entrypointPath) || !existsSync(entrypointPath) || lstatSync(entrypointPath).isSymbolicLink() || !lstatSync(entrypointPath).isFile()) {
|
|
175
|
+
throw new Error('Starter adapter entrypoint must be a regular file inside its package.');
|
|
176
|
+
}
|
|
177
|
+
accessSync(entrypointPath, constants.X_OK);
|
|
178
|
+
const limits = { ...defaultAdapterLimits, ...(options.limits ?? {}) };
|
|
179
|
+
const entries = [];
|
|
180
|
+
walkPackage(root, root, limits, entries);
|
|
181
|
+
entries.sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
|
|
182
|
+
const packageHash = createHash('sha256').update('ewai.starter-adapter-package/v1\0');
|
|
183
|
+
for (const entry of entries) packageHash.update(`${Buffer.byteLength(entry.path)}:`).update(entry.path).update(`:${entry.bytes.length}:`).update(entry.bytes).update('\0');
|
|
184
|
+
return {
|
|
185
|
+
schema: manifest.schema,
|
|
186
|
+
id,
|
|
187
|
+
name,
|
|
188
|
+
publisher,
|
|
189
|
+
version,
|
|
190
|
+
protocolVersion: '1',
|
|
191
|
+
sourceClasses,
|
|
192
|
+
entrypoint,
|
|
193
|
+
manifestDigest: sha256(readFileSync(manifestPath)),
|
|
194
|
+
packageDigest: `sha256:${packageHash.digest('hex')}`,
|
|
195
|
+
packageFiles: entries.length,
|
|
196
|
+
packageBytes: entries.reduce((sum, item) => sum + item.bytes.length, 0),
|
|
197
|
+
trustedRoot: root,
|
|
198
|
+
entrypointPath,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readRegularFile(root, absolute, path, limits, usage) {
|
|
203
|
+
const beforeRealPath = realpathSync(absolute);
|
|
204
|
+
if (!within(root, beforeRealPath)) throw new Error(`Starter tree file escapes its root: ${path}`);
|
|
205
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
206
|
+
const descriptor = openSync(absolute, constants.O_RDONLY | noFollow);
|
|
207
|
+
try {
|
|
208
|
+
const before = fstatSync(descriptor);
|
|
209
|
+
if (!before.isFile()) throw new Error(`Starter tree content must be a regular file: ${path}`);
|
|
210
|
+
if (before.nlink > 1) throw new Error(`Starter tree contains a hard-link alias: ${path}`);
|
|
211
|
+
if (before.size > limits.maximumFileBytes) throw new Error(`Starter tree file exceeds its byte limit: ${path}`);
|
|
212
|
+
const content = readFileSync(descriptor);
|
|
213
|
+
const after = fstatSync(descriptor);
|
|
214
|
+
const afterRealPath = realpathSync(absolute);
|
|
215
|
+
if (!within(root, afterRealPath) || beforeRealPath !== afterRealPath || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || content.length !== after.size) {
|
|
216
|
+
throw new Error(`Starter tree file changed while it was read: ${path}`);
|
|
217
|
+
}
|
|
218
|
+
usage.totalBytes += content.length;
|
|
219
|
+
if (usage.totalBytes > limits.maximumTotalBytes) throw new Error('Starter tree exceeds its total-byte limit.');
|
|
220
|
+
return { content, metadata: before };
|
|
221
|
+
} finally {
|
|
222
|
+
closeSync(descriptor);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function walkTree(root, current, limits, files, collisions, usage, depth = 0) {
|
|
227
|
+
if (depth > limits.maximumDepth) throw new Error('Starter tree exceeds its maximum depth.');
|
|
228
|
+
for (const entry of readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
229
|
+
const absolute = resolve(current, entry.name);
|
|
230
|
+
const metadata = lstatSync(absolute);
|
|
231
|
+
const path = validatePathSegment(relative(root, absolute).replaceAll('\\', '/'), 'Starter tree path');
|
|
232
|
+
const collisionKey = path.normalize('NFC').toLocaleLowerCase('en-US');
|
|
233
|
+
if (collisions.has(collisionKey)) throw new Error(`Starter tree contains a case or Unicode-equivalent path collision: ${path}`);
|
|
234
|
+
collisions.add(collisionKey);
|
|
235
|
+
if (metadata.isSymbolicLink()) throw new Error(`Starter tree may not contain a symbolic link: ${path}`);
|
|
236
|
+
if (metadata.isDirectory()) {
|
|
237
|
+
walkTree(root, absolute, limits, files, collisions, usage, depth + 1);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!metadata.isFile()) throw new Error(`Starter tree contains unsupported special content: ${path}`);
|
|
241
|
+
const { content, metadata: opened } = readRegularFile(root, absolute, path, limits, usage);
|
|
242
|
+
files.push({
|
|
243
|
+
path,
|
|
244
|
+
bytes: content.length,
|
|
245
|
+
digest: sha256(content),
|
|
246
|
+
content,
|
|
247
|
+
absolutePath: absolute,
|
|
248
|
+
device: opened.dev,
|
|
249
|
+
inode: opened.ino,
|
|
250
|
+
});
|
|
251
|
+
if (files.length > limits.maximumFiles) throw new Error('Starter tree exceeds its file-count limit.');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function canonicalStarterTree(root, options = {}) {
|
|
256
|
+
const requestedRoot = resolve(root);
|
|
257
|
+
if (!existsSync(requestedRoot) || lstatSync(requestedRoot).isSymbolicLink() || !lstatSync(requestedRoot).isDirectory()) {
|
|
258
|
+
throw new Error('Starter tree root must be a non-symbolic directory.');
|
|
259
|
+
}
|
|
260
|
+
const realRoot = realpathSync(requestedRoot);
|
|
261
|
+
const limits = { ...defaultTreeLimits, ...(options.limits ?? options ?? {}) };
|
|
262
|
+
const files = [];
|
|
263
|
+
walkTree(realRoot, realRoot, limits, files, new Set(), { totalBytes: 0 });
|
|
264
|
+
files.sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
|
|
265
|
+
return files;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function starterTreeDigest(files) {
|
|
269
|
+
if (!Array.isArray(files)) throw new Error('Starter tree digest requires a file inventory.');
|
|
270
|
+
const hash = createHash('sha256').update('ewai.starter-tree/v1\0');
|
|
271
|
+
for (const file of [...files].sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)))) {
|
|
272
|
+
const path = validatePathSegment(file.path, 'Starter tree digest path');
|
|
273
|
+
const content = Buffer.isBuffer(file.content) ? file.content : readFileSync(file.absolutePath);
|
|
274
|
+
hash.update(`${Buffer.byteLength(path)}:`).update(path).update(`:${content.length}:`).update(content).update('\0');
|
|
275
|
+
}
|
|
276
|
+
return `sha256:${hash.digest('hex')}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function starterPackDigest(targets) {
|
|
280
|
+
if (!Array.isArray(targets) || !targets.length) throw new Error('Governed Starter Pack digest requires target trees.');
|
|
281
|
+
const hash = createHash('sha256').update('ewai.governed-starter-pack/v1\0');
|
|
282
|
+
for (const target of [...targets].sort((left, right) => left.role.localeCompare(right.role))) {
|
|
283
|
+
const role = String(target.role ?? '');
|
|
284
|
+
if (!rolePattern.test(role) || !digestPattern.test(String(target.digest ?? ''))) throw new Error('Governed Starter Pack target digest is invalid.');
|
|
285
|
+
hash.update(`${Buffer.byteLength(role)}:`).update(role).update(`:${target.digest}\0`);
|
|
286
|
+
}
|
|
287
|
+
return `sha256:${hash.digest('hex')}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function assertNoSymbolicTraversal(root, relativePath, label) {
|
|
291
|
+
let cursor = root;
|
|
292
|
+
if (relativePath === '.') return;
|
|
293
|
+
for (const segment of relativePath.split('/')) {
|
|
294
|
+
cursor = resolve(cursor, segment);
|
|
295
|
+
if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) throw new Error(`${label} may not traverse a symbolic link.`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isProtectedMapping(projectRoot, specsRoot, repositoryRoot, mappingPath) {
|
|
300
|
+
if (mappingPath !== '.') {
|
|
301
|
+
const segments = mappingPath.split('/').map((segment) => segment.toLocaleLowerCase('en-US'));
|
|
302
|
+
const last = segments.at(-1);
|
|
303
|
+
if (segments.some((segment) => protectedRoots.has(segment)) || protectedFiles.has(last)) return true;
|
|
304
|
+
}
|
|
305
|
+
const destination = resolve(repositoryRoot, mappingPath);
|
|
306
|
+
return destination === specsRoot || (within(specsRoot, destination) && destination !== projectRoot);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function resolveStarterTargetMappings(projectRoot, receipt) {
|
|
310
|
+
const root = resolve(projectRoot);
|
|
311
|
+
const { config, paths } = loadProjectConfig(root);
|
|
312
|
+
const realSpecsRoot = existsSync(paths.specsRoot) ? realpathSync(paths.specsRoot) : resolve(paths.specsRoot);
|
|
313
|
+
const repositories = new Map();
|
|
314
|
+
for (const repository of config.repositories ?? []) {
|
|
315
|
+
strictObject(repository, new Set(['name', 'path', 'role']), 'Configured repository');
|
|
316
|
+
const name = safeText(repository.name, 'Configured repository name', 120);
|
|
317
|
+
if (repositories.has(name)) throw new Error(`Duplicate configured repository: ${name}`);
|
|
318
|
+
const path = normaliseRelative(repository.path, `Configured repository ${name} path`);
|
|
319
|
+
const absolute = resolve(root, path);
|
|
320
|
+
if (!within(root, absolute) || !existsSync(absolute) || lstatSync(absolute).isSymbolicLink() || !lstatSync(absolute).isDirectory()) {
|
|
321
|
+
throw new Error(`Configured repository must be a non-symbolic directory inside the workspace: ${name}`);
|
|
322
|
+
}
|
|
323
|
+
const realRoot = realpathSync(absolute);
|
|
324
|
+
repositories.set(name, { name, role: safeText(repository.role, 'Configured repository role'), path, root: realRoot });
|
|
325
|
+
}
|
|
326
|
+
const configuredTargets = config.starter_materialisation?.targets;
|
|
327
|
+
if (!Array.isArray(configuredTargets) || !configuredTargets.length) throw new Error('Project starter_materialisation.targets must map every logical role.');
|
|
328
|
+
const byRole = new Map();
|
|
329
|
+
for (const item of configuredTargets) {
|
|
330
|
+
strictObject(item, new Set(['role', 'repository', 'path']), 'Starter target mapping');
|
|
331
|
+
const role = safeText(item.role, 'Starter target role', 80);
|
|
332
|
+
if (!rolePattern.test(role) || byRole.has(role)) throw new Error(`Starter target role is invalid or duplicated: ${role}`);
|
|
333
|
+
const repository = repositories.get(safeText(item.repository, 'Starter target repository', 120));
|
|
334
|
+
if (!repository) throw new Error(`Starter target references an unknown repository: ${item.repository}`);
|
|
335
|
+
const path = normaliseRelative(item.path, `Starter target ${role} path`);
|
|
336
|
+
assertNoSymbolicTraversal(repository.root, path, `Starter target ${role}`);
|
|
337
|
+
if (isProtectedMapping(root, realSpecsRoot, repository.root, path)) throw new Error(`Starter target ${role} resolves into a protected destination.`);
|
|
338
|
+
byRole.set(role, {
|
|
339
|
+
role,
|
|
340
|
+
repository: repository.name,
|
|
341
|
+
repositoryRole: repository.role,
|
|
342
|
+
repositoryPath: repository.path,
|
|
343
|
+
path,
|
|
344
|
+
repositoryRoot: repository.root,
|
|
345
|
+
destinationRoot: resolve(repository.root, path),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const receiptTargets = Array.isArray(receipt?.targets) ? receipt.targets : [];
|
|
349
|
+
if (!receiptTargets.length) throw new Error('Governed Starter Pack receipt declares no logical targets.');
|
|
350
|
+
const required = new Set();
|
|
351
|
+
const resolved = receiptTargets.map((item) => {
|
|
352
|
+
const role = safeText(item.role, 'Governed Starter Pack target role', 80);
|
|
353
|
+
if (!rolePattern.test(role) || required.has(role)) throw new Error(`Governed Starter Pack target role is invalid or duplicated: ${role}`);
|
|
354
|
+
required.add(role);
|
|
355
|
+
const mapping = byRole.get(role);
|
|
356
|
+
if (!mapping) throw new Error(`Project topology does not map Governed Starter Pack role: ${role}`);
|
|
357
|
+
return { ...mapping, sourcePath: normaliseRelative(item.sourcePath ?? item.source_path, `Governed Starter Pack ${role} source path`) };
|
|
358
|
+
});
|
|
359
|
+
const unused = [...byRole.keys()].filter((role) => !required.has(role));
|
|
360
|
+
if (unused.length) throw new Error(`Project topology contains roles not declared by this Governed Starter Pack: ${unused.join(', ')}`);
|
|
361
|
+
for (let index = 0; index < resolved.length; index += 1) {
|
|
362
|
+
for (let other = index + 1; other < resolved.length; other += 1) {
|
|
363
|
+
if (resolved[index].repository !== resolved[other].repository) continue;
|
|
364
|
+
const left = resolved[index].path;
|
|
365
|
+
const right = resolved[other].path;
|
|
366
|
+
if (left === right || left === '.' || right === '.' || left.startsWith(`${right}/`) || right.startsWith(`${left}/`)) {
|
|
367
|
+
throw new Error(`Starter target mappings overlap in repository ${resolved[index].repository}: ${left} and ${right}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return resolved.sort((left, right) => left.role.localeCompare(right.role));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function gitOutput(root, args, fallback = '') {
|
|
375
|
+
try {
|
|
376
|
+
return execFileSync('git', ['-C', root, ...args], {
|
|
377
|
+
encoding: 'utf8',
|
|
378
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
379
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
380
|
+
});
|
|
381
|
+
} catch {
|
|
382
|
+
return fallback;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function starterProjectRevision(targets) {
|
|
387
|
+
const repositories = new Map();
|
|
388
|
+
for (const target of targets ?? []) {
|
|
389
|
+
if (repositories.has(target.repository)) continue;
|
|
390
|
+
const top = gitOutput(target.repositoryRoot, ['rev-parse', '--show-toplevel']).trim();
|
|
391
|
+
if (!top || realpathSync(top) !== realpathSync(target.repositoryRoot)) throw new Error(`Configured repository is not its own Git working tree: ${target.repository}`);
|
|
392
|
+
const head = gitOutput(target.repositoryRoot, ['rev-parse', '--verify', 'HEAD'], 'unborn').trim() || 'unborn';
|
|
393
|
+
const status = gitOutput(target.repositoryRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
|
|
394
|
+
repositories.set(target.repository, {
|
|
395
|
+
name: target.repository,
|
|
396
|
+
revision: sha256(`ewai.starter-repository-revision/v1\0${head}\0${status}`),
|
|
397
|
+
head,
|
|
398
|
+
dirty: Boolean(status),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const ordered = [...repositories.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
402
|
+
return {
|
|
403
|
+
schema: 'ewai.starter-project-revision/v1',
|
|
404
|
+
repositories: ordered,
|
|
405
|
+
digest: sha256(JSON.stringify(ordered)),
|
|
406
|
+
};
|
|
407
|
+
}
|