@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,23 @@
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
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ buildCrudParams,
3
+ listResourceTypes,
4
+ proxyMethodForVerb,
5
+ resolveResourceType,
6
+ type CrudVerb,
7
+ } from '../lib/resources.js';
8
+ import { resolveResourceSchema } from '../lib/forms/index.js';
9
+ import { isInteractive, promptTag } from '../lib/forms/prompt.js';
10
+ import { toInteractiveOpts, type CommandInteractiveFlags } from '../lib/interactive-opts.js';
11
+ import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
12
+ import { requireSession, getSdkProxy } from '../lib/proxy/context.js';
13
+ import { printJson } from '../lib/output.js';
14
+
15
+ const CRUD_VERBS: CrudVerb[] = ['create', 'list', 'get', 'update', 'delete', 'connect'];
16
+
17
+ export function runResourcesList(json: boolean): void {
18
+ printJson({ resource_types: listResourceTypes() }, json);
19
+ }
20
+
21
+ export async function runResourceCrud(
22
+ typeName: string,
23
+ verb: string,
24
+ opts: { tag?: string; file?: string; json?: boolean } & CommandInteractiveFlags,
25
+ extraArgs: string[],
26
+ ): Promise<void> {
27
+ const crud = verb.toLowerCase() as CrudVerb;
28
+ if (!CRUD_VERBS.includes(crud)) {
29
+ throw new Error(`Unknown verb "${verb}". Use: ${CRUD_VERBS.join(', ')}`);
30
+ }
31
+
32
+ const session = requireSession();
33
+ const module = resolveResourceType(typeName);
34
+
35
+ if (module === 'product' || module === 'app') {
36
+ if (crud === 'list') {
37
+ throw new Error(
38
+ `Use \`ductape ${module}s list\` — listing uses the workspace REST API (not sdk-proxy).`,
39
+ );
40
+ }
41
+ if (crud === 'delete') {
42
+ throw new Error(`Use \`ductape ${module}s delete --id <id>\`.`);
43
+ }
44
+ }
45
+
46
+ const method = proxyMethodForVerb(crud);
47
+ const interactive = toInteractiveOpts(opts);
48
+ let tag = opts.tag ?? extraArgs[0];
49
+ if (!tag && ['get', 'update', 'delete'].includes(crud) && isInteractive(interactive)) {
50
+ tag = await promptTag('Component tag');
51
+ }
52
+
53
+ const needsBody = crud === 'create' || crud === 'update' || crud === 'connect';
54
+ const schema = needsBody ? resolveResourceSchema(module, crud) : undefined;
55
+ const body = needsBody
56
+ ? await resolveBody({
57
+ ...interactive,
58
+ file: opts.file,
59
+ schema,
60
+ patch: crud === 'update',
61
+ requireBody: crud === 'create',
62
+ })
63
+ : undefined;
64
+
65
+ if (crud === 'create' && (!body || Object.keys(body).length === 0)) {
66
+ throw new Error(`create requires a body. ${bodyRequiredHint(`${module}:create`)}`);
67
+ }
68
+
69
+ let payload = body;
70
+ if (crud === 'connect' && tag) {
71
+ payload = {
72
+ ...(payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}),
73
+ tag,
74
+ };
75
+ }
76
+
77
+ const params = buildCrudParams(module, method, session.project.product_tag, {
78
+ tag,
79
+ body: payload,
80
+ });
81
+
82
+ const proxy = getSdkProxy(session);
83
+ const result = await proxy.execute(module, method, params);
84
+ printJson(result, Boolean(opts.json));
85
+ }
@@ -0,0 +1,46 @@
1
+ import { isInteractive, promptKey } from '../lib/forms/prompt.js';
2
+ import { toInteractiveOpts, type CommandInteractiveFlags } from '../lib/interactive-opts.js';
3
+ import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
4
+ import { buildCrudParams, proxyMethodForVerb, type CrudVerb } from '../lib/resources.js';
5
+ import { getSdkProxy, requireSession } from '../lib/proxy/context.js';
6
+ import { printJson } from '../lib/output.js';
7
+
8
+ const VERBS: CrudVerb[] = ['create', 'list', 'get', 'update', 'delete'];
9
+
10
+ export async function runSecrets(
11
+ verb: string,
12
+ opts: { key?: string; file?: string; json?: boolean } & CommandInteractiveFlags,
13
+ ): Promise<void> {
14
+ const crud = verb.toLowerCase() as CrudVerb;
15
+ if (!VERBS.includes(crud)) {
16
+ throw new Error(`Unknown secrets verb "${verb}". Use: ${VERBS.join(', ')}`);
17
+ }
18
+
19
+ const session = requireSession();
20
+ const method = proxyMethodForVerb(crud);
21
+ const interactive = toInteractiveOpts(opts);
22
+ let key = opts.key;
23
+ if (!key && ['fetch', 'delete', 'update'].includes(method) && isInteractive(interactive)) {
24
+ key = await promptKey();
25
+ }
26
+ const body =
27
+ crud === 'create' || crud === 'update'
28
+ ? await resolveBody({
29
+ ...interactive,
30
+ file: opts.file,
31
+ schemaKey: `secrets:${crud}`,
32
+ patch: crud === 'update',
33
+ requireBody: crud === 'create',
34
+ })
35
+ : undefined;
36
+ if (crud === 'create' && (!body || Object.keys(body).length === 0)) {
37
+ throw new Error(`create requires a body. ${bodyRequiredHint('secrets:create')}`);
38
+ }
39
+ const params = buildCrudParams('secrets', method, session.project.product_tag, {
40
+ tag: key,
41
+ body,
42
+ });
43
+
44
+ const result = await getSdkProxy(session).execute('secrets', method, params);
45
+ printJson(result, Boolean(opts.json));
46
+ }
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { findProjectConfig, PROJECT_CONFIG_PATH } from '../lib/config.js';
4
+ import { success } from '../lib/output.js';
5
+
6
+ export function runUnlink(opts: { dir?: string }): void {
7
+ const dir = path.resolve(opts.dir ?? process.cwd());
8
+ const found = findProjectConfig(dir);
9
+ if (!found) {
10
+ throw new Error('No linked project in this directory');
11
+ }
12
+ const configPath = path.join(found.dir, PROJECT_CONFIG_PATH);
13
+ fs.unlinkSync(configPath);
14
+ success(`Removed ${configPath}`);
15
+ }
@@ -0,0 +1,27 @@
1
+ import { getActiveProfileName, getApiUrl, loadCredentials, loadGlobalConfig, findProjectConfig } from '../lib/config.js';
2
+ import { getActiveWorkspace } from '../lib/workspaces.js';
3
+ import { printJson } from '../lib/output.js';
4
+
5
+ export function runWhoami(json: boolean): void {
6
+ const creds = loadCredentials();
7
+ const project = findProjectConfig();
8
+ const active = getActiveWorkspace();
9
+ const profile = project?.config
10
+ ? getActiveProfileName(project.config)
11
+ : loadGlobalConfig().default_profile;
12
+
13
+ printJson(
14
+ {
15
+ logged_in: Boolean(creds),
16
+ email: creds?.email,
17
+ user_id: creds?.user_id,
18
+ profile,
19
+ api_url: getApiUrl(profile),
20
+ active_workspace: active
21
+ ? { id: active.id, name: active.name }
22
+ : null,
23
+ project: project?.config ?? null,
24
+ },
25
+ json,
26
+ );
27
+ }
@@ -0,0 +1,98 @@
1
+ import {
2
+ applyActiveWorkspace,
3
+ fetchWorkspaces,
4
+ getActiveWorkspace,
5
+ printWorkspaceTable,
6
+ promptWorkspaceSelection,
7
+ selectAndApplyWorkspace,
8
+ workspaceId,
9
+ workspaceLabel,
10
+ } from '../lib/workspaces.js';
11
+ import { printJson, success } from '../lib/output.js';
12
+
13
+ export async function runWorkspacesList(opts: {
14
+ profile?: string;
15
+ json?: boolean;
16
+ refresh?: boolean;
17
+ }): Promise<void> {
18
+ const workspaces = await fetchWorkspaces(opts.profile);
19
+ const active = getActiveWorkspace();
20
+
21
+ if (opts.json) {
22
+ printJson(
23
+ {
24
+ active_workspace_id: active?.id ?? null,
25
+ active_workspace_name: active?.name ?? null,
26
+ workspaces,
27
+ },
28
+ true,
29
+ );
30
+ return;
31
+ }
32
+
33
+ if (active) {
34
+ console.log(`Active workspace: ${active.name ?? active.id}`);
35
+ console.log(` ${active.id}\n`);
36
+ }
37
+ printWorkspaceTable(workspaces, active?.id);
38
+ }
39
+
40
+ export function runWorkspacesCurrent(json: boolean): void {
41
+ const active = getActiveWorkspace();
42
+ if (!active) {
43
+ if (json) {
44
+ printJson({ active: null }, true);
45
+ return;
46
+ }
47
+ console.log('No active workspace. Run: ductape login or ductape workspaces use');
48
+ return;
49
+ }
50
+ if (json) {
51
+ printJson({ active_workspace_id: active.id, active_workspace_name: active.name }, true);
52
+ return;
53
+ }
54
+ console.log(`Active workspace: ${active.name ?? active.id}`);
55
+ console.log(` ${active.id}`);
56
+ }
57
+
58
+ export async function runWorkspacesUse(
59
+ selector: string | undefined,
60
+ opts: { profile?: string; dir?: string; json?: boolean },
61
+ ): Promise<void> {
62
+ const workspaces = await fetchWorkspaces(opts.profile);
63
+
64
+ let chosen;
65
+ if (!selector) {
66
+ chosen = await promptWorkspaceSelection(workspaces, {
67
+ message: 'Switch workspace:',
68
+ });
69
+ await applyActiveWorkspace(chosen, { syncProject: true, dir: opts.dir });
70
+ } else {
71
+ chosen = await selectAndApplyWorkspace({
72
+ profile: opts.profile,
73
+ selector,
74
+ skipPrompt: true,
75
+ syncProject: true,
76
+ dir: opts.dir,
77
+ });
78
+ }
79
+
80
+ const id = workspaceId(chosen);
81
+ const name = workspaceLabel(chosen);
82
+ if (opts.json) {
83
+ printJson({ active_workspace_id: id, active_workspace_name: name }, true);
84
+ return;
85
+ }
86
+ success(`Switched to workspace: ${name} (${id})`);
87
+ }
88
+
89
+ /** Refresh workspace list from API and update login cache */
90
+ export async function runWorkspacesRefresh(opts: { profile?: string; json?: boolean }): Promise<void> {
91
+ const workspaces = await fetchWorkspaces(opts.profile);
92
+ if (opts.json) {
93
+ printJson({ workspaces, count: workspaces.length }, true);
94
+ return;
95
+ }
96
+ success(`Refreshed ${workspaces.length} workspace(s) from API`);
97
+ await runWorkspacesList({ profile: opts.profile, json: false });
98
+ }
package/src/index.ts ADDED
@@ -0,0 +1,381 @@
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 {
21
+ runWorkspacesCurrent,
22
+ runWorkspacesList,
23
+ runWorkspacesRefresh,
24
+ runWorkspacesUse,
25
+ } from './commands/workspaces.js';
26
+ import { runGeneratePayload, runGenerateSnippet } from './commands/generate.js';
27
+ import { runCompletion } from './commands/completion.js';
28
+ import { runProducts, runProductApps } from './commands/products.js';
29
+ import { runApps } from './commands/apps.js';
30
+ import { runAppsImport } from './commands/apps-import.js';
31
+
32
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
+ const pkg = JSON.parse(
34
+ readFileSync(path.join(__dirname, '../package.json'), 'utf8'),
35
+ ) as { version: string };
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ function wrap(fn: (...args: any[]) => void | Promise<void>) {
39
+ return (...args: any[]) => {
40
+ Promise.resolve(fn(...args)).catch((err: unknown) => {
41
+ console.error(err instanceof Error ? err.message : err);
42
+ process.exit(1);
43
+ });
44
+ };
45
+ }
46
+
47
+ const program = new Command();
48
+ program
49
+ .name('ductape')
50
+ .description(
51
+ 'Ductape CLI — local platform, auth, project linking, and resource CRUD via Workbench-compatible encrypted proxy',
52
+ )
53
+ .version(pkg.version);
54
+
55
+ program
56
+ .command('login')
57
+ .description('Log in (email/password, OAuth token, or browser OAuth)')
58
+ .option('-e, --email <email>')
59
+ .option('-p, --password <password>')
60
+ .option('--token <token>', 'OAuth callback token from browser redirect URL')
61
+ .option('--browser [provider]', 'Open browser for Google OAuth (google|github)')
62
+ .option('--profile <name>', 'API profile', 'cloud')
63
+ .option('-w, --workspace <id>', 'Workspace index, name, or id (skips interactive picker)')
64
+ .option('--skip-workspace-select', 'Keep prior active workspace or default without prompting')
65
+ .action(
66
+ wrap((opts) =>
67
+ runLogin({
68
+ email: opts.email,
69
+ password: opts.password,
70
+ profile: opts.profile,
71
+ token: opts.token,
72
+ browser: opts.browser === true ? 'google' : opts.browser,
73
+ workspace: opts.workspace,
74
+ skipWorkspaceSelect: Boolean(opts.skipWorkspaceSelect),
75
+ }),
76
+ ),
77
+ );
78
+
79
+ program.command('logout').description('Clear local credentials').action(wrap(runLogout));
80
+
81
+ program
82
+ .command('whoami')
83
+ .description('Show login and linked project')
84
+ .option('--json', 'JSON output')
85
+ .action(wrap((opts) => runWhoami(Boolean(opts.json))));
86
+
87
+ program
88
+ .command('install')
89
+ .description('Check Docker/Node prerequisites and prepare platform .env')
90
+ .action(wrap(runInstall));
91
+
92
+ const profiles = program.command('profiles').description('Manage API profiles (cloud / local)');
93
+
94
+ profiles
95
+ .command('list')
96
+ .option('--json', 'JSON output')
97
+ .action(wrap((opts) => runProfilesList(Boolean(opts.json))));
98
+
99
+ profiles.command('use <name>').action(wrap((name: string) => runProfilesUse(name)));
100
+
101
+ const workspaces = program
102
+ .command('workspaces')
103
+ .description('List and switch active workspace (workspace_id is stored globally at login)');
104
+
105
+ workspaces
106
+ .command('list')
107
+ .description('List workspaces (marks active)')
108
+ .option('--profile <name>')
109
+ .option('--json', 'JSON output')
110
+ .action(wrap((opts) => runWorkspacesList({ profile: opts.profile, json: opts.json })));
111
+
112
+ workspaces
113
+ .command('current')
114
+ .description('Show active workspace')
115
+ .option('--json', 'JSON output')
116
+ .action(wrap((opts) => runWorkspacesCurrent(Boolean(opts.json))));
117
+
118
+ workspaces
119
+ .command('use [selector]')
120
+ .description('Switch active workspace (interactive picker). Updates linked .ductape/config.json if present')
121
+ .option('--profile <name>')
122
+ .option('-C, --dir <path>', 'Project directory to sync', process.cwd())
123
+ .option('--json', 'JSON output')
124
+ .action(wrap((selector: string | undefined, opts) => runWorkspacesUse(selector, opts)));
125
+
126
+ workspaces
127
+ .command('switch [selector]')
128
+ .description('Alias for workspaces use')
129
+ .option('--profile <name>')
130
+ .option('-C, --dir <path>', 'Project directory to sync', process.cwd())
131
+ .option('--json', 'JSON output')
132
+ .action(wrap((selector: string | undefined, opts) => runWorkspacesUse(selector, opts)));
133
+
134
+ workspaces
135
+ .command('refresh')
136
+ .description('Refresh workspace list from API')
137
+ .option('--profile <name>')
138
+ .option('--json', 'JSON output')
139
+ .action(wrap((opts) => runWorkspacesRefresh(opts)));
140
+
141
+ program
142
+ .command('link')
143
+ .option('-w, --workspace <id>')
144
+ .option('--product <tag>')
145
+ .option('--env <slug>')
146
+ .option('--profile <name>')
147
+ .option('-C, --dir <path>', 'Project directory', process.cwd())
148
+ .action(
149
+ wrap((opts) =>
150
+ runLink({
151
+ workspace: opts.workspace,
152
+ product: opts.product,
153
+ env: opts.env,
154
+ profile: opts.profile,
155
+ dir: opts.dir,
156
+ }),
157
+ ),
158
+ );
159
+
160
+ program
161
+ .command('unlink')
162
+ .option('-C, --dir <path>', 'Project directory', process.cwd())
163
+ .action(wrap((opts) => runUnlink({ dir: opts.dir })));
164
+
165
+ program
166
+ .command('init')
167
+ .option('--link', 'Run link after init')
168
+ .option('-w, --workspace <id>')
169
+ .option('--product <tag>')
170
+ .option('--env <slug>')
171
+ .option('--language <lang>', 'typescript|python|go|dotnet')
172
+ .action(
173
+ wrap((opts) =>
174
+ runInit({
175
+ link: Boolean(opts.link),
176
+ workspace: opts.workspace,
177
+ product: opts.product,
178
+ env: opts.env,
179
+ language: opts.language,
180
+ }),
181
+ ),
182
+ );
183
+
184
+ program
185
+ .command('start')
186
+ .description('Start platform via docker compose')
187
+ .option('--backend-only', 'Backend without workbench UI')
188
+ .option('--remote', 'Workbench UI only, built against cloud API (no local Nest/Mongo/Kafka)')
189
+ .option('--build', 'Build images before start')
190
+ .action(
191
+ wrap((opts) =>
192
+ runStart({
193
+ backendOnly: Boolean(opts.backendOnly),
194
+ remote: Boolean(opts.remote),
195
+ build: Boolean(opts.build),
196
+ }),
197
+ ),
198
+ );
199
+
200
+ program.command('stop').action(wrap(runStop));
201
+
202
+ program.command('status').option('--json', 'JSON output').action(wrap((opts) => runStatus(Boolean(opts.json))));
203
+
204
+ const products = program
205
+ .command('products')
206
+ .description('Workspace products CRUD (REST + sdk-proxy update)');
207
+
208
+ products
209
+ .command('<verb>')
210
+ .description('list | get | create | update | delete')
211
+ .option('--profile <name>')
212
+ .option('--status <status>', 'Filter for list: all, active, draft, …', 'all')
213
+ .option('--id <id>', 'Product _id (get, delete)')
214
+ .option('-t, --tag <tag>', 'Product tag (get, update)')
215
+ .option('-f, --file <path>', 'JSON body (create, update)')
216
+ .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
217
+ .option('--no-interactive', 'Require -f JSON for create/update')
218
+ .option('--json', 'JSON output')
219
+ .action(
220
+ wrap((verb: string, opts) => runProducts(verb, opts, [])),
221
+ );
222
+
223
+ const productApps = products.command('apps').description('Apps connected to a product');
224
+
225
+ productApps
226
+ .command('list')
227
+ .option('--profile <name>')
228
+ .option('--product <id>', 'Product _id')
229
+ .option('--json', 'JSON output')
230
+ .action(wrap((opts) => runProductApps('list', opts, [])));
231
+
232
+ const apps = program.command('apps').description('Workspace integration apps CRUD (REST)');
233
+
234
+ apps
235
+ .command('<verb>')
236
+ .description('list | get | create | update | delete')
237
+ .option('--profile <name>')
238
+ .option('--status <status>', 'Filter for list', 'all')
239
+ .option('--id <id>', 'App _id')
240
+ .option('-t, --tag <tag>', 'App tag (get; update with --proxy)')
241
+ .option('--proxy', 'Update via sdk-proxy (use with --tag)')
242
+ .option('-f, --file <path>', 'JSON body')
243
+ .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
244
+ .option('--no-interactive', 'Require -f JSON for create/update')
245
+ .option('--json', 'JSON output')
246
+ .action(wrap((verb: string, opts) => runApps(verb, opts, [])));
247
+
248
+ apps
249
+ .command('import <file>')
250
+ .description('Import Postman v2.1 or OpenAPI 3.0 into a new or existing app')
251
+ .requiredOption(
252
+ '-t, --type <format>',
253
+ 'postman | openapi (Postman Collection v2.1 or OpenAPI 3.0)',
254
+ )
255
+ .option('--profile <name>')
256
+ .option('--app-id <id>', 'Update existing app by _id')
257
+ .option('--app-tag <tag>', 'Update existing app by tag')
258
+ .option('--update', 'Replace/update if app already exists')
259
+ .option('--version <tag>', 'App version tag', '0.0.1')
260
+ .option('--json', 'JSON output')
261
+ .action(
262
+ wrap((file: string, opts) =>
263
+ runAppsImport(file, {
264
+ profile: opts.profile,
265
+ type: opts.type,
266
+ appId: opts.appId,
267
+ appTag: opts.appTag,
268
+ update: Boolean(opts.update),
269
+ version: opts.version,
270
+ json: opts.json,
271
+ }),
272
+ ),
273
+ );
274
+
275
+ const resources = program.command('resources').description('Component CRUD (sdk-proxy)');
276
+
277
+ resources.command('types').option('--json', 'JSON output').action(wrap((opts) => runResourcesList(Boolean(opts.json))));
278
+
279
+ resources
280
+ .command('<type> <verb>')
281
+ .option('-t, --tag <tag>')
282
+ .option('-f, --file <path>', 'JSON body (or use interactive prompts)')
283
+ .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
284
+ .option('--no-interactive', 'Require -f JSON for create/update/connect')
285
+ .option('--json', 'JSON output')
286
+ .action(
287
+ wrap((type: string, verb: string, opts) => runResourceCrud(type, verb, opts, [])),
288
+ );
289
+
290
+ const cloud = program.command('cloud').description('Cloud connections & resources');
291
+
292
+ cloud
293
+ .command('connections <verb> [id]')
294
+ .option('-f, --file <path>')
295
+ .option('--json', 'JSON output')
296
+ .action(
297
+ wrap((verb: string, id: string | undefined, opts) =>
298
+ runCloud('connections', verb, { id, file: opts.file, json: opts.json }, []),
299
+ ),
300
+ );
301
+
302
+ cloud
303
+ .command('resources <verb>')
304
+ .option('-f, --file <path>')
305
+ .option('--json', 'JSON output')
306
+ .action(wrap((verb: string, opts) => runCloud('resources', verb, { file: opts.file, json: opts.json }, [])));
307
+
308
+ const db = program.command('db').description('Database runtime (db-proxy)');
309
+
310
+ db.command('context').option('--json', 'JSON output').action(wrap((opts) => runDbContext(Boolean(opts.json))));
311
+
312
+ db
313
+ .command('<verb>')
314
+ .option('-f, --file <path>')
315
+ .option('--json', 'JSON output')
316
+ .action(wrap((verb: string, opts) => runDb(verb, { file: opts.file, json: opts.json })));
317
+
318
+ const graph = program.command('graph').description('Graph runtime (graph-proxy)');
319
+
320
+ graph
321
+ .command('<verb>')
322
+ .option('-f, --file <path>')
323
+ .option('--json', 'JSON output')
324
+ .action(wrap((verb: string, opts) => runGraph(verb, { file: opts.file, json: opts.json })));
325
+
326
+ program
327
+ .command('secrets <verb>')
328
+ .description('Workspace secrets CRUD')
329
+ .option('-k, --key <key>')
330
+ .option('-f, --file <path>')
331
+ .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
332
+ .option('--no-interactive', 'Require -f JSON for create/update')
333
+ .option('--json', 'JSON output')
334
+ .action(
335
+ wrap((verb: string, opts) =>
336
+ runSecrets(verb, opts),
337
+ ),
338
+ );
339
+
340
+ const generate = program.command('generate').description('Payload templates & code snippets');
341
+
342
+ generate
343
+ .command('payload <family> <method>')
344
+ .option('-f, --file <path>', 'targets JSON')
345
+ .option('--json', 'JSON output')
346
+ .action(
347
+ wrap((family: string, method: string, opts) =>
348
+ runGeneratePayload({ family, method, file: opts.file, json: opts.json }),
349
+ ),
350
+ );
351
+
352
+ generate
353
+ .command('snippet <family> <method>')
354
+ .requiredOption('-l, --language <lang>', 'typescript|python|go|dotnet')
355
+ .option('-f, --file <path>')
356
+ .option('-o, --out <path>', 'Write snippet to file')
357
+ .option('--json', 'JSON output')
358
+ .action(
359
+ wrap((family: string, method: string, opts) =>
360
+ runGenerateSnippet({
361
+ family,
362
+ method,
363
+ language: opts.language,
364
+ file: opts.file,
365
+ json: opts.json,
366
+ out: opts.out,
367
+ }),
368
+ ),
369
+ );
370
+
371
+ program
372
+ .command('completion <shell>')
373
+ .description('Print shell completions (bash|zsh)')
374
+ .action(wrap((shell: string) => {
375
+ if (shell !== 'bash' && shell !== 'zsh' && shell !== 'fish') {
376
+ throw new Error('Shell must be bash, zsh, or fish');
377
+ }
378
+ runCompletion(shell);
379
+ }));
380
+
381
+ program.parse();