@wringer/core 0.1.0 → 0.1.2
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/LICENSE +1 -1
- package/README.md +71 -2
- package/dist/answer-supervisor-connection.d.ts +8 -0
- package/dist/app-machine.d.ts +76 -0
- package/dist/append-run-event.d.ts +16 -0
- package/dist/bind-drivers.d.ts +9 -0
- package/dist/build-commands.d.ts +5 -0
- package/dist/build-envelope-json-schema.d.ts +1 -0
- package/dist/build-envelope.d.ts +16 -0
- package/dist/build-launch-guard.d.ts +1 -0
- package/dist/build-manifest-write.d.ts +6 -0
- package/dist/build-promised-object-reads.d.ts +7 -0
- package/dist/build-report.d.ts +10 -0
- package/dist/build-run-handle.d.ts +2 -0
- package/dist/build-run-view.d.ts +29 -0
- package/dist/build-service-key.d.ts +1 -0
- package/dist/build-shut-down-steps.d.ts +14 -0
- package/dist/build-skip-worktree-marks.d.ts +7 -0
- package/dist/build-spawn-error.d.ts +2 -0
- package/dist/build-state-paths.d.ts +21 -0
- package/dist/build-submodule-adds.d.ts +7 -0
- package/dist/builtins/build-doctor-command.d.ts +2 -0
- package/dist/builtins/build-exec-command.d.ts +2 -0
- package/dist/builtins/build-lifecycle-commands.d.ts +3 -0
- package/dist/builtins/build-state-commands.d.ts +2 -0
- package/dist/builtins/build-use-pointers.d.ts +9 -0
- package/dist/builtins/build-verdict-commands.d.ts +2 -0
- package/dist/builtins/call-run-supervisor.d.ts +5 -0
- package/dist/builtins/pick-doctor-outcome.d.ts +13 -0
- package/dist/builtins/pick-start-action.d.ts +10 -0
- package/dist/builtins/pick-stop-plan.d.ts +14 -0
- package/dist/builtins/start-run.d.ts +22 -0
- package/dist/builtins/stop-data-schema.d.ts +6 -0
- package/dist/builtins/stop-run.d.ts +3 -0
- package/dist/call-supervisor.d.ts +10 -0
- package/dist/cap-data.d.ts +17 -0
- package/dist/collect-driver-configs.d.ts +7 -0
- package/dist/create-directory.d.ts +1 -0
- package/dist/create-evidence-writer.d.ts +13 -0
- package/dist/create-file-state.d.ts +36 -0
- package/dist/create-log-writer.d.ts +5 -0
- package/dist/create-node-repo-host.d.ts +2 -0
- package/dist/create-node-state-file-system.d.ts +10 -0
- package/dist/create-repo-state.d.ts +2 -0
- package/dist/create-socket-folder.d.ts +1 -0
- package/dist/create-wringer.d.ts +17 -0
- package/dist/decide-guard.d.ts +12 -0
- package/dist/define-driver.d.ts +13 -0
- package/dist/dispatch.d.ts +10 -0
- package/dist/envelope.d.ts +63 -0
- package/dist/error-codes.d.ts +25 -0
- package/dist/find-cancel-failure.d.ts +2 -0
- package/dist/find-colliding-service-key.d.ts +5 -0
- package/dist/find-own-value.d.ts +1 -0
- package/dist/find-remote-stop-failure.d.ts +2 -0
- package/dist/find-verdict-refusal.d.ts +3 -0
- package/dist/follow-process.d.ts +10 -0
- package/dist/get-lock-value.d.ts +2 -0
- package/dist/get-target.d.ts +2 -0
- package/dist/host.d.ts +19 -0
- package/dist/index.d.ts +60 -0
- package/dist/is-error-code.d.ts +1 -0
- package/dist/keep-cancelled-call.d.ts +1 -0
- package/dist/kill-process-group.d.ts +1 -0
- package/dist/launch-line-schema.d.ts +3 -0
- package/dist/listen-supervisor-socket.d.ts +8 -0
- package/dist/local.d.ts +2 -0
- package/dist/make-fence.d.ts +8 -0
- package/dist/make-fenced-exec.d.ts +10 -0
- package/dist/make-process-supervisor-port.d.ts +7 -0
- package/dist/open-drive.d.ts +23 -0
- package/dist/open-runless-drive.d.ts +17 -0
- package/dist/parse-launch-line.d.ts +2 -0
- package/dist/parse-patch-id.d.ts +2 -0
- package/dist/parse-report-block.d.ts +9 -0
- package/dist/parse-run-log.d.ts +2 -0
- package/dist/pick-access-refusal.d.ts +12 -0
- package/dist/pick-command-needs.d.ts +15 -0
- package/dist/pick-command-plan.d.ts +23 -0
- package/dist/pick-context-target.d.ts +7 -0
- package/dist/pick-drive-kind.d.ts +8 -0
- package/dist/pick-envelope-context.d.ts +1 -0
- package/dist/pick-envelope-payload.d.ts +12 -0
- package/dist/pick-envelope-run.d.ts +6 -0
- package/dist/pick-envelope-target.d.ts +4 -0
- package/dist/pick-late-failure-write.d.ts +13 -0
- package/dist/pick-log-refusal.d.ts +4 -0
- package/dist/pick-merge-base-failure.d.ts +2 -0
- package/dist/pick-merge-base-steps.d.ts +13 -0
- package/dist/pick-pending-cleanups.d.ts +10 -0
- package/dist/pick-submodule-refusal.d.ts +2 -0
- package/dist/pick-supervisor-operation.d.ts +16 -0
- package/dist/prepare-args.d.ts +3 -0
- package/dist/probe-process.d.ts +1 -0
- package/dist/quote-shell-word.d.ts +1 -0
- package/dist/race-deadline.d.ts +1 -0
- package/dist/read-changed-paths.d.ts +7 -0
- package/dist/read-default-base.d.ts +2 -0
- package/dist/read-fingerprint.d.ts +13 -0
- package/dist/read-head-sha.d.ts +2 -0
- package/dist/read-launch-host.d.ts +26 -0
- package/dist/read-pid-namespace.d.ts +1 -0
- package/dist/read-stream-line-batches.d.ts +2 -0
- package/dist/read-stream-lines.d.ts +2 -0
- package/dist/read-text.d.ts +1 -0
- package/dist/read-verdicts.d.ts +5 -0
- package/dist/remove-directory.d.ts +1 -0
- package/dist/remove-file.d.ts +1 -0
- package/dist/render-report-markdown.d.ts +4 -0
- package/dist/report-schema.d.ts +51 -0
- package/dist/run-events.d.ts +129 -0
- package/dist/run-git.d.ts +9 -0
- package/dist/run-process.d.ts +19 -0
- package/dist/run-supervisor-process.d.ts +2 -0
- package/dist/run-supervisor.d.ts +16 -0
- package/dist/settle-dispatch-answer.d.ts +9 -0
- package/dist/settle-envelope.d.ts +16 -0
- package/dist/ssh.d.ts +12 -0
- package/dist/start-launch.d.ts +20 -0
- package/dist/start-services.d.ts +16 -0
- package/dist/supervisor-machine.d.ts +78 -0
- package/dist/supervisor-protocol.d.ts +43 -0
- package/dist/to-wringer-error.d.ts +2 -0
- package/dist/types.d.ts +353 -0
- package/dist/wait-for-cancelled-calls.d.ts +2 -0
- package/dist/wringer-error.d.ts +12 -0
- package/dist/write-envelope-log.d.ts +11 -0
- package/dist/write-late-failure.d.ts +11 -0
- package/dist/write-new-file.d.ts +1 -0
- package/envelope.schema.json +175 -0
- package/package.json +21 -5
- package/src/answer-supervisor-connection.ts +117 -0
- package/src/app-machine.ts +174 -0
- package/src/append-run-event.ts +131 -0
- package/src/bind-drivers.ts +33 -0
- package/src/build-commands.ts +104 -0
- package/src/build-envelope-json-schema.ts +34 -0
- package/src/build-envelope.ts +40 -0
- package/src/build-launch-guard.ts +5 -0
- package/src/build-manifest-write.ts +26 -0
- package/src/build-promised-object-reads.ts +48 -0
- package/src/build-report.ts +55 -0
- package/src/build-run-handle.ts +13 -0
- package/src/build-run-view.ts +120 -0
- package/src/build-service-key.ts +4 -0
- package/src/build-shut-down-steps.ts +54 -0
- package/src/build-skip-worktree-marks.ts +36 -0
- package/src/build-spawn-error.ts +12 -0
- package/src/build-state-paths.ts +53 -0
- package/src/build-submodule-adds.ts +64 -0
- package/src/builtins/build-doctor-command.ts +163 -0
- package/src/builtins/build-exec-command.ts +41 -0
- package/src/builtins/build-lifecycle-commands.ts +115 -0
- package/src/builtins/build-state-commands.ts +152 -0
- package/src/builtins/build-use-pointers.ts +26 -0
- package/src/builtins/build-verdict-commands.ts +266 -0
- package/src/builtins/call-run-supervisor.ts +40 -0
- package/src/builtins/pick-doctor-outcome.ts +32 -0
- package/src/builtins/pick-start-action.ts +35 -0
- package/src/builtins/pick-stop-plan.ts +32 -0
- package/src/builtins/start-run.ts +281 -0
- package/src/builtins/stop-data-schema.ts +8 -0
- package/src/builtins/stop-run.ts +49 -0
- package/src/call-supervisor.ts +109 -0
- package/src/cap-data.ts +43 -0
- package/src/collect-driver-configs.ts +36 -0
- package/src/create-directory.ts +6 -0
- package/src/create-evidence-writer.ts +82 -0
- package/src/create-file-state.ts +193 -0
- package/src/create-log-writer.ts +38 -0
- package/src/create-node-repo-host.ts +20 -0
- package/src/create-node-state-file-system.ts +52 -0
- package/src/create-repo-state.ts +15 -0
- package/src/create-socket-folder.ts +24 -0
- package/src/create-wringer.ts +169 -0
- package/src/decide-guard.ts +47 -0
- package/src/define-driver.ts +165 -0
- package/src/dispatch.ts +589 -0
- package/src/envelope.ts +57 -0
- package/src/error-codes.ts +39 -0
- package/src/find-cancel-failure.ts +7 -0
- package/src/find-colliding-service-key.ts +16 -0
- package/src/find-own-value.ts +8 -0
- package/src/find-remote-stop-failure.ts +19 -0
- package/src/find-verdict-refusal.ts +26 -0
- package/src/follow-process.ts +147 -0
- package/src/get-lock-value.ts +17 -0
- package/src/get-target.ts +15 -0
- package/src/host.ts +26 -0
- package/src/index.ts +75 -0
- package/src/is-error-code.ts +3 -0
- package/src/keep-cancelled-call.ts +21 -0
- package/src/kill-process-group.ts +13 -0
- package/src/launch-line-schema.ts +6 -0
- package/src/listen-supervisor-socket.ts +75 -0
- package/src/local.ts +20 -0
- package/src/make-fence.ts +27 -0
- package/src/make-fenced-exec.ts +52 -0
- package/src/make-process-supervisor-port.ts +65 -0
- package/src/open-drive.ts +151 -0
- package/src/open-runless-drive.ts +71 -0
- package/src/parse-launch-line.ts +19 -0
- package/src/parse-patch-id.ts +8 -0
- package/src/parse-report-block.ts +39 -0
- package/src/parse-run-log.ts +34 -0
- package/src/pick-access-refusal.ts +32 -0
- package/src/pick-command-needs.ts +29 -0
- package/src/pick-command-plan.ts +51 -0
- package/src/pick-context-target.ts +22 -0
- package/src/pick-drive-kind.ts +23 -0
- package/src/pick-envelope-context.ts +10 -0
- package/src/pick-envelope-payload.ts +28 -0
- package/src/pick-envelope-run.ts +25 -0
- package/src/pick-envelope-target.ts +10 -0
- package/src/pick-late-failure-write.ts +44 -0
- package/src/pick-log-refusal.ts +18 -0
- package/src/pick-merge-base-failure.ts +15 -0
- package/src/pick-merge-base-steps.ts +37 -0
- package/src/pick-pending-cleanups.ts +23 -0
- package/src/pick-submodule-refusal.ts +46 -0
- package/src/pick-supervisor-operation.ts +52 -0
- package/src/prepare-args.ts +22 -0
- package/src/probe-process.ts +16 -0
- package/src/quote-shell-word.ts +9 -0
- package/src/race-deadline.ts +23 -0
- package/src/read-changed-paths.ts +39 -0
- package/src/read-default-base.ts +21 -0
- package/src/read-fingerprint.ts +271 -0
- package/src/read-head-sha.ts +23 -0
- package/src/read-launch-host.ts +64 -0
- package/src/read-pid-namespace.ts +18 -0
- package/src/read-stream-line-batches.ts +28 -0
- package/src/read-stream-lines.ts +11 -0
- package/src/read-text.ts +5 -0
- package/src/read-verdicts.ts +57 -0
- package/src/remove-directory.ts +6 -0
- package/src/remove-file.ts +6 -0
- package/src/render-report-markdown.ts +53 -0
- package/src/report-schema.ts +37 -0
- package/src/run-events.ts +71 -0
- package/src/run-git.ts +49 -0
- package/src/run-process.ts +168 -0
- package/src/run-supervisor-process.ts +51 -0
- package/src/run-supervisor.ts +327 -0
- package/src/settle-dispatch-answer.ts +26 -0
- package/src/settle-envelope.ts +34 -0
- package/src/ssh.ts +211 -0
- package/src/start-launch.ts +327 -0
- package/src/start-services.ts +108 -0
- package/src/supervisor-machine.ts +169 -0
- package/src/supervisor-protocol.ts +42 -0
- package/src/to-wringer-error.ts +20 -0
- package/src/types.ts +476 -0
- package/src/wait-for-cancelled-calls.ts +37 -0
- package/src/wringer-error.ts +25 -0
- package/src/write-envelope-log.ts +72 -0
- package/src/write-late-failure.ts +40 -0
- package/src/write-new-file.ts +17 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildBuiltinCommand } from '../build-commands.ts';
|
|
3
|
+
import type { BuiltinCommandSpec, RegisteredCommand } from '../types.ts';
|
|
4
|
+
import { WringerError } from '../wringer-error.ts';
|
|
5
|
+
import { runDataSchema, startRun } from './start-run.ts';
|
|
6
|
+
import type { RunData } from './start-run.ts';
|
|
7
|
+
import { stopDataSchema } from './stop-data-schema.ts';
|
|
8
|
+
import type { StopData } from './stop-data-schema.ts';
|
|
9
|
+
import { stopRun } from './stop-run.ts';
|
|
10
|
+
|
|
11
|
+
export const launchReadyTimeoutMs = 120_000;
|
|
12
|
+
const startArgsSchema = z.strictObject({ restart: z.boolean().default(false) });
|
|
13
|
+
|
|
14
|
+
type StartArgs = z.infer<typeof startArgsSchema>;
|
|
15
|
+
|
|
16
|
+
// `up` and `down` own a launch target's app; `attach` and `detach` never stop an attach target's
|
|
17
|
+
// app. None of them writes to the app, so all four run on a read-only target.
|
|
18
|
+
export function buildLifecycleCommands(): readonly RegisteredCommand[] {
|
|
19
|
+
const up: BuiltinCommandSpec<StartArgs, RunData> = {
|
|
20
|
+
path: ['up'],
|
|
21
|
+
summary: 'Start a run of a launch target: its app, browser and stream recorders',
|
|
22
|
+
args: startArgsSchema,
|
|
23
|
+
data: runDataSchema,
|
|
24
|
+
effect: 'read',
|
|
25
|
+
needsRun: false,
|
|
26
|
+
needsDoctor: false,
|
|
27
|
+
timeoutMs: launchReadyTimeoutMs + 10_000,
|
|
28
|
+
run: (args, ctx) => {
|
|
29
|
+
if (ctx.target.mode !== 'launch') {
|
|
30
|
+
throw new WringerError('usage', {
|
|
31
|
+
message: `Target ${ctx.targetName} is an attach target.`,
|
|
32
|
+
hint: `Run \`attach --target ${ctx.targetName}\`.`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return startRun(ctx, {
|
|
37
|
+
command: 'up',
|
|
38
|
+
restart: args.restart,
|
|
39
|
+
readyTimeoutMs: launchReadyTimeoutMs,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const attach: BuiltinCommandSpec<StartArgs, RunData> = {
|
|
45
|
+
path: ['attach'],
|
|
46
|
+
summary: "Start a run on an attach target's app, without owning the app",
|
|
47
|
+
args: startArgsSchema,
|
|
48
|
+
data: runDataSchema,
|
|
49
|
+
effect: 'read',
|
|
50
|
+
needsRun: false,
|
|
51
|
+
needsDoctor: false,
|
|
52
|
+
timeoutMs: launchReadyTimeoutMs + 10_000,
|
|
53
|
+
run: (args, ctx) => {
|
|
54
|
+
if (ctx.target.mode !== 'attach') {
|
|
55
|
+
throw new WringerError('usage', {
|
|
56
|
+
message: `Target ${ctx.targetName} is a launch target.`,
|
|
57
|
+
hint: `Run \`up --target ${ctx.targetName}\`.`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return startRun(ctx, {
|
|
62
|
+
command: 'attach',
|
|
63
|
+
restart: args.restart,
|
|
64
|
+
readyTimeoutMs: launchReadyTimeoutMs,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const down: BuiltinCommandSpec<Record<string, never>, StopData> = {
|
|
70
|
+
path: ['down'],
|
|
71
|
+
summary: "Stop the launch target's run and its app",
|
|
72
|
+
args: z.strictObject({}),
|
|
73
|
+
data: stopDataSchema,
|
|
74
|
+
effect: 'read',
|
|
75
|
+
needsRun: false,
|
|
76
|
+
needsDoctor: false,
|
|
77
|
+
run: (_args, ctx) => {
|
|
78
|
+
if (ctx.target.mode !== 'launch') {
|
|
79
|
+
throw new WringerError('usage', {
|
|
80
|
+
message: `Target ${ctx.targetName} is an attach target.`,
|
|
81
|
+
hint: `Run \`detach --target ${ctx.targetName}\`.`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return stopRun(ctx);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const detach: BuiltinCommandSpec<Record<string, never>, StopData> = {
|
|
90
|
+
path: ['detach'],
|
|
91
|
+
summary: "End the attach target's run, leaving its app running",
|
|
92
|
+
args: z.strictObject({}),
|
|
93
|
+
data: stopDataSchema,
|
|
94
|
+
effect: 'read',
|
|
95
|
+
needsRun: false,
|
|
96
|
+
needsDoctor: false,
|
|
97
|
+
run: (_args, ctx) => {
|
|
98
|
+
if (ctx.target.mode !== 'attach') {
|
|
99
|
+
throw new WringerError('usage', {
|
|
100
|
+
message: `Target ${ctx.targetName} is a launch target.`,
|
|
101
|
+
hint: `Run \`down --target ${ctx.targetName}\`.`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return stopRun(ctx);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
buildBuiltinCommand(up),
|
|
111
|
+
buildBuiltinCommand(attach),
|
|
112
|
+
buildBuiltinCommand(down),
|
|
113
|
+
buildBuiltinCommand(detach),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { join, relative } from 'node:path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { buildBuiltinCommand } from '../build-commands.ts';
|
|
4
|
+
import { buildRunPaths } from '../build-state-paths.ts';
|
|
5
|
+
import { findOwnValue } from '../find-own-value.ts';
|
|
6
|
+
import { unassignedFeature } from '../open-drive.ts';
|
|
7
|
+
import type { BuiltinCommandSpec, RegisteredCommand } from '../types.ts';
|
|
8
|
+
import { WringerError } from '../wringer-error.ts';
|
|
9
|
+
import { buildUsePointers } from './build-use-pointers.ts';
|
|
10
|
+
|
|
11
|
+
const statusDataSchema = z.strictObject({
|
|
12
|
+
target: z.string(),
|
|
13
|
+
mode: z.enum(['launch', 'attach']),
|
|
14
|
+
access: z.enum(['read-write', 'read-only']),
|
|
15
|
+
instance: z.string().nullable(),
|
|
16
|
+
status: z.enum(['none', 'starting', 'running', 'crashed', 'ended']),
|
|
17
|
+
doctor: z.enum(['none', 'passed', 'failed']),
|
|
18
|
+
feature: z.string().nullable(),
|
|
19
|
+
evidence: z.string().nullable(),
|
|
20
|
+
endpoints: z.record(z.string(), z.string()),
|
|
21
|
+
services: z.array(z.string()),
|
|
22
|
+
cursors: z.record(z.string(), z.number()),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
type StatusData = z.infer<typeof statusDataSchema>;
|
|
26
|
+
|
|
27
|
+
const useArgsSchema = z.strictObject({
|
|
28
|
+
feature: z.string().min(1).optional(),
|
|
29
|
+
target: z.string().min(1).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
type UseArgs = z.infer<typeof useArgsSchema>;
|
|
33
|
+
|
|
34
|
+
const useDataSchema = z.strictObject({
|
|
35
|
+
target: z.string().nullable(),
|
|
36
|
+
feature: z.string().nullable(),
|
|
37
|
+
cursors: z.record(z.string(), z.number()),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
type UseData = z.infer<typeof useDataSchema>;
|
|
41
|
+
|
|
42
|
+
const featureEntrySchema = z.strictObject({
|
|
43
|
+
name: z.string(),
|
|
44
|
+
summary: z.string(),
|
|
45
|
+
paths: z.array(z.string()),
|
|
46
|
+
current: z.boolean(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const featuresDataSchema = z.strictObject({ features: z.array(featureEntrySchema) });
|
|
50
|
+
|
|
51
|
+
type FeaturesData = z.infer<typeof featuresDataSchema>;
|
|
52
|
+
|
|
53
|
+
// `status`, `use` and `features` read and set the worktree's own state, never the target's, so they
|
|
54
|
+
// run without a run and on a read-only target.
|
|
55
|
+
export function buildStateCommands(): readonly RegisteredCommand[] {
|
|
56
|
+
const status: BuiltinCommandSpec<Record<string, never>, StatusData> = {
|
|
57
|
+
path: ['status'],
|
|
58
|
+
summary:
|
|
59
|
+
'Show the target, its run, the doctor state, the feature, the evidence folder and cursors',
|
|
60
|
+
args: z.strictObject({}),
|
|
61
|
+
data: statusDataSchema,
|
|
62
|
+
effect: 'read',
|
|
63
|
+
needsRun: false,
|
|
64
|
+
needsDoctor: false,
|
|
65
|
+
run: async (_args, ctx) => {
|
|
66
|
+
const paths = ctx.run === undefined ? undefined : buildRunPaths(ctx.stateDir, ctx.run.runID);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
target: ctx.targetName,
|
|
70
|
+
mode: ctx.target.mode,
|
|
71
|
+
access: ctx.target.access,
|
|
72
|
+
instance: ctx.run?.runID ?? null,
|
|
73
|
+
status: ctx.run?.status ?? 'none',
|
|
74
|
+
doctor: ctx.run?.doctor ?? 'none',
|
|
75
|
+
feature: ctx.feature,
|
|
76
|
+
evidence:
|
|
77
|
+
paths === undefined
|
|
78
|
+
? null
|
|
79
|
+
: relative(ctx.session.root, join(paths.features, ctx.feature ?? unassignedFeature)),
|
|
80
|
+
endpoints: ctx.run?.endpoints ?? {},
|
|
81
|
+
services: [...(ctx.run?.services ?? [])],
|
|
82
|
+
cursors: ctx.run === undefined ? {} : await ctx.state.readCursors(ctx.run.runID),
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const use: BuiltinCommandSpec<UseArgs, UseData> = {
|
|
88
|
+
path: ['use'],
|
|
89
|
+
summary: 'Set the current feature or target for later commands',
|
|
90
|
+
args: useArgsSchema,
|
|
91
|
+
data: useDataSchema,
|
|
92
|
+
effect: 'read',
|
|
93
|
+
needsRun: false,
|
|
94
|
+
needsDoctor: false,
|
|
95
|
+
run: async (args, ctx) => {
|
|
96
|
+
if (
|
|
97
|
+
args.feature !== undefined &&
|
|
98
|
+
!ctx.wringer.features.some((feature) => feature.name === args.feature)
|
|
99
|
+
) {
|
|
100
|
+
throw new WringerError('usage', {
|
|
101
|
+
message: `The feature map has no feature named ${args.feature}.`,
|
|
102
|
+
hint: 'Run `features` to list them.',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (args.target !== undefined && !ctx.wringer.targets.has(args.target)) {
|
|
107
|
+
throw new WringerError('usage', {
|
|
108
|
+
message: `The repo has no target named ${args.target}.`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The cursors belong to the target the pointers name after this command, which `--target`
|
|
113
|
+
// may change.
|
|
114
|
+
const before = await ctx.state.readPointers();
|
|
115
|
+
|
|
116
|
+
const runID = findOwnValue(before.currentRun, args.target ?? ctx.targetName);
|
|
117
|
+
const cursors = runID === undefined ? {} : await ctx.state.readCursors(runID);
|
|
118
|
+
|
|
119
|
+
const pointers = await ctx.state.writePointers((current) =>
|
|
120
|
+
buildUsePointers(current, {
|
|
121
|
+
target: args.target,
|
|
122
|
+
feature: args.feature,
|
|
123
|
+
cursors,
|
|
124
|
+
runID,
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return { target: pointers.target ?? null, feature: pointers.feature ?? null, cursors };
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const features: BuiltinCommandSpec<Record<string, never>, FeaturesData> = {
|
|
133
|
+
path: ['features'],
|
|
134
|
+
summary: "List the repo's feature map, with each feature's paths",
|
|
135
|
+
args: z.strictObject({}),
|
|
136
|
+
data: featuresDataSchema,
|
|
137
|
+
effect: 'read',
|
|
138
|
+
needsRun: false,
|
|
139
|
+
needsDoctor: false,
|
|
140
|
+
run: (_args, ctx) =>
|
|
141
|
+
Promise.resolve({
|
|
142
|
+
features: ctx.wringer.features.map((feature) => ({
|
|
143
|
+
name: feature.name,
|
|
144
|
+
summary: feature.summary,
|
|
145
|
+
paths: [...feature.paths],
|
|
146
|
+
current: feature.name === ctx.feature,
|
|
147
|
+
})),
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return [buildBuiltinCommand(status), buildBuiltinCommand(use), buildBuiltinCommand(features)];
|
|
152
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Pointers } from '../create-file-state.ts';
|
|
2
|
+
import type { RunID } from '../run-events.ts';
|
|
3
|
+
|
|
4
|
+
export interface UseChange {
|
|
5
|
+
readonly target: string | undefined;
|
|
6
|
+
readonly feature: string | undefined;
|
|
7
|
+
|
|
8
|
+
// The cursors of the run the pointers name after the change, or none when it names no run.
|
|
9
|
+
readonly cursors: Readonly<Record<string, number>>;
|
|
10
|
+
readonly runID: RunID | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// `use` keeps each pointer it is not given, and replaces the cursors with those of the run the
|
|
14
|
+
// new target points at.
|
|
15
|
+
export function buildUsePointers(current: Pointers, change: UseChange): Pointers {
|
|
16
|
+
const target = change.target ?? current.target;
|
|
17
|
+
const feature = change.feature ?? current.feature;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
currentRun: current.currentRun,
|
|
21
|
+
...(target === undefined ? {} : { target }),
|
|
22
|
+
...(feature === undefined ? {} : { feature }),
|
|
23
|
+
cursors: { ...change.cursors },
|
|
24
|
+
...(change.runID === undefined ? {} : { cursorsRun: change.runID }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { isAbsolute, join } from 'node:path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { buildBuiltinCommand } from '../build-commands.ts';
|
|
4
|
+
import { buildReport } from '../build-report.ts';
|
|
5
|
+
import type { Evidence } from '../envelope.ts';
|
|
6
|
+
import { parseReportBlock } from '../parse-report-block.ts';
|
|
7
|
+
import { readChangedPaths } from '../read-changed-paths.ts';
|
|
8
|
+
import { readDefaultBase } from '../read-default-base.ts';
|
|
9
|
+
import { readFingerprint } from '../read-fingerprint.ts';
|
|
10
|
+
import { doneCommand, readVerdicts } from '../read-verdicts.ts';
|
|
11
|
+
import { renderReportMarkdown } from '../render-report-markdown.ts';
|
|
12
|
+
import { reportSchema, verdictSchema } from '../report-schema.ts';
|
|
13
|
+
import type { Report, Verdict } from '../report-schema.ts';
|
|
14
|
+
import type { BuiltinCommandSpec, BuiltinContext, RegisteredCommand } from '../types.ts';
|
|
15
|
+
import { WringerError } from '../wringer-error.ts';
|
|
16
|
+
|
|
17
|
+
const doneArgsSchema = z.strictObject({
|
|
18
|
+
feature: z.string().min(1).optional(),
|
|
19
|
+
base: z.string().min(1).optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
type DoneArgs = z.infer<typeof doneArgsSchema>;
|
|
23
|
+
|
|
24
|
+
const reportArgsSchema = z.strictObject({ base: z.string().min(1).optional() });
|
|
25
|
+
|
|
26
|
+
type ReportArgs = z.infer<typeof reportArgsSchema>;
|
|
27
|
+
|
|
28
|
+
const reportDataSchema = z.strictObject({ report: reportSchema, markdown: z.string() });
|
|
29
|
+
|
|
30
|
+
type ReportData = z.infer<typeof reportDataSchema>;
|
|
31
|
+
|
|
32
|
+
const gateArgsSchema = z.strictObject({ base: z.string().min(1), bodyFile: z.string().min(1) });
|
|
33
|
+
|
|
34
|
+
type GateArgs = z.infer<typeof gateArgsSchema>;
|
|
35
|
+
|
|
36
|
+
const gateFailureSchema = z.strictObject({ name: z.string(), reason: z.string() });
|
|
37
|
+
|
|
38
|
+
const gateDataSchema = z.strictObject({
|
|
39
|
+
fingerprint: z.string(),
|
|
40
|
+
failing: z.array(gateFailureSchema),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
type GateData = z.infer<typeof gateDataSchema>;
|
|
44
|
+
|
|
45
|
+
// `done` turns the evidence of one run into a verdict, `report` merges every verdict of the worktree
|
|
46
|
+
// for the PR body, and `gate` checks that report again in CI, with a fingerprint it computes itself.
|
|
47
|
+
export function buildVerdictCommands(): readonly RegisteredCommand[] {
|
|
48
|
+
const done: BuiltinCommandSpec<DoneArgs, Verdict> = {
|
|
49
|
+
path: [doneCommand],
|
|
50
|
+
summary:
|
|
51
|
+
'Record a verdict for a feature from its commands that touched the app since the doctor passed',
|
|
52
|
+
args: doneArgsSchema,
|
|
53
|
+
data: verdictSchema,
|
|
54
|
+
effect: 'read',
|
|
55
|
+
needsRun: true,
|
|
56
|
+
needsDoctor: false,
|
|
57
|
+
run: (args, ctx) => recordVerdict(args, ctx),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const report: BuiltinCommandSpec<ReportArgs, ReportData> = {
|
|
61
|
+
path: ['report'],
|
|
62
|
+
summary: 'Merge the verdicts into a report: JSON for a gate, Markdown with --human for the PR',
|
|
63
|
+
args: reportArgsSchema,
|
|
64
|
+
data: reportDataSchema,
|
|
65
|
+
effect: 'read',
|
|
66
|
+
needsRun: false,
|
|
67
|
+
needsDoctor: false,
|
|
68
|
+
run: async (args, ctx) => {
|
|
69
|
+
const built = await readReport(ctx, args.base);
|
|
70
|
+
const linked = await linkEvidence(ctx, built);
|
|
71
|
+
|
|
72
|
+
return { report: linked, markdown: renderReportMarkdown(linked) };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const gate: BuiltinCommandSpec<GateArgs, GateData> = {
|
|
77
|
+
path: ['gate'],
|
|
78
|
+
summary:
|
|
79
|
+
'Fail when a changed feature lacks a verdict at the current fingerprint in the PR body',
|
|
80
|
+
args: gateArgsSchema,
|
|
81
|
+
data: gateDataSchema,
|
|
82
|
+
effect: 'read',
|
|
83
|
+
needsRun: false,
|
|
84
|
+
needsDoctor: false,
|
|
85
|
+
run: (args, ctx) => checkGate(args, ctx),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return [buildBuiltinCommand(done), buildBuiltinCommand(report), buildBuiltinCommand(gate)];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function recordVerdict(args: DoneArgs, ctx: BuiltinContext): Promise<Verdict> {
|
|
92
|
+
const feature = args.feature ?? ctx.feature;
|
|
93
|
+
|
|
94
|
+
if (feature === null) {
|
|
95
|
+
throw new WringerError('usage', {
|
|
96
|
+
message: 'No feature is set.',
|
|
97
|
+
hint: 'Pass `--feature <name>`, or run `use --feature <name>` first.',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (ctx.run?.doctor !== 'passed') {
|
|
102
|
+
throw new WringerError('doctor-required', {
|
|
103
|
+
message: `The doctor has not passed on run ${ctx.run?.runID ?? 'none'}.`,
|
|
104
|
+
hint: 'Run `doctor`, then drive the feature again.',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The run took its fingerprint against the default base, so the tree check uses that base; the
|
|
109
|
+
// verdict takes the base the PR's gate will use.
|
|
110
|
+
const defaultBase = await readDefaultBase(ctx.session.repoHost, ctx.session.root);
|
|
111
|
+
const treeFingerprint = await readCurrentFingerprint(ctx, defaultBase);
|
|
112
|
+
|
|
113
|
+
if (ctx.target.mode === 'launch' && treeFingerprint !== ctx.run.fingerprint) {
|
|
114
|
+
throw new WringerError('doctor-required', {
|
|
115
|
+
message: 'The working tree changed since this run started.',
|
|
116
|
+
hint: 'Run `up --restart` and `doctor`, then drive the feature again.',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const drives = ctx.run.drivesAfterDoctor.filter((envelope) => envelope.feature === feature);
|
|
121
|
+
|
|
122
|
+
if (drives.length === 0) {
|
|
123
|
+
throw new WringerError('check-failed', {
|
|
124
|
+
message: `No command of ${feature} that touches the app passed on run ${ctx.run.runID} since the doctor passed.`,
|
|
125
|
+
hint: `Drive the feature with \`--feature ${feature}\`, then run \`done\` again.`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const fingerprint =
|
|
130
|
+
args.base === undefined || args.base === defaultBase
|
|
131
|
+
? treeFingerprint
|
|
132
|
+
: await readCurrentFingerprint(ctx, args.base);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
feature,
|
|
136
|
+
fingerprint,
|
|
137
|
+
run: ctx.run.runID,
|
|
138
|
+
at: ctx.now().toISOString(),
|
|
139
|
+
evidence: drives.flatMap((envelope) => envelope.evidence),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readCurrentFingerprint(ctx: BuiltinContext, base: string): Promise<string> {
|
|
144
|
+
return readFingerprint(ctx.session.repoHost, {
|
|
145
|
+
cwd: ctx.session.root,
|
|
146
|
+
base,
|
|
147
|
+
exclude: [ctx.wringer.stateDir],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readReport(ctx: BuiltinContext, givenBase: string | undefined): Promise<Report> {
|
|
152
|
+
const base = await readBase(ctx, givenBase);
|
|
153
|
+
|
|
154
|
+
const [fingerprint, changedPaths, verdicts] = await Promise.all([
|
|
155
|
+
readCurrentFingerprint(ctx, base),
|
|
156
|
+
readChangedPaths(ctx.session.repoHost, {
|
|
157
|
+
cwd: ctx.session.root,
|
|
158
|
+
base,
|
|
159
|
+
exclude: [ctx.wringer.stateDir],
|
|
160
|
+
}),
|
|
161
|
+
readVerdicts(ctx.state, ctx.session.repoHost, ctx.session.root),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
return buildReport({ base, fingerprint, features: ctx.wringer.features, changedPaths, verdicts });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readBase(ctx: BuiltinContext, givenBase: string | undefined): Promise<string> {
|
|
168
|
+
if (givenBase !== undefined) {
|
|
169
|
+
return Promise.resolve(givenBase);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return readDefaultBase(ctx.session.repoHost, ctx.session.root);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The repo's upload hook turns each local evidence file into a link; the local files never leave
|
|
176
|
+
// the worktree otherwise.
|
|
177
|
+
async function linkEvidence(ctx: BuiltinContext, report: Report): Promise<Report> {
|
|
178
|
+
const upload = ctx.wringer.hooks.upload;
|
|
179
|
+
|
|
180
|
+
if (upload === undefined) {
|
|
181
|
+
return report;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const link = async (evidence: Evidence): Promise<Evidence & { readonly url?: string }> => ({
|
|
185
|
+
...evidence,
|
|
186
|
+
url: await upload({ ...evidence, path: join(ctx.session.root, evidence.path) }),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const features = await Promise.all(
|
|
190
|
+
report.features.map(async (feature) => {
|
|
191
|
+
if (feature.verdict === null || feature.status !== 'verified') {
|
|
192
|
+
return feature;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const evidence = await Promise.all(feature.verdict.evidence.map((item) => link(item)));
|
|
196
|
+
|
|
197
|
+
return { ...feature, verdict: { ...feature.verdict, evidence } };
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return { ...report, features };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function checkGate(args: GateArgs, ctx: BuiltinContext): Promise<GateData> {
|
|
205
|
+
const bodyPath = isAbsolute(args.bodyFile)
|
|
206
|
+
? args.bodyFile
|
|
207
|
+
: join(ctx.session.root, args.bodyFile);
|
|
208
|
+
|
|
209
|
+
const body = await ctx.session.repoHost.readText(bodyPath);
|
|
210
|
+
|
|
211
|
+
const block = parseReportBlock(body);
|
|
212
|
+
|
|
213
|
+
const [fingerprint, changedPaths] = await Promise.all([
|
|
214
|
+
readCurrentFingerprint(ctx, args.base),
|
|
215
|
+
readChangedPaths(ctx.session.repoHost, {
|
|
216
|
+
cwd: ctx.session.root,
|
|
217
|
+
base: args.base,
|
|
218
|
+
exclude: [ctx.wringer.stateDir],
|
|
219
|
+
}),
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const claimed = block.ok
|
|
223
|
+
? block.report.features.flatMap((feature) =>
|
|
224
|
+
feature.verdict === null ? [] : [[feature.name, feature.verdict] as const],
|
|
225
|
+
)
|
|
226
|
+
: [];
|
|
227
|
+
|
|
228
|
+
const verdicts = new Map(claimed);
|
|
229
|
+
|
|
230
|
+
const computed = buildReport({
|
|
231
|
+
base: args.base,
|
|
232
|
+
fingerprint,
|
|
233
|
+
features: ctx.wringer.features,
|
|
234
|
+
changedPaths,
|
|
235
|
+
verdicts,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const failing = computed.features.flatMap((feature) => {
|
|
239
|
+
if (feature.status === 'missing') {
|
|
240
|
+
return [{ name: feature.name, reason: block.ok ? 'no verdict in the report' : block.reason }];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (feature.status === 'stale') {
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
name: feature.name,
|
|
247
|
+
reason: `verdict at ${feature.verdict?.fingerprint ?? ''}, tree at ${fingerprint}`,
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return [];
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const data = { fingerprint, failing };
|
|
256
|
+
|
|
257
|
+
if (failing.length > 0) {
|
|
258
|
+
throw new WringerError('check-failed', {
|
|
259
|
+
message: `${failing.length} changed feature(s) lack a verdict at the current fingerprint: ${failing.map((feature) => feature.name).join(', ')}.`,
|
|
260
|
+
hint: 'Drive each feature, run `done` and `report --human`, and update the PR body.',
|
|
261
|
+
data,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return data;
|
|
266
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RunView } from '../build-run-view.ts';
|
|
2
|
+
import { supervisorServiceName } from '../supervisor-protocol.ts';
|
|
3
|
+
import type { SupervisorPort } from '../types.ts';
|
|
4
|
+
import { WringerError } from '../wringer-error.ts';
|
|
5
|
+
|
|
6
|
+
export type SupervisorReach = 'answered' | 'gone';
|
|
7
|
+
|
|
8
|
+
// `gone` only when nothing listens on the run's socket, or the run never got one. A call that times
|
|
9
|
+
// out or fails in any other way may still reach a live supervisor later, so it rejects, and the
|
|
10
|
+
// caller leaves the run as it is.
|
|
11
|
+
export async function callRunSupervisor(
|
|
12
|
+
supervisor: SupervisorPort,
|
|
13
|
+
run: RunView,
|
|
14
|
+
method: 'ping' | 'stop',
|
|
15
|
+
signal: AbortSignal,
|
|
16
|
+
): Promise<SupervisorReach> {
|
|
17
|
+
if (run.supervisor === null) {
|
|
18
|
+
return 'gone';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await supervisor.call({
|
|
23
|
+
socket: run.supervisor.socket,
|
|
24
|
+
request: { service: supervisorServiceName, method, args: null },
|
|
25
|
+
signal,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return 'answered';
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (isSupervisorGone(error)) {
|
|
31
|
+
return 'gone';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isSupervisorGone(error: unknown): boolean {
|
|
39
|
+
return error instanceof WringerError && error.code === 'not-running';
|
|
40
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CheckResult, RunEvent } from '../run-events.ts';
|
|
2
|
+
import { WringerError } from '../wringer-error.ts';
|
|
3
|
+
|
|
4
|
+
interface DoctorData {
|
|
5
|
+
readonly passed: boolean;
|
|
6
|
+
readonly results: CheckResult[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DoctorOutcome {
|
|
10
|
+
readonly event: RunEvent;
|
|
11
|
+
readonly data: DoctorData;
|
|
12
|
+
|
|
13
|
+
// Thrown after the event is written, so a failed doctor still lands in the run's log.
|
|
14
|
+
readonly failure: WringerError | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function pickDoctorOutcome(results: readonly CheckResult[], at: string): DoctorOutcome {
|
|
18
|
+
const passed = results.every((result) => result.status === 'passed');
|
|
19
|
+
const data = { passed, results: [...results] };
|
|
20
|
+
const failed = results.filter((result) => result.status === 'failed');
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
event: { type: passed ? 'doctor-passed' : 'doctor-failed', at, results: data.results },
|
|
24
|
+
data,
|
|
25
|
+
failure: passed
|
|
26
|
+
? null
|
|
27
|
+
: new WringerError('check-failed', {
|
|
28
|
+
message: `The doctor failed: ${failed.map((result) => result.name).join(', ')}.`,
|
|
29
|
+
data,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RunStatus } from '../append-run-event.ts';
|
|
2
|
+
import type { SupervisorReach } from './call-run-supervisor.ts';
|
|
3
|
+
|
|
4
|
+
// What `up` has learned so far: the current run's status, and the answers of each call it made.
|
|
5
|
+
export interface StartFacts {
|
|
6
|
+
readonly status: RunStatus | null;
|
|
7
|
+
readonly restart: boolean;
|
|
8
|
+
readonly ping: SupervisorReach | null;
|
|
9
|
+
readonly stop: SupervisorReach | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type StartAction = 'ping' | 'stop' | 'reuse' | 'mark-crashed' | 'start';
|
|
13
|
+
|
|
14
|
+
// `up` calls this after each step until it reuses the run or starts a new one. A run still
|
|
15
|
+
// starting is one an earlier `up` gave up waiting for; it is replaced, never reused, so its app and
|
|
16
|
+
// services are stopped first. A run whose supervisor is gone is marked crashed before the new start.
|
|
17
|
+
export function pickStartAction(facts: StartFacts): StartAction {
|
|
18
|
+
if (facts.status !== 'running' && facts.status !== 'starting') {
|
|
19
|
+
return 'start';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (facts.ping === null) {
|
|
23
|
+
return 'ping';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (facts.ping === 'gone' || facts.stop === 'gone') {
|
|
27
|
+
return 'mark-crashed';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (facts.status === 'running' && !facts.restart) {
|
|
31
|
+
return 'reuse';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return facts.stop === null ? 'stop' : 'start';
|
|
35
|
+
}
|