@incajs/cli 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Dual-licensed under MIT OR Apache-2.0.
2
+
3
+ See:
4
+ - https://github.com/tom96da/incajs/blob/main/LICENSE-MIT
5
+ - https://github.com/tom96da/incajs/blob/main/LICENSE-APACHE
package/README.md CHANGED
@@ -1,2 +1,108 @@
1
- Name reservation for `@incajs/cli`; the real implementation is still in
2
- development. See https://github.com/tom96da/incajs.
1
+ <!--
2
+ Copyright (c) 2026 tom96da
3
+ SPDX-License-Identifier: MIT OR Apache-2.0
4
+ -->
5
+
6
+ # @incajs/cli
7
+
8
+ The `inca` command. Internally it wires a bundler adapter's build watch to
9
+ a dev-protocol client talking to `inca-host` — both live inside this
10
+ package (`src/adapter/`, `src/dev-client/`) since neither is ever imported
11
+ on its own.
12
+
13
+ `inca dev` watches an app's entry point, starts `inca-host` once the
14
+ first bundle lands, and reloads it on every rebuild. `inca build` runs
15
+ the same pipeline once, with the watcher removed and production settings
16
+ on, and never starts a host. `inca package` builds the same way, then
17
+ pairs the bundle with a prebuilt `inca-host` into a distributable
18
+ application. `adapter/vite` is the bundler wired in by default for all
19
+ three — swapping it for another `Bundler` (the contract `adapter/types.mts`
20
+ defines) is a dependency change in `defaultBundler.mts`, not an edit
21
+ anywhere else.
22
+
23
+ ## App entry
24
+
25
+ No entry point is required. Drop a `src/App.vue` and that's a whole app —
26
+ `inca` wraps it in `createIncaApp(App).mount()` (from `incajs/vue`)
27
+ itself. Commit a `src/main.mts` instead for full control over
28
+ bootstrapping; it wins outright when both exist.
29
+
30
+ ## `inca package`
31
+
32
+ Emits a platform-native application into `dist/`: a `.app` on macOS, a
33
+ plain directory on Linux — other platforms aren't supported yet. Either
34
+ way, the layout puts the host binary and `bundle.js` beside each other, so
35
+ the app launches with no arguments and no terminal.
36
+
37
+ It needs a release build of the host to bundle — see `resolveHostBin`
38
+ below for how one is found.
39
+
40
+ App metadata comes from the app's own `package.json`, with an optional
41
+ `"inca"` key overriding what's derived from it:
42
+
43
+ ```jsonc
44
+ {
45
+ "name": "click_counter",
46
+ "version": "1.0.0",
47
+ "inca": {
48
+ "productName": "Click Counter", // defaults to "name", scope stripped
49
+ "identifier": "com.example.click-counter", // defaults to a generated org.inca.<slug>
50
+ "icon": "assets/icon.icns" // resolved relative to the app's own directory
51
+ }
52
+ }
53
+ ```
54
+
55
+ An `identifier` should be world-unique, so `inca package` prints a note
56
+ when it falls back to the generated one rather than using it silently.
57
+
58
+ ## `dev-client`
59
+
60
+ The Node end of the dev protocol: resolves and spawns `inca-host --dev
61
+ <bundle>`, and speaks the newline-delimited JSON-RPC 2.0 channel it
62
+ answers on.
63
+
64
+ `HostClient` correlates each request it sends with the response that
65
+ answers it and relays the host's stderr. Two notification methods carry
66
+ meaning of their own: `ready`, whose `params.protocol` it checks against
67
+ the revision it was built for, reporting and terminating the child on any
68
+ mismatch; and `appError`, an app fault the host caught and kept rendering
69
+ past, handed to its own callback rather than a generic one. Any other
70
+ method name is routed, `params` untouched and unparsed, to whichever
71
+ integration the caller registered for it.
72
+
73
+ A line the host writes that doesn't parse as a JSON-RPC message is treated
74
+ as stray output from a dependency, not a protocol violation: it's logged
75
+ and the channel keeps reading.
76
+
77
+ `resolveHostBin` tries, in order:
78
+
79
+ 1. `INCA_HOST_BIN`, if set — names the binary to spawn outright. This is
80
+ the escape hatch for a platform with no published binary yet, or a
81
+ custom-built one.
82
+ 2. The `optionalDependency` matching this OS/arch
83
+ (`@incajs/host-darwin-arm64`, `-darwin-x64`, `-linux-arm64`,
84
+ `-linux-x64` — Windows isn't supported yet), which almost every install
85
+ resolves through without either side ever needing Cargo or Rust.
86
+
87
+ @throws if neither resolves.
88
+
89
+ ## `adapter/vite`
90
+
91
+ Bundles an app's entry point into one self-contained bundle that
92
+ `inca-host` can evaluate: no unresolved imports, no dependency on
93
+ QuickJS having Node.js globals.
94
+
95
+ `watch(options)` builds `options.entry` into a bundle under
96
+ `options.outDir` — the returned `Watcher`'s `bundlePath` names the exact
97
+ file — rebuilding it on every change and reporting each result through
98
+ `options.onBuild`/`options.onError`. `build(options)` runs the same
99
+ pipeline once, minified and with `outDir` cleared first, resolving with
100
+ the bundle's path or rejecting on failure rather than reporting it
101
+ through a callback.
102
+
103
+ The template compiler is retargeted at `@vue/runtime-core` — the only Vue
104
+ runtime package `incajs/vue` itself depends on — instead of the default
105
+ `vue` import. `@vitejs/plugin-vue` itself still imports `vue` directly for
106
+ its own internals, which is why it's a peer dependency of this package:
107
+ that build-time need for `vue` never reaches an app's dependency tree or
108
+ the bundle it produces.
package/bin/inca.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 tom96da
3
+ // SPDX-License-Identifier: MIT OR Apache-2.0
4
+
5
+ import { run } from "../dist/cli.js";
6
+
7
+ await run();
@@ -0,0 +1,47 @@
1
+ /** A running build watch — the value {@link Bundler.watch} resolves to. */
2
+ export interface Watcher {
3
+ /** Stops watching and releases the underlying build process. */
4
+ close(): Promise<void>;
5
+ }
6
+ /** Options for {@link Bundler.watch}. */
7
+ export interface BundlerOptions {
8
+ /** The app's own entry point — may import `.vue` files. */
9
+ entry: string;
10
+ /** Where the build's output is written. */
11
+ outDir: string;
12
+ /** `"development"` keeps a framework's own warnings; `"production"` strips them. */
13
+ mode: "development" | "production";
14
+ /** Called after each successful (re)build, with what it wrote. */
15
+ onBuild: (output: BuildOutput) => void;
16
+ /** Called instead of `onBuild` when a (re)build fails. */
17
+ onError: (error: {
18
+ message: string;
19
+ stack: string | null;
20
+ }) => void;
21
+ }
22
+ /** Options for {@link Bundler.build}. */
23
+ export interface BuildOptions {
24
+ /** The app's own entry point — may import `.vue` files. */
25
+ entry: string;
26
+ /** Where the build's output is written. */
27
+ outDir: string;
28
+ }
29
+ /** What a build wrote — the result of {@link Bundler.build}, and every {@link BundlerOptions.onBuild} call. */
30
+ export interface BuildOutput {
31
+ /** Directory holding every file this build emitted. */
32
+ outDir: string;
33
+ /** Absolute path to the entry module `inca-host` evaluates. */
34
+ entryFile: string;
35
+ /** Every file this build emitted, relative to `outDir` — may include chunks and assets besides the entry itself. */
36
+ files: readonly string[];
37
+ }
38
+ /**
39
+ * The contract a bundler adapter satisfies. Supporting another bundler
40
+ * means adding a sibling adapter module and injecting its `Bundler` here —
41
+ * never editing this package's own types.
42
+ */
43
+ export interface Bundler {
44
+ watch(options: BundlerOptions): Promise<Watcher>;
45
+ /** One-shot production build, used by `inca build` — rejects on failure. */
46
+ build(options: BuildOptions): Promise<BuildOutput>;
47
+ }
@@ -0,0 +1,8 @@
1
+ import { BuildOptions, BuildOutput } from '../types.mts';
2
+ export type { BuildOptions, BuildOutput } from '../types.mts';
3
+ /**
4
+ * Builds `entry` into a minified, production build under `outDir` once, and
5
+ * rejects on failure rather than reporting it through a callback. Never
6
+ * starts or talks to `inca-host` — that's `dev.mts`'s job.
7
+ */
8
+ export declare function build({ entry, outDir }: BuildOptions): Promise<BuildOutput>;
@@ -0,0 +1,15 @@
1
+ import { Plugin } from 'vite';
2
+ /** What one (re)build actually wrote, relative to its own `outDir`. */
3
+ export interface CapturedOutput {
4
+ /** The emitted chunk marked as this build's entry. */
5
+ entryFile: string;
6
+ /** Every emitted file, as the bundler reported it. */
7
+ files: readonly string[];
8
+ }
9
+ /**
10
+ * A Vite plugin reporting `sink` the files a build wrote once they land on
11
+ * disk, via `writeBundle` — the one hook that fires uniformly for a
12
+ * one-shot build and every watch rebuild, so both read the bundler's real
13
+ * output instead of assuming a file name or layout.
14
+ */
15
+ export declare function captureOutput(sink: (output: CapturedOutput) => void): Plugin;
@@ -0,0 +1,26 @@
1
+ import { InlineConfig } from 'vite';
2
+ import { CapturedOutput } from './captureOutput.mts';
3
+ export declare const BUNDLE_FILE_NAME = "bundle.js";
4
+ export interface ResolveConfigOptions {
5
+ /** The app's own entry point — may import `.vue` files. */
6
+ entry: string;
7
+ /** Where the build's output is written. */
8
+ outDir: string;
9
+ /** `"development"` keeps `@vue/runtime-core`'s own warnings; `"production"` strips them. */
10
+ mode: "development" | "production";
11
+ /** Whether Vite should keep rebuilding on file changes. */
12
+ watch: boolean;
13
+ /** Called with what each successful (re)build wrote. */
14
+ onOutput: (output: CapturedOutput) => void;
15
+ }
16
+ /**
17
+ * Builds the Vite config shared by `watch` and `build`, compiling `.vue`
18
+ * files via `@vitejs/plugin-vue` targeted at `@vue/runtime-core` rather than
19
+ * the `vue` meta-package it requires to run.
20
+ *
21
+ * An import the entry doesn't inline — a dynamic `import()`, or a `.vue`
22
+ * file's own `<style>` block — lands as its own chunk or asset alongside
23
+ * the entry, named by `chunkFileNames`/`assetFileNames` below, rather than
24
+ * failing the build.
25
+ */
26
+ export declare function resolveViteConfig({ entry, outDir, mode, watch, onOutput, }: ResolveConfigOptions): InlineConfig;
@@ -0,0 +1,2 @@
1
+ export { build } from './build.mts';
2
+ export { watch } from './watch.mts';
@@ -0,0 +1,8 @@
1
+ import { BundlerOptions, Watcher } from '../types.mts';
2
+ export type { Watcher } from '../types.mts';
3
+ /**
4
+ * Builds `entry` into a build under `outDir` and rebuilds it on every
5
+ * change, calling `onBuild` with what each rebuild wrote. Never starts,
6
+ * reloads, or talks to `inca-host` — that's `dev.mts`'s job.
7
+ */
8
+ export declare function watch({ onBuild, onError, ...buildOptions }: BundlerOptions): Promise<Watcher>;
@@ -0,0 +1,18 @@
1
+ import { Bundler, BuildOutput } from './adapter/types.mts';
2
+ export interface BuildAppOptions {
3
+ /** The app's root directory. Defaults to `process.cwd()`. */
4
+ cwd?: string;
5
+ /**
6
+ * The app's entry point. Defaults to resolving it automatically: a
7
+ * committed `src/main.mts`, or `src/App.vue` wrapped in a synthesized one.
8
+ */
9
+ entry?: string;
10
+ /** Overrides the bundler — see {@link defaultBundler} for what's wired in by default. */
11
+ bundler?: Bundler;
12
+ }
13
+ /**
14
+ * Builds the app once through the same bundler `dev` uses, and returns what
15
+ * it wrote. Rejects on failure — deciding what that means for the process
16
+ * is `cli.mts`'s job.
17
+ */
18
+ export declare function build(options?: BuildAppOptions): Promise<BuildOutput>;
package/dist/cli.d.mts ADDED
@@ -0,0 +1,2 @@
1
+ /** Parses argv and runs the named subcommand: `dev`, `build`, or `package`. */
2
+ export declare function run(argv?: readonly string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,481 @@
1
+ import e from "node:path";
2
+ import { build as t } from "vite";
3
+ import n from "@vitejs/plugin-vue";
4
+ import { existsSync as r } from "node:fs";
5
+ import { chmod as i, cp as a, mkdir as o, readFile as s, readdir as c, rm as l, writeFile as u } from "node:fs/promises";
6
+ import { createRequire as d } from "node:module";
7
+ import { spawn as f } from "node:child_process";
8
+ import { once as p } from "node:events";
9
+ import m from "node:readline";
10
+ import { setTimeout as h } from "node:timers/promises";
11
+ //#region src/adapter/vite/captureOutput.mts
12
+ function g(e) {
13
+ return {
14
+ name: "inca:capture-output",
15
+ writeBundle(t, n) {
16
+ let r = Object.keys(n), i = Object.values(n).find((e) => e.type === "chunk" && e.isEntry);
17
+ if (!i) throw Error("inca: build produced no entry chunk");
18
+ e({
19
+ entryFile: i.fileName,
20
+ files: r
21
+ });
22
+ }
23
+ };
24
+ }
25
+ //#endregion
26
+ //#region src/adapter/vite/config.mts
27
+ var _ = "bundle.js";
28
+ function v({ entry: t, outDir: r, mode: i, watch: a, onOutput: o }) {
29
+ return {
30
+ configFile: !1,
31
+ root: e.dirname(t),
32
+ mode: i,
33
+ clearScreen: !1,
34
+ logLevel: "silent",
35
+ define: { "process.env.NODE_ENV": JSON.stringify(i) },
36
+ plugins: [n({ template: { compilerOptions: { runtimeModuleName: "@vue/runtime-core" } } }), g(o)],
37
+ build: {
38
+ lib: {
39
+ entry: t,
40
+ formats: ["es"],
41
+ fileName: () => _
42
+ },
43
+ outDir: r,
44
+ emptyOutDir: i === "production",
45
+ minify: i === "production",
46
+ watch: a ? {} : void 0,
47
+ rolldownOptions: { output: {
48
+ chunkFileNames: "chunks/[name]-[hash].js",
49
+ assetFileNames: "assets/[name]-[hash][extname]"
50
+ } }
51
+ }
52
+ };
53
+ }
54
+ //#endregion
55
+ //#region src/adapter/vite/build.mts
56
+ async function y({ entry: n, outDir: r }) {
57
+ let i;
58
+ if (await t(v({
59
+ entry: n,
60
+ outDir: r,
61
+ mode: "production",
62
+ watch: !1,
63
+ onOutput: (t) => {
64
+ i = {
65
+ outDir: r,
66
+ entryFile: e.join(r, t.entryFile),
67
+ files: t.files
68
+ };
69
+ }
70
+ })), !i) throw Error("inca: build produced no output");
71
+ return i;
72
+ }
73
+ //#endregion
74
+ //#region src/adapter/vite/watch.mts
75
+ async function b({ onBuild: n, onError: r, ...i }) {
76
+ let { outDir: a } = i, o = await t(v({
77
+ ...i,
78
+ watch: !0,
79
+ onOutput: (t) => {
80
+ n({
81
+ outDir: a,
82
+ entryFile: e.join(a, t.entryFile),
83
+ files: t.files
84
+ });
85
+ }
86
+ }));
87
+ return o.on("event", (e) => {
88
+ e.code === "ERROR" && r({
89
+ message: e.error.message,
90
+ stack: e.error.stack ?? null
91
+ });
92
+ }), { close: () => o.close() };
93
+ }
94
+ //#endregion
95
+ //#region src/defaultBundler.mts
96
+ var x = {
97
+ watch: b,
98
+ build: y
99
+ };
100
+ //#endregion
101
+ //#region src/entry.mts
102
+ async function S(t) {
103
+ let n = e.join(t, "src/main.mts");
104
+ if (r(n)) return n;
105
+ let i = e.join(t, "src/App.vue");
106
+ if (r(i)) return C(i, t);
107
+ throw Error(`no app entry found — expected ${n} or ${i}`);
108
+ }
109
+ async function C(t, n) {
110
+ let r = e.join(n, "node_modules/.inca");
111
+ await o(r, { recursive: !0 });
112
+ let i = e.join(r, "entry.mts"), a = JSON.stringify(t.split(e.sep).join("/"));
113
+ return await u(i, `import { createIncaApp } from "incajs/vue";\nimport App from ${a};\ncreateIncaApp(App).mount();\n`), i;
114
+ }
115
+ //#endregion
116
+ //#region src/build.mts
117
+ async function w(t = {}) {
118
+ let n = t.cwd ?? process.cwd(), r = e.join(n, "dist"), i = t.bundler ?? x, a = t.entry ?? await S(n);
119
+ return i.build({
120
+ entry: a,
121
+ outDir: r
122
+ });
123
+ }
124
+ //#endregion
125
+ //#region src/dev-client/hostBin.mts
126
+ var T = "INCA_HOST_BIN";
127
+ function E() {
128
+ let e = process.platform === "darwin" || process.platform === "linux" ? process.platform : void 0, t = process.arch === "arm64" || process.arch === "x64" ? process.arch : void 0;
129
+ return e && t ? `@incajs/host-${e}-${t}` : void 0;
130
+ }
131
+ function D() {
132
+ let e = E();
133
+ if (e) try {
134
+ return d(import.meta.url).resolve(`${e}/bin/inca-host`);
135
+ } catch {
136
+ return;
137
+ }
138
+ }
139
+ function O() {
140
+ let e = process.env[T];
141
+ if (e) return e;
142
+ let t = D();
143
+ if (t) return t;
144
+ let n = E(), r = n ? `no ${n} package is installed for it` : `this platform (${process.platform}/${process.arch}) has no published package`;
145
+ throw Error(`no inca-host binary found — ${r}, and ${T} isn't set to one`);
146
+ }
147
+ //#endregion
148
+ //#region src/dev-client/hostError.mts
149
+ var k = class extends Error {
150
+ code;
151
+ hostStack;
152
+ constructor(e, t, n = null) {
153
+ super(e), this.code = t, this.hostStack = n, this.name = "HostError";
154
+ }
155
+ };
156
+ function A(e) {
157
+ return typeof e == "object" && !!e && e.jsonrpc === "2.0";
158
+ }
159
+ //#endregion
160
+ //#region src/dev-client/hostClient.mts
161
+ var j = (e) => {
162
+ process.stderr.write(e.endsWith("\n") ? e : `${e}\n`);
163
+ }, M = class {
164
+ #e;
165
+ #t;
166
+ #n = 1;
167
+ #r = /* @__PURE__ */ new Map();
168
+ constructor(e) {
169
+ this.#e = e;
170
+ }
171
+ async start() {
172
+ let e = this.#e.hostBin ?? O(), t = f(e, ["--dev", this.#e.entryFile], { stdio: [
173
+ "pipe",
174
+ "pipe",
175
+ "pipe"
176
+ ] });
177
+ this.#t = t;
178
+ let n = this.#e.onStderr ?? j;
179
+ t.stderr.setEncoding("utf8"), t.stderr.on("data", (e) => n(e)), m.createInterface({ input: t.stdout }).on("line", (e) => {
180
+ this.#i(e, n);
181
+ }), t.on("exit", (e, t) => {
182
+ let n = /* @__PURE__ */ Error(`inca-host exited (code=${String(e)}, signal=${String(t)}) before answering`);
183
+ for (let { reject: e } of this.#r.values()) e(n);
184
+ this.#r.clear();
185
+ }), await p(t, "spawn");
186
+ }
187
+ #i(e, t) {
188
+ let n;
189
+ try {
190
+ n = JSON.parse(e);
191
+ } catch {
192
+ t(`[stray stdout] ${e}`);
193
+ return;
194
+ }
195
+ if (!A(n)) {
196
+ t(`[stray stdout] ${e}`);
197
+ return;
198
+ }
199
+ if ("method" in n) {
200
+ this.#a(n.method, n.params, t);
201
+ return;
202
+ }
203
+ let r = this.#r.get(n.id);
204
+ r && (this.#r.delete(n.id), "error" in n ? r.reject(new k(n.error.message, n.error.code, n.error.data?.stack ?? null)) : r.resolve(n.result));
205
+ }
206
+ #a(e, t, n) {
207
+ switch (e) {
208
+ case "ready": {
209
+ let { protocol: e } = t;
210
+ if (e !== 0) {
211
+ n(`inca-host speaks protocol ${String(e)}, this package was built for 0 — stopping it rather than carrying on`), this.#t && this.#o(this.#t);
212
+ return;
213
+ }
214
+ this.#e.onReady?.();
215
+ return;
216
+ }
217
+ case "appError":
218
+ this.#e.onAppError?.(t);
219
+ return;
220
+ default: {
221
+ let r = this.#e.integrations?.[e];
222
+ r ? r(t) : n(`[unhandled notification] ${e}`);
223
+ }
224
+ }
225
+ }
226
+ async #o(e) {
227
+ let t = p(e, "exit");
228
+ e.kill(), await t;
229
+ }
230
+ async call(e, t) {
231
+ let n = this.#t;
232
+ if (!n) throw Error("HostClient.start() has not been called");
233
+ let r = this.#n++, i = JSON.stringify({
234
+ jsonrpc: "2.0",
235
+ id: r,
236
+ method: e,
237
+ params: t
238
+ });
239
+ return new Promise((e, t) => {
240
+ this.#r.set(r, {
241
+ resolve: e,
242
+ reject: t
243
+ }), n.stdin.write(`${i}\n`);
244
+ });
245
+ }
246
+ async stop(e = 2e3) {
247
+ let t = this.#t;
248
+ if (!t || t.exitCode !== null || t.signalCode !== null) return;
249
+ let n = p(t, "exit");
250
+ this.call("shutdown").catch(() => {}), await Promise.race([n.then(() => "exited"), h(e).then(() => "timeout")]) === "timeout" && (t.kill(), await n);
251
+ }
252
+ };
253
+ //#endregion
254
+ //#region src/fault.mts
255
+ function N(e) {
256
+ return e instanceof k ? {
257
+ message: e.message,
258
+ stack: e.hostStack
259
+ } : e instanceof Error ? {
260
+ message: e.message,
261
+ stack: e.stack ?? null
262
+ } : {
263
+ message: String(e),
264
+ stack: null
265
+ };
266
+ }
267
+ function P(e, t, n) {
268
+ e.write(`[inca] ${t}: ${n.message}\n`), n.stack && e.write(`${n.stack}\n`);
269
+ }
270
+ //#endregion
271
+ //#region src/dev.mts
272
+ async function F(t, n) {
273
+ let r = new Set(n), i;
274
+ try {
275
+ i = await c(t, {
276
+ recursive: !0,
277
+ withFileTypes: !0
278
+ });
279
+ } catch {
280
+ return;
281
+ }
282
+ await Promise.all(i.filter((e) => e.isFile()).map((n) => e.relative(t, e.join(n.parentPath, n.name))).filter((e) => !r.has(e)).map((n) => l(e.join(t, n), { force: !0 })));
283
+ }
284
+ async function I(t) {
285
+ let n = t.cwd ?? process.cwd(), r = e.join(n, "dist"), i = t.bundler ?? x, a = t.stdout ?? process.stdout, o = t.stderr ?? process.stderr, s = t.entry ?? await S(n), c, l = !1, u = !1, d = Promise.resolve();
286
+ async function f() {
287
+ try {
288
+ await c?.call("reload");
289
+ } catch (e) {
290
+ P(o, "reload failed", N(e));
291
+ }
292
+ }
293
+ async function p(e) {
294
+ if (await F(e.outDir, e.files), !c) {
295
+ let n = new M({
296
+ entryFile: e.entryFile,
297
+ hostBin: t.hostBin,
298
+ onStderr: (e) => o.write(e),
299
+ onReady: () => {
300
+ l = !0, a.write("[inca] ready\n"), u && (u = !1, f());
301
+ },
302
+ onAppError: (e) => P(o, "app error", e)
303
+ });
304
+ try {
305
+ await n.start();
306
+ } catch (e) {
307
+ P(o, "failed to start inca-host", N(e));
308
+ return;
309
+ }
310
+ c = n;
311
+ return;
312
+ }
313
+ if (!l) {
314
+ u = !0;
315
+ return;
316
+ }
317
+ await f();
318
+ }
319
+ let m = await i.watch({
320
+ entry: s,
321
+ outDir: r,
322
+ mode: "development",
323
+ onBuild: (e) => {
324
+ d = d.then(() => p(e));
325
+ },
326
+ onError: (e) => {
327
+ P(o, "build failed", e);
328
+ }
329
+ });
330
+ await new Promise((e) => {
331
+ if (t.signal.aborted) {
332
+ e();
333
+ return;
334
+ }
335
+ t.signal.addEventListener("abort", () => e(), { once: !0 });
336
+ }), await d, await c?.stop(), await m.close();
337
+ }
338
+ //#endregion
339
+ //#region src/metadata.mts
340
+ function L(e) {
341
+ let t = e.indexOf("/");
342
+ return e.startsWith("@") && t !== -1 ? e.slice(t + 1) : e;
343
+ }
344
+ function R(e) {
345
+ return e.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
346
+ }
347
+ async function z(t) {
348
+ let n = e.join(t, "package.json"), i;
349
+ try {
350
+ i = await s(n, "utf8");
351
+ } catch {
352
+ throw Error(`no package.json found at ${n}`);
353
+ }
354
+ let a = JSON.parse(i), o = a.inca ?? {}, c = o.productName ?? (a.name ? L(a.name) : void 0);
355
+ if (!c) throw Error(`${n} needs a "name", or "inca": { "productName" }`);
356
+ let l = a.version ?? "0.0.0", u = o.identifier ?? `org.inca.${R(c)}`, d;
357
+ if (o.icon && (d = e.resolve(t, o.icon), !r(d))) throw Error(`"inca.icon" points to ${d}, which doesn't exist`);
358
+ return {
359
+ productName: c,
360
+ identifier: u,
361
+ identifierIsDefault: o.identifier === void 0,
362
+ version: l,
363
+ icon: d
364
+ };
365
+ }
366
+ //#endregion
367
+ //#region src/plist.mts
368
+ function B(e) {
369
+ return e.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
370
+ }
371
+ function V(e) {
372
+ return typeof e == "boolean" ? e ? "<true/>" : "<false/>" : `<string>${B(e)}</string>`;
373
+ }
374
+ function H(e) {
375
+ return `<?xml version="1.0" encoding="UTF-8"?>
376
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
377
+ <plist version="1.0">
378
+ <dict>
379
+ ${Object.entries(e).map(([e, t]) => `\t<key>${B(e)}</key>\n\t${V(t)}`).join("\n")}\n</dict>
380
+ </plist>
381
+ `;
382
+ }
383
+ //#endregion
384
+ //#region src/package.mts
385
+ function U() {
386
+ if (process.platform === "darwin") return "macos";
387
+ if (process.platform === "linux") return "linux";
388
+ throw Error(`inca package doesn't support ${process.platform} yet`);
389
+ }
390
+ async function W(t, n) {
391
+ await Promise.all(t.files.map(async (r) => {
392
+ let i = e.join(n, r);
393
+ await o(e.dirname(i), { recursive: !0 }), await a(e.join(t.outDir, r), i);
394
+ }));
395
+ }
396
+ async function G({ metadata: t, output: n, hostBin: r }) {
397
+ let s = e.join(n.outDir, `${t.productName}.app`);
398
+ await l(s, {
399
+ recursive: !0,
400
+ force: !0
401
+ });
402
+ let c = e.join(s, "Contents"), d = e.join(c, "MacOS"), f = e.join(c, "Resources");
403
+ await o(d, { recursive: !0 }), await o(f, { recursive: !0 });
404
+ let p = e.join(d, t.productName);
405
+ await a(r, p), await i(p, 493), await W(n, f);
406
+ let m = {
407
+ CFBundleName: t.productName,
408
+ CFBundleDisplayName: t.productName,
409
+ CFBundleExecutable: t.productName,
410
+ CFBundleIdentifier: t.identifier,
411
+ CFBundleVersion: t.version,
412
+ CFBundleShortVersionString: t.version,
413
+ CFBundlePackageType: "APPL",
414
+ CFBundleInfoDictionaryVersion: "6.0",
415
+ NSHighResolutionCapable: !0
416
+ };
417
+ if (t.icon) {
418
+ let n = e.basename(t.icon);
419
+ await a(t.icon, e.join(f, n)), m.CFBundleIconFile = n;
420
+ }
421
+ return await u(e.join(c, "Info.plist"), H(m)), await u(e.join(c, "PkgInfo"), "APPL????"), s;
422
+ }
423
+ async function K({ metadata: t, output: n, hostBin: r }) {
424
+ let s = R(t.productName), c = e.join(n.outDir, s);
425
+ await l(c, {
426
+ recursive: !0,
427
+ force: !0
428
+ }), await o(c, { recursive: !0 });
429
+ let u = e.join(c, s);
430
+ return await a(r, u), await i(u, 493), await W(n, c), c;
431
+ }
432
+ async function q(e = {}) {
433
+ let t = e.cwd ?? process.cwd(), n = e.stdout ?? process.stdout, i = e.target ?? U(), a = await z(t);
434
+ a.identifierIsDefault && n.write(`[inca] no "inca.identifier" set in package.json — using generated identifier ${a.identifier}\n`);
435
+ let o = await w({
436
+ cwd: t,
437
+ entry: e.entry,
438
+ bundler: e.bundler
439
+ }), s = e.hostBin ?? O();
440
+ if (!r(s)) throw Error(`no host binary at ${s} — check that it was built and is executable`);
441
+ let c = {
442
+ metadata: a,
443
+ output: o,
444
+ hostBin: s
445
+ };
446
+ return { appPath: i === "macos" ? await G(c) : await K(c) };
447
+ }
448
+ //#endregion
449
+ //#region src/cli.mts
450
+ var J = "Usage: inca <dev|build|package>";
451
+ async function Y(e = process.argv) {
452
+ let t = e[2];
453
+ if (t === "build") {
454
+ try {
455
+ let e = await w();
456
+ process.stdout.write(`[inca] built ${e.entryFile}\n`);
457
+ } catch (e) {
458
+ P(process.stderr, "build failed", N(e)), process.exitCode = 1;
459
+ }
460
+ return;
461
+ }
462
+ if (t === "package") {
463
+ try {
464
+ let { appPath: e } = await q();
465
+ process.stdout.write(`[inca] packaged ${e}\n`);
466
+ } catch (e) {
467
+ P(process.stderr, "package failed", N(e)), process.exitCode = 1;
468
+ }
469
+ return;
470
+ }
471
+ if (t !== "dev") {
472
+ process.stderr.write(`${J}\n`), process.exitCode = 1;
473
+ return;
474
+ }
475
+ let n = new AbortController(), r = () => {
476
+ process.stderr.write("[inca] shutting down\n"), n.abort();
477
+ };
478
+ process.on("SIGINT", r), process.on("SIGTERM", r), await I({ signal: n.signal });
479
+ }
480
+ //#endregion
481
+ export { Y as run };
@@ -0,0 +1,7 @@
1
+ import { Bundler } from './adapter/types.mts';
2
+ /**
3
+ * `adapter/vite` wired in as the one adapter `dev` and `build` share by
4
+ * default — a single place to swap bundlers, and the reason they can't
5
+ * drift onto different ones.
6
+ */
7
+ export declare const defaultBundler: Bundler;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Where `inca-host` itself lives: `INCA_HOST_BIN` if set, otherwise the
3
+ * per-platform npm package for this OS/arch (`@incajs/host-<os>-<arch>`, an
4
+ * `optionalDependency` of this package).
5
+ * @throws if neither resolves — no override is set, and either this
6
+ * platform has no published binary, or the optional dependency carrying
7
+ * it failed to install.
8
+ */
9
+ export declare function resolveHostBin(): string;
@@ -0,0 +1,50 @@
1
+ import { AppErrorParams } from './protocol.mts';
2
+ export interface HostClientOptions {
3
+ /** The entry module passed to the host as `--dev <entryFile>`. */
4
+ entryFile: string;
5
+ /** Overrides which `inca-host` binary gets spawned, in place of automatic resolution. */
6
+ hostBin?: string;
7
+ /**
8
+ * Every diagnostic line the transport itself produces: the host's real
9
+ * stderr, a stray stdout line, and a `ready` protocol mismatch. Defaults
10
+ * to this process's own stderr.
11
+ */
12
+ onStderr?: (line: string) => void;
13
+ /**
14
+ * The window is up and the first bundle has been evaluated, and the host
15
+ * speaks the protocol revision this package was built for.
16
+ */
17
+ onReady?: () => void;
18
+ /** An app's own event listener threw; the host caught it and kept rendering. */
19
+ onAppError?: (error: AppErrorParams) => void;
20
+ /**
21
+ * Handlers for notification `method`s this package doesn't implement
22
+ * itself (e.g. a future Vite integration), keyed by method name —
23
+ * `params` forwarded untouched.
24
+ */
25
+ integrations?: Record<string, (params: unknown) => void>;
26
+ }
27
+ /**
28
+ * Spawns `inca-host --dev <entryFile>` and speaks newline-delimited
29
+ * JSON-RPC 2.0 on its stdin/stdout. One instance owns exactly one child
30
+ * process for its whole lifetime — a reload is a `reload` call on the same
31
+ * child, never a respawn.
32
+ */
33
+ export declare class HostClient {
34
+ #private;
35
+ constructor(options: HostClientOptions);
36
+ /**
37
+ * Starts the child and wires up its streams. Resolves once the process
38
+ * has actually spawned; rejects if it never does (e.g. the resolved
39
+ * binary doesn't exist).
40
+ */
41
+ start(): Promise<void>;
42
+ /** Sends a request and resolves with its result once the host answers. */
43
+ call(method: string, params?: unknown): Promise<unknown>;
44
+ /**
45
+ * Asks the host to `shutdown` and waits for the child to actually exit —
46
+ * that exit is the real acknowledgement, not the response. Kills it once
47
+ * `timeoutMs` passes without that, so a wedged app can't block teardown.
48
+ */
49
+ stop(timeoutMs?: number): Promise<void>;
50
+ }
@@ -0,0 +1,6 @@
1
+ /** A fault the host answered a request with, carrying its JSON-RPC code. */
2
+ export declare class HostError extends Error {
3
+ readonly code: number;
4
+ readonly hostStack: string | null;
5
+ constructor(message: string, code: number, hostStack?: string | null);
6
+ }
@@ -0,0 +1,4 @@
1
+ export { resolveHostBin } from './hostBin.mts';
2
+ export { HostError } from './hostError.mts';
3
+ export { HostClient } from './hostClient.mts';
4
+ export type { HostClientOptions } from './hostClient.mts';
@@ -0,0 +1,38 @@
1
+ export declare const JSONRPC = "2.0";
2
+ /**
3
+ * The host method-set revision this package expects, checked against
4
+ * `ready`'s `params.protocol` — a mismatch means an incompatible host
5
+ * build.
6
+ */
7
+ export declare const HOST_PROTOCOL_VERSION = 0;
8
+ /** `ready`'s notification params. */
9
+ export interface ReadyParams {
10
+ protocol: number;
11
+ }
12
+ /** `appError`'s notification params. */
13
+ export interface AppErrorParams {
14
+ message: string;
15
+ stack: string | null;
16
+ }
17
+ export interface RpcResult {
18
+ jsonrpc: typeof JSONRPC;
19
+ id: number;
20
+ result: unknown;
21
+ }
22
+ export interface RpcFailure {
23
+ jsonrpc: typeof JSONRPC;
24
+ id: number;
25
+ error: {
26
+ code: number;
27
+ message: string;
28
+ data?: {
29
+ stack: string | null;
30
+ };
31
+ };
32
+ }
33
+ export interface RpcNotification {
34
+ jsonrpc: typeof JSONRPC;
35
+ method: string;
36
+ params: unknown;
37
+ }
38
+ export declare function isRpcMessage(value: unknown): value is RpcResult | RpcFailure | RpcNotification;
package/dist/dev.d.mts ADDED
@@ -0,0 +1,25 @@
1
+ import { Bundler } from './adapter/types.mts';
2
+ export interface DevOptions {
3
+ /** The app's root directory. Defaults to `process.cwd()`. */
4
+ cwd?: string;
5
+ /**
6
+ * The app's entry point. Defaults to resolving it automatically: a
7
+ * committed `src/main.mts`, or `src/App.vue` wrapped in a synthesized one.
8
+ */
9
+ entry?: string;
10
+ /** Overrides the bundler — see {@link defaultBundler} for what's wired in by default. */
11
+ bundler?: Bundler;
12
+ /** Overrides host binary resolution — passed straight through to `HostClient`. */
13
+ hostBin?: string;
14
+ /** Defaults to `process.stdout`. */
15
+ stdout?: NodeJS.WritableStream;
16
+ /** Defaults to `process.stderr`. */
17
+ stderr?: NodeJS.WritableStream;
18
+ /** Aborting tears the host and the bundler watcher down and resolves `dev()`. */
19
+ signal: AbortSignal;
20
+ }
21
+ /**
22
+ * Builds the app, starts `inca-host` once the first build lands, and
23
+ * reloads it on every rebuild — until `options.signal` aborts.
24
+ */
25
+ export declare function dev(options: DevOptions): Promise<void>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Resolves an app's entry point. `src/main.mts`, committed by the app,
3
+ * wins outright and is used verbatim — full control over bootstrapping.
4
+ * Otherwise `src/App.vue` is wrapped in a synthesized entry equivalent to
5
+ * `createIncaApp(App).mount()`, so a plain `.vue` file is a whole app
6
+ * with no configuration at all.
7
+ */
8
+ export declare function resolveEntry(cwd: string): Promise<string>;
@@ -0,0 +1,6 @@
1
+ export interface Fault {
2
+ message: string;
3
+ stack: string | null;
4
+ }
5
+ export declare function toFault(error: unknown): Fault;
6
+ export declare function printFault(stream: NodeJS.WritableStream, label: string, fault: Fault): void;
@@ -0,0 +1 @@
1
+ export type { BuildOptions, BuildOutput, Bundler, BundlerOptions, Watcher, } from './adapter/types.mts';
package/dist/index.js ADDED
File without changes
@@ -0,0 +1,30 @@
1
+ /** An app's identity and version, for `inca package` to stamp onto the application it emits. */
2
+ export interface AppMetadata {
3
+ /** Shown to the user — the app's display name. */
4
+ productName: string;
5
+ /** A reverse-DNS-style unique id — e.g. macOS's `CFBundleIdentifier`. */
6
+ identifier: string;
7
+ /** True when `identifier` was derived rather than set by the app. */
8
+ identifierIsDefault: boolean;
9
+ /** The packaged app's version. */
10
+ version: string;
11
+ /** Absolute path to an icon file, if the app declared one. */
12
+ icon?: string;
13
+ }
14
+ /**
15
+ * Turns a display name into a filesystem/URL-safe slug — lowercased, with
16
+ * anything outside `[a-z0-9]` collapsed into a single `-`. Used for the
17
+ * default `identifier` and for the Linux packaged-app directory name.
18
+ */
19
+ export declare function slugify(name: string): string;
20
+ /**
21
+ * Reads an app's package metadata for `inca package`: `productName`,
22
+ * `identifier`, `version`, and an optional `icon`. Everything is derived
23
+ * from the app's own `package.json` — its `name`/`version` fields, and an
24
+ * optional `"inca"` key overriding any of them:
25
+ * `{ "inca": { "productName", "identifier", "icon" } }`.
26
+ *
27
+ * @throws if `package.json` is missing, has neither a `name` nor a
28
+ * `"inca".productName`, or `"inca".icon` doesn't resolve to a real file.
29
+ */
30
+ export declare function readAppMetadata(cwd: string): Promise<AppMetadata>;
@@ -0,0 +1,45 @@
1
+ import { Bundler } from './adapter/types.mts';
2
+ /** A platform `inca package` can emit a distributable application for. */
3
+ export type PackageTarget = "macos" | "linux";
4
+ /** Options for {@link packageApp}. */
5
+ export interface PackageAppOptions {
6
+ /** The app's root directory. Defaults to `process.cwd()`. */
7
+ cwd?: string;
8
+ /**
9
+ * The app's entry point. Defaults to resolving it the same way `inca
10
+ * dev`/`build` do: a committed `src/main.mts`, or `src/App.vue` wrapped
11
+ * in a synthesized one.
12
+ */
13
+ entry?: string;
14
+ /** Overrides the bundler — see {@link build}'s default. */
15
+ bundler?: Bundler;
16
+ /**
17
+ * Overrides which `inca-host` binary gets embedded in the packaged
18
+ * app, in place of automatic resolution (`INCA_HOST_BIN`, then the
19
+ * per-platform `@incajs/host-*` package — see {@link resolveHostBin}).
20
+ */
21
+ hostBin?: string;
22
+ /**
23
+ * The platform to package for. Defaults to whichever one the current
24
+ * process is running on.
25
+ */
26
+ target?: PackageTarget;
27
+ /** Where progress notes go. Defaults to `process.stdout`. */
28
+ stdout?: NodeJS.WritableStream;
29
+ }
30
+ /** The result of a successful {@link packageApp} call. */
31
+ export interface PackageResult {
32
+ /** The packaged app's path — a `.app` directory on macOS, a plain directory on Linux. */
33
+ appPath: string;
34
+ }
35
+ /**
36
+ * Builds the app and pairs it with the prebuilt `inca-host` into a
37
+ * distributable, platform-native application — a `.app` on macOS, a plain
38
+ * directory on Linux — written alongside the build output, under the
39
+ * app's own `dist/`. The app launches with no arguments and no terminal,
40
+ * since `inca-host` finds its own bundle beside its executable.
41
+ *
42
+ * @throws if the app's metadata can't be read, the build fails, or no
43
+ * `inca-host` binary can be resolved for the target platform.
44
+ */
45
+ export declare function packageApp(options?: PackageAppOptions): Promise<PackageResult>;
@@ -0,0 +1,4 @@
1
+ /** A macOS `Info.plist`'s value types — the only ones `inca package` writes. */
2
+ export type PlistValue = string | boolean;
3
+ /** Encodes a flat key/value dict as an `Info.plist` XML document. */
4
+ export declare function encodePlist(dict: Record<string, PlistValue>): string;
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "@incajs/cli",
3
- "version": "0.0.0",
4
- "description": "Incarnative.js — the `inca` CLI (dev/build/package). Name reservation; the real implementation is still in development.",
3
+ "version": "0.0.1",
4
+ "description": "The inca CLI: watches an app's entry point through a bundler adapter, starts inca-host once the first bundle lands, and reloads it on every rebuild.",
5
5
  "keywords": [
6
6
  "bundler",
7
7
  "cli",
8
8
  "desktop-app",
9
+ "dev-server",
9
10
  "gpui",
10
11
  "gui",
11
12
  "incajs",
12
13
  "incarnative.js",
13
- "quickjs"
14
+ "json-rpc",
15
+ "quickjs",
16
+ "vite"
14
17
  ],
15
18
  "license": "(MIT OR Apache-2.0)",
16
19
  "repository": {
@@ -18,7 +21,59 @@
18
21
  "url": "git+https://github.com/tom96da/incajs.git",
19
22
  "directory": "packages/cli"
20
23
  },
24
+ "bin": {
25
+ "inca": "./bin/inca.mjs"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "bin"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.mts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.mts",
38
+ "import": "./dist/index.js"
39
+ }
40
+ },
21
41
  "publishConfig": {
22
42
  "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@vitejs/plugin-vue": "^6.0.8",
46
+ "vite": "^8.2.2"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.4.1",
50
+ "@vue/runtime-core": "^3.5.42",
51
+ "oxfmt": "^0.66.0",
52
+ "oxlint": "^1.81.0",
53
+ "rolldown": "~1.2.4",
54
+ "vue": "^3.5.42"
55
+ },
56
+ "peerDependencies": {
57
+ "vue": "^3.5.42"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "vue": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "optionalDependencies": {
65
+ "@incajs/host-darwin-arm64": "0.0.1",
66
+ "@incajs/host-linux-arm64": "0.0.1",
67
+ "@incajs/host-linux-x64": "0.0.1"
68
+ },
69
+ "engines": {
70
+ "node": ">=22"
71
+ },
72
+ "scripts": {
73
+ "build": "vite build",
74
+ "test": "vitest run",
75
+ "lint": "oxlint --type-aware",
76
+ "format": "oxfmt --check .",
77
+ "typecheck": "oxlint -A all --type-aware --type-check"
23
78
  }
24
79
  }