yamlite 0.0.0-stage → 0.35.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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +746 -2
  3. package/dist/browser/aliases.d.mts +6 -0
  4. package/dist/browser/aliases.mjs +42 -0
  5. package/dist/browser/chokidar.d.mts +17 -0
  6. package/dist/browser/chokidar.mjs +64 -0
  7. package/dist/browser/crypto.d.mts +11 -0
  8. package/dist/browser/crypto.mjs +27 -0
  9. package/dist/browser/expose.d.mts +3 -0
  10. package/dist/browser/expose.mjs +80 -0
  11. package/dist/browser/fs.d.mts +85 -0
  12. package/dist/browser/fs.mjs +1 -0
  13. package/dist/browser/globals.d.mts +1 -0
  14. package/dist/browser/globals.mjs +21 -0
  15. package/dist/browser/os.d.mts +9 -0
  16. package/dist/browser/os.mjs +9 -0
  17. package/dist/browser/path.d.mts +28 -0
  18. package/dist/browser/path.mjs +15 -0
  19. package/dist/browser/sqlite.d.mts +48 -0
  20. package/dist/browser/sqlite.mjs +219 -0
  21. package/dist/browser/unavailable.d.mts +15 -0
  22. package/dist/browser/unavailable.mjs +21 -0
  23. package/dist/cli.d.mts +1 -0
  24. package/dist/cli.mjs +433 -0
  25. package/dist/core.d.mts +527 -0
  26. package/dist/core.mjs +3 -0
  27. package/dist/fs-DZ4QHM93.mjs +251 -0
  28. package/dist/guide.md +171 -0
  29. package/dist/index-BBHZ8yok.d.mts +316 -0
  30. package/dist/index.d.mts +2 -0
  31. package/dist/index.mjs +1 -0
  32. package/dist/src-DKu7thkC.mjs +8030 -0
  33. package/dist/ui/assets/ErdView-Bpo3_oCp.js +8 -0
  34. package/dist/ui/assets/ErdView-DLioOiRN.css +1 -0
  35. package/dist/ui/assets/MarkdownField-CRtDmSzB.js +32 -0
  36. package/dist/ui/assets/PageView-6iig-DcM.js +1 -0
  37. package/dist/ui/assets/index-BIkG4xcY.css +2 -0
  38. package/dist/ui/assets/index-CqIdjmI5.js +125 -0
  39. package/dist/ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  40. package/dist/ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  41. package/dist/ui/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  42. package/dist/ui/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  43. package/dist/ui/assets/load-kYvvjpD1.js +2 -0
  44. package/dist/ui/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
  45. package/dist/ui/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
  46. package/dist/ui/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
  47. package/dist/ui/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
  48. package/dist/ui/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
  49. package/dist/ui/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
  50. package/dist/ui/assets/outfit-latin-700-normal-Cu9v6i1X.woff2 +0 -0
  51. package/dist/ui/assets/outfit-latin-700-normal-D4itBLBr.woff +0 -0
  52. package/dist/ui/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  53. package/dist/ui/assets/sql-wasm-C1D_RJuy.wasm +0 -0
  54. package/dist/ui/assets/sql-wasm-Dnr4gnQb.js +1 -0
  55. package/dist/ui/assets/sql-wasm-browser-CNwHQfY0.js +2 -0
  56. package/dist/ui/assets/theme-CMaLONsO.js +1 -0
  57. package/dist/ui/favicon.svg +6 -0
  58. package/dist/ui/index.html +26 -0
  59. package/dist/ui-embed/ErdView-Dk-iexgy.js +10362 -0
  60. package/dist/ui-embed/HintPopover-D5P2UuTa.js +8257 -0
  61. package/dist/ui-embed/MarkdownField-DTAEh2oz.js +5211 -0
  62. package/dist/ui-embed/PageView-Cpq5JtTW.js +818 -0
  63. package/dist/ui-embed/activity-letLdXN4.js +190 -0
  64. package/dist/ui-embed/dist-CkcIwL4M.js +8142 -0
  65. package/dist/ui-embed/embed.js +18684 -0
  66. package/dist/ui-embed/favicon.svg +6 -0
  67. package/dist/ui-embed/page-frame.js +7 -0
  68. package/dist/ui-embed/pageFrame-Dm62WvvT.js +2556 -0
  69. package/dist/ui-embed/style.css +3 -0
  70. package/dist/ui-embed/tabs-CLBiDAgo.js +17095 -0
  71. package/dist/ui-embed/theme-BwJfOY1V.js +2951 -0
  72. package/dist/workspace-BHg_GGkH.mjs +896 -0
  73. package/package.json +171 -3
@@ -0,0 +1,219 @@
1
+ import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
2
+ //#region src/browser/sqlite.ts
3
+ const sqlite3 = await sqlite3InitModule();
4
+ const opfs = typeof globalThis.FileSystemHandle !== "undefined" ? await sqlite3.installOpfsSAHPoolVfs({ name: "yamlite" }) : null;
5
+ let VFS = opfs?.vfsName;
6
+ function registerVfs(name) {
7
+ VFS = name;
8
+ }
9
+ const { capi, oo1 } = sqlite3;
10
+ const constants = Object.fromEntries(Object.entries(capi).filter(([k, v]) => k.startsWith("SQLITE_") && typeof v === "number"));
11
+ var SqliteError = class extends Error {
12
+ errcode;
13
+ errstr;
14
+ code = "ERR_SQLITE_ERROR";
15
+ constructor(message, errcode, errstr) {
16
+ super(message);
17
+ this.errcode = errcode;
18
+ this.errstr = errstr;
19
+ }
20
+ };
21
+ function sqliteError(db, e) {
22
+ if (e instanceof SqliteError || !(e instanceof Error) || !("resultCode" in e)) return e;
23
+ const ptr = db.pointer;
24
+ const code = ptr ? capi.sqlite3_extended_errcode(ptr) : Number(e.resultCode);
25
+ return new SqliteError(ptr ? capi.sqlite3_errmsg(ptr) : e.message, code, capi.sqlite3_errstr(code));
26
+ }
27
+ const isNamed = (v) => typeof v === "object" && v !== null && !(v instanceof Uint8Array) && !ArrayBuffer.isView(v);
28
+ const finalizer = new FinalizationRegistry((fn) => fn());
29
+ function finalize(stmt) {
30
+ try {
31
+ if (stmt.pointer) stmt.finalize();
32
+ } catch {}
33
+ }
34
+ const finalized = () => Object.assign(/* @__PURE__ */ new Error("statement has been finalized"), { code: "ERR_INVALID_STATE" });
35
+ var StatementSync = class {
36
+ db;
37
+ stmt;
38
+ bigInts = false;
39
+ arrays = false;
40
+ constructor(db, stmt, open) {
41
+ this.db = db;
42
+ this.stmt = stmt;
43
+ open.add(stmt);
44
+ finalizer.register(this, () => {
45
+ finalize(stmt);
46
+ open.delete(stmt);
47
+ });
48
+ }
49
+ setReadBigInts(on) {
50
+ this.bigInts = on;
51
+ }
52
+ setReturnArrays(on) {
53
+ this.arrays = on;
54
+ }
55
+ columns() {
56
+ const p = this.stmt.pointer;
57
+ if (!p) throw finalized();
58
+ const n = capi.sqlite3_column_count(p);
59
+ return Array.from({ length: n }, (_, i) => Object.assign(Object.create(null), {
60
+ column: null,
61
+ database: null,
62
+ name: capi.sqlite3_column_name(p, i),
63
+ table: null,
64
+ type: capi.sqlite3_column_decltype(p, i) ?? null
65
+ }));
66
+ }
67
+ bind(params) {
68
+ const s = this.stmt;
69
+ const p = s.pointer;
70
+ if (!p) throw finalized();
71
+ s.reset();
72
+ s.clearBindings();
73
+ const count = capi.sqlite3_bind_parameter_count(p);
74
+ let named;
75
+ let rest = params;
76
+ if (isNamed(params[0])) {
77
+ named = params[0];
78
+ rest = params.slice(1);
79
+ }
80
+ let next = 0;
81
+ for (let i = 1; i <= count; i++) {
82
+ const name = capi.sqlite3_bind_parameter_name(p, i);
83
+ let v;
84
+ if (name && named) v = name in named ? named[name] : named[name.slice(1)];
85
+ else if (!name || /^\?\d*$/.test(name)) v = rest[name && name.length > 1 ? Number(name.slice(1)) - 1 : next++];
86
+ else continue;
87
+ if (v === void 0) continue;
88
+ if (v === null) capi.sqlite3_bind_null(p, i);
89
+ else if (typeof v === "number") capi.sqlite3_bind_double(p, i, v);
90
+ else if (typeof v === "bigint") capi.sqlite3_bind_int64(p, i, v);
91
+ else if (typeof v === "string" || v instanceof Uint8Array) s.bind(i, v);
92
+ else throw new TypeError(`Provided value cannot be bound to SQLite parameter ${i}.`);
93
+ }
94
+ }
95
+ row() {
96
+ const p = this.stmt.pointer;
97
+ const n = capi.sqlite3_column_count(p);
98
+ const values = [];
99
+ for (let i = 0; i < n; i++) switch (capi.sqlite3_column_type(p, i)) {
100
+ case capi.SQLITE_INTEGER: {
101
+ const v = BigInt(capi.sqlite3_column_int64(p, i));
102
+ if (this.bigInts) values.push(v);
103
+ else if (v > BigInt(Number.MAX_SAFE_INTEGER) || v < BigInt(Number.MIN_SAFE_INTEGER)) {
104
+ const e = /* @__PURE__ */ new RangeError(`Value is too large to be represented as a JavaScript number: ${v}`);
105
+ throw Object.assign(e, { code: "ERR_OUT_OF_RANGE" });
106
+ } else values.push(Number(v));
107
+ break;
108
+ }
109
+ case capi.SQLITE_FLOAT:
110
+ values.push(capi.sqlite3_column_double(p, i));
111
+ break;
112
+ case capi.SQLITE_TEXT:
113
+ values.push(capi.sqlite3_column_text(p, i));
114
+ break;
115
+ case capi.SQLITE_BLOB:
116
+ values.push(this.stmt.get(i, capi.SQLITE_BLOB));
117
+ break;
118
+ default: values.push(null);
119
+ }
120
+ if (this.arrays) return values;
121
+ const row = Object.create(null);
122
+ for (let i = 0; i < n; i++) row[capi.sqlite3_column_name(p, i)] = values[i];
123
+ return row;
124
+ }
125
+ step() {
126
+ try {
127
+ return this.stmt.step();
128
+ } catch (e) {
129
+ const err = sqliteError(this.db, e);
130
+ try {
131
+ this.stmt.reset();
132
+ } catch {}
133
+ throw err;
134
+ }
135
+ }
136
+ all(...params) {
137
+ this.bind(params);
138
+ const rows = [];
139
+ while (this.step()) rows.push(this.row());
140
+ this.stmt.reset();
141
+ return rows;
142
+ }
143
+ get(...params) {
144
+ this.bind(params);
145
+ const row = this.step() ? this.row() : void 0;
146
+ this.stmt.reset();
147
+ return row;
148
+ }
149
+ *iterate(...params) {
150
+ this.bind(params);
151
+ try {
152
+ while (this.step()) yield this.row();
153
+ } finally {
154
+ this.stmt.reset();
155
+ }
156
+ }
157
+ run(...params) {
158
+ this.bind(params);
159
+ while (this.step());
160
+ this.stmt.reset();
161
+ const changes = capi.sqlite3_changes(this.db.pointer);
162
+ const last = BigInt(capi.sqlite3_last_insert_rowid(this.db.pointer));
163
+ return this.bigInts ? {
164
+ changes: BigInt(changes),
165
+ lastInsertRowid: last
166
+ } : {
167
+ changes: Number(changes),
168
+ lastInsertRowid: Number(last)
169
+ };
170
+ }
171
+ };
172
+ var DatabaseSync = class {
173
+ db;
174
+ stmts = /* @__PURE__ */ new Set();
175
+ constructor(path, opts = {}) {
176
+ try {
177
+ this.db = new oo1.DB({
178
+ filename: path,
179
+ flags: opts.readOnly ? "r" : "c",
180
+ ...VFS && path !== ":memory:" ? { vfs: VFS } : {}
181
+ });
182
+ } catch (e) {
183
+ throw new SqliteError(e instanceof Error ? e.message : String(e), capi.SQLITE_CANTOPEN, "unable to open database file");
184
+ }
185
+ capi.sqlite3_extended_result_codes(this.db.pointer, 1);
186
+ }
187
+ get isOpen() {
188
+ return !!this.db.pointer;
189
+ }
190
+ get isTransaction() {
191
+ return capi.sqlite3_get_autocommit(this.db.pointer) === 0;
192
+ }
193
+ exec(sql) {
194
+ try {
195
+ this.db.exec(sql);
196
+ } catch (e) {
197
+ throw sqliteError(this.db, e);
198
+ }
199
+ }
200
+ prepare(sql) {
201
+ try {
202
+ return new StatementSync(this.db, this.db.prepare(sql), this.stmts);
203
+ } catch (e) {
204
+ throw sqliteError(this.db, e);
205
+ }
206
+ }
207
+ setAuthorizer(fn) {
208
+ const cb = fn ? (_p, action, ...args) => fn(action, ...args.map((a) => a === 0 ? null : a)) : 0;
209
+ capi.sqlite3_set_authorizer(this.db.pointer, cb, 0);
210
+ }
211
+ close() {
212
+ if (!this.db.pointer) throw Object.assign(/* @__PURE__ */ new Error("database is not open"), { code: "ERR_INVALID_STATE" });
213
+ for (const s of this.stmts) finalize(s);
214
+ this.stmts.clear();
215
+ this.db.close();
216
+ }
217
+ };
218
+ //#endregion
219
+ export { DatabaseSync, StatementSync, constants, opfs, registerVfs, sqlite3 };
@@ -0,0 +1,15 @@
1
+ //#region src/browser/unavailable.d.ts
2
+ export declare const spawn: () => never;
3
+ export declare const pipeline: () => never;
4
+ export declare class Readable {}
5
+ export declare class Writable {}
6
+ export declare const fileURLToPath: (u: string | URL) => string;
7
+ export declare const watch: () => {
8
+ on: () => /*elided*/ any;
9
+ close: () => Promise<void>;
10
+ add: () => /*elided*/ any;
11
+ unwatch: () => /*elided*/ any;
12
+ };
13
+ declare const _default: {};
14
+ //#endregion
15
+ export { _default as default };
@@ -0,0 +1,21 @@
1
+ //#region src/browser/unavailable.ts
2
+ const no = (what) => () => {
3
+ throw new Error(`${what} is not available in the browser build`);
4
+ };
5
+ const spawn = no("child_process.spawn");
6
+ const pipeline = no("stream.pipeline");
7
+ var Readable = class {};
8
+ var Writable = class {};
9
+ const fileURLToPath = (u) => new URL(u).pathname;
10
+ const watch = () => {
11
+ const w = {
12
+ on: () => w,
13
+ close: async () => {},
14
+ add: () => w,
15
+ unwatch: () => w
16
+ };
17
+ return w;
18
+ };
19
+ var unavailable_default = {};
20
+ //#endregion
21
+ export { Readable, Writable, unavailable_default as default, fileURLToPath, pipeline, spawn, watch };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/cli.mjs ADDED
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env node
2
+ import { I as isLoopback, L as loopbackHosts, P as createHandler, R as check, a as query, c as exportSite, i as formatTable, n as formatCsv, o as generateConfig, r as formatJson, s as init, t as open } from "./src-DKu7thkC.mjs";
3
+ import { n as guide, r as envKeys, t as createWorkspace } from "./workspace-BHg_GGkH.mjs";
4
+ import { createRequire } from "node:module";
5
+ import { relative, resolve } from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { randomBytes } from "node:crypto";
8
+ import { fileURLToPath } from "node:url";
9
+ import { spawn } from "node:child_process";
10
+ import { Command } from "commander";
11
+ import { styleText } from "node:util";
12
+ import { createServer } from "node:http";
13
+ //#region src/format.ts
14
+ const painter = (stream) => (style, text) => styleText(style, text, { stream });
15
+ const MIN_SUMMARY_WIDTH = 38;
16
+ const WATCH_LABEL_WIDTH = 10;
17
+ const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
18
+ function displayPath(path, home = homedir()) {
19
+ return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
20
+ }
21
+ const changeCount = (r) => r.toDb + r.toFile + r.deletedDb + r.deletedFile;
22
+ function schemaCounts(schema) {
23
+ const columns = schema.filter((c) => c.op === "alterColumn" || c.op === "dropColumn").length;
24
+ const views = schema.filter((c) => c.op === "createView" || c.op === "dropView").length;
25
+ const indexes = schema.length - columns - views;
26
+ return [
27
+ columns > 0 ? plural(columns, "column change") : "",
28
+ indexes > 0 ? plural(indexes, "index change") : "",
29
+ views > 0 ? plural(views, "view change") : ""
30
+ ].filter(Boolean);
31
+ }
32
+ function schemaDetail(c, paint, mode) {
33
+ if (c.op === "dropColumn") return `${paint("red", "−")} column ${c.name} ${paint("dim", "(removed from yamlite.yaml)")}`;
34
+ if (c.op === "alterColumn") {
35
+ const note = mode === "status" ? "table will be rebuilt" : "table rebuilt";
36
+ return `${paint("cyan", "↻")} column ${c.name} ${c.definition} ${paint("dim", `(${note})`)}`;
37
+ }
38
+ if (c.op === "createView") return `${paint("green", "+")} view ${c.definition}`;
39
+ if (c.op === "dropView") return `${paint("red", "−")} view ${c.definition}`;
40
+ return c.op === "createIndex" ? `${paint("green", "+")} index ${c.definition}` : `${paint("red", "−")} index ${c.definition}`;
41
+ }
42
+ function summary(r) {
43
+ const parts = [
44
+ r.toDb > 0 ? `${r.toDb} → db` : "",
45
+ r.toFile > 0 ? `${r.toFile} → file` : "",
46
+ r.deletedDb > 0 ? `${plural(r.deletedDb, "row")} deleted` : "",
47
+ r.deletedFile > 0 ? `${plural(r.deletedFile, "file")} deleted` : "",
48
+ r.conflicts.length > 0 ? plural(r.conflicts.length, "conflict") : "",
49
+ ...schemaCounts(r.schema),
50
+ r.registered.length > 0 ? plural(r.registered.length, "new column") : ""
51
+ ].filter(Boolean);
52
+ return parts.length > 0 ? parts.join(" · ") : "in sync";
53
+ }
54
+ function resolution(c, root) {
55
+ const loser = c.winner === "db" ? "file" : "db";
56
+ const saved = c.savedTo ? ` · ${loser} version saved to ${relative(root, c.savedTo)}` : "";
57
+ return `kept ${c.winner}${saved}`;
58
+ }
59
+ function conflictText(c, root, done) {
60
+ return `changed on both sides · ${done ? resolution(c, root) : `${c.winner} will win`}`;
61
+ }
62
+ function report(results, { mode, root, paint, elapsedMs = 0 }) {
63
+ const nameWidth = Math.max(0, ...results.map((r) => r.table.length));
64
+ const summaryWidth = Math.max(MIN_SUMMARY_WIDTH, ...results.map((r) => summary(r).length + 2));
65
+ const indent = " ".repeat(4 + nameWidth + 3);
66
+ const lines = [`${paint("bold", "YAMLite")} ${paint("dim", `· ${displayPath(root)}`)}`, ""];
67
+ for (const r of results) {
68
+ const name = r.table.padEnd(nameWidth);
69
+ if (!r.ok) {
70
+ lines.push(` ${paint("red", "✗")} ${name} ${paint("red", r.error ?? "failed")}`);
71
+ continue;
72
+ }
73
+ const icon = mode === "status" && (changeCount(r) > 0 || r.schema.length > 0 || r.registered.length > 0) ? paint("yellow", "●") : paint("green", "✓");
74
+ const records = paint("dim", plural(r.records, "record"));
75
+ lines.push(` ${icon} ${name} ${summary(r).padEnd(summaryWidth)}${records}`);
76
+ for (const c of r.schema) lines.push(`${indent}${schemaDetail(c, paint, mode)}`);
77
+ const note = mode === "status" ? "will be added to yamlite.yaml" : "added to yamlite.yaml";
78
+ for (const c of r.registered) lines.push(`${indent}${paint("green", "+")} column ${c.column} ${c.type} ${paint("dim", `(${note})`)}`);
79
+ for (const w of r.warnings) lines.push(`${indent}${paint("yellow", "!")} ${w}`);
80
+ for (const c of r.conflicts) lines.push(`${indent}${paint("yellow", "⚠")} ${c.key}: ${conflictText(c, root, mode === "sync")}`);
81
+ }
82
+ const changes = results.reduce((n, r) => n + changeCount(r), 0);
83
+ const newColumns = results.reduce((n, r) => n + r.registered.length, 0);
84
+ const work = [
85
+ changes > 0 ? plural(changes, "change") : "",
86
+ ...schemaCounts(results.flatMap((r) => r.schema)),
87
+ newColumns > 0 ? plural(newColumns, "new column") : ""
88
+ ].filter(Boolean);
89
+ const conflicts = results.reduce((n, r) => n + r.conflicts.length, 0);
90
+ const errors = results.filter((r) => !r.ok).length;
91
+ const extras = [conflicts > 0 ? plural(conflicts, "conflict") : "", errors > 0 ? paint("red", plural(errors, "error")) : ""].filter(Boolean);
92
+ let footer;
93
+ if (mode === "status") {
94
+ if (work.length === 0 && errors === 0) footer = paint("green", "Everything is in sync");
95
+ else {
96
+ const head = work.length > 0 ? `${work.join(" · ")} pending` : "no changes pending";
97
+ const hint = work.length > 0 ? [paint("dim", "run yamlite sync to apply")] : [];
98
+ footer = [
99
+ head,
100
+ ...extras,
101
+ ...hint
102
+ ].join(" · ");
103
+ }
104
+ } else footer = [
105
+ `Synced ${plural(results.length, "table")} in ${Math.round(elapsedMs)}ms`,
106
+ ...work.length > 0 ? work : ["no changes"],
107
+ ...extras
108
+ ].join(" · ");
109
+ lines.push("", ` ${footer}`);
110
+ return lines.join("\n");
111
+ }
112
+ function checkReport(result, { root, paint }) {
113
+ const nameWidth = Math.max(0, ...result.tables.map((t) => t.table.length));
114
+ const indent = " ".repeat(4 + nameWidth + 3);
115
+ const lines = [`${paint("bold", "YAMLite")} ${paint("dim", `· ${displayPath(root)}`)}`, ""];
116
+ const unlisted = new Set(result.unregisteredTables);
117
+ let problems = 0;
118
+ for (const t of result.tables) {
119
+ const name = t.table.padEnd(nameWidth);
120
+ if (!t.ok) {
121
+ problems++;
122
+ lines.push(` ${paint("red", "✗")} ${name} ${paint("red", t.error ?? "failed")}`);
123
+ continue;
124
+ }
125
+ const details = [
126
+ ...unlisted.has(t.table) ? [`${paint("yellow", "+")} table ${paint("dim", "(not in yamlite.yaml)")}`] : [],
127
+ ...t.unregistered.map((c) => `${paint("yellow", "+")} column ${c.column} ${c.type} ${paint("dim", "(not in yamlite.yaml)")}`),
128
+ ...t.warnings.map((w) => `${paint("yellow", "!")} ${w}`)
129
+ ];
130
+ problems += details.length;
131
+ lines.push(` ${details.length > 0 ? paint("red", "✗") : paint("green", "✓")} ${t.table}`);
132
+ for (const d of details) lines.push(`${indent}${d}`);
133
+ }
134
+ lines.push("", problems === 0 ? paint("green", "✓ no problems") : paint("red", `✗ ${plural(problems, "problem")}`));
135
+ return lines.join("\n");
136
+ }
137
+ function watchHeader(tables, root, paint) {
138
+ const names = tables.length > 0 ? tables.join(", ") : "(no tables)";
139
+ return `${paint("bold", "YAMLite")} · watching ${names} in ${displayPath(root)} ${paint("dim", "· Ctrl-C to stop")}`;
140
+ }
141
+ const clock = (now) => [
142
+ now.getHours(),
143
+ now.getMinutes(),
144
+ now.getSeconds()
145
+ ].map((n) => String(n).padStart(2, "0")).join(":");
146
+ function reloadLine(tables, now, paint) {
147
+ const names = tables.length > 0 ? tables.join(", ") : "(no tables)";
148
+ return ` ${paint("dim", clock(now))} ${paint("cyan", "↻ reloaded")} config · watching ${names}`;
149
+ }
150
+ const OP_LABEL = {
151
+ toDb: ["cyan", "→ db"],
152
+ toFile: ["cyan", "→ file"],
153
+ deleteDb: ["red", "− row"],
154
+ deleteFile: ["red", "− file"]
155
+ };
156
+ function watchEvents(r, { root, now, paint, quiet, nameWidth = 0 }) {
157
+ const prefix = ` ${paint("dim", clock(now))} ${r.table.padEnd(nameWidth)} `;
158
+ const line = (style, label, text) => `${prefix}${paint(style, label.padEnd(WATCH_LABEL_WIDTH))} ${text}`;
159
+ if (!r.ok) return [line("red", "✗ error", r.error ?? "failed")];
160
+ const lines = [];
161
+ if (!quiet) {
162
+ for (const c of r.changes) lines.push(line(OP_LABEL[c.op][0], OP_LABEL[c.op][1], c.key));
163
+ for (const c of r.schema) if (c.op === "alterColumn") lines.push(line("cyan", "↻ column", `${c.name} ${c.definition}`));
164
+ else if (c.op === "dropColumn") lines.push(line("red", "− column", c.name));
165
+ else if (c.op === "createIndex") lines.push(line("green", "+ index", c.definition));
166
+ else if (c.op === "createView") lines.push(line("green", "+ view", c.definition));
167
+ else if (c.op === "dropView") lines.push(line("red", "− view", c.definition));
168
+ else lines.push(line("red", "− index", c.definition));
169
+ for (const c of r.registered) lines.push(line("green", "+ column", `${c.column} ${c.type}`));
170
+ }
171
+ for (const c of r.conflicts) lines.push(line("yellow", "⚠ conflict", `${c.key} · ${resolution(c, root)}`));
172
+ if (!quiet) for (const w of r.warnings) lines.push(line("yellow", "! warning", w));
173
+ return lines;
174
+ }
175
+ //#endregion
176
+ //#region src/serve/index.ts
177
+ const DEFAULT_UI = fileURLToPath(new URL("./ui/", import.meta.url));
178
+ const urlHostOf = (h) => h.includes(":") ? `[${h}]` : h;
179
+ function listen(server, port, host) {
180
+ return new Promise((done, fail) => {
181
+ server.once("error", (e) => {
182
+ fail(e.code === "EADDRINUSE" ? /* @__PURE__ */ new Error(`port ${port} is already in use; pick another one with --port`) : e);
183
+ });
184
+ server.listen(port, host, () => done(server.address().port));
185
+ });
186
+ }
187
+ async function serve(opts) {
188
+ const host = opts.host ?? "127.0.0.1";
189
+ let port = 0;
190
+ const w = await createWorkspace({
191
+ root: opts.root,
192
+ db: opts.db,
193
+ watch: opts.watch,
194
+ ai: opts.agent === false ? null : opts.ai ?? {
195
+ keys: envKeys(process.env),
196
+ webSearch: opts.webSearch
197
+ },
198
+ gh: opts.gh,
199
+ git: opts.git
200
+ });
201
+ try {
202
+ const token = randomBytes(24).toString("hex");
203
+ let policy = {
204
+ token,
205
+ allowedHosts: /* @__PURE__ */ new Set()
206
+ };
207
+ const http = createServer(createHandler(w.router, () => policy, opts.uiDir ?? DEFAULT_UI));
208
+ port = await listen(http, opts.port ?? 4610, host);
209
+ policy = {
210
+ token,
211
+ allowedHosts: isLoopback(host) ? loopbackHosts(port) : null
212
+ };
213
+ let closing;
214
+ return {
215
+ url: `http://${urlHostOf(host)}:${port}/?token=${token}`,
216
+ port,
217
+ token,
218
+ context: w.context,
219
+ close() {
220
+ closing ??= (async () => {
221
+ try {
222
+ http.closeAllConnections();
223
+ await new Promise((done) => http.close(() => done()));
224
+ } finally {
225
+ await w.close();
226
+ }
227
+ })();
228
+ return closing;
229
+ }
230
+ };
231
+ } catch (e) {
232
+ await w.close();
233
+ throw e;
234
+ }
235
+ }
236
+ //#endregion
237
+ //#region src/cli.ts
238
+ const { version } = createRequire(import.meta.url)("../package.json");
239
+ const collect = (value, previous) => [...previous, value];
240
+ const out = painter(process.stdout);
241
+ const err = painter(process.stderr);
242
+ function withRoot(cmd) {
243
+ return cmd.argument("[root]", "data directory", ".").option("--db <path>", "SQLite database (default: <root>/.yamlite/db.sqlite)");
244
+ }
245
+ const program = new Command().name("yamlite").description("Sync YAML files and SQLite in both directions").version(version);
246
+ withRoot(program.command("init").description("create yamlite.yaml from the YAML files and the existing database")).option("--force", "overwrite an existing yamlite.yaml").option("--print", "print the generated yamlite.yaml instead of writing it").action((root, o) => {
247
+ if (o.print) {
248
+ process.stdout.write(generateConfig({
249
+ root,
250
+ db: o.db,
251
+ force: true
252
+ }));
253
+ return;
254
+ }
255
+ const result = init({
256
+ root,
257
+ db: o.db,
258
+ force: o.force
259
+ });
260
+ const tables = result.tables.length > 0 ? result.tables.join(", ") : "no tables yet";
261
+ console.log(`${out("green", "✓")} wrote ${displayPath(result.path)} ${out("dim", `· ${tables}`)}`);
262
+ console.log(` next: ${out("bold", "yamlite status")} to preview, ${out("bold", "yamlite sync")} to apply`);
263
+ });
264
+ program.command("guide").description("print the guide to writing yamlite.yaml, for people and AI agents").action(() => {
265
+ process.stdout.write(guide());
266
+ });
267
+ program.command("check").description("check the YAML files against yamlite.yaml without writing anything; exits 1 on any problem").argument("[root]", "data directory", ".").option("--table <name>", "only this table (repeatable)", collect, []).option("--json", "print JSON").action(async (root, o) => {
268
+ const result = await check({
269
+ root,
270
+ tables: o.table
271
+ });
272
+ if (o.json) console.log(JSON.stringify(result, null, 2));
273
+ else console.log(checkReport(result, {
274
+ root: resolve(root),
275
+ paint: out
276
+ }));
277
+ if (!result.ok) process.exitCode = 1;
278
+ });
279
+ const FORMATS = {
280
+ table: formatTable,
281
+ json: formatJson,
282
+ csv: formatCsv
283
+ };
284
+ program.command("query").description("run one read-only SQL statement against the YAML files (or --db) and print the rows").argument("<sql>", "the statement").argument("[root]", "data directory", ".").option("--db <path>", "read this database as it is instead of the YAML files").option("--format <format>", "table, json or csv", "table").option("--limit <n>", "at most this many rows; 0 for no limit", "1000").action(async (sql, root, o) => {
285
+ if (!Object.hasOwn(FORMATS, o.format)) throw new Error(`unknown format ${o.format}: use table, json or csv`);
286
+ const format = FORMATS[o.format];
287
+ const limit = Number(o.limit);
288
+ if (!Number.isInteger(limit) || limit < 0) throw new Error(`--limit must be a whole number, not ${o.limit}`);
289
+ const r = await query({
290
+ root,
291
+ db: o.db,
292
+ sql,
293
+ limit
294
+ });
295
+ for (const w of r.warnings) console.error(`${err("yellow", "!")} ${w}`);
296
+ process.stdout.write(o.format === "csv" ? format(r) : `${format(r)}\n`);
297
+ if (o.format === "table") console.error(err("dim", `${r.rows.length} ${r.rows.length === 1 ? "row" : "rows"}`));
298
+ if (r.truncated) console.error(err("yellow", `… truncated at ${limit} rows (use --limit)`));
299
+ });
300
+ withRoot(program.command("status").description("show what sync would change, without writing")).option("--table <name>", "only this table (repeatable)", collect, []).option("--json", "print JSON").action(async (root, o) => {
301
+ const y = await open({
302
+ root,
303
+ db: o.db
304
+ });
305
+ try {
306
+ const results = await y.status({ tables: o.table });
307
+ if (o.json) console.log(JSON.stringify(results, null, 2));
308
+ else console.log(report(results, {
309
+ mode: "status",
310
+ root: resolve(root),
311
+ paint: out
312
+ }));
313
+ } finally {
314
+ await y.close();
315
+ }
316
+ });
317
+ withRoot(program.command("sync").description("sync once")).option("--table <name>", "only this table (repeatable)", collect, []).option("--force", "allow mass deletions").option("--force-convert", "clear values that cannot be converted when a column type changes").option("--json", "print JSON").action(async (root, o) => {
318
+ const y = await open({
319
+ root,
320
+ db: o.db
321
+ });
322
+ try {
323
+ const started = performance.now();
324
+ const results = await y.sync({
325
+ tables: o.table,
326
+ force: o.force,
327
+ forceConvert: o.forceConvert
328
+ });
329
+ const elapsedMs = performance.now() - started;
330
+ if (o.json) console.log(JSON.stringify(results, null, 2));
331
+ else console.log(report(results, {
332
+ mode: "sync",
333
+ root: resolve(root),
334
+ paint: out,
335
+ elapsedMs
336
+ }));
337
+ if (results.some((r) => !r.ok)) process.exitCode = 1;
338
+ } finally {
339
+ await y.close();
340
+ }
341
+ });
342
+ withRoot(program.command("watch").description("sync continuously")).option("--quiet", "only print conflicts and errors").action(async (root, o) => {
343
+ const y = await open({
344
+ root,
345
+ db: o.db
346
+ });
347
+ const nameWidth = () => Math.max(0, ...y.tables.map((t) => t.name.length));
348
+ await y.watch({
349
+ onSync: (r) => {
350
+ const opts = {
351
+ root: resolve(root),
352
+ now: /* @__PURE__ */ new Date(),
353
+ paint: out,
354
+ quiet: o.quiet ?? false,
355
+ nameWidth: nameWidth()
356
+ };
357
+ for (const line of watchEvents(r, opts)) console.log(line);
358
+ },
359
+ onReload: (tables) => {
360
+ if (!o.quiet) console.log(reloadLine(tables, /* @__PURE__ */ new Date(), out));
361
+ },
362
+ onError: (e, table) => {
363
+ if (table === void 0) console.error(painter(process.stderr)("red", `error: ${e.message}`));
364
+ }
365
+ }).ready;
366
+ if (!o.quiet) console.log(`${watchHeader(y.tables.map((t) => t.name), resolve(root), out)}\n`);
367
+ const stop = () => {
368
+ y.close().then(() => process.exit(0));
369
+ };
370
+ process.once("SIGINT", stop);
371
+ process.once("SIGTERM", stop);
372
+ });
373
+ function openBrowser(url) {
374
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
375
+ "/c",
376
+ "start",
377
+ "",
378
+ url
379
+ ]] : ["xdg-open", [url]];
380
+ spawn(cmd, args, {
381
+ stdio: "ignore",
382
+ detached: true
383
+ }).on("error", () => {}).unref();
384
+ }
385
+ withRoot(program.command("serve").description("open the web UI and sync continuously")).option("--port <number>", "port to listen on", "4610").option("--host <address>", "address to listen on", "127.0.0.1").option("--open", "open the UI in the browser").option("--no-agent", "do not offer Ask AI in the web UI").option("--web-search", "let Ask AI search the web; each search is billed to the API key").action(async (root, o) => {
386
+ const port = Number(o.port);
387
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`invalid port: ${o.port}`);
388
+ const s = await serve({
389
+ root,
390
+ db: o.db,
391
+ port,
392
+ host: o.host,
393
+ agent: o.agent,
394
+ webSearch: o.webSearch
395
+ });
396
+ if (!isLoopback(o.host)) {
397
+ const askAi = o.agent && Object.keys(envKeys(process.env)).length > 0;
398
+ console.error(painter(process.stderr)("yellow", `warning: listening on ${o.host}; anyone who can reach it and has the token can read and change the data${askAi ? " and use Ask AI with the API keys from the environment" : ""}`));
399
+ }
400
+ console.log(`${out("green", "✓")} YAMLite UI · ${out("bold", s.url)}`);
401
+ console.log(out("dim", ` watching ${displayPath(resolve(root))} · Ctrl+C to stop`));
402
+ if (o.open) openBrowser(s.url);
403
+ let stopping = false;
404
+ const stop = () => {
405
+ if (stopping) process.exit(130);
406
+ stopping = true;
407
+ s.close().then(() => process.exit(0), (e) => {
408
+ console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
409
+ process.exit(1);
410
+ });
411
+ };
412
+ process.on("SIGINT", stop);
413
+ process.on("SIGTERM", stop);
414
+ });
415
+ program.command("export").description("write the web UI as a read-only static site").argument("[root]", "data directory", ".").option("--out <dir>", "output folder", ".yamlite-export").option("--table <name>", "only this table (repeatable)", collect, []).option("--force", "replace an output folder that is not a previous export").action(async (root, o) => {
416
+ const r = await exportSite({
417
+ root,
418
+ out: o.out,
419
+ tables: o.table,
420
+ force: o.force
421
+ });
422
+ const count = `${r.tables.length} ${r.tables.length === 1 ? "table" : "tables"}`;
423
+ console.log(`${out("green", "✓")} exported ${count} to ${displayPath(r.out)}`);
424
+ for (const [table, warnings] of Object.entries(r.warnings)) for (const w of warnings) console.log(` ${out("yellow", "!")} ${table}: ${w}`);
425
+ for (const [page, reason] of Object.entries(r.skippedPages)) console.log(` ${out("yellow", "!")} page ${page} not exported: ${reason}`);
426
+ console.log(out("dim", " serve the folder over HTTP to open it; file:// does not work"));
427
+ });
428
+ program.parseAsync().catch((e) => {
429
+ console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
430
+ process.exitCode = 1;
431
+ });
432
+ //#endregion
433
+ export {};