@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,219 @@
|
|
|
1
|
+
import select from '@inquirer/select';
|
|
2
|
+
import { ApiClient } from './api-client.js';
|
|
3
|
+
import { findProjectConfig, getApiUrl, loadCredentials, loadGlobalConfig, saveCredentials, saveGlobalConfig, saveProjectConfig, } from './config.js';
|
|
4
|
+
import { success } from './output.js';
|
|
5
|
+
export function workspaceId(w) {
|
|
6
|
+
const id = w.workspace_id ?? w._id;
|
|
7
|
+
if (!id)
|
|
8
|
+
throw new Error('Workspace entry missing id');
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
export function workspaceLabel(w) {
|
|
12
|
+
return w.workspace_name ?? w.name ?? w.tag ?? workspaceId(w);
|
|
13
|
+
}
|
|
14
|
+
/** Human-readable workspace tag for .ductape/config.json (slug of name, or explicit tag). */
|
|
15
|
+
export function workspaceTag(w) {
|
|
16
|
+
if (w.tag?.trim())
|
|
17
|
+
return w.tag.trim();
|
|
18
|
+
return slugifyTag(workspaceLabel(w));
|
|
19
|
+
}
|
|
20
|
+
export function slugifyTag(value) {
|
|
21
|
+
return value
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
25
|
+
.replace(/^-+|-+$/g, '');
|
|
26
|
+
}
|
|
27
|
+
export function normalizeWorkspaces(raw) {
|
|
28
|
+
return raw
|
|
29
|
+
.filter((w) => Boolean(w) && typeof w === 'object')
|
|
30
|
+
.map((w) => ({
|
|
31
|
+
workspace_id: String(w.workspace_id ?? w._id ?? ''),
|
|
32
|
+
workspace_name: (w.workspace_name ?? w.name ?? w.tag),
|
|
33
|
+
_id: w._id,
|
|
34
|
+
name: w.name,
|
|
35
|
+
tag: w.tag,
|
|
36
|
+
default: Boolean(w.default),
|
|
37
|
+
accepted: w.accepted,
|
|
38
|
+
}))
|
|
39
|
+
.filter((w) => w.workspace_id.length > 0);
|
|
40
|
+
}
|
|
41
|
+
export function getActiveWorkspace() {
|
|
42
|
+
const global = loadGlobalConfig();
|
|
43
|
+
if (global.active_workspace_id) {
|
|
44
|
+
return {
|
|
45
|
+
id: global.active_workspace_id,
|
|
46
|
+
name: global.active_workspace_name,
|
|
47
|
+
tag: global.active_workspace_tag,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
export function setActiveWorkspace(id, name, tag) {
|
|
53
|
+
const global = loadGlobalConfig();
|
|
54
|
+
global.active_workspace_id = id;
|
|
55
|
+
global.active_workspace_name = name;
|
|
56
|
+
if (tag)
|
|
57
|
+
global.active_workspace_tag = tag;
|
|
58
|
+
saveGlobalConfig(global);
|
|
59
|
+
}
|
|
60
|
+
export function clearActiveWorkspace() {
|
|
61
|
+
const global = loadGlobalConfig();
|
|
62
|
+
delete global.active_workspace_id;
|
|
63
|
+
delete global.active_workspace_name;
|
|
64
|
+
delete global.active_workspace_tag;
|
|
65
|
+
saveGlobalConfig(global);
|
|
66
|
+
}
|
|
67
|
+
export async function fetchWorkspaces(profile) {
|
|
68
|
+
const creds = loadCredentials();
|
|
69
|
+
if (!creds)
|
|
70
|
+
throw new Error('Not logged in. Run: ductape login');
|
|
71
|
+
const cached = normalizeWorkspaces(creds.workspaces ?? []);
|
|
72
|
+
const api = ApiClient.forProfile(profile, creds);
|
|
73
|
+
const apiUrl = getApiUrl(profile);
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(`${apiUrl}/workspaces/v1/fetch/${creds.user_id}?user_id=${encodeURIComponent(creds.user_id)}&public_key=${encodeURIComponent(creds.public_key)}`, { headers: { Authorization: `Bearer ${creds.auth_token}` } });
|
|
76
|
+
const body = await res.json();
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(body.message ?? `Failed to list workspaces (${res.status})`);
|
|
79
|
+
const data = body.data ?? body;
|
|
80
|
+
const list = Array.isArray(data) ? data : (data?.result ?? data?.workspaces ?? []);
|
|
81
|
+
const normalized = normalizeWorkspaces(list);
|
|
82
|
+
if (normalized.length > 0) {
|
|
83
|
+
saveCredentials({ ...creds, workspaces: normalized });
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// fall through to cache / api client
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const list = await api.get(`/workspaces/v1/fetch/${creds.user_id}?user_id=${encodeURIComponent(creds.user_id)}&public_key=${encodeURIComponent(creds.public_key)}`);
|
|
92
|
+
const normalized = normalizeWorkspaces(list);
|
|
93
|
+
if (normalized.length > 0) {
|
|
94
|
+
saveCredentials({ ...creds, workspaces: normalized });
|
|
95
|
+
return normalized;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
export function findWorkspace(workspaces, selector) {
|
|
104
|
+
const trimmed = selector.trim();
|
|
105
|
+
const idx = Number(trimmed);
|
|
106
|
+
if (!Number.isNaN(idx) && workspaces[idx])
|
|
107
|
+
return workspaces[idx];
|
|
108
|
+
return workspaces.find((w) => workspaceId(w) === trimmed ||
|
|
109
|
+
workspaceLabel(w).toLowerCase() === trimmed.toLowerCase() ||
|
|
110
|
+
w.tag?.toLowerCase() === trimmed.toLowerCase());
|
|
111
|
+
}
|
|
112
|
+
export function printWorkspaceTable(workspaces, activeId) {
|
|
113
|
+
if (workspaces.length === 0) {
|
|
114
|
+
console.log('No workspaces found for this account.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
workspaces.forEach((w, i) => {
|
|
118
|
+
const id = workspaceId(w);
|
|
119
|
+
const label = workspaceLabel(w);
|
|
120
|
+
const markers = [];
|
|
121
|
+
if (activeId && id === activeId)
|
|
122
|
+
markers.push('active');
|
|
123
|
+
if (w.default)
|
|
124
|
+
markers.push('default');
|
|
125
|
+
const suffix = markers.length ? ` (${markers.join(', ')})` : '';
|
|
126
|
+
console.log(` [${i}] ${label}${suffix}`);
|
|
127
|
+
console.log(` ${id}`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export async function promptWorkspaceSelection(workspaces, opts) {
|
|
131
|
+
if (workspaces.length === 0) {
|
|
132
|
+
throw new Error('No workspaces available. Create one in the Workbench first.');
|
|
133
|
+
}
|
|
134
|
+
if (workspaces.length === 1) {
|
|
135
|
+
const only = workspaces[0];
|
|
136
|
+
setActiveWorkspace(workspaceId(only), workspaceLabel(only), workspaceTag(only));
|
|
137
|
+
return only;
|
|
138
|
+
}
|
|
139
|
+
const active = getActiveWorkspace();
|
|
140
|
+
const activeId = active?.id ?? null;
|
|
141
|
+
const defaultWs = workspaces.find((w) => active && workspaceId(w) === active.id) ??
|
|
142
|
+
workspaces.find((w) => w.default) ??
|
|
143
|
+
workspaces[0];
|
|
144
|
+
const defaultIdx = workspaces.indexOf(defaultWs);
|
|
145
|
+
// Non-interactive (CI, pipes): keep default without prompting.
|
|
146
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
147
|
+
return defaultWs;
|
|
148
|
+
}
|
|
149
|
+
const choiceIndex = await select({
|
|
150
|
+
message: opts?.message ?? 'Select a workspace',
|
|
151
|
+
pageSize: Math.min(12, workspaces.length),
|
|
152
|
+
default: defaultIdx >= 0 ? defaultIdx : 0,
|
|
153
|
+
choices: workspaces.map((w, i) => {
|
|
154
|
+
const id = workspaceId(w);
|
|
155
|
+
const label = workspaceLabel(w);
|
|
156
|
+
const markers = [];
|
|
157
|
+
if (activeId && id === activeId)
|
|
158
|
+
markers.push('active');
|
|
159
|
+
if (w.default)
|
|
160
|
+
markers.push('default');
|
|
161
|
+
const suffix = markers.length ? ` · ${markers.join(', ')}` : '';
|
|
162
|
+
return {
|
|
163
|
+
name: label + suffix,
|
|
164
|
+
value: i,
|
|
165
|
+
description: id,
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
return workspaces[choiceIndex];
|
|
170
|
+
}
|
|
171
|
+
export async function applyActiveWorkspace(workspace, opts) {
|
|
172
|
+
const id = workspaceId(workspace);
|
|
173
|
+
const name = workspaceLabel(workspace);
|
|
174
|
+
const tag = workspaceTag(workspace);
|
|
175
|
+
setActiveWorkspace(id, name, tag);
|
|
176
|
+
if (opts?.syncProject !== false) {
|
|
177
|
+
const found = findProjectConfig(opts?.dir);
|
|
178
|
+
if (found) {
|
|
179
|
+
saveProjectConfig(found.dir, {
|
|
180
|
+
...found.config,
|
|
181
|
+
workspace_tag: tag,
|
|
182
|
+
workspace_id: id,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export async function selectAndApplyWorkspace(opts) {
|
|
188
|
+
const workspaces = await fetchWorkspaces(opts?.profile);
|
|
189
|
+
let chosen;
|
|
190
|
+
if (opts?.selector) {
|
|
191
|
+
const match = findWorkspace(workspaces, opts.selector);
|
|
192
|
+
if (!match)
|
|
193
|
+
throw new Error(`Workspace not found: ${opts.selector}`);
|
|
194
|
+
chosen = match;
|
|
195
|
+
}
|
|
196
|
+
else if (opts?.skipPrompt) {
|
|
197
|
+
const active = getActiveWorkspace();
|
|
198
|
+
const match = active ? findWorkspace(workspaces, active.id) : undefined;
|
|
199
|
+
chosen = match ?? workspaces.find((w) => w.default) ?? workspaces[0];
|
|
200
|
+
if (!chosen)
|
|
201
|
+
throw new Error('No workspace to select');
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
chosen = await promptWorkspaceSelection(workspaces);
|
|
205
|
+
}
|
|
206
|
+
await applyActiveWorkspace(chosen, { syncProject: opts?.syncProject, dir: opts?.dir });
|
|
207
|
+
return chosen;
|
|
208
|
+
}
|
|
209
|
+
export async function finalizeLoginWorkspace(opts) {
|
|
210
|
+
const chosen = await selectAndApplyWorkspace({
|
|
211
|
+
profile: opts?.profile,
|
|
212
|
+
selector: opts?.workspace,
|
|
213
|
+
skipPrompt: opts?.skipPrompt,
|
|
214
|
+
syncProject: true,
|
|
215
|
+
});
|
|
216
|
+
const workspaces = await fetchWorkspaces(opts?.profile);
|
|
217
|
+
const hint = workspaces.length > 1 ? '\nSwitch anytime: ductape workspaces use' : '';
|
|
218
|
+
success(`Active workspace: ${workspaceLabel(chosen)} (${workspaceId(chosen)})${hint}`);
|
|
219
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ductape/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Ductape CLI — local platform, login, link projects, and manage resources via the proxy (Workbench-compatible)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ductape": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"test": "npm run build && node --import tsx --test test/**/*.test.ts",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ductape",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"license": "UNLICENSED",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@inquirer/input": "^5.0.13",
|
|
23
|
+
"@inquirer/password": "^5.0.13",
|
|
24
|
+
"@inquirer/select": "^5.1.5",
|
|
25
|
+
"commander": "^13.1.0",
|
|
26
|
+
"crypto-js": "^4.2.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/crypto-js": "^4.2.0",
|
|
30
|
+
"@types/node": "^22.12.0",
|
|
31
|
+
"tsx": "^4.19.0",
|
|
32
|
+
"typescript": "^5.3.0"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getApp } from '../lib/platform-api.js';
|
|
3
|
+
import { importAppFromFile, parseImportFormat } from '../lib/app-import.js';
|
|
4
|
+
import { printJson, success } from '../lib/output.js';
|
|
5
|
+
import { requireWorkspaceContext } from '../lib/workspace-context.js';
|
|
6
|
+
|
|
7
|
+
export async function runAppsImport(
|
|
8
|
+
file: string,
|
|
9
|
+
opts: {
|
|
10
|
+
profile?: string;
|
|
11
|
+
type: string;
|
|
12
|
+
appId?: string;
|
|
13
|
+
appTag?: string;
|
|
14
|
+
update?: boolean;
|
|
15
|
+
version?: string;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
},
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
20
|
+
const format = parseImportFormat(opts.type);
|
|
21
|
+
|
|
22
|
+
let appId = opts.appId;
|
|
23
|
+
if (!appId && opts.appTag) {
|
|
24
|
+
const app = (await getApp(ctx, { tag: opts.appTag })) as { _id?: string };
|
|
25
|
+
appId = app._id;
|
|
26
|
+
if (!appId) throw new Error(`App not found for tag: ${opts.appTag}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = await importAppFromFile(ctx, {
|
|
30
|
+
filePath: path.resolve(file),
|
|
31
|
+
format,
|
|
32
|
+
appId,
|
|
33
|
+
update: Boolean(opts.update),
|
|
34
|
+
version: opts.version,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
printJson(result, true);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const label = format === 'postman' ? 'Postman v2.1' : 'OpenAPI 3.0';
|
|
42
|
+
success(`Imported ${label} collection from ${path.basename(file)}`);
|
|
43
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
createApp,
|
|
7
|
+
deleteApp,
|
|
8
|
+
getApp,
|
|
9
|
+
listApps,
|
|
10
|
+
updateApp,
|
|
11
|
+
updateAppViaProxy,
|
|
12
|
+
} from '../lib/platform-api.js';
|
|
13
|
+
import { printJson } from '../lib/output.js';
|
|
14
|
+
import { requireWorkspaceContext } from '../lib/workspace-context.js';
|
|
15
|
+
|
|
16
|
+
const VERBS = ['list', 'get', 'create', 'update', 'delete'] as const;
|
|
17
|
+
type Verb = (typeof VERBS)[number];
|
|
18
|
+
|
|
19
|
+
export async function runApps(
|
|
20
|
+
verb: string,
|
|
21
|
+
opts: {
|
|
22
|
+
profile?: string;
|
|
23
|
+
status?: string;
|
|
24
|
+
id?: string;
|
|
25
|
+
tag?: string;
|
|
26
|
+
file?: string;
|
|
27
|
+
json?: boolean;
|
|
28
|
+
proxy?: 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 apps 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('apps', 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' && (opts.proxy || !id) && !tag && isInteractive(interactive)) {
|
|
54
|
+
tag = await promptTag('App tag');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let result: unknown;
|
|
58
|
+
|
|
59
|
+
switch (v) {
|
|
60
|
+
case 'list':
|
|
61
|
+
result = await listApps(ctx, status);
|
|
62
|
+
break;
|
|
63
|
+
case 'get':
|
|
64
|
+
result = await getApp(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('apps:create')}`);
|
|
69
|
+
}
|
|
70
|
+
result = await createApp(ctx, body);
|
|
71
|
+
break;
|
|
72
|
+
case 'update': {
|
|
73
|
+
if (opts.proxy || tag) {
|
|
74
|
+
const appTag = tag ?? (body?.tag as string | undefined);
|
|
75
|
+
if (!appTag) throw new Error('update via proxy requires --tag <app_tag>');
|
|
76
|
+
result = await updateAppViaProxy(ctx, appTag, body ?? {});
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (!id) throw new Error('update requires --id <app_id> or --tag with --proxy');
|
|
80
|
+
result = await updateApp(ctx, id, body ?? {});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'delete':
|
|
84
|
+
if (!id) throw new Error('delete requires --id <app_id>');
|
|
85
|
+
result = await deleteApp(ctx, id);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Unsupported: apps ${v}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
printJson(result, Boolean(opts.json));
|
|
92
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readJsonBody } from '../lib/read-body.js';
|
|
2
|
+
import { getSdkProxy, requireSession } from '../lib/proxy/context.js';
|
|
3
|
+
import { printJson } from '../lib/output.js';
|
|
4
|
+
|
|
5
|
+
type CloudTarget = 'connections' | 'resources';
|
|
6
|
+
|
|
7
|
+
export async function runCloud(
|
|
8
|
+
target: CloudTarget,
|
|
9
|
+
verb: string,
|
|
10
|
+
opts: { id?: string; file?: string; json?: boolean },
|
|
11
|
+
extraArgs: string[],
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
requireSession();
|
|
14
|
+
const proxy = getSdkProxy();
|
|
15
|
+
const body = readJsonBody(opts.file);
|
|
16
|
+
const id = opts.id ?? extraArgs[0];
|
|
17
|
+
|
|
18
|
+
let method: string;
|
|
19
|
+
let params: unknown[];
|
|
20
|
+
|
|
21
|
+
if (target === 'connections') {
|
|
22
|
+
const map: Record<string, [string, unknown[]]> = {
|
|
23
|
+
create: ['connections.create', [body]],
|
|
24
|
+
list: ['connections.list', []],
|
|
25
|
+
get: ['connections.fetch', [id]],
|
|
26
|
+
fetch: ['connections.fetch', [id]],
|
|
27
|
+
delete: ['connections.delete', [id]],
|
|
28
|
+
update: ['connections.update', [id, body]],
|
|
29
|
+
complete: ['connections.complete', [id, body]],
|
|
30
|
+
validate: ['connections.validate', [id]],
|
|
31
|
+
};
|
|
32
|
+
const entry = map[verb.toLowerCase()];
|
|
33
|
+
if (!entry) throw new Error(`Unknown cloud connections verb: ${verb}`);
|
|
34
|
+
[method, params] = entry;
|
|
35
|
+
} else {
|
|
36
|
+
const map: Record<string, [string, unknown[]]> = {
|
|
37
|
+
list: ['resources.list', [body]],
|
|
38
|
+
import: ['resources.import', [body]],
|
|
39
|
+
provision: ['resources.provision', [body]],
|
|
40
|
+
'import-persist': ['resources.importAndPersist', [body]],
|
|
41
|
+
'provision-persist': ['resources.provisionAndPersist', [body]],
|
|
42
|
+
};
|
|
43
|
+
const entry = map[verb.toLowerCase()];
|
|
44
|
+
if (!entry) throw new Error(`Unknown cloud resources verb: ${verb}`);
|
|
45
|
+
[method, params] = entry;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await proxy.execute('cloud', method, params);
|
|
49
|
+
printJson(result, Boolean(opts.json));
|
|
50
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { listResourceTypes } from '../lib/resources.js';
|
|
2
|
+
import { DB_PROXY_METHODS, GRAPH_PROXY_METHODS } from '../lib/db-methods.js';
|
|
3
|
+
|
|
4
|
+
const TOP_LEVEL = [
|
|
5
|
+
'login',
|
|
6
|
+
'logout',
|
|
7
|
+
'whoami',
|
|
8
|
+
'profiles',
|
|
9
|
+
'workspaces',
|
|
10
|
+
'link',
|
|
11
|
+
'unlink',
|
|
12
|
+
'init',
|
|
13
|
+
'install',
|
|
14
|
+
'start',
|
|
15
|
+
'stop',
|
|
16
|
+
'status',
|
|
17
|
+
'products',
|
|
18
|
+
'apps',
|
|
19
|
+
'resources',
|
|
20
|
+
'cloud',
|
|
21
|
+
'db',
|
|
22
|
+
'graph',
|
|
23
|
+
'secrets',
|
|
24
|
+
'generate',
|
|
25
|
+
'completion',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const ENTITY_CRUD = 'list get create update delete';
|
|
29
|
+
|
|
30
|
+
export function runCompletion(shell: 'bash' | 'zsh' | 'fish'): void {
|
|
31
|
+
const resources = listResourceTypes();
|
|
32
|
+
const crud = 'create list get update delete connect';
|
|
33
|
+
|
|
34
|
+
if (shell === 'bash') {
|
|
35
|
+
console.log(`
|
|
36
|
+
_ductape_completions() {
|
|
37
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
38
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
39
|
+
local cmd="\${COMP_WORDS[1]}"
|
|
40
|
+
|
|
41
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
42
|
+
COMPREPLY=( $(compgen -W "${TOP_LEVEL.join(' ')}" -- "$cur") )
|
|
43
|
+
return
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
case "$cmd" in
|
|
47
|
+
products)
|
|
48
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
49
|
+
COMPREPLY=( $(compgen -W "${ENTITY_CRUD}" -- "$cur") )
|
|
50
|
+
fi
|
|
51
|
+
;;
|
|
52
|
+
apps)
|
|
53
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
54
|
+
COMPREPLY=( $(compgen -W "import ${ENTITY_CRUD}" -- "$cur") )
|
|
55
|
+
elif [[ \${COMP_CWORD} -eq 3 && "\${COMP_WORDS[2]}" == "import" ]]; then
|
|
56
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
57
|
+
fi
|
|
58
|
+
;;
|
|
59
|
+
resources)
|
|
60
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
61
|
+
COMPREPLY=( $(compgen -W "types ${resources.join(' ')}" -- "$cur") )
|
|
62
|
+
elif [[ \${COMP_CWORD} -eq 3 ]]; then
|
|
63
|
+
COMPREPLY=( $(compgen -W "${crud}" -- "$cur") )
|
|
64
|
+
fi
|
|
65
|
+
;;
|
|
66
|
+
db)
|
|
67
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
68
|
+
COMPREPLY=( $(compgen -W "${DB_PROXY_METHODS.join(' ')}" -- "$cur") )
|
|
69
|
+
fi
|
|
70
|
+
;;
|
|
71
|
+
graph)
|
|
72
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
73
|
+
COMPREPLY=( $(compgen -W "${GRAPH_PROXY_METHODS.join(' ')}" -- "$cur") )
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
profiles|workspaces)
|
|
77
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
78
|
+
COMPREPLY=( $(compgen -W "list use" -- "$cur") )
|
|
79
|
+
fi
|
|
80
|
+
;;
|
|
81
|
+
generate)
|
|
82
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
83
|
+
COMPREPLY=( $(compgen -W "payload snippet" -- "$cur") )
|
|
84
|
+
fi
|
|
85
|
+
;;
|
|
86
|
+
esac
|
|
87
|
+
}
|
|
88
|
+
complete -F _ductape_completions ductape
|
|
89
|
+
`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (shell === 'zsh') {
|
|
94
|
+
console.log(`
|
|
95
|
+
#compdef ductape
|
|
96
|
+
_ductape() {
|
|
97
|
+
local -a commands
|
|
98
|
+
commands=(
|
|
99
|
+
${TOP_LEVEL.map((c) => `'${c}:command'`).join('\n ')}
|
|
100
|
+
)
|
|
101
|
+
_arguments '1: :->cmd' '*: :->args'
|
|
102
|
+
case $state in
|
|
103
|
+
cmd) _describe 'command' commands ;;
|
|
104
|
+
args)
|
|
105
|
+
case $words[1] in
|
|
106
|
+
resources) _arguments '2: :(types ${resources.join(' ')})' '3: :(${crud.split(' ').join(' ')})' ;;
|
|
107
|
+
db) _arguments '2: :(${DB_PROXY_METHODS.join(' ')})' ;;
|
|
108
|
+
graph) _arguments '2: :(${GRAPH_PROXY_METHODS.join(' ')})' ;;
|
|
109
|
+
esac
|
|
110
|
+
;;
|
|
111
|
+
esac
|
|
112
|
+
}
|
|
113
|
+
_ductape
|
|
114
|
+
`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`# Fish: use ductape completion bash | source`);
|
|
119
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readJsonBody } from '../lib/read-body.js';
|
|
2
|
+
import { setDatabaseContext } from '../lib/context-store.js';
|
|
3
|
+
import { DB_PROXY_METHODS } from '../lib/db-methods.js';
|
|
4
|
+
import { getDbContext, getDbProxy, requireSession } from '../lib/proxy/context.js';
|
|
5
|
+
import { loadRuntimeContext } from '../lib/context-store.js';
|
|
6
|
+
import { printJson } from '../lib/output.js';
|
|
7
|
+
|
|
8
|
+
export async function runDb(
|
|
9
|
+
method: string,
|
|
10
|
+
opts: { file?: string; json?: boolean },
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const session = requireSession();
|
|
13
|
+
const proxy = getDbProxy(session);
|
|
14
|
+
const runtime = loadRuntimeContext(session.project.workspace_id, session.project.product_tag);
|
|
15
|
+
const dbContext = runtime?.database_context ?? null;
|
|
16
|
+
const body = readJsonBody(opts.file) as Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
const m = method.toLowerCase();
|
|
19
|
+
if (!DB_PROXY_METHODS.includes(m as (typeof DB_PROXY_METHODS)[number]) && !m.includes('.')) {
|
|
20
|
+
console.warn(`Method "${m}" not in known list; sending anyway.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let params: unknown[] = [];
|
|
24
|
+
if (m === 'connect') {
|
|
25
|
+
const cfg = {
|
|
26
|
+
product: session.project.product_tag,
|
|
27
|
+
env: session.project.env_slug,
|
|
28
|
+
database: body.database ?? body.tag,
|
|
29
|
+
...body,
|
|
30
|
+
};
|
|
31
|
+
params = [cfg];
|
|
32
|
+
} else if (m === 'list') {
|
|
33
|
+
params = [session.project.product_tag];
|
|
34
|
+
} else if (Object.keys(body).length > 0) {
|
|
35
|
+
params = [
|
|
36
|
+
{
|
|
37
|
+
product: session.project.product_tag,
|
|
38
|
+
env: session.project.env_slug,
|
|
39
|
+
...body,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await proxy.execute(m, params, dbContext);
|
|
45
|
+
|
|
46
|
+
if (m === 'connect' && params[0] && typeof params[0] === 'object') {
|
|
47
|
+
const cfg = params[0] as { database: string; env: string; product: string };
|
|
48
|
+
setDatabaseContext(session.project.workspace_id, session.project.product_tag, session.project.env_slug, {
|
|
49
|
+
database: String(cfg.database),
|
|
50
|
+
env: String(cfg.env),
|
|
51
|
+
product: String(cfg.product),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
printJson(result, Boolean(opts.json));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function runDbContext(json: boolean): void {
|
|
59
|
+
const session = requireSession();
|
|
60
|
+
const ctx = getDbContext(session);
|
|
61
|
+
const runtime = loadRuntimeContext(session.project.workspace_id, session.project.product_tag);
|
|
62
|
+
printJson({ database_context: runtime?.database_context ?? ctx }, json);
|
|
63
|
+
}
|