@markbrutx/promptbook-cli 0.3.0 → 0.4.1
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 +22 -6
- package/dist/bin/promptbook.js +0 -0
- 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/run.d.ts.map +1 -1
- package/dist/src/run.js +11 -5
- 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/run.ts +11 -5
package/README.md
CHANGED
|
@@ -10,15 +10,31 @@ npm i -D @markbrutx/promptbook-cli
|
|
|
10
10
|
## Commands
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
promptbook resolve <name> --explain
|
|
14
|
-
promptbook ls
|
|
15
|
-
promptbook lint <name>
|
|
16
|
-
promptbook eval --case <fixture>
|
|
17
|
-
promptbook bundle -o book.generated.ts
|
|
18
|
-
promptbook
|
|
13
|
+
promptbook resolve <name> --explain # assemble a prompt; print the explain trace
|
|
14
|
+
promptbook ls # list fragments / compositions / code-prompts
|
|
15
|
+
promptbook lint <name> # static checks: token budget, banned tokens, dead rules…
|
|
16
|
+
promptbook eval --case <fixture> # run fixtures against a model adapter → pass-rate
|
|
17
|
+
promptbook bundle -o book.generated.ts # serialize a prompts folder to a portable module
|
|
18
|
+
promptbook bundle --all # rebundle every book in a workspace (one artifact per book)
|
|
19
|
+
promptbook bundle --check [--all] # CI gate: exit 1 when book.generated.ts is stale or missing
|
|
20
|
+
promptbook bundle --exclude-code-prompts # serialize code-prompts as an empty map (runtime-lean bundle)
|
|
21
|
+
promptbook watch # rebundle book.generated.ts on every fragment/rule edit
|
|
22
|
+
promptbook view # open the viewer (needs @markbrutx/promptbook-viewer)
|
|
19
23
|
promptbook annotations list|resolve|clear
|
|
20
24
|
```
|
|
21
25
|
|
|
26
|
+
Dev loop:
|
|
27
|
+
|
|
28
|
+
- `promptbook watch` keeps `book.generated.ts` in sync while you edit
|
|
29
|
+
`fragments/`, `rules/`, `code-prompts/` or `promptbook.json`
|
|
30
|
+
(one rebundle per book, debounced 250 ms; multi-book workspaces rebundle in
|
|
31
|
+
parallel).
|
|
32
|
+
- `promptbook bundle --check --all` is the CI gate: it exits non-zero when any
|
|
33
|
+
book's checked-in `book.generated.ts` drifts from the prompts folder, so
|
|
34
|
+
`book.generated.ts` and the source files cannot fall out of sync silently.
|
|
35
|
+
- `promptbook bundle --exclude-code-prompts` ships a runtime-lean bundle while
|
|
36
|
+
keeping `code-prompts/` on disk as metadata for `ls` / the viewer.
|
|
37
|
+
|
|
22
38
|
The prompts folder is resolved from `--dir`, then `promptbook.json` (the
|
|
23
39
|
nearest one found by walking up from the current directory — same model as
|
|
24
40
|
`git`/`biome`/`eslint`; the `promptsDir` value is taken relative to wherever
|
package/dist/bin/promptbook.js
CHANGED
|
File without changes
|
package/dist/src/args.d.ts
CHANGED
|
@@ -18,8 +18,12 @@ export interface ParsedArgs {
|
|
|
18
18
|
contextFile?: string;
|
|
19
19
|
fragments: boolean;
|
|
20
20
|
compositions: boolean;
|
|
21
|
-
/** ls/resolve: operate across every book in the workspace. */
|
|
21
|
+
/** ls/resolve/bundle: operate across every book in the workspace. */
|
|
22
22
|
all: boolean;
|
|
23
|
+
/** bundle: compare the generated output with the existing artifact and exit non-zero on drift. */
|
|
24
|
+
check: boolean;
|
|
25
|
+
/** bundle: serialize without code-prompts so a runtime bundle stays lean. */
|
|
26
|
+
excludeCodePrompts: boolean;
|
|
23
27
|
/** lint: estimated token ceiling for the token-budget rule. */
|
|
24
28
|
maxTokens?: number;
|
|
25
29
|
/** lint: treat warnings as failures for the exit code. */
|
package/dist/src/args.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/args.ts"],"names":[],"mappings":"AAEA,6EAA6E;AAC7E,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,gEAAgE;IAChE,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/args.ts"],"names":[],"mappings":"AAEA,6EAA6E;AAC7E,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,gEAAgE;IAChE,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,qEAAqE;IACrE,GAAG,EAAE,OAAO,CAAC;IACb,kGAAkG;IAClG,KAAK,EAAE,OAAO,CAAC;IACf,6EAA6E;IAC7E,kBAAkB,EAAE,OAAO,CAAC;IAC5B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,IAAI,EAAE,OAAO,CAAC;IACd,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,MAAM,EAAE,OAAO,CAAC;CACjB;AAgCD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA4EvD"}
|
package/dist/src/args.js
CHANGED
|
@@ -39,6 +39,8 @@ export function parseCliArgs(argv) {
|
|
|
39
39
|
fragments: { type: "boolean" },
|
|
40
40
|
compositions: { type: "boolean" },
|
|
41
41
|
all: { type: "boolean" },
|
|
42
|
+
check: { type: "boolean" },
|
|
43
|
+
"exclude-code-prompts": { type: "boolean" },
|
|
42
44
|
"max-tokens": { type: "string" },
|
|
43
45
|
strict: { type: "boolean" },
|
|
44
46
|
model: { type: "string" },
|
|
@@ -85,6 +87,8 @@ export function parseCliArgs(argv) {
|
|
|
85
87
|
fragments: values.fragments ?? false,
|
|
86
88
|
compositions: values.compositions ?? false,
|
|
87
89
|
all: values.all ?? false,
|
|
90
|
+
check: values.check ?? false,
|
|
91
|
+
excludeCodePrompts: values["exclude-code-prompts"] ?? false,
|
|
88
92
|
maxTokens,
|
|
89
93
|
strict: values.strict ?? false,
|
|
90
94
|
model: values.model,
|
package/dist/src/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/args.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import type { ParsedArgs } from "../args.js";
|
|
2
2
|
import { type IO } from "../io.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Bundle one book: load → relativize → optionally drop code-prompts → serialize.
|
|
5
|
+
* Then either `--check` against the existing artifact, write to `--out`/the
|
|
6
|
+
* default `book.generated.ts`, or print to stdout (single-book, no `--out`).
|
|
7
|
+
*
|
|
8
|
+
* Returns the exit code: 0 on success / up-to-date, 1 on drift / write failure.
|
|
9
|
+
* `bookName` is used in `--check` output and warnings; when `inWorkspace` is
|
|
10
|
+
* true the artifact is always written (not printed), matching `bundle --all`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function bundleOne(io: IO, args: ParsedArgs, promptsDir: string, bookName: string, inWorkspace: boolean): Promise<number>;
|
|
13
|
+
/**
|
|
14
|
+
* `bundle [<dir>]`: load a prompts folder and emit it as an importable module
|
|
15
|
+
* exporting `book: PromptBook`. Writes to stdout, or to a file with `-o`.
|
|
16
|
+
*
|
|
17
|
+
* Flags:
|
|
18
|
+
* - `--json` emits the structured dump instead of the TypeScript module.
|
|
19
|
+
* - `--plain` drops the type-only import (e.g. for Deno consumers).
|
|
20
|
+
* - `--exclude-code-prompts` serializes code-prompts as an empty map so the
|
|
21
|
+
* runtime bundle stays lean while `code-prompts/` keeps living on disk as
|
|
22
|
+
* metadata for `ls` / the viewer.
|
|
23
|
+
* - `--check` compares the would-be output against the existing artifact
|
|
24
|
+
* (`book.generated.ts` next to the prompts folder, or `--out`); exits 1 on
|
|
25
|
+
* drift or a missing artifact and prints a short hint on stderr.
|
|
26
|
+
* - `--all` walks every book in the workspace and writes each to its own
|
|
27
|
+
* `book.generated.ts`; incompatible with `-o`.
|
|
8
28
|
*
|
|
9
29
|
* The folder comes from the positional `<dir>`, else `--dir`, else config /
|
|
10
|
-
* `./prompts`
|
|
30
|
+
* `./prompts`. `--all` does not accept a single-book `<dir>` operand differently.
|
|
11
31
|
*/
|
|
12
32
|
export declare function cmdBundle(args: ParsedArgs, io: IO): Promise<number>;
|
|
13
33
|
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/commands/bundle.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAgB,KAAK,EAAE,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/commands/bundle.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAgB,KAAK,EAAE,EAAE,MAAM,UAAU,CAAC;AAqHjD;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAC7B,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,OAAO,GACnB,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAeD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBzE"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve as resolvePath, sep } from "node:path";
|
|
1
|
+
import { basename, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
|
|
2
2
|
import { loadPrompts, serializeBook, serializeBookJson } from "@markbrutx/promptbook-core";
|
|
3
3
|
import { requirePromptsDir } from "../config.js";
|
|
4
4
|
import { emitWarnings } from "../io.js";
|
|
5
|
+
import { loadWorkspace } from "../workspace.js";
|
|
6
|
+
/** Default name of the artifact file each book emits alongside its sources. */
|
|
7
|
+
const BUNDLE_FILE = "book.generated.ts";
|
|
5
8
|
/** Rewrite an absolute source path to a portable, forward-slash path relative to `dir`. */
|
|
6
9
|
function relativizeSource(sourceFile, dir) {
|
|
7
10
|
if (!isAbsolute(sourceFile)) {
|
|
@@ -26,28 +29,101 @@ function portableBook(book, dir) {
|
|
|
26
29
|
warnings: book.warnings,
|
|
27
30
|
};
|
|
28
31
|
}
|
|
32
|
+
/** Return the book with `codePrompts` cleared — used by `--exclude-code-prompts`. */
|
|
33
|
+
function withoutCodePrompts(book) {
|
|
34
|
+
return { ...book, codePrompts: new Map() };
|
|
35
|
+
}
|
|
36
|
+
/** Normalize EOL so a CRLF-checked-in artifact does not falsely look stale on an LF rebuild. */
|
|
37
|
+
function normalizeEol(text) {
|
|
38
|
+
return text.replace(/\r\n/g, "\n");
|
|
39
|
+
}
|
|
40
|
+
/** First 1-based line index where two strings differ; null when equal. */
|
|
41
|
+
function firstDiffLine(actual, expected) {
|
|
42
|
+
if (actual === expected) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const a = actual.split("\n");
|
|
46
|
+
const b = expected.split("\n");
|
|
47
|
+
const max = Math.max(a.length, b.length);
|
|
48
|
+
for (let i = 0; i < max; i += 1) {
|
|
49
|
+
if (a[i] !== b[i]) {
|
|
50
|
+
return i + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Unreachable: if the strings differ, at least one index in [0, max) must too.
|
|
54
|
+
throw new Error("firstDiffLine: strings differ but no line diff found");
|
|
55
|
+
}
|
|
56
|
+
/** Build the output text for one book (TypeScript module or JSON). */
|
|
57
|
+
function renderOutput(book, args) {
|
|
58
|
+
return args.json ? serializeBookJson(book) : serializeBook(book, { typed: !args.plain });
|
|
59
|
+
}
|
|
60
|
+
/** Pick the artifact path: explicit `--out`, else `book.generated.ts` next to the prompts folder. */
|
|
61
|
+
function targetPath(io, args, promptsDir) {
|
|
62
|
+
if (args.out !== undefined) {
|
|
63
|
+
return resolvePath(io.cwd(), args.out);
|
|
64
|
+
}
|
|
65
|
+
return join(promptsDir, BUNDLE_FILE);
|
|
66
|
+
}
|
|
67
|
+
async function checkOne(io, output, path) {
|
|
68
|
+
let existing;
|
|
69
|
+
try {
|
|
70
|
+
existing = await io.fs.readFile(path);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { status: "stale", reason: "missing", path };
|
|
74
|
+
}
|
|
75
|
+
const line = firstDiffLine(normalizeEol(output), normalizeEol(existing));
|
|
76
|
+
if (line === null) {
|
|
77
|
+
return { status: "up-to-date", path };
|
|
78
|
+
}
|
|
79
|
+
return { status: "stale", reason: "diff", firstDiffLine: line, path };
|
|
80
|
+
}
|
|
81
|
+
function emitCheckResult(io, args, bookName, outcome) {
|
|
82
|
+
if (args.json) {
|
|
83
|
+
const diff = outcome.status === "stale"
|
|
84
|
+
? outcome.reason === "missing"
|
|
85
|
+
? { reason: "missing", path: outcome.path }
|
|
86
|
+
: { reason: "diff", firstDiffLine: outcome.firstDiffLine }
|
|
87
|
+
: null;
|
|
88
|
+
io.stderr(`${JSON.stringify({ book: bookName, status: outcome.status, diff })}\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (outcome.status === "up-to-date") {
|
|
92
|
+
io.stderr(`${bookName} up to date\n`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (outcome.reason === "missing") {
|
|
96
|
+
io.stderr(`${bookName} stale (missing ${outcome.path})\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
io.stderr(`${bookName} stale (first diff at line ${outcome.firstDiffLine})\n`);
|
|
100
|
+
}
|
|
29
101
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* emits a structured dump instead of the TypeScript module.
|
|
102
|
+
* Bundle one book: load → relativize → optionally drop code-prompts → serialize.
|
|
103
|
+
* Then either `--check` against the existing artifact, write to `--out`/the
|
|
104
|
+
* default `book.generated.ts`, or print to stdout (single-book, no `--out`).
|
|
34
105
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
106
|
+
* Returns the exit code: 0 on success / up-to-date, 1 on drift / write failure.
|
|
107
|
+
* `bookName` is used in `--check` output and warnings; when `inWorkspace` is
|
|
108
|
+
* true the artifact is always written (not printed), matching `bundle --all`.
|
|
37
109
|
*/
|
|
38
|
-
export async function
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const book = portableBook(await loadPrompts(promptsDir, io.fs), promptsDir);
|
|
110
|
+
export async function bundleOne(io, args, promptsDir, bookName, inWorkspace) {
|
|
111
|
+
const loaded = await loadPrompts(promptsDir, io.fs);
|
|
112
|
+
// `--exclude-code-prompts` discards them outright; skipping the relativize pass is the whole point.
|
|
113
|
+
const source = args.excludeCodePrompts ? withoutCodePrompts(loaded) : loaded;
|
|
114
|
+
const book = portableBook(source, promptsDir);
|
|
44
115
|
emitWarnings(io, book.warnings);
|
|
45
|
-
const output =
|
|
46
|
-
if (args.
|
|
116
|
+
const output = renderOutput(book, args);
|
|
117
|
+
if (args.check) {
|
|
118
|
+
const outcome = await checkOne(io, output, targetPath(io, args, promptsDir));
|
|
119
|
+
emitCheckResult(io, args, bookName, outcome);
|
|
120
|
+
return outcome.status === "up-to-date" ? 0 : 1;
|
|
121
|
+
}
|
|
122
|
+
if (args.out === undefined && !inWorkspace) {
|
|
47
123
|
io.stdout(output);
|
|
48
124
|
return 0;
|
|
49
125
|
}
|
|
50
|
-
const outPath =
|
|
126
|
+
const outPath = targetPath(io, args, promptsDir);
|
|
51
127
|
try {
|
|
52
128
|
await io.writeFile(outPath, output);
|
|
53
129
|
}
|
|
@@ -58,4 +134,47 @@ export async function cmdBundle(args, io) {
|
|
|
58
134
|
io.stderr(`wrote ${outPath}\n`);
|
|
59
135
|
return 0;
|
|
60
136
|
}
|
|
137
|
+
/** Run `bundleOne` over every book in the workspace in parallel; the worst exit code wins. */
|
|
138
|
+
async function bundleAll(io, args, root) {
|
|
139
|
+
const workspace = await loadWorkspace(io, root);
|
|
140
|
+
if (workspace.books.length === 0) {
|
|
141
|
+
io.stderr(`error: no books found under ${root}\n`);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
const codes = await Promise.all(workspace.books.map((book) => bundleOne(io, args, book.dir, book.name, true)));
|
|
145
|
+
return Math.max(0, ...codes);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `bundle [<dir>]`: load a prompts folder and emit it as an importable module
|
|
149
|
+
* exporting `book: PromptBook`. Writes to stdout, or to a file with `-o`.
|
|
150
|
+
*
|
|
151
|
+
* Flags:
|
|
152
|
+
* - `--json` emits the structured dump instead of the TypeScript module.
|
|
153
|
+
* - `--plain` drops the type-only import (e.g. for Deno consumers).
|
|
154
|
+
* - `--exclude-code-prompts` serializes code-prompts as an empty map so the
|
|
155
|
+
* runtime bundle stays lean while `code-prompts/` keeps living on disk as
|
|
156
|
+
* metadata for `ls` / the viewer.
|
|
157
|
+
* - `--check` compares the would-be output against the existing artifact
|
|
158
|
+
* (`book.generated.ts` next to the prompts folder, or `--out`); exits 1 on
|
|
159
|
+
* drift or a missing artifact and prints a short hint on stderr.
|
|
160
|
+
* - `--all` walks every book in the workspace and writes each to its own
|
|
161
|
+
* `book.generated.ts`; incompatible with `-o`.
|
|
162
|
+
*
|
|
163
|
+
* The folder comes from the positional `<dir>`, else `--dir`, else config /
|
|
164
|
+
* `./prompts`. `--all` does not accept a single-book `<dir>` operand differently.
|
|
165
|
+
*/
|
|
166
|
+
export async function cmdBundle(args, io) {
|
|
167
|
+
if (args.all && args.out !== undefined) {
|
|
168
|
+
io.stderr("error: --all is not compatible with -o/--out\n");
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
const promptsDir = await requirePromptsDir(io, args.operands[0] ?? args.dir);
|
|
172
|
+
if (promptsDir === null) {
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
if (args.all) {
|
|
176
|
+
return bundleAll(io, args, promptsDir);
|
|
177
|
+
}
|
|
178
|
+
return bundleOne(io, args, promptsDir, basename(promptsDir), false);
|
|
179
|
+
}
|
|
61
180
|
//# sourceMappingURL=bundle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../../src/commands/bundle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,IAAI,WAAW,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../../src/commands/bundle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,IAAI,WAAW,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAE9F,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAE3F,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,YAAY,EAAW,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,+EAA+E;AAC/E,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAExC,2FAA2F;AAC3F,SAAS,gBAAgB,CAAC,UAAkB,EAAE,GAAW;IACvD,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,gEAAgE;AAChE,SAAS,aAAa,CAAmC,GAAmB,EAAE,GAAW;IACvF,OAAO,IAAI,GAAG,CACZ,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,gBAAgB,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CACzG,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,IAAgB,EAAE,GAAW;IACjD,OAAO;QACL,SAAS,EAAE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC;QAC7C,YAAY,EAAE,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC;QACnD,WAAW,EAAE,aAAa,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC;QACjD,QAAQ,EAAE,IAAI,CAAC,QAAQ;KACxB,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,SAAS,kBAAkB,CAAC,IAAgB;IAC1C,OAAO,EAAE,GAAG,IAAI,EAAE,WAAW,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;AAC7C,CAAC;AAED,gGAAgG;AAChG,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AACrC,CAAC;AAED,0EAA0E;AAC1E,SAAS,aAAa,CAAC,MAAc,EAAE,QAAgB;IACrD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAChC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAClB,OAAO,CAAC,GAAG,CAAC,CAAC;QACf,CAAC;IACH,CAAC;IACD,+EAA+E;IAC/E,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;AAC1E,CAAC;AAED,sEAAsE;AACtE,SAAS,YAAY,CAAC,IAAgB,EAAE,IAAgB;IACtD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AAC3F,CAAC;AAED,qGAAqG;AACrG,SAAS,UAAU,CAAC,EAAM,EAAE,IAAgB,EAAE,UAAkB;IAC9D,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AACvC,CAAC;AAQD,KAAK,UAAU,QAAQ,CAAC,EAAM,EAAE,MAAc,EAAE,IAAY;IAC1D,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACtD,CAAC;IACD,MAAM,IAAI,GAAG,aAAa,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzE,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACxE,CAAC;AAED,SAAS,eAAe,CAAC,EAAM,EAAE,IAAgB,EAAE,QAAgB,EAAE,OAAqB;IACxF,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,IAAI,GACR,OAAO,CAAC,MAAM,KAAK,OAAO;YACxB,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS;gBAC5B,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE;gBAC3C,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE;YAC5D,CAAC,CAAC,IAAI,CAAC;QACX,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;QACnF,OAAO;IACT,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QACpC,EAAE,CAAC,MAAM,CAAC,GAAG,QAAQ,eAAe,CAAC,CAAC;QACtC,OAAO;IACT,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,EAAE,CAAC,MAAM,CAAC,GAAG,QAAQ,mBAAmB,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IACD,EAAE,CAAC,MAAM,CAAC,GAAG,QAAQ,8BAA8B,OAAO,CAAC,aAAa,KAAK,CAAC,CAAC;AACjF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,EAAM,EACN,IAAgB,EAChB,UAAkB,EAClB,QAAgB,EAChB,WAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IACpD,oGAAoG;IACpG,MAAM,MAAM,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7E,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9C,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAEhC,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAExC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;QAC7E,eAAe,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC7C,OAAO,OAAO,CAAC,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,WAAW,EAAE,CAAC;QAC3C,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAClB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,OAAO,GAAG,UAAU,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,EAAE,CAAC,MAAM,CAAC,wBAAwB,OAAO,MAAO,KAAe,CAAC,OAAO,IAAI,CAAC,CAAC;QAC7E,OAAO,CAAC,CAAC;IACX,CAAC;IACD,EAAE,CAAC,MAAM,CAAC,SAAS,OAAO,IAAI,CAAC,CAAC;IAChC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,8FAA8F;AAC9F,KAAK,UAAU,SAAS,CAAC,EAAM,EAAE,IAAgB,EAAE,IAAY;IAC7D,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAChD,IAAI,SAAS,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,EAAE,CAAC,MAAM,CAAC,+BAA+B,IAAI,IAAI,CAAC,CAAC;QACnD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAC9E,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAgB,EAAE,EAAM;IACtD,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QACvC,EAAE,CAAC,MAAM,CAAC,gDAAgD,CAAC,CAAC;QAC5D,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7E,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,OAAO,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ParsedArgs } from "../args.js";
|
|
2
|
+
import { type IO } from "../io.js";
|
|
3
|
+
/**
|
|
4
|
+
* `watch [<dir>]`: rebuild `book.generated.ts` whenever fragments, rules,
|
|
5
|
+
* compositions, code-prompts or `promptbook.json` change. Streams one short
|
|
6
|
+
* line per rebuild to stderr (`[clock] <book> bundled (<bytes> B, <ms>ms)`);
|
|
7
|
+
* stdout stays empty (the contract: stdout = payload, watch has no payload).
|
|
8
|
+
*
|
|
9
|
+
* Discovers every book under the prompts folder and rebuilds each once on
|
|
10
|
+
* startup, then debounces per-book events with a 250 ms window so a burst of
|
|
11
|
+
* edits collapses into one rebuild. SIGINT / SIGTERM closes the watcher and
|
|
12
|
+
* exits 0. Honors `--plain`, `--exclude-code-prompts`, `--json`, and the
|
|
13
|
+
* config / `--dir` resolution chain.
|
|
14
|
+
*
|
|
15
|
+
* `--out <file>` only works in a single-book workspace; the multi-book mode
|
|
16
|
+
* always writes each book's artifact next to its sources.
|
|
17
|
+
*/
|
|
18
|
+
export declare function cmdWatch(args: ParsedArgs, io: IO): Promise<number>;
|
|
19
|
+
//# sourceMappingURL=watch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../../src/commands/watch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAgB,KAAK,EAAE,EAAE,MAAM,UAAU,CAAC;AA8HjD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2ExE"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { relative, sep } from "node:path";
|
|
2
|
+
import { watch as chokidarWatch } from "chokidar";
|
|
3
|
+
import { requirePromptsDir } from "../config.js";
|
|
4
|
+
import { colorEnabled } from "../io.js";
|
|
5
|
+
import { makeStyle } from "../style.js";
|
|
6
|
+
import { loadWorkspace } from "../workspace.js";
|
|
7
|
+
import { bundleOne } from "./bundle.js";
|
|
8
|
+
/** Folders inside a book whose edits warrant a rebuild (mirrors core's loader layout). */
|
|
9
|
+
const BOOK_DIRS = ["fragments", "rules", "code-prompts"];
|
|
10
|
+
/** Single-book root files we re-bundle on touch (config edits change the assembly). */
|
|
11
|
+
const BOOK_FILES = ["promptbook.json"];
|
|
12
|
+
/** Debounce window per book: a burst of edits collapses into one rebuild. */
|
|
13
|
+
const DEBOUNCE_MS = 250;
|
|
14
|
+
/** Local-time clock prefix (`hours:minutes:seconds`); slices off Date#toTimeString's TZ tail. */
|
|
15
|
+
function clock() {
|
|
16
|
+
return new Date().toTimeString().slice(0, 8);
|
|
17
|
+
}
|
|
18
|
+
/** True when `event` happened inside one of the book's watched folders (or is a root file). */
|
|
19
|
+
function eventInBook(book, eventPath) {
|
|
20
|
+
const rel = relative(book.dir, eventPath);
|
|
21
|
+
if (rel === "" || rel.startsWith("..")) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const parts = rel.split(sep);
|
|
25
|
+
if (parts.length === 1) {
|
|
26
|
+
return BOOK_FILES.includes(parts[0]);
|
|
27
|
+
}
|
|
28
|
+
return BOOK_DIRS.includes(parts[0]);
|
|
29
|
+
}
|
|
30
|
+
/** Ignore artifacts, tests, fixtures, and everything VCS / dep manager touches. */
|
|
31
|
+
function shouldIgnore(eventPath) {
|
|
32
|
+
const lower = eventPath.toLowerCase();
|
|
33
|
+
if (lower.endsWith("book.generated.ts")) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (lower.endsWith(".test.ts") || lower.endsWith(".test.js")) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const segments = eventPath.split(sep);
|
|
40
|
+
for (const segment of segments) {
|
|
41
|
+
if (segment === "node_modules" || segment === ".git" || segment === "fixtures") {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/** Wrap `io` so `bundleOne`'s `wrote <path>` chatter is dropped and the artifact size is captured. */
|
|
48
|
+
function captureBytes(io) {
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
const sink = {
|
|
51
|
+
...io,
|
|
52
|
+
stderr(text) {
|
|
53
|
+
if (text.startsWith("wrote ")) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
io.stderr(text);
|
|
57
|
+
},
|
|
58
|
+
async writeFile(path, contents) {
|
|
59
|
+
bytes = contents.length;
|
|
60
|
+
await io.writeFile(path, contents);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return { sink, bytes: () => bytes };
|
|
64
|
+
}
|
|
65
|
+
function emitEvent(io, args, style, event, payload = {}) {
|
|
66
|
+
if (args.json) {
|
|
67
|
+
io.stderr(`${JSON.stringify({ event, ts: clock(), ...payload })}\n`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const ts = style.dim(`[${clock()}]`);
|
|
71
|
+
if (event === "started") {
|
|
72
|
+
const books = payload.books;
|
|
73
|
+
io.stderr(`${ts} watching ${books.length} book(s): ${books.join(", ")}\n`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (event === "bundled") {
|
|
77
|
+
io.stderr(`${ts} ${payload.book} bundled (${payload.bytes} B, ${payload.ms}ms)\n`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (event === "stopped") {
|
|
81
|
+
io.stderr("stopped\n");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
io.stderr(`${ts} ${payload.book} ${style.red("ERROR")}: ${payload.message}\n`);
|
|
85
|
+
}
|
|
86
|
+
async function rebuild(io, args, book) {
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
const capture = captureBytes(io);
|
|
89
|
+
try {
|
|
90
|
+
const code = await bundleOne(capture.sink, args, book.dir, book.name, true);
|
|
91
|
+
if (code !== 0) {
|
|
92
|
+
return new Error(`bundle exited with code ${code}`);
|
|
93
|
+
}
|
|
94
|
+
return { bytes: capture.bytes(), ms: Date.now() - start };
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Run one rebuild and emit its bundled/error event. */
|
|
101
|
+
async function rebuildAndReport(io, args, style, book) {
|
|
102
|
+
const result = await rebuild(io, args, book);
|
|
103
|
+
if (result instanceof Error) {
|
|
104
|
+
emitEvent(io, args, style, "error", { book: book.name, message: result.message });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
emitEvent(io, args, style, "bundled", { book: book.name, bytes: result.bytes, ms: result.ms });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `watch [<dir>]`: rebuild `book.generated.ts` whenever fragments, rules,
|
|
112
|
+
* compositions, code-prompts or `promptbook.json` change. Streams one short
|
|
113
|
+
* line per rebuild to stderr (`[clock] <book> bundled (<bytes> B, <ms>ms)`);
|
|
114
|
+
* stdout stays empty (the contract: stdout = payload, watch has no payload).
|
|
115
|
+
*
|
|
116
|
+
* Discovers every book under the prompts folder and rebuilds each once on
|
|
117
|
+
* startup, then debounces per-book events with a 250 ms window so a burst of
|
|
118
|
+
* edits collapses into one rebuild. SIGINT / SIGTERM closes the watcher and
|
|
119
|
+
* exits 0. Honors `--plain`, `--exclude-code-prompts`, `--json`, and the
|
|
120
|
+
* config / `--dir` resolution chain.
|
|
121
|
+
*
|
|
122
|
+
* `--out <file>` only works in a single-book workspace; the multi-book mode
|
|
123
|
+
* always writes each book's artifact next to its sources.
|
|
124
|
+
*/
|
|
125
|
+
export async function cmdWatch(args, io) {
|
|
126
|
+
if (args.check) {
|
|
127
|
+
io.stderr("error: --check is not supported by watch (run `bundle --check --all` from CI)\n");
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
const promptsDir = await requirePromptsDir(io, args.operands[0] ?? args.dir);
|
|
131
|
+
if (promptsDir === null) {
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const workspace = await loadWorkspace(io, promptsDir);
|
|
135
|
+
if (workspace.books.length === 0) {
|
|
136
|
+
io.stderr(`error: no books found under ${promptsDir}\n`);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
if (args.out !== undefined && workspace.books.length > 1) {
|
|
140
|
+
io.stderr("error: --out requires a single book; drop --out to write each book's book.generated.ts\n");
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
const style = makeStyle(colorEnabled(io));
|
|
144
|
+
emitEvent(io, args, style, "started", { books: workspace.books.map((b) => b.name) });
|
|
145
|
+
// Initial pass runs in parallel: each book writes its own artifact, no contention.
|
|
146
|
+
await Promise.all(workspace.books.map((book) => rebuildAndReport(io, args, style, book)));
|
|
147
|
+
const watcher = chokidarWatch(promptsDir, {
|
|
148
|
+
ignoreInitial: true,
|
|
149
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
|
|
150
|
+
ignored: (eventPath) => shouldIgnore(eventPath),
|
|
151
|
+
});
|
|
152
|
+
const timers = new Map();
|
|
153
|
+
const scheduleRebuild = (book) => {
|
|
154
|
+
const existing = timers.get(book.name);
|
|
155
|
+
if (existing !== undefined) {
|
|
156
|
+
clearTimeout(existing);
|
|
157
|
+
}
|
|
158
|
+
const timer = setTimeout(() => {
|
|
159
|
+
timers.delete(book.name);
|
|
160
|
+
void rebuildAndReport(io, args, style, book);
|
|
161
|
+
}, DEBOUNCE_MS);
|
|
162
|
+
timers.set(book.name, timer);
|
|
163
|
+
};
|
|
164
|
+
watcher.on("all", (_event, eventPath) => {
|
|
165
|
+
const book = workspace.books.find((b) => eventInBook(b, eventPath));
|
|
166
|
+
if (book === undefined) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
scheduleRebuild(book);
|
|
170
|
+
});
|
|
171
|
+
// Hold the promise open until SIGINT / SIGTERM closes the watcher.
|
|
172
|
+
return new Promise((resolveDone) => {
|
|
173
|
+
let stopped = false;
|
|
174
|
+
const stop = async () => {
|
|
175
|
+
if (stopped) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
stopped = true;
|
|
179
|
+
for (const timer of timers.values()) {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
}
|
|
182
|
+
timers.clear();
|
|
183
|
+
await watcher.close();
|
|
184
|
+
emitEvent(io, args, style, "stopped");
|
|
185
|
+
process.off("SIGINT", stop);
|
|
186
|
+
process.off("SIGTERM", stop);
|
|
187
|
+
resolveDone(0);
|
|
188
|
+
};
|
|
189
|
+
process.once("SIGINT", stop);
|
|
190
|
+
process.once("SIGTERM", stop);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=watch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.js","sourceRoot":"","sources":["../../../src/commands/watch.ts"],"names":[],"mappings":"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"}
|
package/dist/src/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"AAWA,OAAO,EAAa,KAAK,EAAE,EAAE,MAAM,SAAS,CAAC;AAyD7C;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,EAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2C/E"}
|
package/dist/src/run.js
CHANGED
|
@@ -8,8 +8,9 @@ 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 } from "./io.js";
|
|
12
|
-
const HELP = `promptbook
|
|
13
|
+
const HELP = `promptbook · compose prompts from reusable fragments
|
|
13
14
|
|
|
14
15
|
Usage:
|
|
15
16
|
promptbook <command> [options]
|
|
@@ -18,7 +19,8 @@ Commands:
|
|
|
18
19
|
resolve [<book>/]<prompt> Assemble a prompt and print it to stdout (--all: every book)
|
|
19
20
|
lint [<prompt>] Run static checks; with no prompt, book rules only
|
|
20
21
|
eval [<name|glob>] Run fixtures through a model adapter, report pass-rate
|
|
21
|
-
bundle [<dir>] Compile a prompts folder into an importable book module
|
|
22
|
+
bundle [<dir>] Compile a prompts folder into an importable book module (--all/--check)
|
|
23
|
+
watch [<dir>] Rebuild book.generated.ts as fragments/rules/compositions change
|
|
22
24
|
view Start the local web viewer over the workspace (book switcher)
|
|
23
25
|
annotations <action> Drain the viewer's feedback queue: list | resolve <id> | clear
|
|
24
26
|
ls List compositions and fragments (--all: cross-book inventory)
|
|
@@ -38,13 +40,15 @@ Options:
|
|
|
38
40
|
--samples N eval: default samples per fixture (default 1; a fixture's own samples wins)
|
|
39
41
|
--threshold R eval: a fixture passes when passRate >= R (default 1)
|
|
40
42
|
--lint eval: run a static lint gate over every variant first
|
|
41
|
-
-o, --out <file> bundle: write
|
|
42
|
-
--plain bundle: emit a plain module (no type-only import; e.g. for Deno)
|
|
43
|
+
-o, --out <file> bundle/watch: write to a file (default: stdout for bundle, <bookDir>/book.generated.ts for watch/--all)
|
|
44
|
+
--plain bundle/watch: emit a plain module (no type-only import; e.g. for Deno)
|
|
45
|
+
--check bundle: compare with the existing output; exit 1 on drift or missing artifact
|
|
46
|
+
--exclude-code-prompts bundle/watch: serialize code-prompts as an empty map (runtime-lean bundle)
|
|
43
47
|
--port N view: port for the viewer server (default: a free port)
|
|
44
48
|
--no-open view: do not open the browser after starting
|
|
45
49
|
--fragments ls: list fragments only
|
|
46
50
|
--compositions ls: list compositions only
|
|
47
|
-
--all ls/resolve: span every book in the workspace
|
|
51
|
+
--all ls/resolve/bundle: span every book in the workspace
|
|
48
52
|
-h, --help Show this help
|
|
49
53
|
-v, --version Show the version
|
|
50
54
|
|
|
@@ -95,6 +99,8 @@ export async function run(argv, io = defaultIO()) {
|
|
|
95
99
|
return cmdEval(args, io);
|
|
96
100
|
case "bundle":
|
|
97
101
|
return cmdBundle(args, io);
|
|
102
|
+
case "watch":
|
|
103
|
+
return cmdWatch(args, io);
|
|
98
104
|
case "view":
|
|
99
105
|
return cmdView(args, io);
|
|
100
106
|
case "annotations":
|
package/dist/src/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAW,MAAM,SAAS,CAAC;AAE7C,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markbrutx/promptbook-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Thin terminal surface over @markbrutx/promptbook-core: resolve and ls for agents and CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,11 +48,12 @@
|
|
|
48
48
|
"check": "biome check ."
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@markbrutx/promptbook-core": "^0.
|
|
52
|
-
"@markbrutx/promptbook-openrouter": "^0.
|
|
51
|
+
"@markbrutx/promptbook-core": "^0.4.1",
|
|
52
|
+
"@markbrutx/promptbook-openrouter": "^0.4.1",
|
|
53
|
+
"chokidar": "^4.0.3"
|
|
53
54
|
},
|
|
54
55
|
"optionalDependencies": {
|
|
55
|
-
"@markbrutx/promptbook-viewer": "^0.
|
|
56
|
+
"@markbrutx/promptbook-viewer": "^0.4.1"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@biomejs/biome": "latest",
|
package/src/args.ts
CHANGED
|
@@ -20,8 +20,12 @@ export interface ParsedArgs {
|
|
|
20
20
|
contextFile?: string;
|
|
21
21
|
fragments: boolean;
|
|
22
22
|
compositions: boolean;
|
|
23
|
-
/** ls/resolve: operate across every book in the workspace. */
|
|
23
|
+
/** ls/resolve/bundle: operate across every book in the workspace. */
|
|
24
24
|
all: boolean;
|
|
25
|
+
/** bundle: compare the generated output with the existing artifact and exit non-zero on drift. */
|
|
26
|
+
check: boolean;
|
|
27
|
+
/** bundle: serialize without code-prompts so a runtime bundle stays lean. */
|
|
28
|
+
excludeCodePrompts: boolean;
|
|
25
29
|
/** lint: estimated token ceiling for the token-budget rule. */
|
|
26
30
|
maxTokens?: number;
|
|
27
31
|
/** lint: treat warnings as failures for the exit code. */
|
|
@@ -91,6 +95,8 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
|
91
95
|
fragments: { type: "boolean" },
|
|
92
96
|
compositions: { type: "boolean" },
|
|
93
97
|
all: { type: "boolean" },
|
|
98
|
+
check: { type: "boolean" },
|
|
99
|
+
"exclude-code-prompts": { type: "boolean" },
|
|
94
100
|
"max-tokens": { type: "string" },
|
|
95
101
|
strict: { type: "boolean" },
|
|
96
102
|
model: { type: "string" },
|
|
@@ -137,6 +143,8 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
|
137
143
|
fragments: values.fragments ?? false,
|
|
138
144
|
compositions: values.compositions ?? false,
|
|
139
145
|
all: values.all ?? false,
|
|
146
|
+
check: values.check ?? false,
|
|
147
|
+
excludeCodePrompts: values["exclude-code-prompts"] ?? false,
|
|
140
148
|
maxTokens,
|
|
141
149
|
strict: values.strict ?? false,
|
|
142
150
|
model: values.model,
|
package/src/commands/bundle.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve as resolvePath, sep } from "node:path";
|
|
1
|
+
import { basename, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
|
|
2
2
|
import type { PromptBook } from "@markbrutx/promptbook-core";
|
|
3
3
|
import { loadPrompts, serializeBook, serializeBookJson } from "@markbrutx/promptbook-core";
|
|
4
4
|
import type { ParsedArgs } from "../args.js";
|
|
5
5
|
import { requirePromptsDir } from "../config.js";
|
|
6
6
|
import { emitWarnings, type IO } from "../io.js";
|
|
7
|
+
import { loadWorkspace } from "../workspace.js";
|
|
8
|
+
|
|
9
|
+
/** Default name of the artifact file each book emits alongside its sources. */
|
|
10
|
+
const BUNDLE_FILE = "book.generated.ts";
|
|
7
11
|
|
|
8
12
|
/** Rewrite an absolute source path to a portable, forward-slash path relative to `dir`. */
|
|
9
13
|
function relativizeSource(sourceFile: string, dir: string): string {
|
|
@@ -34,32 +38,124 @@ function portableBook(book: PromptBook, dir: string): PromptBook {
|
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
/** Return the book with `codePrompts` cleared — used by `--exclude-code-prompts`. */
|
|
42
|
+
function withoutCodePrompts(book: PromptBook): PromptBook {
|
|
43
|
+
return { ...book, codePrompts: new Map() };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Normalize EOL so a CRLF-checked-in artifact does not falsely look stale on an LF rebuild. */
|
|
47
|
+
function normalizeEol(text: string): string {
|
|
48
|
+
return text.replace(/\r\n/g, "\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** First 1-based line index where two strings differ; null when equal. */
|
|
52
|
+
function firstDiffLine(actual: string, expected: string): number | null {
|
|
53
|
+
if (actual === expected) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const a = actual.split("\n");
|
|
57
|
+
const b = expected.split("\n");
|
|
58
|
+
const max = Math.max(a.length, b.length);
|
|
59
|
+
for (let i = 0; i < max; i += 1) {
|
|
60
|
+
if (a[i] !== b[i]) {
|
|
61
|
+
return i + 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Unreachable: if the strings differ, at least one index in [0, max) must too.
|
|
65
|
+
throw new Error("firstDiffLine: strings differ but no line diff found");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Build the output text for one book (TypeScript module or JSON). */
|
|
69
|
+
function renderOutput(book: PromptBook, args: ParsedArgs): string {
|
|
70
|
+
return args.json ? serializeBookJson(book) : serializeBook(book, { typed: !args.plain });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Pick the artifact path: explicit `--out`, else `book.generated.ts` next to the prompts folder. */
|
|
74
|
+
function targetPath(io: IO, args: ParsedArgs, promptsDir: string): string {
|
|
75
|
+
if (args.out !== undefined) {
|
|
76
|
+
return resolvePath(io.cwd(), args.out);
|
|
77
|
+
}
|
|
78
|
+
return join(promptsDir, BUNDLE_FILE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One book's `--check` outcome: file present and matching, drifted, or missing. */
|
|
82
|
+
type CheckOutcome =
|
|
83
|
+
| { status: "up-to-date"; path: string }
|
|
84
|
+
| { status: "stale"; reason: "missing"; path: string }
|
|
85
|
+
| { status: "stale"; reason: "diff"; firstDiffLine: number; path: string };
|
|
86
|
+
|
|
87
|
+
async function checkOne(io: IO, output: string, path: string): Promise<CheckOutcome> {
|
|
88
|
+
let existing: string;
|
|
89
|
+
try {
|
|
90
|
+
existing = await io.fs.readFile(path);
|
|
91
|
+
} catch {
|
|
92
|
+
return { status: "stale", reason: "missing", path };
|
|
93
|
+
}
|
|
94
|
+
const line = firstDiffLine(normalizeEol(output), normalizeEol(existing));
|
|
95
|
+
if (line === null) {
|
|
96
|
+
return { status: "up-to-date", path };
|
|
97
|
+
}
|
|
98
|
+
return { status: "stale", reason: "diff", firstDiffLine: line, path };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function emitCheckResult(io: IO, args: ParsedArgs, bookName: string, outcome: CheckOutcome): void {
|
|
102
|
+
if (args.json) {
|
|
103
|
+
const diff =
|
|
104
|
+
outcome.status === "stale"
|
|
105
|
+
? outcome.reason === "missing"
|
|
106
|
+
? { reason: "missing", path: outcome.path }
|
|
107
|
+
: { reason: "diff", firstDiffLine: outcome.firstDiffLine }
|
|
108
|
+
: null;
|
|
109
|
+
io.stderr(`${JSON.stringify({ book: bookName, status: outcome.status, diff })}\n`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (outcome.status === "up-to-date") {
|
|
113
|
+
io.stderr(`${bookName} up to date\n`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (outcome.reason === "missing") {
|
|
117
|
+
io.stderr(`${bookName} stale (missing ${outcome.path})\n`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
io.stderr(`${bookName} stale (first diff at line ${outcome.firstDiffLine})\n`);
|
|
121
|
+
}
|
|
122
|
+
|
|
37
123
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* emits a structured dump instead of the TypeScript module.
|
|
124
|
+
* Bundle one book: load → relativize → optionally drop code-prompts → serialize.
|
|
125
|
+
* Then either `--check` against the existing artifact, write to `--out`/the
|
|
126
|
+
* default `book.generated.ts`, or print to stdout (single-book, no `--out`).
|
|
42
127
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
128
|
+
* Returns the exit code: 0 on success / up-to-date, 1 on drift / write failure.
|
|
129
|
+
* `bookName` is used in `--check` output and warnings; when `inWorkspace` is
|
|
130
|
+
* true the artifact is always written (not printed), matching `bundle --all`.
|
|
45
131
|
*/
|
|
46
|
-
export async function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
132
|
+
export async function bundleOne(
|
|
133
|
+
io: IO,
|
|
134
|
+
args: ParsedArgs,
|
|
135
|
+
promptsDir: string,
|
|
136
|
+
bookName: string,
|
|
137
|
+
inWorkspace: boolean,
|
|
138
|
+
): Promise<number> {
|
|
139
|
+
const loaded = await loadPrompts(promptsDir, io.fs);
|
|
140
|
+
// `--exclude-code-prompts` discards them outright; skipping the relativize pass is the whole point.
|
|
141
|
+
const source = args.excludeCodePrompts ? withoutCodePrompts(loaded) : loaded;
|
|
142
|
+
const book = portableBook(source, promptsDir);
|
|
53
143
|
emitWarnings(io, book.warnings);
|
|
54
144
|
|
|
55
|
-
const output =
|
|
145
|
+
const output = renderOutput(book, args);
|
|
56
146
|
|
|
57
|
-
if (args.
|
|
147
|
+
if (args.check) {
|
|
148
|
+
const outcome = await checkOne(io, output, targetPath(io, args, promptsDir));
|
|
149
|
+
emitCheckResult(io, args, bookName, outcome);
|
|
150
|
+
return outcome.status === "up-to-date" ? 0 : 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (args.out === undefined && !inWorkspace) {
|
|
58
154
|
io.stdout(output);
|
|
59
155
|
return 0;
|
|
60
156
|
}
|
|
61
157
|
|
|
62
|
-
const outPath =
|
|
158
|
+
const outPath = targetPath(io, args, promptsDir);
|
|
63
159
|
try {
|
|
64
160
|
await io.writeFile(outPath, output);
|
|
65
161
|
} catch (error) {
|
|
@@ -69,3 +165,53 @@ export async function cmdBundle(args: ParsedArgs, io: IO): Promise<number> {
|
|
|
69
165
|
io.stderr(`wrote ${outPath}\n`);
|
|
70
166
|
return 0;
|
|
71
167
|
}
|
|
168
|
+
|
|
169
|
+
/** Run `bundleOne` over every book in the workspace in parallel; the worst exit code wins. */
|
|
170
|
+
async function bundleAll(io: IO, args: ParsedArgs, root: string): Promise<number> {
|
|
171
|
+
const workspace = await loadWorkspace(io, root);
|
|
172
|
+
if (workspace.books.length === 0) {
|
|
173
|
+
io.stderr(`error: no books found under ${root}\n`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const codes = await Promise.all(
|
|
177
|
+
workspace.books.map((book) => bundleOne(io, args, book.dir, book.name, true)),
|
|
178
|
+
);
|
|
179
|
+
return Math.max(0, ...codes);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `bundle [<dir>]`: load a prompts folder and emit it as an importable module
|
|
184
|
+
* exporting `book: PromptBook`. Writes to stdout, or to a file with `-o`.
|
|
185
|
+
*
|
|
186
|
+
* Flags:
|
|
187
|
+
* - `--json` emits the structured dump instead of the TypeScript module.
|
|
188
|
+
* - `--plain` drops the type-only import (e.g. for Deno consumers).
|
|
189
|
+
* - `--exclude-code-prompts` serializes code-prompts as an empty map so the
|
|
190
|
+
* runtime bundle stays lean while `code-prompts/` keeps living on disk as
|
|
191
|
+
* metadata for `ls` / the viewer.
|
|
192
|
+
* - `--check` compares the would-be output against the existing artifact
|
|
193
|
+
* (`book.generated.ts` next to the prompts folder, or `--out`); exits 1 on
|
|
194
|
+
* drift or a missing artifact and prints a short hint on stderr.
|
|
195
|
+
* - `--all` walks every book in the workspace and writes each to its own
|
|
196
|
+
* `book.generated.ts`; incompatible with `-o`.
|
|
197
|
+
*
|
|
198
|
+
* The folder comes from the positional `<dir>`, else `--dir`, else config /
|
|
199
|
+
* `./prompts`. `--all` does not accept a single-book `<dir>` operand differently.
|
|
200
|
+
*/
|
|
201
|
+
export async function cmdBundle(args: ParsedArgs, io: IO): Promise<number> {
|
|
202
|
+
if (args.all && args.out !== undefined) {
|
|
203
|
+
io.stderr("error: --all is not compatible with -o/--out\n");
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const promptsDir = await requirePromptsDir(io, args.operands[0] ?? args.dir);
|
|
208
|
+
if (promptsDir === null) {
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (args.all) {
|
|
213
|
+
return bundleAll(io, args, promptsDir);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return bundleOne(io, args, promptsDir, basename(promptsDir), false);
|
|
217
|
+
}
|
|
@@ -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/run.ts
CHANGED
|
@@ -8,9 +8,10 @@ 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
|
-
const HELP = `promptbook
|
|
14
|
+
const HELP = `promptbook · compose prompts from reusable fragments
|
|
14
15
|
|
|
15
16
|
Usage:
|
|
16
17
|
promptbook <command> [options]
|
|
@@ -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":
|