@ductape/cli 0.3.0 → 0.3.2
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/README.md +2 -0
- package/dist/commands/apps-curate-postman.d.ts +25 -0
- package/dist/commands/apps-curate-postman.js +328 -0
- package/dist/commands/db-migrate.js +47 -8
- package/dist/commands/marketplace.d.ts +8 -0
- package/dist/commands/marketplace.js +30 -0
- package/dist/commands/migrate-codebase.d.ts +21 -0
- package/dist/commands/migrate-codebase.js +60 -0
- package/dist/commands/migration-artifact.d.ts +11 -15
- package/dist/commands/migration-artifact.js +80 -27
- package/dist/commands/migration-capabilities.d.ts +53 -0
- package/dist/commands/migration-capabilities.js +66 -0
- package/dist/commands/migration-database.d.ts +5 -0
- package/dist/commands/migration-database.js +2 -2
- package/dist/index.js +35 -2
- package/dist/lib/migration-artifact.d.ts +6 -0
- package/dist/lib/migration-artifact.js +13 -2
- package/dist/lib/platform-api.d.ts +8 -0
- package/dist/lib/platform-api.js +55 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,6 +77,8 @@ ductape cloud resources list|import|provision …
|
|
|
77
77
|
ductape db <verb> # db-proxy runtime
|
|
78
78
|
ductape graph <verb> # graph-proxy runtime
|
|
79
79
|
ductape secrets <verb>
|
|
80
|
+
ductape marketplace search payments --json
|
|
81
|
+
ductape marketplace get paystack --json
|
|
80
82
|
ductape generate payload|snippet …
|
|
81
83
|
ductape completion bash|zsh
|
|
82
84
|
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface PostmanCandidate {
|
|
2
|
+
uid: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
publisher: string;
|
|
6
|
+
publisherUrl?: string;
|
|
7
|
+
collectionUrl?: string;
|
|
8
|
+
logo?: string;
|
|
9
|
+
forkCount: number;
|
|
10
|
+
verified: boolean;
|
|
11
|
+
official: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseExplorePage(html: string): PostmanCandidate[];
|
|
14
|
+
export declare function reputation(candidate: PostmanCandidate): {
|
|
15
|
+
score: number;
|
|
16
|
+
reasons: string[];
|
|
17
|
+
};
|
|
18
|
+
export declare function runAppsCuratePostman(opts: {
|
|
19
|
+
profile?: string;
|
|
20
|
+
maxPages?: string;
|
|
21
|
+
minScore?: string;
|
|
22
|
+
state?: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import input from '@inquirer/input';
|
|
4
|
+
import select from '@inquirer/select';
|
|
5
|
+
import { importAppFromFile } from '../lib/app-import.js';
|
|
6
|
+
import { getApp, listApps, listMarketplaceCategories, updateApp, } from '../lib/platform-api.js';
|
|
7
|
+
import { success } from '../lib/output.js';
|
|
8
|
+
import { requireWorkspaceContext } from '../lib/workspace-context.js';
|
|
9
|
+
const EXPLORE_URL = 'https://www.postman.com/explore/collections';
|
|
10
|
+
const DEFAULT_STATE = '.ductape/postman-marketplace-curator.json';
|
|
11
|
+
function text(value) {
|
|
12
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
13
|
+
}
|
|
14
|
+
function number(value) {
|
|
15
|
+
return typeof value === 'number' ? value : Number(value) || 0;
|
|
16
|
+
}
|
|
17
|
+
function slugify(value) {
|
|
18
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
19
|
+
}
|
|
20
|
+
function walk(value, visit) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
value.forEach((item) => walk(item, visit));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!value || typeof value !== 'object')
|
|
26
|
+
return;
|
|
27
|
+
const object = value;
|
|
28
|
+
visit(object);
|
|
29
|
+
Object.values(object).forEach((item) => walk(item, visit));
|
|
30
|
+
}
|
|
31
|
+
function candidateFromObject(item) {
|
|
32
|
+
const kind = text(item.entityType || item.type || item.elementType).toLowerCase();
|
|
33
|
+
const uid = text(item.uid || item.collectionUid || item.collectionId || item.id);
|
|
34
|
+
const name = text(item.name || item.title);
|
|
35
|
+
const hasCollectionIdentity = Boolean(item.collectionUid || item.collectionId);
|
|
36
|
+
if (!uid || !name || (kind ? !kind.includes('collection') : !hasCollectionIdentity))
|
|
37
|
+
return null;
|
|
38
|
+
const publisherObject = (item.publisher || item.organization || item.team || item.owner || {});
|
|
39
|
+
const publisher = text(item.publisherName || item.teamName || publisherObject.name || publisherObject.username);
|
|
40
|
+
const collectionUrl = text(item.url || item.href || item.publicUrl);
|
|
41
|
+
return {
|
|
42
|
+
uid,
|
|
43
|
+
name,
|
|
44
|
+
description: text(item.description || item.summary),
|
|
45
|
+
publisher,
|
|
46
|
+
publisherUrl: text(publisherObject.url || publisherObject.profileUrl) || undefined,
|
|
47
|
+
collectionUrl: collectionUrl || undefined,
|
|
48
|
+
logo: text(item.logo || item.icon || publisherObject.logo) || undefined,
|
|
49
|
+
forkCount: number(item.forkCount || item.forks || item.fork_count),
|
|
50
|
+
verified: Boolean(item.verified || item.isVerified || publisherObject.verified),
|
|
51
|
+
official: Boolean(item.official || item.isOfficial || item.publisherType === 'official'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function parseExplorePage(html) {
|
|
55
|
+
const found = new Map();
|
|
56
|
+
const scriptPattern = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
|
57
|
+
for (const match of html.matchAll(scriptPattern)) {
|
|
58
|
+
const raw = match[1]?.trim();
|
|
59
|
+
if (!raw || (!raw.startsWith('{') && !raw.startsWith('[')))
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
walk(JSON.parse(raw), (item) => {
|
|
63
|
+
const candidate = candidateFromObject(item);
|
|
64
|
+
if (candidate)
|
|
65
|
+
found.set(candidate.uid, candidate);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Framework payloads that are not plain JSON are covered by the URL fallback below.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const linkPattern = /https?:\/\/www\.postman\.com\/([^/"'?#]+)\/([^/"'?#]+)\/(?:collection|documentation)\/([a-zA-Z0-9-]+)(?:\/([^"'?#<]+))?/g;
|
|
73
|
+
for (const match of html.matchAll(linkPattern)) {
|
|
74
|
+
const uid = match[3];
|
|
75
|
+
if (!uid || found.has(uid))
|
|
76
|
+
continue;
|
|
77
|
+
found.set(uid, {
|
|
78
|
+
uid,
|
|
79
|
+
name: (match[4] || uid).split('/')[0].replace(/-/g, ' '),
|
|
80
|
+
description: '',
|
|
81
|
+
publisher: match[1],
|
|
82
|
+
collectionUrl: match[0],
|
|
83
|
+
forkCount: 0,
|
|
84
|
+
verified: false,
|
|
85
|
+
official: false,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return [...found.values()];
|
|
89
|
+
}
|
|
90
|
+
export function reputation(candidate) {
|
|
91
|
+
let score = 0;
|
|
92
|
+
const reasons = [];
|
|
93
|
+
if (candidate.official) {
|
|
94
|
+
score += 5;
|
|
95
|
+
reasons.push('official publisher');
|
|
96
|
+
}
|
|
97
|
+
if (candidate.verified) {
|
|
98
|
+
score += 4;
|
|
99
|
+
reasons.push('verified publisher');
|
|
100
|
+
}
|
|
101
|
+
if (candidate.forkCount >= 10_000) {
|
|
102
|
+
score += 4;
|
|
103
|
+
reasons.push(`${candidate.forkCount.toLocaleString()} forks`);
|
|
104
|
+
}
|
|
105
|
+
else if (candidate.forkCount >= 1_000) {
|
|
106
|
+
score += 3;
|
|
107
|
+
reasons.push(`${candidate.forkCount.toLocaleString()} forks`);
|
|
108
|
+
}
|
|
109
|
+
else if (candidate.forkCount >= 100) {
|
|
110
|
+
score += 2;
|
|
111
|
+
reasons.push(`${candidate.forkCount.toLocaleString()} forks`);
|
|
112
|
+
}
|
|
113
|
+
const publisher = slugify(candidate.publisher);
|
|
114
|
+
const name = slugify(candidate.name);
|
|
115
|
+
if (publisher && name && (name.includes(publisher) || publisher.includes(name.split('-')[0]))) {
|
|
116
|
+
score += 2;
|
|
117
|
+
reasons.push('publisher/API identity match');
|
|
118
|
+
}
|
|
119
|
+
if (candidate.publisherUrl) {
|
|
120
|
+
score += 1;
|
|
121
|
+
reasons.push('publisher profile');
|
|
122
|
+
}
|
|
123
|
+
return { score, reasons };
|
|
124
|
+
}
|
|
125
|
+
async function fetchJson(url, apiKey) {
|
|
126
|
+
const response = await fetch(url, {
|
|
127
|
+
headers: {
|
|
128
|
+
Accept: 'application/json',
|
|
129
|
+
...(apiKey ? { 'X-Api-Key': apiKey } : {}),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok)
|
|
133
|
+
throw new Error(`${url} returned HTTP ${response.status}`);
|
|
134
|
+
return await response.json();
|
|
135
|
+
}
|
|
136
|
+
async function downloadCollection(candidate, apiKey) {
|
|
137
|
+
const urls = [
|
|
138
|
+
`https://api.postman.com/collections/${encodeURIComponent(candidate.uid)}`,
|
|
139
|
+
`https://api.getpostman.com/collections/${encodeURIComponent(candidate.uid)}`,
|
|
140
|
+
`https://www.postman.com/collections/${encodeURIComponent(candidate.uid)}`,
|
|
141
|
+
];
|
|
142
|
+
let lastError;
|
|
143
|
+
for (const url of urls) {
|
|
144
|
+
try {
|
|
145
|
+
const result = await fetchJson(url, apiKey);
|
|
146
|
+
const collection = result.collection;
|
|
147
|
+
if (collection && typeof collection === 'object')
|
|
148
|
+
return collection;
|
|
149
|
+
if (result.info && result.item)
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
lastError = error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`Unable to download ${candidate.uid}. Set POSTMAN_API_KEY to use the official Postman API. ${String(lastError)}`);
|
|
157
|
+
}
|
|
158
|
+
function loadState(file) {
|
|
159
|
+
if (!fs.existsSync(file))
|
|
160
|
+
return { processed: {} };
|
|
161
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
162
|
+
}
|
|
163
|
+
function saveState(file, state) {
|
|
164
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
165
|
+
fs.writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`);
|
|
166
|
+
}
|
|
167
|
+
function suggestDomains(candidate, domains) {
|
|
168
|
+
const haystack = `${candidate.name} ${candidate.description}`.toLowerCase();
|
|
169
|
+
const aliases = {
|
|
170
|
+
payments: ['payment', 'checkout', 'payout', 'fintech'],
|
|
171
|
+
'subscriptions-billing': ['subscription', 'billing', 'invoice'],
|
|
172
|
+
'crm-sales': ['crm', 'sales', 'customer support'],
|
|
173
|
+
messaging: ['sms', 'message', 'chat', 'push'],
|
|
174
|
+
email: ['email'],
|
|
175
|
+
'ai-machine-learning': [' ai ', 'machine learning', 'model'],
|
|
176
|
+
'developer-tools': ['developer', 'api', 'cloud', 'code'],
|
|
177
|
+
'maps-location': ['map', 'location', 'geocod'],
|
|
178
|
+
'shipping-delivery': ['shipping', 'delivery', 'logistics'],
|
|
179
|
+
};
|
|
180
|
+
return domains
|
|
181
|
+
.filter((domain) => {
|
|
182
|
+
const slug = text(domain.slug);
|
|
183
|
+
return (aliases[slug] || []).some((term) => ` ${haystack} `.includes(term));
|
|
184
|
+
})
|
|
185
|
+
.map((domain) => text(domain.slug));
|
|
186
|
+
}
|
|
187
|
+
function domainIds(slugs, domains) {
|
|
188
|
+
return domains
|
|
189
|
+
.filter((domain) => slugs.includes(text(domain.slug)))
|
|
190
|
+
.map((domain) => text(domain._id))
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
}
|
|
193
|
+
function marketplaceMetadata(candidate, collection) {
|
|
194
|
+
const info = (collection.info || {});
|
|
195
|
+
const description = text(info.description) || candidate.description ||
|
|
196
|
+
`${candidate.name} API collection published by ${candidate.publisher}.`;
|
|
197
|
+
const requestCount = JSON.stringify(collection).match(/"request"\s*:/g)?.length ?? 0;
|
|
198
|
+
const source = candidate.collectionUrl || `https://www.postman.com/explore/collections`;
|
|
199
|
+
const aboutText = [
|
|
200
|
+
description,
|
|
201
|
+
`Imported from the public Postman API Network collection maintained by ${candidate.publisher || 'its publisher'}.`,
|
|
202
|
+
`This version contains approximately ${requestCount} documented requests. Review authentication, environment variables, and provider terms before production use.`,
|
|
203
|
+
`Source: ${source}`,
|
|
204
|
+
].join('\n\n').slice(0, 5000);
|
|
205
|
+
const aboutHTML = `<p>${aboutText
|
|
206
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
207
|
+
.replace(/\n\n/g, '</p><p>')}</p>`.slice(0, 5000);
|
|
208
|
+
return {
|
|
209
|
+
app_name: candidate.name.slice(0, 100),
|
|
210
|
+
description: description.slice(0, 1000),
|
|
211
|
+
aboutText,
|
|
212
|
+
aboutHTML,
|
|
213
|
+
website: candidate.publisherUrl || candidate.collectionUrl,
|
|
214
|
+
...(candidate.logo?.startsWith('http') ? { logo: candidate.logo } : {}),
|
|
215
|
+
require_whitelist: false,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function findImportedApp(ctx, beforeIds, collectionName) {
|
|
219
|
+
const apps = await listApps(ctx, 'all');
|
|
220
|
+
const created = apps.find((app) => !beforeIds.has(text(app._id)));
|
|
221
|
+
const named = apps.find((app) => text(app.app_name) === collectionName);
|
|
222
|
+
const result = created || named;
|
|
223
|
+
if (!result?._id)
|
|
224
|
+
throw new Error(`Import completed but app "${collectionName}" could not be located`);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
export async function runAppsCuratePostman(opts) {
|
|
228
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
229
|
+
const maxPages = Math.max(1, Number(opts.maxPages || 250));
|
|
230
|
+
const minScore = Math.max(0, Number(opts.minScore || 5));
|
|
231
|
+
const stateFile = path.resolve(opts.state || DEFAULT_STATE);
|
|
232
|
+
const state = loadState(stateFile);
|
|
233
|
+
const domains = await listMarketplaceCategories(ctx);
|
|
234
|
+
const apiKey = opts.apiKey || process.env.POSTMAN_API_KEY;
|
|
235
|
+
let emptyPages = 0;
|
|
236
|
+
for (let page = 1; page <= maxPages && emptyPages < 2; page++) {
|
|
237
|
+
const url = `${EXPLORE_URL}?sort=forkCount&page=${page}&filter=this_week`;
|
|
238
|
+
const response = await fetch(url, { headers: { Accept: 'text/html' } });
|
|
239
|
+
if (!response.ok)
|
|
240
|
+
throw new Error(`Postman Explore page ${page} returned HTTP ${response.status}`);
|
|
241
|
+
const candidates = parseExplorePage(await response.text());
|
|
242
|
+
if (candidates.length === 0) {
|
|
243
|
+
emptyPages++;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
emptyPages = 0;
|
|
247
|
+
for (const candidate of candidates) {
|
|
248
|
+
if (state.processed[candidate.uid])
|
|
249
|
+
continue;
|
|
250
|
+
const trust = reputation(candidate);
|
|
251
|
+
if (trust.score < minScore)
|
|
252
|
+
continue;
|
|
253
|
+
console.log(`\n${candidate.name} by ${candidate.publisher || 'unknown publisher'}`);
|
|
254
|
+
console.log(`Reputation ${trust.score}: ${trust.reasons.join(', ') || 'limited evidence'}`);
|
|
255
|
+
if (candidate.collectionUrl)
|
|
256
|
+
console.log(candidate.collectionUrl);
|
|
257
|
+
const decision = await select({
|
|
258
|
+
message: 'Add or update this Ductape app?',
|
|
259
|
+
choices: [
|
|
260
|
+
{ name: 'Skip', value: 'skip' },
|
|
261
|
+
{ name: 'Add as new app', value: 'add' },
|
|
262
|
+
{ name: 'Update existing app/version', value: 'update' },
|
|
263
|
+
{ name: 'Stop and save progress', value: 'stop' },
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
if (decision === 'stop') {
|
|
267
|
+
saveState(stateFile, state);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (decision === 'skip') {
|
|
271
|
+
state.processed[candidate.uid] = 'skipped';
|
|
272
|
+
saveState(stateFile, state);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const defaultTag = `${slugify(candidate.publisher || candidate.name)}:${slugify(candidate.name)}`;
|
|
276
|
+
const appTag = await input({ message: 'Ductape app tag', default: defaultTag });
|
|
277
|
+
const version = await input({
|
|
278
|
+
message: 'App version',
|
|
279
|
+
default: new Date().toISOString().slice(0, 10).replace(/-/g, '.'),
|
|
280
|
+
});
|
|
281
|
+
const suggested = suggestDomains(candidate, domains);
|
|
282
|
+
const selectedSlugs = (await input({
|
|
283
|
+
message: 'Domain slugs (comma-separated)',
|
|
284
|
+
default: suggested.join(','),
|
|
285
|
+
})).split(',').map((item) => item.trim()).filter(Boolean);
|
|
286
|
+
const selectedDomainIds = domainIds(selectedSlugs, domains);
|
|
287
|
+
if (selectedDomainIds.length !== selectedSlugs.length) {
|
|
288
|
+
const known = new Set(domains.map((domain) => text(domain.slug)));
|
|
289
|
+
const unknown = selectedSlugs.filter((slug) => !known.has(slug));
|
|
290
|
+
throw new Error(`Unknown marketplace domain slug(s): ${unknown.join(', ')}`);
|
|
291
|
+
}
|
|
292
|
+
const collection = await downloadCollection(candidate, apiKey);
|
|
293
|
+
const tempFile = path.join(path.dirname(stateFile), `${slugify(candidate.uid)}.postman_collection.json`);
|
|
294
|
+
fs.mkdirSync(path.dirname(tempFile), { recursive: true });
|
|
295
|
+
fs.writeFileSync(tempFile, `${JSON.stringify(collection, null, 2)}\n`);
|
|
296
|
+
const before = await listApps(ctx, 'all');
|
|
297
|
+
const beforeIds = new Set(before.map((app) => text(app._id)));
|
|
298
|
+
let target;
|
|
299
|
+
if (decision === 'update') {
|
|
300
|
+
target = await getApp(ctx, { tag: appTag });
|
|
301
|
+
if (!target?._id)
|
|
302
|
+
throw new Error(`Existing app not found: ${appTag}`);
|
|
303
|
+
}
|
|
304
|
+
await importAppFromFile(ctx, {
|
|
305
|
+
filePath: tempFile,
|
|
306
|
+
format: 'postman',
|
|
307
|
+
appId: target ? text(target._id) : undefined,
|
|
308
|
+
update: decision === 'update',
|
|
309
|
+
version,
|
|
310
|
+
});
|
|
311
|
+
target = target || await findImportedApp(ctx, beforeIds, text(collection.info?.name) || candidate.name);
|
|
312
|
+
await updateApp(ctx, text(target._id), {
|
|
313
|
+
component: 'app',
|
|
314
|
+
tag: appTag,
|
|
315
|
+
version,
|
|
316
|
+
domains: selectedDomainIds,
|
|
317
|
+
status: 'pending_review',
|
|
318
|
+
workspace_id: ctx.workspaceId,
|
|
319
|
+
...marketplaceMetadata(candidate, collection),
|
|
320
|
+
});
|
|
321
|
+
fs.unlinkSync(tempFile);
|
|
322
|
+
state.processed[candidate.uid] = decision === 'update' ? 'updated' : 'imported';
|
|
323
|
+
saveState(stateFile, state);
|
|
324
|
+
success(`${candidate.name} submitted for marketplace review as ${appTag}@${version}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
saveState(stateFile, state);
|
|
328
|
+
}
|
|
@@ -11,15 +11,41 @@ async function getAppliedTags(proxy, dbContext) {
|
|
|
11
11
|
const result = await proxy.execute('migration.history', [], dbContext);
|
|
12
12
|
const list = Array.isArray(result)
|
|
13
13
|
? result
|
|
14
|
-
:
|
|
14
|
+
: result !== null
|
|
15
|
+
&& typeof result === 'object'
|
|
16
|
+
&& Array.isArray(result.data)
|
|
15
17
|
? result.data
|
|
16
|
-
:
|
|
17
|
-
|
|
18
|
+
: null;
|
|
19
|
+
if (!Array.isArray(list)) {
|
|
20
|
+
throw new Error('migration.history returned an invalid response instead of a history array');
|
|
21
|
+
}
|
|
22
|
+
return list.map((entry, index) => {
|
|
23
|
+
if (entry === null || typeof entry !== 'object' || typeof entry.tag !== 'string') {
|
|
24
|
+
throw new Error(`migration.history returned an invalid entry at index ${index}`);
|
|
25
|
+
}
|
|
26
|
+
return entry.tag;
|
|
27
|
+
});
|
|
18
28
|
}
|
|
19
|
-
catch {
|
|
20
|
-
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Could not read migration history for ${dbContext.product}/${dbContext.database}:${dbContext.env}. ` +
|
|
31
|
+
'Migration state is unknown; no migrations were classified as pending.', { cause: error });
|
|
21
32
|
}
|
|
22
33
|
}
|
|
34
|
+
function getMigrationResult(response, migrationTag) {
|
|
35
|
+
if (response === null || typeof response !== 'object')
|
|
36
|
+
return null;
|
|
37
|
+
const result = response[migrationTag];
|
|
38
|
+
if (result === null || typeof result !== 'object')
|
|
39
|
+
return null;
|
|
40
|
+
const success = result.success;
|
|
41
|
+
if (typeof success !== 'boolean')
|
|
42
|
+
return null;
|
|
43
|
+
const error = result.error;
|
|
44
|
+
return {
|
|
45
|
+
success,
|
|
46
|
+
...(typeof error === 'string' ? { error } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
23
49
|
export async function runDbMigrate(opts) {
|
|
24
50
|
const found = findProjectConfig();
|
|
25
51
|
if (!found)
|
|
@@ -59,13 +85,26 @@ export async function runDbMigrate(opts) {
|
|
|
59
85
|
}
|
|
60
86
|
process.stdout.write(` Applying ${migration.tag}... `);
|
|
61
87
|
try {
|
|
62
|
-
await proxy.execute('migration.run', [[migration]], dbContext);
|
|
88
|
+
const response = await proxy.execute('migration.run', [[migration]], dbContext);
|
|
89
|
+
const migrationResult = getMigrationResult(response, migration.tag);
|
|
90
|
+
if (migrationResult && !migrationResult.success) {
|
|
91
|
+
throw new Error(migrationResult.error ?? 'SDK reported an unsuccessful migration');
|
|
92
|
+
}
|
|
93
|
+
// A successful HTTP response is not proof that the migration was
|
|
94
|
+
// recorded. Verify history before claiming success, including when
|
|
95
|
+
// talking to older proxies that serialize Map results as `{}`.
|
|
96
|
+
const appliedAfterRun = await getAppliedTags(proxy, dbContext);
|
|
97
|
+
if (!appliedAfterRun.includes(migration.tag)) {
|
|
98
|
+
throw new Error('the operation returned without error, but the migration tag is absent from migration history');
|
|
99
|
+
}
|
|
63
100
|
console.log('done');
|
|
64
101
|
totalApplied++;
|
|
65
102
|
}
|
|
66
103
|
catch (err) {
|
|
67
|
-
console.log('
|
|
68
|
-
fail(`Migration "${migration.tag}"
|
|
104
|
+
console.log('unverified');
|
|
105
|
+
fail(`Migration "${migration.tag}" could not be verified: ` +
|
|
106
|
+
`${err instanceof Error ? err.message : String(err)}. ` +
|
|
107
|
+
'Do not rerun it until the live schema and migration history have been inspected.');
|
|
69
108
|
}
|
|
70
109
|
}
|
|
71
110
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getMarketplaceApp, listMarketplaceCategories, searchMarketplaceApps, } from '../lib/platform-api.js';
|
|
2
|
+
import { printJson } from '../lib/output.js';
|
|
3
|
+
import { requireWorkspaceContext } from '../lib/workspace-context.js';
|
|
4
|
+
export async function runMarketplace(verb, opts, args) {
|
|
5
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
6
|
+
let result;
|
|
7
|
+
switch (verb.toLowerCase()) {
|
|
8
|
+
case 'search':
|
|
9
|
+
case 'list':
|
|
10
|
+
result = await searchMarketplaceApps(ctx, {
|
|
11
|
+
query: opts.query ?? (args.join(' ') || undefined),
|
|
12
|
+
category: opts.category,
|
|
13
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
14
|
+
});
|
|
15
|
+
break;
|
|
16
|
+
case 'get': {
|
|
17
|
+
const tag = opts.tag ?? args[0];
|
|
18
|
+
if (!tag)
|
|
19
|
+
throw new Error('marketplace get requires <tag> or --tag <tag>');
|
|
20
|
+
result = await getMarketplaceApp(ctx, tag);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
case 'categories':
|
|
24
|
+
result = await listMarketplaceCategories(ctx);
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
throw new Error('Unknown marketplace verb. Use: search, list, get, categories');
|
|
28
|
+
}
|
|
29
|
+
printJson(result, Boolean(opts.json));
|
|
30
|
+
}
|
|
@@ -4,6 +4,18 @@ interface MigrationPlan {
|
|
|
4
4
|
version: 2;
|
|
5
5
|
artifact_kind: 'ai_review_bootstrap';
|
|
6
6
|
authority: 'advisory_only';
|
|
7
|
+
execution_provenance?: {
|
|
8
|
+
mcp_client?: {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
source: 'mcp_initialize_handshake';
|
|
12
|
+
trust: 'protocol_asserted_unverified';
|
|
13
|
+
};
|
|
14
|
+
ai_model: {
|
|
15
|
+
status: 'unavailable';
|
|
16
|
+
reason: 'MCP does not provide a server-verifiable model identity';
|
|
17
|
+
};
|
|
18
|
+
};
|
|
7
19
|
source: string;
|
|
8
20
|
mode: MigrationMode;
|
|
9
21
|
destination: string;
|
|
@@ -137,6 +149,13 @@ interface MigrationPlan {
|
|
|
137
149
|
ai_working_standards: {
|
|
138
150
|
global: string[];
|
|
139
151
|
languages: Record<Language, string[]>;
|
|
152
|
+
feature_classification: {
|
|
153
|
+
definition: string;
|
|
154
|
+
categories: Record<'FEATURE' | 'FEATURE_STEP' | 'DOMAIN_SERVICE' | 'UTILITY' | 'INFRASTRUCTURE_ADAPTER', string>;
|
|
155
|
+
discovery: string[];
|
|
156
|
+
required_output_fields: string[];
|
|
157
|
+
grouping_rule: string;
|
|
158
|
+
};
|
|
140
159
|
};
|
|
141
160
|
output_paths: {
|
|
142
161
|
plan: string;
|
|
@@ -172,5 +191,7 @@ export declare function runMigrateCodebase(opts: {
|
|
|
172
191
|
ensureProduct?: boolean;
|
|
173
192
|
write?: boolean;
|
|
174
193
|
json?: boolean;
|
|
194
|
+
mcpClientName?: string;
|
|
195
|
+
mcpClientVersion?: string;
|
|
175
196
|
}): Promise<void>;
|
|
176
197
|
export {};
|
|
@@ -288,17 +288,26 @@ function detectSecretReferences(texts) {
|
|
|
288
288
|
const found = new Map();
|
|
289
289
|
const rules = [
|
|
290
290
|
['github', /\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g],
|
|
291
|
+
['github', /\$\{\{\s*secrets\[['"]([A-Za-z_][A-Za-z0-9_]*)['"]\]\s*\}\}/g],
|
|
291
292
|
['gitlab', /\$\{?([A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL)[A-Z0-9_]*)\}?/g],
|
|
293
|
+
['gitlab', /\bvault\s*:\s*['"]?([A-Za-z0-9_./@-]+)/g],
|
|
292
294
|
['jenkins', /\bcredentials\(\s*['"]([^'"]+)['"]\s*\)/g],
|
|
295
|
+
['jenkins', /\bcredentialsId\s*:\s*['"]([^'"]+)['"]/g],
|
|
293
296
|
['kubernetes', /\bsecretKeyRef\s*:\s*(?:\r?\n[ \t]+[^\n]*)*?\r?\n[ \t]+key\s*:\s*['"]?([A-Za-z0-9_.-]+)/g],
|
|
294
297
|
['kubernetes', /\bremoteRef\s*:\s*(?:\r?\n[ \t]+[^\n]*)*?\r?\n[ \t]+key\s*:\s*['"]?([A-Za-z0-9_./-]+)/g],
|
|
298
|
+
['kubernetes', /\b(?:secretName|secretProviderClass)\s*:\s*['"]?([A-Za-z0-9_.-]+)/g],
|
|
299
|
+
['kubernetes', /\bsecretRef\s*:\s*(?:\r?\n[ \t]+[^\n]*)*?\r?\n[ \t]+name\s*:\s*['"]?([A-Za-z0-9_.-]+)/g],
|
|
295
300
|
['spring', /\$\{([A-Za-z_][A-Za-z0-9_.-]*)(?::[^}]*)?\}/g],
|
|
296
301
|
['dotnet', /(?:GetConnectionString\(\s*|Configuration\s*\[\s*)['"]([^'"]+)['"]/g],
|
|
297
302
|
['dotnet', /<UserSecretsId>\s*([^<\r\n]+)\s*<\/UserSecretsId>/g],
|
|
298
303
|
['terraform', /\bvar\.([A-Za-z_][A-Za-z0-9_]*)/g],
|
|
299
304
|
['aws', /(?:secretsmanager|secret-id|secretId)[/:="'\s]+([A-Za-z0-9_./-]+)/gi],
|
|
305
|
+
['aws', /\{\{resolve:(?:secretsmanager|ssm-secure):([^}:]+)/gi],
|
|
306
|
+
['aws', /arn:aws:secretsmanager:[^:\s]+:[^:\s]+:secret:([A-Za-z0-9_./+=@-]+)/gi],
|
|
300
307
|
['gcp', /(?:secretmanager|secretVersion|secret-version)[/:="'\s]+([A-Za-z0-9_./-]+)/gi],
|
|
308
|
+
['gcp', /projects\/[^/\s]+\/secrets\/([A-Za-z0-9_-]+)/gi],
|
|
301
309
|
['azure', /(?:vault\.azure\.net\/secrets\/|secretName\s*[:=]\s*['"])([A-Za-z0-9_.-]+)/gi],
|
|
310
|
+
['azure', /@Microsoft\.KeyVault\(\s*SecretUri=https:\/\/[^/\s]+\.vault\.azure\.net\/secrets\/([A-Za-z0-9_.-]+)/gi],
|
|
302
311
|
['docker', /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::?-[^}]*)?\}/g],
|
|
303
312
|
['circleci', /\$\{?([A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL)[A-Z0-9_]*)\}?/g],
|
|
304
313
|
['azure_devops', /\$\(([A-Za-z_][A-Za-z0-9_.-]*(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL)[A-Za-z0-9_.-]*)\)/gi],
|
|
@@ -470,6 +479,8 @@ const FILE_REVIEW_CHECKLIST = [
|
|
|
470
479
|
'configuration and secret-name references',
|
|
471
480
|
'database reads, writes, transactions, schemas, and migrations',
|
|
472
481
|
'internal events, queues, schedules, and background work',
|
|
482
|
+
'named capability candidates across exports, public methods, controllers, consumers, jobs, repeated orchestration, typed operations, documentation, and routes',
|
|
483
|
+
'whether related low-level operations collectively form one FEATURE, FEATURE_STEP, DOMAIN_SERVICE, UTILITY, or INFRASTRUCTURE_ADAPTER boundary',
|
|
473
484
|
'external HTTP/API integrations and authentication',
|
|
474
485
|
'sessions, actor propagation, authorization, and privacy',
|
|
475
486
|
'cache, storage, notifications, graph, and vector usage',
|
|
@@ -621,6 +632,27 @@ function buildPlan(source, files, scanPolicy, mode, destination, productTag, pro
|
|
|
621
632
|
'Preserve request scope and dispose SDK-owned resources through the host lifecycle.',
|
|
622
633
|
],
|
|
623
634
|
},
|
|
635
|
+
feature_classification: {
|
|
636
|
+
definition: 'A named, reusable product capability with a stable input/output contract that benefits from managed execution, composition, observability, retries, versioning, policy enforcement, or explicit execution steps. It may be synchronous and local.',
|
|
637
|
+
categories: {
|
|
638
|
+
FEATURE: 'Independently meaningful product capability; make it a standalone Ductape Feature.',
|
|
639
|
+
FEATURE_STEP: 'Meaningful stage without a useful independent boundary; expose it as a named ctx.step inside another Feature.',
|
|
640
|
+
DOMAIN_SERVICE: 'Reusable domain logic without a useful independent managed-execution boundary; keep it as domain logic called by a Feature.',
|
|
641
|
+
UTILITY: 'Low-level hashing, formatting, redaction, normalization, conversion, or type-guard helper; keep it as a utility.',
|
|
642
|
+
INFRASTRUCTURE_ADAPTER: 'Database, Event, HTTP, cache, storage, transport, provider, or framework integration; keep it as an infrastructure adapter.',
|
|
643
|
+
},
|
|
644
|
+
discovery: [
|
|
645
|
+
'Exported functions and public service methods; export alone is neither sufficient nor necessary.',
|
|
646
|
+
'Controller entry points, Event consumers, scheduled jobs, and repeated orchestration sequences.',
|
|
647
|
+
'Domain operations reused from several locations and functions with substantial typed inputs and outputs.',
|
|
648
|
+
'Functions composing several meaningful domain stages and product terminology in documentation and API routes.',
|
|
649
|
+
],
|
|
650
|
+
required_output_fields: [
|
|
651
|
+
'candidate', 'classification', 'executionStyle', 'evidence', 'suggestedSteps',
|
|
652
|
+
'signalsRequired', 'eventsRequired', 'recommendation',
|
|
653
|
+
],
|
|
654
|
+
grouping_rule: 'Group related low-level operations into one coherent capability candidate; never recommend one Feature per exported function.',
|
|
655
|
+
},
|
|
624
656
|
},
|
|
625
657
|
output_paths: {
|
|
626
658
|
plan: planPath,
|
|
@@ -656,6 +688,20 @@ ${plan.ai_working_standards.global.map((rule) => `- ${rule}`).join('\n')}
|
|
|
656
688
|
|
|
657
689
|
${languageSections || 'No supported server language was detected. The AI must inspect the repository before proceeding.'}
|
|
658
690
|
|
|
691
|
+
## Feature capability classification
|
|
692
|
+
|
|
693
|
+
${plan.ai_working_standards.feature_classification.definition}
|
|
694
|
+
|
|
695
|
+
${Object.entries(plan.ai_working_standards.feature_classification.categories)
|
|
696
|
+
.map(([category, rule]) => `- **${category}**: ${rule}`).join('\n')}
|
|
697
|
+
|
|
698
|
+
Discovery must cover:
|
|
699
|
+
${plan.ai_working_standards.feature_classification.discovery.map((item) => `- ${item}`).join('\n')}
|
|
700
|
+
|
|
701
|
+
- ${plan.ai_working_standards.feature_classification.grouping_rule}
|
|
702
|
+
- Every recommendation must include: ${plan.ai_working_standards.feature_classification.required_output_fields.join(', ')}.
|
|
703
|
+
- Signals and Events are optional; synchronous multi-step capabilities remain valid Feature candidates.
|
|
704
|
+
|
|
659
705
|
## Workflow
|
|
660
706
|
|
|
661
707
|
1. Before this plan, create missing project-level E2E tests, run them against the original codebase, and record a passing checksum-bound \`migration-e2e\` baseline.
|
|
@@ -727,6 +773,20 @@ export async function runMigrateCodebase(opts) {
|
|
|
727
773
|
});
|
|
728
774
|
const productTag = plan.product.tag;
|
|
729
775
|
const productName = plan.product.name;
|
|
776
|
+
if (opts.mcpClientName && opts.mcpClientVersion) {
|
|
777
|
+
plan.execution_provenance = {
|
|
778
|
+
mcp_client: {
|
|
779
|
+
name: opts.mcpClientName,
|
|
780
|
+
version: opts.mcpClientVersion,
|
|
781
|
+
source: 'mcp_initialize_handshake',
|
|
782
|
+
trust: 'protocol_asserted_unverified',
|
|
783
|
+
},
|
|
784
|
+
ai_model: {
|
|
785
|
+
status: 'unavailable',
|
|
786
|
+
reason: 'MCP does not provide a server-verifiable model identity',
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
}
|
|
730
790
|
if (opts.ensureProduct) {
|
|
731
791
|
const context = requireWorkspaceContext({});
|
|
732
792
|
try {
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
+
type JsonSchema = {
|
|
2
|
+
$schema: string;
|
|
3
|
+
title: string;
|
|
4
|
+
type: 'object';
|
|
5
|
+
required: string[];
|
|
6
|
+
properties: Record<string, unknown>;
|
|
7
|
+
additionalProperties: boolean;
|
|
8
|
+
};
|
|
1
9
|
export declare const MIGRATION_ARTIFACT_SCHEMAS: {
|
|
2
10
|
version: number;
|
|
3
|
-
artifacts:
|
|
4
|
-
|
|
5
|
-
title: string;
|
|
6
|
-
type: string;
|
|
7
|
-
required: string[];
|
|
8
|
-
properties: {
|
|
9
|
-
version: {
|
|
10
|
-
const: number;
|
|
11
|
-
};
|
|
12
|
-
artifact_kind: {
|
|
13
|
-
const: string;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
additionalProperties: boolean;
|
|
17
|
-
}[];
|
|
11
|
+
artifacts: JsonSchema[];
|
|
12
|
+
definitions: JsonSchema[];
|
|
18
13
|
};
|
|
19
14
|
export declare function runMigrationArtifactValidate(opts: {
|
|
20
15
|
file: string;
|
|
@@ -30,3 +25,4 @@ export declare function runMigrationArtifactMigrate(opts: {
|
|
|
30
25
|
json?: boolean;
|
|
31
26
|
}): void;
|
|
32
27
|
export declare function runMigrationArtifactSchemas(json?: boolean): void;
|
|
28
|
+
export {};
|
|
@@ -1,39 +1,92 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { printJson } from '../lib/output.js';
|
|
3
3
|
import { migrateMigrationJson, readMigrationJson, recoverMigrationJson, } from '../lib/migration-artifact.js';
|
|
4
|
+
const objectSchema = (title, required, properties = {}) => ({
|
|
5
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
6
|
+
title,
|
|
7
|
+
type: 'object',
|
|
8
|
+
required,
|
|
9
|
+
properties,
|
|
10
|
+
additionalProperties: true,
|
|
11
|
+
});
|
|
12
|
+
const artifact = (kind, required) => objectSchema(kind, ['version', 'artifact_kind', ...required], { version: { const: 1 }, artifact_kind: { const: kind } });
|
|
4
13
|
export const MIGRATION_ARTIFACT_SCHEMAS = {
|
|
5
|
-
version:
|
|
14
|
+
version: 2,
|
|
6
15
|
artifacts: [
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
artifact('ai_review_bootstrap', ['authority', 'source', 'mode', 'destination', 'review_queue']),
|
|
17
|
+
artifact('ai_migration_review_ledger', ['source', 'entries']),
|
|
18
|
+
artifact('ductape_migration_slice', ['tag', 'name', 'ledger', 'files', 'surfaces', 'product_boundary']),
|
|
19
|
+
artifact('ductape_migration_portfolio', ['ledger', 'partitions', 'slices', 'cross_slice_contracts']),
|
|
20
|
+
artifact('ductape_migration_partition_proposal', ['authority', 'ledger', 'limits', 'partitions']),
|
|
21
|
+
artifact('ductape_migration_e2e_baseline', ['source', 'created_at', 'command', 'suite', 'baseline', 'final']),
|
|
22
|
+
artifact('ductape_migration_generated_vendor_evidence', ['analysis', 'generated', 'vendor']),
|
|
23
|
+
artifact('ductape_migration_assurance', [
|
|
24
|
+
'session_flows', 'feature_resilience', 'third_party_calls', 'components',
|
|
25
|
+
'provider_migrations', 'runtime', 'sdk_matrix',
|
|
26
|
+
]),
|
|
27
|
+
artifact('ductape_migration_verification_matrix', ['categories']),
|
|
28
|
+
artifact('ductape_database_baseline', [
|
|
29
|
+
'migration_id', 'database_tag', 'owners', 'providers', 'schema_snapshots',
|
|
30
|
+
'structural_comparison',
|
|
31
|
+
]),
|
|
32
|
+
artifact('ductape_database_data', [
|
|
33
|
+
'migration_id', 'transformations', 'batching', 'checkpoints', 'idempotency',
|
|
34
|
+
'resume_strategy', 'validation', 'reconciliation',
|
|
35
|
+
]),
|
|
36
|
+
artifact('ductape_database_cutover', [
|
|
37
|
+
'migration_id', 'phases', 'expand', 'backfill', 'dual_compatibility', 'contract',
|
|
38
|
+
'deployment_order', 'migration_locking', 'backup', 'restore_test', 'rollback',
|
|
39
|
+
]),
|
|
40
|
+
artifact('ductape_review_ledger_signature', [
|
|
41
|
+
'ledger', 'ledger_sha256', 'algorithm', 'key_id', 'signature', 'signed_at',
|
|
42
|
+
]),
|
|
43
|
+
artifact('ductape_service_product_map', ['analysis', 'mappings', 'assets', 'promotions']),
|
|
44
|
+
artifact('ductape_secret_migration_map', ['analysis', 'classifications']),
|
|
45
|
+
artifact('ductape_sdk_capability_matrix', ['authority', 'unsupported_policy', 'sdks']),
|
|
46
|
+
],
|
|
47
|
+
definitions: [
|
|
48
|
+
objectSchema('migration_e2e_definition', ['command', 'suite_files', 'baseline']),
|
|
49
|
+
objectSchema('migration_e2e_final_evidence', ['command', 'status', 'evidence', 'environment', 'suite_root']),
|
|
50
|
+
objectSchema('migration_slice_definition', [
|
|
51
|
+
'surfaces', 'product_boundary', 'sdk_capabilities', 'interface_contracts',
|
|
52
|
+
'functional_requirements', 'operational_requirements', 'parity_evidence',
|
|
53
|
+
'frontend_parity', 'required_assets', 'tests', 'failure_tests', 'runtime_evidence',
|
|
54
|
+
'smoke_checks', 'cutover_conditions', 'rollback_conditions', 'deployment_cutover',
|
|
55
|
+
]),
|
|
56
|
+
objectSchema('migration_portfolio_definition', ['partitions', 'slices', 'cross_slice_contracts']),
|
|
57
|
+
objectSchema('migration_database_definition', ['baseline', 'data', 'cutover']),
|
|
58
|
+
objectSchema('migration_product_map_definition', ['mappings', 'assets', 'promotions']),
|
|
59
|
+
objectSchema('migration_secret_map_definition', ['classifications']),
|
|
60
|
+
objectSchema('migration_generated_vendor_definition', ['generated', 'vendor']),
|
|
61
|
+
objectSchema('migration_review_record_definition', [
|
|
62
|
+
'purpose', 'dependencies', 'interfaces', 'functional_parity_requirements',
|
|
63
|
+
'operational_parity_requirements', 'findings',
|
|
64
|
+
]),
|
|
65
|
+
objectSchema('migration_assurance_definition', [
|
|
66
|
+
'version', 'artifact_kind', 'session_flows', 'feature_resilience', 'third_party_calls',
|
|
67
|
+
'components', 'provider_migrations', 'runtime', 'sdk_matrix',
|
|
68
|
+
]),
|
|
69
|
+
objectSchema('migration_verification_definition', ['version', 'artifact_kind', 'categories']),
|
|
70
|
+
objectSchema('migration_environment_inventory', ['environments']),
|
|
71
|
+
],
|
|
30
72
|
};
|
|
31
73
|
export function runMigrationArtifactValidate(opts) {
|
|
32
74
|
const value = readMigrationJson(opts.file);
|
|
33
|
-
const schema = MIGRATION_ARTIFACT_SCHEMAS.artifacts.find((candidate) => candidate.properties.artifact_kind
|
|
34
|
-
if (!schema || value.version !== 1)
|
|
75
|
+
const schema = MIGRATION_ARTIFACT_SCHEMAS.artifacts.find((candidate) => candidate.properties.artifact_kind?.const === value.artifact_kind);
|
|
76
|
+
if (!schema || value.version !== 1) {
|
|
35
77
|
throw new Error('[ARTIFACT_VERSION_UNSUPPORTED] Unknown artifact kind or version.');
|
|
36
|
-
|
|
78
|
+
}
|
|
79
|
+
const missing = schema.required.filter((field) => !(field in value) || value[field] == null);
|
|
80
|
+
if (missing.length) {
|
|
81
|
+
throw new Error(`[ARTIFACT_SCHEMA_INVALID] Missing required fields: ${missing.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
printJson({
|
|
84
|
+
file: path.resolve(opts.file),
|
|
85
|
+
valid: true,
|
|
86
|
+
artifact_kind: value.artifact_kind,
|
|
87
|
+
version: value.version,
|
|
88
|
+
schema_catalog_version: MIGRATION_ARTIFACT_SCHEMAS.version,
|
|
89
|
+
}, Boolean(opts.json));
|
|
37
90
|
}
|
|
38
91
|
export function runMigrationArtifactRecover(opts) {
|
|
39
92
|
recoverMigrationJson(opts.file);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type SupportedLanguage = 'typescript' | 'go' | 'java' | 'dotnet';
|
|
2
|
+
export declare const SDK_CAPABILITY_MATRIX: {
|
|
3
|
+
readonly version: 1;
|
|
4
|
+
readonly artifact_kind: "ductape_sdk_capability_matrix";
|
|
5
|
+
readonly authority: "source_verified_catalog";
|
|
6
|
+
readonly generated_from: "repository source; update and test this catalog with every supported SDK release";
|
|
7
|
+
readonly unsupported_policy: {
|
|
8
|
+
readonly rule: "Never infer cross-language parity or synthesize a missing API.";
|
|
9
|
+
readonly action: "Inspect the installed package exports, record the capability as unsupported, and stop for an approved fallback.";
|
|
10
|
+
};
|
|
11
|
+
readonly sdks: readonly [{
|
|
12
|
+
readonly language: "typescript";
|
|
13
|
+
readonly package: "@ductape/sdk";
|
|
14
|
+
readonly version: "0.1.101";
|
|
15
|
+
readonly status: "supported";
|
|
16
|
+
readonly source_version: "sdk/ts/package.json";
|
|
17
|
+
readonly public_surface_evidence: "sdk/ts/src/index.ts";
|
|
18
|
+
readonly capabilities: readonly ["actions", "agents", "cache", "cloud", "databases", "events", "features", "graphs", "imports", "jobs", "logs", "models", "notifications", "products", "resilience", "secrets", "sessions", "storage", "vectors", "warehouse"];
|
|
19
|
+
readonly lifecycle: readonly ["await asynchronous operations", "close/disconnect owned clients during application shutdown"];
|
|
20
|
+
}, {
|
|
21
|
+
readonly language: "go";
|
|
22
|
+
readonly package: "github.com/ductape/ductape/sdk/go";
|
|
23
|
+
readonly version: "v0.0.1";
|
|
24
|
+
readonly status: "supported";
|
|
25
|
+
readonly source_version: "sdk/go git tag v0.0.1";
|
|
26
|
+
readonly public_surface_evidence: "sdk/go/ductape";
|
|
27
|
+
readonly capabilities: readonly ["actions", "agents", "cache", "cloud", "databases", "events", "features", "graphs", "imports", "jobs", "logs", "models", "notifications", "products", "resilience", "secrets", "sessions", "storage", "vectors", "warehouse"];
|
|
28
|
+
readonly lifecycle: readonly ["propagate context.Context cancellation", "close owned clients during application shutdown"];
|
|
29
|
+
}, {
|
|
30
|
+
readonly language: "java";
|
|
31
|
+
readonly package: "app.ductape:ductape-sdk";
|
|
32
|
+
readonly version: "0.1.9-SNAPSHOT";
|
|
33
|
+
readonly status: "supported-source-snapshot";
|
|
34
|
+
readonly source_version: "sdk/java/build.gradle";
|
|
35
|
+
readonly public_surface_evidence: "sdk/java/src/main/java/app/ductape/sdk/Ductape.java";
|
|
36
|
+
readonly capabilities: readonly ["actions", "agents", "cache", "cloud", "databases", "events", "features", "graphs", "imports", "jobs", "logs", "models", "notifications", "products", "resilience", "secrets", "sessions", "storage", "vectors", "warehouse"];
|
|
37
|
+
readonly lifecycle: readonly ["propagate interruption/cancellation", "close owned clients during application shutdown"];
|
|
38
|
+
}, {
|
|
39
|
+
readonly language: "dotnet";
|
|
40
|
+
readonly package: "Ductape.Sdk";
|
|
41
|
+
readonly version: "0.1.12";
|
|
42
|
+
readonly status: "supported";
|
|
43
|
+
readonly source_version: "sdk/dotnet/src/Ductape.Sdk/Ductape.Sdk.csproj";
|
|
44
|
+
readonly public_surface_evidence: "sdk/dotnet/src/Ductape.Sdk/Ductape.cs";
|
|
45
|
+
readonly capabilities: readonly ["actions", "agents", "cache", "cloud", "databases", "events", "features", "graphs", "imports", "jobs", "logs", "models", "notifications", "products", "resilience", "secrets", "sessions", "storage", "vectors", "warehouse"];
|
|
46
|
+
readonly lifecycle: readonly ["propagate CancellationToken", "dispose owned clients during application shutdown"];
|
|
47
|
+
}];
|
|
48
|
+
};
|
|
49
|
+
export declare function runMigrationCapabilities(opts: {
|
|
50
|
+
language?: SupportedLanguage;
|
|
51
|
+
version?: string;
|
|
52
|
+
json?: boolean;
|
|
53
|
+
}): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { printJson } from '../lib/output.js';
|
|
2
|
+
const COMMON_RUNTIME_CAPABILITIES = [
|
|
3
|
+
'actions', 'agents', 'cache', 'cloud', 'databases', 'events', 'features',
|
|
4
|
+
'graphs', 'imports', 'jobs', 'logs', 'models', 'notifications', 'products',
|
|
5
|
+
'resilience', 'secrets', 'sessions', 'storage', 'vectors', 'warehouse',
|
|
6
|
+
];
|
|
7
|
+
export const SDK_CAPABILITY_MATRIX = {
|
|
8
|
+
version: 1,
|
|
9
|
+
artifact_kind: 'ductape_sdk_capability_matrix',
|
|
10
|
+
authority: 'source_verified_catalog',
|
|
11
|
+
generated_from: 'repository source; update and test this catalog with every supported SDK release',
|
|
12
|
+
unsupported_policy: {
|
|
13
|
+
rule: 'Never infer cross-language parity or synthesize a missing API.',
|
|
14
|
+
action: 'Inspect the installed package exports, record the capability as unsupported, and stop for an approved fallback.',
|
|
15
|
+
},
|
|
16
|
+
sdks: [
|
|
17
|
+
{
|
|
18
|
+
language: 'typescript',
|
|
19
|
+
package: '@ductape/sdk',
|
|
20
|
+
version: '0.1.101',
|
|
21
|
+
status: 'supported',
|
|
22
|
+
source_version: 'sdk/ts/package.json',
|
|
23
|
+
public_surface_evidence: 'sdk/ts/src/index.ts',
|
|
24
|
+
capabilities: [...COMMON_RUNTIME_CAPABILITIES],
|
|
25
|
+
lifecycle: ['await asynchronous operations', 'close/disconnect owned clients during application shutdown'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
language: 'go',
|
|
29
|
+
package: 'github.com/ductape/ductape/sdk/go',
|
|
30
|
+
version: 'v0.0.1',
|
|
31
|
+
status: 'supported',
|
|
32
|
+
source_version: 'sdk/go git tag v0.0.1',
|
|
33
|
+
public_surface_evidence: 'sdk/go/ductape',
|
|
34
|
+
capabilities: [...COMMON_RUNTIME_CAPABILITIES],
|
|
35
|
+
lifecycle: ['propagate context.Context cancellation', 'close owned clients during application shutdown'],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
language: 'java',
|
|
39
|
+
package: 'app.ductape:ductape-sdk',
|
|
40
|
+
version: '0.1.9-SNAPSHOT',
|
|
41
|
+
status: 'supported-source-snapshot',
|
|
42
|
+
source_version: 'sdk/java/build.gradle',
|
|
43
|
+
public_surface_evidence: 'sdk/java/src/main/java/app/ductape/sdk/Ductape.java',
|
|
44
|
+
capabilities: [...COMMON_RUNTIME_CAPABILITIES],
|
|
45
|
+
lifecycle: ['propagate interruption/cancellation', 'close owned clients during application shutdown'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
language: 'dotnet',
|
|
49
|
+
package: 'Ductape.Sdk',
|
|
50
|
+
version: '0.1.12',
|
|
51
|
+
status: 'supported',
|
|
52
|
+
source_version: 'sdk/dotnet/src/Ductape.Sdk/Ductape.Sdk.csproj',
|
|
53
|
+
public_surface_evidence: 'sdk/dotnet/src/Ductape.Sdk/Ductape.cs',
|
|
54
|
+
capabilities: [...COMMON_RUNTIME_CAPABILITIES],
|
|
55
|
+
lifecycle: ['propagate CancellationToken', 'dispose owned clients during application shutdown'],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
export function runMigrationCapabilities(opts) {
|
|
60
|
+
const sdks = SDK_CAPABILITY_MATRIX.sdks.filter((sdk) => (!opts.language || sdk.language === opts.language) &&
|
|
61
|
+
(!opts.version || sdk.version === opts.version));
|
|
62
|
+
if (!sdks.length) {
|
|
63
|
+
throw new Error(`No supported SDK capability matrix matches ${opts.language ?? '*'}@${opts.version ?? '*'}.`);
|
|
64
|
+
}
|
|
65
|
+
printJson({ ...SDK_CAPABILITY_MATRIX, sdks }, Boolean(opts.json));
|
|
66
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export declare const REQUIRED_DATABASE_FIELDS: {
|
|
2
|
+
readonly baseline: readonly ["database_tag", "owners", "providers", "code_schema_evidence", "migration_history_evidence", "applied_history_evidence", "live_snd_schema_evidence", "proposed_schema_evidence", "drift", "objects", "security", "topology", "compatibility", "performance_baseline"];
|
|
3
|
+
readonly data: readonly ["transformations", "batching", "checkpoints", "idempotency", "resume_strategy", "validation", "reconciliation", "failure_recovery", "pii_controls", "retention", "seed_data"];
|
|
4
|
+
readonly cutover: readonly ["phases", "expand", "backfill", "dual_compatibility", "contract", "deployment_order", "migration_locking", "backup", "restore_test", "rollback", "irreversible_changes", "monitoring", "reconciliation", "approval"];
|
|
5
|
+
};
|
|
1
6
|
export declare function initDatabaseMigration(opts: {
|
|
2
7
|
definition: string;
|
|
3
8
|
output: string;
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { printJson } from '../lib/output.js';
|
|
5
5
|
import { readMigrationJson, writeMigrationJson } from '../lib/migration-artifact.js';
|
|
6
|
-
const
|
|
6
|
+
export const REQUIRED_DATABASE_FIELDS = {
|
|
7
7
|
baseline: ['database_tag', 'owners', 'providers', 'code_schema_evidence', 'migration_history_evidence', 'applied_history_evidence', 'live_snd_schema_evidence', 'proposed_schema_evidence', 'drift', 'objects', 'security', 'topology', 'compatibility', 'performance_baseline'],
|
|
8
8
|
data: ['transformations', 'batching', 'checkpoints', 'idempotency', 'resume_strategy', 'validation', 'reconciliation', 'failure_recovery', 'pii_controls', 'retention', 'seed_data'],
|
|
9
9
|
cutover: ['phases', 'expand', 'backfill', 'dual_compatibility', 'contract', 'deployment_order', 'migration_locking', 'backup', 'restore_test', 'rollback', 'irreversible_changes', 'monitoring', 'reconciliation', 'approval'],
|
|
@@ -47,7 +47,7 @@ export function validateDatabaseMigration(directory) {
|
|
|
47
47
|
if (artifact.version !== 1 || artifact.artifact_kind !== `ductape_database_${name}`) {
|
|
48
48
|
blockers.push(`${name}: unsupported artifact version or kind`);
|
|
49
49
|
}
|
|
50
|
-
for (const field of missing(artifact,
|
|
50
|
+
for (const field of missing(artifact, REQUIRED_DATABASE_FIELDS[name]))
|
|
51
51
|
blockers.push(`${name}.${field}: evidence required`);
|
|
52
52
|
if (!text(artifact.migration_id))
|
|
53
53
|
blockers.push(`${name}.migration_id: required for cross-artifact consistency`);
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { runCompletion } from './commands/completion.js';
|
|
|
25
25
|
import { runProducts, runProductApps, runProductComponents, runProductEnvironments } from './commands/products.js';
|
|
26
26
|
import { runApps } from './commands/apps.js';
|
|
27
27
|
import { runAppsImport } from './commands/apps-import.js';
|
|
28
|
+
import { runMarketplace } from './commands/marketplace.js';
|
|
28
29
|
import { runApply } from './commands/apply.js';
|
|
29
30
|
import { runMigrateCodebase } from './commands/migrate-codebase.js';
|
|
30
31
|
import { runMigrationReviewInit, runMigrationReviewRecord, runMigrationReviewFollowUp, runMigrationReviewExclude, runMigrationReviewParity, runMigrationReviewReopen, runMigrationReviewMove, runMigrationReviewMerge, runMigrationReviewSign, runMigrationReviewSnapshot, runMigrationReviewVerifySignature, runMigrationReviewValidate, } from './commands/migration-review.js';
|
|
@@ -39,13 +40,18 @@ import { runMigrationGeneratedInit, runMigrationGeneratedValidate, } from './com
|
|
|
39
40
|
import { runMigrationAssuranceValidate } from './commands/migration-assurance.js';
|
|
40
41
|
import { runMigrationVerificationValidate } from './commands/migration-verification.js';
|
|
41
42
|
import { runMigrationArtifactMigrate, runMigrationArtifactRecover, runMigrationArtifactSchemas, runMigrationArtifactValidate, } from './commands/migration-artifact.js';
|
|
43
|
+
import { runMigrationCapabilities } from './commands/migration-capabilities.js';
|
|
44
|
+
import { structuredMigrationError } from './lib/migration-artifact.js';
|
|
42
45
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
43
46
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
|
|
44
47
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
48
|
function wrap(fn) {
|
|
46
49
|
return (...args) => {
|
|
47
|
-
Promise.resolve(fn(...args)).catch((err) => {
|
|
48
|
-
|
|
50
|
+
Promise.resolve().then(() => fn(...args)).catch((err) => {
|
|
51
|
+
const command = process.argv[2] ?? '';
|
|
52
|
+
console.error(command === 'migrate-codebase' || command.startsWith('migration-')
|
|
53
|
+
? structuredMigrationError(err)
|
|
54
|
+
: err instanceof Error ? err.message : err);
|
|
49
55
|
process.exit(1);
|
|
50
56
|
});
|
|
51
57
|
};
|
|
@@ -171,6 +177,8 @@ program
|
|
|
171
177
|
.option('--exclude <patterns>', 'Comma-separated repository-relative glob patterns')
|
|
172
178
|
.option('--ensure-product', 'Create the product when it does not exist')
|
|
173
179
|
.option('--write', 'Write redacted advisory artifacts only; never writes application code or executable assets')
|
|
180
|
+
.option('--mcp-client-name <name>', 'Protocol-asserted MCP client name (unverified)', undefined)
|
|
181
|
+
.option('--mcp-client-version <version>', 'Protocol-asserted MCP client version (unverified)', undefined)
|
|
174
182
|
.option('--json', 'JSON output')
|
|
175
183
|
.action(wrap((opts) => runMigrateCodebase({
|
|
176
184
|
source: opts.source,
|
|
@@ -186,6 +194,8 @@ program
|
|
|
186
194
|
ensureProduct: Boolean(opts.ensureProduct),
|
|
187
195
|
write: Boolean(opts.write),
|
|
188
196
|
json: Boolean(opts.json),
|
|
197
|
+
mcpClientName: opts.mcpClientName,
|
|
198
|
+
mcpClientVersion: opts.mcpClientVersion,
|
|
189
199
|
})));
|
|
190
200
|
const migrationE2E = program
|
|
191
201
|
.command('migration-e2e')
|
|
@@ -240,6 +250,17 @@ program
|
|
|
240
250
|
strict: Boolean(opts.strict),
|
|
241
251
|
json: Boolean(opts.json),
|
|
242
252
|
})));
|
|
253
|
+
program
|
|
254
|
+
.command('migration-capabilities')
|
|
255
|
+
.description('Print the source-verified capability matrix for supported SDK language/version pairs')
|
|
256
|
+
.option('--language <language>', 'typescript | go | java | dotnet')
|
|
257
|
+
.option('--version <version>', 'Exact supported SDK version')
|
|
258
|
+
.option('--json', 'JSON output')
|
|
259
|
+
.action(wrap((opts) => runMigrationCapabilities({
|
|
260
|
+
language: opts.language,
|
|
261
|
+
version: opts.version,
|
|
262
|
+
json: Boolean(opts.json),
|
|
263
|
+
})));
|
|
243
264
|
program
|
|
244
265
|
.command('migration-verification')
|
|
245
266
|
.description('Validate cited automation evidence for large-repo, parity, frontend, database, and asset migration')
|
|
@@ -595,6 +616,18 @@ apps
|
|
|
595
616
|
version: opts.version,
|
|
596
617
|
json: opts.json,
|
|
597
618
|
})));
|
|
619
|
+
program
|
|
620
|
+
.command('marketplace')
|
|
621
|
+
.description('Discover public apps by capability and inspect their actions')
|
|
622
|
+
.argument('<verb>', 'search | list | get | categories')
|
|
623
|
+
.argument('[terms...]', 'Capability terms for search, or app tag for get')
|
|
624
|
+
.option('--profile <name>')
|
|
625
|
+
.option('-q, --query <query>', 'Capability search query')
|
|
626
|
+
.option('-c, --category <category>', 'Filter by category/domain name')
|
|
627
|
+
.option('-l, --limit <count>', 'Maximum results', '20')
|
|
628
|
+
.option('-t, --tag <tag>', 'Marketplace app tag for get')
|
|
629
|
+
.option('--json', 'JSON output')
|
|
630
|
+
.action(wrap((verb, terms, opts) => runMarketplace(verb, opts, terms)));
|
|
598
631
|
const events = program.command('events').description('Message broker CRUD (sdk-proxy, requires access key login)');
|
|
599
632
|
const eventTopics = events.command('topics').description('Topic CRUD for a message broker');
|
|
600
633
|
eventTopics
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export type MigrationArtifactErrorCode = 'ARTIFACT_TOO_LARGE' | 'ARTIFACT_INVALID_JSON' | 'ARTIFACT_SECRET_MATERIAL' | 'ARTIFACT_LOCKED' | 'ARTIFACT_EXISTS' | 'ARTIFACT_VERSION_UNSUPPORTED' | 'ARTIFACT_RECOVERY_FAILED';
|
|
2
|
+
export declare const MIGRATION_ERROR_CODES: {
|
|
3
|
+
readonly validation: "MIGRATION_VALIDATION_FAILED";
|
|
4
|
+
readonly configuration: "MIGRATION_CONFIGURATION_INVALID";
|
|
5
|
+
readonly external: "MIGRATION_EXTERNAL_OPERATION_FAILED";
|
|
6
|
+
};
|
|
7
|
+
export declare function structuredMigrationError(error: unknown): string;
|
|
2
8
|
export declare class MigrationArtifactError extends Error {
|
|
3
9
|
readonly code: MigrationArtifactErrorCode;
|
|
4
10
|
constructor(code: MigrationArtifactErrorCode, message: string);
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
export const MIGRATION_ERROR_CODES = {
|
|
4
|
+
validation: 'MIGRATION_VALIDATION_FAILED',
|
|
5
|
+
configuration: 'MIGRATION_CONFIGURATION_INVALID',
|
|
6
|
+
external: 'MIGRATION_EXTERNAL_OPERATION_FAILED',
|
|
7
|
+
};
|
|
8
|
+
export function structuredMigrationError(error) {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
+
if (/^\[[A-Z0-9_]+\]/.test(message))
|
|
11
|
+
return message;
|
|
12
|
+
return `[${MIGRATION_ERROR_CODES.validation}] ${message}`;
|
|
13
|
+
}
|
|
3
14
|
export class MigrationArtifactError extends Error {
|
|
4
15
|
code;
|
|
5
16
|
constructor(code, message) {
|
|
@@ -37,8 +48,8 @@ export function readMigrationJson(file, maxBytes = 5_000_000) {
|
|
|
37
48
|
try {
|
|
38
49
|
value = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
39
50
|
}
|
|
40
|
-
catch
|
|
41
|
-
throw new MigrationArtifactError('ARTIFACT_INVALID_JSON', `${resolved}:
|
|
51
|
+
catch {
|
|
52
|
+
throw new MigrationArtifactError('ARTIFACT_INVALID_JSON', `${resolved}: JSON parsing failed; parser context is intentionally suppressed to prevent evidence leakage.`);
|
|
42
53
|
}
|
|
43
54
|
inspect(value);
|
|
44
55
|
return value;
|
|
@@ -17,6 +17,14 @@ export declare function createApp(ctx: WorkspaceContext, body: Record<string, un
|
|
|
17
17
|
export declare function updateApp(ctx: WorkspaceContext, appId: string, body: Record<string, unknown>): Promise<unknown>;
|
|
18
18
|
export declare function updateAppViaProxy(ctx: WorkspaceContext, tag: string, body: Record<string, unknown>): Promise<unknown>;
|
|
19
19
|
export declare function deleteApp(ctx: WorkspaceContext, appId: string): Promise<unknown>;
|
|
20
|
+
export interface MarketplaceSearchOptions {
|
|
21
|
+
query?: string;
|
|
22
|
+
category?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function listMarketplaceCategories(ctx: WorkspaceContext): Promise<unknown[]>;
|
|
26
|
+
export declare function getMarketplaceApp(ctx: WorkspaceContext, tag: string): Promise<unknown>;
|
|
27
|
+
export declare function searchMarketplaceApps(ctx: WorkspaceContext, opts: MarketplaceSearchOptions): Promise<unknown[]>;
|
|
20
28
|
export interface TierQuery {
|
|
21
29
|
provider?: string;
|
|
22
30
|
resource_type?: string;
|
package/dist/lib/platform-api.js
CHANGED
|
@@ -135,6 +135,61 @@ export async function deleteApp(ctx, appId) {
|
|
|
135
135
|
};
|
|
136
136
|
return client(ctx).delete(`/apps/v1/${appId}?${new URLSearchParams(q).toString()}`);
|
|
137
137
|
}
|
|
138
|
+
function searchableMarketplaceText(app) {
|
|
139
|
+
if (!app || typeof app !== 'object')
|
|
140
|
+
return '';
|
|
141
|
+
const value = app;
|
|
142
|
+
const version = value.currentVersion && typeof value.currentVersion === 'object'
|
|
143
|
+
? value.currentVersion
|
|
144
|
+
: {};
|
|
145
|
+
const actions = Array.isArray(version.actions) ? version.actions : [];
|
|
146
|
+
const webhooks = Array.isArray(version.webhooks) ? version.webhooks : [];
|
|
147
|
+
return [
|
|
148
|
+
value.app_name,
|
|
149
|
+
value.tag,
|
|
150
|
+
value.description,
|
|
151
|
+
value.aboutText,
|
|
152
|
+
...(Array.isArray(value.domains) ? value.domains : []),
|
|
153
|
+
...actions.flatMap((action) => {
|
|
154
|
+
if (!action || typeof action !== 'object')
|
|
155
|
+
return [];
|
|
156
|
+
const item = action;
|
|
157
|
+
return [item.name, item.tag, item.description];
|
|
158
|
+
}),
|
|
159
|
+
...webhooks.flatMap((webhook) => {
|
|
160
|
+
if (!webhook || typeof webhook !== 'object')
|
|
161
|
+
return [];
|
|
162
|
+
const item = webhook;
|
|
163
|
+
return [item.name, item.tag, item.description];
|
|
164
|
+
}),
|
|
165
|
+
].filter((item) => typeof item === 'string').join(' ').toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
export async function listMarketplaceCategories(ctx) {
|
|
168
|
+
const result = await client(ctx).getPath('/apps/v1/domains');
|
|
169
|
+
return unwrapList(result);
|
|
170
|
+
}
|
|
171
|
+
export async function getMarketplaceApp(ctx, tag) {
|
|
172
|
+
const result = await client(ctx).getPath('/apps/v1/fetch/tag', { tag });
|
|
173
|
+
return unwrapOne(result);
|
|
174
|
+
}
|
|
175
|
+
export async function searchMarketplaceApps(ctx, opts) {
|
|
176
|
+
const result = await client(ctx).getPath('/apps/v1/domains/all');
|
|
177
|
+
const apps = unwrapList(result);
|
|
178
|
+
const terms = (opts.query ?? '').toLowerCase().split(/\s+/).filter(Boolean);
|
|
179
|
+
const category = opts.category?.toLowerCase();
|
|
180
|
+
const filtered = apps.filter((app) => {
|
|
181
|
+
const text = searchableMarketplaceText(app);
|
|
182
|
+
if (terms.length > 0 && !terms.every((term) => text.includes(term)))
|
|
183
|
+
return false;
|
|
184
|
+
if (!category)
|
|
185
|
+
return true;
|
|
186
|
+
if (!app || typeof app !== 'object')
|
|
187
|
+
return false;
|
|
188
|
+
const domains = app.domains;
|
|
189
|
+
return Array.isArray(domains) && domains.some((domain) => typeof domain === 'string' && domain.toLowerCase().includes(category));
|
|
190
|
+
});
|
|
191
|
+
return filtered.slice(0, Math.max(1, Math.min(opts.limit ?? 20, 100)));
|
|
192
|
+
}
|
|
138
193
|
export async function listCloudTiers(ctx, q) {
|
|
139
194
|
const params = workspaceAuthQuery(ctx);
|
|
140
195
|
if (q.provider)
|
package/package.json
CHANGED