@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
@@ -1,57 +0,0 @@
1
- import fs from 'node:fs';
2
- import {
3
- getFormSchema,
4
- isInteractive,
5
- promptForm,
6
- type InteractiveOpts,
7
- } from './forms/index.js';
8
- import type { FormSchema } from './forms/types.js';
9
-
10
- export function readJsonBody(filePath?: string): unknown {
11
- if (!filePath) return undefined;
12
- const raw = fs.readFileSync(filePath, 'utf8');
13
- return JSON.parse(raw) as unknown;
14
- }
15
-
16
- function asRecord(value: unknown): Record<string, unknown> | undefined {
17
- if (value === undefined) return undefined;
18
- if (value && typeof value === 'object' && !Array.isArray(value)) {
19
- return value as Record<string, unknown>;
20
- }
21
- throw new Error('Body must be a JSON object');
22
- }
23
-
24
- export async function resolveBody(
25
- opts: InteractiveOpts & {
26
- file?: string;
27
- schemaKey?: string;
28
- schema?: FormSchema;
29
- patch?: boolean;
30
- requireBody?: boolean;
31
- },
32
- ): Promise<Record<string, unknown> | undefined> {
33
- if (opts.file) {
34
- return asRecord(readJsonBody(opts.file));
35
- }
36
-
37
- const schema = opts.schema ?? (opts.schemaKey ? getFormSchema(opts.schemaKey) : undefined);
38
- if (schema && isInteractive(opts)) {
39
- return promptForm(schema, { patch: opts.patch ?? opts.schemaKey?.endsWith(':update') });
40
- }
41
-
42
- if (opts.requireBody && isInteractive(opts) === false) {
43
- return undefined;
44
- }
45
-
46
- if (opts.requireBody) {
47
- return undefined;
48
- }
49
-
50
- return undefined;
51
- }
52
-
53
- export function bodyRequiredHint(schemaKey?: string): string {
54
- const json = 'Pass -f <file.json> or run interactively (TTY)';
55
- if (schemaKey) return `${json} — schema: ${schemaKey}`;
56
- return json;
57
- }
@@ -1,160 +0,0 @@
1
- import type { SDKModule } from './proxy/sdk-proxy.js';
2
-
3
- /** CLI resource type → sdk-proxy module */
4
- export const RESOURCE_MODULES: Record<string, SDKModule> = {
5
- storage: 'storage',
6
- databases: 'databases',
7
- database: 'databases',
8
- graph: 'graph',
9
- graphs: 'graph',
10
- vector: 'vector',
11
- vectors: 'vector',
12
- cache: 'caches',
13
- caches: 'caches',
14
- job: 'jobs',
15
- jobs: 'jobs',
16
- feature: 'features',
17
- features: 'features',
18
- action: 'actions',
19
- actions: 'actions',
20
- notification: 'notifications',
21
- notifications: 'notifications',
22
- event: 'messageBrokers',
23
- events: 'messageBrokers',
24
- broker: 'messageBrokers',
25
- brokers: 'messageBrokers',
26
- 'message-brokers': 'messageBrokers',
27
- messagebrokers: 'messageBrokers',
28
- session: 'sessions',
29
- sessions: 'sessions',
30
- secret: 'secrets',
31
- secrets: 'secrets',
32
- quota: 'quotas',
33
- quotas: 'quotas',
34
- health: 'health',
35
- healthcheck: 'health',
36
- fallback: 'fallback',
37
- product: 'product',
38
- app: 'app',
39
- };
40
-
41
- export type CrudVerb = 'create' | 'list' | 'get' | 'update' | 'delete' | 'connect';
42
-
43
- export function resolveResourceType(name: string): SDKModule {
44
- const mod = RESOURCE_MODULES[name.toLowerCase()];
45
- if (!mod) {
46
- throw new Error(
47
- `Unknown resource type "${name}". Try: storage, databases, graph, product, app, … — or \`ductape products\` / \`ductape apps\` for workspace CRUD`,
48
- );
49
- }
50
- return mod;
51
- }
52
-
53
- export function proxyMethodForVerb(verb: CrudVerb): string {
54
- if (verb === 'get') return 'fetch';
55
- return verb;
56
- }
57
-
58
- /** Build params array matching Workbench sdkProxy.ts conventions */
59
- export function buildCrudParams(
60
- module: SDKModule,
61
- method: string,
62
- productTag: string,
63
- args: { tag?: string; body?: unknown },
64
- ): unknown[] {
65
- const { tag, body } = args;
66
-
67
- if (module === 'vector') {
68
- if (method === 'list') return [{ product: productTag }];
69
- if (method === 'fetch' || method === 'delete') {
70
- if (!tag) throw new Error('tag is required');
71
- return [{ product: productTag, tag }];
72
- }
73
- if (method === 'create' || method === 'update' || method === 'connect') {
74
- const payload =
75
- body && typeof body === 'object'
76
- ? { product: productTag, ...(body as object) }
77
- : { product: productTag };
78
- return [payload];
79
- }
80
- }
81
-
82
- if ((module === 'storage' || module === 'databases') && method === 'create') {
83
- // databases.create and storage.create expect a single config object with product embedded,
84
- // not the default (productTag, data) two-argument form.
85
- const payload =
86
- body && typeof body === 'object'
87
- ? { product: productTag, ...(body as object) }
88
- : { product: productTag };
89
- return [payload];
90
- }
91
-
92
- if (module === 'secrets') {
93
- if (method === 'list') return [];
94
- if (method === 'fetch' || method === 'delete') {
95
- if (!tag) throw new Error('key/tag is required');
96
- return method === 'delete' ? [tag] : [tag];
97
- }
98
- if (method === 'create') return [body ?? {}];
99
- if (method === 'update') {
100
- if (!tag) throw new Error('key is required');
101
- return [tag, body ?? {}];
102
- }
103
- }
104
-
105
- if (module === 'product' || module === 'app') {
106
- if (method === 'create') return [body ?? {}];
107
- if (method === 'fetch') {
108
- if (!tag) throw new Error('tag is required');
109
- return [tag];
110
- }
111
- if (method === 'update') {
112
- if (!tag) throw new Error('tag is required');
113
- return [tag, body ?? {}];
114
- }
115
- if (method === 'list') return [];
116
- }
117
-
118
- // Default product-scoped components
119
- if (method === 'create') return [productTag, body ?? {}];
120
- if (method === 'list') return [productTag];
121
- if (method === 'fetch' || method === 'delete') {
122
- if (!tag) throw new Error('tag is required');
123
- return [productTag, tag];
124
- }
125
- if (method === 'update') {
126
- if (!tag) throw new Error('tag is required');
127
- return [productTag, tag, body ?? {}];
128
- }
129
- if (method === 'connect') {
130
- const payload =
131
- body && typeof body === 'object'
132
- ? { product: productTag, ...(body as object) }
133
- : { product: productTag };
134
- return [payload];
135
- }
136
-
137
- throw new Error(`Unsupported operation ${module}.${method}`);
138
- }
139
-
140
- export function listResourceTypes(): string[] {
141
- return [
142
- 'storage',
143
- 'databases',
144
- 'graph',
145
- 'vector',
146
- 'caches',
147
- 'jobs',
148
- 'features',
149
- 'actions',
150
- 'notifications',
151
- 'events',
152
- 'sessions',
153
- 'secrets',
154
- 'quotas',
155
- 'health',
156
- 'fallback',
157
- 'product',
158
- 'app',
159
- ];
160
- }
@@ -1,152 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { PROJECT_CONFIG_DIR } from './config.js';
4
- import type { FieldType, IFieldDefinition } from './db-types.js';
5
-
6
- export interface MongooseFieldDef {
7
- type?: string | string[];
8
- required?: boolean;
9
- unique?: boolean;
10
- default?: unknown;
11
- maxlength?: number;
12
- enum?: string[];
13
- primaryKey?: boolean;
14
- autoGenerate?: boolean;
15
- float?: boolean;
16
- ref?: string;
17
- index?: boolean;
18
- }
19
-
20
- export interface SchemaEntry {
21
- db: string;
22
- tables: Record<string, Record<string, MongooseFieldDef>>;
23
- }
24
-
25
- export function getSchemaPath(projectDir: string): string {
26
- return path.join(projectDir, PROJECT_CONFIG_DIR, 'database', 'schema.json');
27
- }
28
-
29
- export function loadSchemaFile(projectDir: string): SchemaEntry[] {
30
- const schemaPath = getSchemaPath(projectDir);
31
- if (!fs.existsSync(schemaPath)) {
32
- throw new Error(
33
- `No schema.json found at ${schemaPath}. Run \`ductape init\` to scaffold it.`,
34
- );
35
- }
36
- const raw = JSON.parse(fs.readFileSync(schemaPath, 'utf8')) as unknown;
37
- if (!Array.isArray(raw)) {
38
- throw new Error('schema.json must be a JSON array of { db, tables } objects.');
39
- }
40
- return raw as SchemaEntry[];
41
- }
42
-
43
- const TYPE_MAP: Record<string, FieldType> = {
44
- string: 'string',
45
- String: 'string',
46
- text: 'text',
47
- Text: 'text',
48
- number: 'integer',
49
- Number: 'integer',
50
- integer: 'integer',
51
- Integer: 'integer',
52
- int: 'integer',
53
- Int: 'integer',
54
- boolean: 'boolean',
55
- Boolean: 'boolean',
56
- bool: 'boolean',
57
- Bool: 'boolean',
58
- date: 'datetime',
59
- Date: 'datetime',
60
- datetime: 'datetime',
61
- DateTime: 'datetime',
62
- timestamp: 'timestamp',
63
- Timestamp: 'timestamp',
64
- buffer: 'binary',
65
- Buffer: 'binary',
66
- binary: 'binary',
67
- Binary: 'binary',
68
- mixed: 'object',
69
- Mixed: 'object',
70
- object: 'object',
71
- Object: 'object',
72
- array: 'array',
73
- Array: 'array',
74
- objectid: 'uuid',
75
- ObjectId: 'uuid',
76
- uuid: 'uuid',
77
- UUID: 'uuid',
78
- decimal128: 'decimal',
79
- Decimal128: 'decimal',
80
- decimal: 'decimal',
81
- Decimal: 'decimal',
82
- map: 'object',
83
- Map: 'object',
84
- bigint: 'bigint',
85
- BigInt: 'bigint',
86
- float: 'float',
87
- Float: 'float',
88
- double: 'double',
89
- Double: 'double',
90
- json: 'json',
91
- JSON: 'json',
92
- blob: 'blob',
93
- Blob: 'blob',
94
- smallint: 'smallint',
95
- SmallInt: 'smallint',
96
- time: 'time',
97
- Time: 'time',
98
- };
99
-
100
- function resolveBaseType(raw: string | string[] | undefined, def: MongooseFieldDef): FieldType {
101
- const t = Array.isArray(raw) ? raw[0] : (raw ?? 'String');
102
- if (def.enum?.length) return 'enum';
103
- const mapped = TYPE_MAP[t];
104
- if (!mapped) return 'string';
105
- if (mapped === 'integer' && def.float) return 'float';
106
- return mapped;
107
- }
108
-
109
- export function parseField(name: string, def: MongooseFieldDef): IFieldDefinition {
110
- const type = resolveBaseType(def.type, def);
111
- const field: IFieldDefinition = { name, type };
112
-
113
- if (def.required === true) field.nullable = false;
114
- if (def.unique === true) field.unique = true;
115
- if (def.primaryKey === true) field.primaryKey = true;
116
- if (def.autoGenerate === true) field.autoGenerate = true;
117
- if (def.maxlength != null) field.maxLength = def.maxlength;
118
-
119
- if (def.default !== undefined) {
120
- field.defaultValue =
121
- def.default === 'now' || def.default === 'NOW' ? 'CURRENT_TIMESTAMP' : def.default;
122
- }
123
-
124
- if (type === 'enum' && def.enum?.length) {
125
- field.enumValues = def.enum;
126
- }
127
-
128
- return field;
129
- }
130
-
131
- export interface ParsedTable {
132
- name: string;
133
- fields: IFieldDefinition[];
134
- indexFields: Array<{ fieldName: string; unique: boolean }>;
135
- }
136
-
137
- export function parseTableDef(
138
- tableName: string,
139
- fieldDefs: Record<string, MongooseFieldDef>,
140
- ): ParsedTable {
141
- const fields: IFieldDefinition[] = [];
142
- const indexFields: Array<{ fieldName: string; unique: boolean }> = [];
143
-
144
- for (const [name, def] of Object.entries(fieldDefs)) {
145
- fields.push(parseField(name, def));
146
- if (def.index === true) {
147
- indexFields.push({ fieldName: name, unique: def.unique === true });
148
- }
149
- }
150
-
151
- return { name: tableName, fields, indexFields };
152
- }
@@ -1,165 +0,0 @@
1
- const ALLOWED_SNIPPET_METHODS: Record<string, string[]> = {
2
- action: ['run', 'dispatch', 'execute'],
3
- workflow: ['run', 'dispatch', 'execute'],
4
- database: ['dispatch', 'find', 'insert', 'update', 'delete', 'query', 'upsert', 'count', 'aggregate'],
5
- graph: ['dispatch', 'find', 'insert', 'update', 'delete', 'query', 'execute'],
6
- vector: ['query', 'find', 'findSimilar', 'upsert', 'insert', 'delete', 'dispatch'],
7
- storage: ['dispatch', 'upload', 'download', 'remove', 'listFiles', 'getSignedUrl', 'stats'],
8
- notification: ['dispatch', 'send', 'email.send', 'push.send', 'sms.send', 'callback.send'],
9
- messaging: ['dispatch', 'send', 'publish', 'produce', 'consume'],
10
- broker: ['dispatch', 'send', 'publish', 'produce', 'consume'],
11
- quota: ['run', 'dispatch', 'check', 'consume'],
12
- fallback: ['run', 'dispatch'],
13
- healthcheck: ['run', 'check', 'status'],
14
- health: ['run', 'check', 'status'],
15
- session: ['start', 'verify', 'refresh', 'revoke', 'listActive'],
16
- cache: ['get', 'set', 'clear', 'clearAll', 'dispatch', 'fetchValues'],
17
- };
18
-
19
- export function ensureSupportedSnippetOperation(operationFamily: string, method: string): void {
20
- const family = operationFamily.toLowerCase();
21
- const allowed = ALLOWED_SNIPPET_METHODS[family] ?? [];
22
- if (!allowed.includes(method)) {
23
- throw new Error(
24
- `Unsupported snippet operation "${family}.${method}". Allowed: ${allowed.join(', ') || '(none)'}`,
25
- );
26
- }
27
- }
28
-
29
- function resolveSdkCallPath(operationFamily: string, method: string): string {
30
- const family = operationFamily.toLowerCase();
31
- const m = method.toLowerCase();
32
-
33
- if (family === 'action') return `actions.${m}`;
34
- if (family === 'workflow') return `workflows.${m}`;
35
- if (family === 'database') return m === 'dispatch' ? 'databases.dispatch' : `databases.${m}`;
36
- if (family === 'graph') return m === 'dispatch' ? 'graph.dispatch' : `graph.${m}`;
37
- if (family === 'vector') return `vector.${m}`;
38
- if (family === 'storage') return m === 'dispatch' ? 'storage.dispatch' : `storage.${m}`;
39
- if (family === 'notification') return m === 'dispatch' ? 'notifications.dispatch' : `notifications.${m}`;
40
- if (family === 'messaging' || family === 'broker') {
41
- if (m === 'dispatch') return 'messageBrokers.dispatch';
42
- if (m === 'send' || m === 'publish' || m === 'produce') return 'messageBrokers.produce';
43
- return `messageBrokers.${m}`;
44
- }
45
- if (family === 'quota') return `quotas.${m}`;
46
- if (family === 'fallback') return `fallback.${m}`;
47
- if (family === 'healthcheck' || family === 'health') return `health.${m}`;
48
- if (family === 'session') return `sessions.${m}`;
49
- if (family === 'cache') return (m === 'dispatch' ? 'caches.dispatch' : `caches.${m}`);
50
- return `${family}.${m}`;
51
- }
52
-
53
- function buildInvocationArgs(payload: Record<string, unknown>): Record<string, unknown> {
54
- const input = (payload.input as Record<string, unknown>) ?? {};
55
- return {
56
- product: payload.product_tag,
57
- env: payload.env,
58
- ...input,
59
- ...(payload.session ? { session: payload.session } : {}),
60
- ...(payload.cache ? { cache: payload.cache } : {}),
61
- };
62
- }
63
-
64
- function prettyJson(v: unknown): string {
65
- return JSON.stringify(v ?? {}, null, 2);
66
- }
67
-
68
- export function buildTypeScriptSnippet(
69
- payload: Record<string, unknown>,
70
- operationFamily: string,
71
- method: string,
72
- ): string {
73
- const callPath = resolveSdkCallPath(operationFamily, method);
74
- const args = buildInvocationArgs(payload);
75
- return `import Ductape from '@ductape/sdk';
76
-
77
- const ductape = new Ductape({
78
- publishableKey: process.env.DUCTAPE_PUBLISHABLE_KEY!,
79
- product: process.env.DUCTAPE_PRODUCT!,
80
- env: process.env.DUCTAPE_ENV!,
81
- });
82
-
83
- async function main() {
84
- const result = await ductape.${callPath}(${prettyJson(args)});
85
- console.log(JSON.stringify(result, null, 2));
86
- }
87
-
88
- main().catch(console.error);
89
- `;
90
- }
91
-
92
- export function buildPythonSnippet(
93
- payload: Record<string, unknown>,
94
- operationFamily: string,
95
- method: string,
96
- ): string {
97
- const callPath = resolveSdkCallPath(operationFamily, method).replace(/\./g, '_');
98
- const args = buildInvocationArgs(payload);
99
- return `import os
100
- import json
101
- from ductape import Ductape
102
-
103
- ductape = Ductape(
104
- publishable_key=os.environ["DUCTAPE_PUBLISHABLE_KEY"],
105
- product=os.environ["DUCTAPE_PRODUCT"],
106
- env=os.environ["DUCTAPE_ENV"],
107
- )
108
-
109
- def main():
110
- args = ${prettyJson(args)}
111
- result = ductape.${callPath}(args)
112
- print(json.dumps(result, indent=2))
113
-
114
- if __name__ == "__main__":
115
- main()
116
- `;
117
- }
118
-
119
- export function buildGoSnippet(
120
- payload: Record<string, unknown>,
121
- operationFamily: string,
122
- method: string,
123
- ): string {
124
- const callPath = resolveSdkCallPath(operationFamily, method);
125
- return `package main
126
-
127
- // go get github.com/ductape/ductape/sdk/go
128
- // See https://docs.ductape.app for ${operationFamily}.${method}
129
- // Payload preview:
130
- // ${prettyJson(payload).split('\n').join('\n// ')}
131
- func main() {
132
- // ductape.${callPath}(...)
133
- }
134
- `;
135
- }
136
-
137
- export function buildDotnetSnippet(
138
- payload: Record<string, unknown>,
139
- operationFamily: string,
140
- method: string,
141
- ): string {
142
- const callPath = resolveSdkCallPath(operationFamily, method);
143
- return `// dotnet add package Ductape.Sdk
144
- // Operation: ${operationFamily}.${method} -> ${callPath}
145
- // Payload: ${JSON.stringify(payload)}
146
- `;
147
- }
148
-
149
- export function buildSnippet(
150
- language: 'typescript' | 'python' | 'go' | 'dotnet',
151
- payload: Record<string, unknown>,
152
- operationFamily: string,
153
- method: string,
154
- ): string {
155
- switch (language) {
156
- case 'python':
157
- return buildPythonSnippet(payload, operationFamily, method);
158
- case 'go':
159
- return buildGoSnippet(payload, operationFamily, method);
160
- case 'dotnet':
161
- return buildDotnetSnippet(payload, operationFamily, method);
162
- default:
163
- return buildTypeScriptSnippet(payload, operationFamily, method);
164
- }
165
- }
@@ -1,157 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
-
5
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
- const TEMPLATES_ROOT = path.resolve(__dirname, '../../templates');
7
-
8
- export type InitLanguage = 'typescript' | 'python' | 'go' | 'dotnet' | 'unknown';
9
-
10
- export function detectLanguage(dir: string): InitLanguage {
11
- if (fs.existsSync(path.join(dir, 'package.json'))) return 'typescript';
12
- if (fs.existsSync(path.join(dir, 'pyproject.toml')) || fs.existsSync(path.join(dir, 'requirements.txt'))) {
13
- return 'python';
14
- }
15
- if (fs.existsSync(path.join(dir, 'go.mod'))) return 'go';
16
- try {
17
- if (fs.readdirSync(dir).some((f) => f.endsWith('.csproj'))) return 'dotnet';
18
- } catch {
19
- /* ignore */
20
- }
21
- return 'unknown';
22
- }
23
-
24
- function copyTemplate(lang: InitLanguage, targetDir: string): void {
25
- const map: Record<InitLanguage, { src: string; dest: string } | null> = {
26
- typescript: { src: 'typescript/ductape.example.ts', dest: 'ductape.example.ts' },
27
- python: { src: 'python/ductape_example.py', dest: 'ductape_example.py' },
28
- go: { src: 'go/main.go', dest: 'ductape_example.go' },
29
- dotnet: { src: 'dotnet/Program.cs', dest: 'DuctapeExample.cs' },
30
- unknown: null,
31
- };
32
- const entry = map[lang];
33
- if (!entry) return;
34
- const srcPath = path.join(TEMPLATES_ROOT, entry.src);
35
- const destPath = path.join(targetDir, entry.dest);
36
- if (!fs.existsSync(destPath) && fs.existsSync(srcPath)) {
37
- fs.copyFileSync(srcPath, destPath);
38
- }
39
- }
40
-
41
- export function writeInitArtifacts(dir: string, lang: InitLanguage): void {
42
- const envContent = `# Ductape — credentials from Workbench → SDK Access
43
- DUCTAPE_ACCESS_KEY=
44
- # Passed to new Ductape({ accessKey, product, env }) — constructor is the only place for product/env defaults
45
- DUCTAPE_PRODUCT=
46
- DUCTAPE_ENV=dev
47
- `;
48
-
49
- const envPath = path.join(dir, '.env.ductape.example');
50
- if (!fs.existsSync(envPath)) fs.writeFileSync(envPath, envContent);
51
-
52
- const ductapeDir = path.join(dir, 'ductape');
53
- fs.mkdirSync(ductapeDir, { recursive: true });
54
-
55
- const readme = `# Ductape project
56
-
57
- Linked via \`ductape link\`. Manage resources with:
58
-
59
- \`\`\`bash
60
- ductape resources storage list
61
- ductape resources databases create -f database.json
62
- \`\`\`
63
-
64
- Docs: https://docs.ductape.app/docs/cli/
65
- `;
66
- const readmePath = path.join(ductapeDir, 'README.md');
67
- if (!fs.existsSync(readmePath)) fs.writeFileSync(readmePath, readme);
68
-
69
- const dbDir = path.join(ductapeDir, 'database');
70
- const migrationsDir = path.join(dbDir, 'migrations');
71
- fs.mkdirSync(migrationsDir, { recursive: true });
72
-
73
- const schemaPath = path.join(dbDir, 'schema.json');
74
- if (!fs.existsSync(schemaPath)) {
75
- const schemaTemplate = [
76
- {
77
- db: 'your_db_tag',
78
- tables: {
79
- example_table: {
80
- id: { type: 'String', primaryKey: true, autoGenerate: true },
81
- name: { type: 'String', required: true },
82
- createdAt: { type: 'Date', default: 'now' },
83
- },
84
- },
85
- },
86
- ];
87
- fs.writeFileSync(schemaPath, JSON.stringify(schemaTemplate, null, 2) + '\n');
88
- }
89
-
90
- const sessionsPath = path.join(ductapeDir, 'sessions.json');
91
- if (!fs.existsSync(sessionsPath)) {
92
- const sessionsTemplate = [
93
- {
94
- tag: 'user-session',
95
- name: 'User session',
96
- description: 'Standard authenticated user session',
97
- expiry: 604800,
98
- refresh_expiry: 2592000,
99
- },
100
- ];
101
- fs.writeFileSync(sessionsPath, JSON.stringify(sessionsTemplate, null, 2) + '\n');
102
- }
103
-
104
- const notificationsPath = path.join(ductapeDir, 'notifications.json');
105
- if (!fs.existsSync(notificationsPath)) {
106
- const notificationsTemplate = [
107
- {
108
- tag: 'transactional-email',
109
- name: 'Transactional email',
110
- description: 'Email channel for transactional messages',
111
- envs: [
112
- {
113
- slug: 'dev',
114
- emails: {
115
- host: 'smtp.example.com',
116
- port: 587,
117
- user: 'your-smtp-user',
118
- pass: 'your-smtp-pass',
119
- sender: 'no-reply@example.com',
120
- },
121
- },
122
- ],
123
- },
124
- ];
125
- fs.writeFileSync(notificationsPath, JSON.stringify(notificationsTemplate, null, 2) + '\n');
126
- }
127
-
128
- const eventsPath = path.join(ductapeDir, 'events.json');
129
- if (!fs.existsSync(eventsPath)) {
130
- const eventsTemplate = [
131
- {
132
- tag: 'main-event-bus',
133
- name: 'Main event bus',
134
- description: 'Primary message broker for application events',
135
- envs: [
136
- {
137
- slug: 'dev',
138
- type: 'kafka',
139
- brokers: ['localhost:9092'],
140
- client_id: 'my-app-dev',
141
- },
142
- ],
143
- },
144
- ];
145
- fs.writeFileSync(eventsPath, JSON.stringify(eventsTemplate, null, 2) + '\n');
146
- }
147
-
148
- copyTemplate(lang, dir);
149
-
150
- const gitignorePath = path.join(dir, '.gitignore');
151
- if (fs.existsSync(gitignorePath)) {
152
- const gi = fs.readFileSync(gitignorePath, 'utf8');
153
- if (!gi.includes('.env') && !gi.includes('.env.local')) {
154
- fs.appendFileSync(gitignorePath, '\n.env\n.env.local\n');
155
- }
156
- }
157
- }