@ductape/cli 0.2.10 → 0.2.11

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.
@@ -3,4 +3,8 @@ interface SchemaGenerateOpts {
3
3
  destructive?: boolean;
4
4
  }
5
5
  export declare function runDbSchemaGenerate(opts: SchemaGenerateOpts): Promise<void>;
6
+ interface SchemaPushOpts {
7
+ db: string;
8
+ }
9
+ export declare function runDbSchemaPush(opts: SchemaPushOpts): Promise<void>;
6
10
  export {};
@@ -5,6 +5,7 @@ import { loadSchemaFile, getSchemaPath, parseTableDef } from '../lib/schema-load
5
5
  import { loadMigrationFiles, writeMigrationFile } from '../lib/migration-files.js';
6
6
  import { fail } from '../lib/output.js';
7
7
  import { getDbProxy, requireSession } from '../lib/proxy/context.js';
8
+ import { pushDatabaseSchema } from '../lib/integrations.js';
8
9
  function replayMigrations(migrations) {
9
10
  const schema = new Map();
10
11
  for (const migration of migrations) {
@@ -338,3 +339,72 @@ function requireSilent() {
338
339
  return null;
339
340
  }
340
341
  }
342
+ function adaptColumn(col) {
343
+ return {
344
+ name: String(col.name ?? ''),
345
+ type: String(col.type ?? 'string'),
346
+ nullable: col.nullable !== false && col.nullable !== 'NO',
347
+ primaryKey: Boolean(col.primaryKey ?? col.isPrimaryKey),
348
+ unique: Boolean(col.unique ?? col.isUnique),
349
+ autoIncrement: Boolean(col.autoIncrement ?? col.isAutoIncrement ?? col.autoGenerate),
350
+ ...(col.defaultValue != null ? { defaultValue: col.defaultValue } : {}),
351
+ };
352
+ }
353
+ export async function runDbSchemaPush(opts) {
354
+ const session = requireSession();
355
+ const proxy = getDbProxy(session);
356
+ const envSlug = session.project.env_slug;
357
+ const productTag = session.project.product_tag;
358
+ const dbContext = { database: opts.db, env: envSlug, product: productTag };
359
+ console.log(`Connecting to database "${opts.db}" (env: ${envSlug})...`);
360
+ let tableNames = [];
361
+ try {
362
+ const raw = await proxy.execute('schema.list', [null], dbContext);
363
+ if (Array.isArray(raw)) {
364
+ tableNames = raw.map((r) => typeof r === 'string' ? r : String(r.name ?? '')).filter(Boolean);
365
+ }
366
+ }
367
+ catch (e) {
368
+ throw new Error(`Failed to list tables: ${e.message}. Make sure the database is connected.`);
369
+ }
370
+ if (tableNames.length === 0) {
371
+ console.log('No tables found in the database. Nothing to push.');
372
+ return;
373
+ }
374
+ console.log(`Found ${tableNames.length} table(s): ${tableNames.join(', ')}`);
375
+ const now = new Date().toISOString();
376
+ const tableSchemas = [];
377
+ for (const tableName of tableNames) {
378
+ try {
379
+ const tableSchema = await proxy.execute('schema.describe', [tableName], dbContext);
380
+ const cols = Array.isArray(tableSchema?.columns)
381
+ ? tableSchema.columns
382
+ : Array.isArray(tableSchema) ? tableSchema : [];
383
+ if (cols.length === 0) {
384
+ console.warn(` [skip] ${tableName}: no columns returned`);
385
+ continue;
386
+ }
387
+ const columns = cols
388
+ .filter((c) => c.name)
389
+ .map(adaptColumn);
390
+ tableSchemas.push({ table: tableName, columns, updated_at: now });
391
+ console.log(` ${tableName}: ${columns.length} column(s)`);
392
+ }
393
+ catch (e) {
394
+ console.warn(` [skip] ${tableName}: ${e.message}`);
395
+ }
396
+ }
397
+ if (tableSchemas.length === 0) {
398
+ console.log('No schema data collected. Nothing pushed.');
399
+ return;
400
+ }
401
+ console.log(`\nPushing schema for ${tableSchemas.length} table(s) to Ductape...`);
402
+ const result = await pushDatabaseSchema(session.apiUrl, session.credentials, {
403
+ workspace_id: session.project.workspace_id,
404
+ product_tag: productTag,
405
+ database_tag: opts.db,
406
+ table_schemas: tableSchemas,
407
+ });
408
+ console.log(`Done. Synced ${result.synced} table(s): ${result.tables.join(', ')}`);
409
+ console.log('The AI payload generator will now use this schema for field guidance.');
410
+ }
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { runResourceCrud, runResourcesList } from './commands/resources.js';
16
16
  import { runCloud } from './commands/cloud.js';
17
17
  import { runDb, runDbContext } from './commands/db.js';
18
18
  import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
19
- import { runDbSchemaGenerate } from './commands/db-schema.js';
19
+ import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
20
20
  import { runGraph } from './commands/graph.js';
21
21
  import { runSecrets } from './commands/secrets.js';
22
22
  import { runWorkspacesCurrent, runWorkspacesList, runWorkspacesRefresh, runWorkspacesUse, } from './commands/workspaces.js';
@@ -305,6 +305,11 @@ dbSchema
305
305
  .option('--db <tag>', 'Limit to one db entry from schema.json')
306
306
  .option('--destructive', 'Also generate drop migrations for removed fields/tables')
307
307
  .action(wrap((opts) => runDbSchemaGenerate({ db: opts.db, destructive: Boolean(opts.destructive) })));
308
+ dbSchema
309
+ .command('push')
310
+ .description('Read live table schema from the database and sync it to the Ductape server (enables AI field guidance)')
311
+ .requiredOption('--db <tag>', 'Database tag to sync')
312
+ .action(wrap((opts) => runDbSchemaPush({ db: opts.db })));
308
313
  db
309
314
  .argument('[verb]', 'connect | query | …')
310
315
  .option('-f, --file <path>')
@@ -1,4 +1,18 @@
1
1
  import type { Credentials } from './config.js';
2
+ export interface TableColumnRecord {
3
+ name: string;
4
+ type: string;
5
+ nullable: boolean;
6
+ primaryKey: boolean;
7
+ unique: boolean;
8
+ autoIncrement: boolean;
9
+ defaultValue?: unknown;
10
+ }
11
+ export interface TableSchemaRecord {
12
+ table: string;
13
+ columns: TableColumnRecord[];
14
+ updated_at: string;
15
+ }
2
16
  export interface GeneratePayloadRequest {
3
17
  workspace_id: string;
4
18
  user_id: string;
@@ -18,3 +32,12 @@ export interface GeneratePayloadResponse {
18
32
  meta: Record<string, unknown>;
19
33
  }
20
34
  export declare function generateExecutablePayload(apiUrl: string, creds: Credentials, request: GeneratePayloadRequest): Promise<GeneratePayloadResponse>;
35
+ export declare function pushDatabaseSchema(apiUrl: string, creds: Credentials, opts: {
36
+ workspace_id: string;
37
+ product_tag: string;
38
+ database_tag: string;
39
+ table_schemas: TableSchemaRecord[];
40
+ }): Promise<{
41
+ synced: number;
42
+ tables: string[];
43
+ }>;
@@ -18,3 +18,30 @@ export async function generateExecutablePayload(apiUrl, creds, request) {
18
18
  }
19
19
  return (body.data ?? body);
20
20
  }
21
+ export async function pushDatabaseSchema(apiUrl, creds, opts) {
22
+ const url = `${apiUrl}/integrations/v1/databases/schema-sync`;
23
+ const res = await fetch(url, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ Authorization: `Bearer ${creds.auth_token}`,
28
+ 'x-access-key': creds.public_key,
29
+ },
30
+ body: JSON.stringify({
31
+ workspace_id: opts.workspace_id,
32
+ product_tag: opts.product_tag,
33
+ database_tag: opts.database_tag,
34
+ table_schemas: opts.table_schemas,
35
+ }),
36
+ });
37
+ const body = await res.json();
38
+ if (!res.ok) {
39
+ const msg = body.errors ?? body.message;
40
+ throw new Error(typeof msg === 'string' ? msg : `Schema sync failed (${res.status})`);
41
+ }
42
+ if (body.status === false) {
43
+ const msg = body.errors ?? body.message;
44
+ throw new Error(typeof msg === 'string' ? msg : 'Schema sync failed');
45
+ }
46
+ return (body.data ?? body);
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/cli",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Ductape CLI — local platform, login, link projects, and manage resources via the proxy (Workbench-compatible)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -5,6 +5,8 @@ import { loadSchemaFile, getSchemaPath, parseTableDef } from '../lib/schema-load
5
5
  import { loadMigrationFiles, writeMigrationFile } from '../lib/migration-files.js';
6
6
  import { fail } from '../lib/output.js';
7
7
  import { getDbProxy, requireSession } from '../lib/proxy/context.js';
8
+ import { pushDatabaseSchema } from '../lib/integrations.js';
9
+ import type { TableSchemaRecord, TableColumnRecord } from '../lib/integrations.js';
8
10
  import type { SchemaEntry, MongooseFieldDef } from '../lib/schema-loader.js';
9
11
  import type {
10
12
  IMigration,
@@ -335,3 +337,91 @@ export async function runDbSchemaGenerate(opts: SchemaGenerateOpts): Promise<voi
335
337
  function requireSilent(): ReturnType<typeof requireSession> | null {
336
338
  try { return requireSession(); } catch { return null; }
337
339
  }
340
+
341
+ interface SchemaPushOpts {
342
+ db: string;
343
+ }
344
+
345
+ function adaptColumn(col: Record<string, unknown>): TableColumnRecord {
346
+ return {
347
+ name: String(col.name ?? ''),
348
+ type: String(col.type ?? 'string'),
349
+ nullable: col.nullable !== false && col.nullable !== 'NO',
350
+ primaryKey: Boolean(col.primaryKey ?? col.isPrimaryKey),
351
+ unique: Boolean(col.unique ?? col.isUnique),
352
+ autoIncrement: Boolean(col.autoIncrement ?? col.isAutoIncrement ?? col.autoGenerate),
353
+ ...(col.defaultValue != null ? { defaultValue: col.defaultValue } : {}),
354
+ };
355
+ }
356
+
357
+ export async function runDbSchemaPush(opts: SchemaPushOpts): Promise<void> {
358
+ const session = requireSession();
359
+ const proxy = getDbProxy(session);
360
+ const envSlug = session.project.env_slug;
361
+ const productTag = session.project.product_tag;
362
+ const dbContext = { database: opts.db, env: envSlug, product: productTag };
363
+
364
+ console.log(`Connecting to database "${opts.db}" (env: ${envSlug})...`);
365
+
366
+ let tableNames: string[] = [];
367
+ try {
368
+ const raw = await proxy.execute<unknown>('schema.list', [null], dbContext);
369
+ if (Array.isArray(raw)) {
370
+ tableNames = (raw as unknown[]).map((r) =>
371
+ typeof r === 'string' ? r : String((r as Record<string, unknown>).name ?? ''),
372
+ ).filter(Boolean);
373
+ }
374
+ } catch (e) {
375
+ throw new Error(`Failed to list tables: ${(e as Error).message}. Make sure the database is connected.`);
376
+ }
377
+
378
+ if (tableNames.length === 0) {
379
+ console.log('No tables found in the database. Nothing to push.');
380
+ return;
381
+ }
382
+
383
+ console.log(`Found ${tableNames.length} table(s): ${tableNames.join(', ')}`);
384
+
385
+ const now = new Date().toISOString();
386
+ const tableSchemas: TableSchemaRecord[] = [];
387
+
388
+ for (const tableName of tableNames) {
389
+ try {
390
+ const tableSchema = await proxy.execute<unknown>('schema.describe', [tableName], dbContext);
391
+ const cols: unknown[] = Array.isArray((tableSchema as Record<string, unknown>)?.columns)
392
+ ? ((tableSchema as Record<string, unknown>).columns as unknown[])
393
+ : Array.isArray(tableSchema) ? (tableSchema as unknown[]) : [];
394
+
395
+ if (cols.length === 0) {
396
+ console.warn(` [skip] ${tableName}: no columns returned`);
397
+ continue;
398
+ }
399
+
400
+ const columns = (cols as Record<string, unknown>[])
401
+ .filter((c) => c.name)
402
+ .map(adaptColumn);
403
+
404
+ tableSchemas.push({ table: tableName, columns, updated_at: now });
405
+ console.log(` ${tableName}: ${columns.length} column(s)`);
406
+ } catch (e) {
407
+ console.warn(` [skip] ${tableName}: ${(e as Error).message}`);
408
+ }
409
+ }
410
+
411
+ if (tableSchemas.length === 0) {
412
+ console.log('No schema data collected. Nothing pushed.');
413
+ return;
414
+ }
415
+
416
+ console.log(`\nPushing schema for ${tableSchemas.length} table(s) to Ductape...`);
417
+
418
+ const result = await pushDatabaseSchema(session.apiUrl, session.credentials, {
419
+ workspace_id: session.project.workspace_id,
420
+ product_tag: productTag,
421
+ database_tag: opts.db,
422
+ table_schemas: tableSchemas,
423
+ });
424
+
425
+ console.log(`Done. Synced ${result.synced} table(s): ${result.tables.join(', ')}`);
426
+ console.log('The AI payload generator will now use this schema for field guidance.');
427
+ }
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@ import { runResourceCrud, runResourcesList } from './commands/resources.js';
16
16
  import { runCloud } from './commands/cloud.js';
17
17
  import { runDb, runDbContext } from './commands/db.js';
18
18
  import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
19
- import { runDbSchemaGenerate } from './commands/db-schema.js';
19
+ import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
20
20
  import { runGraph } from './commands/graph.js';
21
21
  import { runSecrets } from './commands/secrets.js';
22
22
  import {
@@ -398,6 +398,12 @@ dbSchema
398
398
  .option('--destructive', 'Also generate drop migrations for removed fields/tables')
399
399
  .action(wrap((opts) => runDbSchemaGenerate({ db: opts.db, destructive: Boolean(opts.destructive) })));
400
400
 
401
+ dbSchema
402
+ .command('push')
403
+ .description('Read live table schema from the database and sync it to the Ductape server (enables AI field guidance)')
404
+ .requiredOption('--db <tag>', 'Database tag to sync')
405
+ .action(wrap((opts) => runDbSchemaPush({ db: opts.db })));
406
+
401
407
  db
402
408
  .argument('[verb]', 'connect | query | …')
403
409
  .option('-f, --file <path>')
@@ -1,5 +1,21 @@
1
1
  import type { Credentials } from './config.js';
2
2
 
3
+ export interface TableColumnRecord {
4
+ name: string;
5
+ type: string;
6
+ nullable: boolean;
7
+ primaryKey: boolean;
8
+ unique: boolean;
9
+ autoIncrement: boolean;
10
+ defaultValue?: unknown;
11
+ }
12
+
13
+ export interface TableSchemaRecord {
14
+ table: string;
15
+ columns: TableColumnRecord[];
16
+ updated_at: string;
17
+ }
18
+
3
19
  export interface GeneratePayloadRequest {
4
20
  workspace_id: string;
5
21
  user_id: string;
@@ -45,3 +61,41 @@ export async function generateExecutablePayload(
45
61
  }
46
62
  return (body.data ?? body) as GeneratePayloadResponse;
47
63
  }
64
+
65
+ export async function pushDatabaseSchema(
66
+ apiUrl: string,
67
+ creds: Credentials,
68
+ opts: {
69
+ workspace_id: string;
70
+ product_tag: string;
71
+ database_tag: string;
72
+ table_schemas: TableSchemaRecord[];
73
+ },
74
+ ): Promise<{ synced: number; tables: string[] }> {
75
+ const url = `${apiUrl}/integrations/v1/databases/schema-sync`;
76
+ const res = await fetch(url, {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ Authorization: `Bearer ${creds.auth_token}`,
81
+ 'x-access-key': creds.public_key,
82
+ },
83
+ body: JSON.stringify({
84
+ workspace_id: opts.workspace_id,
85
+ product_tag: opts.product_tag,
86
+ database_tag: opts.database_tag,
87
+ table_schemas: opts.table_schemas,
88
+ }),
89
+ });
90
+
91
+ const body = await res.json() as Record<string, unknown>;
92
+ if (!res.ok) {
93
+ const msg = (body as any).errors ?? (body as any).message;
94
+ throw new Error(typeof msg === 'string' ? msg : `Schema sync failed (${res.status})`);
95
+ }
96
+ if (body.status === false) {
97
+ const msg = (body as any).errors ?? (body as any).message;
98
+ throw new Error(typeof msg === 'string' ? msg : 'Schema sync failed');
99
+ }
100
+ return (body.data ?? body) as { synced: number; tables: string[] };
101
+ }