supatool 0.6.1 → 0.6.2

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.
@@ -25,6 +25,7 @@ Common Options:
25
25
  -c, --connection <string> Connection string (postgresql:// or postgres://)
26
26
  -o, --output-dir <path> Output directory
27
27
  --schema <schemas> Target schemas (comma-separated, default: public)
28
+ --schema-only Regenerate only the specified schema files; skip index files
28
29
  --config <path> Configuration file path
29
30
  -f, --force Force overwrite
30
31
 
@@ -77,6 +77,7 @@ program
77
77
  .option('--schema <schemas>', 'Target schemas, comma-separated (default: public)')
78
78
  .option('--all-schemas', 'Target all schemas in the DB (use with -e to exclude some)')
79
79
  .option('-e, --exclude-schema <schemas>', 'Schemas to exclude, comma-separated. Without --schema, targets all schemas automatically.')
80
+ .option('--schema-only', 'Regenerate only the specified schema files; do not touch other schemas or index files (llms.txt, schema_index.json, etc.)')
80
81
  .option('--config <path>', 'Configuration file path')
81
82
  .option('-f, --force', 'Force overwrite without confirmation')
82
83
  .action(async (options) => {
@@ -86,6 +87,18 @@ program
86
87
  if (!config.connectionString)
87
88
  connectionRequiredError();
88
89
  try {
90
+ if (options.schemaOnly && !options.schema) {
91
+ console.error('⚠️ --schema-only requires --schema to be specified.');
92
+ process.exit(1);
93
+ }
94
+ if (options.schemaOnly && options.all) {
95
+ console.error('⚠️ --schema-only cannot be combined with --all. Use --schema-only without --all to regenerate only the specified schema files.');
96
+ process.exit(1);
97
+ }
98
+ if (options.schemaOnly && options.allSchemas) {
99
+ console.error('⚠️ --schema-only cannot be combined with --all-schemas.');
100
+ process.exit(1);
101
+ }
89
102
  let schemas = ['public'];
90
103
  if (options.schema) {
91
104
  schemas = options.schema.split(',').map((s) => s.trim());
@@ -106,6 +119,7 @@ program
106
119
  allSchemas: options.allSchemas || false,
107
120
  excludeSchemas,
108
121
  schemasExplicit: !!options.schema,
122
+ schemaOnly: options.schemaOnly || false,
109
123
  version: package_json_1.version
110
124
  });
111
125
  }
@@ -257,12 +257,11 @@ async function fetchRelationList(client, schemas = ['public']) {
257
257
  */
258
258
  async function fetchAllSchemas(client) {
259
259
  const result = await client.query(`
260
- SELECT schema_name
261
- FROM information_schema.schemata
262
- WHERE schema_name NOT LIKE 'pg_%'
263
- AND schema_name != 'information_schema'
264
- AND schema_name != 'pg_catalog'
265
- ORDER BY schema_name
260
+ SELECT nspname AS schema_name
261
+ FROM pg_catalog.pg_namespace
262
+ WHERE nspname NOT LIKE 'pg_%'
263
+ AND nspname != 'information_schema'
264
+ ORDER BY nspname
266
265
  `);
267
266
  return result.rows.map((r) => r.schema_name);
268
267
  }
@@ -1039,7 +1038,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
1039
1038
  /**
1040
1039
  * Save definitions to files (merge RLS/triggers into table/view; schema folders when multi-schema)
1041
1040
  */
1042
- async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true, schemas = ['public'], relations = [], rpcTables = [], allSchemas = [], version, tableRlsStatus = [], force = false) {
1041
+ async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true, schemas = ['public'], relations = [], rpcTables = [], allSchemas = [], version, tableRlsStatus = [], force = false, schemaOnly = false) {
1043
1042
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1044
1043
  const path = await Promise.resolve().then(() => __importStar(require('path')));
1045
1044
  const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
@@ -1116,7 +1115,10 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
1116
1115
  ...cronJobs,
1117
1116
  ...customTypes
1118
1117
  ];
1119
- const multiSchema = schemas.length > 1;
1118
+ // schemaOnly always uses schema subdirectories (outputDir/<schema>/type/)
1119
+ // so that single-schema --schema-only runs write to the correct location
1120
+ // and don't interfere with other schemas already on disk.
1121
+ const multiSchema = schemas.length > 1 || schemaOnly;
1120
1122
  const typeDirNames = {
1121
1123
  table: 'tables',
1122
1124
  view: 'views',
@@ -1153,7 +1155,8 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
1153
1155
  }
1154
1156
  await fsPromises.writeFile(filePath, newContent);
1155
1157
  }
1156
- // When force: delete SQL files that no longer correspond to any extracted object
1158
+ // When force: delete SQL files that no longer correspond to any extracted object.
1159
+ // With schemaOnly: restrict deletion to the target schema directories only.
1157
1160
  if (force && fs.existsSync(outputDir)) {
1158
1161
  const deleteStaleSqlFiles = (dir) => {
1159
1162
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -1166,10 +1169,41 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
1166
1169
  }
1167
1170
  }
1168
1171
  };
1169
- deleteStaleSqlFiles(outputDir);
1172
+ if (schemaOnly) {
1173
+ // Only scan within the target schema directories
1174
+ for (const schemaName of schemas) {
1175
+ const schemaDir = path.join(outputDir, schemaName);
1176
+ if (fs.existsSync(schemaDir)) {
1177
+ deleteStaleSqlFiles(schemaDir);
1178
+ }
1179
+ }
1180
+ }
1181
+ else {
1182
+ deleteStaleSqlFiles(outputDir);
1183
+ }
1184
+ }
1185
+ // When schemaOnly: skip index file regeneration to avoid overwriting
1186
+ // shared index files (llms.txt, schema_index.json, etc.) with partial data.
1187
+ if (schemaOnly) {
1188
+ return;
1170
1189
  }
1171
1190
  await generateIndexFile(toWrite, outputDir, separateDirectories, multiSchema, relations, rpcTables, allSchemas, schemas, version, tableRlsStatus);
1172
1191
  }
1192
+ /**
1193
+ * Write a file only when content has changed, to avoid unnecessary Git diffs.
1194
+ * When skipFirstLine is true, the first line (typically a date-bearing header) is
1195
+ * excluded from the comparison so that daily date changes don't trigger writes.
1196
+ */
1197
+ function writeFileIfChanged(filePath, newContent, skipFirstLine = false) {
1198
+ const fs = require('fs');
1199
+ if (fs.existsSync(filePath)) {
1200
+ const existing = fs.readFileSync(filePath, 'utf8');
1201
+ const strip = (c) => skipFirstLine ? c.split('\n').slice(1).join('\n') : c;
1202
+ if (strip(existing) === strip(newContent))
1203
+ return;
1204
+ }
1205
+ fs.writeFileSync(filePath, newContent, 'utf8');
1206
+ }
1173
1207
  /**
1174
1208
  * Generate index file for DB objects (RLS/triggers already merged into table/view)
1175
1209
  */
@@ -1291,8 +1325,8 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
1291
1325
  }
1292
1326
  const readmePath = path.join(outputDir, 'README.md');
1293
1327
  const llmsPath = path.join(outputDir, 'llms.txt');
1294
- fs.writeFileSync(readmePath, readmeContent);
1295
- fs.writeFileSync(llmsPath, llmsContent);
1328
+ writeFileIfChanged(readmePath, readmeContent, true);
1329
+ writeFileIfChanged(llmsPath, llmsContent, true);
1296
1330
  // schema_index.json (same data for agents that parse JSON)
1297
1331
  const schemaIndex = {
1298
1332
  objects: definitions.map(def => {
@@ -1319,7 +1353,7 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
1319
1353
  }
1320
1354
  : undefined
1321
1355
  };
1322
- fs.writeFileSync(path.join(outputDir, 'schema_index.json'), JSON.stringify(schemaIndex, null, 2), 'utf8');
1356
+ writeFileIfChanged(path.join(outputDir, 'schema_index.json'), JSON.stringify(schemaIndex, null, 2) + '\n');
1323
1357
  // schema_summary.md (one-file overview for AI) — include RLS status per table
1324
1358
  let summaryMd = '# Schema summary\n\n';
1325
1359
  const tableDefs = definitions.filter(d => d.type === 'table' || d.type === 'view');
@@ -1352,7 +1386,7 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
1352
1386
  summaryMd += `- Extracted: ${allSchemas.filter(s => extractedSet.has(s)).join(', ') || '(none)'}\n`;
1353
1387
  summaryMd += `- Not extracted: ${allSchemas.filter(s => !extractedSet.has(s)).join(', ') || '(none)'}\n`;
1354
1388
  }
1355
- fs.writeFileSync(path.join(outputDir, 'schema_summary.md'), summaryMd, 'utf8');
1389
+ writeFileIfChanged(path.join(outputDir, 'schema_summary.md'), summaryMd);
1356
1390
  // RLS disabled tables warning doc (tables only; RLS enabled with 0 policies is not warned)
1357
1391
  const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
1358
1392
  if (rlsNotEnabled.length > 0) {
@@ -1363,14 +1397,14 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
1363
1397
  rlsNotEnabled.forEach(s => {
1364
1398
  warnMd += `| ${s.schema} | ${s.table} |\n`;
1365
1399
  });
1366
- fs.writeFileSync(path.join(outputDir, 'rls_warnings.md'), warnMd, 'utf8');
1400
+ writeFileIfChanged(path.join(outputDir, 'rls_warnings.md'), warnMd);
1367
1401
  }
1368
1402
  }
1369
1403
  /**
1370
1404
  * Classify and output definitions
1371
1405
  */
1372
1406
  async function extractDefinitions(options) {
1373
- const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas: schemasOption = ['public'], excludeSchemas = [], allSchemas: useAllSchemas = false, schemasExplicit = false, version } = options;
1407
+ const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas: schemasOption = ['public'], excludeSchemas = [], allSchemas: useAllSchemas = false, schemasExplicit = false, schemaOnly = false, version } = options;
1374
1408
  // schemas will be resolved after DB connect when useAllSchemas is true
1375
1409
  let schemas = schemasOption;
1376
1410
  // Disable Node.js SSL certificate verification
@@ -1390,18 +1424,13 @@ async function extractDefinitions(options) {
1390
1424
  }
1391
1425
  // URL encode password part
1392
1426
  let encodedConnectionString = connectionString;
1393
- console.log('🔍 Original connection string:', connectionString);
1394
1427
  try {
1395
1428
  // Special handling when password contains @
1396
1429
  if (connectionString.includes('@') && connectionString.split('@').length > 2) {
1397
- console.log('⚠️ Password contains @, executing special handling');
1398
1430
  // Use last @ as delimiter
1399
1431
  const parts = connectionString.split('@');
1400
1432
  const lastPart = parts.pop(); // Last part (host:port/database)
1401
1433
  const firstParts = parts.join('@'); // First part (postgresql://user:password)
1402
- console.log(' Split result:');
1403
- console.log(' First part:', firstParts);
1404
- console.log(' Last part:', lastPart);
1405
1434
  // Encode password part
1406
1435
  const colonIndex = firstParts.lastIndexOf(':');
1407
1436
  if (colonIndex > 0) {
@@ -1409,50 +1438,40 @@ async function extractDefinitions(options) {
1409
1438
  const password = firstParts.substring(colonIndex + 1);
1410
1439
  const encodedPassword = encodeURIComponent(password);
1411
1440
  encodedConnectionString = `${protocolAndUser}:${encodedPassword}@${lastPart}`;
1412
- console.log(' Encode result:');
1413
- console.log(' Protocol+User:', protocolAndUser);
1414
- console.log(' Original password:', password);
1415
- console.log(' Encoded password:', encodedPassword);
1416
- console.log(' Final connection string:', encodedConnectionString);
1417
1441
  }
1418
1442
  }
1419
1443
  else {
1420
- console.log('✅ Executing normal URL parsing');
1421
1444
  // Normal URL parsing
1422
1445
  const url = new URL(connectionString);
1423
- // Handle username containing dots
1424
- if (url.username && url.username.includes('.')) {
1425
- console.log(`Username (with dots): ${url.username}`);
1426
- }
1427
1446
  if (url.password) {
1428
1447
  // Encode only password part
1429
1448
  const encodedPassword = encodeURIComponent(url.password);
1430
1449
  url.password = encodedPassword;
1431
1450
  encodedConnectionString = url.toString();
1432
- console.log(' Password encoded:', encodedPassword);
1433
1451
  }
1434
1452
  }
1435
1453
  // Add SSL settings for Supabase connection
1436
1454
  if (!encodedConnectionString.includes('sslmode=')) {
1437
1455
  const separator = encodedConnectionString.includes('?') ? '&' : '?';
1438
1456
  encodedConnectionString += `${separator}sslmode=require`;
1439
- console.log(' SSL setting added:', encodedConnectionString);
1440
1457
  }
1441
- // Display debug info (password hidden)
1442
- const debugUrl = new URL(encodedConnectionString);
1443
- const maskedPassword = debugUrl.password ? '*'.repeat(debugUrl.password.length) : '';
1444
- debugUrl.password = maskedPassword;
1445
- console.log('🔍 Connection info:');
1446
- console.log(` Host: ${debugUrl.hostname}`);
1447
- console.log(` Port: ${debugUrl.port}`);
1448
- console.log(` Database: ${debugUrl.pathname.slice(1)}`);
1449
- console.log(` User: ${debugUrl.username}`);
1450
- console.log(` SSL: ${debugUrl.searchParams.get('sslmode') || 'require'}`);
1458
+ // Debug-only: connection info (no credentials)
1459
+ if (process.env.SUPATOOL_DEBUG) {
1460
+ const debugUrl = new URL(encodedConnectionString);
1461
+ console.log('🔍 Connection info:');
1462
+ console.log(` Host: ${debugUrl.hostname}`);
1463
+ console.log(` Port: ${debugUrl.port}`);
1464
+ console.log(` Database: ${debugUrl.pathname.slice(1)}`);
1465
+ console.log(` User: ${debugUrl.username}`);
1466
+ console.log(` SSL: ${debugUrl.searchParams.get('sslmode') || 'require'}`);
1467
+ }
1451
1468
  }
1452
1469
  catch (error) {
1453
1470
  // Use original string if URL parsing fails
1454
- console.warn('Failed to parse connection string URL. May contain special characters.');
1455
- console.warn('Error details:', error instanceof Error ? error.message : String(error));
1471
+ if (process.env.SUPATOOL_DEBUG) {
1472
+ console.warn('Failed to parse connection string URL. May contain special characters.');
1473
+ console.warn('Error details:', error instanceof Error ? error.message : String(error));
1474
+ }
1456
1475
  }
1457
1476
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1458
1477
  const readline = await Promise.resolve().then(() => __importStar(require('readline')));
@@ -1485,10 +1504,6 @@ async function extractDefinitions(options) {
1485
1504
  }
1486
1505
  });
1487
1506
  try {
1488
- // Debug before connect
1489
- console.log('🔧 Connection settings:');
1490
- console.log(` SSL: rejectUnauthorized=false`);
1491
- console.log(` Connection string length: ${encodedConnectionString.length}`);
1492
1507
  await client.connect();
1493
1508
  spinner.text = 'Connected to database';
1494
1509
  // Resolve schemas: --all-schemas or -e alone (without explicit --schema) → fetch all from DB and subtract excludeSchemas
@@ -1653,7 +1668,7 @@ async function extractDefinitions(options) {
1653
1668
  }
1654
1669
  // Save definitions (table+RLS+triggers merged, schema folders)
1655
1670
  spinner.text = 'Saving definitions to files...';
1656
- await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version, tableRlsStatus, force);
1671
+ await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version, tableRlsStatus, force, schemaOnly);
1657
1672
  // Warn at extract time when any table has RLS disabled
1658
1673
  const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
1659
1674
  if (rlsNotEnabled.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supatool",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "CLI for PostgreSQL (Cloud SQL / Supabase): extract schema to files, deploy schema diffs, apply migrations, seed export.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",