@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.
- package/CHANGELOG.md +7 -0
- package/README.md +7 -0
- package/dist/commands/generate.js +1 -1
- package/dist/commands/migrate-codebase.d.ts +176 -0
- package/dist/commands/migrate-codebase.js +753 -0
- package/dist/commands/migration-artifact.d.ts +32 -0
- package/dist/commands/migration-artifact.js +48 -0
- package/dist/commands/migration-assurance.d.ts +10 -0
- package/dist/commands/migration-assurance.js +120 -0
- package/dist/commands/migration-database.d.ts +21 -0
- package/dist/commands/migration-database.js +162 -0
- package/dist/commands/migration-e2e.d.ts +68 -0
- package/dist/commands/migration-e2e.js +189 -0
- package/dist/commands/migration-environments.d.ts +20 -0
- package/dist/commands/migration-environments.js +58 -0
- package/dist/commands/migration-generated.d.ts +68 -0
- package/dist/commands/migration-generated.js +217 -0
- package/dist/commands/migration-portfolio.d.ts +88 -0
- package/dist/commands/migration-portfolio.js +223 -0
- package/dist/commands/migration-products.d.ts +23 -0
- package/dist/commands/migration-products.js +88 -0
- package/dist/commands/migration-review.d.ts +248 -0
- package/dist/commands/migration-review.js +624 -0
- package/dist/commands/migration-secrets.d.ts +24 -0
- package/dist/commands/migration-secrets.js +93 -0
- package/dist/commands/migration-slice.d.ts +132 -0
- package/dist/commands/migration-slice.js +266 -0
- package/dist/commands/migration-verification.d.ts +12 -0
- package/dist/commands/migration-verification.js +194 -0
- package/dist/commands/products.d.ts +1 -0
- package/dist/commands/products.js +33 -1
- package/dist/commands/secrets.d.ts +8 -0
- package/dist/commands/secrets.js +44 -0
- package/dist/index.js +354 -2
- package/dist/lib/integrations.js +6 -1
- package/dist/lib/migration-artifact.d.ts +13 -0
- package/dist/lib/migration-artifact.js +103 -0
- package/package.json +8 -1
- package/API_PARITY.md +0 -58
- package/DB_MIGRATE_PLAN.md +0 -223
- package/ductape.example.ts +0 -18
- package/src/commands/apply.ts +0 -153
- package/src/commands/apps-import.ts +0 -43
- package/src/commands/apps.ts +0 -92
- package/src/commands/cloud.ts +0 -68
- package/src/commands/completion.ts +0 -119
- package/src/commands/db-migrate.ts +0 -209
- package/src/commands/db-schema.ts +0 -429
- package/src/commands/db.ts +0 -63
- package/src/commands/generate.ts +0 -74
- package/src/commands/graph.ts +0 -51
- package/src/commands/init.ts +0 -39
- package/src/commands/install.ts +0 -76
- package/src/commands/link.ts +0 -84
- package/src/commands/login.ts +0 -145
- package/src/commands/logout.ts +0 -7
- package/src/commands/platform.ts +0 -123
- package/src/commands/products.ts +0 -228
- package/src/commands/profiles.ts +0 -23
- package/src/commands/resources.ts +0 -213
- package/src/commands/secrets.ts +0 -46
- package/src/commands/unlink.ts +0 -15
- package/src/commands/whoami.ts +0 -27
- package/src/commands/workspaces.ts +0 -98
- package/src/index.ts +0 -523
- package/src/lib/api-client.ts +0 -88
- package/src/lib/app-import.ts +0 -82
- package/src/lib/apply-loaders.ts +0 -30
- package/src/lib/config.ts +0 -230
- package/src/lib/context-store.ts +0 -77
- package/src/lib/db-methods.ts +0 -56
- package/src/lib/db-types.ts +0 -104
- package/src/lib/encryption.ts +0 -12
- package/src/lib/forms/enums.ts +0 -52
- package/src/lib/forms/index.ts +0 -8
- package/src/lib/forms/prompt.ts +0 -209
- package/src/lib/forms/schemas.ts +0 -288
- package/src/lib/forms/types.ts +0 -27
- package/src/lib/http.ts +0 -76
- package/src/lib/integrations.ts +0 -105
- package/src/lib/interactive-opts.ts +0 -13
- package/src/lib/migration-files.ts +0 -58
- package/src/lib/output.ts +0 -39
- package/src/lib/platform-api.ts +0 -209
- package/src/lib/proxy/context.ts +0 -94
- package/src/lib/proxy/db-proxy.ts +0 -40
- package/src/lib/proxy/graph-proxy.ts +0 -64
- package/src/lib/proxy/sdk-proxy.ts +0 -58
- package/src/lib/read-body.ts +0 -57
- package/src/lib/resources.ts +0 -160
- package/src/lib/schema-loader.ts +0 -152
- package/src/lib/snippets.ts +0 -165
- package/src/lib/templates.ts +0 -157
- package/src/lib/workspace-context.ts +0 -43
- package/src/lib/workspaces.ts +0 -283
- package/test/apply-loaders.test.ts +0 -40
- package/test/encryption.test.ts +0 -20
- package/test/forms.test.ts +0 -25
- package/test/http.test.ts +0 -15
- package/test/platform-api.test.ts +0 -42
- package/test/resources.test.ts +0 -23
- package/test/workspace-resolve.test.ts +0 -29
- package/tsconfig.json +0 -15
package/src/lib/read-body.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/resources.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/schema-loader.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/snippets.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/templates.ts
DELETED
|
@@ -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
|
-
}
|