@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
@@ -0,0 +1,194 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { printJson } from '../lib/output.js';
5
+ import { readMigrationJson } from '../lib/migration-artifact.js';
6
+ const REQUIRED = {
7
+ large_codebase: [
8
+ 'dependency_ordered_partitions', 'token_byte_estimates', 'scc_preserving_split',
9
+ 'cited_service_summaries', 'transitive_summary_invalidation', 'resumable_checkpoints',
10
+ 'multiple_repositories', 'cross_repository_contracts',
11
+ ],
12
+ parity: [
13
+ 'category_schemas', 'before_after_normalized', 'nondeterminism_tolerances',
14
+ 'test_strength', 'mutation_signal', 'safe_shadow_dual_run', 'residual_uncertainty_decision',
15
+ ],
16
+ frontend: [
17
+ 'angular_svelte_integration', 'browser_automation', 'accessibility_thresholds',
18
+ 'visual_baseline_approval', 'web_vitals_bundle_size', 'browser_version_matrix',
19
+ 'offline_background_reconnect', 'browser_ssr_security', 'native_mobile',
20
+ ],
21
+ database_baseline: [
22
+ 'relational_fields', 'indexes_constraints', 'views_triggers_procedures',
23
+ 'mongodb_documents', 'security_roles_encryption_audit', 'tenancy_partition_sharding',
24
+ 'timezone_collation_encoding_precision', 'query_plan_performance', 'multiple_database_order',
25
+ ],
26
+ database_data: [
27
+ 'deterministic_selection_order', 'backfill_throttling', 'retry_poison_records',
28
+ 'idempotency_keys', 'transformation_versioning', 'quality_thresholds',
29
+ 'pii_least_privilege', 'large_binary_objects', 'cdc_outbox_coordination',
30
+ 'pause_resume_abort_drills', 'resumable_execution_evidence',
31
+ ],
32
+ database_cutover: [
33
+ 'old_new_schema_compatibility', 'dual_write_conflict_resolution',
34
+ 'migration_lock_concurrency', 'backup_identity_encryption_retention',
35
+ 'restore_rehearsal', 'irreversible_approval', 'monitoring_observation_thresholds',
36
+ 'post_cutover_reconciliation', 'slice_portfolio_link', 'event_feature_cdc_order',
37
+ 'sandbox_readonly_authorization',
38
+ ],
39
+ assets: [
40
+ 'component_designs', 'live_inventory_reconciliation', 'environment_configuration_gaps',
41
+ 'provider_capability_matrix', 'payload_sdk_schema_validation', 'asset_dependency_order',
42
+ ],
43
+ third_party: ['client_family_context_review', 'app_action_design_templates'],
44
+ };
45
+ function read(file) {
46
+ return readMigrationJson(file);
47
+ }
48
+ function digest(file) {
49
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
50
+ }
51
+ function text(value) {
52
+ return typeof value === 'string' && value.trim().length > 0;
53
+ }
54
+ function recordComplete(record) {
55
+ const errors = [];
56
+ if (record.status !== 'passed')
57
+ errors.push('status must be passed');
58
+ if (!record.evidence?.length || !record.evidence.every(text))
59
+ errors.push('evidence is required');
60
+ if (!record.tests?.length || !record.tests.every(text))
61
+ errors.push('tests are required');
62
+ if (!record.sources?.length)
63
+ errors.push('source citations are required');
64
+ if (!record.automation || !text(record.automation.tool) || !text(record.automation.command) ||
65
+ !text(record.automation.result) || !text(record.automation.environment)) {
66
+ errors.push('automation tool, command, result, and environment are required');
67
+ }
68
+ if (!record.details || typeof record.details !== 'object' || Array.isArray(record.details) ||
69
+ Object.keys(record.details).length === 0) {
70
+ errors.push('structured details are required');
71
+ }
72
+ return errors;
73
+ }
74
+ function validateSpecial(record) {
75
+ const errors = [];
76
+ const details = record.details;
77
+ if (record.requirement === 'before_after_normalized') {
78
+ for (const field of ['before', 'after', 'normalization', 'comparison']) {
79
+ if (!text(details[field]))
80
+ errors.push(`details.${field} is required`);
81
+ }
82
+ }
83
+ if (record.requirement === 'nondeterminism_tolerances') {
84
+ if (!Array.isArray(details.fields) || !details.fields.length || !text(details.approved_differences)) {
85
+ errors.push('nondeterministic fields and approved differences are required');
86
+ }
87
+ }
88
+ if (record.requirement === 'dependency_ordered_partitions' ||
89
+ record.requirement === 'asset_dependency_order' ||
90
+ record.requirement === 'multiple_database_order' ||
91
+ record.requirement === 'event_feature_cdc_order') {
92
+ if (!Array.isArray(details.nodes) || !details.nodes.length ||
93
+ !Array.isArray(details.edges) || !Array.isArray(details.order) || !details.order.length) {
94
+ errors.push('dependency nodes, edges, and order are required');
95
+ }
96
+ }
97
+ if (record.requirement === 'token_byte_estimates') {
98
+ if (!Number.isFinite(details.max_tokens) || Number(details.max_tokens) <= 0 ||
99
+ !Number.isFinite(details.max_bytes) || Number(details.max_bytes) <= 0) {
100
+ errors.push('positive token and byte limits are required');
101
+ }
102
+ }
103
+ if (record.requirement === 'cited_service_summaries' && record.sources.length < 2) {
104
+ errors.push('service summaries require at least two cited source ranges');
105
+ }
106
+ if (record.requirement === 'irreversible_approval' ||
107
+ record.requirement === 'visual_baseline_approval' ||
108
+ record.requirement === 'residual_uncertainty_decision') {
109
+ if (!record.approval || !text(record.approval.actor) ||
110
+ !text(record.approval.authority) || !text(record.approval.evidence)) {
111
+ errors.push('trusted approval evidence is required');
112
+ }
113
+ }
114
+ return errors;
115
+ }
116
+ export function validateMigrationVerification(file) {
117
+ const resolved = path.resolve(file);
118
+ const matrix = read(resolved);
119
+ if (matrix.artifact_kind !== 'ductape_migration_verification_matrix' || matrix.version !== 1) {
120
+ throw new Error('Unsupported migration verification matrix.');
121
+ }
122
+ const blockers = [];
123
+ const invalid = new Set();
124
+ const recordsById = new Map();
125
+ for (const [category, requirements] of Object.entries(REQUIRED)) {
126
+ const records = matrix.categories?.[category];
127
+ if (!Array.isArray(records)) {
128
+ blockers.push(`${category}: category is missing`);
129
+ continue;
130
+ }
131
+ const byRequirement = new Map(records.map((record) => [record.requirement, record]));
132
+ for (const requirement of requirements) {
133
+ const record = byRequirement.get(requirement);
134
+ if (!record) {
135
+ blockers.push(`${category}.${requirement}: verification record is missing`);
136
+ continue;
137
+ }
138
+ const recordId = `${category}.${requirement}`;
139
+ recordsById.set(recordId, record);
140
+ for (const error of [...recordComplete(record), ...validateSpecial(record)]) {
141
+ blockers.push(`${category}.${requirement}: ${error}`);
142
+ invalid.add(recordId);
143
+ }
144
+ for (const [index, source] of (record.sources ?? []).entries()) {
145
+ if (!path.isAbsolute(source.repository) || path.isAbsolute(source.file) ||
146
+ !Number.isInteger(source.start_line) || !Number.isInteger(source.end_line) ||
147
+ source.start_line < 1 || source.end_line < source.start_line ||
148
+ !/^[a-f0-9]{64}$/.test(source.checksum)) {
149
+ blockers.push(`${category}.${requirement}.sources[${index}]: invalid citation`);
150
+ continue;
151
+ }
152
+ const sourceFile = path.resolve(source.repository, source.file);
153
+ if (!fs.existsSync(sourceFile) || digest(sourceFile) !== source.checksum) {
154
+ blockers.push(`${category}.${requirement}.sources[${index}]: source is stale or missing`);
155
+ invalid.add(recordId);
156
+ }
157
+ }
158
+ }
159
+ const duplicates = records.map((record) => record.requirement)
160
+ .filter((requirement, index, all) => all.indexOf(requirement) !== index);
161
+ if (duplicates.length)
162
+ blockers.push(`${category}: duplicate requirements: ${[...new Set(duplicates)].join(', ')}`);
163
+ }
164
+ let expanded = true;
165
+ while (expanded) {
166
+ expanded = false;
167
+ for (const [recordId, record] of recordsById) {
168
+ if (invalid.has(recordId))
169
+ continue;
170
+ const dependencies = Array.isArray(record.details?.depends_on)
171
+ ? record.details.depends_on.filter((value) => typeof value === 'string')
172
+ : [];
173
+ if (dependencies.some((dependency) => invalid.has(dependency))) {
174
+ invalid.add(recordId);
175
+ blockers.push(`${recordId}: invalidated by stale dependency`);
176
+ expanded = true;
177
+ }
178
+ }
179
+ }
180
+ return {
181
+ file: resolved,
182
+ ready: blockers.length === 0,
183
+ categories: Object.keys(REQUIRED).length,
184
+ requirements: Object.values(REQUIRED).flat().length,
185
+ blockers,
186
+ };
187
+ }
188
+ export function runMigrationVerificationValidate(opts) {
189
+ const result = validateMigrationVerification(opts.file);
190
+ printJson(result, Boolean(opts.json));
191
+ if (opts.strict && !result.ready) {
192
+ throw new Error('Migration verification matrix is not ready: resolve every reported blocker.');
193
+ }
194
+ }
@@ -22,5 +22,6 @@ export declare function runProductEnvironments(verb: string, opts: {
22
22
  product?: string;
23
23
  tag?: string;
24
24
  slug?: string;
25
+ file?: string;
25
26
  json?: boolean;
26
27
  }, extraArgs: string[]): Promise<void>;
@@ -160,7 +160,7 @@ export async function runProductComponents(verb, opts) {
160
160
  }
161
161
  export async function runProductEnvironments(verb, opts, extraArgs) {
162
162
  const v = verb.toLowerCase();
163
- const allowed = ['list', 'get', 'fetch'];
163
+ const allowed = ['list', 'get', 'fetch', 'create', 'update'];
164
164
  if (!allowed.includes(v)) {
165
165
  throw new Error(`Unknown environments verb "${verb}". Use: ${allowed.join(', ')}`);
166
166
  }
@@ -174,6 +174,38 @@ export async function runProductEnvironments(verb, opts, extraArgs) {
174
174
  printJson(result, Boolean(opts.json));
175
175
  return;
176
176
  }
177
+ if (v === 'create' || v === 'update') {
178
+ if (!opts.file)
179
+ throw new Error(`${v} requires --file <environment.json>`);
180
+ const body = JSON.parse((await import('node:fs')).readFileSync(opts.file, 'utf8'));
181
+ if (v === 'create') {
182
+ for (const field of ['env_name', 'description', 'slug']) {
183
+ if (typeof body[field] !== 'string' || !body[field].trim()) {
184
+ throw new Error(`Environment create requires non-empty ${field}.`);
185
+ }
186
+ }
187
+ const existing = await proxy.execute('product', 'environments.fetch', [productTag, body.slug]);
188
+ if (existing) {
189
+ printJson({ product: productTag, slug: body.slug, created: false, existing }, Boolean(opts.json));
190
+ return;
191
+ }
192
+ await proxy.execute('product', 'environments.create', [productTag, body]);
193
+ const verified = await proxy.execute('product', 'environments.fetch', [productTag, body.slug]);
194
+ if (!verified)
195
+ throw new Error(`Environment "${body.slug}" was not found after creation.`);
196
+ printJson({ product: productTag, slug: body.slug, created: true, environment: verified }, Boolean(opts.json));
197
+ return;
198
+ }
199
+ const updateSlug = opts.slug ?? opts.tag ?? extraArgs[1];
200
+ if (!updateSlug)
201
+ throw new Error('Environment update requires --slug <slug>.');
202
+ await proxy.execute('product', 'environments.update', [productTag, updateSlug, body]);
203
+ const verified = await proxy.execute('product', 'environments.fetch', [productTag, body.slug ?? updateSlug]);
204
+ if (!verified)
205
+ throw new Error(`Environment "${updateSlug}" was not found after update.`);
206
+ printJson({ product: productTag, slug: body.slug ?? updateSlug, updated: true, environment: verified }, Boolean(opts.json));
207
+ return;
208
+ }
177
209
  const slug = opts.slug ?? opts.tag ?? extraArgs[1] ?? extraArgs[0];
178
210
  if (!slug)
179
211
  throw new Error('Provide the environment slug as the second argument or --slug <slug>');
@@ -4,3 +4,11 @@ export declare function runSecrets(verb: string, opts: {
4
4
  file?: string;
5
5
  json?: boolean;
6
6
  } & CommandInteractiveFlags): Promise<void>;
7
+ export declare function runSecretImportEnv(opts: {
8
+ envFile: string;
9
+ sourceKey: string;
10
+ key?: string;
11
+ env?: string;
12
+ description?: string;
13
+ json?: boolean;
14
+ }): Promise<void>;
@@ -4,6 +4,8 @@ import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
4
4
  import { buildCrudParams, proxyMethodForVerb } from '../lib/resources.js';
5
5
  import { getSdkProxy, requireSession } from '../lib/proxy/context.js';
6
6
  import { printJson } from '../lib/output.js';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
7
9
  const VERBS = ['create', 'list', 'get', 'update', 'delete'];
8
10
  export async function runSecrets(verb, opts) {
9
11
  const crud = verb.toLowerCase();
@@ -36,3 +38,45 @@ export async function runSecrets(verb, opts) {
36
38
  const result = await getSdkProxy(session).execute('secrets', method, params);
37
39
  printJson(result, Boolean(opts.json));
38
40
  }
41
+ function readEnvValue(file, sourceKey) {
42
+ const resolved = path.resolve(file);
43
+ const line = fs.readFileSync(resolved, 'utf8')
44
+ .split(/\r?\n/)
45
+ .find((candidate) => new RegExp(`^\\s*(?:export\\s+)?${sourceKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`).test(candidate));
46
+ if (!line)
47
+ throw new Error(`Environment key "${sourceKey}" was not found in ${resolved}`);
48
+ let value = line.slice(line.indexOf('=') + 1).trim();
49
+ if ((value.startsWith('"') && value.endsWith('"')) ||
50
+ (value.startsWith("'") && value.endsWith("'")))
51
+ value = value.slice(1, -1);
52
+ if (!value)
53
+ throw new Error(`Environment key "${sourceKey}" is empty in ${resolved}`);
54
+ return value.replace(/\\n/g, '\n');
55
+ }
56
+ function redactSecretResult(value) {
57
+ if (Array.isArray(value))
58
+ return value.map(redactSecretResult);
59
+ if (!value || typeof value !== 'object')
60
+ return value;
61
+ const output = {};
62
+ for (const [key, entry] of Object.entries(value)) {
63
+ output[key] = /^(?:value|encrypted_value|private_key)$/i.test(key)
64
+ ? '[REDACTED]'
65
+ : redactSecretResult(entry);
66
+ }
67
+ return output;
68
+ }
69
+ export async function runSecretImportEnv(opts) {
70
+ const session = requireSession();
71
+ const key = opts.key ?? opts.sourceKey;
72
+ const value = readEnvValue(opts.envFile, opts.sourceKey);
73
+ const body = {
74
+ key,
75
+ value,
76
+ description: opts.description ?? `Imported locally from ${path.basename(opts.envFile)}:${opts.sourceKey}`,
77
+ ...(opts.env ? { envs: [opts.env] } : {}),
78
+ };
79
+ const params = buildCrudParams('secrets', 'create', session.project.product_tag, { tag: key, body });
80
+ const result = await getSdkProxy(session).execute('secrets', 'create', params);
81
+ printJson(redactSecretResult(result), Boolean(opts.json));
82
+ }