@ductape/cli 0.2.22 → 0.3.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/CHANGELOG.md +7 -0
- package/README.md +7 -0
- package/dist/commands/generate.js +1 -1
- package/dist/commands/migrate-codebase.d.ts +176 -0
- package/dist/commands/migrate-codebase.js +753 -0
- package/dist/commands/migration-artifact.d.ts +32 -0
- package/dist/commands/migration-artifact.js +48 -0
- package/dist/commands/migration-assurance.d.ts +10 -0
- package/dist/commands/migration-assurance.js +120 -0
- package/dist/commands/migration-database.d.ts +21 -0
- package/dist/commands/migration-database.js +162 -0
- package/dist/commands/migration-e2e.d.ts +68 -0
- package/dist/commands/migration-e2e.js +189 -0
- package/dist/commands/migration-environments.d.ts +20 -0
- package/dist/commands/migration-environments.js +58 -0
- package/dist/commands/migration-generated.d.ts +68 -0
- package/dist/commands/migration-generated.js +217 -0
- package/dist/commands/migration-portfolio.d.ts +88 -0
- package/dist/commands/migration-portfolio.js +223 -0
- package/dist/commands/migration-products.d.ts +23 -0
- package/dist/commands/migration-products.js +88 -0
- package/dist/commands/migration-review.d.ts +248 -0
- package/dist/commands/migration-review.js +624 -0
- package/dist/commands/migration-secrets.d.ts +24 -0
- package/dist/commands/migration-secrets.js +93 -0
- package/dist/commands/migration-slice.d.ts +132 -0
- package/dist/commands/migration-slice.js +266 -0
- package/dist/commands/migration-verification.d.ts +12 -0
- package/dist/commands/migration-verification.js +194 -0
- package/dist/commands/products.d.ts +1 -0
- package/dist/commands/products.js +33 -1
- package/dist/commands/secrets.d.ts +8 -0
- package/dist/commands/secrets.js +44 -0
- package/dist/index.js +354 -2
- package/dist/lib/integrations.js +6 -1
- package/dist/lib/migration-artifact.d.ts +13 -0
- package/dist/lib/migration-artifact.js +103 -0
- package/package.json +8 -1
- package/API_PARITY.md +0 -58
- package/DB_MIGRATE_PLAN.md +0 -223
- package/ductape.example.ts +0 -18
- package/src/commands/apply.ts +0 -153
- package/src/commands/apps-import.ts +0 -43
- package/src/commands/apps.ts +0 -92
- package/src/commands/cloud.ts +0 -68
- package/src/commands/completion.ts +0 -119
- package/src/commands/db-migrate.ts +0 -209
- package/src/commands/db-schema.ts +0 -429
- package/src/commands/db.ts +0 -63
- package/src/commands/generate.ts +0 -74
- package/src/commands/graph.ts +0 -51
- package/src/commands/init.ts +0 -39
- package/src/commands/install.ts +0 -76
- package/src/commands/link.ts +0 -84
- package/src/commands/login.ts +0 -145
- package/src/commands/logout.ts +0 -7
- package/src/commands/platform.ts +0 -123
- package/src/commands/products.ts +0 -228
- package/src/commands/profiles.ts +0 -23
- package/src/commands/resources.ts +0 -213
- package/src/commands/secrets.ts +0 -46
- package/src/commands/unlink.ts +0 -15
- package/src/commands/whoami.ts +0 -27
- package/src/commands/workspaces.ts +0 -98
- package/src/index.ts +0 -523
- package/src/lib/api-client.ts +0 -88
- package/src/lib/app-import.ts +0 -82
- package/src/lib/apply-loaders.ts +0 -30
- package/src/lib/config.ts +0 -230
- package/src/lib/context-store.ts +0 -77
- package/src/lib/db-methods.ts +0 -56
- package/src/lib/db-types.ts +0 -104
- package/src/lib/encryption.ts +0 -12
- package/src/lib/forms/enums.ts +0 -52
- package/src/lib/forms/index.ts +0 -8
- package/src/lib/forms/prompt.ts +0 -209
- package/src/lib/forms/schemas.ts +0 -288
- package/src/lib/forms/types.ts +0 -27
- package/src/lib/http.ts +0 -76
- package/src/lib/integrations.ts +0 -105
- package/src/lib/interactive-opts.ts +0 -13
- package/src/lib/migration-files.ts +0 -58
- package/src/lib/output.ts +0 -39
- package/src/lib/platform-api.ts +0 -209
- package/src/lib/proxy/context.ts +0 -94
- package/src/lib/proxy/db-proxy.ts +0 -40
- package/src/lib/proxy/graph-proxy.ts +0 -64
- package/src/lib/proxy/sdk-proxy.ts +0 -58
- package/src/lib/read-body.ts +0 -57
- package/src/lib/resources.ts +0 -160
- package/src/lib/schema-loader.ts +0 -152
- package/src/lib/snippets.ts +0 -165
- package/src/lib/templates.ts +0 -157
- package/src/lib/workspace-context.ts +0 -43
- package/src/lib/workspaces.ts +0 -283
- package/test/apply-loaders.test.ts +0 -40
- package/test/encryption.test.ts +0 -20
- package/test/forms.test.ts +0 -25
- package/test/http.test.ts +0 -15
- package/test/platform-api.test.ts +0 -42
- package/test/resources.test.ts +0 -23
- package/test/workspace-resolve.test.ts +0 -29
- package/tsconfig.json +0 -15
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getActiveProfileName,
|
|
3
|
-
getActiveWorkspaceId,
|
|
4
|
-
getApiUrl,
|
|
5
|
-
loadCredentials,
|
|
6
|
-
type Credentials,
|
|
7
|
-
} from './config.js';
|
|
8
|
-
import { requireCredentials } from './proxy/context.js';
|
|
9
|
-
|
|
10
|
-
export interface WorkspaceContext {
|
|
11
|
-
credentials: Credentials;
|
|
12
|
-
workspaceId: string;
|
|
13
|
-
apiUrl: string;
|
|
14
|
-
profile: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function requireWorkspaceContext(opts?: { profile?: string }): WorkspaceContext {
|
|
18
|
-
const credentials = requireCredentials();
|
|
19
|
-
const workspaceId = getActiveWorkspaceId();
|
|
20
|
-
if (!workspaceId) {
|
|
21
|
-
throw new Error('No active workspace. Run: ductape login or ductape workspaces use');
|
|
22
|
-
}
|
|
23
|
-
const profile = opts?.profile ?? getActiveProfileName(null);
|
|
24
|
-
return {
|
|
25
|
-
credentials,
|
|
26
|
-
workspaceId,
|
|
27
|
-
apiUrl: getApiUrl(profile),
|
|
28
|
-
profile,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Query params required by integrations/apps REST endpoints (matches Workbench). */
|
|
33
|
-
export function workspaceAuthQuery(
|
|
34
|
-
ctx: WorkspaceContext,
|
|
35
|
-
extra?: Record<string, string>,
|
|
36
|
-
): Record<string, string> {
|
|
37
|
-
return {
|
|
38
|
-
user_id: ctx.credentials.user_id,
|
|
39
|
-
public_key: ctx.credentials.public_key,
|
|
40
|
-
workspace_id: ctx.workspaceId,
|
|
41
|
-
...extra,
|
|
42
|
-
};
|
|
43
|
-
}
|
package/src/lib/workspaces.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import select from '@inquirer/select';
|
|
2
|
-
import { ApiClient } from './api-client.js';
|
|
3
|
-
import {
|
|
4
|
-
findProjectConfig,
|
|
5
|
-
getApiUrl,
|
|
6
|
-
loadCredentials,
|
|
7
|
-
loadGlobalConfig,
|
|
8
|
-
saveCredentials,
|
|
9
|
-
saveGlobalConfig,
|
|
10
|
-
saveProjectConfig,
|
|
11
|
-
type Credentials,
|
|
12
|
-
} from './config.js';
|
|
13
|
-
import { success } from './output.js';
|
|
14
|
-
|
|
15
|
-
/** Workspace entry from login or workspaces API (field names vary). */
|
|
16
|
-
export interface WorkspaceSummary {
|
|
17
|
-
workspace_id?: string;
|
|
18
|
-
workspace_name?: string;
|
|
19
|
-
_id?: string;
|
|
20
|
-
name?: string;
|
|
21
|
-
tag?: string;
|
|
22
|
-
default?: boolean;
|
|
23
|
-
accepted?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function workspaceId(w: WorkspaceSummary): string {
|
|
27
|
-
const id = w.workspace_id ?? w._id;
|
|
28
|
-
if (!id) throw new Error('Workspace entry missing id');
|
|
29
|
-
return id;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function workspaceLabel(w: WorkspaceSummary): string {
|
|
33
|
-
return w.workspace_name ?? w.name ?? w.tag ?? workspaceId(w);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Human-readable workspace tag for .ductape/config.json (slug of name, or explicit tag). */
|
|
37
|
-
export function workspaceTag(w: WorkspaceSummary): string {
|
|
38
|
-
if (w.tag?.trim()) return w.tag.trim();
|
|
39
|
-
return slugifyTag(workspaceLabel(w));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function slugifyTag(value: string): string {
|
|
43
|
-
return value
|
|
44
|
-
.trim()
|
|
45
|
-
.toLowerCase()
|
|
46
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
47
|
-
.replace(/^-+|-+$/g, '');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function normalizeWorkspaces(raw: unknown[]): WorkspaceSummary[] {
|
|
51
|
-
return raw
|
|
52
|
-
.filter((w): w is Record<string, unknown> => Boolean(w) && typeof w === 'object')
|
|
53
|
-
.map((w) => ({
|
|
54
|
-
workspace_id: String(w.workspace_id ?? w._id ?? ''),
|
|
55
|
-
workspace_name: (w.workspace_name ?? w.name ?? w.tag) as string | undefined,
|
|
56
|
-
_id: w._id as string | undefined,
|
|
57
|
-
name: w.name as string | undefined,
|
|
58
|
-
tag: w.tag as string | undefined,
|
|
59
|
-
default: Boolean(w.default),
|
|
60
|
-
accepted: w.accepted as boolean | undefined,
|
|
61
|
-
}))
|
|
62
|
-
.filter((w) => w.workspace_id.length > 0);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function getActiveWorkspace(): { id: string; name?: string; tag?: string } | null {
|
|
66
|
-
const global = loadGlobalConfig();
|
|
67
|
-
if (global.active_workspace_id) {
|
|
68
|
-
return {
|
|
69
|
-
id: global.active_workspace_id,
|
|
70
|
-
name: global.active_workspace_name,
|
|
71
|
-
tag: global.active_workspace_tag,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function setActiveWorkspace(id: string, name?: string, tag?: string): void {
|
|
78
|
-
const global = loadGlobalConfig();
|
|
79
|
-
global.active_workspace_id = id;
|
|
80
|
-
global.active_workspace_name = name;
|
|
81
|
-
if (tag) global.active_workspace_tag = tag;
|
|
82
|
-
saveGlobalConfig(global);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function clearActiveWorkspace(): void {
|
|
86
|
-
const global = loadGlobalConfig();
|
|
87
|
-
delete global.active_workspace_id;
|
|
88
|
-
delete global.active_workspace_name;
|
|
89
|
-
delete global.active_workspace_tag;
|
|
90
|
-
saveGlobalConfig(global);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function fetchWorkspaces(profile?: string): Promise<WorkspaceSummary[]> {
|
|
94
|
-
const creds = loadCredentials();
|
|
95
|
-
if (!creds) throw new Error('Not logged in. Run: ductape login');
|
|
96
|
-
|
|
97
|
-
const cached = normalizeWorkspaces(creds.workspaces ?? []);
|
|
98
|
-
const api = ApiClient.forProfile(profile, creds);
|
|
99
|
-
const apiUrl = getApiUrl(profile);
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
const res = await fetch(
|
|
103
|
-
`${apiUrl}/workspaces/v1/fetch/${creds.user_id}?user_id=${encodeURIComponent(creds.user_id)}&public_key=${encodeURIComponent(creds.public_key)}`,
|
|
104
|
-
{ headers: { Authorization: `Bearer ${creds.auth_token}` } },
|
|
105
|
-
);
|
|
106
|
-
const body = await res.json();
|
|
107
|
-
if (!res.ok) throw new Error(body.message ?? `Failed to list workspaces (${res.status})`);
|
|
108
|
-
const data = body.data ?? body;
|
|
109
|
-
const list = Array.isArray(data) ? data : (data?.result ?? data?.workspaces ?? []);
|
|
110
|
-
const normalized = normalizeWorkspaces(list as unknown[]);
|
|
111
|
-
if (normalized.length > 0) {
|
|
112
|
-
saveCredentials({ ...creds, workspaces: normalized as Credentials['workspaces'] });
|
|
113
|
-
return normalized;
|
|
114
|
-
}
|
|
115
|
-
} catch {
|
|
116
|
-
// fall through to cache / api client
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const list = await api.get<WorkspaceSummary[]>(
|
|
121
|
-
`/workspaces/v1/fetch/${creds.user_id}?user_id=${encodeURIComponent(creds.user_id)}&public_key=${encodeURIComponent(creds.public_key)}`,
|
|
122
|
-
);
|
|
123
|
-
const normalized = normalizeWorkspaces(list as unknown[]);
|
|
124
|
-
if (normalized.length > 0) {
|
|
125
|
-
saveCredentials({ ...creds, workspaces: normalized as Credentials['workspaces'] });
|
|
126
|
-
return normalized;
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
// ignore
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return cached;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function findWorkspace(
|
|
136
|
-
workspaces: WorkspaceSummary[],
|
|
137
|
-
selector: string,
|
|
138
|
-
): WorkspaceSummary | undefined {
|
|
139
|
-
const trimmed = selector.trim();
|
|
140
|
-
const idx = Number(trimmed);
|
|
141
|
-
if (!Number.isNaN(idx) && workspaces[idx]) return workspaces[idx];
|
|
142
|
-
|
|
143
|
-
return workspaces.find(
|
|
144
|
-
(w) =>
|
|
145
|
-
workspaceId(w) === trimmed ||
|
|
146
|
-
workspaceLabel(w).toLowerCase() === trimmed.toLowerCase() ||
|
|
147
|
-
w.tag?.toLowerCase() === trimmed.toLowerCase(),
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function printWorkspaceTable(
|
|
152
|
-
workspaces: WorkspaceSummary[],
|
|
153
|
-
activeId?: string | null,
|
|
154
|
-
): void {
|
|
155
|
-
if (workspaces.length === 0) {
|
|
156
|
-
console.log('No workspaces found for this account.');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
workspaces.forEach((w, i) => {
|
|
160
|
-
const id = workspaceId(w);
|
|
161
|
-
const label = workspaceLabel(w);
|
|
162
|
-
const markers: string[] = [];
|
|
163
|
-
if (activeId && id === activeId) markers.push('active');
|
|
164
|
-
if (w.default) markers.push('default');
|
|
165
|
-
const suffix = markers.length ? ` (${markers.join(', ')})` : '';
|
|
166
|
-
console.log(` [${i}] ${label}${suffix}`);
|
|
167
|
-
console.log(` ${id}`);
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export async function promptWorkspaceSelection(
|
|
172
|
-
workspaces: WorkspaceSummary[],
|
|
173
|
-
opts?: { message?: string },
|
|
174
|
-
): Promise<WorkspaceSummary> {
|
|
175
|
-
if (workspaces.length === 0) {
|
|
176
|
-
throw new Error('No workspaces available. Create one in the Workbench first.');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (workspaces.length === 1) {
|
|
180
|
-
const only = workspaces[0];
|
|
181
|
-
setActiveWorkspace(workspaceId(only), workspaceLabel(only), workspaceTag(only));
|
|
182
|
-
return only;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const active = getActiveWorkspace();
|
|
186
|
-
const activeId = active?.id ?? null;
|
|
187
|
-
const defaultWs =
|
|
188
|
-
workspaces.find((w) => active && workspaceId(w) === active.id) ??
|
|
189
|
-
workspaces.find((w) => w.default) ??
|
|
190
|
-
workspaces[0];
|
|
191
|
-
const defaultIdx = workspaces.indexOf(defaultWs);
|
|
192
|
-
|
|
193
|
-
// Non-interactive (CI, pipes): keep default without prompting.
|
|
194
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
195
|
-
return defaultWs;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const choiceIndex = await select<number>({
|
|
199
|
-
message: opts?.message ?? 'Select a workspace',
|
|
200
|
-
pageSize: Math.min(12, workspaces.length),
|
|
201
|
-
default: defaultIdx >= 0 ? defaultIdx : 0,
|
|
202
|
-
choices: workspaces.map((w, i) => {
|
|
203
|
-
const id = workspaceId(w);
|
|
204
|
-
const label = workspaceLabel(w);
|
|
205
|
-
const markers: string[] = [];
|
|
206
|
-
if (activeId && id === activeId) markers.push('active');
|
|
207
|
-
if (w.default) markers.push('default');
|
|
208
|
-
const suffix = markers.length ? ` · ${markers.join(', ')}` : '';
|
|
209
|
-
return {
|
|
210
|
-
name: label + suffix,
|
|
211
|
-
value: i,
|
|
212
|
-
description: id,
|
|
213
|
-
};
|
|
214
|
-
}),
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
return workspaces[choiceIndex];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export async function applyActiveWorkspace(
|
|
221
|
-
workspace: WorkspaceSummary,
|
|
222
|
-
opts?: { syncProject?: boolean; dir?: string },
|
|
223
|
-
): Promise<void> {
|
|
224
|
-
const id = workspaceId(workspace);
|
|
225
|
-
const name = workspaceLabel(workspace);
|
|
226
|
-
const tag = workspaceTag(workspace);
|
|
227
|
-
setActiveWorkspace(id, name, tag);
|
|
228
|
-
|
|
229
|
-
if (opts?.syncProject !== false) {
|
|
230
|
-
const found = findProjectConfig(opts?.dir);
|
|
231
|
-
if (found) {
|
|
232
|
-
saveProjectConfig(found.dir, {
|
|
233
|
-
...found.config,
|
|
234
|
-
workspace_tag: tag,
|
|
235
|
-
workspace_id: id,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export async function selectAndApplyWorkspace(opts?: {
|
|
242
|
-
profile?: string;
|
|
243
|
-
selector?: string;
|
|
244
|
-
skipPrompt?: boolean;
|
|
245
|
-
syncProject?: boolean;
|
|
246
|
-
dir?: string;
|
|
247
|
-
}): Promise<WorkspaceSummary> {
|
|
248
|
-
const workspaces = await fetchWorkspaces(opts?.profile);
|
|
249
|
-
|
|
250
|
-
let chosen: WorkspaceSummary;
|
|
251
|
-
if (opts?.selector) {
|
|
252
|
-
const match = findWorkspace(workspaces, opts.selector);
|
|
253
|
-
if (!match) throw new Error(`Workspace not found: ${opts.selector}`);
|
|
254
|
-
chosen = match;
|
|
255
|
-
} else if (opts?.skipPrompt) {
|
|
256
|
-
const active = getActiveWorkspace();
|
|
257
|
-
const match = active ? findWorkspace(workspaces, active.id) : undefined;
|
|
258
|
-
chosen = match ?? workspaces.find((w) => w.default) ?? workspaces[0];
|
|
259
|
-
if (!chosen) throw new Error('No workspace to select');
|
|
260
|
-
} else {
|
|
261
|
-
chosen = await promptWorkspaceSelection(workspaces);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
await applyActiveWorkspace(chosen, { syncProject: opts?.syncProject, dir: opts?.dir });
|
|
265
|
-
return chosen;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export async function finalizeLoginWorkspace(opts?: {
|
|
269
|
-
profile?: string;
|
|
270
|
-
workspace?: string;
|
|
271
|
-
skipPrompt?: boolean;
|
|
272
|
-
}): Promise<void> {
|
|
273
|
-
const chosen = await selectAndApplyWorkspace({
|
|
274
|
-
profile: opts?.profile,
|
|
275
|
-
selector: opts?.workspace,
|
|
276
|
-
skipPrompt: opts?.skipPrompt,
|
|
277
|
-
syncProject: true,
|
|
278
|
-
});
|
|
279
|
-
const workspaces = await fetchWorkspaces(opts?.profile);
|
|
280
|
-
const hint =
|
|
281
|
-
workspaces.length > 1 ? '\nSwitch anytime: ductape workspaces use' : '';
|
|
282
|
-
success(`Active workspace: ${workspaceLabel(chosen)} (${workspaceId(chosen)})${hint}`);
|
|
283
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import test from 'node:test';
|
|
6
|
-
import { loadNotifications } from '../src/lib/apply-loaders.js';
|
|
7
|
-
|
|
8
|
-
test('notifications declaration must be a top-level array', () => {
|
|
9
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ductape-notifications-'));
|
|
10
|
-
const configDir = path.join(dir, 'ductape');
|
|
11
|
-
fs.mkdirSync(configDir);
|
|
12
|
-
fs.writeFileSync(
|
|
13
|
-
path.join(configDir, 'notifications.json'),
|
|
14
|
-
JSON.stringify({ notifications: [], messages: [] }),
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
assert.throws(
|
|
18
|
-
() => loadNotifications(dir),
|
|
19
|
-
/notifications\.json must be a JSON array/,
|
|
20
|
-
);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('notifications declaration accepts nested messages', () => {
|
|
24
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ductape-notifications-'));
|
|
25
|
-
const configDir = path.join(dir, 'ductape');
|
|
26
|
-
fs.mkdirSync(configDir);
|
|
27
|
-
fs.writeFileSync(
|
|
28
|
-
path.join(configDir, 'notifications.json'),
|
|
29
|
-
JSON.stringify([
|
|
30
|
-
{
|
|
31
|
-
tag: 'game-alerts-critical',
|
|
32
|
-
messages: [{ tag: 'game-alerts-critical:match-launch', name: 'Match launch' }],
|
|
33
|
-
},
|
|
34
|
-
]),
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const result = loadNotifications(dir);
|
|
38
|
-
assert.equal(result?.length, 1);
|
|
39
|
-
assert.equal(result?.[0].tag, 'game-alerts-critical');
|
|
40
|
-
});
|
package/test/encryption.test.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import CryptoJS from 'crypto-js';
|
|
4
|
-
import { encryptProxyPayload } from '../src/lib/encryption.js';
|
|
5
|
-
|
|
6
|
-
describe('encryptProxyPayload', () => {
|
|
7
|
-
it('produces iv:ciphertext format compatible with backend', () => {
|
|
8
|
-
const publicKey = 'test-public-key-12345';
|
|
9
|
-
const data = { module: 'storage', method: 'list', params: ['my-product'], user_id: 'u1' };
|
|
10
|
-
const encrypted = encryptProxyPayload(data, publicKey);
|
|
11
|
-
assert.match(encrypted, /^[0-9a-f]+:/i);
|
|
12
|
-
|
|
13
|
-
const [ivHex, cipherText] = encrypted.split(':');
|
|
14
|
-
const iv = CryptoJS.enc.Hex.parse(ivHex);
|
|
15
|
-
const hashedKey = CryptoJS.SHA256(publicKey);
|
|
16
|
-
const decrypted = CryptoJS.AES.decrypt(cipherText, hashedKey, { iv });
|
|
17
|
-
const json = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8));
|
|
18
|
-
assert.deepEqual(json, data);
|
|
19
|
-
});
|
|
20
|
-
});
|
package/test/forms.test.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { getFormSchema, schemaKeyForEntity, schemaKeyForResource } from '../src/lib/forms/schemas.js';
|
|
4
|
-
|
|
5
|
-
describe('forms schemas', () => {
|
|
6
|
-
it('resolves product and component keys', () => {
|
|
7
|
-
assert.equal(schemaKeyForEntity('products', 'create'), 'products:create');
|
|
8
|
-
assert.equal(schemaKeyForResource('storage', 'create'), 'storage:create');
|
|
9
|
-
assert.equal(schemaKeyForResource('databases', 'connect'), 'databases:connect');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('storage create includes enum type in envs', () => {
|
|
13
|
-
const schema = getFormSchema('storage:create');
|
|
14
|
-
assert.ok(schema);
|
|
15
|
-
const envs = schema!.fields.find((f) => f.kind === 'envs');
|
|
16
|
-
assert.equal(envs?.envsVariant, 'storage');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('databases create has database type enum', () => {
|
|
20
|
-
const schema = getFormSchema('databases:create');
|
|
21
|
-
const typeField = schema!.fields.find((f) => f.key === 'type');
|
|
22
|
-
assert.equal(typeField?.kind, 'enum');
|
|
23
|
-
assert.ok(typeField?.choices?.includes('postgresql'));
|
|
24
|
-
});
|
|
25
|
-
});
|
package/test/http.test.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { parseJsonResponse } from '../src/lib/http.js';
|
|
4
|
-
|
|
5
|
-
test('surfaces an HTML gateway timeout as a structured proxy failure', async () => {
|
|
6
|
-
const response = new Response('<html><h1>504 Gateway Time-out</h1></html>', {
|
|
7
|
-
status: 504,
|
|
8
|
-
headers: { 'content-type': 'text/html' },
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
await assert.rejects(
|
|
12
|
-
() => parseJsonResponse(response, 'https://api.ductape.app/proxy/v1/sdk-proxy/execute'),
|
|
13
|
-
/Ductape proxy gateway timeout \(HTTP 504\).*did not return a verifiable result/,
|
|
14
|
-
);
|
|
15
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
// Test unwrap helpers indirectly via exported patterns — use small inline copies for unit scope
|
|
7
|
-
function unwrapList<T>(result: unknown): T[] {
|
|
8
|
-
if (Array.isArray(result)) return result;
|
|
9
|
-
if (result && typeof result === 'object' && Array.isArray((result as { data?: T[] }).data)) {
|
|
10
|
-
return (result as { data: T[] }).data;
|
|
11
|
-
}
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('platform-api helpers', () => {
|
|
16
|
-
it('unwraps array responses', () => {
|
|
17
|
-
assert.deepEqual(unwrapList([{ tag: 'a' }]), [{ tag: 'a' }]);
|
|
18
|
-
assert.deepEqual(unwrapList({ data: [{ tag: 'b' }] }), [{ tag: 'b' }]);
|
|
19
|
-
assert.deepEqual(unwrapList({}), []);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('routes product fetch-by-tag through the authenticated CLI SDK proxy', () => {
|
|
23
|
-
const source = readFileSync(
|
|
24
|
-
fileURLToPath(new URL('../src/lib/platform-api.ts', import.meta.url)),
|
|
25
|
-
'utf8',
|
|
26
|
-
);
|
|
27
|
-
assert.match(source, /proxy\(ctx\)\.execute<unknown>\('product', 'fetch', \[opts\.tag\]\)/);
|
|
28
|
-
assert.doesNotMatch(
|
|
29
|
-
source,
|
|
30
|
-
/getPath<unknown>\(`\/integrations\/v1\/fetch\/tag`/,
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('exposes notification message administration', () => {
|
|
35
|
-
const source = readFileSync(
|
|
36
|
-
fileURLToPath(new URL('../src/commands/resources.ts', import.meta.url)),
|
|
37
|
-
'utf8',
|
|
38
|
-
);
|
|
39
|
-
assert.match(source, /runNotificationMessageCrud/);
|
|
40
|
-
assert.match(source, /`messages\.\$\{crud === 'get' \? 'fetch' : crud\}`/);
|
|
41
|
-
});
|
|
42
|
-
});
|
package/test/resources.test.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { buildCrudParams, proxyMethodForVerb, resolveResourceType } from '../src/lib/resources.js';
|
|
4
|
-
|
|
5
|
-
describe('resources', () => {
|
|
6
|
-
it('maps get to fetch', () => {
|
|
7
|
-
assert.equal(proxyMethodForVerb('get'), 'fetch');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('builds storage list params', () => {
|
|
11
|
-
const params = buildCrudParams('storage', 'list', 'my-product', {});
|
|
12
|
-
assert.deepEqual(params, ['my-product']);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('builds vector fetch params', () => {
|
|
16
|
-
const params = buildCrudParams('vector', 'fetch', 'my-product', { tag: 'v1' });
|
|
17
|
-
assert.deepEqual(params, [{ product: 'my-product', tag: 'v1' }]);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('resolves aliases', () => {
|
|
21
|
-
assert.equal(resolveResourceType('message-brokers'), 'messageBrokers');
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { resolveWorkspaceId, type ProjectConfig } from '../src/lib/config.js';
|
|
4
|
-
import { slugifyTag, workspaceTag } from '../src/lib/workspaces.js';
|
|
5
|
-
|
|
6
|
-
describe('workspace resolve', () => {
|
|
7
|
-
it('resolves workspace_tag from cached credentials', () => {
|
|
8
|
-
const cfg: ProjectConfig = {
|
|
9
|
-
workspace_tag: 'my-team',
|
|
10
|
-
product_tag: 'api',
|
|
11
|
-
env_slug: 'dev',
|
|
12
|
-
};
|
|
13
|
-
const id = resolveWorkspaceId(cfg, {
|
|
14
|
-
user_id: 'u1',
|
|
15
|
-
email: 'a@b.c',
|
|
16
|
-
auth_token: 't',
|
|
17
|
-
public_key: 'pk',
|
|
18
|
-
workspaces: [
|
|
19
|
-
{ workspace_id: 'ws-abc', workspace_name: 'My Team', tag: 'my-team' },
|
|
20
|
-
],
|
|
21
|
-
});
|
|
22
|
-
assert.equal(id, 'ws-abc');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('slugifies workspace tag from name', () => {
|
|
26
|
-
assert.equal(workspaceTag({ workspace_id: '1', workspace_name: 'My Team' }), 'my-team');
|
|
27
|
-
assert.equal(slugifyTag('Acme Corp'), 'acme-corp');
|
|
28
|
-
});
|
|
29
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"rootDir": "src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"declaration": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*"],
|
|
14
|
-
"exclude": ["node_modules"]
|
|
15
|
-
}
|