🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

openpocket

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

openpocket - npm Package Compare versions

Comparing version
0.2.0-mvp-ts
to
0.2.1
+298
dist/environment/android-prerequisites.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureAndroidPrerequisites = ensureAndroidPrerequisites;
const node_fs_1 = __importDefault(require("node:fs"));
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const node_child_process_1 = require("node:child_process");
const paths_1 = require("../utils/paths");
function run(cmd, args, options = {}) {
const result = (0, node_child_process_1.spawnSync)(cmd, args, {
encoding: "utf-8",
env: options.env,
input: options.input,
stdio: options.inherit ? "inherit" : ["pipe", "pipe", "pipe"],
});
const status = result.status ?? 1;
const stdout = typeof result.stdout === "string" ? result.stdout : "";
const stderr = typeof result.stderr === "string" ? result.stderr : "";
return {
ok: status === 0 && !result.error,
status,
stdout,
stderr,
error: result.error ? String(result.error.message || result.error) : null,
};
}
function canExecute(filePath) {
try {
node_fs_1.default.accessSync(filePath, node_fs_1.default.constants.X_OK);
return true;
}
catch {
return false;
}
}
function firstExecutable(candidates) {
for (const candidate of candidates) {
if (!candidate) {
continue;
}
const resolved = node_path_1.default.resolve(candidate);
if (node_fs_1.default.existsSync(resolved) && canExecute(resolved)) {
return resolved;
}
}
return null;
}
function findInPath(binName) {
const entries = (process.env.PATH ?? "")
.split(node_path_1.default.delimiter)
.map((v) => v.trim())
.filter(Boolean);
for (const entry of entries) {
const candidate = node_path_1.default.join(entry, binName);
if (node_fs_1.default.existsSync(candidate) && canExecute(candidate)) {
return candidate;
}
}
return null;
}
function collectSdkRoot(config) {
const configured = config.emulator.androidSdkRoot.trim();
if (configured) {
return { sdkRoot: node_path_1.default.resolve(configured), configUpdated: false };
}
const envRoot = process.env.ANDROID_SDK_ROOT?.trim() || process.env.ANDROID_HOME?.trim() || "";
const sdkRoot = envRoot ? node_path_1.default.resolve(envRoot) : node_path_1.default.join(node_os_1.default.homedir(), "Library", "Android", "sdk");
config.emulator.androidSdkRoot = sdkRoot;
return { sdkRoot, configUpdated: true };
}
function detectTools(sdkRoot) {
const fallbackSdk = node_path_1.default.join(node_os_1.default.homedir(), "Library", "Android", "sdk");
const sdkRoots = Array.from(new Set([sdkRoot, fallbackSdk]));
const adbCandidates = sdkRoots
.map((root) => node_path_1.default.join(root, "platform-tools", "adb"))
.concat(["/opt/homebrew/bin/adb", "/usr/local/bin/adb"]);
const emulatorCandidates = sdkRoots
.map((root) => node_path_1.default.join(root, "emulator", "emulator"))
.concat([
"/opt/homebrew/share/android-commandlinetools/emulator/emulator",
"/usr/local/share/android-commandlinetools/emulator/emulator",
]);
const sdkManagerCandidates = sdkRoots
.map((root) => node_path_1.default.join(root, "cmdline-tools", "latest", "bin", "sdkmanager"))
.concat([
"/opt/homebrew/share/android-commandlinetools/cmdline-tools/latest/bin/sdkmanager",
"/usr/local/share/android-commandlinetools/cmdline-tools/latest/bin/sdkmanager",
]);
const avdManagerCandidates = sdkRoots
.map((root) => node_path_1.default.join(root, "cmdline-tools", "latest", "bin", "avdmanager"))
.concat([
"/opt/homebrew/share/android-commandlinetools/cmdline-tools/latest/bin/avdmanager",
"/usr/local/share/android-commandlinetools/cmdline-tools/latest/bin/avdmanager",
]);
return {
adb: firstExecutable(adbCandidates) ?? findInPath("adb"),
emulator: firstExecutable(emulatorCandidates) ?? findInPath("emulator"),
sdkmanager: firstExecutable(sdkManagerCandidates) ?? findInPath("sdkmanager"),
avdmanager: firstExecutable(avdManagerCandidates) ?? findInPath("avdmanager"),
};
}
function missingTools(toolPaths) {
const required = ["adb", "emulator", "sdkmanager", "avdmanager"];
return required.filter((name) => !toolPaths[name]);
}
function resolveBrewBinary() {
return firstExecutable(["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]) ?? findInPath("brew");
}
function extendProcessPathForBrew() {
const extra = ["/opt/homebrew/bin", "/usr/local/bin"];
const entries = (process.env.PATH ?? "")
.split(node_path_1.default.delimiter)
.filter(Boolean);
for (const candidate of extra) {
if (!entries.includes(candidate) && node_fs_1.default.existsSync(candidate)) {
entries.unshift(candidate);
}
}
process.env.PATH = entries.join(node_path_1.default.delimiter);
}
function installHomebrew(logger) {
logger("Homebrew not found. Installing Homebrew...");
const result = run("/usr/bin/env", [
"bash",
"-lc",
"NONINTERACTIVE=1 /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"",
], { inherit: true });
if (!result.ok) {
throw new Error("Failed to install Homebrew automatically.");
}
extendProcessPathForBrew();
}
function installBrewCask(brew, cask, logger) {
const exists = run(brew, ["list", "--cask", cask]);
if (exists.ok) {
logger(`brew cask '${cask}' already installed (skip).`);
return false;
}
logger(`Installing brew cask '${cask}'...`);
const installed = run(brew, ["install", "--cask", cask], { inherit: true });
if (!installed.ok) {
throw new Error(`brew install --cask ${cask} failed.`);
}
return true;
}
function acceptSdkLicenses(sdkmanager, sdkRoot, logger) {
logger("Accepting Android SDK licenses...");
const res = run(sdkmanager, [`--sdk_root=${sdkRoot}`, "--licenses"], {
input: `${"y\n".repeat(200)}`,
});
if (!res.ok) {
logger("SDK licenses command returned non-zero; continuing.");
}
}
function installSdkPackages(sdkmanager, sdkRoot, logger) {
logger("Installing Android SDK packages: platform-tools, emulator, platforms;android-34 ...");
const result = run(sdkmanager, [`--sdk_root=${sdkRoot}`, "platform-tools", "emulator", "platforms;android-34"], { inherit: true });
if (!result.ok) {
throw new Error("Failed to install required Android SDK packages.");
}
}
function installOneSystemImage(sdkmanager, sdkRoot, logger) {
const archTag = process.arch === "arm64" ? "arm64-v8a" : "x86_64";
const candidates = Array.from(new Set([
`system-images;android-34;google_apis_playstore;${archTag}`,
"system-images;android-34;google_apis_playstore;x86_64",
"system-images;android-34;google_apis_playstore;arm64-v8a",
"system-images;android-34;google_apis;x86_64",
"system-images;android-34;google_apis;arm64-v8a",
]));
for (const pkg of candidates) {
logger(`Trying system image: ${pkg}`);
const res = run(sdkmanager, [`--sdk_root=${sdkRoot}`, pkg], { inherit: true });
if (res.ok) {
logger(`System image ready: ${pkg}`);
return pkg;
}
}
logger("Could not install a system image automatically.");
return null;
}
function listAvdNames(avdmanager, sdkRoot) {
const env = {
...process.env,
ANDROID_SDK_ROOT: sdkRoot,
ANDROID_HOME: sdkRoot,
};
const result = run(avdmanager, ["list", "avd"], { env });
if (!result.ok) {
return [];
}
const names = [];
const regex = /^Name:\s*(.+)$/gm;
let match = regex.exec(result.stdout);
while (match) {
names.push(match[1].trim());
match = regex.exec(result.stdout);
}
return names;
}
function createAvd(avdmanager, sdkRoot, avdName, imagePackage, logger) {
logger(`Creating AVD '${avdName}' with image '${imagePackage}'...`);
const env = {
...process.env,
ANDROID_SDK_ROOT: sdkRoot,
ANDROID_HOME: sdkRoot,
};
const result = run(avdmanager, ["create", "avd", "--force", "-n", avdName, "-k", imagePackage], { env, input: "no\n" });
if (!result.ok) {
logger("Failed to create AVD automatically.");
return false;
}
return true;
}
async function ensureAndroidPrerequisites(config, options = {}) {
const logger = options.logger ?? (() => { });
const autoInstall = options.autoInstall !== false;
if (process.env.OPENPOCKET_SKIP_ENV_SETUP === "1") {
const { sdkRoot, configUpdated } = collectSdkRoot(config);
return {
skipped: true,
configUpdated,
sdkRoot,
toolPaths: detectTools(sdkRoot),
installedSteps: [],
avdCreated: false,
};
}
const { sdkRoot, configUpdated } = collectSdkRoot(config);
(0, paths_1.ensureDir)(sdkRoot);
let tools = detectTools(sdkRoot);
let missing = missingTools(tools);
const installedSteps = [];
let avdCreated = false;
if (missing.length > 0) {
if (!autoInstall) {
throw new Error(`Missing Android prerequisites: ${missing.join(", ")}`);
}
if (process.platform !== "darwin") {
throw new Error(`Missing Android prerequisites on ${process.platform}: ${missing.join(", ")}. Auto-install currently supports macOS only.`);
}
let brew = resolveBrewBinary();
if (!brew) {
installHomebrew(logger);
installedSteps.push("homebrew");
brew = resolveBrewBinary();
}
if (!brew) {
throw new Error("Homebrew was not found after installation attempt.");
}
if (installBrewCask(brew, "android-platform-tools", logger)) {
installedSteps.push("brew:android-platform-tools");
}
if (installBrewCask(brew, "android-commandlinetools", logger)) {
installedSteps.push("brew:android-commandlinetools");
}
tools = detectTools(sdkRoot);
missing = missingTools(tools);
}
if (missing.length > 0) {
throw new Error(`Missing Android prerequisites after installation: ${missing.join(", ")}`);
}
const sdkmanager = tools.sdkmanager;
const avdmanager = tools.avdmanager;
if (!sdkmanager || !avdmanager) {
throw new Error("sdkmanager or avdmanager is not available.");
}
acceptSdkLicenses(sdkmanager, sdkRoot, logger);
installSdkPackages(sdkmanager, sdkRoot, logger);
installedSteps.push("sdk:platform-tools,emulator,platforms;android-34");
const currentAvds = listAvdNames(avdmanager, sdkRoot);
if (!currentAvds.includes(config.emulator.avdName)) {
const image = installOneSystemImage(sdkmanager, sdkRoot, logger);
if (image) {
installedSteps.push(`sdk:${image}`);
avdCreated = createAvd(avdmanager, sdkRoot, config.emulator.avdName, image, logger);
if (!avdCreated) {
throw new Error(`Failed to create AVD '${config.emulator.avdName}'.`);
}
installedSteps.push(`avd:${config.emulator.avdName}`);
}
else if (currentAvds.length === 0) {
throw new Error("No AVD exists and no installable system image was found.");
}
}
tools = detectTools(sdkRoot);
return {
skipped: false,
configUpdated,
sdkRoot,
toolPaths: tools,
installedSteps,
avdCreated,
};
}
+131
-39

@@ -20,2 +20,4 @@ #!/usr/bin/env node

const cli_shortcut_1 = require("./install/cli-shortcut");
const android_prerequisites_1 = require("./environment/android-prerequisites");
const DEFAULT_PANEL_RELEASE_URL = "https://github.com/SergioChan/openpocket/releases/latest";
function printHelp() {

@@ -25,5 +27,3 @@ // eslint-disable-next-line no-console

Usage:
openpocket [--config <path>] init
openpocket [--config <path>] install-cli
openpocket [--config <path>] setup
openpocket [--config <path>] onboard

@@ -44,6 +44,8 @@ openpocket [--config <path>] config-show

Legacy aliases (deprecated):
openpocket [--config <path>] init
openpocket [--config <path>] setup
Examples:
openpocket init
openpocket install-cli
openpocket setup
openpocket onboard
openpocket emulator start

@@ -57,2 +59,51 @@ openpocket agent --model gpt-5.2-codex "Open Chrome and search weather"

}
function getPanelReleaseUrl() {
const fromEnv = process.env.OPENPOCKET_PANEL_RELEASE_URL?.trim();
if (fromEnv) {
return fromEnv;
}
try {
const packageJsonPath = node_path_1.default.resolve(__dirname, "..", "package.json");
const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf-8"));
if (pkg.homepage?.trim()) {
return pkg.homepage.includes("/releases")
? pkg.homepage
: `${pkg.homepage.replace(/\/$/, "")}/releases/latest`;
}
const repoUrlRaw = typeof pkg.repository === "string"
? pkg.repository
: pkg.repository?.url;
const repoUrl = repoUrlRaw?.replace(/^git\+/, "").replace(/\.git$/, "");
if (repoUrl?.includes("github.com")) {
const normalized = repoUrl.replace(/^git@github.com:/, "https://github.com/");
return `${normalized.replace(/\/$/, "")}/releases/latest`;
}
}
catch {
// ignore and fallback
}
return DEFAULT_PANEL_RELEASE_URL;
}
function resolveInstalledPanelApp() {
const home = process.env.HOME ?? "";
const candidates = [
"/Applications/OpenPocket Control Panel.app",
node_path_1.default.join(home, "Applications", "OpenPocket Control Panel.app"),
"/Applications/OpenPocketMenuBar.app",
node_path_1.default.join(home, "Applications", "OpenPocketMenuBar.app"),
].filter(Boolean);
for (const appPath of candidates) {
if (node_fs_1.default.existsSync(appPath)) {
return appPath;
}
}
return null;
}
function openPanelApp(appPath) {
const result = (0, node_child_process_1.spawnSync)("/usr/bin/open", [appPath], { stdio: "ignore" });
return (result.status ?? 1) === 0;
}
function openReleasePage(url) {
(0, node_child_process_1.spawnSync)("/usr/bin/open", [url], { stdio: "ignore" });
}
function takeOption(args, name) {

@@ -155,5 +206,16 @@ const out = [];

}
async function runSetupCommand(configPath) {
async function runBootstrapCommand(configPath) {
const cfg = (0, config_1.loadConfig)(configPath);
await (0, android_prerequisites_1.ensureAndroidPrerequisites)(cfg, {
autoInstall: true,
logger: (line) => {
// eslint-disable-next-line no-console
console.log(`[OpenPocket][env] ${line}`);
},
});
(0, config_1.saveConfig)(cfg);
return cfg;
}
async function runOnboardCommand(configPath) {
const cfg = await runBootstrapCommand(configPath);
await (0, setup_wizard_1.runSetupWizard)(cfg);

@@ -245,35 +307,53 @@ return 0;

}
const installedApp = resolveInstalledPanelApp();
if (installedApp) {
if (!openPanelApp(installedApp)) {
throw new Error(`Failed to open installed panel app: ${installedApp}`);
}
// eslint-disable-next-line no-console
console.log(`OpenPocket Control Panel opened: ${installedApp}`);
return 0;
}
const panelDir = node_path_1.default.resolve(__dirname, "..", "apps", "openpocket-menubar");
const buildScript = node_path_1.default.join(panelDir, "scripts", "build.sh");
const runScript = node_path_1.default.join(panelDir, "scripts", "run.sh");
if (!node_fs_1.default.existsSync(runScript) || !node_fs_1.default.existsSync(buildScript)) {
throw new Error(`Menu bar app launcher not found: ${runScript}`);
const hasBundledSource = node_fs_1.default.existsSync(runScript) && node_fs_1.default.existsSync(buildScript);
if (hasBundledSource) {
const buildResult = (0, node_child_process_1.spawnSync)("/usr/bin/env", ["bash", buildScript], {
stdio: "inherit",
cwd: panelDir,
});
if (buildResult.error) {
throw buildResult.error;
}
if ((buildResult.status ?? 1) !== 0) {
return buildResult.status ?? 1;
}
const appBinary = node_path_1.default.join(panelDir, ".build", "debug", "OpenPocketMenuBar");
if (!node_fs_1.default.existsSync(appBinary)) {
throw new Error(`Built menu bar app not found: ${appBinary}`);
}
const env = { ...process.env };
if (configPath?.trim()) {
env.OPENPOCKET_CONFIG_PATH = node_path_1.default.resolve(configPath.trim());
}
const child = (0, node_child_process_1.spawn)(appBinary, [], {
cwd: panelDir,
detached: true,
stdio: "ignore",
env,
});
child.unref();
// eslint-disable-next-line no-console
console.log(`OpenPocket Control Panel started (pid=${child.pid ?? "unknown"}).`);
return 0;
}
const buildResult = (0, node_child_process_1.spawnSync)("/usr/bin/env", ["bash", buildScript], {
stdio: "inherit",
cwd: panelDir,
});
if (buildResult.error) {
throw buildResult.error;
}
if ((buildResult.status ?? 1) !== 0) {
return buildResult.status ?? 1;
}
const appBinary = node_path_1.default.join(panelDir, ".build", "debug", "OpenPocketMenuBar");
if (!node_fs_1.default.existsSync(appBinary)) {
throw new Error(`Built menu bar app not found: ${appBinary}`);
}
const env = { ...process.env };
if (configPath?.trim()) {
env.OPENPOCKET_CONFIG_PATH = node_path_1.default.resolve(configPath.trim());
}
const child = (0, node_child_process_1.spawn)(appBinary, [], {
cwd: panelDir,
detached: true,
stdio: "ignore",
env,
});
child.unref();
const releaseUrl = getPanelReleaseUrl();
// eslint-disable-next-line no-console
console.log(`OpenPocket Control Panel started (pid=${child.pid ?? "unknown"}).`);
console.log("OpenPocket panel app is not installed on this Mac.");
// eslint-disable-next-line no-console
console.log(`Opening download page: ${releaseUrl}`);
openReleasePage(releaseUrl);
// eslint-disable-next-line no-console
console.log("Install the macOS PKG from Releases, then run: openpocket panel start");
return 0;

@@ -289,6 +369,13 @@ }

if (command === "init") {
const cfg = (0, config_1.loadConfig)(configPath ?? undefined);
(0, config_1.saveConfig)(cfg);
// eslint-disable-next-line no-console
console.log(`OpenPocket initialized.\nConfig: ${cfg.configPath}`);
console.log("[OpenPocket] `init` is deprecated. Use `openpocket onboard`.");
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
if (interactive) {
return runOnboardCommand(configPath ?? undefined);
}
const cfg = await runBootstrapCommand(configPath ?? undefined);
// eslint-disable-next-line no-console
console.log(`OpenPocket bootstrap completed.\nConfig: ${cfg.configPath}`);
// eslint-disable-next-line no-console
console.log("Run `openpocket onboard` in an interactive terminal to complete consent/model/API key onboarding.");
return 0;

@@ -323,5 +410,10 @@ }

}
if (command === "setup" || command === "onboard") {
return runSetupCommand(configPath ?? undefined);
if (command === "setup") {
// eslint-disable-next-line no-console
console.log("[OpenPocket] `setup` is deprecated. Use `openpocket onboard`.");
return runOnboardCommand(configPath ?? undefined);
}
if (command === "onboard") {
return runOnboardCommand(configPath ?? undefined);
}
throw new Error(`Unknown command: ${command}`);

@@ -328,0 +420,0 @@ }

{
"name": "openpocket",
"version": "0.2.0-mvp-ts",
"version": "0.2.1",
"description": "OpenPocket Node.js TypeScript runtime",
"repository": {
"type": "git",
"url": "git+https://github.com/SergioChan/openpocket.git"
},
"homepage": "https://github.com/SergioChan/openpocket",
"main": "dist/cli.js",

@@ -23,2 +28,3 @@ "bin": {

"docs:build": "vitepress build docs",
"docs:build:pages": "DOCS_BASE=/openpocket/ vitepress build docs",
"docs:preview": "vitepress preview docs --host"

@@ -36,6 +42,8 @@ },

"@types/node-telegram-bot-api": "^0.64.7",
"mermaid": "^11.12.3",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"vitepress": "^1.6.4"
"vitepress": "^1.6.4",
"vitepress-plugin-mermaid": "^2.0.17"
}
}
+91
-30

@@ -57,26 +57,45 @@ # OpenPocket

### 2. Install and initialize (npm package)
### 2. Option A: Use the npm package (no source code required)
After publishing to npm, use:
```bash
npm install -g openpocket
openpocket init
openpocket onboard
```
### 3. Install and initialize (local clone, no global install)
If you use the native macOS panel, install the release package from:
- [OpenPocket Releases](https://github.com/SergioChan/openpocket/releases)
Then start the panel:
```bash
cd /Users/sergiochan/Documents/GitHub/phone-use-agent
openpocket panel start
```
### 3. Option B: Use a local source clone (for contributors)
```bash
git clone git@github.com:SergioChan/openpocket.git
cd openpocket
npm install
./openpocket init
npm run build
./openpocket onboard
```
`./openpocket` automatically runs `dist/cli.js` when present, and falls back to `tsx src/cli.ts` in dev installs.
`./openpocket` runs `dist/cli.js` when present and falls back to `tsx src/cli.ts` in local dev installs.
`openpocket onboard` automatically verifies Android runtime dependencies:
1. If local tools are already installed, dependency installation is skipped.
2. If tools are missing on macOS, OpenPocket tries automatic installation (Homebrew, Android SDK packages, and default AVD bootstrap).
You can skip this step in CI/tests with:
```bash
export OPENPOCKET_SKIP_ENV_SETUP=1
```
### 4. Start runtime
Use `openpocket ...` (npm global install) or `./openpocket ...` (local clone):
For npm package install:

@@ -88,9 +107,17 @@ ```bash

### 5. Command resolution and PATH behavior
For local source clone:
- `init` / `setup` / `onboard` do **not** modify your shell config or PATH.
- Use `openpocket ...` when installed from npm globally.
- Use `./openpocket ...` when running from a local cloned repository.
- `openpocket install-cli` is optional and explicit; run it only if you want a user-local launcher under `~/.local/bin/openpocket`.
```bash
./openpocket emulator start
./openpocket gateway start
```
### 5. Optional: install a user-local command
```bash
./openpocket install-cli
```
This installs `~/.local/bin/openpocket` and updates shell rc files when needed.
## Configuration

@@ -119,22 +146,40 @@

Use `openpocket ...` if installed via npm global package, or `./openpocket ...` in a local clone.
Command prefix by install mode:
- npm package install: use `openpocket ...`
- local source clone: use `./openpocket ...` (or `openpocket ...` after `install-cli`)
```bash
openpocket --help
openpocket init
openpocket install-cli
openpocket setup
openpocket onboard
openpocket config-show
openpocket emulator start
openpocket emulator status
openpocket agent --model gpt-5.2-codex "Open Chrome and search weather"
openpocket script run --text "echo hello"
openpocket skills list
openpocket gateway start
openpocket panel start
./openpocket --help
./openpocket install-cli
./openpocket onboard
./openpocket config-show
./openpocket emulator start
./openpocket emulator status
./openpocket agent --model gpt-5.2-codex "Open Chrome and search weather"
./openpocket script run --text "echo hello"
./openpocket skills list
./openpocket gateway start
./openpocket panel start
```
Legacy aliases still work (deprecated): `openpocket init`, `openpocket setup`.
`openpocket panel start` on macOS uses this order:
1. Open an already-installed panel app from `/Applications` or `~/Applications`.
2. If running from a source clone with `apps/openpocket-menubar`, build and launch from source.
3. If neither is available (typical npm install), open GitHub Releases and guide PKG installation.
## Documentation
### Where the frontend is
The documentation frontend is implemented in this repository:
- Site source: [`/docs`](./docs)
- VitePress config: [`/docs/.vitepress/config.mjs`](./docs/.vitepress/config.mjs)
- Custom homepage: [`/docs/index.md`](./docs/index.md)
- Custom theme styles: [`/docs/.vitepress/theme/custom.css`](./docs/.vitepress/theme/custom.css)
### Documentation Website

@@ -154,2 +199,8 @@

- Build for GitHub Pages (project-path base):
```bash
npm run docs:build:pages
```
- Preview built docs:

@@ -161,2 +212,12 @@

### Deployment options
- GitHub Pages workflow: [`.github/workflows/deploy-docs-pages.yml`](./.github/workflows/deploy-docs-pages.yml)
- Vercel config: [`vercel.json`](./vercel.json)
- Deployment guide: [`/docs/get-started/deploy-docs.md`](./docs/get-started/deploy-docs.md)
Expected GitHub Pages URL for this repo:
- `https://sergiochan.github.io/openpocket/`
### Docs entry points

@@ -166,4 +227,4 @@

- [Documentation Hubs](./docs/hubs.md)
- [Get Started](./docs/get-started/README.md)
- [Reference](./docs/reference/README.md)
- [Get Started](./docs/get-started/index.md)
- [Reference](./docs/reference/index.md)
- [Ops Runbook](./docs/ops/runbook.md)

@@ -170,0 +231,0 @@