@devframes/hub-ui-onboard 1.3.1 → 1.3.3

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.
@@ -1,4 +1,4 @@
1
- //#region src/client/.generated/css.ts
1
+ // src/client/.generated/css.ts
2
2
  var e = import.meta.url, t = new URL("./", e), n = 1e3, r = "devframes-onboard-hidden", i = (e, n) => fetch(new URL(`__onboard/${e}`, t), n);
3
3
  async function a() {
4
4
  return (await i("status")).json();
@@ -112,21 +112,13 @@ function p(e) {
112
112
  }
113
113
  async function W() {
114
114
  let e = await a();
115
- if (e.state === "ready") {
116
- t.remove(), await o();
117
- return;
118
- }
119
- U(e), e.state === "installing" && setTimeout(() => void W(), n);
115
+ e.state === "ready" ? (t.remove(), await o()) : (U(e), e.state === "installing" && setTimeout(() => void W(), n));
120
116
  }
121
117
  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
- }
126
- U({
118
+ P.disabled = !0, I.textContent = h.confirming, (await i("install", { method: "POST" })).ok ? (U({
127
119
  ...e,
128
120
  state: "installing"
129
- }), await W();
121
+ }), await W()) : (await W(), N.hidden = !1, N.textContent = h.refused);
130
122
  }
131
123
  async function K() {
132
124
  (await i("disable", { method: "POST" })).ok && t.remove();
@@ -136,13 +128,6 @@ function p(e) {
136
128
  async function m() {
137
129
  if (window.parent !== window || sessionStorage.getItem(r)) return;
138
130
  let e = await a();
139
- if (e.state !== "disabled" && e.state !== "installed") {
140
- if (e.state === "ready") {
141
- await o();
142
- return;
143
- }
144
- p(e);
145
- }
131
+ e.state !== "disabled" && e.state !== "installed" && (e.state === "ready" ? await o() : p(e));
146
132
  }
147
133
  m();
148
- //#endregion
package/dist/index.d.mts CHANGED
@@ -1,4 +1,3 @@
1
- //#region src/types.d.ts
2
1
  /** A logo URL, or one per color scheme. Same shape as hub-ui's `BrandingLogo`. */
3
2
  type OnboardingLogo = string | {
4
3
  light: string;
@@ -54,6 +53,12 @@ interface CreateOnboardingOptions {
54
53
  stateDir?: string;
55
54
  /** Install as a devDependency. Default: `true`. */
56
55
  dev?: boolean;
56
+ /**
57
+ * Stop the install when it runs longer than this many milliseconds, and
58
+ * show an error that asks the user to run the command in a terminal.
59
+ * Default: `300_000` (5 minutes).
60
+ */
61
+ installTimeout?: number;
57
62
  branding?: OnboardingBranding;
58
63
  messages?: Partial<OnboardingMessages>;
59
64
  /**
@@ -102,8 +107,6 @@ interface Onboarding {
102
107
  /** `<base>embedded.js`: the `<script type="module">` URL to inject. */
103
108
  scriptSrc: string;
104
109
  }
105
- //#endregion
106
- //#region src/index.d.ts
107
110
  export declare function defaultMessages(productName: string): OnboardingMessages;
108
111
  /**
109
112
  * Create the stand-in for a hub UI that is not installed yet: a request
@@ -111,5 +114,4 @@ export declare function defaultMessages(productName: string): OnboardingMessages
111
114
  * `__onboard/*` routes it talks to.
112
115
  */
113
116
  export declare function createOnboarding(options: CreateOnboardingOptions): Onboarding;
114
- //#endregion
115
117
  export type { CreateOnboardingOptions, Onboarding, OnboardingBranding, OnboardingConfirmation, OnboardingHandler, OnboardingLogo, OnboardingMessages, OnboardingState, OnboardingStatus };
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
9
9
  import { Diagnostic, createConsoleReporter, defineDiagnostics } from "nostics";
10
10
  import { detect, resolveCommand } from "package-manager-detector";
11
11
  import { exec } from "tinyexec";
12
- //#region src/diagnostics.ts
12
+ // src/diagnostics.ts
13
13
  /**
14
14
  * `DF90xx` is this package's range (see `.agents/08-diagnostics.md`). It
15
15
  * uses `nostics` directly: the package must stay free of `devframe` so a
@@ -54,11 +54,14 @@ const diagnostics = defineDiagnostics({
54
54
  DF9008: {
55
55
  why: (p) => `Nobody allowed \`${p.command}\` within ${p.minutes} minutes. The install was refused.`,
56
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()`."
57
+ },
58
+ DF9009: {
59
+ why: (p) => `\`${p.command}\` did not finish within ${p.seconds} seconds, so it was stopped.`,
60
+ fix: "Run the command in a terminal to see where the package manager stops, then click Install again. If the install is only slow, raise `installTimeout`."
57
61
  }
58
62
  }
59
63
  });
60
- //#endregion
61
- //#region src/install.ts
64
+ // src/install.ts
62
65
  /** The `add` command for the project's package manager, `npm` when none is detected. */
63
66
  async function resolveInstallCommand(plan) {
64
67
  const agent = (await detect({ cwd: plan.cwd }))?.agent ?? "npm";
@@ -67,18 +70,35 @@ async function resolveInstallCommand(plan) {
67
70
  }
68
71
  /**
69
72
  * Run the install and make sure that each named package landed in
70
- * `<cwd>/node_modules`. Throws a `DF9001` / `DF9002` diagnostic on failure.
73
+ * `<cwd>/node_modules`. Throws a `DF9001` / `DF9002` / `DF9009` diagnostic on failure.
71
74
  */
72
75
  async function runInstall(plan, command) {
73
- const result = await exec(command.command, command.args, { nodeOptions: {
76
+ const commandLine = [command.command, ...command.args].join(" ");
77
+ const child = exec(command.command, command.args, { nodeOptions: {
74
78
  cwd: plan.cwd,
75
79
  env: {
76
80
  ...process.env,
77
81
  CI: "1"
78
82
  }
79
83
  } });
84
+ let timer;
85
+ let killed;
86
+ const timedOut = new Promise((resolve) => {
87
+ timer = setTimeout(() => {
88
+ killed = killTree(child);
89
+ killed.then(() => resolve(void 0));
90
+ }, plan.timeout);
91
+ });
92
+ const result = await Promise.race([child, timedOut]).finally(() => clearTimeout(timer));
93
+ if (killed || !result) {
94
+ await killed;
95
+ throw diagnostics.DF9009({
96
+ command: commandLine,
97
+ seconds: Math.round(plan.timeout / 1e3)
98
+ });
99
+ }
80
100
  if (result.exitCode !== 0) throw diagnostics.DF9001({
81
- command: [command.command, ...command.args].join(" "),
101
+ command: commandLine,
82
102
  exitCode: result.exitCode,
83
103
  stderr: result.stderr.trim().slice(-2048)
84
104
  });
@@ -88,6 +108,23 @@ async function runInstall(plan, command) {
88
108
  });
89
109
  }
90
110
  /**
111
+ * Kill the install with everything it started. On Windows, tinyexec runs a
112
+ * package manager's `.cmd` shim through `cmd.exe`, and a kill does not reach
113
+ * child processes, so `taskkill /T /F` ends the whole tree.
114
+ */
115
+ async function killTree(child) {
116
+ const pid = child.process?.pid;
117
+ if (process.platform === "win32" && pid !== void 0) {
118
+ if ((await exec("taskkill", [
119
+ "/pid",
120
+ String(pid),
121
+ "/T",
122
+ "/F"
123
+ ]).then((r) => r, () => void 0))?.exitCode === 0) return;
124
+ }
125
+ child.kill();
126
+ }
127
+ /**
91
128
  * `true` when every named package is already in `<cwd>/node_modules`, so the
92
129
  * onboarding has nothing to install. Path, URL and alias specs cannot be
93
130
  * checked by name, so a plan made only of those is never "already installed".
@@ -109,8 +146,7 @@ function namedPackages(plan) {
109
146
  function packageName(spec) {
110
147
  return /^((?:@[\w.~-]+\/)?[\w.~-]+)(?:@[^/]*)?$/.exec(spec)?.[1];
111
148
  }
112
- //#endregion
113
- //#region src/terminal-confirm.ts
149
+ // src/terminal-confirm.ts
114
150
  const ANSWER_MINUTES = 5;
115
151
  /**
116
152
  * The default `onConfirm`: a `y/N` question in the terminal that runs the dev
@@ -149,9 +185,9 @@ function confirmInTerminal(command, input = process.stdin) {
149
185
  input.on("data", onData);
150
186
  });
151
187
  }
152
- //#endregion
153
- //#region src/index.ts
188
+ // src/index.ts
154
189
  const STATE_FILE = "hub-ui-onboard.json";
190
+ const DEFAULT_INSTALL_TIMEOUT = 3e5;
155
191
  /** `devframes` → `/devframes/`, same rule as `normalizeHubBase` in `@devframes/hub`. */
156
192
  function normalizeBase(base) {
157
193
  return `/${base}/`.replace(/\/{2,}/g, "/");
@@ -245,7 +281,8 @@ function createOnboarding(options) {
245
281
  const plan = {
246
282
  cwd,
247
283
  packages: options.packages,
248
- dev: options.dev ?? true
284
+ dev: options.dev ?? true,
285
+ timeout: options.installTimeout ?? DEFAULT_INSTALL_TIMEOUT
249
286
  };
250
287
  const branding = options.branding ?? {};
251
288
  const messages = {
@@ -375,5 +412,4 @@ function createOnboarding(options) {
375
412
  scriptSrc: `${base}embedded.js`
376
413
  };
377
414
  }
378
- //#endregion
379
415
  export { createOnboarding, defaultMessages };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devframes/hub-ui-onboard",
3
3
  "type": "module",
4
- "version": "1.3.1",
4
+ "version": "1.3.3",
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",
@@ -33,12 +33,12 @@
33
33
  "tinyexec": "^1.3.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@types/node": "^26.6.5",
37
- "tsdown": "0.23.0",
36
+ "@types/node": "^26.6.6",
37
+ "tsdown": "^0.23.7",
38
38
  "tsx": "^4.23.15",
39
- "unocss": "^66.11.0",
40
- "vite": "^8.3.4",
41
- "vitest": "^5.0.5"
39
+ "unocss": "^66.12.0",
40
+ "vite": "^8.3.6",
41
+ "vitest": "^5.1.0"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "pnpm run build:css && tsdown && vite build --config vite.client.config.ts",