@unbound-app/cli 0.1.3 → 0.6.0
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/dist/index.mjs +69 -14
- package/package.json +4 -7
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { cli, command } from "cleye";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, watch, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { ControllerClient } from "@unbound-app/debugger-protocol/controller";
|
|
7
7
|
import { CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT } from "@unbound-app/debugger-protocol";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -439,7 +439,8 @@ function scaffoldTemplate(from, to, tokens) {
|
|
|
439
439
|
mkdirSync(to, { recursive: true });
|
|
440
440
|
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
441
441
|
const source = join(from, entry.name);
|
|
442
|
-
const
|
|
442
|
+
const name = entry.name === GITIGNORE_SOURCE ? ".gitignore" : substitute(entry.name, tokens);
|
|
443
|
+
const target = join(to, name);
|
|
443
444
|
if (entry.isDirectory()) {
|
|
444
445
|
scaffoldTemplate(source, target, tokens);
|
|
445
446
|
continue;
|
|
@@ -583,12 +584,13 @@ const createCommand = {
|
|
|
583
584
|
function printOutro(result) {
|
|
584
585
|
const kindDocs = `${DOCS_BASE_URL}/${result.kind}s/introduction`;
|
|
585
586
|
const manifestDocs = `${DOCS_BASE_URL}/addons/manifest`;
|
|
586
|
-
|
|
587
|
+
const steps = [
|
|
587
588
|
`cd ${relative(process.cwd(), result.dir) || "."}`,
|
|
588
589
|
"ubd dev",
|
|
589
590
|
`Learn the ${result.kind} API: ${kindDocs}`,
|
|
590
591
|
`Manifest reference: ${manifestDocs}`
|
|
591
|
-
]
|
|
592
|
+
];
|
|
593
|
+
note(steps.join("\n"), "Next steps");
|
|
592
594
|
outro(`${result.name} ready.`);
|
|
593
595
|
}
|
|
594
596
|
//#endregion
|
|
@@ -767,13 +769,65 @@ const evalCommand = {
|
|
|
767
769
|
//#endregion
|
|
768
770
|
//#region src/lib/reload-transport.ts
|
|
769
771
|
/**
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
772
|
+
* How long the watcher waits for a dropped device to reconnect before warning and dropping a reload.
|
|
773
|
+
* Longer than a one-shot command's poll (`DEVICE_WAIT_TIMEOUT_MS`): a `ubd dev` session runs for
|
|
774
|
+
* hours, and a backgrounded app or slept device can take a few seconds to redial the bridge.
|
|
773
775
|
*/
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
776
|
+
const RECONNECT_WAIT_MS = 5e3;
|
|
777
|
+
/**
|
|
778
|
+
* @description Builds the real hot-reload transport over the shared bridge controller. Each reload
|
|
779
|
+
* reads the freshly built bundle and manifest from disk and pushes them to the device via
|
|
780
|
+
* {@link ControllerClient.pushPlugin}. Theme hot reload isn't wired yet, so a theme is skipped with a
|
|
781
|
+
* notice. If the device isn't connected it waits briefly for a reconnect, then warns and drops the
|
|
782
|
+
* reload; any disk or transport failure is reported. Nothing here throws, so the watcher keeps running.
|
|
783
|
+
* @param context A bearer of the bridge client shared by every command; its client dials on first use.
|
|
784
|
+
* @param reconnectWaitMs How long to wait for a dropped device to reconnect before dropping a reload.
|
|
785
|
+
* @returns A {@link ReloadTransport} bound to that context.
|
|
786
|
+
*/
|
|
787
|
+
function createReloadTransport(context, reconnectWaitMs = RECONNECT_WAIT_MS) {
|
|
788
|
+
return { async reload(addon) {
|
|
789
|
+
if (addon.kind !== "plugin") {
|
|
790
|
+
process.stdout.write(`Rebuilt ${addon.id}, but ${addon.kind} hot reload isn't available yet — restart the app to load it.\n`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const { client } = context;
|
|
794
|
+
if (!client.isDeviceConnected) await waitForDevice(client, reconnectWaitMs);
|
|
795
|
+
if (!client.isDeviceConnected) {
|
|
796
|
+
process.stderr.write(`Device not connected — skipping reload of ${addon.id}; save again once reconnected.\n`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
const { manifest, bundle } = readBuiltAddon(addon);
|
|
801
|
+
process.stdout.write(`Pushing ${addon.id}…\n`);
|
|
802
|
+
const result = await client.pushPlugin({
|
|
803
|
+
addonId: addon.id,
|
|
804
|
+
bundle,
|
|
805
|
+
manifest
|
|
806
|
+
});
|
|
807
|
+
if (result.ok) process.stdout.write(`Reloaded ${addon.id}.\n`);
|
|
808
|
+
else process.stderr.write(`Failed to reload ${addon.id}: ${result.error}\n`);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
process.stderr.write(`Failed to reload ${addon.id}: ${error?.message ?? error}\n`);
|
|
811
|
+
}
|
|
812
|
+
} };
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* @description Reads a built addon's manifest and bundle off disk. A static addon ships its files
|
|
816
|
+
* as-is, so its bundle is the file named by `main` in its directory; a built addon's bundle is the
|
|
817
|
+
* JavaScript the build emitted into the output directory, named after `main` with a `.js` extension.
|
|
818
|
+
* @param addon The freshly built addon.
|
|
819
|
+
* @returns The parsed manifest and the bundle source.
|
|
820
|
+
*/
|
|
821
|
+
function readBuiltAddon(addon) {
|
|
822
|
+
const manifest = JSON.parse(readFileSync(join(addon.dir, "manifest.json"), "utf8"));
|
|
823
|
+
const bundleName = addon.static ? manifest.main : basename(manifest.main).replace(/\.\w+$/, ".js");
|
|
824
|
+
const bundlePath = join(addon.output, bundleName);
|
|
825
|
+
if (!existsSync(bundlePath)) throw new Error(`Built bundle not found at ${bundlePath}. Hot reload expects the build to emit ${bundleName} into ${addon.output}.`);
|
|
826
|
+
return {
|
|
827
|
+
manifest,
|
|
828
|
+
bundle: readFileSync(bundlePath, "utf8")
|
|
829
|
+
};
|
|
830
|
+
}
|
|
777
831
|
//#endregion
|
|
778
832
|
//#region src/lib/dev-server.ts
|
|
779
833
|
/** How long file changes inside one addon settle before that addon rebuilds. */
|
|
@@ -866,7 +920,8 @@ function toBuiltAddon(addon) {
|
|
|
866
920
|
id: addon.id,
|
|
867
921
|
kind: addon.kind,
|
|
868
922
|
dir: addon.dir,
|
|
869
|
-
output: addon.output
|
|
923
|
+
output: addon.output,
|
|
924
|
+
static: addon.static
|
|
870
925
|
};
|
|
871
926
|
}
|
|
872
927
|
//#endregion
|
|
@@ -885,9 +940,9 @@ const commands = [
|
|
|
885
940
|
cli: true,
|
|
886
941
|
mcp: false
|
|
887
942
|
},
|
|
888
|
-
schema: z.object({}),
|
|
889
|
-
async handler() {
|
|
890
|
-
const server = new DevServer(loadConfig(),
|
|
943
|
+
schema: z.object({ port: z.number().default(DEFAULT_BRIDGE_PORT).describe("The port the debugger bridge listens on.") }),
|
|
944
|
+
async handler(_input, context) {
|
|
945
|
+
const server = new DevServer(loadConfig(), createReloadTransport(context));
|
|
891
946
|
process.on("SIGINT", () => {
|
|
892
947
|
server.stop();
|
|
893
948
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unbound-app/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The `ubd` command line for the Unbound client: evaluate on-device code and serve the debugger as an MCP.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mario P.",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"directory": "packages/cli"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"ubd": "
|
|
16
|
+
"ubd": "dist/index.mjs"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
@@ -27,15 +27,12 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
-
"@unbound-app/debugger-protocol": "0.
|
|
30
|
+
"@unbound-app/debugger-protocol": "0.6.0",
|
|
31
|
+
"@unbound-app/types": "0.6.0",
|
|
31
32
|
"cleye": "^1.3.4",
|
|
32
33
|
"picocolors": "^1.1.1",
|
|
33
34
|
"zod": "^4.0.0"
|
|
34
35
|
},
|
|
35
|
-
"devDependencies": {
|
|
36
|
-
"@types/bun": "latest",
|
|
37
|
-
"tsdown": "^0.22.9"
|
|
38
|
-
},
|
|
39
36
|
"peerDependencies": {
|
|
40
37
|
"typescript": "^5.9.3"
|
|
41
38
|
}
|