@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,16 @@
1
+ export declare function runWorkspacesList(opts: {
2
+ profile?: string;
3
+ json?: boolean;
4
+ refresh?: boolean;
5
+ }): Promise<void>;
6
+ export declare function runWorkspacesCurrent(json: boolean): void;
7
+ export declare function runWorkspacesUse(selector: string | undefined, opts: {
8
+ profile?: string;
9
+ dir?: string;
10
+ json?: boolean;
11
+ }): Promise<void>;
12
+ /** Refresh workspace list from API and update login cache */
13
+ export declare function runWorkspacesRefresh(opts: {
14
+ profile?: string;
15
+ json?: boolean;
16
+ }): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { applyActiveWorkspace, fetchWorkspaces, getActiveWorkspace, printWorkspaceTable, promptWorkspaceSelection, selectAndApplyWorkspace, workspaceId, workspaceLabel, } from '../lib/workspaces.js';
2
+ import { printJson, success } from '../lib/output.js';
3
+ export async function runWorkspacesList(opts) {
4
+ const workspaces = await fetchWorkspaces(opts.profile);
5
+ const active = getActiveWorkspace();
6
+ if (opts.json) {
7
+ printJson({
8
+ active_workspace_id: active?.id ?? null,
9
+ active_workspace_name: active?.name ?? null,
10
+ workspaces,
11
+ }, true);
12
+ return;
13
+ }
14
+ if (active) {
15
+ console.log(`Active workspace: ${active.name ?? active.id}`);
16
+ console.log(` ${active.id}\n`);
17
+ }
18
+ printWorkspaceTable(workspaces, active?.id);
19
+ }
20
+ export function runWorkspacesCurrent(json) {
21
+ const active = getActiveWorkspace();
22
+ if (!active) {
23
+ if (json) {
24
+ printJson({ active: null }, true);
25
+ return;
26
+ }
27
+ console.log('No active workspace. Run: ductape login or ductape workspaces use');
28
+ return;
29
+ }
30
+ if (json) {
31
+ printJson({ active_workspace_id: active.id, active_workspace_name: active.name }, true);
32
+ return;
33
+ }
34
+ console.log(`Active workspace: ${active.name ?? active.id}`);
35
+ console.log(` ${active.id}`);
36
+ }
37
+ export async function runWorkspacesUse(selector, opts) {
38
+ const workspaces = await fetchWorkspaces(opts.profile);
39
+ let chosen;
40
+ if (!selector) {
41
+ chosen = await promptWorkspaceSelection(workspaces, {
42
+ message: 'Switch workspace:',
43
+ });
44
+ await applyActiveWorkspace(chosen, { syncProject: true, dir: opts.dir });
45
+ }
46
+ else {
47
+ chosen = await selectAndApplyWorkspace({
48
+ profile: opts.profile,
49
+ selector,
50
+ skipPrompt: true,
51
+ syncProject: true,
52
+ dir: opts.dir,
53
+ });
54
+ }
55
+ const id = workspaceId(chosen);
56
+ const name = workspaceLabel(chosen);
57
+ if (opts.json) {
58
+ printJson({ active_workspace_id: id, active_workspace_name: name }, true);
59
+ return;
60
+ }
61
+ success(`Switched to workspace: ${name} (${id})`);
62
+ }
63
+ /** Refresh workspace list from API and update login cache */
64
+ export async function runWorkspacesRefresh(opts) {
65
+ const workspaces = await fetchWorkspaces(opts.profile);
66
+ if (opts.json) {
67
+ printJson({ workspaces, count: workspaces.length }, true);
68
+ return;
69
+ }
70
+ success(`Refreshed ${workspaces.length} workspace(s) from API`);
71
+ await runWorkspacesList({ profile: opts.profile, json: false });
72
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import path from 'node:path';
6
+ import { runLogin } from './commands/login.js';
7
+ import { runLogout } from './commands/logout.js';
8
+ import { runWhoami } from './commands/whoami.js';
9
+ import { runProfilesList, runProfilesUse } from './commands/profiles.js';
10
+ import { runLink } from './commands/link.js';
11
+ import { runUnlink } from './commands/unlink.js';
12
+ import { runInit } from './commands/init.js';
13
+ import { runInstall } from './commands/install.js';
14
+ import { runStart, runStop, runStatus } from './commands/platform.js';
15
+ import { runResourceCrud, runResourcesList } from './commands/resources.js';
16
+ import { runCloud } from './commands/cloud.js';
17
+ import { runDb, runDbContext } from './commands/db.js';
18
+ import { runGraph } from './commands/graph.js';
19
+ import { runSecrets } from './commands/secrets.js';
20
+ import { runWorkspacesCurrent, runWorkspacesList, runWorkspacesRefresh, runWorkspacesUse, } from './commands/workspaces.js';
21
+ import { runGeneratePayload, runGenerateSnippet } from './commands/generate.js';
22
+ import { runCompletion } from './commands/completion.js';
23
+ import { runProducts, runProductApps } from './commands/products.js';
24
+ import { runApps } from './commands/apps.js';
25
+ import { runAppsImport } from './commands/apps-import.js';
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ function wrap(fn) {
30
+ return (...args) => {
31
+ Promise.resolve(fn(...args)).catch((err) => {
32
+ console.error(err instanceof Error ? err.message : err);
33
+ process.exit(1);
34
+ });
35
+ };
36
+ }
37
+ const program = new Command();
38
+ program
39
+ .name('ductape')
40
+ .description('Ductape CLI — local platform, auth, project linking, and resource CRUD via Workbench-compatible encrypted proxy')
41
+ .version(pkg.version);
42
+ program
43
+ .command('login')
44
+ .description('Log in (email/password, OAuth token, or browser OAuth)')
45
+ .option('-e, --email <email>')
46
+ .option('-p, --password <password>')
47
+ .option('--token <token>', 'OAuth callback token from browser redirect URL')
48
+ .option('--browser [provider]', 'Open browser for Google OAuth (google|github)')
49
+ .option('--profile <name>', 'API profile', 'cloud')
50
+ .option('-w, --workspace <id>', 'Workspace index, name, or id (skips interactive picker)')
51
+ .option('--skip-workspace-select', 'Keep prior active workspace or default without prompting')
52
+ .action(wrap((opts) => runLogin({
53
+ email: opts.email,
54
+ password: opts.password,
55
+ profile: opts.profile,
56
+ token: opts.token,
57
+ browser: opts.browser === true ? 'google' : opts.browser,
58
+ workspace: opts.workspace,
59
+ skipWorkspaceSelect: Boolean(opts.skipWorkspaceSelect),
60
+ })));
61
+ program.command('logout').description('Clear local credentials').action(wrap(runLogout));
62
+ program
63
+ .command('whoami')
64
+ .description('Show login and linked project')
65
+ .option('--json', 'JSON output')
66
+ .action(wrap((opts) => runWhoami(Boolean(opts.json))));
67
+ program
68
+ .command('install')
69
+ .description('Check Docker/Node prerequisites and prepare platform .env')
70
+ .action(wrap(runInstall));
71
+ const profiles = program.command('profiles').description('Manage API profiles (cloud / local)');
72
+ profiles
73
+ .command('list')
74
+ .option('--json', 'JSON output')
75
+ .action(wrap((opts) => runProfilesList(Boolean(opts.json))));
76
+ profiles.command('use <name>').action(wrap((name) => runProfilesUse(name)));
77
+ const workspaces = program
78
+ .command('workspaces')
79
+ .description('List and switch active workspace (workspace_id is stored globally at login)');
80
+ workspaces
81
+ .command('list')
82
+ .description('List workspaces (marks active)')
83
+ .option('--profile <name>')
84
+ .option('--json', 'JSON output')
85
+ .action(wrap((opts) => runWorkspacesList({ profile: opts.profile, json: opts.json })));
86
+ workspaces
87
+ .command('current')
88
+ .description('Show active workspace')
89
+ .option('--json', 'JSON output')
90
+ .action(wrap((opts) => runWorkspacesCurrent(Boolean(opts.json))));
91
+ workspaces
92
+ .command('use [selector]')
93
+ .description('Switch active workspace (interactive picker). Updates linked .ductape/config.json if present')
94
+ .option('--profile <name>')
95
+ .option('-C, --dir <path>', 'Project directory to sync', process.cwd())
96
+ .option('--json', 'JSON output')
97
+ .action(wrap((selector, opts) => runWorkspacesUse(selector, opts)));
98
+ workspaces
99
+ .command('switch [selector]')
100
+ .description('Alias for workspaces use')
101
+ .option('--profile <name>')
102
+ .option('-C, --dir <path>', 'Project directory to sync', process.cwd())
103
+ .option('--json', 'JSON output')
104
+ .action(wrap((selector, opts) => runWorkspacesUse(selector, opts)));
105
+ workspaces
106
+ .command('refresh')
107
+ .description('Refresh workspace list from API')
108
+ .option('--profile <name>')
109
+ .option('--json', 'JSON output')
110
+ .action(wrap((opts) => runWorkspacesRefresh(opts)));
111
+ program
112
+ .command('link')
113
+ .option('-w, --workspace <id>')
114
+ .option('--product <tag>')
115
+ .option('--env <slug>')
116
+ .option('--profile <name>')
117
+ .option('-C, --dir <path>', 'Project directory', process.cwd())
118
+ .action(wrap((opts) => runLink({
119
+ workspace: opts.workspace,
120
+ product: opts.product,
121
+ env: opts.env,
122
+ profile: opts.profile,
123
+ dir: opts.dir,
124
+ })));
125
+ program
126
+ .command('unlink')
127
+ .option('-C, --dir <path>', 'Project directory', process.cwd())
128
+ .action(wrap((opts) => runUnlink({ dir: opts.dir })));
129
+ program
130
+ .command('init')
131
+ .option('--link', 'Run link after init')
132
+ .option('-w, --workspace <id>')
133
+ .option('--product <tag>')
134
+ .option('--env <slug>')
135
+ .option('--language <lang>', 'typescript|python|go|dotnet')
136
+ .action(wrap((opts) => runInit({
137
+ link: Boolean(opts.link),
138
+ workspace: opts.workspace,
139
+ product: opts.product,
140
+ env: opts.env,
141
+ language: opts.language,
142
+ })));
143
+ program
144
+ .command('start')
145
+ .description('Start platform via docker compose')
146
+ .option('--backend-only', 'Backend without workbench UI')
147
+ .option('--remote', 'Workbench UI only, built against cloud API (no local Nest/Mongo/Kafka)')
148
+ .option('--build', 'Build images before start')
149
+ .action(wrap((opts) => runStart({
150
+ backendOnly: Boolean(opts.backendOnly),
151
+ remote: Boolean(opts.remote),
152
+ build: Boolean(opts.build),
153
+ })));
154
+ program.command('stop').action(wrap(runStop));
155
+ program.command('status').option('--json', 'JSON output').action(wrap((opts) => runStatus(Boolean(opts.json))));
156
+ const products = program
157
+ .command('products')
158
+ .description('Workspace products CRUD (REST + sdk-proxy update)');
159
+ products
160
+ .command('<verb>')
161
+ .description('list | get | create | update | delete')
162
+ .option('--profile <name>')
163
+ .option('--status <status>', 'Filter for list: all, active, draft, …', 'all')
164
+ .option('--id <id>', 'Product _id (get, delete)')
165
+ .option('-t, --tag <tag>', 'Product tag (get, update)')
166
+ .option('-f, --file <path>', 'JSON body (create, update)')
167
+ .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
168
+ .option('--no-interactive', 'Require -f JSON for create/update')
169
+ .option('--json', 'JSON output')
170
+ .action(wrap((verb, opts) => runProducts(verb, opts, [])));
171
+ const productApps = products.command('apps').description('Apps connected to a product');
172
+ productApps
173
+ .command('list')
174
+ .option('--profile <name>')
175
+ .option('--product <id>', 'Product _id')
176
+ .option('--json', 'JSON output')
177
+ .action(wrap((opts) => runProductApps('list', opts, [])));
178
+ const apps = program.command('apps').description('Workspace integration apps CRUD (REST)');
179
+ apps
180
+ .command('<verb>')
181
+ .description('list | get | create | update | delete')
182
+ .option('--profile <name>')
183
+ .option('--status <status>', 'Filter for list', 'all')
184
+ .option('--id <id>', 'App _id')
185
+ .option('-t, --tag <tag>', 'App tag (get; update with --proxy)')
186
+ .option('--proxy', 'Update via sdk-proxy (use with --tag)')
187
+ .option('-f, --file <path>', 'JSON body')
188
+ .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
189
+ .option('--no-interactive', 'Require -f JSON for create/update')
190
+ .option('--json', 'JSON output')
191
+ .action(wrap((verb, opts) => runApps(verb, opts, [])));
192
+ apps
193
+ .command('import <file>')
194
+ .description('Import Postman v2.1 or OpenAPI 3.0 into a new or existing app')
195
+ .requiredOption('-t, --type <format>', 'postman | openapi (Postman Collection v2.1 or OpenAPI 3.0)')
196
+ .option('--profile <name>')
197
+ .option('--app-id <id>', 'Update existing app by _id')
198
+ .option('--app-tag <tag>', 'Update existing app by tag')
199
+ .option('--update', 'Replace/update if app already exists')
200
+ .option('--version <tag>', 'App version tag', '0.0.1')
201
+ .option('--json', 'JSON output')
202
+ .action(wrap((file, opts) => runAppsImport(file, {
203
+ profile: opts.profile,
204
+ type: opts.type,
205
+ appId: opts.appId,
206
+ appTag: opts.appTag,
207
+ update: Boolean(opts.update),
208
+ version: opts.version,
209
+ json: opts.json,
210
+ })));
211
+ const resources = program.command('resources').description('Component CRUD (sdk-proxy)');
212
+ resources.command('types').option('--json', 'JSON output').action(wrap((opts) => runResourcesList(Boolean(opts.json))));
213
+ resources
214
+ .command('<type> <verb>')
215
+ .option('-t, --tag <tag>')
216
+ .option('-f, --file <path>', 'JSON body (or use interactive prompts)')
217
+ .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
218
+ .option('--no-interactive', 'Require -f JSON for create/update/connect')
219
+ .option('--json', 'JSON output')
220
+ .action(wrap((type, verb, opts) => runResourceCrud(type, verb, opts, [])));
221
+ const cloud = program.command('cloud').description('Cloud connections & resources');
222
+ cloud
223
+ .command('connections <verb> [id]')
224
+ .option('-f, --file <path>')
225
+ .option('--json', 'JSON output')
226
+ .action(wrap((verb, id, opts) => runCloud('connections', verb, { id, file: opts.file, json: opts.json }, [])));
227
+ cloud
228
+ .command('resources <verb>')
229
+ .option('-f, --file <path>')
230
+ .option('--json', 'JSON output')
231
+ .action(wrap((verb, opts) => runCloud('resources', verb, { file: opts.file, json: opts.json }, [])));
232
+ const db = program.command('db').description('Database runtime (db-proxy)');
233
+ db.command('context').option('--json', 'JSON output').action(wrap((opts) => runDbContext(Boolean(opts.json))));
234
+ db
235
+ .command('<verb>')
236
+ .option('-f, --file <path>')
237
+ .option('--json', 'JSON output')
238
+ .action(wrap((verb, opts) => runDb(verb, { file: opts.file, json: opts.json })));
239
+ const graph = program.command('graph').description('Graph runtime (graph-proxy)');
240
+ graph
241
+ .command('<verb>')
242
+ .option('-f, --file <path>')
243
+ .option('--json', 'JSON output')
244
+ .action(wrap((verb, opts) => runGraph(verb, { file: opts.file, json: opts.json })));
245
+ program
246
+ .command('secrets <verb>')
247
+ .description('Workspace secrets CRUD')
248
+ .option('-k, --key <key>')
249
+ .option('-f, --file <path>')
250
+ .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
251
+ .option('--no-interactive', 'Require -f JSON for create/update')
252
+ .option('--json', 'JSON output')
253
+ .action(wrap((verb, opts) => runSecrets(verb, opts)));
254
+ const generate = program.command('generate').description('Payload templates & code snippets');
255
+ generate
256
+ .command('payload <family> <method>')
257
+ .option('-f, --file <path>', 'targets JSON')
258
+ .option('--json', 'JSON output')
259
+ .action(wrap((family, method, opts) => runGeneratePayload({ family, method, file: opts.file, json: opts.json })));
260
+ generate
261
+ .command('snippet <family> <method>')
262
+ .requiredOption('-l, --language <lang>', 'typescript|python|go|dotnet')
263
+ .option('-f, --file <path>')
264
+ .option('-o, --out <path>', 'Write snippet to file')
265
+ .option('--json', 'JSON output')
266
+ .action(wrap((family, method, opts) => runGenerateSnippet({
267
+ family,
268
+ method,
269
+ language: opts.language,
270
+ file: opts.file,
271
+ json: opts.json,
272
+ out: opts.out,
273
+ })));
274
+ program
275
+ .command('completion <shell>')
276
+ .description('Print shell completions (bash|zsh)')
277
+ .action(wrap((shell) => {
278
+ if (shell !== 'bash' && shell !== 'zsh' && shell !== 'fish') {
279
+ throw new Error('Shell must be bash, zsh, or fish');
280
+ }
281
+ runCompletion(shell);
282
+ }));
283
+ program.parse();
@@ -0,0 +1,18 @@
1
+ import type { Credentials } from './config.js';
2
+ export interface ApiEnvelope<T> {
3
+ status?: boolean;
4
+ data?: T;
5
+ message?: string;
6
+ }
7
+ export declare class ApiClient {
8
+ private readonly apiUrl;
9
+ private readonly token?;
10
+ constructor(apiUrl: string, token?: string | undefined);
11
+ static forProfile(profileName?: string, creds?: Credentials | null): ApiClient;
12
+ request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
13
+ post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
14
+ put<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
15
+ delete<T>(path: string, headers?: Record<string, string>): Promise<T>;
16
+ get<T>(path: string, headers?: Record<string, string>): Promise<T>;
17
+ getPath<T>(path: string, query?: Record<string, string>): Promise<T>;
18
+ }
@@ -0,0 +1,65 @@
1
+ import { getApiUrl } from './config.js';
2
+ export class ApiClient {
3
+ apiUrl;
4
+ token;
5
+ constructor(apiUrl, token) {
6
+ this.apiUrl = apiUrl;
7
+ this.token = token;
8
+ }
9
+ static forProfile(profileName, creds) {
10
+ return new ApiClient(getApiUrl(profileName), creds?.auth_token);
11
+ }
12
+ async request(method, path, body, extraHeaders) {
13
+ const url = `${this.apiUrl}${path.startsWith('/') ? path : `/${path}`}`;
14
+ const headers = {
15
+ 'Content-Type': 'application/json',
16
+ ...extraHeaders,
17
+ };
18
+ if (this.token) {
19
+ headers.Authorization = `Bearer ${this.token}`;
20
+ }
21
+ const res = await fetch(url, {
22
+ method,
23
+ headers,
24
+ body: body !== undefined ? JSON.stringify(body) : undefined,
25
+ });
26
+ const text = await res.text();
27
+ let parsed;
28
+ try {
29
+ parsed = text ? JSON.parse(text) : {};
30
+ }
31
+ catch {
32
+ throw new Error(`Invalid JSON from ${url}: ${text.slice(0, 200)}`);
33
+ }
34
+ if (!res.ok) {
35
+ const msg = parsed.message ??
36
+ parsed.error ??
37
+ `HTTP ${res.status}`;
38
+ throw new Error(msg);
39
+ }
40
+ if (typeof parsed.status === 'boolean' && !parsed.status) {
41
+ throw new Error(parsed.message ?? 'Request failed');
42
+ }
43
+ const data = parsed.data;
44
+ if (data && typeof data === 'object' && 'result' in data) {
45
+ return data.result;
46
+ }
47
+ return (data ?? parsed);
48
+ }
49
+ post(path, body, headers) {
50
+ return this.request('POST', path, body, headers);
51
+ }
52
+ put(path, body, headers) {
53
+ return this.request('PUT', path, body, headers);
54
+ }
55
+ delete(path, headers) {
56
+ return this.request('DELETE', path, undefined, headers);
57
+ }
58
+ get(path, headers) {
59
+ return this.request('GET', path, undefined, headers);
60
+ }
61
+ getPath(path, query) {
62
+ const qs = query ? `?${new URLSearchParams(query).toString()}` : '';
63
+ return this.get(`${path}${qs}`);
64
+ }
65
+ }
@@ -0,0 +1,15 @@
1
+ import type { WorkspaceContext } from './workspace-context.js';
2
+ /** Values accepted by POST /apps/v1/import `type` field */
3
+ export declare const IMPORT_DOC_TYPES: {
4
+ readonly postman: "PostmanV2.1";
5
+ readonly openapi: "OpenApiV3.0";
6
+ };
7
+ export type ImportFormat = keyof typeof IMPORT_DOC_TYPES;
8
+ export declare function parseImportFormat(value: string): ImportFormat;
9
+ export declare function importAppFromFile(ctx: WorkspaceContext, opts: {
10
+ filePath: string;
11
+ format: ImportFormat;
12
+ appId?: string;
13
+ update?: boolean;
14
+ version?: string;
15
+ }): Promise<unknown>;
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Values accepted by POST /apps/v1/import `type` field */
4
+ export const IMPORT_DOC_TYPES = {
5
+ postman: 'PostmanV2.1',
6
+ openapi: 'OpenApiV3.0',
7
+ };
8
+ export function parseImportFormat(value) {
9
+ const v = value.toLowerCase().replace(/[_\s]/g, '');
10
+ if (v === 'postman' || v === 'postmanv21' || v === 'postman2.1' || v === 'postmanv2.1') {
11
+ return 'postman';
12
+ }
13
+ if (v === 'openapi' || v === 'openapiv30' || v === 'openapi3.0' || v === 'openapi3') {
14
+ return 'openapi';
15
+ }
16
+ throw new Error(`Unknown import type "${value}". Use: postman (Postman Collection v2.1) or openapi (OpenAPI 3.0)`);
17
+ }
18
+ function uploadFilename(filePath) {
19
+ const base = path.basename(filePath);
20
+ if (base.endsWith('.yaml') || base.endsWith('.yml'))
21
+ return base;
22
+ if (base.endsWith('.json'))
23
+ return base;
24
+ return `${base}.json`;
25
+ }
26
+ export async function importAppFromFile(ctx, opts) {
27
+ const abs = path.resolve(opts.filePath);
28
+ if (!fs.existsSync(abs))
29
+ throw new Error(`File not found: ${abs}`);
30
+ const buffer = fs.readFileSync(abs);
31
+ const filename = uploadFilename(abs);
32
+ const form = new FormData();
33
+ form.append('file', new Blob([buffer]), filename);
34
+ form.append('workspace_id', ctx.workspaceId);
35
+ form.append('public_key', ctx.credentials.public_key);
36
+ form.append('user_id', ctx.credentials.user_id);
37
+ form.append('type', IMPORT_DOC_TYPES[opts.format]);
38
+ form.append('version', opts.version ?? '0.0.1');
39
+ form.append('update', opts.update ? 'true' : 'false');
40
+ if (opts.appId)
41
+ form.append('app_id', opts.appId);
42
+ const url = `${ctx.apiUrl}/apps/v1/import`;
43
+ const res = await fetch(url, {
44
+ method: 'POST',
45
+ headers: { Authorization: `Bearer ${ctx.credentials.auth_token}` },
46
+ body: form,
47
+ });
48
+ const text = await res.text();
49
+ let parsed;
50
+ try {
51
+ parsed = text ? JSON.parse(text) : {};
52
+ }
53
+ catch {
54
+ throw new Error(`Invalid JSON from import API: ${text.slice(0, 200)}`);
55
+ }
56
+ if (!res.ok) {
57
+ throw new Error(parsed.message ?? `Import failed (HTTP ${res.status})`);
58
+ }
59
+ if (parsed.status === false) {
60
+ throw new Error(parsed.message ?? 'Import failed');
61
+ }
62
+ return parsed.data ?? parsed;
63
+ }
@@ -0,0 +1,63 @@
1
+ export declare const GLOBAL_DIR: string;
2
+ export declare const CREDENTIALS_PATH: string;
3
+ export declare const GLOBAL_CONFIG_PATH: string;
4
+ export declare const PROJECT_CONFIG_DIR = ".ductape";
5
+ export declare const PROJECT_CONFIG_PATH: string;
6
+ export interface Credentials {
7
+ user_id: string;
8
+ email: string;
9
+ auth_token: string;
10
+ public_key: string;
11
+ firstname?: string;
12
+ lastname?: string;
13
+ workspaces?: Array<{
14
+ workspace_id?: string;
15
+ workspace_name?: string;
16
+ _id?: string;
17
+ name?: string;
18
+ tag?: string;
19
+ default?: boolean;
20
+ }>;
21
+ }
22
+ export interface Profile {
23
+ api_url: string;
24
+ }
25
+ export interface GlobalConfig {
26
+ default_profile: string;
27
+ profiles: Record<string, Profile>;
28
+ /** Set at login or via `ductape workspaces use` */
29
+ active_workspace_id?: string;
30
+ active_workspace_name?: string;
31
+ active_workspace_tag?: string;
32
+ }
33
+ /** Linked project config — use workspace_tag (human-readable); workspace_id is resolved by the CLI. */
34
+ export interface ProjectConfig {
35
+ workspace_tag?: string;
36
+ /** Resolved automatically; do not edit unless debugging */
37
+ workspace_id?: string;
38
+ product_tag: string;
39
+ env_slug: string;
40
+ linked_profile?: string;
41
+ project_id?: string;
42
+ }
43
+ export interface ResolvedProjectConfig extends ProjectConfig {
44
+ workspace_id: string;
45
+ }
46
+ export declare function ensureGlobalDir(): void;
47
+ export declare function loadCredentials(): Credentials | null;
48
+ export declare function saveCredentials(creds: Credentials): void;
49
+ export declare function clearCredentials(): void;
50
+ export declare function getActiveWorkspaceId(): string | null;
51
+ export declare function loadGlobalConfig(): GlobalConfig;
52
+ export declare function saveGlobalConfig(config: GlobalConfig): void;
53
+ export declare function getActiveProfileName(project?: ProjectConfig | null): string;
54
+ export declare function getApiUrl(profileName?: string): string;
55
+ /** Resolve workspace_id from tag (cached workspaces), explicit id, or global active workspace. */
56
+ export declare function resolveWorkspaceId(cfg: ProjectConfig, creds?: Credentials | null): string | null;
57
+ export declare function findProjectConfig(startDir?: string): {
58
+ dir: string;
59
+ config: ProjectConfig;
60
+ } | null;
61
+ export declare function loadProjectConfig(): ResolvedProjectConfig;
62
+ export declare function saveProjectConfig(dir: string, config: ProjectConfig): void;
63
+ export declare function resolvePlatformDir(): string;