@ductape/cli 0.2.9 → 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.
@@ -6,9 +6,9 @@ import { fail, printJson } from '../lib/output.js';
6
6
  function buildDbContext(dbTag, envSlug, productTag) {
7
7
  return { database: dbTag, env: envSlug, product: productTag };
8
8
  }
9
- async function getAppliedTags(proxy, dbContext, productTag, envSlug) {
9
+ async function getAppliedTags(proxy, dbContext) {
10
10
  try {
11
- const result = await proxy.execute('migration.list', [{ product: productTag, env: envSlug, database: dbContext.database }], dbContext);
11
+ const result = await proxy.execute('migration.history', [], dbContext);
12
12
  const list = Array.isArray(result)
13
13
  ? result
14
14
  : Array.isArray(result.data)
@@ -44,7 +44,7 @@ export async function runDbMigrate(opts) {
44
44
  console.log(`[${db}] No migration files found. Run \`ductape db schema generate\` first.`);
45
45
  continue;
46
46
  }
47
- const appliedTags = await getAppliedTags(proxy, dbContext, productTag, envSlug);
47
+ const appliedTags = await getAppliedTags(proxy, dbContext);
48
48
  const pending = migrations.filter((m) => !appliedTags.includes(m.tag));
49
49
  if (pending.length === 0) {
50
50
  console.log(`[${db}] Already up to date.`);
@@ -92,7 +92,7 @@ export async function runDbMigrateStatus(opts) {
92
92
  const { db } = entry;
93
93
  const dbContext = buildDbContext(db, envSlug, productTag);
94
94
  const migrations = loadMigrationFiles(dir, db);
95
- const appliedTags = await getAppliedTags(proxy, dbContext, productTag, envSlug);
95
+ const appliedTags = await getAppliedTags(proxy, dbContext);
96
96
  const applied = migrations.filter((m) => appliedTags.includes(m.tag)).map((m) => m.tag);
97
97
  const pending = migrations.filter((m) => !appliedTags.includes(m.tag)).map((m) => m.tag);
98
98
  statusReport[db] = { applied, pending };
@@ -132,7 +132,7 @@ export async function runDbMigrateRollback(opts) {
132
132
  const { db } = entry;
133
133
  const dbContext = buildDbContext(db, envSlug, productTag);
134
134
  const migrations = loadMigrationFiles(dir, db);
135
- const appliedTags = await getAppliedTags(proxy, dbContext, productTag, envSlug);
135
+ const appliedTags = await getAppliedTags(proxy, dbContext);
136
136
  const applied = migrations.filter((m) => appliedTags.includes(m.tag));
137
137
  const toRollback = applied.slice(-rollbackCount).reverse();
138
138
  if (toRollback.length === 0) {
@@ -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.9",
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",
@@ -30,13 +30,11 @@ function buildDbContext(dbTag: string, envSlug: string, productTag: string): Dat
30
30
  async function getAppliedTags(
31
31
  proxy: ReturnType<typeof getDbProxy>,
32
32
  dbContext: DatabaseContext,
33
- productTag: string,
34
- envSlug: string,
35
33
  ): Promise<string[]> {
36
34
  try {
37
35
  const result = await proxy.execute<unknown>(
38
- 'migration.list',
39
- [{ product: productTag, env: envSlug, database: dbContext.database }],
36
+ 'migration.history',
37
+ [],
40
38
  dbContext,
41
39
  );
42
40
  const list = Array.isArray(result)
@@ -80,7 +78,7 @@ export async function runDbMigrate(opts: MigrateOpts): Promise<void> {
80
78
  continue;
81
79
  }
82
80
 
83
- const appliedTags = await getAppliedTags(proxy, dbContext, productTag, envSlug);
81
+ const appliedTags = await getAppliedTags(proxy, dbContext);
84
82
  const pending = migrations.filter((m) => !appliedTags.includes(m.tag));
85
83
 
86
84
  if (pending.length === 0) {
@@ -139,7 +137,7 @@ export async function runDbMigrateStatus(opts: MigrateStatusOpts): Promise<void>
139
137
  const { db } = entry;
140
138
  const dbContext = buildDbContext(db, envSlug, productTag);
141
139
  const migrations = loadMigrationFiles(dir, db);
142
- const appliedTags = await getAppliedTags(proxy, dbContext, productTag, envSlug);
140
+ const appliedTags = await getAppliedTags(proxy, dbContext);
143
141
 
144
142
  const applied = migrations.filter((m) => appliedTags.includes(m.tag)).map((m) => m.tag);
145
143
  const pending = migrations.filter((m) => !appliedTags.includes(m.tag)).map((m) => m.tag);
@@ -183,7 +181,7 @@ export async function runDbMigrateRollback(opts: MigrateRollbackOpts): Promise<v
183
181
  const { db } = entry;
184
182
  const dbContext = buildDbContext(db, envSlug, productTag);
185
183
  const migrations = loadMigrationFiles(dir, db);
186
- const appliedTags = await getAppliedTags(proxy, dbContext, productTag, envSlug);
184
+ const appliedTags = await getAppliedTags(proxy, dbContext);
187
185
 
188
186
  const applied = migrations.filter((m) => appliedTags.includes(m.tag));
189
187
  const toRollback = applied.slice(-rollbackCount).reverse();
@@ -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
+ }