@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,76 +0,0 @@
1
- import { spawnSync, spawn } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { HUB_PLATFORM_DIR } from '../lib/config.js';
6
- import { success } from '../lib/output.js';
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const TEMPLATES_DIR = path.resolve(__dirname, '../../templates');
10
-
11
- const HUB_IMAGES = [
12
- 'ductape/platform-api:latest',
13
- 'ductape/platform-workbench:latest',
14
- ] as const;
15
-
16
- function checkCommand(cmd: string, args: string[]): boolean {
17
- const r = spawnSync(cmd, args, { stdio: 'ignore' });
18
- return r.status === 0;
19
- }
20
-
21
- function pull(image: string): Promise<number> {
22
- return new Promise((resolve, reject) => {
23
- console.log(`Pulling ${image} …`);
24
- const child = spawn('docker', ['pull', image], {
25
- stdio: 'inherit',
26
- shell: process.platform === 'win32',
27
- });
28
- child.on('error', reject);
29
- child.on('close', (code) => resolve(code ?? 1));
30
- });
31
- }
32
-
33
- function copyTemplate(name: string, dest: string, overwrite = false): void {
34
- const src = path.join(TEMPLATES_DIR, name);
35
- if (!overwrite && fs.existsSync(dest)) return;
36
- fs.copyFileSync(src, dest);
37
- }
38
-
39
- export async function runInstall(): Promise<void> {
40
- const issues: string[] = [];
41
-
42
- if (!checkCommand('docker', ['--version'])) {
43
- issues.push('Docker is not installed or not on PATH');
44
- } else if (!checkCommand('docker', ['compose', 'version'])) {
45
- issues.push('Docker Compose v2 is required (docker compose)');
46
- }
47
-
48
- if (issues.length > 0) {
49
- console.error('Install check failed:\n' + issues.map((i) => ` - ${i}`).join('\n'));
50
- process.exit(1);
51
- }
52
-
53
- // Pull platform images from Docker Hub
54
- for (const image of HUB_IMAGES) {
55
- const code = await pull(image);
56
- if (code !== 0) {
57
- console.error(`Failed to pull ${image} (exit ${code}). Check your internet connection or run \`docker login\`.`);
58
- process.exit(1);
59
- }
60
- }
61
-
62
- // Write platform files to ~/.ductape/platform/
63
- fs.mkdirSync(HUB_PLATFORM_DIR, { recursive: true });
64
-
65
- copyTemplate('docker-compose.release.yml', path.join(HUB_PLATFORM_DIR, 'docker-compose.yml'), true);
66
- copyTemplate('api-gateway.conf', path.join(HUB_PLATFORM_DIR, 'api-gateway.conf'), true);
67
- copyTemplate('platform.env', path.join(HUB_PLATFORM_DIR, '.env'), false); // don't overwrite existing .env
68
-
69
- success(`Platform installed to ${HUB_PLATFORM_DIR}`);
70
- console.log('');
71
- console.log(' Edit ~/.ductape/platform/.env to add your AWS credentials (needed for cloud connections).');
72
- console.log(' Then run: ductape start');
73
- console.log('');
74
- console.log(' Workbench: http://localhost:4310');
75
- console.log(' API: http://localhost:4311');
76
- }
@@ -1,84 +0,0 @@
1
- import readline from 'node:readline/promises';
2
- import { stdin as input, stdout as output } from 'node:process';
3
- import path from 'node:path';
4
- import {
5
- loadCredentials,
6
- saveProjectConfig,
7
- type ProjectConfig,
8
- } from '../lib/config.js';
9
- import {
10
- fetchWorkspaces,
11
- findWorkspace,
12
- getActiveWorkspace,
13
- promptWorkspaceSelection,
14
- setActiveWorkspace,
15
- workspaceId,
16
- workspaceLabel,
17
- workspaceTag,
18
- type WorkspaceSummary,
19
- } from '../lib/workspaces.js';
20
- import { success } from '../lib/output.js';
21
-
22
- export async function runLink(opts: {
23
- workspace?: string;
24
- product?: string;
25
- env?: string;
26
- profile?: string;
27
- dir?: string;
28
- }): Promise<void> {
29
- const creds = loadCredentials();
30
- if (!creds) throw new Error('Not logged in. Run: ductape login');
31
-
32
- const rl = readline.createInterface({ input, output });
33
-
34
- let workspaceSummary: WorkspaceSummary;
35
- const selector = opts.workspace?.trim();
36
- if (!selector) {
37
- const active = getActiveWorkspace();
38
- if (active?.id) {
39
- console.log(`Using active workspace: ${active.name ?? active.tag ?? active.id}`);
40
- workspaceSummary = {
41
- workspace_id: active.id,
42
- workspace_name: active.name,
43
- tag: active.tag,
44
- };
45
- } else {
46
- const workspaces = await fetchWorkspaces(opts.profile);
47
- if (workspaces.length === 0) {
48
- throw new Error('No workspaces on this account. Create one in the Workbench first.');
49
- } else {
50
- workspaceSummary = await promptWorkspaceSelection(workspaces, {
51
- message: 'Link project to workspace:',
52
- });
53
- }
54
- }
55
- } else {
56
- const workspaces = await fetchWorkspaces(opts.profile);
57
- const match = findWorkspace(workspaces, selector);
58
- if (!match) {
59
- throw new Error(`Workspace not found: ${selector}`);
60
- }
61
- workspaceSummary = match;
62
- setActiveWorkspace(workspaceId(match), workspaceLabel(match), workspaceTag(match));
63
- }
64
-
65
- const workspaceIdValue = workspaceId(workspaceSummary);
66
- const workspaceTagValue = workspaceTag(workspaceSummary);
67
-
68
- const productTag = opts.product ?? (await rl.question('Product tag: '));
69
- const envSlug = opts.env ?? (await rl.question('Environment slug (e.g. dev): '));
70
- rl.close();
71
-
72
- const targetDir = path.resolve(opts.dir ?? process.cwd());
73
- const config: ProjectConfig = {
74
- workspace_tag: workspaceTagValue,
75
- workspace_id: workspaceIdValue,
76
- product_tag: productTag.trim(),
77
- env_slug: envSlug.trim(),
78
- linked_profile: opts.profile,
79
- };
80
-
81
- saveProjectConfig(targetDir, config);
82
- setActiveWorkspace(workspaceIdValue, workspaceLabel(workspaceSummary), workspaceTagValue);
83
- success(`Linked project in ${path.join(targetDir, '.ductape/config.json')}`);
84
- }
@@ -1,145 +0,0 @@
1
- import inputPrompt from '@inquirer/input';
2
- import passwordPrompt from '@inquirer/password';
3
- import { spawn } from 'node:child_process';
4
- import { getApiUrl, getActiveProfileName, saveCredentials, type Credentials } from '../lib/config.js';
5
- import { parseJsonResponse } from '../lib/http.js';
6
- import { finalizeLoginWorkspace } from '../lib/workspaces.js';
7
- import { success } from '../lib/output.js';
8
-
9
- interface LoginResult {
10
- _id: string;
11
- email: string;
12
- firstname?: string;
13
- lastname?: string;
14
- auth_token: string;
15
- public_key: string;
16
- workspaces?: Credentials['workspaces'];
17
- requires_verification?: boolean;
18
- }
19
-
20
- function saveFromResult(user: LoginResult): void {
21
- saveCredentials({
22
- user_id: user._id,
23
- email: user.email,
24
- auth_token: user.auth_token,
25
- public_key: user.public_key,
26
- firstname: user.firstname,
27
- lastname: user.lastname,
28
- workspaces: user.workspaces,
29
- });
30
- }
31
-
32
- async function exchangeOAuthToken(apiUrl: string, token: string): Promise<LoginResult> {
33
- const res = await fetch(`${apiUrl}/users/v1/auth/oauth-session`, {
34
- method: 'POST',
35
- headers: { 'Content-Type': 'application/json' },
36
- body: JSON.stringify({ token }),
37
- });
38
- const body = await parseJsonResponse<{ message?: string; data?: { result?: LoginResult } & LoginResult }>(
39
- res,
40
- `${apiUrl}/users/v1/auth/oauth-session`,
41
- );
42
- if (!res.ok) throw new Error(body.message ?? `OAuth exchange failed (${res.status})`);
43
- const user = (body.data?.result ?? body.data) as LoginResult;
44
- if (!user?.auth_token) throw new Error(body.message ?? 'Invalid OAuth token');
45
- return user;
46
- }
47
-
48
- function openBrowser(url: string): void {
49
- const cmd =
50
- process.platform === 'darwin'
51
- ? 'open'
52
- : process.platform === 'win32'
53
- ? 'start'
54
- : 'xdg-open';
55
- spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
56
- }
57
-
58
- export async function runLogin(opts: {
59
- email?: string;
60
- password?: string;
61
- profile?: string;
62
- token?: string;
63
- browser?: 'google' | 'github';
64
- workspace?: string;
65
- skipWorkspaceSelect?: boolean;
66
- }): Promise<void> {
67
- const profileName = opts.profile ?? getActiveProfileName();
68
- const apiUrl = getApiUrl(opts.profile);
69
-
70
- if (opts.token) {
71
- const user = await exchangeOAuthToken(apiUrl, opts.token);
72
- saveFromResult(user);
73
- success(`Logged in as ${user.email} (OAuth)`);
74
- await finalizeLoginWorkspace({
75
- profile: opts.profile,
76
- workspace: opts.workspace,
77
- skipPrompt: opts.skipWorkspaceSelect,
78
- });
79
- return;
80
- }
81
-
82
- if (opts.browser) {
83
- const path =
84
- opts.browser === 'github' ? '/users/v1/auth/github' : '/users/v1/auth/google';
85
- const authUrl = `${apiUrl}${path}`;
86
- console.log('Opening browser for OAuth login…');
87
- console.log(`If the browser does not open, visit:\n ${authUrl}\n`);
88
- console.log(
89
- 'After sign-in you will be redirected with ?loggedIn=true&token=… in the URL.\n' +
90
- 'Copy the token value and run:\n' +
91
- ' ductape login --token "<paste-token-here>"\n',
92
- );
93
- openBrowser(authUrl);
94
- return;
95
- }
96
-
97
- let email = opts.email;
98
- let password = opts.password;
99
-
100
- if (!email || !password) {
101
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
102
- throw new Error(
103
- 'Interactive login requires a TTY. Pass --email and --password, or use --token / --browser.',
104
- );
105
- }
106
- email = email ?? (await inputPrompt({ message: 'Email' }));
107
- password =
108
- password ??
109
- (await passwordPrompt({
110
- message: 'Password',
111
- mask: '*',
112
- }));
113
- }
114
-
115
- const loginUrl = `${apiUrl}/users/v1/login`;
116
- const res = await fetch(loginUrl, {
117
- method: 'POST',
118
- headers: { 'Content-Type': 'application/json' },
119
- body: JSON.stringify({ email, password }),
120
- });
121
-
122
- const body = await parseJsonResponse<{ message?: string; data?: { result?: LoginResult } & LoginResult }>(
123
- res,
124
- loginUrl,
125
- );
126
- if (!res.ok) {
127
- throw new Error(
128
- body.message ?? `Login failed (HTTP ${res.status}) — profile "${profileName}", ${loginUrl}`,
129
- );
130
- }
131
-
132
- const user = (body.data?.result ?? body.data) as LoginResult;
133
- if (!user?.auth_token) throw new Error(body.message ?? 'Login failed');
134
- if (user.requires_verification) {
135
- throw new Error('Account requires email verification. Complete verification in the Workbench first.');
136
- }
137
-
138
- saveFromResult(user);
139
- success(`Logged in as ${user.email}`);
140
- await finalizeLoginWorkspace({
141
- profile: opts.profile,
142
- workspace: opts.workspace,
143
- skipPrompt: opts.skipWorkspaceSelect,
144
- });
145
- }
@@ -1,7 +0,0 @@
1
- import { clearCredentials } from '../lib/config.js';
2
- import { success } from '../lib/output.js';
3
-
4
- export function runLogout(): void {
5
- clearCredentials();
6
- success('Logged out');
7
- }
@@ -1,123 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import path from 'node:path';
3
- import { getApiUrl, loadGlobalConfig, resolvePlatformDir } from '../lib/config.js';
4
- import { printJson, success } from '../lib/output.js';
5
-
6
- const WORKBENCH_ONLY_COMPOSE = [
7
- '-f',
8
- 'docker-compose.yml',
9
- '-f',
10
- 'docker-compose.workbench.yml',
11
- ] as const;
12
-
13
- function normalizeViteApiBase(url: string): string {
14
- return url.endsWith('/') ? url : `${url}/`;
15
- }
16
-
17
- function runCompose(args: string[], cwd: string): Promise<number> {
18
- return new Promise((resolve, reject) => {
19
- const child = spawn('docker', ['compose', ...args], {
20
- cwd,
21
- stdio: 'inherit',
22
- shell: process.platform === 'win32',
23
- });
24
- child.on('error', reject);
25
- child.on('close', (code) => resolve(code ?? 1));
26
- });
27
- }
28
-
29
- export async function runStart(opts: {
30
- backendOnly?: boolean;
31
- build?: boolean;
32
- remote?: boolean;
33
- }): Promise<void> {
34
- const platformDir = resolvePlatformDir();
35
-
36
- if (opts.remote) {
37
- const apiBase = normalizeViteApiBase(getApiUrl('cloud'));
38
- const composeArgs = [...WORKBENCH_ONLY_COMPOSE];
39
- if (opts.build) {
40
- const buildCode = await runCompose(
41
- [
42
- ...composeArgs,
43
- 'build',
44
- '--build-arg',
45
- `VITE_API_BASE_URL=${apiBase}`,
46
- '--build-arg',
47
- 'VITE_APP_ENV=self',
48
- 'workbench',
49
- ],
50
- platformDir,
51
- );
52
- if (buildCode !== 0) throw new Error(`docker compose build exited with ${buildCode}`);
53
- }
54
- const code = await runCompose([...composeArgs, 'up', '-d', 'workbench'], platformDir);
55
- if (code !== 0) throw new Error(`docker compose exited with ${code}`);
56
- success(`Workbench UI: http://localhost:4310 (API → ${apiBase})`);
57
- return;
58
- }
59
-
60
- const composeArgs = opts.backendOnly
61
- ? ['-f', 'docker-compose.yml', '-f', 'docker-compose.backend.yml', 'up', '-d']
62
- : ['up', '-d'];
63
- if (opts.build) {
64
- const upIdx = composeArgs.indexOf('up');
65
- composeArgs.splice(upIdx + 1, 0, '--build');
66
- }
67
- const code = await runCompose(composeArgs, platformDir);
68
- if (code !== 0) throw new Error(`docker compose exited with ${code}`);
69
- success('Ductape platform started (see platform/README.md for URLs)');
70
- }
71
-
72
- export async function runStop(): Promise<void> {
73
- const platformDir = resolvePlatformDir();
74
- const code = await runCompose(['down'], platformDir);
75
- if (code !== 0) throw new Error(`docker compose exited with ${code}`);
76
- success('Ductape platform stopped');
77
- }
78
-
79
- async function probeHealth(url: string): Promise<{
80
- url: string;
81
- ok: boolean;
82
- status?: number;
83
- error?: string;
84
- hint?: string;
85
- }> {
86
- try {
87
- const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
88
- const hint =
89
- res.status === 502
90
- ? 'nginx returned 502 Bad Gateway — the proxy container is usually not running or not listening yet. Try: docker compose ps proxy && docker compose logs proxy --tail 50'
91
- : res.status === 404
92
- ? 'route not found — check api-gateway nginx config vs Nest proxy routes'
93
- : undefined;
94
- return { url, ok: res.ok, status: res.status, hint };
95
- } catch (e) {
96
- return {
97
- url,
98
- ok: false,
99
- error: e instanceof Error ? e.message : String(e),
100
- hint: 'cannot reach host — run `ductape start` (full stack) or ensure port 4311 / 8020 is up',
101
- };
102
- }
103
- }
104
-
105
- export async function runStatus(json: boolean): Promise<void> {
106
- const global = loadGlobalConfig();
107
- const localUrl = global.profiles.local?.api_url ?? 'http://localhost:4311/api';
108
- const localBase = localUrl.replace(/\/$/, '');
109
- const checks: Record<string, unknown> = {
110
- profiles: global.profiles,
111
- platform_dir: resolvePlatformDir(),
112
- workbench_ui: 'http://localhost:4310',
113
- };
114
-
115
- checks.local_proxy_health = await probeHealth(`${localBase}/proxy/v1/sdk-proxy/health`);
116
- checks.local_proxy_direct = await probeHealth(
117
- 'http://localhost:8020/proxy/v1/sdk-proxy/health',
118
- );
119
-
120
- checks.cloud_api = getApiUrl('cloud');
121
-
122
- printJson(checks, json);
123
- }
@@ -1,228 +0,0 @@
1
- import { schemaKeyForEntity } from '../lib/forms/index.js';
2
- import { isInteractive, promptTag } from '../lib/forms/prompt.js';
3
- import { toInteractiveOpts, type CommandInteractiveFlags } from '../lib/interactive-opts.js';
4
- import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
5
- import {
6
- createProduct,
7
- deleteProduct,
8
- getProduct,
9
- listProductApps,
10
- listProducts,
11
- updateProduct,
12
- } from '../lib/platform-api.js';
13
- import { printJson } from '../lib/output.js';
14
- import { requireWorkspaceContext } from '../lib/workspace-context.js';
15
- import { requireSession, getSdkProxy } from '../lib/proxy/context.js';
16
-
17
- const VERBS = ['list', 'get', 'create', 'update', 'delete'] as const;
18
- type Verb = (typeof VERBS)[number];
19
-
20
- export async function runProducts(
21
- verb: string,
22
- opts: {
23
- profile?: string;
24
- status?: string;
25
- id?: string;
26
- tag?: string;
27
- file?: string;
28
- json?: boolean;
29
- } & CommandInteractiveFlags,
30
- extraArgs: string[],
31
- ): Promise<void> {
32
- const v = verb.toLowerCase() as Verb;
33
- if (!VERBS.includes(v)) {
34
- throw new Error(`Unknown products verb "${verb}". Use: ${VERBS.join(', ')}`);
35
- }
36
-
37
- const ctx = requireWorkspaceContext({ profile: opts.profile });
38
- const status = opts.status ?? 'all';
39
- const interactive = toInteractiveOpts(opts);
40
- const schemaKey = schemaKeyForEntity('products', v);
41
- const body =
42
- v === 'create' || v === 'update'
43
- ? await resolveBody({
44
- ...interactive,
45
- file: opts.file,
46
- schemaKey,
47
- patch: v === 'update',
48
- requireBody: v === 'create',
49
- })
50
- : undefined;
51
- const id = opts.id ?? extraArgs[0];
52
- let tag = opts.tag ?? (extraArgs[0] && !opts.id ? extraArgs[0] : undefined);
53
- if (v === 'update' && !tag && isInteractive(interactive)) {
54
- tag = await promptTag('Product tag');
55
- }
56
-
57
- let result: unknown;
58
-
59
- switch (v) {
60
- case 'list':
61
- result = await listProducts(ctx, status);
62
- break;
63
- case 'get':
64
- result = await getProduct(ctx, { id, tag });
65
- break;
66
- case 'create':
67
- if (!body || Object.keys(body).length === 0) {
68
- throw new Error(`create requires a body. ${bodyRequiredHint('products:create')}`);
69
- }
70
- result = await createProduct(ctx, body);
71
- break;
72
- case 'update': {
73
- const productTag = tag ?? (body?.tag as string | undefined);
74
- if (!productTag) throw new Error('update requires --tag <product_tag> (or tag in body)');
75
- const patch = body ?? {};
76
- result = await updateProduct(ctx, productTag, patch);
77
- break;
78
- }
79
- case 'delete':
80
- if (!id) throw new Error('delete requires --id <product_id>');
81
- result = await deleteProduct(ctx, id);
82
- break;
83
- default:
84
- throw new Error(`Unsupported: products ${v}`);
85
- }
86
-
87
- printJson(result, Boolean(opts.json));
88
- }
89
-
90
- export async function runProductApps(
91
- verb: string,
92
- opts: { profile?: string; product?: string; json?: boolean },
93
- extraArgs: string[],
94
- ): Promise<void> {
95
- if (verb.toLowerCase() !== 'list') {
96
- throw new Error('Only supported: ductape products apps list --product <id>');
97
- }
98
-
99
- const ctx = requireWorkspaceContext({ profile: opts.profile });
100
- const productId = opts.product ?? extraArgs[0];
101
- if (!productId) throw new Error('Provide --product <product_id>');
102
-
103
- const result = await listProductApps(ctx, productId);
104
- printJson(result, Boolean(opts.json));
105
- }
106
-
107
- const COMPONENT_KEYS: Record<string, string[]> = {
108
- apps: ['apps'],
109
- databases: ['databases'],
110
- storage: ['storage'],
111
- caches: ['caches'],
112
- graphs: ['graphs'],
113
- vectors: ['vectors', 'vector_databases'],
114
- notifications: ['notifications'],
115
- events: ['message_brokers', 'messageBrokers', 'brokers'],
116
- sessions: ['sessions'],
117
- features: ['features'],
118
- jobs: ['jobs'],
119
- };
120
-
121
- function unwrapProduct(value: unknown): Record<string, unknown> {
122
- if (!value || typeof value !== 'object') return {};
123
- const record = value as Record<string, unknown>;
124
- return record.data && typeof record.data === 'object'
125
- ? (record.data as Record<string, unknown>)
126
- : record;
127
- }
128
-
129
- function summarizeComponent(value: unknown): Record<string, unknown> {
130
- const component = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>;
131
- const summary: Record<string, unknown> = {};
132
- for (const key of ['_id', 'id', 'tag', 'name', 'description', 'type', 'status']) {
133
- if (component[key] !== undefined) summary[key] = component[key];
134
- }
135
- if (Array.isArray(component.envs)) {
136
- summary.envs = component.envs.map((env) => {
137
- const item = (env && typeof env === 'object' ? env : {}) as Record<string, unknown>;
138
- return { slug: item.slug, type: item.type, cloud: item.cloud };
139
- });
140
- }
141
- for (const nestedKey of ['topics', 'messages']) {
142
- if (Array.isArray(component[nestedKey])) {
143
- summary[nestedKey] = (component[nestedKey] as unknown[]).map((nested) => {
144
- const item = (nested && typeof nested === 'object' ? nested : {}) as Record<string, unknown>;
145
- return {
146
- tag: item.tag,
147
- name: item.name,
148
- description: item.description,
149
- };
150
- });
151
- }
152
- }
153
- return summary;
154
- }
155
-
156
- export async function runProductComponents(
157
- verb: string,
158
- opts: { profile?: string; tag?: string; type?: string; json?: boolean },
159
- ): Promise<void> {
160
- const normalizedVerb = verb.toLowerCase();
161
- if (!['list', 'get'].includes(normalizedVerb)) {
162
- throw new Error('Product components verb must be: list | get');
163
- }
164
- if (!opts.tag) throw new Error('--tag <product_tag> is required');
165
-
166
- const ctx = requireWorkspaceContext({ profile: opts.profile });
167
- const product = unwrapProduct(await getProduct(ctx, { tag: opts.tag }));
168
- const selectedTypes =
169
- normalizedVerb === 'get'
170
- ? [opts.type?.toLowerCase()].filter(Boolean) as string[]
171
- : Object.keys(COMPONENT_KEYS);
172
- if (normalizedVerb === 'get' && !selectedTypes.length) {
173
- throw new Error('--type <component_type> is required for get');
174
- }
175
- for (const type of selectedTypes) {
176
- if (!COMPONENT_KEYS[type]) {
177
- throw new Error(`Unknown component type "${type}". Use: ${Object.keys(COMPONENT_KEYS).join(', ')}`);
178
- }
179
- }
180
-
181
- const components: Record<string, unknown[]> = {};
182
- for (const type of selectedTypes) {
183
- const sourceKey = COMPONENT_KEYS[type].find((key) => Array.isArray(product[key]));
184
- components[type] = sourceKey
185
- ? (product[sourceKey] as unknown[]).map(summarizeComponent)
186
- : [];
187
- }
188
- const envSource = Array.isArray(product.envs) ? product.envs : [];
189
- printJson(
190
- {
191
- tag: product.tag ?? opts.tag,
192
- environments: envSource.map((env) => {
193
- const item = (env && typeof env === 'object' ? env : {}) as Record<string, unknown>;
194
- return { slug: item.slug, name: item.name ?? item.env_name };
195
- }),
196
- components,
197
- },
198
- Boolean(opts.json),
199
- );
200
- }
201
-
202
- export async function runProductEnvironments(
203
- verb: string,
204
- opts: { product?: string; tag?: string; slug?: string; json?: boolean },
205
- extraArgs: string[],
206
- ): Promise<void> {
207
- const v = verb.toLowerCase();
208
- const allowed = ['list', 'get', 'fetch'];
209
- if (!allowed.includes(v)) {
210
- throw new Error(`Unknown environments verb "${verb}". Use: ${allowed.join(', ')}`);
211
- }
212
-
213
- const session = requireSession();
214
- const proxy = getSdkProxy(session);
215
- const productTag = opts.product ?? extraArgs[0] ?? session.project?.product_tag;
216
- if (!productTag) throw new Error('Provide the product tag as the first argument or --product <tag>');
217
-
218
- if (v === 'list') {
219
- const result = await proxy.execute('product', 'environments.list', [productTag]);
220
- printJson(result, Boolean(opts.json));
221
- return;
222
- }
223
-
224
- const slug = opts.slug ?? opts.tag ?? extraArgs[1] ?? extraArgs[0];
225
- if (!slug) throw new Error('Provide the environment slug as the second argument or --slug <slug>');
226
- const result = await proxy.execute('product', 'environments.fetch', [productTag, slug]);
227
- printJson(result, Boolean(opts.json));
228
- }
@@ -1,23 +0,0 @@
1
- import { loadGlobalConfig, saveGlobalConfig } from '../lib/config.js';
2
- import { printJson, success } from '../lib/output.js';
3
-
4
- export function runProfilesList(json: boolean): void {
5
- const cfg = loadGlobalConfig();
6
- printJson(
7
- {
8
- default_profile: cfg.default_profile,
9
- profiles: cfg.profiles,
10
- },
11
- json,
12
- );
13
- }
14
-
15
- export function runProfilesUse(name: string): void {
16
- const cfg = loadGlobalConfig();
17
- if (!cfg.profiles[name]) {
18
- throw new Error(`Unknown profile "${name}"`);
19
- }
20
- cfg.default_profile = name;
21
- saveGlobalConfig(cfg);
22
- success(`Default profile set to "${name}" (${cfg.profiles[name].api_url})`);
23
- }