@bilig/create-workpaper 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @bilig/create-workpaper
2
+
3
+ Create a runnable Bilig WorkPaper starter for Node services and agent tools.
4
+
5
+ ```sh
6
+ npm create @bilig/workpaper@latest pricing-workpaper
7
+ cd pricing-workpaper
8
+ npm install
9
+ npm run smoke
10
+ ```
11
+
12
+ The generated starter builds a quote-approval workbook, writes inputs through an
13
+ API-style handler, recalculates formulas, persists JSON, restores the workbook,
14
+ and prints `verified: true`.
15
+
16
+ After the smoke proof passes, it also prints a star/bookmark link for the GitHub
17
+ repo: <https://github.com/proompteng/bilig/stargazers>.
18
+
19
+ Use this when you want to evaluate `@bilig/headless` from a blank directory
20
+ without cloning the full monorepo.
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { cp, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises'
3
+ import { dirname, join, relative, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
7
+ const templateRoot = join(packageRoot, 'template')
8
+
9
+ const args = process.argv.slice(2)
10
+ const wantsHelp = args.includes('--help') || args.includes('-h')
11
+ const force = args.includes('--force')
12
+ const targetArg = args.find((arg) => !arg.startsWith('-'))
13
+
14
+ if (wantsHelp) {
15
+ printHelp()
16
+ process.exit(0)
17
+ }
18
+
19
+ const targetDirectory = resolve(process.cwd(), targetArg ?? 'bilig-workpaper-starter')
20
+ const projectName = normalizePackageName(targetArg ?? 'bilig-workpaper-starter')
21
+
22
+ await ensureWritableTarget(targetDirectory, force)
23
+ await copyTemplate(targetDirectory, projectName)
24
+
25
+ console.log(`Created ${relative(process.cwd(), targetDirectory) || '.'}`)
26
+ console.log('')
27
+ console.log('Next:')
28
+ console.log(` cd ${relative(process.cwd(), targetDirectory) || '.'}`)
29
+ console.log(' npm install')
30
+ console.log(' npm run smoke')
31
+ console.log('')
32
+ console.log('Expected smoke output includes:')
33
+ console.log(' "verified": true')
34
+
35
+ function printHelp() {
36
+ console.log(`@bilig/create-workpaper
37
+
38
+ Usage:
39
+ npm create @bilig/workpaper@latest <directory>
40
+ npm exec @bilig/create-workpaper@latest <directory>
41
+
42
+ Options:
43
+ --force Allow writing into an existing directory.
44
+ -h, --help
45
+ `)
46
+ }
47
+
48
+ async function ensureWritableTarget(directory, allowExisting) {
49
+ try {
50
+ const existing = await stat(directory)
51
+ if (!existing.isDirectory()) {
52
+ throw new Error(`${directory} exists and is not a directory`)
53
+ }
54
+ const entries = await readdir(directory)
55
+ if (entries.length > 0 && !allowExisting) {
56
+ throw new Error(`${directory} is not empty. Re-run with --force to write into it.`)
57
+ }
58
+ } catch (error) {
59
+ if (error?.code === 'ENOENT') {
60
+ await mkdir(directory, { recursive: true })
61
+ return
62
+ }
63
+ throw error
64
+ }
65
+ }
66
+
67
+ async function copyTemplate(outputDirectory, packageName) {
68
+ await cp(templateRoot, outputDirectory, {
69
+ recursive: true,
70
+ filter: async (source) => {
71
+ const sourceStat = await stat(source)
72
+ if (sourceStat.isDirectory()) {
73
+ return true
74
+ }
75
+ const relativePath = relative(templateRoot, source)
76
+ const targetPath = join(outputDirectory, relativePath)
77
+ const text = await readFile(source, 'utf8')
78
+ await mkdir(dirname(targetPath), { recursive: true })
79
+ await writeFile(targetPath, text.replaceAll('__PROJECT_NAME__', packageName))
80
+ return false
81
+ },
82
+ })
83
+ }
84
+
85
+ function normalizePackageName(name) {
86
+ const parts = name.split(/[\\/]/)
87
+ let base = 'bilig-workpaper-starter'
88
+ for (let index = parts.length - 1; index >= 0; index -= 1) {
89
+ if (parts[index] !== '') {
90
+ base = parts[index]
91
+ break
92
+ }
93
+ }
94
+ const normalized = base
95
+ .toLowerCase()
96
+ .replace(/[^a-z0-9._-]+/g, '-')
97
+ .replace(/^-+|-+$/g, '')
98
+
99
+ return normalized || 'bilig-workpaper-starter'
100
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@bilig/create-workpaper",
3
+ "version": "0.17.0",
4
+ "description": "Create a runnable Bilig WorkPaper starter for Node services and agent tools.",
5
+ "keywords": [
6
+ "bilig",
7
+ "create",
8
+ "formula-engine",
9
+ "headless-spreadsheet",
10
+ "nodejs",
11
+ "spreadsheet-formulas",
12
+ "typescript",
13
+ "workbook-api",
14
+ "workpaper",
15
+ "xlsx-recalculation"
16
+ ],
17
+ "homepage": "https://proompteng.github.io/bilig/",
18
+ "bugs": {
19
+ "url": "https://github.com/proompteng/bilig/issues"
20
+ },
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/proompteng/bilig.git",
25
+ "directory": "packages/create-workpaper"
26
+ },
27
+ "bin": {
28
+ "create-workpaper": "bin/create-bilig-workpaper.js"
29
+ },
30
+ "files": [
31
+ "bin",
32
+ "template",
33
+ "README.md"
34
+ ],
35
+ "type": "module",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "smoke": "node ./bin/create-bilig-workpaper.js --help"
41
+ },
42
+ "engines": {
43
+ "node": ">=22.0.0"
44
+ }
45
+ }
@@ -0,0 +1,27 @@
1
+ # __PROJECT_NAME__
2
+
3
+ Formula-backed quote approval API built with `@bilig/headless`.
4
+
5
+ ```sh
6
+ npm install
7
+ npm run smoke
8
+ ```
9
+
10
+ The smoke run writes quote inputs, recalculates workbook formulas, persists the
11
+ WorkPaper as JSON, restores it, and checks that the restored formula output
12
+ matches the live output.
13
+
14
+ Expected output includes `verified: true`. After that verification passes, the
15
+ starter prints a repo star/bookmark link so Bilig is easier to find later.
16
+
17
+ Run a local API:
18
+
19
+ ```sh
20
+ npm run dev
21
+ curl http://localhost:8788/api/quote/approval
22
+ curl -X POST http://localhost:8788/api/quote/approval \
23
+ -H 'content-type: application/json' \
24
+ -d '{"units":40,"listPrice":1200,"discount":0.05,"unitCost":760,"minimumMargin":0.3}'
25
+ ```
26
+
27
+ Learn more: <https://github.com/proompteng/bilig>
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx src/index.ts --serve",
7
+ "smoke": "tsx src/index.ts",
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "dependencies": {
11
+ "@bilig/headless": "latest"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "latest",
15
+ "tsx": "latest",
16
+ "typescript": "latest"
17
+ }
18
+ }
@@ -0,0 +1,321 @@
1
+ import { createServer, type IncomingMessage } from 'node:http'
2
+ import { pathToFileURL } from 'node:url'
3
+
4
+ import {
5
+ WorkPaper,
6
+ createWorkPaperFromDocument,
7
+ exportWorkPaperDocument,
8
+ parseWorkPaperDocument,
9
+ serializeWorkPaperDocument,
10
+ } from '@bilig/headless'
11
+
12
+ type WorkPaperInstance = ReturnType<typeof WorkPaper.buildFromSheets>
13
+
14
+ type QuoteInput = {
15
+ units: number
16
+ listPrice: number
17
+ discount: number
18
+ unitCost: number
19
+ minimumMargin: number
20
+ }
21
+
22
+ type QuoteSummary = {
23
+ listRevenue: number
24
+ discountAmount: number
25
+ netRevenue: number
26
+ totalCost: number
27
+ grossMargin: number
28
+ decision: string
29
+ }
30
+
31
+ type WorkbookStorage = {
32
+ loadWorkbookJson(): Promise<string> | string
33
+ saveWorkbookJson(nextWorkbookJson: string): Promise<void> | void
34
+ }
35
+
36
+ const inputCells = {
37
+ units: 'Inputs!B2',
38
+ listPrice: 'Inputs!B3',
39
+ discount: 'Inputs!B4',
40
+ unitCost: 'Inputs!B5',
41
+ minimumMargin: 'Inputs!B6',
42
+ } as const
43
+
44
+ export function createQuoteApprovalRequestHandler(storage: WorkbookStorage) {
45
+ return async function handleQuoteApprovalRequest(request: Request): Promise<Response> {
46
+ const url = new URL(request.url)
47
+
48
+ if (request.method === 'GET' && url.pathname === '/api/quote/approval') {
49
+ const workbook = await loadWorkbook(storage)
50
+ return json({ summary: readQuoteSummary(workbook), inputCells })
51
+ }
52
+
53
+ if (request.method === 'POST' && url.pathname === '/api/quote/approval') {
54
+ try {
55
+ const input = parseQuoteInput(await request.json())
56
+ const workbook = await loadWorkbook(storage)
57
+ const before = readQuoteSummary(workbook)
58
+ writeQuoteInputs(workbook, input)
59
+ const after = readQuoteSummary(workbook)
60
+ const workbookJson = serializeWorkbook(workbook)
61
+ await storage.saveWorkbookJson(workbookJson)
62
+
63
+ const restored = createWorkPaperFromDocument(parseWorkPaperDocument(workbookJson))
64
+ const restoredSummary = readQuoteSummary(restored)
65
+
66
+ return json({
67
+ input,
68
+ inputCells,
69
+ before,
70
+ after,
71
+ restored: restoredSummary,
72
+ checks: {
73
+ decisionChanged: before.decision !== after.decision,
74
+ formulasPersisted: workbookJson.includes('=IF(B6>=Inputs!B6'),
75
+ restoredMatchesAfter: JSON.stringify(restoredSummary) === JSON.stringify(after),
76
+ serializedBytes: Buffer.byteLength(workbookJson, 'utf8'),
77
+ },
78
+ })
79
+ } catch (error) {
80
+ return json({ error: error instanceof Error ? error.message : String(error) }, 400)
81
+ }
82
+ }
83
+
84
+ return json({ error: 'not found' }, 404)
85
+ }
86
+ }
87
+
88
+ function createQuoteApprovalWorkbook(): WorkPaperInstance {
89
+ return WorkPaper.buildFromSheets({
90
+ Inputs: [
91
+ ['Metric', 'Value'],
92
+ ['Units', 40],
93
+ ['List price', 1200],
94
+ ['Discount', 0.1],
95
+ ['Unit cost', 760],
96
+ ['Minimum margin', 0.3],
97
+ ],
98
+ Summary: [
99
+ ['Metric', 'Value'],
100
+ ['List revenue', '=Inputs!B2*Inputs!B3'],
101
+ ['Discount amount', '=B2*Inputs!B4'],
102
+ ['Net revenue', '=B2-B3'],
103
+ ['Total cost', '=Inputs!B2*Inputs!B5'],
104
+ ['Gross margin', '=(B4-B5)/B4'],
105
+ ['Decision', '=IF(B6>=Inputs!B6,"approved","review")'],
106
+ ],
107
+ })
108
+ }
109
+
110
+ function createMemoryStorage(): WorkbookStorage {
111
+ let workbookJson = serializeWorkbook(createQuoteApprovalWorkbook())
112
+ return {
113
+ loadWorkbookJson() {
114
+ return workbookJson
115
+ },
116
+ saveWorkbookJson(nextWorkbookJson) {
117
+ workbookJson = nextWorkbookJson
118
+ },
119
+ }
120
+ }
121
+
122
+ function writeQuoteInputs(workbook: WorkPaperInstance, input: QuoteInput): void {
123
+ const sheet = requireSheet(workbook, 'Inputs')
124
+ workbook.setCellContents({ sheet, row: 1, col: 1 }, input.units)
125
+ workbook.setCellContents({ sheet, row: 2, col: 1 }, input.listPrice)
126
+ workbook.setCellContents({ sheet, row: 3, col: 1 }, input.discount)
127
+ workbook.setCellContents({ sheet, row: 4, col: 1 }, input.unitCost)
128
+ workbook.setCellContents({ sheet, row: 5, col: 1 }, input.minimumMargin)
129
+ }
130
+
131
+ async function loadWorkbook(storage: WorkbookStorage): Promise<WorkPaperInstance> {
132
+ return createWorkPaperFromDocument(parseWorkPaperDocument(await storage.loadWorkbookJson()))
133
+ }
134
+
135
+ function serializeWorkbook(workbook: WorkPaperInstance): string {
136
+ return serializeWorkPaperDocument(exportWorkPaperDocument(workbook, { includeConfig: true }))
137
+ }
138
+
139
+ function readQuoteSummary(workbook: WorkPaperInstance): QuoteSummary {
140
+ const sheet = requireSheet(workbook, 'Summary')
141
+ return {
142
+ listRevenue: readNumber(workbook, sheet, 1, 1, 'List revenue'),
143
+ discountAmount: readNumber(workbook, sheet, 2, 1, 'Discount amount'),
144
+ netRevenue: readNumber(workbook, sheet, 3, 1, 'Net revenue'),
145
+ totalCost: readNumber(workbook, sheet, 4, 1, 'Total cost'),
146
+ grossMargin: readRoundedNumber(workbook, sheet, 5, 1, 'Gross margin'),
147
+ decision: readString(workbook, sheet, 6, 1, 'Decision'),
148
+ }
149
+ }
150
+
151
+ function parseQuoteInput(value: unknown): QuoteInput {
152
+ const record = readRecord(value, 'request body')
153
+ return {
154
+ units: readBoundedNumber(record.units, 'units', 1),
155
+ listPrice: readBoundedNumber(record.listPrice, 'listPrice', 0),
156
+ discount: readBoundedNumber(record.discount, 'discount', 0, 0.95),
157
+ unitCost: readBoundedNumber(record.unitCost, 'unitCost', 0),
158
+ minimumMargin: readBoundedNumber(record.minimumMargin, 'minimumMargin', 0, 1),
159
+ }
160
+ }
161
+
162
+ function requireSheet(workbook: WorkPaperInstance, name: string): number {
163
+ const sheet = workbook.getSheetId(name)
164
+ if (sheet === undefined) {
165
+ throw new Error(`missing sheet: ${name}`)
166
+ }
167
+ return sheet
168
+ }
169
+
170
+ function readNumber(workbook: WorkPaperInstance, sheet: number, row: number, col: number, label: string): number {
171
+ return Math.round(readCellNumber(workbook, sheet, row, col, label) * 100) / 100
172
+ }
173
+
174
+ function readRoundedNumber(workbook: WorkPaperInstance, sheet: number, row: number, col: number, label: string): number {
175
+ return Math.round(readCellNumber(workbook, sheet, row, col, label) * 10_000) / 10_000
176
+ }
177
+
178
+ function readCellNumber(workbook: WorkPaperInstance, sheet: number, row: number, col: number, label: string): number {
179
+ const cell: unknown = workbook.getCellValue({ sheet, row, col })
180
+ if (!isRecord(cell) || typeof cell.value !== 'number') {
181
+ throw new Error(`expected ${label} to be numeric, received ${JSON.stringify(cell)}`)
182
+ }
183
+ return cell.value
184
+ }
185
+
186
+ function readString(workbook: WorkPaperInstance, sheet: number, row: number, col: number, label: string): string {
187
+ const cell: unknown = workbook.getCellValue({ sheet, row, col })
188
+ if (!isRecord(cell) || typeof cell.value !== 'string') {
189
+ throw new Error(`expected ${label} to be text, received ${JSON.stringify(cell)}`)
190
+ }
191
+ return cell.value
192
+ }
193
+
194
+ function readBoundedNumber(value: unknown, label: string, min: number, max = Number.POSITIVE_INFINITY): number {
195
+ const numberValue = Number(value)
196
+ if (!Number.isFinite(numberValue) || numberValue < min || numberValue > max) {
197
+ throw new Error(`${label} must be a finite number between ${min.toString()} and ${max.toString()}`)
198
+ }
199
+ return numberValue
200
+ }
201
+
202
+ function readRecord(value: unknown, label: string): Record<string, unknown> {
203
+ if (!isRecord(value)) {
204
+ throw new Error(`${label} must be a JSON object`)
205
+ }
206
+ return value
207
+ }
208
+
209
+ function isRecord(value: unknown): value is Record<string, unknown> {
210
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
211
+ }
212
+
213
+ function json(payload: unknown, status = 200): Response {
214
+ return Response.json(payload, { status, headers: { 'cache-control': 'no-store' } })
215
+ }
216
+
217
+ async function runSmoke(): Promise<void> {
218
+ const handler = createQuoteApprovalRequestHandler(createMemoryStorage())
219
+ const before = await requestJson(handler, '/api/quote/approval')
220
+ const edit = await requestJson(handler, '/api/quote/approval', {
221
+ method: 'POST',
222
+ headers: { 'content-type': 'application/json' },
223
+ body: JSON.stringify({
224
+ units: 40,
225
+ listPrice: 1200,
226
+ discount: 0.05,
227
+ unitCost: 760,
228
+ minimumMargin: 0.3,
229
+ }),
230
+ })
231
+
232
+ const output = {
233
+ before,
234
+ edit,
235
+ verified: true,
236
+ nextStep:
237
+ 'If this proof matches your service or agent workflow, star or bookmark Bilig: https://github.com/proompteng/bilig/stargazers',
238
+ }
239
+ assertSmokeOutput(output)
240
+ console.log(JSON.stringify(output, null, 2))
241
+ }
242
+
243
+ async function requestJson(handler: (request: Request) => Promise<Response>, path: string, init?: RequestInit): Promise<unknown> {
244
+ const response = await handler(new Request(`http://localhost:8788${path}`, init))
245
+ const body: unknown = await response.json()
246
+ if (!response.ok) {
247
+ throw new Error(`request failed: ${response.status.toString()} ${JSON.stringify(body)}`)
248
+ }
249
+ return body
250
+ }
251
+
252
+ function assertSmokeOutput(value: unknown): void {
253
+ const output = readRecord(value, 'smoke output')
254
+ const edit = readRecord(output.edit, 'smoke edit')
255
+ const checks = readRecord(edit.checks, 'smoke checks')
256
+ const after = readRecord(edit.after, 'smoke after')
257
+ const restored = readRecord(edit.restored, 'smoke restored')
258
+
259
+ if (
260
+ output.verified !== true ||
261
+ after.decision !== 'approved' ||
262
+ JSON.stringify(after) !== JSON.stringify(restored) ||
263
+ checks.decisionChanged !== true ||
264
+ checks.formulasPersisted !== true ||
265
+ checks.restoredMatchesAfter !== true ||
266
+ Number(checks.serializedBytes) <= 0
267
+ ) {
268
+ throw new Error(`unexpected smoke output: ${JSON.stringify(value)}`)
269
+ }
270
+ }
271
+
272
+ async function toWebRequest(incoming: IncomingMessage): Promise<Request> {
273
+ const origin = `http://${incoming.headers.host ?? 'localhost:8788'}`
274
+ const headers = new Headers()
275
+
276
+ for (const [name, value] of Object.entries(incoming.headers)) {
277
+ if (Array.isArray(value)) {
278
+ headers.set(name, value.join(', '))
279
+ } else if (value !== undefined) {
280
+ headers.set(name, value)
281
+ }
282
+ }
283
+
284
+ return new Request(new URL(incoming.url ?? '/', origin), {
285
+ method: incoming.method,
286
+ headers,
287
+ body: incoming.method === 'GET' || incoming.method === 'HEAD' ? undefined : await readIncomingBody(incoming),
288
+ duplex: 'half',
289
+ } as RequestInit & { duplex: 'half' })
290
+ }
291
+
292
+ function readIncomingBody(incoming: IncomingMessage): Promise<string> {
293
+ return new Promise((resolve, reject) => {
294
+ const chunks: Uint8Array[] = []
295
+ incoming.on('data', (chunk: Buffer | string) => {
296
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
297
+ })
298
+ incoming.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
299
+ incoming.on('error', reject)
300
+ })
301
+ }
302
+
303
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
304
+ if (process.argv.includes('--serve')) {
305
+ const handler = createQuoteApprovalRequestHandler(createMemoryStorage())
306
+ createServer(async (incoming, outgoing) => {
307
+ try {
308
+ const response = await handler(await toWebRequest(incoming))
309
+ outgoing.writeHead(response.status, Object.fromEntries(response.headers))
310
+ outgoing.end(Buffer.from(await response.arrayBuffer()))
311
+ } catch (error) {
312
+ outgoing.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
313
+ outgoing.end(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`)
314
+ }
315
+ }).listen(8788, () => {
316
+ console.log('Quote approval WorkPaper API listening on http://localhost:8788')
317
+ })
318
+ } else {
319
+ await runSmoke()
320
+ }
321
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "types": ["node"]
10
+ },
11
+ "include": ["src/**/*.ts"]
12
+ }