@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,76 @@
|
|
|
1
|
+
const { heading, info, error, getErrorMessage } = require("../shared");
|
|
2
|
+
const { MARKETPLACE_API_BASE_URL } = require("../internal/constants");
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
command: "view <id>",
|
|
6
|
+
describe: "View plugin details by ID from Marketplace",
|
|
7
|
+
builder: (yargs) => {
|
|
8
|
+
return yargs
|
|
9
|
+
.positional("id", {
|
|
10
|
+
type: "string",
|
|
11
|
+
describe:
|
|
12
|
+
"string; plugin ID used to fetch details from Marketplace (required)",
|
|
13
|
+
demandOption: true,
|
|
14
|
+
})
|
|
15
|
+
.option("json", {
|
|
16
|
+
type: "boolean",
|
|
17
|
+
describe:
|
|
18
|
+
"boolean; currently does not change output (the command always prints the full JSON payload)",
|
|
19
|
+
default: false,
|
|
20
|
+
})
|
|
21
|
+
.example(
|
|
22
|
+
"orderly view trading-plugin-id",
|
|
23
|
+
"Fetch and print plugin details",
|
|
24
|
+
)
|
|
25
|
+
.example(
|
|
26
|
+
"orderly view trading-plugin-id --json",
|
|
27
|
+
"Fetch and print plugin details as JSON (flag currently does not alter output)",
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
handler: async (argv) => {
|
|
31
|
+
heading("Marketplace Plugin Details");
|
|
32
|
+
|
|
33
|
+
const pluginId = String(argv.id || "").trim();
|
|
34
|
+
if (!pluginId) {
|
|
35
|
+
error("Plugin ID is required.");
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const url = `${MARKETPLACE_API_BASE_URL}/plugins/${encodeURIComponent(pluginId)}`;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Use explicit Accept header for consistent JSON responses across API gateways.
|
|
44
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
45
|
+
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method: "GET",
|
|
48
|
+
headers,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const responseData = await response.json().catch(() => null);
|
|
52
|
+
|
|
53
|
+
if (response.status === 404) {
|
|
54
|
+
error(`Plugin not found: ${pluginId}`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const serverMessage = getErrorMessage(responseData, response.status);
|
|
61
|
+
error(`Failed to fetch plugin: ${serverMessage}`);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Always print full payload to avoid losing fields in formatted output.
|
|
67
|
+
console.log(JSON.stringify(responseData ?? {}, null, 2));
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Show the exact request target so operators can debug network/env issues quickly.
|
|
70
|
+
const cause = e?.message || String(e);
|
|
71
|
+
error(`Request failed while calling ${url}: ${cause}`);
|
|
72
|
+
info("Please verify network connectivity and API availability.");
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { heading, info, success, warn } = require("../shared");
|
|
2
|
+
const { getEmail, isLoggedIn } = require("../internal/auth");
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
command: "whoami",
|
|
6
|
+
describe: "Display current logged in user",
|
|
7
|
+
handler: async () => {
|
|
8
|
+
heading("Current User");
|
|
9
|
+
|
|
10
|
+
if (!isLoggedIn()) {
|
|
11
|
+
warn("You are not logged in.");
|
|
12
|
+
info("Run 'orderly login' to authenticate.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const email = getEmail();
|
|
17
|
+
success(`Logged in as: ${email}`);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { MARKETPLACE_API_BASE_URL } = require("./constants");
|
|
4
|
+
|
|
5
|
+
const AUTH_DIR = path.join(
|
|
6
|
+
process.env.HOME || process.env.USERPROFILE,
|
|
7
|
+
".orderly",
|
|
8
|
+
);
|
|
9
|
+
const AUTH_FILE = path.join(AUTH_DIR, "auth.json");
|
|
10
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 15000;
|
|
11
|
+
const HTTP_TIMEOUT_MS = Number.parseInt(
|
|
12
|
+
process.env.ORDERLY_HTTP_TIMEOUT_MS || `${DEFAULT_HTTP_TIMEOUT_MS}`,
|
|
13
|
+
10,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
function ensureAuthDir() {
|
|
17
|
+
if (!fs.existsSync(AUTH_DIR)) {
|
|
18
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readAuth() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Ignore errors, return empty
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeAuth(data) {
|
|
34
|
+
ensureAuthDir();
|
|
35
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function saveToken(token, email = null) {
|
|
39
|
+
writeAuth({ token, email });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveOAuthToken(tokenData) {
|
|
43
|
+
// Use email if available, otherwise use username
|
|
44
|
+
writeAuth({
|
|
45
|
+
token: tokenData.token,
|
|
46
|
+
refreshToken: tokenData.refreshToken || null,
|
|
47
|
+
email: tokenData.email || null,
|
|
48
|
+
username: tokenData.username || null,
|
|
49
|
+
avatarUrl: tokenData.avatarUrl || null,
|
|
50
|
+
loginMethod: "github",
|
|
51
|
+
loginAt: new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getToken() {
|
|
56
|
+
const auth = readAuth();
|
|
57
|
+
return auth?.token || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getEmail() {
|
|
61
|
+
const auth = readAuth();
|
|
62
|
+
// Prefer email, fall back to username
|
|
63
|
+
return auth?.email || auth?.username || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getRefreshToken() {
|
|
67
|
+
const auth = readAuth();
|
|
68
|
+
return auth?.refreshToken || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a deterministic timeout budget for CLI HTTP calls.
|
|
73
|
+
* @returns {number}
|
|
74
|
+
*/
|
|
75
|
+
function getHttpTimeoutMs() {
|
|
76
|
+
if (Number.isFinite(HTTP_TIMEOUT_MS) && HTTP_TIMEOUT_MS > 0) {
|
|
77
|
+
return HTTP_TIMEOUT_MS;
|
|
78
|
+
}
|
|
79
|
+
return DEFAULT_HTTP_TIMEOUT_MS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Execute fetch with timeout so CLI does not wait forever on stalled network.
|
|
84
|
+
* @param {string} url
|
|
85
|
+
* @param {RequestInit} init
|
|
86
|
+
* @param {number} timeoutMs
|
|
87
|
+
* @returns {Promise<Response>}
|
|
88
|
+
*/
|
|
89
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const timeoutId = setTimeout(() => {
|
|
92
|
+
controller.abort(`Request timed out after ${timeoutMs}ms`);
|
|
93
|
+
}, timeoutMs);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
return await fetch(url, {
|
|
97
|
+
...init,
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timeoutId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Rotate CLI tokens using refresh token and persist new credentials.
|
|
107
|
+
* @param {{ onAuthEvent?: (event: string, details?: Record<string, unknown>) => void }} options
|
|
108
|
+
*/
|
|
109
|
+
async function refreshCliToken(options = {}) {
|
|
110
|
+
const { onAuthEvent } = options;
|
|
111
|
+
const auth = readAuth();
|
|
112
|
+
const refreshToken = auth?.refreshToken;
|
|
113
|
+
if (!refreshToken) {
|
|
114
|
+
onAuthEvent?.("refresh_missing");
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const refreshUrl = `${MARKETPLACE_API_BASE_URL}/auth/refresh-cli`;
|
|
120
|
+
onAuthEvent?.("refresh_started", { url: refreshUrl });
|
|
121
|
+
const response = await fetchWithTimeout(
|
|
122
|
+
refreshUrl,
|
|
123
|
+
{
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
Accept: "application/json",
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({ refreshToken }),
|
|
130
|
+
},
|
|
131
|
+
getHttpTimeoutMs(),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
onAuthEvent?.("refresh_failed", { status: response.status });
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const data = await response.json();
|
|
140
|
+
if (!data?.accessToken || !data?.refreshToken) {
|
|
141
|
+
onAuthEvent?.("refresh_invalid_payload");
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
writeAuth({
|
|
146
|
+
...auth,
|
|
147
|
+
token: data.accessToken,
|
|
148
|
+
refreshToken: data.refreshToken,
|
|
149
|
+
refreshedAt: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
onAuthEvent?.("refresh_succeeded");
|
|
152
|
+
return data.accessToken;
|
|
153
|
+
} catch (e) {
|
|
154
|
+
onAuthEvent?.("refresh_error", {
|
|
155
|
+
message: e?.message || String(e),
|
|
156
|
+
});
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Send authenticated request and retry once after token refresh on 401.
|
|
163
|
+
* @param {{ onAuthEvent?: (event: string, details?: Record<string, unknown>) => void }} options
|
|
164
|
+
*/
|
|
165
|
+
async function authenticatedFetch(url, init = {}, options = {}) {
|
|
166
|
+
const { onAuthEvent } = options;
|
|
167
|
+
const token = getToken();
|
|
168
|
+
const timeoutMs = getHttpTimeoutMs();
|
|
169
|
+
if (!token) {
|
|
170
|
+
return fetchWithTimeout(url, init, timeoutMs);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const firstHeaders = new Headers(init.headers || {});
|
|
174
|
+
firstHeaders.set("Authorization", `Bearer ${token}`);
|
|
175
|
+
let response = await fetchWithTimeout(
|
|
176
|
+
url,
|
|
177
|
+
{ ...init, headers: firstHeaders },
|
|
178
|
+
timeoutMs,
|
|
179
|
+
);
|
|
180
|
+
if (response.status !== 401) {
|
|
181
|
+
return response;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
onAuthEvent?.("request_unauthorized", { status: response.status });
|
|
185
|
+
const nextToken = await refreshCliToken({ onAuthEvent });
|
|
186
|
+
if (!nextToken) {
|
|
187
|
+
return response;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const retryHeaders = new Headers(init.headers || {});
|
|
191
|
+
retryHeaders.set("Authorization", `Bearer ${nextToken}`);
|
|
192
|
+
onAuthEvent?.("request_retry_started");
|
|
193
|
+
response = await fetchWithTimeout(
|
|
194
|
+
url,
|
|
195
|
+
{ ...init, headers: retryHeaders },
|
|
196
|
+
timeoutMs,
|
|
197
|
+
);
|
|
198
|
+
return response;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isLoggedIn() {
|
|
202
|
+
const token = getToken();
|
|
203
|
+
return !!token;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function logout() {
|
|
207
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
208
|
+
fs.unlinkSync(AUTH_FILE);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
saveToken,
|
|
214
|
+
saveOAuthToken,
|
|
215
|
+
getToken,
|
|
216
|
+
getEmail,
|
|
217
|
+
getRefreshToken,
|
|
218
|
+
isLoggedIn,
|
|
219
|
+
logout,
|
|
220
|
+
authenticatedFetch,
|
|
221
|
+
refreshCliToken,
|
|
222
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Known interceptor targets from the plugin system
|
|
2
|
+
const INTERCEPTOR_TARGETS = [
|
|
3
|
+
"Trading.Layout.Desktop",
|
|
4
|
+
"Trading.Layout.Mobile",
|
|
5
|
+
"Trading.OrderEntry.TypeTabs",
|
|
6
|
+
"Trading.OrderEntry.BuySellSwitch",
|
|
7
|
+
"Trading.OrderEntry.Available",
|
|
8
|
+
"Trading.OrderEntry.QuantitySlider",
|
|
9
|
+
"Trading.OrderEntry.SubmitSection",
|
|
10
|
+
"OrderBook.Desktop.Asks",
|
|
11
|
+
"OrderBook.Desktop.Bids",
|
|
12
|
+
"Deposit.DepositForm",
|
|
13
|
+
"Deposit.WithdrawForm",
|
|
14
|
+
"Account.AccountMenu",
|
|
15
|
+
"Layout.MainMenus",
|
|
16
|
+
"Table.EmptyDataIdentifier",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Module types supported
|
|
20
|
+
const MODULE_TYPES = ["page", "component", "hook", "utils", "module"];
|
|
21
|
+
|
|
22
|
+
const MARKETPLACE_API_BASE_URL =
|
|
23
|
+
process.env.ORDERLY_API_URL ||
|
|
24
|
+
"https://orderly-plugin-marketplace-server.vercel.app";
|
|
25
|
+
const MARKETPLACE_WEB_BASE_URL =
|
|
26
|
+
process.env.ORDERLY_WEB_URL ||
|
|
27
|
+
"https://orderly-plugin-marketplace.vercel.app";
|
|
28
|
+
|
|
29
|
+
const MARKETPLACE_API_PLUGINS_URL = `${MARKETPLACE_API_BASE_URL}/plugins`;
|
|
30
|
+
const MARKETPLACE_API_MY_PLUGINS_URL = `${MARKETPLACE_API_BASE_URL}/my-plugins`;
|
|
31
|
+
/**
|
|
32
|
+
* Build plugin self-status endpoint for author-owned status updates.
|
|
33
|
+
* @param {string} pluginId
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function getMarketplaceApiPluginSelfStatusUrl(pluginId) {
|
|
37
|
+
return `${MARKETPLACE_API_PLUGINS_URL}/${encodeURIComponent(pluginId)}/self-status`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build plugin detail endpoint for plugin-level operations.
|
|
41
|
+
* @param {string} pluginId
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function getMarketplaceApiPluginUrl(pluginId) {
|
|
45
|
+
return `${MARKETPLACE_API_PLUGINS_URL}/${encodeURIComponent(pluginId)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CLI_CALLBACK_PORT = 9876;
|
|
49
|
+
const CLI_LOGIN_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
|
|
50
|
+
const MARKETPLACE_WEB_LOGIN_URL = `${MARKETPLACE_WEB_BASE_URL}/cli/login`;
|
|
51
|
+
|
|
52
|
+
/** GitHub shorthand for https://github.com/OrderlyNetwork/orderly-skills (Vercel skills CLI). */
|
|
53
|
+
const ORDERLY_SKILLS_REPO = "OrderlyNetwork/orderly-skills";
|
|
54
|
+
|
|
55
|
+
/** Default skill IDs from orderly-skills README (install all four non-interactively). */
|
|
56
|
+
const ORDERLY_PLUGIN_SKILL_NAMES = [
|
|
57
|
+
"orderly-plugin-create",
|
|
58
|
+
"orderly-plugin-write",
|
|
59
|
+
"orderly-plugin-add",
|
|
60
|
+
"orderly-plugin-submit",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** Default MCP server key in mcpServers (matches @orderly.network/sdk-docs install --name default). */
|
|
64
|
+
const DEFAULT_ORDERLY_SDK_DOCS_MCP_NAME = "orderly-sdk-docs";
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
INTERCEPTOR_TARGETS,
|
|
68
|
+
MODULE_TYPES,
|
|
69
|
+
MARKETPLACE_API_BASE_URL,
|
|
70
|
+
MARKETPLACE_API_PLUGINS_URL,
|
|
71
|
+
MARKETPLACE_API_MY_PLUGINS_URL,
|
|
72
|
+
getMarketplaceApiPluginUrl,
|
|
73
|
+
getMarketplaceApiPluginSelfStatusUrl,
|
|
74
|
+
CLI_CALLBACK_PORT,
|
|
75
|
+
CLI_LOGIN_TIMEOUT_MS,
|
|
76
|
+
MARKETPLACE_WEB_LOGIN_URL,
|
|
77
|
+
ORDERLY_SKILLS_REPO,
|
|
78
|
+
ORDERLY_PLUGIN_SKILL_NAMES,
|
|
79
|
+
DEFAULT_ORDERLY_SDK_DOCS_MCP_NAME,
|
|
80
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const { MARKETPLACE_WEB_BASE_URL } = require("./constants");
|
|
3
|
+
|
|
4
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
5
|
+
<html>
|
|
6
|
+
<head><meta charset="utf-8"><title>Login Successful</title></head>
|
|
7
|
+
<body style="display:flex;justify-content:center;align-items:center;height:100vh;margin:0;font-family:system-ui,sans-serif;background:#0f172a;color:#e2e8f0">
|
|
8
|
+
<div style="text-align:center">
|
|
9
|
+
<div style="font-size:48px;margin-bottom:16px">✓</div>
|
|
10
|
+
<h1 style="margin:0 0 8px">Login Successful!</h1>
|
|
11
|
+
<p style="color:#94a3b8">You can close this tab and return to the CLI.</p>
|
|
12
|
+
</div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>`;
|
|
15
|
+
|
|
16
|
+
const ERROR_HTML = (msg) => `<!DOCTYPE html>
|
|
17
|
+
<html>
|
|
18
|
+
<head><meta charset="utf-8"><title>Login Failed</title></head>
|
|
19
|
+
<body style="display:flex;justify-content:center;align-items:center;height:100vh;margin:0;font-family:system-ui,sans-serif;background:#0f172a;color:#e2e8f0">
|
|
20
|
+
<div style="text-align:center">
|
|
21
|
+
<div style="font-size:48px;margin-bottom:16px">✗</div>
|
|
22
|
+
<h1 style="margin:0 0 8px">Login Failed</h1>
|
|
23
|
+
<p style="color:#f87171">${msg}</p>
|
|
24
|
+
</div>
|
|
25
|
+
</body>
|
|
26
|
+
</html>`;
|
|
27
|
+
|
|
28
|
+
function setCorsHeaders(res) {
|
|
29
|
+
// Allow configured marketplace web origin and keep localhost fallback for local dev login pages.
|
|
30
|
+
const allowedOrigin = MARKETPLACE_WEB_BASE_URL || "http://localhost:3030";
|
|
31
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
32
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
33
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function startCallbackServer({ port, state }) {
|
|
37
|
+
let resolveToken;
|
|
38
|
+
const waitForToken = new Promise((resolve) => {
|
|
39
|
+
resolveToken = resolve;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const server = http.createServer((req, res) => {
|
|
43
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
44
|
+
|
|
45
|
+
// Only handle /callback
|
|
46
|
+
if (url.pathname !== "/callback") {
|
|
47
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
48
|
+
res.end("Not Found");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// CORS preflight
|
|
53
|
+
if (req.method === "OPTIONS") {
|
|
54
|
+
setCorsHeaders(res);
|
|
55
|
+
res.writeHead(204);
|
|
56
|
+
res.end();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Only accept GET
|
|
61
|
+
if (req.method !== "GET") {
|
|
62
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
63
|
+
res.end("Method Not Allowed");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setCorsHeaders(res);
|
|
68
|
+
|
|
69
|
+
// Read token and state from URL query parameters
|
|
70
|
+
// Support both "token" and "access_token" for compatibility
|
|
71
|
+
const token =
|
|
72
|
+
url.searchParams.get("token") || url.searchParams.get("access_token");
|
|
73
|
+
const refreshToken = url.searchParams.get("refresh_token");
|
|
74
|
+
const receivedState = url.searchParams.get("state");
|
|
75
|
+
const username = url.searchParams.get("username");
|
|
76
|
+
const avatarUrl = url.searchParams.get("avatar_url");
|
|
77
|
+
|
|
78
|
+
// Validate CSRF state
|
|
79
|
+
if (!receivedState || receivedState !== state) {
|
|
80
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
81
|
+
res.end(ERROR_HTML("Invalid state token. Please try again."));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate token
|
|
86
|
+
if (!token || typeof token !== "string") {
|
|
87
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
88
|
+
res.end(ERROR_HTML("Missing token. Please try again."));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!refreshToken || typeof refreshToken !== "string") {
|
|
92
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
93
|
+
res.end(ERROR_HTML("Missing refresh token. Please try again."));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Success — respond first, then resolve
|
|
98
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
99
|
+
res.end(SUCCESS_HTML);
|
|
100
|
+
|
|
101
|
+
resolveToken({
|
|
102
|
+
token,
|
|
103
|
+
refreshToken,
|
|
104
|
+
state: receivedState,
|
|
105
|
+
username,
|
|
106
|
+
avatarUrl: avatarUrl,
|
|
107
|
+
});
|
|
108
|
+
server.close();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return { server, waitForToken };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { startCallbackServer };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
const MANIFEST_FILENAME = ".orderly-manifest.json";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 从 git remote 获取 repoUrl
|
|
9
|
+
*/
|
|
10
|
+
function getRepoUrl() {
|
|
11
|
+
try {
|
|
12
|
+
// Use execSync with git command - safer approach
|
|
13
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
16
|
+
windowsHide: true,
|
|
17
|
+
}).trim();
|
|
18
|
+
|
|
19
|
+
// Convert git@github.com:user/repo.git to https://github.com/user/repo
|
|
20
|
+
if (remoteUrl.startsWith("git@")) {
|
|
21
|
+
return remoteUrl
|
|
22
|
+
.replace("git@", "")
|
|
23
|
+
.replace(":", "/")
|
|
24
|
+
.replace(".git", "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Remove .git suffix if present
|
|
28
|
+
return remoteUrl.replace(/\.git$/, "");
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 从 package.json 读取 npmName
|
|
36
|
+
*/
|
|
37
|
+
function getNpmName(packageJsonPath) {
|
|
38
|
+
try {
|
|
39
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
40
|
+
return packageJson.name || null;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 生成 manifest 文件
|
|
48
|
+
* @param {string} pluginDir - 插件目录
|
|
49
|
+
* @param {Object} pluginInfo - 插件信息 (pluginId, tags, storybookUrl 等)
|
|
50
|
+
*/
|
|
51
|
+
function generateManifest(pluginDir, pluginInfo = {}) {
|
|
52
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
55
|
+
throw new Error("package.json not found in plugin directory");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const manifest = {
|
|
59
|
+
npmName: getNpmName(packageJsonPath),
|
|
60
|
+
pluginId: pluginInfo.pluginId || null,
|
|
61
|
+
repoUrl: pluginInfo.repoUrl || getRepoUrl() || null,
|
|
62
|
+
tags: pluginInfo.tags || [],
|
|
63
|
+
storybookUrl: pluginInfo.storybookUrl || null,
|
|
64
|
+
storybookTooltip: pluginInfo.storybookTooltip || null,
|
|
65
|
+
usagePrompt: pluginInfo.usagePrompt || null,
|
|
66
|
+
createdAt: new Date().toISOString(),
|
|
67
|
+
updatedAt: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const manifestPath = path.join(pluginDir, MANIFEST_FILENAME);
|
|
71
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
72
|
+
|
|
73
|
+
return manifest;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 读取 manifest 文件
|
|
78
|
+
* @param {string} pluginDir - 插件目录
|
|
79
|
+
* @returns {Object|null} manifest 对象,如果不存在则返回 null
|
|
80
|
+
*/
|
|
81
|
+
function readManifest(pluginDir) {
|
|
82
|
+
const manifestPath = path.join(pluginDir, MANIFEST_FILENAME);
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(manifestPath)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`Warning: Failed to parse ${MANIFEST_FILENAME}: ${e.message}. Ignoring corrupted manifest.`,
|
|
93
|
+
);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve plugin metadata for submit: use `.orderly-manifest.json` when present,
|
|
100
|
+
* otherwise derive from `package.json` and git (manual plugins may skip the manifest).
|
|
101
|
+
* @param {string} pluginDir - Plugin root directory
|
|
102
|
+
* @returns {Object|null} Manifest-shaped object, or null if no manifest and no package.json
|
|
103
|
+
*/
|
|
104
|
+
function resolvePluginManifest(pluginDir) {
|
|
105
|
+
const fromFile = readManifest(pluginDir);
|
|
106
|
+
if (fromFile) {
|
|
107
|
+
return fromFile;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
111
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
npmName: getNpmName(packageJsonPath),
|
|
117
|
+
pluginId: null,
|
|
118
|
+
repoUrl: getRepoUrl(),
|
|
119
|
+
tags: [],
|
|
120
|
+
storybookUrl: null,
|
|
121
|
+
storybookTooltip: null,
|
|
122
|
+
usagePrompt: null,
|
|
123
|
+
coverImages: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 更新 manifest 文件中的字段
|
|
129
|
+
* @param {string} pluginDir - 插件目录
|
|
130
|
+
* @param {Object} updates - 要更新的字段
|
|
131
|
+
*/
|
|
132
|
+
function updateManifest(pluginDir, updates) {
|
|
133
|
+
const manifestPath = path.join(pluginDir, MANIFEST_FILENAME);
|
|
134
|
+
const manifest = readManifest(pluginDir);
|
|
135
|
+
|
|
136
|
+
if (!manifest) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"Manifest file not found. Run 'orderly create plugin' first.",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const updatedManifest = {
|
|
143
|
+
...manifest,
|
|
144
|
+
...updates,
|
|
145
|
+
updatedAt: new Date().toISOString(),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(manifestPath, JSON.stringify(updatedManifest, null, 2));
|
|
149
|
+
|
|
150
|
+
return updatedManifest;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 检查 manifest 是否包含必需的提交字段
|
|
155
|
+
* @param {string} pluginDir - 插件目录
|
|
156
|
+
* @returns {{ valid: boolean, missing: string[] }}
|
|
157
|
+
*/
|
|
158
|
+
function validateManifest(pluginDir) {
|
|
159
|
+
const manifest = readManifest(pluginDir);
|
|
160
|
+
const missing = [];
|
|
161
|
+
|
|
162
|
+
if (!manifest) {
|
|
163
|
+
return { valid: false, missing: ["manifest file not found"] };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!manifest.npmName) {
|
|
167
|
+
missing.push("npmName (from package.json)");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!manifest.repoUrl) {
|
|
171
|
+
missing.push("repoUrl (configure git remote or run in git repository)");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
valid: missing.length === 0,
|
|
176
|
+
missing,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
MANIFEST_FILENAME,
|
|
182
|
+
getRepoUrl,
|
|
183
|
+
getNpmName,
|
|
184
|
+
generateManifest,
|
|
185
|
+
readManifest,
|
|
186
|
+
resolvePluginManifest,
|
|
187
|
+
updateManifest,
|
|
188
|
+
validateManifest,
|
|
189
|
+
};
|