@ductape/cli 0.2.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 (155) hide show
  1. package/API_PARITY.md +58 -0
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +89 -0
  4. package/dist/commands/apps-import.d.ts +9 -0
  5. package/dist/commands/apps-import.js +29 -0
  6. package/dist/commands/apps.d.ts +10 -0
  7. package/dist/commands/apps.js +68 -0
  8. package/dist/commands/cloud.d.ts +7 -0
  9. package/dist/commands/cloud.js +42 -0
  10. package/dist/commands/completion.d.ts +1 -0
  11. package/dist/commands/completion.js +113 -0
  12. package/dist/commands/db.d.ts +5 -0
  13. package/dist/commands/db.js +55 -0
  14. package/dist/commands/generate.d.ts +14 -0
  15. package/dist/commands/generate.js +52 -0
  16. package/dist/commands/graph.d.ts +4 -0
  17. package/dist/commands/graph.js +44 -0
  18. package/dist/commands/init.d.ts +7 -0
  19. package/dist/commands/init.js +28 -0
  20. package/dist/commands/install.d.ts +1 -0
  21. package/dist/commands/install.js +42 -0
  22. package/dist/commands/link.d.ts +7 -0
  23. package/dist/commands/link.js +61 -0
  24. package/dist/commands/login.d.ts +9 -0
  25. package/dist/commands/login.js +103 -0
  26. package/dist/commands/logout.d.ts +1 -0
  27. package/dist/commands/logout.js +6 -0
  28. package/dist/commands/platform.d.ts +7 -0
  29. package/dist/commands/platform.js +99 -0
  30. package/dist/commands/products.d.ts +14 -0
  31. package/dist/commands/products.js +74 -0
  32. package/dist/commands/profiles.d.ts +2 -0
  33. package/dist/commands/profiles.js +18 -0
  34. package/dist/commands/resources.d.ts +7 -0
  35. package/dist/commands/resources.js +61 -0
  36. package/dist/commands/secrets.d.ts +6 -0
  37. package/dist/commands/secrets.js +38 -0
  38. package/dist/commands/unlink.d.ts +3 -0
  39. package/dist/commands/unlink.js +14 -0
  40. package/dist/commands/whoami.d.ts +1 -0
  41. package/dist/commands/whoami.js +22 -0
  42. package/dist/commands/workspaces.d.ts +16 -0
  43. package/dist/commands/workspaces.js +72 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.js +283 -0
  46. package/dist/lib/api-client.d.ts +18 -0
  47. package/dist/lib/api-client.js +65 -0
  48. package/dist/lib/app-import.d.ts +15 -0
  49. package/dist/lib/app-import.js +63 -0
  50. package/dist/lib/config.d.ts +63 -0
  51. package/dist/lib/config.js +155 -0
  52. package/dist/lib/context-store.d.ts +17 -0
  53. package/dist/lib/context-store.js +43 -0
  54. package/dist/lib/db-methods.d.ts +3 -0
  55. package/dist/lib/db-methods.js +55 -0
  56. package/dist/lib/encryption.d.ts +1 -0
  57. package/dist/lib/encryption.js +11 -0
  58. package/dist/lib/forms/enums.d.ts +11 -0
  59. package/dist/lib/forms/enums.js +42 -0
  60. package/dist/lib/forms/index.d.ts +2 -0
  61. package/dist/lib/forms/index.js +2 -0
  62. package/dist/lib/forms/prompt.d.ts +11 -0
  63. package/dist/lib/forms/prompt.js +203 -0
  64. package/dist/lib/forms/schemas.d.ts +9 -0
  65. package/dist/lib/forms/schemas.js +253 -0
  66. package/dist/lib/forms/types.d.ts +18 -0
  67. package/dist/lib/forms/types.js +1 -0
  68. package/dist/lib/http.d.ts +10 -0
  69. package/dist/lib/http.js +51 -0
  70. package/dist/lib/integrations.d.ts +20 -0
  71. package/dist/lib/integrations.js +20 -0
  72. package/dist/lib/interactive-opts.d.ts +6 -0
  73. package/dist/lib/interactive-opts.js +6 -0
  74. package/dist/lib/output.d.ts +3 -0
  75. package/dist/lib/output.js +22 -0
  76. package/dist/lib/platform-api.d.ts +19 -0
  77. package/dist/lib/platform-api.js +123 -0
  78. package/dist/lib/proxy/context.d.ts +16 -0
  79. package/dist/lib/proxy/context.js +73 -0
  80. package/dist/lib/proxy/db-proxy.d.ts +13 -0
  81. package/dist/lib/proxy/db-proxy.js +28 -0
  82. package/dist/lib/proxy/graph-proxy.d.ts +19 -0
  83. package/dist/lib/proxy/graph-proxy.js +43 -0
  84. package/dist/lib/proxy/sdk-proxy.d.ts +14 -0
  85. package/dist/lib/proxy/sdk-proxy.js +22 -0
  86. package/dist/lib/read-body.d.ts +11 -0
  87. package/dist/lib/read-body.js +38 -0
  88. package/dist/lib/resources.d.ts +12 -0
  89. package/dist/lib/resources.js +147 -0
  90. package/dist/lib/snippets.d.ts +6 -0
  91. package/dist/lib/snippets.js +146 -0
  92. package/dist/lib/templates.d.ts +3 -0
  93. package/dist/lib/templates.js +74 -0
  94. package/dist/lib/workspace-context.d.ts +12 -0
  95. package/dist/lib/workspace-context.js +25 -0
  96. package/dist/lib/workspaces.d.ts +45 -0
  97. package/dist/lib/workspaces.js +219 -0
  98. package/package.json +37 -0
  99. package/src/commands/apps-import.ts +43 -0
  100. package/src/commands/apps.ts +92 -0
  101. package/src/commands/cloud.ts +50 -0
  102. package/src/commands/completion.ts +119 -0
  103. package/src/commands/db.ts +63 -0
  104. package/src/commands/generate.ts +74 -0
  105. package/src/commands/graph.ts +51 -0
  106. package/src/commands/init.ts +39 -0
  107. package/src/commands/install.ts +48 -0
  108. package/src/commands/link.ts +84 -0
  109. package/src/commands/login.ts +145 -0
  110. package/src/commands/logout.ts +7 -0
  111. package/src/commands/platform.ts +123 -0
  112. package/src/commands/products.ts +104 -0
  113. package/src/commands/profiles.ts +23 -0
  114. package/src/commands/resources.ts +85 -0
  115. package/src/commands/secrets.ts +46 -0
  116. package/src/commands/unlink.ts +15 -0
  117. package/src/commands/whoami.ts +27 -0
  118. package/src/commands/workspaces.ts +98 -0
  119. package/src/index.ts +381 -0
  120. package/src/lib/api-client.ts +88 -0
  121. package/src/lib/app-import.ts +82 -0
  122. package/src/lib/config.ts +225 -0
  123. package/src/lib/context-store.ts +77 -0
  124. package/src/lib/db-methods.ts +56 -0
  125. package/src/lib/encryption.ts +12 -0
  126. package/src/lib/forms/enums.ts +52 -0
  127. package/src/lib/forms/index.ts +8 -0
  128. package/src/lib/forms/prompt.ts +209 -0
  129. package/src/lib/forms/schemas.ts +288 -0
  130. package/src/lib/forms/types.ts +27 -0
  131. package/src/lib/http.ts +70 -0
  132. package/src/lib/integrations.ts +47 -0
  133. package/src/lib/interactive-opts.ts +13 -0
  134. package/src/lib/output.ts +24 -0
  135. package/src/lib/platform-api.ts +178 -0
  136. package/src/lib/proxy/context.ts +94 -0
  137. package/src/lib/proxy/db-proxy.ts +40 -0
  138. package/src/lib/proxy/graph-proxy.ts +64 -0
  139. package/src/lib/proxy/sdk-proxy.ts +58 -0
  140. package/src/lib/read-body.ts +57 -0
  141. package/src/lib/resources.ts +156 -0
  142. package/src/lib/snippets.ts +165 -0
  143. package/src/lib/templates.ts +78 -0
  144. package/src/lib/workspace-context.ts +43 -0
  145. package/src/lib/workspaces.ts +283 -0
  146. package/templates/dotnet/Program.cs +2 -0
  147. package/templates/go/main.go +8 -0
  148. package/templates/python/ductape_example.py +16 -0
  149. package/templates/typescript/ductape.example.ts +18 -0
  150. package/test/encryption.test.ts +20 -0
  151. package/test/forms.test.ts +25 -0
  152. package/test/platform-api.test.ts +19 -0
  153. package/test/resources.test.ts +23 -0
  154. package/test/workspace-resolve.test.ts +29 -0
  155. package/tsconfig.json +15 -0
@@ -0,0 +1,88 @@
1
+ import type { Credentials } from './config.js';
2
+ import { getApiUrl } from './config.js';
3
+
4
+ export interface ApiEnvelope<T> {
5
+ status?: boolean;
6
+ data?: T;
7
+ message?: string;
8
+ }
9
+
10
+ export class ApiClient {
11
+ constructor(
12
+ private readonly apiUrl: string,
13
+ private readonly token?: string,
14
+ ) {}
15
+
16
+ static forProfile(profileName?: string, creds?: Credentials | null): ApiClient {
17
+ return new ApiClient(getApiUrl(profileName), creds?.auth_token);
18
+ }
19
+
20
+ async request<T>(
21
+ method: string,
22
+ path: string,
23
+ body?: unknown,
24
+ extraHeaders?: Record<string, string>,
25
+ ): Promise<T> {
26
+ const url = `${this.apiUrl}${path.startsWith('/') ? path : `/${path}`}`;
27
+ const headers: Record<string, string> = {
28
+ 'Content-Type': 'application/json',
29
+ ...extraHeaders,
30
+ };
31
+ if (this.token) {
32
+ headers.Authorization = `Bearer ${this.token}`;
33
+ }
34
+
35
+ const res = await fetch(url, {
36
+ method,
37
+ headers,
38
+ body: body !== undefined ? JSON.stringify(body) : undefined,
39
+ });
40
+
41
+ const text = await res.text();
42
+ let parsed: ApiEnvelope<T> & T;
43
+ try {
44
+ parsed = text ? JSON.parse(text) : {};
45
+ } catch {
46
+ throw new Error(`Invalid JSON from ${url}: ${text.slice(0, 200)}`);
47
+ }
48
+
49
+ if (!res.ok) {
50
+ const msg =
51
+ (parsed as ApiEnvelope<T>).message ??
52
+ (parsed as { error?: string }).error ??
53
+ `HTTP ${res.status}`;
54
+ throw new Error(msg);
55
+ }
56
+
57
+ if (typeof (parsed as ApiEnvelope<T>).status === 'boolean' && !(parsed as ApiEnvelope<T>).status) {
58
+ throw new Error((parsed as ApiEnvelope<T>).message ?? 'Request failed');
59
+ }
60
+
61
+ const data = (parsed as ApiEnvelope<{ result?: T }>).data;
62
+ if (data && typeof data === 'object' && 'result' in data) {
63
+ return (data as { result: T }).result;
64
+ }
65
+ return (data ?? parsed) as T;
66
+ }
67
+
68
+ post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
69
+ return this.request<T>('POST', path, body, headers);
70
+ }
71
+
72
+ put<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
73
+ return this.request<T>('PUT', path, body, headers);
74
+ }
75
+
76
+ delete<T>(path: string, headers?: Record<string, string>): Promise<T> {
77
+ return this.request<T>('DELETE', path, undefined, headers);
78
+ }
79
+
80
+ get<T>(path: string, headers?: Record<string, string>): Promise<T> {
81
+ return this.request<T>('GET', path, undefined, headers);
82
+ }
83
+
84
+ getPath<T>(path: string, query?: Record<string, string>): Promise<T> {
85
+ const qs = query ? `?${new URLSearchParams(query).toString()}` : '';
86
+ return this.get<T>(`${path}${qs}`);
87
+ }
88
+ }
@@ -0,0 +1,82 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { WorkspaceContext } from './workspace-context.js';
4
+
5
+ /** Values accepted by POST /apps/v1/import `type` field */
6
+ export const IMPORT_DOC_TYPES = {
7
+ postman: 'PostmanV2.1',
8
+ openapi: 'OpenApiV3.0',
9
+ } as const;
10
+
11
+ export type ImportFormat = keyof typeof IMPORT_DOC_TYPES;
12
+
13
+ export function parseImportFormat(value: string): ImportFormat {
14
+ const v = value.toLowerCase().replace(/[_\s]/g, '');
15
+ if (v === 'postman' || v === 'postmanv21' || v === 'postman2.1' || v === 'postmanv2.1') {
16
+ return 'postman';
17
+ }
18
+ if (v === 'openapi' || v === 'openapiv30' || v === 'openapi3.0' || v === 'openapi3') {
19
+ return 'openapi';
20
+ }
21
+ throw new Error(
22
+ `Unknown import type "${value}". Use: postman (Postman Collection v2.1) or openapi (OpenAPI 3.0)`,
23
+ );
24
+ }
25
+
26
+ function uploadFilename(filePath: string): string {
27
+ const base = path.basename(filePath);
28
+ if (base.endsWith('.yaml') || base.endsWith('.yml')) return base;
29
+ if (base.endsWith('.json')) return base;
30
+ return `${base}.json`;
31
+ }
32
+
33
+ export async function importAppFromFile(
34
+ ctx: WorkspaceContext,
35
+ opts: {
36
+ filePath: string;
37
+ format: ImportFormat;
38
+ appId?: string;
39
+ update?: boolean;
40
+ version?: string;
41
+ },
42
+ ): Promise<unknown> {
43
+ const abs = path.resolve(opts.filePath);
44
+ if (!fs.existsSync(abs)) throw new Error(`File not found: ${abs}`);
45
+
46
+ const buffer = fs.readFileSync(abs);
47
+ const filename = uploadFilename(abs);
48
+ const form = new FormData();
49
+
50
+ form.append('file', new Blob([buffer]), filename);
51
+ form.append('workspace_id', ctx.workspaceId);
52
+ form.append('public_key', ctx.credentials.public_key);
53
+ form.append('user_id', ctx.credentials.user_id);
54
+ form.append('type', IMPORT_DOC_TYPES[opts.format]);
55
+ form.append('version', opts.version ?? '0.0.1');
56
+ form.append('update', opts.update ? 'true' : 'false');
57
+ if (opts.appId) form.append('app_id', opts.appId);
58
+
59
+ const url = `${ctx.apiUrl}/apps/v1/import`;
60
+ const res = await fetch(url, {
61
+ method: 'POST',
62
+ headers: { Authorization: `Bearer ${ctx.credentials.auth_token}` },
63
+ body: form,
64
+ });
65
+
66
+ const text = await res.text();
67
+ let parsed: { status?: boolean; data?: unknown; message?: string };
68
+ try {
69
+ parsed = text ? JSON.parse(text) : {};
70
+ } catch {
71
+ throw new Error(`Invalid JSON from import API: ${text.slice(0, 200)}`);
72
+ }
73
+
74
+ if (!res.ok) {
75
+ throw new Error(parsed.message ?? `Import failed (HTTP ${res.status})`);
76
+ }
77
+ if (parsed.status === false) {
78
+ throw new Error(parsed.message ?? 'Import failed');
79
+ }
80
+
81
+ return parsed.data ?? parsed;
82
+ }
@@ -0,0 +1,225 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ export const GLOBAL_DIR = path.join(os.homedir(), '.ductape');
9
+ export const CREDENTIALS_PATH = path.join(GLOBAL_DIR, 'credentials.json');
10
+ export const GLOBAL_CONFIG_PATH = path.join(GLOBAL_DIR, 'config.json');
11
+ export const PROJECT_CONFIG_DIR = '.ductape';
12
+ export const PROJECT_CONFIG_PATH = path.join(PROJECT_CONFIG_DIR, 'config.json');
13
+
14
+ export interface Credentials {
15
+ user_id: string;
16
+ email: string;
17
+ auth_token: string;
18
+ public_key: string;
19
+ firstname?: string;
20
+ lastname?: string;
21
+ workspaces?: Array<{
22
+ workspace_id?: string;
23
+ workspace_name?: string;
24
+ _id?: string;
25
+ name?: string;
26
+ tag?: string;
27
+ default?: boolean;
28
+ }>;
29
+ }
30
+
31
+ export interface Profile {
32
+ api_url: string;
33
+ }
34
+
35
+ export interface GlobalConfig {
36
+ default_profile: string;
37
+ profiles: Record<string, Profile>;
38
+ /** Set at login or via `ductape workspaces use` */
39
+ active_workspace_id?: string;
40
+ active_workspace_name?: string;
41
+ active_workspace_tag?: string;
42
+ }
43
+
44
+ /** Linked project config — use workspace_tag (human-readable); workspace_id is resolved by the CLI. */
45
+ export interface ProjectConfig {
46
+ workspace_tag?: string;
47
+ /** Resolved automatically; do not edit unless debugging */
48
+ workspace_id?: string;
49
+ product_tag: string;
50
+ env_slug: string;
51
+ linked_profile?: string;
52
+ project_id?: string;
53
+ }
54
+
55
+ export interface ResolvedProjectConfig extends ProjectConfig {
56
+ workspace_id: string;
57
+ }
58
+
59
+ const DEFAULT_GLOBAL: GlobalConfig = {
60
+ default_profile: 'cloud',
61
+ profiles: {
62
+ /** Production gateway — paths are /users/v1/…, /proxy/v1/… (no /api prefix). */
63
+ cloud: { api_url: 'https://api.ductape.app' },
64
+ /** Local docker api-gateway — paths are /api/users/v1/…, /api/proxy/v1/… */
65
+ local: { api_url: 'http://localhost:4311/api' },
66
+ },
67
+ };
68
+
69
+ function readJson<T>(filePath: string): T | null {
70
+ if (!fs.existsSync(filePath)) return null;
71
+ return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T;
72
+ }
73
+
74
+ function writeJson(filePath: string, data: unknown, mode = 0o644): void {
75
+ const dir = path.dirname(filePath);
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', { mode });
78
+ }
79
+
80
+ export function ensureGlobalDir(): void {
81
+ fs.mkdirSync(GLOBAL_DIR, { recursive: true });
82
+ }
83
+
84
+ export function loadCredentials(): Credentials | null {
85
+ return readJson<Credentials>(CREDENTIALS_PATH);
86
+ }
87
+
88
+ export function saveCredentials(creds: Credentials): void {
89
+ ensureGlobalDir();
90
+ writeJson(CREDENTIALS_PATH, creds, 0o600);
91
+ }
92
+
93
+ export function clearCredentials(): void {
94
+ if (fs.existsSync(CREDENTIALS_PATH)) fs.unlinkSync(CREDENTIALS_PATH);
95
+ const global = loadGlobalConfig();
96
+ delete global.active_workspace_id;
97
+ delete global.active_workspace_name;
98
+ delete global.active_workspace_tag;
99
+ saveGlobalConfig(global);
100
+ }
101
+
102
+ export function getActiveWorkspaceId(): string | null {
103
+ const global = loadGlobalConfig();
104
+ return global.active_workspace_id ?? null;
105
+ }
106
+
107
+ export function loadGlobalConfig(): GlobalConfig {
108
+ const cfg = readJson<GlobalConfig>(GLOBAL_CONFIG_PATH) ?? {
109
+ ...DEFAULT_GLOBAL,
110
+ profiles: { ...DEFAULT_GLOBAL.profiles },
111
+ };
112
+ // Migrate older configs that used /api on production (returns HTML 404).
113
+ if (cfg.profiles.cloud?.api_url?.replace(/\/$/, '') === 'https://api.ductape.app/api') {
114
+ cfg.profiles.cloud.api_url = DEFAULT_GLOBAL.profiles.cloud.api_url;
115
+ }
116
+ return cfg;
117
+ }
118
+
119
+ export function saveGlobalConfig(config: GlobalConfig): void {
120
+ ensureGlobalDir();
121
+ writeJson(GLOBAL_CONFIG_PATH, config);
122
+ }
123
+
124
+ export function getActiveProfileName(project?: ProjectConfig | null): string {
125
+ if (project?.linked_profile) return project.linked_profile;
126
+ return loadGlobalConfig().default_profile;
127
+ }
128
+
129
+ export function getApiUrl(profileName?: string): string {
130
+ const global = loadGlobalConfig();
131
+ const name = profileName ?? global.default_profile;
132
+ const profile = global.profiles[name];
133
+ if (!profile) {
134
+ throw new Error(`Unknown profile "${name}". Run: ductape profiles list`);
135
+ }
136
+ return profile.api_url.replace(/\/$/, '');
137
+ }
138
+
139
+ function matchWorkspaceFromCredentials(
140
+ workspaces: NonNullable<Credentials['workspaces']>,
141
+ selector: string,
142
+ ): string | null {
143
+ const trimmed = selector.trim();
144
+ const idx = Number(trimmed);
145
+ if (!Number.isNaN(idx) && workspaces[idx]) {
146
+ const w = workspaces[idx];
147
+ return w.workspace_id ?? w._id ?? null;
148
+ }
149
+ for (const w of workspaces) {
150
+ const id = w.workspace_id ?? w._id;
151
+ const name = w.workspace_name ?? w.name ?? '';
152
+ const tag = w.tag ?? '';
153
+ if (
154
+ id === trimmed ||
155
+ name.toLowerCase() === trimmed.toLowerCase() ||
156
+ tag.toLowerCase() === trimmed.toLowerCase()
157
+ ) {
158
+ return id ?? null;
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /** Resolve workspace_id from tag (cached workspaces), explicit id, or global active workspace. */
165
+ export function resolveWorkspaceId(cfg: ProjectConfig, creds?: Credentials | null): string | null {
166
+ if (cfg.workspace_id?.trim()) return cfg.workspace_id.trim();
167
+ if (cfg.workspace_tag?.trim() && creds?.workspaces?.length) {
168
+ const id = matchWorkspaceFromCredentials(creds.workspaces, cfg.workspace_tag);
169
+ if (id) return id;
170
+ }
171
+ return getActiveWorkspaceId();
172
+ }
173
+
174
+ export function findProjectConfig(startDir = process.cwd()): { dir: string; config: ProjectConfig } | null {
175
+ let dir = startDir;
176
+ const root = path.parse(dir).root;
177
+ while (true) {
178
+ const candidate = path.join(dir, PROJECT_CONFIG_PATH);
179
+ const cfg = readJson<ProjectConfig>(candidate);
180
+ if (cfg?.product_tag && (cfg.workspace_tag || cfg.workspace_id)) {
181
+ return { dir, config: cfg };
182
+ }
183
+ if (dir === root) break;
184
+ dir = path.dirname(dir);
185
+ }
186
+ return null;
187
+ }
188
+
189
+ export function loadProjectConfig(): ResolvedProjectConfig {
190
+ const found = findProjectConfig();
191
+ if (!found) {
192
+ throw new Error(
193
+ 'No linked project. Run `ductape link` from your project directory (or `ductape init`).',
194
+ );
195
+ }
196
+ const creds = loadCredentials();
197
+ const cfg = { ...found.config };
198
+ const workspace_id = resolveWorkspaceId(cfg, creds);
199
+ if (!workspace_id) {
200
+ throw new Error(
201
+ 'No workspace resolved. Set workspace_tag in .ductape/config.json or run `ductape workspaces use`.',
202
+ );
203
+ }
204
+ return { ...cfg, workspace_id };
205
+ }
206
+
207
+ export function saveProjectConfig(dir: string, config: ProjectConfig): void {
208
+ const configDir = path.join(dir, PROJECT_CONFIG_DIR);
209
+ fs.mkdirSync(configDir, { recursive: true });
210
+ writeJson(path.join(configDir, 'config.json'), config);
211
+ }
212
+
213
+ export function resolvePlatformDir(): string {
214
+ if (process.env.DUCTAPE_PLATFORM_DIR) {
215
+ return path.resolve(process.env.DUCTAPE_PLATFORM_DIR);
216
+ }
217
+ // cli/ -> repo root -> platform/
218
+ const fromPackage = path.resolve(__dirname, '../../../platform');
219
+ if (fs.existsSync(path.join(fromPackage, 'docker-compose.yml'))) {
220
+ return fromPackage;
221
+ }
222
+ throw new Error(
223
+ 'Platform directory not found. Set DUCTAPE_PLATFORM_DIR to the folder containing docker-compose.yml',
224
+ );
225
+ }
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { GLOBAL_DIR } from './config.js';
4
+ import type { GraphContext } from './proxy/graph-proxy.js';
5
+
6
+ export interface DatabaseContext {
7
+ database: string;
8
+ env: string;
9
+ product: string;
10
+ }
11
+
12
+ export interface ProjectRuntimeContext {
13
+ workspace_id: string;
14
+ product_tag: string;
15
+ env_slug: string;
16
+ database_context?: DatabaseContext | null;
17
+ graph_context?: GraphContext | null;
18
+ }
19
+
20
+ const RUNTIME_PATH = path.join(GLOBAL_DIR, 'runtime-context.json');
21
+
22
+ function readAll(): Record<string, ProjectRuntimeContext> {
23
+ if (!fs.existsSync(RUNTIME_PATH)) return {};
24
+ return JSON.parse(fs.readFileSync(RUNTIME_PATH, 'utf8')) as Record<string, ProjectRuntimeContext>;
25
+ }
26
+
27
+ function writeAll(data: Record<string, ProjectRuntimeContext>): void {
28
+ fs.mkdirSync(GLOBAL_DIR, { recursive: true });
29
+ fs.writeFileSync(RUNTIME_PATH, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
30
+ }
31
+
32
+ function projectKey(workspaceId: string, productTag: string): string {
33
+ return `${workspaceId}:${productTag}`;
34
+ }
35
+
36
+ export function loadRuntimeContext(workspaceId: string, productTag: string): ProjectRuntimeContext | null {
37
+ const all = readAll();
38
+ return all[projectKey(workspaceId, productTag)] ?? null;
39
+ }
40
+
41
+ export function saveRuntimeContext(ctx: ProjectRuntimeContext): void {
42
+ const all = readAll();
43
+ all[projectKey(ctx.workspace_id, ctx.product_tag)] = ctx;
44
+ writeAll(all);
45
+ }
46
+
47
+ export function setDatabaseContext(
48
+ workspaceId: string,
49
+ productTag: string,
50
+ envSlug: string,
51
+ db: DatabaseContext | null,
52
+ ): void {
53
+ const existing =
54
+ loadRuntimeContext(workspaceId, productTag) ?? {
55
+ workspace_id: workspaceId,
56
+ product_tag: productTag,
57
+ env_slug: envSlug,
58
+ };
59
+ existing.database_context = db;
60
+ saveRuntimeContext(existing);
61
+ }
62
+
63
+ export function setGraphContext(
64
+ workspaceId: string,
65
+ productTag: string,
66
+ envSlug: string,
67
+ graph: GraphContext | null,
68
+ ): void {
69
+ const existing =
70
+ loadRuntimeContext(workspaceId, productTag) ?? {
71
+ workspace_id: workspaceId,
72
+ product_tag: productTag,
73
+ env_slug: envSlug,
74
+ };
75
+ existing.graph_context = graph;
76
+ saveRuntimeContext(existing);
77
+ }
@@ -0,0 +1,56 @@
1
+ /** db-proxy methods (runtime). Dot notation supported: schema.list */
2
+ export const DB_PROXY_METHODS = [
3
+ 'connect',
4
+ 'disconnect',
5
+ 'closeAll',
6
+ 'testConnection',
7
+ 'getCurrentContext',
8
+ 'connection',
9
+ 'query',
10
+ 'execute',
11
+ 'insert',
12
+ 'update',
13
+ 'delete',
14
+ 'upsert',
15
+ 'count',
16
+ 'sum',
17
+ 'avg',
18
+ 'min',
19
+ 'max',
20
+ 'aggregate',
21
+ 'listTables',
22
+ 'listTablesWithInfo',
23
+ 'describe',
24
+ 'getSchema',
25
+ 'beginTransaction',
26
+ 'schema.create',
27
+ 'schema.drop',
28
+ 'schema.list',
29
+ 'schema.describe',
30
+ 'schema.addField',
31
+ 'schema.dropField',
32
+ 'migration.list',
33
+ 'migration.run',
34
+ ] as const;
35
+
36
+ export const GRAPH_PROXY_METHODS = [
37
+ 'connect',
38
+ 'disconnect',
39
+ 'disconnectAll',
40
+ 'testConnection',
41
+ 'getCurrentContext',
42
+ 'create',
43
+ 'fetchAll',
44
+ 'fetch',
45
+ 'update',
46
+ 'delete',
47
+ 'createNode',
48
+ 'findNodes',
49
+ 'query',
50
+ 'traverse',
51
+ 'shortestPath',
52
+ 'countNodes',
53
+ 'dispatch',
54
+ 'execute',
55
+ 'executeTransaction',
56
+ ] as const;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Proxy payload encryption — matches platform/workbench/src/utils/proxyEncryption.ts
3
+ */
4
+ import CryptoJS from 'crypto-js';
5
+
6
+ export function encryptProxyPayload(data: object, publicKey: string): string {
7
+ const jsonString = JSON.stringify(data);
8
+ const iv = CryptoJS.lib.WordArray.random(16);
9
+ const hashedKey = CryptoJS.SHA256(publicKey);
10
+ const encrypted = CryptoJS.AES.encrypt(jsonString, hashedKey, { iv });
11
+ return iv.toString(CryptoJS.enc.Hex) + ':' + encrypted.toString();
12
+ }
@@ -0,0 +1,52 @@
1
+ /** Enum values aligned with @ductape/sdk — used for CLI dropdowns */
2
+
3
+ export const STORAGE_PROVIDERS = ['aws', 'gcp', 'azure'] as const;
4
+
5
+ export const DATABASE_TYPES = ['postgresql', 'mongodb', 'mysql'] as const;
6
+
7
+ export const GRAPH_TYPES = ['neo4j', 'neptune', 'arangodb', 'memgraph'] as const;
8
+
9
+ export const VECTOR_DB_TYPES = [
10
+ 'pinecone',
11
+ 'qdrant',
12
+ 'weaviate',
13
+ 'chroma',
14
+ 'milvus',
15
+ 'pgvector',
16
+ 'memory',
17
+ ] as const;
18
+
19
+ export const VECTOR_DISTANCE_METRICS = ['cosine', 'euclidean', 'dotproduct'] as const;
20
+
21
+ export const MESSAGE_BROKER_TYPES = [
22
+ 'rabbitmq',
23
+ 'kafka',
24
+ 'aws_sqs',
25
+ 'redis',
26
+ 'google_pubsub',
27
+ 'nats',
28
+ ] as const;
29
+
30
+ export const JOB_EVENT_TYPES = [
31
+ 'action',
32
+ 'notification',
33
+ 'database_action',
34
+ 'database_operation',
35
+ 'graph_action',
36
+ 'graph_operation',
37
+ 'workflow',
38
+ 'storage',
39
+ ] as const;
40
+
41
+ export const DATABASE_ACTION_TYPES = [
42
+ 'read',
43
+ 'count',
44
+ 'create',
45
+ 'update',
46
+ 'delete',
47
+ 'aggregate',
48
+ ] as const;
49
+
50
+ export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
51
+
52
+ export const NOTIFIERS = ['email', 'sms', 'push', 'callback'] as const;
@@ -0,0 +1,8 @@
1
+ export { isInteractive, promptForm, promptKey, promptTag, type InteractiveOpts } from './prompt.js';
2
+ export {
3
+ genericComponentSchema,
4
+ getFormSchema,
5
+ resolveResourceSchema,
6
+ schemaKeyForEntity,
7
+ schemaKeyForResource,
8
+ } from './schemas.js';