simplemdg-dev-cli 2.4.5 → 2.6.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 (69) hide show
  1. package/README.md +33 -0
  2. package/USER_GUIDE.md +57 -0
  3. package/dist/commands/cache.command.d.ts +2 -0
  4. package/dist/commands/cache.command.js +129 -0
  5. package/dist/commands/cache.command.js.map +1 -0
  6. package/dist/commands/cf.command.js +201 -122
  7. package/dist/commands/cf.command.js.map +1 -1
  8. package/dist/commands/gitlab.command.js +33 -23
  9. package/dist/commands/gitlab.command.js.map +1 -1
  10. package/dist/core/cache/smart-cache-events.d.ts +3 -0
  11. package/dist/core/cache/smart-cache-events.js +20 -0
  12. package/dist/core/cache/smart-cache-events.js.map +1 -0
  13. package/dist/core/cache/smart-cache-manager.d.ts +20 -0
  14. package/dist/core/cache/smart-cache-manager.js +148 -0
  15. package/dist/core/cache/smart-cache-manager.js.map +1 -0
  16. package/dist/core/cache/smart-cache-store.d.ts +8 -0
  17. package/dist/core/cache/smart-cache-store.js +74 -0
  18. package/dist/core/cache/smart-cache-store.js.map +1 -0
  19. package/dist/core/cache/smart-cache.d.ts +18 -0
  20. package/dist/core/cache/smart-cache.js +117 -0
  21. package/dist/core/cache/smart-cache.js.map +1 -0
  22. package/dist/core/cache/smart-cache.types.d.ts +62 -0
  23. package/dist/core/cache/smart-cache.types.js +17 -0
  24. package/dist/core/cache/smart-cache.types.js.map +1 -0
  25. package/dist/core/cf/cf-target-cache.d.ts +7 -0
  26. package/dist/core/cf/cf-target-cache.js +58 -0
  27. package/dist/core/cf/cf-target-cache.js.map +1 -0
  28. package/dist/core/cf/cf-target.types.d.ts +11 -0
  29. package/dist/core/cf/cf-target.types.js +11 -0
  30. package/dist/core/cf/cf-target.types.js.map +1 -0
  31. package/dist/core/db/db-studio-client.d.ts +1 -1
  32. package/dist/core/db/db-studio-client.js +173 -44
  33. package/dist/core/db/db-studio-client.js.map +1 -1
  34. package/dist/core/db/db-studio-server.js +125 -0
  35. package/dist/core/db/db-studio-server.js.map +1 -1
  36. package/dist/core/db/db-studio-styles.d.ts +1 -1
  37. package/dist/core/db/db-studio-styles.js +36 -0
  38. package/dist/core/db/db-studio-styles.js.map +1 -1
  39. package/dist/core/db/db-types.d.ts +54 -0
  40. package/dist/core/db/studio/sql-formatter.d.ts +25 -0
  41. package/dist/core/db/studio/sql-formatter.js +139 -0
  42. package/dist/core/db/studio/sql-formatter.js.map +1 -0
  43. package/dist/core/db/studio/studio-settings.d.ts +4 -0
  44. package/dist/core/db/studio/studio-settings.js +39 -0
  45. package/dist/core/db/studio/studio-settings.js.map +1 -0
  46. package/dist/core/db/studio/workspace-cache.d.ts +3 -0
  47. package/dist/core/db/studio/workspace-cache.js +51 -0
  48. package/dist/core/db/studio/workspace-cache.js.map +1 -0
  49. package/dist/index.js +3 -1
  50. package/dist/index.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/commands/cache.command.ts +159 -0
  53. package/src/commands/cf.command.ts +232 -129
  54. package/src/commands/gitlab.command.ts +37 -21
  55. package/src/core/cache/smart-cache-events.ts +20 -0
  56. package/src/core/cache/smart-cache-manager.ts +169 -0
  57. package/src/core/cache/smart-cache-store.ts +83 -0
  58. package/src/core/cache/smart-cache.ts +97 -0
  59. package/src/core/cache/smart-cache.types.ts +79 -0
  60. package/src/core/cf/cf-target-cache.ts +61 -0
  61. package/src/core/cf/cf-target.types.ts +17 -0
  62. package/src/core/db/db-studio-client.ts +173 -44
  63. package/src/core/db/db-studio-server.ts +109 -1
  64. package/src/core/db/db-studio-styles.ts +36 -0
  65. package/src/core/db/db-types.ts +61 -0
  66. package/src/core/db/studio/sql-formatter.ts +139 -0
  67. package/src/core/db/studio/studio-settings.ts +36 -0
  68. package/src/core/db/studio/workspace-cache.ts +51 -0
  69. package/src/index.ts +3 -1
@@ -23,7 +23,7 @@ import {
23
23
  generateSelectSql,
24
24
  looksLikeProduction,
25
25
  } from "./db-metadata";
26
- import type { TResolvedDatabaseConnection, TTableChangeSet } from "./db-types";
26
+ import type { TGridSortState, TResolvedDatabaseConnection, TStudioWorkspaceState, TTableChangeSet } from "./db-types";
27
27
  import {
28
28
  deleteSavedQuery,
29
29
  listSavedQueries,
@@ -32,6 +32,15 @@ import {
32
32
  } from "./db-query-files";
33
33
  import { deleteRow, insertRow, saveTableChanges, updateRow } from "./db-row";
34
34
  import { appendQueryHistory, listQueryHistory } from "./db-query-history";
35
+ import { readWorkspace, writeWorkspace } from "./studio/workspace-cache";
36
+ import { readStudioSettings, writeStudioSettings } from "./studio/studio-settings";
37
+ import {
38
+ formatSql,
39
+ generateInsertTemplate,
40
+ generateTableQuery,
41
+ generateUpdateTemplate,
42
+ splitStatements,
43
+ } from "./studio/sql-formatter";
35
44
  import {
36
45
  detectAppDatabaseServices,
37
46
  ensureCloudFoundrySession,
@@ -39,6 +48,7 @@ import {
39
48
  importConnectionFromApp,
40
49
  listCloudFoundryAppsWithCache,
41
50
  } from "./db-btp";
51
+ import { onCacheEvent } from "../cache/smart-cache";
42
52
  import type { TDatabaseObjectKind, TDatabaseType } from "./db-types";
43
53
 
44
54
  export type TStudioServerOptions = {
@@ -200,6 +210,25 @@ export async function startStudioServer(options: TStudioServerOptions = {}): Pro
200
210
  return;
201
211
  }
202
212
 
213
+ // Server-Sent Events: stream smart-cache background-refresh notifications.
214
+ if (pathname === "/api/events" && method === "GET") {
215
+ res.writeHead(200, {
216
+ "content-type": "text/event-stream; charset=utf-8",
217
+ "cache-control": "no-cache",
218
+ connection: "keep-alive",
219
+ });
220
+ res.write(": connected\n\n");
221
+ const unsubscribe = onCacheEvent((event) => {
222
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
223
+ });
224
+ const keepAlive = setInterval(() => res.write(": ping\n\n"), 25000);
225
+ req.on("close", () => {
226
+ clearInterval(keepAlive);
227
+ unsubscribe();
228
+ });
229
+ return;
230
+ }
231
+
203
232
  // --- Connections ---------------------------------------------------------
204
233
  if (pathname === "/api/connections" && method === "GET") {
205
234
  sendJson(res, { connections: await listPublicConnections() });
@@ -565,12 +594,91 @@ export async function startStudioServer(options: TStudioServerOptions = {}): Pro
565
594
  return;
566
595
  }
567
596
 
597
+ // --- Workspace + settings ------------------------------------------------
598
+ if (pathname === "/api/studio/workspace" && method === "GET") {
599
+ sendJson(res, { workspace: await readWorkspace() });
600
+ return;
601
+ }
602
+
603
+ if (pathname === "/api/studio/workspace" && method === "PUT") {
604
+ const body = await readJsonBody(req);
605
+ const workspace = await writeWorkspace(body as unknown as TStudioWorkspaceState);
606
+ sendJson(res, { workspace });
607
+ return;
608
+ }
609
+
610
+ if (pathname === "/api/studio/settings" && method === "GET") {
611
+ sendJson(res, { settings: await readStudioSettings() });
612
+ return;
613
+ }
614
+
615
+ if (pathname === "/api/studio/settings" && method === "PUT") {
616
+ const body = await readJsonBody(req);
617
+ const settings = await writeStudioSettings(body);
618
+ sendJson(res, { settings });
619
+ return;
620
+ }
621
+
622
+ // --- SQL helpers ---------------------------------------------------------
623
+ if (pathname === "/api/sql/format" && method === "POST") {
624
+ const body = await readJsonBody(req);
625
+ sendJson(res, { sql: formatSql(getString(body, "sql")) });
626
+ return;
627
+ }
628
+
629
+ if (pathname === "/api/sql/parse-statements" && method === "POST") {
630
+ const body = await readJsonBody(req);
631
+ sendJson(res, { statements: splitStatements(getString(body, "sql")) });
632
+ return;
633
+ }
634
+
635
+ if (pathname === "/api/sql/generate-table-query" && method === "POST") {
636
+ const body = await readJsonBody(req);
637
+ const adapter = await pool.getAdapter(getString(body, "connectionId"));
638
+ const sql = generateTableQuery({
639
+ type: adapter.type,
640
+ schema: getString(body, "schema"),
641
+ table: getString(body, "table"),
642
+ where: getString(body, "where") || undefined,
643
+ sort: Array.isArray(body.sort) ? (body.sort as TGridSortState[]) : undefined,
644
+ limit: getNumber(body, "limit", 100),
645
+ offset: getNumber(body, "offset", 0),
646
+ });
647
+ sendJson(res, { sql });
648
+ return;
649
+ }
650
+
651
+ if (pathname === "/api/table/generate-sql" && method === "POST") {
652
+ const body = await readJsonBody(req);
653
+ const adapter = await pool.getAdapter(getString(body, "connectionId"));
654
+ const schema = getString(body, "schema");
655
+ const table = getString(body, "table");
656
+ const [columns, primaryKey] = await Promise.all([
657
+ adapter.listColumns(schema, table).catch(() => []),
658
+ adapter.getPrimaryKey(schema, table).catch(() => ({ columns: [] })),
659
+ ]);
660
+ sendJson(res, {
661
+ select: generateSelectSql(adapter.type, schema, table, getNumber(body, "limit", 100)),
662
+ count: generateCountSql(adapter.type, schema, table),
663
+ insert: generateInsertTemplate(adapter.type, schema, table, columns),
664
+ update: generateUpdateTemplate(adapter.type, schema, table, columns, primaryKey.columns),
665
+ });
666
+ return;
667
+ }
668
+
568
669
  // --- History -------------------------------------------------------------
569
670
  if (pathname === "/api/history" && method === "GET") {
570
671
  sendJson(res, { history: await listQueryHistory(100) });
571
672
  return;
572
673
  }
573
674
 
675
+ if (pathname === "/api/history" && method === "DELETE") {
676
+ const { clearQueryHistory } = await import("./db-query-history");
677
+ await clearQueryHistory();
678
+ sendJson(res, { cleared: true });
679
+ return;
680
+ }
681
+
574
682
  // --- Export --------------------------------------------------------------
575
683
  if (pathname === "/api/export/csv" && method === "POST") {
576
684
  const body = await readJsonBody(req);
@@ -218,4 +218,40 @@ table.grid tr.row-err td{box-shadow:inset 0 0 0 1px var(--red)}
218
218
  .toast.ok{border-left-color:var(--green)}.toast.err{border-left-color:var(--red)}.toast.warn{border-left-color:var(--yellow)}
219
219
  @keyframes slideup{from{transform:translateY(8px);opacity:0}to{transform:translateY(0);opacity:1}}
220
220
  .empty{color:var(--faint);padding:16px;text-align:center}
221
+ mark.hl{background:#facc15;color:#10151f;border-radius:3px;padding:0 1px}
222
+ /* tabs: pin + drag */
223
+ .wtab.pinned{background:#101a2b}
224
+ .wtab.pinned .t-title{max-width:90px}
225
+ .wtab.dragging{opacity:.5}
226
+ .wtab.dragover{box-shadow:inset 2px 0 0 var(--accent)}
227
+ .wtab .pin{color:var(--amber)}
228
+ /* sql preview popover */
229
+ .popover{position:absolute;z-index:55;background:var(--bg-2);border:1px solid var(--border-2);border-radius:10px;box-shadow:0 16px 50px rgba(0,0,0,.5);padding:10px;width:560px;max-width:90vw}
230
+ .popover pre{margin:0;background:#0a1018;border:1px solid var(--border);border-radius:8px;padding:10px;color:#cfe7ff;font-family:Consolas,monospace;font-size:12.5px;white-space:pre-wrap;max-height:240px;overflow:auto}
231
+ /* change summary bar */
232
+ .changebar{display:flex;align-items:center;gap:10px;padding:7px 10px;background:#2a230a;border:1px solid #a16207;border-radius:9px;margin:0 10px 8px;color:#fde68a}
233
+ .changebar .grow{flex:1}
234
+ /* breadcrumb */
235
+ .crumbs{display:flex;align-items:center;gap:6px;padding:6px 10px;color:var(--muted);font-size:12px;border-bottom:1px solid var(--border);background:var(--bg-2)}
236
+ .crumbs a{color:#8fc6ff;cursor:pointer}
237
+ .crumbs .sep{color:var(--faint)}
238
+ /* command palette */
239
+ .palette{position:fixed;top:80px;left:50%;transform:translateX(-50%);width:560px;max-width:92vw;background:var(--bg-2);border:1px solid var(--border-2);border-radius:12px;box-shadow:0 24px 80px rgba(0,0,0,.55);z-index:85;overflow:hidden}
240
+ .palette input{width:100%;background:#0a1018;border:0;border-bottom:1px solid var(--border);padding:13px 15px;color:#fff;font-size:15px}
241
+ .palette .pitems{max-height:50vh;overflow:auto}
242
+ .palette .pitem{padding:9px 14px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
243
+ .palette .pitem.sel,.palette .pitem:hover{background:var(--accent);color:#fff}
244
+ .palette .pitem .kbd{color:var(--faint);font-size:11px}
245
+ .kbd{font-family:Consolas,monospace;background:var(--chip);border:1px solid var(--border);border-radius:5px;padding:1px 6px;font-size:11px}
246
+ .shorts{display:grid;grid-template-columns:1fr 1fr;gap:6px 20px}
247
+ .shorts .srow{display:flex;justify-content:space-between;gap:10px;padding:3px 0;border-bottom:1px solid var(--border)}
248
+ /* editor gutter */
249
+ .editwrap{display:flex;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#0a1018}
250
+ .gutter{padding:11px 8px;text-align:right;color:var(--faint);font-family:Consolas,monospace;font-size:13px;line-height:1.5;user-select:none;background:#0c131e;min-width:42px;overflow:hidden}
251
+ .editwrap .editor{border:0;border-radius:0;flex:1;line-height:1.5}
252
+ .ac{position:absolute;z-index:70;background:var(--bg-2);border:1px solid var(--border-2);border-radius:8px;box-shadow:0 12px 40px rgba(0,0,0,.5);max-height:220px;overflow:auto;min-width:220px}
253
+ .ac .aci{padding:6px 11px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
254
+ .ac .aci.sel,.ac .aci:hover{background:var(--accent);color:#fff}
255
+ .ac .aci .t{color:var(--faint);font-size:11px}
256
+ .toggle{display:flex;align-items:center;gap:8px;padding:6px 0}
221
257
  `;
@@ -221,6 +221,67 @@ export type TSaveTableChangesResult = {
221
221
  rowResults: TSaveRowResult[];
222
222
  };
223
223
 
224
+ export type TGridSortState = {
225
+ column: string;
226
+ direction: "asc" | "desc";
227
+ };
228
+
229
+ export type TStudioTabType = "welcome" | "sql" | "data-grid" | "metadata" | "query-file" | "history";
230
+
231
+ export type TStudioTabState = {
232
+ id: string;
233
+ type: TStudioTabType;
234
+ title: string;
235
+ groupId?: string;
236
+ pinned?: boolean;
237
+ dirty?: boolean;
238
+ connectionId?: string;
239
+ schema?: string;
240
+ objectName?: string;
241
+ objectType?: "table" | "view";
242
+ sql?: string;
243
+ filter?: string;
244
+ pageSize?: number;
245
+ pageIndex?: number;
246
+ sort?: TGridSortState[];
247
+ openedAt: string;
248
+ updatedAt: string;
249
+ };
250
+
251
+ export type TStudioTabGroup = {
252
+ id: string;
253
+ name: string;
254
+ color: string;
255
+ collapsed?: boolean;
256
+ };
257
+
258
+ export type TStudioLayoutState = {
259
+ sidebarWidth?: number;
260
+ readOnly?: boolean;
261
+ };
262
+
263
+ export type TStudioWorkspaceState = {
264
+ version: number;
265
+ activeTabId?: string;
266
+ tabs: TStudioTabState[];
267
+ tabGroups: TStudioTabGroup[];
268
+ layout: TStudioLayoutState;
269
+ updatedAt: string;
270
+ };
271
+
272
+ export type TStudioSettings = {
273
+ restoreWorkspace: boolean;
274
+ defaultRowLimit: number;
275
+ defaultSchema?: string;
276
+ readOnlyByDefault: boolean;
277
+ queryTimeoutMs: number;
278
+ autoFormatGeneratedSql: boolean;
279
+ autoSaveDelayMs: number;
280
+ maxHistoryItems: number;
281
+ showProductionWarning: boolean;
282
+ theme: string;
283
+ };
284
+
224
285
  export interface IDatabaseAdapter {
225
286
  readonly type: TDatabaseType;
226
287
  connect(): Promise<void>;
@@ -0,0 +1,139 @@
1
+ import { buildQualifiedName, quoteIdentifier } from "../db-metadata";
2
+ import type { TDatabaseColumn, TDatabaseType, TGridSortState } from "../db-types";
3
+
4
+ const CLAUSE_KEYWORDS = [
5
+ "select", "from", "where", "group by", "having", "order by", "limit", "offset",
6
+ "union all", "union", "left join", "right join", "inner join", "full join", "join", "on",
7
+ "insert into", "values", "update", "set", "delete from", "create table", "alter table",
8
+ ];
9
+
10
+ /** Lightweight SQL pretty printer: uppercases clause keywords and breaks lines. */
11
+ export function formatSql(sql: string): string {
12
+ let text = sql.replace(/\s+/g, " ").trim();
13
+
14
+ for (const keyword of CLAUSE_KEYWORDS) {
15
+ const pattern = new RegExp(`\\s*\\b${keyword.replace(/ /g, "\\s+")}\\b\\s*`, "gi");
16
+ text = text.replace(pattern, (match) => {
17
+ const upper = keyword.toUpperCase();
18
+ const isJoinOrOn = /join|^on$/i.test(keyword);
19
+ return (isJoinOrOn ? "\n " : "\n") + upper + " ";
20
+ });
21
+ }
22
+
23
+ return text.replace(/\n\s*\n/g, "\n").replace(/[ \t]+\n/g, "\n").trim();
24
+ }
25
+
26
+ export type TStatementRange = { sql: string; start: number; end: number };
27
+
28
+ /** Split SQL into statements on `;`, ignoring semicolons inside strings/comments. */
29
+ export function splitStatementRanges(sql: string): TStatementRange[] {
30
+ const ranges: TStatementRange[] = [];
31
+ let buffer = "";
32
+ let bufferStart = -1;
33
+ let inString = false;
34
+ let quote = "";
35
+ let inLine = false;
36
+ let inBlock = false;
37
+
38
+ const push = (endIndex: number): void => {
39
+ if (buffer.trim()) {
40
+ ranges.push({ sql: buffer.trim(), start: bufferStart, end: endIndex });
41
+ }
42
+ buffer = "";
43
+ bufferStart = -1;
44
+ };
45
+
46
+ for (let i = 0; i < sql.length; i += 1) {
47
+ const ch = sql[i];
48
+ const next = sql[i + 1];
49
+
50
+ if (bufferStart === -1 && !/\s/.test(ch)) {
51
+ bufferStart = i;
52
+ }
53
+
54
+ if (inLine) {
55
+ buffer += ch;
56
+ if (ch === "\n") inLine = false;
57
+ continue;
58
+ }
59
+ if (inBlock) {
60
+ buffer += ch;
61
+ if (ch === "*" && next === "/") { buffer += next; i += 1; inBlock = false; }
62
+ continue;
63
+ }
64
+ if (inString) {
65
+ buffer += ch;
66
+ if (ch === quote) inString = false;
67
+ continue;
68
+ }
69
+ if (ch === "-" && next === "-") { inLine = true; buffer += ch; continue; }
70
+ if (ch === "/" && next === "*") { inBlock = true; buffer += ch; continue; }
71
+ if (ch === "'" || ch === '"') { inString = true; quote = ch; buffer += ch; continue; }
72
+ if (ch === ";") { push(i); continue; }
73
+ buffer += ch;
74
+ }
75
+
76
+ push(sql.length);
77
+ return ranges;
78
+ }
79
+
80
+ export function splitStatements(sql: string): string[] {
81
+ return splitStatementRanges(sql).map((range) => range.sql);
82
+ }
83
+
84
+ /** The statement that contains the given cursor offset (for "Run current statement"). */
85
+ export function statementAtOffset(sql: string, offset: number): string {
86
+ const ranges = splitStatementRanges(sql);
87
+ const hit = ranges.find((range) => offset >= range.start && offset <= range.end + 1);
88
+ return (hit ?? ranges[ranges.length - 1])?.sql ?? sql.trim();
89
+ }
90
+
91
+ export type TGenerateTableQueryInput = {
92
+ type: TDatabaseType;
93
+ schema: string;
94
+ table: string;
95
+ where?: string;
96
+ sort?: TGridSortState[];
97
+ limit?: number;
98
+ offset?: number;
99
+ };
100
+
101
+ export function generateTableQuery(input: TGenerateTableQueryInput): string {
102
+ const qualified = buildQualifiedName(input.type, input.schema, input.table);
103
+ const lines = ["SELECT *", `FROM ${qualified}`];
104
+
105
+ if (input.where && input.where.trim()) {
106
+ lines.push(`WHERE ${input.where.trim()}`);
107
+ }
108
+
109
+ if (input.sort && input.sort.length > 0) {
110
+ const order = input.sort
111
+ .map((sort) => `${quoteIdentifier(input.type, sort.column)} ${sort.direction === "desc" ? "DESC" : "ASC"}`)
112
+ .join(", ");
113
+ lines.push(`ORDER BY ${order}`);
114
+ }
115
+
116
+ if (input.limit && input.limit > 0) {
117
+ lines.push(`LIMIT ${input.limit}`);
118
+ lines.push(`OFFSET ${input.offset ?? 0}`);
119
+ }
120
+
121
+ return `${lines.join("\n")};`;
122
+ }
123
+
124
+ export function generateInsertTemplate(type: TDatabaseType, schema: string, table: string, columns: TDatabaseColumn[]): string {
125
+ const cols = columns.length > 0 ? columns : [{ name: "column1" } as TDatabaseColumn, { name: "column2" } as TDatabaseColumn];
126
+ const colNames = cols.map((column) => quoteIdentifier(type, column.name));
127
+ const placeholders = cols.map((column) => `<${column.name}>`);
128
+ return `INSERT INTO ${buildQualifiedName(type, schema, table)} (\n ${colNames.join(", ")}\n) VALUES (\n ${placeholders.join(", ")}\n);`;
129
+ }
130
+
131
+ export function generateUpdateTemplate(type: TDatabaseType, schema: string, table: string, columns: TDatabaseColumn[], primaryKey: string[]): string {
132
+ const keyCols = primaryKey.length > 0 ? primaryKey : columns.slice(0, 1).map((column) => column.name);
133
+ const setCols = columns.filter((column) => !keyCols.includes(column.name));
134
+ const setClause = (setCols.length > 0 ? setCols : columns)
135
+ .map((column) => `${quoteIdentifier(type, column.name)} = <${column.name}>`)
136
+ .join(",\n ");
137
+ const whereClause = keyCols.map((column) => `${quoteIdentifier(type, column)} = <${column}>`).join("\n AND ");
138
+ return `UPDATE ${buildQualifiedName(type, schema, table)}\nSET\n ${setClause}\nWHERE\n ${whereClause};`;
139
+ }
@@ -0,0 +1,36 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
+ import type { TStudioSettings } from "../db-types";
5
+
6
+ const SETTINGS_PATH = path.join(os.homedir(), ".simplemdg", "db-studio-settings.json");
7
+
8
+ export const DEFAULT_SETTINGS: TStudioSettings = {
9
+ restoreWorkspace: true,
10
+ defaultRowLimit: 100,
11
+ defaultSchema: undefined,
12
+ readOnlyByDefault: false,
13
+ queryTimeoutMs: 30000,
14
+ autoFormatGeneratedSql: true,
15
+ autoSaveDelayMs: 500,
16
+ maxHistoryItems: 300,
17
+ showProductionWarning: true,
18
+ theme: "dark",
19
+ };
20
+
21
+ export async function readStudioSettings(): Promise<TStudioSettings> {
22
+ if (!(await fs.pathExists(SETTINGS_PATH))) {
23
+ return { ...DEFAULT_SETTINGS };
24
+ }
25
+
26
+ const parsed = await fs.readJson(SETTINGS_PATH).catch(() => ({})) as Partial<TStudioSettings>;
27
+ return { ...DEFAULT_SETTINGS, ...parsed };
28
+ }
29
+
30
+ export async function writeStudioSettings(patch: Partial<TStudioSettings>): Promise<TStudioSettings> {
31
+ const current = await readStudioSettings();
32
+ const next: TStudioSettings = { ...current, ...patch };
33
+ await fs.ensureDir(path.dirname(SETTINGS_PATH));
34
+ await fs.writeJson(SETTINGS_PATH, next, { spaces: 2 });
35
+ return next;
36
+ }
@@ -0,0 +1,51 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
+ import type { TStudioWorkspaceState } from "../db-types";
5
+
6
+ const WORKSPACE_PATH = path.join(os.homedir(), ".simplemdg", "db-studio-workspace.json");
7
+ const WORKSPACE_VERSION = 1;
8
+
9
+ const EMPTY_WORKSPACE: TStudioWorkspaceState = {
10
+ version: WORKSPACE_VERSION,
11
+ activeTabId: undefined,
12
+ tabs: [],
13
+ tabGroups: [],
14
+ layout: {},
15
+ updatedAt: new Date(0).toISOString(),
16
+ };
17
+
18
+ export async function readWorkspace(): Promise<TStudioWorkspaceState> {
19
+ if (!(await fs.pathExists(WORKSPACE_PATH))) {
20
+ return { ...EMPTY_WORKSPACE };
21
+ }
22
+
23
+ const parsed = await fs.readJson(WORKSPACE_PATH).catch(() => undefined) as Partial<TStudioWorkspaceState> | undefined;
24
+
25
+ if (!parsed || !Array.isArray(parsed.tabs)) {
26
+ return { ...EMPTY_WORKSPACE };
27
+ }
28
+
29
+ return {
30
+ version: WORKSPACE_VERSION,
31
+ activeTabId: parsed.activeTabId,
32
+ tabs: parsed.tabs,
33
+ tabGroups: Array.isArray(parsed.tabGroups) ? parsed.tabGroups : [],
34
+ layout: parsed.layout ?? {},
35
+ updatedAt: parsed.updatedAt ?? new Date().toISOString(),
36
+ };
37
+ }
38
+
39
+ export async function writeWorkspace(state: TStudioWorkspaceState): Promise<TStudioWorkspaceState> {
40
+ const next: TStudioWorkspaceState = {
41
+ version: WORKSPACE_VERSION,
42
+ activeTabId: state.activeTabId,
43
+ tabs: Array.isArray(state.tabs) ? state.tabs : [],
44
+ tabGroups: Array.isArray(state.tabGroups) ? state.tabGroups : [],
45
+ layout: state.layout ?? {},
46
+ updatedAt: new Date().toISOString(),
47
+ };
48
+ await fs.ensureDir(path.dirname(WORKSPACE_PATH));
49
+ await fs.writeJson(WORKSPACE_PATH, next, { spaces: 2 });
50
+ return next;
51
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import { registerCloudFoundryCommands } from "./commands/cf.command";
23
23
  import { registerCdsCommands } from "./commands/cds.command";
24
24
  import { registerNpmrcCommands } from "./commands/npmrc.command";
25
25
  import { registerGitLabCommands } from "./commands/gitlab.command";
26
+ import { registerCacheCommands } from "./commands/cache.command";
26
27
  import { enableInteractiveNavigation, runGroupNavigator } from "./core/navigator";
27
28
  import type { TInstallCommandOptions, TKeyValueMap } from "./types-local";
28
29
 
@@ -319,7 +320,7 @@ async function runInstallCommand(options: TInstallCommandOptions): Promise<void>
319
320
  process.exitCode = installResult.exitCode;
320
321
  }
321
322
 
322
- program.name("simplemdg").description("SimpleMDG local development helper").version("2.4.0");
323
+ program.name("simplemdg").description("SimpleMDG local development helper").version("2.6.0");
323
324
 
324
325
 
325
326
  program
@@ -418,6 +419,7 @@ registerCloudFoundryCommands(program);
418
419
  registerCdsCommands(program);
419
420
  registerNpmrcCommands(program);
420
421
  registerGitLabCommands(program);
422
+ registerCacheCommands(program);
421
423
 
422
424
  // Turn every group command (cf, cf db, cds, npmrc, gitlab, ...) into an
423
425
  // interactive menu so a partial command like `smdg cf` or `smdg cf db` lists