@devframes/hub-ui-onboard 1.2.4 → 1.2.6

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.
@@ -119,10 +119,14 @@ function p(e) {
119
119
  U(e), e.state === "installing" && setTimeout(() => void W(), n);
120
120
  }
121
121
  async function G() {
122
+ if (P.disabled = !0, I.textContent = h.confirming, !(await i("install", { method: "POST" })).ok) {
123
+ await W(), N.hidden = !1, N.textContent = h.refused;
124
+ return;
125
+ }
122
126
  U({
123
127
  ...e,
124
128
  state: "installing"
125
- }), await i("install", { method: "POST" }), await W();
129
+ }), await W();
126
130
  }
127
131
  async function K() {
128
132
  (await i("disable", { method: "POST" })).ok && t.remove();
package/dist/index.d.mts CHANGED
@@ -19,6 +19,10 @@ interface OnboardingMessages {
19
19
  /** Short sentence under the heading. */
20
20
  description: string;
21
21
  install: string;
22
+ /** Install button label while `onConfirm` waits for the developer. */
23
+ confirming: string;
24
+ /** Shown when `onConfirm` refused the install. */
25
+ refused: string;
22
26
  /** Hides the button for this browser tab (`sessionStorage`). */
23
27
  hide: string;
24
28
  /** Writes the state file; the host stops injecting the button. */
@@ -30,6 +34,15 @@ interface OnboardingMessages {
30
34
  }
31
35
  /** A web-standard request handler the hub exposes once installed. */
32
36
  type OnboardingHandler = (request: Request) => Response | Promise<Response>;
37
+ /** What `onConfirm` receives when the panel asks to install. */
38
+ interface OnboardingConfirmation {
39
+ /** The packages Install adds, as given in `packages`. */
40
+ packages: string[];
41
+ /** The exact command Install runs. */
42
+ command: string[];
43
+ /** The panel's request, for example to read its `Origin` or `User-Agent`. */
44
+ request: Request;
45
+ }
33
46
  interface CreateOnboardingOptions {
34
47
  /** Packages to install when the user clicks Install. Package specs as the package manager accepts them. */
35
48
  packages: string[];
@@ -50,6 +63,15 @@ interface CreateOnboardingOptions {
50
63
  * user to restart the dev server.
51
64
  */
52
65
  onInstalled?: () => void | OnboardingHandler | Promise<void | OnboardingHandler>;
66
+ /**
67
+ * Ask the developer to allow an install that the panel requested; the
68
+ * dev server can be reachable from other devices. Return `true` to run
69
+ * it. Only one call is open at a time: clicks meanwhile share its answer.
70
+ * Default: a `y/N` question in the terminal. It refuses at once when stdin
71
+ * is not a TTY or another reader holds it in raw mode, and after 5 minutes
72
+ * without an answer.
73
+ */
74
+ onConfirm?: (confirmation: OnboardingConfirmation) => boolean | Promise<boolean>;
53
75
  }
54
76
  type OnboardingState = 'idle' | 'installing' | 'installed' | 'ready' | 'error' | 'disabled';
55
77
  /** The `GET <base>__onboard/status` body. */
@@ -90,4 +112,4 @@ export declare function defaultMessages(productName: string): OnboardingMessages
90
112
  */
91
113
  export declare function createOnboarding(options: CreateOnboardingOptions): Onboarding;
92
114
  //#endregion
93
- export type { CreateOnboardingOptions, Onboarding, OnboardingBranding, OnboardingHandler, OnboardingLogo, OnboardingMessages, OnboardingState, OnboardingStatus };
115
+ export type { CreateOnboardingOptions, Onboarding, OnboardingBranding, OnboardingConfirmation, OnboardingHandler, OnboardingLogo, OnboardingMessages, OnboardingState, OnboardingStatus };
package/dist/index.mjs CHANGED
@@ -38,6 +38,22 @@ const diagnostics = defineDiagnostics({
38
38
  DF9004: {
39
39
  why: (p) => `The onboarding state file ${p.file} could not be read or written.`,
40
40
  fix: "Make sure that `stateDir` is writable, or point it at another directory."
41
+ },
42
+ DF9005: {
43
+ why: (p) => `A browser asked to run \`${p.command}\`. Type y and press Enter within ${p.minutes} minutes to allow it.`,
44
+ fix: "If you did not click Install, press Enter to refuse. Another device can reach this dev server, so bind it to localhost."
45
+ },
46
+ DF9006: {
47
+ why: (p) => `A browser asked to run \`${p.command}\`, but no interactive terminal can confirm it. The install was refused.`,
48
+ fix: "Run the command yourself, or pass an `onConfirm` function to `createOnboarding()`."
49
+ },
50
+ DF9007: {
51
+ why: (p) => `A browser asked to run \`${p.command}\`, but another program reads this terminal key by key, so it cannot show the question. The install was refused.`,
52
+ fix: "Run the command yourself. If a CLI owns the terminal, pass an `onConfirm` function to `createOnboarding()` that asks through that CLI."
53
+ },
54
+ DF9008: {
55
+ why: (p) => `Nobody allowed \`${p.command}\` within ${p.minutes} minutes. The install was refused.`,
56
+ fix: "Click Install again and answer in the terminal that runs the dev server. If the answer does not arrive, another program reads that terminal: pass an `onConfirm` function to `createOnboarding()`."
41
57
  }
42
58
  }
43
59
  });
@@ -94,6 +110,46 @@ function packageName(spec) {
94
110
  return /^((?:@[\w.~-]+\/)?[\w.~-]+)(?:@[^/]*)?$/.exec(spec)?.[1];
95
111
  }
96
112
  //#endregion
113
+ //#region src/terminal-confirm.ts
114
+ const ANSWER_MINUTES = 5;
115
+ /**
116
+ * The default `onConfirm`: a `y/N` question in the terminal that runs the dev
117
+ * server. When nobody can answer it, it refuses.
118
+ */
119
+ function confirmInTerminal(command, input = process.stdin) {
120
+ const line = command.join(" ");
121
+ if (!input.isTTY) {
122
+ diagnostics.DF9006({ command: line });
123
+ return Promise.resolve(false);
124
+ }
125
+ if (input.isRaw) {
126
+ diagnostics.DF9007({ command: line });
127
+ return Promise.resolve(false);
128
+ }
129
+ diagnostics.DF9005({
130
+ command: line,
131
+ minutes: ANSWER_MINUTES
132
+ });
133
+ const flowing = input.readableFlowing;
134
+ return new Promise((resolve) => {
135
+ const timer = setTimeout(() => {
136
+ diagnostics.DF9008({
137
+ command: line,
138
+ minutes: ANSWER_MINUTES
139
+ });
140
+ settle(false);
141
+ }, ANSWER_MINUTES * 6e4);
142
+ const onData = (chunk) => settle(/^y(?:es)?$/i.test(String(chunk).trim()));
143
+ function settle(allowed) {
144
+ clearTimeout(timer);
145
+ input.off("data", onData);
146
+ if (!flowing) input.pause();
147
+ resolve(allowed);
148
+ }
149
+ input.on("data", onData);
150
+ });
151
+ }
152
+ //#endregion
97
153
  //#region src/index.ts
98
154
  const STATE_FILE = "hub-ui-onboard.json";
99
155
  /** `devframes` → `/devframes/`, same rule as `normalizeHubBase` in `@devframes/hub`. */
@@ -115,6 +171,8 @@ function defaultMessages(productName) {
115
171
  title: productName,
116
172
  description: `${productName} enables development features in your project. Install it with:`,
117
173
  install: `Install ${productName}`,
174
+ confirming: "Confirm in your terminal...",
175
+ refused: "The dev server did not allow the install. Check its terminal, or run the command above yourself.",
118
176
  hide: "Hide in this session",
119
177
  disable: "Disable entirely",
120
178
  installing: "Installing...",
@@ -258,11 +316,29 @@ function createOnboarding(options) {
258
316
  state = "disabled";
259
317
  return new Response(null, { status: 204 });
260
318
  }
319
+ const commandLine = async () => {
320
+ const resolved = await getCommand();
321
+ return [resolved.command, ...resolved.args];
322
+ };
323
+ const onConfirm = options.onConfirm ?? (({ command }) => confirmInTerminal(command));
324
+ let asking;
325
+ async function startInstall(request) {
326
+ if (state !== "idle" && state !== "error") return new Response(null, { status: 202 });
327
+ asking ??= commandLine().then((command) => onConfirm({
328
+ packages: plan.packages,
329
+ command,
330
+ request
331
+ })).finally(() => {
332
+ asking = void 0;
333
+ });
334
+ if (!await asking) return new Response(null, { status: 403 });
335
+ if (state === "idle" || state === "error") install();
336
+ return new Response(null, { status: 202 });
337
+ }
261
338
  async function status() {
262
- const command = await getCommand();
263
339
  return {
264
340
  state,
265
- command: [command.command, ...command.args],
341
+ command: await commandLine(),
266
342
  branding,
267
343
  messages,
268
344
  error
@@ -281,9 +357,7 @@ function createOnboarding(options) {
281
357
  if (route === "GET __onboard/status") return json(await status());
282
358
  if (route === "POST __onboard/install" || route === "POST __onboard/disable") {
283
359
  if (!isSameOrigin(request)) return new Response(null, { status: 403 });
284
- if (route.endsWith("disable")) return disable();
285
- if (state === "idle" || state === "error") install();
286
- return new Response(null, { status: 202 });
360
+ return route.endsWith("disable") ? disable() : startInstall(request);
287
361
  }
288
362
  return new Response(null, { status: 404 });
289
363
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devframes/hub-ui-onboard",
3
3
  "type": "module",
4
- "version": "1.2.4",
4
+ "version": "1.2.6",
5
5
  "description": "A tiny stand-in for the devframe hub UI: a floating button that installs it on demand.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",