walover-line-harness-gui 0.6.0 → 0.7.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "walover-line-harness-gui",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "LINE Harness のセットアップを、ターミナルを開かずにブラウザから行うためのローカルツール",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -213,7 +213,10 @@ async function route(req, res, pathname, method) {
213
213
  }
214
214
  const body = await readJson(req);
215
215
  try {
216
- if (body?.manual === true) {
216
+ if (body?.manual === true && typeof body.key === "string") {
217
+ // 決まったキーだけ。**画面から任意の列を送らせない**(#59)
218
+ run.sendKey(body.key);
219
+ } else if (body?.manual === true) {
217
220
  // 止まっているときに、画面を見た人が自分で打つ逃げ道
218
221
  run.answerManually(String(body.text ?? ""));
219
222
  } else {
@@ -25,6 +25,9 @@ const KEY_ENTER = "\r";
25
25
  const KEY_DOWN = "\x1b[B";
26
26
  const KEY_UP = "\x1b[A";
27
27
 
28
+ /** 止まったときに画面から送れるキー。**ここに無いものは送れない** */
29
+ const MANUAL_KEYS = { up: KEY_UP, down: KEY_DOWN, enter: KEY_ENTER };
30
+
28
31
  /** 画面が落ち着くまでの待ち時間。clack は1文字ごとに描き直すため、まとめてから判定する */
29
32
  const QUIET_MS = 80;
30
33
 
@@ -104,6 +107,8 @@ export class SetupRun extends EventEmitter {
104
107
  this.halt = null;
105
108
  /** 無反応を見張る時計。出力が来なくても判定を動かす @type {NodeJS.Timeout|null} */
106
109
  this.watchdog = null;
110
+ /** 人が矢印で選び直している最中か。出力が来ても停止表示を消さない */
111
+ this.manualHold = false;
107
112
 
108
113
  /** 直近に検出した枠 @type {import("./prompt-detector.js").DetectedPrompt|null} */
109
114
  this.detected = null;
@@ -216,6 +221,15 @@ export class SetupRun extends EventEmitter {
216
221
  this.exit = info;
217
222
  this.status = "finished";
218
223
  if (this.watchdog) clearTimeout(this.watchdog);
224
+
225
+ // 「画面が止まっています」は、終わってしまえば意味を持たない。
226
+ //
227
+ // Windows は pty の終了通知が遅れるので、最後の出力のあと・終了の前に
228
+ // 見張りタイマーが発火しうる。それを終了後まで持ち越すと、
229
+ // 完走したのに「止まっています」が残る(CI で実際に出た)。
230
+ //
231
+ // 他の理由(未知のプロンプトなど)は残す。なぜ止めたかは終わったあとも意味がある
232
+ if (this.halt?.reason === "no-prompt-stalled") this.halt = null;
219
233
  // 仕上げの手作業に使う値を取り出す。読めなければ端末表示のままにする
220
234
  this.completion = parseCompletion(this.term.snapshot(), {
221
235
  // 画面から読めなくても、フォームに入っていた値で補える
@@ -228,6 +242,19 @@ export class SetupRun extends EventEmitter {
228
242
  }
229
243
 
230
244
  #onData(chunk) {
245
+ // 「画面が止まっている」という判断は、**出力が来た時点で覆る**。
246
+ //
247
+ // 沈黙だけを根拠にしているので、動き出したら取り消すのが筋。
248
+ // 取り消さないと、本家が黙って作業していただけの場面で
249
+ // 「止まっています」が居座る(Windows の遅い pty で実際に出た)。
250
+ //
251
+ // ただし人が矢印で選び直している最中は消さない。
252
+ // 選択肢が動くたびに出力が来るので、押すボタンが消えてしまう
253
+ if (this.halt?.reason === "no-prompt-stalled" && !this.manualHold) {
254
+ this.halt = null;
255
+ this.status = "running";
256
+ }
257
+
231
258
  // **出力が来ている = 本家は動いている。**
232
259
  //
233
260
  // 進捗を「こちらが答えたとき」だけで測っていたため、本家が長く作業すると
@@ -291,7 +318,26 @@ export class SetupRun extends EventEmitter {
291
318
  this.detectedFrameId = detected ? this.#frameId(detected) : null;
292
319
 
293
320
  if (!detected || !detected.complete) {
294
- // プロンプトが出ていない = 本家が作業中
321
+ // プロンプトが出ていない = 本家が作業中……とは限らない。
322
+ //
323
+ // **本家が呼ぶ外部コマンドは、こちらが知らない形で対話を求めてくる。**
324
+ // 実測では wrangler がこれを出し、画面は生きているのに操作できなくなった。
325
+ //
326
+ // ? The project you specified does not exist ... Would you like to create it? ›
327
+ // ❯ Create a new project
328
+ //
329
+ // clack の枠しか読めない以上、**形は分からなくても「止まっている」ことは分かる**
330
+ // 出力が途切れたまま動かないなら、黙って待たずに人へ渡す(#59)。
331
+ if (this.status === "running" && Date.now() - this.progressedAt > this.stuckMs) {
332
+ this.#haltWith(
333
+ "no-prompt-stalled",
334
+ "画面が止まっています",
335
+ "本家のツールが何か入力を待っているようですが、このツールでは形が分かりません。" +
336
+ "下の画面を見て、必要なものを送ってください。",
337
+ null,
338
+ );
339
+ return;
340
+ }
295
341
  this.spec = null;
296
342
  this.pending = null;
297
343
  if (this.status !== "running") this.status = "running";
@@ -422,6 +468,7 @@ export class SetupRun extends EventEmitter {
422
468
  this.status = "halted";
423
469
  this.pending = null;
424
470
  this.pendingSelect = null;
471
+ this.manualHold = false;
425
472
  this.halt = {
426
473
  reason,
427
474
  message,
@@ -571,6 +618,34 @@ export class SetupRun extends EventEmitter {
571
618
  *
572
619
  * @param {string} text
573
620
  */
621
+ /**
622
+ * 止まっているときに、決まったキーだけ送る。
623
+ *
624
+ * 選択肢を動かすには矢印キーが要るが、**画面から任意の文字列を送らせない**。
625
+ * ここに無いものは送れない(`\x1b[A` のような列を本文で受け取らない)。
626
+ */
627
+ sendKey(name) {
628
+ if (this.status !== "halted") throw new Error("止まっているときだけ送れます");
629
+ const key = MANUAL_KEYS[name];
630
+ if (!key) throw new Error("送れないキーです");
631
+
632
+ this.progressedAt = Date.now();
633
+ this.pty?.write(key);
634
+
635
+ // 矢印は「選び直している」だけなので、止まったままにする。
636
+ // 決定(Enter)で初めて先へ進む
637
+ if (name !== "enter") {
638
+ // 選択肢が動くと出力が来る。それで停止表示を消さない
639
+ this.manualHold = true;
640
+ return;
641
+ }
642
+
643
+ this.manualHold = false;
644
+ this.halt = null;
645
+ this.status = "running";
646
+ if (this.detected) this.answeredFrames.add(this.#frameId(this.detected));
647
+ }
648
+
574
649
  answerManually(text) {
575
650
  if (this.status !== "halted") throw new Error("止まっているときだけ手入力できます");
576
651
  // 人が答えたのも進捗。ここを忘れると、復帰した直後に同じ判定へ落ちる
@@ -772,11 +772,35 @@
772
772
  el("p", { textContent: "下の画面を見て、必要であれば自分で入力してください。" }),
773
773
  );
774
774
 
775
+ // **キーを押せるようにする。**
776
+ //
777
+ // 本家が呼ぶ外部コマンドは、矢印で選んで Enter で決める形の対話を出すことがある。
778
+ // 実測では wrangler がそれを出し、画面は生きているのに操作できなかった(#59)。
779
+ // 文字を打つ欄だけでは、そこから抜けられない。
780
+ box.append(
781
+ el("p", {
782
+ className: "cli-echo",
783
+ textContent:
784
+ "下の黒い枠が選択肢を出している場合は、↑ ↓ で選んでから「Enter を送る」を押してください。",
785
+ }),
786
+ );
787
+ const keys = el("div", { className: "choices" });
788
+ for (const [label, key] of [["↑", "up"], ["↓", "down"]]) {
789
+ const button = el("button", { type: "button", textContent: label });
790
+ button.addEventListener("click", () => answer({ manual: true, key }));
791
+ keys.append(button);
792
+ }
793
+ const enter = el("button", { className: "go", type: "button", textContent: "Enter を送る" });
794
+ enter.addEventListener("click", () => answer({ manual: true, key: "enter" }));
795
+ keys.append(enter);
796
+ box.append(keys);
797
+
775
798
  const input = el("input", { type: "text", autocomplete: "off" });
776
799
  const go = el("button", { className: "go", type: "button", textContent: "送信" });
777
800
  go.addEventListener("click", () =>
778
801
  answer({ manual: true, text: input.value }).then(() => (input.value = "")),
779
802
  );
803
+ box.append(el("p", { className: "cli-echo", textContent: "文字を打つ必要がある場合はこちら:" }));
780
804
  box.append(el("div", { className: "row" }, [el("div", { className: "row" }, [input])]));
781
805
  // 打つ内容が分からないこともある。そのときのために、やり直す道も並べる
782
806
  box.append(el("div", { className: "choices" }, [go, rerunButton("中止して最初からやり直す")]));