diff --git a/.agents/skills/automate-zero-native/SKILL.md b/.agents/skills/automate-zero-native/SKILL.md deleted file mode 100644 index 5598894..0000000 --- a/.agents/skills/automate-zero-native/SKILL.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: automate-zero-native -description: Automate and inspect running zero-native WebView shell apps via the built-in automation server. Use when the user asks to test the app, list windows, take a screenshot, inspect a snapshot, reload the WebView, or verify a running zero-native example. ---- - -# Automate zero-native apps - -zero-native has a built-in automation system for inspecting running WebView shell apps. It works through file-based IPC in `.zig-cache/zero-native-automation/`. - -## Prerequisites - -Run an app with automation enabled: - -```bash -zig build run-webview -Dplatform=macos -Dautomation=true -``` - -## Commands - -```bash -zig build -zig-out/bin/zero-native automate list -zig-out/bin/zero-native automate snapshot -zig-out/bin/zero-native automate screenshot [path] -zig-out/bin/zero-native automate reload -``` - -## Workflow - -1. Start the app with automation enabled. -2. Run `zig-out/bin/zero-native automate snapshot` to confirm the window and WebView source. -3. Use `zig-out/bin/zero-native automate reload` to request a reload. -4. Use `zig-out/bin/zero-native automate screenshot [path]` when a placeholder screenshot artifact is enough. - -## Notes - -- Automation is compile-time gated: apps built without `-Dautomation=true` ignore automation files. -- The current screenshot artifact is a placeholder PPM. -- WebView DOM interaction is intentionally out of scope for this file-based automation layer. diff --git a/docs/src/app/cli/page.mdx b/docs/src/app/cli/page.mdx index d7df276..588d00a 100644 --- a/docs/src/app/cli/page.mdx +++ b/docs/src/app/cli/page.mdx @@ -68,6 +68,14 @@ The `zero-native` CLI provides project scaffolding, validation, packaging, and d zero-native automate <command> Interact with the automation server + + zero-native skills list + List built-in AI agent skills + + + zero-native skills get <name> [--full] + Output built-in AI agent skill content + zero-native version Print the zero-native version @@ -75,6 +83,35 @@ The `zero-native` CLI provides project scaffolding, validation, packaging, and d +## `zero-native skills` subcommands + + + + + + + + + + + + + + + + + + + + + + + + + + +
SubcommandDescription
skills listList built-in skills served by the installed CLI
skills get coreOutput the zero-native app-building guide for AI agents
skills get automationOutput the running-app automation guide for AI agents
skills get --all [--full]Output every non-hidden skill; --full also includes reference and template files when present
+ ## `zero-native cef` flags diff --git a/packages/zero-native/README.md b/packages/zero-native/README.md index ebae2d8..ac551ba 100644 --- a/packages/zero-native/README.md +++ b/packages/zero-native/README.md @@ -29,6 +29,8 @@ The first run installs the generated frontend dependencies automatically. | `zero-native package` | Package the app for distribution | | `zero-native bundle-assets` | Copy frontend assets into the build output | | `zero-native automate` | Interact with a running app's automation server | +| `zero-native skills list` | List built-in AI agent skills | +| `zero-native skills get ` | Output AI agent skill content | | `zero-native version` | Print the zero-native version | ## More diff --git a/packages/zero-native/package.json b/packages/zero-native/package.json index 828fd10..64af080 100644 --- a/packages/zero-native/package.json +++ b/packages/zero-native/package.json @@ -9,6 +9,8 @@ "types": "./zero-native.d.ts", "files": [ "bin", + "skill-data", + "skills", "scripts", "src", "zero-native.d.ts", diff --git a/packages/zero-native/scripts/copy-framework.js b/packages/zero-native/scripts/copy-framework.js index 9fd6ee5..c465df0 100644 --- a/packages/zero-native/scripts/copy-framework.js +++ b/packages/zero-native/scripts/copy-framework.js @@ -15,3 +15,11 @@ rmSync(targetDir, { recursive: true, force: true }); cpSync(sourceDir, targetDir, { recursive: true }); console.log(`✓ Copied framework sources to ${targetDir}`); + +for (const dir of ['skills', 'skill-data']) { + const source = join(repoRoot, dir); + const target = join(projectRoot, dir); + rmSync(target, { recursive: true, force: true }); + cpSync(source, target, { recursive: true }); + console.log(`✓ Copied ${dir} to ${target}`); +} diff --git a/skill-data/automation/SKILL.md b/skill-data/automation/SKILL.md new file mode 100644 index 0000000..bd131c7 --- /dev/null +++ b/skill-data/automation/SKILL.md @@ -0,0 +1,169 @@ +--- +name: automation +description: Automation and verification guide for running zero-native WebView shell apps. Use when the user asks to test an app, inspect runtime state, list windows, wait for readiness, reload a WebView, send bridge commands, debug why automation is not connected, create smoke tests, or verify a zero-native example in a GUI-capable session. +--- + +# Automate zero-native apps + +zero-native has a built-in automation system for inspecting running WebView shell apps. It works through file-based IPC in `.zig-cache/zero-native-automation/` and is intended for smoke tests, CI checks with a GUI session, and quick runtime inspection. + +Automation is not browser DOM automation. It reports runtime/window/source state and can ask the runtime to reload or dispatch bridge requests. For frontend DOM testing, use the frontend framework's tests or a browser automation tool against the dev server. + +## What automation can verify + +- An automation-enabled app started and published `ready=true`. +- The runtime loaded the expected app name, source kind, and window metadata. +- The main window exists and is focused/open. +- The JavaScript-to-Zig bridge can round-trip a request through `zero-native automate bridge`. +- Builtin window/WebView commands work when exercised by a smoke test. +- Reload requests are accepted by the runtime. + +## What automation cannot verify + +- Real screenshots. Current `screenshot` support is a placeholder/unsupported depending on backend. +- Arbitrary DOM queries and clicks. +- Visual layout correctness. +- Browser network assertions. + +## Prerequisites + +Build/run an app with automation enabled. Generated examples usually expose `-Dautomation=true`: + +```bash +zig build run -Dplatform=macos -Dautomation=true +``` + +Repository examples may have specialized steps: + +```bash +zig build run-webview -Dplatform=macos -Dautomation=true +zig build test-webview-smoke -Dplatform=macos +``` + +The runner must pass an automation server into `RuntimeOptions`: + +```zig +const server = zero_native.automation.Server.init(io, ".zig-cache/zero-native-automation", "My App"); +var runtime = zero_native.Runtime.init(.{ + .platform = my_platform, + .automation = server, +}); +``` + +Apps built without `-Dautomation=true` usually ignore automation files. + +## Commands + +```bash +zero-native automate wait +zero-native automate list +zero-native automate snapshot +zero-native automate reload +zero-native automate bridge '{"id":"smoke","command":"native.ping","payload":{"source":"automation"}}' +``` + +If using the repository-built CLI: + +```bash +zig-out/bin/zero-native automate wait +zig-out/bin/zero-native automate snapshot +``` + +## Standard workflow + +1. Start the app with automation enabled. +2. Run `zero-native automate wait` to block until `snapshot.txt` contains `ready=true`. +3. Run `zero-native automate snapshot` to confirm app/window/source metadata. +4. Run `zero-native automate list` to inspect window summaries. +5. Run `zero-native automate bridge '...'` for bridge round-trip checks. +6. Use `zero-native automate reload` to request a WebView reload. + +## Bridge smoke test pattern + +The request must be JSON with an ID, command, and payload: + +```bash +zero-native automate bridge '{"id":"smoke","command":"native.ping","payload":{"source":"automation"}}' +``` + +Automation sends the request with origin `zero://inline`. The app's bridge policy must allow that origin or the call will reject with `permission_denied`. For packaged asset origins, app code often allows `zero://app`; for automation smoke tests, add `zero://inline` only when the test needs it. + +Expected response shape depends on the handler. A typical `native.ping` handler returns: + +```json +{"id":"smoke","ok":true,"result":{"message":"pong","count":1}} +``` + +If the command fails, inspect the bridge error code: + +- `unknown_command`: no handler registered or wrong command name. +- `permission_denied`: origin or permission policy blocked it. +- `handler_failed`: Zig handler returned an error or invalid JSON. +- `payload_too_large`: request exceeded bridge limits. + +## File protocol + +The default directory is `.zig-cache/zero-native-automation/`. + +Files: + +- `snapshot.txt`: app name, readiness, source kind, source size, window metadata, accessibility summary. +- `windows.txt`: window list. +- `command.txt`: command input written by CLI and consumed by runtime. +- `bridge-response.txt`: last bridge response. +- `screenshot.ppm`: placeholder screenshot artifact when supported by the runtime layer. + +The runtime polls `command.txt`. After processing a command, it writes `done`. + +## Debugging automation failures + +If `zero-native automate wait` times out: + +1. Confirm the app is still running. +2. Confirm it was built with `-Dautomation=true`. +3. Confirm the runner passes `automation` into `Runtime.init`. +4. Check `.zig-cache/zero-native-automation/snapshot.txt`. +5. Delete stale files in `.zig-cache/zero-native-automation/` and restart the app. +6. Run with more tracing, for example `zig build run -Dtrace=all`. + +If `snapshot` says no app connected: + +- The automation directory may not exist yet. +- The app may be running from a different working directory. +- The app may be built without automation. +- The app may not have reached runtime startup. + +If bridge automation fails: + +- Check command name spelling. +- Check app handler registration. +- Check bridge policy origins for `zero://inline`. +- Check runtime permissions. +- Check that the handler returns valid JSON. + +## CI and smoke tests + +Use automation for minimal integration confidence: + +```bash +zig build test-webview-smoke -Dplatform=macos +``` + +A good smoke test: + +1. Builds an example with `-Dautomation=true` and `-Djs-bridge=true`. +2. Starts the app in a GUI-capable session. +3. Waits for readiness. +4. Verifies snapshot metadata. +5. Sends `native.ping`. +6. Exercises builtin windows/WebViews if the app enables them. +7. Fails on timeout or unexpected bridge response. + +Do not use automation for exhaustive UI testing. It is a runtime and bridge smoke layer. + +## Notes + +- Automation is compile-time gated: apps built without `-Dautomation=true` ignore automation files. +- The current screenshot artifact is a placeholder PPM or unavailable depending on backend. +- WebView DOM interaction is intentionally out of scope for this file-based automation layer. +- Use `zero-native skills get core --full` for app architecture, bridge policy, packaging, and debugging context. diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md new file mode 100644 index 0000000..c7cf021 --- /dev/null +++ b/skill-data/core/SKILL.md @@ -0,0 +1,239 @@ +--- +name: core +description: Core zero-native guide for AI agents. Read this before explaining zero-native or changing a zero-native app. Covers the mental model, project structure, app.zon, App and Runtime patterns, frontend integration, web engines, JavaScript bridge commands, permissions, windows, WebViews, dialogs, packaging, debugging, testing, and when to load deeper references. Use when the user asks what zero-native is, how to build or modify an app, how to package or debug it, or how to add native capabilities. +--- + +# Build zero-native apps + +zero-native is a Zig desktop app shell for modern web frontends. A zero-native app is native Zig code that owns windows, policies, lifecycle, and platform services while rendering web UI in a WebView. The default engine is the platform WebView: WKWebView on macOS and WebKitGTK on Linux. Apps can also bundle Chromium through CEF where supported. + +Agents should assume they do not know zero-native from general model knowledge. Read this skill first. For implementation work, run `zero-native skills get core --full` so the referenced files are included in the CLI output. + +## Mental model + +- `App` describes the product: app state, name, WebView source, lifecycle callbacks, and optional bridge dispatcher. +- `Runtime` owns the event loop, windows, bridge dispatch, security checks, automation, tracing, platform services, and window state. +- `WebViewSource` tells the runtime what to load: inline HTML, a URL, or packaged assets from a local app origin. +- `app.zon` is the app manifest: identity, icons, windows, frontend assets, web engine, permissions, bridge policy, security policy, and packaging inputs. +- `src/runner.zig` is the generated runtime wiring. Edit it when adding runtime services, security policy, builtin bridge policy, tracing, automation, or platform setup. +- `src/main.zig` is app behavior. Edit it when changing app state, source selection, lifecycle callbacks, or app-defined bridge handlers. +- `frontend/` is normal web code. It talks to native Zig through `window.zero.invoke()` or builtin helpers when those are enabled. + +## Task router + +These references are included by `zero-native skills get core --full`. Use them when the task touches the topic: + +- Project creation, generated files, build steps: `references/project-anatomy.md` +- `App`, `Runtime`, callbacks, embedding, tests: `references/app-model-runtime.md` +- React/Vue/Svelte/Next/Vite, dev server, bundled assets: `references/frontend-assets.md` +- App-defined bridge commands, builtin commands, permissions, windows, WebViews, dialogs: `references/bridge-security-native-capabilities.md` +- Web engine choice, CEF, packaging, signing, doctor, logs, debugging: `references/web-engines-packaging-debugging.md` +- Running-app inspection and smoke tests: `zero-native skills get automation` + +## Quick start + +Use the CLI for new apps: + +```bash +npm install -g zero-native +zero-native init my_app --frontend next +cd my_app +zig build run +``` + +Frontend choices are `next`, `vite`, `react`, `svelte`, and `vue`. The first `zig build run` installs frontend dependencies, builds the native shell, and opens a desktop window. + +## Workflow for existing apps + +Before editing an existing zero-native app: + +1. Read `app.zon`, `src/main.zig`, `src/runner.zig`, and `build.zig`. +2. Identify whether the change is app metadata/policy, runtime wiring, app-native behavior, frontend behavior, packaging, or automation. +3. Follow the generated code and examples in the repository instead of inventing a new app layout. +4. Prefer exact security policy changes over broad allowances. +5. Validate with the narrowest useful command. + +Common file ownership: + +- `app.zon`: app identity, version, icons, windows, permissions, capabilities, bridge command policy, allowed origins, frontend dist/dev config, web engine, CEF config. +- `src/main.zig`: `App` state, source selection, lifecycle callbacks, custom bridge handlers. +- `src/runner.zig`: `Runtime.init`, platform selection, security policy, builtin bridge policy, `js_window_api`, automation server, trace sinks, panic capture, window state store. +- `build.zig`: build options, frontend build/dev/package steps, platform link setup, test steps. +- `frontend/`: web app implementation, `window.zero` calls, dev/build config. + +## Core app model + +The minimal Zig app returns `zero_native.App` with `context`, `name`, and a WebView source: + +```zig +const App = struct { + fn app(self: *@This()) zero_native.App { + return .{ + .context = self, + .name = "my-app", + .source = zero_native.WebViewSource.html("

Hello from zero-native

"), + }; + } +}; +``` + +Use these source constructors: + +- `zero_native.WebViewSource.html(content)` for small inline demos. +- `zero_native.WebViewSource.url(address)` for an explicit URL. +- `zero_native.WebViewSource.assets(.{ .root_path = "frontend/dist", .entry = "index.html" })` for packaged frontend assets. + +For framework apps, prefer a dynamic source so development loads the local dev server and production loads bundled assets: + +```zig +fn source(context: *anyopaque) anyerror!zero_native.WebViewSource { + const self: *App = @ptrCast(@alignCast(context)); + return zero_native.frontend.sourceFromEnv(self.env_map, .{ + .dist = "frontend/dist", + .entry = "index.html", + }); +} +``` + +`sourceFromEnv` reads `ZERO_NATIVE_FRONTEND_URL`; otherwise it serves the configured asset directory. Use it for most framework apps. + +## app.zon essentials + +Keep `app.zon` as the source of truth for app-level behavior: + +```zig +.{ + .id = "com.example.my-app", + .name = "my-app", + .display_name = "My App", + .version = "0.1.0", + .icons = .{ "assets/icon.icns" }, + .platforms = .{ "macos", "linux" }, + .permissions = .{}, + .capabilities = .{ "webview" }, + .frontend = .{ + .dist = "frontend/dist", + .entry = "index.html", + .spa_fallback = true, + .dev = .{ + .url = "http://127.0.0.1:5173/", + .command = .{ "npm", "--prefix", "frontend", "run", "dev", "--", "--host", "127.0.0.1" }, + .ready_path = "/", + .timeout_ms = 30000, + }, + }, + .security = .{ + .navigation = .{ + .allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" }, + .external_links = .{ .action = "deny" }, + }, + }, + .web_engine = "system", + .windows = .{ + .{ .label = "main", .title = "My App", .width = 960, .height = 640, .restore_state = true }, + }, +} +``` + +Use exact local origins for dev servers. Add `zero://inline` only for inline HTML sources. + +## Common implementation recipes + +### Add a new framework app + +Use `zero-native init --frontend `. Then inspect the generated `app.zon`, `src/main.zig`, and `build.zig` before customizing. For framework behavior, keep frontend work in `frontend/` and use `sourceFromEnv` so development and packaged builds share one app shell. + +### Add a native bridge command + +1. Add state and a handler in `src/main.zig`. +2. Register the handler in `bridge()`. +3. Allow the command in `app.zon` and in the runtime bridge policy if the runner reads manifest policy into runtime. +4. Call it from JavaScript with `window.zero.invoke("namespace.command", payload)`. +5. Return valid JSON from Zig. Use `zero_native.bridge.writeJsonStringValue()` for user-controlled strings. + +Bridge calls are size-limited, origin-checked, permission-checked, and routed only to registered handlers. + +### Add windows, child WebViews, or dialogs + +Use builtin bridge commands only after enabling a policy for the exact commands and origins. Window and child WebView commands need the `window` permission when permissions are configured. Dialog commands are always default-deny and require explicit `builtin_bridge` policy. See `references/bridge-security-native-capabilities.md`. + +### Choose a web engine + +Default to `.web_engine = "system"` for small apps and native footprint. Use `.web_engine = "chromium"` plus `.cef` when the app needs a pinned Chromium platform or rendering consistency. Chromium apps must install/package the matching CEF layout. + +### Package an app + +Keep package metadata in `app.zon`, build the frontend assets, build the native binary, then package: + +```bash +zig build package +zero-native doctor --manifest app.zon --strict +``` + +Use signing and CEF options only when the product requires them. + +## Development commands + +For iterative frontend work, use the managed dev server flow: + +```bash +zig build dev +``` + +Or run the CLI directly after building the binary: + +```bash +zero-native dev --manifest app.zon --binary zig-out/bin/MyApp +``` + +Vite usually uses `http://127.0.0.1:5173/`; Next.js usually uses `http://127.0.0.1:3000/`. The app WebView loads the dev URL directly, so framework HMR remains owned by Vite, Next.js, or the selected dev server. + +## Security defaults + +Treat WebView content as untrusted: + +- List only needed `permissions` and `capabilities`. +- Prefer exact bridge command origins over `"*"`. +- Keep main-frame navigation allowlisted in `security.navigation.allowed_origins`. +- Keep external links denied unless the product explicitly needs them. +- Use a strict CSP for packaged frontend assets. +- Built-in dialogs are always default-deny and require explicit `builtin_bridge` policy. +- Child WebViews receive `window.zero` only when explicitly created with `bridge: true`. + +Common bridge failure codes are `invalid_request`, `unknown_command`, `permission_denied`, `handler_failed`, `payload_too_large`, and `internal_error`. + +## Validate changes + +Useful commands: + +```bash +zig build run +zig build dev +zig build test +zig build test-tooling +zero-native validate app.zon +zero-native doctor --manifest app.zon --strict +zig build package +``` + +For GUI smoke tests, build with automation enabled and use the `automation` skill: + +```bash +zig build run -Dplatform=macos -Dautomation=true +zig-out/bin/zero-native automate snapshot +``` + +When changing app behavior, add focused Zig tests when the code can run headlessly. Use automation-based tests only for WebView/runtime integration that requires a GUI-capable session. + +## Examples to inspect + +- `examples/hello`: smallest inline HTML app. +- `examples/webview`: bridge and WebView runtime example. +- `examples/browser`: layered WebView/browser-style example. +- `examples/next`: Next.js with production assets. +- `examples/react`, `examples/svelte`, `examples/vue`: Vite frontend apps. +- `examples/ios`, `examples/android`: mobile host embedding examples. + +## When answering users + +Explain zero-native in concrete terms: Zig owns native app lifecycle and security; web UI renders in a WebView; the bridge is opt-in and policy controlled; `app.zon` is the manifest; framework frontend development uses a dev server in development and bundled assets in production. If asked to implement, read the app files first and make the smallest change in the correct layer. diff --git a/skill-data/core/references/app-model-runtime.md b/skill-data/core/references/app-model-runtime.md new file mode 100644 index 0000000..04c3383 --- /dev/null +++ b/skill-data/core/references/app-model-runtime.md @@ -0,0 +1,141 @@ +# App Model and Runtime + +Use this when editing `src/main.zig`, `src/runner.zig`, lifecycle behavior, runtime setup, or tests. + +## `App` + +A zero-native app returns a `zero_native.App` value: + +```zig +const App = struct { + fn app(self: *@This()) zero_native.App { + return .{ + .context = self, + .name = "my-app", + .source = zero_native.WebViewSource.html("

Hello

"), + .source_fn = source, + .start_fn = start, + .event_fn = event, + .stop_fn = stop, + }; + } +}; +``` + +Required fields: + +- `context`: pointer to app state. +- `name`: app name for traces and automation snapshots. +- `source`: initial WebView source. Overridden by `source_fn` when present. + +Optional callbacks: + +- `source_fn`: dynamic source resolver. +- `start_fn`: called after runtime start and initial load. +- `event_fn`: receives lifecycle and runtime events. +- `stop_fn`: called before shutdown. + +## `WebViewSource` + +Choose one: + +```zig +zero_native.WebViewSource.html("

Inline

") +zero_native.WebViewSource.url("http://127.0.0.1:5173/") +zero_native.WebViewSource.assets(.{ + .root_path = "frontend/dist", + .entry = "index.html", + .origin = "zero://app", + .spa_fallback = true, +}) +``` + +Use inline HTML only for small examples and smoke tests. Use URL sources for explicit local/remote loading. Use assets for packaged apps. + +## Runtime setup + +Generated runners create a `Runtime` with platform services: + +```zig +var runtime = zero_native.Runtime.init(.{ + .platform = my_platform, + .trace_sink = fanout.sink(), + .bridge = my_app.bridge(), + .builtin_bridge = .{ .enabled = true, .commands = &builtin_policies }, + .security = .{ + .permissions = &app_permissions, + .navigation = .{ .allowed_origins = &.{ "zero://app" } }, + }, + .js_window_api = true, + .window_state_store = state_store, + .automation = if (build_options.automation) automation_server else null, +}); +try runtime.run(my_app.app()); +``` + +`RuntimeOptions` fields agents commonly touch: + +- `platform`: macOS, Linux, Windows, or `NullPlatform`. +- `trace_sink`: stdout/file/fanout trace destination. +- `bridge`: app-defined bridge dispatcher. +- `builtin_bridge`: policy for built-in windows, WebViews, and dialogs. +- `security`: permissions, navigation allowlist, external links. +- `automation`: file-based automation server. +- `window_state_store`: persisted window geometry. +- `js_window_api`: exposes `window.zero.windows` and `window.zero.webviews`. + +## Windows from Zig + +Use runtime methods for native window management: + +```zig +const info = try runtime.createWindow(.{ + .label = "tools", + .title = "Tools", + .default_frame = zero_native.geometry.RectF.init(80, 80, 420, 320), +}); +try runtime.focusWindow(info.id); +``` + +Window limits: + +- Max windows: 16. +- Max label bytes: 64. +- Max title bytes: 128. + +Persisted window state is keyed primarily by `label`, so labels should be stable. + +## EmbeddedApp + +Use `EmbeddedApp` when another host owns the main loop: + +```zig +var embedded = zero_native.embed.EmbeddedApp.init(my_app.app(), my_platform); +try embedded.start(); +try embedded.frame(); +try embedded.resize(new_surface); +try embedded.stop(); +``` + +This is useful for mobile hosts, game engines, custom render loops, and headless tests. The repository includes iOS and Android examples that link `libzero-native.a` through Swift/Kotlin host apps. + +## Headless tests + +Use `NullPlatform` or `TestHarness` when GUI behavior is not required: + +```zig +var null_platform = zero_native.NullPlatform.init(.{}); +var runtime = zero_native.Runtime.init(.{ + .platform = null_platform.platform(), +}); +``` + +Good headless test targets: + +- source selection +- bridge handler logic +- bridge policy enforcement +- lifecycle callbacks +- manifest/tooling behavior + +Use automation smoke tests for real WebView/window integration. diff --git a/skill-data/core/references/bridge-security-native-capabilities.md b/skill-data/core/references/bridge-security-native-capabilities.md new file mode 100644 index 0000000..041f496 --- /dev/null +++ b/skill-data/core/references/bridge-security-native-capabilities.md @@ -0,0 +1,230 @@ +# Bridge, Security, and Native Capabilities + +Use this when adding JavaScript-to-Zig calls, builtin commands, permissions, windows, child WebViews, dialogs, navigation policies, or external links. + +## Bridge architecture + +JavaScript calls native Zig through: + +```javascript +const result = await window.zero.invoke("native.ping", { source: "webview" }); +``` + +The runtime: + +1. Parses the JSON request. +2. Enforces message size limits. +3. Checks origin and permissions. +4. Looks up a registered handler. +5. Runs the handler and returns a JSON response. + +Bridge commands are default-deny. A command must be registered in Zig and allowed by policy. + +## Handler pattern + +```zig +fn ping(context: *anyopaque, invocation: zero_native.bridge.Invocation, output: []u8) anyerror![]const u8 { + _ = invocation; + const self: *App = @ptrCast(@alignCast(context)); + self.ping_count += 1; + return std.fmt.bufPrint(output, "{{\"message\":\"pong\",\"count\":{d}}}", .{self.ping_count}); +} +``` + +Dispatcher pattern: + +```zig +fn bridge(self: *App) zero_native.BridgeDispatcher { + self.handlers = .{.{ .name = "native.ping", .context = self, .invoke_fn = ping }}; + return .{ + .policy = .{ .enabled = true, .commands = &policies }, + .registry = .{ .handlers = &self.handlers }, + }; +} +``` + +When returning user-controlled strings, escape them: + +```zig +return zero_native.bridge.writeJsonStringValue(output, user_name); +``` + +## Size limits + +- Request message: 16 KiB. +- Response: 16 KiB. +- Handler result: 12 KiB. +- Request ID: 64 bytes. +- Command name: 128 bytes. + +For large data, do not force everything through one bridge response. Use native files/resources or chunking patterns. + +## Security policy + +Core defaults: + +- No permissions granted unless listed. +- Bridge commands denied unless policy allows them. +- Navigation blocked unless origin is allowlisted. +- External links denied unless configured. +- Dialog builtin commands always denied unless explicitly listed in `builtin_bridge`. + +Manifest examples: + +```zig +.permissions = .{ "window" }, +.capabilities = .{ "webview", "js_bridge" }, +.bridge = .{ + .commands = .{ + .{ .name = "native.ping", .origins = .{ "zero://app" } }, + }, +}, +.security = .{ + .navigation = .{ + .allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" }, + .external_links = .{ .action = "deny" }, + }, +}, +``` + +Prefer exact origins over `"*"`. Use `"*"` only for commands that expose no native state and only when the project already accepts that risk. + +## Builtin commands + +zero-native includes builtin bridge commands for windows, layered WebViews, and dialogs. These are controlled separately from app-defined commands via `builtin_bridge`. + +Window commands: + +- `zero-native.window.list` +- `zero-native.window.create` +- `zero-native.window.focus` +- `zero-native.window.close` + +Layered WebView commands: + +- `zero-native.webview.create` +- `zero-native.webview.list` +- `zero-native.webview.setFrame` +- `zero-native.webview.navigate` +- `zero-native.webview.setZoom` +- `zero-native.webview.setLayer` +- `zero-native.webview.close` + +Dialog commands: + +- `zero-native.dialog.openFile` +- `zero-native.dialog.saveFile` +- `zero-native.dialog.showMessage` + +Enable explicitly: + +```zig +const app_permissions = [_][]const u8{zero_native.security.permission_window}; + +.security = .{ + .permissions = &app_permissions, + .navigation = .{ .allowed_origins = &.{ "zero://app" } }, +}, +.builtin_bridge = .{ + .enabled = true, + .commands = &.{ + .{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "zero-native.webview.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "zero-native.dialog.openFile", .origins = .{ "zero://app" } }, + }, +}, +``` + +`js_window_api = true` exposes `window.zero.windows.*` and `window.zero.webviews.*`, but it does not bypass origin or permission checks. + +## Windows from JavaScript + +```javascript +const win = await window.zero.windows.create({ + label: "tools", + title: "Tools", + width: 420, + height: 320, +}); + +const all = await window.zero.windows.list(); +await window.zero.windows.focus(win.id); +await window.zero.windows.close(win.id); +``` + +Window state persistence uses stable labels. Use meaningful labels like `main`, `settings`, `tools`, or `preview`. + +## Layered WebViews + +Child WebViews are native WebViews layered inside a native window: + +```javascript +const preview = await window.zero.webviews.create({ + label: "preview", + url: "https://example.com", + frame: { x: 24, y: 24, width: 480, height: 320 }, + layer: 10, + bridge: false, +}); + +await preview.setZoom(1.25); +await preview.setLayer(20); +await preview.close(); +``` + +Rules: + +- WebView URLs must pass navigation policy. +- Commands target only the calling native window. +- `main` is reserved for the startup WebView. +- Child WebViews receive `window.zero` only with `bridge: true`. +- Backend gaps should reject with `invalid_request`. + +## Dialogs + +Dialogs require explicit `builtin_bridge` policy. + +```javascript +const files = await window.zero.invoke("zero-native.dialog.openFile", { + title: "Select a file", + defaultPath: "/home", + allowMultiple: true, + allowDirectories: false, +}); + +const path = await window.zero.invoke("zero-native.dialog.saveFile", { + title: "Save as", + defaultName: "untitled.txt", +}); + +const result = await window.zero.invoke("zero-native.dialog.showMessage", { + style: "warning", + title: "Confirm", + message: "Delete this item?", + primaryButton: "Delete", + secondaryButton: "Cancel", +}); +``` + +Use native dialogs for trusted app UI. Do not expose arbitrary filesystem access to remote or untrusted origins. + +## Error handling + +JavaScript bridge calls reject with `error.code`: + +- `invalid_request`: malformed input, unsupported operation, denied navigation URL, missing target, duplicate/reserved label. +- `unknown_command`: no registered handler. +- `permission_denied`: origin or permission failed. +- `handler_failed`: handler returned an error. +- `payload_too_large`: request too large. +- `internal_error`: unexpected runtime failure. + +Always handle errors in frontend code: + +```javascript +try { + await window.zero.invoke("native.save", payload); +} catch (error) { + console.error(error.code, error.message); +} +``` diff --git a/skill-data/core/references/frontend-assets.md b/skill-data/core/references/frontend-assets.md new file mode 100644 index 0000000..5d7bd37 --- /dev/null +++ b/skill-data/core/references/frontend-assets.md @@ -0,0 +1,124 @@ +# Frontend and Assets + +Use this when working with React, Vue, Svelte, Next.js, Vite, dev servers, HMR, static assets, or packaged frontend output. + +## Recommended source pattern + +Framework apps should use a dynamic source: + +```zig +fn source(context: *anyopaque) anyerror!zero_native.WebViewSource { + const self: *App = @ptrCast(@alignCast(context)); + return zero_native.frontend.sourceFromEnv(self.env_map, .{ + .dist = "frontend/dist", + .entry = "index.html", + }); +} +``` + +`sourceFromEnv` checks `ZERO_NATIVE_FRONTEND_URL`. If set, the WebView loads the dev server URL. If not set, it serves packaged assets from `dist`. + +## Manifest frontend config + +```zig +.frontend = .{ + .dist = "frontend/dist", + .entry = "index.html", + .spa_fallback = true, + .dev = .{ + .url = "http://127.0.0.1:5173/", + .command = .{ "npm", "--prefix", "frontend", "run", "dev", "--", "--host", "127.0.0.1" }, + .ready_path = "/", + .timeout_ms = 30000, + }, +}, +``` + +Fields: + +- `dist`: built frontend output path. +- `entry`: HTML entry file within `dist`. +- `spa_fallback`: serve `entry` for unknown routes. +- `dev.url`: local dev server URL. +- `dev.command`: command zero-native can spawn. +- `dev.ready_path`: path to poll for readiness. +- `dev.timeout_ms`: readiness timeout. + +## Dev server workflow + +Use: + +```bash +zig build dev +``` + +Or directly: + +```bash +zero-native dev --manifest app.zon --binary zig-out/bin/MyApp +``` + +The dev command starts the frontend process, waits for readiness, sets `ZERO_NATIVE_FRONTEND_URL`, launches the native shell, and terminates the frontend when the shell exits. + +Framework defaults: + +- Vite, React, Vue, Svelte: `http://127.0.0.1:5173/`, command `npm run dev -- --host 127.0.0.1`. +- Next.js: `http://127.0.0.1:3000/`, command `npm run dev -- --hostname 127.0.0.1`. + +## Production assets + +For packaged builds, serve local assets from `zero://app`: + +```zig +return zero_native.frontend.productionSource(.{ + .dist = "frontend/dist", + .entry = "index.html", +}); +``` + +The package/build flow should build the frontend before packaging. `zig build bundle-assets` and `zero-native bundle-assets` copy the configured dist directory into build/package output. + +## Security and frontend origins + +Add exact origins to `security.navigation.allowed_origins`: + +```zig +.security = .{ + .navigation = .{ + .allowed_origins = .{ + "zero://app", + "http://127.0.0.1:5173", + }, + }, +}, +``` + +Use `zero://inline` only for inline examples. Do not allow `"*"` for production navigation. + +For packaged HTML, start with a strict CSP: + +```html + +``` + +For dev servers, keep development CSP separate and add only the local dev/WebSocket endpoints required by the framework. + +## Frontend to native calls + +Use: + +```javascript +const result = await window.zero.invoke("native.ping", { source: "webview" }); +``` + +For builtin windows/WebViews helpers, enable `js_window_api` and policy in the runner. For custom commands, register handlers in Zig and allow the command in policy. + +## Example projects + +- `examples/next`: Next.js app with production assets under `frontend/out`. +- `examples/react`: React with Vite. +- `examples/svelte`: Svelte with Vite. +- `examples/vue`: Vue with Vite. + +When unsure about frontend build commands or output directories, inspect the matching example before editing. diff --git a/skill-data/core/references/project-anatomy.md b/skill-data/core/references/project-anatomy.md new file mode 100644 index 0000000..9a8c386 --- /dev/null +++ b/skill-data/core/references/project-anatomy.md @@ -0,0 +1,105 @@ +# Project Anatomy + +Use this when creating, orienting in, or restructuring a zero-native app. + +## Generated project files + +- `build.zig`: Zig build graph. Generated examples expose platform selection, trace mode, debug overlay, automation, JS bridge, web engine overrides, frontend install/build/dev steps, tests, and package steps. +- `build.zig.zon`: Zig package manifest and dependency declaration. +- `app.zon`: app manifest read by CLI/build/package/doctor tooling. +- `src/main.zig`: app state, `app()` method, source resolver, optional bridge dispatcher, lifecycle callbacks. +- `src/runner.zig`: platform and runtime setup: native backend, trace sinks, panic capture, log paths, state store, security policy, builtin bridge policy, automation. +- `assets/`: icons and other package resources. +- `frontend/`: framework app when using Next, Vite, React, Svelte, or Vue. + +## app.zon responsibilities + +Keep product-level metadata and policies in `app.zon`: + +```zig +.{ + .id = "com.example.my-app", + .name = "my-app", + .display_name = "My App", + .version = "0.1.0", + .icons = .{ "assets/icon.icns" }, + .platforms = .{ "macos", "linux" }, + .permissions = .{ "window" }, + .capabilities = .{ "webview", "js_bridge" }, + .bridge = .{ + .commands = .{ + .{ .name = "native.ping", .origins = .{ "zero://app" } }, + }, + }, + .security = .{ + .navigation = .{ + .allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" }, + .external_links = .{ .action = "deny" }, + }, + }, + .frontend = .{ + .dist = "frontend/dist", + .entry = "index.html", + .spa_fallback = true, + .dev = .{ + .url = "http://127.0.0.1:5173/", + .command = .{ "npm", "--prefix", "frontend", "run", "dev", "--", "--host", "127.0.0.1" }, + .ready_path = "/", + .timeout_ms = 30000, + }, + }, + .web_engine = "system", + .windows = .{ + .{ .label = "main", .title = "My App", .width = 960, .height = 640, .restore_state = true }, + }, +} +``` + +Important manifest fields: + +- `id`: reverse-DNS bundle identifier. Used for bundle metadata and log/state paths. +- `name`: short machine name. +- `display_name`: human app name. +- `version`: package and bundle version. +- `icons`: package resources. +- `platforms`: package targets. +- `permissions`: runtime grants checked by bridge and builtin commands. +- `capabilities`: broad feature declarations. +- `bridge.commands`: app-defined command allowlist. +- `security.navigation.allowed_origins`: main-frame navigation allowlist. +- `security.navigation.external_links`: external link policy. +- `frontend`: production asset and dev server config. +- `web_engine`: `system` or `chromium`. +- `cef`: CEF layout config for Chromium. +- `windows`: initial window definitions. + +## Build steps to know + +Common generated steps: + +```bash +zig build run +zig build dev +zig build test +zig build package +zig build frontend-install +zig build frontend-build +``` + +Repository-level useful steps: + +```bash +zig build test +zig build test-tooling +zig build test-webview-smoke -Dplatform=macos +zig build test-package-cef-layout -Dplatform=macos +``` + +## Layering rule + +- If changing app identity, packaging inputs, permissions, origins, windows, frontend dist/dev paths, or web engine, update `app.zon`. +- If changing runtime services, platform setup, automation, logging, security wiring, or builtin bridge policy, update `src/runner.zig`. +- If changing native business behavior, lifecycle callbacks, bridge handlers, or source selection, update `src/main.zig`. +- If changing UI, routes, CSS, or web calls, update `frontend/`. + +Do not put package metadata in Zig app state unless the generated project already does that. Do not bypass `app.zon` policy for convenience. diff --git a/skill-data/core/references/web-engines-packaging-debugging.md b/skill-data/core/references/web-engines-packaging-debugging.md new file mode 100644 index 0000000..072c6db --- /dev/null +++ b/skill-data/core/references/web-engines-packaging-debugging.md @@ -0,0 +1,199 @@ +# Web Engines, Packaging, and Debugging + +Use this when choosing system WebView vs Chromium, installing CEF, packaging, signing, running doctor, finding logs, or debugging runtime behavior. + +## Web engine choice + +Default: + +```zig +.web_engine = "system", +``` + +System mode: + +- Uses the OS web engine. +- macOS: WKWebView. +- Linux: WebKitGTK. +- Smallest app footprint. +- Fastest startup. +- Rendering depends on the user's OS. + +Chromium mode: + +```zig +.web_engine = "chromium", +.cef = .{ .dir = "third_party/cef/macos", .auto_install = false }, +``` + +Chromium mode: + +- Bundles CEF. +- Gives predictable Chromium behavior. +- Increases package size and startup cost. +- Requires matching CEF layout at build and package time. + +Use Chromium when the product needs a pinned web platform, complex frontend rendering consistency, or Chromium-only behavior. Otherwise prefer `system`. + +## CEF setup + +Install the prepared runtime: + +```bash +zero-native cef install +zero-native doctor --manifest app.zon +``` + +Pin CEF in app setup or CI when reproducibility matters: + +```bash +zero-native cef install --version +``` + +Useful overrides: + +```bash +zig build run -Dweb-engine=chromium -Dcef-dir=third_party/cef/macos +zig build run -Dweb-engine=chromium -Dcef-auto-install=true +zero-native package --web-engine chromium --cef-dir third_party/cef/macos +``` + +Normal product configuration should live in `app.zon`; CLI/build flags are for temporary overrides. + +## Packaging + +Simple path: + +```bash +zig build package +``` + +CLI path: + +```bash +zero-native package --target macos --manifest app.zon --binary zig-out/bin/MyApp +``` + +Important package manifest fields: + +- `id`: bundle ID, desktop ID, log/state prefix. +- `display_name`: app/menu/window title fallback. +- `version`: package metadata. +- `icons`: copied into package resources. +- `platforms`: intended package targets. +- `frontend`: asset directory and entry file. +- `web_engine` and `cef`: engine and Chromium runtime config. + +For frontend apps, package the built frontend assets. The build step usually wires this automatically. If using CLI directly, pass `--assets frontend/dist`. + +## macOS packages + +`zig build package` creates a `.app` bundle with: + +- `Contents/MacOS/` +- `Contents/Resources/icon.icns` +- `Contents/Info.plist` +- `Contents/Resources/dist/` when frontend assets are configured +- `Contents/Frameworks/Chromium Embedded Framework.framework` for Chromium apps + +macOS minimum system version is 11.0. + +Signing modes: + +```bash +zero-native package --target macos --signing none +zero-native package --target macos --signing adhoc +zero-native package --target macos --signing identity --identity "Developer ID Application: Your Name" +``` + +For Chromium apps, verify the CEF framework and resources are included and signed before notarization. + +## Linux and Windows packages + +Linux creates an install tree with: + +- `bin/` +- `share/applications/.desktop` +- icons under `share/icons/hicolor/...` + +Windows packaging is early support and creates a directory-based distributable layout. + +Shortcut commands: + +```bash +zero-native package-linux --binary zig-out/bin/MyApp +zero-native package-windows --binary zig-out/bin/MyApp.exe +``` + +## Doctor and validation + +Validate manifest schema: + +```bash +zero-native validate app.zon +``` + +Check environment and package readiness: + +```bash +zero-native doctor +zero-native doctor --manifest app.zon --strict +zero-native doctor --manifest app.zon --web-engine chromium --cef-dir third_party/cef/macos +``` + +Doctor checks: + +- host platform +- WebView availability +- manifest validity +- log directory writability +- CEF layout when Chromium is selected +- signing tools + +Use `--strict` in CI or before release so warnings fail the command. + +## Debugging + +Trace modes: + +- `off` +- `events` +- `runtime` +- `all` + +Build/run flags commonly include: + +```bash +zig build run -Dtrace=all +zig build run-webview -Ddebug-overlay=true +``` + +Log defaults: + +- macOS: `~/Library/Logs//zero-native.jsonl` +- Linux: `~/.local/state//logs/zero-native.jsonl` +- Windows: `%LOCALAPPDATA%\\Logs\zero-native.jsonl` + +Environment variables: + +```bash +ZERO_NATIVE_LOG_DIR=/tmp/my-logs zig build run +ZERO_NATIVE_LOG_FORMAT=text zig build run +``` + +Panic capture: + +```zig +pub const panic = std.debug.FullPanic(zero_native.debug.capturePanic); +``` + +Generated runners usually install panic capture so crashes write `last-panic.txt` and append a fatal trace record. + +## Common failures + +- App window opens blank: check `WebViewSource`, `frontend.dist`, `frontend.entry`, and allowed origins. +- Dev server never loads: check `app.zon frontend.dev.url`, command, readiness path, and `ZERO_NATIVE_FRONTEND_URL`. +- Bridge call rejects with `permission_denied`: check command origin and permissions in policy. +- Bridge call rejects with `unknown_command`: handler was not registered or command name differs. +- Chromium app fails at launch: check CEF layout, version mismatch, bundle Frameworks layout, and signing. +- Package misses frontend: check `frontend.dist`, frontend build step, and `--assets`. diff --git a/skills/zero-native/SKILL.md b/skills/zero-native/SKILL.md new file mode 100644 index 0000000..0c42295 --- /dev/null +++ b/skills/zero-native/SKILL.md @@ -0,0 +1,33 @@ +--- +name: zero-native +description: Discovery skill for zero-native, a Zig desktop app shell for building native apps with web UIs. Use when the user asks what zero-native is, how to build a zero-native app, scaffold a frontend app, configure app.zon, choose a WebView engine, add bridge commands, package an app, test a running app, or automate a zero-native WebView shell. +allowed-tools: Bash(zero-native:*), Bash(npx zero-native:*) +hidden: true +--- + +# zero-native + +zero-native is a Zig desktop app shell for building native desktop apps with web UIs. It uses the platform WebView for small native-footprint apps and can bundle Chromium through CEF where supported. + +## Start here + +This file is a discovery stub for agents that installed zero-native once with a skills installer such as `npx skills add zero-native`. Before implementing or explaining zero-native app work, use the installed CLI to discover and load the current skill content: + +```bash +zero-native skills list +zero-native skills get core +zero-native skills get core --full +``` + +Use `zero-native skills get core` for initial orientation. Use `zero-native skills get core --full` for implementation tasks because it includes the reference files for project anatomy, runtime, frontend assets, bridge/security/native capabilities, packaging, and debugging. Use `zero-native skills get automation` when testing a running app, taking snapshots, requesting reloads, or using the built-in automation server. + +## Quick orientation + +```bash +npm install -g zero-native +zero-native init my_app --frontend next +cd my_app +zig build run +``` + +Generated apps center on `app.zon`, `src/main.zig`, `src/runner.zig`, `build.zig`, and `frontend/`. Inspect those files before editing an existing app. diff --git a/tools/zero-native/main.zig b/tools/zero-native/main.zig index bfba0a7..07f0e64 100644 --- a/tools/zero-native/main.zig +++ b/tools/zero-native/main.zig @@ -1,5 +1,6 @@ const std = @import("std"); const automation_cli = @import("automation.zig"); +const skills_cli = @import("skills.zig"); const tooling = @import("tooling"); const version = "0.1.9"; @@ -130,6 +131,11 @@ pub fn main(init: std.process.Init) !void { tooling.package.printDiagnostic(stats); } else if (std.mem.eql(u8, command, "automate")) { try automation_cli.run(allocator, init.io, args[2..]); + } else if (std.mem.eql(u8, command, "skills")) { + skills_cli.run(allocator, init.io, init.environ_map, args[2..]) catch |err| switch (err) { + error.WriteFailed => return, + else => return err, + }; } else { return usage(); } @@ -152,6 +158,7 @@ fn usage() void { \\ package-ios [--output path] [--binary path] \\ package-android [--output path] [--binary path] \\ automate + \\ skills list|get \\ version \\ , .{}); diff --git a/tools/zero-native/skills.zig b/tools/zero-native/skills.zig new file mode 100644 index 0000000..8661ee7 --- /dev/null +++ b/tools/zero-native/skills.zig @@ -0,0 +1,252 @@ +const std = @import("std"); + +const skill_dirs = [_][]const u8{ "skills", "skill-data" }; + +const SkillInfo = struct { + name: []const u8, + description: []const u8, + dir: []const u8, + hidden: bool, +}; + +pub fn run(allocator: std.mem.Allocator, io: std.Io, env_map: *std.process.Environ.Map, args: []const []const u8) !void { + var stdout_buffer: [4096]u8 = undefined; + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); + const stdout = &stdout_writer.interface; + defer stdout.flush() catch {}; + + if (args.len == 0 or isHelp(args[0])) return usage(stdout); + + const root = try findPackageRoot(allocator, io, env_map) orelse { + std.debug.print("zero-native skills: could not find skills/ or skill-data/ near the zero-native executable\n", .{}); + std.process.exit(1); + }; + defer allocator.free(root); + + var skills = try discoverSkills(allocator, io, root); + defer skills.deinit(allocator); + + const command = args[0]; + if (std.mem.eql(u8, command, "list")) { + const visible_count = try printSkillList(stdout, skills.items); + if (visible_count == 0) try stdout.print("No skills found.\n", .{}); + } else if (std.mem.eql(u8, command, "get")) { + if (args.len < 2) return fail("No skill name provided. Usage: zero-native skills get "); + const include_supplementary = hasFlag(args[2..], "--full"); + if (std.mem.eql(u8, args[1], "--all")) { + try printAllSkills(allocator, io, stdout, skills.items, include_supplementary); + return; + } + const skill = findSkill(skills.items, args[1]) orelse { + std.debug.print("Unknown skill: {s}\n", .{args[1]}); + std.process.exit(1); + }; + try printSkill(allocator, io, stdout, skill, include_supplementary); + } else { + return usage(stdout); + } +} + +fn usage(stdout: *std.Io.Writer) !void { + try stdout.print( + \\usage: zero-native skills + \\ + \\commands: + \\ skills list + \\ skills get [--full] + \\ skills get --all [--full] + \\ + \\examples: + \\ zero-native skills list + \\ zero-native skills get core + \\ zero-native skills get core --full + \\ zero-native skills get automation + \\ + , .{}); +} + +fn fail(message: []const u8) noreturn { + std.debug.print("{s}\n", .{message}); + std.process.exit(1); +} + +fn isHelp(arg: []const u8) bool { + return std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "help"); +} + +fn hasFlag(args: []const []const u8, name: []const u8) bool { + for (args) |arg| { + if (std.mem.eql(u8, arg, name)) return true; + } + return false; +} + +fn findPackageRoot(allocator: std.mem.Allocator, io: std.Io, env_map: *std.process.Environ.Map) !?[]const u8 { + if (env_map.get("ZERO_NATIVE_SKILLS_ROOT")) |root| { + if (try hasSkillDirs(allocator, io, root)) return try allocator.dupe(u8, root); + } + + var buffer: [std.fs.max_path_bytes]u8 = undefined; + const executable_len = std.process.executablePath(io, &buffer) catch return null; + const executable_path = buffer[0..executable_len]; + var dir = std.fs.path.dirname(executable_path) orelse return null; + + while (true) { + if (try hasSkillDirs(allocator, io, dir)) return try allocator.dupe(u8, dir); + dir = std.fs.path.dirname(dir) orelse break; + } + + return null; +} + +fn hasSkillDirs(allocator: std.mem.Allocator, io: std.Io, root: []const u8) !bool { + const skills_path = try std.fs.path.join(allocator, &.{ root, "skills" }); + defer allocator.free(skills_path); + if (dirExists(io, skills_path)) return true; + + const data_path = try std.fs.path.join(allocator, &.{ root, "skill-data" }); + defer allocator.free(data_path); + return dirExists(io, data_path); +} + +fn dirExists(io: std.Io, path: []const u8) bool { + var dir = std.Io.Dir.cwd().openDir(io, path, .{}) catch return false; + dir.close(io); + return true; +} + +fn discoverSkills(allocator: std.mem.Allocator, io: std.Io, root: []const u8) !std.ArrayList(SkillInfo) { + var skills: std.ArrayList(SkillInfo) = .empty; + errdefer skills.deinit(allocator); + + for (skill_dirs) |dir_name| { + const base = try std.fs.path.join(allocator, &.{ root, dir_name }); + defer allocator.free(base); + + var dir = std.Io.Dir.cwd().openDir(io, base, .{ .iterate = true }) catch continue; + defer dir.close(io); + + var walker = try dir.walk(allocator); + defer walker.deinit(); + + while (try walker.next(io)) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.eql(u8, std.fs.path.basename(entry.path), "SKILL.md")) continue; + + const skill_dir_rel = std.fs.path.dirname(entry.path) orelse ""; + const skill_dir = try std.fs.path.join(allocator, &.{ base, skill_dir_rel }); + errdefer allocator.free(skill_dir); + + const skill_path = try std.fs.path.join(allocator, &.{ base, entry.path }); + defer allocator.free(skill_path); + + const content = try std.Io.Dir.cwd().readFileAlloc(io, skill_path, allocator, .limited(1024 * 1024)); + defer allocator.free(content); + + const parsed = parseFrontmatter(content) orelse continue; + try skills.append(allocator, .{ + .name = try allocator.dupe(u8, parsed.name), + .description = try allocator.dupe(u8, parsed.description), + .dir = skill_dir, + .hidden = parsed.hidden, + }); + } + } + + std.mem.sort(SkillInfo, skills.items, {}, lessSkill); + return skills; +} + +fn lessSkill(_: void, a: SkillInfo, b: SkillInfo) bool { + return std.mem.lessThan(u8, a.name, b.name); +} + +const ParsedFrontmatter = struct { + name: []const u8, + description: []const u8, + hidden: bool, +}; + +fn parseFrontmatter(content: []const u8) ?ParsedFrontmatter { + if (!std.mem.startsWith(u8, content, "---\n")) return null; + const frontmatter_end = std.mem.indexOf(u8, content[4..], "\n---") orelse return null; + const frontmatter = content[4 .. 4 + frontmatter_end]; + + var name: ?[]const u8 = null; + var description: []const u8 = ""; + var hidden = false; + + var lines = std.mem.splitScalar(u8, frontmatter, '\n'); + while (lines.next()) |line| { + if (std.mem.startsWith(u8, line, "name:")) { + name = std.mem.trim(u8, line["name:".len..], " \t"); + } else if (std.mem.startsWith(u8, line, "description:")) { + description = std.mem.trim(u8, line["description:".len..], " \t"); + } else if (std.mem.startsWith(u8, line, "hidden:")) { + const value = std.mem.trim(u8, line["hidden:".len..], " \t"); + hidden = std.mem.eql(u8, value, "true") or std.mem.eql(u8, value, "yes"); + } + } + + return .{ .name = name orelse return null, .description = description, .hidden = hidden }; +} + +fn printSkillList(stdout: *std.Io.Writer, skills: []const SkillInfo) !usize { + var visible_count: usize = 0; + for (skills) |skill| { + if (skill.hidden) continue; + visible_count += 1; + if (skill.description.len > 0) { + try stdout.print("{s}\t{s}\n", .{ skill.name, skill.description }); + } else { + try stdout.print("{s}\n", .{skill.name}); + } + } + return visible_count; +} + +fn findSkill(skills: []const SkillInfo, name: []const u8) ?SkillInfo { + for (skills) |skill| { + if (std.mem.eql(u8, skill.name, name)) return skill; + } + return null; +} + +fn printAllSkills(allocator: std.mem.Allocator, io: std.Io, stdout: *std.Io.Writer, skills: []const SkillInfo, include_supplementary: bool) !void { + var first = true; + for (skills) |skill| { + if (skill.hidden) continue; + if (!first) try stdout.print("\n---\n\n", .{}); + first = false; + try printSkill(allocator, io, stdout, skill, include_supplementary); + } +} + +fn printSkill(allocator: std.mem.Allocator, io: std.Io, stdout: *std.Io.Writer, skill: SkillInfo, include_supplementary: bool) !void { + const skill_path = try std.fs.path.join(allocator, &.{ skill.dir, "SKILL.md" }); + defer allocator.free(skill_path); + const content = try std.Io.Dir.cwd().readFileAlloc(io, skill_path, allocator, .limited(1024 * 1024)); + defer allocator.free(content); + try stdout.writeAll(content); + if (include_supplementary) try printSupplementaryFiles(allocator, io, stdout, skill.dir); +} + +fn printSupplementaryFiles(allocator: std.mem.Allocator, io: std.Io, stdout: *std.Io.Writer, skill_dir: []const u8) !void { + const subdirs = [_][]const u8{ "references", "templates" }; + for (subdirs) |subdir| { + const root = try std.fs.path.join(allocator, &.{ skill_dir, subdir }); + defer allocator.free(root); + var dir = std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true }) catch continue; + defer dir.close(io); + var walker = try dir.walk(allocator); + defer walker.deinit(); + while (try walker.next(io)) |entry| { + if (entry.kind != .file) continue; + const path = try std.fs.path.join(allocator, &.{ root, entry.path }); + defer allocator.free(path); + const content = try std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(1024 * 1024)); + defer allocator.free(content); + try stdout.print("\n\n---\n# {s}/{s}\n\n{s}", .{ subdir, entry.path, content }); + } + } +}