@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,479 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { buildTeamHubEnvelope, buildTeamHubSnapshot, inspectTeamHubDisclosure, validateTeamHubReceipt } from '../team-hub.mjs';
|
|
6
|
+
import { runtimePaths } from './paths.mjs';
|
|
7
|
+
import { validateTeamHubResourcePackage } from '../team-hub-resources.mjs';
|
|
8
|
+
import { parseOrganisationBlueprintPack } from '../organisation-blueprints.mjs';
|
|
9
|
+
import { parseDesignSystemPack } from '../design-systems.mjs';
|
|
10
|
+
import { defaultPackRoot, listPacks } from '../packs.mjs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
|
|
13
|
+
const connectionSchema = z.object({
|
|
14
|
+
schema: z.literal('ewai.team-hub-connection/v1'),
|
|
15
|
+
endpoint: z.string().url().max(500),
|
|
16
|
+
projectId: z.string().min(1).max(120).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/),
|
|
17
|
+
tokenEnv: z.string().regex(/^[A-Z][A-Z0-9_]{2,100}$/),
|
|
18
|
+
disclosureDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
19
|
+
connectedAt: z.string().datetime({ offset: true }),
|
|
20
|
+
}).strict();
|
|
21
|
+
|
|
22
|
+
function safeEndpoint(value) {
|
|
23
|
+
let url;
|
|
24
|
+
try { url = new URL(String(value ?? '')); } catch { throw new Error('Team Hub endpoint must be a valid URL.'); }
|
|
25
|
+
if (url.username || url.password || url.search || url.hash) throw new Error('Team Hub endpoint must not contain credentials, query parameters or fragments.');
|
|
26
|
+
const loopback = ['127.0.0.1', 'localhost', '::1'].includes(url.hostname);
|
|
27
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) throw new Error('Team Hub endpoint must use HTTPS, except for loopback development.');
|
|
28
|
+
return url.toString().replace(/\/$/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJson(path) {
|
|
32
|
+
if (!existsSync(path)) return null;
|
|
33
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function localMetadata(path) {
|
|
37
|
+
try { return lstatSync(path); }
|
|
38
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertLocalDirectoryChain(projectRoot, target, options = {}) {
|
|
42
|
+
const root = resolve(projectRoot);
|
|
43
|
+
const destination = resolve(target);
|
|
44
|
+
const boundary = relative(root, destination);
|
|
45
|
+
if (!boundary || boundary.startsWith('..') || isAbsolute(boundary)) throw new Error('Team Hub managed path escapes the project root.');
|
|
46
|
+
let cursor = root;
|
|
47
|
+
for (const segment of boundary.split(/[/\\]/)) {
|
|
48
|
+
cursor = resolve(cursor, segment);
|
|
49
|
+
let metadata = localMetadata(cursor);
|
|
50
|
+
if (!metadata) {
|
|
51
|
+
if (options.create === true) mkdirSync(cursor, { mode: 0o700 });
|
|
52
|
+
else return destination;
|
|
53
|
+
metadata = lstatSync(cursor);
|
|
54
|
+
}
|
|
55
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) throw new Error('Team Hub managed paths may not traverse symbolic or non-directory content.');
|
|
56
|
+
}
|
|
57
|
+
return destination;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertLocalRegularFile(path) {
|
|
61
|
+
const metadata = localMetadata(path);
|
|
62
|
+
if (!metadata) return;
|
|
63
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) throw new Error('Team Hub managed state must be a regular file.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function privateJson(path, value) {
|
|
67
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
68
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
69
|
+
try {
|
|
70
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
71
|
+
renameSync(temporary, path);
|
|
72
|
+
} finally { rmSync(temporary, { force: true }); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function safeConnection(path) {
|
|
76
|
+
const parsed = connectionSchema.safeParse(readJson(path));
|
|
77
|
+
return parsed.success ? parsed.data : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function attemptFiles(root) {
|
|
81
|
+
if (!existsSync(root)) return [];
|
|
82
|
+
return readdirSync(root).filter((name) => /^attempt-[a-zA-Z0-9-]+\.json$/.test(name)).sort().reverse();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readTeamHubWorkspace(projectRoot) {
|
|
86
|
+
const paths = runtimePaths(projectRoot);
|
|
87
|
+
const connection = safeConnection(paths.teamHubConnectionPath);
|
|
88
|
+
const latestAttemptPath = attemptFiles(paths.teamHubAttemptsRoot)[0];
|
|
89
|
+
const lastAttempt = latestAttemptPath ? readJson(resolve(paths.teamHubAttemptsRoot, latestAttemptPath)) : null;
|
|
90
|
+
const lastAcceptedReceipt = readJson(paths.teamHubAcceptedReceiptPath);
|
|
91
|
+
return {
|
|
92
|
+
schema: 'ewai.team-hub-workspace/v1',
|
|
93
|
+
mode: connection ? 'connected' : 'single',
|
|
94
|
+
connection,
|
|
95
|
+
disclosure: inspectTeamHubDisclosure(),
|
|
96
|
+
lastAttempt,
|
|
97
|
+
lastAcceptedReceipt,
|
|
98
|
+
resources: readTeamHubResourceState(projectRoot),
|
|
99
|
+
localWorkAvailable: true,
|
|
100
|
+
authorityNotice: inspectTeamHubDisclosure().authorityNotice,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function safeResourceIdentity(id, version = '') {
|
|
105
|
+
if (!/^org\.[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(String(id ?? ''))) throw new Error('Team Hub resource ID is invalid.');
|
|
106
|
+
if (version && !/^\d+\.\d+\.\d+$/.test(String(version))) throw new Error('Team Hub resource version is invalid.');
|
|
107
|
+
return { id: String(id), version: String(version) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function connectionToken(connection, options = {}) {
|
|
111
|
+
const token = String((options.env ?? process.env)[connection.tokenEnv] ?? '');
|
|
112
|
+
if (!token) throw Object.assign(new Error(`Team Hub token environment variable is unavailable: ${connection.tokenEnv}`), { code: 'missing-token' });
|
|
113
|
+
return token;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function resourceRequest(connection, path, options = {}) {
|
|
117
|
+
const token = connectionToken(connection, options);
|
|
118
|
+
const response = await (options.fetch ?? globalThis.fetch)(`${connection.endpoint}${path}`, {
|
|
119
|
+
headers: { authorization: `Bearer ${token}` },
|
|
120
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 8_000),
|
|
121
|
+
});
|
|
122
|
+
const body = await response.json().catch(() => null);
|
|
123
|
+
if (!response.ok) throw new Error(body?.error?.message ?? `Hub rejected the resource request with HTTP ${response.status}.`);
|
|
124
|
+
return body;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function installedState(projectRoot) {
|
|
128
|
+
const path = runtimePaths(projectRoot).teamHubResourceInstalledPath;
|
|
129
|
+
assertLocalDirectoryChain(projectRoot, dirname(path));
|
|
130
|
+
assertLocalRegularFile(path);
|
|
131
|
+
const value = readJson(path);
|
|
132
|
+
return value?.schema === 'ewai.team-hub-resource-installed-state/v1' && value.resources && typeof value.resources === 'object'
|
|
133
|
+
? value
|
|
134
|
+
: { schema: 'ewai.team-hub-resource-installed-state/v1', resources: {} };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function listTeamHubResourceReceipts(projectRoot) {
|
|
138
|
+
const root = runtimePaths(projectRoot).teamHubResourceReceiptsRoot;
|
|
139
|
+
assertLocalDirectoryChain(projectRoot, root);
|
|
140
|
+
if (!existsSync(root)) return [];
|
|
141
|
+
return readdirSync(root)
|
|
142
|
+
.filter((name) => /^receipt-[a-zA-Z0-9-]+\.json$/.test(name))
|
|
143
|
+
.sort()
|
|
144
|
+
.map((name) => {
|
|
145
|
+
const path = resolve(root, name);
|
|
146
|
+
assertLocalRegularFile(path);
|
|
147
|
+
return readJson(path);
|
|
148
|
+
})
|
|
149
|
+
.filter((value) => value?.schema === 'ewai.team-hub-resource-install-receipt/v1')
|
|
150
|
+
.reverse();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function readTeamHubResourceState(projectRoot) {
|
|
154
|
+
const state = installedState(projectRoot);
|
|
155
|
+
return projectTeamHubResourceState(state, listTeamHubResourceReceipts(projectRoot));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function projectTeamHubResourceState(state, receipts) {
|
|
159
|
+
return {
|
|
160
|
+
schema: 'ewai.team-hub-resource-workspace/v1',
|
|
161
|
+
installed: Object.values(state.resources).sort((left, right) => left.id.localeCompare(right.id)),
|
|
162
|
+
receipts,
|
|
163
|
+
authority: 'Installed means available locally. Select or apply the resource separately.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function listConnectedTeamHubResources(projectRoot, options = {}) {
|
|
168
|
+
const connection = safeConnection(runtimePaths(projectRoot).teamHubConnectionPath);
|
|
169
|
+
if (!connection) throw new Error('Team Hub is not connected.');
|
|
170
|
+
const catalogue = await resourceRequest(connection, '/api/v1/resources', options);
|
|
171
|
+
if (catalogue?.schema !== 'ewai.team-hub-resource-catalogue/v1' || !Array.isArray(catalogue.releases)) throw new Error('Team Hub returned an invalid resource catalogue.');
|
|
172
|
+
return { ...catalogue, local: readTeamHubResourceState(projectRoot) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function inspectConnectedTeamHubResource(projectRoot, id, version, options = {}) {
|
|
176
|
+
const identity = safeResourceIdentity(id, version);
|
|
177
|
+
const connection = safeConnection(runtimePaths(projectRoot).teamHubConnectionPath);
|
|
178
|
+
if (!connection) throw new Error('Team Hub is not connected.');
|
|
179
|
+
const value = await resourceRequest(connection, `/api/v1/resources/${encodeURIComponent(identity.id)}/releases/${encodeURIComponent(identity.version)}`, options);
|
|
180
|
+
const resourcePackage = validateTeamHubResourcePackage(value);
|
|
181
|
+
if (resourcePackage.resource.id !== identity.id || resourcePackage.resource.version !== identity.version) throw new Error('Team Hub resource identity disagrees with the requested release.');
|
|
182
|
+
return {
|
|
183
|
+
schema: 'ewai.team-hub-resource-inspection/v1',
|
|
184
|
+
resource: resourcePackage.resource,
|
|
185
|
+
digest: resourcePackage.digest,
|
|
186
|
+
files: resourcePackage.files.map(({ path, size, digest }) => ({ path, size, digest })),
|
|
187
|
+
installed: installedState(projectRoot).resources[identity.id] ?? null,
|
|
188
|
+
authority: 'Inspection does not install, select or apply this resource.',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function writePackageFolder(folder, resourcePackage) {
|
|
193
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
194
|
+
for (const file of resourcePackage.files) {
|
|
195
|
+
const destination = resolve(folder, file.path);
|
|
196
|
+
if (!destination.startsWith(`${resolve(folder)}/`)) throw new Error(`Resource file escapes staging: ${file.path}`);
|
|
197
|
+
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
198
|
+
writeFileSync(destination, file.content, { encoding: 'utf8', mode: 0o600 });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function validateStagedResource(folder, resourcePackage) {
|
|
203
|
+
const manifest = resolve(folder, 'pack.yaml');
|
|
204
|
+
const parsed = resourcePackage.resource.kind === 'organisation-blueprint'
|
|
205
|
+
? parseOrganisationBlueprintPack(manifest, 'team-hub-managed')
|
|
206
|
+
: parseDesignSystemPack(manifest, 'team-hub-managed');
|
|
207
|
+
if (!parsed || parsed.id !== resourcePackage.resource.id || parsed.version !== resourcePackage.resource.version) {
|
|
208
|
+
throw new Error('Staged resource identity disagrees with its package.');
|
|
209
|
+
}
|
|
210
|
+
return parsed;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function outsideCollision(projectRoot, id, managedPath, options = {}) {
|
|
214
|
+
const roots = [defaultPackRoot, resolve(options.home ?? homedir(), '.ewai/packs'), resolve(projectRoot, '.ewai-pipeline/packs')];
|
|
215
|
+
for (const root of roots) {
|
|
216
|
+
for (const pack of listPacks(root)) {
|
|
217
|
+
if (pack.id === id && resolve(dirname(pack.path)) !== resolve(managedPath)) return pack.path;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function compareVersion(left, right) {
|
|
224
|
+
const a = String(left).split('.').map(Number);
|
|
225
|
+
const b = String(right).split('.').map(Number);
|
|
226
|
+
for (let index = 0; index < 3; index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function processIsActive(pid) {
|
|
231
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
232
|
+
try {
|
|
233
|
+
process.kill(pid, 0);
|
|
234
|
+
return true;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return error?.code === 'EPERM';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function acquireResourceInstallLease(projectRoot, paths, options = {}) {
|
|
241
|
+
const leasePath = resolve(paths.teamHubResourcesRoot, 'install.lock');
|
|
242
|
+
const metadataPath = resolve(leasePath, 'lease.json');
|
|
243
|
+
const now = options.at ?? new Date().toISOString();
|
|
244
|
+
const staleAfterMs = Math.max(1_000, Number(options.installLeaseStaleAfterMs ?? 15 * 60_000));
|
|
245
|
+
const leaseId = randomUUID();
|
|
246
|
+
|
|
247
|
+
const create = () => {
|
|
248
|
+
try {
|
|
249
|
+
mkdirSync(leasePath, { mode: 0o700 });
|
|
250
|
+
privateJson(metadataPath, {
|
|
251
|
+
schema: 'ewai.team-hub-resource-install-lease/v1',
|
|
252
|
+
leaseId,
|
|
253
|
+
pid: process.pid,
|
|
254
|
+
acquiredAt: now,
|
|
255
|
+
});
|
|
256
|
+
return true;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error?.code === 'EEXIST') return false;
|
|
259
|
+
rmSync(leasePath, { recursive: true, force: true });
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (!create()) {
|
|
265
|
+
const current = readJson(metadataPath);
|
|
266
|
+
const acquiredAt = Date.parse(current?.acquiredAt ?? '');
|
|
267
|
+
const leaseModifiedAt = localMetadata(leasePath)?.mtimeMs;
|
|
268
|
+
const observedAt = Number.isFinite(Date.parse(now)) ? Date.parse(now) : Date.now();
|
|
269
|
+
const age = Number.isFinite(acquiredAt)
|
|
270
|
+
? Math.max(0, observedAt - acquiredAt)
|
|
271
|
+
: Math.max(0, observedAt - Number(leaseModifiedAt ?? observedAt));
|
|
272
|
+
const validMetadata = current?.schema === 'ewai.team-hub-resource-install-lease/v1';
|
|
273
|
+
const stale = validMetadata
|
|
274
|
+
? !processIsActive(Number(current.pid))
|
|
275
|
+
: age > staleAfterMs;
|
|
276
|
+
if (stale) {
|
|
277
|
+
rmSync(leasePath, { recursive: true, force: true });
|
|
278
|
+
if (!create()) throw Object.assign(new Error('Another Team Hub resource installation is already in progress.'), { code: 'resource-install-busy', status: 409 });
|
|
279
|
+
} else {
|
|
280
|
+
throw Object.assign(new Error('Another Team Hub resource installation is already in progress.'), { code: 'resource-install-busy', status: 409 });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return () => {
|
|
285
|
+
const current = readJson(metadataPath);
|
|
286
|
+
if (current?.leaseId === leaseId) rmSync(leasePath, { recursive: true, force: true });
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function installConnectedTeamHubResource(projectRoot, input = {}, options = {}) {
|
|
291
|
+
if (input.confirmed !== true) throw new Error('Team Hub resource installation requires explicit confirmation.');
|
|
292
|
+
const approvedBy = String(input.approvedBy ?? '').trim();
|
|
293
|
+
if (!approvedBy) throw new Error('Team Hub resource installation requires the approving person or role.');
|
|
294
|
+
const expectedDigest = String(input.expectedDigest ?? '');
|
|
295
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(expectedDigest)) throw new Error('Team Hub resource installation requires an exact expected digest.');
|
|
296
|
+
const identity = safeResourceIdentity(input.id, input.version);
|
|
297
|
+
const paths = runtimePaths(projectRoot);
|
|
298
|
+
for (const root of [
|
|
299
|
+
dirname(paths.teamHubResourceInstalledPath), paths.teamHubResourceCacheRoot,
|
|
300
|
+
paths.teamHubResourceStagingRoot, paths.teamHubResourceReceiptsRoot, paths.teamHubManagedPackRoot,
|
|
301
|
+
]) assertLocalDirectoryChain(projectRoot, root, { create: true });
|
|
302
|
+
const releaseLease = acquireResourceInstallLease(projectRoot, paths, options);
|
|
303
|
+
const attemptId = randomUUID();
|
|
304
|
+
const staging = resolve(paths.teamHubResourceStagingRoot, `attempt-${attemptId}`);
|
|
305
|
+
const managedPath = resolve(paths.teamHubManagedPackRoot, identity.id);
|
|
306
|
+
const candidate = resolve(paths.teamHubManagedPackRoot, `.candidate-${attemptId}`);
|
|
307
|
+
const backup = resolve(paths.teamHubManagedPackRoot, `.backup-${identity.id}`);
|
|
308
|
+
try {
|
|
309
|
+
const cacheRoot = resolve(paths.teamHubResourceCacheRoot, identity.id, identity.version, expectedDigest.slice(7));
|
|
310
|
+
assertLocalDirectoryChain(projectRoot, cacheRoot);
|
|
311
|
+
assertLocalRegularFile(resolve(cacheRoot, 'package.json'));
|
|
312
|
+
let resourcePackage = readJson(resolve(cacheRoot, 'package.json'));
|
|
313
|
+
if (resourcePackage) resourcePackage = validateTeamHubResourcePackage(resourcePackage);
|
|
314
|
+
if (!resourcePackage) {
|
|
315
|
+
const connection = safeConnection(paths.teamHubConnectionPath);
|
|
316
|
+
if (!connection) throw new Error('Team Hub is not connected and this exact release is not cached.');
|
|
317
|
+
resourcePackage = validateTeamHubResourcePackage(await resourceRequest(
|
|
318
|
+
connection,
|
|
319
|
+
`/api/v1/resources/${encodeURIComponent(identity.id)}/releases/${encodeURIComponent(identity.version)}`,
|
|
320
|
+
options,
|
|
321
|
+
));
|
|
322
|
+
}
|
|
323
|
+
if (resourcePackage.resource.id !== identity.id || resourcePackage.resource.version !== identity.version || resourcePackage.digest !== expectedDigest) {
|
|
324
|
+
throw new Error('Downloaded Team Hub resource identity or digest disagrees with the confirmed release.');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const collision = outsideCollision(projectRoot, identity.id, managedPath, options);
|
|
328
|
+
if (collision) throw new Error(`Resource ID collides with a pack outside Team Hub management: ${identity.id}`);
|
|
329
|
+
writePackageFolder(staging, resourcePackage);
|
|
330
|
+
validateStagedResource(staging, resourcePackage);
|
|
331
|
+
if (!existsSync(cacheRoot)) {
|
|
332
|
+
mkdirSync(cacheRoot, { recursive: true, mode: 0o700 });
|
|
333
|
+
assertLocalDirectoryChain(projectRoot, cacheRoot);
|
|
334
|
+
writePackageFolder(resolve(cacheRoot, 'files'), resourcePackage);
|
|
335
|
+
privateJson(resolve(cacheRoot, 'package.json'), resourcePackage);
|
|
336
|
+
}
|
|
337
|
+
writePackageFolder(candidate, resourcePackage);
|
|
338
|
+
validateStagedResource(candidate, resourcePackage);
|
|
339
|
+
|
|
340
|
+
const statePathExisted = existsSync(paths.teamHubResourceInstalledPath);
|
|
341
|
+
const state = installedState(projectRoot);
|
|
342
|
+
const previousState = structuredClone(state);
|
|
343
|
+
const previous = state.resources[identity.id] ?? null;
|
|
344
|
+
if (previous?.digest === expectedDigest && existsSync(managedPath)) {
|
|
345
|
+
return { schema: 'ewai.team-hub-resource-install-result/v1', replayed: true, receipt: null, workspace: readTeamHubResourceState(projectRoot) };
|
|
346
|
+
}
|
|
347
|
+
const previousReceipts = listTeamHubResourceReceipts(projectRoot);
|
|
348
|
+
rmSync(backup, { recursive: true, force: true });
|
|
349
|
+
if (existsSync(managedPath)) renameSync(managedPath, backup);
|
|
350
|
+
try { renameSync(candidate, managedPath); }
|
|
351
|
+
catch (error) {
|
|
352
|
+
if (existsSync(backup) && !existsSync(managedPath)) renameSync(backup, managedPath);
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
validateStagedResource(managedPath, resourcePackage);
|
|
357
|
+
if (typeof options.afterPromotion === 'function') options.afterPromotion();
|
|
358
|
+
const action = previous ? (compareVersion(identity.version, previous.version) < 0 ? 'restored' : 'updated') : 'installed';
|
|
359
|
+
const installedAt = input.at ?? new Date().toISOString();
|
|
360
|
+
const current = {
|
|
361
|
+
id: identity.id,
|
|
362
|
+
name: resourcePackage.resource.name,
|
|
363
|
+
kind: resourcePackage.resource.kind,
|
|
364
|
+
version: identity.version,
|
|
365
|
+
digest: expectedDigest,
|
|
366
|
+
publisher: resourcePackage.resource.publisher,
|
|
367
|
+
installedAt,
|
|
368
|
+
installedBy: approvedBy,
|
|
369
|
+
};
|
|
370
|
+
const receipt = {
|
|
371
|
+
schema: 'ewai.team-hub-resource-install-receipt/v1',
|
|
372
|
+
receiptId: `receipt-${randomUUID()}`,
|
|
373
|
+
resourceId: identity.id,
|
|
374
|
+
action,
|
|
375
|
+
previous: previous ? { version: previous.version, digest: previous.digest } : null,
|
|
376
|
+
installed: { version: identity.version, digest: expectedDigest },
|
|
377
|
+
approvedBy,
|
|
378
|
+
installedAt,
|
|
379
|
+
authority: 'Installation made this release available locally; it did not select or apply it.',
|
|
380
|
+
};
|
|
381
|
+
state.resources[identity.id] = current;
|
|
382
|
+
if (typeof options.beforeStateWrite === 'function') options.beforeStateWrite();
|
|
383
|
+
privateJson(paths.teamHubResourceInstalledPath, state);
|
|
384
|
+
if (typeof options.beforeReceiptWrite === 'function') options.beforeReceiptWrite();
|
|
385
|
+
privateJson(resolve(paths.teamHubResourceReceiptsRoot, `${receipt.receiptId}.json`), receipt);
|
|
386
|
+
try { rmSync(backup, { recursive: true, force: true }); } catch { /* A later install safely clears a stale backup. */ }
|
|
387
|
+
return {
|
|
388
|
+
schema: 'ewai.team-hub-resource-install-result/v1', replayed: false, receipt,
|
|
389
|
+
workspace: projectTeamHubResourceState(state, [receipt, ...previousReceipts]),
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
rmSync(managedPath, { recursive: true, force: true });
|
|
393
|
+
if (existsSync(backup)) renameSync(backup, managedPath);
|
|
394
|
+
if (statePathExisted) privateJson(paths.teamHubResourceInstalledPath, previousState);
|
|
395
|
+
else rmSync(paths.teamHubResourceInstalledPath, { force: true });
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
} finally {
|
|
399
|
+
rmSync(staging, { recursive: true, force: true });
|
|
400
|
+
rmSync(candidate, { recursive: true, force: true });
|
|
401
|
+
releaseLease();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function connectTeamHub(projectRoot, input = {}) {
|
|
406
|
+
if (input.confirmed !== true) throw new Error('Team Hub connection requires explicit confirmation.');
|
|
407
|
+
if (input.disclosureAcknowledged !== true) throw new Error('Team Hub connection requires acknowledgement of the disclosure contract.');
|
|
408
|
+
const disclosure = inspectTeamHubDisclosure();
|
|
409
|
+
const value = {
|
|
410
|
+
schema: 'ewai.team-hub-connection/v1',
|
|
411
|
+
endpoint: safeEndpoint(input.endpoint),
|
|
412
|
+
projectId: input.projectId ?? `project-${randomUUID()}`,
|
|
413
|
+
tokenEnv: String(input.tokenEnv ?? ''),
|
|
414
|
+
disclosureDigest: disclosure.contractDigest,
|
|
415
|
+
connectedAt: input.at ?? new Date().toISOString(),
|
|
416
|
+
};
|
|
417
|
+
const parsed = connectionSchema.safeParse(value);
|
|
418
|
+
if (!parsed.success) throw new Error(`Team Hub connection is invalid: ${parsed.error.issues.map((issue) => issue.message).join('; ')}`);
|
|
419
|
+
privateJson(runtimePaths(projectRoot).teamHubConnectionPath, parsed.data);
|
|
420
|
+
return parsed.data;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function recordAttempt(projectRoot, attempt) {
|
|
424
|
+
const paths = runtimePaths(projectRoot);
|
|
425
|
+
const path = resolve(paths.teamHubAttemptsRoot, `attempt-${attempt.at.replace(/[^0-9]/g, '')}-${attempt.attemptId}.json`);
|
|
426
|
+
privateJson(path, attempt);
|
|
427
|
+
return attempt;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export async function syncTeamHub(projectRoot, options = {}) {
|
|
431
|
+
if (options.confirmed !== true) throw new Error('Team Hub sync requires explicit confirmation.');
|
|
432
|
+
const paths = runtimePaths(projectRoot);
|
|
433
|
+
const connection = safeConnection(paths.teamHubConnectionPath);
|
|
434
|
+
if (!connection) throw new Error('Team Hub is not connected.');
|
|
435
|
+
const snapshot = options.snapshot ?? buildTeamHubSnapshot(projectRoot, { projectId: connection.projectId });
|
|
436
|
+
if (snapshot.project.id !== connection.projectId) throw new Error('Team Hub snapshot project identity disagrees with the connection.');
|
|
437
|
+
const envelope = buildTeamHubEnvelope(snapshot);
|
|
438
|
+
const attemptId = randomUUID();
|
|
439
|
+
const at = options.at ?? new Date().toISOString();
|
|
440
|
+
const base = { schema: 'ewai.team-hub-attempt/v1', attemptId, at, projectId: connection.projectId, snapshotDigest: envelope.snapshotDigest };
|
|
441
|
+
let attempt;
|
|
442
|
+
let receipt = null;
|
|
443
|
+
try {
|
|
444
|
+
const token = String((options.env ?? process.env)[connection.tokenEnv] ?? '');
|
|
445
|
+
if (!token) {
|
|
446
|
+
const error = new Error(`Team Hub token environment variable is unavailable: ${connection.tokenEnv}`);
|
|
447
|
+
error.code = 'missing-token';
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
451
|
+
const response = await fetcher(`${connection.endpoint}/api/v1/snapshots`, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
454
|
+
body: JSON.stringify(envelope),
|
|
455
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 8_000),
|
|
456
|
+
});
|
|
457
|
+
if (!response.ok) throw new Error(`Hub rejected the snapshot with HTTP ${response.status}.`);
|
|
458
|
+
receipt = validateTeamHubReceipt(await response.json());
|
|
459
|
+
if (receipt.projectId !== connection.projectId || receipt.snapshotDigest !== envelope.snapshotDigest || receipt.idempotencyKey !== envelope.idempotencyKey) {
|
|
460
|
+
throw new Error('Hub receipt identity or digest disagrees with the submitted snapshot.');
|
|
461
|
+
}
|
|
462
|
+
attempt = { ...base, status: 'accepted', receiptId: receipt.receiptId, error: null };
|
|
463
|
+
privateJson(paths.teamHubAcceptedReceiptPath, receipt);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
attempt = {
|
|
466
|
+
...base, status: 'failed', receiptId: null,
|
|
467
|
+
error: { code: error?.code === 'missing-token' ? 'missing-token' : error?.name === 'TimeoutError' ? 'timeout' : 'submission-failed', message: String(error?.message ?? 'Team Hub submission failed.').slice(0, 240) },
|
|
468
|
+
};
|
|
469
|
+
receipt = null;
|
|
470
|
+
}
|
|
471
|
+
recordAttempt(projectRoot, attempt);
|
|
472
|
+
return { attempt, receipt, workspace: readTeamHubWorkspace(projectRoot) };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function disconnectTeamHub(projectRoot, options = {}) {
|
|
476
|
+
if (options.confirmed !== true) throw new Error('Team Hub disconnect requires explicit confirmation.');
|
|
477
|
+
rmSync(runtimePaths(projectRoot).teamHubConnectionPath, { force: true });
|
|
478
|
+
return readTeamHubWorkspace(projectRoot);
|
|
479
|
+
}
|