linersh 0.5.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 +168 -0
- package/bin/liner.js +126 -0
- package/dist/App.js +195 -0
- package/dist/App.js.map +1 -0
- package/dist/agents/detect.js +102 -0
- package/dist/agents/detect.js.map +1 -0
- package/dist/agents/events.js +8 -0
- package/dist/agents/events.js.map +1 -0
- package/dist/agents/jtbd-clarify.js +144 -0
- package/dist/agents/jtbd-clarify.js.map +1 -0
- package/dist/agents/parsers.js +316 -0
- package/dist/agents/parsers.js.map +1 -0
- package/dist/agents/prompts.js +301 -0
- package/dist/agents/prompts.js.map +1 -0
- package/dist/agents/run-log.js +71 -0
- package/dist/agents/run-log.js.map +1 -0
- package/dist/agents/runner.js +180 -0
- package/dist/agents/runner.js.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/bin-resolver.js +92 -0
- package/dist/bin-resolver.js.map +1 -0
- package/dist/browser-support.js +62 -0
- package/dist/browser-support.js.map +1 -0
- package/dist/colors.js +105 -0
- package/dist/colors.js.map +1 -0
- package/dist/components/Header.js +17 -0
- package/dist/components/Header.js.map +1 -0
- package/dist/components/KeyHints.js +10 -0
- package/dist/components/KeyHints.js.map +1 -0
- package/dist/components/LabeledBox.js +53 -0
- package/dist/components/LabeledBox.js.map +1 -0
- package/dist/components/NextSteps.js +18 -0
- package/dist/components/NextSteps.js.map +1 -0
- package/dist/config.js +88 -0
- package/dist/config.js.map +1 -0
- package/dist/editor.js +40 -0
- package/dist/editor.js.map +1 -0
- package/dist/gates.js +121 -0
- package/dist/gates.js.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc.js +136 -0
- package/dist/ipc.js.map +1 -0
- package/dist/phases.js +348 -0
- package/dist/phases.js.map +1 -0
- package/dist/progress.js +185 -0
- package/dist/progress.js.map +1 -0
- package/dist/screens/AgentSetup.js +217 -0
- package/dist/screens/AgentSetup.js.map +1 -0
- package/dist/screens/BootSplash.js +159 -0
- package/dist/screens/BootSplash.js.map +1 -0
- package/dist/screens/BrowserSetup.js +181 -0
- package/dist/screens/BrowserSetup.js.map +1 -0
- package/dist/screens/CompileView.js +297 -0
- package/dist/screens/CompileView.js.map +1 -0
- package/dist/screens/FramingConfirmReview.js +193 -0
- package/dist/screens/FramingConfirmReview.js.map +1 -0
- package/dist/screens/Gate1Review.js +143 -0
- package/dist/screens/Gate1Review.js.map +1 -0
- package/dist/screens/Gate2Review.js +158 -0
- package/dist/screens/Gate2Review.js.map +1 -0
- package/dist/screens/ImportPrompt.js +65 -0
- package/dist/screens/ImportPrompt.js.map +1 -0
- package/dist/screens/LocalFilePicker.js +139 -0
- package/dist/screens/LocalFilePicker.js.map +1 -0
- package/dist/screens/NewMixtapeWizard.js +662 -0
- package/dist/screens/NewMixtapeWizard.js.map +1 -0
- package/dist/screens/NewProjectPrompt.js +66 -0
- package/dist/screens/NewProjectPrompt.js.map +1 -0
- package/dist/screens/PhaseRunner.js +1119 -0
- package/dist/screens/PhaseRunner.js.map +1 -0
- package/dist/screens/ProcessManifest.js +158 -0
- package/dist/screens/ProcessManifest.js.map +1 -0
- package/dist/screens/ProjectHub.js +546 -0
- package/dist/screens/ProjectHub.js.map +1 -0
- package/dist/screens/Settings.js +72 -0
- package/dist/screens/Settings.js.map +1 -0
- package/dist/screens/SourceModal.js +269 -0
- package/dist/screens/SourceModal.js.map +1 -0
- package/dist/screens/TapeBrowser.js +277 -0
- package/dist/screens/TapeBrowser.js.map +1 -0
- package/dist/screens/TapeDraftReview.js +440 -0
- package/dist/screens/TapeDraftReview.js.map +1 -0
- package/dist/screens/TapeEditor.js +310 -0
- package/dist/screens/TapeEditor.js.map +1 -0
- package/dist/screens/cassette-art.js +72 -0
- package/dist/screens/cassette-art.js.map +1 -0
- package/dist/store.js +10 -0
- package/dist/store.js.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/yaml-io.js +345 -0
- package/dist/yaml-io.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Liner TUI
|
|
2
|
+
|
|
3
|
+
Interactive authoring UI for [Liner](../..) — create, edit, compile, and share mixtape **project folders** from the keyboard.
|
|
4
|
+
|
|
5
|
+
A *tape file* (`tape.yaml`) is the recipe. A *mixtape* is the project folder Liner builds from it. The TUI is for authoring: it scaffolds folders, edits tapes, surfaces the synthesis status, then runs `liner compile` to produce the consumable `MIXTAPE.md` + `sources/` artifacts.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
# One-shot:
|
|
11
|
+
npx linersh
|
|
12
|
+
|
|
13
|
+
# Persistent:
|
|
14
|
+
npm install -g linersh # adds `liner` to PATH
|
|
15
|
+
liner
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The npm package includes the TUI and resolves the Python core from a per-platform optional package (`linersh-<platform>-<arch>`). With no arguments, `liner` opens the TUI. CLI subcommands such as `liner setup-js`, `liner compile`, `liner share`, and `liner status` pass through to the bundled core.
|
|
19
|
+
|
|
20
|
+
For JavaScript-rendered pages, run the setup command from the same npm install:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
liner setup-js
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That command is idempotent and downloads the Playwright browser used for `render: js` and automatic JS-wall fallback. For debugging or local development, `LINER_BIN=/path/to/liner liner` overrides the bundled binary.
|
|
27
|
+
|
|
28
|
+
## Run
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
cd ~/projects/my-thing
|
|
32
|
+
liner
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The TUI looks for (and creates) a `./mixtapes/` folder in your current directory. Each subdirectory inside it is a mixtape project folder (must contain a `tape.yaml`).
|
|
36
|
+
|
|
37
|
+
Override the workspace with `LINER_DIR`:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
LINER_DIR=~/mixtapes liner
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Keys
|
|
44
|
+
|
|
45
|
+
### Browser
|
|
46
|
+
| Key | Action |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `↑` `↓` or `j` `k` | move selection |
|
|
49
|
+
| `enter` or `e` | open in editor |
|
|
50
|
+
| `n` | new mixtape (prompts for slug) |
|
|
51
|
+
| `c` | compile selected |
|
|
52
|
+
| `s` | share selected (zip to `.mixtape`) |
|
|
53
|
+
| `i` | import a `.mixtape` archive |
|
|
54
|
+
| `d` | delete selected folder (with confirm) |
|
|
55
|
+
| `r` | refresh |
|
|
56
|
+
| `q` | quit |
|
|
57
|
+
|
|
58
|
+
### Editor
|
|
59
|
+
| Key | Action |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `t` `d` `u` | edit title / description / curator |
|
|
62
|
+
| `m` | toggle mode (`quick` ⇄ `methodology`) |
|
|
63
|
+
| `g` | edit JTBD (job-to-be-done) |
|
|
64
|
+
| `E` | open `synthesis.md` in `$EDITOR` |
|
|
65
|
+
| `↑` `↓` | select source |
|
|
66
|
+
| `a` `e` `x` | add / edit / remove source |
|
|
67
|
+
| `K` `J` | reorder selected source up/down |
|
|
68
|
+
| `s` | save `tape.yaml` |
|
|
69
|
+
| `c` | save + compile (blocked if synthesis is empty or still a placeholder) |
|
|
70
|
+
| `esc` | back (warns on unsaved changes) |
|
|
71
|
+
|
|
72
|
+
A soft warning appears when any source is missing a curator note. Notes are load-bearing — see [CURATION.md](https://liner.sh) §6.
|
|
73
|
+
|
|
74
|
+
#### Local sources
|
|
75
|
+
|
|
76
|
+
`local_file` sources reference content you've placed under `<project>/personal/` — book chapter PDFs, Reader-Mode-saved articles, exported HIG pages. Use them for material that isn't on the public web or that lives behind a paywall you have legitimate access to.
|
|
77
|
+
|
|
78
|
+
Workflow:
|
|
79
|
+
|
|
80
|
+
1. Save the file into `<project>/personal/`. The TUI creates the folder lazily — the file picker offers a "new file (paste path)" option that you can use after saving the file manually with your OS file manager.
|
|
81
|
+
2. Add a source in the editor; press `T` in the source modal to switch the type to `local_file`.
|
|
82
|
+
3. Pick the file via the picker; supply a `citation` (author, title, publication, date — the AI uses this to weight the source).
|
|
83
|
+
4. Compile. The CLI extracts the content (PDFs via `pdfplumber`, HTML via the same readability pipeline as web sources, `.md`/`.txt` as-is).
|
|
84
|
+
|
|
85
|
+
`liner share` includes `personal/` by default. Pass `--no-personal` (or use the corresponding TUI flag) to exclude it — required for any tape intended as a library submission.
|
|
86
|
+
|
|
87
|
+
The synthesis indicator shows one of:
|
|
88
|
+
- `✗ missing` — `liner compile` will hard-fail
|
|
89
|
+
- `⚠ placeholder` — file exists but still has the boilerplate from `liner init`
|
|
90
|
+
- `⚠ empty` — file exists but is empty
|
|
91
|
+
- `✓ N chars` — ready to compile
|
|
92
|
+
|
|
93
|
+
### Source modal
|
|
94
|
+
| Key | Action |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `↑` `↓` | move between fields |
|
|
97
|
+
| `enter` | edit the focused field (or open the file picker on the path field for `local_file` sources) |
|
|
98
|
+
| `tab` | next field while editing |
|
|
99
|
+
| `T` | toggle source type (`web` ⇄ `local_file`) |
|
|
100
|
+
| `R` | toggle `render: server` ⇄ `render: js` (web sources only) |
|
|
101
|
+
| `p` | toggle required/optional |
|
|
102
|
+
| `s` | save source |
|
|
103
|
+
| `esc` | cancel |
|
|
104
|
+
|
|
105
|
+
For `web` sources the URL is the load-bearing field; the type is auto-detected as `youtube` or `web`. For `local_file` sources the modal shows `path`, `citation`, `note`, `section`, `priority` — pick the path via the file picker (lists files under `personal/`, with a "paste path" affordance for new files).
|
|
106
|
+
|
|
107
|
+
### Compile
|
|
108
|
+
| Key | Action |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `esc` | cancel running compile |
|
|
111
|
+
| `y` | copy compiled `MIXTAPE.md` to clipboard (`pbcopy` / `xclip`) |
|
|
112
|
+
| `s` | share the compiled mixtape (zip to `.mixtape`) |
|
|
113
|
+
| `r` | re-run compile |
|
|
114
|
+
| `b` | back to editor |
|
|
115
|
+
|
|
116
|
+
The compile writes `MIXTAPE.md` and `sources/NN-<slug>.md` files directly into the project folder. There's nothing to "save" afterwards — the artifacts are already on disk.
|
|
117
|
+
|
|
118
|
+
## Uninstalling
|
|
119
|
+
|
|
120
|
+
The TUI ships in the same `linersh` package as the CLI. The cleanest path is the repo's uninstaller:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
./scripts/uninstall.sh # interactive
|
|
124
|
+
./scripts/uninstall.sh --yes # no prompts
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It removes the optional Playwright browser, the local cache, any pipx-installed CLI, and any globally installed TUI (`npm install -g linersh`). Your mixtape project folders are left alone unless you pass `--purge-workspace`. See the root [README — Uninstalling](../../README.md#uninstalling) for the manual recipe.
|
|
128
|
+
|
|
129
|
+
## Develop
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
cd packages/tui
|
|
133
|
+
npm install
|
|
134
|
+
npm run dev # tsx (no watch — Ctrl+C and re-run after code changes)
|
|
135
|
+
npm run dev:built # builds, then runs the compiled output (closer to ship)
|
|
136
|
+
npm run typecheck
|
|
137
|
+
npm test # vitest
|
|
138
|
+
npm run build # emits dist/
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Why no `tsx watch`?** `tsx watch` keeps the TTY in raw mode for its own restart UI (Enter to restart, "rs" to force, Esc to clear). Ink TUIs also want raw stdin, and the two collide — typed characters get eaten, the app gets surprise-restarted, screens flicker. For interactive UIs, restart manually after code changes.
|
|
142
|
+
|
|
143
|
+
In dev, the bin-resolver tries `LINER_BIN`, the bundled optional package, the repo-local `.venv/bin/liner`, then `liner` on `$PATH`. To point at a specific binary:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
LINER_BIN=/path/to/liner npm run dev
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Architecture
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
TapeBrowser ────► NewProjectPrompt ─┐
|
|
153
|
+
│ │
|
|
154
|
+
│ ImportPrompt ─────┤
|
|
155
|
+
│ │
|
|
156
|
+
├────► TapeEditor ⇄ SourceModal
|
|
157
|
+
│ │
|
|
158
|
+
│ ▼
|
|
159
|
+
│ CompileView (streams NDJSON from `liner compile --emit-events`)
|
|
160
|
+
│
|
|
161
|
+
└────► (share/delete inline)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
State is global via `AppContext`. YAML is parsed and re-written in Node via the `yaml` package's `Document` API so user-written block comments survive edits. All folder operations (`init`, `compile`, `share`, `import`, `list`) go through the Python CLI as subprocesses — the TUI never reimplements them.
|
|
165
|
+
|
|
166
|
+
The CLI's `compile --emit-events` streams NDJSON progress events to stdout; the TUI parses each line and updates per-source state live. The final `result` event reports the produced folder, `MIXTAPE.md` path, and per-source file paths.
|
|
167
|
+
|
|
168
|
+
The same NDJSON protocol will power the planned MCP server, which calls into the same Python core directly without going through the CLI.
|
package/bin/liner.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry shim for `npx linersh` / `liner`.
|
|
3
|
+
//
|
|
4
|
+
// No args: launch the Ink TUI from dist/.
|
|
5
|
+
// CLI args: forward to the bundled Python core (or LINER_BIN/dev fallback).
|
|
6
|
+
//
|
|
7
|
+
// This lets the npm package expose one binary name while still supporting
|
|
8
|
+
// mechanical commands like `liner setup-js`, `liner compile`, and `liner status`.
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const packageRoot = join(here, "..");
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
const CLI_COMMANDS = new Set([
|
|
20
|
+
"init",
|
|
21
|
+
"replay",
|
|
22
|
+
"compile",
|
|
23
|
+
"share",
|
|
24
|
+
"import",
|
|
25
|
+
"clone",
|
|
26
|
+
"setup-js",
|
|
27
|
+
"list",
|
|
28
|
+
"cache",
|
|
29
|
+
"manifest",
|
|
30
|
+
"status",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function wantsCli(args) {
|
|
34
|
+
if (args.length === 0) return false;
|
|
35
|
+
const first = args[0];
|
|
36
|
+
return first === "--help" || first === "--version" || first === "-h" || CLI_COMMANDS.has(first);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (wantsCli(argv)) {
|
|
40
|
+
const resolved = resolveCliBinary();
|
|
41
|
+
const child = spawn(resolved.command, [...resolved.args, ...argv], {
|
|
42
|
+
stdio: "inherit",
|
|
43
|
+
env: { ...process.env, LINER_TUI_SHIM: "1" },
|
|
44
|
+
});
|
|
45
|
+
child.on("exit", (code, signal) => {
|
|
46
|
+
if (signal) {
|
|
47
|
+
process.kill(process.pid, signal);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
process.exit(code ?? 1);
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
await import(join(packageRoot, "dist", "index.js"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveCliBinary() {
|
|
57
|
+
const envBin = process.env.LINER_BIN;
|
|
58
|
+
if (envBin && existsSync(envBin)) {
|
|
59
|
+
return { command: envBin, args: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const bundled = findBundledBinary();
|
|
63
|
+
if (bundled) return { command: bundled, args: [] };
|
|
64
|
+
|
|
65
|
+
const repoVenv = findRepoVenvBinary();
|
|
66
|
+
if (repoVenv) return { command: repoVenv, args: [] };
|
|
67
|
+
|
|
68
|
+
const pathHit = findPathBinary();
|
|
69
|
+
if (pathHit) return { command: pathHit, args: [] };
|
|
70
|
+
|
|
71
|
+
console.error(
|
|
72
|
+
[
|
|
73
|
+
"Could not find the Liner CLI binary.",
|
|
74
|
+
"",
|
|
75
|
+
"Expected one of:",
|
|
76
|
+
" - LINER_BIN=/path/to/liner",
|
|
77
|
+
" - bundled platform package (linersh-<platform>-<arch>)",
|
|
78
|
+
" - repo-local .venv/bin/liner",
|
|
79
|
+
" - a separate liner on PATH",
|
|
80
|
+
].join("\n"),
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findBundledBinary() {
|
|
86
|
+
const require = createRequire(import.meta.url);
|
|
87
|
+
const target = `linersh-${process.platform}-${process.arch}`;
|
|
88
|
+
try {
|
|
89
|
+
const pkgJson = require.resolve(`${target}/package.json`);
|
|
90
|
+
const baseDir = pkgJson.replace(/[\\/]package\.json$/, "");
|
|
91
|
+
const candidate = join(baseDir, exeName());
|
|
92
|
+
return existsSync(candidate) ? candidate : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findRepoVenvBinary() {
|
|
99
|
+
const venvBinDir = process.platform === "win32" ? "Scripts" : "bin";
|
|
100
|
+
let dir = packageRoot;
|
|
101
|
+
for (let i = 0; i < 12; i += 1) {
|
|
102
|
+
const candidate = join(dir, ".venv", venvBinDir, exeName());
|
|
103
|
+
if (existsSync(candidate)) return candidate;
|
|
104
|
+
const parent = dirname(dir);
|
|
105
|
+
if (parent === dir) break;
|
|
106
|
+
dir = parent;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function findPathBinary() {
|
|
112
|
+
const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["liner"], {
|
|
113
|
+
encoding: "utf8",
|
|
114
|
+
});
|
|
115
|
+
if (probe.status !== 0) return null;
|
|
116
|
+
const hits = probe.stdout
|
|
117
|
+
.split(/\r?\n/)
|
|
118
|
+
.map((s) => s.trim())
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
const thisShim = fileURLToPath(import.meta.url);
|
|
121
|
+
return hits.find((hit) => hit !== thisShim) ?? null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function exeName() {
|
|
125
|
+
return process.platform === "win32" ? "liner.exe" : "liner";
|
|
126
|
+
}
|
package/dist/App.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { AppContext } from "./store.js";
|
|
4
|
+
import { ERROR } from "./colors.js";
|
|
5
|
+
import { TapeBrowser } from "./screens/TapeBrowser.js";
|
|
6
|
+
import { TapeEditor } from "./screens/TapeEditor.js";
|
|
7
|
+
import { SourceModal } from "./screens/SourceModal.js";
|
|
8
|
+
import { CompileView } from "./screens/CompileView.js";
|
|
9
|
+
import { NewMixtapeWizard } from "./screens/NewMixtapeWizard.js";
|
|
10
|
+
import { ProjectHub } from "./screens/ProjectHub.js";
|
|
11
|
+
import { PhaseRunner } from "./screens/PhaseRunner.js";
|
|
12
|
+
import { FramingConfirmReview } from "./screens/FramingConfirmReview.js";
|
|
13
|
+
import { Gate1Review } from "./screens/Gate1Review.js";
|
|
14
|
+
import { Gate2Review } from "./screens/Gate2Review.js";
|
|
15
|
+
import { TapeDraftReview } from "./screens/TapeDraftReview.js";
|
|
16
|
+
import { ImportPrompt } from "./screens/ImportPrompt.js";
|
|
17
|
+
import { AgentSetup } from "./screens/AgentSetup.js";
|
|
18
|
+
import { ProcessManifest } from "./screens/ProcessManifest.js";
|
|
19
|
+
import { BootSplash, shouldShowSplash } from "./screens/BootSplash.js";
|
|
20
|
+
import { configExists } from "./config.js";
|
|
21
|
+
import { detectAgents } from "./agents/detect.js";
|
|
22
|
+
import { listProjects } from "./ipc.js";
|
|
23
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
const NOTIFICATION_MS = 3500;
|
|
26
|
+
/** Cap on the back stack — deeper history isn't useful and just costs memory. */
|
|
27
|
+
const HISTORY_LIMIT = 10;
|
|
28
|
+
export function App() {
|
|
29
|
+
const baseDir = useRef(resolveBaseDir()).current;
|
|
30
|
+
// Decide what the user sees first. Three states, in priority order:
|
|
31
|
+
//
|
|
32
|
+
// 1. Agent setup — first launch on a machine with multiple agents
|
|
33
|
+
// installed AND no config file yet. Force a choice before the splash
|
|
34
|
+
// so the methodology phases don't hit the mid-phase picker.
|
|
35
|
+
// 2. Splash — the normal greeting (cassette art + launch menu).
|
|
36
|
+
// 3. Browser — fallback when stdout isn't a TTY or LINER_NO_SPLASH is
|
|
37
|
+
// set (CI runs, narrow terminals).
|
|
38
|
+
//
|
|
39
|
+
// Single-agent installs skip step 1: there's nothing to pick. Repeat runs
|
|
40
|
+
// skip it too because config.yaml exists.
|
|
41
|
+
const initialScreen = resolveInitialScreen();
|
|
42
|
+
const [screen, setScreen] = useState(initialScreen);
|
|
43
|
+
const [history, setHistory] = useState([]);
|
|
44
|
+
const [projects, setProjects] = useState([]);
|
|
45
|
+
const [notification, setNotification] = useState(null);
|
|
46
|
+
const [bootError, setBootError] = useState(null);
|
|
47
|
+
// navigate(next) pushes the current screen onto a back stack and replaces
|
|
48
|
+
// it with `next`. Side effect: clears the terminal scrollback when the
|
|
49
|
+
// user moves between meaningfully different surfaces (any kind change, or
|
|
50
|
+
// a phaseRunner re-entry with a different phaseId). Clearing happens
|
|
51
|
+
// before setScreen so Ink's next render lands on a fresh canvas — vs.
|
|
52
|
+
// clearing after, which would briefly show then erase the new screen.
|
|
53
|
+
const navigate = useCallback((next) => {
|
|
54
|
+
setScreen((current) => {
|
|
55
|
+
setHistory((h) => [...h, current].slice(-HISTORY_LIMIT));
|
|
56
|
+
if (process.stdout.isTTY && shouldClearOnNavigate(current, next)) {
|
|
57
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
58
|
+
}
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
}, []);
|
|
62
|
+
// back() pops the history stack and navigates to the prior screen. When
|
|
63
|
+
// the stack is empty (root) it falls back to the browser. We pop INSIDE
|
|
64
|
+
// setScreen so the two updates batch and Ink renders once.
|
|
65
|
+
const back = useCallback(() => {
|
|
66
|
+
setHistory((h) => {
|
|
67
|
+
if (h.length === 0) {
|
|
68
|
+
if (process.stdout.isTTY)
|
|
69
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
70
|
+
setScreen({ kind: "browser" });
|
|
71
|
+
return h;
|
|
72
|
+
}
|
|
73
|
+
const prev = h[h.length - 1];
|
|
74
|
+
if (process.stdout.isTTY)
|
|
75
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
76
|
+
setScreen(prev);
|
|
77
|
+
return h.slice(0, -1);
|
|
78
|
+
});
|
|
79
|
+
}, []);
|
|
80
|
+
const refreshProjects = useCallback(async () => {
|
|
81
|
+
try {
|
|
82
|
+
const result = await listProjects(baseDir);
|
|
83
|
+
setProjects(result);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
setBootError(e.message);
|
|
87
|
+
}
|
|
88
|
+
}, [baseDir]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
void refreshProjects();
|
|
91
|
+
}, [refreshProjects]);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!notification)
|
|
94
|
+
return;
|
|
95
|
+
const timer = setTimeout(() => setNotification(null), NOTIFICATION_MS);
|
|
96
|
+
return () => clearTimeout(timer);
|
|
97
|
+
}, [notification]);
|
|
98
|
+
if (bootError) {
|
|
99
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
100
|
+
React.createElement(Text, { color: ERROR }, bootError),
|
|
101
|
+
React.createElement(Text, { dimColor: true }, "Press Ctrl+C to exit.")));
|
|
102
|
+
}
|
|
103
|
+
const ctx = {
|
|
104
|
+
screen,
|
|
105
|
+
baseDir,
|
|
106
|
+
projects,
|
|
107
|
+
notification,
|
|
108
|
+
navigate,
|
|
109
|
+
back,
|
|
110
|
+
refreshProjects,
|
|
111
|
+
setNotification,
|
|
112
|
+
};
|
|
113
|
+
return (React.createElement(AppContext.Provider, { value: ctx },
|
|
114
|
+
React.createElement(ScreenRoot, { screen: screen })));
|
|
115
|
+
}
|
|
116
|
+
function ScreenRoot({ screen }) {
|
|
117
|
+
switch (screen.kind) {
|
|
118
|
+
case "splash":
|
|
119
|
+
return React.createElement(BootSplash, null);
|
|
120
|
+
case "agentSetup":
|
|
121
|
+
return React.createElement(AgentSetup, null);
|
|
122
|
+
case "browser":
|
|
123
|
+
return React.createElement(TapeBrowser, null);
|
|
124
|
+
case "newProject":
|
|
125
|
+
return React.createElement(NewMixtapeWizard, null);
|
|
126
|
+
case "hub":
|
|
127
|
+
return React.createElement(ProjectHub, { tape: screen.tape, folder: screen.folder });
|
|
128
|
+
case "phaseRunner":
|
|
129
|
+
// `key` forces React to unmount and remount when phaseId changes —
|
|
130
|
+
// PhaseRunner's auto-launch effect has empty deps and only fires on
|
|
131
|
+
// initial mount, so without the key, navigating Phase 2 → Phase 4 (both
|
|
132
|
+
// "phaseRunner" kind) leaves the component dormant: same DOM, no
|
|
133
|
+
// agent launch. The remount is also what resets the tool list,
|
|
134
|
+
// finalText, and stats from the previous phase.
|
|
135
|
+
return (React.createElement(PhaseRunner, { key: screen.phaseId, tape: screen.tape, folder: screen.folder, phaseId: screen.phaseId }));
|
|
136
|
+
case "gate0":
|
|
137
|
+
return React.createElement(FramingConfirmReview, { tape: screen.tape, folder: screen.folder });
|
|
138
|
+
case "gate1":
|
|
139
|
+
return React.createElement(Gate1Review, { tape: screen.tape, folder: screen.folder });
|
|
140
|
+
case "gate2":
|
|
141
|
+
return React.createElement(Gate2Review, { tape: screen.tape, folder: screen.folder });
|
|
142
|
+
case "tapeDraft":
|
|
143
|
+
return React.createElement(TapeDraftReview, { tape: screen.tape, folder: screen.folder });
|
|
144
|
+
case "import":
|
|
145
|
+
return React.createElement(ImportPrompt, null);
|
|
146
|
+
case "editor":
|
|
147
|
+
return React.createElement(TapeEditor, { tape: screen.tape, folder: screen.folder });
|
|
148
|
+
case "sourceModal":
|
|
149
|
+
return (React.createElement(SourceModal, { tape: screen.tape, folder: screen.folder, editingIndex: screen.editingIndex }));
|
|
150
|
+
case "compile":
|
|
151
|
+
return React.createElement(CompileView, { tape: screen.tape, folder: screen.folder });
|
|
152
|
+
case "manifest":
|
|
153
|
+
return React.createElement(ProcessManifest, { tape: screen.tape, folder: screen.folder });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Pick the screen the user lands on at startup. See the inline doc in
|
|
158
|
+
* `App()` for the priority order.
|
|
159
|
+
*/
|
|
160
|
+
function resolveInitialScreen() {
|
|
161
|
+
// If no splash (non-TTY / narrow / opted-out), don't show setup either —
|
|
162
|
+
// the user is probably in CI or scripting and just wants the browser.
|
|
163
|
+
if (!shouldShowSplash())
|
|
164
|
+
return { kind: "browser" };
|
|
165
|
+
// First-run setup only when we have a real choice to surface. One agent
|
|
166
|
+
// (or zero) → just go to splash; setup would be a no-op.
|
|
167
|
+
if (!configExists()) {
|
|
168
|
+
const installed = detectAgents();
|
|
169
|
+
if (installed.length >= 2)
|
|
170
|
+
return { kind: "agentSetup" };
|
|
171
|
+
}
|
|
172
|
+
return { kind: "splash" };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Decide whether navigation `prev → next` should clear the terminal. We clear
|
|
176
|
+
* on any kind change, and on phaseRunner re-entry with a different phaseId so
|
|
177
|
+
* "entering Phase 3" feels like starting fresh, not stacking on top of Phase 2.
|
|
178
|
+
*/
|
|
179
|
+
function shouldClearOnNavigate(prev, next) {
|
|
180
|
+
if (prev.kind !== next.kind)
|
|
181
|
+
return true;
|
|
182
|
+
if (prev.kind === "phaseRunner" && next.kind === "phaseRunner") {
|
|
183
|
+
return prev.phaseId !== next.phaseId;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
function resolveBaseDir() {
|
|
188
|
+
const override = process.env["LINER_DIR"];
|
|
189
|
+
const dir = override || join(process.cwd(), "mixtapes");
|
|
190
|
+
if (!existsSync(dir)) {
|
|
191
|
+
mkdirSync(dir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
return dir;
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=App.js.map
|
package/dist/App.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"App.js","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve an agent binary path. Resolution order:
|
|
6
|
+
*
|
|
7
|
+
* 1. Explicit env override (`LINER_CLAUDE_BIN` / `LINER_CODEX_BIN`).
|
|
8
|
+
* Honored even if the path is missing — we surface the path the user
|
|
9
|
+
* named so they can see exactly what was tried.
|
|
10
|
+
* 2. `which <name>` against the inherited PATH. Fast and correct for the
|
|
11
|
+
* common case (user launched the TUI from their normal shell).
|
|
12
|
+
* 3. `bash -lc 'command -v <name>'` against the *login* shell. Costs ~50ms
|
|
13
|
+
* but sources the user's shell init (.bashrc / .zshrc / fnm / nvm /
|
|
14
|
+
* asdf), catching version-manager shims that aren't in the TUI's
|
|
15
|
+
* inherited PATH — e.g. cmux-launched sessions where fnm hasn't run.
|
|
16
|
+
*
|
|
17
|
+
* Returning null means "definitely not installed" — both shells failed and
|
|
18
|
+
* no env override was set.
|
|
19
|
+
*/
|
|
20
|
+
function resolveBin(name, envVar) {
|
|
21
|
+
const override = process.env[envVar];
|
|
22
|
+
if (override && override.trim())
|
|
23
|
+
return override.trim();
|
|
24
|
+
const direct = spawnSync("which", [name], { encoding: "utf8" });
|
|
25
|
+
if (direct.status === 0) {
|
|
26
|
+
const out = direct.stdout.trim();
|
|
27
|
+
if (out)
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
// Fall back to a login shell so shims (fnm / nvm / asdf / pyenv / etc.)
|
|
31
|
+
// get a chance to register. `command -v` is POSIX-portable; `-l` makes
|
|
32
|
+
// bash source the login profile that puts those shims on PATH.
|
|
33
|
+
const viaLoginShell = spawnSync("bash", ["-lc", `command -v ${name}`], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
});
|
|
36
|
+
if (viaLoginShell.status === 0) {
|
|
37
|
+
const out = viaLoginShell.stdout.trim();
|
|
38
|
+
if (out)
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Names the env var that overrides binary detection for an agent. Mirrors
|
|
45
|
+
* the LINER_BIN pattern from bin-resolver — explicit > inferred.
|
|
46
|
+
*/
|
|
47
|
+
export function envVarForAgent(id) {
|
|
48
|
+
return id === "claude" ? "LINER_CLAUDE_BIN" : "LINER_CODEX_BIN";
|
|
49
|
+
}
|
|
50
|
+
/** Catalog of agents the TUI knows how to drive. Single source of truth so
|
|
51
|
+
* `detectAgents` and the "not detected" UI in AgentSetup stay in sync. */
|
|
52
|
+
export const KNOWN_AGENTS = [
|
|
53
|
+
{ id: "claude", name: "Claude Code", envVar: "LINER_CLAUDE_BIN" },
|
|
54
|
+
{ id: "codex", name: "OpenAI Codex", envVar: "LINER_CODEX_BIN" },
|
|
55
|
+
];
|
|
56
|
+
export function detectAgents() {
|
|
57
|
+
const found = [];
|
|
58
|
+
for (const meta of KNOWN_AGENTS) {
|
|
59
|
+
const bin = resolveBin(meta.id, meta.envVar);
|
|
60
|
+
if (bin)
|
|
61
|
+
found.push({ id: meta.id, name: meta.name, bin });
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Find the curating-mixtapes skill bundle on disk.
|
|
67
|
+
*
|
|
68
|
+
* Resolution order:
|
|
69
|
+
* 1. `LINER_SKILL_PATH` env var (must contain SKILL.md).
|
|
70
|
+
* 2. Walk up from cwd looking for `cli-update-docs/SKILL.md` or
|
|
71
|
+
* `skills/curating-mixtapes/SKILL.md`.
|
|
72
|
+
* 3. Walk up from the TUI binary location (covers the dev/global-install case).
|
|
73
|
+
*/
|
|
74
|
+
export function resolveSkillPath() {
|
|
75
|
+
const env = process.env["LINER_SKILL_PATH"];
|
|
76
|
+
if (env && existsSync(join(env, "SKILL.md")))
|
|
77
|
+
return env;
|
|
78
|
+
const candidates = ["cli-update-docs", "skills/curating-mixtapes"];
|
|
79
|
+
const searchFromRoots = [process.cwd(), dirname(process.argv[1] ?? "")].filter(Boolean);
|
|
80
|
+
for (const root of searchFromRoots) {
|
|
81
|
+
const hit = walkUp(root, candidates);
|
|
82
|
+
if (hit)
|
|
83
|
+
return hit;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function walkUp(start, candidates) {
|
|
88
|
+
let dir = start;
|
|
89
|
+
for (let i = 0; i < 12; i++) {
|
|
90
|
+
for (const rel of candidates) {
|
|
91
|
+
const guess = join(dir, rel);
|
|
92
|
+
if (existsSync(join(guess, "SKILL.md")))
|
|
93
|
+
return guess;
|
|
94
|
+
}
|
|
95
|
+
const parent = dirname(dir);
|
|
96
|
+
if (parent === dir)
|
|
97
|
+
return null;
|
|
98
|
+
dir = parent;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../../src/agents/detect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Normalized event stream used by the TUI to render agent progress.
|
|
2
|
+
//
|
|
3
|
+
// Both Claude (`--output-format stream-json --verbose`) and Codex (`--json`)
|
|
4
|
+
// emit newline-delimited JSON. Their schemas differ; per-CLI parsers in this
|
|
5
|
+
// directory convert each line into one of the AgentEvent variants below so
|
|
6
|
+
// the PhaseRunner UI is agent-agnostic.
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/agents/events.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,2EAA2E;AAC3E,wCAAwC"}
|