burnless 0.1.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/index.js ADDED
@@ -0,0 +1,1550 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __createRequire } from 'node:module';
3
+ import { fileURLToPath as __fileURLToPath } from 'node:url';
4
+ import { dirname as __pathDirname } from 'node:path';
5
+ const require = __createRequire(import.meta.url);
6
+ const __filename = __fileURLToPath(import.meta.url);
7
+ const __dirname = __pathDirname(__filename);
8
+ import {
9
+ pAdd,
10
+ pDisable,
11
+ pEnable,
12
+ pList,
13
+ pRemove,
14
+ pSetDefault,
15
+ pSetKey,
16
+ resolveProviderForTest,
17
+ resolveProviderId,
18
+ verifyConnection
19
+ } from "./chunk-I5ODYEMY.js";
20
+ import {
21
+ readSecret
22
+ } from "./chunk-M7YWG4WP.js";
23
+ import {
24
+ instanceEnvPath,
25
+ loadInstanceEnv,
26
+ readInstanceEnv,
27
+ setInstanceEnvVar
28
+ } from "./chunk-CFKTW5EF.js";
29
+ import {
30
+ BRAND,
31
+ DIRTY,
32
+ EMPTY_PATH,
33
+ INVALID,
34
+ NEVER,
35
+ OK,
36
+ ParseStatus,
37
+ ZodAny,
38
+ ZodArray,
39
+ ZodBigInt,
40
+ ZodBoolean,
41
+ ZodBranded,
42
+ ZodCatch,
43
+ ZodDate,
44
+ ZodDefault,
45
+ ZodDiscriminatedUnion,
46
+ ZodEffects,
47
+ ZodEnum,
48
+ ZodError,
49
+ ZodFirstPartyTypeKind,
50
+ ZodFunction,
51
+ ZodIntersection,
52
+ ZodIssueCode,
53
+ ZodLazy,
54
+ ZodLiteral,
55
+ ZodMap,
56
+ ZodNaN,
57
+ ZodNativeEnum,
58
+ ZodNever,
59
+ ZodNull,
60
+ ZodNullable,
61
+ ZodNumber,
62
+ ZodObject,
63
+ ZodOptional,
64
+ ZodParsedType,
65
+ ZodPipeline,
66
+ ZodPromise,
67
+ ZodReadonly,
68
+ ZodRecord,
69
+ ZodSet,
70
+ ZodString,
71
+ ZodSymbol,
72
+ ZodTuple,
73
+ ZodType,
74
+ ZodUndefined,
75
+ ZodUnion,
76
+ ZodUnknown,
77
+ ZodVoid,
78
+ addIssueToContext,
79
+ anyType,
80
+ arrayType,
81
+ bigIntType,
82
+ booleanType,
83
+ buildRemoteProgram,
84
+ coerce,
85
+ custom,
86
+ dateType,
87
+ datetimeRegex,
88
+ dim,
89
+ discriminatedUnionType,
90
+ effectsType,
91
+ en_default,
92
+ enumType,
93
+ functionType,
94
+ getErrorMap,
95
+ getParsedType,
96
+ green,
97
+ instanceOfType,
98
+ intersectionType,
99
+ isAborted,
100
+ isAsync,
101
+ isDirty,
102
+ isValid,
103
+ late,
104
+ lazyType,
105
+ literalType,
106
+ makeIssue,
107
+ mapType,
108
+ nanType,
109
+ nativeEnumType,
110
+ neverType,
111
+ nullType,
112
+ nullableType,
113
+ numberType,
114
+ objectType,
115
+ objectUtil,
116
+ oboolean,
117
+ onumber,
118
+ optionalType,
119
+ ostring,
120
+ pipelineType,
121
+ preprocessType,
122
+ promiseType,
123
+ quotelessJson,
124
+ recordType,
125
+ red,
126
+ renderBanner,
127
+ runAction,
128
+ setErrorMap,
129
+ setType,
130
+ strictObjectType,
131
+ stringType,
132
+ symbolType,
133
+ topVerb,
134
+ tupleType,
135
+ undefinedType,
136
+ unionType,
137
+ unknownType,
138
+ util,
139
+ versionString,
140
+ voidType
141
+ } from "./chunk-L24B5MJD.js";
142
+ import {
143
+ ensureArtifact,
144
+ flipCurrent,
145
+ versionsDir
146
+ } from "./chunk-N7IP6VPJ.js";
147
+ import "./chunk-BN3QFA6Z.js";
148
+ import {
149
+ UsageError
150
+ } from "./chunk-VKDF3OUE.js";
151
+ import {
152
+ PROVIDER_KINDS,
153
+ isKnownKind
154
+ } from "./chunk-OYCC45VC.js";
155
+ import {
156
+ addAiProviderModel,
157
+ applyMigrations,
158
+ closeDatabase,
159
+ createClient,
160
+ createUser,
161
+ getOwnerUser,
162
+ initDatabase,
163
+ isDatabaseBooted,
164
+ isOwnerClaimed,
165
+ listAiProviderModels,
166
+ listUsers,
167
+ resolveDriver,
168
+ setDefaultAiProviderModel,
169
+ setUserPassword
170
+ } from "./chunk-KSP7MJZ3.js";
171
+ import "./chunk-IBT7EFKB.js";
172
+ import "./chunk-MHVBJUPX.js";
173
+ import {
174
+ sql
175
+ } from "./chunk-RDP4RLLI.js";
176
+ import {
177
+ __export
178
+ } from "./chunk-BMDIVUSL.js";
179
+
180
+ // src/index.ts
181
+ import { pathToFileURL } from "url";
182
+
183
+ // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
184
+ var external_exports = {};
185
+ __export(external_exports, {
186
+ BRAND: () => BRAND,
187
+ DIRTY: () => DIRTY,
188
+ EMPTY_PATH: () => EMPTY_PATH,
189
+ INVALID: () => INVALID,
190
+ NEVER: () => NEVER,
191
+ OK: () => OK,
192
+ ParseStatus: () => ParseStatus,
193
+ Schema: () => ZodType,
194
+ ZodAny: () => ZodAny,
195
+ ZodArray: () => ZodArray,
196
+ ZodBigInt: () => ZodBigInt,
197
+ ZodBoolean: () => ZodBoolean,
198
+ ZodBranded: () => ZodBranded,
199
+ ZodCatch: () => ZodCatch,
200
+ ZodDate: () => ZodDate,
201
+ ZodDefault: () => ZodDefault,
202
+ ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
203
+ ZodEffects: () => ZodEffects,
204
+ ZodEnum: () => ZodEnum,
205
+ ZodError: () => ZodError,
206
+ ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
207
+ ZodFunction: () => ZodFunction,
208
+ ZodIntersection: () => ZodIntersection,
209
+ ZodIssueCode: () => ZodIssueCode,
210
+ ZodLazy: () => ZodLazy,
211
+ ZodLiteral: () => ZodLiteral,
212
+ ZodMap: () => ZodMap,
213
+ ZodNaN: () => ZodNaN,
214
+ ZodNativeEnum: () => ZodNativeEnum,
215
+ ZodNever: () => ZodNever,
216
+ ZodNull: () => ZodNull,
217
+ ZodNullable: () => ZodNullable,
218
+ ZodNumber: () => ZodNumber,
219
+ ZodObject: () => ZodObject,
220
+ ZodOptional: () => ZodOptional,
221
+ ZodParsedType: () => ZodParsedType,
222
+ ZodPipeline: () => ZodPipeline,
223
+ ZodPromise: () => ZodPromise,
224
+ ZodReadonly: () => ZodReadonly,
225
+ ZodRecord: () => ZodRecord,
226
+ ZodSchema: () => ZodType,
227
+ ZodSet: () => ZodSet,
228
+ ZodString: () => ZodString,
229
+ ZodSymbol: () => ZodSymbol,
230
+ ZodTransformer: () => ZodEffects,
231
+ ZodTuple: () => ZodTuple,
232
+ ZodType: () => ZodType,
233
+ ZodUndefined: () => ZodUndefined,
234
+ ZodUnion: () => ZodUnion,
235
+ ZodUnknown: () => ZodUnknown,
236
+ ZodVoid: () => ZodVoid,
237
+ addIssueToContext: () => addIssueToContext,
238
+ any: () => anyType,
239
+ array: () => arrayType,
240
+ bigint: () => bigIntType,
241
+ boolean: () => booleanType,
242
+ coerce: () => coerce,
243
+ custom: () => custom,
244
+ date: () => dateType,
245
+ datetimeRegex: () => datetimeRegex,
246
+ defaultErrorMap: () => en_default,
247
+ discriminatedUnion: () => discriminatedUnionType,
248
+ effect: () => effectsType,
249
+ enum: () => enumType,
250
+ function: () => functionType,
251
+ getErrorMap: () => getErrorMap,
252
+ getParsedType: () => getParsedType,
253
+ instanceof: () => instanceOfType,
254
+ intersection: () => intersectionType,
255
+ isAborted: () => isAborted,
256
+ isAsync: () => isAsync,
257
+ isDirty: () => isDirty,
258
+ isValid: () => isValid,
259
+ late: () => late,
260
+ lazy: () => lazyType,
261
+ literal: () => literalType,
262
+ makeIssue: () => makeIssue,
263
+ map: () => mapType,
264
+ nan: () => nanType,
265
+ nativeEnum: () => nativeEnumType,
266
+ never: () => neverType,
267
+ null: () => nullType,
268
+ nullable: () => nullableType,
269
+ number: () => numberType,
270
+ object: () => objectType,
271
+ objectUtil: () => objectUtil,
272
+ oboolean: () => oboolean,
273
+ onumber: () => onumber,
274
+ optional: () => optionalType,
275
+ ostring: () => ostring,
276
+ pipeline: () => pipelineType,
277
+ preprocess: () => preprocessType,
278
+ promise: () => promiseType,
279
+ quotelessJson: () => quotelessJson,
280
+ record: () => recordType,
281
+ set: () => setType,
282
+ setErrorMap: () => setErrorMap,
283
+ strictObject: () => strictObjectType,
284
+ string: () => stringType,
285
+ symbol: () => symbolType,
286
+ transformer: () => effectsType,
287
+ tuple: () => tupleType,
288
+ undefined: () => undefinedType,
289
+ union: () => unionType,
290
+ unknown: () => unknownType,
291
+ util: () => util,
292
+ void: () => voidType
293
+ });
294
+
295
+ // src/local/artifact.ts
296
+ import { existsSync } from "fs";
297
+ import { dirname, join, resolve } from "path";
298
+ import { fileURLToPath } from "url";
299
+
300
+ // src/local/artifact-layout.ts
301
+ var ARTIFACT_MARKER = ".burnless-artifact";
302
+ var ARTIFACT_LAYOUT = {
303
+ /** tsup CLI bundle entry (both faces). */
304
+ cliEntry: "cli/index.js",
305
+ /** Next standalone server entry → BURNLESS_SERVER_ENTRY. */
306
+ serverEntry: "web/apps/web/server.js",
307
+ /** drizzle migrations dir → BURNLESS_MIGRATIONS_DIR. */
308
+ migrationsDir: "drizzle",
309
+ /** pgvector loadable-extension tarball → BURNLESS_PGLITE_VECTOR_BUNDLE. */
310
+ vectorBundle: "node_modules/@electric-sql/pglite-pgvector/dist/vector.tar.gz",
311
+ /** Optional launcher-managed Node the installer (P5) may stage; else system Node. */
312
+ managedNode: "runtime/bin/node",
313
+ /** POSIX launcher at the artifact root → what bin/burnless + delegateToArtifact exec. */
314
+ launcher: "burnless"
315
+ };
316
+
317
+ // src/local/artifact.ts
318
+ function detectArtifactRoot(entryUrl = import.meta.url) {
319
+ let here;
320
+ try {
321
+ here = dirname(fileURLToPath(entryUrl));
322
+ } catch {
323
+ return null;
324
+ }
325
+ const root = resolve(here, "..");
326
+ return existsSync(join(root, ARTIFACT_MARKER)) ? root : null;
327
+ }
328
+ function prepareArtifactEnv(opts = {}) {
329
+ const env = opts.env ?? process.env;
330
+ const root = detectArtifactRoot(opts.entryUrl);
331
+ if (!root) return;
332
+ const setIfUnset = (key, value) => {
333
+ if (!env[key]?.trim()) env[key] = value;
334
+ };
335
+ setIfUnset("BURNLESS_MIGRATIONS_DIR", join(root, ARTIFACT_LAYOUT.migrationsDir));
336
+ setIfUnset("BURNLESS_PGLITE_VECTOR_BUNDLE", join(root, ARTIFACT_LAYOUT.vectorBundle));
337
+ setIfUnset("BURNLESS_SERVER_ENTRY", join(root, ARTIFACT_LAYOUT.serverEntry));
338
+ }
339
+ function resolveNodeBinary(opts = {}) {
340
+ const env = opts.env ?? process.env;
341
+ const override = env.BURNLESS_NODE?.trim();
342
+ if (override) return override;
343
+ const root = detectArtifactRoot(opts.entryUrl);
344
+ if (root) {
345
+ const homeManaged = resolve(root, "..", "..", ARTIFACT_LAYOUT.managedNode);
346
+ if (existsSync(homeManaged)) return homeManaged;
347
+ const managed = join(root, ARTIFACT_LAYOUT.managedNode);
348
+ if (existsSync(managed)) return managed;
349
+ }
350
+ return process.execPath;
351
+ }
352
+
353
+ // src/local/ai-model-ops.ts
354
+ async function withDb(fn) {
355
+ const owned = !isDatabaseBooted();
356
+ await initDatabase();
357
+ try {
358
+ return await fn();
359
+ } finally {
360
+ if (owned) await closeDatabase();
361
+ }
362
+ }
363
+ async function mList(providerName) {
364
+ return withDb(async () => {
365
+ const { id } = await resolveProviderId(providerName);
366
+ return listAiProviderModels(id);
367
+ });
368
+ }
369
+ async function mAdd(providerName, modelId) {
370
+ return withDb(async () => {
371
+ const { id } = await resolveProviderId(providerName);
372
+ return addAiProviderModel(id, { modelId, source: "manual" });
373
+ });
374
+ }
375
+ async function mDefault(providerName, modelId) {
376
+ await withDb(async () => {
377
+ const { id: providerId } = await resolveProviderId(providerName);
378
+ const row = (await listAiProviderModels(providerId)).find((m) => m.modelId === modelId);
379
+ if (!row) throw new UsageError(`Provider "${providerName}" has no model "${modelId}". Add it with \`burnless model add\`.`);
380
+ await setDefaultAiProviderModel(row.id, providerId);
381
+ });
382
+ }
383
+
384
+ // src/commands/ai-config.ts
385
+ function isLocalProfile(baseUrl) {
386
+ let host;
387
+ try {
388
+ host = new URL(baseUrl).hostname;
389
+ } catch {
390
+ return false;
391
+ }
392
+ return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
393
+ }
394
+ function assertLocalProfile(baseUrl) {
395
+ if (isLocalProfile(baseUrl)) return;
396
+ throw new UsageError(
397
+ "AI provider config from the CLI manages the LOCAL instance only. For a remote/cloud instance, use its Settings \u2192 AI tab. (Remote CLI provider config is a planned follow-up.)"
398
+ );
399
+ }
400
+ function prepLocal() {
401
+ prepareArtifactEnv();
402
+ loadInstanceEnv({ env: process.env });
403
+ }
404
+ function registerProvider(program) {
405
+ const provider = program.command("provider").description("Manage AI providers (local instance)");
406
+ provider.command("list").action(async (_opts, cmd) => {
407
+ await runAction(cmd, async (ctx) => {
408
+ assertLocalProfile(ctx.profile.baseUrl);
409
+ prepLocal();
410
+ const rows = await pList();
411
+ if (ctx.json) process.stdout.write(JSON.stringify({ providers: rows }) + "\n");
412
+ else
413
+ for (const p of rows)
414
+ process.stderr.write(
415
+ `${p.name} [${p.kind}]${p.isDefault ? " *default" : ""}${p.enabled ? "" : " (disabled)"} \u2014 ${p.apiKeySet ? "key set" : "no key"}, ${p.modelCount} model(s)
416
+ `
417
+ );
418
+ }, { allowMissingProfile: true });
419
+ });
420
+ provider.command("add").argument("<name>", "a label for this provider").requiredOption("--kind <kind>", `provider kind (${PROVIDER_KINDS.join("|")})`).option("--base-url <url>", "API base URL (prefilled for openrouter/ollama)").option("--key-stdin", "read the API key from stdin (else a masked prompt; omit for keyless e.g. ollama)").option("--no-key", "create without an API key (e.g. ollama)").action(async (name, opts, cmd) => {
421
+ await runAction(cmd, async (ctx) => {
422
+ assertLocalProfile(ctx.profile.baseUrl);
423
+ prepLocal();
424
+ if (!isKnownKind(opts.kind)) throw new UsageError(`Unknown kind "${opts.kind}" (expected ${PROVIDER_KINDS.join("|")}).`);
425
+ const kind = opts.kind;
426
+ let apiKey;
427
+ if (opts.key !== false) {
428
+ apiKey = await readSecret({ stdin: opts.keyStdin, label: "API key: " });
429
+ if (!apiKey) throw new UsageError("Empty API key \u2014 pass --no-key for a keyless provider.");
430
+ }
431
+ const created = await pAdd({ name, kind, baseUrl: opts.baseUrl, apiKey });
432
+ if (ctx.json) process.stdout.write(JSON.stringify({ id: created.id, name: created.name }) + "\n");
433
+ else process.stderr.write(`Added provider ${created.name} (${created.kind})${created.isDefault ? ", set as default" : ""}.
434
+ `);
435
+ }, { allowMissingProfile: true });
436
+ });
437
+ provider.command("test").argument("<name>").action(async (name, _opts, cmd) => {
438
+ await runAction(cmd, async (ctx) => {
439
+ assertLocalProfile(ctx.profile.baseUrl);
440
+ prepLocal();
441
+ const { baseUrl, apiKey } = await resolveProviderForTest(name);
442
+ if (!baseUrl) throw new UsageError(`Provider "${name}" has no base URL to test (vendor providers test from the UI).`);
443
+ const r = await verifyConnection({ baseUrl, apiKey });
444
+ if (ctx.json) process.stdout.write(JSON.stringify(r) + "\n");
445
+ else process.stderr.write(`${r.ok ? "OK" : "FAILED"}: ${r.detail}
446
+ `);
447
+ if (!r.ok) process.exitCode = 1;
448
+ }, { allowMissingProfile: true });
449
+ });
450
+ for (const [verb, fn, msg] of [
451
+ ["enable", pEnable, "enabled"],
452
+ ["disable", pDisable, "disabled"],
453
+ ["default", pSetDefault, "set as default"]
454
+ ]) {
455
+ provider.command(verb).argument("<name>").action(async (name, _opts, cmd) => {
456
+ await runAction(cmd, async (ctx) => {
457
+ assertLocalProfile(ctx.profile.baseUrl);
458
+ prepLocal();
459
+ await fn(name);
460
+ process.stderr.write(`Provider ${name} ${msg}.
461
+ `);
462
+ }, { allowMissingProfile: true });
463
+ });
464
+ }
465
+ provider.command("remove").argument("<name>").action(async (name, _opts, cmd) => {
466
+ await runAction(cmd, async (ctx) => {
467
+ assertLocalProfile(ctx.profile.baseUrl);
468
+ prepLocal();
469
+ const ok = await pRemove(name);
470
+ process.stderr.write(ok ? `Removed provider ${name}.
471
+ ` : `No provider named ${name}.
472
+ `);
473
+ }, { allowMissingProfile: true });
474
+ });
475
+ }
476
+ function registerKey(program) {
477
+ const key = program.command("key").description("Manage AI provider API keys (local instance)");
478
+ key.command("set").argument("<provider>", "provider name").option("--stdin", "read the key from stdin instead of a prompt").action(async (providerName, opts, cmd) => {
479
+ await runAction(cmd, async (ctx) => {
480
+ assertLocalProfile(ctx.profile.baseUrl);
481
+ prepLocal();
482
+ const apiKey = await readSecret({ stdin: opts.stdin, label: "API key: " });
483
+ if (!apiKey) throw new UsageError("Empty key \u2014 aborted.");
484
+ await pSetKey(providerName, apiKey);
485
+ process.stderr.write(`Key set for ${providerName}.
486
+ `);
487
+ }, { allowMissingProfile: true });
488
+ });
489
+ }
490
+ function registerModel(program) {
491
+ const model = program.command("model").description("Manage AI provider models (local instance)");
492
+ model.command("list").argument("<provider>", "provider name").action(async (providerName, _opts, cmd) => {
493
+ await runAction(cmd, async (ctx) => {
494
+ assertLocalProfile(ctx.profile.baseUrl);
495
+ prepLocal();
496
+ const rows = await mList(providerName);
497
+ if (ctx.json) process.stdout.write(JSON.stringify({ models: rows }) + "\n");
498
+ else for (const m of rows) process.stderr.write(`${m.modelId}${m.isDefault ? " *default" : ""} [${m.source}]
499
+ `);
500
+ }, { allowMissingProfile: true });
501
+ });
502
+ model.command("add").argument("<provider>").argument("<modelId>").action(async (providerName, modelId, _opts, cmd) => {
503
+ await runAction(cmd, async (ctx) => {
504
+ assertLocalProfile(ctx.profile.baseUrl);
505
+ prepLocal();
506
+ await mAdd(providerName, modelId);
507
+ process.stderr.write(`Added model ${modelId} to ${providerName}.
508
+ `);
509
+ }, { allowMissingProfile: true });
510
+ });
511
+ model.command("default").argument("<provider>").argument("<modelId>").action(async (providerName, modelId, _opts, cmd) => {
512
+ await runAction(cmd, async (ctx) => {
513
+ assertLocalProfile(ctx.profile.baseUrl);
514
+ prepLocal();
515
+ await mDefault(providerName, modelId);
516
+ process.stderr.write(`Default model for ${providerName} set to ${modelId}.
517
+ `);
518
+ }, { allowMissingProfile: true });
519
+ });
520
+ }
521
+
522
+ // src/local/db.ts
523
+ async function withHandle(fn) {
524
+ const handle = await createClient(resolveDriver(process.env));
525
+ try {
526
+ return await fn(handle);
527
+ } finally {
528
+ await handle.close();
529
+ }
530
+ }
531
+ async function runMigrate() {
532
+ return withHandle(async (h) => {
533
+ await applyMigrations(h);
534
+ return { driver: h.dialect };
535
+ });
536
+ }
537
+ async function dbStatus() {
538
+ return withHandle(async (h) => {
539
+ await h.db.execute(sql`SELECT 1`);
540
+ return { driver: h.dialect, connected: true };
541
+ });
542
+ }
543
+
544
+ // src/local/preflight.ts
545
+ import { createServer } from "net";
546
+ var MIN_NODE = [20, 9, 0];
547
+ function assertNodeVersion(version = process.versions.node) {
548
+ const parts = version.split(".").map((n) => Number.parseInt(n, 10));
549
+ const [maj = 0, min = 0, pat = 0] = parts;
550
+ const ok = maj > MIN_NODE[0] || maj === MIN_NODE[0] && (min > MIN_NODE[1] || min === MIN_NODE[1] && pat >= MIN_NODE[2]);
551
+ if (!ok) {
552
+ throw new UsageError(
553
+ `burnless needs Node >= ${MIN_NODE.join(".")} but found ${version}.
554
+ Upgrade Node (e.g. via your version manager) and re-run.`
555
+ );
556
+ }
557
+ }
558
+ function isPortFree(port, host) {
559
+ return new Promise((resolve2) => {
560
+ const srv = createServer();
561
+ srv.once("error", () => resolve2(false));
562
+ srv.once("listening", () => srv.close(() => resolve2(true)));
563
+ srv.listen(port, host);
564
+ });
565
+ }
566
+ function nodeCheck() {
567
+ const major = Number(process.versions.node.split(".")[0]);
568
+ const ok = major >= 20;
569
+ return { name: "node", ok, detail: `Node ${process.versions.node}${ok ? "" : " (need >= 20)"}` };
570
+ }
571
+ function keyCheck(home) {
572
+ const present = (process.env.SECRETS_ENCRYPTION_KEY?.trim().length ?? 0) > 0 || (readInstanceEnv(home).SECRETS_ENCRYPTION_KEY?.length ?? 0) > 0;
573
+ return {
574
+ name: "secrets_key",
575
+ ok: present,
576
+ detail: present ? "SECRETS_ENCRYPTION_KEY resolved" : "will be generated on first start"
577
+ };
578
+ }
579
+ function driverCheck() {
580
+ try {
581
+ const r = resolveDriver(process.env);
582
+ return {
583
+ name: "db_driver",
584
+ ok: true,
585
+ detail: r.driver === "pglite" ? `PGLite @ ${r.dataDir}` : "Postgres (DATABASE_URL)"
586
+ };
587
+ } catch (e) {
588
+ return { name: "db_driver", ok: false, detail: e.message };
589
+ }
590
+ }
591
+ async function doctor(opts) {
592
+ const free = await isPortFree(opts.port, opts.host);
593
+ return [
594
+ nodeCheck(),
595
+ driverCheck(),
596
+ keyCheck(opts.home),
597
+ {
598
+ name: "port",
599
+ ok: free,
600
+ detail: free ? `${opts.host}:${opts.port} is free` : `${opts.host}:${opts.port} is in use`
601
+ }
602
+ ];
603
+ }
604
+ async function health(opts) {
605
+ return [driverCheck(), keyCheck(opts.home)];
606
+ }
607
+
608
+ // src/local/secrets.ts
609
+ import { randomBytes } from "crypto";
610
+ function ensureSecret(name, opts) {
611
+ const env = opts.env ?? process.env;
612
+ const fromEnv = env[name]?.trim();
613
+ if (fromEnv) return fromEnv;
614
+ const persisted = readInstanceEnv(opts.home)[name];
615
+ if (persisted) {
616
+ env[name] = persisted;
617
+ return persisted;
618
+ }
619
+ const value = randomBytes(32).toString("base64");
620
+ setInstanceEnvVar(name, value, opts.home);
621
+ env[name] = value;
622
+ return value;
623
+ }
624
+ function ensureSecretsKey(opts = {}) {
625
+ return ensureSecret("SECRETS_ENCRYPTION_KEY", opts);
626
+ }
627
+ function ensureAuthSecret(opts = {}) {
628
+ return ensureSecret("AUTH_SECRET", opts);
629
+ }
630
+
631
+ // src/commands/bootstrap.ts
632
+ async function runBootstrap(opts = {}) {
633
+ prepareArtifactEnv();
634
+ assertNodeVersion();
635
+ const hadKey = (process.env.SECRETS_ENCRYPTION_KEY?.trim().length ?? 0) > 0 || (readInstanceEnv(opts.home).SECRETS_ENCRYPTION_KEY?.length ?? 0) > 0;
636
+ ensureSecretsKey({ home: opts.home, env: process.env });
637
+ ensureAuthSecret({ home: opts.home, env: process.env });
638
+ const { driver } = await runMigrate();
639
+ if (process.env.BURNLESS_DEPLOYMENT !== "cloud") {
640
+ const {
641
+ closeDatabase: closeDatabase2,
642
+ createOwnerCompanyIfNone,
643
+ createOwnerUserIfNone,
644
+ getOwnerUser: getOwnerUser2,
645
+ initDatabase: initDatabase2,
646
+ isDatabaseBooted: isDatabaseBooted2
647
+ } = await import("./src-ADT5KSGE.js");
648
+ const owned = !isDatabaseBooted2();
649
+ await initDatabase2();
650
+ try {
651
+ await createOwnerUserIfNone();
652
+ const owner = await getOwnerUser2();
653
+ if (owner) await createOwnerCompanyIfNone(owner.id);
654
+ } finally {
655
+ if (owned) await closeDatabase2();
656
+ }
657
+ }
658
+ return { driver, keyGenerated: !hadKey };
659
+ }
660
+ function registerBootstrap(program) {
661
+ program.command("bootstrap").description("One-shot headless setup: generate the secrets key and migrate").action(async (_opts, cmd) => {
662
+ await runAction(
663
+ cmd,
664
+ async (ctx) => {
665
+ const result = await runBootstrap();
666
+ if (ctx.json) {
667
+ process.stdout.write(JSON.stringify(result) + "\n");
668
+ } else {
669
+ process.stderr.write(
670
+ `Bootstrap done (${result.driver}; key ${result.keyGenerated ? "generated" : "already set"}). Run \`burnless start\` to launch.
671
+ `
672
+ );
673
+ }
674
+ },
675
+ { allowMissingProfile: true }
676
+ );
677
+ });
678
+ }
679
+
680
+ // src/commands/config.ts
681
+ var OFF_VALUES = /* @__PURE__ */ new Set(["off", "false", "0", "no"]);
682
+ function runConfigGet(opts) {
683
+ return readInstanceEnv(opts.home)[opts.key];
684
+ }
685
+ function runConfigList(opts = {}) {
686
+ return readInstanceEnv(opts.home);
687
+ }
688
+ async function runConfigSet(opts) {
689
+ if (opts.key === "BURNLESS_CAP_AUTO_LOGIN" && OFF_VALUES.has(opts.value.trim().toLowerCase())) {
690
+ await initDatabase();
691
+ let claimed;
692
+ try {
693
+ claimed = await isOwnerClaimed();
694
+ } finally {
695
+ await closeDatabase();
696
+ }
697
+ if (!claimed) {
698
+ throw new UsageError(
699
+ "Refusing to disable auto-login: the owner has no password yet, so this would lock you out (live session expires, then there's no way to sign in).\nSet a password first: `burnless users passwd`, then re-run this."
700
+ );
701
+ }
702
+ }
703
+ setInstanceEnvVar(opts.key, opts.value, opts.home);
704
+ }
705
+ async function runConfigUnset(opts) {
706
+ const current = readInstanceEnv(opts.home);
707
+ if (!(opts.key in current)) return;
708
+ delete current[opts.key];
709
+ const { writeFileSync, mkdirSync } = await import("fs");
710
+ const { dirname: dirname2 } = await import("path");
711
+ const path = instanceEnvPath(opts.home);
712
+ const body = Object.entries(current).map(([k, v]) => `${k}=${v}`).join("\n");
713
+ mkdirSync(dirname2(path), { recursive: true, mode: 448 });
714
+ writeFileSync(path, body.length ? body + "\n" : "", { mode: 384 });
715
+ }
716
+ function registerConfig(program) {
717
+ const cfg = program.command("config").description("Read/write local instance configuration (~/.burnless/instance.env)");
718
+ cfg.command("get").argument("<key>").action(async (key, _opts, cmd) => {
719
+ await runAction(
720
+ cmd,
721
+ async (ctx) => {
722
+ const value = runConfigGet({ key });
723
+ if (ctx.json) process.stdout.write(JSON.stringify({ key, value: value ?? null }) + "\n");
724
+ else process.stdout.write((value ?? "") + "\n");
725
+ },
726
+ { allowMissingProfile: true }
727
+ );
728
+ });
729
+ cfg.command("set").argument("<key>").argument("<value>").action(async (key, value, _opts, cmd) => {
730
+ await runAction(
731
+ cmd,
732
+ async () => {
733
+ await runConfigSet({ key, value });
734
+ process.stderr.write(`Set ${key}.
735
+ `);
736
+ },
737
+ { allowMissingProfile: true }
738
+ );
739
+ });
740
+ cfg.command("list").action(async (_opts, cmd) => {
741
+ await runAction(
742
+ cmd,
743
+ async (ctx) => {
744
+ const all = runConfigList();
745
+ if (ctx.json) process.stdout.write(JSON.stringify(all) + "\n");
746
+ else for (const [k, v] of Object.entries(all)) process.stdout.write(`${k}=${v}
747
+ `);
748
+ },
749
+ { allowMissingProfile: true }
750
+ );
751
+ });
752
+ cfg.command("unset").argument("<key>").action(async (key, _opts, cmd) => {
753
+ await runAction(
754
+ cmd,
755
+ async () => {
756
+ await runConfigUnset({ key });
757
+ process.stderr.write(`Unset ${key}.
758
+ `);
759
+ },
760
+ { allowMissingProfile: true }
761
+ );
762
+ });
763
+ }
764
+
765
+ // src/commands/db.ts
766
+ function prep() {
767
+ prepareArtifactEnv();
768
+ }
769
+ function registerDb(program) {
770
+ const db = program.command("db").description("Local database operations");
771
+ db.command("migrate").description("Apply pending migrations to the local database").action(async (_opts, cmd) => {
772
+ await runAction(cmd, async (ctx) => {
773
+ prep();
774
+ const { driver } = await runMigrate();
775
+ if (ctx.json) process.stdout.write(JSON.stringify({ ok: true, driver }) + "\n");
776
+ else process.stderr.write(`Migrations applied (${driver}).
777
+ `);
778
+ }, { allowMissingProfile: true });
779
+ });
780
+ db.command("status").description("Show the local database driver and connectivity").action(async (_opts, cmd) => {
781
+ await runAction(cmd, async (ctx) => {
782
+ prep();
783
+ const status = await dbStatus();
784
+ if (ctx.json) process.stdout.write(JSON.stringify(status) + "\n");
785
+ else process.stderr.write(`Driver: ${status.driver} \u2014 connected: ${status.connected}
786
+ `);
787
+ }, { allowMissingProfile: true });
788
+ });
789
+ db.command("push").description("Dev-only schema push (not available in the shipped artifact)").action(async (_opts, cmd) => {
790
+ await runAction(cmd, async () => {
791
+ prep();
792
+ throw new UsageError(
793
+ "`db push` is a dev-only convenience (no migration files). Use `burnless db migrate` for the shipped artifact, or `pnpm db:push` in the repo."
794
+ );
795
+ }, { allowMissingProfile: true });
796
+ });
797
+ }
798
+
799
+ // src/commands/doctor.ts
800
+ function registerDoctor(program) {
801
+ program.command("doctor").description("Run deep preflight checks for a local instance").option("--port <port>", "port to probe", "2876").option("--host <host>", "host to probe", "127.0.0.1").action(async (opts, cmd) => {
802
+ await runAction(
803
+ cmd,
804
+ async (ctx) => {
805
+ const checks = await doctor({ port: Number(opts.port), host: opts.host });
806
+ if (ctx.json) {
807
+ process.stdout.write(JSON.stringify({ checks }) + "\n");
808
+ } else {
809
+ for (const c of checks) {
810
+ process.stderr.write(`${c.ok ? green("\u2713") : red("\u2717")} ${c.name}: ${c.detail}
811
+ `);
812
+ }
813
+ }
814
+ if (checks.some((c) => !c.ok)) process.exitCode = 1;
815
+ },
816
+ { allowMissingProfile: true }
817
+ );
818
+ });
819
+ }
820
+
821
+ // src/commands/health.ts
822
+ function registerHealth(program) {
823
+ program.command("health").description("Quick liveness check for a local instance").action(async (_opts, cmd) => {
824
+ await runAction(
825
+ cmd,
826
+ async (ctx) => {
827
+ const checks = await health({});
828
+ if (ctx.json) {
829
+ process.stdout.write(JSON.stringify({ checks }) + "\n");
830
+ } else {
831
+ for (const c of checks) {
832
+ process.stderr.write(`${c.ok ? green("\u2713") : red("\u2717")} ${c.name}: ${c.detail}
833
+ `);
834
+ }
835
+ }
836
+ if (checks.some((c) => !c.ok)) process.exitCode = 1;
837
+ },
838
+ { allowMissingProfile: true }
839
+ );
840
+ });
841
+ }
842
+
843
+ // src/local/server.ts
844
+ import { spawn } from "child_process";
845
+ function resolveServerEntry(env = process.env) {
846
+ const entry = env.BURNLESS_SERVER_ENTRY?.trim();
847
+ return entry && entry.length > 0 ? entry : null;
848
+ }
849
+ function startServer(opts) {
850
+ const doSpawn = opts.spawnFn ?? spawn;
851
+ const nodeBin = opts.nodeBin ?? process.execPath;
852
+ return doSpawn(nodeBin, [opts.entry], {
853
+ stdio: "inherit",
854
+ env: { ...opts.env, PORT: String(opts.port), HOSTNAME: opts.host }
855
+ });
856
+ }
857
+
858
+ // src/commands/start.ts
859
+ var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
860
+ function assertExposureAllowed(host, unsafeExpose) {
861
+ if (LOOPBACK.has(host) || unsafeExpose) return;
862
+ throw new UsageError(
863
+ `Refusing to bind ${host}: auto-login has no password gate, so a non-loopback bind would let anyone on the network in as the owner.
864
+ Bind 127.0.0.1 (default), or pass --unsafe-expose if you understand the risk (set a password first with a claimed identity).`
865
+ );
866
+ }
867
+ function defaultAppUrlForLoopback(host, port, env) {
868
+ if (!LOOPBACK.has(host)) return null;
869
+ if (env.NEXT_PUBLIC_APP_URL?.trim() || env.ALLOWED_ORIGINS?.trim()) return null;
870
+ const displayHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
871
+ return `http://${displayHost}:${port}`;
872
+ }
873
+ function exposeWarning(host, ownerClaimed) {
874
+ if (LOOPBACK.has(host) || ownerClaimed) return "";
875
+ return `\u26A0 Exposing ${host} with an UNCLAIMED owner (no password). Auto-login will admit anyone who can reach this address as the owner. Set a password (\`burnless users passwd\`) and consider \`config set BURNLESS_CAP_AUTO_LOGIN=off\`.`;
876
+ }
877
+ function registerStart(program) {
878
+ program.command("start").description("Start the local burnless instance").option("--host <host>", "host to bind", "127.0.0.1").option("--port <port>", "port to listen on", "2876").option("--no-migrate", "skip applying migrations before boot").option("--open", "open the app in a browser after start").option("--unsafe-expose", "allow binding a non-loopback host (see the warning)").action(
879
+ async (opts, cmd) => {
880
+ await runAction(
881
+ cmd,
882
+ async (ctx) => {
883
+ prepareArtifactEnv();
884
+ assertNodeVersion();
885
+ loadInstanceEnv();
886
+ assertExposureAllowed(opts.host, opts.unsafeExpose === true);
887
+ const port = Number(opts.port);
888
+ const displayHost = opts.host.includes(":") ? `[${opts.host}]` : opts.host;
889
+ if (!ctx.json) process.stderr.write(renderBanner(versionString()) + "\n");
890
+ const checks = await doctor({ port, host: opts.host });
891
+ const failed = checks.filter((c) => !c.ok && c.name !== "secrets_key");
892
+ if (failed.length > 0) {
893
+ throw new UsageError(
894
+ `Preflight failed:
895
+ ` + failed.map((c) => ` - ${c.name}: ${c.detail}`).join("\n")
896
+ );
897
+ }
898
+ ensureSecretsKey({ env: process.env });
899
+ ensureAuthSecret({ env: process.env });
900
+ const appUrl = defaultAppUrlForLoopback(opts.host, port, process.env);
901
+ if (appUrl) {
902
+ process.env.ALLOWED_ORIGINS = appUrl;
903
+ process.env.NEXT_PUBLIC_APP_URL = appUrl;
904
+ }
905
+ try {
906
+ const { shouldOfferAiSetup, runFirstRunAiSetup } = await import("./first-run-ai-SP5SERHZ.js");
907
+ if (!ctx.json && shouldOfferAiSetup({ isTTY: process.stdin.isTTY === true, env: process.env })) {
908
+ const { createInterface } = await import("readline");
909
+ const { PROVIDER_KINDS: PROVIDER_KINDS2 } = await import("./ai-catalog-6AA75OA3.js");
910
+ const { verifyConnection: verifyConnection2 } = await import("./ai-provider-ops-3342GI65.js");
911
+ const { readSecret: readSecret2 } = await import("./prompt-QYBNOJ26.js");
912
+ process.stderr.write("\nConfigure an AI provider for autofill during onboarding? (Enter a number, or blank to skip)\n");
913
+ PROVIDER_KINDS2.forEach((k, i) => process.stderr.write(` ${i + 1}) ${k}
914
+ `));
915
+ const chooseKind = async () => {
916
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
917
+ const answer = await new Promise((res) => rl.question("> ", (a) => {
918
+ rl.close();
919
+ res(a.trim());
920
+ }));
921
+ const idx = Number(answer) - 1;
922
+ return Number.isInteger(idx) && idx >= 0 && idx < PROVIDER_KINDS2.length ? PROVIDER_KINDS2[idx] : null;
923
+ };
924
+ const result = await runFirstRunAiSetup({
925
+ chooseKind,
926
+ readApiKey: () => readSecret2({ label: "API key (input hidden): " }),
927
+ verify: verifyConnection2
928
+ });
929
+ process.stderr.write(
930
+ result.configured ? "AI provider configured \u2014 onboarding will use it.\n\n" : `Skipped AI setup${result.detail ? ` (${result.detail})` : ""} \u2014 onboarding will use manual entry.
931
+
932
+ `
933
+ );
934
+ }
935
+ } catch {
936
+ }
937
+ if (opts.migrate) await runMigrate();
938
+ if (!(/* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"])).has(opts.host)) {
939
+ try {
940
+ const { initDatabase: initDatabase2, isOwnerClaimed: isOwnerClaimed2, closeDatabase: closeDatabase2 } = await import("./src-ADT5KSGE.js");
941
+ await initDatabase2();
942
+ try {
943
+ const claimed = await isOwnerClaimed2();
944
+ const warning = exposeWarning(opts.host, claimed);
945
+ if (warning) process.stderr.write(warning + "\n");
946
+ } finally {
947
+ await closeDatabase2();
948
+ }
949
+ } catch {
950
+ }
951
+ }
952
+ const entry = resolveServerEntry();
953
+ if (!entry) {
954
+ throw new UsageError(
955
+ "No server entry found (BURNLESS_SERVER_ENTRY unset). The bundled server is produced by the fat-artifact packaging (a later release); set BURNLESS_SERVER_ENTRY to a built server.js to start manually."
956
+ );
957
+ }
958
+ process.stderr.write(dim(`Starting on http://${displayHost}:${port} \u2026`) + "\n");
959
+ const child = startServer({
960
+ entry,
961
+ host: opts.host,
962
+ port,
963
+ env: process.env,
964
+ nodeBin: resolveNodeBinary()
965
+ });
966
+ if (opts.open) {
967
+ void import("child_process").then(({ exec }) => {
968
+ const url = `http://${displayHost}:${port}`;
969
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
970
+ exec(`${opener} ${url}`);
971
+ });
972
+ }
973
+ await new Promise((resolve2) => child.on("exit", () => resolve2()));
974
+ },
975
+ { allowMissingProfile: true }
976
+ );
977
+ }
978
+ );
979
+ }
980
+
981
+ // src/commands/update.ts
982
+ import { execFile } from "child_process";
983
+ import { readlinkSync } from "fs";
984
+ import { basename, join as join2 } from "path";
985
+ import { promisify } from "util";
986
+ var execFileAsync = promisify(execFile);
987
+ function currentVersion(home) {
988
+ const link = readlinkSync(join2(versionsDir(home), "current"));
989
+ return basename(link);
990
+ }
991
+ function shortReason(raw) {
992
+ let msg = "";
993
+ if (raw && typeof raw === "object") {
994
+ const e = raw;
995
+ if (typeof e.stderr === "string" && e.stderr.trim()) msg = e.stderr;
996
+ else if (typeof e.message === "string") msg = e.message;
997
+ } else if (typeof raw === "string") {
998
+ msg = raw;
999
+ }
1000
+ msg = msg.replace(/\s+/g, " ").trim();
1001
+ return msg.length > 200 ? `${msg.slice(0, 197)}...` : msg;
1002
+ }
1003
+ async function defaultCheck(opts) {
1004
+ try {
1005
+ const launcher = join2(versionsDir(opts.home), opts.version, "burnless");
1006
+ await execFileAsync(launcher, ["doctor", "--json"]);
1007
+ return { ok: true };
1008
+ } catch (err) {
1009
+ return { ok: false, error: shortReason(err) || "post-swap doctor check failed" };
1010
+ }
1011
+ }
1012
+ async function runUpdate(opts) {
1013
+ const cur = currentVersion(opts.home);
1014
+ if (cur === opts.targetVersion) {
1015
+ return { from: cur, to: cur, rolledBack: false, noop: true };
1016
+ }
1017
+ const ensure = opts.ensureFn ?? (async (o) => {
1018
+ await ensureArtifact({ base: o.base, version: o.version, home: o.home, flip: false });
1019
+ });
1020
+ await ensure({ version: opts.targetVersion, home: opts.home, base: opts.base });
1021
+ const from = cur;
1022
+ flipCurrent(opts.targetVersion, opts.home);
1023
+ const check = opts.checkFn ? { ok: await opts.checkFn({ version: opts.targetVersion, home: opts.home }) } : await defaultCheck({ version: opts.targetVersion, home: opts.home });
1024
+ if (check.ok) {
1025
+ return { from, to: opts.targetVersion, rolledBack: false };
1026
+ }
1027
+ flipCurrent(from, opts.home);
1028
+ return { from, to: opts.targetVersion, rolledBack: true, error: check.error };
1029
+ }
1030
+ function registerUpdate(program) {
1031
+ program.command("update [version]").description("Update the local fat-artifact to a specific version (atomic flip + rollback-on-fail)").option("--base <url>", "release base URL override (download source)").action(async (version, opts, cmd) => {
1032
+ await runAction(
1033
+ cmd,
1034
+ async (ctx) => {
1035
+ if (version === void 0 || version.trim() === "") {
1036
+ throw new UsageError(
1037
+ 'specify a version to update to (e.g. `burnless update 0.2.0`); to grab the newest, resolve it first: `burnless update "$(curl -fsSL https://burnless.ai/latest)"`'
1038
+ );
1039
+ }
1040
+ const result = await runUpdate({
1041
+ home: ctx.homeDir,
1042
+ targetVersion: version,
1043
+ base: opts.base
1044
+ });
1045
+ if (ctx.json) {
1046
+ process.stdout.write(JSON.stringify(result) + "\n");
1047
+ } else if (result.noop) {
1048
+ process.stderr.write(`${green("\u2713")} already at ${result.to}
1049
+ `);
1050
+ } else if (result.rolledBack) {
1051
+ const reason = result.error ? ` (reason: ${result.error})` : "";
1052
+ process.stderr.write(
1053
+ `${red("\u2717")} update to ${result.to} failed post-swap check \u2014 rolled back to ${result.from}${reason}
1054
+ `
1055
+ );
1056
+ } else {
1057
+ process.stderr.write(`${green("\u2713")} updated ${result.from} \u2192 ${result.to}
1058
+ `);
1059
+ }
1060
+ if (result.rolledBack) process.exitCode = 1;
1061
+ },
1062
+ { allowMissingProfile: true }
1063
+ );
1064
+ });
1065
+ }
1066
+
1067
+ // ../types/src/api/validators.ts
1068
+ var MAX_AMOUNT = 1e16;
1069
+ var positiveAmount = () => external_exports.number().finite().min(0).max(MAX_AMOUNT);
1070
+ var percentage = () => external_exports.number().finite().min(0).max(100);
1071
+ var ratio = () => external_exports.number().finite().min(0).max(1);
1072
+ var toLocalDate = (s) => new Date(/^\d{4}-\d{2}-\d{2}$/.test(s) ? `${s}T00:00:00` : s);
1073
+ var dateString = () => external_exports.string().min(1, "Date is required").refine((s) => !Number.isNaN(Date.parse(s)), "Invalid date").transform(toLocalDate);
1074
+ var nullableDateString = () => external_exports.string().min(1, "Date is required").refine((s) => !Number.isNaN(Date.parse(s)), "Invalid date").transform(toLocalDate).nullable().default(null);
1075
+ var forecastLineName = () => external_exports.string().trim().min(1).max(64).regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
1076
+ var toEpoch = (v) => {
1077
+ if (v == null) return null;
1078
+ if (v instanceof Date) {
1079
+ const t2 = v.getTime();
1080
+ return Number.isNaN(t2) ? null : t2;
1081
+ }
1082
+ const t = Date.parse(String(v));
1083
+ return Number.isNaN(t) ? null : t;
1084
+ };
1085
+ var withDateRange = (schema) => schema.refine(
1086
+ (data) => {
1087
+ const start = toEpoch(data?.startDate);
1088
+ const end = toEpoch(data?.endDate);
1089
+ if (start == null || end == null) return true;
1090
+ return start <= end;
1091
+ },
1092
+ { message: "endDate must be on or after startDate", path: ["endDate"] }
1093
+ );
1094
+
1095
+ // ../types/src/api/schemas.ts
1096
+ var accountTypeEnum = external_exports.enum(["income", "expense", "asset", "liability", "equity"]);
1097
+ var accountCategoryEnum = external_exports.enum([
1098
+ "revenue",
1099
+ "cogs",
1100
+ "operating_expense",
1101
+ "other_income",
1102
+ "other_expense",
1103
+ "asset",
1104
+ "liability",
1105
+ "equity"
1106
+ ]);
1107
+ var forecastMethodEnum = external_exports.enum(["fixed", "growth_rate", "per_unit", "percentage_of", "custom_formula"]);
1108
+ var revenueStreamTypeEnum = external_exports.enum([
1109
+ "subscription",
1110
+ "one_time",
1111
+ "usage_based",
1112
+ "services",
1113
+ "marketplace",
1114
+ "ecommerce",
1115
+ "hardware"
1116
+ ]);
1117
+ var fundingRoundTypeEnum = external_exports.enum(["pre_seed", "seed", "series_a", "series_b", "series_c_plus", "debt", "grant", "safe", "convertible"]);
1118
+ var integrationTypeEnum = external_exports.enum(["quickbooks", "xero", "freshbooks", "plaid", "mercury", "gusto", "stripe"]);
1119
+ var aiDataModeEnum = external_exports.enum(["full", "show_cached", "hide_all"]);
1120
+ var aiWriteModeEnum = external_exports.enum(["full", "confirm", "read_only"]);
1121
+ var createAccountSchema = external_exports.object({
1122
+ name: external_exports.string().min(1),
1123
+ type: accountTypeEnum,
1124
+ category: accountCategoryEnum,
1125
+ parentId: external_exports.string().nullable().default(null),
1126
+ isSystem: external_exports.boolean().default(false),
1127
+ sortOrder: external_exports.number().int().default(0),
1128
+ coversHeadcount: external_exports.boolean().default(false)
1129
+ });
1130
+ var updateAccountSchema = external_exports.object({
1131
+ name: external_exports.string().min(1).optional(),
1132
+ type: accountTypeEnum.optional(),
1133
+ category: accountCategoryEnum.optional(),
1134
+ parentId: external_exports.string().nullable().optional(),
1135
+ sortOrder: external_exports.number().int().optional(),
1136
+ coversHeadcount: external_exports.boolean().optional()
1137
+ });
1138
+ var createDepartmentSchema = external_exports.object({
1139
+ name: external_exports.string().min(1),
1140
+ parentId: external_exports.string().nullable().default(null)
1141
+ });
1142
+ var updateDepartmentSchema = external_exports.object({
1143
+ name: external_exports.string().min(1).optional(),
1144
+ parentId: external_exports.string().nullable().optional()
1145
+ });
1146
+ var scenarioSourceEnum = external_exports.enum(["blank", "ai", "template", "clone", "backup"]);
1147
+ var scenarioStatusEnumZ = external_exports.enum(["active", "promoted", "archived"]);
1148
+ var createScenarioSchema = external_exports.object({
1149
+ name: external_exports.string().min(1),
1150
+ description: external_exports.string().nullable().default(null),
1151
+ source: scenarioSourceEnum.default("blank"),
1152
+ color: external_exports.string().nullable().default(null),
1153
+ sourceScenarioId: external_exports.string().nullable().default(null)
1154
+ });
1155
+ var updateScenarioSchema = external_exports.object({
1156
+ name: external_exports.string().min(1).optional(),
1157
+ description: external_exports.string().nullable().optional(),
1158
+ color: external_exports.string().nullable().optional(),
1159
+ status: scenarioStatusEnumZ.optional(),
1160
+ autoDeleteAt: external_exports.string().datetime().nullable().optional()
1161
+ });
1162
+ var headcountEmployeeTypeEnum = external_exports.enum(["full_time", "part_time", "contractor"]);
1163
+ var createHeadcountSchema = withDateRange(
1164
+ external_exports.object({
1165
+ departmentId: external_exports.string(),
1166
+ title: external_exports.string().min(1),
1167
+ name: external_exports.string().nullable().optional(),
1168
+ employeeType: headcountEmployeeTypeEnum.default("full_time"),
1169
+ count: external_exports.number().min(0).max(99.99).default(1),
1170
+ salary: positiveAmount(),
1171
+ hourlyRate: external_exports.number().nonnegative().nullable().optional(),
1172
+ hoursPerWeek: external_exports.number().min(0).max(168).nullable().optional(),
1173
+ startDate: dateString(),
1174
+ endDate: nullableDateString(),
1175
+ benefitsRate: ratio().default(0.2),
1176
+ parameters: external_exports.record(external_exports.unknown()).optional()
1177
+ })
1178
+ );
1179
+ var updateHeadcountSchema = external_exports.object({
1180
+ departmentId: external_exports.string().optional(),
1181
+ title: external_exports.string().min(1).optional(),
1182
+ name: external_exports.string().nullable().optional(),
1183
+ employeeType: headcountEmployeeTypeEnum.optional(),
1184
+ count: external_exports.number().min(0).max(99.99).optional(),
1185
+ salary: positiveAmount().optional(),
1186
+ hourlyRate: external_exports.number().nonnegative().nullable().optional(),
1187
+ hoursPerWeek: external_exports.number().min(0).max(168).nullable().optional(),
1188
+ startDate: dateString().optional(),
1189
+ endDate: external_exports.string().nullable().transform((s) => s ? new Date(s) : null).optional(),
1190
+ benefitsRate: ratio().optional(),
1191
+ parameters: external_exports.record(external_exports.unknown()).optional()
1192
+ });
1193
+ var createSalaryChangeSchema = external_exports.object({
1194
+ effectiveDate: dateString(),
1195
+ newSalary: positiveAmount(),
1196
+ reason: external_exports.string().nullable().optional()
1197
+ });
1198
+ var updateSalaryChangeSchema = external_exports.object({
1199
+ effectiveDate: dateString().optional(),
1200
+ newSalary: positiveAmount().optional(),
1201
+ reason: external_exports.string().nullable().optional()
1202
+ });
1203
+ var bonusTypeEnumZ = external_exports.enum(["signing", "performance", "retention", "other"]);
1204
+ var createBonusSchema = external_exports.object({
1205
+ payoutMonth: dateString(),
1206
+ amount: positiveAmount(),
1207
+ type: bonusTypeEnumZ.default("performance"),
1208
+ notes: external_exports.string().nullable().optional()
1209
+ });
1210
+ var updateBonusSchema = external_exports.object({
1211
+ payoutMonth: dateString().optional(),
1212
+ amount: positiveAmount().optional(),
1213
+ type: bonusTypeEnumZ.optional(),
1214
+ notes: external_exports.string().nullable().optional()
1215
+ });
1216
+ var equityGrantTypeEnumZ = external_exports.enum(["iso", "nso", "rsu"]);
1217
+ var vestingMilestoneSchema = external_exports.object({
1218
+ type: external_exports.enum(["cliff", "monthly", "quarterly", "annual", "milestone"]),
1219
+ date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/),
1220
+ sharesVested: external_exports.number().nonnegative()
1221
+ });
1222
+ var createEquityGrantSchema = external_exports.object({
1223
+ grantDate: dateString(),
1224
+ shares: external_exports.number().positive(),
1225
+ strikePrice: external_exports.number().nonnegative().nullable().optional(),
1226
+ grantType: equityGrantTypeEnumZ.default("iso"),
1227
+ parameters: external_exports.object({
1228
+ vestingSchedule: external_exports.array(vestingMilestoneSchema).default([])
1229
+ }).passthrough().optional()
1230
+ });
1231
+ var updateEquityGrantSchema = external_exports.object({
1232
+ grantDate: dateString().optional(),
1233
+ shares: external_exports.number().positive().optional(),
1234
+ strikePrice: external_exports.number().nonnegative().nullable().optional(),
1235
+ grantType: equityGrantTypeEnumZ.optional(),
1236
+ parameters: external_exports.object({
1237
+ vestingSchedule: external_exports.array(vestingMilestoneSchema).optional()
1238
+ }).passthrough().optional()
1239
+ });
1240
+ var createRevenueStreamSchema = withDateRange(
1241
+ external_exports.object({
1242
+ name: external_exports.string().min(1),
1243
+ type: revenueStreamTypeEnum.default("subscription"),
1244
+ startDate: external_exports.string().date(),
1245
+ // ISO YYYY-MM-DD, required
1246
+ endDate: external_exports.string().date().nullable().optional(),
1247
+ // ISO YYYY-MM-DD or null
1248
+ parameters: external_exports.record(external_exports.unknown()).default({})
1249
+ })
1250
+ );
1251
+ var updateRevenueStreamSchema = withDateRange(
1252
+ external_exports.object({
1253
+ name: external_exports.string().min(1).optional(),
1254
+ type: revenueStreamTypeEnum.optional(),
1255
+ startDate: external_exports.string().date().optional(),
1256
+ endDate: external_exports.string().date().nullable().optional(),
1257
+ parameters: external_exports.record(external_exports.unknown()).optional()
1258
+ })
1259
+ );
1260
+ var createFundingRoundSchema = external_exports.object({
1261
+ name: external_exports.string().min(1),
1262
+ // FUND-01: the Add form sends `roundType` (matches the AI tool + DB intent); the
1263
+ // POST route maps roundType -> the DB `type` column. Renamed from `type` (which
1264
+ // 400'd every create). roundType is immutable post-creation (omitted from update).
1265
+ roundType: fundingRoundTypeEnum,
1266
+ amount: positiveAmount(),
1267
+ date: dateString(),
1268
+ preMoneyValuation: positiveAmount().nullable().default(null),
1269
+ dilutionPercent: percentage().nullable().default(null),
1270
+ isProjected: external_exports.boolean().default(false),
1271
+ // FUND-04: type-specific data + close/notes were silently stripped on create
1272
+ // (present on update only). Mirror updateFundingRoundSchema so they persist.
1273
+ closeDate: nullableDateString(),
1274
+ notes: external_exports.string().nullable().optional(),
1275
+ parameters: external_exports.record(external_exports.unknown()).optional()
1276
+ });
1277
+ var updateFundingRoundSchema = external_exports.object({
1278
+ name: external_exports.string().min(1).optional(),
1279
+ // NOTE: `type`/`roundType` are intentionally omitted — they are immutable
1280
+ // post-creation. The route-level peek-strip enforces this with a 400 before
1281
+ // Zod ever runs; omitting them here keeps the schema in sync with intent.
1282
+ amount: positiveAmount().optional(),
1283
+ date: dateString().optional(),
1284
+ preMoneyValuation: positiveAmount().nullable().optional(),
1285
+ dilutionPercent: percentage().nullable().optional(),
1286
+ isProjected: external_exports.boolean().optional(),
1287
+ // Phase 2 D fields — were previously silently dropped by Zod strip.
1288
+ closeDate: external_exports.string().nullable().optional(),
1289
+ notes: external_exports.string().nullable().optional(),
1290
+ parameters: external_exports.record(external_exports.unknown()).optional()
1291
+ });
1292
+ var shareClassTypeEnumZ = external_exports.enum(["common", "preferred"]);
1293
+ var MAX_SHARES = 1e18;
1294
+ var shareCount = () => external_exports.number().int().nonnegative().max(MAX_SHARES);
1295
+ var createShareClassSchema = external_exports.object({
1296
+ name: external_exports.string().min(1),
1297
+ // Defaults to 'preferred' (matches the DB column default); founder common
1298
+ // stock is created by explicitly passing classType:'common'.
1299
+ classType: shareClassTypeEnumZ.default("preferred"),
1300
+ totalAuthorized: shareCount(),
1301
+ totalIssued: shareCount().default(0),
1302
+ // numeric(7,4) liquidation multiple, e.g. 1× non-participating. Not a share
1303
+ // count — plain (possibly fractional) number.
1304
+ liquidationPreference: external_exports.number().finite().min(0).max(999.9999).default(1),
1305
+ // numeric(18,6) par value (money). Optional — defaults at the DB level.
1306
+ parValue: external_exports.number().finite().min(0).optional()
1307
+ }).refine((d) => d.totalIssued <= d.totalAuthorized, {
1308
+ path: ["totalIssued"],
1309
+ message: "totalIssued cannot exceed totalAuthorized"
1310
+ });
1311
+ var updateShareClassSchema = external_exports.object({
1312
+ name: external_exports.string().min(1).optional(),
1313
+ classType: shareClassTypeEnumZ.optional(),
1314
+ totalAuthorized: shareCount().optional(),
1315
+ totalIssued: shareCount().optional(),
1316
+ liquidationPreference: external_exports.number().finite().min(0).max(999.9999).optional(),
1317
+ parValue: external_exports.number().finite().min(0).optional()
1318
+ }).refine(
1319
+ (d) => d.totalAuthorized == null || d.totalIssued == null || d.totalIssued <= d.totalAuthorized,
1320
+ { path: ["totalIssued"], message: "totalIssued cannot exceed totalAuthorized" }
1321
+ );
1322
+ var createOptionPoolSchema = external_exports.object({
1323
+ name: external_exports.string().min(1),
1324
+ totalReserved: shareCount(),
1325
+ refreshDate: nullableDateString().optional()
1326
+ });
1327
+ var updateOptionPoolSchema = external_exports.object({
1328
+ name: external_exports.string().min(1).optional(),
1329
+ totalReserved: shareCount().optional(),
1330
+ refreshDate: nullableDateString().optional()
1331
+ });
1332
+ var expenseFrequencyEnum = external_exports.enum(["monthly", "quarterly", "annual"]);
1333
+ var createForecastLineSchema = withDateRange(
1334
+ external_exports.object({
1335
+ accountId: external_exports.string(),
1336
+ method: forecastMethodEnum.default("fixed"),
1337
+ parameters: external_exports.record(external_exports.unknown()).default({}),
1338
+ startDate: dateString(),
1339
+ endDate: nullableDateString(),
1340
+ // Phase 4 §4.1 — stable identifier referenced by other lines' custom_formula.
1341
+ name: forecastLineName().nullable().optional(),
1342
+ // ── Phase 1 §1.5 / §2.C additions ───────────────────────────────────────
1343
+ notes: external_exports.string().nullable().optional(),
1344
+ vendor: external_exports.string().nullable().optional(),
1345
+ // Explicit per-line category override (set in the expense form). string = set,
1346
+ // null = clear (derive automatically), undefined = leave as-is.
1347
+ subcategory: external_exports.string().trim().min(1).max(100).nullable().optional(),
1348
+ departmentId: external_exports.string().nullable().optional(),
1349
+ frequency: expenseFrequencyEnum.default("monthly"),
1350
+ isOneTime: external_exports.boolean().default(false),
1351
+ // Tri-state: true | false | null (cleared) | undefined (untouched).
1352
+ isRecurring: external_exports.boolean().nullable().optional()
1353
+ })
1354
+ );
1355
+ var updateForecastLineSchema = external_exports.object({
1356
+ method: forecastMethodEnum.optional(),
1357
+ parameters: external_exports.record(external_exports.unknown()).optional(),
1358
+ startDate: dateString().optional(),
1359
+ endDate: external_exports.string().nullable().transform((s) => s ? new Date(s) : null).optional(),
1360
+ // Phase 4 §4.1 — stable identifier referenced by other lines' custom_formula.
1361
+ name: forecastLineName().nullable().optional(),
1362
+ // ── Phase 1 §1.5 / §2.C additions ───────────────────────────────────────
1363
+ notes: external_exports.string().nullable().optional(),
1364
+ vendor: external_exports.string().nullable().optional(),
1365
+ // Explicit per-line category override (set in the expense form). string = set,
1366
+ // null = clear (derive automatically), undefined = leave as-is.
1367
+ subcategory: external_exports.string().trim().min(1).max(100).nullable().optional(),
1368
+ departmentId: external_exports.string().nullable().optional(),
1369
+ frequency: expenseFrequencyEnum.optional(),
1370
+ isOneTime: external_exports.boolean().optional(),
1371
+ // Tri-state: explicit `null` clears the column back to NULL.
1372
+ isRecurring: external_exports.boolean().nullable().optional()
1373
+ });
1374
+ var createIntegrationSchema = external_exports.object({
1375
+ type: integrationTypeEnum,
1376
+ metadata: external_exports.record(external_exports.unknown()).optional()
1377
+ });
1378
+ var updateAiFeaturesSchema = external_exports.object({
1379
+ masterEnabled: external_exports.boolean().optional(),
1380
+ dataMode: aiDataModeEnum.optional(),
1381
+ writeMode: aiWriteModeEnum.optional(),
1382
+ features: external_exports.object({
1383
+ onboarding: external_exports.boolean().optional(),
1384
+ chat: external_exports.boolean().optional(),
1385
+ insights: external_exports.boolean().optional(),
1386
+ uiPersonalization: external_exports.boolean().optional(),
1387
+ autoCategorization: external_exports.boolean().optional()
1388
+ }).optional(),
1389
+ companionName: external_exports.string().min(1).max(50).optional()
1390
+ // S6 W1.1: legacy single-provider BYOK fields removed — AI provider config
1391
+ // lives in the aiProviders model (managed via /api/ai-providers).
1392
+ });
1393
+ var inviteCodeTypeEnum = external_exports.enum(["single_use", "multi_use"]);
1394
+ var createInviteCodeSchema = external_exports.object({
1395
+ code: external_exports.string().min(4).max(64).regex(/^[A-Za-z0-9_-]+$/, "Code must be alphanumeric (hyphens and underscores allowed)"),
1396
+ type: inviteCodeTypeEnum.default("single_use"),
1397
+ maxRedemptions: external_exports.number().int().min(1).default(1),
1398
+ expiresAt: external_exports.string().datetime().nullable().optional(),
1399
+ freePlatformDays: external_exports.number().int().min(0).default(30),
1400
+ aiCreditsCents: external_exports.number().int().min(0).default(5e3),
1401
+ note: external_exports.string().max(500).nullable().optional()
1402
+ });
1403
+ var updateInviteCodeSchema = external_exports.object({
1404
+ isActive: external_exports.boolean().optional(),
1405
+ maxRedemptions: external_exports.number().int().min(1).optional(),
1406
+ expiresAt: external_exports.string().datetime().nullable().optional(),
1407
+ freePlatformDays: external_exports.number().int().min(0).optional(),
1408
+ aiCreditsCents: external_exports.number().int().min(0).optional(),
1409
+ note: external_exports.string().max(500).nullable().optional()
1410
+ });
1411
+ var redeemInviteCodeSchema = external_exports.object({
1412
+ code: external_exports.string().min(1)
1413
+ });
1414
+
1415
+ // ../types/src/password.ts
1416
+ var ITERATIONS = 1e5;
1417
+ var KEY_LENGTH = 32;
1418
+ var SALT_LENGTH = 32;
1419
+ async function hashPassword(password) {
1420
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
1421
+ const key = await deriveKey(password, salt);
1422
+ const keyBytes = new Uint8Array(await crypto.subtle.exportKey("raw", key));
1423
+ const saltHex = Buffer.from(salt).toString("hex");
1424
+ const hashHex = Buffer.from(keyBytes).toString("hex");
1425
+ return `pbkdf2:${ITERATIONS}:${saltHex}:${hashHex}`;
1426
+ }
1427
+ async function deriveKey(password, salt, iterations = ITERATIONS) {
1428
+ const encoder = new TextEncoder();
1429
+ const keyMaterial = await crypto.subtle.importKey(
1430
+ "raw",
1431
+ encoder.encode(password),
1432
+ "PBKDF2",
1433
+ false,
1434
+ ["deriveBits", "deriveKey"]
1435
+ );
1436
+ return crypto.subtle.deriveKey(
1437
+ { name: "PBKDF2", salt, iterations, hash: "SHA-256" },
1438
+ keyMaterial,
1439
+ { name: "AES-GCM", length: KEY_LENGTH * 8 },
1440
+ true,
1441
+ ["encrypt"]
1442
+ );
1443
+ }
1444
+
1445
+ // src/commands/users.ts
1446
+ async function withDb2(fn) {
1447
+ const owned = !isDatabaseBooted();
1448
+ await initDatabase();
1449
+ try {
1450
+ return await fn();
1451
+ } finally {
1452
+ if (owned) await closeDatabase();
1453
+ }
1454
+ }
1455
+ async function runUsersList() {
1456
+ return withDb2(() => listUsers());
1457
+ }
1458
+ async function runUsersPasswd(input) {
1459
+ await withDb2(async () => {
1460
+ const email = input.email ?? (await getOwnerUser())?.email;
1461
+ if (!email) throw new UsageError("No user to set a password for (run `burnless bootstrap` first).");
1462
+ const hash = await hashPassword(input.password);
1463
+ const n = await setUserPassword(email, hash);
1464
+ if (n === 0) throw new UsageError(`No user with email "${email}".`);
1465
+ });
1466
+ }
1467
+ async function runUsersCreate(input) {
1468
+ return withDb2(async () => {
1469
+ const hash = await hashPassword(input.password);
1470
+ return createUser({ email: input.email, name: input.name, passwordHash: hash });
1471
+ });
1472
+ }
1473
+ function registerUsers(program) {
1474
+ const usersCmd = program.command("users").description("Manage local users (owner, recovery)");
1475
+ usersCmd.command("list").description("List local users").action(async (_opts, cmd) => {
1476
+ await runAction(
1477
+ cmd,
1478
+ async (ctx) => {
1479
+ const rows = await runUsersList();
1480
+ if (ctx.json) process.stdout.write(JSON.stringify({ users: rows }) + "\n");
1481
+ else
1482
+ for (const u of rows)
1483
+ process.stderr.write(`${u.email}${u.claimed ? "" : " (unclaimed)"} \u2014 ${u.id}
1484
+ `);
1485
+ },
1486
+ { allowMissingProfile: true }
1487
+ );
1488
+ });
1489
+ usersCmd.command("passwd").description("Set/reset a user's password (default: the owner \u2014 claims it). Reads the password from a masked prompt or --stdin.").argument("[email]", "target user email (defaults to the owner)").option("--stdin", "read the new password from stdin instead of a prompt").action(async (email, opts, cmd) => {
1490
+ await runAction(
1491
+ cmd,
1492
+ async () => {
1493
+ const password = await readSecret({ stdin: opts.stdin, label: "New password: " });
1494
+ if (!password) throw new UsageError("Empty password \u2014 aborted.");
1495
+ await runUsersPasswd({ email, password });
1496
+ process.stderr.write(`Password set for ${email ?? "the owner"}.
1497
+ `);
1498
+ },
1499
+ { allowMissingProfile: true }
1500
+ );
1501
+ });
1502
+ usersCmd.command("create").description("Create an additional local user. Reads the password from a masked prompt or --stdin.").requiredOption("--email <email>", "the new user's email").option("--name <name>", "the new user's display name").option("--stdin", "read the password from stdin instead of a prompt").action(async (opts, cmd) => {
1503
+ await runAction(
1504
+ cmd,
1505
+ async (ctx) => {
1506
+ const password = await readSecret({ stdin: opts.stdin, label: "Password: " });
1507
+ if (!password) throw new UsageError("Empty password \u2014 aborted.");
1508
+ const { id } = await runUsersCreate({ email: opts.email, name: opts.name, password });
1509
+ if (ctx.json) process.stdout.write(JSON.stringify({ id, email: opts.email }) + "\n");
1510
+ else process.stderr.write(`Created user ${opts.email} (${id}).
1511
+ `);
1512
+ },
1513
+ { allowMissingProfile: true }
1514
+ );
1515
+ });
1516
+ }
1517
+
1518
+ // src/index.ts
1519
+ function buildProgram() {
1520
+ const program = buildRemoteProgram();
1521
+ registerStart(program);
1522
+ registerDb(program);
1523
+ registerHealth(program);
1524
+ registerDoctor(program);
1525
+ registerBootstrap(program);
1526
+ registerUsers(program);
1527
+ registerConfig(program);
1528
+ registerProvider(program);
1529
+ registerKey(program);
1530
+ registerModel(program);
1531
+ registerUpdate(program);
1532
+ return program;
1533
+ }
1534
+ var isMain = process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href;
1535
+ if (isMain) {
1536
+ const program = buildProgram();
1537
+ program.exitOverride((err) => {
1538
+ process.exit(err.exitCode === 0 ? 0 : 2);
1539
+ });
1540
+ const verbForBare = topVerb(process.argv);
1541
+ const wantsVersion = process.argv.includes("-V") || process.argv.includes("--version");
1542
+ if (verbForBare === void 0 && !wantsVersion) {
1543
+ program.outputHelp();
1544
+ process.exit(0);
1545
+ }
1546
+ await program.parseAsync(process.argv);
1547
+ }
1548
+ export {
1549
+ buildProgram
1550
+ };