@markbrutx/promptbook-cli 0.2.0 → 0.4.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 +27 -8
- package/dist/src/args.d.ts +5 -1
- package/dist/src/args.d.ts.map +1 -1
- package/dist/src/args.js +4 -0
- package/dist/src/args.js.map +1 -1
- package/dist/src/commands/bundle.d.ts +25 -5
- package/dist/src/commands/bundle.d.ts.map +1 -1
- package/dist/src/commands/bundle.js +135 -16
- package/dist/src/commands/bundle.js.map +1 -1
- package/dist/src/commands/watch.d.ts +19 -0
- package/dist/src/commands/watch.d.ts.map +1 -0
- package/dist/src/commands/watch.js +193 -0
- package/dist/src/commands/watch.js.map +1 -0
- package/dist/src/config.d.ts +29 -12
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +52 -32
- package/dist/src/config.js.map +1 -1
- package/dist/src/run.d.ts.map +1 -1
- package/dist/src/run.js +10 -4
- package/dist/src/run.js.map +1 -1
- package/package.json +5 -4
- package/src/args.ts +9 -1
- package/src/commands/bundle.ts +163 -17
- package/src/commands/watch.ts +221 -0
- package/src/config.ts +59 -35
- package/src/run.ts +10 -4
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { relative, sep } from "node:path";
|
|
2
|
+
import { watch as chokidarWatch } from "chokidar";
|
|
3
|
+
import type { ParsedArgs } from "../args.js";
|
|
4
|
+
import { requirePromptsDir } from "../config.js";
|
|
5
|
+
import { colorEnabled, type IO } from "../io.js";
|
|
6
|
+
import { makeStyle, type Style } from "../style.js";
|
|
7
|
+
import { type Book, loadWorkspace } from "../workspace.js";
|
|
8
|
+
import { bundleOne } from "./bundle.js";
|
|
9
|
+
|
|
10
|
+
/** Folders inside a book whose edits warrant a rebuild (mirrors core's loader layout). */
|
|
11
|
+
const BOOK_DIRS = ["fragments", "rules", "code-prompts"];
|
|
12
|
+
|
|
13
|
+
/** Single-book root files we re-bundle on touch (config edits change the assembly). */
|
|
14
|
+
const BOOK_FILES = ["promptbook.json"];
|
|
15
|
+
|
|
16
|
+
/** Debounce window per book: a burst of edits collapses into one rebuild. */
|
|
17
|
+
const DEBOUNCE_MS = 250;
|
|
18
|
+
|
|
19
|
+
interface RebuildStats {
|
|
20
|
+
bytes: number;
|
|
21
|
+
ms: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Local-time clock prefix (`hours:minutes:seconds`); slices off Date#toTimeString's TZ tail. */
|
|
25
|
+
function clock(): string {
|
|
26
|
+
return new Date().toTimeString().slice(0, 8);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when `event` happened inside one of the book's watched folders (or is a root file). */
|
|
30
|
+
function eventInBook(book: Book, eventPath: string): boolean {
|
|
31
|
+
const rel = relative(book.dir, eventPath);
|
|
32
|
+
if (rel === "" || rel.startsWith("..")) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const parts = rel.split(sep);
|
|
36
|
+
if (parts.length === 1) {
|
|
37
|
+
return BOOK_FILES.includes(parts[0] as string);
|
|
38
|
+
}
|
|
39
|
+
return BOOK_DIRS.includes(parts[0] as string);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Ignore artifacts, tests, fixtures, and everything VCS / dep manager touches. */
|
|
43
|
+
function shouldIgnore(eventPath: string): boolean {
|
|
44
|
+
const lower = eventPath.toLowerCase();
|
|
45
|
+
if (lower.endsWith("book.generated.ts")) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (lower.endsWith(".test.ts") || lower.endsWith(".test.js")) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const segments = eventPath.split(sep);
|
|
52
|
+
for (const segment of segments) {
|
|
53
|
+
if (segment === "node_modules" || segment === ".git" || segment === "fixtures") {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Wrap `io` so `bundleOne`'s `wrote <path>` chatter is dropped and the artifact size is captured. */
|
|
61
|
+
function captureBytes(io: IO): { sink: IO; bytes: () => number } {
|
|
62
|
+
let bytes = 0;
|
|
63
|
+
const sink: IO = {
|
|
64
|
+
...io,
|
|
65
|
+
stderr(text) {
|
|
66
|
+
if (text.startsWith("wrote ")) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
io.stderr(text);
|
|
70
|
+
},
|
|
71
|
+
async writeFile(path, contents) {
|
|
72
|
+
bytes = contents.length;
|
|
73
|
+
await io.writeFile(path, contents);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
return { sink, bytes: () => bytes };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function emitEvent(
|
|
80
|
+
io: IO,
|
|
81
|
+
args: ParsedArgs,
|
|
82
|
+
style: Style,
|
|
83
|
+
event: "started" | "bundled" | "error" | "stopped",
|
|
84
|
+
payload: Record<string, unknown> = {},
|
|
85
|
+
): void {
|
|
86
|
+
if (args.json) {
|
|
87
|
+
io.stderr(`${JSON.stringify({ event, ts: clock(), ...payload })}\n`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const ts = style.dim(`[${clock()}]`);
|
|
91
|
+
if (event === "started") {
|
|
92
|
+
const books = payload.books as string[];
|
|
93
|
+
io.stderr(`${ts} watching ${books.length} book(s): ${books.join(", ")}\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (event === "bundled") {
|
|
97
|
+
io.stderr(`${ts} ${payload.book} bundled (${payload.bytes} B, ${payload.ms}ms)\n`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (event === "stopped") {
|
|
101
|
+
io.stderr("stopped\n");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
io.stderr(`${ts} ${payload.book} ${style.red("ERROR")}: ${payload.message}\n`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function rebuild(io: IO, args: ParsedArgs, book: Book): Promise<RebuildStats | Error> {
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
const capture = captureBytes(io);
|
|
110
|
+
try {
|
|
111
|
+
const code = await bundleOne(capture.sink, args, book.dir, book.name, true);
|
|
112
|
+
if (code !== 0) {
|
|
113
|
+
return new Error(`bundle exited with code ${code}`);
|
|
114
|
+
}
|
|
115
|
+
return { bytes: capture.bytes(), ms: Date.now() - start };
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return error as Error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Run one rebuild and emit its bundled/error event. */
|
|
122
|
+
async function rebuildAndReport(io: IO, args: ParsedArgs, style: Style, book: Book): Promise<void> {
|
|
123
|
+
const result = await rebuild(io, args, book);
|
|
124
|
+
if (result instanceof Error) {
|
|
125
|
+
emitEvent(io, args, style, "error", { book: book.name, message: result.message });
|
|
126
|
+
} else {
|
|
127
|
+
emitEvent(io, args, style, "bundled", { book: book.name, bytes: result.bytes, ms: result.ms });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `watch [<dir>]`: rebuild `book.generated.ts` whenever fragments, rules,
|
|
133
|
+
* compositions, code-prompts or `promptbook.json` change. Streams one short
|
|
134
|
+
* line per rebuild to stderr (`[clock] <book> bundled (<bytes> B, <ms>ms)`);
|
|
135
|
+
* stdout stays empty (the contract: stdout = payload, watch has no payload).
|
|
136
|
+
*
|
|
137
|
+
* Discovers every book under the prompts folder and rebuilds each once on
|
|
138
|
+
* startup, then debounces per-book events with a 250 ms window so a burst of
|
|
139
|
+
* edits collapses into one rebuild. SIGINT / SIGTERM closes the watcher and
|
|
140
|
+
* exits 0. Honors `--plain`, `--exclude-code-prompts`, `--json`, and the
|
|
141
|
+
* config / `--dir` resolution chain.
|
|
142
|
+
*
|
|
143
|
+
* `--out <file>` only works in a single-book workspace; the multi-book mode
|
|
144
|
+
* always writes each book's artifact next to its sources.
|
|
145
|
+
*/
|
|
146
|
+
export async function cmdWatch(args: ParsedArgs, io: IO): Promise<number> {
|
|
147
|
+
if (args.check) {
|
|
148
|
+
io.stderr("error: --check is not supported by watch (run `bundle --check --all` from CI)\n");
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const promptsDir = await requirePromptsDir(io, args.operands[0] ?? args.dir);
|
|
153
|
+
if (promptsDir === null) {
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const workspace = await loadWorkspace(io, promptsDir);
|
|
158
|
+
if (workspace.books.length === 0) {
|
|
159
|
+
io.stderr(`error: no books found under ${promptsDir}\n`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
if (args.out !== undefined && workspace.books.length > 1) {
|
|
163
|
+
io.stderr("error: --out requires a single book; drop --out to write each book's book.generated.ts\n");
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const style = makeStyle(colorEnabled(io));
|
|
168
|
+
emitEvent(io, args, style, "started", { books: workspace.books.map((b) => b.name) });
|
|
169
|
+
|
|
170
|
+
// Initial pass runs in parallel: each book writes its own artifact, no contention.
|
|
171
|
+
await Promise.all(workspace.books.map((book) => rebuildAndReport(io, args, style, book)));
|
|
172
|
+
|
|
173
|
+
const watcher = chokidarWatch(promptsDir, {
|
|
174
|
+
ignoreInitial: true,
|
|
175
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
|
|
176
|
+
ignored: (eventPath) => shouldIgnore(eventPath),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const timers = new Map<string, NodeJS.Timeout>();
|
|
180
|
+
const scheduleRebuild = (book: Book): void => {
|
|
181
|
+
const existing = timers.get(book.name);
|
|
182
|
+
if (existing !== undefined) {
|
|
183
|
+
clearTimeout(existing);
|
|
184
|
+
}
|
|
185
|
+
const timer = setTimeout(() => {
|
|
186
|
+
timers.delete(book.name);
|
|
187
|
+
void rebuildAndReport(io, args, style, book);
|
|
188
|
+
}, DEBOUNCE_MS);
|
|
189
|
+
timers.set(book.name, timer);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
watcher.on("all", (_event, eventPath) => {
|
|
193
|
+
const book = workspace.books.find((b) => eventInBook(b, eventPath));
|
|
194
|
+
if (book === undefined) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
scheduleRebuild(book);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Hold the promise open until SIGINT / SIGTERM closes the watcher.
|
|
201
|
+
return new Promise<number>((resolveDone) => {
|
|
202
|
+
let stopped = false;
|
|
203
|
+
const stop = async (): Promise<void> => {
|
|
204
|
+
if (stopped) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
stopped = true;
|
|
208
|
+
for (const timer of timers.values()) {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
}
|
|
211
|
+
timers.clear();
|
|
212
|
+
await watcher.close();
|
|
213
|
+
emitEvent(io, args, style, "stopped");
|
|
214
|
+
process.off("SIGINT", stop);
|
|
215
|
+
process.off("SIGTERM", stop);
|
|
216
|
+
resolveDone(0);
|
|
217
|
+
};
|
|
218
|
+
process.once("SIGINT", stop);
|
|
219
|
+
process.once("SIGTERM", stop);
|
|
220
|
+
});
|
|
221
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve as resolvePath } from "node:path";
|
|
1
|
+
import { dirname, resolve as resolvePath } from "node:path";
|
|
2
2
|
import type { Context, ContextValue } from "@markbrutx/promptbook-core";
|
|
3
3
|
import type { IO } from "./io.js";
|
|
4
4
|
|
|
@@ -90,6 +90,14 @@ interface PromptbookConfig {
|
|
|
90
90
|
eval?: unknown;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/** A loaded `promptbook.json`: parsed data plus the directory that held it. */
|
|
94
|
+
export interface LoadedConfig {
|
|
95
|
+
/** Parsed JSON object (or `{}` when no file was found or parsing failed). */
|
|
96
|
+
data: PromptbookConfig;
|
|
97
|
+
/** Absolute directory where the config was found; `undefined` if nothing matched up the tree. */
|
|
98
|
+
dir?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
/** lint options sourced from the `lint` section of `promptbook.json`. */
|
|
94
102
|
export interface LintConfig {
|
|
95
103
|
maxTokens?: number;
|
|
@@ -97,30 +105,45 @@ export interface LintConfig {
|
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
108
|
+
* Walk up from `io.cwd()` to find the first `promptbook.json`, parse it, and
|
|
109
|
+
* return its data + the directory it lived in. Walking up (rather than only
|
|
110
|
+
* checking cwd) is what makes `promptbook` work like `git`/`biome`/`eslint`:
|
|
111
|
+
* one config at the repo root reaches every subfolder. Path-valued keys
|
|
112
|
+
* (currently just `promptsDir`) are resolved relative to {@link LoadedConfig.dir}
|
|
113
|
+
* by {@link resolvePromptsDir}, not relative to wherever the shell happens to
|
|
114
|
+
* be — so `pnpm exec` snapping cwd to a workspace package cannot break the
|
|
115
|
+
* lookup. Missing, unreadable or malformed files yield an empty config
|
|
116
|
+
* (best-effort), so callers can layer flags on top.
|
|
104
117
|
*/
|
|
105
|
-
export async function loadConfig(io: IO): Promise<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
raw
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
export async function loadConfig(io: IO): Promise<LoadedConfig> {
|
|
119
|
+
let dir = resolvePath(io.cwd());
|
|
120
|
+
for (;;) {
|
|
121
|
+
const configPath = resolvePath(dir, "promptbook.json");
|
|
122
|
+
let raw: string | undefined;
|
|
123
|
+
try {
|
|
124
|
+
raw = await io.fs.readFile(configPath);
|
|
125
|
+
} catch {
|
|
126
|
+
// not found at this level; try the parent
|
|
127
|
+
}
|
|
128
|
+
if (raw !== undefined) {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
131
|
+
return { data: isJsonObject(parsed) ? parsed : {}, dir };
|
|
132
|
+
} catch {
|
|
133
|
+
return { data: {}, dir };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const parent = dirname(dir);
|
|
137
|
+
if (parent === dir) {
|
|
138
|
+
return { data: {} };
|
|
139
|
+
}
|
|
140
|
+
dir = parent;
|
|
118
141
|
}
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
/** Extract the `lint` section from an already-loaded config. */
|
|
122
|
-
export function lintConfigFrom(
|
|
123
|
-
const section =
|
|
145
|
+
export function lintConfigFrom(loaded: LoadedConfig): LintConfig {
|
|
146
|
+
const section = loaded.data.lint;
|
|
124
147
|
if (!isJsonObject(section)) {
|
|
125
148
|
return {};
|
|
126
149
|
}
|
|
@@ -146,8 +169,8 @@ export interface EvalConfig {
|
|
|
146
169
|
}
|
|
147
170
|
|
|
148
171
|
/** Extract the `eval` section from an already-loaded config. */
|
|
149
|
-
export function evalConfigFrom(
|
|
150
|
-
const section =
|
|
172
|
+
export function evalConfigFrom(loaded: LoadedConfig): EvalConfig {
|
|
173
|
+
const section = loaded.data.eval;
|
|
151
174
|
if (!isJsonObject(section)) {
|
|
152
175
|
return {};
|
|
153
176
|
}
|
|
@@ -162,21 +185,22 @@ export function evalConfigFrom(config: PromptbookConfig): EvalConfig {
|
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
/**
|
|
165
|
-
* Resolve the prompts folder by priority:
|
|
166
|
-
*
|
|
167
|
-
*
|
|
188
|
+
* Resolve the prompts folder by priority:
|
|
189
|
+
* 1. `--dir <path>` — relative to **cwd** (explicit per-invocation override).
|
|
190
|
+
* 2. `promptbook.json` `promptsDir` — relative to **the config file's directory**
|
|
191
|
+
* (so the value can stay stable while the user shells around in subfolders).
|
|
192
|
+
* 3. `./prompts` — relative to cwd (back-compat default when no config exists).
|
|
193
|
+
*
|
|
194
|
+
* All results are absolute. Pass a preloaded {@link LoadedConfig} to reuse a
|
|
195
|
+
* single read; otherwise it is loaded here.
|
|
168
196
|
*/
|
|
169
|
-
export async function resolvePromptsDir(
|
|
170
|
-
io: IO,
|
|
171
|
-
dirFlag?: string,
|
|
172
|
-
config?: PromptbookConfig,
|
|
173
|
-
): Promise<string> {
|
|
197
|
+
export async function resolvePromptsDir(io: IO, dirFlag?: string, loaded?: LoadedConfig): Promise<string> {
|
|
174
198
|
if (dirFlag !== undefined) {
|
|
175
199
|
return resolvePath(io.cwd(), dirFlag);
|
|
176
200
|
}
|
|
177
|
-
const resolved =
|
|
178
|
-
if (typeof resolved.promptsDir === "string") {
|
|
179
|
-
return resolvePath(
|
|
201
|
+
const resolved = loaded ?? (await loadConfig(io));
|
|
202
|
+
if (typeof resolved.data.promptsDir === "string" && resolved.dir !== undefined) {
|
|
203
|
+
return resolvePath(resolved.dir, resolved.data.promptsDir);
|
|
180
204
|
}
|
|
181
205
|
return resolvePath(io.cwd(), "prompts");
|
|
182
206
|
}
|
|
@@ -198,9 +222,9 @@ async function dirExists(io: IO, dir: string): Promise<boolean> {
|
|
|
198
222
|
export async function requirePromptsDir(
|
|
199
223
|
io: IO,
|
|
200
224
|
dirFlag?: string,
|
|
201
|
-
|
|
225
|
+
loaded?: LoadedConfig,
|
|
202
226
|
): Promise<string | null> {
|
|
203
|
-
const promptsDir = await resolvePromptsDir(io, dirFlag,
|
|
227
|
+
const promptsDir = await resolvePromptsDir(io, dirFlag, loaded);
|
|
204
228
|
if (!(await dirExists(io, promptsDir))) {
|
|
205
229
|
io.stderr(`error: prompts folder not found: ${promptsDir}\n`);
|
|
206
230
|
return null;
|
package/src/run.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { cmdLint } from "./commands/lint.js";
|
|
|
8
8
|
import { cmdLs } from "./commands/ls.js";
|
|
9
9
|
import { cmdResolve } from "./commands/resolve.js";
|
|
10
10
|
import { cmdView } from "./commands/view.js";
|
|
11
|
+
import { cmdWatch } from "./commands/watch.js";
|
|
11
12
|
import { defaultIO, type IO } from "./io.js";
|
|
12
13
|
|
|
13
14
|
const HELP = `promptbook — compose prompts from reusable fragments
|
|
@@ -19,7 +20,8 @@ Commands:
|
|
|
19
20
|
resolve [<book>/]<prompt> Assemble a prompt and print it to stdout (--all: every book)
|
|
20
21
|
lint [<prompt>] Run static checks; with no prompt, book rules only
|
|
21
22
|
eval [<name|glob>] Run fixtures through a model adapter, report pass-rate
|
|
22
|
-
bundle [<dir>] Compile a prompts folder into an importable book module
|
|
23
|
+
bundle [<dir>] Compile a prompts folder into an importable book module (--all/--check)
|
|
24
|
+
watch [<dir>] Rebuild book.generated.ts as fragments/rules/compositions change
|
|
23
25
|
view Start the local web viewer over the workspace (book switcher)
|
|
24
26
|
annotations <action> Drain the viewer's feedback queue: list | resolve <id> | clear
|
|
25
27
|
ls List compositions and fragments (--all: cross-book inventory)
|
|
@@ -39,13 +41,15 @@ Options:
|
|
|
39
41
|
--samples N eval: default samples per fixture (default 1; a fixture's own samples wins)
|
|
40
42
|
--threshold R eval: a fixture passes when passRate >= R (default 1)
|
|
41
43
|
--lint eval: run a static lint gate over every variant first
|
|
42
|
-
-o, --out <file> bundle: write
|
|
43
|
-
--plain bundle: emit a plain module (no type-only import; e.g. for Deno)
|
|
44
|
+
-o, --out <file> bundle/watch: write to a file (default: stdout for bundle, <bookDir>/book.generated.ts for watch/--all)
|
|
45
|
+
--plain bundle/watch: emit a plain module (no type-only import; e.g. for Deno)
|
|
46
|
+
--check bundle: compare with the existing output; exit 1 on drift or missing artifact
|
|
47
|
+
--exclude-code-prompts bundle/watch: serialize code-prompts as an empty map (runtime-lean bundle)
|
|
44
48
|
--port N view: port for the viewer server (default: a free port)
|
|
45
49
|
--no-open view: do not open the browser after starting
|
|
46
50
|
--fragments ls: list fragments only
|
|
47
51
|
--compositions ls: list compositions only
|
|
48
|
-
--all ls/resolve: span every book in the workspace
|
|
52
|
+
--all ls/resolve/bundle: span every book in the workspace
|
|
49
53
|
-h, --help Show this help
|
|
50
54
|
-v, --version Show the version
|
|
51
55
|
|
|
@@ -98,6 +102,8 @@ export async function run(argv: string[], io: IO = defaultIO()): Promise<number>
|
|
|
98
102
|
return cmdEval(args, io);
|
|
99
103
|
case "bundle":
|
|
100
104
|
return cmdBundle(args, io);
|
|
105
|
+
case "watch":
|
|
106
|
+
return cmdWatch(args, io);
|
|
101
107
|
case "view":
|
|
102
108
|
return cmdView(args, io);
|
|
103
109
|
case "annotations":
|