@ductape/cli 0.2.23 → 0.3.1

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 (104) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +7 -0
  3. package/dist/commands/db-migrate.js +47 -8
  4. package/dist/commands/migrate-codebase.d.ts +190 -0
  5. package/dist/commands/migrate-codebase.js +776 -0
  6. package/dist/commands/migration-artifact.d.ts +28 -0
  7. package/dist/commands/migration-artifact.js +101 -0
  8. package/dist/commands/migration-assurance.d.ts +10 -0
  9. package/dist/commands/migration-assurance.js +120 -0
  10. package/dist/commands/migration-capabilities.d.ts +53 -0
  11. package/dist/commands/migration-capabilities.js +66 -0
  12. package/dist/commands/migration-database.d.ts +26 -0
  13. package/dist/commands/migration-database.js +162 -0
  14. package/dist/commands/migration-e2e.d.ts +68 -0
  15. package/dist/commands/migration-e2e.js +189 -0
  16. package/dist/commands/migration-environments.d.ts +20 -0
  17. package/dist/commands/migration-environments.js +58 -0
  18. package/dist/commands/migration-generated.d.ts +68 -0
  19. package/dist/commands/migration-generated.js +217 -0
  20. package/dist/commands/migration-portfolio.d.ts +88 -0
  21. package/dist/commands/migration-portfolio.js +223 -0
  22. package/dist/commands/migration-products.d.ts +23 -0
  23. package/dist/commands/migration-products.js +88 -0
  24. package/dist/commands/migration-review.d.ts +248 -0
  25. package/dist/commands/migration-review.js +624 -0
  26. package/dist/commands/migration-secrets.d.ts +24 -0
  27. package/dist/commands/migration-secrets.js +93 -0
  28. package/dist/commands/migration-slice.d.ts +132 -0
  29. package/dist/commands/migration-slice.js +266 -0
  30. package/dist/commands/migration-verification.d.ts +12 -0
  31. package/dist/commands/migration-verification.js +194 -0
  32. package/dist/commands/products.d.ts +1 -0
  33. package/dist/commands/products.js +33 -1
  34. package/dist/commands/secrets.d.ts +8 -0
  35. package/dist/commands/secrets.js +44 -0
  36. package/dist/index.js +376 -4
  37. package/dist/lib/migration-artifact.d.ts +19 -0
  38. package/dist/lib/migration-artifact.js +114 -0
  39. package/package.json +8 -1
  40. package/API_PARITY.md +0 -58
  41. package/DB_MIGRATE_PLAN.md +0 -223
  42. package/ductape.example.ts +0 -18
  43. package/src/commands/apply.ts +0 -153
  44. package/src/commands/apps-import.ts +0 -43
  45. package/src/commands/apps.ts +0 -92
  46. package/src/commands/cloud.ts +0 -68
  47. package/src/commands/completion.ts +0 -119
  48. package/src/commands/db-migrate.ts +0 -209
  49. package/src/commands/db-schema.ts +0 -429
  50. package/src/commands/db.ts +0 -63
  51. package/src/commands/generate.ts +0 -74
  52. package/src/commands/graph.ts +0 -51
  53. package/src/commands/init.ts +0 -39
  54. package/src/commands/install.ts +0 -76
  55. package/src/commands/link.ts +0 -84
  56. package/src/commands/login.ts +0 -145
  57. package/src/commands/logout.ts +0 -7
  58. package/src/commands/platform.ts +0 -123
  59. package/src/commands/products.ts +0 -228
  60. package/src/commands/profiles.ts +0 -23
  61. package/src/commands/resources.ts +0 -213
  62. package/src/commands/secrets.ts +0 -46
  63. package/src/commands/unlink.ts +0 -15
  64. package/src/commands/whoami.ts +0 -27
  65. package/src/commands/workspaces.ts +0 -98
  66. package/src/index.ts +0 -523
  67. package/src/lib/api-client.ts +0 -88
  68. package/src/lib/app-import.ts +0 -82
  69. package/src/lib/apply-loaders.ts +0 -30
  70. package/src/lib/config.ts +0 -230
  71. package/src/lib/context-store.ts +0 -77
  72. package/src/lib/db-methods.ts +0 -56
  73. package/src/lib/db-types.ts +0 -104
  74. package/src/lib/encryption.ts +0 -12
  75. package/src/lib/forms/enums.ts +0 -52
  76. package/src/lib/forms/index.ts +0 -8
  77. package/src/lib/forms/prompt.ts +0 -209
  78. package/src/lib/forms/schemas.ts +0 -288
  79. package/src/lib/forms/types.ts +0 -27
  80. package/src/lib/http.ts +0 -76
  81. package/src/lib/integrations.ts +0 -110
  82. package/src/lib/interactive-opts.ts +0 -13
  83. package/src/lib/migration-files.ts +0 -58
  84. package/src/lib/output.ts +0 -39
  85. package/src/lib/platform-api.ts +0 -209
  86. package/src/lib/proxy/context.ts +0 -94
  87. package/src/lib/proxy/db-proxy.ts +0 -40
  88. package/src/lib/proxy/graph-proxy.ts +0 -64
  89. package/src/lib/proxy/sdk-proxy.ts +0 -58
  90. package/src/lib/read-body.ts +0 -57
  91. package/src/lib/resources.ts +0 -160
  92. package/src/lib/schema-loader.ts +0 -152
  93. package/src/lib/snippets.ts +0 -165
  94. package/src/lib/templates.ts +0 -157
  95. package/src/lib/workspace-context.ts +0 -43
  96. package/src/lib/workspaces.ts +0 -283
  97. package/test/apply-loaders.test.ts +0 -40
  98. package/test/encryption.test.ts +0 -20
  99. package/test/forms.test.ts +0 -25
  100. package/test/http.test.ts +0 -15
  101. package/test/platform-api.test.ts +0 -42
  102. package/test/resources.test.ts +0 -23
  103. package/test/workspace-resolve.test.ts +0 -29
  104. 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,13 +26,31 @@ 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';
42
+ import { runMigrationCapabilities } from './commands/migration-capabilities.js';
43
+ import { structuredMigrationError } from './lib/migration-artifact.js';
29
44
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
45
  const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
31
46
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
47
  function wrap(fn) {
33
48
  return (...args) => {
34
- Promise.resolve(fn(...args)).catch((err) => {
35
- console.error(err instanceof Error ? err.message : err);
49
+ Promise.resolve().then(() => fn(...args)).catch((err) => {
50
+ const command = process.argv[2] ?? '';
51
+ console.error(command === 'migrate-codebase' || command.startsWith('migration-')
52
+ ? structuredMigrationError(err)
53
+ : err instanceof Error ? err.message : err);
36
54
  process.exit(1);
37
55
  });
38
56
  };
@@ -143,6 +161,331 @@ program
143
161
  env: opts.env,
144
162
  language: opts.language,
145
163
  })));
164
+ program
165
+ .command('migrate-codebase')
166
+ .description('Inspect a codebase and write advisory guidance for an AI-led Ductape migration')
167
+ .option('-C, --source <path>', 'Existing codebase', process.cwd())
168
+ .requiredOption('--e2e-baseline <path>', 'Passing immutable original E2E baseline manifest')
169
+ .option('--mode <mode>', 'in-place | new-codebase', 'new-codebase')
170
+ .option('--destination <path>', 'Destination for new-codebase mode')
171
+ .option('--product <tag>', 'Ductape product tag')
172
+ .option('--name <name>', 'Product display name')
173
+ .option('--database <tag>', 'Database tag for generated Ductape schema')
174
+ .option('--max-file-bytes <bytes>', 'Maximum reviewable file size', '2000000')
175
+ .option('--include <patterns>', 'Comma-separated repository-relative glob patterns')
176
+ .option('--exclude <patterns>', 'Comma-separated repository-relative glob patterns')
177
+ .option('--ensure-product', 'Create the product when it does not exist')
178
+ .option('--write', 'Write redacted advisory artifacts only; never writes application code or executable assets')
179
+ .option('--mcp-client-name <name>', 'Protocol-asserted MCP client name (unverified)', undefined)
180
+ .option('--mcp-client-version <version>', 'Protocol-asserted MCP client version (unverified)', undefined)
181
+ .option('--json', 'JSON output')
182
+ .action(wrap((opts) => runMigrateCodebase({
183
+ source: opts.source,
184
+ e2eBaseline: opts.e2eBaseline,
185
+ mode: opts.mode,
186
+ destination: opts.destination,
187
+ product: opts.product,
188
+ name: opts.name,
189
+ database: opts.database,
190
+ maxFileBytes: Number(opts.maxFileBytes),
191
+ include: opts.include ? String(opts.include).split(',').map((value) => value.trim()).filter(Boolean) : [],
192
+ exclude: opts.exclude ? String(opts.exclude).split(',').map((value) => value.trim()).filter(Boolean) : [],
193
+ ensureProduct: Boolean(opts.ensureProduct),
194
+ write: Boolean(opts.write),
195
+ json: Boolean(opts.json),
196
+ mcpClientName: opts.mcpClientName,
197
+ mcpClientVersion: opts.mcpClientVersion,
198
+ })));
199
+ const migrationE2E = program
200
+ .command('migration-e2e')
201
+ .description('Bind the original E2E suite and validate final migration parity without executing tests');
202
+ migrationE2E
203
+ .command('init')
204
+ .requiredOption('-C, --source <path>', 'Original codebase')
205
+ .requiredOption('--definition <path>', 'JSON containing suite files, command, and passing baseline evidence')
206
+ .option('--output <path>', 'Baseline manifest output path')
207
+ .option('--json', 'JSON output')
208
+ .action(wrap((opts) => runMigrationE2EInit({ ...opts, json: Boolean(opts.json) })));
209
+ migrationE2E
210
+ .command('final')
211
+ .requiredOption('--manifest <path>', 'Original E2E baseline manifest')
212
+ .requiredOption('--evidence <path>', 'JSON containing passing final-run evidence')
213
+ .option('--json', 'JSON output')
214
+ .action(wrap((opts) => runMigrationE2EFinal({ ...opts, json: Boolean(opts.json) })));
215
+ migrationE2E
216
+ .command('validate')
217
+ .requiredOption('--manifest <path>', 'Original E2E baseline manifest')
218
+ .option('--strict', 'Fail unless baseline integrity and the final run are verified')
219
+ .option('--json', 'JSON output')
220
+ .action(wrap((opts) => runMigrationE2EValidate({ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json) })));
221
+ const migrationGenerated = program
222
+ .command('migration-generated')
223
+ .description('Validate generated contracts and locally modified vendor source without executing generators');
224
+ migrationGenerated
225
+ .command('init')
226
+ .requiredOption('--analysis <path>', 'Migration analysis containing generated-source hints')
227
+ .requiredOption('--definition <path>', 'Contextually reviewed generated/vendor definition JSON')
228
+ .option('--output <path>', 'Evidence manifest output path')
229
+ .option('--json', 'JSON output')
230
+ .action(wrap((opts) => runMigrationGeneratedInit({ ...opts, json: Boolean(opts.json) })));
231
+ migrationGenerated
232
+ .command('validate')
233
+ .requiredOption('--file <path>', 'Generated/vendor evidence manifest')
234
+ .option('--strict', 'Fail on stale inputs, regeneration mismatch, or missing contract evidence')
235
+ .option('--json', 'JSON output')
236
+ .action(wrap((opts) => runMigrationGeneratedValidate({
237
+ ...opts,
238
+ strict: Boolean(opts.strict),
239
+ json: Boolean(opts.json),
240
+ })));
241
+ program
242
+ .command('migration-assurance')
243
+ .description('Validate repository-wide component, session, resilience, runtime, API, and SDK evidence')
244
+ .requiredOption('--file <path>', 'Versioned migration assurance JSON')
245
+ .option('--strict', 'Fail unless every required assurance category is evidenced and tested')
246
+ .option('--json', 'JSON output')
247
+ .action(wrap((opts) => runMigrationAssuranceValidate({
248
+ file: opts.file,
249
+ strict: Boolean(opts.strict),
250
+ json: Boolean(opts.json),
251
+ })));
252
+ program
253
+ .command('migration-capabilities')
254
+ .description('Print the source-verified capability matrix for supported SDK language/version pairs')
255
+ .option('--language <language>', 'typescript | go | java | dotnet')
256
+ .option('--version <version>', 'Exact supported SDK version')
257
+ .option('--json', 'JSON output')
258
+ .action(wrap((opts) => runMigrationCapabilities({
259
+ language: opts.language,
260
+ version: opts.version,
261
+ json: Boolean(opts.json),
262
+ })));
263
+ program
264
+ .command('migration-verification')
265
+ .description('Validate cited automation evidence for large-repo, parity, frontend, database, and asset migration')
266
+ .requiredOption('--file <path>', 'Versioned migration verification matrix JSON')
267
+ .option('--strict', 'Fail unless every verification requirement is passing, cited, and current')
268
+ .option('--json', 'JSON output')
269
+ .action(wrap((opts) => runMigrationVerificationValidate({
270
+ file: opts.file,
271
+ strict: Boolean(opts.strict),
272
+ json: Boolean(opts.json),
273
+ })));
274
+ const migrationArtifact = program
275
+ .command('migration-artifact')
276
+ .description('Validate, recover, and version-migrate secured migration JSON artifacts');
277
+ migrationArtifact.command('validate').requiredOption('--file <path>').option('--json')
278
+ .action(wrap((opts) => runMigrationArtifactValidate({ file: opts.file, json: Boolean(opts.json) })));
279
+ migrationArtifact.command('recover').requiredOption('--file <path>').option('--json')
280
+ .action(wrap((opts) => runMigrationArtifactRecover({ file: opts.file, json: Boolean(opts.json) })));
281
+ migrationArtifact.command('migrate').requiredOption('--file <path>').requiredOption('--output <path>').option('--json')
282
+ .action(wrap((opts) => runMigrationArtifactMigrate({ file: opts.file, output: opts.output, json: Boolean(opts.json) })));
283
+ migrationArtifact.command('schemas').option('--json')
284
+ .action(wrap((opts) => runMigrationArtifactSchemas(Boolean(opts.json))));
285
+ const migrationReview = program
286
+ .command('migration-review')
287
+ .description('Track and validate an AI-led migration review without generating application code');
288
+ migrationReview
289
+ .command('init')
290
+ .requiredOption('--analysis <path>', 'Path to migration-guidance/analysis.json')
291
+ .option('--output <path>', 'Review ledger output path')
292
+ .option('--json', 'JSON output')
293
+ .action(wrap((opts) => runMigrationReviewInit({
294
+ analysis: opts.analysis,
295
+ output: opts.output,
296
+ json: Boolean(opts.json),
297
+ })));
298
+ migrationReview
299
+ .command('record')
300
+ .requiredOption('--ledger <path>', 'Review ledger path')
301
+ .requiredOption('--file <path>', 'Repository-relative file')
302
+ .requiredOption('--data <path>', 'JSON file containing contextual review evidence')
303
+ .option('--json', 'JSON output')
304
+ .action(wrap((opts) => runMigrationReviewRecord({ ...opts, json: Boolean(opts.json) })));
305
+ migrationReview
306
+ .command('follow-up')
307
+ .requiredOption('--ledger <path>', 'Review ledger path')
308
+ .requiredOption('--file <path>', 'Repository-relative source file')
309
+ .requiredOption('--follow-up <path>', 'Repository-relative file discovered during review')
310
+ .option('--json', 'JSON output')
311
+ .action(wrap((opts) => runMigrationReviewFollowUp({ ...opts, json: Boolean(opts.json) })));
312
+ migrationReview
313
+ .command('exclude')
314
+ .requiredOption('--ledger <path>', 'Review ledger path')
315
+ .requiredOption('--file <path>', 'Repository-relative file')
316
+ .requiredOption('--classification <type>', 'generated | vendor | build | snapshot | other')
317
+ .requiredOption('--reason <text>', 'Why the file is excluded')
318
+ .option('--owner <text>', 'Owner or source of the excluded file')
319
+ .option('--generator <text>', 'Generator command/tool (required for generated)')
320
+ .option('--regeneration-test <text>', 'Regeneration verification (required for generated)')
321
+ .option('--json', 'JSON output')
322
+ .action(wrap((opts) => runMigrationReviewExclude({ ...opts, json: Boolean(opts.json) })));
323
+ migrationReview
324
+ .command('parity')
325
+ .requiredOption('--ledger <path>', 'Review ledger path')
326
+ .requiredOption('--file <path>', 'Repository-relative file')
327
+ .requiredOption('--concern <type>', 'interface | functional | operational')
328
+ .requiredOption('--status <status>', 'verified | not_applicable')
329
+ .option('--evidence <text>', 'Concrete parity evidence (required for verified)')
330
+ .option('--reason <text>', 'Reason (required for not_applicable)')
331
+ .option('--json', 'JSON output')
332
+ .action(wrap((opts) => runMigrationReviewParity({ ...opts, json: Boolean(opts.json) })));
333
+ migrationReview
334
+ .command('reopen')
335
+ .requiredOption('--ledger <path>', 'Review ledger path')
336
+ .requiredOption('--file <path>', 'Repository-relative file')
337
+ .option('--json', 'JSON output')
338
+ .action(wrap((opts) => runMigrationReviewReopen({ ...opts, json: Boolean(opts.json) })));
339
+ migrationReview
340
+ .command('move')
341
+ .requiredOption('--ledger <path>', 'Review ledger path')
342
+ .requiredOption('--from <path>', 'Previous repository-relative file')
343
+ .requiredOption('--to <path>', 'New repository-relative file with identical reviewed content')
344
+ .option('--json', 'JSON output')
345
+ .action(wrap((opts) => runMigrationReviewMove({ ...opts, json: Boolean(opts.json) })));
346
+ migrationReview
347
+ .command('merge')
348
+ .requiredOption('--base <path>', 'Common-base review ledger')
349
+ .requiredOption('--current <path>', 'Current review ledger')
350
+ .requiredOption('--incoming <path>', 'Incoming parallel review ledger')
351
+ .requiredOption('--output <path>', 'New merged ledger; existing ledgers are never overwritten')
352
+ .option('--json', 'JSON output')
353
+ .action(wrap((opts) => runMigrationReviewMerge({ ...opts, json: Boolean(opts.json) })));
354
+ migrationReview
355
+ .command('sign')
356
+ .requiredOption('--ledger <path>', 'Review ledger')
357
+ .requiredOption('--private-key <path>', 'Trusted Ed25519 private key PEM file')
358
+ .requiredOption('--key-id <id>', 'Trusted signing identity')
359
+ .option('--output <path>', 'Detached signature output')
360
+ .option('--json', 'JSON output')
361
+ .action(wrap((opts) => runMigrationReviewSign({ ...opts, json: Boolean(opts.json) })));
362
+ migrationReview
363
+ .command('verify-signature')
364
+ .requiredOption('--ledger <path>', 'Review ledger')
365
+ .requiredOption('--signature <path>', 'Detached signature JSON')
366
+ .requiredOption('--public-key <path>', 'Trusted Ed25519 public key PEM file')
367
+ .option('--json', 'JSON output')
368
+ .action(wrap((opts) => runMigrationReviewVerifySignature({ ...opts, json: Boolean(opts.json) })));
369
+ migrationReview
370
+ .command('snapshot')
371
+ .requiredOption('--ledger <path>', 'Review ledger path')
372
+ .requiredOption('--file <path>', 'Repository-relative reviewed file')
373
+ .option('--json', 'JSON output')
374
+ .action(wrap((opts) => runMigrationReviewSnapshot({
375
+ ledger: opts.ledger,
376
+ file: opts.file,
377
+ json: Boolean(opts.json),
378
+ })));
379
+ migrationReview
380
+ .command('validate')
381
+ .requiredOption('--ledger <path>', 'Review ledger path')
382
+ .option('--strict', 'Fail unless coverage, staleness, context, follow-ups, and parity are complete')
383
+ .option('--json', 'JSON output')
384
+ .action(wrap((opts) => runMigrationReviewValidate({
385
+ ledger: opts.ledger,
386
+ strict: Boolean(opts.strict),
387
+ json: Boolean(opts.json),
388
+ })));
389
+ const migrationSlice = program
390
+ .command('migration-slice')
391
+ .description('Define and validate a parity-gated vertical migration slice');
392
+ migrationSlice
393
+ .command('init')
394
+ .requiredOption('--ledger <path>', 'Review ledger path')
395
+ .requiredOption('--tag <tag>', 'Stable slice tag')
396
+ .requiredOption('--name <name>', 'Slice display name')
397
+ .requiredOption('--files <path>', 'JSON array of repository-relative slice files')
398
+ .option('--output <path>', 'Slice manifest output path')
399
+ .option('--json', 'JSON output')
400
+ .action(wrap((opts) => runMigrationSliceInit({ ...opts, json: Boolean(opts.json) })));
401
+ migrationSlice
402
+ .command('define')
403
+ .requiredOption('--slice <path>', 'Migration slice manifest path')
404
+ .requiredOption('--data <path>', 'JSON file containing contracts, parity requirements, assets, tests, and cutover controls')
405
+ .option('--json', 'JSON output')
406
+ .action(wrap((opts) => runMigrationSliceDefine({
407
+ slice: opts.slice,
408
+ data: opts.data,
409
+ json: Boolean(opts.json),
410
+ })));
411
+ migrationSlice
412
+ .command('validate')
413
+ .requiredOption('--slice <path>', 'Migration slice manifest path')
414
+ .option('--strict', 'Fail unless review, parity, assets, tests, cutover, and rollback are complete')
415
+ .option('--json', 'JSON output')
416
+ .action(wrap((opts) => runMigrationSliceValidate({
417
+ slice: opts.slice,
418
+ strict: Boolean(opts.strict),
419
+ json: Boolean(opts.json),
420
+ })));
421
+ const migrationPortfolio = program
422
+ .command('migration-portfolio')
423
+ .description('Partition and validate a large migration across bounded AI contexts');
424
+ migrationPortfolio
425
+ .command('propose')
426
+ .requiredOption('--ledger <path>', 'Review ledger with contextual internal references')
427
+ .option('--max-bytes <bytes>', 'Maximum estimated bytes per proposed partition', '128000')
428
+ .option('--max-tokens <tokens>', 'Maximum estimated tokens per proposed partition', '32000')
429
+ .option('--output <path>', 'Advisory proposal output path')
430
+ .option('--json', 'JSON output')
431
+ .action(wrap((opts) => runMigrationPortfolioPropose({
432
+ ledger: opts.ledger,
433
+ maxBytes: Number(opts.maxBytes),
434
+ maxTokens: Number(opts.maxTokens),
435
+ output: opts.output,
436
+ json: Boolean(opts.json),
437
+ })));
438
+ migrationPortfolio
439
+ .command('init')
440
+ .requiredOption('--ledger <path>', 'Review ledger path')
441
+ .requiredOption('--definition <path>', 'JSON definition containing partitions, slices, and cross-slice contracts')
442
+ .option('--output <path>', 'Portfolio output path')
443
+ .option('--json', 'JSON output')
444
+ .action(wrap((opts) => runMigrationPortfolioInit({ ...opts, json: Boolean(opts.json) })));
445
+ migrationPortfolio
446
+ .command('validate')
447
+ .requiredOption('--portfolio <path>', 'Migration portfolio path')
448
+ .option('--strict', 'Fail unless partitions, provenance, slices, and cross-slice contracts are complete')
449
+ .option('--json', 'JSON output')
450
+ .action(wrap((opts) => runMigrationPortfolioValidate({
451
+ portfolio: opts.portfolio,
452
+ strict: Boolean(opts.strict),
453
+ json: Boolean(opts.json),
454
+ })));
455
+ const migrationDatabase = program.command('migration-database').description('Validate database baseline, data movement, and cutover evidence');
456
+ migrationDatabase.command('init').requiredOption('--definition <path>').requiredOption('--output <path>').option('--json')
457
+ .action(wrap((opts) => runMigrationDatabaseInit({ ...opts, json: Boolean(opts.json) })));
458
+ migrationDatabase.command('validate').requiredOption('--directory <path>').option('--strict').option('--json')
459
+ .action(wrap((opts) => runMigrationDatabaseValidate({ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json) })));
460
+ program.command('migration-environments')
461
+ .description('Reconcile detected environments with an authenticated product environment inventory')
462
+ .requiredOption('--analysis <path>')
463
+ .requiredOption('--inventory <path>')
464
+ .option('--strict')
465
+ .option('--json')
466
+ .action(wrap((opts) => runMigrationEnvironmentsReconcile({
467
+ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json),
468
+ })));
469
+ const migrationProducts = program.command('migration-products')
470
+ .description('Validate monorepo service ownership, product mapping, asset environment coverage, and promotion');
471
+ migrationProducts.command('init')
472
+ .requiredOption('--analysis <path>').requiredOption('--definition <path>').requiredOption('--output <path>')
473
+ .option('--json').action(wrap((opts) => runMigrationProductsInit({ ...opts, json: Boolean(opts.json) })));
474
+ migrationProducts.command('validate')
475
+ .requiredOption('--file <path>').option('--strict').option('--json')
476
+ .action(wrap((opts) => runMigrationProductsValidate({
477
+ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json),
478
+ })));
479
+ const migrationSecrets = program.command('migration-secrets')
480
+ .description('Classify and reconcile names-only secret references and rotation plans');
481
+ migrationSecrets.command('init')
482
+ .requiredOption('--analysis <path>').requiredOption('--definition <path>').requiredOption('--output <path>')
483
+ .option('--json').action(wrap((opts) => runMigrationSecretsInit({ ...opts, json: Boolean(opts.json) })));
484
+ migrationSecrets.command('validate')
485
+ .requiredOption('--file <path>').option('--strict').option('--json')
486
+ .action(wrap((opts) => runMigrationSecretsValidate({
487
+ ...opts, strict: Boolean(opts.strict), json: Boolean(opts.json),
488
+ })));
146
489
  const APPLY_TYPES = ['sessions', 'notifications', 'events'];
147
490
  program
148
491
  .command('apply')
@@ -210,13 +553,25 @@ productComponents
210
553
  .action(wrap((opts) => runProductComponents('get', opts)));
211
554
  const productEnvironments = products
212
555
  .command('environments')
213
- .description('Product environments (list, get)');
556
+ .description('Product environments (list, get, idempotent create, update)');
214
557
  productEnvironments
215
558
  .command('list [product_tag]')
216
559
  .description('List all environments for a product')
217
560
  .option('--product <tag>', 'Product tag (defaults to linked project)')
218
561
  .option('--json', 'JSON output')
219
562
  .action(wrap((productTagArg, opts) => runProductEnvironments('list', { product: opts.product ?? productTagArg, json: opts.json }, [])));
563
+ productEnvironments
564
+ .command('create <product_tag>')
565
+ .description('Idempotently create a product environment and verify persistence')
566
+ .requiredOption('-f, --file <path>', 'Environment JSON: env_name, description, slug, active?')
567
+ .option('--json', 'JSON output')
568
+ .action(wrap((productTag, opts) => runProductEnvironments('create', { product: productTag, file: opts.file, json: opts.json }, [])));
569
+ productEnvironments
570
+ .command('update <product_tag> <slug>')
571
+ .description('Update a product environment and verify persistence')
572
+ .requiredOption('-f, --file <path>', 'Environment patch JSON')
573
+ .option('--json', 'JSON output')
574
+ .action(wrap((productTag, slug, opts) => runProductEnvironments('update', { product: productTag, slug, file: opts.file, json: opts.json }, [])));
220
575
  productEnvironments
221
576
  .command('get <product_tag> [slug]')
222
577
  .description('Fetch a single product environment by slug')
@@ -369,6 +724,23 @@ program
369
724
  .option('--no-interactive', 'Require -f JSON for create/update')
370
725
  .option('--json', 'JSON output')
371
726
  .action(wrap((verb, opts) => runSecrets(verb, opts)));
727
+ program
728
+ .command('secrets-import-env')
729
+ .description('Import one secret directly from a local .env file without placing its value in CLI arguments')
730
+ .requiredOption('--env-file <path>', 'Local environment file')
731
+ .requiredOption('--source-key <name>', 'Environment variable name to read')
732
+ .option('-k, --key <key>', 'Destination Ductape secret key; defaults to source key')
733
+ .option('--env <slug>', 'Ductape environment scope')
734
+ .option('--description <text>')
735
+ .option('--json', 'Redacted JSON output')
736
+ .action(wrap((opts) => runSecretImportEnv({
737
+ envFile: opts.envFile,
738
+ sourceKey: opts.sourceKey,
739
+ key: opts.key,
740
+ env: opts.env,
741
+ description: opts.description,
742
+ json: Boolean(opts.json),
743
+ })));
372
744
  const generate = program.command('generate').description('Payload templates & code snippets');
373
745
  generate
374
746
  .command('payload <family> <method>')
@@ -0,0 +1,19 @@
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 const MIGRATION_ERROR_CODES: {
3
+ readonly validation: "MIGRATION_VALIDATION_FAILED";
4
+ readonly configuration: "MIGRATION_CONFIGURATION_INVALID";
5
+ readonly external: "MIGRATION_EXTERNAL_OPERATION_FAILED";
6
+ };
7
+ export declare function structuredMigrationError(error: unknown): string;
8
+ export declare class MigrationArtifactError extends Error {
9
+ readonly code: MigrationArtifactErrorCode;
10
+ constructor(code: MigrationArtifactErrorCode, message: string);
11
+ }
12
+ export declare function readMigrationJson<T>(file: string, maxBytes?: number): T;
13
+ export declare function writeMigrationJson(file: string, value: unknown, opts?: {
14
+ createOnly?: boolean;
15
+ backup?: boolean;
16
+ mode?: number;
17
+ }): void;
18
+ export declare function recoverMigrationJson(file: string): void;
19
+ export declare function migrateMigrationJson(file: string, output: string): void;
@@ -0,0 +1,114 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const MIGRATION_ERROR_CODES = {
4
+ validation: 'MIGRATION_VALIDATION_FAILED',
5
+ configuration: 'MIGRATION_CONFIGURATION_INVALID',
6
+ external: 'MIGRATION_EXTERNAL_OPERATION_FAILED',
7
+ };
8
+ export function structuredMigrationError(error) {
9
+ const message = error instanceof Error ? error.message : String(error);
10
+ if (/^\[[A-Z0-9_]+\]/.test(message))
11
+ return message;
12
+ return `[${MIGRATION_ERROR_CODES.validation}] ${message}`;
13
+ }
14
+ export class MigrationArtifactError extends Error {
15
+ code;
16
+ constructor(code, message) {
17
+ super(`[${code}] ${message}`);
18
+ this.code = code;
19
+ this.name = 'MigrationArtifactError';
20
+ }
21
+ }
22
+ const SECRET_KEY = /^(?:value|password|passwd|token|access_key|accesskey|private_key|credential|client_secret)$/i;
23
+ 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;
24
+ const SAFE_PLACEHOLDER = /^\[(?:REDACTED|NOT_READ|NOT_RETURNED)\]$|^\$Secret\{[^}]+\}$/;
25
+ function inspect(value, location = '$') {
26
+ if (Array.isArray(value)) {
27
+ value.forEach((item, index) => inspect(item, `${location}[${index}]`));
28
+ return;
29
+ }
30
+ if (!value || typeof value !== 'object')
31
+ return;
32
+ for (const [key, child] of Object.entries(value)) {
33
+ const childLocation = `${location}.${key}`;
34
+ if (typeof child === 'string' &&
35
+ ((SECRET_KEY.test(key) && child.trim() && !SAFE_PLACEHOLDER.test(child)) || SECRET_VALUE.test(child))) {
36
+ 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.`);
37
+ }
38
+ inspect(child, childLocation);
39
+ }
40
+ }
41
+ export function readMigrationJson(file, maxBytes = 5_000_000) {
42
+ const resolved = path.resolve(file);
43
+ const stats = fs.statSync(resolved);
44
+ if (stats.size > maxBytes) {
45
+ throw new MigrationArtifactError('ARTIFACT_TOO_LARGE', `${resolved} exceeds ${maxBytes} bytes.`);
46
+ }
47
+ let value;
48
+ try {
49
+ value = JSON.parse(fs.readFileSync(resolved, 'utf8'));
50
+ }
51
+ catch {
52
+ throw new MigrationArtifactError('ARTIFACT_INVALID_JSON', `${resolved}: JSON parsing failed; parser context is intentionally suppressed to prevent evidence leakage.`);
53
+ }
54
+ inspect(value);
55
+ return value;
56
+ }
57
+ export function writeMigrationJson(file, value, opts = {}) {
58
+ inspect(value);
59
+ const resolved = path.resolve(file);
60
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
61
+ const lock = `${resolved}.lock`;
62
+ let descriptor;
63
+ try {
64
+ try {
65
+ descriptor = fs.openSync(lock, 'wx');
66
+ }
67
+ catch (error) {
68
+ if (error.code === 'EEXIST') {
69
+ throw new MigrationArtifactError('ARTIFACT_LOCKED', `${resolved} is being written by another process.`);
70
+ }
71
+ throw error;
72
+ }
73
+ if (opts.createOnly && fs.existsSync(resolved)) {
74
+ throw new MigrationArtifactError('ARTIFACT_EXISTS', `${resolved} already exists.`);
75
+ }
76
+ if (opts.backup && fs.existsSync(resolved)) {
77
+ fs.copyFileSync(resolved, `${resolved}.bak`);
78
+ }
79
+ const temporary = `${resolved}.tmp-${process.pid}-${Date.now()}`;
80
+ fs.writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: opts.mode });
81
+ fs.renameSync(temporary, resolved);
82
+ }
83
+ finally {
84
+ if (descriptor !== undefined) {
85
+ fs.closeSync(descriptor);
86
+ if (fs.existsSync(lock))
87
+ fs.unlinkSync(lock);
88
+ }
89
+ }
90
+ }
91
+ export function recoverMigrationJson(file) {
92
+ const resolved = path.resolve(file);
93
+ const backup = `${resolved}.bak`;
94
+ try {
95
+ readMigrationJson(backup);
96
+ fs.copyFileSync(backup, resolved);
97
+ readMigrationJson(resolved);
98
+ }
99
+ catch (error) {
100
+ throw new MigrationArtifactError('ARTIFACT_RECOVERY_FAILED', error instanceof Error ? error.message : String(error));
101
+ }
102
+ }
103
+ export function migrateMigrationJson(file, output) {
104
+ const value = readMigrationJson(file);
105
+ if (value.version === 1) {
106
+ writeMigrationJson(output, value, { createOnly: true });
107
+ return;
108
+ }
109
+ if (value.version === 0 && typeof value.artifact_kind === 'string') {
110
+ writeMigrationJson(output, { ...value, version: 1, migrated_from_version: 0 }, { createOnly: true });
111
+ return;
112
+ }
113
+ throw new MigrationArtifactError('ARTIFACT_VERSION_UNSUPPORTED', `No controlled migration exists for version ${String(value.version)}.`);
114
+ }
package/package.json CHANGED
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "@ductape/cli",
3
- "version": "0.2.23",
3
+ "version": "0.3.1",
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.