@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
package/API_PARITY.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CLI ↔ API parity (Nest / Workbench)
|
|
2
|
+
|
|
3
|
+
The CLI uses the **same REST paths as the Workbench and TypeScript SDK**.
|
|
4
|
+
|
|
5
|
+
| Profile | `api_url` | Example login URL |
|
|
6
|
+
|---------|-----------|-------------------|
|
|
7
|
+
| `cloud` | `https://api.ductape.app` | `https://api.ductape.app/users/v1/login` |
|
|
8
|
+
| `local` | `http://localhost:4311/api` | `http://localhost:4311/api/users/v1/login` |
|
|
9
|
+
|
|
10
|
+
Production does **not** use an `/api` prefix; the local docker gateway does.
|
|
11
|
+
|
|
12
|
+
## Gateway routing
|
|
13
|
+
|
|
14
|
+
| Client calls | Gateway (`/api/...`) | Nest service receives |
|
|
15
|
+
|--------------|----------------------|------------------------|
|
|
16
|
+
| `{api}/users/v1/login` | `/api/users/` → users:8002 | `/users/v1/login` |
|
|
17
|
+
| `{api}/workspaces/v1/fetch/...` | `/api/workspaces/` → workspaces:8001 | `/workspaces/v1/...` |
|
|
18
|
+
| `{api}/apps/v1/import` | `/api/apps/` → apps:8004 | `/apps/v1/import` |
|
|
19
|
+
| `{api}/integrations/v1/create` | `/api/integrations/` → integrations:8009 | `/integrations/v1/create` |
|
|
20
|
+
| `{api}/proxy/v1/sdk-proxy/execute` | `/api/proxy/` → proxy:8020 | encrypted sdk-proxy |
|
|
21
|
+
|
|
22
|
+
Legacy Express services are **not** called by the CLI. All commands target Nest controllers under `platform/nest/apps/*/src/**/controllers/`.
|
|
23
|
+
|
|
24
|
+
## Command → endpoint map
|
|
25
|
+
|
|
26
|
+
| CLI | Method | Path (after `{api_url}`) | Workbench equivalent |
|
|
27
|
+
|-----|--------|---------------------------|-------------------|
|
|
28
|
+
| `ductape login` | POST | `/users/v1/login` | Login modal |
|
|
29
|
+
| `ductape workspaces list` | GET | `/workspaces/v1/fetch/:user_id` | Workspace list |
|
|
30
|
+
| `ductape products list` | GET | `/integrations/v1/workspace/:id/:status` | Products sidebar |
|
|
31
|
+
| `ductape products create` | POST | `/integrations/v1/create` | Create product |
|
|
32
|
+
| `ductape products update` | sdk-proxy | `product.update` | Product settings |
|
|
33
|
+
| `ductape products delete` | DELETE | `/integrations/v1/:id` | Delete product |
|
|
34
|
+
| `ductape apps list` | GET | `/apps/v1/workspace/:id/:status` | Apps sidebar |
|
|
35
|
+
| `ductape apps create` | POST | `/apps/v1/create` | Create app |
|
|
36
|
+
| `ductape apps import` | POST multipart | `/apps/v1/import` | App import dialog |
|
|
37
|
+
| `ductape apps update` | PUT | `/apps/v1/:app_id` | App settings |
|
|
38
|
+
| `ductape apps delete` | DELETE | `/apps/v1/:app_id` | Delete app |
|
|
39
|
+
| `ductape resources *` | POST | `/proxy/v1/sdk-proxy/execute` | SDK proxy (encrypted) |
|
|
40
|
+
| `ductape db *` | POST | `/proxy/v1/db-proxy/...` | Database proxy |
|
|
41
|
+
| `ductape graph *` | POST | `/proxy/v1/graph-proxy/...` | Graph proxy |
|
|
42
|
+
|
|
43
|
+
## Import body fields (`apps import`)
|
|
44
|
+
|
|
45
|
+
Matches `AppsImportController` and `@ductape/sdk` `AppApiService.importApp`:
|
|
46
|
+
|
|
47
|
+
| Field | CLI / SDK value |
|
|
48
|
+
|-------|-----------------|
|
|
49
|
+
| `type` | `PostmanV2.1` or `OpenApiV3.0` |
|
|
50
|
+
| `version` | default `0.0.1` |
|
|
51
|
+
| `update` | `true` / `false` |
|
|
52
|
+
| `workspace_id`, `user_id`, `public_key` | from session |
|
|
53
|
+
| `app_id` | optional (`--app-id` / `--app-tag`) |
|
|
54
|
+
| `file` | multipart upload |
|
|
55
|
+
|
|
56
|
+
## SDK base URL note
|
|
57
|
+
|
|
58
|
+
The TS SDK client uses host `https://api.ductape.app` with paths like `/apps/v1/...` (no `/api` segment). Production routing may terminate TLS at a layer that forwards directly to services. **CLI and Workbench** use `{host}/api` as `api_url` — always set `ductape profiles use local` to `http://localhost:4311/api` for local parity.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `ductape apps import <file> --type postman|openapi` (Postman v2.1 / OpenAPI 3.0)
|
|
8
|
+
- Project config uses `workspace_tag` (CLI resolves `workspace_id` automatically)
|
|
9
|
+
- `ductape products` and `ductape apps` CRUD (workspace REST + sdk-proxy update)
|
|
10
|
+
- `ductape products apps list --product <id>`
|
|
11
|
+
- CLI docs split into `docs/docs/cli/` folder with per-topic pages
|
|
12
|
+
- Interactive workspace selection after `ductape login` (stored in `~/.ductape/config.json`)
|
|
13
|
+
- `ductape workspaces current`, `workspaces switch`, `workspaces refresh`
|
|
14
|
+
- `ductape login --workspace`, `--skip-workspace-select`
|
|
15
|
+
- `ductape link` defaults to active workspace; `whoami` shows `active_workspace`
|
|
16
|
+
- Switching workspaces updates linked `.ductape/config.json` when present
|
|
17
|
+
|
|
18
|
+
## 0.2.0
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- OAuth login: `ductape login --browser`, `ductape login --token`
|
|
23
|
+
- `ductape install`, `ductape unlink`, `ductape workspaces list|use`
|
|
24
|
+
- `ductape graph <verb>` (graph-proxy, persisted graph context)
|
|
25
|
+
- Expanded `ductape db` (db-proxy, `db context`, more methods)
|
|
26
|
+
- `ductape secrets` CRUD
|
|
27
|
+
- `ductape generate payload|snippet`
|
|
28
|
+
- Shell completions: `ductape completion bash|zsh`
|
|
29
|
+
- Language bootstrap templates (TypeScript, Python, Go, .NET)
|
|
30
|
+
- Gzip support for large proxy payloads
|
|
31
|
+
- Runtime context store (`~/.ductape/runtime-context.json`)
|
|
32
|
+
- Unit tests (`npm test`)
|
|
33
|
+
|
|
34
|
+
## 0.1.0
|
|
35
|
+
|
|
36
|
+
- Initial CLI: login, profiles, link, init, start/stop/status, resources CRUD, cloud, db basics
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @ductape/cli
|
|
2
|
+
|
|
3
|
+
Terminal interface for Ductape — **same transport as the Workbench** (encrypted sdk-proxy / db-proxy / graph-proxy).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd cli
|
|
9
|
+
npm install
|
|
10
|
+
npm run build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Global `ductape` command (optional)
|
|
14
|
+
|
|
15
|
+
`npm link` installs into npm’s global prefix. If you see `EACCES` on `/usr/local/lib/node_modules`, that prefix is owned by root (common with the system Node installer). Use one of these:
|
|
16
|
+
|
|
17
|
+
**Option A — run without linking (no sudo)**
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm exec ductape -- login
|
|
21
|
+
npm exec ductape -- resources storage list
|
|
22
|
+
# or: node dist/index.js login
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Option B — user-owned global prefix (recommended for `npm link`)**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
mkdir -p ~/.npm-global
|
|
29
|
+
npm config set prefix "$HOME/.npm-global"
|
|
30
|
+
# Add to ~/.zshrc (or ~/.bashrc):
|
|
31
|
+
# export PATH="$HOME/.npm-global/bin:$PATH"
|
|
32
|
+
source ~/.zshrc
|
|
33
|
+
cd cli && npm link
|
|
34
|
+
ductape --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Do not use `sudo npm link`; it mixes root-owned globals with your normal user.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ductape install
|
|
43
|
+
ductape login # uses `cloud` profile (https://api.ductape.app)
|
|
44
|
+
# Local platform (UI + API on your machine):
|
|
45
|
+
ductape profiles use local
|
|
46
|
+
ductape start
|
|
47
|
+
ductape login --profile local
|
|
48
|
+
|
|
49
|
+
# Workbench UI locally, cloud API (no local backend):
|
|
50
|
+
ductape start --remote --build
|
|
51
|
+
ductape login
|
|
52
|
+
ductape init --link
|
|
53
|
+
ductape resources storage list
|
|
54
|
+
ductape generate snippet storage upload -l typescript -o upload.ts
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Command overview
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
ductape login|logout|whoami|install
|
|
61
|
+
ductape profiles list|use <name>
|
|
62
|
+
ductape workspaces list|current|use # interactive picker on login/link/use
|
|
63
|
+
ductape products list|get|create|update|delete
|
|
64
|
+
ductape apps list|get|create|update|delete|import
|
|
65
|
+
ductape init|link|unlink
|
|
66
|
+
ductape start|stop|status # status explains proxy 502 when backend is down
|
|
67
|
+
ductape resources <type> <verb> # CRUD via sdk-proxy (interactive prompts or -f JSON)
|
|
68
|
+
ductape cloud connections list|create|get|update|complete|validate|delete
|
|
69
|
+
ductape cloud resources list|import|provision …
|
|
70
|
+
ductape db <verb> # db-proxy runtime
|
|
71
|
+
ductape graph <verb> # graph-proxy runtime
|
|
72
|
+
ductape secrets <verb>
|
|
73
|
+
ductape generate payload|snippet …
|
|
74
|
+
ductape completion bash|zsh
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Documentation
|
|
78
|
+
|
|
79
|
+
Full reference: [docs/docs/cli/](../docs/docs/cli/index.mdx)
|
|
80
|
+
|
|
81
|
+
## Tests
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm test
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Changelog
|
|
88
|
+
|
|
89
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
export async function runAppsImport(file, opts) {
|
|
7
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
8
|
+
const format = parseImportFormat(opts.type);
|
|
9
|
+
let appId = opts.appId;
|
|
10
|
+
if (!appId && opts.appTag) {
|
|
11
|
+
const app = (await getApp(ctx, { tag: opts.appTag }));
|
|
12
|
+
appId = app._id;
|
|
13
|
+
if (!appId)
|
|
14
|
+
throw new Error(`App not found for tag: ${opts.appTag}`);
|
|
15
|
+
}
|
|
16
|
+
const result = await importAppFromFile(ctx, {
|
|
17
|
+
filePath: path.resolve(file),
|
|
18
|
+
format,
|
|
19
|
+
appId,
|
|
20
|
+
update: Boolean(opts.update),
|
|
21
|
+
version: opts.version,
|
|
22
|
+
});
|
|
23
|
+
if (opts.json) {
|
|
24
|
+
printJson(result, true);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const label = format === 'postman' ? 'Postman v2.1' : 'OpenAPI 3.0';
|
|
28
|
+
success(`Imported ${label} collection from ${path.basename(file)}`);
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CommandInteractiveFlags } from '../lib/interactive-opts.js';
|
|
2
|
+
export declare function runApps(verb: string, opts: {
|
|
3
|
+
profile?: string;
|
|
4
|
+
status?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
tag?: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
proxy?: boolean;
|
|
10
|
+
} & CommandInteractiveFlags, extraArgs: string[]): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { createApp, deleteApp, getApp, listApps, updateApp, updateAppViaProxy, } 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 runApps(verb, opts, extraArgs) {
|
|
10
|
+
const v = verb.toLowerCase();
|
|
11
|
+
if (!VERBS.includes(v)) {
|
|
12
|
+
throw new Error(`Unknown apps 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('apps', 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' && (opts.proxy || !id) && !tag && isInteractive(interactive)) {
|
|
30
|
+
tag = await promptTag('App tag');
|
|
31
|
+
}
|
|
32
|
+
let result;
|
|
33
|
+
switch (v) {
|
|
34
|
+
case 'list':
|
|
35
|
+
result = await listApps(ctx, status);
|
|
36
|
+
break;
|
|
37
|
+
case 'get':
|
|
38
|
+
result = await getApp(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('apps:create')}`);
|
|
43
|
+
}
|
|
44
|
+
result = await createApp(ctx, body);
|
|
45
|
+
break;
|
|
46
|
+
case 'update': {
|
|
47
|
+
if (opts.proxy || tag) {
|
|
48
|
+
const appTag = tag ?? body?.tag;
|
|
49
|
+
if (!appTag)
|
|
50
|
+
throw new Error('update via proxy requires --tag <app_tag>');
|
|
51
|
+
result = await updateAppViaProxy(ctx, appTag, body ?? {});
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (!id)
|
|
55
|
+
throw new Error('update requires --id <app_id> or --tag with --proxy');
|
|
56
|
+
result = await updateApp(ctx, id, body ?? {});
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case 'delete':
|
|
60
|
+
if (!id)
|
|
61
|
+
throw new Error('delete requires --id <app_id>');
|
|
62
|
+
result = await deleteApp(ctx, id);
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`Unsupported: apps ${v}`);
|
|
66
|
+
}
|
|
67
|
+
printJson(result, Boolean(opts.json));
|
|
68
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
export async function runCloud(target, verb, opts, extraArgs) {
|
|
5
|
+
requireSession();
|
|
6
|
+
const proxy = getSdkProxy();
|
|
7
|
+
const body = readJsonBody(opts.file);
|
|
8
|
+
const id = opts.id ?? extraArgs[0];
|
|
9
|
+
let method;
|
|
10
|
+
let params;
|
|
11
|
+
if (target === 'connections') {
|
|
12
|
+
const map = {
|
|
13
|
+
create: ['connections.create', [body]],
|
|
14
|
+
list: ['connections.list', []],
|
|
15
|
+
get: ['connections.fetch', [id]],
|
|
16
|
+
fetch: ['connections.fetch', [id]],
|
|
17
|
+
delete: ['connections.delete', [id]],
|
|
18
|
+
update: ['connections.update', [id, body]],
|
|
19
|
+
complete: ['connections.complete', [id, body]],
|
|
20
|
+
validate: ['connections.validate', [id]],
|
|
21
|
+
};
|
|
22
|
+
const entry = map[verb.toLowerCase()];
|
|
23
|
+
if (!entry)
|
|
24
|
+
throw new Error(`Unknown cloud connections verb: ${verb}`);
|
|
25
|
+
[method, params] = entry;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const map = {
|
|
29
|
+
list: ['resources.list', [body]],
|
|
30
|
+
import: ['resources.import', [body]],
|
|
31
|
+
provision: ['resources.provision', [body]],
|
|
32
|
+
'import-persist': ['resources.importAndPersist', [body]],
|
|
33
|
+
'provision-persist': ['resources.provisionAndPersist', [body]],
|
|
34
|
+
};
|
|
35
|
+
const entry = map[verb.toLowerCase()];
|
|
36
|
+
if (!entry)
|
|
37
|
+
throw new Error(`Unknown cloud resources verb: ${verb}`);
|
|
38
|
+
[method, params] = entry;
|
|
39
|
+
}
|
|
40
|
+
const result = await proxy.execute('cloud', method, params);
|
|
41
|
+
printJson(result, Boolean(opts.json));
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCompletion(shell: 'bash' | 'zsh' | 'fish'): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { listResourceTypes } from '../lib/resources.js';
|
|
2
|
+
import { DB_PROXY_METHODS, GRAPH_PROXY_METHODS } from '../lib/db-methods.js';
|
|
3
|
+
const TOP_LEVEL = [
|
|
4
|
+
'login',
|
|
5
|
+
'logout',
|
|
6
|
+
'whoami',
|
|
7
|
+
'profiles',
|
|
8
|
+
'workspaces',
|
|
9
|
+
'link',
|
|
10
|
+
'unlink',
|
|
11
|
+
'init',
|
|
12
|
+
'install',
|
|
13
|
+
'start',
|
|
14
|
+
'stop',
|
|
15
|
+
'status',
|
|
16
|
+
'products',
|
|
17
|
+
'apps',
|
|
18
|
+
'resources',
|
|
19
|
+
'cloud',
|
|
20
|
+
'db',
|
|
21
|
+
'graph',
|
|
22
|
+
'secrets',
|
|
23
|
+
'generate',
|
|
24
|
+
'completion',
|
|
25
|
+
];
|
|
26
|
+
const ENTITY_CRUD = 'list get create update delete';
|
|
27
|
+
export function runCompletion(shell) {
|
|
28
|
+
const resources = listResourceTypes();
|
|
29
|
+
const crud = 'create list get update delete connect';
|
|
30
|
+
if (shell === 'bash') {
|
|
31
|
+
console.log(`
|
|
32
|
+
_ductape_completions() {
|
|
33
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
34
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
35
|
+
local cmd="\${COMP_WORDS[1]}"
|
|
36
|
+
|
|
37
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
38
|
+
COMPREPLY=( $(compgen -W "${TOP_LEVEL.join(' ')}" -- "$cur") )
|
|
39
|
+
return
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
case "$cmd" in
|
|
43
|
+
products)
|
|
44
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
45
|
+
COMPREPLY=( $(compgen -W "${ENTITY_CRUD}" -- "$cur") )
|
|
46
|
+
fi
|
|
47
|
+
;;
|
|
48
|
+
apps)
|
|
49
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
50
|
+
COMPREPLY=( $(compgen -W "import ${ENTITY_CRUD}" -- "$cur") )
|
|
51
|
+
elif [[ \${COMP_CWORD} -eq 3 && "\${COMP_WORDS[2]}" == "import" ]]; then
|
|
52
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
53
|
+
fi
|
|
54
|
+
;;
|
|
55
|
+
resources)
|
|
56
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
57
|
+
COMPREPLY=( $(compgen -W "types ${resources.join(' ')}" -- "$cur") )
|
|
58
|
+
elif [[ \${COMP_CWORD} -eq 3 ]]; then
|
|
59
|
+
COMPREPLY=( $(compgen -W "${crud}" -- "$cur") )
|
|
60
|
+
fi
|
|
61
|
+
;;
|
|
62
|
+
db)
|
|
63
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
64
|
+
COMPREPLY=( $(compgen -W "${DB_PROXY_METHODS.join(' ')}" -- "$cur") )
|
|
65
|
+
fi
|
|
66
|
+
;;
|
|
67
|
+
graph)
|
|
68
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
69
|
+
COMPREPLY=( $(compgen -W "${GRAPH_PROXY_METHODS.join(' ')}" -- "$cur") )
|
|
70
|
+
fi
|
|
71
|
+
;;
|
|
72
|
+
profiles|workspaces)
|
|
73
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
74
|
+
COMPREPLY=( $(compgen -W "list use" -- "$cur") )
|
|
75
|
+
fi
|
|
76
|
+
;;
|
|
77
|
+
generate)
|
|
78
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
79
|
+
COMPREPLY=( $(compgen -W "payload snippet" -- "$cur") )
|
|
80
|
+
fi
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
}
|
|
84
|
+
complete -F _ductape_completions ductape
|
|
85
|
+
`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (shell === 'zsh') {
|
|
89
|
+
console.log(`
|
|
90
|
+
#compdef ductape
|
|
91
|
+
_ductape() {
|
|
92
|
+
local -a commands
|
|
93
|
+
commands=(
|
|
94
|
+
${TOP_LEVEL.map((c) => `'${c}:command'`).join('\n ')}
|
|
95
|
+
)
|
|
96
|
+
_arguments '1: :->cmd' '*: :->args'
|
|
97
|
+
case $state in
|
|
98
|
+
cmd) _describe 'command' commands ;;
|
|
99
|
+
args)
|
|
100
|
+
case $words[1] in
|
|
101
|
+
resources) _arguments '2: :(types ${resources.join(' ')})' '3: :(${crud.split(' ').join(' ')})' ;;
|
|
102
|
+
db) _arguments '2: :(${DB_PROXY_METHODS.join(' ')})' ;;
|
|
103
|
+
graph) _arguments '2: :(${GRAPH_PROXY_METHODS.join(' ')})' ;;
|
|
104
|
+
esac
|
|
105
|
+
;;
|
|
106
|
+
esac
|
|
107
|
+
}
|
|
108
|
+
_ductape
|
|
109
|
+
`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(`# Fish: use ductape completion bash | source`);
|
|
113
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
export async function runDb(method, opts) {
|
|
8
|
+
const session = requireSession();
|
|
9
|
+
const proxy = getDbProxy(session);
|
|
10
|
+
const runtime = loadRuntimeContext(session.project.workspace_id, session.project.product_tag);
|
|
11
|
+
const dbContext = runtime?.database_context ?? null;
|
|
12
|
+
const body = readJsonBody(opts.file);
|
|
13
|
+
const m = method.toLowerCase();
|
|
14
|
+
if (!DB_PROXY_METHODS.includes(m) && !m.includes('.')) {
|
|
15
|
+
console.warn(`Method "${m}" not in known list; sending anyway.`);
|
|
16
|
+
}
|
|
17
|
+
let params = [];
|
|
18
|
+
if (m === 'connect') {
|
|
19
|
+
const cfg = {
|
|
20
|
+
product: session.project.product_tag,
|
|
21
|
+
env: session.project.env_slug,
|
|
22
|
+
database: body.database ?? body.tag,
|
|
23
|
+
...body,
|
|
24
|
+
};
|
|
25
|
+
params = [cfg];
|
|
26
|
+
}
|
|
27
|
+
else if (m === 'list') {
|
|
28
|
+
params = [session.project.product_tag];
|
|
29
|
+
}
|
|
30
|
+
else if (Object.keys(body).length > 0) {
|
|
31
|
+
params = [
|
|
32
|
+
{
|
|
33
|
+
product: session.project.product_tag,
|
|
34
|
+
env: session.project.env_slug,
|
|
35
|
+
...body,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
const result = await proxy.execute(m, params, dbContext);
|
|
40
|
+
if (m === 'connect' && params[0] && typeof params[0] === 'object') {
|
|
41
|
+
const cfg = params[0];
|
|
42
|
+
setDatabaseContext(session.project.workspace_id, session.project.product_tag, session.project.env_slug, {
|
|
43
|
+
database: String(cfg.database),
|
|
44
|
+
env: String(cfg.env),
|
|
45
|
+
product: String(cfg.product),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
printJson(result, Boolean(opts.json));
|
|
49
|
+
}
|
|
50
|
+
export function runDbContext(json) {
|
|
51
|
+
const session = requireSession();
|
|
52
|
+
const ctx = getDbContext(session);
|
|
53
|
+
const runtime = loadRuntimeContext(session.project.workspace_id, session.project.product_tag);
|
|
54
|
+
printJson({ database_context: runtime?.database_context ?? ctx }, json);
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function runGeneratePayload(opts: {
|
|
2
|
+
family: string;
|
|
3
|
+
method: string;
|
|
4
|
+
file?: string;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
export declare function runGenerateSnippet(opts: {
|
|
8
|
+
family: string;
|
|
9
|
+
method: string;
|
|
10
|
+
language: 'typescript' | 'python' | 'go' | 'dotnet';
|
|
11
|
+
file?: string;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
out?: string;
|
|
14
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { requireSession } from '../lib/proxy/context.js';
|
|
3
|
+
import { generateExecutablePayload } from '../lib/integrations.js';
|
|
4
|
+
import { buildSnippet, ensureSupportedSnippetOperation, } from '../lib/snippets.js';
|
|
5
|
+
import { printJson } from '../lib/output.js';
|
|
6
|
+
export async function runGeneratePayload(opts) {
|
|
7
|
+
const session = requireSession();
|
|
8
|
+
let targets;
|
|
9
|
+
if (opts.file) {
|
|
10
|
+
targets = JSON.parse(fs.readFileSync(opts.file, 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
const result = await generateExecutablePayload(session.apiUrl, session.credentials, {
|
|
13
|
+
workspace_id: session.project.workspace_id,
|
|
14
|
+
user_id: session.credentials.user_id,
|
|
15
|
+
public_key: session.credentials.public_key,
|
|
16
|
+
product_tag: session.project.product_tag,
|
|
17
|
+
env_slug: session.project.env_slug,
|
|
18
|
+
operation_family: opts.family,
|
|
19
|
+
method: opts.method,
|
|
20
|
+
targets,
|
|
21
|
+
include_session: true,
|
|
22
|
+
include_cache: true,
|
|
23
|
+
schema_mode: 'best_effort',
|
|
24
|
+
});
|
|
25
|
+
printJson(result, Boolean(opts.json));
|
|
26
|
+
}
|
|
27
|
+
export async function runGenerateSnippet(opts) {
|
|
28
|
+
ensureSupportedSnippetOperation(opts.family, opts.method);
|
|
29
|
+
const session = requireSession();
|
|
30
|
+
let targets;
|
|
31
|
+
if (opts.file) {
|
|
32
|
+
targets = JSON.parse(fs.readFileSync(opts.file, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
const generated = await generateExecutablePayload(session.apiUrl, session.credentials, {
|
|
35
|
+
workspace_id: session.project.workspace_id,
|
|
36
|
+
user_id: session.credentials.user_id,
|
|
37
|
+
public_key: session.credentials.public_key,
|
|
38
|
+
product_tag: session.project.product_tag,
|
|
39
|
+
env_slug: session.project.env_slug,
|
|
40
|
+
operation_family: opts.family,
|
|
41
|
+
method: opts.method,
|
|
42
|
+
targets,
|
|
43
|
+
});
|
|
44
|
+
const payload = generated.payload ?? {};
|
|
45
|
+
const snippet = buildSnippet(opts.language, payload, opts.family, opts.method);
|
|
46
|
+
const output = { payload: generated, snippet };
|
|
47
|
+
if (opts.out) {
|
|
48
|
+
fs.writeFileSync(opts.out, snippet);
|
|
49
|
+
console.log(`Wrote snippet to ${opts.out}`);
|
|
50
|
+
}
|
|
51
|
+
printJson(output, Boolean(opts.json));
|
|
52
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readJsonBody } from '../lib/read-body.js';
|
|
2
|
+
import { setGraphContext } from '../lib/context-store.js';
|
|
3
|
+
import { GRAPH_PROXY_METHODS } from '../lib/db-methods.js';
|
|
4
|
+
import { getGraphProxy, requireSession } from '../lib/proxy/context.js';
|
|
5
|
+
import { printJson } from '../lib/output.js';
|
|
6
|
+
export async function runGraph(method, opts) {
|
|
7
|
+
const session = requireSession();
|
|
8
|
+
const proxy = getGraphProxy(session);
|
|
9
|
+
const body = readJsonBody(opts.file);
|
|
10
|
+
const m = method.includes('.') ? method : method.toLowerCase();
|
|
11
|
+
if (!GRAPH_PROXY_METHODS.includes(m) && !m.includes('.')) {
|
|
12
|
+
console.warn(`Method "${m}" not in known list; sending anyway. Known: ${GRAPH_PROXY_METHODS.join(', ')}`);
|
|
13
|
+
}
|
|
14
|
+
const params = [];
|
|
15
|
+
if (m === 'connect') {
|
|
16
|
+
const cfg = {
|
|
17
|
+
product: session.project.product_tag,
|
|
18
|
+
env: session.project.env_slug,
|
|
19
|
+
graph: body.graph ?? body.tag,
|
|
20
|
+
...body,
|
|
21
|
+
};
|
|
22
|
+
params.push(cfg);
|
|
23
|
+
}
|
|
24
|
+
else if (Object.keys(body).length > 0) {
|
|
25
|
+
params.push({
|
|
26
|
+
product: session.project.product_tag,
|
|
27
|
+
env: session.project.env_slug,
|
|
28
|
+
...body,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else if (m === 'fetchAll' || m === 'list') {
|
|
32
|
+
params.push(session.project.product_tag);
|
|
33
|
+
}
|
|
34
|
+
const result = await proxy.execute(m === 'list' ? 'fetchAll' : m, params);
|
|
35
|
+
if (m === 'connect' && params[0] && typeof params[0] === 'object') {
|
|
36
|
+
const cfg = params[0];
|
|
37
|
+
setGraphContext(session.project.workspace_id, session.project.product_tag, session.project.env_slug, {
|
|
38
|
+
graph: String(cfg.graph),
|
|
39
|
+
env: String(cfg.env),
|
|
40
|
+
product: String(cfg.product),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
printJson(result, Boolean(opts.json));
|
|
44
|
+
}
|