@kodiak-finance/orderly-devkit 1.0.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 +160 -0
- package/bin/cli.js +112 -0
- package/package.json +37 -0
- package/src/commands/create/module.js +49 -0
- package/src/commands/create/plugin.js +270 -0
- package/src/commands/delete.js +224 -0
- package/src/commands/disable.js +219 -0
- package/src/commands/list.js +196 -0
- package/src/commands/login.js +147 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/mcp/detect.js +128 -0
- package/src/commands/mcp/install.js +122 -0
- package/src/commands/mcp.js +9 -0
- package/src/commands/skills/install.js +211 -0
- package/src/commands/skills.js +10 -0
- package/src/commands/submit.js +457 -0
- package/src/commands/update.js +240 -0
- package/src/commands/view.js +76 -0
- package/src/commands/whoami.js +19 -0
- package/src/internal/auth.js +222 -0
- package/src/internal/constants.js +80 -0
- package/src/internal/login-server.js +114 -0
- package/src/internal/manifest.js +189 -0
- package/src/internal/orderlySdkDocsMcpDetect.js +255 -0
- package/src/internal/templateGenerator.js +294 -0
- package/src/shared.js +136 -0
- package/src/version.ts +13 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
input,
|
|
5
|
+
select,
|
|
6
|
+
heading,
|
|
7
|
+
info,
|
|
8
|
+
success,
|
|
9
|
+
warn,
|
|
10
|
+
error,
|
|
11
|
+
getApiErrorInfo,
|
|
12
|
+
} = require("../shared");
|
|
13
|
+
const {
|
|
14
|
+
isLoggedIn,
|
|
15
|
+
getToken,
|
|
16
|
+
authenticatedFetch,
|
|
17
|
+
} = require("../internal/auth");
|
|
18
|
+
const { MARKETPLACE_API_PLUGINS_URL } = require("../internal/constants");
|
|
19
|
+
const { resolvePluginManifest, getRepoUrl } = require("../internal/manifest");
|
|
20
|
+
const {
|
|
21
|
+
maybePrintOrderlyDevEnvironmentHints,
|
|
22
|
+
} = require("../internal/orderlySdkDocsMcpDetect");
|
|
23
|
+
|
|
24
|
+
// Valid tags from the API
|
|
25
|
+
const VALID_TAGS = [
|
|
26
|
+
"UI",
|
|
27
|
+
"Indicator",
|
|
28
|
+
"Order Entry",
|
|
29
|
+
"Trading",
|
|
30
|
+
"Chart",
|
|
31
|
+
"Portfolio",
|
|
32
|
+
"Analytics",
|
|
33
|
+
"Tool",
|
|
34
|
+
"Widget",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Regex patterns & limits matching backend createPluginSchema
|
|
38
|
+
const NPM_NAME_REGEX =
|
|
39
|
+
/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
40
|
+
const GITHUB_URL_REGEX = /^https:\/\/github\.com\/[\w-]+\/[\w-]+$/;
|
|
41
|
+
// pluginId: first char letter, then letters, digits, or hyphens
|
|
42
|
+
const PLUGIN_ID_REGEX = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
43
|
+
const UPLOADS_PATH_REGEX = /^\/uploads\/.+$/;
|
|
44
|
+
const MAX_TAGS = 5;
|
|
45
|
+
const MAX_COVER_IMAGES = 10;
|
|
46
|
+
const MAX_USAGE_PROMPT_LENGTH = 8192;
|
|
47
|
+
const SUBMIT_PROGRESS_TOTAL_STEPS = 7;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Print a consistent progress message so users can track submit lifecycle.
|
|
51
|
+
* @param {number} step
|
|
52
|
+
* @param {string} message
|
|
53
|
+
*/
|
|
54
|
+
function printProgress(step, message) {
|
|
55
|
+
info(`[${step}/${SUBMIT_PROGRESS_TOTAL_STEPS}] ${message}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Render actionable submit failure hints while preserving server message.
|
|
60
|
+
* @param {number} status
|
|
61
|
+
* @param {string | null} errorCode
|
|
62
|
+
* @param {string} serverMessage
|
|
63
|
+
*/
|
|
64
|
+
function printSubmitFailure(status, errorCode, serverMessage) {
|
|
65
|
+
const normalizedCode = (errorCode || "").toUpperCase();
|
|
66
|
+
const normalizedMessage = serverMessage || `HTTP ${status}`;
|
|
67
|
+
|
|
68
|
+
// Keep conflict guidance explicit since duplicate plugin IDs are common.
|
|
69
|
+
if (status === 409 || normalizedCode === "CONFLICT") {
|
|
70
|
+
error(`Plugin registration conflict: ${normalizedMessage}`);
|
|
71
|
+
info(
|
|
72
|
+
"Try a different pluginId in your manifest, then rerun 'orderly submit'.",
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Distinguish missing upstream resources from generic failures.
|
|
78
|
+
if (status === 404 || normalizedCode === "NOT_FOUND") {
|
|
79
|
+
error(`Resource not found: ${normalizedMessage}`);
|
|
80
|
+
info(
|
|
81
|
+
"Please verify npmName/repoUrl/pluginId in your manifest and try again.",
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Bad request means user input can be fixed without retrying infrastructure.
|
|
87
|
+
if (status === 400 || normalizedCode === "BAD_REQUEST") {
|
|
88
|
+
error(`Invalid submission data: ${normalizedMessage}`);
|
|
89
|
+
info("Please correct your manifest fields and run submit again.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (status === 401) {
|
|
94
|
+
error("Unauthorized. Please run 'orderly login' again.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (status >= 500) {
|
|
99
|
+
error(`Marketplace server error (HTTP ${status}): ${normalizedMessage}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
error(`Submission failed (HTTP ${status}): ${normalizedMessage}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate submission payload against backend schema rules.
|
|
108
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
109
|
+
*/
|
|
110
|
+
function validateSubmission({
|
|
111
|
+
npmName,
|
|
112
|
+
repoUrl,
|
|
113
|
+
pluginId,
|
|
114
|
+
tags,
|
|
115
|
+
coverImages,
|
|
116
|
+
usagePrompt,
|
|
117
|
+
}) {
|
|
118
|
+
const errors = [];
|
|
119
|
+
|
|
120
|
+
if (!npmName) {
|
|
121
|
+
errors.push("npmName is required (set in package.json)");
|
|
122
|
+
} else if (!NPM_NAME_REGEX.test(npmName)) {
|
|
123
|
+
errors.push(`npmName "${npmName}" is not a valid npm package name`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!repoUrl) {
|
|
127
|
+
errors.push(
|
|
128
|
+
"repoUrl is required (configure git remote or set in manifest)",
|
|
129
|
+
);
|
|
130
|
+
} else if (!GITHUB_URL_REGEX.test(repoUrl)) {
|
|
131
|
+
errors.push(
|
|
132
|
+
`repoUrl must be a valid GitHub URL (https://github.com/<owner>/<repo>), got: ${repoUrl}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!pluginId) {
|
|
137
|
+
errors.push("pluginId is required (set in manifest or pass interactively)");
|
|
138
|
+
} else if (!PLUGIN_ID_REGEX.test(pluginId)) {
|
|
139
|
+
errors.push(
|
|
140
|
+
`pluginId must start with a letter and contain only letters, digits, or hyphens, got: ${pluginId}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (tags.length > MAX_TAGS) {
|
|
145
|
+
errors.push(`Too many tags (${tags.length}), maximum is ${MAX_TAGS}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (coverImages && coverImages.length > MAX_COVER_IMAGES) {
|
|
149
|
+
errors.push(
|
|
150
|
+
`Too many cover images (${coverImages.length}), maximum is ${MAX_COVER_IMAGES}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (coverImages && coverImages.length > 0) {
|
|
154
|
+
const invalidCoverImage = coverImages.find((image) => {
|
|
155
|
+
if (typeof image !== "string" || image.length === 0) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Keep parity with backend schema: each item must be a URL or /uploads/* path.
|
|
160
|
+
return !URL.canParse(image) && !UPLOADS_PATH_REGEX.test(image);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (invalidCoverImage) {
|
|
164
|
+
errors.push(
|
|
165
|
+
`coverImages contains an invalid value: ${invalidCoverImage}. Each value must be an absolute URL or a path that starts with /uploads/`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (usagePrompt && usagePrompt.length > MAX_USAGE_PROMPT_LENGTH) {
|
|
171
|
+
errors.push(
|
|
172
|
+
`usagePrompt is too long (${usagePrompt.length} chars), maximum is ${MAX_USAGE_PROMPT_LENGTH}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { valid: errors.length === 0, errors };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
command: "submit",
|
|
181
|
+
describe: "Submit a plugin to Orderly Marketplace",
|
|
182
|
+
builder: (yargs) => {
|
|
183
|
+
return yargs
|
|
184
|
+
.option("path", {
|
|
185
|
+
alias: "p",
|
|
186
|
+
type: "string",
|
|
187
|
+
describe:
|
|
188
|
+
"string; path to the plugin directory. If omitted, you'll be prompted (default: ./)",
|
|
189
|
+
demandOption: false,
|
|
190
|
+
})
|
|
191
|
+
.option("tags", {
|
|
192
|
+
alias: "t",
|
|
193
|
+
type: "string",
|
|
194
|
+
describe:
|
|
195
|
+
"string; comma-separated tag list (e.g., 'UI,Trading'). Values are trimmed; invalid tags are ignored with a warning",
|
|
196
|
+
demandOption: false,
|
|
197
|
+
})
|
|
198
|
+
.option("storybook-url", {
|
|
199
|
+
type: "string",
|
|
200
|
+
describe:
|
|
201
|
+
"string; optional Storybook base URL (for the plugin). Overrides manifest.storybookUrl when provided",
|
|
202
|
+
demandOption: false,
|
|
203
|
+
})
|
|
204
|
+
.option("dry-run", {
|
|
205
|
+
alias: "d",
|
|
206
|
+
type: "boolean",
|
|
207
|
+
describe:
|
|
208
|
+
"boolean; validate the submission payload and print it without calling the marketplace API",
|
|
209
|
+
default: false,
|
|
210
|
+
})
|
|
211
|
+
.example(
|
|
212
|
+
"orderly submit --path ./my-plugin --dry-run",
|
|
213
|
+
"Validate the plugin payload from a local folder",
|
|
214
|
+
)
|
|
215
|
+
.example(
|
|
216
|
+
"orderly submit --path ./my-plugin --tags UI,Trading --storybook-url https://example.com/storybook",
|
|
217
|
+
"Submit with tags and a Storybook URL",
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
handler: async (argv) => {
|
|
221
|
+
heading("Submit to Orderly Marketplace");
|
|
222
|
+
info("This command will submit your plugin to the marketplace.\n");
|
|
223
|
+
printProgress(1, "Checking authentication status...");
|
|
224
|
+
|
|
225
|
+
// Check if user is logged in
|
|
226
|
+
if (!isLoggedIn()) {
|
|
227
|
+
warn("You are not logged in.");
|
|
228
|
+
info("Please run 'orderly login' first to authenticate.");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const token = getToken();
|
|
233
|
+
info(`Authenticated as: (token starts with ${token.substring(0, 8)}...)\n`);
|
|
234
|
+
|
|
235
|
+
// Step 1: Path
|
|
236
|
+
printProgress(2, "Resolving plugin path...");
|
|
237
|
+
const targetPath = argv.path || (await input("Path to plugin:", "./"));
|
|
238
|
+
const resolvedPath = path.resolve(targetPath);
|
|
239
|
+
|
|
240
|
+
// Step 2: Resolve metadata (.orderly-manifest.json optional; package.json + git is enough)
|
|
241
|
+
printProgress(3, `Reading plugin metadata from ${resolvedPath}...`);
|
|
242
|
+
|
|
243
|
+
const manifest = resolvePluginManifest(resolvedPath);
|
|
244
|
+
if (!manifest) {
|
|
245
|
+
error("No plugin metadata found.");
|
|
246
|
+
info(
|
|
247
|
+
"Add a package.json with a valid \"name\" field, or create .orderly-manifest.json (e.g. via 'orderly create plugin').",
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Step 3: Try to auto-fill repoUrl from git remote if missing
|
|
253
|
+
if (!manifest.repoUrl) {
|
|
254
|
+
const repoUrl = getRepoUrl();
|
|
255
|
+
if (repoUrl) {
|
|
256
|
+
info("Found repo URL from git remote, adding to manifest...");
|
|
257
|
+
manifest.repoUrl = repoUrl;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
printProgress(4, "Preparing submission fields...");
|
|
262
|
+
if (!manifest.pluginId) {
|
|
263
|
+
manifest.pluginId = await input("Plugin ID (required):");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
info(`Package: ${manifest.npmName}`);
|
|
267
|
+
if (manifest.pluginId) {
|
|
268
|
+
info(`Plugin ID: ${manifest.pluginId}`);
|
|
269
|
+
}
|
|
270
|
+
if (manifest.repoUrl) {
|
|
271
|
+
info(`Repository: ${manifest.repoUrl}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Step 4: Collect optional fields
|
|
275
|
+
let tags = manifest.tags || [];
|
|
276
|
+
if (argv.tags) {
|
|
277
|
+
tags = argv.tags.split(",").map((t) => t.trim());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validate tag values
|
|
281
|
+
const invalidTags = tags.filter((t) => !VALID_TAGS.includes(t));
|
|
282
|
+
if (invalidTags.length > 0) {
|
|
283
|
+
warn(`Invalid tags: ${invalidTags.join(", ")}`);
|
|
284
|
+
info(`Valid tags: ${VALID_TAGS.join(", ")}`);
|
|
285
|
+
tags = tags.filter((t) => VALID_TAGS.includes(t));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const storybookUrl = argv.storybookUrl || manifest.storybookUrl || null;
|
|
289
|
+
const storybookTooltip = manifest.storybookTooltip || null;
|
|
290
|
+
const usagePrompt = manifest.usagePrompt || null;
|
|
291
|
+
const coverImages = manifest.coverImages || [];
|
|
292
|
+
|
|
293
|
+
// Step 5: Validate all fields against backend schema
|
|
294
|
+
printProgress(5, "Validating submission payload...");
|
|
295
|
+
const submission = validateSubmission({
|
|
296
|
+
npmName: manifest.npmName,
|
|
297
|
+
repoUrl: manifest.repoUrl,
|
|
298
|
+
pluginId: manifest.pluginId,
|
|
299
|
+
tags,
|
|
300
|
+
coverImages,
|
|
301
|
+
usagePrompt,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!submission.valid) {
|
|
305
|
+
error("\nValidation failed. Please fix the following issues:");
|
|
306
|
+
submission.errors.forEach((e) => info(` - ${e}`));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (argv["dry-run"]) {
|
|
311
|
+
success("\nDry-run completed! Plugin is valid for submission.");
|
|
312
|
+
info(`\nSubmission payload:`);
|
|
313
|
+
console.log(
|
|
314
|
+
JSON.stringify(
|
|
315
|
+
{
|
|
316
|
+
npmName: manifest.npmName,
|
|
317
|
+
repoUrl: manifest.repoUrl,
|
|
318
|
+
pluginId: manifest.pluginId,
|
|
319
|
+
tags,
|
|
320
|
+
coverImages,
|
|
321
|
+
storybookUrl,
|
|
322
|
+
storybookTooltip,
|
|
323
|
+
usagePrompt,
|
|
324
|
+
},
|
|
325
|
+
null,
|
|
326
|
+
2,
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Step 6: Submit to marketplace
|
|
333
|
+
printProgress(
|
|
334
|
+
6,
|
|
335
|
+
`Submitting request to Orderly Marketplace (${MARKETPLACE_API_PLUGINS_URL})...`,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const payload = {
|
|
339
|
+
npmName: manifest.npmName,
|
|
340
|
+
repoUrl: manifest.repoUrl,
|
|
341
|
+
pluginId: manifest.pluginId,
|
|
342
|
+
tags,
|
|
343
|
+
coverImages,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Add optional fields if present
|
|
347
|
+
if (storybookUrl) {
|
|
348
|
+
payload.storybookUrl = storybookUrl;
|
|
349
|
+
}
|
|
350
|
+
if (storybookTooltip) {
|
|
351
|
+
payload.storybookTooltip = storybookTooltip;
|
|
352
|
+
}
|
|
353
|
+
if (usagePrompt) {
|
|
354
|
+
payload.usagePrompt = usagePrompt;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const response = await authenticatedFetch(
|
|
359
|
+
MARKETPLACE_API_PLUGINS_URL,
|
|
360
|
+
{
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: {
|
|
363
|
+
"Content-Type": "application/json",
|
|
364
|
+
Authorization: `Bearer ${token}`,
|
|
365
|
+
},
|
|
366
|
+
body: JSON.stringify(payload),
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
// Expose auth refresh lifecycle so users can see why step [6/7] is taking longer.
|
|
370
|
+
onAuthEvent: (event, details = {}) => {
|
|
371
|
+
if (event === "request_unauthorized") {
|
|
372
|
+
info(
|
|
373
|
+
"[6/7] Access token expired (HTTP 401), attempting token refresh...",
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (event === "refresh_started") {
|
|
379
|
+
info(
|
|
380
|
+
`[6/7] Refreshing CLI token via ${details.url || "refresh endpoint"}...`,
|
|
381
|
+
);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (event === "refresh_succeeded") {
|
|
386
|
+
info("[6/7] Token refresh succeeded, retrying submit request...");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (event === "refresh_missing") {
|
|
391
|
+
warn(
|
|
392
|
+
"[6/7] No refresh token found. Please run 'orderly login' again if submit fails.",
|
|
393
|
+
);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (event === "refresh_failed") {
|
|
398
|
+
warn(
|
|
399
|
+
`[6/7] Token refresh failed (HTTP ${details.status || "unknown"}).`,
|
|
400
|
+
);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (event === "refresh_error") {
|
|
405
|
+
warn(
|
|
406
|
+
`[6/7] Token refresh request error: ${details.message || "unknown error"}`,
|
|
407
|
+
);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (event === "request_retry_started") {
|
|
412
|
+
info(
|
|
413
|
+
"[6/7] Sending retried submit request with refreshed token...",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
printProgress(
|
|
421
|
+
7,
|
|
422
|
+
`Received server response (HTTP ${response.status}), processing...`,
|
|
423
|
+
);
|
|
424
|
+
const responseData = await response.json().catch(() => ({}));
|
|
425
|
+
|
|
426
|
+
if (response.status === 201) {
|
|
427
|
+
success("\nSubmission successful!");
|
|
428
|
+
info(`Plugin ID: ${responseData.id || "N/A"}`);
|
|
429
|
+
info(`NPM Name: ${responseData.npmName || manifest.npmName}`);
|
|
430
|
+
info(`Status: ${responseData.status || "under_review"}`);
|
|
431
|
+
maybePrintOrderlyDevEnvironmentHints(resolvedPath);
|
|
432
|
+
} else {
|
|
433
|
+
const { code: errorCode, message: errorMessage } = getApiErrorInfo(
|
|
434
|
+
responseData,
|
|
435
|
+
response.status,
|
|
436
|
+
);
|
|
437
|
+
printSubmitFailure(response.status, errorCode, errorMessage);
|
|
438
|
+
|
|
439
|
+
if (responseData.details) {
|
|
440
|
+
info("Details:");
|
|
441
|
+
console.log(JSON.stringify(responseData.details, null, 2));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (errorCode) {
|
|
445
|
+
info(`Error code: ${errorCode}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch (e) {
|
|
449
|
+
// Include endpoint context so publish users can diagnose non-local failures.
|
|
450
|
+
const cause = e?.message || String(e);
|
|
451
|
+
error(
|
|
452
|
+
`Submission failed while calling ${MARKETPLACE_API_PLUGINS_URL}: ${cause}`,
|
|
453
|
+
);
|
|
454
|
+
info("Please verify network connectivity and API availability.");
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const {
|
|
3
|
+
heading,
|
|
4
|
+
info,
|
|
5
|
+
success,
|
|
6
|
+
warn,
|
|
7
|
+
error,
|
|
8
|
+
input,
|
|
9
|
+
getErrorMessage,
|
|
10
|
+
} = require("../shared");
|
|
11
|
+
const {
|
|
12
|
+
isLoggedIn,
|
|
13
|
+
getToken,
|
|
14
|
+
authenticatedFetch,
|
|
15
|
+
} = require("../internal/auth");
|
|
16
|
+
const { MARKETPLACE_API_PLUGINS_URL } = require("../internal/constants");
|
|
17
|
+
const { resolvePluginManifest } = require("../internal/manifest");
|
|
18
|
+
|
|
19
|
+
// Keep the same tag whitelist as submit to ensure local validation stays consistent.
|
|
20
|
+
const VALID_TAGS = [
|
|
21
|
+
"UI",
|
|
22
|
+
"Indicator",
|
|
23
|
+
"Order Entry",
|
|
24
|
+
"Trading",
|
|
25
|
+
"Chart",
|
|
26
|
+
"Portfolio",
|
|
27
|
+
"Analytics",
|
|
28
|
+
"Tool",
|
|
29
|
+
"Widget",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const MAX_TAGS = 5;
|
|
33
|
+
const MAX_COVER_IMAGES = 10;
|
|
34
|
+
const MAX_USAGE_PROMPT_LENGTH = 8192;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate update payload against marketplace schema constraints.
|
|
38
|
+
* @param {object} payload
|
|
39
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
40
|
+
*/
|
|
41
|
+
function validateUpdatePayload(payload) {
|
|
42
|
+
const errors = [];
|
|
43
|
+
|
|
44
|
+
if (payload.tags && payload.tags.length > MAX_TAGS) {
|
|
45
|
+
errors.push(
|
|
46
|
+
`Too many tags (${payload.tags.length}), maximum is ${MAX_TAGS}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (payload.coverImages && payload.coverImages.length > MAX_COVER_IMAGES) {
|
|
51
|
+
errors.push(
|
|
52
|
+
`Too many cover images (${payload.coverImages.length}), maximum is ${MAX_COVER_IMAGES}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
payload.usagePrompt &&
|
|
58
|
+
payload.usagePrompt.length > MAX_USAGE_PROMPT_LENGTH
|
|
59
|
+
) {
|
|
60
|
+
errors.push(
|
|
61
|
+
`usagePrompt is too long (${payload.usagePrompt.length} chars), maximum is ${MAX_USAGE_PROMPT_LENGTH}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { valid: errors.length === 0, errors };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Include only user-editable fields supported by PATCH /plugins/{id}.
|
|
70
|
+
* Undefined/null/empty values are skipped to avoid accidental field clearing.
|
|
71
|
+
* @param {object} manifest
|
|
72
|
+
* @returns {object}
|
|
73
|
+
*/
|
|
74
|
+
function buildUpdatePayload(manifest) {
|
|
75
|
+
const payload = {};
|
|
76
|
+
|
|
77
|
+
const directFields = [
|
|
78
|
+
"name",
|
|
79
|
+
"description",
|
|
80
|
+
"coverImages",
|
|
81
|
+
"storybookUrl",
|
|
82
|
+
"storybookTooltip",
|
|
83
|
+
"usagePrompt",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
directFields.forEach((field) => {
|
|
87
|
+
const value = manifest[field];
|
|
88
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
89
|
+
payload[field] = value;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const tags = Array.isArray(manifest.tags) ? manifest.tags : [];
|
|
94
|
+
if (tags.length > 0) {
|
|
95
|
+
// Filter unexpected values locally so users get clear feedback before request.
|
|
96
|
+
const validTags = tags.filter((tag) => VALID_TAGS.includes(tag));
|
|
97
|
+
const invalidTags = tags.filter((tag) => !VALID_TAGS.includes(tag));
|
|
98
|
+
|
|
99
|
+
if (invalidTags.length > 0) {
|
|
100
|
+
warn(`Ignored invalid tags from manifest: ${invalidTags.join(", ")}`);
|
|
101
|
+
info(`Valid tags: ${VALID_TAGS.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (validTags.length > 0) {
|
|
105
|
+
payload.tags = validTags;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
command: "update",
|
|
114
|
+
describe: "Update plugin metadata in Orderly Marketplace",
|
|
115
|
+
builder: (yargs) => {
|
|
116
|
+
return yargs
|
|
117
|
+
.option("path", {
|
|
118
|
+
alias: "p",
|
|
119
|
+
type: "string",
|
|
120
|
+
describe:
|
|
121
|
+
"string; path to the plugin directory (must contain .orderly-manifest.json with a pluginId, or package.json-derived metadata). If omitted, you will be prompted",
|
|
122
|
+
demandOption: false,
|
|
123
|
+
})
|
|
124
|
+
.option("dry-run", {
|
|
125
|
+
alias: "d",
|
|
126
|
+
type: "boolean",
|
|
127
|
+
describe:
|
|
128
|
+
"boolean; validate and print the PATCH payload without calling the marketplace API",
|
|
129
|
+
default: false,
|
|
130
|
+
})
|
|
131
|
+
.example(
|
|
132
|
+
"orderly update --path ./my-plugin --dry-run",
|
|
133
|
+
"Validate the update payload from a local plugin folder",
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
handler: async (argv) => {
|
|
137
|
+
heading("Update plugin on Orderly Marketplace");
|
|
138
|
+
|
|
139
|
+
if (!isLoggedIn()) {
|
|
140
|
+
warn("You are not logged in.");
|
|
141
|
+
info("Please run 'orderly login' first to authenticate.");
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const token = getToken();
|
|
147
|
+
if (!token) {
|
|
148
|
+
error("Authentication token not found.");
|
|
149
|
+
info("Please run 'orderly login' again.");
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const targetPath = argv.path || (await input("Path to plugin:", "./"));
|
|
155
|
+
const resolvedPath = path.resolve(targetPath);
|
|
156
|
+
info(`Reading plugin metadata from ${resolvedPath}...`);
|
|
157
|
+
|
|
158
|
+
// Resolve metadata with manifest-first behavior for plugin projects.
|
|
159
|
+
const manifest = resolvePluginManifest(resolvedPath);
|
|
160
|
+
if (!manifest) {
|
|
161
|
+
error("No plugin metadata found.");
|
|
162
|
+
info(
|
|
163
|
+
"Please ensure .orderly-manifest.json exists in your plugin project and contains pluginId.",
|
|
164
|
+
);
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const pluginId = String(manifest.pluginId || "").trim();
|
|
170
|
+
if (!pluginId) {
|
|
171
|
+
error("pluginId is required in .orderly-manifest.json for update.");
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const payload = buildUpdatePayload(manifest);
|
|
177
|
+
const validation = validateUpdatePayload(payload);
|
|
178
|
+
|
|
179
|
+
if (!validation.valid) {
|
|
180
|
+
error("Validation failed. Please fix the following issues:");
|
|
181
|
+
validation.errors.forEach((validationError) =>
|
|
182
|
+
info(` - ${validationError}`),
|
|
183
|
+
);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (Object.keys(payload).length === 0) {
|
|
189
|
+
warn("No updatable fields found in manifest.");
|
|
190
|
+
info(
|
|
191
|
+
"Supported fields: name, description, tags, coverImages, storybookUrl, storybookTooltip, usagePrompt.",
|
|
192
|
+
);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const apiUrl = `${MARKETPLACE_API_PLUGINS_URL}/${encodeURIComponent(pluginId)}`;
|
|
198
|
+
|
|
199
|
+
if (argv["dry-run"]) {
|
|
200
|
+
success("Dry-run completed. Update payload is valid.");
|
|
201
|
+
info("PATCH target:");
|
|
202
|
+
console.log(apiUrl);
|
|
203
|
+
info("\nPayload:");
|
|
204
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
info("Submitting update request...");
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const response = await authenticatedFetch(apiUrl, {
|
|
212
|
+
method: "PATCH",
|
|
213
|
+
headers: {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
Authorization: `Bearer ${token}`,
|
|
216
|
+
},
|
|
217
|
+
body: JSON.stringify(payload),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const responseData = await response.json().catch(() => null);
|
|
221
|
+
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
const serverMessage = getErrorMessage(responseData, response.status);
|
|
224
|
+
error(`Update failed: ${serverMessage}`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
success("Plugin updated successfully!");
|
|
230
|
+
info(`Plugin ID: ${responseData?.id || pluginId}`);
|
|
231
|
+
info(`Status: ${responseData?.status || "N/A"}`);
|
|
232
|
+
} catch (requestError) {
|
|
233
|
+
// Include request target so operators can triage connectivity issues.
|
|
234
|
+
const cause = requestError?.message || String(requestError);
|
|
235
|
+
error(`Request failed while calling ${apiUrl}: ${cause}`);
|
|
236
|
+
info("Please verify network connectivity and API availability.");
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
};
|