const std = @import("std"); const automation_cli = @import("automation.zig"); const markup_cli = @import("markup.zig"); const skills_cli = @import("skills.zig"); const tooling = @import("tooling"); const automation_protocol = @import("automation_protocol"); const cli_build_info = @import("cli_build_info"); const version = "0.5.0"; pub fn main(init: std.process.Init) !void { const allocator = init.arena.allocator(); const args = try init.minimal.args.toSlice(allocator); if (args.len <= 1) { usage(); std.process.exit(1); } const command = args[1]; if (std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "-h") or std.mem.eql(u8, command, "help")) { // Asked-for help is a success: print it and exit 0 (usage() alone // is reserved for the exit-1 "you didn't tell me what to do" path). usage(); return; } else if (std.mem.eql(u8, command, "--version") or std.mem.eql(u8, command, "version")) { // Payload, not a diagnostic: scripts parse `native version`, so it // belongs on stdout (see automation.zig's emitPayload contract). // Commit + automation protocol make binary/framework skew a // one-command check (a stale zig-out `native` binary once // silently drove a days-old dropbox). var stdout_buffer: [128]u8 = undefined; var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &stdout_buffer); try stdout_writer.interface.print("native {s} (commit {s}, automation protocol v{d})\n", .{ version, cli_build_info.build_commit, automation_protocol.version }); try stdout_writer.interface.flush(); } else if (std.mem.eql(u8, command, "init")) { checkVerbFlags("init", args[2..], .{ .usage = "init [path] [--template ] [--frontend ] [--framework ] [--full]", .value_flags = &.{ "--frontend", "--framework", "--template" }, .bool_flags = &.{"--full"}, }); const destination = positionalArg(args[2..]) orelse "."; const frontend_str = flagValue(args, "--frontend") catch fail("--frontend requires a value: native, next, vite, react, svelte, vue") orelse "native"; const frontend = tooling.templates.Frontend.parse(frontend_str) orelse fail("invalid --frontend value: use native (default), next, vite, react, svelte, or vue"); const shape: tooling.templates.Shape = if (flagBool(args, "--full")) .full else .slim; // Which core the native template writes (ts-core is the default). // The choice leaves no residue: the tree IS the truth, and the // build graph re-detects it on every build. const core: tooling.templates.CoreTemplate = if (flagValue(args, "--template") catch fail("--template requires a value: ts-core (default) or zig-core")) |value| tooling.templates.CoreTemplate.parse(value) orelse fail("invalid --template value: use ts-core (default) or zig-core") else .ts; if (core == .zig and frontend != .native) fail("--template applies to the native frontend (web frontends have no core tier)"); const app_name, const free_app_name = try initAppName(allocator, init.io, destination); defer if (free_app_name) allocator.free(app_name); const explicit_framework = try flagValue(args, "--framework"); const framework_path, const free_framework_path = if (explicit_framework) |value| .{ value, false } else try initFrameworkPath(allocator, init.io, init.environ_map); defer if (free_framework_path) allocator.free(framework_path); if (!hasFrameworkRoot(allocator, init.io, framework_path)) { if (explicit_framework) |value| { std.debug.print("error: --framework {s} is not a Native SDK checkout (no src/root.zig there)\n", .{value}); } else { std.debug.print("error: could not locate the Native SDK framework from this `native` binary's location\n" ++ " `native init` records where the framework lives so the new app can build against it.\n" ++ " Run the `native` built inside an SDK checkout (zig-out/bin/native) or installed via npm,\n" ++ " or pass --framework .\n", .{}); } std.process.exit(1); } tooling.templates.writeDefaultApp(allocator, init.io, destination, .{ .app_name = app_name, .framework_path = framework_path, .frontend = frontend, .shape = shape, .core = core }) catch |err| return failVerb(err); std.debug.print("created Native SDK app at {s} ({s})\n", .{ destination, frontend_str }); printInitNextSteps(destination, frontend, shape); } else if (std.mem.eql(u8, command, "build") or std.mem.eql(u8, command, "test")) { const verb: tooling.verbs.Verb = if (std.mem.eql(u8, command, "build")) .build else .@"test"; checkVerbFlags(command, args[2..], .{ .usage = if (verb == .build) "build [dir] [--yes] [-D... zig build flags]" else "test [dir] [--yes] [-D... zig build flags]", .bool_flags = &.{"--yes"}, .forwards_build_flags = true, }); const verb_args = parseVerbArgs(allocator, args[2..], &.{}) catch fail("usage: native build|test [dir] [--yes] [-D... zig build flags]"); try enterAppDir(init.io, verb_args.dir); tooling.verbs.run(allocator, init.io, verb, .{ .base_env = init.environ_map, .assume_yes = verb_args.assume_yes, .forwarded_args = verb_args.forwarded, }) catch |err| return failVerb(err); } else if (std.mem.eql(u8, command, "check")) { checkVerbFlags("check", args[2..], .{ .usage = "check [dir] [--strict]", .bool_flags = &.{ "--strict", "--yes" }, }); const verb_args = parseVerbArgs(allocator, args[2..], &.{}) catch fail("usage: native check [dir] [--strict]"); try enterAppDir(init.io, verb_args.dir); runCheck(allocator, init.io, init.environ_map, flagBool(args, "--strict")) catch |err| return failVerb(err); } else if (std.mem.eql(u8, command, "eject")) { // `eject component ` is dispatched before the plain build // eject so `component` is never mistaken for an app directory. if (args.len > 2 and std.mem.eql(u8, args[2], "component")) { checkVerbFlags("eject component", args[3..], .{ .usage = "eject component [dir]", }); const name = positionalArg(args[3..]) orelse { std.debug.print("which component? ejectable components: {s}\nusage: native eject component [dir]\n", .{tooling.eject_components.component_list}); std.process.exit(1); }; const verb_args = parseVerbArgs(allocator, args[4..], &.{}) catch fail("usage: native eject component [dir]"); try enterAppDir(init.io, verb_args.dir); runEjectComponent(init.io, name) catch |err| return failVerb(err); } else { checkVerbFlags("eject", args[2..], .{ .usage = "eject [dir]", .bool_flags = &.{"--yes"}, }); const verb_args = parseVerbArgs(allocator, args[2..], &.{}) catch fail("usage: native eject [dir]"); try enterAppDir(init.io, verb_args.dir); runEject(allocator, init.io, init.environ_map) catch |err| return failVerb(err); } } else if (std.mem.eql(u8, command, "doctor")) { try tooling.doctor.run(allocator, init.io, init.environ_map, args[2..]); } else if (std.mem.eql(u8, command, "cef")) { tooling.cef.run(allocator, init.io, init.environ_map, args[2..]) catch |err| switch (err) { error.InvalidArguments, error.UnsupportedPlatform, error.MissingLayout, error.CommandFailed, error.WrapperBuildFailed, => std.process.exit(1), else => return err, }; } else if (std.mem.eql(u8, command, "markup")) { try markup_cli.run(allocator, init.io, args[2..]); } else if (std.mem.eql(u8, command, "validate")) { checkVerbFlags("validate", args[2..], .{ .usage = "validate [app.zon]" }); const path = if (args.len >= 3) args[2] else "app.zon"; const result = tooling.manifest.validateFile(allocator, init.io, path) catch |err| switch (err) { error.FileNotFound => { std.debug.print("error: {s} not found - run this from your app's root (the folder containing app.zon), or pass a path: native validate \n", .{path}); std.process.exit(1); }, else => return err, }; tooling.manifest.printDiagnostic(result); // Exit directly: the diagnostic above is the whole story, and a // returned error would bury it under the CLI's own return trace. if (!result.ok) std.process.exit(1); } else if (std.mem.eql(u8, command, "bundle-assets")) { checkVerbFlags("bundle-assets", args[2..], .{ .usage = "bundle-assets [app.zon] [assets] [output]" }); const manifest_path = if (args.len >= 3) args[2] else "app.zon"; const metadata = try tooling.manifest.readMetadata(allocator, init.io, manifest_path); const assets_dir = if (args.len >= 4) args[3] else if (metadata.frontend) |frontend| frontend.dist else "assets"; const output_dir = if (args.len >= 5) args[4] else "zig-out/assets"; const stats = try tooling.assets.bundle(allocator, init.io, assets_dir, output_dir); std.debug.print("bundled {d} assets into {s}\n", .{ stats.asset_count, output_dir }); } else if (std.mem.eql(u8, command, "package")) { checkVerbFlags("package", args[2..], .{ .usage = "package [--target macos] [--output path] [--binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--team-id id] [--archive]", .value_flags = &.{ "--manifest", "--target", "--output", "--binary", "--assets", "--web-engine", "--web-layer", "--cef-dir", "--signing", "--identity", "--entitlements", "--team-id", "--optimize" }, .bool_flags = &.{ "--cef-auto-install", "--archive" }, }); const manifest_path = try flagValue(args, "--manifest") orelse "app.zon"; const metadata = tooling.manifest.readMetadata(allocator, init.io, manifest_path) catch |err| switch (err) { error.FileNotFound => { std.debug.print("error: {s} not found - run this from your app's root (the folder containing app.zon), or pass --manifest \n", .{manifest_path}); std.process.exit(1); }, else => return err, }; const target_name = try flagValue(args, "--target") orelse "macos"; const target = tooling.package.PackageTarget.parse(target_name) orelse fail("invalid package target"); const web_engine_override = if (try flagValue(args, "--web-engine")) |value| tooling.web_engine.Engine.parse(value) orelse fail("invalid web engine") else null; const web_engine = try tooling.web_engine.resolve(.{ .web_engine = metadata.web_engine, .cef = metadata.cef }, .{ .web_engine = web_engine_override, .cef_dir = try flagValue(args, "--cef-dir"), .cef_auto_install = if (flagBool(args, "--cef-auto-install")) true else null, }); // `--web-layer` beats app.zon's `.webview_layer` the same way // `-Dweb-layer` beats it in the build graph; the standard build // graphs pass their resolved layer decision through this flag so // the package always agrees with the exe it wraps. const web_layer_setting = if (try flagValue(args, "--web-layer")) |value| tooling.manifest.parseWebViewLayerSetting(value) orelse fail("invalid --web-layer value: use auto, include, or exclude") else null; const signing_name = try flagValue(args, "--signing") orelse "none"; const signing = tooling.package.SigningMode.parse(signing_name) orelse fail("invalid signing mode"); const default_output = switch (target) { .macos => try std.fmt.allocPrint(allocator, "zig-out/package/{s}.app", .{metadata.name}), else => try std.fmt.allocPrint(allocator, "zig-out/package/{s}-{s}", .{ metadata.name, target_name }), }; const output_dir = try flagValue(args, "--output") orelse if (args.len >= 3 and args[2].len > 0 and args[2][0] != '-') args[2] else default_output; const archive = flagBool(args, "--archive"); // Packaging is release-shaped on every target, and the optimize // label must reflect the binary actually packaged: the mobile // targets build the embed static library as ReleaseFast, and the // desktop targets pick up the ReleaseFast binary `native build` // installs. The label names artifacts (the .dmg/.zip/.tar.gz and // the package report), so a Debug default would stamp "Debug" on // a ReleaseFast binary. Pass --optimize when packaging a binary // built any other way. const optimize_value = try flagValue(args, "--optimize") orelse "ReleaseFast"; const binary_path = try flagValue(args, "--binary") orelse switch (target) { .ios => try iosPackageLibrary(allocator, init.io, init.environ_map, metadata.name, optimize_value), .android => try androidPackageLibrary(allocator, init.io, init.environ_map, metadata.name, optimize_value), else => try discoverAppBinary(allocator, init.io, metadata.name, target), }; if (binary_path == null and target != .ios and target != .android) { std.debug.print("warning[package.no-binary]: no app binary at zig-out/bin/{s} and no --binary flag - the package will not contain an executable\n" ++ " build the app first (`zig build`) or pass --binary \n", .{metadata.name}); } if (web_engine.engine == .chromium and web_engine.cef_auto_install) { try tooling.cef.run(allocator, init.io, init.environ_map, &.{ "install", "--dir", web_engine.cef_dir }); } const stats = try tooling.package.createPackage(allocator, init.io, .{ .metadata = metadata, .target = target, .optimize = optimize_value, .output_path = output_dir, .binary_path = binary_path, .assets_dir = try flagValue(args, "--assets") orelse if (metadata.frontend) |frontend| frontend.dist else "assets", .frontend = metadata.frontend, .web_engine = web_engine.engine, .web_layer_setting = web_layer_setting, .cef_dir = web_engine.cef_dir, .signing = .{ .mode = signing, .identity = try flagValue(args, "--identity"), .entitlements = try flagValue(args, "--entitlements"), .team_id = try flagValue(args, "--team-id") }, .archive = archive, .env_map = init.environ_map, }); tooling.package.printDiagnostic(stats); } else if (std.mem.eql(u8, command, "dev")) { checkVerbFlags("dev", args[2..], .{ .usage = "dev [dir] [--yes] [--target ios|android] [--device name] [--url url] [--command \"npm run dev\"] [--timeout-ms n] [-D... zig build flags]\n native dev [dir] --core [--script msgs.ndjson] [--watch]\n native dev [--manifest app.zon] --binary path [--url url] [--command \"npm run dev\"] [--timeout-ms n]", .value_flags = &.{ "--url", "--command", "--timeout-ms", "--binary", "--manifest", "--target", "--device", "--script" }, .bool_flags = &.{ "--yes", "--core", "--watch" }, .forwards_build_flags = true, }); if (flagBool(args, "--core")) { // The TypeScript core-logic loop under node (update/effects on a // virtual host) - not a renderer; plain `native dev` runs the app. const verb_args = parseVerbArgs(allocator, args[2..], &.{"--script"}) catch fail("usage: native dev [dir] --core [--script msgs.ndjson] [--watch]"); try enterAppDir(init.io, verb_args.dir); const framework_root = try tooling.buildgraph.resolveFrameworkRoot(allocator, init.io, init.environ_map) orelse { std.debug.print("cannot locate the Native SDK framework; set NATIVE_SDK_PATH to your framework checkout\n", .{}); std.process.exit(1); }; defer allocator.free(framework_root); // The editor surface rides along with the dev loop too (TS // trees only — never leave node_modules in a non-app dir). if (tooling.ts_core.detect(init.io) == .ts) { tooling.ts_core.selfHealEditorPackage(allocator, init.io, framework_root); } tooling.ts_core.runDevHost(allocator, init.io, framework_root, .{ .base_env = init.environ_map, .script = try flagValue(args, "--script"), .watch = flagBool(args, "--watch"), }) catch |err| return failVerb(err); return; } if (try flagValue(args, "--target")) |dev_target| { // The mobile dev loop: build the embed library for the // simulator/emulator, wrap it in the toolkit-owned host, // install + launch, and stream the app log. const is_ios = std.mem.eql(u8, dev_target, "ios"); const is_android = std.mem.eql(u8, dev_target, "android"); if (!is_ios and !is_android) { fail("`native dev --target` supports: ios, android (desktop is the default without --target)"); } const verb_args = parseVerbArgs(allocator, args[2..], &.{ "--target", "--device", "--url", "--command", "--timeout-ms" }) catch fail("usage: native dev [dir] --target ios|android [--device name] [--yes] [-D... zig build flags]"); try enterAppDir(init.io, verb_args.dir); if (is_ios) { tooling.ios.runDev(allocator, init.io, .{ .base_env = init.environ_map, .assume_yes = verb_args.assume_yes, .forwarded_args = verb_args.forwarded, .device = try flagValue(args, "--device"), }) catch |err| return failVerb(err); } else { tooling.android.runDev(allocator, init.io, .{ .base_env = init.environ_map, .assume_yes = verb_args.assume_yes, .forwarded_args = verb_args.forwarded, .device = try flagValue(args, "--device"), }) catch |err| return failVerb(err); } } else if ((try flagValue(args, "--binary")) != null) { // Legacy shape (`--binary` provided): the caller already built // the shell — e.g. the expanded template's `zig build dev` step — // so only run the frontend-server + shell flow. Unchanged. const manifest_path = try flagValue(args, "--manifest") orelse "app.zon"; const metadata = try tooling.manifest.readMetadata(allocator, init.io, manifest_path); const command_override = if (try flagValue(args, "--command")) |value| try splitCommand(allocator, value) else null; try tooling.dev.run(allocator, init.io, .{ .metadata = metadata, .base_env = init.environ_map, .binary_path = try flagValue(args, "--binary"), .url_override = try flagValue(args, "--url"), .command_override = command_override, .timeout_ms = if (try flagValue(args, "--timeout-ms")) |value| try std.fmt.parseUnsigned(u32, value, 10) else null, }); } else { const verb_args = parseVerbArgs(allocator, args[2..], &.{ "--url", "--command", "--timeout-ms" }) catch fail("usage: native dev [dir] [--yes] [--url url] [--command \"npm run dev\"] [--timeout-ms n] [-D... zig build flags]"); try enterAppDir(init.io, verb_args.dir); const command_override = if (try flagValue(args, "--command")) |value| try splitCommand(allocator, value) else null; tooling.verbs.run(allocator, init.io, .dev, .{ .base_env = init.environ_map, .assume_yes = verb_args.assume_yes, .forwarded_args = verb_args.forwarded, .url_override = try flagValue(args, "--url"), .command_override = command_override, .timeout_ms = if (try flagValue(args, "--timeout-ms")) |value| try std.fmt.parseUnsigned(u32, value, 10) else null, }) catch |err| return failVerb(err); } } else if (std.mem.eql(u8, command, "package-windows")) { checkPackageShortcutFlags(command, args[2..]); try packageShortcut(allocator, init.io, init.environ_map, args, .windows, "zig-out/package/windows"); } else if (std.mem.eql(u8, command, "package-linux")) { checkPackageShortcutFlags(command, args[2..]); try packageShortcut(allocator, init.io, init.environ_map, args, .linux, "zig-out/package/linux"); } else if (std.mem.eql(u8, command, "package-ios")) { checkPackageShortcutFlags(command, args[2..]); const metadata = try tooling.manifest.readMetadata(allocator, init.io, try flagValue(args, "--manifest") orelse "app.zon"); const web_engine = try tooling.web_engine.resolve(.{ .web_engine = metadata.web_engine, .cef = metadata.cef }, .{}); const binary_path = try flagValue(args, "--binary") orelse try iosPackageLibrary(allocator, init.io, init.environ_map, metadata.name, "ReleaseFast"); const stats = try tooling.package.createPackage(allocator, init.io, .{ .metadata = metadata, .target = .ios, .optimize = "ReleaseFast", .output_path = try flagValue(args, "--output") orelse if (args.len >= 3 and args[2].len > 0 and args[2][0] != '-') args[2] else "zig-out/mobile/ios", .binary_path = binary_path, .assets_dir = try flagValue(args, "--assets") orelse if (metadata.frontend) |frontend| frontend.dist else "assets", .frontend = metadata.frontend, .web_engine = web_engine.engine, .cef_dir = web_engine.cef_dir, .env_map = init.environ_map, }); tooling.package.printDiagnostic(stats); } else if (std.mem.eql(u8, command, "package-android")) { checkPackageShortcutFlags(command, args[2..]); const metadata = try tooling.manifest.readMetadata(allocator, init.io, try flagValue(args, "--manifest") orelse "app.zon"); const web_engine = try tooling.web_engine.resolve(.{ .web_engine = metadata.web_engine, .cef = metadata.cef }, .{}); const binary_path = try flagValue(args, "--binary") orelse try androidPackageLibrary(allocator, init.io, init.environ_map, metadata.name, "ReleaseFast"); const stats = try tooling.package.createPackage(allocator, init.io, .{ .metadata = metadata, .target = .android, .optimize = "ReleaseFast", .output_path = try flagValue(args, "--output") orelse if (args.len >= 3 and args[2].len > 0 and args[2][0] != '-') args[2] else "zig-out/mobile/android", .binary_path = binary_path, .assets_dir = try flagValue(args, "--assets") orelse if (metadata.frontend) |frontend| frontend.dist else "assets", .frontend = metadata.frontend, .web_engine = web_engine.engine, .cef_dir = web_engine.cef_dir, .env_map = init.environ_map, }); tooling.package.printDiagnostic(stats); } else if (std.mem.eql(u8, command, "automate")) { try automation_cli.run(allocator, init.io, init.environ_map, 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 { std.debug.print("unknown command: {s}\n\n", .{command}); usage(); std.process.exit(1); } } fn usage() void { std.debug.print( \\usage: native \\ \\commands: \\ init [path] [--template ] [--frontend ] [--framework ] [--full] (defaults: native + ts-core) \\ dev [dir] [--yes] [-D... zig build flags] build a Debug binary and run it (markup hot reload) \\ dev [dir] --target ios [--device name] build for the iOS simulator, install + launch, stream the log (experimental) \\ dev [dir] --target android [--device name] build a debug APK, install + launch on an emulator via adb, stream the log (experimental) \\ dev [dir] --core [--script msgs.ndjson] [--watch] run the TypeScript core's logic loop under node (update/effects transcript; not a renderer) \\ build [dir] [--yes] [-D... zig build flags] build a ReleaseFast binary into zig-out/bin/ \\ test [dir] [--yes] [-D... zig build flags] run the app's test suite \\ check [dir] [--strict] validate the core (src/core.ts through the subset checker), src/*.native markup, and app.zon \\ eject [dir] write an owned build.zig/build.zig.zon into the app \\ eject component [dir] write an owned copy of a library composite into src/components/ \\ cef install|path|doctor [--dir path] [--version version] [--source prepared|official] [--force] \\ doctor [--strict] [--manifest app.zon] [--web-engine system|chromium] [--cef-dir path] [--cef-auto-install] \\ validate [app.zon] \\ bundle-assets [app.zon] [assets] [output] \\ package [--target macos|windows|linux|ios|android] [--output path] [--binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--team-id id] [--archive] \\ dev [--manifest app.zon] --binary path [--url http://127.0.0.1:5173/] [--command "npm run dev"] [--timeout-ms 30000] \\ package-windows [--output path] [--binary path] \\ package-linux [--output path] [--binary path] \\ package-ios [--output path] [--binary path] \\ package-android [--output path] [--binary path] \\ markup check [more files...] [--strict] | markup dump [--out doc.nsui] | markup lsp \\ automate \\ skills list|get \\ version \\ , .{}); } fn fail(message: []const u8) noreturn { std.debug.print("{s}\n", .{message}); std.process.exit(1); } /// Flag discipline for every verb parsed here: ` --help` prints the /// verb's usage and exits 0, and an unrecognized flag prints the same usage /// and exits 1. A flag typo must never fall through to a real run (`native /// init --help` once scaffolded a real app named after the cwd). const VerbSpec = struct { usage: []const u8, /// Flags that consume the following argument. value_flags: []const []const u8 = &.{}, /// Boolean flags. bool_flags: []const []const u8 = &.{}, /// Verb forwards -D.../--release... to `zig build`. forwards_build_flags: bool = false, }; fn checkPackageShortcutFlags(verb: []const u8, args: []const []const u8) void { var usage_buffer: [128]u8 = undefined; const usage_text = std.fmt.bufPrint(&usage_buffer, "{s} [--output path] [--binary path] [--manifest app.zon] [--assets path]", .{verb}) catch verb; checkVerbFlags(verb, args, .{ .usage = usage_text, .value_flags = &.{ "--manifest", "--output", "--binary", "--assets" }, }); } fn checkVerbFlags(verb: []const u8, args: []const []const u8, spec: VerbSpec) void { var index: usize = 0; args: while (index < args.len) : (index += 1) { const arg = args[index]; if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { std.debug.print("usage: native {s}\n", .{spec.usage}); std.process.exit(0); } if (!std.mem.startsWith(u8, arg, "-")) continue; if (spec.forwards_build_flags and (std.mem.startsWith(u8, arg, "-D") or std.mem.startsWith(u8, arg, "--release"))) continue; for (spec.bool_flags) |flag| { if (std.mem.eql(u8, arg, flag)) continue :args; } for (spec.value_flags) |flag| { if (std.mem.eql(u8, arg, flag)) { index += 1; continue :args; } } std.debug.print("unknown flag {s} for `native {s}`\nusage: native {s}\n", .{ arg, verb, spec.usage }); std.process.exit(1); } } /// Expected verb failures already printed a teaching message (or zig's own /// compile errors are on screen); exit without a Zig error-return trace. fn failVerb(err: anyerror) anyerror!void { switch (err) { error.MissingManifest, error.MissingFramework, error.CrossVolumeFramework, error.ZigUnavailable, error.DownloadDeclined, error.UnsupportedPlatform, error.ChecksumMismatch, error.ZigBuildFailed, error.InvalidManifest, error.MarkupCheckFailed, error.BothCores, error.MissingTsCore, error.MissingNode, error.MissingTranspiler, error.CoreCheckFailed, error.DevHostFailed, error.HostCompileFailed, error.SimulatorUnavailable, error.SimulatorCommandFailed, => std.process.exit(1), else => return err, } } fn printInitNextSteps(destination: []const u8, frontend: tooling.templates.Frontend, shape: tooling.templates.Shape) void { std.debug.print("\nNext steps:\n", .{}); if (!std.mem.eql(u8, destination, ".")) { std.debug.print(" cd {s}\n", .{destination}); } if (frontend == .native and shape == .slim) { std.debug.print(" native dev\n", .{}); } else { std.debug.print(" zig build run\n", .{}); } } const VerbArgs = struct { dir: []const u8 = ".", assume_yes: bool = false, forwarded: []const []const u8 = &.{}, }; /// Parse `native ` arguments: an optional app directory, --yes, and /// -D/--release flags forwarded verbatim to `zig build`. `value_flags` /// names verb-specific flags whose values must be skipped (handled by the /// caller through flagValue). fn parseVerbArgs(allocator: std.mem.Allocator, args: []const []const u8, value_flags: []const []const u8) !VerbArgs { var out: VerbArgs = .{}; var forwarded: std.ArrayList([]const u8) = .empty; errdefer forwarded.deinit(allocator); var index: usize = 0; args: while (index < args.len) : (index += 1) { const arg = args[index]; if (std.mem.eql(u8, arg, "--yes")) { out.assume_yes = true; continue; } // Verb-specific boolean flags read by the caller via flagBool. if (std.mem.eql(u8, arg, "--strict")) continue; if (std.mem.eql(u8, arg, "--core")) continue; if (std.mem.eql(u8, arg, "--watch")) continue; if (std.mem.startsWith(u8, arg, "-D") or std.mem.startsWith(u8, arg, "--release")) { try forwarded.append(allocator, arg); continue; } for (value_flags) |flag| { if (std.mem.eql(u8, arg, flag)) { index += 1; if (index >= args.len) return error.InvalidArguments; continue :args; } } if (std.mem.startsWith(u8, arg, "-")) return error.InvalidArguments; if (!std.mem.eql(u8, out.dir, ".")) return error.InvalidArguments; out.dir = arg; } out.forwarded = try forwarded.toOwnedSlice(allocator); return out; } fn enterAppDir(io: std.Io, dir: []const u8) !void { if (std.mem.eql(u8, dir, ".")) return; std.process.setCurrentPath(io, dir) catch { std.debug.print("cannot enter app directory {s}\n", .{dir}); return error.MissingAppDirectory; }; } /// `native check`: validate every markup file under src/ plus app.zon — the /// no-build confidence pass (markup vocabulary + manifest schema). With a /// fresh model-contract artifact in zig-out (refreshed by `native test`), /// the markup pass also verifies /// bindings, iterables, message tags, and expression types against the /// app's actual Model/Msg, and reports unused model state as warnings /// (--strict promotes warnings to failures). fn runCheck(allocator: std.mem.Allocator, io: std.Io, env_map: *std.process.Environ.Map, strict: bool) !void { if (!tooling.buildgraph.fileExists(io, "app.zon")) { std.debug.print("no app.zon here — `native check` runs inside an app directory (or pass one: `native check path/to/app`)\n", .{}); return error.MissingManifest; } // The core tier first: a TypeScript core runs the @native-sdk/core checker // (subset rules + tsc), whose NS diagnostics surface verbatim - the // same teaching UX Zig apps get from `zig build`/`native test`. const core_tree = tooling.ts_core.detect(io); if (core_tree == .both) return tooling.ts_core.failBothCores(); if (core_tree == .ts) { const framework_root = try tooling.buildgraph.resolveFrameworkRoot(allocator, io, env_map) orelse { std.debug.print("cannot locate the Native SDK framework; set NATIVE_SDK_PATH to your framework checkout\n", .{}); return error.MissingFramework; }; defer allocator.free(framework_root); // Self-heal the editor surface first: node_modules/@native-sdk/core // (the pre-publish copy stock tsc resolves) refreshes whenever it is // missing or version-skewed vs the SDK — one info line, never a // check failure. tooling.ts_core.selfHealEditorPackage(allocator, io, framework_root); try tooling.ts_core.checkCore(allocator, io, env_map, framework_root); } var markup_files: std.ArrayList([]const u8) = .empty; defer markup_files.deinit(allocator); try collectMarkupFiles(allocator, io, "src", &markup_files); var outcome = markup_cli.CheckOutcome{}; if (markup_files.items.len > 0) { outcome = try markup_cli.checkFiles(allocator, io, markup_files.items); if (outcome.failures > 0) return error.MarkupCheckFailed; } const result = try tooling.manifest.validateFile(allocator, io, "app.zon"); tooling.manifest.printDiagnostic(result); if (!result.ok) return error.InvalidManifest; // The build-graph web-layer inference, surfaced where authors look: // whether this app ships the embedded web layer, and why. `check` // takes no engine flag, so the manifest's own engine is the resolved // engine here. const metadata = try tooling.manifest.readMetadata(allocator, io, "app.zon"); if (tooling.manifest.webLayerFromManifest(metadata)) |layer| { std.debug.print("web layer: {s} ({s})\n", .{ if (layer.enabled) "included" else "none", layer.sourceText() }); } else |_| { // Contradictions and invalid values were already rejected by the // validation pass above; nothing more to add here. } const checked_markup = markup_files.items.len; const contract_note: []const u8 = if (outcome.contract_checked) " against the model contract" else ""; const core_note: []const u8 = if (core_tree == .ts) " and src/core.ts (subset checker clean)" else ""; std.debug.print("checked {d} markup file{s}{s}, app.zon{s}\n", .{ checked_markup, if (checked_markup == 1) "" else "s", contract_note, core_note }); if (strict and outcome.warnings > 0) { std.debug.print("{d} warning{s} promoted to errors (--strict)\n", .{ outcome.warnings, if (outcome.warnings == 1) "" else "s" }); return error.MarkupCheckFailed; } } /// Every `.native` file under the root. fn collectMarkupFiles(allocator: std.mem.Allocator, io: std.Io, root_path: []const u8, out: *std.ArrayList([]const u8)) !void { var root = std.Io.Dir.cwd().openDir(io, root_path, .{ .iterate = true }) catch return; defer root.close(io); var walker = try root.walk(allocator); defer walker.deinit(); while (try walker.next(io)) |entry| { if (entry.kind == .file and markup_cli.hasMarkupExtension(entry.path)) { try out.append(allocator, try std.fs.path.join(allocator, &.{ root_path, entry.path })); } } } /// `native eject`: transfer build ownership to the app exactly once. fn runEject(allocator: std.mem.Allocator, io: std.Io, env_map: *std.process.Environ.Map) !void { if (!tooling.buildgraph.fileExists(io, "app.zon")) { std.debug.print("no app.zon here — `native eject` runs inside an app directory (or pass one: `native eject path/to/app`)\n", .{}); return error.MissingManifest; } const metadata = try tooling.manifest.readMetadata(allocator, io, "app.zon"); const framework_root = try tooling.buildgraph.resolveFrameworkRoot(allocator, io, env_map) orelse { std.debug.print("cannot locate the Native SDK framework; set NATIVE_SDK_PATH to your framework checkout\n", .{}); return error.MissingFramework; }; defer allocator.free(framework_root); tooling.buildgraph.eject(allocator, io, ".", .{ .app_name = metadata.name, .framework_root = framework_root, }) catch |err| switch (err) { error.AlreadyEjected => { std.debug.print("build.zig or build.zig.zon already exists — eject writes the owned build exactly once and never overwrites it\n", .{}); std.process.exit(1); }, else => return err, }; std.debug.print( \\ejected: build.zig and build.zig.zon now belong to this app. \\`native dev|build|test` drive them via `zig build` from now on; the \\generated graph under .native/ is unused and safe to delete. \\ , .{}); } /// `native eject component `: transfer ownership of one library /// composite to the app — write its canonical source into /// src/components/, exactly once. The component registry, the writer, /// and the did-you-mean live in tooling (`eject_components.zig`); this /// wrapper owns the CLI's teaching messages. fn runEjectComponent(io: std.Io, name: []const u8) !void { if (!tooling.buildgraph.fileExists(io, "app.zon")) { std.debug.print("no app.zon here — `native eject component` runs inside an app directory (or pass one: `native eject component {s} path/to/app`)\n", .{name}); return error.MissingManifest; } const component = tooling.eject_components.find(name) orelse { if (tooling.eject_components.suggestion(name)) |suggested| { std.debug.print("unknown component \"{s}\" (did you mean \"{s}\"?) — ejectable components: {s}\n", .{ name, suggested, tooling.eject_components.component_list }); } else { std.debug.print("unknown component \"{s}\" — ejectable components: {s}\n", .{ name, tooling.eject_components.component_list }); } std.process.exit(1); }; tooling.eject_components.eject(io, ".", component) catch |err| switch (err) { error.AlreadyEjected => { std.debug.print("already ejected at {s} - delete it to re-eject\n", .{component.path}); std.process.exit(1); }, error.WriteFailed => { std.debug.print("cannot write {s} — check the app directory is writable\n", .{component.path}); std.process.exit(1); }, }; std.debug.print( \\ejected: {s} now belongs to this app ({s}). \\The file's header comment walks through migrating call sites; the \\library form keeps working wherever you have not migrated. Run \\`native check` to validate the app afterwards. \\ , .{ component.path, component.form }); } fn initAppName(allocator: std.mem.Allocator, io: std.Io, destination: []const u8) !struct { []const u8, bool } { if (!std.mem.eql(u8, destination, ".")) { return .{ std.fs.path.basename(destination), false }; } const cwd = try std.process.currentPathAlloc(io, allocator); defer allocator.free(cwd); const basename = std.fs.path.basename(cwd); if (basename.len == 0) return .{ try allocator.dupe(u8, "native-sdk-app"), true }; return .{ try allocator.dupe(u8, basename), true }; } /// `native package --target ios` without --binary: build the DEVICE /// slice through the app's `zig build lib` step — always a fresh build, /// never a path discovery, and the install stage is keyed by the target /// triple (.native/embed/), so the dev loop's SIMULATOR slice /// and the Android tier can never leave wrong-target bytes where this /// build looks. A failed build degrades to a libraryless project with a /// teaching warning (the repo-shaped webview apps have no mobile UiApp /// to compile) instead of aborting packaging. fn iosPackageLibrary(allocator: std.mem.Allocator, io: std.Io, env_map: *std.process.Environ.Map, app_name: []const u8, optimize: []const u8) !?[]const u8 { std.debug.print("info[package.ios]: building the embed static library ({s}, {s})\n", .{ tooling.ios.LibSlice.device.zigTriple(), optimize }); return tooling.ios.buildEmbedLib(allocator, io, app_name, .{ .base_env = env_map, .slice = .device, .optimize = optimize, }) catch |err| switch (err) { error.ZigBuildFailed, error.MissingFramework => { std.debug.print("warning[package.no-binary]: could not build the embed library - the project will not contain Libraries/libnative-sdk.a\n" ++ " build it first (`zig build lib -Dtarget={s}`) or pass --binary ; the app must expose a mobile UiApp (`mobileOptions`)\n", .{tooling.ios.LibSlice.device.zigTriple()}); return null; }, // Cross-volume SDK: the junction bridge failed, so the generated // host project could not build against the SDK either — a // libraryless project would ship broken. The teaching message is // already on screen; fail the whole package verb without a trace. error.CrossVolumeFramework => std.process.exit(1), else => return err, }; } /// `native package --target android` without --binary: build the /// aarch64-linux-android embed static library the generated host project /// (and its APK assembly) links. A failed build degrades to a /// libraryless project with a teaching warning instead of aborting /// packaging, mirroring the iOS path. fn androidPackageLibrary(allocator: std.mem.Allocator, io: std.Io, env_map: *std.process.Environ.Map, app_name: []const u8, optimize: []const u8) !?[]const u8 { std.debug.print("info[package.android]: building the embed static library ({s}, {s})\n", .{ tooling.android.zig_triple, optimize }); return tooling.android.buildEmbedLib(allocator, io, app_name, .{ .base_env = env_map, .optimize = optimize, }) catch |err| switch (err) { error.ZigBuildFailed, error.MissingFramework => { std.debug.print("warning[package.no-binary]: could not build the embed library - the project will not contain Libraries/libnative-sdk.a\n" ++ " build it first (`zig build lib -Dtarget={s}`) or pass --binary ; the app must expose a mobile UiApp (`mobileOptions`)\n", .{tooling.android.zig_triple}); return null; }, // Cross-volume SDK: the junction bridge failed, so the generated // host project could not build against the SDK either — a // libraryless project would ship broken. The teaching message is // already on screen; fail the whole package verb without a trace. error.CrossVolumeFramework => std.process.exit(1), else => return err, }; } /// `native package` without --binary: the scaffolded build installs the /// app binary at zig-out/bin/, so look there before /// falling back to a binaryless bundle. fn discoverAppBinary(allocator: std.mem.Allocator, io: std.Io, app_name: []const u8, target: tooling.package.PackageTarget) !?[]const u8 { const suffix: []const u8 = if (target == .windows) ".exe" else ""; const candidate = try std.fmt.allocPrint(allocator, "zig-out/bin/{s}{s}", .{ app_name, suffix }); var file = std.Io.Dir.cwd().openFile(io, candidate, .{}) catch { allocator.free(candidate); return null; }; file.close(io); std.debug.print("info[package.binary]: using zig-out/bin/{s}\n", .{app_name}); return candidate; } /// Where `native init` points the new app's SDK dependency when no /// --framework flag is given: the same resolution `native dev|build|test` /// use (NATIVE_SDK_PATH, then the CLI executable's own location — see /// buildgraph.resolveFrameworkRoot), so init and the verbs can never /// disagree about which SDK an app builds against. fn initFrameworkPath(allocator: std.mem.Allocator, io: std.Io, env_map: *std.process.Environ.Map) !struct { []const u8, bool } { if (try tooling.buildgraph.resolveFrameworkRoot(allocator, io, env_map)) |path| return .{ path, true }; return .{ ".", false }; } fn hasFrameworkRoot(allocator: std.mem.Allocator, io: std.Io, root: []const u8) bool { return tooling.buildgraph.hasFrameworkRoot(allocator, io, root); } fn flagValue(args: []const []const u8, name: []const u8) error{MissingFlagValue}!?[]const u8 { for (args, 0..) |arg, index| { if (std.mem.eql(u8, arg, name)) { if (index + 1 < args.len) return args[index + 1]; return error.MissingFlagValue; } } return null; } fn flagBool(args: []const []const u8, name: []const u8) bool { for (args) |arg| { if (std.mem.eql(u8, arg, name)) return true; } return false; } fn positionalArg(args: []const []const u8) ?[]const u8 { var skip_next = false; for (args) |arg| { if (skip_next) { skip_next = false; continue; } if (std.mem.startsWith(u8, arg, "--")) { if (std.mem.eql(u8, arg, "--frontend") or std.mem.eql(u8, arg, "--framework") or std.mem.eql(u8, arg, "--template") or std.mem.eql(u8, arg, "--script") or std.mem.eql(u8, arg, "--manifest") or std.mem.eql(u8, arg, "--target") or std.mem.eql(u8, arg, "--output") or std.mem.eql(u8, arg, "--binary") or std.mem.eql(u8, arg, "--assets") or std.mem.eql(u8, arg, "--web-engine") or std.mem.eql(u8, arg, "--web-layer") or std.mem.eql(u8, arg, "--cef-dir") or std.mem.eql(u8, arg, "--signing") or std.mem.eql(u8, arg, "--identity") or std.mem.eql(u8, arg, "--entitlements") or std.mem.eql(u8, arg, "--team-id") or std.mem.eql(u8, arg, "--command") or std.mem.eql(u8, arg, "--url") or std.mem.eql(u8, arg, "--timeout-ms") or std.mem.eql(u8, arg, "--device")) { skip_next = true; } continue; } return arg; } return null; } fn splitCommand(allocator: std.mem.Allocator, value: []const u8) ![]const []const u8 { var parts: std.ArrayList([]const u8) = .empty; errdefer parts.deinit(allocator); var tokens = std.mem.tokenizeScalar(u8, value, ' '); while (tokens.next()) |token| { try parts.append(allocator, try allocator.dupe(u8, token)); } return parts.toOwnedSlice(allocator); } fn packageShortcut(allocator: std.mem.Allocator, io: std.Io, env_map: *std.process.Environ.Map, args: []const []const u8, target: tooling.package.PackageTarget, default_output: []const u8) !void { const metadata = try tooling.manifest.readMetadata(allocator, io, try flagValue(args, "--manifest") orelse "app.zon"); const web_engine = try tooling.web_engine.resolve(.{ .web_engine = metadata.web_engine, .cef = metadata.cef }, .{}); const stats = try tooling.package.createPackage(allocator, io, .{ .metadata = metadata, .target = target, .output_path = try flagValue(args, "--output") orelse default_output, .binary_path = try flagValue(args, "--binary"), .assets_dir = try flagValue(args, "--assets") orelse if (metadata.frontend) |frontend| frontend.dist else "assets", .frontend = metadata.frontend, .web_engine = web_engine.engine, .cef_dir = web_engine.cef_dir, .env_map = env_map, }); tooling.package.printDiagnostic(stats); }