@hanamorilabs/tab 0.1.1 → 0.1.3
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 -0
- package/dist/alias.js +79 -0
- package/dist/cli.js +64 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,6 +59,8 @@ Codex shows `FlockTab - Self-hosted` as its provider.
|
|
|
59
59
|
tab login approve this machine in the console, once
|
|
60
60
|
tab <agent> [args] claude, codex, grok, kimi, gemini, or any command, on the tab
|
|
61
61
|
tab use pick or change the Agent this folder runs as (.flocktab)
|
|
62
|
+
tab alias setup <name>... make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
63
|
+
tab alias remove <name>... undo that; `tab alias list` shows them
|
|
62
64
|
tab status flock, folder Agent, proxy health, provider key state
|
|
63
65
|
tab version tab and proxy versions
|
|
64
66
|
tab up | down self-hosted: start or stop the local proxy
|
|
@@ -67,6 +69,23 @@ tab logout forget the session and keys on this machine
|
|
|
67
69
|
tab help this list
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
## Aliases: plain `codex` on the tab
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
tab alias setup codex claude
|
|
76
|
+
export PATH="$HOME/.flocktab/bin:$PATH" # once, in your shell rc
|
|
77
|
+
codex # now runs: tab codex
|
|
78
|
+
tab alias remove codex # back to the plain codex
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`tab alias setup` writes a shim named after the command into
|
|
82
|
+
`~/.flocktab/bin` (`codex.cmd` on Windows). With that folder first on
|
|
83
|
+
`PATH`, the plain command runs on the tab. When `tab` spawns the real agent
|
|
84
|
+
it removes its own shim folder from the child's `PATH`, so nothing loops.
|
|
85
|
+
`tab alias list` shows them; `tab alias remove <name>` deletes the shim, and
|
|
86
|
+
the plain command is the real one again. Prefer a shell alias?
|
|
87
|
+
`alias codex='tab codex'` in your rc does the same for that shell only.
|
|
88
|
+
|
|
70
89
|
## Secrets and colour
|
|
71
90
|
|
|
72
91
|
The unlock and provider keys are typed with echo off (raw mode); the unlock is checked against the proxy before saving; a wrong unlock is refused, and `tab status` says `unlock ok` or `unlock is WRONG`. The check is `GET /v1/whoami` with the unlock, which decrypts the envelope in memory and answers `ok`/`wrong`, never the key. Output is coloured on a terminal; `NO_COLOR` turns it off, `FORCE_COLOR` turns it on for pipes.
|
package/dist/alias.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tab alias codex`: a shim named `codex` in ~/.flocktab/bin that runs
|
|
3
|
+
* `tab codex "$@"`. With that folder first on PATH, the plain command is on
|
|
4
|
+
* the tab. No shell rc is edited here: `tab alias` prints the one PATH line
|
|
5
|
+
* and the person adds it where they like.
|
|
6
|
+
*
|
|
7
|
+
* The shim shadows the real binary by name, so when `tab` spawns the agent
|
|
8
|
+
* it removes ~/.flocktab/bin from the child's PATH (`pathWithoutShims`);
|
|
9
|
+
* otherwise `codex` would resolve to the shim and loop forever.
|
|
10
|
+
*/
|
|
11
|
+
import { chmod, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { configDir } from "./config.js";
|
|
14
|
+
export function shimDir(env = process.env) {
|
|
15
|
+
return path.join(configDir(env), "bin");
|
|
16
|
+
}
|
|
17
|
+
const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
18
|
+
export function validAliasName(name) {
|
|
19
|
+
return NAME.test(name) && !["tab", "flocktab-proxy"].includes(name.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
function shimBody(name, platform = process.platform) {
|
|
22
|
+
if (platform === "win32") {
|
|
23
|
+
return { file: `${name}.cmd`, body: `@echo off\r\ntab ${name} %*\r\n` };
|
|
24
|
+
}
|
|
25
|
+
return { file: name, body: `#!/bin/sh\nexec tab ${name} "$@"\n` };
|
|
26
|
+
}
|
|
27
|
+
export async function writeAlias(name, env = process.env, platform = process.platform) {
|
|
28
|
+
const dir = shimDir(env);
|
|
29
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
30
|
+
const { file, body } = shimBody(name, platform);
|
|
31
|
+
const target = path.join(dir, file);
|
|
32
|
+
await writeFile(target, body, { mode: 0o755 });
|
|
33
|
+
await chmod(target, 0o755);
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
export async function removeAlias(name, env = process.env, platform = process.platform) {
|
|
37
|
+
const { file } = shimBody(name, platform);
|
|
38
|
+
const target = path.join(shimDir(env), file);
|
|
39
|
+
try {
|
|
40
|
+
await rm(target);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Alias names present in the shim folder (the proxy binary is not one). */
|
|
48
|
+
export async function listAliases(env = process.env) {
|
|
49
|
+
try {
|
|
50
|
+
return (await readdir(shimDir(env)))
|
|
51
|
+
.filter((f) => !f.startsWith("flocktab-proxy") && !f.endsWith(".part"))
|
|
52
|
+
.map((f) => f.replace(/\.cmd$/i, ""))
|
|
53
|
+
.sort();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Whether the shim folder is on PATH, so an alias would actually be found. */
|
|
60
|
+
export function shimDirOnPath(env = process.env, platform = process.platform) {
|
|
61
|
+
const sep = platform === "win32" ? ";" : ":";
|
|
62
|
+
const dir = path.resolve(shimDir(env));
|
|
63
|
+
return (env.PATH ?? "").split(sep).some((p) => p && path.resolve(p) === dir);
|
|
64
|
+
}
|
|
65
|
+
/** PATH for the child agent: everything except the shim folder. */
|
|
66
|
+
export function pathWithoutShims(env = process.env, platform = process.platform) {
|
|
67
|
+
const sep = platform === "win32" ? ";" : ":";
|
|
68
|
+
const dir = path.resolve(shimDir(env));
|
|
69
|
+
return (env.PATH ?? "")
|
|
70
|
+
.split(sep)
|
|
71
|
+
.filter((p) => p && path.resolve(p) !== dir)
|
|
72
|
+
.join(sep);
|
|
73
|
+
}
|
|
74
|
+
/** The line to add to a shell rc so the shims win. */
|
|
75
|
+
export function pathLine(platform = process.platform) {
|
|
76
|
+
return platform === "win32"
|
|
77
|
+
? `setx PATH "%USERPROFILE%\\.flocktab\\bin;%PATH%"`
|
|
78
|
+
: `export PATH="$HOME/.flocktab/bin:$PATH"`;
|
|
79
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* tab codex [...args] Codex on the tab
|
|
8
8
|
* tab <cmd> [...args] anything else, both APIs pointed at the tab
|
|
9
9
|
* tab use pick (or change) the Agent this folder runs as
|
|
10
|
+
* tab alias setup codex make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)
|
|
11
|
+
* tab alias remove codex undo that; `tab alias list` shows them
|
|
10
12
|
* tab status which flock, which Agent here, is the proxy up
|
|
11
13
|
* tab version tab and proxy versions
|
|
12
14
|
* tab up | down | update self-hosted: start, stop, or fetch the newest proxy
|
|
@@ -28,6 +30,7 @@ import { ConsoleApiError, createAgent, issueAgentKey, listAgents } from "./conso
|
|
|
28
30
|
import { consoleUrlFor, DeviceLoginError, startDeviceLogin, waitForApproval } from "./device-login.js";
|
|
29
31
|
import { clearConfig, configDir, configPath, HOSTED_PROXY, isLocalProxy, loadConfig, LOCAL_PROXY, normalizeProxyUrl, presentedKey, saveConfig, } from "./config.js";
|
|
30
32
|
import { reportFolder } from "./folder.js";
|
|
33
|
+
import { listAliases, pathLine, pathWithoutShims, removeAlias, shimDir, shimDirOnPath, validAliasName, writeAlias } from "./alias.js";
|
|
31
34
|
import { agentNameFor, projectRoot, readProject, writeProject } from "./project.js";
|
|
32
35
|
import { downloadProxy, hasProxyEnv, packagedBinPath, proxyBinPath, proxyEnvPath, proxyInstalled, proxyLogPath, PROXY_VERSION, startProxy, stopProxy, tailProxyLog, writeProxyEnv, } from "./proxy-bin.js";
|
|
33
36
|
import { proxyHealth, waitHealthy } from "./selfhost.js";
|
|
@@ -48,6 +51,8 @@ function usage() {
|
|
|
48
51
|
cmd(others.join(" | tab "), "the same, for those agents"),
|
|
49
52
|
cmd("<command> [args]", "any other agent, both APIs pointed at the tab"),
|
|
50
53
|
cmd("use", "pick or change the Agent this folder runs as (.flocktab)"),
|
|
54
|
+
cmd("alias setup <name>...", "make plain `codex` run `tab codex` (shims in ~/.flocktab/bin)"),
|
|
55
|
+
cmd("alias remove <name>...", "undo that; `tab alias list` shows them"),
|
|
51
56
|
cmd("status", "which flock, which Agent here, is the proxy up"),
|
|
52
57
|
cmd("version", "tab and proxy versions"),
|
|
53
58
|
cmd("up", "self-hosted: start the local proxy (fetches it the first time)"),
|
|
@@ -487,6 +492,8 @@ async function runAgent(name, args) {
|
|
|
487
492
|
const key = presentedKey({ key: agent.key, unlock: config.unlock });
|
|
488
493
|
reportFolder(config.proxyUrl, key);
|
|
489
494
|
const { spec, env } = envFor({ name, proxyUrl: config.proxyUrl, presentedKey: key });
|
|
495
|
+
// An alias shim named like the agent must not be what we spawn.
|
|
496
|
+
env.PATH = pathWithoutShims();
|
|
490
497
|
if (spec.isolatedHome === "codex") {
|
|
491
498
|
env.CODEX_HOME = await prepareCodexHome({
|
|
492
499
|
baseDir: configDir(),
|
|
@@ -511,6 +518,59 @@ async function runAgent(name, args) {
|
|
|
511
518
|
child.on("close", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
|
|
512
519
|
});
|
|
513
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* `tab alias codex claude`: shims so the plain commands run on the tab.
|
|
523
|
+
* `tab alias` alone lists them; `tab unalias codex` removes one.
|
|
524
|
+
*/
|
|
525
|
+
async function alias(args, removeFlag = false) {
|
|
526
|
+
// `tab alias setup codex`, `tab alias remove codex`, `tab alias list`;
|
|
527
|
+
// the bare `tab alias codex` and `tab unalias codex` mean the same.
|
|
528
|
+
const [first, ...rest] = args;
|
|
529
|
+
let names = args;
|
|
530
|
+
let remove = removeFlag;
|
|
531
|
+
if (first === "setup" || first === "add")
|
|
532
|
+
names = rest;
|
|
533
|
+
else if (first === "remove" || first === "rm") {
|
|
534
|
+
names = rest;
|
|
535
|
+
remove = true;
|
|
536
|
+
}
|
|
537
|
+
else if (first === "list" || first === "ls")
|
|
538
|
+
names = [];
|
|
539
|
+
if (names.length === 0 && !remove) {
|
|
540
|
+
const existing = await listAliases();
|
|
541
|
+
if (existing.length === 0)
|
|
542
|
+
say(dim(`No aliases. Example: ${bold("tab alias codex claude")}`));
|
|
543
|
+
else
|
|
544
|
+
say(rows(existing.map((n) => [n, dim(`tab ${n}`)]), 0));
|
|
545
|
+
return pathHint();
|
|
546
|
+
}
|
|
547
|
+
if (names.length === 0) {
|
|
548
|
+
fail("tab alias remove needs a name, e.g. tab alias remove codex");
|
|
549
|
+
return 2;
|
|
550
|
+
}
|
|
551
|
+
for (const name of names) {
|
|
552
|
+
if (!validAliasName(name)) {
|
|
553
|
+
fail(`${name} is not a usable alias name.`);
|
|
554
|
+
return 2;
|
|
555
|
+
}
|
|
556
|
+
if (remove) {
|
|
557
|
+
(await removeAlias(name)) ? ok(`Removed ${name}.`) : warn(`${name} was not an alias.`);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
const file = await writeAlias(name);
|
|
561
|
+
ok(`${bold(name)} now runs ${bold(`tab ${name}`)} ${dim(`(${file})`)}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return remove ? 0 : pathHint();
|
|
565
|
+
}
|
|
566
|
+
function pathHint() {
|
|
567
|
+
if (shimDirOnPath())
|
|
568
|
+
return 0;
|
|
569
|
+
say("");
|
|
570
|
+
say(`Put ${dim(shimDir())} first on your PATH so the aliases win. Add to your shell rc, then open a new terminal:`);
|
|
571
|
+
say(` ${bold(pathLine())}`);
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
514
574
|
/** `tab use`: pick or change the Agent for this folder. */
|
|
515
575
|
async function use() {
|
|
516
576
|
const config = await ensureLogin();
|
|
@@ -605,6 +665,10 @@ async function main(argv) {
|
|
|
605
665
|
return 0;
|
|
606
666
|
case "use":
|
|
607
667
|
return use();
|
|
668
|
+
case "alias":
|
|
669
|
+
return alias(rest);
|
|
670
|
+
case "unalias":
|
|
671
|
+
return alias(rest, true);
|
|
608
672
|
case "version":
|
|
609
673
|
case "--version":
|
|
610
674
|
case "-v":
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Written by scripts/write-version.mjs from package.json at build; `tab version` prints it. */
|
|
2
|
-
export const TAB_VERSION = "0.1.
|
|
2
|
+
export const TAB_VERSION = "0.1.3";
|