@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.
- package/API_PARITY.md +58 -0
- package/CHANGELOG.md +36 -0
- package/README.md +89 -0
- package/dist/commands/apps-import.d.ts +9 -0
- package/dist/commands/apps-import.js +29 -0
- package/dist/commands/apps.d.ts +10 -0
- package/dist/commands/apps.js +68 -0
- package/dist/commands/cloud.d.ts +7 -0
- package/dist/commands/cloud.js +42 -0
- package/dist/commands/completion.d.ts +1 -0
- package/dist/commands/completion.js +113 -0
- package/dist/commands/db.d.ts +5 -0
- package/dist/commands/db.js +55 -0
- package/dist/commands/generate.d.ts +14 -0
- package/dist/commands/generate.js +52 -0
- package/dist/commands/graph.d.ts +4 -0
- package/dist/commands/graph.js +44 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +28 -0
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +42 -0
- package/dist/commands/link.d.ts +7 -0
- package/dist/commands/link.js +61 -0
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.js +103 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +6 -0
- package/dist/commands/platform.d.ts +7 -0
- package/dist/commands/platform.js +99 -0
- package/dist/commands/products.d.ts +14 -0
- package/dist/commands/products.js +74 -0
- package/dist/commands/profiles.d.ts +2 -0
- package/dist/commands/profiles.js +18 -0
- package/dist/commands/resources.d.ts +7 -0
- package/dist/commands/resources.js +61 -0
- package/dist/commands/secrets.d.ts +6 -0
- package/dist/commands/secrets.js +38 -0
- package/dist/commands/unlink.d.ts +3 -0
- package/dist/commands/unlink.js +14 -0
- package/dist/commands/whoami.d.ts +1 -0
- package/dist/commands/whoami.js +22 -0
- package/dist/commands/workspaces.d.ts +16 -0
- package/dist/commands/workspaces.js +72 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +283 -0
- package/dist/lib/api-client.d.ts +18 -0
- package/dist/lib/api-client.js +65 -0
- package/dist/lib/app-import.d.ts +15 -0
- package/dist/lib/app-import.js +63 -0
- package/dist/lib/config.d.ts +63 -0
- package/dist/lib/config.js +155 -0
- package/dist/lib/context-store.d.ts +17 -0
- package/dist/lib/context-store.js +43 -0
- package/dist/lib/db-methods.d.ts +3 -0
- package/dist/lib/db-methods.js +55 -0
- package/dist/lib/encryption.d.ts +1 -0
- package/dist/lib/encryption.js +11 -0
- package/dist/lib/forms/enums.d.ts +11 -0
- package/dist/lib/forms/enums.js +42 -0
- package/dist/lib/forms/index.d.ts +2 -0
- package/dist/lib/forms/index.js +2 -0
- package/dist/lib/forms/prompt.d.ts +11 -0
- package/dist/lib/forms/prompt.js +203 -0
- package/dist/lib/forms/schemas.d.ts +9 -0
- package/dist/lib/forms/schemas.js +253 -0
- package/dist/lib/forms/types.d.ts +18 -0
- package/dist/lib/forms/types.js +1 -0
- package/dist/lib/http.d.ts +10 -0
- package/dist/lib/http.js +51 -0
- package/dist/lib/integrations.d.ts +20 -0
- package/dist/lib/integrations.js +20 -0
- package/dist/lib/interactive-opts.d.ts +6 -0
- package/dist/lib/interactive-opts.js +6 -0
- package/dist/lib/output.d.ts +3 -0
- package/dist/lib/output.js +22 -0
- package/dist/lib/platform-api.d.ts +19 -0
- package/dist/lib/platform-api.js +123 -0
- package/dist/lib/proxy/context.d.ts +16 -0
- package/dist/lib/proxy/context.js +73 -0
- package/dist/lib/proxy/db-proxy.d.ts +13 -0
- package/dist/lib/proxy/db-proxy.js +28 -0
- package/dist/lib/proxy/graph-proxy.d.ts +19 -0
- package/dist/lib/proxy/graph-proxy.js +43 -0
- package/dist/lib/proxy/sdk-proxy.d.ts +14 -0
- package/dist/lib/proxy/sdk-proxy.js +22 -0
- package/dist/lib/read-body.d.ts +11 -0
- package/dist/lib/read-body.js +38 -0
- package/dist/lib/resources.d.ts +12 -0
- package/dist/lib/resources.js +147 -0
- package/dist/lib/snippets.d.ts +6 -0
- package/dist/lib/snippets.js +146 -0
- package/dist/lib/templates.d.ts +3 -0
- package/dist/lib/templates.js +74 -0
- package/dist/lib/workspace-context.d.ts +12 -0
- package/dist/lib/workspace-context.js +25 -0
- package/dist/lib/workspaces.d.ts +45 -0
- package/dist/lib/workspaces.js +219 -0
- package/package.json +37 -0
- package/src/commands/apps-import.ts +43 -0
- package/src/commands/apps.ts +92 -0
- package/src/commands/cloud.ts +50 -0
- package/src/commands/completion.ts +119 -0
- package/src/commands/db.ts +63 -0
- package/src/commands/generate.ts +74 -0
- package/src/commands/graph.ts +51 -0
- package/src/commands/init.ts +39 -0
- package/src/commands/install.ts +48 -0
- package/src/commands/link.ts +84 -0
- package/src/commands/login.ts +145 -0
- package/src/commands/logout.ts +7 -0
- package/src/commands/platform.ts +123 -0
- package/src/commands/products.ts +104 -0
- package/src/commands/profiles.ts +23 -0
- package/src/commands/resources.ts +85 -0
- package/src/commands/secrets.ts +46 -0
- package/src/commands/unlink.ts +15 -0
- package/src/commands/whoami.ts +27 -0
- package/src/commands/workspaces.ts +98 -0
- package/src/index.ts +381 -0
- package/src/lib/api-client.ts +88 -0
- package/src/lib/app-import.ts +82 -0
- package/src/lib/config.ts +225 -0
- package/src/lib/context-store.ts +77 -0
- package/src/lib/db-methods.ts +56 -0
- package/src/lib/encryption.ts +12 -0
- package/src/lib/forms/enums.ts +52 -0
- package/src/lib/forms/index.ts +8 -0
- package/src/lib/forms/prompt.ts +209 -0
- package/src/lib/forms/schemas.ts +288 -0
- package/src/lib/forms/types.ts +27 -0
- package/src/lib/http.ts +70 -0
- package/src/lib/integrations.ts +47 -0
- package/src/lib/interactive-opts.ts +13 -0
- package/src/lib/output.ts +24 -0
- package/src/lib/platform-api.ts +178 -0
- package/src/lib/proxy/context.ts +94 -0
- package/src/lib/proxy/db-proxy.ts +40 -0
- package/src/lib/proxy/graph-proxy.ts +64 -0
- package/src/lib/proxy/sdk-proxy.ts +58 -0
- package/src/lib/read-body.ts +57 -0
- package/src/lib/resources.ts +156 -0
- package/src/lib/snippets.ts +165 -0
- package/src/lib/templates.ts +78 -0
- package/src/lib/workspace-context.ts +43 -0
- package/src/lib/workspaces.ts +283 -0
- package/templates/dotnet/Program.cs +2 -0
- package/templates/go/main.go +8 -0
- package/templates/python/ductape_example.py +16 -0
- package/templates/typescript/ductape.example.ts +18 -0
- package/test/encryption.test.ts +20 -0
- package/test/forms.test.ts +25 -0
- package/test/platform-api.test.ts +19 -0
- package/test/resources.test.ts +23 -0
- package/test/workspace-resolve.test.ts +29 -0
- 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();
|