@ductape/cli 0.2.23 → 0.3.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 (101) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +7 -0
  3. package/dist/commands/migrate-codebase.d.ts +176 -0
  4. package/dist/commands/migrate-codebase.js +753 -0
  5. package/dist/commands/migration-artifact.d.ts +32 -0
  6. package/dist/commands/migration-artifact.js +48 -0
  7. package/dist/commands/migration-assurance.d.ts +10 -0
  8. package/dist/commands/migration-assurance.js +120 -0
  9. package/dist/commands/migration-database.d.ts +21 -0
  10. package/dist/commands/migration-database.js +162 -0
  11. package/dist/commands/migration-e2e.d.ts +68 -0
  12. package/dist/commands/migration-e2e.js +189 -0
  13. package/dist/commands/migration-environments.d.ts +20 -0
  14. package/dist/commands/migration-environments.js +58 -0
  15. package/dist/commands/migration-generated.d.ts +68 -0
  16. package/dist/commands/migration-generated.js +217 -0
  17. package/dist/commands/migration-portfolio.d.ts +88 -0
  18. package/dist/commands/migration-portfolio.js +223 -0
  19. package/dist/commands/migration-products.d.ts +23 -0
  20. package/dist/commands/migration-products.js +88 -0
  21. package/dist/commands/migration-review.d.ts +248 -0
  22. package/dist/commands/migration-review.js +624 -0
  23. package/dist/commands/migration-secrets.d.ts +24 -0
  24. package/dist/commands/migration-secrets.js +93 -0
  25. package/dist/commands/migration-slice.d.ts +132 -0
  26. package/dist/commands/migration-slice.js +266 -0
  27. package/dist/commands/migration-verification.d.ts +12 -0
  28. package/dist/commands/migration-verification.js +194 -0
  29. package/dist/commands/products.d.ts +1 -0
  30. package/dist/commands/products.js +33 -1
  31. package/dist/commands/secrets.d.ts +8 -0
  32. package/dist/commands/secrets.js +44 -0
  33. package/dist/index.js +354 -2
  34. package/dist/lib/migration-artifact.d.ts +13 -0
  35. package/dist/lib/migration-artifact.js +103 -0
  36. package/package.json +8 -1
  37. package/API_PARITY.md +0 -58
  38. package/DB_MIGRATE_PLAN.md +0 -223
  39. package/ductape.example.ts +0 -18
  40. package/src/commands/apply.ts +0 -153
  41. package/src/commands/apps-import.ts +0 -43
  42. package/src/commands/apps.ts +0 -92
  43. package/src/commands/cloud.ts +0 -68
  44. package/src/commands/completion.ts +0 -119
  45. package/src/commands/db-migrate.ts +0 -209
  46. package/src/commands/db-schema.ts +0 -429
  47. package/src/commands/db.ts +0 -63
  48. package/src/commands/generate.ts +0 -74
  49. package/src/commands/graph.ts +0 -51
  50. package/src/commands/init.ts +0 -39
  51. package/src/commands/install.ts +0 -76
  52. package/src/commands/link.ts +0 -84
  53. package/src/commands/login.ts +0 -145
  54. package/src/commands/logout.ts +0 -7
  55. package/src/commands/platform.ts +0 -123
  56. package/src/commands/products.ts +0 -228
  57. package/src/commands/profiles.ts +0 -23
  58. package/src/commands/resources.ts +0 -213
  59. package/src/commands/secrets.ts +0 -46
  60. package/src/commands/unlink.ts +0 -15
  61. package/src/commands/whoami.ts +0 -27
  62. package/src/commands/workspaces.ts +0 -98
  63. package/src/index.ts +0 -523
  64. package/src/lib/api-client.ts +0 -88
  65. package/src/lib/app-import.ts +0 -82
  66. package/src/lib/apply-loaders.ts +0 -30
  67. package/src/lib/config.ts +0 -230
  68. package/src/lib/context-store.ts +0 -77
  69. package/src/lib/db-methods.ts +0 -56
  70. package/src/lib/db-types.ts +0 -104
  71. package/src/lib/encryption.ts +0 -12
  72. package/src/lib/forms/enums.ts +0 -52
  73. package/src/lib/forms/index.ts +0 -8
  74. package/src/lib/forms/prompt.ts +0 -209
  75. package/src/lib/forms/schemas.ts +0 -288
  76. package/src/lib/forms/types.ts +0 -27
  77. package/src/lib/http.ts +0 -76
  78. package/src/lib/integrations.ts +0 -110
  79. package/src/lib/interactive-opts.ts +0 -13
  80. package/src/lib/migration-files.ts +0 -58
  81. package/src/lib/output.ts +0 -39
  82. package/src/lib/platform-api.ts +0 -209
  83. package/src/lib/proxy/context.ts +0 -94
  84. package/src/lib/proxy/db-proxy.ts +0 -40
  85. package/src/lib/proxy/graph-proxy.ts +0 -64
  86. package/src/lib/proxy/sdk-proxy.ts +0 -58
  87. package/src/lib/read-body.ts +0 -57
  88. package/src/lib/resources.ts +0 -160
  89. package/src/lib/schema-loader.ts +0 -152
  90. package/src/lib/snippets.ts +0 -165
  91. package/src/lib/templates.ts +0 -157
  92. package/src/lib/workspace-context.ts +0 -43
  93. package/src/lib/workspaces.ts +0 -283
  94. package/test/apply-loaders.test.ts +0 -40
  95. package/test/encryption.test.ts +0 -20
  96. package/test/forms.test.ts +0 -25
  97. package/test/http.test.ts +0 -15
  98. package/test/platform-api.test.ts +0 -42
  99. package/test/resources.test.ts +0 -23
  100. package/test/workspace-resolve.test.ts +0 -29
  101. package/tsconfig.json +0 -15
package/src/index.ts DELETED
@@ -1,523 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { readFileSync } from 'node:fs';
4
- import { fileURLToPath } from 'node:url';
5
- import path from 'node:path';
6
- import { runLogin } from './commands/login.js';
7
- import { runLogout } from './commands/logout.js';
8
- import { runWhoami } from './commands/whoami.js';
9
- import { runProfilesList, runProfilesUse } from './commands/profiles.js';
10
- import { runLink } from './commands/link.js';
11
- import { runUnlink } from './commands/unlink.js';
12
- import { runInit } from './commands/init.js';
13
- import { runInstall } from './commands/install.js';
14
- import { runStart, runStop, runStatus } from './commands/platform.js';
15
- import { runResourceCrud, runResourcesList, runEventTopicCrud, runNotificationMessageCrud } from './commands/resources.js';
16
- import { runCloud } from './commands/cloud.js';
17
- import { runDb, runDbContext } from './commands/db.js';
18
- import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
19
- import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
20
- import { runGraph } from './commands/graph.js';
21
- import { runSecrets } from './commands/secrets.js';
22
- import {
23
- runWorkspacesCurrent,
24
- runWorkspacesList,
25
- runWorkspacesRefresh,
26
- runWorkspacesUse,
27
- } from './commands/workspaces.js';
28
- import { runGeneratePayload, runGenerateSnippet } from './commands/generate.js';
29
- import { runCompletion } from './commands/completion.js';
30
- import { runProducts, runProductApps, runProductComponents, runProductEnvironments } from './commands/products.js';
31
- import { runApps } from './commands/apps.js';
32
- import { runAppsImport } from './commands/apps-import.js';
33
- import { runApply, type ApplyType } from './commands/apply.js';
34
-
35
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
- const pkg = JSON.parse(
37
- readFileSync(path.join(__dirname, '../package.json'), 'utf8'),
38
- ) as { version: string };
39
-
40
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
- function wrap(fn: (...args: any[]) => void | Promise<void>) {
42
- return (...args: any[]) => {
43
- Promise.resolve(fn(...args)).catch((err: unknown) => {
44
- console.error(err instanceof Error ? err.message : err);
45
- process.exit(1);
46
- });
47
- };
48
- }
49
-
50
- const program = new Command();
51
- program
52
- .name('ductape')
53
- .description(
54
- 'Ductape CLI — local platform, auth, project linking, and resource CRUD via Workbench-compatible encrypted proxy',
55
- )
56
- .version(pkg.version);
57
-
58
- program
59
- .command('login')
60
- .description('Log in (email/password, OAuth token, or browser OAuth)')
61
- .option('-e, --email <email>')
62
- .option('-p, --password <password>')
63
- .option('--token <token>', 'OAuth callback token from browser redirect URL')
64
- .option('--browser [provider]', 'Open browser for Google OAuth (google|github)')
65
- .option('--profile <name>', 'API profile', 'cloud')
66
- .option('-w, --workspace <id>', 'Workspace index, name, or id (skips interactive picker)')
67
- .option('--skip-workspace-select', 'Keep prior active workspace or default without prompting')
68
- .action(
69
- wrap((opts) =>
70
- runLogin({
71
- email: opts.email,
72
- password: opts.password,
73
- profile: opts.profile,
74
- token: opts.token,
75
- browser: opts.browser === true ? 'google' : opts.browser,
76
- workspace: opts.workspace,
77
- skipWorkspaceSelect: Boolean(opts.skipWorkspaceSelect),
78
- }),
79
- ),
80
- );
81
-
82
- program.command('logout').description('Clear local credentials').action(wrap(runLogout));
83
-
84
- program
85
- .command('whoami')
86
- .description('Show login and linked project')
87
- .option('--json', 'JSON output')
88
- .action(wrap((opts) => runWhoami(Boolean(opts.json))));
89
-
90
- program
91
- .command('install')
92
- .description('Check Docker/Node prerequisites and prepare platform .env')
93
- .action(wrap(runInstall));
94
-
95
- const profiles = program.command('profiles').description('Manage API profiles (cloud / local)');
96
-
97
- profiles
98
- .command('list')
99
- .option('--json', 'JSON output')
100
- .action(wrap((opts) => runProfilesList(Boolean(opts.json))));
101
-
102
- profiles.command('use <name>').action(wrap((name: string) => runProfilesUse(name)));
103
-
104
- const workspaces = program
105
- .command('workspaces')
106
- .description('List and switch active workspace (workspace_id is stored globally at login)');
107
-
108
- workspaces
109
- .command('list')
110
- .description('List workspaces (marks active)')
111
- .option('--profile <name>')
112
- .option('--json', 'JSON output')
113
- .action(wrap((opts) => runWorkspacesList({ profile: opts.profile, json: opts.json })));
114
-
115
- workspaces
116
- .command('current')
117
- .description('Show active workspace')
118
- .option('--json', 'JSON output')
119
- .action(wrap((opts) => runWorkspacesCurrent(Boolean(opts.json))));
120
-
121
- workspaces
122
- .command('use [selector]')
123
- .description('Switch active workspace (interactive picker). Updates linked .ductape/config.json if present')
124
- .option('--profile <name>')
125
- .option('-C, --dir <path>', 'Project directory to sync', process.cwd())
126
- .option('--json', 'JSON output')
127
- .action(wrap((selector: string | undefined, opts) => runWorkspacesUse(selector, opts)));
128
-
129
- workspaces
130
- .command('switch [selector]')
131
- .description('Alias for workspaces use')
132
- .option('--profile <name>')
133
- .option('-C, --dir <path>', 'Project directory to sync', process.cwd())
134
- .option('--json', 'JSON output')
135
- .action(wrap((selector: string | undefined, opts) => runWorkspacesUse(selector, opts)));
136
-
137
- workspaces
138
- .command('refresh')
139
- .description('Refresh workspace list from API')
140
- .option('--profile <name>')
141
- .option('--json', 'JSON output')
142
- .action(wrap((opts) => runWorkspacesRefresh(opts)));
143
-
144
- program
145
- .command('link')
146
- .option('-w, --workspace <id>')
147
- .option('--product <tag>')
148
- .option('--env <slug>')
149
- .option('--profile <name>')
150
- .option('-C, --dir <path>', 'Project directory', process.cwd())
151
- .action(
152
- wrap((opts) =>
153
- runLink({
154
- workspace: opts.workspace,
155
- product: opts.product,
156
- env: opts.env,
157
- profile: opts.profile,
158
- dir: opts.dir,
159
- }),
160
- ),
161
- );
162
-
163
- program
164
- .command('unlink')
165
- .option('-C, --dir <path>', 'Project directory', process.cwd())
166
- .action(wrap((opts) => runUnlink({ dir: opts.dir })));
167
-
168
- program
169
- .command('init')
170
- .option('--link', 'Run link after init')
171
- .option('-w, --workspace <id>')
172
- .option('--product <tag>')
173
- .option('--env <slug>')
174
- .option('--language <lang>', 'typescript|python|go|dotnet')
175
- .action(
176
- wrap((opts) =>
177
- runInit({
178
- link: Boolean(opts.link),
179
- workspace: opts.workspace,
180
- product: opts.product,
181
- env: opts.env,
182
- language: opts.language,
183
- }),
184
- ),
185
- );
186
-
187
- const APPLY_TYPES = ['sessions', 'notifications', 'events'] as const;
188
-
189
- program
190
- .command('apply')
191
- .description('Sync declared sessions, notifications and events to your product')
192
- .argument('[type]', `${APPLY_TYPES.join(' | ')}`)
193
- .option('--dry-run', 'Print what would be created/updated without applying')
194
- .action(
195
- wrap((type: string | undefined, opts) => {
196
- if (type && !(APPLY_TYPES as readonly string[]).includes(type)) {
197
- throw new Error(`Unknown type "${type}". Use: ${APPLY_TYPES.join(', ')}`);
198
- }
199
- return runApply(type as ApplyType | undefined, { dryRun: Boolean(opts.dryRun) });
200
- }),
201
- );
202
-
203
- program
204
- .command('start')
205
- .description('Start platform via docker compose')
206
- .option('--backend-only', 'Backend without workbench UI')
207
- .option('--remote', 'Workbench UI only, built against cloud API (no local Nest/Mongo/Kafka)')
208
- .option('--build', 'Build images before start')
209
- .action(
210
- wrap((opts) =>
211
- runStart({
212
- backendOnly: Boolean(opts.backendOnly),
213
- remote: Boolean(opts.remote),
214
- build: Boolean(opts.build),
215
- }),
216
- ),
217
- );
218
-
219
- program.command('stop').action(wrap(runStop));
220
-
221
- program.command('status').option('--json', 'JSON output').action(wrap((opts) => runStatus(Boolean(opts.json))));
222
-
223
- const products = program
224
- .command('products')
225
- .description('Workspace products CRUD (REST + sdk-proxy update)')
226
- .argument('[verb]', 'list | get | create | update | delete')
227
- .option('--profile <name>')
228
- .option('--status <status>', 'Filter for list: all, active, draft, …', 'all')
229
- .option('--id <id>', 'Product _id (get, delete)')
230
- .option('-t, --tag <tag>', 'Product tag (get, update)')
231
- .option('-f, --file <path>', 'JSON body (create, update)')
232
- .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
233
- .option('--no-interactive', 'Require -f JSON for create/update')
234
- .option('--json', 'JSON output')
235
- .action(
236
- wrap((verb: string | undefined, opts) => {
237
- if (!verb) throw new Error('Specify a verb: list | get | create | update | delete');
238
- return runProducts(verb, opts, []);
239
- }),
240
- );
241
-
242
- const productApps = products.command('apps').description('Apps connected to a product');
243
-
244
- productApps
245
- .command('list')
246
- .option('--profile <name>')
247
- .option('--product <id>', 'Product _id')
248
- .option('--json', 'JSON output')
249
- .action(wrap((opts) => runProductApps('list', opts, [])));
250
-
251
- const productComponents = products
252
- .command('components')
253
- .description('Compact, non-secret product component inventory');
254
-
255
- productComponents
256
- .command('list')
257
- .requiredOption('-t, --tag <tag>', 'Product tag')
258
- .option('--profile <name>')
259
- .option('--json', 'JSON output')
260
- .action(wrap((opts) => runProductComponents('list', opts)));
261
-
262
- productComponents
263
- .command('get')
264
- .requiredOption('-t, --tag <tag>', 'Product tag')
265
- .requiredOption('--type <type>', 'Component type, e.g. notifications or events')
266
- .option('--profile <name>')
267
- .option('--json', 'JSON output')
268
- .action(wrap((opts) => runProductComponents('get', opts)));
269
-
270
- const productEnvironments = products
271
- .command('environments')
272
- .description('Product environments (list, get)');
273
-
274
- productEnvironments
275
- .command('list [product_tag]')
276
- .description('List all environments for a product')
277
- .option('--product <tag>', 'Product tag (defaults to linked project)')
278
- .option('--json', 'JSON output')
279
- .action(wrap((productTagArg: string | undefined, opts) =>
280
- runProductEnvironments('list', { product: opts.product ?? productTagArg, json: opts.json }, []),
281
- ));
282
-
283
- productEnvironments
284
- .command('get <product_tag> [slug]')
285
- .description('Fetch a single product environment by slug')
286
- .option('--slug <slug>', 'Environment slug')
287
- .option('--json', 'JSON output')
288
- .action(wrap((productTagArg: string, slugArg: string | undefined, opts) =>
289
- runProductEnvironments('get', { product: productTagArg, slug: opts.slug ?? slugArg, json: opts.json }, []),
290
- ));
291
-
292
- const apps = program
293
- .command('apps')
294
- .description('Workspace integration apps CRUD (REST)')
295
- .argument('[verb]', 'list | get | create | update | delete')
296
- .option('--profile <name>')
297
- .option('--status <status>', 'Filter for list', 'all')
298
- .option('--id <id>', 'App _id')
299
- .option('-t, --tag <tag>', 'App tag (get; update with --proxy)')
300
- .option('--proxy', 'Update via sdk-proxy (use with --tag)')
301
- .option('-f, --file <path>', 'JSON body')
302
- .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
303
- .option('--no-interactive', 'Require -f JSON for create/update')
304
- .option('--json', 'JSON output')
305
- .action(
306
- wrap((verb: string | undefined, opts) => {
307
- if (!verb) throw new Error('Specify a verb: list | get | create | update | delete');
308
- return runApps(verb, opts, []);
309
- }),
310
- );
311
-
312
- apps
313
- .command('import <file>')
314
- .description('Import Postman v2.1 or OpenAPI 3.0 into a new or existing app')
315
- .requiredOption(
316
- '-t, --type <format>',
317
- 'postman | openapi (Postman Collection v2.1 or OpenAPI 3.0)',
318
- )
319
- .option('--profile <name>')
320
- .option('--app-id <id>', 'Update existing app by _id')
321
- .option('--app-tag <tag>', 'Update existing app by tag')
322
- .option('--update', 'Replace/update if app already exists')
323
- .option('--version <tag>', 'App version tag', '0.0.1')
324
- .option('--json', 'JSON output')
325
- .action(
326
- wrap((file: string, opts) =>
327
- runAppsImport(file, {
328
- profile: opts.profile,
329
- type: opts.type,
330
- appId: opts.appId,
331
- appTag: opts.appTag,
332
- update: Boolean(opts.update),
333
- version: opts.version,
334
- json: opts.json,
335
- }),
336
- ),
337
- );
338
-
339
- const events = program.command('events').description('Message broker CRUD (sdk-proxy, requires access key login)');
340
-
341
- const eventTopics = events.command('topics').description('Topic CRUD for a message broker');
342
-
343
- eventTopics
344
- .argument('<verb>', 'list | get | create | update | delete')
345
- .option('-t, --tag <tag>', 'Broker tag (list) or topic tag in broker:topic form (get, update, delete)')
346
- .option('-f, --file <path>', 'JSON body (create, update)')
347
- .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
348
- .option('--no-interactive', 'Require -f JSON for create/update')
349
- .option('--json', 'JSON output')
350
- .action(wrap((verb: string, opts) => runEventTopicCrud(verb, opts, [])));
351
-
352
- const resources = program.command('resources').description('Component CRUD (sdk-proxy)');
353
-
354
- const notificationMessages = program.command('notifications').description('Notification templates');
355
- notificationMessages
356
- .command('messages <verb>')
357
- .option('-t, --tag <tag>', 'Full notification:message tag')
358
- .option('-n, --notification <tag>', 'Notification tag for list')
359
- .option('-f, --file <path>', 'JSON template body for create/update')
360
- .option('--json', 'JSON output')
361
- .action(wrap((verb: string, opts) => runNotificationMessageCrud(verb, opts)));
362
-
363
- resources.command('types').option('--json', 'JSON output').action(wrap((opts) => runResourcesList(Boolean(opts.json))));
364
-
365
- resources
366
- .argument('<type>', 'Resource type (e.g. storage, database, cache …)')
367
- .argument('<verb>', 'list | get | create | update | delete | connect')
368
- .option('-t, --tag <tag>')
369
- .option('-f, --file <path>', 'JSON body (or use interactive prompts)')
370
- .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
371
- .option('--no-interactive', 'Require -f JSON for create/update/connect')
372
- .option('--json', 'JSON output')
373
- .action(
374
- wrap((type: string, verb: string, opts) => runResourceCrud(type, verb, opts, [])),
375
- );
376
-
377
- const cloud = program.command('cloud').description('Cloud connections & resources');
378
-
379
- cloud
380
- .command('connections <verb> [id]')
381
- .option('-f, --file <path>')
382
- .option('--json', 'JSON output')
383
- .action(
384
- wrap((verb: string, id: string | undefined, opts) =>
385
- runCloud('connections', verb, { id, file: opts.file, json: opts.json }, []),
386
- ),
387
- );
388
-
389
- cloud
390
- .command('resources <verb>')
391
- .option('-f, --file <path>')
392
- .option('--tier <name>', 'Tier name from `cloud tiers list` (e.g. db.t3.micro, Standard_B1ms, M0)')
393
- .option('--json', 'JSON output')
394
- .action(wrap((verb: string, opts) => runCloud('resources', verb, { file: opts.file, tier: opts.tier, json: opts.json }, [])));
395
-
396
- cloud
397
- .command('tiers')
398
- .description('List available cloud resource tiers and estimated costs')
399
- .option('--provider <name>', 'Filter by provider: aws, gcp, azure, mongodb_atlas, neo4j_aura')
400
- .option('--type <resource_type>', 'Filter by resource type: database, storage, graph')
401
- .option('--db-type <db_type>', 'Filter by database type: postgresql, mysql, mongodb, dynamodb, neo4j')
402
- .option('--json', 'JSON output')
403
- .action(wrap((opts) => runCloud('tiers', 'list', { provider: opts.provider, type: opts.type, dbType: opts.dbType, json: opts.json }, [])));
404
-
405
- const db = program.command('db').description('Database runtime (db-proxy)');
406
-
407
- db.command('context').option('--json', 'JSON output').action(wrap((opts) => runDbContext(Boolean(opts.json))));
408
-
409
- const dbMigrate = db
410
- .command('migrate')
411
- .description('Run pending migrations from ductape/database/migrations/')
412
- .option('--env <tag>', 'Override environment slug')
413
- .option('--db <tag>', 'Limit to one db entry from schema.json')
414
- .option('--dry-run', 'Print what would run without applying')
415
- .action(wrap((opts) => runDbMigrate({ env: opts.env, db: opts.db, dryRun: Boolean(opts.dryRun) })));
416
-
417
- dbMigrate
418
- .command('status')
419
- .description('Show applied vs pending migrations')
420
- .option('--env <tag>', 'Override environment slug')
421
- .option('--db <tag>', 'Limit to one db entry')
422
- .option('--json', 'JSON output')
423
- .action(wrap((opts) => runDbMigrateStatus({ env: opts.env, db: opts.db, json: Boolean(opts.json) })));
424
-
425
- dbMigrate
426
- .command('rollback')
427
- .description('Roll back the last N migrations')
428
- .option('--env <tag>', 'Override environment slug')
429
- .option('--db <tag>', 'Limit to one db entry')
430
- .option('-n <count>', 'Number of migrations to roll back', '1')
431
- .action(wrap((opts) => runDbMigrateRollback({ env: opts.env, db: opts.db, n: Number(opts.n) })));
432
-
433
- const dbSchema = db.command('schema').description('Schema management for ductape/database/schema.json');
434
-
435
- dbSchema
436
- .command('generate')
437
- .description('Diff schema.json vs applied migrations and write new migration files')
438
- .option('--db <tag>', 'Limit to one db entry from schema.json')
439
- .option('--destructive', 'Also generate drop migrations for removed fields/tables')
440
- .action(wrap((opts) => runDbSchemaGenerate({ db: opts.db, destructive: Boolean(opts.destructive) })));
441
-
442
- dbSchema
443
- .command('push')
444
- .description('Read live table schema from the database and sync it to the Ductape server (enables AI field guidance)')
445
- .requiredOption('--db <tag>', 'Database tag to sync')
446
- .requiredOption('--env <slug>', 'Environment slug (e.g. snd, prd) whose live database to read from')
447
- .action(wrap((opts) => runDbSchemaPush({ db: opts.db, env: opts.env })));
448
-
449
- db
450
- .argument('[verb]', 'connect | query | …')
451
- .option('-f, --file <path>')
452
- .option('--json', 'JSON output')
453
- .action(
454
- wrap((verb: string | undefined, opts) => {
455
- if (!verb) throw new Error('Specify a verb: connect | query');
456
- return runDb(verb, { file: opts.file, json: opts.json });
457
- }),
458
- );
459
-
460
- const graph = program.command('graph').description('Graph runtime (graph-proxy)');
461
-
462
- graph
463
- .argument('<verb>', 'connect | query | …')
464
- .option('-f, --file <path>')
465
- .option('--json', 'JSON output')
466
- .action(wrap((verb: string, opts) => runGraph(verb, { file: opts.file, json: opts.json })));
467
-
468
- program
469
- .command('secrets <verb>')
470
- .description('Workspace secrets CRUD')
471
- .option('-k, --key <key>')
472
- .option('-f, --file <path>')
473
- .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
474
- .option('--no-interactive', 'Require -f JSON for create/update')
475
- .option('--json', 'JSON output')
476
- .action(
477
- wrap((verb: string, opts) =>
478
- runSecrets(verb, opts),
479
- ),
480
- );
481
-
482
- const generate = program.command('generate').description('Payload templates & code snippets');
483
-
484
- generate
485
- .command('payload <family> <method>')
486
- .option('-f, --file <path>', 'targets JSON')
487
- .option('--json', 'JSON output')
488
- .action(
489
- wrap((family: string, method: string, opts) =>
490
- runGeneratePayload({ family, method, file: opts.file, json: opts.json }),
491
- ),
492
- );
493
-
494
- generate
495
- .command('snippet <family> <method>')
496
- .requiredOption('-l, --language <lang>', 'typescript|python|go|dotnet')
497
- .option('-f, --file <path>')
498
- .option('-o, --out <path>', 'Write snippet to file')
499
- .option('--json', 'JSON output')
500
- .action(
501
- wrap((family: string, method: string, opts) =>
502
- runGenerateSnippet({
503
- family,
504
- method,
505
- language: opts.language,
506
- file: opts.file,
507
- json: opts.json,
508
- out: opts.out,
509
- }),
510
- ),
511
- );
512
-
513
- program
514
- .command('completion <shell>')
515
- .description('Print shell completions (bash|zsh)')
516
- .action(wrap((shell: string) => {
517
- if (shell !== 'bash' && shell !== 'zsh' && shell !== 'fish') {
518
- throw new Error('Shell must be bash, zsh, or fish');
519
- }
520
- runCompletion(shell);
521
- }));
522
-
523
- program.parse();
@@ -1,88 +0,0 @@
1
- import type { Credentials } from './config.js';
2
- import { getApiUrl } from './config.js';
3
-
4
- export interface ApiEnvelope<T> {
5
- status?: boolean;
6
- data?: T;
7
- message?: string;
8
- }
9
-
10
- export class ApiClient {
11
- constructor(
12
- private readonly apiUrl: string,
13
- private readonly token?: string,
14
- ) {}
15
-
16
- static forProfile(profileName?: string, creds?: Credentials | null): ApiClient {
17
- return new ApiClient(getApiUrl(profileName), creds?.auth_token);
18
- }
19
-
20
- async request<T>(
21
- method: string,
22
- path: string,
23
- body?: unknown,
24
- extraHeaders?: Record<string, string>,
25
- ): Promise<T> {
26
- const url = `${this.apiUrl}${path.startsWith('/') ? path : `/${path}`}`;
27
- const headers: Record<string, string> = {
28
- 'Content-Type': 'application/json',
29
- ...extraHeaders,
30
- };
31
- if (this.token) {
32
- headers.Authorization = `Bearer ${this.token}`;
33
- }
34
-
35
- const res = await fetch(url, {
36
- method,
37
- headers,
38
- body: body !== undefined ? JSON.stringify(body) : undefined,
39
- });
40
-
41
- const text = await res.text();
42
- let parsed: ApiEnvelope<T> & T;
43
- try {
44
- parsed = text ? JSON.parse(text) : {};
45
- } catch {
46
- throw new Error(`Invalid JSON from ${url}: ${text.slice(0, 200)}`);
47
- }
48
-
49
- if (!res.ok) {
50
- const msg =
51
- (parsed as ApiEnvelope<T>).message ??
52
- (parsed as { error?: string }).error ??
53
- `HTTP ${res.status}`;
54
- throw new Error(msg);
55
- }
56
-
57
- if (typeof (parsed as ApiEnvelope<T>).status === 'boolean' && !(parsed as ApiEnvelope<T>).status) {
58
- throw new Error((parsed as ApiEnvelope<T>).message ?? 'Request failed');
59
- }
60
-
61
- const data = (parsed as ApiEnvelope<{ result?: T }>).data;
62
- if (data && typeof data === 'object' && 'result' in data) {
63
- return (data as { result: T }).result;
64
- }
65
- return (data ?? parsed) as T;
66
- }
67
-
68
- post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
69
- return this.request<T>('POST', path, body, headers);
70
- }
71
-
72
- put<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
73
- return this.request<T>('PUT', path, body, headers);
74
- }
75
-
76
- delete<T>(path: string, headers?: Record<string, string>): Promise<T> {
77
- return this.request<T>('DELETE', path, undefined, headers);
78
- }
79
-
80
- get<T>(path: string, headers?: Record<string, string>): Promise<T> {
81
- return this.request<T>('GET', path, undefined, headers);
82
- }
83
-
84
- getPath<T>(path: string, query?: Record<string, string>): Promise<T> {
85
- const qs = query ? `?${new URLSearchParams(query).toString()}` : '';
86
- return this.get<T>(`${path}${qs}`);
87
- }
88
- }
@@ -1,82 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import type { WorkspaceContext } from './workspace-context.js';
4
-
5
- /** Values accepted by POST /apps/v1/import `type` field */
6
- export const IMPORT_DOC_TYPES = {
7
- postman: 'PostmanV2.1',
8
- openapi: 'OpenApiV3.0',
9
- } as const;
10
-
11
- export type ImportFormat = keyof typeof IMPORT_DOC_TYPES;
12
-
13
- export function parseImportFormat(value: string): ImportFormat {
14
- const v = value.toLowerCase().replace(/[_\s]/g, '');
15
- if (v === 'postman' || v === 'postmanv21' || v === 'postman2.1' || v === 'postmanv2.1') {
16
- return 'postman';
17
- }
18
- if (v === 'openapi' || v === 'openapiv30' || v === 'openapi3.0' || v === 'openapi3') {
19
- return 'openapi';
20
- }
21
- throw new Error(
22
- `Unknown import type "${value}". Use: postman (Postman Collection v2.1) or openapi (OpenAPI 3.0)`,
23
- );
24
- }
25
-
26
- function uploadFilename(filePath: string): string {
27
- const base = path.basename(filePath);
28
- if (base.endsWith('.yaml') || base.endsWith('.yml')) return base;
29
- if (base.endsWith('.json')) return base;
30
- return `${base}.json`;
31
- }
32
-
33
- export async function importAppFromFile(
34
- ctx: WorkspaceContext,
35
- opts: {
36
- filePath: string;
37
- format: ImportFormat;
38
- appId?: string;
39
- update?: boolean;
40
- version?: string;
41
- },
42
- ): Promise<unknown> {
43
- const abs = path.resolve(opts.filePath);
44
- if (!fs.existsSync(abs)) throw new Error(`File not found: ${abs}`);
45
-
46
- const buffer = fs.readFileSync(abs);
47
- const filename = uploadFilename(abs);
48
- const form = new FormData();
49
-
50
- form.append('file', new Blob([buffer]), filename);
51
- form.append('workspace_id', ctx.workspaceId);
52
- form.append('public_key', ctx.credentials.public_key);
53
- form.append('user_id', ctx.credentials.user_id);
54
- form.append('type', IMPORT_DOC_TYPES[opts.format]);
55
- form.append('version', opts.version ?? '0.0.1');
56
- form.append('update', opts.update ? 'true' : 'false');
57
- if (opts.appId) form.append('app_id', opts.appId);
58
-
59
- const url = `${ctx.apiUrl}/apps/v1/import`;
60
- const res = await fetch(url, {
61
- method: 'POST',
62
- headers: { Authorization: `Bearer ${ctx.credentials.auth_token}` },
63
- body: form,
64
- });
65
-
66
- const text = await res.text();
67
- let parsed: { status?: boolean; data?: unknown; message?: string };
68
- try {
69
- parsed = text ? JSON.parse(text) : {};
70
- } catch {
71
- throw new Error(`Invalid JSON from import API: ${text.slice(0, 200)}`);
72
- }
73
-
74
- if (!res.ok) {
75
- throw new Error(parsed.message ?? `Import failed (HTTP ${res.status})`);
76
- }
77
- if (parsed.status === false) {
78
- throw new Error(parsed.message ?? 'Import failed');
79
- }
80
-
81
- return parsed.data ?? parsed;
82
- }