@b00dea/cockpit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/README.md +3 -0
- package/cockpit.schema.json +73 -3
- package/dist/autonomy.d.ts +3 -2
- package/dist/autonomy.js +30 -13
- package/dist/browser/commands.d.ts +57 -2
- package/dist/browser/commands.js +114 -10
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +4 -1
- package/dist/browser/surfaces.d.ts +7 -7
- package/dist/browser/surfaces.js +5 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-main.d.ts +40 -3
- package/dist/cli-main.js +241 -62
- package/dist/command-table.d.ts +19 -11
- package/dist/command-table.js +190 -38
- package/dist/config/load.d.ts +16 -4
- package/dist/config/load.js +25 -3
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/diagnostics.js +10 -1
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.js +6 -2
- package/dist/environment/application/doctor.d.ts +12 -7
- package/dist/environment/application/doctor.js +20 -8
- package/dist/environment/application/guard.d.ts +12 -6
- package/dist/environment/application/guard.js +87 -32
- package/dist/environment/application/health.d.ts +14 -5
- package/dist/environment/application/health.js +8 -2
- package/dist/environment/application/lifecycle-context.d.ts +10 -3
- package/dist/environment/application/lifecycle-state.js +4 -3
- package/dist/environment/application/lifecycle.d.ts +2 -1
- package/dist/environment/application/lifecycle.js +11 -3
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +10 -10
- package/dist/environment/claims.js +3 -11
- package/dist/environment/compose-topology.d.ts +15 -7
- package/dist/environment/compose-topology.js +100 -23
- package/dist/environment/contract.d.ts +42 -9
- package/dist/environment/contract.js +33 -0
- package/dist/environment/executable.d.ts +12 -0
- package/dist/environment/executable.js +46 -23
- package/dist/environment/generated-application/check.js +2 -0
- package/dist/environment/generated-application/prepare.js +2 -0
- package/dist/environment/generated-application/shared.d.ts +10 -2
- package/dist/environment/generated-application/shared.js +6 -1
- package/dist/environment/generated-application/surface-outputs.js +23 -12
- package/dist/environment/generated-application/surfaces.js +16 -11
- package/dist/environment/generated-application/sync.js +19 -4
- package/dist/environment/git-observation.d.ts +25 -6
- package/dist/environment/git-observation.js +37 -28
- package/dist/environment/health-composition.d.ts +3 -2
- package/dist/environment/health-composition.js +19 -2
- package/dist/environment/init-application.d.ts +39 -14
- package/dist/environment/init-application.js +39 -10
- package/dist/environment/lifecycle-composition.d.ts +4 -2
- package/dist/environment/lifecycle-composition.js +84 -17
- package/dist/environment/lifecycle-stage.d.ts +8 -3
- package/dist/environment/lifecycle-stage.js +11 -1
- package/dist/environment/logs.d.ts +1 -1
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +1 -1
- package/dist/environment/observations.js +2 -0
- package/dist/environment/operator-application.d.ts +35 -6
- package/dist/environment/operator-application.js +42 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +8 -3
- package/dist/environment/read-composition.js +53 -8
- package/dist/environment/readiness-composition.d.ts +20 -1
- package/dist/environment/readiness-composition.js +23 -2
- package/dist/environment/reclaim.d.ts +56 -19
- package/dist/environment/reclaim.js +147 -42
- package/dist/environment/runtime.d.ts +64 -18
- package/dist/environment/runtime.js +73 -3
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +48 -9
- package/dist/environment/state.js +142 -2
- package/dist/environment/tool-registration.js +3 -7
- package/dist/environment-exec/execute.d.ts +6 -3
- package/dist/environment-exec/execute.js +4 -0
- package/dist/fix-catalogue.d.ts +1 -1
- package/dist/generated-environment/adopt.js +4 -8
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +20 -3
- package/dist/guards/evaluator.js +39 -7
- package/dist/guards/local-identity.d.ts +6 -2
- package/dist/guards/local-identity.js +10 -4
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +1 -1
- package/dist/guards/policy-pin.js +14 -12
- package/dist/init/command.d.ts +4 -3
- package/dist/init/command.js +36 -16
- package/dist/init/derive.d.ts +8 -6
- package/dist/init/derive.js +140 -36
- package/dist/init/draft.d.ts +13 -11
- package/dist/init/draft.js +54 -31
- package/dist/init/render.d.ts +18 -6
- package/dist/init/render.js +31 -9
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema.d.ts +42 -75
- package/dist/schema.js +62 -11
- package/dist/surfaces/harness-registry.d.ts +80 -0
- package/dist/surfaces/harness-registry.js +29 -0
- package/dist/surfaces/mcp.d.ts +67 -17
- package/dist/surfaces/mcp.js +194 -31
- package/dist/surfaces/projection.d.ts +7 -7
- package/dist/surfaces/projection.js +41 -6
- package/dist/surfaces/render.d.ts +5 -44
- package/dist/surfaces/render.js +5 -2
- package/dist/team-metadata.js +2 -1
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +4 -97
- package/dist/work-orders/application.js +94 -254
- package/dist/work-orders/composition.d.ts +35 -58
- package/dist/work-orders/composition.js +216 -24
- package/dist/work-orders/contract.d.ts +22 -6
- package/dist/work-orders/contract.js +34 -16
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +54 -5
- package/dist/work-orders/store.js +118 -24
- package/docs/README.md +6 -3
- package/docs/capability-matrix.md +17 -9
- package/docs/cli-reference.md +209 -1
- package/docs/command-list.md +12 -1
- package/docs/concepts.md +6 -4
- package/docs/configuration-essentials.md +7 -2
- package/docs/configuration-reference.md +18 -3
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +194 -6
- package/docs/faq.md +5 -3
- package/docs/getting-started.md +109 -25
- package/docs/harness-surfaces.md +69 -2
- package/docs/safety-and-autonomy.md +56 -12
- package/docs/troubleshooting.md +29 -8
- package/docs/tutorial.md +3 -2
- package/docs/work-and-browser.md +180 -14
- package/libexec/browser-operation.cjs +86 -2
- package/package.json +2 -2
- package/surfaces/skills/cockpit-browser.skill.md +7 -2
- package/surfaces/skills/cockpit-explain.skill.md +1 -1
- package/surfaces/skills/cockpit-fix.skill.md +1 -1
- package/surfaces/skills/cockpit-setup.skill.md +3 -3
- package/surfaces/skills/cockpit-work.skill.md +1 -1
- package/surfaces/skills/procedures/explain.md +9 -4
- package/surfaces/skills/procedures/fix.md +25 -11
- package/surfaces/skills/procedures/setup.md +113 -40
- package/surfaces/skills/procedures/work.md +45 -16
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// One-service verbs on this checkout's own running stack: `ps`, `restart`, `rebuild` and `exec`. Each
|
|
2
|
+
// reaches the stack through the topology its runtime readiness authenticated, so the agent never names
|
|
3
|
+
// the hashed Compose project, its files or its profiles, and every step is one the runtime owner plans.
|
|
4
|
+
import { constants as osConstants } from 'node:os';
|
|
5
|
+
import { cockpitCommand } from '../../fix-catalogue.js';
|
|
6
|
+
import { ENVIRONMENT_CODES, errorCause, forwardedRefusal } from '../contract.js';
|
|
7
|
+
import { DeclarationRefusalError, serviceNotDeclared } from '../declaration.js';
|
|
8
|
+
import { admitRuntimeServiceHealth, observeRuntimeServiceStates, planRuntimeServiceExec, restartRuntimeService, startRuntimeService, } from '../runtime.js';
|
|
9
|
+
import { collectFailureLogs } from './lifecycle.js';
|
|
10
|
+
import { lifecycleRefusal } from './shared.js';
|
|
11
|
+
async function observeStack(verb, configPath, adapters, service) {
|
|
12
|
+
let observed;
|
|
13
|
+
try {
|
|
14
|
+
observed = await adapters.observe({ configPath });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof DeclarationRefusalError)
|
|
18
|
+
return forwardedRefusal([error.diagnostic]);
|
|
19
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, `${verb} could not reach this checkout's running stack: ${errorCause(error)}`, verb, `start the stack with ${cockpitCommand('up', { config: configPath })}, then rerun ${verb}`);
|
|
20
|
+
}
|
|
21
|
+
const declared = observed.topology.services.map(({ id }) => id);
|
|
22
|
+
if (service !== undefined && !declared.includes(service)) {
|
|
23
|
+
return forwardedRefusal([serviceNotDeclared(service, declared)]);
|
|
24
|
+
}
|
|
25
|
+
return observed;
|
|
26
|
+
}
|
|
27
|
+
export async function runServicePs(input, adapters) {
|
|
28
|
+
const observed = await observeStack('ps', input.configPath, adapters);
|
|
29
|
+
if ('kind' in observed)
|
|
30
|
+
return observed;
|
|
31
|
+
const states = await observeRuntimeServiceStates({ topology: observed.topology, execute: adapters.execute });
|
|
32
|
+
if (states.kind !== 'complete')
|
|
33
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, `the stack's service states are unavailable: ${states.reason}`, 'ps');
|
|
34
|
+
return { kind: 'complete', status: 0, diagnostics: [], result: { services: states.services } };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `restart` restarts the container in place; `rebuild` builds the service's image and replaces its
|
|
38
|
+
* container, for a change baked into the image. Both then wait for the service as `up` does and
|
|
39
|
+
* report its logs when it does not become healthy.
|
|
40
|
+
*/
|
|
41
|
+
export async function runServiceRestart(input, adapters) {
|
|
42
|
+
const observed = await observeStack(input.verb, input.configPath, adapters, input.service);
|
|
43
|
+
if ('kind' in observed)
|
|
44
|
+
return observed;
|
|
45
|
+
const run = { topology: observed.topology, service: input.service, execute: adapters.execute };
|
|
46
|
+
const outcome = input.verb === 'rebuild' ? await startRuntimeService({ ...run, rebuild: true }) : await restartRuntimeService(run);
|
|
47
|
+
const evidence = outcome.kind === 'observed' ? admitRuntimeServiceHealth(outcome.observation) : undefined;
|
|
48
|
+
if (!evidence) {
|
|
49
|
+
const logs = await collectFailureLogs(outcome.kind === 'observed' ? outcome.observation
|
|
50
|
+
: outcome.kind === 'failure' ? outcome.logs : undefined, input.service, adapters);
|
|
51
|
+
return forwardedRefusal([
|
|
52
|
+
{ severity: 'error', code: ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, target: input.service,
|
|
53
|
+
message: `service ${input.service} did not become healthy after ${input.verb}`,
|
|
54
|
+
fix: `fix the error in the log excerpt, then rerun ${input.verb}` },
|
|
55
|
+
...logs
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
const recorded = observed.completedServices.find(({ service }) => service === input.service);
|
|
59
|
+
if (recorded && recorded.containerId !== evidence.containerId) {
|
|
60
|
+
const refused = await observed.recordContainer(input.service, evidence.containerId);
|
|
61
|
+
if (refused.length > 0)
|
|
62
|
+
return forwardedRefusal(refused);
|
|
63
|
+
}
|
|
64
|
+
return { kind: 'complete', status: 0, diagnostics: [],
|
|
65
|
+
result: { service: input.service, containerId: evidence.containerId, change: input.verb === 'rebuild' ? 'rebuilt' : 'restarted' } };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Runs a command inside one service's container in the foreground. A command that exits nonzero
|
|
69
|
+
* is the command's answer, not a cockpit failure, so its status passes through as `session` does.
|
|
70
|
+
*/
|
|
71
|
+
export async function runServiceExec(input, adapters) {
|
|
72
|
+
const observed = await observeStack('exec', input.configPath, adapters, input.service);
|
|
73
|
+
if ('kind' in observed)
|
|
74
|
+
return observed;
|
|
75
|
+
const planned = planRuntimeServiceExec({ topology: observed.topology, service: input.service, argv: input.argv, tty: input.tty });
|
|
76
|
+
if (!planned)
|
|
77
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, `the command for service ${input.service} could not be planned from the stack's topology`, input.service);
|
|
78
|
+
const child = await adapters.executeForeground(planned);
|
|
79
|
+
if (child.kind === 'refusal')
|
|
80
|
+
return { kind: 'refusal', status: child.code === ENVIRONMENT_CODES.EXECUTION_ABORTED ? 130 : 2, result: null,
|
|
81
|
+
diagnostics: [{ severity: 'error', code: child.code, target: input.service,
|
|
82
|
+
message: `the command in service ${input.service} could not be run to completion` }] };
|
|
83
|
+
const signalNumber = child.signal ? osConstants.signals[child.signal] : undefined;
|
|
84
|
+
const status = signalNumber !== undefined ? 128 + signalNumber : child.exitCode;
|
|
85
|
+
return { kind: 'complete', status, diagnostics: status === 0 ? [] : [{
|
|
86
|
+
severity: 'error', code: ENVIRONMENT_CODES.EXECUTION_STEP_FAILED, target: input.service,
|
|
87
|
+
message: `the command in service ${input.service} ${child.signal ? `was terminated by ${child.signal}` : `exited with status ${child.exitCode}`}`,
|
|
88
|
+
}], result: { exitCode: child.exitCode, signal: child.signal } };
|
|
89
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ExecuteProbeOutcome } from '../../environment-exec/execute.js';
|
|
2
|
+
import { type Diagnostic, type EnvironmentDiagnostic, type ExecutionStep } from '../contract.js';
|
|
3
|
+
import type { LoadedCockpitConfigResult } from './health.js';
|
|
4
|
+
type Execute = (step: ExecutionStep) => Promise<ExecuteProbeOutcome>;
|
|
5
|
+
interface SiblingCommandOutcome {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
8
|
+
readonly result: unknown;
|
|
9
|
+
readonly artifact?: string | null;
|
|
10
|
+
readonly destination?: null;
|
|
11
|
+
}
|
|
12
|
+
export interface UseCommandInput {
|
|
13
|
+
readonly configPath: string;
|
|
14
|
+
readonly slug: string;
|
|
15
|
+
/** Absolute; absent with `clear`. */
|
|
16
|
+
readonly checkout?: string;
|
|
17
|
+
readonly clear: boolean;
|
|
18
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
19
|
+
}
|
|
20
|
+
export declare function runUse(input: UseCommandInput, dependencies: Readonly<{
|
|
21
|
+
execute: Execute;
|
|
22
|
+
}>): Promise<SiblingCommandOutcome>;
|
|
23
|
+
export interface PullCommandInput {
|
|
24
|
+
readonly configPath: string;
|
|
25
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
26
|
+
}
|
|
27
|
+
export declare function runPull(input: PullCommandInput, dependencies: Readonly<{
|
|
28
|
+
execute: Execute;
|
|
29
|
+
}>): Promise<SiblingCommandOutcome>;
|
|
30
|
+
/**
|
|
31
|
+
* What doctor and preflight report about the served checkouts: each override and where it came
|
|
32
|
+
* from, an override that no longer names a checkout of its sibling, the environment files a worktree
|
|
33
|
+
* lacks, and a running stack whose siblings' dependencies or migrations moved since it came up.
|
|
34
|
+
*/
|
|
35
|
+
export declare function inspectServedSiblings(input: Readonly<{
|
|
36
|
+
configPath: string;
|
|
37
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
38
|
+
/** The configuration the command already loaded; read again from `configPath` when absent. */
|
|
39
|
+
loaded?: LoadedCockpitConfigResult;
|
|
40
|
+
}>, dependencies: Readonly<{
|
|
41
|
+
execute: Execute;
|
|
42
|
+
}>): Promise<readonly EnvironmentDiagnostic[]>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequences the commands that choose and refresh the checkouts a stack serves: `use` points a
|
|
3
|
+
* sibling at another checkout of its repository for this cockpit checkout, `pull` fast-forwards
|
|
4
|
+
* every checkout to its upstream, and doctor and preflight read the findings both leave behind.
|
|
5
|
+
* The decisions live in `sibling-checkouts.ts`, `sibling-pull.ts` and `stack-inputs.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import { realpath } from 'node:fs/promises';
|
|
8
|
+
import { dirname } from 'node:path';
|
|
9
|
+
import { loadCockpitConfig } from '../../config/load.js';
|
|
10
|
+
import { cockpitCommand } from '../../fix-catalogue.js';
|
|
11
|
+
import { refusalOutcome } from '../../refusal.js';
|
|
12
|
+
import { mounted } from '../../schema.js';
|
|
13
|
+
import { listedSurfaceTargets } from '../../surfaces/harness-registry.js';
|
|
14
|
+
import { ENVIRONMENT_CODES, errorCause, isMissing } from '../contract.js';
|
|
15
|
+
import { createGitRunner, inspectSiblingCheckout, loadServedSiblings, missingEnvironmentFileDiagnostics, missingEnvironmentFiles, readSiblingCheckouts, SIBLING_CHECKOUTS_PATH, servedSiblings, vanishedServedCheckouts, writeSiblingCheckouts, } from '../sibling-checkouts.js';
|
|
16
|
+
import { fastForwardCheckout } from '../sibling-pull.js';
|
|
17
|
+
import { mapLimit } from '../map-limit.js';
|
|
18
|
+
import { applyAgentGrants, inside, planAgentGrants } from '../served-checkout-grants.js';
|
|
19
|
+
import { staleStackDiagnostics } from '../stack-inputs.js';
|
|
20
|
+
async function gitOrRefusal(env, execute) {
|
|
21
|
+
const git = await createGitRunner(env, execute);
|
|
22
|
+
return 'unavailable' in git
|
|
23
|
+
? refusalOutcome(ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, `Git is needed to read the checkouts and is unavailable: ${git.unavailable}`, 'git', 'install Git and put it on PATH, then rerun this command')
|
|
24
|
+
: git;
|
|
25
|
+
}
|
|
26
|
+
export async function runUse(input, dependencies) {
|
|
27
|
+
const loaded = await loadCockpitConfig(input.configPath);
|
|
28
|
+
if (loaded.kind !== 'complete')
|
|
29
|
+
return { status: 2, result: null, diagnostics: loaded.diagnostics };
|
|
30
|
+
const configPath = loaded.result.source.path;
|
|
31
|
+
const config = loaded.result.config;
|
|
32
|
+
const root = dirname(configPath);
|
|
33
|
+
const recorded = await readSiblingCheckouts(root);
|
|
34
|
+
if (recorded.kind !== 'complete')
|
|
35
|
+
return { status: 2, result: null, diagnostics: [recorded.diagnostic] };
|
|
36
|
+
const sibling = config.sibling.find(row => row.slug === input.slug);
|
|
37
|
+
// A sibling removed from cockpit.toml can still be forgotten, or doctor would name it forever.
|
|
38
|
+
if (!sibling && input.clear && Object.hasOwn(recorded.checkouts, input.slug)) {
|
|
39
|
+
const { [input.slug]: forgotten, ...others } = recorded.checkouts;
|
|
40
|
+
const grants = await planAgentGrants({ root, slug: input.slug, declared: undefined, served: undefined, targets: [] });
|
|
41
|
+
if (grants.kind !== 'complete')
|
|
42
|
+
return { status: 2, result: null, diagnostics: [grants.diagnostic] };
|
|
43
|
+
const written = await writeSiblingCheckouts(root, others);
|
|
44
|
+
if (written.kind !== 'complete')
|
|
45
|
+
return recordUnwritable(configPath, root, written.message);
|
|
46
|
+
const granted = await applyAgentGrants(root, grants.plan);
|
|
47
|
+
if (granted.kind !== 'complete')
|
|
48
|
+
return { status: 2, result: null, diagnostics: [granted.diagnostic] };
|
|
49
|
+
return { status: 0, diagnostics: [], destination: null, artifact: `Forgot the checkout recorded for ${input.slug}, which cockpit.toml no longer declares.\n`,
|
|
50
|
+
result: { slug: input.slug, overridden: false, previous: forgotten } };
|
|
51
|
+
}
|
|
52
|
+
if (!sibling) {
|
|
53
|
+
return refusalOutcome(ENVIRONMENT_CODES.SIBLING_CHECKOUT_UNKNOWN, `cockpit.toml declares no sibling ${input.slug}; it declares ${config.sibling.map(row => row.slug).join(', ')}`, input.slug, `name a declared sibling, or declare this one with ${cockpitCommand('sibling add', { operand: input.slug, config: configPath })}`);
|
|
54
|
+
}
|
|
55
|
+
const declaredPath = servedSiblings(root, config, {}).get(sibling.slug).declared;
|
|
56
|
+
// Compared with a resolved candidate, so a siblings root reached through a link still matches. A
|
|
57
|
+
// declared checkout not cloned yet stays as declared; one that cannot be resolved is not guessed at.
|
|
58
|
+
let declared = declaredPath;
|
|
59
|
+
try {
|
|
60
|
+
declared = await realpath(declaredPath);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!isMissing(error))
|
|
64
|
+
return refusalOutcome(ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE, `the declared checkout of sibling ${sibling.slug}, ${declaredPath}, could not be resolved: ${errorCause(error)}`, declaredPath, 'make that directory and every directory above it readable, or correct sibling.dir in cockpit.toml, then run this command again');
|
|
65
|
+
}
|
|
66
|
+
const previous = recorded.checkouts[sibling.slug];
|
|
67
|
+
let served = declared;
|
|
68
|
+
let evidence;
|
|
69
|
+
if (!input.clear) {
|
|
70
|
+
const git = await gitOrRefusal(input.env, dependencies.execute);
|
|
71
|
+
if ('status' in git)
|
|
72
|
+
return git;
|
|
73
|
+
const inspected = await inspectSiblingCheckout({ sibling, declared, candidate: input.checkout, git });
|
|
74
|
+
if (inspected.kind === 'refusal')
|
|
75
|
+
return { status: 2, result: null, diagnostics: [inspected.diagnostic] };
|
|
76
|
+
served = inspected.directory;
|
|
77
|
+
evidence = inspected.evidence;
|
|
78
|
+
}
|
|
79
|
+
const { [sibling.slug]: _cleared, ...others } = recorded.checkouts;
|
|
80
|
+
// Naming the declared checkout is the same as clearing: it is what serves the sibling by default.
|
|
81
|
+
const checkouts = served === declared ? others : { ...others, [sibling.slug]: served };
|
|
82
|
+
// The tools that hold a committed grant for this sibling, as sync writes them.
|
|
83
|
+
const targets = mounted(config, config.environment.profiles).some(row => row.slug === sibling.slug) ? listedSurfaceTargets(config.surfaces.tools) : [];
|
|
84
|
+
const grants = await planAgentGrants({ root, slug: sibling.slug, declared, served, targets });
|
|
85
|
+
if (grants.kind !== 'complete')
|
|
86
|
+
return { status: 2, result: null, diagnostics: [grants.diagnostic] };
|
|
87
|
+
const written = previous === checkouts[sibling.slug] ? { kind: 'complete' } : await writeSiblingCheckouts(root, checkouts);
|
|
88
|
+
if (written.kind !== 'complete')
|
|
89
|
+
return recordUnwritable(configPath, root, written.message);
|
|
90
|
+
const granted = await applyAgentGrants(root, grants.plan);
|
|
91
|
+
if (granted.kind !== 'complete')
|
|
92
|
+
return { status: 2, result: null, diagnostics: [{ ...granted.diagnostic,
|
|
93
|
+
message: `${served} was recorded for sibling ${sibling.slug}, but ${granted.diagnostic.message}` }] };
|
|
94
|
+
const current = servedSiblings(root, config, checkouts);
|
|
95
|
+
const entry = current.get(sibling.slug);
|
|
96
|
+
const diagnostics = [
|
|
97
|
+
...missingEnvironmentFileDiagnostics(entry, entry.overridden ? await missingEnvironmentFiles(entry, sibling) : []),
|
|
98
|
+
// A running stack keeps the checkout it mounted until it is brought down and up again.
|
|
99
|
+
...await staleStackDiagnostics(root, current),
|
|
100
|
+
];
|
|
101
|
+
const covered = inside(served, declared);
|
|
102
|
+
const artifact = [
|
|
103
|
+
entry.overridden ? `Sibling ${sibling.slug} is now served from ${served} (${evidence === 'object-store' ? 'a worktree of' : 'a clone with the same origin as'} ${declared}).`
|
|
104
|
+
: `Sibling ${sibling.slug} is served from its declared checkout ${declared}${previous === undefined ? '' : ` again, no longer from ${previous}`}.`,
|
|
105
|
+
`Recorded for this cockpit checkout only, in ${SIBLING_CHECKOUTS_PATH}; up, the guard, doctor and preflight read it.`,
|
|
106
|
+
...(grants.plan.lines.length > 0 ? [`${served} lies outside ${declared}, which the committed directory grants name:`, ...grants.plan.lines.map(line => ` ${line}`)] : []),
|
|
107
|
+
'',
|
|
108
|
+
].join('\n');
|
|
109
|
+
return { status: 0, diagnostics, destination: null, artifact,
|
|
110
|
+
result: { slug: sibling.slug, declared, served, overridden: entry.overridden, ...(evidence ? { evidence } : {}),
|
|
111
|
+
...(previous === undefined ? {} : { previous }), insideDeclared: covered, ...(grants.plan.report ? { agentGrants: grants.plan.report } : {}) } };
|
|
112
|
+
}
|
|
113
|
+
function recordUnwritable(configPath, root, reason) {
|
|
114
|
+
return refusalOutcome(ENVIRONMENT_CODES.SIBLING_CHECKOUT_RECORD_INVALID, `the sibling checkout record could not be written: ${reason}`, SIBLING_CHECKOUTS_PATH, `run ${cockpitCommand('sync', { config: configPath })} first if ${root}/.cockpit does not exist yet, or make it a plain directory you can write; then run this command again`);
|
|
115
|
+
}
|
|
116
|
+
const PULL_CONCURRENCY = 4;
|
|
117
|
+
export async function runPull(input, dependencies) {
|
|
118
|
+
const loaded = await loadCockpitConfig(input.configPath);
|
|
119
|
+
if (loaded.kind !== 'complete')
|
|
120
|
+
return { status: 2, result: null, diagnostics: loaded.diagnostics };
|
|
121
|
+
const config = loaded.result.config;
|
|
122
|
+
const root = dirname(loaded.result.source.path);
|
|
123
|
+
const served = await loadServedSiblings(root, config);
|
|
124
|
+
if (served.kind !== 'complete')
|
|
125
|
+
return { status: 2, result: null, diagnostics: [served.diagnostic] };
|
|
126
|
+
const git = await gitOrRefusal(input.env, dependencies.execute);
|
|
127
|
+
if ('status' in git)
|
|
128
|
+
return git;
|
|
129
|
+
// The cockpit root first, then each sibling's declared checkout and any checkout `use` put in its
|
|
130
|
+
// place. A sibling that is not a repository (acquire = "none") is expected to have no Git history,
|
|
131
|
+
// and a cockpit root outside Git is doctor's readiness finding, not pull's.
|
|
132
|
+
const targets = [{ label: 'cockpit', path: root, expected: false }];
|
|
133
|
+
for (const sibling of config.sibling) {
|
|
134
|
+
const entry = served.siblings.get(sibling.slug);
|
|
135
|
+
targets.push({ label: sibling.slug, path: entry.declared, expected: sibling.acquire !== 'none' });
|
|
136
|
+
if (entry.overridden)
|
|
137
|
+
targets.push({ label: `${sibling.slug} (served)`, path: entry.served, expected: true });
|
|
138
|
+
}
|
|
139
|
+
// Where Git puts each target's repository: a string, nothing when there is no checkout there, or
|
|
140
|
+
// why a toplevel Git named could not be resolved, which is reported against the target's label.
|
|
141
|
+
const located = await Promise.all(targets.map(async (target) => {
|
|
142
|
+
const output = await git.run(target.path, 'pull:root', ['rev-parse', '--show-toplevel']);
|
|
143
|
+
if (output.kind !== 'complete')
|
|
144
|
+
return undefined;
|
|
145
|
+
try {
|
|
146
|
+
return await realpath(output.stdout);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return isMissing(error) ? undefined : { unresolved: `its repository at ${output.stdout} could not be resolved: ${errorCause(error)}` };
|
|
150
|
+
}
|
|
151
|
+
}));
|
|
152
|
+
// Two targets in one repository (an in-repo cockpit is its own sibling) move once, under the first one's label.
|
|
153
|
+
const distinct = new Map();
|
|
154
|
+
located.forEach((toplevel, index) => { if (typeof toplevel === 'string' && !distinct.has(toplevel))
|
|
155
|
+
distinct.set(toplevel, targets[index]); });
|
|
156
|
+
const advanced = new Map(await mapLimit([...distinct], PULL_CONCURRENCY, async ([toplevel, target]) => [toplevel, { label: target.label, ...await fastForwardCheckout(git, toplevel) }]));
|
|
157
|
+
const rows = [];
|
|
158
|
+
const diagnostics = [];
|
|
159
|
+
const reported = new Set();
|
|
160
|
+
located.forEach((toplevel, index) => {
|
|
161
|
+
const target = targets[index];
|
|
162
|
+
if (toplevel === undefined) {
|
|
163
|
+
const row = { label: target.label, state: 'skipped', path: target.path, reason: 'there is no Git checkout there',
|
|
164
|
+
fix: `run ${cockpitCommand('setup', { config: loaded.result.source.path })} to clone it`, blocked: target.expected };
|
|
165
|
+
rows.push(row);
|
|
166
|
+
if (row.blocked)
|
|
167
|
+
diagnostics.push(skippedDiagnostic(row));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (typeof toplevel !== 'string') {
|
|
171
|
+
const row = { label: target.label, state: 'skipped', path: target.path, reason: toplevel.unresolved,
|
|
172
|
+
fix: 'make that directory and every directory above it readable, then run pull again', blocked: true };
|
|
173
|
+
rows.push(row);
|
|
174
|
+
diagnostics.push(skippedDiagnostic(row));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (reported.has(toplevel))
|
|
178
|
+
return;
|
|
179
|
+
reported.add(toplevel);
|
|
180
|
+
const row = advanced.get(toplevel);
|
|
181
|
+
rows.push(row);
|
|
182
|
+
if (row.state === 'skipped' && row.blocked)
|
|
183
|
+
diagnostics.push(skippedDiagnostic(row));
|
|
184
|
+
});
|
|
185
|
+
// A pulled lockfile or migration behind a running stack is the next thing to go wrong.
|
|
186
|
+
if (rows.some(row => row.state === 'advanced'))
|
|
187
|
+
diagnostics.push(...await staleStackDiagnostics(root, served.siblings));
|
|
188
|
+
const line = (row) => row.state === 'advanced'
|
|
189
|
+
? `${row.label}: ${row.branch} fast-forwarded ${row.from.slice(0, 12)} -> ${row.to.slice(0, 12)} (${row.commits} commit${row.commits === 1 ? '' : 's'} from ${row.upstream})`
|
|
190
|
+
: row.state === 'current' ? `${row.label}: ${row.branch} is up to date with ${row.upstream}${row.ahead > 0 ? ` (${row.ahead} ahead)` : ''}`
|
|
191
|
+
: `${row.label}: skipped, ${row.reason}`;
|
|
192
|
+
return { status: 0, diagnostics, destination: null, artifact: `${rows.map(line).join('\n')}\n`, result: { repositories: rows } };
|
|
193
|
+
}
|
|
194
|
+
function skippedDiagnostic(row) {
|
|
195
|
+
return {
|
|
196
|
+
severity: 'warning',
|
|
197
|
+
code: 'PULL_SKIPPED',
|
|
198
|
+
message: `${row.label} at ${row.path} was left as it was: ${row.reason}`,
|
|
199
|
+
target: row.path,
|
|
200
|
+
fix: row.fix,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* What doctor and preflight report about the served checkouts: each override and where it came
|
|
205
|
+
* from, an override that no longer names a checkout of its sibling, the environment files a worktree
|
|
206
|
+
* lacks, and a running stack whose siblings' dependencies or migrations moved since it came up.
|
|
207
|
+
*/
|
|
208
|
+
export async function inspectServedSiblings(input, dependencies) {
|
|
209
|
+
try {
|
|
210
|
+
return await servedSiblingFindings(input, dependencies);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return [{ severity: 'warning', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE,
|
|
214
|
+
message: `the checkouts serving the siblings could not be inspected: ${errorCause(error)}`, target: SIBLING_CHECKOUTS_PATH,
|
|
215
|
+
fix: 'rerun this command; if it persists, check that the sibling checkouts and .cockpit/local are readable' }];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function servedSiblingFindings(input, dependencies) {
|
|
219
|
+
const loaded = input.loaded ?? await loadCockpitConfig(input.configPath);
|
|
220
|
+
// An unloadable cockpit.toml is reported by the command itself.
|
|
221
|
+
if (loaded.kind !== 'complete')
|
|
222
|
+
return [];
|
|
223
|
+
const config = loaded.result.config;
|
|
224
|
+
const root = dirname(loaded.result.source.path);
|
|
225
|
+
const recorded = await readSiblingCheckouts(root);
|
|
226
|
+
// `up` refuses an unreadable record, so the served-checkouts row fails on it too; this says why.
|
|
227
|
+
if (recorded.kind !== 'complete')
|
|
228
|
+
return [recorded.diagnostic];
|
|
229
|
+
const served = servedSiblings(root, config, recorded.checkouts);
|
|
230
|
+
const findings = [];
|
|
231
|
+
for (const slug of Object.keys(recorded.checkouts).filter(slug => !served.has(slug)))
|
|
232
|
+
findings.push({
|
|
233
|
+
severity: 'warning', code: 'SIBLING_CHECKOUT_UNKNOWN',
|
|
234
|
+
message: `${SIBLING_CHECKOUTS_PATH} names a checkout for ${slug}, which cockpit.toml no longer declares, so nothing serves it`,
|
|
235
|
+
target: SIBLING_CHECKOUTS_PATH,
|
|
236
|
+
fix: `forget it with ${cockpitCommand('use', { operand: slug, args: '--clear' })}`,
|
|
237
|
+
});
|
|
238
|
+
// A directory that is gone is the served-checkouts row's finding, which `up` refuses as well; it is not repeated here.
|
|
239
|
+
const gone = new Set((await vanishedServedCheckouts(served)).map(entry => entry.slug));
|
|
240
|
+
const overridden = [...served.values()].filter(entry => entry.overridden && !gone.has(entry.slug));
|
|
241
|
+
const git = overridden.length === 0 ? undefined : await createGitRunner(input.env, dependencies.execute);
|
|
242
|
+
const perEntry = await Promise.all(overridden.map(async (entry) => {
|
|
243
|
+
const sibling = config.sibling.find(row => row.slug === entry.slug);
|
|
244
|
+
const rows = [{
|
|
245
|
+
severity: 'info', code: 'SIBLING_CHECKOUT_OVERRIDDEN',
|
|
246
|
+
message: `sibling ${entry.slug} is served from ${entry.served}, recorded by use in ${SIBLING_CHECKOUTS_PATH}, instead of its declared checkout ${entry.declared}`,
|
|
247
|
+
target: `sibling:${entry.slug}`,
|
|
248
|
+
fix: `serve the declared checkout again with ${cockpitCommand('use', { operand: entry.slug, args: '--clear' })}`,
|
|
249
|
+
}];
|
|
250
|
+
if (git === undefined)
|
|
251
|
+
return rows;
|
|
252
|
+
if ('unavailable' in git) {
|
|
253
|
+
rows.push({ severity: 'warning', code: ENVIRONMENT_CODES.EVIDENCE_UNAVAILABLE,
|
|
254
|
+
message: `whether ${entry.served} is still a checkout of sibling ${entry.slug} could not be checked: ${git.unavailable}`,
|
|
255
|
+
target: entry.served, fix: 'install Git and put it on PATH, then rerun this command' });
|
|
256
|
+
return rows;
|
|
257
|
+
}
|
|
258
|
+
const inspected = await inspectSiblingCheckout({ sibling, declared: entry.declared, candidate: entry.served, git });
|
|
259
|
+
// `up` does not refuse a checkout of another repository, so these commands report it as a warning.
|
|
260
|
+
if (inspected.kind === 'refusal')
|
|
261
|
+
rows.push({ ...inspected.diagnostic, severity: 'warning' });
|
|
262
|
+
else
|
|
263
|
+
rows.push(...missingEnvironmentFileDiagnostics(entry, await missingEnvironmentFiles(entry, sibling)));
|
|
264
|
+
return rows;
|
|
265
|
+
}));
|
|
266
|
+
findings.push(...perEntry.flat());
|
|
267
|
+
findings.push(...await staleStackDiagnostics(root, served));
|
|
268
|
+
return findings;
|
|
269
|
+
}
|
|
@@ -24,7 +24,7 @@ export declare function observeClaimState(input: {
|
|
|
24
24
|
readonly stackId: StackId;
|
|
25
25
|
readonly observedAt: string;
|
|
26
26
|
}): Promise<EnvironmentObservation>;
|
|
27
|
-
|
|
27
|
+
interface ClaimHolder {
|
|
28
28
|
readonly holderId: HolderId;
|
|
29
29
|
readonly checkoutId: CheckoutId;
|
|
30
30
|
readonly claimedAt: string;
|
|
@@ -47,8 +47,8 @@ export interface CoordinationRecord {
|
|
|
47
47
|
readonly lastReleasedHolder: HolderId | null;
|
|
48
48
|
readonly drain?: DrainReservation | null;
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
type DrainPurpose = 'sleep' | 'down' | 'reset';
|
|
51
|
+
type DrainAction = {
|
|
52
52
|
readonly stage: 'runtime-stop';
|
|
53
53
|
readonly targetId: string;
|
|
54
54
|
readonly validForMs: number;
|
|
@@ -84,7 +84,7 @@ export interface DrainReservation extends DrainReservationRequest {
|
|
|
84
84
|
readonly progressRevision: number;
|
|
85
85
|
readonly attempt: DrainAttempt | null;
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
interface DrainAttempt {
|
|
88
88
|
readonly attemptId: string;
|
|
89
89
|
readonly reservationId: string;
|
|
90
90
|
readonly purpose: DrainPurpose;
|
|
@@ -100,7 +100,7 @@ export interface DrainAttempt {
|
|
|
100
100
|
readonly failedRevision: number | null;
|
|
101
101
|
readonly failedAt: string | null;
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
interface FailedReclaimRecoveryTarget {
|
|
104
104
|
readonly targetId: string;
|
|
105
105
|
readonly targetCommitment: DrainTargetCommitment;
|
|
106
106
|
}
|
|
@@ -168,7 +168,7 @@ type ClaimMutationRefusal = {
|
|
|
168
168
|
readonly kind: 'refusal';
|
|
169
169
|
readonly diagnostics: readonly EnvironmentDiagnostic[];
|
|
170
170
|
};
|
|
171
|
-
|
|
171
|
+
type ReleaseClaimResult = {
|
|
172
172
|
readonly kind: 'complete';
|
|
173
173
|
readonly change: 'released' | 'unchanged';
|
|
174
174
|
readonly decision: 'stop' | 'retain';
|
|
@@ -215,7 +215,7 @@ export declare function observeProcessStart(pid: number, host: ProcessHost): Pro
|
|
|
215
215
|
/** The production `ProcessHost`: signal 0 and ps cross the executor, and /proc is read directly. */
|
|
216
216
|
export declare function processHost(execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>): ProcessHost;
|
|
217
217
|
export declare function liveHolderMessage(holders: readonly ClaimHolder[]): string;
|
|
218
|
-
|
|
218
|
+
type ReleaseDeadSessionHoldersResult = {
|
|
219
219
|
readonly kind: 'complete';
|
|
220
220
|
readonly released: readonly ClaimHolder[];
|
|
221
221
|
} | ClaimMutationRefusal;
|
|
@@ -264,7 +264,7 @@ export declare function addCoordinationHolder(value: CoordinationRecord, holder:
|
|
|
264
264
|
export declare function removeExactCoordinationHolder(value: CoordinationRecord, holderId: HolderId): CoordinationRecord | undefined;
|
|
265
265
|
export declare function coordinationStateRecord(namespace: StateNamespace, stackId: StackId, current: StateRecord | null, now: string, value: CoordinationRecord): StateRecord;
|
|
266
266
|
export declare function isDrainActive(value: CoordinationRecord): boolean;
|
|
267
|
-
|
|
267
|
+
type DrainReservationResult = {
|
|
268
268
|
readonly kind: 'complete';
|
|
269
269
|
readonly reservation: DrainReservation;
|
|
270
270
|
} | ClaimMutationRefusal;
|
|
@@ -315,7 +315,7 @@ export declare function matchReclaimDrainContinuation(input: {
|
|
|
315
315
|
readonly continuation: ReclaimDrainStep | null;
|
|
316
316
|
readonly record: StateRecord;
|
|
317
317
|
}): 'progressed' | 'failed' | 'unmatched';
|
|
318
|
-
|
|
318
|
+
type SleepDrainRecoveryInspection = {
|
|
319
319
|
readonly kind: 'none';
|
|
320
320
|
} | {
|
|
321
321
|
readonly kind: 'repairable';
|
|
@@ -328,7 +328,7 @@ export type SleepDrainRecoveryInspection = {
|
|
|
328
328
|
/** The record inspected; recovery acts only while the stored record is still exactly this one. */
|
|
329
329
|
readonly record: StateRecord;
|
|
330
330
|
} | ClaimMutationRefusal;
|
|
331
|
-
|
|
331
|
+
type DestructiveDrainRecoveryInspection = {
|
|
332
332
|
readonly kind: 'none';
|
|
333
333
|
} | {
|
|
334
334
|
readonly kind: 'repairable';
|
|
@@ -4,6 +4,7 @@ import { dirname, isAbsolute, join, normalize } from 'node:path';
|
|
|
4
4
|
import { createHash, randomBytes } from 'node:crypto';
|
|
5
5
|
import { platform } from 'node:os';
|
|
6
6
|
import { STRUCTURED_INPUT_ENTRY_LIMIT } from '../schema.js';
|
|
7
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
7
8
|
import { hasErrorCode, RefusalError, ENVIRONMENT_CODES, errorCause, isCanonicalTimestamp, snapshotJson, snapshotExecutionPlan, snapshotEnvironmentObservation, } from './contract.js';
|
|
8
9
|
import { authenticatedExecutionOutcome, consumeFencedExecutionTermination } from '../environment-exec/execute.js';
|
|
9
10
|
import { resolveExecutablePath } from './executable.js';
|
|
@@ -108,18 +109,9 @@ const DrainAttemptSchema = z.object({
|
|
|
108
109
|
context.addIssue({ code: 'custom', message: 'drain attempt targets are not canonical' });
|
|
109
110
|
}
|
|
110
111
|
});
|
|
111
|
-
function canonicalJsonValue(value) {
|
|
112
|
-
if (Array.isArray(value))
|
|
113
|
-
return value.map(canonicalJsonValue);
|
|
114
|
-
if (value !== null && typeof value === 'object') {
|
|
115
|
-
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
116
|
-
.map(([key, item]) => [key, canonicalJsonValue(item)]));
|
|
117
|
-
}
|
|
118
|
-
return value;
|
|
119
|
-
}
|
|
120
112
|
function isCanonicalJson(body) {
|
|
121
113
|
try {
|
|
122
|
-
return
|
|
114
|
+
return canonicalJson(JSON.parse(body)) === body;
|
|
123
115
|
}
|
|
124
116
|
catch {
|
|
125
117
|
// Unparseable JSON is simply not canonical.
|
|
@@ -907,7 +899,7 @@ function exactRecordFingerprint(record) {
|
|
|
907
899
|
|| typeof candidate.updatedAt !== 'string' || !isCanonicalTimestamp(candidate.updatedAt)
|
|
908
900
|
|| !z.json().safeParse(candidate.value).success)
|
|
909
901
|
return undefined;
|
|
910
|
-
return
|
|
902
|
+
return canonicalJson(snapshot);
|
|
911
903
|
}
|
|
912
904
|
export function isDrainActive(value) {
|
|
913
905
|
return value.drain != null;
|
|
@@ -8,7 +8,7 @@ import type { ResolvedExecutable } from './executable.js';
|
|
|
8
8
|
* repository's Compose file names `./api` relative to the repository.
|
|
9
9
|
*/
|
|
10
10
|
export declare function composeProjectDirectory(root: string, references: readonly string[]): string;
|
|
11
|
-
|
|
11
|
+
interface ComposeTopologyInput {
|
|
12
12
|
/** Canonical absolute cockpit root. */
|
|
13
13
|
readonly root: string;
|
|
14
14
|
/** The declared `environment.compose_files`, relative to `root`. */
|
|
@@ -20,7 +20,7 @@ export interface ComposeTopologyInput {
|
|
|
20
20
|
/** Filtered to the Docker connection variables before it reaches the probe. */
|
|
21
21
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
interface ComposePublishedPort {
|
|
24
24
|
/** The host side exactly as written, `null` when Compose picks an ephemeral host port. */
|
|
25
25
|
readonly published: string | null;
|
|
26
26
|
/** The host interface exactly as written, `null` when none is given, so Docker binds every interface. */
|
|
@@ -33,20 +33,24 @@ export interface ComposeServiceTopology {
|
|
|
33
33
|
readonly healthcheck: boolean;
|
|
34
34
|
readonly image: string | null;
|
|
35
35
|
readonly build: boolean;
|
|
36
|
+
/** The service's fixed `container_name`, `null` when Compose names the container itself. */
|
|
37
|
+
readonly containerName: string | null;
|
|
36
38
|
/**
|
|
37
39
|
* Each volume source as Compose reports it without interpolation: the absolute host path of a
|
|
38
40
|
* bind, or the raw `${VAR}` text of a source still waiting for its variable.
|
|
39
41
|
*/
|
|
40
42
|
readonly mountSources: readonly string[];
|
|
43
|
+
/** The Compose profiles that enable the service; absent or empty when it always runs. */
|
|
44
|
+
readonly profiles?: readonly string[];
|
|
41
45
|
}
|
|
42
46
|
/** A `[[sibling]]` row as the mount detector needs it: its absolute checkout path and variable. */
|
|
43
|
-
|
|
47
|
+
interface ComposeSiblingMount {
|
|
44
48
|
readonly slug: string;
|
|
45
49
|
readonly var: string;
|
|
46
50
|
readonly mount: string;
|
|
47
51
|
readonly path: string;
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
type ComposeTopologyObservation = Readonly<{
|
|
50
54
|
kind: 'complete';
|
|
51
55
|
services: readonly ComposeServiceTopology[];
|
|
52
56
|
}> | Readonly<{
|
|
@@ -93,11 +97,11 @@ export declare function compareBindSources(binds: readonly string[], declared: r
|
|
|
93
97
|
* Docker follows a symbolic link in the bind source, so `./api/cache -> /` would otherwise hand
|
|
94
98
|
* the host root to a container under a mount = "always" checkout.
|
|
95
99
|
*/
|
|
96
|
-
export declare function redirectedBindSources(binds: readonly string[], declared: readonly string[]): Promise<readonly Readonly<{
|
|
100
|
+
export declare function redirectedBindSources(binds: readonly string[], declared: readonly string[], resolve?: (path: string) => Promise<string>): Promise<readonly Readonly<{
|
|
97
101
|
bind: string;
|
|
98
102
|
holder: string;
|
|
99
103
|
}>[]>;
|
|
100
|
-
|
|
104
|
+
interface ComposeTopologyDeclarations {
|
|
101
105
|
/** From composeProjectDirectory; a relative bind source, or a variable's relative default, resolves against it. */
|
|
102
106
|
readonly projectDirectory: string;
|
|
103
107
|
readonly portVariables: readonly string[];
|
|
@@ -107,11 +111,15 @@ export interface ComposeTopologyDeclarations {
|
|
|
107
111
|
declared: string;
|
|
108
112
|
path: string;
|
|
109
113
|
}>[];
|
|
114
|
+
/** The `environment.profiles` every `up` activates. */
|
|
115
|
+
readonly profiles?: readonly string[];
|
|
110
116
|
}
|
|
111
117
|
/**
|
|
112
118
|
* Judges a cockpit's own Compose topology. A host port that is not drawn from a leased
|
|
113
119
|
* `ports.bases` variable is the same number in every worktree and parallel stack, so the second
|
|
114
|
-
* stack to start fails to bind it; the
|
|
120
|
+
* stack to start fails to bind it; a fixed `container_name` fails the same way, and a fixed tag on a
|
|
121
|
+
* built image is shared the same way. The remaining findings are warnings about what cockpit can
|
|
115
122
|
* then promise, not about whether the stack runs.
|
|
116
123
|
*/
|
|
117
124
|
export declare function assessComposeTopology(services: readonly ComposeServiceTopology[], declarations: ComposeTopologyDeclarations): Promise<readonly Diagnostic[]>;
|
|
125
|
+
export {};
|