@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/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { runDb, runDbContext } from './commands/db.js';
18
18
  import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
19
19
  import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
20
20
  import { runGraph } from './commands/graph.js';
21
- import { runSecrets } from './commands/secrets.js';
21
+ import { runSecretImportEnv, runSecrets } from './commands/secrets.js';
22
22
  import { runWorkspacesCurrent, runWorkspacesList, runWorkspacesRefresh, runWorkspacesUse, } from './commands/workspaces.js';
23
23
  import { runGeneratePayload, runGenerateSnippet } from './commands/generate.js';
24
24
  import { runCompletion } from './commands/completion.js';
@@ -26,6 +26,19 @@ import { runProducts, runProductApps, runProductComponents, runProductEnvironmen
26
26
  import { runApps } from './commands/apps.js';
27
27
  import { runAppsImport } from './commands/apps-import.js';
28
28
  import { runApply } from './commands/apply.js';
29
+ import { runMigrateCodebase } from './commands/migrate-codebase.js';
30
+ import { runMigrationReviewInit, runMigrationReviewRecord, runMigrationReviewFollowUp, runMigrationReviewExclude, runMigrationReviewParity, runMigrationReviewReopen, runMigrationReviewMove, runMigrationReviewMerge, runMigrationReviewSign, runMigrationReviewSnapshot, runMigrationReviewVerifySignature, runMigrationReviewValidate, } from './commands/migration-review.js';
31
+ import { runMigrationSliceDefine, runMigrationSliceInit, runMigrationSliceValidate, } from './commands/migration-slice.js';
32
+ import { runMigrationPortfolioInit, runMigrationPortfolioPropose, runMigrationPortfolioValidate, } from './commands/migration-portfolio.js';
33
+ import { runMigrationDatabaseInit, runMigrationDatabaseValidate } from './commands/migration-database.js';
34
+ import { runMigrationEnvironmentsReconcile } from './commands/migration-environments.js';
35
+ import { runMigrationProductsInit, runMigrationProductsValidate } from './commands/migration-products.js';
36
+ import { runMigrationSecretsInit, runMigrationSecretsValidate } from './commands/migration-secrets.js';
37
+ import { runMigrationE2EFinal, runMigrationE2EInit, runMigrationE2EValidate, } from './commands/migration-e2e.js';
38
+ import { runMigrationGeneratedInit, runMigrationGeneratedValidate, } from './commands/migration-generated.js';
39
+ import { runMigrationAssuranceValidate } from './commands/migration-assurance.js';
40
+ import { runMigrationVerificationValidate } from './commands/migration-verification.js';
41
+ import { runMigrationArtifactMigrate, runMigrationArtifactRecover, runMigrationArtifactSchemas, runMigrationArtifactValidate, } from './commands/migration-artifact.js';
29
42
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
43
  const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
31
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -143,6 +156,316 @@ program
143
156
  env: opts.env,
144
157
  language: opts.language,
145
158
  })));
159
+ program
160
+ .command('migrate-codebase')
161
+ .description('Inspect a codebase and write advisory guidance for an AI-led Ductape migration')
162
+ .option('-C, --source <path>', 'Existing codebase', process.cwd())
163
+ .requiredOption('--e2e-baseline <path>', 'Passing immutable original E2E baseline manifest')
164
+ .option('--mode <mode>', 'in-place | new-codebase', 'new-codebase')
165
+ .option('--destination <path>', 'Destination for new-codebase mode')
166
+ .option('--product <tag>', 'Ductape product tag')
167
+ .option('--name <name>', 'Product display name')
168
+ .option('--database <tag>', 'Database tag for generated Ductape schema')
169
+ .option('--max-file-bytes <bytes>', 'Maximum reviewable file size', '2000000')
170
+ .option('--include <patterns>', 'Comma-separated repository-relative glob patterns')
171
+ .option('--exclude <patterns>', 'Comma-separated repository-relative glob patterns')
172
+ .option('--ensure-product', 'Create the product when it does not exist')
173
+ .option('--write', 'Write redacted advisory artifacts only; never writes application code or executable assets')
174
+ .option('--json', 'JSON output')
175
+ .action(wrap((opts) => runMigrateCodebase({
176
+ source: opts.source,
177
+ e2eBaseline: opts.e2eBaseline,
178
+ mode: opts.mode,
179
+ destination: opts.destination,
180
+ product: opts.product,
181
+ name: opts.name,
182
+ database: opts.database,
183
+ maxFileBytes: Number(opts.maxFileBytes),
184
+ include: opts.include ? String(opts.include).split(',').map((value) => value.trim()).filter(Boolean) : [],
185
+ exclude: opts.exclude ? String(opts.exclude).split(',').map((value) => value.trim()).filter(Boolean) : [],
186
+ ensureProduct: Boolean(opts.ensureProduct),
187
+ write: Boolean(opts.write),
188
+ json: Boolean(opts.json),
189
+ })));
190
+ const migrationE2E = program
191
+ .command('migration-e2e')
192
+ .description('Bind the original E2E suite and validate final migration parity without executing tests');
193
+ migrationE2E
194
+ .command('init')
195
+ .requiredOption('-C, --source <path>', 'Original codebase')
196
+ .requiredOption('--definition <path>', 'JSON containing suite files, command, and passing baseline evidence')
197
+ .option('--output <path>', 'Baseline manifest output path')
198
+ .option('--json', 'JSON output')
199
+ .action(wrap((opts) => runMigrationE2EInit({ ...opts, json: Boolean(opts.json) })));
200
+ migrationE2E
201
+ .command('final')
202
+ .requiredOption('--manifest <path>', 'Original E2E baseline manifest')
203
+ .requiredOption('--evidence <path>', 'JSON containing passing final-run evidence')
204
+ .option('--json', 'JSON output')
205
+ .action(wrap((opts) => runMigrationE2EFinal({ ...opts, json: Boolean(opts.json) })));
206
+ migrationE2E
207
+ .command('validate')
208
+ .requiredOption('--manifest <path>', 'Original E2E baseline manifest')
209
+ .option('--strict', 'Fail unless baseline integrity and the final run are verified')
210
+ .option('--json', 'JSON output')
211
+ .action(wrap((opts) => runMigrationE2EValidate({ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json) })));
212
+ const migrationGenerated = program
213
+ .command('migration-generated')
214
+ .description('Validate generated contracts and locally modified vendor source without executing generators');
215
+ migrationGenerated
216
+ .command('init')
217
+ .requiredOption('--analysis <path>', 'Migration analysis containing generated-source hints')
218
+ .requiredOption('--definition <path>', 'Contextually reviewed generated/vendor definition JSON')
219
+ .option('--output <path>', 'Evidence manifest output path')
220
+ .option('--json', 'JSON output')
221
+ .action(wrap((opts) => runMigrationGeneratedInit({ ...opts, json: Boolean(opts.json) })));
222
+ migrationGenerated
223
+ .command('validate')
224
+ .requiredOption('--file <path>', 'Generated/vendor evidence manifest')
225
+ .option('--strict', 'Fail on stale inputs, regeneration mismatch, or missing contract evidence')
226
+ .option('--json', 'JSON output')
227
+ .action(wrap((opts) => runMigrationGeneratedValidate({
228
+ ...opts,
229
+ strict: Boolean(opts.strict),
230
+ json: Boolean(opts.json),
231
+ })));
232
+ program
233
+ .command('migration-assurance')
234
+ .description('Validate repository-wide component, session, resilience, runtime, API, and SDK evidence')
235
+ .requiredOption('--file <path>', 'Versioned migration assurance JSON')
236
+ .option('--strict', 'Fail unless every required assurance category is evidenced and tested')
237
+ .option('--json', 'JSON output')
238
+ .action(wrap((opts) => runMigrationAssuranceValidate({
239
+ file: opts.file,
240
+ strict: Boolean(opts.strict),
241
+ json: Boolean(opts.json),
242
+ })));
243
+ program
244
+ .command('migration-verification')
245
+ .description('Validate cited automation evidence for large-repo, parity, frontend, database, and asset migration')
246
+ .requiredOption('--file <path>', 'Versioned migration verification matrix JSON')
247
+ .option('--strict', 'Fail unless every verification requirement is passing, cited, and current')
248
+ .option('--json', 'JSON output')
249
+ .action(wrap((opts) => runMigrationVerificationValidate({
250
+ file: opts.file,
251
+ strict: Boolean(opts.strict),
252
+ json: Boolean(opts.json),
253
+ })));
254
+ const migrationArtifact = program
255
+ .command('migration-artifact')
256
+ .description('Validate, recover, and version-migrate secured migration JSON artifacts');
257
+ migrationArtifact.command('validate').requiredOption('--file <path>').option('--json')
258
+ .action(wrap((opts) => runMigrationArtifactValidate({ file: opts.file, json: Boolean(opts.json) })));
259
+ migrationArtifact.command('recover').requiredOption('--file <path>').option('--json')
260
+ .action(wrap((opts) => runMigrationArtifactRecover({ file: opts.file, json: Boolean(opts.json) })));
261
+ migrationArtifact.command('migrate').requiredOption('--file <path>').requiredOption('--output <path>').option('--json')
262
+ .action(wrap((opts) => runMigrationArtifactMigrate({ file: opts.file, output: opts.output, json: Boolean(opts.json) })));
263
+ migrationArtifact.command('schemas').option('--json')
264
+ .action(wrap((opts) => runMigrationArtifactSchemas(Boolean(opts.json))));
265
+ const migrationReview = program
266
+ .command('migration-review')
267
+ .description('Track and validate an AI-led migration review without generating application code');
268
+ migrationReview
269
+ .command('init')
270
+ .requiredOption('--analysis <path>', 'Path to migration-guidance/analysis.json')
271
+ .option('--output <path>', 'Review ledger output path')
272
+ .option('--json', 'JSON output')
273
+ .action(wrap((opts) => runMigrationReviewInit({
274
+ analysis: opts.analysis,
275
+ output: opts.output,
276
+ json: Boolean(opts.json),
277
+ })));
278
+ migrationReview
279
+ .command('record')
280
+ .requiredOption('--ledger <path>', 'Review ledger path')
281
+ .requiredOption('--file <path>', 'Repository-relative file')
282
+ .requiredOption('--data <path>', 'JSON file containing contextual review evidence')
283
+ .option('--json', 'JSON output')
284
+ .action(wrap((opts) => runMigrationReviewRecord({ ...opts, json: Boolean(opts.json) })));
285
+ migrationReview
286
+ .command('follow-up')
287
+ .requiredOption('--ledger <path>', 'Review ledger path')
288
+ .requiredOption('--file <path>', 'Repository-relative source file')
289
+ .requiredOption('--follow-up <path>', 'Repository-relative file discovered during review')
290
+ .option('--json', 'JSON output')
291
+ .action(wrap((opts) => runMigrationReviewFollowUp({ ...opts, json: Boolean(opts.json) })));
292
+ migrationReview
293
+ .command('exclude')
294
+ .requiredOption('--ledger <path>', 'Review ledger path')
295
+ .requiredOption('--file <path>', 'Repository-relative file')
296
+ .requiredOption('--classification <type>', 'generated | vendor | build | snapshot | other')
297
+ .requiredOption('--reason <text>', 'Why the file is excluded')
298
+ .option('--owner <text>', 'Owner or source of the excluded file')
299
+ .option('--generator <text>', 'Generator command/tool (required for generated)')
300
+ .option('--regeneration-test <text>', 'Regeneration verification (required for generated)')
301
+ .option('--json', 'JSON output')
302
+ .action(wrap((opts) => runMigrationReviewExclude({ ...opts, json: Boolean(opts.json) })));
303
+ migrationReview
304
+ .command('parity')
305
+ .requiredOption('--ledger <path>', 'Review ledger path')
306
+ .requiredOption('--file <path>', 'Repository-relative file')
307
+ .requiredOption('--concern <type>', 'interface | functional | operational')
308
+ .requiredOption('--status <status>', 'verified | not_applicable')
309
+ .option('--evidence <text>', 'Concrete parity evidence (required for verified)')
310
+ .option('--reason <text>', 'Reason (required for not_applicable)')
311
+ .option('--json', 'JSON output')
312
+ .action(wrap((opts) => runMigrationReviewParity({ ...opts, json: Boolean(opts.json) })));
313
+ migrationReview
314
+ .command('reopen')
315
+ .requiredOption('--ledger <path>', 'Review ledger path')
316
+ .requiredOption('--file <path>', 'Repository-relative file')
317
+ .option('--json', 'JSON output')
318
+ .action(wrap((opts) => runMigrationReviewReopen({ ...opts, json: Boolean(opts.json) })));
319
+ migrationReview
320
+ .command('move')
321
+ .requiredOption('--ledger <path>', 'Review ledger path')
322
+ .requiredOption('--from <path>', 'Previous repository-relative file')
323
+ .requiredOption('--to <path>', 'New repository-relative file with identical reviewed content')
324
+ .option('--json', 'JSON output')
325
+ .action(wrap((opts) => runMigrationReviewMove({ ...opts, json: Boolean(opts.json) })));
326
+ migrationReview
327
+ .command('merge')
328
+ .requiredOption('--base <path>', 'Common-base review ledger')
329
+ .requiredOption('--current <path>', 'Current review ledger')
330
+ .requiredOption('--incoming <path>', 'Incoming parallel review ledger')
331
+ .requiredOption('--output <path>', 'New merged ledger; existing ledgers are never overwritten')
332
+ .option('--json', 'JSON output')
333
+ .action(wrap((opts) => runMigrationReviewMerge({ ...opts, json: Boolean(opts.json) })));
334
+ migrationReview
335
+ .command('sign')
336
+ .requiredOption('--ledger <path>', 'Review ledger')
337
+ .requiredOption('--private-key <path>', 'Trusted Ed25519 private key PEM file')
338
+ .requiredOption('--key-id <id>', 'Trusted signing identity')
339
+ .option('--output <path>', 'Detached signature output')
340
+ .option('--json', 'JSON output')
341
+ .action(wrap((opts) => runMigrationReviewSign({ ...opts, json: Boolean(opts.json) })));
342
+ migrationReview
343
+ .command('verify-signature')
344
+ .requiredOption('--ledger <path>', 'Review ledger')
345
+ .requiredOption('--signature <path>', 'Detached signature JSON')
346
+ .requiredOption('--public-key <path>', 'Trusted Ed25519 public key PEM file')
347
+ .option('--json', 'JSON output')
348
+ .action(wrap((opts) => runMigrationReviewVerifySignature({ ...opts, json: Boolean(opts.json) })));
349
+ migrationReview
350
+ .command('snapshot')
351
+ .requiredOption('--ledger <path>', 'Review ledger path')
352
+ .requiredOption('--file <path>', 'Repository-relative reviewed file')
353
+ .option('--json', 'JSON output')
354
+ .action(wrap((opts) => runMigrationReviewSnapshot({
355
+ ledger: opts.ledger,
356
+ file: opts.file,
357
+ json: Boolean(opts.json),
358
+ })));
359
+ migrationReview
360
+ .command('validate')
361
+ .requiredOption('--ledger <path>', 'Review ledger path')
362
+ .option('--strict', 'Fail unless coverage, staleness, context, follow-ups, and parity are complete')
363
+ .option('--json', 'JSON output')
364
+ .action(wrap((opts) => runMigrationReviewValidate({
365
+ ledger: opts.ledger,
366
+ strict: Boolean(opts.strict),
367
+ json: Boolean(opts.json),
368
+ })));
369
+ const migrationSlice = program
370
+ .command('migration-slice')
371
+ .description('Define and validate a parity-gated vertical migration slice');
372
+ migrationSlice
373
+ .command('init')
374
+ .requiredOption('--ledger <path>', 'Review ledger path')
375
+ .requiredOption('--tag <tag>', 'Stable slice tag')
376
+ .requiredOption('--name <name>', 'Slice display name')
377
+ .requiredOption('--files <path>', 'JSON array of repository-relative slice files')
378
+ .option('--output <path>', 'Slice manifest output path')
379
+ .option('--json', 'JSON output')
380
+ .action(wrap((opts) => runMigrationSliceInit({ ...opts, json: Boolean(opts.json) })));
381
+ migrationSlice
382
+ .command('define')
383
+ .requiredOption('--slice <path>', 'Migration slice manifest path')
384
+ .requiredOption('--data <path>', 'JSON file containing contracts, parity requirements, assets, tests, and cutover controls')
385
+ .option('--json', 'JSON output')
386
+ .action(wrap((opts) => runMigrationSliceDefine({
387
+ slice: opts.slice,
388
+ data: opts.data,
389
+ json: Boolean(opts.json),
390
+ })));
391
+ migrationSlice
392
+ .command('validate')
393
+ .requiredOption('--slice <path>', 'Migration slice manifest path')
394
+ .option('--strict', 'Fail unless review, parity, assets, tests, cutover, and rollback are complete')
395
+ .option('--json', 'JSON output')
396
+ .action(wrap((opts) => runMigrationSliceValidate({
397
+ slice: opts.slice,
398
+ strict: Boolean(opts.strict),
399
+ json: Boolean(opts.json),
400
+ })));
401
+ const migrationPortfolio = program
402
+ .command('migration-portfolio')
403
+ .description('Partition and validate a large migration across bounded AI contexts');
404
+ migrationPortfolio
405
+ .command('propose')
406
+ .requiredOption('--ledger <path>', 'Review ledger with contextual internal references')
407
+ .option('--max-bytes <bytes>', 'Maximum estimated bytes per proposed partition', '128000')
408
+ .option('--max-tokens <tokens>', 'Maximum estimated tokens per proposed partition', '32000')
409
+ .option('--output <path>', 'Advisory proposal output path')
410
+ .option('--json', 'JSON output')
411
+ .action(wrap((opts) => runMigrationPortfolioPropose({
412
+ ledger: opts.ledger,
413
+ maxBytes: Number(opts.maxBytes),
414
+ maxTokens: Number(opts.maxTokens),
415
+ output: opts.output,
416
+ json: Boolean(opts.json),
417
+ })));
418
+ migrationPortfolio
419
+ .command('init')
420
+ .requiredOption('--ledger <path>', 'Review ledger path')
421
+ .requiredOption('--definition <path>', 'JSON definition containing partitions, slices, and cross-slice contracts')
422
+ .option('--output <path>', 'Portfolio output path')
423
+ .option('--json', 'JSON output')
424
+ .action(wrap((opts) => runMigrationPortfolioInit({ ...opts, json: Boolean(opts.json) })));
425
+ migrationPortfolio
426
+ .command('validate')
427
+ .requiredOption('--portfolio <path>', 'Migration portfolio path')
428
+ .option('--strict', 'Fail unless partitions, provenance, slices, and cross-slice contracts are complete')
429
+ .option('--json', 'JSON output')
430
+ .action(wrap((opts) => runMigrationPortfolioValidate({
431
+ portfolio: opts.portfolio,
432
+ strict: Boolean(opts.strict),
433
+ json: Boolean(opts.json),
434
+ })));
435
+ const migrationDatabase = program.command('migration-database').description('Validate database baseline, data movement, and cutover evidence');
436
+ migrationDatabase.command('init').requiredOption('--definition <path>').requiredOption('--output <path>').option('--json')
437
+ .action(wrap((opts) => runMigrationDatabaseInit({ ...opts, json: Boolean(opts.json) })));
438
+ migrationDatabase.command('validate').requiredOption('--directory <path>').option('--strict').option('--json')
439
+ .action(wrap((opts) => runMigrationDatabaseValidate({ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json) })));
440
+ program.command('migration-environments')
441
+ .description('Reconcile detected environments with an authenticated product environment inventory')
442
+ .requiredOption('--analysis <path>')
443
+ .requiredOption('--inventory <path>')
444
+ .option('--strict')
445
+ .option('--json')
446
+ .action(wrap((opts) => runMigrationEnvironmentsReconcile({
447
+ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json),
448
+ })));
449
+ const migrationProducts = program.command('migration-products')
450
+ .description('Validate monorepo service ownership, product mapping, asset environment coverage, and promotion');
451
+ migrationProducts.command('init')
452
+ .requiredOption('--analysis <path>').requiredOption('--definition <path>').requiredOption('--output <path>')
453
+ .option('--json').action(wrap((opts) => runMigrationProductsInit({ ...opts, json: Boolean(opts.json) })));
454
+ migrationProducts.command('validate')
455
+ .requiredOption('--file <path>').option('--strict').option('--json')
456
+ .action(wrap((opts) => runMigrationProductsValidate({
457
+ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json),
458
+ })));
459
+ const migrationSecrets = program.command('migration-secrets')
460
+ .description('Classify and reconcile names-only secret references and rotation plans');
461
+ migrationSecrets.command('init')
462
+ .requiredOption('--analysis <path>').requiredOption('--definition <path>').requiredOption('--output <path>')
463
+ .option('--json').action(wrap((opts) => runMigrationSecretsInit({ ...opts, json: Boolean(opts.json) })));
464
+ migrationSecrets.command('validate')
465
+ .requiredOption('--file <path>').option('--strict').option('--json')
466
+ .action(wrap((opts) => runMigrationSecretsValidate({
467
+ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json),
468
+ })));
146
469
  const APPLY_TYPES = ['sessions', 'notifications', 'events'];
147
470
  program
148
471
  .command('apply')
@@ -210,13 +533,25 @@ productComponents
210
533
  .action(wrap((opts) => runProductComponents('get', opts)));
211
534
  const productEnvironments = products
212
535
  .command('environments')
213
- .description('Product environments (list, get)');
536
+ .description('Product environments (list, get, idempotent create, update)');
214
537
  productEnvironments
215
538
  .command('list [product_tag]')
216
539
  .description('List all environments for a product')
217
540
  .option('--product <tag>', 'Product tag (defaults to linked project)')
218
541
  .option('--json', 'JSON output')
219
542
  .action(wrap((productTagArg, opts) => runProductEnvironments('list', { product: opts.product ?? productTagArg, json: opts.json }, [])));
543
+ productEnvironments
544
+ .command('create <product_tag>')
545
+ .description('Idempotently create a product environment and verify persistence')
546
+ .requiredOption('-f, --file <path>', 'Environment JSON: env_name, description, slug, active?')
547
+ .option('--json', 'JSON output')
548
+ .action(wrap((productTag, opts) => runProductEnvironments('create', { product: productTag, file: opts.file, json: opts.json }, [])));
549
+ productEnvironments
550
+ .command('update <product_tag> <slug>')
551
+ .description('Update a product environment and verify persistence')
552
+ .requiredOption('-f, --file <path>', 'Environment patch JSON')
553
+ .option('--json', 'JSON output')
554
+ .action(wrap((productTag, slug, opts) => runProductEnvironments('update', { product: productTag, slug, file: opts.file, json: opts.json }, [])));
220
555
  productEnvironments
221
556
  .command('get <product_tag> [slug]')
222
557
  .description('Fetch a single product environment by slug')
@@ -369,6 +704,23 @@ program
369
704
  .option('--no-interactive', 'Require -f JSON for create/update')
370
705
  .option('--json', 'JSON output')
371
706
  .action(wrap((verb, opts) => runSecrets(verb, opts)));
707
+ program
708
+ .command('secrets-import-env')
709
+ .description('Import one secret directly from a local .env file without placing its value in CLI arguments')
710
+ .requiredOption('--env-file <path>', 'Local environment file')
711
+ .requiredOption('--source-key <name>', 'Environment variable name to read')
712
+ .option('-k, --key <key>', 'Destination Ductape secret key; defaults to source key')
713
+ .option('--env <slug>', 'Ductape environment scope')
714
+ .option('--description <text>')
715
+ .option('--json', 'Redacted JSON output')
716
+ .action(wrap((opts) => runSecretImportEnv({
717
+ envFile: opts.envFile,
718
+ sourceKey: opts.sourceKey,
719
+ key: opts.key,
720
+ env: opts.env,
721
+ description: opts.description,
722
+ json: Boolean(opts.json),
723
+ })));
372
724
  const generate = program.command('generate').description('Payload templates & code snippets');
373
725
  generate
374
726
  .command('payload <family> <method>')
@@ -0,0 +1,13 @@
1
+ export type MigrationArtifactErrorCode = 'ARTIFACT_TOO_LARGE' | 'ARTIFACT_INVALID_JSON' | 'ARTIFACT_SECRET_MATERIAL' | 'ARTIFACT_LOCKED' | 'ARTIFACT_EXISTS' | 'ARTIFACT_VERSION_UNSUPPORTED' | 'ARTIFACT_RECOVERY_FAILED';
2
+ export declare class MigrationArtifactError extends Error {
3
+ readonly code: MigrationArtifactErrorCode;
4
+ constructor(code: MigrationArtifactErrorCode, message: string);
5
+ }
6
+ export declare function readMigrationJson<T>(file: string, maxBytes?: number): T;
7
+ export declare function writeMigrationJson(file: string, value: unknown, opts?: {
8
+ createOnly?: boolean;
9
+ backup?: boolean;
10
+ mode?: number;
11
+ }): void;
12
+ export declare function recoverMigrationJson(file: string): void;
13
+ export declare function migrateMigrationJson(file: string, output: string): void;
@@ -0,0 +1,103 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export class MigrationArtifactError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ super(`[${code}] ${message}`);
7
+ this.code = code;
8
+ this.name = 'MigrationArtifactError';
9
+ }
10
+ }
11
+ const SECRET_KEY = /^(?:value|password|passwd|token|access_key|accesskey|private_key|credential|client_secret)$/i;
12
+ const SECRET_VALUE = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|(?:^|\s)eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}|[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/i;
13
+ const SAFE_PLACEHOLDER = /^\[(?:REDACTED|NOT_READ|NOT_RETURNED)\]$|^\$Secret\{[^}]+\}$/;
14
+ function inspect(value, location = '$') {
15
+ if (Array.isArray(value)) {
16
+ value.forEach((item, index) => inspect(item, `${location}[${index}]`));
17
+ return;
18
+ }
19
+ if (!value || typeof value !== 'object')
20
+ return;
21
+ for (const [key, child] of Object.entries(value)) {
22
+ const childLocation = `${location}.${key}`;
23
+ if (typeof child === 'string' &&
24
+ ((SECRET_KEY.test(key) && child.trim() && !SAFE_PLACEHOLDER.test(child)) || SECRET_VALUE.test(child))) {
25
+ throw new MigrationArtifactError('ARTIFACT_SECRET_MATERIAL', `Potential secret material or value-bearing field is forbidden at ${childLocation}; store only a name or $Secret{tag} reference.`);
26
+ }
27
+ inspect(child, childLocation);
28
+ }
29
+ }
30
+ export function readMigrationJson(file, maxBytes = 5_000_000) {
31
+ const resolved = path.resolve(file);
32
+ const stats = fs.statSync(resolved);
33
+ if (stats.size > maxBytes) {
34
+ throw new MigrationArtifactError('ARTIFACT_TOO_LARGE', `${resolved} exceeds ${maxBytes} bytes.`);
35
+ }
36
+ let value;
37
+ try {
38
+ value = JSON.parse(fs.readFileSync(resolved, 'utf8'));
39
+ }
40
+ catch (error) {
41
+ throw new MigrationArtifactError('ARTIFACT_INVALID_JSON', `${resolved}: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ inspect(value);
44
+ return value;
45
+ }
46
+ export function writeMigrationJson(file, value, opts = {}) {
47
+ inspect(value);
48
+ const resolved = path.resolve(file);
49
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
50
+ const lock = `${resolved}.lock`;
51
+ let descriptor;
52
+ try {
53
+ try {
54
+ descriptor = fs.openSync(lock, 'wx');
55
+ }
56
+ catch (error) {
57
+ if (error.code === 'EEXIST') {
58
+ throw new MigrationArtifactError('ARTIFACT_LOCKED', `${resolved} is being written by another process.`);
59
+ }
60
+ throw error;
61
+ }
62
+ if (opts.createOnly && fs.existsSync(resolved)) {
63
+ throw new MigrationArtifactError('ARTIFACT_EXISTS', `${resolved} already exists.`);
64
+ }
65
+ if (opts.backup && fs.existsSync(resolved)) {
66
+ fs.copyFileSync(resolved, `${resolved}.bak`);
67
+ }
68
+ const temporary = `${resolved}.tmp-${process.pid}-${Date.now()}`;
69
+ fs.writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: opts.mode });
70
+ fs.renameSync(temporary, resolved);
71
+ }
72
+ finally {
73
+ if (descriptor !== undefined) {
74
+ fs.closeSync(descriptor);
75
+ if (fs.existsSync(lock))
76
+ fs.unlinkSync(lock);
77
+ }
78
+ }
79
+ }
80
+ export function recoverMigrationJson(file) {
81
+ const resolved = path.resolve(file);
82
+ const backup = `${resolved}.bak`;
83
+ try {
84
+ readMigrationJson(backup);
85
+ fs.copyFileSync(backup, resolved);
86
+ readMigrationJson(resolved);
87
+ }
88
+ catch (error) {
89
+ throw new MigrationArtifactError('ARTIFACT_RECOVERY_FAILED', error instanceof Error ? error.message : String(error));
90
+ }
91
+ }
92
+ export function migrateMigrationJson(file, output) {
93
+ const value = readMigrationJson(file);
94
+ if (value.version === 1) {
95
+ writeMigrationJson(output, value, { createOnly: true });
96
+ return;
97
+ }
98
+ if (value.version === 0 && typeof value.artifact_kind === 'string') {
99
+ writeMigrationJson(output, { ...value, version: 1, migrated_from_version: 0 }, { createOnly: true });
100
+ return;
101
+ }
102
+ throw new MigrationArtifactError('ARTIFACT_VERSION_UNSUPPORTED', `No controlled migration exists for version ${String(value.version)}.`);
103
+ }
package/package.json CHANGED
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "@ductape/cli",
3
- "version": "0.2.23",
3
+ "version": "0.3.0",
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",
7
7
  "bin": {
8
8
  "ductape": "dist/index.js"
9
9
  },
10
+ "files": [
11
+ "dist",
12
+ "templates",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ ".env.ductape.example"
16
+ ],
10
17
  "scripts": {
11
18
  "build": "tsc",
12
19
  "dev": "tsx src/index.ts",
package/API_PARITY.md DELETED
@@ -1,58 +0,0 @@
1
- # CLI ↔ API parity (Nest / Workbench)
2
-
3
- The CLI uses the **same REST paths as the Workbench and TypeScript SDK**.
4
-
5
- | Profile | `api_url` | Example login URL |
6
- |---------|-----------|-------------------|
7
- | `cloud` | `https://api.ductape.app` | `https://api.ductape.app/users/v1/login` |
8
- | `local` | `http://localhost:4311/api` | `http://localhost:4311/api/users/v1/login` |
9
-
10
- Production does **not** use an `/api` prefix; the local docker gateway does.
11
-
12
- ## Gateway routing
13
-
14
- | Client calls | Gateway (`/api/...`) | Nest service receives |
15
- |--------------|----------------------|------------------------|
16
- | `{api}/users/v1/login` | `/api/users/` → users:8002 | `/users/v1/login` |
17
- | `{api}/workspaces/v1/fetch/...` | `/api/workspaces/` → workspaces:8001 | `/workspaces/v1/...` |
18
- | `{api}/apps/v1/import` | `/api/apps/` → apps:8004 | `/apps/v1/import` |
19
- | `{api}/integrations/v1/create` | `/api/integrations/` → integrations:8009 | `/integrations/v1/create` |
20
- | `{api}/proxy/v1/sdk-proxy/execute` | `/api/proxy/` → proxy:8020 | encrypted sdk-proxy |
21
-
22
- Legacy Express services are **not** called by the CLI. All commands target Nest controllers under `platform/nest/apps/*/src/**/controllers/`.
23
-
24
- ## Command → endpoint map
25
-
26
- | CLI | Method | Path (after `{api_url}`) | Workbench equivalent |
27
- |-----|--------|---------------------------|-------------------|
28
- | `ductape login` | POST | `/users/v1/login` | Login modal |
29
- | `ductape workspaces list` | GET | `/workspaces/v1/fetch/:user_id` | Workspace list |
30
- | `ductape products list` | GET | `/integrations/v1/workspace/:id/:status` | Products sidebar |
31
- | `ductape products create` | POST | `/integrations/v1/create` | Create product |
32
- | `ductape products update` | sdk-proxy | `product.update` | Product settings |
33
- | `ductape products delete` | DELETE | `/integrations/v1/:id` | Delete product |
34
- | `ductape apps list` | GET | `/apps/v1/workspace/:id/:status` | Apps sidebar |
35
- | `ductape apps create` | POST | `/apps/v1/create` | Create app |
36
- | `ductape apps import` | POST multipart | `/apps/v1/import` | App import dialog |
37
- | `ductape apps update` | PUT | `/apps/v1/:app_id` | App settings |
38
- | `ductape apps delete` | DELETE | `/apps/v1/:app_id` | Delete app |
39
- | `ductape resources *` | POST | `/proxy/v1/sdk-proxy/execute` | SDK proxy (encrypted) |
40
- | `ductape db *` | POST | `/proxy/v1/db-proxy/...` | Database proxy |
41
- | `ductape graph *` | POST | `/proxy/v1/graph-proxy/...` | Graph proxy |
42
-
43
- ## Import body fields (`apps import`)
44
-
45
- Matches `AppsImportController` and `@ductape/sdk` `AppApiService.importApp`:
46
-
47
- | Field | CLI / SDK value |
48
- |-------|-----------------|
49
- | `type` | `PostmanV2.1` or `OpenApiV3.0` |
50
- | `version` | default `0.0.1` |
51
- | `update` | `true` / `false` |
52
- | `workspace_id`, `user_id`, `public_key` | from session |
53
- | `app_id` | optional (`--app-id` / `--app-tag`) |
54
- | `file` | multipart upload |
55
-
56
- ## SDK base URL note
57
-
58
- The TS SDK client uses host `https://api.ductape.app` with paths like `/apps/v1/...` (no `/api` segment). Production routing may terminate TLS at a layer that forwards directly to services. **CLI and Workbench** use `{host}/api` as `api_url` — always set `ductape profiles use local` to `http://localhost:4311/api` for local parity.