@markbrutx/promptbook-cli 0.1.0 → 0.2.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/dist/src/args.d.ts +2 -0
- package/dist/src/args.d.ts.map +1 -1
- package/dist/src/args.js +2 -0
- package/dist/src/args.js.map +1 -1
- package/dist/src/commands/annotations.d.ts.map +1 -1
- package/dist/src/commands/annotations.js +1 -1
- package/dist/src/commands/annotations.js.map +1 -1
- package/dist/src/commands/eval.d.ts.map +1 -1
- package/dist/src/commands/eval.js +1 -1
- package/dist/src/commands/eval.js.map +1 -1
- package/dist/src/commands/ls.d.ts +6 -5
- package/dist/src/commands/ls.d.ts.map +1 -1
- package/dist/src/commands/ls.js +90 -21
- package/dist/src/commands/ls.js.map +1 -1
- package/dist/src/commands/resolve.d.ts +6 -3
- package/dist/src/commands/resolve.d.ts.map +1 -1
- package/dist/src/commands/resolve.js +84 -11
- package/dist/src/commands/resolve.js.map +1 -1
- package/dist/src/run.d.ts.map +1 -1
- package/dist/src/run.js +7 -3
- package/dist/src/run.js.map +1 -1
- package/dist/src/workspace.d.ts +51 -0
- package/dist/src/workspace.d.ts.map +1 -0
- package/dist/src/workspace.js +103 -0
- package/dist/src/workspace.js.map +1 -0
- package/package.json +22 -5
- package/src/args.ts +4 -0
- package/src/commands/ls.ts +114 -21
- package/src/commands/resolve.ts +104 -12
- package/src/run.ts +7 -3
- package/src/workspace.ts +144 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { basename, join, relative } from "node:path";
|
|
2
|
+
import { loadPrompts } from "@markbrutx/promptbook-core";
|
|
3
|
+
/** Folder names that are book internals or noise, never workspace sub-books. */
|
|
4
|
+
const NON_BOOK_DIRS = new Set(["node_modules", "fragments", "rules", "code-prompts"]);
|
|
5
|
+
/** Markers that make a folder a loadable book (a config or a loadable form). */
|
|
6
|
+
const BOOK_MARKERS = ["promptbook.json", "rules", "code-prompts"];
|
|
7
|
+
/** True when `dir` looks like a prompts book (has a config or a loadable form). */
|
|
8
|
+
export async function isBook(io, dir) {
|
|
9
|
+
let entries;
|
|
10
|
+
try {
|
|
11
|
+
entries = await io.fs.readDir(dir);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return BOOK_MARKERS.some((marker) => entries.includes(marker));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Find the books directly under `rootDir`: each non-internal subfolder that is
|
|
20
|
+
* itself a book. One level deep, not recursive; sorted by name for a stable
|
|
21
|
+
* menu. Dot/underscore folders and book internals are skipped.
|
|
22
|
+
*/
|
|
23
|
+
export async function discoverBooks(io, rootDir) {
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = await io.fs.readDir(rootDir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const books = [];
|
|
32
|
+
for (const name of [...entries].sort()) {
|
|
33
|
+
if (name.startsWith(".") || name.startsWith("_") || NON_BOOK_DIRS.has(name)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const dir = join(rootDir, name);
|
|
37
|
+
if (await isBook(io, dir)) {
|
|
38
|
+
books.push({ name, dir });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return books;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read a workspace from `rootDir`. When the root is itself a book it is the only
|
|
45
|
+
* book (back-compat single-book path, named after the folder); otherwise the
|
|
46
|
+
* root is a workspace of its sub-books.
|
|
47
|
+
*/
|
|
48
|
+
export async function loadWorkspace(io, rootDir) {
|
|
49
|
+
if (await isBook(io, rootDir)) {
|
|
50
|
+
return { root: rootDir, books: [{ name: basename(rootDir), dir: rootDir }] };
|
|
51
|
+
}
|
|
52
|
+
return { root: rootDir, books: await discoverBooks(io, rootDir) };
|
|
53
|
+
}
|
|
54
|
+
/** Split an operand on the first `/`: `book/comp` (comp may itself contain `/`). */
|
|
55
|
+
export function parseAddress(operand) {
|
|
56
|
+
const slash = operand.indexOf("/");
|
|
57
|
+
if (slash === -1) {
|
|
58
|
+
return { comp: operand };
|
|
59
|
+
}
|
|
60
|
+
return { book: operand.slice(0, slash), comp: operand.slice(slash + 1) };
|
|
61
|
+
}
|
|
62
|
+
/** A book's directory relative to the workspace root (`.` when it is the root). */
|
|
63
|
+
export function bookDir(workspace, book) {
|
|
64
|
+
return relative(workspace.root, book.dir) || ".";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve an operand to a concrete book + composition. A `<book>/<comp>` prefix
|
|
68
|
+
* that names a known book addresses it directly; otherwise the whole operand is
|
|
69
|
+
* a bare composition name, resolved by uniqueness across the workspace's books
|
|
70
|
+
* (a single-book workspace always uses its one book). Throws a clear error when
|
|
71
|
+
* a bare name is missing or ambiguous.
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveAddress(io, workspace, operand) {
|
|
74
|
+
const address = parseAddress(operand);
|
|
75
|
+
if (address.book !== undefined) {
|
|
76
|
+
const book = workspace.books.find((b) => b.name === address.book);
|
|
77
|
+
if (book !== undefined) {
|
|
78
|
+
return { book, comp: address.comp };
|
|
79
|
+
}
|
|
80
|
+
// Prefix names no book: fall through and treat the whole operand as a
|
|
81
|
+
// (possibly path-like) bare composition name.
|
|
82
|
+
}
|
|
83
|
+
if (workspace.books.length === 1) {
|
|
84
|
+
return { book: workspace.books[0], comp: operand };
|
|
85
|
+
}
|
|
86
|
+
const matches = [];
|
|
87
|
+
for (const book of workspace.books) {
|
|
88
|
+
const loaded = await loadPrompts(book.dir, io.fs);
|
|
89
|
+
if (loaded.compositions.has(operand)) {
|
|
90
|
+
matches.push({ book, loaded });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const [first] = matches;
|
|
94
|
+
if (matches.length === 1 && first !== undefined) {
|
|
95
|
+
return { book: first.book, comp: operand, loaded: first.loaded };
|
|
96
|
+
}
|
|
97
|
+
if (matches.length === 0) {
|
|
98
|
+
throw new Error(`Unknown prompt "${operand}" in any book. Qualify it as <book>/<comp>.`);
|
|
99
|
+
}
|
|
100
|
+
const names = matches.map((m) => m.book.name).join(", ");
|
|
101
|
+
throw new Error(`Ambiguous prompt "${operand}"; found in ${matches.length} books (${names}). Qualify it as <book>/<comp>.`);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/workspace.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markbrutx/promptbook-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"prompt",
|
|
9
|
+
"prompt-engineering",
|
|
10
|
+
"prompt-management",
|
|
11
|
+
"llm",
|
|
12
|
+
"ai",
|
|
13
|
+
"cli",
|
|
14
|
+
"composition",
|
|
15
|
+
"storybook"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/markbrutx/promptbook.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/markbrutx/promptbook#readme",
|
|
23
|
+
"bugs": "https://github.com/markbrutx/promptbook/issues",
|
|
7
24
|
"bin": {
|
|
8
|
-
"promptbook": "
|
|
25
|
+
"promptbook": "dist/bin/promptbook.js"
|
|
9
26
|
},
|
|
10
27
|
"exports": {
|
|
11
28
|
".": {
|
|
@@ -31,11 +48,11 @@
|
|
|
31
48
|
"check": "biome check ."
|
|
32
49
|
},
|
|
33
50
|
"dependencies": {
|
|
34
|
-
"@markbrutx/promptbook-core": "^0.
|
|
35
|
-
"@markbrutx/promptbook-openrouter": "^0.
|
|
51
|
+
"@markbrutx/promptbook-core": "^0.2.0",
|
|
52
|
+
"@markbrutx/promptbook-openrouter": "^0.2.0"
|
|
36
53
|
},
|
|
37
54
|
"optionalDependencies": {
|
|
38
|
-
"@markbrutx/promptbook-viewer": "^0.
|
|
55
|
+
"@markbrutx/promptbook-viewer": "^0.2.0"
|
|
39
56
|
},
|
|
40
57
|
"devDependencies": {
|
|
41
58
|
"@biomejs/biome": "latest",
|
package/src/args.ts
CHANGED
|
@@ -20,6 +20,8 @@ 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. */
|
|
24
|
+
all: boolean;
|
|
23
25
|
/** lint: estimated token ceiling for the token-budget rule. */
|
|
24
26
|
maxTokens?: number;
|
|
25
27
|
/** lint: treat warnings as failures for the exit code. */
|
|
@@ -88,6 +90,7 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
|
88
90
|
"context-file": { type: "string" },
|
|
89
91
|
fragments: { type: "boolean" },
|
|
90
92
|
compositions: { type: "boolean" },
|
|
93
|
+
all: { type: "boolean" },
|
|
91
94
|
"max-tokens": { type: "string" },
|
|
92
95
|
strict: { type: "boolean" },
|
|
93
96
|
model: { type: "string" },
|
|
@@ -133,6 +136,7 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
|
133
136
|
contextFile: values["context-file"],
|
|
134
137
|
fragments: values.fragments ?? false,
|
|
135
138
|
compositions: values.compositions ?? false,
|
|
139
|
+
all: values.all ?? false,
|
|
136
140
|
maxTokens,
|
|
137
141
|
strict: values.strict ?? false,
|
|
138
142
|
model: values.model,
|
package/src/commands/ls.ts
CHANGED
|
@@ -1,15 +1,112 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
|
-
import {
|
|
1
|
+
import { basename, relative } from "node:path";
|
|
2
|
+
import type { PromptBook, RequiredContext } from "@markbrutx/promptbook-core";
|
|
3
|
+
import { loadPrompts, requiredContext } from "@markbrutx/promptbook-core";
|
|
3
4
|
import type { ParsedArgs } from "../args.js";
|
|
4
5
|
import { requirePromptsDir } from "../config.js";
|
|
5
6
|
import { emitWarnings, type IO } from "../io.js";
|
|
7
|
+
import { bookDir, loadWorkspace } from "../workspace.js";
|
|
8
|
+
|
|
9
|
+
interface CompositionEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
base: number;
|
|
12
|
+
rules: number;
|
|
13
|
+
requiredContext: RequiredContext;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CodePromptEntry {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string | null;
|
|
19
|
+
samples: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Composition rows for the menu: counts plus the statically-declared context. */
|
|
23
|
+
function compositionEntries(book: PromptBook): CompositionEntry[] {
|
|
24
|
+
return [...book.compositions.values()]
|
|
25
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
26
|
+
.map((c) => ({
|
|
27
|
+
name: c.name,
|
|
28
|
+
base: c.base.length,
|
|
29
|
+
rules: c.rules.length,
|
|
30
|
+
requiredContext: requiredContext(book, c.name),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Code-prompt rows: builder metadata only (opaque; no requiredContext). */
|
|
35
|
+
function codePromptEntries(book: PromptBook): CodePromptEntry[] {
|
|
36
|
+
return [...book.codePrompts.values()]
|
|
37
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
38
|
+
.map((c) => ({ name: c.name, description: c.description ?? null, samples: c.samples.length }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Compact one-line context summary for the plain `--all` tree. */
|
|
42
|
+
function ctxSummary(rc: RequiredContext): string {
|
|
43
|
+
const vars = rc.vars.length > 0 ? rc.vars.join(",") : "-";
|
|
44
|
+
const axes =
|
|
45
|
+
Object.entries(rc.axes)
|
|
46
|
+
.map(([key, values]) => `${key}=${values.join("|")}`)
|
|
47
|
+
.join(" ") || "-";
|
|
48
|
+
return `vars=${vars} axes=${axes}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `ls --all`: a cross-book inventory of the whole workspace. Discovers every
|
|
53
|
+
* book under the root, loads each, and emits the menu (compositions with their
|
|
54
|
+
* required context + code-prompts) — `--json` as `{ workspace, books[] }`,
|
|
55
|
+
* plain as an indented book→composition tree. Order is deterministic (books and
|
|
56
|
+
* compositions sorted by name).
|
|
57
|
+
*/
|
|
58
|
+
async function cmdLsAll(io: IO, root: string, json: boolean): Promise<number> {
|
|
59
|
+
const workspace = await loadWorkspace(io, root);
|
|
60
|
+
const books = await Promise.all(
|
|
61
|
+
workspace.books.map(async (b) => {
|
|
62
|
+
const book = await loadPrompts(b.dir, io.fs);
|
|
63
|
+
return {
|
|
64
|
+
name: b.name,
|
|
65
|
+
dir: bookDir(workspace, b),
|
|
66
|
+
compositions: compositionEntries(book),
|
|
67
|
+
codePrompts: codePromptEntries(book),
|
|
68
|
+
warnings: book.warnings,
|
|
69
|
+
};
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (json) {
|
|
74
|
+
io.stdout(`${JSON.stringify({ workspace: basename(workspace.root), books }, null, 2)}\n`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lines: string[] = [`workspace: ${basename(workspace.root)}`];
|
|
79
|
+
if (books.length === 0) {
|
|
80
|
+
lines.push(" (no books found)");
|
|
81
|
+
}
|
|
82
|
+
for (const book of books) {
|
|
83
|
+
lines.push("", `${book.name} (${book.dir})`);
|
|
84
|
+
lines.push(" compositions:");
|
|
85
|
+
if (book.compositions.length === 0) {
|
|
86
|
+
lines.push(" (none)");
|
|
87
|
+
}
|
|
88
|
+
for (const c of book.compositions) {
|
|
89
|
+
lines.push(` ${c.name} base=${c.base} rules=${c.rules} ctx: ${ctxSummary(c.requiredContext)}`);
|
|
90
|
+
}
|
|
91
|
+
if (book.codePrompts.length > 0) {
|
|
92
|
+
lines.push(" code-prompts:");
|
|
93
|
+
for (const c of book.codePrompts) {
|
|
94
|
+
lines.push(` ${c.name} kind=code samples=${c.samples}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
emitWarnings(io, book.warnings);
|
|
98
|
+
}
|
|
99
|
+
io.stdout(`${lines.join("\n")}\n`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
6
102
|
|
|
7
103
|
/**
|
|
8
|
-
* `ls`: list compositions (name, base length, rule count),
|
|
9
|
-
* (name, description, sample count) and fragments (id, kind, tags,
|
|
10
|
-
* the unified menu of every prompt in the book. `--fragments
|
|
11
|
-
* narrow the output (code-prompts ride with compositions, both
|
|
12
|
-
* `--json` emits a structured list
|
|
104
|
+
* `ls`: list compositions (name, base length, rule count, required context),
|
|
105
|
+
* code-prompts (name, description, sample count) and fragments (id, kind, tags,
|
|
106
|
+
* source) — the unified menu of every prompt in the book. `--fragments`/
|
|
107
|
+
* `--compositions` narrow the output (code-prompts ride with compositions, both
|
|
108
|
+
* being prompts); `--json` emits a structured list; `--all` spans every book in
|
|
109
|
+
* the workspace instead of one folder.
|
|
13
110
|
*/
|
|
14
111
|
export async function cmdLs(args: ParsedArgs, io: IO): Promise<number> {
|
|
15
112
|
const promptsDir = await requirePromptsDir(io, args.dir);
|
|
@@ -17,6 +114,10 @@ export async function cmdLs(args: ParsedArgs, io: IO): Promise<number> {
|
|
|
17
114
|
return 1;
|
|
18
115
|
}
|
|
19
116
|
|
|
117
|
+
if (args.all) {
|
|
118
|
+
return cmdLsAll(io, promptsDir, args.json);
|
|
119
|
+
}
|
|
120
|
+
|
|
20
121
|
const book = await loadPrompts(promptsDir, io.fs);
|
|
21
122
|
emitWarnings(io, book.warnings);
|
|
22
123
|
|
|
@@ -24,23 +125,15 @@ export async function cmdLs(args: ParsedArgs, io: IO): Promise<number> {
|
|
|
24
125
|
const showCompositions = args.compositions || !onlyOneSection;
|
|
25
126
|
const showFragments = args.fragments || !onlyOneSection;
|
|
26
127
|
|
|
27
|
-
const compositions =
|
|
28
|
-
const codePrompts =
|
|
128
|
+
const compositions = compositionEntries(book);
|
|
129
|
+
const codePrompts = codePromptEntries(book);
|
|
29
130
|
const fragments = [...book.fragments.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
30
131
|
|
|
31
132
|
if (args.json) {
|
|
32
133
|
const out: Record<string, unknown> = {};
|
|
33
134
|
if (showCompositions) {
|
|
34
|
-
out.compositions = compositions
|
|
35
|
-
|
|
36
|
-
base: c.base.length,
|
|
37
|
-
rules: c.rules.length,
|
|
38
|
-
}));
|
|
39
|
-
out.codePrompts = codePrompts.map((c) => ({
|
|
40
|
-
name: c.name,
|
|
41
|
-
description: c.description ?? null,
|
|
42
|
-
samples: c.samples.length,
|
|
43
|
-
}));
|
|
135
|
+
out.compositions = compositions;
|
|
136
|
+
out.codePrompts = codePrompts;
|
|
44
137
|
}
|
|
45
138
|
if (showFragments) {
|
|
46
139
|
out.fragments = fragments.map((f) => ({
|
|
@@ -61,13 +154,13 @@ export async function cmdLs(args: ParsedArgs, io: IO): Promise<number> {
|
|
|
61
154
|
lines.push(" (none)");
|
|
62
155
|
}
|
|
63
156
|
for (const c of compositions) {
|
|
64
|
-
lines.push(` ${c.name} base=${c.base
|
|
157
|
+
lines.push(` ${c.name} base=${c.base} rules=${c.rules}`);
|
|
65
158
|
}
|
|
66
159
|
if (codePrompts.length > 0) {
|
|
67
160
|
lines.push("");
|
|
68
161
|
lines.push("code-prompts:");
|
|
69
162
|
for (const c of codePrompts) {
|
|
70
|
-
lines.push(` ${c.name} kind=code samples=${c.samples
|
|
163
|
+
lines.push(` ${c.name} kind=code samples=${c.samples}`);
|
|
71
164
|
}
|
|
72
165
|
}
|
|
73
166
|
}
|
package/src/commands/resolve.ts
CHANGED
|
@@ -1,31 +1,123 @@
|
|
|
1
|
-
import type { ResolveResult } from "@markbrutx/promptbook-core";
|
|
2
|
-
import { resolve } from "@markbrutx/promptbook-core";
|
|
1
|
+
import type { Context, ResolveResult } from "@markbrutx/promptbook-core";
|
|
2
|
+
import { loadPrompts, resolve, resolveBook } from "@markbrutx/promptbook-core";
|
|
3
3
|
import type { ParsedArgs } from "../args.js";
|
|
4
4
|
import { buildContext, requirePromptsDir } from "../config.js";
|
|
5
5
|
import { colorEnabled, emitWarnings, type IO } from "../io.js";
|
|
6
6
|
import { renderExplain } from "../render-explain.js";
|
|
7
|
+
import { loadWorkspace, resolveAddress } from "../workspace.js";
|
|
8
|
+
|
|
9
|
+
/** One node of a book's menu in deterministic (name) order, typed by kind. */
|
|
10
|
+
interface BookNode {
|
|
11
|
+
name: string;
|
|
12
|
+
kind: "composition" | "code-prompt";
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
|
-
* `resolve
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* `resolve --all`: assemble every composition of every book in the workspace.
|
|
17
|
+
* Missing variables become stderr warnings, never throws, so the run always
|
|
18
|
+
* completes. `--json` emits a `{ "book/comp": value }` map (compositions carry
|
|
19
|
+
* `text`+`trace`; code-prompts ride along as inventory `{kind, samples}` so
|
|
20
|
+
* builders are not silently dropped); plain prints each composition under a
|
|
21
|
+
* `=== book/comp ===` header and notes the skipped code-prompts. Order is by
|
|
22
|
+
* book then by node name.
|
|
12
23
|
*/
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
async function cmdResolveAll(io: IO, root: string, context: Context, json: boolean): Promise<number> {
|
|
25
|
+
const workspace = await loadWorkspace(io, root);
|
|
26
|
+
const blocks: string[] = [];
|
|
27
|
+
const skipped: string[] = [];
|
|
28
|
+
const map: Record<string, unknown> = {};
|
|
29
|
+
|
|
30
|
+
// Load every book up front (concurrently); iterate the sorted result for
|
|
31
|
+
// deterministic output, matching `ls --all`.
|
|
32
|
+
const loadedBooks = await Promise.all(
|
|
33
|
+
workspace.books.map(async (book) => ({ book, loaded: await loadPrompts(book.dir, io.fs) })),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
for (const { book, loaded } of loadedBooks) {
|
|
37
|
+
emitWarnings(
|
|
38
|
+
io,
|
|
39
|
+
loaded.warnings.map((w) => `${book.name}: ${w}`),
|
|
40
|
+
);
|
|
41
|
+
const nodes: BookNode[] = [
|
|
42
|
+
...[...loaded.compositions.keys()].map((name): BookNode => ({ name, kind: "composition" })),
|
|
43
|
+
...[...loaded.codePrompts.keys()].map((name): BookNode => ({ name, kind: "code-prompt" })),
|
|
44
|
+
].sort((a, b) => a.name.localeCompare(b.name));
|
|
45
|
+
|
|
46
|
+
for (const node of nodes) {
|
|
47
|
+
const key = `${book.name}/${node.name}`;
|
|
48
|
+
if (node.kind === "code-prompt") {
|
|
49
|
+
const codePrompt = loaded.codePrompts.get(node.name);
|
|
50
|
+
skipped.push(key);
|
|
51
|
+
if (json) {
|
|
52
|
+
map[key] = { kind: "code-prompt", samples: codePrompt?.samples ?? [] };
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const { text, trace } = resolveBook(loaded, node.name, context);
|
|
57
|
+
emitWarnings(
|
|
58
|
+
io,
|
|
59
|
+
trace.warnings.map((w) => `${key}: ${w}`),
|
|
60
|
+
);
|
|
61
|
+
if (json) {
|
|
62
|
+
map[key] = { kind: "composition", text, trace };
|
|
63
|
+
} else {
|
|
64
|
+
blocks.push(`=== ${key} ===\n${text}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
18
67
|
}
|
|
19
68
|
|
|
69
|
+
if (json) {
|
|
70
|
+
io.stdout(`${JSON.stringify(map, null, 2)}\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
io.stdout(`${blocks.join("\n\n")}\n`);
|
|
74
|
+
if (skipped.length > 0) {
|
|
75
|
+
io.stderr(`note: skipped ${skipped.length} code-prompt(s) (not resolvable): ${skipped.join(", ")}\n`);
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `resolve [<book>/]<prompt>`: assemble the prompt and print its text to stdout.
|
|
82
|
+
* A `<book>/` prefix addresses a specific book; a bare name resolves by
|
|
83
|
+
* uniqueness across the workspace (and works as before in a single-book root).
|
|
84
|
+
* `--json` prints `{ text, trace }`; `--explain` adds the trace to stderr;
|
|
85
|
+
* `--all` assembles every composition of every book. Warnings always go to
|
|
86
|
+
* stderr so nothing is lost.
|
|
87
|
+
*/
|
|
88
|
+
export async function cmdResolve(args: ParsedArgs, io: IO): Promise<number> {
|
|
20
89
|
const promptsDir = await requirePromptsDir(io, args.dir);
|
|
21
90
|
if (promptsDir === null) {
|
|
22
91
|
return 1;
|
|
23
92
|
}
|
|
24
93
|
|
|
94
|
+
let context: Context;
|
|
95
|
+
try {
|
|
96
|
+
context = await buildContext(io, args.ctx, args.contextFile);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
io.stderr(`error: ${(error as Error).message}\n`);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (args.all) {
|
|
103
|
+
return cmdResolveAll(io, promptsDir, context, args.json);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const operand = args.operands[0];
|
|
107
|
+
if (operand === undefined) {
|
|
108
|
+
io.stderr('error: resolve requires a <prompt> name (or --all). Run "promptbook --help".\n');
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
25
112
|
let result: ResolveResult;
|
|
26
113
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
114
|
+
const workspace = await loadWorkspace(io, promptsDir);
|
|
115
|
+
const { book, comp, loaded } = await resolveAddress(io, workspace, operand);
|
|
116
|
+
// Bare-name resolution already loaded the matched book — reuse it; otherwise load now.
|
|
117
|
+
result =
|
|
118
|
+
loaded !== undefined
|
|
119
|
+
? resolveBook(loaded, comp, context)
|
|
120
|
+
: await resolve({ promptsDir: book.dir, prompt: comp, context, fs: io.fs });
|
|
29
121
|
} catch (error) {
|
|
30
122
|
io.stderr(`error: ${(error as Error).message}\n`);
|
|
31
123
|
return 1;
|
package/src/run.ts
CHANGED
|
@@ -16,13 +16,16 @@ Usage:
|
|
|
16
16
|
promptbook <command> [options]
|
|
17
17
|
|
|
18
18
|
Commands:
|
|
19
|
-
resolve <prompt>
|
|
19
|
+
resolve [<book>/]<prompt> Assemble a prompt and print it to stdout (--all: every book)
|
|
20
20
|
lint [<prompt>] Run static checks; with no prompt, book rules only
|
|
21
21
|
eval [<name|glob>] Run fixtures through a model adapter, report pass-rate
|
|
22
22
|
bundle [<dir>] Compile a prompts folder into an importable book module
|
|
23
|
-
view Start the local web viewer over the
|
|
23
|
+
view Start the local web viewer over the workspace (book switcher)
|
|
24
24
|
annotations <action> Drain the viewer's feedback queue: list | resolve <id> | clear
|
|
25
|
-
ls List compositions and fragments
|
|
25
|
+
ls List compositions and fragments (--all: cross-book inventory)
|
|
26
|
+
|
|
27
|
+
A <book>/<comp> operand addresses one book in a multi-book workspace; a bare
|
|
28
|
+
name resolves by uniqueness. With a single-book --dir, names work unqualified.
|
|
26
29
|
|
|
27
30
|
Options:
|
|
28
31
|
--dir <path> Prompts folder (default: promptbook.json promptsDir, else ./prompts)
|
|
@@ -42,6 +45,7 @@ Options:
|
|
|
42
45
|
--no-open view: do not open the browser after starting
|
|
43
46
|
--fragments ls: list fragments only
|
|
44
47
|
--compositions ls: list compositions only
|
|
48
|
+
--all ls/resolve: span every book in the workspace
|
|
45
49
|
-h, --help Show this help
|
|
46
50
|
-v, --version Show the version
|
|
47
51
|
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { basename, join, relative } from "node:path";
|
|
2
|
+
import type { PromptBook } from "@markbrutx/promptbook-core";
|
|
3
|
+
import { loadPrompts } from "@markbrutx/promptbook-core";
|
|
4
|
+
import type { IO } from "./io.js";
|
|
5
|
+
|
|
6
|
+
/** A discovered prompts book: its folder name and absolute directory. */
|
|
7
|
+
export interface Book {
|
|
8
|
+
name: string;
|
|
9
|
+
dir: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A workspace root and the books found directly under it (sorted by name). */
|
|
13
|
+
export interface Workspace {
|
|
14
|
+
root: string;
|
|
15
|
+
books: Book[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A `<book>/<comp>` operand split into its parts (`book` absent for bare names). */
|
|
19
|
+
export interface Address {
|
|
20
|
+
book?: string;
|
|
21
|
+
comp: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A bare/qualified operand resolved to a concrete book + composition name. */
|
|
25
|
+
export interface ResolvedAddress {
|
|
26
|
+
book: Book;
|
|
27
|
+
comp: string;
|
|
28
|
+
/** The matched book, already loaded during bare-name resolution (reuse to skip a reload). */
|
|
29
|
+
loaded?: PromptBook;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Folder names that are book internals or noise, never workspace sub-books. */
|
|
33
|
+
const NON_BOOK_DIRS = new Set(["node_modules", "fragments", "rules", "code-prompts"]);
|
|
34
|
+
|
|
35
|
+
/** Markers that make a folder a loadable book (a config or a loadable form). */
|
|
36
|
+
const BOOK_MARKERS = ["promptbook.json", "rules", "code-prompts"];
|
|
37
|
+
|
|
38
|
+
/** True when `dir` looks like a prompts book (has a config or a loadable form). */
|
|
39
|
+
export async function isBook(io: IO, dir: string): Promise<boolean> {
|
|
40
|
+
let entries: string[];
|
|
41
|
+
try {
|
|
42
|
+
entries = await io.fs.readDir(dir);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return BOOK_MARKERS.some((marker) => entries.includes(marker));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find the books directly under `rootDir`: each non-internal subfolder that is
|
|
51
|
+
* itself a book. One level deep, not recursive; sorted by name for a stable
|
|
52
|
+
* menu. Dot/underscore folders and book internals are skipped.
|
|
53
|
+
*/
|
|
54
|
+
export async function discoverBooks(io: IO, rootDir: string): Promise<Book[]> {
|
|
55
|
+
let entries: string[];
|
|
56
|
+
try {
|
|
57
|
+
entries = await io.fs.readDir(rootDir);
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const books: Book[] = [];
|
|
62
|
+
for (const name of [...entries].sort()) {
|
|
63
|
+
if (name.startsWith(".") || name.startsWith("_") || NON_BOOK_DIRS.has(name)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const dir = join(rootDir, name);
|
|
67
|
+
if (await isBook(io, dir)) {
|
|
68
|
+
books.push({ name, dir });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return books;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read a workspace from `rootDir`. When the root is itself a book it is the only
|
|
76
|
+
* book (back-compat single-book path, named after the folder); otherwise the
|
|
77
|
+
* root is a workspace of its sub-books.
|
|
78
|
+
*/
|
|
79
|
+
export async function loadWorkspace(io: IO, rootDir: string): Promise<Workspace> {
|
|
80
|
+
if (await isBook(io, rootDir)) {
|
|
81
|
+
return { root: rootDir, books: [{ name: basename(rootDir), dir: rootDir }] };
|
|
82
|
+
}
|
|
83
|
+
return { root: rootDir, books: await discoverBooks(io, rootDir) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Split an operand on the first `/`: `book/comp` (comp may itself contain `/`). */
|
|
87
|
+
export function parseAddress(operand: string): Address {
|
|
88
|
+
const slash = operand.indexOf("/");
|
|
89
|
+
if (slash === -1) {
|
|
90
|
+
return { comp: operand };
|
|
91
|
+
}
|
|
92
|
+
return { book: operand.slice(0, slash), comp: operand.slice(slash + 1) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A book's directory relative to the workspace root (`.` when it is the root). */
|
|
96
|
+
export function bookDir(workspace: Workspace, book: Book): string {
|
|
97
|
+
return relative(workspace.root, book.dir) || ".";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve an operand to a concrete book + composition. A `<book>/<comp>` prefix
|
|
102
|
+
* that names a known book addresses it directly; otherwise the whole operand is
|
|
103
|
+
* a bare composition name, resolved by uniqueness across the workspace's books
|
|
104
|
+
* (a single-book workspace always uses its one book). Throws a clear error when
|
|
105
|
+
* a bare name is missing or ambiguous.
|
|
106
|
+
*/
|
|
107
|
+
export async function resolveAddress(
|
|
108
|
+
io: IO,
|
|
109
|
+
workspace: Workspace,
|
|
110
|
+
operand: string,
|
|
111
|
+
): Promise<ResolvedAddress> {
|
|
112
|
+
const address = parseAddress(operand);
|
|
113
|
+
if (address.book !== undefined) {
|
|
114
|
+
const book = workspace.books.find((b) => b.name === address.book);
|
|
115
|
+
if (book !== undefined) {
|
|
116
|
+
return { book, comp: address.comp };
|
|
117
|
+
}
|
|
118
|
+
// Prefix names no book: fall through and treat the whole operand as a
|
|
119
|
+
// (possibly path-like) bare composition name.
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (workspace.books.length === 1) {
|
|
123
|
+
return { book: workspace.books[0] as Book, comp: operand };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const matches: { book: Book; loaded: PromptBook }[] = [];
|
|
127
|
+
for (const book of workspace.books) {
|
|
128
|
+
const loaded = await loadPrompts(book.dir, io.fs);
|
|
129
|
+
if (loaded.compositions.has(operand)) {
|
|
130
|
+
matches.push({ book, loaded });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const [first] = matches;
|
|
134
|
+
if (matches.length === 1 && first !== undefined) {
|
|
135
|
+
return { book: first.book, comp: operand, loaded: first.loaded };
|
|
136
|
+
}
|
|
137
|
+
if (matches.length === 0) {
|
|
138
|
+
throw new Error(`Unknown prompt "${operand}" in any book. Qualify it as <book>/<comp>.`);
|
|
139
|
+
}
|
|
140
|
+
const names = matches.map((m) => m.book.name).join(", ");
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Ambiguous prompt "${operand}"; found in ${matches.length} books (${names}). Qualify it as <book>/<comp>.`,
|
|
143
|
+
);
|
|
144
|
+
}
|