walover-line-harness-gui 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/server/cli.js +10 -0
- package/src/server/index.js +9 -1
- package/src/server/leftover.js +7 -2
- package/src/server/runner.js +43 -14
- package/src/server/runtime.js +72 -0
- package/src/web/index.html +111 -15
package/package.json
CHANGED
package/src/server/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { start, shutdown } from "./index.js";
|
|
|
3
3
|
import { openBrowser } from "./browser.js";
|
|
4
4
|
import { ptyBackend } from "./pty.js";
|
|
5
5
|
import { getVersionStatus } from "./cli-version.js";
|
|
6
|
+
import { checkNodeRuntime } from "./runtime.js";
|
|
6
7
|
|
|
7
8
|
const { server, url } = await start();
|
|
8
9
|
|
|
@@ -14,6 +15,15 @@ console.log("");
|
|
|
14
15
|
console.log(" ブラウザが開かない場合は、上の URL をコピーして開いてください。");
|
|
15
16
|
console.log(" 終了するには Ctrl+C を押してください。");
|
|
16
17
|
|
|
18
|
+
const runtime = checkNodeRuntime();
|
|
19
|
+
if (!runtime.ok) {
|
|
20
|
+
// 画面を開く前に、ターミナルにも出す。ここで気づければ何も入力せずに済む
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log(` ⚠ ${runtime.problem.title}`);
|
|
23
|
+
console.log(` ${runtime.problem.body}`);
|
|
24
|
+
for (const line of runtime.problem.fix) console.log(` - ${line}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
if (ptyBackend() === null) {
|
|
18
28
|
// 本家 CLI は実際の端末(PTY)を要求する。無いと起動すらできないので先に伝える
|
|
19
29
|
console.log("");
|
package/src/server/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./state.js";
|
|
15
15
|
import { ptyBackend } from "./pty.js";
|
|
16
16
|
import { getVersionStatus } from "./cli-version.js";
|
|
17
|
+
import { checkNodeRuntime } from "./runtime.js";
|
|
17
18
|
import { checkLeftoverState, leftoverWarning } from "./leftover.js";
|
|
18
19
|
|
|
19
20
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -114,7 +115,14 @@ async function route(req, res, pathname, method) {
|
|
|
114
115
|
|
|
115
116
|
if (pathname === "/api/health" && method === "GET") {
|
|
116
117
|
const backend = ptyBackend();
|
|
117
|
-
|
|
118
|
+
// **始めても無駄な条件は、始める前に返す。**
|
|
119
|
+
// 擬似端末が無い / Node が機械に合っていない、はどちらも必ず失敗する
|
|
120
|
+
json(res, 200, {
|
|
121
|
+
ok: true,
|
|
122
|
+
pty: backend !== null,
|
|
123
|
+
ptyBackend: backend,
|
|
124
|
+
node: checkNodeRuntime(),
|
|
125
|
+
});
|
|
118
126
|
return true;
|
|
119
127
|
}
|
|
120
128
|
|
package/src/server/leftover.js
CHANGED
|
@@ -99,8 +99,13 @@ export function leftoverWarning(leftover) {
|
|
|
99
99
|
"このファイルには**チャネルシークレットとアクセストークンが、そのまま読める形で**入っています。",
|
|
100
100
|
"**このファイルも、開いた画面も、誰にも送らないでください。**(サポートへの問い合わせでも同じです)",
|
|
101
101
|
"",
|
|
102
|
-
"
|
|
103
|
-
"
|
|
102
|
+
"もう一度セットアップを実行し、最後まで完了すれば、このファイルは自動で消えます。",
|
|
103
|
+
"本家のツールは途中から再開するので、済んだところを訊かれることはありません",
|
|
104
|
+
"(選択を求められたら「続行」を選んでください)。",
|
|
105
|
+
"",
|
|
106
|
+
"**この画面の入力欄が空になっている場合は、もう一度入れてください。**",
|
|
107
|
+
"このツールは入力をメモリ上だけで扱い、ディスクに保存しないため、",
|
|
108
|
+
"ターミナルを終了すると消えます。終了せずに「もう一度実行する」を押せば、入力し直す必要はありません。",
|
|
104
109
|
],
|
|
105
110
|
// 消すのはこちらの判断でやらない。再開に必要な進捗も同じファイルに入っている
|
|
106
111
|
canDelete: false,
|
package/src/server/runner.js
CHANGED
|
@@ -102,6 +102,8 @@ export class SetupRun extends EventEmitter {
|
|
|
102
102
|
this.pending = null;
|
|
103
103
|
/** フェイルクローズで止まった理由 @type {object|null} */
|
|
104
104
|
this.halt = null;
|
|
105
|
+
/** 無反応を見張る時計。出力が来なくても判定を動かす @type {NodeJS.Timeout|null} */
|
|
106
|
+
this.watchdog = null;
|
|
105
107
|
|
|
106
108
|
/** 直近に検出した枠 @type {import("./prompt-detector.js").DetectedPrompt|null} */
|
|
107
109
|
this.detected = null;
|
|
@@ -198,6 +200,7 @@ export class SetupRun extends EventEmitter {
|
|
|
198
200
|
this.stop();
|
|
199
201
|
if (this.timer) clearTimeout(this.timer);
|
|
200
202
|
if (this.forced) clearTimeout(this.forced);
|
|
203
|
+
if (this.watchdog) clearTimeout(this.watchdog);
|
|
201
204
|
for (const key of Object.keys(this.answers)) {
|
|
202
205
|
this.answers[key] = "";
|
|
203
206
|
delete this.answers[key];
|
|
@@ -212,6 +215,7 @@ export class SetupRun extends EventEmitter {
|
|
|
212
215
|
#onExit(info) {
|
|
213
216
|
this.exit = info;
|
|
214
217
|
this.status = "finished";
|
|
218
|
+
if (this.watchdog) clearTimeout(this.watchdog);
|
|
215
219
|
// 仕上げの手作業に使う値を取り出す。読めなければ端末表示のままにする
|
|
216
220
|
this.completion = parseCompletion(this.term.snapshot(), {
|
|
217
221
|
// 画面から読めなくても、フォームに入っていた値で補える
|
|
@@ -224,6 +228,13 @@ export class SetupRun extends EventEmitter {
|
|
|
224
228
|
}
|
|
225
229
|
|
|
226
230
|
#onData(chunk) {
|
|
231
|
+
// **出力が来ている = 本家は動いている。**
|
|
232
|
+
//
|
|
233
|
+
// 進捗を「こちらが答えたとき」だけで測っていたため、本家が長く作業すると
|
|
234
|
+
// タイマーが止まったままになり、作業明けの最初のプロンプトが
|
|
235
|
+
// 照合される前に stuck で切られていた(#44)。
|
|
236
|
+
// npx のインストールだけで1〜2分かかるので、構造的に起きていた。
|
|
237
|
+
this.progressedAt = Date.now();
|
|
227
238
|
this.term.write(chunk);
|
|
228
239
|
this.#schedule();
|
|
229
240
|
}
|
|
@@ -248,9 +259,23 @@ export class SetupRun extends EventEmitter {
|
|
|
248
259
|
} catch (err) {
|
|
249
260
|
this.#haltWith("internal-error", "内部エラーで自動応答を止めました", String(err));
|
|
250
261
|
}
|
|
262
|
+
this.#watch();
|
|
251
263
|
this.emit("update", this.state());
|
|
252
264
|
}
|
|
253
265
|
|
|
266
|
+
/**
|
|
267
|
+
* 無反応を見張る。
|
|
268
|
+
*
|
|
269
|
+
* `#evaluate` は出力が来たときにしか呼ばれない。つまり**本家が黙り込むと、
|
|
270
|
+
* 判定そのものが動かない**。それでは「答えたのに反応が無い」を検知できないので、
|
|
271
|
+
* ここで時計を持つ。
|
|
272
|
+
*/
|
|
273
|
+
#watch() {
|
|
274
|
+
if (this.watchdog) clearTimeout(this.watchdog);
|
|
275
|
+
if (this.status === "finished" || this.status === "halted") return;
|
|
276
|
+
this.watchdog = setTimeout(() => this.#fire(), this.stuckMs);
|
|
277
|
+
}
|
|
278
|
+
|
|
254
279
|
// ── 判定 ───────────────────────────────────────────────────────────
|
|
255
280
|
|
|
256
281
|
#frameId(detected) {
|
|
@@ -275,18 +300,6 @@ export class SetupRun extends EventEmitter {
|
|
|
275
300
|
|
|
276
301
|
const frameId = this.#frameId(detected);
|
|
277
302
|
|
|
278
|
-
// 同じ枠が出たまま何も進んでいないなら、諦めて人に委ねる。
|
|
279
|
-
// 黙って固まると、参加者は何が起きているのか分からない
|
|
280
|
-
if (this.status === "running" && Date.now() - this.progressedAt > this.stuckMs) {
|
|
281
|
-
this.#haltWith(
|
|
282
|
-
"stuck",
|
|
283
|
-
"画面が進まなくなりました",
|
|
284
|
-
"この端末では自動で答えられない場面のようです。下の画面を見て、必要な入力を自分で送ってください。",
|
|
285
|
-
detected,
|
|
286
|
-
);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
303
|
// select の移動後の確認待ち
|
|
291
304
|
if (this.pendingSelect) {
|
|
292
305
|
this.#confirmSelect(detected, frameId);
|
|
@@ -312,8 +325,22 @@ export class SetupRun extends EventEmitter {
|
|
|
312
325
|
return;
|
|
313
326
|
}
|
|
314
327
|
|
|
315
|
-
//
|
|
316
|
-
|
|
328
|
+
// 同じ枠には二度送らない(エコーで枠が描き直されるため)。
|
|
329
|
+
//
|
|
330
|
+
// **stuck を見るのはここだけ。** 答えたのに枠が残ったまま出力も止まった、
|
|
331
|
+
// という場面に限る。まだ答えていない枠は、まず照合してみる(#44)。
|
|
332
|
+
// 未応答の枠を時間で切ると、答えられるプロンプトまで巻き添えにする。
|
|
333
|
+
if (this.answeredFrames.has(frameId)) {
|
|
334
|
+
if (this.status === "running" && Date.now() - this.progressedAt > this.stuckMs) {
|
|
335
|
+
this.#haltWith(
|
|
336
|
+
"stuck",
|
|
337
|
+
"本家 CLI が反応しなくなりました",
|
|
338
|
+
"送った入力のあと、画面が動かなくなりました。下の画面を見て、必要であれば自分で入力してください。",
|
|
339
|
+
detected,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
317
344
|
|
|
318
345
|
const matches = matchPrompt(detected);
|
|
319
346
|
|
|
@@ -546,6 +573,8 @@ export class SetupRun extends EventEmitter {
|
|
|
546
573
|
*/
|
|
547
574
|
answerManually(text) {
|
|
548
575
|
if (this.status !== "halted") throw new Error("止まっているときだけ手入力できます");
|
|
576
|
+
// 人が答えたのも進捗。ここを忘れると、復帰した直後に同じ判定へ落ちる
|
|
577
|
+
this.progressedAt = Date.now();
|
|
549
578
|
this.pty?.write(`${text}${KEY_ENTER}`);
|
|
550
579
|
this.halt = null;
|
|
551
580
|
this.status = "running";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 動かしている Node が、この機械に合っているか。
|
|
3
|
+
*
|
|
4
|
+
* Apple Silicon で **x86_64 の Node** を使うと、本家 CLI が依存として使う
|
|
5
|
+
* esbuild が x64 版を選び、Rosetta 上で落ちる。実測ではこう出た。
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* ◇ D1 データベース作成完了
|
|
9
|
+
* ◇ bootstrap 適用に失敗
|
|
10
|
+
* ■ wrangler d1 failed:
|
|
11
|
+
* Error: The service is no longer running ← esbuild の子プロセスが死んだ
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* **失敗するのは D1 を作ったあと。** R2(#26)や cron(#42)と同じ
|
|
15
|
+
* 「事前に分かるのに、作るものを作ってから失敗する」型で、やり直しのコストが高い。
|
|
16
|
+
*
|
|
17
|
+
* ただしこれは人に確認させる必要すら無い。**プロセスの中で分かる。**
|
|
18
|
+
* `pty.js` の `childEnv` が「起動に使った Node」を子へ引き継ぐので、
|
|
19
|
+
* ここで見ているのはそのまま本家 CLI が使う Node でもある。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { cpus } from "node:os";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} RuntimeCheck
|
|
26
|
+
* @property {boolean} ok
|
|
27
|
+
* @property {string} arch プロセスの arch
|
|
28
|
+
* @property {string} cpu CPU の名前
|
|
29
|
+
* @property {RuntimeProblem|null} problem
|
|
30
|
+
*
|
|
31
|
+
* @typedef {object} RuntimeProblem
|
|
32
|
+
* @property {string} id
|
|
33
|
+
* @property {string} title
|
|
34
|
+
* @property {string} body
|
|
35
|
+
* @property {string[]} fix 直し方。**既存の mise / nvm を壊さない手順**にする
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{ arch?: string, cpu?: string }} [override] テスト用
|
|
40
|
+
* @returns {RuntimeCheck}
|
|
41
|
+
*/
|
|
42
|
+
export function checkNodeRuntime(override = {}) {
|
|
43
|
+
const arch = override.arch ?? process.arch;
|
|
44
|
+
const cpu = override.cpu ?? cpus()[0]?.model ?? "";
|
|
45
|
+
|
|
46
|
+
// Apple 製 CPU なのに x64 で動いている = Rosetta 経由。
|
|
47
|
+
// arm64 で動いていれば何も言わない(Intel Mac の x64 も正常)
|
|
48
|
+
if (!(arch === "x64" && /\bApple\b/.test(cpu))) {
|
|
49
|
+
return { ok: true, arch, cpu, problem: null };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
arch,
|
|
55
|
+
cpu,
|
|
56
|
+
problem: {
|
|
57
|
+
id: "x64-on-apple-silicon",
|
|
58
|
+
title: "この Node ではセットアップできません",
|
|
59
|
+
body:
|
|
60
|
+
`この Mac は ${cpu}(Apple Silicon)ですが、いま動いている Node は x86_64 版です。` +
|
|
61
|
+
"このまま進めると、途中まで作られたあとで必ず失敗します" +
|
|
62
|
+
"(本家が使う esbuild が x86_64 版を選び、Rosetta 上で落ちるため)。",
|
|
63
|
+
fix: [
|
|
64
|
+
"nodejs.org から macOS の ARM64 版(LTS)を入れてください。",
|
|
65
|
+
"いま使っている mise や nvm の設定は変えなくて構いません。" +
|
|
66
|
+
"別の場所に置いて、そこから起動すれば足ります。",
|
|
67
|
+
"例: ~/.local/node-arm64 に展開したなら、" +
|
|
68
|
+
"~/.local/node-arm64/bin/npx walover-line-harness-gui で起動する",
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/web/index.html
CHANGED
|
@@ -285,8 +285,17 @@
|
|
|
285
285
|
throw new Error("no server");
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* 始めても必ず失敗する条件。
|
|
290
|
+
*
|
|
291
|
+
* 見つけたら**開始させない**。途中まで作られてから失敗すると、
|
|
292
|
+
* Cloudflare 上に中途半端なものが残り、やり直しのコストが高い。
|
|
293
|
+
*/
|
|
294
|
+
const blockers = [];
|
|
295
|
+
|
|
288
296
|
if (health.pty === false) {
|
|
289
297
|
// 擬似端末が無いと本家 CLI は起動できない。値を入れてもらう前に伝える
|
|
298
|
+
blockers.push("擬似端末を用意できていません");
|
|
290
299
|
banner(
|
|
291
300
|
"セットアップを開始できません",
|
|
292
301
|
"擬似端末を用意できませんでした(python3 が見つかりません)。" +
|
|
@@ -295,6 +304,20 @@
|
|
|
295
304
|
);
|
|
296
305
|
}
|
|
297
306
|
|
|
307
|
+
// Apple Silicon で x86_64 の Node を使っている場合。
|
|
308
|
+
// 進めると D1 を作ったあとで必ず失敗する(実測)
|
|
309
|
+
if (health.node && health.node.ok === false) {
|
|
310
|
+
const { title, body, fix } = health.node.problem;
|
|
311
|
+
blockers.push(title);
|
|
312
|
+
const node = el("div", { className: "banner bad" });
|
|
313
|
+
node.append(el("strong", { textContent: title }));
|
|
314
|
+
node.append(document.createTextNode(body));
|
|
315
|
+
const list = el("ul");
|
|
316
|
+
for (const line of fix) list.append(el("li", { textContent: line }));
|
|
317
|
+
node.append(list);
|
|
318
|
+
$("banners").append(node);
|
|
319
|
+
}
|
|
320
|
+
|
|
298
321
|
// 本家 CLI の版のズレ。
|
|
299
322
|
//
|
|
300
323
|
// 固定版で動かすので、新しい版が出ているだけなら**参加者はこのまま進めてよい**。
|
|
@@ -367,8 +390,8 @@
|
|
|
367
390
|
}
|
|
368
391
|
$("preflight").hidden = gates.length === 0;
|
|
369
392
|
|
|
370
|
-
/**
|
|
371
|
-
const gateReady = () => gates.every((g) => g.checked);
|
|
393
|
+
/** 始められるか。前提の確認と、始めても無駄な条件の両方を見る */
|
|
394
|
+
const gateReady = () => blockers.length === 0 && gates.every((g) => g.checked);
|
|
372
395
|
|
|
373
396
|
for (const group of schema.groups) {
|
|
374
397
|
const fields = schema.fields.filter((f) => f.group === group.id);
|
|
@@ -560,6 +583,8 @@
|
|
|
560
583
|
const screen = $("run-screen");
|
|
561
584
|
let startButton = null;
|
|
562
585
|
let gateNote = null;
|
|
586
|
+
/** 直近の実行状態。やり直すとき、本家がまだ動いているかの判断に使う */
|
|
587
|
+
let lastStatus = "idle";
|
|
563
588
|
/** 開始を投げている最中。この間にチェックを外されても、押し直せるようにはしない */
|
|
564
589
|
let starting = false;
|
|
565
590
|
|
|
@@ -578,7 +603,10 @@
|
|
|
578
603
|
const ready = gateReady();
|
|
579
604
|
startButton.disabled = !ready;
|
|
580
605
|
gateNote.replaceChildren();
|
|
581
|
-
if (
|
|
606
|
+
if (blockers.length > 0) {
|
|
607
|
+
// 参加者の操作では解決しない。上の赤い枠の指示に従ってもらうしかない
|
|
608
|
+
gateNote.textContent = `このままでは開始できません: ${blockers.join(" / ")}`;
|
|
609
|
+
} else if (!ready) {
|
|
582
610
|
gateNote.append(document.createTextNode("開始するには、"));
|
|
583
611
|
gateNote.append(el("a", { href: "#preflight", textContent: "ページ上部の前提条件" }));
|
|
584
612
|
gateNote.append(document.createTextNode("を確認してください。"));
|
|
@@ -605,19 +633,71 @@
|
|
|
605
633
|
syncGate();
|
|
606
634
|
return;
|
|
607
635
|
}
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
submit.disabled = true;
|
|
636
|
+
// 入力はサーバーに渡した。ここから先は実行の画面。
|
|
637
|
+
// **値は消さない。** やり直すときに入れ直させないため(#45)
|
|
638
|
+
setFormEnabled(false);
|
|
612
639
|
runBox.hidden = false;
|
|
613
640
|
runBox.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
614
641
|
listen();
|
|
615
642
|
}
|
|
616
643
|
|
|
644
|
+
/** 実行中は触らせない。終わったら戻す(値はそのまま) */
|
|
645
|
+
function setFormEnabled(on) {
|
|
646
|
+
for (const input of inputs.values()) input.disabled = !on;
|
|
647
|
+
for (const gate of gates) gate.disabled = !on;
|
|
648
|
+
submit.disabled = !on;
|
|
649
|
+
if (startButton) startButton.disabled = !on;
|
|
650
|
+
if (on) syncGate();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* もう一度実行する。
|
|
655
|
+
*
|
|
656
|
+
* **ツールを落とさせない。** 入力はメモリ上にしか無いので、
|
|
657
|
+
* ターミナルを終了すると消える。落とさずにやり直せれば入れ直しが要らない(#45)。
|
|
658
|
+
*/
|
|
659
|
+
async function restartRun() {
|
|
660
|
+
// 止まっている最中は本家がまだ動いている。先に終わらせる
|
|
661
|
+
if (!/finished/.test(lastStatus)) {
|
|
662
|
+
await fetch("/api/run/stop", { method: "POST" });
|
|
663
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
664
|
+
}
|
|
665
|
+
askBox.replaceChildren();
|
|
666
|
+
screen.textContent = "";
|
|
667
|
+
document.getElementById("leftover")?.remove();
|
|
668
|
+
|
|
669
|
+
starting = true;
|
|
670
|
+
const res = await fetch("/api/run/start", {
|
|
671
|
+
method: "POST",
|
|
672
|
+
headers: { "Content-Type": "application/json" },
|
|
673
|
+
body: "{}",
|
|
674
|
+
});
|
|
675
|
+
if (!res.ok) {
|
|
676
|
+
starting = false;
|
|
677
|
+
setFormEnabled(true);
|
|
678
|
+
result.textContent = (await res.json()).error ?? "開始できませんでした。";
|
|
679
|
+
result.className = "result bad";
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
setFormEnabled(false);
|
|
683
|
+
listen();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** もう一度実行するためのボタン。止まったときと、失敗して終わったときに出す */
|
|
687
|
+
function rerunButton(label) {
|
|
688
|
+
const button = el("button", { className: "go", type: "button", textContent: label });
|
|
689
|
+
button.addEventListener("click", () => {
|
|
690
|
+
button.disabled = true;
|
|
691
|
+
restartRun();
|
|
692
|
+
});
|
|
693
|
+
return button;
|
|
694
|
+
}
|
|
695
|
+
|
|
617
696
|
function listen() {
|
|
618
697
|
const source = new EventSource("/api/run/stream");
|
|
619
698
|
source.addEventListener("update", (event) => {
|
|
620
699
|
const { lines, state } = JSON.parse(event.data);
|
|
700
|
+
lastStatus = state.status;
|
|
621
701
|
// 端末の再描画はサーバー側で解決済み。ここは行を並べるだけ
|
|
622
702
|
const atBottom = screen.scrollTop + screen.clientHeight >= screen.scrollHeight - 24;
|
|
623
703
|
screen.textContent = lines.join("\n");
|
|
@@ -625,6 +705,9 @@
|
|
|
625
705
|
renderAsk(state);
|
|
626
706
|
if (state.status === "finished") {
|
|
627
707
|
source.close();
|
|
708
|
+
starting = false;
|
|
709
|
+
// もう一度やり直せるように戻す。**値は消さない**(#45)
|
|
710
|
+
setFormEnabled(true);
|
|
628
711
|
// 失敗・中断だとシークレットを含むファイルが残る。終わった直後に必ず見る
|
|
629
712
|
showLeftover();
|
|
630
713
|
}
|
|
@@ -636,23 +719,35 @@
|
|
|
636
719
|
askBox.replaceChildren();
|
|
637
720
|
|
|
638
721
|
if (state.status === "finished") {
|
|
639
|
-
|
|
640
|
-
|
|
722
|
+
// **シグナルで落とされた場合も code は 0 になる**(実測: code 0 / signal 15)。
|
|
723
|
+
// code だけ見ると、中断を「成功」として見せてしまう
|
|
724
|
+
const ok = state.exit?.code === 0 && !state.exit?.signal;
|
|
725
|
+
|
|
726
|
+
if (ok && state.hasCompletion) {
|
|
641
727
|
// 仕上げの手作業に使う値を、貼り付けやすい形で出す。
|
|
642
728
|
// 端末表示は畳む(もう読む必要がない)
|
|
643
729
|
showCompletion();
|
|
644
730
|
return;
|
|
645
731
|
}
|
|
646
|
-
|
|
647
|
-
|
|
732
|
+
|
|
733
|
+
// ここに来たのは、完了の案内を出せなかったということ。
|
|
734
|
+
// どの場合でも**やり直せる道を必ず添える**(#45)
|
|
735
|
+
const box = el("div", { className: "ask stopped" });
|
|
736
|
+
box.append(el("h3", { textContent: ok ? "終わりましたが、完了の案内が読み取れませんでした" : "途中で終わりました" }));
|
|
737
|
+
box.append(
|
|
738
|
+
el("p", {
|
|
739
|
+
textContent: "画面の最後に出ているメッセージを確認してください。原因を直したら、下のボタンでやり直せます。",
|
|
740
|
+
}),
|
|
741
|
+
);
|
|
742
|
+
// **ターミナルを終了させない。** 落とすと入力が消える(#45)
|
|
648
743
|
box.append(
|
|
649
744
|
el("p", {
|
|
650
745
|
textContent:
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
: "画面の最後に出ているメッセージを確認してください。同じ手順でやり直すと、続きから再開できます。",
|
|
746
|
+
"入力した値はこの画面に残っています。ターミナルは終了しないでください。" +
|
|
747
|
+
"上の入力欄を直してから、やり直すこともできます。",
|
|
654
748
|
}),
|
|
655
749
|
);
|
|
750
|
+
box.append(el("div", { className: "choices" }, [rerunButton("もう一度実行する")]));
|
|
656
751
|
askBox.append(box);
|
|
657
752
|
return;
|
|
658
753
|
}
|
|
@@ -671,7 +766,8 @@
|
|
|
671
766
|
answer({ manual: true, text: input.value }).then(() => (input.value = "")),
|
|
672
767
|
);
|
|
673
768
|
box.append(el("div", { className: "row" }, [el("div", { className: "row" }, [input])]));
|
|
674
|
-
|
|
769
|
+
// 打つ内容が分からないこともある。そのときのために、やり直す道も並べる
|
|
770
|
+
box.append(el("div", { className: "choices" }, [go, rerunButton("中止して最初からやり直す")]));
|
|
675
771
|
askBox.append(box);
|
|
676
772
|
return;
|
|
677
773
|
}
|