@msn-control/liftoff 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPER.md +26 -24
- package/README.md +18 -18
- package/assets/governance/single-maintainer-gitflow/activation-v2-graph.json +1583 -0
- package/dist/adapters/credentials/github-enrollment.d.ts +51 -0
- package/dist/adapters/credentials/github-enrollment.js +226 -0
- package/dist/adapters/credentials/github-enrollment.js.map +1 -0
- package/dist/adapters/credentials/protected-input.d.ts +7 -0
- package/dist/adapters/credentials/protected-input.js +68 -0
- package/dist/adapters/credentials/protected-input.js.map +1 -0
- package/dist/adapters/filesystem/executables.d.ts +2 -0
- package/dist/adapters/filesystem/executables.js +70 -0
- package/dist/adapters/filesystem/executables.js.map +1 -0
- package/dist/adapters/filesystem/update-previews.d.ts +9 -0
- package/dist/adapters/filesystem/update-previews.js +45 -2
- package/dist/adapters/filesystem/update-previews.js.map +1 -1
- package/dist/adapters/filesystem/workstation-attempts.d.ts +13 -0
- package/dist/adapters/filesystem/workstation-attempts.js +78 -0
- package/dist/adapters/filesystem/workstation-attempts.js.map +1 -0
- package/dist/adapters/github/activation-rest.d.ts +40 -0
- package/dist/adapters/github/activation-rest.js +272 -0
- package/dist/adapters/github/activation-rest.js.map +1 -0
- package/dist/adapters/process/shell-command.d.ts +1 -0
- package/dist/adapters/process/shell-command.js +5 -2
- package/dist/adapters/process/shell-command.js.map +1 -1
- package/dist/adapters/state/azure-blob.d.ts +43 -0
- package/dist/adapters/state/azure-blob.js +335 -0
- package/dist/adapters/state/azure-blob.js.map +1 -0
- package/dist/adapters/state/darwin-capabilities.d.ts +33 -0
- package/dist/adapters/state/darwin-capabilities.js +146 -0
- package/dist/adapters/state/darwin-capabilities.js.map +1 -0
- package/dist/adapters/state/darwin-native-host.d.ts +32 -0
- package/dist/adapters/state/darwin-native-host.js +294 -0
- package/dist/adapters/state/darwin-native-host.js.map +1 -0
- package/dist/adapters/state/darwin-profile.d.ts +6 -0
- package/dist/adapters/state/darwin-profile.js +56 -0
- package/dist/adapters/state/darwin-profile.js.map +1 -0
- package/dist/adapters/state/darwin-system-program.d.ts +1 -0
- package/dist/adapters/state/darwin-system-program.js +94 -0
- package/dist/adapters/state/darwin-system-program.js.map +1 -0
- package/dist/adapters/state/local-qualification.d.ts +16 -0
- package/dist/adapters/state/local-qualification.js +299 -0
- package/dist/adapters/state/local-qualification.js.map +1 -0
- package/dist/adapters/state/local.d.ts +25 -0
- package/dist/adapters/state/local.js +145 -0
- package/dist/adapters/state/local.js.map +1 -0
- package/dist/adapters/state/native-command.d.ts +21 -0
- package/dist/adapters/state/native-command.js +82 -0
- package/dist/adapters/state/native-command.js.map +1 -0
- package/dist/adapters/state/native-files.d.ts +2 -0
- package/dist/adapters/state/native-files.js +47 -0
- package/dist/adapters/state/native-files.js.map +1 -0
- package/dist/adapters/state/native-system.d.ts +38 -0
- package/dist/adapters/state/native-system.js +86 -0
- package/dist/adapters/state/native-system.js.map +1 -0
- package/dist/adapters/state/opentofu.d.ts +35 -0
- package/dist/adapters/state/opentofu.js +350 -0
- package/dist/adapters/state/opentofu.js.map +1 -0
- package/dist/adapters/state/owned-process.d.ts +43 -0
- package/dist/adapters/state/owned-process.js +252 -0
- package/dist/adapters/state/owned-process.js.map +1 -0
- package/dist/adapters/state/posix-lock-program.d.ts +1 -0
- package/dist/adapters/state/posix-lock-program.js +185 -0
- package/dist/adapters/state/posix-lock-program.js.map +1 -0
- package/dist/adapters/state/posix-native-lock.d.ts +15 -0
- package/dist/adapters/state/posix-native-lock.js +145 -0
- package/dist/adapters/state/posix-native-lock.js.map +1 -0
- package/dist/adapters/state/protected-workspace.d.ts +57 -0
- package/dist/adapters/state/protected-workspace.js +412 -0
- package/dist/adapters/state/protected-workspace.js.map +1 -0
- package/dist/application/context.d.ts +3 -0
- package/dist/application/diagnose/doctor.d.ts +5 -0
- package/dist/application/diagnose/doctor.js +21 -5
- package/dist/application/diagnose/doctor.js.map +1 -1
- package/dist/application/diagnose/generated-project.js +2 -7
- package/dist/application/diagnose/generated-project.js.map +1 -1
- package/dist/application/initialize/fixture.js +8 -1
- package/dist/application/initialize/fixture.js.map +1 -1
- package/dist/application/initialize/use-case.js +70 -36
- package/dist/application/initialize/use-case.js.map +1 -1
- package/dist/application/project/manifest.js +5 -4
- package/dist/application/project/manifest.js.map +1 -1
- package/dist/application/state-migration/execution.d.ts +32 -0
- package/dist/application/state-migration/execution.js +440 -0
- package/dist/application/state-migration/execution.js.map +1 -0
- package/dist/application/state-migration/index.d.ts +22 -0
- package/dist/application/state-migration/index.js +17 -0
- package/dist/application/state-migration/index.js.map +1 -0
- package/dist/application/state-migration/lifecycle.d.ts +3 -0
- package/dist/application/state-migration/lifecycle.js +149 -0
- package/dist/application/state-migration/lifecycle.js.map +1 -0
- package/dist/application/state-migration/planning.d.ts +7 -0
- package/dist/application/state-migration/planning.js +150 -0
- package/dist/application/state-migration/planning.js.map +1 -0
- package/dist/application/state-migration/recovery.d.ts +5 -0
- package/dist/application/state-migration/recovery.js +145 -0
- package/dist/application/state-migration/recovery.js.map +1 -0
- package/dist/application/state-migration/runtime.d.ts +32 -0
- package/dist/application/state-migration/runtime.js +188 -0
- package/dist/application/state-migration/runtime.js.map +1 -0
- package/dist/application/state-migration/service.d.ts +2 -0
- package/dist/application/state-migration/service.js +20 -0
- package/dist/application/state-migration/service.js.map +1 -0
- package/dist/application/update/command-guidance.d.ts +19 -2
- package/dist/application/update/command-guidance.js +20 -6
- package/dist/application/update/command-guidance.js.map +1 -1
- package/dist/application/update/guidance-context.d.ts +2 -0
- package/dist/application/update/guidance-context.js +35 -0
- package/dist/application/update/guidance-context.js.map +1 -0
- package/dist/application/update/inspection.d.ts +21 -4
- package/dist/application/update/inspection.js +88 -29
- package/dist/application/update/inspection.js.map +1 -1
- package/dist/application/update/migration-runtime.js +25 -13
- package/dist/application/update/migration-runtime.js.map +1 -1
- package/dist/application/update/output.d.ts +4 -1
- package/dist/application/update/output.js +14 -4
- package/dist/application/update/output.js.map +1 -1
- package/dist/application/update/preview.d.ts +10 -1
- package/dist/application/update/preview.js +26 -4
- package/dist/application/update/preview.js.map +1 -1
- package/dist/application/update/revalidation-plan.d.ts +1 -0
- package/dist/application/update/revalidation-plan.js +27 -15
- package/dist/application/update/revalidation-plan.js.map +1 -1
- package/dist/application/update/revalidation.d.ts +4 -1
- package/dist/application/update/revalidation.js +64 -23
- package/dist/application/update/revalidation.js.map +1 -1
- package/dist/application/update/use-case.js +50 -29
- package/dist/application/update/use-case.js.map +1 -1
- package/dist/cli/args/definitions.js +11 -3
- package/dist/cli/args/definitions.js.map +1 -1
- package/dist/cli/args/parser.js +29 -3
- package/dist/cli/args/parser.js.map +1 -1
- package/dist/domain/governance/activation/approvals.d.ts +264 -7
- package/dist/domain/governance/activation/approvals.js +166 -23
- package/dist/domain/governance/activation/approvals.js.map +1 -1
- package/dist/domain/governance/activation/capabilities.js +5 -2
- package/dist/domain/governance/activation/capabilities.js.map +1 -1
- package/dist/domain/governance/activation/evidence.d.ts +2 -1
- package/dist/domain/governance/activation/evidence.js +64 -11
- package/dist/domain/governance/activation/evidence.js.map +1 -1
- package/dist/domain/governance/activation/graph.d.ts +11 -384
- package/dist/domain/governance/activation/graph.js +424 -379
- package/dist/domain/governance/activation/graph.js.map +1 -1
- package/dist/domain/governance/activation/inputs.d.ts +3 -1
- package/dist/domain/governance/activation/inputs.js +47 -6
- package/dist/domain/governance/activation/inputs.js.map +1 -1
- package/dist/domain/governance/activation/operations.d.ts +4 -1
- package/dist/domain/governance/activation/operations.js +110 -22
- package/dist/domain/governance/activation/operations.js.map +1 -1
- package/dist/domain/governance/activation/readiness.d.ts +10 -0
- package/dist/domain/governance/activation/readiness.js +77 -38
- package/dist/domain/governance/activation/readiness.js.map +1 -1
- package/dist/domain/governance/activation/types.d.ts +151 -9
- package/dist/domain/governance/activation/types.js +23 -1
- package/dist/domain/governance/activation/types.js.map +1 -1
- package/dist/domain/governance/activation/validators.d.ts +4 -1
- package/dist/domain/governance/activation/validators.js +419 -26
- package/dist/domain/governance/activation/validators.js.map +1 -1
- package/dist/domain/governance/policy/identity.d.ts +55 -13
- package/dist/domain/governance/policy/identity.js +28 -8
- package/dist/domain/governance/policy/identity.js.map +1 -1
- package/dist/domain/project/artifact-lifecycle.d.ts +2 -1
- package/dist/domain/project/artifact-lifecycle.js +7 -1
- package/dist/domain/project/artifact-lifecycle.js.map +1 -1
- package/dist/domain/project/catalog.d.ts +50 -1
- package/dist/domain/project/catalog.js +104 -4
- package/dist/domain/project/catalog.js.map +1 -1
- package/dist/domain/project/contracts.d.ts +9 -1
- package/dist/domain/project/infrastructure-layout.d.ts +1 -1
- package/dist/domain/project/infrastructure-layout.js +3 -1
- package/dist/domain/project/infrastructure-layout.js.map +1 -1
- package/dist/domain/project/manifest/governance.d.ts +1 -1
- package/dist/domain/project/manifest/governance.js +9 -10
- package/dist/domain/project/manifest/governance.js.map +1 -1
- package/dist/domain/project/planning.js +4 -5
- package/dist/domain/project/planning.js.map +1 -1
- package/dist/domain/repair/stateful-bounded.d.ts +1 -0
- package/dist/domain/repair/stateful-bounded.js +37 -0
- package/dist/domain/repair/stateful-bounded.js.map +1 -0
- package/dist/domain/repair/stateful-invariants.d.ts +28 -0
- package/dist/domain/repair/stateful-invariants.js +278 -0
- package/dist/domain/repair/stateful-invariants.js.map +1 -0
- package/dist/domain/repair/stateful.d.ts +690 -0
- package/dist/domain/repair/stateful.js +26 -0
- package/dist/domain/repair/stateful.js.map +1 -0
- package/dist/domain/workstation/constraints.d.ts +1 -0
- package/dist/domain/workstation/constraints.js.map +1 -1
- package/dist/domain/workstation/contracts.d.ts +42 -0
- package/dist/domain/workstation/contracts.js +2 -0
- package/dist/domain/workstation/contracts.js.map +1 -0
- package/dist/domain/workstation/executables.d.ts +19 -0
- package/dist/domain/workstation/executables.js +68 -0
- package/dist/domain/workstation/executables.js.map +1 -0
- package/dist/domain/workstation/versions.d.ts +8 -0
- package/dist/domain/workstation/versions.js +145 -0
- package/dist/domain/workstation/versions.js.map +1 -0
- package/dist/framework-adapters.d.ts +2 -0
- package/dist/framework-adapters.js +148 -22
- package/dist/framework-adapters.js.map +1 -1
- package/dist/framework-validation.d.ts +6 -0
- package/dist/framework-validation.js +89 -24
- package/dist/framework-validation.js.map +1 -1
- package/dist/generators/common/base.js +57 -17
- package/dist/generators/common/base.js.map +1 -1
- package/dist/governance-activation/activation-state.js +39 -7
- package/dist/governance-activation/activation-state.js.map +1 -1
- package/dist/governance-activation/authority-records.d.ts +4 -0
- package/dist/governance-activation/authority-records.js +24 -0
- package/dist/governance-activation/authority-records.js.map +1 -0
- package/dist/governance-activation/commands.d.ts +9 -1
- package/dist/governance-activation/commands.js +392 -97
- package/dist/governance-activation/commands.js.map +1 -1
- package/dist/governance-activation/compatibility.d.ts +11 -8
- package/dist/governance-activation/compatibility.js +37 -23
- package/dist/governance-activation/compatibility.js.map +1 -1
- package/dist/governance-activation/doctor.js +40 -16
- package/dist/governance-activation/doctor.js.map +1 -1
- package/dist/governance-activation/github-config.d.ts +17 -0
- package/dist/governance-activation/github-config.js +88 -0
- package/dist/governance-activation/github-config.js.map +1 -0
- package/dist/governance-activation/github-discovery.d.ts +18 -0
- package/dist/governance-activation/github-discovery.js +229 -0
- package/dist/governance-activation/github-discovery.js.map +1 -0
- package/dist/governance-activation/github-ports.d.ts +12 -0
- package/dist/governance-activation/github-ports.js +4 -0
- package/dist/governance-activation/github-ports.js.map +1 -0
- package/dist/governance-activation/github-publication.d.ts +3 -0
- package/dist/governance-activation/github-publication.js +190 -0
- package/dist/governance-activation/github-publication.js.map +1 -0
- package/dist/governance-activation/historical-common.d.ts +98 -0
- package/dist/governance-activation/historical-common.js +269 -0
- package/dist/governance-activation/historical-common.js.map +1 -0
- package/dist/governance-activation/historical-credential-policy.d.ts +3 -0
- package/dist/governance-activation/historical-credential-policy.js +91 -0
- package/dist/governance-activation/historical-credential-policy.js.map +1 -0
- package/dist/governance-activation/historical-inputs.d.ts +6 -0
- package/dist/governance-activation/historical-inputs.js +57 -0
- package/dist/governance-activation/historical-inputs.js.map +1 -0
- package/dist/governance-activation/historical-safety.d.ts +9 -0
- package/dist/governance-activation/historical-safety.js +102 -0
- package/dist/governance-activation/historical-safety.js.map +1 -0
- package/dist/governance-activation/historical-source-metadata.d.ts +31 -0
- package/dist/governance-activation/historical-source-metadata.js +55 -0
- package/dist/governance-activation/historical-source-metadata.js.map +1 -0
- package/dist/governance-activation/historical-state.d.ts +12 -50
- package/dist/governance-activation/historical-state.js +195 -308
- package/dist/governance-activation/historical-state.js.map +1 -1
- package/dist/governance-activation/historical-v1-auxiliary.d.ts +1 -0
- package/dist/governance-activation/historical-v1-auxiliary.js +43 -0
- package/dist/governance-activation/historical-v1-auxiliary.js.map +1 -0
- package/dist/governance-activation/historical-v1-phase-contracts.d.ts +31 -0
- package/dist/governance-activation/historical-v1-phase-contracts.js +46 -0
- package/dist/governance-activation/historical-v1-phase-contracts.js.map +1 -0
- package/dist/governance-activation/historical-v2.d.ts +53 -0
- package/dist/governance-activation/historical-v2.js +313 -0
- package/dist/governance-activation/historical-v2.js.map +1 -0
- package/dist/governance-activation/history-contracts.d.ts +21 -12
- package/dist/governance-activation/history-contracts.js +99 -27
- package/dist/governance-activation/history-contracts.js.map +1 -1
- package/dist/governance-activation/inputs.d.ts +8 -2
- package/dist/governance-activation/inputs.js +41 -6
- package/dist/governance-activation/inputs.js.map +1 -1
- package/dist/governance-activation/migration-history.d.ts +43 -5
- package/dist/governance-activation/migration-history.js +231 -50
- package/dist/governance-activation/migration-history.js.map +1 -1
- package/dist/governance-activation/migration.d.ts +7 -4
- package/dist/governance-activation/migration.js +53 -252
- package/dist/governance-activation/migration.js.map +1 -1
- package/dist/governance-activation/phase-azure.d.ts +3 -0
- package/dist/governance-activation/phase-azure.js +205 -0
- package/dist/governance-activation/phase-azure.js.map +1 -0
- package/dist/governance-activation/phase-bootstrap-state.js +3 -0
- package/dist/governance-activation/phase-bootstrap-state.js.map +1 -1
- package/dist/governance-activation/phase-github.d.ts +3 -0
- package/dist/governance-activation/phase-github.js +192 -0
- package/dist/governance-activation/phase-github.js.map +1 -0
- package/dist/governance-activation/phase-governance.js +8 -2
- package/dist/governance-activation/phase-governance.js.map +1 -1
- package/dist/governance-activation/public-plans.d.ts +36 -0
- package/dist/governance-activation/public-plans.js +100 -0
- package/dist/governance-activation/public-plans.js.map +1 -0
- package/dist/governance-activation/read-only.d.ts +3 -1
- package/dist/governance-activation/read-only.js +7 -3
- package/dist/governance-activation/read-only.js.map +1 -1
- package/dist/governance-activation/reconciliation.js +10 -0
- package/dist/governance-activation/reconciliation.js.map +1 -1
- package/dist/governance-activation/seed-lifecycle.js +14 -25
- package/dist/governance-activation/seed-lifecycle.js.map +1 -1
- package/dist/governance-activation/source-of-truth.d.ts +21 -5
- package/dist/governance-activation/source-of-truth.js +102 -13
- package/dist/governance-activation/source-of-truth.js.map +1 -1
- package/dist/governance-activation/task-projection.d.ts +4 -1
- package/dist/governance-activation/task-projection.js +32 -3
- package/dist/governance-activation/task-projection.js.map +1 -1
- package/dist/governance-activation/task-writes.d.ts +42 -0
- package/dist/governance-activation/task-writes.js +250 -0
- package/dist/governance-activation/task-writes.js.map +1 -0
- package/dist/governance-activation/transition-files.d.ts +12 -0
- package/dist/governance-activation/transition-files.js +109 -0
- package/dist/governance-activation/transition-files.js.map +1 -0
- package/dist/governance-activation/transition-planning.d.ts +3 -1
- package/dist/governance-activation/transition-planning.js +158 -20
- package/dist/governance-activation/transition-planning.js.map +1 -1
- package/dist/governance-activation/transition-ports.d.ts +31 -3
- package/dist/governance-activation/transition-records.d.ts +11 -1
- package/dist/governance-activation/transition-records.js +52 -6
- package/dist/governance-activation/transition-records.js.map +1 -1
- package/dist/governance-activation/transitions.d.ts +5 -0
- package/dist/governance-activation/transitions.js +271 -37
- package/dist/governance-activation/transitions.js.map +1 -1
- package/dist/governance-assessment/engine.js +22 -15
- package/dist/governance-assessment/engine.js.map +1 -1
- package/dist/governance-assessment/project.d.ts +2 -0
- package/dist/governance-assessment/project.js +2 -1
- package/dist/governance-assessment/project.js.map +1 -1
- package/dist/interactive.d.ts +0 -1
- package/dist/interactive.js +3 -22
- package/dist/interactive.js.map +1 -1
- package/dist/openspec-profile.d.ts +2 -1
- package/dist/openspec-profile.js +11 -41
- package/dist/openspec-profile.js.map +1 -1
- package/dist/repository-governance.d.ts +10 -5
- package/dist/repository-governance.js +178 -92
- package/dist/repository-governance.js.map +1 -1
- package/dist/telemetry/contract.d.ts +1 -1
- package/dist/telemetry/contract.js +3 -0
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/workstation-catalog.d.ts +13 -1
- package/dist/workstation-catalog.js +122 -0
- package/dist/workstation-catalog.js.map +1 -1
- package/dist/workstation.d.ts +69 -14
- package/dist/workstation.js +600 -215
- package/dist/workstation.js.map +1 -1
- package/docs/cli-reference.md +94 -23
- package/docs/configuration-and-manifests.md +11 -10
- package/docs/prerequisites.md +93 -18
- package/docs/repository-governance.md +68 -40
- package/docs/spec-workflows-and-agents.md +132 -13
- package/docs/troubleshooting.md +28 -0
- package/package.json +1 -1
package/dist/workstation.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
3
3
|
import { packagedSupportedStack as supportedStack } from './adapters/packaged-assets/supported-stack.js';
|
|
4
4
|
import { workstationRequirementCatalog } from './workstation-catalog.js';
|
|
5
|
+
import { formatCommand, NodeCommandRunner } from './process-runner.js';
|
|
6
|
+
import { compareVersionCores, compareVersions, extractVersion, isPrereleaseVersion, matchesReleaseLine } from './domain/workstation/versions.js';
|
|
7
|
+
import { environmentValue, hostPath, unavailableExecutableObserver } from './domain/workstation/executables.js';
|
|
8
|
+
import { nativeExecutableObserver } from './adapters/filesystem/executables.js';
|
|
9
|
+
import { canonicalSha256 } from './domain/governance/activation/canonical-json.js';
|
|
10
|
+
export { extractVersion } from './domain/workstation/versions.js';
|
|
11
|
+
export { nativeExecutableObserver } from './adapters/filesystem/executables.js';
|
|
5
12
|
const REQUIREMENT_ORDER = [
|
|
6
13
|
'node',
|
|
7
14
|
'npm',
|
|
@@ -14,37 +21,11 @@ const REQUIREMENT_ORDER = [
|
|
|
14
21
|
'openspec',
|
|
15
22
|
'spec-kit',
|
|
16
23
|
'github-copilot',
|
|
17
|
-
'claude'
|
|
24
|
+
'claude',
|
|
25
|
+
'codex',
|
|
26
|
+
'github-cli'
|
|
18
27
|
];
|
|
19
|
-
const MISSING_ERROR_CODES = new Set(['ENOENT'
|
|
20
|
-
function compareVersions(left, right) {
|
|
21
|
-
const leftParts = versionCore(left).split('.').map(Number);
|
|
22
|
-
const rightParts = versionCore(right).split('.').map(Number);
|
|
23
|
-
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
24
|
-
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
25
|
-
if (difference !== 0) {
|
|
26
|
-
return difference;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return 0;
|
|
30
|
-
}
|
|
31
|
-
export function extractVersion(output) {
|
|
32
|
-
return output.match(/(?:^|[^0-9A-Za-z])(?:v|go)?([0-9]+\.[0-9]+(?:\.[0-9]+)?(?:(?:-[0-9A-Za-z]|[A-Za-z])[0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z.-]+)?)(?=$|[^0-9A-Za-z.+-])/i)?.[1];
|
|
33
|
-
}
|
|
34
|
-
function versionCore(value) {
|
|
35
|
-
return value
|
|
36
|
-
.split('+', 1)[0]
|
|
37
|
-
.match(/^\d+\.\d+(?:\.\d+)?/)?.[0] ?? value;
|
|
38
|
-
}
|
|
39
|
-
function isPrereleaseVersion(value) {
|
|
40
|
-
const withoutBuild = value.split('+', 1)[0];
|
|
41
|
-
return withoutBuild.slice(versionCore(withoutBuild).length).length > 0;
|
|
42
|
-
}
|
|
43
|
-
function matchesReleaseLine(value, releaseLine) {
|
|
44
|
-
const versionParts = versionCore(value).split('.');
|
|
45
|
-
const releaseParts = releaseLine.split('.');
|
|
46
|
-
return releaseParts.every((part, index) => versionParts[index] === part);
|
|
47
|
-
}
|
|
28
|
+
const MISSING_ERROR_CODES = new Set(['ENOENT']);
|
|
48
29
|
export function selectLiftoffRuntimeRequirements() {
|
|
49
30
|
const definition = workstationRequirementCatalog.node;
|
|
50
31
|
return [{
|
|
@@ -58,6 +39,7 @@ export function selectLiftoffRuntimeRequirements() {
|
|
|
58
39
|
}];
|
|
59
40
|
}
|
|
60
41
|
export function selectWorkstationRequirements(plan, options = {}) {
|
|
42
|
+
const scope = options.scope ?? 'initialization';
|
|
61
43
|
const selected = new Map();
|
|
62
44
|
const add = (id, reason, overrides = {}) => {
|
|
63
45
|
const definition = workstationRequirementCatalog[id];
|
|
@@ -66,6 +48,8 @@ export function selectWorkstationRequirements(plan, options = {}) {
|
|
|
66
48
|
const exactVersion = overrides.exactVersion ?? definition.exactVersion;
|
|
67
49
|
if (existing) {
|
|
68
50
|
existing.reasons.push(reason);
|
|
51
|
+
if (overrides.severity === 'blocking')
|
|
52
|
+
existing.severity = 'blocking';
|
|
69
53
|
if (minimumVersion && (!existing.minimumVersion || compareVersions(minimumVersion, existing.minimumVersion) > 0)) {
|
|
70
54
|
existing.minimumVersion = minimumVersion;
|
|
71
55
|
}
|
|
@@ -79,7 +63,8 @@ export function selectWorkstationRequirements(plan, options = {}) {
|
|
|
79
63
|
...(minimumVersion ? { minimumVersion } : {}),
|
|
80
64
|
...(exactVersion ? { exactVersion } : {}),
|
|
81
65
|
...(definition.releaseLine ? { releaseLine: definition.releaseLine } : {}),
|
|
82
|
-
allowPrerelease: definition.allowPrerelease ?? false
|
|
66
|
+
allowPrerelease: definition.allowPrerelease ?? false,
|
|
67
|
+
scope
|
|
83
68
|
});
|
|
84
69
|
};
|
|
85
70
|
const workload = typeof plan.workload === 'string'
|
|
@@ -95,6 +80,18 @@ export function selectWorkstationRequirements(plan, options = {}) {
|
|
|
95
80
|
add('node', 'Liftoff runtime', {
|
|
96
81
|
minimumVersion: supportedStack.runtimes.node.minimumVersion
|
|
97
82
|
});
|
|
83
|
+
if (scope === 'activation' || scope === 'migration' || scope === 'lifecycle') {
|
|
84
|
+
if (scope !== 'lifecycle')
|
|
85
|
+
add('opentofu', `${scope} infrastructure operations`, { severity: 'blocking' });
|
|
86
|
+
if (scope === 'activation') {
|
|
87
|
+
add('github-cli', 'approved GitHub activation operations', { severity: 'blocking' });
|
|
88
|
+
if (workload.provider.id === 'azure')
|
|
89
|
+
add('azure-cli', 'approved Azure activation operations', { severity: 'blocking' });
|
|
90
|
+
}
|
|
91
|
+
for (const id of options.requiredTools ?? [])
|
|
92
|
+
add(id, `required ${scope} operation`, { severity: 'blocking' });
|
|
93
|
+
return REQUIREMENT_ORDER.flatMap((id) => selected.has(id) ? [selected.get(id)] : []);
|
|
94
|
+
}
|
|
98
95
|
if (workload.apiStack.id === 'node-fastify') {
|
|
99
96
|
add('npm', 'selected Node.js API dependency manager');
|
|
100
97
|
}
|
|
@@ -130,169 +127,259 @@ export function selectWorkstationRequirements(plan, options = {}) {
|
|
|
130
127
|
add('spec-kit', 'selected spec-driven framework', { exactVersion: plan.framework.version });
|
|
131
128
|
}
|
|
132
129
|
}
|
|
133
|
-
add('docker', 'generated local development stack');
|
|
134
|
-
add('opentofu', 'generated infrastructure');
|
|
135
|
-
if (workload.provider.id === 'azure') {
|
|
130
|
+
add('docker', scope === 'local' ? 'local Docker Compose configuration check' : 'generated local development stack', { severity: scope === 'local' ? 'blocking' : 'advisory' });
|
|
131
|
+
add('opentofu', scope === 'local' ? 'local backend-disabled infrastructure checks' : 'generated infrastructure', { severity: scope === 'local' ? 'blocking' : 'advisory' });
|
|
132
|
+
if (scope === 'initialization' && workload.provider.id === 'azure') {
|
|
136
133
|
add('azure-cli', 'selected Azure cloud');
|
|
137
134
|
}
|
|
138
135
|
for (const agent of plan.agents) {
|
|
139
136
|
add(agent.id, `selected ${agent.label} coding agent`);
|
|
140
137
|
}
|
|
138
|
+
for (const id of options.requiredTools ?? [])
|
|
139
|
+
add(id, `required ${scope} operation`, { severity: 'blocking' });
|
|
141
140
|
return REQUIREMENT_ORDER.flatMap((id) => {
|
|
142
141
|
const requirement = selected.get(id);
|
|
143
142
|
return requirement ? [requirement] : [];
|
|
144
143
|
});
|
|
145
144
|
}
|
|
146
|
-
function
|
|
145
|
+
function commandMissing(result) {
|
|
146
|
+
return result.errorCode !== undefined && MISSING_ERROR_CODES.has(result.errorCode);
|
|
147
|
+
}
|
|
148
|
+
function requiredConstraint(requirement) {
|
|
149
|
+
return {
|
|
150
|
+
...(requirement.minimumVersion ? { minimumVersion: requirement.minimumVersion } : {}),
|
|
151
|
+
...(requirement.exactVersion ? { exactVersion: requirement.exactVersion } : {}),
|
|
152
|
+
...(requirement.releaseLine ? { releaseLine: requirement.releaseLine } : {}),
|
|
153
|
+
allowPrerelease: requirement.allowPrerelease ?? false
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function registeredRequirement(requirement) {
|
|
157
|
+
const definition = workstationRequirementCatalog[requirement.id];
|
|
158
|
+
const requestedMinimum = requirement.minimumVersion ?? definition.minimumVersion;
|
|
159
|
+
const minimumVersion = requestedMinimum && definition.minimumVersion &&
|
|
160
|
+
extractVersion(requestedMinimum) === requestedMinimum && compareVersionCores(requestedMinimum, definition.minimumVersion) < 0
|
|
161
|
+
? definition.minimumVersion : requestedMinimum;
|
|
162
|
+
return {
|
|
163
|
+
...requirement, definition, minimumVersion,
|
|
164
|
+
exactVersion: requirement.exactVersion ?? definition.exactVersion,
|
|
165
|
+
releaseLine: requirement.releaseLine ?? definition.releaseLine,
|
|
166
|
+
allowPrerelease: definition.allowPrerelease === true && requirement.allowPrerelease !== false
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function unsupportedConstraint(requirement) {
|
|
170
|
+
for (const field of ['minimumVersion', 'exactVersion']) {
|
|
171
|
+
if (requirement[field] !== undefined && extractVersion(requirement[field]) !== requirement[field]) {
|
|
172
|
+
return `The requested ${field} is not a supported version constraint.`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (requirement.definition.exactVersion && requirement.exactVersion !== requirement.definition.exactVersion) {
|
|
176
|
+
return `The registered ${requirement.definition.label} pin is ${requirement.definition.exactVersion}; another requested pin is unsupported.`;
|
|
177
|
+
}
|
|
178
|
+
if (requirement.releaseLine !== undefined && (!/^\d+(?:\.\d+)*$/.test(requirement.releaseLine) ||
|
|
179
|
+
(requirement.definition.releaseLine && requirement.releaseLine !== requirement.definition.releaseLine))) {
|
|
180
|
+
return `The requested release line does not match the registered ${requirement.definition.label} constraint.`;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
function observationContext(requirement, options) {
|
|
185
|
+
return {
|
|
186
|
+
platform: options.host?.platform ?? (process.platform === 'win32' || process.platform === 'darwin' ? process.platform : 'linux'),
|
|
187
|
+
cwd: options.cwd ?? process.cwd(),
|
|
188
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
189
|
+
definition: requirement.definition
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function executableObserver(runner, options) {
|
|
193
|
+
return options.executableObserver ??
|
|
194
|
+
(runner instanceof NodeCommandRunner ? nativeExecutableObserver : unavailableExecutableObserver);
|
|
195
|
+
}
|
|
196
|
+
function safeDetail(value) {
|
|
197
|
+
return stripVTControlCharacters(value)
|
|
198
|
+
.replace(/\b(?:gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|sk-[A-Za-z0-9_-]+)\b/g, '<redacted>')
|
|
199
|
+
.replace(/\b(?:authorization|password|secret|token)\s*[:=]\s*\S+/gi, '<redacted>')
|
|
200
|
+
.split(/\r?\n/, 1)[0].replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 600);
|
|
201
|
+
}
|
|
202
|
+
async function runToolCommand(runner, command, options) {
|
|
203
|
+
try {
|
|
204
|
+
return await runner.run(command, options);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
const errorCode = error && typeof error === 'object' && 'code' in error &&
|
|
208
|
+
typeof error.code === 'string' && /^[A-Z0-9_]{1,64}$/.test(error.code)
|
|
209
|
+
? error.code : 'COMMAND_RUNNER_ERROR';
|
|
210
|
+
return {
|
|
211
|
+
command, displayCommand: formatCommand(command), status: null, signal: null,
|
|
212
|
+
stdout: '', stderr: '', timedOut: false, errorCode,
|
|
213
|
+
errorMessage: error instanceof Error ? safeDetail(error.message) : 'The command runner could not complete the observation.'
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function resultWithCause(requirement, identity, reasonCode, detail, fields = {}) {
|
|
218
|
+
const states = {
|
|
219
|
+
compatible: 'ready',
|
|
220
|
+
'missing-executable': 'missing',
|
|
221
|
+
'observation-unavailable': 'not-observable',
|
|
222
|
+
'probe-failed': 'unhealthy',
|
|
223
|
+
'version-unparseable': 'unhealthy',
|
|
224
|
+
'unsupported-constraint': 'not-observable',
|
|
225
|
+
'below-minimum': 'outdated',
|
|
226
|
+
'release-line-mismatch': 'outdated',
|
|
227
|
+
'exact-version-mismatch': 'outdated',
|
|
228
|
+
'incompatible-channel': 'outdated'
|
|
229
|
+
};
|
|
147
230
|
return {
|
|
148
231
|
requirement,
|
|
149
|
-
state:
|
|
232
|
+
state: states[reasonCode],
|
|
233
|
+
reasonCode,
|
|
150
234
|
detail,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
235
|
+
identity,
|
|
236
|
+
required: requiredConstraint(requirement),
|
|
237
|
+
observations: [],
|
|
238
|
+
notices: [],
|
|
239
|
+
...fields
|
|
154
240
|
};
|
|
155
241
|
}
|
|
156
|
-
function
|
|
157
|
-
return
|
|
242
|
+
function missingResult(requirement, identity, detail = 'command not found') {
|
|
243
|
+
return resultWithCause(requirement, identity, 'missing-executable', detail, {
|
|
244
|
+
remedy: requirement.definition.missingRemedy ?? `Install ${requirement.definition.label}.`
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function runObservedProbe(requirement, command, runner, options) {
|
|
248
|
+
let identity;
|
|
249
|
+
try {
|
|
250
|
+
identity = await executableObserver(runner, options)
|
|
251
|
+
.resolve(command.executable, observationContext(requirement, options));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
identity = {
|
|
255
|
+
executable: command.executable, resolution: 'not-observable',
|
|
256
|
+
origin: 'unknown', evidence: 'unavailable'
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const result = await runToolCommand(runner, command, {
|
|
260
|
+
timeoutMs: 15_000, maxOutputBytes: 16_384, cwd: options.cwd, env: options.env
|
|
261
|
+
});
|
|
262
|
+
if (!commandMissing(result) && result.status === 0 && !result.timedOut &&
|
|
263
|
+
!result.outputLimitExceeded && !result.aborted && !result.errorCode && !result.signal) {
|
|
264
|
+
identity = {
|
|
265
|
+
...identity,
|
|
266
|
+
resolution: 'resolved',
|
|
267
|
+
evidence: identity.resolvedPath ? identity.evidence : 'version-probe'
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { result, identity };
|
|
271
|
+
}
|
|
272
|
+
function observed(classified, command, result) {
|
|
273
|
+
classified.observations = [{
|
|
274
|
+
command,
|
|
275
|
+
identity: classified.identity,
|
|
276
|
+
status: result.status,
|
|
277
|
+
timedOut: result.timedOut,
|
|
278
|
+
...(result.errorCode ? { errorCode: result.errorCode } : {}),
|
|
279
|
+
reasonCode: classified.reasonCode,
|
|
280
|
+
...(classified.detectedVersion ? { detectedVersion: classified.detectedVersion } : {})
|
|
281
|
+
}];
|
|
282
|
+
return classified;
|
|
158
283
|
}
|
|
159
284
|
async function probeCommandCandidates(requirement, runner, options) {
|
|
160
285
|
const results = [];
|
|
161
286
|
for (const command of requirement.definition.probes) {
|
|
162
|
-
const result = await
|
|
163
|
-
|
|
164
|
-
results.push(result);
|
|
165
|
-
}
|
|
287
|
+
const { result, identity } = await runObservedProbe(requirement, command, runner, options);
|
|
288
|
+
results.push(observed(classifyVersion(requirement, command, result, identity), command, result));
|
|
166
289
|
}
|
|
167
290
|
return results;
|
|
168
291
|
}
|
|
169
292
|
async function copilotFallback(requirement, runner, options) {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
state: 'unhealthy',
|
|
184
|
-
detail: result.stderr.trim() || 'VS Code extension discovery failed.',
|
|
185
|
-
remedy: 'Run `code --list-extensions` and repair the VS Code CLI before retrying.',
|
|
186
|
-
notices: []
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
const extensions = result.stdout.toLowerCase().split(/\r?\n/);
|
|
293
|
+
const command = { executable: 'code', args: ['--list-extensions'] };
|
|
294
|
+
const { result, identity } = await runObservedProbe(requirement, command, runner, options);
|
|
295
|
+
if (commandMissing(result) && identity.resolution !== 'resolved') {
|
|
296
|
+
return observed(resultWithCause(requirement, identity, 'observation-unavailable', 'Copilot CLI and the VS Code CLI are unavailable, so agent presence cannot be observed.', {
|
|
297
|
+
remedy: 'Install the Copilot CLI, or verify that GitHub.copilot or GitHub.copilot-chat is enabled in VS Code.'
|
|
298
|
+
}), command, result);
|
|
299
|
+
}
|
|
300
|
+
if (result.status !== 0 || result.timedOut || result.outputLimitExceeded || result.aborted || result.errorCode || result.signal) {
|
|
301
|
+
return observed(resultWithCause(requirement, identity, 'probe-failed', safeDetail(result.stderr) || 'VS Code extension discovery failed.', {
|
|
302
|
+
remedy: 'Run `code --list-extensions` and repair the VS Code CLI before retrying.'
|
|
303
|
+
}), command, result);
|
|
304
|
+
}
|
|
305
|
+
const extensions = result.stdout.toLowerCase().split(/\r?\n/).map((line) => line.trim());
|
|
190
306
|
const installed = extensions.some((id) => id === 'github.copilot' || id === 'github.copilot-chat');
|
|
191
|
-
return installed
|
|
192
|
-
? {
|
|
193
|
-
requirement,
|
|
194
|
-
state: 'ready',
|
|
195
|
-
detail: 'GitHub Copilot is installed as a VS Code extension.',
|
|
307
|
+
return observed(installed
|
|
308
|
+
? resultWithCause(requirement, identity, 'compatible', 'GitHub Copilot is installed as a VS Code extension.', {
|
|
196
309
|
detectedBy: 'code --list-extensions',
|
|
197
310
|
notices: [{
|
|
198
311
|
label: 'GitHub Copilot authentication',
|
|
312
|
+
code: 'authentication',
|
|
199
313
|
state: 'not-observable',
|
|
200
314
|
detail: 'Authentication is managed by the selected editor or agent.',
|
|
201
315
|
remedy: 'Open GitHub Copilot and sign in if prompted.'
|
|
202
316
|
}]
|
|
203
|
-
}
|
|
204
|
-
: missingResult(requirement, 'GitHub Copilot CLI and supported VS Code extensions were not found.');
|
|
317
|
+
})
|
|
318
|
+
: missingResult(requirement, identity, 'GitHub Copilot CLI and supported VS Code extensions were not found.'), command, result);
|
|
205
319
|
}
|
|
206
|
-
function classifyVersion(requirement, command, result) {
|
|
320
|
+
function classifyVersion(requirement, command, result, identity) {
|
|
321
|
+
if (commandMissing(result) && identity.resolution !== 'resolved') {
|
|
322
|
+
return missingResult(requirement, { ...identity, resolution: 'missing' }, safeDetail(result.stderr || result.errorMessage || '') || 'command not found');
|
|
323
|
+
}
|
|
207
324
|
if (result.timedOut) {
|
|
208
|
-
return {
|
|
209
|
-
requirement
|
|
210
|
-
|
|
211
|
-
detail: `${command.executable} version probe timed out.`,
|
|
212
|
-
remedy: `Repair ${requirement.definition.label} and retry.`,
|
|
213
|
-
notices: []
|
|
214
|
-
};
|
|
325
|
+
return resultWithCause(requirement, identity, 'probe-failed', `${command.executable} version probe timed out.`, {
|
|
326
|
+
remedy: `Repair ${requirement.definition.label} and retry.`
|
|
327
|
+
});
|
|
215
328
|
}
|
|
216
|
-
if (result.status !== 0) {
|
|
217
|
-
return {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
remedy: `Repair ${requirement.definition.label} and retry.`,
|
|
222
|
-
notices: []
|
|
223
|
-
};
|
|
329
|
+
if (result.status !== 0 || result.outputLimitExceeded || result.aborted || result.errorCode || result.signal) {
|
|
330
|
+
return resultWithCause(requirement, identity, 'probe-failed', result.outputLimitExceeded ? `${command.executable} version probe exceeded the output limit.` :
|
|
331
|
+
safeDetail(result.stderr || result.errorMessage || '') || `${command.executable} exited with status ${result.status}.`, {
|
|
332
|
+
remedy: `Repair ${requirement.definition.label} and retry.`
|
|
333
|
+
});
|
|
224
334
|
}
|
|
225
|
-
const output = `${result.stdout}\n${result.stderr}`.trim();
|
|
226
|
-
const version = extractVersion(output);
|
|
227
|
-
if (
|
|
228
|
-
return {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
remedy: `Verify ${requirement.definition.label} manually and reinstall it if necessary.`,
|
|
233
|
-
notices: []
|
|
234
|
-
};
|
|
335
|
+
const output = requirement.id === 'azure-cli' ? result.stdout.trim() : `${result.stdout}\n${result.stderr}`.trim();
|
|
336
|
+
const version = extractVersion(output, requirement.id);
|
|
337
|
+
if (!version) {
|
|
338
|
+
return resultWithCause(requirement, identity, 'version-unparseable', `Unable to parse a supported ${requirement.definition.label} version from the successful probe output.`, {
|
|
339
|
+
detectedBy: command.executable,
|
|
340
|
+
remedy: `Verify the ${requirement.definition.label} executable and its version output before selecting a remedy.`
|
|
341
|
+
});
|
|
235
342
|
}
|
|
343
|
+
const detected = { detectedVersion: version, detectedBy: command.executable };
|
|
236
344
|
if (version &&
|
|
237
345
|
!requirement.allowPrerelease &&
|
|
238
346
|
isPrereleaseVersion(version)) {
|
|
239
|
-
return {
|
|
240
|
-
|
|
241
|
-
state: 'outdated',
|
|
242
|
-
detail: `Found prerelease ${version}; a stable release is required.`,
|
|
243
|
-
detectedVersion: version,
|
|
244
|
-
detectedBy: command.executable,
|
|
347
|
+
return resultWithCause(requirement, identity, 'incompatible-channel', `Found prerelease ${version}; a stable release is required.`, {
|
|
348
|
+
...detected,
|
|
245
349
|
remedy: requirement.exactVersion
|
|
246
350
|
? `Install ${requirement.definition.label} ${requirement.exactVersion}.`
|
|
247
|
-
: `Install a stable ${requirement.definition.label} release${requirement.releaseLine ? ` in the supported ${requirement.releaseLine} line` : ''}
|
|
248
|
-
|
|
249
|
-
};
|
|
351
|
+
: `Install a stable ${requirement.definition.label} release${requirement.releaseLine ? ` in the supported ${requirement.releaseLine} line` : ''}.`
|
|
352
|
+
});
|
|
250
353
|
}
|
|
251
354
|
if (version &&
|
|
252
355
|
requirement.releaseLine &&
|
|
253
356
|
!matchesReleaseLine(version, requirement.releaseLine)) {
|
|
254
|
-
return {
|
|
255
|
-
|
|
256
|
-
state: 'outdated',
|
|
257
|
-
detail: `Found ${version}; the supported release line is ${requirement.releaseLine}.`,
|
|
258
|
-
detectedVersion: version,
|
|
259
|
-
detectedBy: command.executable,
|
|
357
|
+
return resultWithCause(requirement, identity, 'release-line-mismatch', `Found ${version}; the supported release line is ${requirement.releaseLine}.`, {
|
|
358
|
+
...detected,
|
|
260
359
|
remedy: requirement.exactVersion
|
|
261
360
|
? `Install ${requirement.definition.label} ${requirement.exactVersion}.`
|
|
262
|
-
: `Install ${requirement.definition.label} ${requirement.releaseLine}.x at or above ${requirement.minimumVersion}
|
|
263
|
-
|
|
264
|
-
};
|
|
361
|
+
: `Install ${requirement.definition.label} ${requirement.releaseLine}.x at or above ${requirement.minimumVersion}.`
|
|
362
|
+
});
|
|
265
363
|
}
|
|
266
|
-
if (
|
|
267
|
-
return {
|
|
268
|
-
requirement
|
|
269
|
-
|
|
270
|
-
detail: `Found ${version}; Liftoff tested this integration with exactly ${requirement.exactVersion}.`,
|
|
271
|
-
detectedVersion: version,
|
|
272
|
-
detectedBy: command.executable,
|
|
273
|
-
remedy: `Install ${requirement.definition.label} ${requirement.exactVersion}.`,
|
|
274
|
-
notices: []
|
|
275
|
-
};
|
|
364
|
+
if (requirement.exactVersion && compareVersionCores(version, requirement.exactVersion) !== 0) {
|
|
365
|
+
return resultWithCause(requirement, identity, 'exact-version-mismatch', `Found ${version}; Liftoff tested this integration with exactly ${requirement.exactVersion}.`, {
|
|
366
|
+
...detected, remedy: `Install ${requirement.definition.label} ${requirement.exactVersion}.`
|
|
367
|
+
});
|
|
276
368
|
}
|
|
277
|
-
if (
|
|
278
|
-
return {
|
|
279
|
-
requirement
|
|
280
|
-
|
|
281
|
-
detail: `Found ${version}; version ${requirement.minimumVersion} or newer is required.`,
|
|
282
|
-
detectedVersion: version,
|
|
283
|
-
detectedBy: command.executable,
|
|
284
|
-
remedy: `Upgrade ${requirement.definition.label} to ${requirement.minimumVersion} or newer.`,
|
|
285
|
-
notices: []
|
|
286
|
-
};
|
|
369
|
+
if (requirement.minimumVersion && compareVersionCores(version, requirement.minimumVersion) < 0) {
|
|
370
|
+
return resultWithCause(requirement, identity, 'below-minimum', `Found ${version}; version ${requirement.minimumVersion} or newer is required.`, {
|
|
371
|
+
...detected, remedy: `Upgrade ${requirement.definition.label} to ${requirement.minimumVersion} or newer.`
|
|
372
|
+
});
|
|
287
373
|
}
|
|
288
|
-
return {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
374
|
+
return resultWithCause(requirement, identity, 'compatible', `Version ${version}`, {
|
|
375
|
+
...detected,
|
|
376
|
+
notices: isPrereleaseVersion(version) ? [{
|
|
377
|
+
code: 'preview-channel',
|
|
378
|
+
label: `${requirement.definition.label} release channel`,
|
|
379
|
+
state: 'notice',
|
|
380
|
+
detail: `Compatible preview ${version}; a stable-channel downgrade is not required.`
|
|
381
|
+
}] : []
|
|
382
|
+
});
|
|
296
383
|
}
|
|
297
384
|
async function healthNotices(requirement, runner, options) {
|
|
298
385
|
let command;
|
|
@@ -317,44 +404,90 @@ async function healthNotices(requirement, runner, options) {
|
|
|
317
404
|
case 'github-copilot':
|
|
318
405
|
return [{
|
|
319
406
|
label: 'GitHub Copilot authentication',
|
|
407
|
+
code: 'authentication',
|
|
320
408
|
state: 'not-observable',
|
|
321
409
|
detail: 'Liftoff does not automate or persist Copilot credentials.',
|
|
322
410
|
remedy: 'Run `copilot` and sign in if prompted.'
|
|
323
411
|
}];
|
|
412
|
+
case 'codex':
|
|
413
|
+
return [{
|
|
414
|
+
label: 'OpenAI Codex authentication',
|
|
415
|
+
code: 'authentication',
|
|
416
|
+
state: 'not-observable',
|
|
417
|
+
detail: 'Authentication remains owned by Codex; Liftoff does not collect or persist credentials.',
|
|
418
|
+
remedy: 'Run `codex` and sign in if prompted.'
|
|
419
|
+
}];
|
|
420
|
+
case 'github-cli':
|
|
421
|
+
command = { executable: 'gh', args: ['auth', 'status'] };
|
|
422
|
+
label = 'GitHub authentication';
|
|
423
|
+
remedy = 'Run `gh auth login` with the required account and scope.';
|
|
424
|
+
break;
|
|
324
425
|
default:
|
|
325
426
|
return [];
|
|
326
427
|
}
|
|
327
|
-
const result = await runner
|
|
328
|
-
|
|
329
|
-
|
|
428
|
+
const result = await runToolCommand(runner, command, {
|
|
429
|
+
timeoutMs: 20_000, maxOutputBytes: 16_384, cwd: options.cwd, env: options.env
|
|
430
|
+
});
|
|
431
|
+
const code = requirement.id === 'azure-cli' || requirement.id === 'github-cli' ? 'authentication' : 'health';
|
|
432
|
+
if (result.status === 0 && !result.timedOut && !result.outputLimitExceeded && !result.aborted && !result.errorCode && !result.signal) {
|
|
433
|
+
return [{ label, code, state: 'ready', detail: 'Health probe succeeded.' }];
|
|
330
434
|
}
|
|
331
435
|
return [{
|
|
332
436
|
label,
|
|
437
|
+
code,
|
|
333
438
|
state: 'unhealthy',
|
|
334
|
-
detail: result.timedOut ? 'Health probe timed out.' : result.stderr
|
|
439
|
+
detail: result.timedOut ? 'Health probe timed out.' : safeDetail(result.stderr) || 'Health probe failed.',
|
|
335
440
|
remedy
|
|
336
441
|
}];
|
|
337
442
|
}
|
|
338
443
|
export async function probeRequirement(requirement, runner, options = {}) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
:
|
|
444
|
+
requirement = registeredRequirement(requirement);
|
|
445
|
+
const unsupported = unsupportedConstraint(requirement);
|
|
446
|
+
if (unsupported) {
|
|
447
|
+
return resultWithCause(requirement, {
|
|
448
|
+
executable: requirement.definition.probes[0]?.executable ?? requirement.id,
|
|
449
|
+
resolution: 'not-observable', origin: 'unknown', evidence: 'unavailable'
|
|
450
|
+
}, 'unsupported-constraint', unsupported, {
|
|
451
|
+
remedy: 'Use the registered tested tool constraints; no installation or PATH change can repair an unsupported requirement.'
|
|
452
|
+
});
|
|
344
453
|
}
|
|
345
|
-
const
|
|
346
|
-
const ready =
|
|
454
|
+
const candidates = await probeCommandCandidates(requirement, runner, options);
|
|
455
|
+
const ready = candidates.find((result) => result.state === 'ready');
|
|
347
456
|
if (ready) {
|
|
348
|
-
|
|
457
|
+
if (options.includeHealthNotices !== false) {
|
|
458
|
+
ready.notices.push(...await healthNotices(requirement, runner, options));
|
|
459
|
+
}
|
|
460
|
+
const update = options.availableUpdates?.[requirement.id];
|
|
461
|
+
const updateVersion = update ? extractVersion(update.version) : undefined;
|
|
462
|
+
if (update && updateVersion && ready.detectedVersion &&
|
|
463
|
+
compareVersions(updateVersion, ready.detectedVersion) > 0) {
|
|
464
|
+
ready.notices.push({
|
|
465
|
+
code: 'update-available',
|
|
466
|
+
label: `${requirement.definition.label} update`,
|
|
467
|
+
state: 'notice',
|
|
468
|
+
detail: `Version ${updateVersion} is available (${safeDetail(update.source)}); installed ${ready.detectedVersion} remains compatible.`
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
ready.observations = candidates.flatMap((candidate) => candidate.observations);
|
|
349
472
|
return ready;
|
|
350
473
|
}
|
|
474
|
+
let fallback;
|
|
351
475
|
if (requirement.id === 'github-copilot') {
|
|
352
|
-
|
|
353
|
-
if (fallback.state === 'ready') {
|
|
476
|
+
fallback = await copilotFallback(requirement, runner, options);
|
|
477
|
+
if (fallback.state === 'ready' || candidates.every((candidate) => candidate.reasonCode === 'missing-executable')) {
|
|
478
|
+
fallback.observations = [...candidates.flatMap((candidate) => candidate.observations), ...fallback.observations];
|
|
354
479
|
return fallback;
|
|
355
480
|
}
|
|
356
481
|
}
|
|
357
|
-
|
|
482
|
+
const selected = candidates.find((candidate) => candidate.state === 'outdated') ??
|
|
483
|
+
candidates.find((candidate) => candidate.reasonCode !== 'missing-executable') ?? candidates[0];
|
|
484
|
+
if (!selected) {
|
|
485
|
+
return resultWithCause(requirement, {
|
|
486
|
+
executable: requirement.id, resolution: 'not-observable', origin: 'unknown', evidence: 'unavailable'
|
|
487
|
+
}, 'observation-unavailable', 'No registered version probe is available.');
|
|
488
|
+
}
|
|
489
|
+
selected.observations = [...candidates.flatMap((candidate) => candidate.observations), ...(fallback?.observations ?? [])];
|
|
490
|
+
return selected;
|
|
358
491
|
}
|
|
359
492
|
export async function probeWorkstation(requirements, runner, options = {}) {
|
|
360
493
|
return Promise.all(requirements.map((requirement) => probeRequirement(requirement, runner, options)));
|
|
@@ -389,22 +522,61 @@ export async function detectHostEnvironment(platform = process.platform, osRelea
|
|
|
389
522
|
}
|
|
390
523
|
return { platform: 'linux', linuxFamily: parseLinuxFamily(osRelease) };
|
|
391
524
|
}
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
525
|
+
export function selectRemediation(requirement, probe, host, requestedRecipeId) {
|
|
526
|
+
requirement = registeredRequirement(requirement);
|
|
527
|
+
const definition = workstationRequirementCatalog[requirement.id];
|
|
528
|
+
const matchingRequirement = probe.requirement.id === requirement.id &&
|
|
529
|
+
JSON.stringify(requiredConstraint(probe.requirement)) === JSON.stringify(requiredConstraint(requirement)) &&
|
|
530
|
+
!unsupportedConstraint(requirement);
|
|
531
|
+
if (matchingRequirement && probe.state === 'ready' && probe.reasonCode === 'compatible') {
|
|
532
|
+
return { state: 'not-needed', reasonCode: 'compatible', detail: `${requirement.definition.label} is already compatible.` };
|
|
533
|
+
}
|
|
534
|
+
const recipes = matchingRequirement && requirement.exactVersion === definition.exactVersion
|
|
535
|
+
? definition.remedies ?? []
|
|
536
|
+
: [];
|
|
537
|
+
const recipe = recipes.find((candidate) => (!requestedRecipeId || candidate.id === requestedRecipeId) &&
|
|
538
|
+
candidate.platforms.includes(host.platform) &&
|
|
539
|
+
candidate.causes.includes(probe.reasonCode) &&
|
|
540
|
+
candidate.origins.includes(probe.identity?.origin ?? 'unknown') &&
|
|
541
|
+
(host.platform !== 'linux' || candidate.manager === 'npm' || candidate.manager === 'uv') &&
|
|
542
|
+
!(candidate.operation === 'upgrade' && requirement.exactVersion &&
|
|
543
|
+
(!probe.detectedVersion || compareVersions(probe.detectedVersion, requirement.exactVersion) >= 0)));
|
|
544
|
+
if (recipe) {
|
|
545
|
+
return {
|
|
546
|
+
state: 'available',
|
|
547
|
+
reasonCode: probe.reasonCode,
|
|
548
|
+
detail: `${recipe.operation} ${definition.label} using its registered ${recipe.manager} recipe.`,
|
|
549
|
+
recipe
|
|
550
|
+
};
|
|
396
551
|
}
|
|
397
|
-
return
|
|
552
|
+
return {
|
|
553
|
+
state: 'manual',
|
|
554
|
+
reasonCode: probe.reasonCode,
|
|
555
|
+
detail: `No safe automatic ${definition.label} remedy is registered for ${probe.reasonCode ?? 'an unknown cause'} ` +
|
|
556
|
+
`from ${probe.identity?.origin ?? 'an unobserved installation origin'} on ${host.platform}.`,
|
|
557
|
+
remedy: `${probe.remedy ? `${probe.remedy} ` : ''}${manualRemedy(requirement, host)} ` +
|
|
558
|
+
'Review the installation origin and required version/channel; Liftoff will not uninstall, downgrade, or repeat a missing-tool installer to guess a repair.'
|
|
559
|
+
};
|
|
398
560
|
}
|
|
399
561
|
async function managerAvailable(recipe, runner, options) {
|
|
400
|
-
|
|
401
|
-
|
|
562
|
+
if (recipe.manager === 'npm' || recipe.manager === 'uv') {
|
|
563
|
+
const definition = workstationRequirementCatalog[recipe.manager];
|
|
564
|
+
const probe = await probeRequirement({
|
|
565
|
+
id: definition.id, definition, severity: 'blocking', reasons: ['registered remedy prerequisite'],
|
|
566
|
+
minimumVersion: definition.minimumVersion, releaseLine: definition.releaseLine,
|
|
567
|
+
allowPrerelease: definition.allowPrerelease ?? false
|
|
568
|
+
}, runner, { ...options, includeHealthNotices: false });
|
|
569
|
+
return probe.state === 'ready';
|
|
570
|
+
}
|
|
571
|
+
const result = await runToolCommand(runner, { executable: recipe.command.executable, args: ['--version'] }, { timeoutMs: 10_000, maxOutputBytes: 16_384, cwd: options.cwd, env: options.env });
|
|
572
|
+
return !result.errorCode && result.status === 0 && !result.timedOut &&
|
|
573
|
+
!result.outputLimitExceeded && !result.aborted && !result.signal;
|
|
402
574
|
}
|
|
403
575
|
function pathRemedy(recipe, requirement) {
|
|
404
576
|
const executable = requirement.definition.probes[0]?.executable ?? requirement.id;
|
|
405
577
|
switch (recipe.manager) {
|
|
406
578
|
case 'brew':
|
|
407
|
-
return `Run \`brew --prefix\`, ensure its bin directory is on PATH, open a new terminal, then retry ${executable}.`;
|
|
579
|
+
return `Run \`brew --prefix${requirement.definition.packageIdentities?.brew?.includes('@') ? ` ${requirement.definition.packageIdentities.brew}` : ''}\`, ensure its bin directory is on PATH, open a new terminal, then retry ${executable}.`;
|
|
408
580
|
case 'winget':
|
|
409
581
|
return `Open a new terminal and retry ${executable}; if it is still missing, inspect the WinGet package installation and PATH aliases.`;
|
|
410
582
|
case 'npm':
|
|
@@ -414,10 +586,17 @@ function pathRemedy(recipe, requirement) {
|
|
|
414
586
|
}
|
|
415
587
|
}
|
|
416
588
|
async function documentedInstallLocations(recipe, requirement, context) {
|
|
589
|
+
const nativePath = hostPath(context.host.platform);
|
|
590
|
+
const observerContext = observationContext(requirement, context);
|
|
591
|
+
const observer = executableObserver(context.runner, context);
|
|
417
592
|
let binDirectories = [];
|
|
418
593
|
if (recipe.manager === 'winget') {
|
|
419
|
-
|
|
420
|
-
|
|
594
|
+
const localAppData = environmentValue(observerContext.env, 'LOCALAPPDATA', context.host.platform);
|
|
595
|
+
if (localAppData && !/[\u0000-\u001f\u007f]/.test(localAppData) && nativePath.isAbsolute(localAppData)) {
|
|
596
|
+
binDirectories = [
|
|
597
|
+
nativePath.join(localAppData, 'Microsoft', 'WindowsApps'),
|
|
598
|
+
nativePath.join(localAppData, 'Microsoft', 'WinGet', 'Links')
|
|
599
|
+
];
|
|
421
600
|
}
|
|
422
601
|
}
|
|
423
602
|
else {
|
|
@@ -425,116 +604,322 @@ async function documentedInstallLocations(recipe, requirement, context) {
|
|
|
425
604
|
? { executable: recipe.command.executable, args: ['prefix', '-g'] }
|
|
426
605
|
: recipe.manager === 'uv'
|
|
427
606
|
? { executable: recipe.command.executable, args: ['tool', 'dir', '--bin'] }
|
|
428
|
-
: {
|
|
429
|
-
|
|
607
|
+
: {
|
|
608
|
+
executable: recipe.command.executable,
|
|
609
|
+
args: [
|
|
610
|
+
'--prefix',
|
|
611
|
+
...(requirement.definition.packageIdentities?.brew?.includes('@') ? [requirement.definition.packageIdentities.brew] : [])
|
|
612
|
+
]
|
|
613
|
+
};
|
|
614
|
+
const location = await runToolCommand(context.runner, locationCommand, {
|
|
430
615
|
cwd: context.cwd,
|
|
431
|
-
|
|
616
|
+
env: context.env,
|
|
617
|
+
timeoutMs: 10_000,
|
|
618
|
+
maxOutputBytes: 16_384
|
|
432
619
|
});
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
620
|
+
const root = location.stdout.trim();
|
|
621
|
+
if (location.status === 0 && !location.timedOut && !location.outputLimitExceeded &&
|
|
622
|
+
!location.aborted && !location.errorCode && !location.signal &&
|
|
623
|
+
!/[\u0000-\u001f\u007f]/.test(root) && nativePath.isAbsolute(root)) {
|
|
624
|
+
binDirectories = recipe.manager === 'brew' || (recipe.manager === 'npm' && context.host.platform !== 'win32')
|
|
625
|
+
? [nativePath.join(root, 'bin')]
|
|
626
|
+
: [root];
|
|
440
627
|
}
|
|
441
628
|
}
|
|
442
|
-
const extensions = context.host.platform === 'win32' ? ['', '.
|
|
629
|
+
const extensions = context.host.platform === 'win32' ? ['.exe', '.cmd', '.bat', '.com'] : [''];
|
|
443
630
|
const candidates = new Set();
|
|
444
631
|
for (const directory of binDirectories) {
|
|
445
632
|
for (const probe of requirement.definition.probes) {
|
|
446
633
|
for (const extension of extensions) {
|
|
447
|
-
candidates.add(
|
|
634
|
+
candidates.add(nativePath.join(directory, `${probe.executable}${extension}`));
|
|
448
635
|
}
|
|
449
636
|
}
|
|
450
637
|
}
|
|
451
638
|
const found = [];
|
|
639
|
+
let complete = candidates.size > 0;
|
|
452
640
|
for (const candidate of candidates) {
|
|
453
641
|
try {
|
|
454
|
-
await
|
|
455
|
-
|
|
642
|
+
const identity = await observer.inspect(candidate, observerContext);
|
|
643
|
+
if (identity.resolution === 'resolved')
|
|
644
|
+
found.push(identity);
|
|
645
|
+
else if (identity.resolution === 'not-observable')
|
|
646
|
+
complete = false;
|
|
456
647
|
}
|
|
457
648
|
catch {
|
|
458
|
-
|
|
649
|
+
complete = false;
|
|
459
650
|
}
|
|
460
651
|
}
|
|
461
|
-
return found;
|
|
652
|
+
return { checkedLocations: [...candidates], found, complete };
|
|
462
653
|
}
|
|
463
654
|
function manualRemedy(requirement, host) {
|
|
464
655
|
return host.platform === 'linux'
|
|
465
656
|
? requirement.definition.linuxRemedies[host.linuxFamily]
|
|
466
657
|
: `Install ${requirement.definition.label} manually and retry.`;
|
|
467
658
|
}
|
|
659
|
+
const attemptsByRunner = new WeakMap();
|
|
660
|
+
function fingerprintIdentity(identity) {
|
|
661
|
+
return {
|
|
662
|
+
executable: identity.executable, resolution: identity.resolution,
|
|
663
|
+
resolvedPath: identity.resolvedPath ?? null, realPath: identity.realPath ?? null,
|
|
664
|
+
kind: identity.kind ?? null, origin: identity.origin, evidence: identity.evidence
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function observationFingerprint(probe) {
|
|
668
|
+
return canonicalSha256({
|
|
669
|
+
id: probe.requirement.id,
|
|
670
|
+
required: requiredConstraint(probe.requirement),
|
|
671
|
+
state: probe.state,
|
|
672
|
+
reasonCode: probe.reasonCode,
|
|
673
|
+
identity: fingerprintIdentity(probe.identity),
|
|
674
|
+
detectedVersion: probe.detectedVersion ?? null,
|
|
675
|
+
observations: probe.observations.map((observation) => ({
|
|
676
|
+
command: { executable: observation.command.executable, args: observation.command.args },
|
|
677
|
+
identity: fingerprintIdentity(observation.identity),
|
|
678
|
+
status: observation.status,
|
|
679
|
+
timedOut: observation.timedOut,
|
|
680
|
+
errorCode: observation.errorCode ?? null,
|
|
681
|
+
reasonCode: observation.reasonCode,
|
|
682
|
+
detectedVersion: observation.detectedVersion ?? null
|
|
683
|
+
}))
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function attemptFingerprint(probe, recipe, context) {
|
|
687
|
+
const observation = observationContext(probe.requirement, context);
|
|
688
|
+
const managerVariables = {
|
|
689
|
+
brew: ['HOMEBREW_PREFIX', 'HOMEBREW_CELLAR', 'HOMEBREW_CASKROOM'],
|
|
690
|
+
winget: [],
|
|
691
|
+
npm: ['npm_config_prefix', 'NPM_CONFIG_PREFIX', 'NPM_CONFIG_USERCONFIG', 'npm_config_userconfig'],
|
|
692
|
+
uv: ['UV_TOOL_DIR', 'UV_TOOL_BIN_DIR', 'UV_PYTHON_INSTALL_DIR']
|
|
693
|
+
};
|
|
694
|
+
const locationVariables = [
|
|
695
|
+
'PATH', 'PATHEXT', 'HOME', 'USERPROFILE', 'LOCALAPPDATA', 'APPDATA', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME',
|
|
696
|
+
...managerVariables[recipe.manager]
|
|
697
|
+
];
|
|
698
|
+
// Cwd, PWD, temporary-directory names, and credentials are not progress. Actual resolved launchers retain path identity.
|
|
699
|
+
return canonicalSha256({
|
|
700
|
+
schemaVersion: 1,
|
|
701
|
+
observation: observationFingerprint(probe),
|
|
702
|
+
recipeId: recipe.id,
|
|
703
|
+
command: { executable: recipe.command.executable, args: recipe.command.args },
|
|
704
|
+
host: { platform: context.host.platform, linuxFamily: context.host.linuxFamily },
|
|
705
|
+
environment: Object.fromEntries(locationVariables.map((name) => [name, environmentValue(observation.env, name, observation.platform) ?? null]))
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function storedNoProgressAttempt(attempt, recipeId, inputFingerprint) {
|
|
709
|
+
if (attempt.recipeId !== recipeId || attempt.inputFingerprint !== inputFingerprint ||
|
|
710
|
+
attempt.outputFingerprint !== inputFingerprint || attempt.outcome !== 'unchanged') {
|
|
711
|
+
throw new Error('Stored no-progress history does not match the current recipe and unchanged observation binding.');
|
|
712
|
+
}
|
|
713
|
+
return { recipeId, inputFingerprint, outputFingerprint: inputFingerprint, outcome: 'unchanged' };
|
|
714
|
+
}
|
|
715
|
+
function historyFailure(requirement, probe, recipe, operation, error, attempt, command) {
|
|
716
|
+
const detail = error instanceof Error ? safeDetail(error.message) : 'The private no-progress store is unavailable.';
|
|
717
|
+
return {
|
|
718
|
+
requirement, probe, recipe, state: 'failed', reasonCode: 'history-storage-failed', historyError: operation,
|
|
719
|
+
progress: operation === 'record' ? 'unchanged' : 'indeterminate',
|
|
720
|
+
...(attempt ? { attempt } : {}),
|
|
721
|
+
...(command ? { command } : {}),
|
|
722
|
+
detail: operation === 'lookup'
|
|
723
|
+
? `Workstation no-progress history could not be read. No remedy command was run. ${detail}`
|
|
724
|
+
: `The authorized remedy exited zero and the independent probe is unchanged, but its no-progress receipt could not be preserved. ${detail}`,
|
|
725
|
+
remedy: 'Resolve the private workstation remediation history error before retrying; history is not silently ignored or replaced.'
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
export function compareRequirementObservations(before, after) {
|
|
729
|
+
if (after.state === 'ready' && after.reasonCode === 'compatible')
|
|
730
|
+
return 'ready';
|
|
731
|
+
if (observationFingerprint(before) === observationFingerprint(after))
|
|
732
|
+
return 'unchanged';
|
|
733
|
+
const discovery = new Set(['missing-executable', 'observation-unavailable']);
|
|
734
|
+
if ((discovery.has(before.reasonCode) && !discovery.has(after.reasonCode)) ||
|
|
735
|
+
(before.reasonCode === 'version-unparseable' && Boolean(after.detectedVersion)) ||
|
|
736
|
+
(before.reasonCode === 'incompatible-channel' && after.reasonCode !== 'incompatible-channel' && Boolean(after.detectedVersion)) ||
|
|
737
|
+
(before.reasonCode === 'below-minimum' && after.reasonCode === 'below-minimum' &&
|
|
738
|
+
before.detectedVersion && after.detectedVersion && compareVersions(after.detectedVersion, before.detectedVersion) > 0)) {
|
|
739
|
+
return 'improved';
|
|
740
|
+
}
|
|
741
|
+
return 'changed';
|
|
742
|
+
}
|
|
468
743
|
export async function installRequirement(requirement, currentProbe, context) {
|
|
744
|
+
requirement = registeredRequirement(requirement);
|
|
745
|
+
const selection = selectRemediation(requirement, currentProbe, context.host, context.approvedRemediationId);
|
|
746
|
+
if (selection.state === 'not-needed') {
|
|
747
|
+
return { requirement, state: 'not-needed', reasonCode: 'verified', detail: selection.detail, probe: currentProbe, progress: 'ready' };
|
|
748
|
+
}
|
|
469
749
|
if (!context.authorized) {
|
|
470
750
|
return {
|
|
471
751
|
requirement,
|
|
472
752
|
state: 'declined',
|
|
473
|
-
|
|
753
|
+
reasonCode: 'not-authorized',
|
|
754
|
+
detail: 'Tool remediation was not authorized.',
|
|
474
755
|
probe: currentProbe,
|
|
475
756
|
remedy: currentProbe.remedy
|
|
476
757
|
};
|
|
477
758
|
}
|
|
478
|
-
const recipe =
|
|
759
|
+
const recipe = selection.recipe;
|
|
479
760
|
if (!recipe) {
|
|
480
761
|
return {
|
|
481
762
|
requirement,
|
|
482
763
|
state: 'manual',
|
|
483
|
-
|
|
764
|
+
reasonCode: 'recipe-unavailable',
|
|
765
|
+
detail: selection.detail,
|
|
484
766
|
probe: currentProbe,
|
|
485
|
-
remedy:
|
|
767
|
+
remedy: selection.remedy
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
if (recipe.requiresExplicitReview && context.approvedRemediationId !== recipe.id) {
|
|
771
|
+
return {
|
|
772
|
+
requirement, state: 'manual', reasonCode: 'review-required', recipe, probe: currentProbe,
|
|
773
|
+
detail: `The ${recipe.operation} recipe requires separate review because it can replace the installed version or channel.`,
|
|
774
|
+
remedy: `Review ${formatCommand(recipe.command)} and explicitly approve remedy ${recipe.id}; generic tool-install consent does not authorize a downgrade or channel change.`
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const inputFingerprint = attemptFingerprint(currentProbe, recipe, context);
|
|
778
|
+
let persisted;
|
|
779
|
+
if (context.noProgressStore) {
|
|
780
|
+
try {
|
|
781
|
+
const stored = await context.noProgressStore.find(recipe.id, inputFingerprint);
|
|
782
|
+
if (stored)
|
|
783
|
+
persisted = storedNoProgressAttempt(stored, recipe.id, inputFingerprint);
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
return historyFailure(requirement, currentProbe, recipe, 'lookup', error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const history = [
|
|
790
|
+
...(context.previousAttempts ?? []),
|
|
791
|
+
...(currentProbe.remediationAttempts ?? []),
|
|
792
|
+
...(attemptsByRunner.get(context.runner) ?? []),
|
|
793
|
+
...(persisted ? [persisted] : [])
|
|
794
|
+
];
|
|
795
|
+
if (history.some((attempt) => attempt.recipeId === recipe.id &&
|
|
796
|
+
attempt.inputFingerprint === inputFingerprint && attempt.outcome === 'unchanged')) {
|
|
797
|
+
return {
|
|
798
|
+
requirement, state: 'unchanged', reasonCode: 'no-progress', progress: 'unchanged', recipe, probe: currentProbe,
|
|
799
|
+
...(persisted ? { attempt: persisted } : {}),
|
|
800
|
+
detail: `The same ${recipe.id} remedy already made no progress for this executable and constraint; it was not run again.`,
|
|
801
|
+
remedy: 'Reinspect after the installation or environment changes, or review a different registered remedy. Do not repeat the unchanged command.'
|
|
486
802
|
};
|
|
487
803
|
}
|
|
488
|
-
if (!await managerAvailable(recipe, context.runner,
|
|
804
|
+
if (!await managerAvailable(recipe, context.runner, context)) {
|
|
489
805
|
return {
|
|
490
806
|
requirement,
|
|
491
807
|
state: 'manual',
|
|
492
|
-
|
|
808
|
+
reasonCode: 'manager-unavailable',
|
|
809
|
+
recipe,
|
|
810
|
+
detail: `${recipe.manager} is unavailable, incompatible, or unhealthy; Liftoff does not bootstrap package managers.`,
|
|
493
811
|
probe: currentProbe,
|
|
494
|
-
remedy: `
|
|
812
|
+
remedy: `Prepare a compatible ${recipe.manager} through its official instructions, then retry.`
|
|
495
813
|
};
|
|
496
814
|
}
|
|
497
|
-
const result = await context.runner
|
|
815
|
+
const result = await runToolCommand(context.runner, recipe.command, {
|
|
498
816
|
cwd: context.cwd,
|
|
817
|
+
env: context.env,
|
|
499
818
|
timeoutMs: 10 * 60_000,
|
|
819
|
+
maxOutputBytes: 1_048_576,
|
|
500
820
|
stream: true,
|
|
501
821
|
...context.streamOptions
|
|
502
822
|
});
|
|
503
|
-
if (result.status !== 0 || result.timedOut) {
|
|
823
|
+
if (result.status !== 0 || result.timedOut || result.outputLimitExceeded || result.aborted || result.errorCode || result.signal) {
|
|
824
|
+
const attempt = { recipeId: recipe.id, inputFingerprint, outcome: 'failed' };
|
|
504
825
|
return {
|
|
505
826
|
requirement,
|
|
506
827
|
state: 'failed',
|
|
828
|
+
reasonCode: 'execution-failed',
|
|
829
|
+
recipe,
|
|
830
|
+
attempt,
|
|
831
|
+
progress: 'indeterminate',
|
|
507
832
|
detail: result.timedOut
|
|
508
833
|
? `${requirement.definition.label} installation timed out.`
|
|
509
|
-
: result.stderr.
|
|
834
|
+
: safeDetail(result.stderr || result.errorMessage || '') || `Remedy exited with status ${result.status}.`,
|
|
510
835
|
command: result.displayCommand,
|
|
511
836
|
probe: currentProbe,
|
|
512
|
-
remedy: manualRemedy(requirement, context.host)
|
|
837
|
+
remedy: `The ${recipe.operation} failed; no successful installation or rollback is inferred. ${manualRemedy(requirement, context.host)}`
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const nextProbe = await probeRequirement(requirement, context.runner, context);
|
|
841
|
+
const observedProgress = compareRequirementObservations(currentProbe, nextProbe);
|
|
842
|
+
const outputFingerprint = attemptFingerprint(nextProbe, recipe, context);
|
|
843
|
+
const progress = observedProgress === 'unchanged' && inputFingerprint !== outputFingerprint ? 'changed' : observedProgress;
|
|
844
|
+
const attempt = {
|
|
845
|
+
recipeId: recipe.id,
|
|
846
|
+
inputFingerprint,
|
|
847
|
+
outputFingerprint,
|
|
848
|
+
outcome: progress
|
|
849
|
+
};
|
|
850
|
+
const ownHistory = [...(currentProbe.remediationAttempts ?? []), attempt];
|
|
851
|
+
nextProbe.remediationAttempts = ownHistory;
|
|
852
|
+
attemptsByRunner.set(context.runner, [...(attemptsByRunner.get(context.runner) ?? []), attempt].slice(-100));
|
|
853
|
+
if (context.noProgressStore && progress === 'unchanged') {
|
|
854
|
+
try {
|
|
855
|
+
await context.noProgressStore.record({ ...attempt, outcome: 'unchanged', outputFingerprint });
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
return historyFailure(requirement, nextProbe, recipe, 'record', error, attempt, result.displayCommand);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (nextProbe.state === 'ready' && nextProbe.reasonCode === 'compatible') {
|
|
862
|
+
return {
|
|
863
|
+
requirement, state: 'installed', reasonCode: 'verified', recipe, progress, attempt,
|
|
864
|
+
detail: `${requirement.definition.label} is ready after verification; installer file changes were not observed.`,
|
|
865
|
+
command: result.displayCommand, probe: nextProbe
|
|
513
866
|
};
|
|
514
867
|
}
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
868
|
+
const discoveryFailed = nextProbe.reasonCode === 'missing-executable' ||
|
|
869
|
+
(nextProbe.reasonCode === 'observation-unavailable' &&
|
|
870
|
+
nextProbe.observations.some((observation) => observation.command.executable === requirement.definition.probes[0]?.executable &&
|
|
871
|
+
observation.reasonCode === 'missing-executable'));
|
|
872
|
+
if (discoveryFailed) {
|
|
873
|
+
const discovery = await documentedInstallLocations(recipe, requirement, context);
|
|
874
|
+
if (discovery.complete && discovery.found.length > 0) {
|
|
875
|
+
return {
|
|
876
|
+
requirement, state: 'restart-required', reasonCode: 'executable-discovery', recipe, progress, attempt, discovery,
|
|
877
|
+
detail: `The remedy exited zero, but ${nextProbe.detail} Existing executable candidates were observed at ${discovery.found.map((item) => safeDetail(item.resolvedPath ?? item.executable)).join(', ')}; this does not prove the installer wrote them.`,
|
|
878
|
+
command: result.displayCommand, probe: nextProbe, remedy: pathRemedy(recipe, requirement)
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const discoveryDetail = discovery.complete
|
|
882
|
+
? 'No executable candidate was observed in the documented install locations.'
|
|
883
|
+
: 'Documented install locations could not be fully observed.';
|
|
518
884
|
return {
|
|
519
|
-
requirement,
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
command: result.displayCommand,
|
|
525
|
-
probe: nextProbe,
|
|
526
|
-
remedy: pathRemedy(recipe, requirement)
|
|
885
|
+
requirement, state: progress === 'unchanged' ? 'unchanged' : 'unresolved',
|
|
886
|
+
reasonCode: progress === 'unchanged' ? 'no-progress' : 'verification-unresolved',
|
|
887
|
+
recipe, progress, attempt, discovery, command: result.displayCommand, probe: nextProbe,
|
|
888
|
+
detail: `The remedy exited zero, but ${nextProbe.detail} ${discoveryDetail} No file changes were verified.`,
|
|
889
|
+
remedy: 'Inspect the installation and its documented executable location before selecting another remedy.'
|
|
527
890
|
};
|
|
528
891
|
}
|
|
529
892
|
return {
|
|
530
|
-
requirement,
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
893
|
+
requirement, state: progress === 'unchanged' ? 'unchanged' : 'unresolved',
|
|
894
|
+
reasonCode: progress === 'unchanged' ? 'no-progress' : 'verification-unresolved',
|
|
895
|
+
recipe, progress, attempt, command: result.displayCommand, probe: nextProbe,
|
|
896
|
+
detail: `The remedy exited zero${progress === 'unchanged' ? ' with no progress' : ''}; ${nextProbe.detail} No installer file changes were verified.`,
|
|
897
|
+
remedy: `${nextProbe.remedy ?? 'Inspect the failed version or health probe.'} ` +
|
|
898
|
+
'Review the installation origin and supported version/channel operation instead of repeating the same installer.'
|
|
535
899
|
};
|
|
536
900
|
}
|
|
537
901
|
export function blockingReadinessFailures(results) {
|
|
538
|
-
return results.filter((result) => result.requirement.severity === 'blocking' && result.state !== 'ready');
|
|
902
|
+
return results.filter((result) => result.requirement.severity === 'blocking' && (result.state !== 'ready' || result.reasonCode !== 'compatible'));
|
|
903
|
+
}
|
|
904
|
+
/** Workstation prerequisites only; provider permissions, private access, and protected state remain separate gates. */
|
|
905
|
+
export function workstationScopeReadiness(results, scope) {
|
|
906
|
+
const toolFailures = blockingReadinessFailures(results);
|
|
907
|
+
const authenticationFailures = [];
|
|
908
|
+
if (scope === 'activation' || scope === 'migration' || scope === 'lifecycle') {
|
|
909
|
+
for (const result of results) {
|
|
910
|
+
if (result.requirement.severity !== 'blocking' || result.state !== 'ready' ||
|
|
911
|
+
(result.requirement.id !== 'azure-cli' && result.requirement.id !== 'github-cli'))
|
|
912
|
+
continue;
|
|
913
|
+
const notice = result.notices.find((item) => item.code === 'authentication') ?? {
|
|
914
|
+
code: 'authentication',
|
|
915
|
+
label: `${result.requirement.definition.label} authentication`,
|
|
916
|
+
state: 'not-observable',
|
|
917
|
+
detail: 'The required authentication capability has not been observed.'
|
|
918
|
+
};
|
|
919
|
+
if (notice.state !== 'ready')
|
|
920
|
+
authenticationFailures.push({ requirementId: result.requirement.id, notice });
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return { scope, ready: toolFailures.length === 0 && authenticationFailures.length === 0, toolFailures, authenticationFailures };
|
|
539
924
|
}
|
|
540
925
|
//# sourceMappingURL=workstation.js.map
|