@ductape/cli 0.2.22 → 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 (103) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +7 -0
  3. package/dist/commands/generate.js +1 -1
  4. package/dist/commands/migrate-codebase.d.ts +176 -0
  5. package/dist/commands/migrate-codebase.js +753 -0
  6. package/dist/commands/migration-artifact.d.ts +32 -0
  7. package/dist/commands/migration-artifact.js +48 -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-database.d.ts +21 -0
  11. package/dist/commands/migration-database.js +162 -0
  12. package/dist/commands/migration-e2e.d.ts +68 -0
  13. package/dist/commands/migration-e2e.js +189 -0
  14. package/dist/commands/migration-environments.d.ts +20 -0
  15. package/dist/commands/migration-environments.js +58 -0
  16. package/dist/commands/migration-generated.d.ts +68 -0
  17. package/dist/commands/migration-generated.js +217 -0
  18. package/dist/commands/migration-portfolio.d.ts +88 -0
  19. package/dist/commands/migration-portfolio.js +223 -0
  20. package/dist/commands/migration-products.d.ts +23 -0
  21. package/dist/commands/migration-products.js +88 -0
  22. package/dist/commands/migration-review.d.ts +248 -0
  23. package/dist/commands/migration-review.js +624 -0
  24. package/dist/commands/migration-secrets.d.ts +24 -0
  25. package/dist/commands/migration-secrets.js +93 -0
  26. package/dist/commands/migration-slice.d.ts +132 -0
  27. package/dist/commands/migration-slice.js +266 -0
  28. package/dist/commands/migration-verification.d.ts +12 -0
  29. package/dist/commands/migration-verification.js +194 -0
  30. package/dist/commands/products.d.ts +1 -0
  31. package/dist/commands/products.js +33 -1
  32. package/dist/commands/secrets.d.ts +8 -0
  33. package/dist/commands/secrets.js +44 -0
  34. package/dist/index.js +354 -2
  35. package/dist/lib/integrations.js +6 -1
  36. package/dist/lib/migration-artifact.d.ts +13 -0
  37. package/dist/lib/migration-artifact.js +103 -0
  38. package/package.json +8 -1
  39. package/API_PARITY.md +0 -58
  40. package/DB_MIGRATE_PLAN.md +0 -223
  41. package/ductape.example.ts +0 -18
  42. package/src/commands/apply.ts +0 -153
  43. package/src/commands/apps-import.ts +0 -43
  44. package/src/commands/apps.ts +0 -92
  45. package/src/commands/cloud.ts +0 -68
  46. package/src/commands/completion.ts +0 -119
  47. package/src/commands/db-migrate.ts +0 -209
  48. package/src/commands/db-schema.ts +0 -429
  49. package/src/commands/db.ts +0 -63
  50. package/src/commands/generate.ts +0 -74
  51. package/src/commands/graph.ts +0 -51
  52. package/src/commands/init.ts +0 -39
  53. package/src/commands/install.ts +0 -76
  54. package/src/commands/link.ts +0 -84
  55. package/src/commands/login.ts +0 -145
  56. package/src/commands/logout.ts +0 -7
  57. package/src/commands/platform.ts +0 -123
  58. package/src/commands/products.ts +0 -228
  59. package/src/commands/profiles.ts +0 -23
  60. package/src/commands/resources.ts +0 -213
  61. package/src/commands/secrets.ts +0 -46
  62. package/src/commands/unlink.ts +0 -15
  63. package/src/commands/whoami.ts +0 -27
  64. package/src/commands/workspaces.ts +0 -98
  65. package/src/index.ts +0 -523
  66. package/src/lib/api-client.ts +0 -88
  67. package/src/lib/app-import.ts +0 -82
  68. package/src/lib/apply-loaders.ts +0 -30
  69. package/src/lib/config.ts +0 -230
  70. package/src/lib/context-store.ts +0 -77
  71. package/src/lib/db-methods.ts +0 -56
  72. package/src/lib/db-types.ts +0 -104
  73. package/src/lib/encryption.ts +0 -12
  74. package/src/lib/forms/enums.ts +0 -52
  75. package/src/lib/forms/index.ts +0 -8
  76. package/src/lib/forms/prompt.ts +0 -209
  77. package/src/lib/forms/schemas.ts +0 -288
  78. package/src/lib/forms/types.ts +0 -27
  79. package/src/lib/http.ts +0 -76
  80. package/src/lib/integrations.ts +0 -105
  81. package/src/lib/interactive-opts.ts +0 -13
  82. package/src/lib/migration-files.ts +0 -58
  83. package/src/lib/output.ts +0 -39
  84. package/src/lib/platform-api.ts +0 -209
  85. package/src/lib/proxy/context.ts +0 -94
  86. package/src/lib/proxy/db-proxy.ts +0 -40
  87. package/src/lib/proxy/graph-proxy.ts +0 -64
  88. package/src/lib/proxy/sdk-proxy.ts +0 -58
  89. package/src/lib/read-body.ts +0 -57
  90. package/src/lib/resources.ts +0 -160
  91. package/src/lib/schema-loader.ts +0 -152
  92. package/src/lib/snippets.ts +0 -165
  93. package/src/lib/templates.ts +0 -157
  94. package/src/lib/workspace-context.ts +0 -43
  95. package/src/lib/workspaces.ts +0 -283
  96. package/test/apply-loaders.test.ts +0 -40
  97. package/test/encryption.test.ts +0 -20
  98. package/test/forms.test.ts +0 -25
  99. package/test/http.test.ts +0 -15
  100. package/test/platform-api.test.ts +0 -42
  101. package/test/resources.test.ts +0 -23
  102. package/test/workspace-resolve.test.ts +0 -29
  103. package/tsconfig.json +0 -15
@@ -1,209 +0,0 @@
1
- import input from '@inquirer/input';
2
- import select from '@inquirer/select';
3
- import { MESSAGE_BROKER_TYPES, STORAGE_PROVIDERS } from './enums.js';
4
- import type { EnvsVariant, FormField, FormSchema } from './types.js';
5
-
6
- export type InteractiveOpts = {
7
- interactive?: boolean;
8
- noInteractive?: boolean;
9
- };
10
-
11
- export function isInteractive(opts?: InteractiveOpts): boolean {
12
- if (opts?.noInteractive) return false;
13
- if (opts?.interactive === true) return true;
14
- if (opts?.interactive === false) return false;
15
- return Boolean(process.stdin.isTTY && !process.env.CI && process.env.DUCTAPE_NO_INTERACTIVE !== '1');
16
- }
17
-
18
- async function askYesNo(message: string, defaultYes = true): Promise<boolean> {
19
- return select({
20
- message,
21
- choices: [
22
- { name: 'Yes', value: true },
23
- { name: 'No', value: false },
24
- ],
25
- default: defaultYes,
26
- });
27
- }
28
-
29
- async function promptJsonObject(message: string, required = true): Promise<Record<string, unknown>> {
30
- const raw = await input({
31
- message: `${message} (JSON object${required ? '' : ', optional'})`,
32
- });
33
- if (!raw.trim()) {
34
- if (required) throw new Error('JSON object is required');
35
- return {};
36
- }
37
- const parsed = JSON.parse(raw) as unknown;
38
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
39
- throw new Error('Expected a JSON object');
40
- }
41
- return parsed as Record<string, unknown>;
42
- }
43
-
44
- async function promptStorageConfig(provider: string): Promise<Record<string, unknown>> {
45
- if (provider === 'aws') {
46
- return {
47
- bucketName: await input({ message: 'S3 bucket name' }),
48
- accessKeyId: await input({ message: 'AWS access key ID' }),
49
- secretAccessKey: await input({ message: 'AWS secret access key' }),
50
- region: await input({ message: 'AWS region' }),
51
- };
52
- }
53
- if (provider === 'azure') {
54
- return {
55
- containerName: await input({ message: 'Container name' }),
56
- connectionString: await input({ message: 'Connection string' }),
57
- };
58
- }
59
- if (provider === 'gcp') {
60
- return {
61
- bucketName: await input({ message: 'GCS bucket name' }),
62
- config: await promptJsonObject('GCP service account config'),
63
- };
64
- }
65
- return promptJsonObject(`Config for ${provider}`);
66
- }
67
-
68
- async function promptEnvs(variant: EnvsVariant, context?: { vectorType?: string }): Promise<unknown[]> {
69
- const envs: unknown[] = [];
70
- let addMore = true;
71
- while (addMore) {
72
- const slug = await input({
73
- message: `Environment slug #${envs.length + 1} (3 letters)`,
74
- validate: (v) => (v.length >= 1 && v.length <= 3 ? true : 'Use 1–3 characters'),
75
- });
76
-
77
- if (variant === 'database' || variant === 'graph') {
78
- const connection_url = await input({
79
- message: 'Connection URL',
80
- });
81
- const description = await input({ message: 'Description (optional)', default: '' });
82
- const row: Record<string, unknown> = { slug, connection_url };
83
- if (description.trim()) row.description = description.trim();
84
- envs.push(row);
85
- } else if (variant === 'storage') {
86
- const type = await select({
87
- message: 'Storage provider',
88
- choices: STORAGE_PROVIDERS.map((p) => ({ name: p, value: p })),
89
- });
90
- const config = await promptStorageConfig(type);
91
- envs.push({ slug, type, config });
92
- } else if (variant === 'broker') {
93
- const type = await select({
94
- message: 'Broker type',
95
- choices: MESSAGE_BROKER_TYPES.map((p) => ({ name: p, value: p })),
96
- });
97
- const config = await promptJsonObject('Broker config');
98
- envs.push({ slug, type, config });
99
- } else if (variant === 'vector') {
100
- const vectorType = context?.vectorType ?? 'pinecone';
101
- const row: Record<string, unknown> = { slug };
102
- if (vectorType === 'memory') {
103
- const index = await input({ message: 'Index name (optional)', default: '' });
104
- if (index.trim()) row.index = index.trim();
105
- } else {
106
- row.endpoint = await input({ message: 'Endpoint URL' });
107
- const apiKey = await input({ message: 'API key (optional)', default: '' });
108
- const index = await input({ message: 'Index / collection (optional)', default: '' });
109
- if (apiKey.trim()) row.apiKey = apiKey.trim();
110
- if (index.trim()) row.index = index.trim();
111
- }
112
- envs.push(row);
113
- } else {
114
- envs.push({ slug });
115
- }
116
-
117
- addMore = await askYesNo('Add another environment?', false);
118
- }
119
- return envs;
120
- }
121
-
122
- async function promptField(field: FormField, patch: boolean): Promise<unknown> {
123
- const required = patch ? field.required : field.required !== false;
124
-
125
- if (field.kind === 'enum' && field.choices?.length) {
126
- const value = await select({
127
- message: field.label,
128
- choices: field.choices.map((c) => ({ name: c, value: c })),
129
- default: field.default as string | undefined,
130
- });
131
- return value;
132
- }
133
-
134
- if (field.kind === 'number') {
135
- const raw = await input({
136
- message: field.label,
137
- default: field.default !== undefined ? String(field.default) : undefined,
138
- });
139
- if (!raw.trim() && !required) return undefined;
140
- const n = Number(raw);
141
- if (Number.isNaN(n)) throw new Error(`${field.label} must be a number`);
142
- return n;
143
- }
144
-
145
- if (field.kind === 'envs') {
146
- if (!field.envsVariant) throw new Error('envs field missing envsVariant');
147
- return promptEnvs(field.envsVariant);
148
- }
149
-
150
- const value = await input({
151
- message: field.label,
152
- default: field.default !== undefined ? String(field.default) : undefined,
153
- });
154
- if (!value.trim() && !required) return undefined;
155
- if (!value.trim() && required) throw new Error(`${field.label} is required`);
156
- return value.trim();
157
- }
158
-
159
- export async function promptForm(schema: FormSchema, opts?: { patch?: boolean }): Promise<Record<string, unknown>> {
160
- const patch = Boolean(opts?.patch);
161
- const body: Record<string, unknown> = {};
162
- let vectorType: string | undefined;
163
-
164
- for (const field of schema.fields) {
165
- if (patch && !field.required) {
166
- const set = await askYesNo(`Set ${field.label}?`, false);
167
- if (!set) continue;
168
- }
169
-
170
- if (field.key === 'type' && field.kind === 'enum') {
171
- const value = await promptField(field, patch);
172
- if (value !== undefined) {
173
- body.type = value;
174
- vectorType = String(value);
175
- }
176
- continue;
177
- }
178
-
179
- if (field.kind === 'envs' && field.envsVariant === 'vector') {
180
- const envs = await promptEnvs('vector', { vectorType });
181
- if (envs.length) body.envs = envs;
182
- continue;
183
- }
184
-
185
- const value = await promptField(field, patch);
186
- if (value !== undefined && value !== '') {
187
- body[field.key] = value;
188
- }
189
- }
190
-
191
- if (schema.allowExtraJson) {
192
- const useExtra = await askYesNo('Add extra fields as JSON?', false);
193
- if (useExtra) {
194
- Object.assign(body, await promptJsonObject('Extra fields', false));
195
- }
196
- }
197
-
198
- return body;
199
- }
200
-
201
- export async function promptTag(label = 'Tag'): Promise<string> {
202
- const value = await input({ message: label });
203
- if (!value.trim()) throw new Error(`${label} is required`);
204
- return value.trim();
205
- }
206
-
207
- export async function promptKey(label = 'Secret key'): Promise<string> {
208
- return promptTag(label);
209
- }
@@ -1,288 +0,0 @@
1
- import type { SDKModule } from '../proxy/sdk-proxy.js';
2
- import type { CrudVerb } from '../resources.js';
3
- import {
4
- DATABASE_TYPES,
5
- GRAPH_TYPES,
6
- HTTP_METHODS,
7
- JOB_EVENT_TYPES,
8
- MESSAGE_BROKER_TYPES,
9
- NOTIFIERS,
10
- STORAGE_PROVIDERS,
11
- VECTOR_DB_TYPES,
12
- VECTOR_DISTANCE_METRICS,
13
- } from './enums.js';
14
- import type { FormField, FormSchema } from './types.js';
15
-
16
- const TAG: FormField = { key: 'tag', label: 'Tag (no spaces)', kind: 'text', required: true };
17
- const NAME: FormField = { key: 'name', label: 'Name', kind: 'text', required: true };
18
- const DESC: FormField = { key: 'description', label: 'Description', kind: 'text' };
19
-
20
- function fields(...items: FormField[]): FormField[] {
21
- return items;
22
- }
23
-
24
- function optional(fields: FormField[]): FormField[] {
25
- return fields.map((f) => ({ ...f, required: false }));
26
- }
27
-
28
- const PRODUCT_CREATE: FormSchema = {
29
- fields: fields(
30
- { key: 'name', label: 'Product name', kind: 'text', required: true },
31
- { key: 'description', label: 'Description', kind: 'text', required: true },
32
- TAG,
33
- ),
34
- };
35
-
36
- const APP_CREATE: FormSchema = {
37
- fields: fields(
38
- { key: 'app_name', label: 'App name', kind: 'text', required: true },
39
- { key: 'description', label: 'Description', kind: 'text', required: true },
40
- TAG,
41
- ),
42
- };
43
-
44
- const COMPONENT_BASE: FormField[] = [NAME, DESC, TAG];
45
-
46
- const SCHEMAS: Record<string, FormSchema> = {
47
- 'products:create': PRODUCT_CREATE,
48
- 'products:update': { fields: optional(PRODUCT_CREATE.fields) },
49
- 'apps:create': APP_CREATE,
50
- 'apps:update': {
51
- fields: optional([
52
- { key: 'app_name', label: 'App name', kind: 'text' },
53
- { key: 'description', label: 'Description', kind: 'text' },
54
- TAG,
55
- ]),
56
- },
57
- 'storage:create': {
58
- fields: [
59
- ...COMPONENT_BASE,
60
- { key: 'envs', label: 'Environments', kind: 'envs', required: true, envsVariant: 'storage' },
61
- ],
62
- },
63
- 'storage:update': {
64
- fields: [
65
- ...optional(COMPONENT_BASE),
66
- { key: 'envs', label: 'Environments', kind: 'envs', envsVariant: 'storage' },
67
- ],
68
- allowExtraJson: true,
69
- },
70
- 'databases:create': {
71
- fields: [
72
- ...COMPONENT_BASE,
73
- {
74
- key: 'type',
75
- label: 'Database type',
76
- kind: 'enum',
77
- required: true,
78
- choices: DATABASE_TYPES,
79
- },
80
- { key: 'envs', label: 'Environments', kind: 'envs', required: true, envsVariant: 'database' },
81
- ],
82
- },
83
- 'databases:update': {
84
- fields: [
85
- ...optional(COMPONENT_BASE),
86
- { key: 'type', label: 'Database type', kind: 'enum', choices: DATABASE_TYPES },
87
- { key: 'envs', label: 'Environments', kind: 'envs', envsVariant: 'database' },
88
- ],
89
- allowExtraJson: true,
90
- },
91
- 'databases:connect': { fields: [TAG] },
92
- 'graph:create': {
93
- fields: [
94
- ...COMPONENT_BASE,
95
- { key: 'type', label: 'Graph type', kind: 'enum', required: true, choices: GRAPH_TYPES },
96
- { key: 'envs', label: 'Environments', kind: 'envs', required: true, envsVariant: 'graph' },
97
- ],
98
- },
99
- 'graph:update': {
100
- fields: [
101
- ...optional(COMPONENT_BASE),
102
- { key: 'type', label: 'Graph type', kind: 'enum', choices: GRAPH_TYPES },
103
- { key: 'envs', label: 'Environments', kind: 'envs', envsVariant: 'graph' },
104
- ],
105
- allowExtraJson: true,
106
- },
107
- 'graph:connect': { fields: [TAG] },
108
- 'caches:create': {
109
- fields: [
110
- ...COMPONENT_BASE,
111
- { key: 'expiry', label: 'Expiry (seconds)', kind: 'number', required: true },
112
- ],
113
- },
114
- 'caches:update': {
115
- fields: optional([...COMPONENT_BASE, { key: 'expiry', label: 'Expiry (seconds)', kind: 'number' }]),
116
- allowExtraJson: true,
117
- },
118
- 'jobs:create': {
119
- fields: [
120
- TAG,
121
- NAME,
122
- { key: 'description', label: 'Description', kind: 'text', required: true },
123
- { key: 'type', label: 'Job type', kind: 'enum', choices: JOB_EVENT_TYPES },
124
- { key: 'event', label: 'Event tag', kind: 'text', required: true },
125
- { key: 'executions', label: 'Max executions', kind: 'number', required: true },
126
- { key: 'intervals', label: 'Interval (seconds)', kind: 'number', required: true },
127
- { key: 'app', label: 'App tag (required for action jobs)', kind: 'text' },
128
- ],
129
- allowExtraJson: true,
130
- },
131
- 'jobs:update': {
132
- fields: optional([
133
- TAG,
134
- NAME,
135
- { key: 'description', label: 'Description', kind: 'text' },
136
- { key: 'type', label: 'Job type', kind: 'enum', choices: JOB_EVENT_TYPES },
137
- { key: 'event', label: 'Event tag', kind: 'text' },
138
- { key: 'executions', label: 'Max executions', kind: 'number' },
139
- { key: 'intervals', label: 'Interval (seconds)', kind: 'number' },
140
- { key: 'app', label: 'App tag', kind: 'text' },
141
- ]),
142
- allowExtraJson: true,
143
- },
144
- 'vector:create': {
145
- fields: [
146
- ...COMPONENT_BASE,
147
- {
148
- key: 'type',
149
- label: 'Vector DB type',
150
- kind: 'enum',
151
- required: true,
152
- choices: VECTOR_DB_TYPES,
153
- },
154
- { key: 'dimensions', label: 'Dimensions', kind: 'number', required: true },
155
- {
156
- key: 'metric',
157
- label: 'Distance metric',
158
- kind: 'enum',
159
- choices: VECTOR_DISTANCE_METRICS,
160
- default: 'cosine',
161
- },
162
- { key: 'envs', label: 'Environments', kind: 'envs', required: true, envsVariant: 'vector' },
163
- ],
164
- },
165
- 'vector:update': {
166
- fields: [
167
- ...optional(COMPONENT_BASE),
168
- { key: 'type', label: 'Vector DB type', kind: 'enum', choices: VECTOR_DB_TYPES },
169
- { key: 'dimensions', label: 'Dimensions', kind: 'number' },
170
- { key: 'metric', label: 'Distance metric', kind: 'enum', choices: VECTOR_DISTANCE_METRICS },
171
- { key: 'envs', label: 'Environments', kind: 'envs', envsVariant: 'vector' },
172
- ],
173
- allowExtraJson: true,
174
- },
175
- 'vector:connect': { fields: [TAG] },
176
- 'messageBrokers:create': {
177
- fields: [
178
- ...COMPONENT_BASE,
179
- { key: 'envs', label: 'Environments', kind: 'envs', required: true, envsVariant: 'broker' },
180
- ],
181
- },
182
- 'messageBrokers:update': {
183
- fields: [
184
- ...optional(COMPONENT_BASE),
185
- { key: 'envs', label: 'Environments', kind: 'envs', envsVariant: 'broker' },
186
- ],
187
- allowExtraJson: true,
188
- },
189
- 'notifications:create': {
190
- fields: [
191
- ...COMPONENT_BASE,
192
- { key: 'notifier', label: 'Notifier', kind: 'enum', required: true, choices: NOTIFIERS },
193
- ],
194
- allowExtraJson: true,
195
- },
196
- 'notifications:update': {
197
- fields: optional([
198
- ...COMPONENT_BASE,
199
- { key: 'notifier', label: 'Notifier', kind: 'enum', choices: NOTIFIERS },
200
- ]),
201
- allowExtraJson: true,
202
- },
203
- 'actions:create': {
204
- fields: [
205
- TAG,
206
- NAME,
207
- DESC,
208
- { key: 'method', label: 'HTTP method', kind: 'enum', choices: HTTP_METHODS },
209
- ],
210
- allowExtraJson: true,
211
- },
212
- 'actions:update': {
213
- fields: optional([
214
- TAG,
215
- NAME,
216
- DESC,
217
- { key: 'method', label: 'HTTP method', kind: 'enum', choices: HTTP_METHODS },
218
- ]),
219
- allowExtraJson: true,
220
- },
221
- 'features:create': {
222
- fields: [...COMPONENT_BASE],
223
- allowExtraJson: true,
224
- },
225
- 'features:update': { fields: optional(COMPONENT_BASE), allowExtraJson: true },
226
- 'secrets:create': {
227
- fields: [
228
- { key: 'key', label: 'Secret key', kind: 'text', required: true },
229
- { key: 'value', label: 'Secret value', kind: 'text', required: true },
230
- DESC,
231
- ],
232
- },
233
- 'secrets:update': {
234
- fields: optional([
235
- { key: 'value', label: 'Secret value', kind: 'text' },
236
- DESC,
237
- ]),
238
- },
239
- 'quotas:create': { fields: [...COMPONENT_BASE], allowExtraJson: true },
240
- 'quotas:update': { fields: optional(COMPONENT_BASE), allowExtraJson: true },
241
- 'health:create': { fields: [...COMPONENT_BASE], allowExtraJson: true },
242
- 'health:update': { fields: optional(COMPONENT_BASE), allowExtraJson: true },
243
- 'fallback:create': { fields: [...COMPONENT_BASE], allowExtraJson: true },
244
- 'fallback:update': { fields: optional(COMPONENT_BASE), allowExtraJson: true },
245
- 'sessions:create': { fields: [...COMPONENT_BASE], allowExtraJson: true },
246
- 'sessions:update': { fields: optional(COMPONENT_BASE), allowExtraJson: true },
247
- };
248
-
249
- const GENERIC_COMPONENT: FormSchema = {
250
- fields: [...COMPONENT_BASE],
251
- allowExtraJson: true,
252
- };
253
-
254
- const CONNECT_MODULES = new Set<SDKModule>(['databases', 'graph', 'vector', 'storage']);
255
-
256
- export function getFormSchema(schemaKey: string): FormSchema | undefined {
257
- return SCHEMAS[schemaKey] ?? undefined;
258
- }
259
-
260
- export function schemaKeyForResource(module: SDKModule, verb: CrudVerb): string | undefined {
261
- if (verb === 'connect' && CONNECT_MODULES.has(module)) {
262
- return `${module}:connect`;
263
- }
264
- if (verb === 'create' || verb === 'update') {
265
- return `${module}:${verb}`;
266
- }
267
- return undefined;
268
- }
269
-
270
- export function schemaKeyForEntity(entity: 'products' | 'apps', verb: string): string | undefined {
271
- if (verb === 'create' || verb === 'update') return `${entity}:${verb}`;
272
- return undefined;
273
- }
274
-
275
- /** Fallback schema for component types without a dedicated form */
276
- export function genericComponentSchema(verb: 'create' | 'update'): FormSchema {
277
- if (verb === 'create') return GENERIC_COMPONENT;
278
- return { fields: optional(COMPONENT_BASE), allowExtraJson: true };
279
- }
280
-
281
- export function resolveResourceSchema(
282
- module: SDKModule,
283
- verb: CrudVerb,
284
- ): FormSchema | undefined {
285
- const key = schemaKeyForResource(module, verb);
286
- if (!key) return undefined;
287
- return getFormSchema(key) ?? (verb === 'create' || verb === 'update' ? genericComponentSchema(verb) : undefined);
288
- }
@@ -1,27 +0,0 @@
1
- export type FieldKind = 'text' | 'number' | 'enum' | 'envs';
2
-
3
- export type EnvsVariant =
4
- | 'database'
5
- | 'graph'
6
- | 'storage'
7
- | 'broker'
8
- | 'vector'
9
- | 'simple';
10
-
11
- export interface FormField {
12
- key: string;
13
- label: string;
14
- kind: FieldKind;
15
- required?: boolean;
16
- default?: string | number;
17
- /** enum */
18
- choices?: readonly string[];
19
- /** envs */
20
- envsVariant?: EnvsVariant;
21
- }
22
-
23
- export interface FormSchema {
24
- fields: FormField[];
25
- /** Merge optional JSON object after prompts */
26
- allowExtraJson?: boolean;
27
- }
package/src/lib/http.ts DELETED
@@ -1,76 +0,0 @@
1
- import { gzipSync } from 'node:zlib';
2
-
3
- const GZIP_THRESHOLD = 200 * 1024;
4
-
5
- /** Parse fetch body as JSON; surface HTML/misconfigured api_url clearly. */
6
- export async function parseJsonResponse<T = Record<string, unknown>>(
7
- res: Response,
8
- url: string,
9
- ): Promise<T> {
10
- const text = await res.text();
11
- const contentType = res.headers.get('content-type') ?? '';
12
- if (!text.trim()) {
13
- if (!res.ok) throw new Error(`Empty response from ${url} (HTTP ${res.status})`);
14
- return {} as T;
15
- }
16
- try {
17
- return JSON.parse(text) as T;
18
- } catch {
19
- if (res.status === 504) {
20
- throw new Error(
21
- `Ductape proxy gateway timeout (HTTP 504) for ${url}. The operation did not return a ` +
22
- 'verifiable result; retry only after checking whether the asset was created.',
23
- );
24
- }
25
- const htmlHint = text.trimStart().startsWith('<')
26
- ? [
27
- '',
28
- 'The server returned HTML instead of JSON. Check your API profile:',
29
- ' ductape profiles list',
30
- ' ductape profiles use local → http://localhost:4311/api (run `ductape start` first)',
31
- ' ductape profiles use cloud → https://api.ductape.app/api',
32
- 'Then: ductape login --profile local',
33
- ].join('\n')
34
- : '';
35
- throw new Error(
36
- `Invalid JSON from ${url} (HTTP ${res.status}, Content-Type: ${contentType}): ${text.slice(0, 200)}${htmlHint}`,
37
- );
38
- }
39
- }
40
-
41
- export interface ProxyJsonResponse<T> {
42
- status?: boolean;
43
- data?: { data: T };
44
- message?: string;
45
- }
46
-
47
- export async function postProxyJson<T>(
48
- url: string,
49
- body: Record<string, unknown>,
50
- headers: Record<string, string>,
51
- ): Promise<T> {
52
- const json = JSON.stringify(body);
53
- const reqHeaders: Record<string, string> = { ...headers, 'Content-Type': 'application/json' };
54
- let bodyInit: BodyInit = json;
55
-
56
- if (json.length > GZIP_THRESHOLD) {
57
- const gz = gzipSync(json);
58
- bodyInit = new Uint8Array(gz);
59
- reqHeaders['Content-Encoding'] = 'gzip';
60
- }
61
-
62
- const res = await fetch(url, {
63
- method: 'POST',
64
- headers: reqHeaders,
65
- body: bodyInit,
66
- });
67
-
68
- const parsed = await parseJsonResponse<ProxyJsonResponse<T>>(res, url);
69
- if (!res.ok) {
70
- throw new Error(parsed.message ?? `Request failed: ${res.status}`);
71
- }
72
- if (typeof parsed.status === 'boolean' && !parsed.status) {
73
- throw new Error(parsed.message ?? 'Proxy operation failed');
74
- }
75
- return parsed.data?.data as T;
76
- }