@ikenga/cli 0.2.0 → 0.3.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/README.md +19 -1
- package/dist/index.js +283 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,11 +26,29 @@ ikenga update <pkg> # update one
|
|
|
26
26
|
ikenga update --all # update everything outdated
|
|
27
27
|
ikenga remove com.ikenga.hello # by manifest id, or...
|
|
28
28
|
ikenga remove @ikenga/pkg-hello # ...by npm name
|
|
29
|
+
ikenga dev ./my-pkg # hot-mount into running shell
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
`list / add / update / remove` mutate the shell's pkgs directory (overridable with `IKENGA_APP_DATA_DIR`); the shell registers them on next boot.
|
|
33
|
+
|
|
34
|
+
### `ikenga dev <path>` — hot-mount for development
|
|
35
|
+
|
|
36
|
+
Different shape: `dev` talks to a **running** shell over its localhost iyke bridge instead of touching disk. Registers the pkg with `source.kind = "dev"` (auto-trusted, regardless of id namespace) and spawns a manifest watcher in the kernel so edits to `manifest.json` or any `restart_when_changed` glob trip an in-place reload — no shell restart.
|
|
37
|
+
|
|
38
|
+
Requires the shell to be running. The CLI discovers its port + bearer token from `control.json` in the shell's local data dir.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ikenga dev /home/me/code/my-pkg
|
|
42
|
+
# → mounted as com.example.my-pkg v0.1.0
|
|
43
|
+
# → Routes: /pkg/com.example.my-pkg/
|
|
44
|
+
# Edit manifest.json or watched src/ files; reload fires automatically.
|
|
45
|
+
# Ctrl-C to unregister.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Iframe code changes flow through your dev server's HMR (Vite, Next, …); sidecar / MCP source edits respawn via the supervisor watcher; manifest edits trigger a full pkg reload that emits a `pkg-reloaded` event the shell's iframe + webview hosts listen for. See [`docs/pkg-patterns/07-dev-mode.md`](https://github.com/Royalti-io/ikenga/blob/main/docs/pkg-patterns/07-dev-mode.md) for the kernel semantics.
|
|
32
49
|
|
|
33
50
|
## Versioning
|
|
34
51
|
|
|
52
|
+
`v0.3.0` — adds `ikenga dev <path>` for hot-mounting pkgs into a running shell via the iyke localhost bridge. Watcher-driven manifest reload + clean `Ctrl-C` unregister. Requires the corresponding shell-side dev-mode kernel (lands in shell `v0.0.5+`).
|
|
35
53
|
`v0.2.0` — JS-source npm distribution; requires Bun on `$PATH`.
|
|
36
54
|
`v0.1.x` — bun-compiled standalone binaries (deprecated; available on the GitHub Releases page until the next archive sweep).
|
package/dist/index.js
CHANGED
|
@@ -9208,20 +9208,277 @@ function parseSpec(spec) {
|
|
|
9208
9208
|
return { name: spec.slice(0, idx), version: spec.slice(idx + 1) || undefined };
|
|
9209
9209
|
}
|
|
9210
9210
|
|
|
9211
|
-
// src/
|
|
9212
|
-
import { existsSync as
|
|
9211
|
+
// src/commands/dev.ts
|
|
9212
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
9213
|
+
import { resolve } from "path";
|
|
9214
|
+
|
|
9215
|
+
// src/lib/iyke-bridge.ts
|
|
9216
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync, unlinkSync } from "fs";
|
|
9217
|
+
import { homedir as homedir2, platform as platform2 } from "os";
|
|
9213
9218
|
import { join as join3 } from "path";
|
|
9219
|
+
var APP_IDENTIFIER = "app.ikenga";
|
|
9220
|
+
var STALE_THRESHOLD_SECS = 5 * 60;
|
|
9221
|
+
function appLocalDataDir() {
|
|
9222
|
+
const override = process.env.IKENGA_APP_LOCAL_DATA_DIR;
|
|
9223
|
+
if (override)
|
|
9224
|
+
return override;
|
|
9225
|
+
const home = homedir2();
|
|
9226
|
+
switch (platform2()) {
|
|
9227
|
+
case "darwin":
|
|
9228
|
+
return join3(home, "Library", "Application Support", APP_IDENTIFIER);
|
|
9229
|
+
case "win32": {
|
|
9230
|
+
const local = process.env.LOCALAPPDATA;
|
|
9231
|
+
if (local)
|
|
9232
|
+
return join3(local, APP_IDENTIFIER);
|
|
9233
|
+
return join3(home, "AppData", "Local", APP_IDENTIFIER);
|
|
9234
|
+
}
|
|
9235
|
+
default: {
|
|
9236
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
9237
|
+
if (xdg)
|
|
9238
|
+
return join3(xdg, APP_IDENTIFIER);
|
|
9239
|
+
return join3(home, ".local", "share", APP_IDENTIFIER);
|
|
9240
|
+
}
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
function controlPath() {
|
|
9244
|
+
return join3(appLocalDataDir(), "control.json");
|
|
9245
|
+
}
|
|
9246
|
+
function isPidAlive(pid) {
|
|
9247
|
+
if (platform2() === "win32")
|
|
9248
|
+
return true;
|
|
9249
|
+
try {
|
|
9250
|
+
process.kill(pid, 0);
|
|
9251
|
+
return true;
|
|
9252
|
+
} catch (e) {
|
|
9253
|
+
const err2 = e;
|
|
9254
|
+
return err2.code === "EPERM";
|
|
9255
|
+
}
|
|
9256
|
+
}
|
|
9257
|
+
function loadControl() {
|
|
9258
|
+
const path = controlPath();
|
|
9259
|
+
if (!existsSync2(path)) {
|
|
9260
|
+
return { kind: "missing" };
|
|
9261
|
+
}
|
|
9262
|
+
const raw = readFileSync2(path, "utf8");
|
|
9263
|
+
const cf = JSON.parse(raw);
|
|
9264
|
+
if (cf.schema_version !== 1) {
|
|
9265
|
+
throw new Error(`unsupported control.json schema_version: ${cf.schema_version} (CLI built for v1)`);
|
|
9266
|
+
}
|
|
9267
|
+
if (isPidAlive(cf.pid)) {
|
|
9268
|
+
return { kind: "ok", cf };
|
|
9269
|
+
}
|
|
9270
|
+
const nowMs = Date.now();
|
|
9271
|
+
const ageMs = Math.max(0, nowMs - cf.started_at_unix_ms);
|
|
9272
|
+
const ageSecs = Math.floor(ageMs / 1000);
|
|
9273
|
+
if (ageSecs >= STALE_THRESHOLD_SECS) {
|
|
9274
|
+
try {
|
|
9275
|
+
unlinkSync(path);
|
|
9276
|
+
} catch {}
|
|
9277
|
+
return { kind: "stale_removed" };
|
|
9278
|
+
}
|
|
9279
|
+
return { kind: "stale_young", age_secs: ageSecs };
|
|
9280
|
+
}
|
|
9281
|
+
|
|
9282
|
+
class IykeClient {
|
|
9283
|
+
base;
|
|
9284
|
+
token;
|
|
9285
|
+
constructor(cf) {
|
|
9286
|
+
this.base = `http://127.0.0.1:${cf.port}`;
|
|
9287
|
+
this.token = cf.token;
|
|
9288
|
+
}
|
|
9289
|
+
async post(path, body) {
|
|
9290
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
9291
|
+
method: "POST",
|
|
9292
|
+
headers: {
|
|
9293
|
+
"Content-Type": "application/json",
|
|
9294
|
+
Authorization: `Bearer ${this.token}`
|
|
9295
|
+
},
|
|
9296
|
+
body: JSON.stringify(body)
|
|
9297
|
+
});
|
|
9298
|
+
if (!res.ok) {
|
|
9299
|
+
const text2 = await res.text().catch(() => "");
|
|
9300
|
+
throw new Error(`POST ${path} failed: ${res.status} ${text2}`);
|
|
9301
|
+
}
|
|
9302
|
+
const text = await res.text();
|
|
9303
|
+
return text ? JSON.parse(text) : {};
|
|
9304
|
+
}
|
|
9305
|
+
async get(path) {
|
|
9306
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
9307
|
+
method: "GET",
|
|
9308
|
+
headers: { Authorization: `Bearer ${this.token}` }
|
|
9309
|
+
});
|
|
9310
|
+
if (!res.ok) {
|
|
9311
|
+
const text = await res.text().catch(() => "");
|
|
9312
|
+
throw new Error(`GET ${path} failed: ${res.status} ${text}`);
|
|
9313
|
+
}
|
|
9314
|
+
return await res.json();
|
|
9315
|
+
}
|
|
9316
|
+
}
|
|
9317
|
+
function connectOrThrow() {
|
|
9318
|
+
const outcome = loadControl();
|
|
9319
|
+
switch (outcome.kind) {
|
|
9320
|
+
case "ok":
|
|
9321
|
+
return new IykeClient(outcome.cf);
|
|
9322
|
+
case "missing":
|
|
9323
|
+
throw new Error(`Ikenga shell is not running.
|
|
9324
|
+
Expected control.json at ${controlPath()}
|
|
9325
|
+
Start the shell, then re-run this command.`);
|
|
9326
|
+
case "stale_removed":
|
|
9327
|
+
throw new Error(`Ikenga shell control file was stale (crashed shell) \u2014 cleaned up.
|
|
9328
|
+
Start the shell and re-run this command.`);
|
|
9329
|
+
case "stale_young":
|
|
9330
|
+
throw new Error(`Ikenga shell control file is ${outcome.age_secs}s old but the PID is dead.
|
|
9331
|
+
Likely a launch race. Retry in a moment or remove ${controlPath()} manually.`);
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
|
|
9335
|
+
// src/commands/dev.ts
|
|
9336
|
+
async function devCommand(rawPath) {
|
|
9337
|
+
const path = resolve(rawPath);
|
|
9338
|
+
if (!existsSync3(path)) {
|
|
9339
|
+
process.stderr.write(`error: ${path} does not exist
|
|
9340
|
+
`);
|
|
9341
|
+
return 1;
|
|
9342
|
+
}
|
|
9343
|
+
if (!statSync2(path).isDirectory()) {
|
|
9344
|
+
process.stderr.write(`error: ${path} is not a directory
|
|
9345
|
+
`);
|
|
9346
|
+
return 1;
|
|
9347
|
+
}
|
|
9348
|
+
const manifestPath = `${path}/manifest.json`;
|
|
9349
|
+
if (!existsSync3(manifestPath)) {
|
|
9350
|
+
process.stderr.write(`error: no manifest.json at ${manifestPath}
|
|
9351
|
+
`);
|
|
9352
|
+
return 1;
|
|
9353
|
+
}
|
|
9354
|
+
let manifest;
|
|
9355
|
+
try {
|
|
9356
|
+
manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
9357
|
+
} catch (err2) {
|
|
9358
|
+
process.stderr.write(`error: failed to parse manifest.json: ${err2.message}
|
|
9359
|
+
`);
|
|
9360
|
+
return 1;
|
|
9361
|
+
}
|
|
9362
|
+
if (!manifest.id || !manifest.name || !manifest.version || !manifest.ikenga_api) {
|
|
9363
|
+
process.stderr.write(`error: manifest missing required fields (id, name, version, ikenga_api)
|
|
9364
|
+
`);
|
|
9365
|
+
return 1;
|
|
9366
|
+
}
|
|
9367
|
+
let client;
|
|
9368
|
+
try {
|
|
9369
|
+
client = connectOrThrow();
|
|
9370
|
+
} catch (err2) {
|
|
9371
|
+
process.stderr.write(`error: ${err2.message}
|
|
9372
|
+
`);
|
|
9373
|
+
return 1;
|
|
9374
|
+
}
|
|
9375
|
+
process.stdout.write(`\u2192 registering ${manifest.id}@${manifest.version} from ${path}
|
|
9376
|
+
`);
|
|
9377
|
+
let registered;
|
|
9378
|
+
try {
|
|
9379
|
+
registered = await client.post("/iyke/pkg/dev/register", {
|
|
9380
|
+
install_path: path
|
|
9381
|
+
});
|
|
9382
|
+
} catch (err2) {
|
|
9383
|
+
process.stderr.write(`error: ${err2.message}
|
|
9384
|
+
`);
|
|
9385
|
+
return 1;
|
|
9386
|
+
}
|
|
9387
|
+
process.stdout.write(`
|
|
9388
|
+
\u2713 mounted as ${registered.installed.id} v${registered.installed.version}
|
|
9389
|
+
`);
|
|
9390
|
+
printPkgSurface(manifest);
|
|
9391
|
+
process.stdout.write(`
|
|
9392
|
+
Watching manifest.json + restart_when_changed globs.
|
|
9393
|
+
`);
|
|
9394
|
+
process.stdout.write(` Edit the manifest to trigger a reload (250ms debounce).
|
|
9395
|
+
`);
|
|
9396
|
+
process.stdout.write(` Sidecar / MCP code changes restart via the supervisor watcher.
|
|
9397
|
+
`);
|
|
9398
|
+
process.stdout.write(` Iframe code changes flow through your dev server's HMR.
|
|
9399
|
+
`);
|
|
9400
|
+
process.stdout.write(`
|
|
9401
|
+
Ctrl-C to unregister and exit.
|
|
9402
|
+
`);
|
|
9403
|
+
const pkgId = registered.installed.id;
|
|
9404
|
+
return new Promise((resolveExit) => {
|
|
9405
|
+
let unregistering = false;
|
|
9406
|
+
const cleanup = async () => {
|
|
9407
|
+
if (unregistering)
|
|
9408
|
+
return;
|
|
9409
|
+
unregistering = true;
|
|
9410
|
+
process.stdout.write(`
|
|
9411
|
+
\u2192 unregistering ${pkgId}
|
|
9412
|
+
`);
|
|
9413
|
+
try {
|
|
9414
|
+
await client.post("/iyke/pkg/dev/unregister", { pkg_id: pkgId });
|
|
9415
|
+
process.stdout.write(`\u2713 done
|
|
9416
|
+
`);
|
|
9417
|
+
resolveExit(0);
|
|
9418
|
+
} catch (err2) {
|
|
9419
|
+
process.stderr.write(`error: ${err2.message}
|
|
9420
|
+
`);
|
|
9421
|
+
resolveExit(1);
|
|
9422
|
+
}
|
|
9423
|
+
};
|
|
9424
|
+
process.on("SIGINT", () => {
|
|
9425
|
+
cleanup();
|
|
9426
|
+
});
|
|
9427
|
+
process.on("SIGTERM", () => {
|
|
9428
|
+
cleanup();
|
|
9429
|
+
});
|
|
9430
|
+
setInterval(() => {}, 60000);
|
|
9431
|
+
});
|
|
9432
|
+
}
|
|
9433
|
+
function printPkgSurface(m2) {
|
|
9434
|
+
if (m2.ui?.routes?.length) {
|
|
9435
|
+
process.stdout.write(`
|
|
9436
|
+
Routes:
|
|
9437
|
+
`);
|
|
9438
|
+
for (const r of m2.ui.routes) {
|
|
9439
|
+
process.stdout.write(` /pkg/${m2.id}${r.path} (${r.kind}: ${r.source})
|
|
9440
|
+
`);
|
|
9441
|
+
}
|
|
9442
|
+
}
|
|
9443
|
+
if (m2.mcp?.length) {
|
|
9444
|
+
process.stdout.write(`
|
|
9445
|
+
MCP servers:
|
|
9446
|
+
`);
|
|
9447
|
+
for (const s3 of m2.mcp) {
|
|
9448
|
+
process.stdout.write(` ${s3.name}
|
|
9449
|
+
`);
|
|
9450
|
+
}
|
|
9451
|
+
}
|
|
9452
|
+
if (m2.sidecars?.length) {
|
|
9453
|
+
process.stdout.write(`
|
|
9454
|
+
Sidecars:
|
|
9455
|
+
`);
|
|
9456
|
+
for (const s3 of m2.sidecars) {
|
|
9457
|
+
process.stdout.write(` ${s3.name}
|
|
9458
|
+
`);
|
|
9459
|
+
}
|
|
9460
|
+
}
|
|
9461
|
+
if (m2.engine?.agentId) {
|
|
9462
|
+
process.stdout.write(`
|
|
9463
|
+
Engine: ${m2.engine.agentId}
|
|
9464
|
+
`);
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9467
|
+
|
|
9468
|
+
// src/lib/installed.ts
|
|
9469
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
9470
|
+
import { join as join4 } from "path";
|
|
9214
9471
|
function listInstalled() {
|
|
9215
9472
|
const root = pkgsDir();
|
|
9216
|
-
if (!
|
|
9473
|
+
if (!existsSync4(root))
|
|
9217
9474
|
return [];
|
|
9218
9475
|
const out = [];
|
|
9219
9476
|
for (const name of readdirSync(root)) {
|
|
9220
9477
|
if (name.startsWith("."))
|
|
9221
9478
|
continue;
|
|
9222
|
-
const dir =
|
|
9479
|
+
const dir = join4(root, name);
|
|
9223
9480
|
try {
|
|
9224
|
-
if (!
|
|
9481
|
+
if (!statSync3(dir).isDirectory())
|
|
9225
9482
|
continue;
|
|
9226
9483
|
} catch {
|
|
9227
9484
|
continue;
|
|
@@ -9247,11 +9504,11 @@ function findInstalled(pkgId) {
|
|
|
9247
9504
|
return listInstalled().find((p2) => p2.id === pkgId) ?? null;
|
|
9248
9505
|
}
|
|
9249
9506
|
function readManifest(dir) {
|
|
9250
|
-
const path =
|
|
9251
|
-
if (!
|
|
9507
|
+
const path = join4(dir, "manifest.json");
|
|
9508
|
+
if (!existsSync4(path))
|
|
9252
9509
|
return null;
|
|
9253
9510
|
try {
|
|
9254
|
-
const json = JSON.parse(
|
|
9511
|
+
const json = JSON.parse(readFileSync4(path, "utf8"));
|
|
9255
9512
|
if (typeof json?.id !== "string" || typeof json?.version !== "string") {
|
|
9256
9513
|
return null;
|
|
9257
9514
|
}
|
|
@@ -9442,7 +9699,7 @@ function npmNameToPkgId2(npmName) {
|
|
|
9442
9699
|
}
|
|
9443
9700
|
|
|
9444
9701
|
// src/index.ts
|
|
9445
|
-
var SUBCOMMANDS = ["list", "add", "update", "remove"];
|
|
9702
|
+
var SUBCOMMANDS = ["list", "add", "update", "remove", "dev"];
|
|
9446
9703
|
function usage() {
|
|
9447
9704
|
return `ikenga \u2014 pkg manager for the Ikenga shell
|
|
9448
9705
|
|
|
@@ -9451,6 +9708,7 @@ Usage:
|
|
|
9451
9708
|
ikenga add <pkg>[@<version>] [--dry-run]
|
|
9452
9709
|
ikenga update [<pkg> | --all] [--dry-run]
|
|
9453
9710
|
ikenga remove <pkg>
|
|
9711
|
+
ikenga dev <path>
|
|
9454
9712
|
|
|
9455
9713
|
Examples:
|
|
9456
9714
|
ikenga list # what's installed locally
|
|
@@ -9460,9 +9718,15 @@ Examples:
|
|
|
9460
9718
|
ikenga update --all # update everything outdated
|
|
9461
9719
|
ikenga remove com.ikenga.hello # by manifest id, or...
|
|
9462
9720
|
ikenga remove @ikenga/pkg-hello # ...by npm name
|
|
9721
|
+
ikenga dev ./my-pkg # hot-mount into running shell
|
|
9463
9722
|
|
|
9464
9723
|
Installs land in the shell's pkgs directory (overridable with
|
|
9465
9724
|
IKENGA_APP_DATA_DIR). The shell registers them on next boot.
|
|
9725
|
+
|
|
9726
|
+
\`ikenga dev <path>\` is different \u2014 it talks to a running shell over its
|
|
9727
|
+
localhost iyke bridge, registers the pkg with hot-reload semantics
|
|
9728
|
+
(manifest edits trigger an in-place reload, no shell restart), and
|
|
9729
|
+
unregisters cleanly on Ctrl-C. Requires the shell to be running.
|
|
9466
9730
|
`;
|
|
9467
9731
|
}
|
|
9468
9732
|
async function main() {
|
|
@@ -9473,7 +9737,7 @@ async function main() {
|
|
|
9473
9737
|
return 0;
|
|
9474
9738
|
}
|
|
9475
9739
|
if (sub === "--version" || sub === "-V") {
|
|
9476
|
-
process.stdout.write(`ikenga ${"0.
|
|
9740
|
+
process.stdout.write(`ikenga ${"0.3.0"}
|
|
9477
9741
|
`);
|
|
9478
9742
|
return 0;
|
|
9479
9743
|
}
|
|
@@ -9514,6 +9778,15 @@ async function main() {
|
|
|
9514
9778
|
}
|
|
9515
9779
|
return removeCommand(spec);
|
|
9516
9780
|
}
|
|
9781
|
+
case "dev": {
|
|
9782
|
+
const path = rest.find((a) => !a.startsWith("-"));
|
|
9783
|
+
if (!path) {
|
|
9784
|
+
process.stderr.write(`usage: ikenga dev <path>
|
|
9785
|
+
`);
|
|
9786
|
+
return 1;
|
|
9787
|
+
}
|
|
9788
|
+
return devCommand(path);
|
|
9789
|
+
}
|
|
9517
9790
|
}
|
|
9518
9791
|
}
|
|
9519
9792
|
var code = await main();
|