@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,28 @@
|
|
|
1
|
+
import { saveProjectConfig } from '../lib/config.js';
|
|
2
|
+
import { detectLanguage, writeInitArtifacts } from '../lib/templates.js';
|
|
3
|
+
import { success } from '../lib/output.js';
|
|
4
|
+
import { runLink } from './link.js';
|
|
5
|
+
export async function runInit(opts) {
|
|
6
|
+
const dir = process.cwd();
|
|
7
|
+
const lang = opts.language && ['typescript', 'python', 'go', 'dotnet'].includes(opts.language)
|
|
8
|
+
? opts.language
|
|
9
|
+
: detectLanguage(dir);
|
|
10
|
+
writeInitArtifacts(dir, lang);
|
|
11
|
+
if (!opts.link) {
|
|
12
|
+
const stub = {
|
|
13
|
+
workspace_tag: '',
|
|
14
|
+
product_tag: '',
|
|
15
|
+
env_slug: opts.env ?? 'dev',
|
|
16
|
+
};
|
|
17
|
+
saveProjectConfig(dir, stub);
|
|
18
|
+
success(`Initialized Ductape (${lang}). Run \`ductape link\` to connect workspace/product.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await runLink({
|
|
22
|
+
workspace: opts.workspace,
|
|
23
|
+
product: opts.product,
|
|
24
|
+
env: opts.env,
|
|
25
|
+
dir,
|
|
26
|
+
});
|
|
27
|
+
success(`Initialized and linked Ductape project (${lang})`);
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runInstall(): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { resolvePlatformDir } from '../lib/config.js';
|
|
5
|
+
import { success } from '../lib/output.js';
|
|
6
|
+
function checkCommand(cmd, args) {
|
|
7
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore' });
|
|
8
|
+
return r.status === 0;
|
|
9
|
+
}
|
|
10
|
+
export function runInstall() {
|
|
11
|
+
const issues = [];
|
|
12
|
+
if (!checkCommand('docker', ['--version'])) {
|
|
13
|
+
issues.push('Docker is not installed or not on PATH');
|
|
14
|
+
}
|
|
15
|
+
else if (!checkCommand('docker', ['compose', 'version'])) {
|
|
16
|
+
issues.push('Docker Compose v2 is required (docker compose)');
|
|
17
|
+
}
|
|
18
|
+
if (!checkCommand('node', ['--version'])) {
|
|
19
|
+
issues.push('Node.js 18+ is recommended for running the CLI');
|
|
20
|
+
}
|
|
21
|
+
let platformDir;
|
|
22
|
+
try {
|
|
23
|
+
platformDir = resolvePlatformDir();
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
issues.push(e instanceof Error ? e.message : String(e));
|
|
27
|
+
platformDir = '';
|
|
28
|
+
}
|
|
29
|
+
if (platformDir) {
|
|
30
|
+
const envExample = path.join(platformDir, '.env.example');
|
|
31
|
+
const envFile = path.join(platformDir, '.env');
|
|
32
|
+
if (fs.existsSync(envExample) && !fs.existsSync(envFile)) {
|
|
33
|
+
fs.copyFileSync(envExample, envFile);
|
|
34
|
+
success(`Created ${envFile} from .env.example`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (issues.length > 0) {
|
|
38
|
+
console.error('Install check failed:\n' + issues.map((i) => ` - ${i}`).join('\n'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
success('Prerequisites OK. Run `ductape start` to launch the local platform.');
|
|
42
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadCredentials, saveProjectConfig, } from '../lib/config.js';
|
|
5
|
+
import { fetchWorkspaces, findWorkspace, getActiveWorkspace, promptWorkspaceSelection, setActiveWorkspace, workspaceId, workspaceLabel, workspaceTag, } from '../lib/workspaces.js';
|
|
6
|
+
import { success } from '../lib/output.js';
|
|
7
|
+
export async function runLink(opts) {
|
|
8
|
+
const creds = loadCredentials();
|
|
9
|
+
if (!creds)
|
|
10
|
+
throw new Error('Not logged in. Run: ductape login');
|
|
11
|
+
const rl = readline.createInterface({ input, output });
|
|
12
|
+
let workspaceSummary;
|
|
13
|
+
const selector = opts.workspace?.trim();
|
|
14
|
+
if (!selector) {
|
|
15
|
+
const active = getActiveWorkspace();
|
|
16
|
+
if (active?.id) {
|
|
17
|
+
console.log(`Using active workspace: ${active.name ?? active.tag ?? active.id}`);
|
|
18
|
+
workspaceSummary = {
|
|
19
|
+
workspace_id: active.id,
|
|
20
|
+
workspace_name: active.name,
|
|
21
|
+
tag: active.tag,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const workspaces = await fetchWorkspaces(opts.profile);
|
|
26
|
+
if (workspaces.length === 0) {
|
|
27
|
+
throw new Error('No workspaces on this account. Create one in the Workbench first.');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
workspaceSummary = await promptWorkspaceSelection(workspaces, {
|
|
31
|
+
message: 'Link project to workspace:',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const workspaces = await fetchWorkspaces(opts.profile);
|
|
38
|
+
const match = findWorkspace(workspaces, selector);
|
|
39
|
+
if (!match) {
|
|
40
|
+
throw new Error(`Workspace not found: ${selector}`);
|
|
41
|
+
}
|
|
42
|
+
workspaceSummary = match;
|
|
43
|
+
setActiveWorkspace(workspaceId(match), workspaceLabel(match), workspaceTag(match));
|
|
44
|
+
}
|
|
45
|
+
const workspaceIdValue = workspaceId(workspaceSummary);
|
|
46
|
+
const workspaceTagValue = workspaceTag(workspaceSummary);
|
|
47
|
+
const productTag = opts.product ?? (await rl.question('Product tag: '));
|
|
48
|
+
const envSlug = opts.env ?? (await rl.question('Environment slug (e.g. dev): '));
|
|
49
|
+
rl.close();
|
|
50
|
+
const targetDir = path.resolve(opts.dir ?? process.cwd());
|
|
51
|
+
const config = {
|
|
52
|
+
workspace_tag: workspaceTagValue,
|
|
53
|
+
workspace_id: workspaceIdValue,
|
|
54
|
+
product_tag: productTag.trim(),
|
|
55
|
+
env_slug: envSlug.trim(),
|
|
56
|
+
linked_profile: opts.profile,
|
|
57
|
+
};
|
|
58
|
+
saveProjectConfig(targetDir, config);
|
|
59
|
+
setActiveWorkspace(workspaceIdValue, workspaceLabel(workspaceSummary), workspaceTagValue);
|
|
60
|
+
success(`Linked project in ${path.join(targetDir, '.ductape/config.json')}`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import inputPrompt from '@inquirer/input';
|
|
2
|
+
import passwordPrompt from '@inquirer/password';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { getApiUrl, getActiveProfileName, saveCredentials } from '../lib/config.js';
|
|
5
|
+
import { parseJsonResponse } from '../lib/http.js';
|
|
6
|
+
import { finalizeLoginWorkspace } from '../lib/workspaces.js';
|
|
7
|
+
import { success } from '../lib/output.js';
|
|
8
|
+
function saveFromResult(user) {
|
|
9
|
+
saveCredentials({
|
|
10
|
+
user_id: user._id,
|
|
11
|
+
email: user.email,
|
|
12
|
+
auth_token: user.auth_token,
|
|
13
|
+
public_key: user.public_key,
|
|
14
|
+
firstname: user.firstname,
|
|
15
|
+
lastname: user.lastname,
|
|
16
|
+
workspaces: user.workspaces,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async function exchangeOAuthToken(apiUrl, token) {
|
|
20
|
+
const res = await fetch(`${apiUrl}/users/v1/auth/oauth-session`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ token }),
|
|
24
|
+
});
|
|
25
|
+
const body = await parseJsonResponse(res, `${apiUrl}/users/v1/auth/oauth-session`);
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
throw new Error(body.message ?? `OAuth exchange failed (${res.status})`);
|
|
28
|
+
const user = (body.data?.result ?? body.data);
|
|
29
|
+
if (!user?.auth_token)
|
|
30
|
+
throw new Error(body.message ?? 'Invalid OAuth token');
|
|
31
|
+
return user;
|
|
32
|
+
}
|
|
33
|
+
function openBrowser(url) {
|
|
34
|
+
const cmd = process.platform === 'darwin'
|
|
35
|
+
? 'open'
|
|
36
|
+
: process.platform === 'win32'
|
|
37
|
+
? 'start'
|
|
38
|
+
: 'xdg-open';
|
|
39
|
+
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
40
|
+
}
|
|
41
|
+
export async function runLogin(opts) {
|
|
42
|
+
const profileName = opts.profile ?? getActiveProfileName();
|
|
43
|
+
const apiUrl = getApiUrl(opts.profile);
|
|
44
|
+
if (opts.token) {
|
|
45
|
+
const user = await exchangeOAuthToken(apiUrl, opts.token);
|
|
46
|
+
saveFromResult(user);
|
|
47
|
+
success(`Logged in as ${user.email} (OAuth)`);
|
|
48
|
+
await finalizeLoginWorkspace({
|
|
49
|
+
profile: opts.profile,
|
|
50
|
+
workspace: opts.workspace,
|
|
51
|
+
skipPrompt: opts.skipWorkspaceSelect,
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (opts.browser) {
|
|
56
|
+
const path = opts.browser === 'github' ? '/users/v1/auth/github' : '/users/v1/auth/google';
|
|
57
|
+
const authUrl = `${apiUrl}${path}`;
|
|
58
|
+
console.log('Opening browser for OAuth login…');
|
|
59
|
+
console.log(`If the browser does not open, visit:\n ${authUrl}\n`);
|
|
60
|
+
console.log('After sign-in you will be redirected with ?loggedIn=true&token=… in the URL.\n' +
|
|
61
|
+
'Copy the token value and run:\n' +
|
|
62
|
+
' ductape login --token "<paste-token-here>"\n');
|
|
63
|
+
openBrowser(authUrl);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let email = opts.email;
|
|
67
|
+
let password = opts.password;
|
|
68
|
+
if (!email || !password) {
|
|
69
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
70
|
+
throw new Error('Interactive login requires a TTY. Pass --email and --password, or use --token / --browser.');
|
|
71
|
+
}
|
|
72
|
+
email = email ?? (await inputPrompt({ message: 'Email' }));
|
|
73
|
+
password =
|
|
74
|
+
password ??
|
|
75
|
+
(await passwordPrompt({
|
|
76
|
+
message: 'Password',
|
|
77
|
+
mask: '*',
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
const loginUrl = `${apiUrl}/users/v1/login`;
|
|
81
|
+
const res = await fetch(loginUrl, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ email, password }),
|
|
85
|
+
});
|
|
86
|
+
const body = await parseJsonResponse(res, loginUrl);
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(body.message ?? `Login failed (HTTP ${res.status}) — profile "${profileName}", ${loginUrl}`);
|
|
89
|
+
}
|
|
90
|
+
const user = (body.data?.result ?? body.data);
|
|
91
|
+
if (!user?.auth_token)
|
|
92
|
+
throw new Error(body.message ?? 'Login failed');
|
|
93
|
+
if (user.requires_verification) {
|
|
94
|
+
throw new Error('Account requires email verification. Complete verification in the Workbench first.');
|
|
95
|
+
}
|
|
96
|
+
saveFromResult(user);
|
|
97
|
+
success(`Logged in as ${user.email}`);
|
|
98
|
+
await finalizeLoginWorkspace({
|
|
99
|
+
profile: opts.profile,
|
|
100
|
+
workspace: opts.workspace,
|
|
101
|
+
skipPrompt: opts.skipWorkspaceSelect,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runLogout(): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { getApiUrl, loadGlobalConfig, resolvePlatformDir } from '../lib/config.js';
|
|
3
|
+
import { printJson, success } from '../lib/output.js';
|
|
4
|
+
const WORKBENCH_ONLY_COMPOSE = [
|
|
5
|
+
'-f',
|
|
6
|
+
'docker-compose.yml',
|
|
7
|
+
'-f',
|
|
8
|
+
'docker-compose.workbench.yml',
|
|
9
|
+
];
|
|
10
|
+
function normalizeViteApiBase(url) {
|
|
11
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
12
|
+
}
|
|
13
|
+
function runCompose(args, cwd) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn('docker', ['compose', ...args], {
|
|
16
|
+
cwd,
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
shell: process.platform === 'win32',
|
|
19
|
+
});
|
|
20
|
+
child.on('error', reject);
|
|
21
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function runStart(opts) {
|
|
25
|
+
const platformDir = resolvePlatformDir();
|
|
26
|
+
if (opts.remote) {
|
|
27
|
+
const apiBase = normalizeViteApiBase(getApiUrl('cloud'));
|
|
28
|
+
const composeArgs = [...WORKBENCH_ONLY_COMPOSE];
|
|
29
|
+
if (opts.build) {
|
|
30
|
+
const buildCode = await runCompose([
|
|
31
|
+
...composeArgs,
|
|
32
|
+
'build',
|
|
33
|
+
'--build-arg',
|
|
34
|
+
`VITE_API_BASE_URL=${apiBase}`,
|
|
35
|
+
'--build-arg',
|
|
36
|
+
'VITE_APP_ENV=development',
|
|
37
|
+
'workbench',
|
|
38
|
+
], platformDir);
|
|
39
|
+
if (buildCode !== 0)
|
|
40
|
+
throw new Error(`docker compose build exited with ${buildCode}`);
|
|
41
|
+
}
|
|
42
|
+
const code = await runCompose([...composeArgs, 'up', '-d', 'workbench'], platformDir);
|
|
43
|
+
if (code !== 0)
|
|
44
|
+
throw new Error(`docker compose exited with ${code}`);
|
|
45
|
+
success(`Workbench UI: http://localhost:4310 (API → ${apiBase})`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const composeArgs = opts.backendOnly
|
|
49
|
+
? ['-f', 'docker-compose.yml', '-f', 'docker-compose.backend.yml', 'up', '-d']
|
|
50
|
+
: ['up', '-d'];
|
|
51
|
+
if (opts.build) {
|
|
52
|
+
const upIdx = composeArgs.indexOf('up');
|
|
53
|
+
composeArgs.splice(upIdx + 1, 0, '--build');
|
|
54
|
+
}
|
|
55
|
+
const code = await runCompose(composeArgs, platformDir);
|
|
56
|
+
if (code !== 0)
|
|
57
|
+
throw new Error(`docker compose exited with ${code}`);
|
|
58
|
+
success('Ductape platform started (see platform/README.md for URLs)');
|
|
59
|
+
}
|
|
60
|
+
export async function runStop() {
|
|
61
|
+
const platformDir = resolvePlatformDir();
|
|
62
|
+
const code = await runCompose(['down'], platformDir);
|
|
63
|
+
if (code !== 0)
|
|
64
|
+
throw new Error(`docker compose exited with ${code}`);
|
|
65
|
+
success('Ductape platform stopped');
|
|
66
|
+
}
|
|
67
|
+
async function probeHealth(url) {
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
70
|
+
const hint = res.status === 502
|
|
71
|
+
? 'nginx returned 502 Bad Gateway — the proxy container is usually not running or not listening yet. Try: docker compose ps proxy && docker compose logs proxy --tail 50'
|
|
72
|
+
: res.status === 404
|
|
73
|
+
? 'route not found — check api-gateway nginx config vs Nest proxy routes'
|
|
74
|
+
: undefined;
|
|
75
|
+
return { url, ok: res.ok, status: res.status, hint };
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return {
|
|
79
|
+
url,
|
|
80
|
+
ok: false,
|
|
81
|
+
error: e instanceof Error ? e.message : String(e),
|
|
82
|
+
hint: 'cannot reach host — run `ductape start` (full stack) or ensure port 4311 / 8020 is up',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function runStatus(json) {
|
|
87
|
+
const global = loadGlobalConfig();
|
|
88
|
+
const localUrl = global.profiles.local?.api_url ?? 'http://localhost:4311/api';
|
|
89
|
+
const localBase = localUrl.replace(/\/$/, '');
|
|
90
|
+
const checks = {
|
|
91
|
+
profiles: global.profiles,
|
|
92
|
+
platform_dir: resolvePlatformDir(),
|
|
93
|
+
workbench_ui: 'http://localhost:4310',
|
|
94
|
+
};
|
|
95
|
+
checks.local_proxy_health = await probeHealth(`${localBase}/proxy/v1/sdk-proxy/health`);
|
|
96
|
+
checks.local_proxy_direct = await probeHealth('http://localhost:8020/proxy/v1/sdk-proxy/health');
|
|
97
|
+
checks.cloud_api = getApiUrl('cloud');
|
|
98
|
+
printJson(checks, json);
|
|
99
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CommandInteractiveFlags } from '../lib/interactive-opts.js';
|
|
2
|
+
export declare function runProducts(verb: string, opts: {
|
|
3
|
+
profile?: string;
|
|
4
|
+
status?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
tag?: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
} & CommandInteractiveFlags, extraArgs: string[]): Promise<void>;
|
|
10
|
+
export declare function runProductApps(verb: string, opts: {
|
|
11
|
+
profile?: string;
|
|
12
|
+
product?: string;
|
|
13
|
+
json?: boolean;
|
|
14
|
+
}, extraArgs: string[]): Promise<void>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { schemaKeyForEntity } from '../lib/forms/index.js';
|
|
2
|
+
import { isInteractive, promptTag } from '../lib/forms/prompt.js';
|
|
3
|
+
import { toInteractiveOpts } from '../lib/interactive-opts.js';
|
|
4
|
+
import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
|
|
5
|
+
import { createProduct, deleteProduct, getProduct, listProductApps, listProducts, updateProduct, } from '../lib/platform-api.js';
|
|
6
|
+
import { printJson } from '../lib/output.js';
|
|
7
|
+
import { requireWorkspaceContext } from '../lib/workspace-context.js';
|
|
8
|
+
const VERBS = ['list', 'get', 'create', 'update', 'delete'];
|
|
9
|
+
export async function runProducts(verb, opts, extraArgs) {
|
|
10
|
+
const v = verb.toLowerCase();
|
|
11
|
+
if (!VERBS.includes(v)) {
|
|
12
|
+
throw new Error(`Unknown products verb "${verb}". Use: ${VERBS.join(', ')}`);
|
|
13
|
+
}
|
|
14
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
15
|
+
const status = opts.status ?? 'all';
|
|
16
|
+
const interactive = toInteractiveOpts(opts);
|
|
17
|
+
const schemaKey = schemaKeyForEntity('products', v);
|
|
18
|
+
const body = v === 'create' || v === 'update'
|
|
19
|
+
? await resolveBody({
|
|
20
|
+
...interactive,
|
|
21
|
+
file: opts.file,
|
|
22
|
+
schemaKey,
|
|
23
|
+
patch: v === 'update',
|
|
24
|
+
requireBody: v === 'create',
|
|
25
|
+
})
|
|
26
|
+
: undefined;
|
|
27
|
+
const id = opts.id ?? extraArgs[0];
|
|
28
|
+
let tag = opts.tag ?? (extraArgs[0] && !opts.id ? extraArgs[0] : undefined);
|
|
29
|
+
if (v === 'update' && !tag && isInteractive(interactive)) {
|
|
30
|
+
tag = await promptTag('Product tag');
|
|
31
|
+
}
|
|
32
|
+
let result;
|
|
33
|
+
switch (v) {
|
|
34
|
+
case 'list':
|
|
35
|
+
result = await listProducts(ctx, status);
|
|
36
|
+
break;
|
|
37
|
+
case 'get':
|
|
38
|
+
result = await getProduct(ctx, { id, tag });
|
|
39
|
+
break;
|
|
40
|
+
case 'create':
|
|
41
|
+
if (!body || Object.keys(body).length === 0) {
|
|
42
|
+
throw new Error(`create requires a body. ${bodyRequiredHint('products:create')}`);
|
|
43
|
+
}
|
|
44
|
+
result = await createProduct(ctx, body);
|
|
45
|
+
break;
|
|
46
|
+
case 'update': {
|
|
47
|
+
const productTag = tag ?? body?.tag;
|
|
48
|
+
if (!productTag)
|
|
49
|
+
throw new Error('update requires --tag <product_tag> (or tag in body)');
|
|
50
|
+
const patch = body ?? {};
|
|
51
|
+
result = await updateProduct(ctx, productTag, patch);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'delete':
|
|
55
|
+
if (!id)
|
|
56
|
+
throw new Error('delete requires --id <product_id>');
|
|
57
|
+
result = await deleteProduct(ctx, id);
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`Unsupported: products ${v}`);
|
|
61
|
+
}
|
|
62
|
+
printJson(result, Boolean(opts.json));
|
|
63
|
+
}
|
|
64
|
+
export async function runProductApps(verb, opts, extraArgs) {
|
|
65
|
+
if (verb.toLowerCase() !== 'list') {
|
|
66
|
+
throw new Error('Only supported: ductape products apps list --product <id>');
|
|
67
|
+
}
|
|
68
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
69
|
+
const productId = opts.product ?? extraArgs[0];
|
|
70
|
+
if (!productId)
|
|
71
|
+
throw new Error('Provide --product <product_id>');
|
|
72
|
+
const result = await listProductApps(ctx, productId);
|
|
73
|
+
printJson(result, Boolean(opts.json));
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { loadGlobalConfig, saveGlobalConfig } from '../lib/config.js';
|
|
2
|
+
import { printJson, success } from '../lib/output.js';
|
|
3
|
+
export function runProfilesList(json) {
|
|
4
|
+
const cfg = loadGlobalConfig();
|
|
5
|
+
printJson({
|
|
6
|
+
default_profile: cfg.default_profile,
|
|
7
|
+
profiles: cfg.profiles,
|
|
8
|
+
}, json);
|
|
9
|
+
}
|
|
10
|
+
export function runProfilesUse(name) {
|
|
11
|
+
const cfg = loadGlobalConfig();
|
|
12
|
+
if (!cfg.profiles[name]) {
|
|
13
|
+
throw new Error(`Unknown profile "${name}"`);
|
|
14
|
+
}
|
|
15
|
+
cfg.default_profile = name;
|
|
16
|
+
saveGlobalConfig(cfg);
|
|
17
|
+
success(`Default profile set to "${name}" (${cfg.profiles[name].api_url})`);
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CommandInteractiveFlags } from '../lib/interactive-opts.js';
|
|
2
|
+
export declare function runResourcesList(json: boolean): void;
|
|
3
|
+
export declare function runResourceCrud(typeName: string, verb: string, opts: {
|
|
4
|
+
tag?: string;
|
|
5
|
+
file?: string;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
} & CommandInteractiveFlags, extraArgs: string[]): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { buildCrudParams, listResourceTypes, proxyMethodForVerb, resolveResourceType, } from '../lib/resources.js';
|
|
2
|
+
import { resolveResourceSchema } from '../lib/forms/index.js';
|
|
3
|
+
import { isInteractive, promptTag } from '../lib/forms/prompt.js';
|
|
4
|
+
import { toInteractiveOpts } from '../lib/interactive-opts.js';
|
|
5
|
+
import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
|
|
6
|
+
import { requireSession, getSdkProxy } from '../lib/proxy/context.js';
|
|
7
|
+
import { printJson } from '../lib/output.js';
|
|
8
|
+
const CRUD_VERBS = ['create', 'list', 'get', 'update', 'delete', 'connect'];
|
|
9
|
+
export function runResourcesList(json) {
|
|
10
|
+
printJson({ resource_types: listResourceTypes() }, json);
|
|
11
|
+
}
|
|
12
|
+
export async function runResourceCrud(typeName, verb, opts, extraArgs) {
|
|
13
|
+
const crud = verb.toLowerCase();
|
|
14
|
+
if (!CRUD_VERBS.includes(crud)) {
|
|
15
|
+
throw new Error(`Unknown verb "${verb}". Use: ${CRUD_VERBS.join(', ')}`);
|
|
16
|
+
}
|
|
17
|
+
const session = requireSession();
|
|
18
|
+
const module = resolveResourceType(typeName);
|
|
19
|
+
if (module === 'product' || module === 'app') {
|
|
20
|
+
if (crud === 'list') {
|
|
21
|
+
throw new Error(`Use \`ductape ${module}s list\` — listing uses the workspace REST API (not sdk-proxy).`);
|
|
22
|
+
}
|
|
23
|
+
if (crud === 'delete') {
|
|
24
|
+
throw new Error(`Use \`ductape ${module}s delete --id <id>\`.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const method = proxyMethodForVerb(crud);
|
|
28
|
+
const interactive = toInteractiveOpts(opts);
|
|
29
|
+
let tag = opts.tag ?? extraArgs[0];
|
|
30
|
+
if (!tag && ['get', 'update', 'delete'].includes(crud) && isInteractive(interactive)) {
|
|
31
|
+
tag = await promptTag('Component tag');
|
|
32
|
+
}
|
|
33
|
+
const needsBody = crud === 'create' || crud === 'update' || crud === 'connect';
|
|
34
|
+
const schema = needsBody ? resolveResourceSchema(module, crud) : undefined;
|
|
35
|
+
const body = needsBody
|
|
36
|
+
? await resolveBody({
|
|
37
|
+
...interactive,
|
|
38
|
+
file: opts.file,
|
|
39
|
+
schema,
|
|
40
|
+
patch: crud === 'update',
|
|
41
|
+
requireBody: crud === 'create',
|
|
42
|
+
})
|
|
43
|
+
: undefined;
|
|
44
|
+
if (crud === 'create' && (!body || Object.keys(body).length === 0)) {
|
|
45
|
+
throw new Error(`create requires a body. ${bodyRequiredHint(`${module}:create`)}`);
|
|
46
|
+
}
|
|
47
|
+
let payload = body;
|
|
48
|
+
if (crud === 'connect' && tag) {
|
|
49
|
+
payload = {
|
|
50
|
+
...(payload && typeof payload === 'object' ? payload : {}),
|
|
51
|
+
tag,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const params = buildCrudParams(module, method, session.project.product_tag, {
|
|
55
|
+
tag,
|
|
56
|
+
body: payload,
|
|
57
|
+
});
|
|
58
|
+
const proxy = getSdkProxy(session);
|
|
59
|
+
const result = await proxy.execute(module, method, params);
|
|
60
|
+
printJson(result, Boolean(opts.json));
|
|
61
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isInteractive, promptKey } from '../lib/forms/prompt.js';
|
|
2
|
+
import { toInteractiveOpts } from '../lib/interactive-opts.js';
|
|
3
|
+
import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
|
|
4
|
+
import { buildCrudParams, proxyMethodForVerb } from '../lib/resources.js';
|
|
5
|
+
import { getSdkProxy, requireSession } from '../lib/proxy/context.js';
|
|
6
|
+
import { printJson } from '../lib/output.js';
|
|
7
|
+
const VERBS = ['create', 'list', 'get', 'update', 'delete'];
|
|
8
|
+
export async function runSecrets(verb, opts) {
|
|
9
|
+
const crud = verb.toLowerCase();
|
|
10
|
+
if (!VERBS.includes(crud)) {
|
|
11
|
+
throw new Error(`Unknown secrets verb "${verb}". Use: ${VERBS.join(', ')}`);
|
|
12
|
+
}
|
|
13
|
+
const session = requireSession();
|
|
14
|
+
const method = proxyMethodForVerb(crud);
|
|
15
|
+
const interactive = toInteractiveOpts(opts);
|
|
16
|
+
let key = opts.key;
|
|
17
|
+
if (!key && ['fetch', 'delete', 'update'].includes(method) && isInteractive(interactive)) {
|
|
18
|
+
key = await promptKey();
|
|
19
|
+
}
|
|
20
|
+
const body = crud === 'create' || crud === 'update'
|
|
21
|
+
? await resolveBody({
|
|
22
|
+
...interactive,
|
|
23
|
+
file: opts.file,
|
|
24
|
+
schemaKey: `secrets:${crud}`,
|
|
25
|
+
patch: crud === 'update',
|
|
26
|
+
requireBody: crud === 'create',
|
|
27
|
+
})
|
|
28
|
+
: undefined;
|
|
29
|
+
if (crud === 'create' && (!body || Object.keys(body).length === 0)) {
|
|
30
|
+
throw new Error(`create requires a body. ${bodyRequiredHint('secrets:create')}`);
|
|
31
|
+
}
|
|
32
|
+
const params = buildCrudParams('secrets', method, session.project.product_tag, {
|
|
33
|
+
tag: key,
|
|
34
|
+
body,
|
|
35
|
+
});
|
|
36
|
+
const result = await getSdkProxy(session).execute('secrets', method, params);
|
|
37
|
+
printJson(result, Boolean(opts.json));
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export function runUnlink(opts) {
|
|
6
|
+
const dir = path.resolve(opts.dir ?? process.cwd());
|
|
7
|
+
const found = findProjectConfig(dir);
|
|
8
|
+
if (!found) {
|
|
9
|
+
throw new Error('No linked project in this directory');
|
|
10
|
+
}
|
|
11
|
+
const configPath = path.join(found.dir, PROJECT_CONFIG_PATH);
|
|
12
|
+
fs.unlinkSync(configPath);
|
|
13
|
+
success(`Removed ${configPath}`);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runWhoami(json: boolean): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
export function runWhoami(json) {
|
|
5
|
+
const creds = loadCredentials();
|
|
6
|
+
const project = findProjectConfig();
|
|
7
|
+
const active = getActiveWorkspace();
|
|
8
|
+
const profile = project?.config
|
|
9
|
+
? getActiveProfileName(project.config)
|
|
10
|
+
: loadGlobalConfig().default_profile;
|
|
11
|
+
printJson({
|
|
12
|
+
logged_in: Boolean(creds),
|
|
13
|
+
email: creds?.email,
|
|
14
|
+
user_id: creds?.user_id,
|
|
15
|
+
profile,
|
|
16
|
+
api_url: getApiUrl(profile),
|
|
17
|
+
active_workspace: active
|
|
18
|
+
? { id: active.id, name: active.name }
|
|
19
|
+
: null,
|
|
20
|
+
project: project?.config ?? null,
|
|
21
|
+
}, json);
|
|
22
|
+
}
|