mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 00:55:13 +08:00
🔥 Sync 2026-07-19 20:20:12
This commit is contained in:
1 parent
7974ca67ae
commit
c0107db2e8
29 files changed
+1161
-1032
No files matched your search
@@ -1,4 +1,4 @@
|
||||
# Optional — run `pnpm setup` once and you usually don't need a .env at all.
|
||||
# Optional — run `pnpm setup:router` once and it writes/updates every value below.
|
||||
|
||||
# OpenWrt router address — bare IP/hostname; the web proxy target and the
|
||||
# .ut-sync ssh target both derive from it (ssh key selection etc. belongs
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Fail `pnpm install` early with a clear message on unsupported Node versions
|
||||
# (the range in package.json `engines` mirrors Vite 7's requirement) instead
|
||||
# of letting the install die somewhere deep inside a dependency.
|
||||
engine-strict=true
|
||||
@@ -4,7 +4,7 @@ This guide covers the complete development workflow for the Aurora theme, from e
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **[Node.js v20.19+](https://nodejs.org/en/download)** - JavaScript runtime
|
||||
- **[Node.js 20.19+ / 22.12+](https://nodejs.org/en/download)** - JavaScript runtime (mirrors Vite 7's support range — odd releases like 21.x don't qualify; enforced at `pnpm install` time via `engines` + `engine-strict`, so an unsupported Node fails fast with a clear message)
|
||||
- **pnpm** - Package manager (managed via [Corepack](https://github.com/nodejs/corepack))
|
||||
- **Tailwind CSS knowledge** - Required for styling. See [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||||
- **Network access** - Development machine must be on the same network as your OpenWrt router
|
||||
@@ -28,11 +28,29 @@ pnpm install
|
||||
### 2. Configure Environment
|
||||
|
||||
```bash
|
||||
# One-shot wizard: asks for the router IP, generates/installs an SSH key
|
||||
# (one router-password prompt), and writes .env
|
||||
pnpm setup
|
||||
# One-shot wizard: asks for every .env value (router IP, dev-server host/port,
|
||||
# current .env entries as defaults), generates/installs an SSH key (one
|
||||
# router-password prompt), verifies template sync end-to-end, and writes .env
|
||||
pnpm setup:router
|
||||
|
||||
# Non-interactive: pass the IP directly (no prompts; dev-server values keep
|
||||
# their saved .env entries or defaults)
|
||||
pnpm setup:router 192.168.2.1
|
||||
```
|
||||
|
||||
> The script is named `setup:router` (not `setup`) because `pnpm setup` resolves to pnpm's own built-in setup command and would never run a package script.
|
||||
|
||||
**What it does** (`scripts/setup.js`), in order:
|
||||
|
||||
1. **Collects the `.env` values.** With no argument it prompts for each variable in turn, showing the current `.env` entry as the default (an empty answer keeps it). With an IP argument it takes that as `VITE_OPENWRT_HOST` non-interactively and leaves the dev-server values alone. Nothing is written yet — `.env` is only updated once every step below has succeeded.
|
||||
2. **Pre-flights the connection.** A raw TCP probe of `<host>:22` with a 2s timeout, so an unreachable device or one with SSH disabled fails immediately with a clear message instead of hanging inside ssh.
|
||||
3. **Finds or generates an SSH key.** It looks for `~/.ssh/id_ed25519.pub`, `id_rsa.pub`, `id_ecdsa.pub` in that order and reuses the first one present — an existing key is never overwritten. **If you have no key at all, it generates one** (`ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519`). The empty passphrase is deliberate: `.ut` sync has to run unattended for the whole `pnpm dev` session.
|
||||
4. **Installs the public key on the router.** It first tests whether passwordless SSH already works (`ssh -o BatchMode=yes … echo ok`) and skips the install if so — after a full reflash that wipes the key, the test fails and the install simply runs again. Otherwise it appends the key to `/etc/dropbear/authorized_keys` over one interactive SSH session; the append is guarded by `grep -qxF`, so re-running never duplicates the entry. **This is the only step that asks for the router's root password, and it asks once.** Afterwards it re-tests passwordless auth and classifies any failure (wrong password / host unreachable / other). On a device running openssh rather than dropbear the key belongs in `/root/.ssh/authorized_keys` instead — install it manually there and this step detects it and skips.
|
||||
5. **Verifies the sync end-to-end.** Rather than trusting `echo ok`, it pushes `ucode/template/themes/aurora/` through the exact `tar -cf - | ssh … tar -xf -` pipeline the `ut-sync` plugin uses (see [Template (`.ut`) Live Sync](#template-ut-live-sync)). If this passes, `pnpm dev`'s template sync will too.
|
||||
6. **Writes `.env`.** Managed keys are rewritten in place so surrounding comments keep their meaning, missing ones are appended, and any other line passes through untouched.
|
||||
|
||||
> The password prompt in step 4 comes from the **local** ssh client, which reads it straight from `/dev/tty` rather than stdin — so it shows up normally even though the script pipes ssh's stderr in order to classify errors. It does mean the step needs a controlling terminal **on the development machine** (the router side is unaffected): run it from a normal shell, not from CI, `nohup`, or an editor task pane, where ssh has no way to ask and the run fails at authentication. Check with `tty` if unsure; in such an environment, install the key by hand instead.
|
||||
|
||||
If the router is at the default `192.168.1.1` and passwordless SSH already works, no `.env` is needed at all — every value below has a working default.
|
||||
|
||||
**Environment Variables** (all optional):
|
||||
@@ -182,11 +200,11 @@ For LuCI-specific JavaScript development, refer to the official API documentatio
|
||||
|
||||
- **CSS changes**: Trigger full page reload via custom HMR handler
|
||||
- **JS changes**: Trigger full page reload via custom HMR handler
|
||||
- **Template changes** (`.ut` files): Auto-synced to router over SSH and trigger full page reload (one-time `pnpm setup` required, see below)
|
||||
- **Template changes** (`.ut` files): Auto-synced to router over SSH and trigger full page reload (one-time `pnpm setup:router` required, see below)
|
||||
|
||||
### Template (`.ut`) Live Sync
|
||||
|
||||
The `.ut` template files are rendered server-side on the OpenWrt device, so unlike CSS/JS they can't be served locally — the dev server pushes them to the router instead. Run `pnpm setup` once to configure passwordless SSH; after that it's fully automatic:
|
||||
The `.ut` template files are rendered server-side on the OpenWrt device, so unlike CSS/JS they can't be served locally — the dev server pushes them to the router instead. Run `pnpm setup:router` once to configure passwordless SSH; after that it's fully automatic:
|
||||
|
||||
- **On startup**, the whole template directory is pushed (as one tarball over ssh stdin — Dropbear has no SFTP server for scp), so edits made while the dev server was down never leave the router stale.
|
||||
- **On save**, changes are debounced and the directory is pushed again, then the browser reloads.
|
||||
@@ -195,7 +213,7 @@ The `.ut` template files are rendered server-side on the OpenWrt device, so unli
|
||||
**Troubleshooting** — sync errors are printed with the fix:
|
||||
|
||||
- **Host key mismatch** (device was reflashed): Run `ssh-keygen -R <device-ip>`, then restart the dev server
|
||||
- **Authentication failed** (public key not on device): Run `pnpm setup`
|
||||
- **Authentication failed** (public key not on device, e.g. after a reflash): Run `pnpm setup:router`
|
||||
- **Connection refused/timed out**: Check that the device is online and SSH is enabled
|
||||
|
||||
A failed sync is retried on the next `.ut` change; CSS/JS dev features work normally without SSH.
|
||||
@@ -271,7 +289,8 @@ luci-theme-aurora/
|
||||
│ │ └── images/ # Theme images + PWA icons
|
||||
│ ├── scripts/ # Build scripts
|
||||
│ │ ├── clean.js # Build cleanup utility
|
||||
│ │ └── gen-tokens.js # Regenerates src/media/_tokens.css from @eamonxg/aurora-tokens
|
||||
│ │ ├── gen-tokens.js # Regenerates src/media/_tokens.css from @eamonxg/aurora-tokens
|
||||
│ │ └── setup.js # pnpm setup:router — .env wizard + passwordless SSH to the router
|
||||
│ ├── src/ # Source code
|
||||
│ │ ├── assets/icons/ # SVG icons
|
||||
│ │ ├── media/ # CSS source (Tailwind CSS v4)
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"setup": "node scripts/setup.js",
|
||||
"setup:router": "node scripts/setup.js",
|
||||
"build": "npm run clean && npm run gen:tokens && vite build",
|
||||
"clean": "node scripts/clean.js",
|
||||
"gen:tokens": "node scripts/gen-tokens.js",
|
||||
"test": "node --test tests/*.test.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eamonxg/aurora-tokens": "^1.2.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
/**
|
||||
* One-shot dev environment setup: writes .env and installs an SSH key on the
|
||||
* router so ut-sync works without any manual ssh configuration.
|
||||
* Router connection setup: writes .env and installs an SSH key on the router
|
||||
* so ut-sync works without any manual ssh configuration.
|
||||
*
|
||||
* Usage: pnpm setup (asks for the router IP, then one router password prompt)
|
||||
* Usage: pnpm setup:router [ip]
|
||||
* ip router address; omit for the interactive wizard, which asks for
|
||||
* every .env value (router IP, dev-server host/port) with the
|
||||
* current .env entries as defaults
|
||||
*
|
||||
* (The script is named `setup:router` because plain `pnpm setup` resolves to
|
||||
* pnpm's own built-in setup command, never a package.json script.)
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { execSync, spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { connect } from "node:net";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
@@ -18,22 +25,121 @@ const SSH_OPTS = [
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
];
|
||||
|
||||
// Must match the ut-sync plugin in vite.config.ts — the final push below is a
|
||||
// real template sync, so setup only succeeds if `pnpm dev` sync will too.
|
||||
const UT_TEMPLATE_DIR = resolve(
|
||||
process.cwd(),
|
||||
"../ucode/template/themes/aurora",
|
||||
);
|
||||
const UT_REMOTE_DIR = "/usr/share/ucode/luci/template/themes/aurora";
|
||||
|
||||
function canAuth(host) {
|
||||
return (
|
||||
spawnSync(
|
||||
"ssh",
|
||||
[...SSH_OPTS, "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", host, "echo ok"],
|
||||
[
|
||||
...SSH_OPTS,
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=5",
|
||||
host,
|
||||
"echo ok",
|
||||
],
|
||||
{ stdio: "ignore" },
|
||||
).status === 0
|
||||
);
|
||||
}
|
||||
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
const ip =
|
||||
(await rl.question("Router IP [192.168.1.1]: ")).trim() || "192.168.1.1";
|
||||
rl.close();
|
||||
function canReachSsh(hostname, timeout = 2000) {
|
||||
return new Promise((done) => {
|
||||
const sock = connect({ host: hostname, port: 22, timeout });
|
||||
const finish = (ok) => {
|
||||
sock.destroy();
|
||||
done(ok);
|
||||
};
|
||||
sock.once("connect", () => finish(true));
|
||||
sock.once("timeout", () => finish(false));
|
||||
sock.once("error", () => finish(false));
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Aurora router setup — passwordless SSH + .env for `pnpm dev`\n");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const argIp = args.find((a) => !a.startsWith("-"));
|
||||
|
||||
// Every variable the wizard manages (the full .env surface, see
|
||||
// .env.example); any other lines in .env pass through untouched.
|
||||
const ENV_VARS = [
|
||||
{ key: "VITE_OPENWRT_HOST", label: "Router IP", fallback: "192.168.1.1" },
|
||||
{ key: "VITE_DEV_HOST", label: "Dev server host", fallback: "127.0.0.1" },
|
||||
{ key: "VITE_DEV_PORT", label: "Dev server port", fallback: "5173" },
|
||||
];
|
||||
|
||||
const envPath = resolve(process.cwd(), ".env");
|
||||
const envLines = existsSync(envPath)
|
||||
? readFileSync(envPath, "utf-8").split("\n")
|
||||
: [];
|
||||
const savedValue = (key) =>
|
||||
envLines
|
||||
.find((l) => l.startsWith(`${key}=`))
|
||||
?.slice(key.length + 1)
|
||||
.trim();
|
||||
const values = Object.fromEntries(
|
||||
ENV_VARS.map(({ key, fallback }) => [key, savedValue(key) || fallback]),
|
||||
);
|
||||
|
||||
if (argIp) {
|
||||
// Arg mode is non-interactive: take the IP from argv and keep the saved
|
||||
// (or default) dev-server values.
|
||||
values.VITE_OPENWRT_HOST = argIp;
|
||||
} else {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
// Answers come through the async iterator — unlike rl.question, it buffers
|
||||
// lines, so piped input (`printf 'ip\nhost\nport\n' | …`) is not dropped
|
||||
// between questions, and EOF just leaves the remaining defaults in place.
|
||||
const lines = rl[Symbol.asyncIterator]();
|
||||
for (const { key, label } of ENV_VARS) {
|
||||
rl.setPrompt(`${label} [${values[key]}]: `);
|
||||
rl.prompt();
|
||||
const { value, done } = await lines.next();
|
||||
if (done) {
|
||||
console.log();
|
||||
break;
|
||||
}
|
||||
const answer = value.trim();
|
||||
if (answer) values[key] = answer;
|
||||
}
|
||||
rl.close();
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(values.VITE_DEV_PORT)) {
|
||||
console.log(
|
||||
` Ignoring invalid port "${values.VITE_DEV_PORT}" — using 5173.`,
|
||||
);
|
||||
values.VITE_DEV_PORT = "5173";
|
||||
}
|
||||
|
||||
const ip = values.VITE_OPENWRT_HOST;
|
||||
|
||||
// Accept the same forms vite.config.ts does (bare IP, host:port, http:// URL);
|
||||
// ssh always targets the bare hostname — port/key overrides live in ~/.ssh/config.
|
||||
const hostname = new URL(/^https?:\/\//.test(ip) ? ip : `http://${ip}`)
|
||||
.hostname;
|
||||
const host = `root@${hostname}`;
|
||||
|
||||
if (!(await canReachSsh(hostname))) {
|
||||
console.error(`✗ Cannot reach ${hostname}:22 (2s timeout).`);
|
||||
console.error(
|
||||
" Check that the device is powered on, on the same network, and has SSH enabled.",
|
||||
);
|
||||
console.error(
|
||||
" (If it listens on a non-standard port, add a Host block to ~/.ssh/config.)",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const host = `root@${ip}`;
|
||||
const sshDir = join(homedir(), ".ssh");
|
||||
|
||||
let pubKeyPath = ["id_ed25519.pub", "id_rsa.pub", "id_ecdsa.pub"]
|
||||
@@ -55,39 +161,93 @@ if (!pubKeyPath) {
|
||||
}
|
||||
|
||||
if (canAuth(host)) {
|
||||
console.log(`✓ Passwordless SSH to ${host} already works.`);
|
||||
console.log(
|
||||
`✓ Passwordless SSH to ${host} already works (checked: ssh echo).`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Installing ${pubKeyPath} on the router (enter the router password once):`,
|
||||
);
|
||||
const pubKey = readFileSync(pubKeyPath, "utf-8").trim();
|
||||
// stderr is piped so failures can be classified below; ssh's password prompt
|
||||
// goes straight to the tty, so interactive login still works.
|
||||
const install = spawnSync(
|
||||
"ssh",
|
||||
[
|
||||
...SSH_OPTS,
|
||||
"-o",
|
||||
"ConnectTimeout=5",
|
||||
host,
|
||||
`mkdir -p /etc/dropbear && echo '${pubKey}' >> /etc/dropbear/authorized_keys && chmod 600 /etc/dropbear/authorized_keys`,
|
||||
`mkdir -p /etc/dropbear && touch /etc/dropbear/authorized_keys && (grep -qxF '${pubKey}' /etc/dropbear/authorized_keys || echo '${pubKey}' >> /etc/dropbear/authorized_keys) && chmod 600 /etc/dropbear/authorized_keys`,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
{ stdio: ["inherit", "inherit", "pipe"], encoding: "utf-8" },
|
||||
);
|
||||
if (install.status !== 0 || !canAuth(host)) {
|
||||
if (install.status !== 0) {
|
||||
const stderr = (install.stderr || "").trim();
|
||||
if (/Permission denied|Authentication failed/i.test(stderr)) {
|
||||
console.error(
|
||||
"✗ Authentication failed — likely a wrong password. Re-run pnpm setup:router.",
|
||||
);
|
||||
console.error(
|
||||
" (If the device runs openssh instead of dropbear, keys belong in /root/.ssh/authorized_keys.)",
|
||||
);
|
||||
} else if (
|
||||
/timed out|No route to host|Connection refused|Could not resolve/i.test(
|
||||
stderr,
|
||||
)
|
||||
) {
|
||||
console.error(`✗ ${hostname} did not respond as an SSH host.`);
|
||||
console.error(
|
||||
" Check the IP and that SSH is enabled on the device. (A VPN/proxy in TUN mode can also intercept the connection.)",
|
||||
);
|
||||
} else {
|
||||
console.error("✗ Key install failed:");
|
||||
}
|
||||
if (stderr) console.error(` ${stderr.replace(/\n/g, "\n ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!canAuth(host)) {
|
||||
console.error("✗ Key installed, but passwordless SSH still fails.");
|
||||
console.error(
|
||||
`✗ Key install failed. Check that the device at ${ip} is reachable and the password is correct, then re-run pnpm setup.`,
|
||||
" Check the dropbear config on the device (PubkeyAuth) and /etc/dropbear/authorized_keys.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("✓ Passwordless SSH configured.");
|
||||
}
|
||||
|
||||
const envPath = resolve(process.cwd(), ".env");
|
||||
const kept = existsSync(envPath)
|
||||
? readFileSync(envPath, "utf-8")
|
||||
.split("\n")
|
||||
.filter((l) => l && !l.startsWith("VITE_OPENWRT_HOST="))
|
||||
: [];
|
||||
writeFileSync(
|
||||
envPath,
|
||||
[`VITE_OPENWRT_HOST=${ip}`, ...kept].join("\n") + "\n",
|
||||
// End-to-end check: push the templates through the exact pipeline ut-sync uses.
|
||||
// `ssh echo ok` alone can pass while the real tar-over-ssh sync would not.
|
||||
const started = Date.now();
|
||||
try {
|
||||
execSync(
|
||||
`tar -C "${UT_TEMPLATE_DIR}" -cf - . | ssh ${SSH_OPTS.join(" ")} -o BatchMode=yes -o ConnectTimeout=5 "${host}" "mkdir -p '${UT_REMOTE_DIR}' && tar -xf - -C '${UT_REMOTE_DIR}'"`,
|
||||
{ stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
console.log(
|
||||
`✓ Templates pushed to ${host}:${UT_REMOTE_DIR} (${Date.now() - started}ms) — dev sync verified.`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"✗ Template push failed — `pnpm dev` template sync will not work:",
|
||||
);
|
||||
console.error(` ${String(err?.stderr || err?.message || err).trim()}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Update managed keys in place so surrounding comments keep their meaning,
|
||||
// then append any the file does not have yet; other lines pass through.
|
||||
const out = envLines.map((l) => {
|
||||
const managed = ENV_VARS.find(({ key }) => l.startsWith(`${key}=`));
|
||||
return managed ? `${managed.key}=${values[managed.key]}` : l;
|
||||
});
|
||||
while (out.length && !out[out.length - 1]) out.pop();
|
||||
for (const { key } of ENV_VARS) {
|
||||
if (!out.some((l) => l.startsWith(`${key}=`)))
|
||||
out.push(`${key}=${values[key]}`);
|
||||
}
|
||||
writeFileSync(envPath, out.join("\n") + "\n");
|
||||
console.log(
|
||||
`✓ Wrote .env (${ENV_VARS.map(({ key }) => `${key}=${values[key]}`).join(", ")})`,
|
||||
);
|
||||
console.log(`✓ Wrote .env (VITE_OPENWRT_HOST=${ip})`);
|
||||
console.log("Done — run `pnpm dev`.");
|
||||
@@ -185,7 +185,7 @@ function reportSshError(err: any, sshHost: string): void {
|
||||
) {
|
||||
console.error(`\n${utTag()} SSH authentication failed for ${sshHost}.`);
|
||||
console.error(
|
||||
`${utTag()} Run \`pnpm setup\` to configure passwordless login.\n`,
|
||||
`${utTag()} Run \`pnpm setup:router\` to configure passwordless login.\n`,
|
||||
);
|
||||
} else if (
|
||||
stderr.includes("Connection refused") ||
|
||||
|
||||
@@ -8,7 +8,7 @@ All dev commands run from `.dev/`:
|
||||
|
||||
```bash
|
||||
cd .dev/
|
||||
pnpm setup # One-shot dev setup: router IP → .env, installs SSH key on device
|
||||
pnpm setup:router # One-shot dev setup: all .env values (router IP, dev host/port) → .env, installs SSH key on device ([ip] to run non-interactively; safe to re-run after a reflash)
|
||||
pnpm dev # Start Vite dev server (proxies to OpenWrt device)
|
||||
pnpm build # Clean + regenerate tokens + build production assets to htdocs/luci-static/
|
||||
pnpm gen:tokens # Regenerate src/media/_tokens.css from tokens/ (also runs as part of build)
|
||||
|
||||
@@ -9,7 +9,7 @@ LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CS
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=1.1.0
|
||||
PKG_RELEASE:=49
|
||||
PKG_RELEASE:=50
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
Reference in new issue
Block a user