@bridge_gpt/mcp-server 0.2.36 → 0.2.38
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/README.md +48 -8
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-user-config-doctor.js +317 -0
- package/build/commands.generated.js +2 -1
- package/build/conductor/bridge-api-client.js +178 -4
- package/build/conductor-bin.js +1 -1
- package/build/conductor-bundle-artifacts.js +7 -6
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +239 -80
- package/build/executor/cli.js +51 -1
- package/build/executor/credentials.js +1 -7
- package/build/executor/deps.js +18 -1
- package/build/executor/env.js +51 -25
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +422 -22
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +104 -8
- package/build/executor/preflight.js +32 -0
- package/build/executor/runner.js +8 -0
- package/build/executor/test-clock.js +67 -3
- package/build/executor/types.js +4 -1
- package/build/executor/worker-command.js +11 -3
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +614 -244
- package/build/init.js +363 -73
- package/build/install-bridge.js +568 -80
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +30 -7
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +22 -9
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -0
- package/build/setup-epic.js +662 -27
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +70 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-body.js +92 -0
- package/build/sfcc/read-projection.js +185 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +57 -34
- package/build/sfcc/reads-site-preference.js +86 -33
- package/build/sfcc/reads-system-object.js +50 -38
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +71 -15
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-custom-object-def.js +6 -2
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +417 -101
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +23 -8
- package/package.json +3 -3
- package/pipelines/implement-ticket.json +15 -5
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plane wind-down (BAPI-756, AC-6).
|
|
3
|
+
*
|
|
4
|
+
* One primitive serves both attended Ctrl-C and a separate `plane down`
|
|
5
|
+
* invocation, so the two paths cannot drift into different escalation
|
|
6
|
+
* semantics.
|
|
7
|
+
*
|
|
8
|
+
* The honest caveat this compensates for: a SIGKILLed supervisor cannot kill
|
|
9
|
+
* its own group at that instant. Nothing can fix that in-process — so the
|
|
10
|
+
* manifest exists, `plane down` reads it from a *different* process, and the
|
|
11
|
+
* recorded process group is swept from there. That is the designed
|
|
12
|
+
* compensation, not a fallback.
|
|
13
|
+
*
|
|
14
|
+
* Two safety rules govern every branch:
|
|
15
|
+
* - **Never signal an unvalidated record.** A malformed manifest is reported,
|
|
16
|
+
* not acted on. A kill list is the last thing that should be parsed loosely.
|
|
17
|
+
* - **Never clear on an unconfirmed death.** If a survivor remains, or its
|
|
18
|
+
* liveness is unknown, the manifest is retained so a later `plane down` can
|
|
19
|
+
* retry. Clearing it would orphan the survivor permanently.
|
|
20
|
+
*/
|
|
21
|
+
import { clearPlaneManifest, probeManifestMembers, readPlaneManifest, } from "./manifest.js";
|
|
22
|
+
/** SIGTERM→SIGKILL grace period. */
|
|
23
|
+
export const PLANE_SHUTDOWN_GRACE_MS = 10_000;
|
|
24
|
+
/** How long stragglers get after SIGKILL before they are declared unknown. */
|
|
25
|
+
export const PLANE_SHUTDOWN_KILL_WAIT_MS = 3_000;
|
|
26
|
+
/** Liveness poll interval during both waits. */
|
|
27
|
+
export const PLANE_SHUTDOWN_POLL_MS = 250;
|
|
28
|
+
/**
|
|
29
|
+
* Signal the recorded process group, escalate to SIGKILL, and clear the
|
|
30
|
+
* manifest only once every recorded process is confirmed dead.
|
|
31
|
+
*/
|
|
32
|
+
export async function shutdownPlane(repoRoot, deps) {
|
|
33
|
+
const graceMs = deps.graceMs ?? PLANE_SHUTDOWN_GRACE_MS;
|
|
34
|
+
const killWaitMs = deps.killWaitMs ?? PLANE_SHUTDOWN_KILL_WAIT_MS;
|
|
35
|
+
const pollMs = deps.pollIntervalMs ?? PLANE_SHUTDOWN_POLL_MS;
|
|
36
|
+
const read = await readPlaneManifest(repoRoot, deps.fs);
|
|
37
|
+
if (read.kind === "missing") {
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
outcome: "no-plane",
|
|
41
|
+
escalated: false,
|
|
42
|
+
manifestCleared: false,
|
|
43
|
+
members: [],
|
|
44
|
+
messages: ["No plane manifest found — nothing to wind down."],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (read.kind !== "valid") {
|
|
48
|
+
// Deliberately before any liveness probe: an unvalidated record must never
|
|
49
|
+
// become a signal target, not even a signal-zero one.
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: "unvalidated-manifest",
|
|
53
|
+
message: `.bridge/plane/plane.json could not be validated (${read.error}). No process was ` +
|
|
54
|
+
"signalled. Inspect the file and remove it by hand once you have confirmed no plane " +
|
|
55
|
+
"is running.",
|
|
56
|
+
members: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const manifest = read.manifest;
|
|
60
|
+
const messages = [];
|
|
61
|
+
if (!isAnythingAlive(manifest, deps.proc, deps.selfPid)) {
|
|
62
|
+
const cleared = await clearPlaneManifest(repoRoot, manifest.planeId, deps.fs);
|
|
63
|
+
if (!cleared.ok) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: "clear-failed",
|
|
67
|
+
message: cleared.message,
|
|
68
|
+
members: reportMembers(manifest, deps.proc),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
outcome: "already-dead",
|
|
74
|
+
escalated: false,
|
|
75
|
+
manifestCleared: cleared.removed,
|
|
76
|
+
members: reportMembers(manifest, deps.proc),
|
|
77
|
+
messages: ["Every recorded plane process was already gone; manifest cleared."],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
messages.push(deps.selfPid === undefined
|
|
81
|
+
? `Sending SIGTERM to plane process group ${manifest.supervisorPgid}.`
|
|
82
|
+
: "Sending SIGTERM to every plane member.");
|
|
83
|
+
signalPlane(manifest, "SIGTERM", deps.proc, deps.selfPid);
|
|
84
|
+
await waitForDeath(manifest, deps, graceMs, pollMs);
|
|
85
|
+
let escalated = false;
|
|
86
|
+
if (isAnythingAlive(manifest, deps.proc, deps.selfPid)) {
|
|
87
|
+
escalated = true;
|
|
88
|
+
messages.push("Grace period elapsed with survivors — escalating to SIGKILL.");
|
|
89
|
+
signalPlane(manifest, "SIGKILL", deps.proc, deps.selfPid);
|
|
90
|
+
await waitForDeath(manifest, deps, killWaitMs, pollMs);
|
|
91
|
+
}
|
|
92
|
+
const members = reportMembers(manifest, deps.proc);
|
|
93
|
+
if (isAnythingAlive(manifest, deps.proc, deps.selfPid)) {
|
|
94
|
+
const survivors = members
|
|
95
|
+
.filter((m) => m.liveness !== "dead")
|
|
96
|
+
.map((m) => `${m.name}(pid ${m.pid ?? "?"}, ${m.liveness})`);
|
|
97
|
+
if (deps.selfPid !== manifest.supervisorPid &&
|
|
98
|
+
deps.proc.isAlive(manifest.supervisorPid) !== "dead") {
|
|
99
|
+
survivors.push(`supervisor(pid ${manifest.supervisorPid})`);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
reason: "survivors",
|
|
104
|
+
message: `wind-down could not confirm every process was terminated: ${survivors.join(", ")}. ` +
|
|
105
|
+
"The manifest was RETAINED so `plane down` can be retried.",
|
|
106
|
+
members,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const cleared = await clearPlaneManifest(repoRoot, manifest.planeId, deps.fs);
|
|
110
|
+
if (!cleared.ok) {
|
|
111
|
+
return { ok: false, reason: "clear-failed", message: cleared.message, members };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
outcome: "terminated",
|
|
116
|
+
escalated,
|
|
117
|
+
manifestCleared: cleared.removed,
|
|
118
|
+
members,
|
|
119
|
+
messages,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Deliver a signal to the whole plane.
|
|
124
|
+
*
|
|
125
|
+
* The process group is addressed first — that is what reaches members whose own
|
|
126
|
+
* pid the manifest may not have recorded yet. Individual pids are then signalled
|
|
127
|
+
* as a backstop for the platform case where group delivery is unavailable; a
|
|
128
|
+
* signal to an already-dead pid is a no-op, so the belt-and-braces costs nothing.
|
|
129
|
+
*/
|
|
130
|
+
function signalPlane(manifest, signal, proc, selfPid) {
|
|
131
|
+
// The group signal is what reaches a member whose pid the manifest may not
|
|
132
|
+
// have recorded yet — but it also reaches the supervisor, so it is skipped
|
|
133
|
+
// when the supervisor is the one calling. Individual member pids are then the
|
|
134
|
+
// complete target set, because the runtime records each pid immediately after
|
|
135
|
+
// the spawn that produced it.
|
|
136
|
+
if (selfPid === undefined) {
|
|
137
|
+
proc.signal(-manifest.supervisorPgid, signal);
|
|
138
|
+
proc.signal(manifest.supervisorPid, signal);
|
|
139
|
+
}
|
|
140
|
+
for (const member of manifest.members) {
|
|
141
|
+
if (member.pid === null || member.pid === selfPid)
|
|
142
|
+
continue;
|
|
143
|
+
if (member.state === "exited")
|
|
144
|
+
continue;
|
|
145
|
+
proc.signal(member.pid, signal);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function isAnythingAlive(manifest, proc, selfPid) {
|
|
149
|
+
if (manifest.supervisorPid !== selfPid &&
|
|
150
|
+
proc.isAlive(manifest.supervisorPid) !== "dead") {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return probeManifestMembers(manifest, proc).some((probe) => probe.member.pid !== selfPid && probe.liveness !== "dead");
|
|
154
|
+
}
|
|
155
|
+
function reportMembers(manifest, proc) {
|
|
156
|
+
return probeManifestMembers(manifest, proc).map((probe) => ({
|
|
157
|
+
name: probe.member.name,
|
|
158
|
+
pid: probe.member.pid,
|
|
159
|
+
liveness: probe.liveness,
|
|
160
|
+
exitCode: probe.member.exitCode,
|
|
161
|
+
exitSignal: probe.member.exitSignal,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
/** Poll liveness on the injected clock until everything is dead or time runs out. */
|
|
165
|
+
async function waitForDeath(manifest, deps, budgetMs, pollMs) {
|
|
166
|
+
let waited = 0;
|
|
167
|
+
while (waited < budgetMs) {
|
|
168
|
+
if (!isAnythingAlive(manifest, deps.proc, deps.selfPid))
|
|
169
|
+
return;
|
|
170
|
+
await deps.clock.sleep(pollMs);
|
|
171
|
+
waited += pollMs;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Render a wind-down result for the terminal. */
|
|
175
|
+
export function formatPlaneShutdown(result) {
|
|
176
|
+
const lines = [];
|
|
177
|
+
if (!result.ok) {
|
|
178
|
+
lines.push(`plane down FAILED: ${result.message}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
for (const message of result.messages)
|
|
182
|
+
lines.push(message);
|
|
183
|
+
}
|
|
184
|
+
for (const member of result.members) {
|
|
185
|
+
const detail = member.exitSignal !== null
|
|
186
|
+
? `signal ${member.exitSignal}`
|
|
187
|
+
: member.exitCode !== null
|
|
188
|
+
? `exit ${member.exitCode}`
|
|
189
|
+
: "no recorded exit status";
|
|
190
|
+
lines.push(` ${member.name.padEnd(12)} ${member.liveness.padEnd(7)} (${detail})`);
|
|
191
|
+
}
|
|
192
|
+
if (result.ok && result.manifestCleared)
|
|
193
|
+
lines.push("Manifest cleared.");
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `plane status` — read-only inspection (BAPI-756).
|
|
3
|
+
*
|
|
4
|
+
* Status runs in a different process from the supervisor, so it reads the
|
|
5
|
+
* manifest and probes pids. It never signals, never writes, and never repairs:
|
|
6
|
+
* a hole in the roster (a member that crashed and was deliberately not
|
|
7
|
+
* restarted) is precisely what this command exists to *show*.
|
|
8
|
+
*/
|
|
9
|
+
import { probeManifestMembers, readPlaneManifest, } from "./manifest.js";
|
|
10
|
+
import { PLANE_RUNTIME_DIR, PLANE_RUNTIME_LOG_PATH, } from "./types.js";
|
|
11
|
+
/** Read the manifest and probe every recorded pid. Strictly read-only. */
|
|
12
|
+
export async function getPlaneStatus(repoRoot, deps) {
|
|
13
|
+
const read = await readPlaneManifest(repoRoot, deps.fs);
|
|
14
|
+
if (read.kind === "missing")
|
|
15
|
+
return { ok: true, kind: "no-plane" };
|
|
16
|
+
if (read.kind !== "valid") {
|
|
17
|
+
// No liveness probe and no signal: an unvalidated manifest yields no pid a
|
|
18
|
+
// caller could act on.
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
error: `.bridge/plane/plane.json could not be validated (${read.error}). No process was ` +
|
|
22
|
+
"probed. Inspect the file and remove it by hand once you have confirmed no plane " +
|
|
23
|
+
"is running.",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const manifest = read.manifest;
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
kind: "plane",
|
|
30
|
+
planeId: manifest.planeId,
|
|
31
|
+
supervisorPid: manifest.supervisorPid,
|
|
32
|
+
supervisorPgid: manifest.supervisorPgid,
|
|
33
|
+
supervisorLiveness: deps.proc.isAlive(manifest.supervisorPid),
|
|
34
|
+
createdAt: manifest.createdAt,
|
|
35
|
+
updatedAt: manifest.updatedAt,
|
|
36
|
+
members: probeManifestMembers(manifest, deps.proc).map((probe) => ({
|
|
37
|
+
name: probe.member.name,
|
|
38
|
+
pid: probe.member.pid,
|
|
39
|
+
state: probe.member.state,
|
|
40
|
+
liveness: probe.liveness,
|
|
41
|
+
exitCode: probe.member.exitCode,
|
|
42
|
+
exitSignal: probe.member.exitSignal,
|
|
43
|
+
logPath: probe.member.logPath,
|
|
44
|
+
})),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** One line per member: name, state, exit status where known, log path. */
|
|
48
|
+
export function formatPlaneStatus(result) {
|
|
49
|
+
if (!result.ok)
|
|
50
|
+
return `plane status FAILED: ${result.error}`;
|
|
51
|
+
if (result.kind === "no-plane") {
|
|
52
|
+
return `No plane is running (no ${PLANE_RUNTIME_DIR}/plane.json).`;
|
|
53
|
+
}
|
|
54
|
+
const lines = [
|
|
55
|
+
`Plane ${result.planeId}`,
|
|
56
|
+
` supervisor pid ${result.supervisorPid} (group ${result.supervisorPgid}) — ${result.supervisorLiveness}`,
|
|
57
|
+
` started ${result.createdAt}`,
|
|
58
|
+
` updated ${result.updatedAt}`,
|
|
59
|
+
"",
|
|
60
|
+
];
|
|
61
|
+
for (const member of result.members) {
|
|
62
|
+
lines.push(` ${member.name.padEnd(12)} ${describeMemberState(member, result.supervisorLiveness)} ` +
|
|
63
|
+
`${member.logPath}`);
|
|
64
|
+
}
|
|
65
|
+
const neverStarted = result.members.filter((m) => isPreStart(m) && result.supervisorLiveness === "dead");
|
|
66
|
+
if (neverStarted.length > 0) {
|
|
67
|
+
// These members have no log to read — pointing the operator at an empty file
|
|
68
|
+
// is what made BAPI-768 look like "nothing happened" instead of "the runtime
|
|
69
|
+
// died before it could spawn anything". The runtime trace is the evidence.
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push(`${neverStarted.length} member(s) were NEVER STARTED: the supervisor died before it ` +
|
|
72
|
+
"reached them, so their log files above may be empty or missing entirely.");
|
|
73
|
+
lines.push(`Read the runtime trace instead: ${PLANE_RUNTIME_LOG_PATH}`);
|
|
74
|
+
}
|
|
75
|
+
// Members that genuinely ran and exited keep the original no-restart warning;
|
|
76
|
+
// one that never started was not restarted for an entirely different reason,
|
|
77
|
+
// and telling the operator to "read the log above" would be a dead end.
|
|
78
|
+
const holes = result.members.filter((m) => m.liveness !== "alive" && !neverStarted.includes(m));
|
|
79
|
+
if (holes.length > 0) {
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push(`${holes.length} member(s) are not running. They are NOT restarted automatically — ` +
|
|
82
|
+
"read the log(s) above, then `plane down` and `plane up` when you have a fix.");
|
|
83
|
+
}
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* True for a member the manifest still records as pre-spawn.
|
|
88
|
+
*
|
|
89
|
+
* `state: "spawning"` with no pid is written by the runtime up front, for the
|
|
90
|
+
* whole roster, before it starts anything. So it does not mean "this member
|
|
91
|
+
* exited without a status" — it means the runtime never got this far.
|
|
92
|
+
*/
|
|
93
|
+
function isPreStart(member) {
|
|
94
|
+
return member.state === "spawning" && member.pid === null;
|
|
95
|
+
}
|
|
96
|
+
export function describeMemberState(member, supervisorLiveness) {
|
|
97
|
+
// Precedence below is unchanged and load-bearing: a live pid, then a recorded
|
|
98
|
+
// signal, then a recorded code, then unverifiable liveness. The pre-start
|
|
99
|
+
// cases are appended after all four, so they can only ever claim a member
|
|
100
|
+
// about which nothing else is known.
|
|
101
|
+
if (member.liveness === "alive") {
|
|
102
|
+
return `running (pid ${member.pid ?? "?"})`.padEnd(34);
|
|
103
|
+
}
|
|
104
|
+
if (member.exitSignal !== null) {
|
|
105
|
+
return `exited (signal ${member.exitSignal})`.padEnd(34);
|
|
106
|
+
}
|
|
107
|
+
if (member.exitCode !== null) {
|
|
108
|
+
return `exited (code ${member.exitCode})`.padEnd(34);
|
|
109
|
+
}
|
|
110
|
+
if (member.liveness === "unknown") {
|
|
111
|
+
return `unknown (pid ${member.pid ?? "?"}, liveness unverifiable)`.padEnd(34);
|
|
112
|
+
}
|
|
113
|
+
if (isPreStart(member)) {
|
|
114
|
+
if (supervisorLiveness === "alive") {
|
|
115
|
+
return `starting (not yet spawned)`.padEnd(34);
|
|
116
|
+
}
|
|
117
|
+
if (supervisorLiveness === "dead") {
|
|
118
|
+
return `never started`.padEnd(34);
|
|
119
|
+
}
|
|
120
|
+
// The supervisor's own liveness is what separates "still coming up" from
|
|
121
|
+
// "died on the way", so without it neither claim is honest.
|
|
122
|
+
return `unknown (supervisor liveness unverifiable)`.padEnd(34);
|
|
123
|
+
}
|
|
124
|
+
return `exited (no recorded exit status)`.padEnd(34);
|
|
125
|
+
}
|