@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,255 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { dim } = require("../shared");
|
|
4
|
+
const { DEFAULT_ORDERLY_SDK_DOCS_MCP_NAME } = require("./constants");
|
|
5
|
+
|
|
6
|
+
// Config paths per agent — keep in sync with packages/sdk-docs/src/install/clients.ts.
|
|
7
|
+
|
|
8
|
+
const ALL_CLIENTS = /** @type {const} */ ([
|
|
9
|
+
"claude",
|
|
10
|
+
"codex",
|
|
11
|
+
"cursor",
|
|
12
|
+
"opencode",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const CANONICAL_COMMAND = "npx";
|
|
16
|
+
const CANONICAL_ARGS = [
|
|
17
|
+
"-y",
|
|
18
|
+
"@orderly.network/sdk-docs",
|
|
19
|
+
"orderly-sdk-docs-mcp",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @returns {Record<string, { user: string; project: string }>}
|
|
24
|
+
*/
|
|
25
|
+
function getClientPaths() {
|
|
26
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
27
|
+
return {
|
|
28
|
+
claude: {
|
|
29
|
+
user: path.join(home, ".claude.json"),
|
|
30
|
+
project: ".claude.json",
|
|
31
|
+
},
|
|
32
|
+
codex: {
|
|
33
|
+
user: path.join(home, ".codex", "config.json"),
|
|
34
|
+
project: ".codex/config.json",
|
|
35
|
+
},
|
|
36
|
+
cursor: {
|
|
37
|
+
user: path.join(home, ".cursor", "mcp.json"),
|
|
38
|
+
project: ".cursor/mcp.json",
|
|
39
|
+
},
|
|
40
|
+
opencode: {
|
|
41
|
+
user: path.join(home, ".opencode", "config.json"),
|
|
42
|
+
project: ".opencode/config.json",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Same semantics as sdk-docs readJsonConfig: missing/empty → {}.
|
|
49
|
+
* @param {string} configPath
|
|
50
|
+
* @returns {{ ok: true; data: Record<string, unknown> } | { ok: false; error: string }}
|
|
51
|
+
*/
|
|
52
|
+
function readJsonConfigSafe(configPath) {
|
|
53
|
+
if (!fs.existsSync(configPath)) {
|
|
54
|
+
return { ok: true, data: {} };
|
|
55
|
+
}
|
|
56
|
+
const rawText = fs.readFileSync(configPath, "utf8").trim();
|
|
57
|
+
if (!rawText) {
|
|
58
|
+
return { ok: true, data: {} };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(rawText);
|
|
62
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
63
|
+
return { ok: false, error: "Root value must be an object" };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, data: /** @type {Record<string, unknown>} */ (parsed) };
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: e instanceof Error ? e.message : String(e),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {unknown} args
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
function argsMatchCanonical(args) {
|
|
79
|
+
if (!Array.isArray(args)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return JSON.stringify(args) === JSON.stringify(CANONICAL_ARGS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Strict match for the default server key (same as sdk-docs merge noop check).
|
|
87
|
+
* @param {unknown} entry
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
function entryMatchesCanonical(entry) {
|
|
91
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const o = /** @type {Record<string, unknown>} */ (entry);
|
|
95
|
+
return o.command === CANONICAL_COMMAND && argsMatchCanonical(o.args);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Loose match for custom --name installs.
|
|
100
|
+
* @param {unknown} entry
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
function entryMatchesFuzzyOrderly(entry) {
|
|
104
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const o = /** @type {Record<string, unknown>} */ (entry);
|
|
108
|
+
if (o.command !== "npx") {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const args = o.args;
|
|
112
|
+
if (!Array.isArray(args)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
args.includes("orderly-sdk-docs-mcp") &&
|
|
117
|
+
args.includes("@orderly.network/sdk-docs")
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {Record<string, unknown>} data
|
|
123
|
+
* @param {string} defaultName
|
|
124
|
+
* @returns {{ configured: boolean; serverKey?: string }}
|
|
125
|
+
*/
|
|
126
|
+
function analyzeMcpServers(data, defaultName) {
|
|
127
|
+
const mcpServersRaw = data.mcpServers;
|
|
128
|
+
const mcpServers =
|
|
129
|
+
mcpServersRaw &&
|
|
130
|
+
typeof mcpServersRaw === "object" &&
|
|
131
|
+
!Array.isArray(mcpServersRaw)
|
|
132
|
+
? /** @type {Record<string, unknown>} */ (mcpServersRaw)
|
|
133
|
+
: {};
|
|
134
|
+
|
|
135
|
+
const named = mcpServers[defaultName];
|
|
136
|
+
if (named && entryMatchesCanonical(named)) {
|
|
137
|
+
return { configured: true, serverKey: defaultName };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const [key, val] of Object.entries(mcpServers)) {
|
|
141
|
+
if (entryMatchesFuzzyOrderly(val)) {
|
|
142
|
+
return { configured: true, serverKey: key };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { configured: false };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {string} configPath
|
|
150
|
+
* @param {string} defaultName
|
|
151
|
+
* @returns {{
|
|
152
|
+
* configPath: string;
|
|
153
|
+
* exists: boolean;
|
|
154
|
+
* configured: boolean;
|
|
155
|
+
* serverKey?: string;
|
|
156
|
+
* error?: string;
|
|
157
|
+
* }}
|
|
158
|
+
*/
|
|
159
|
+
function inspectConfigFile(configPath, defaultName) {
|
|
160
|
+
const exists = fs.existsSync(configPath);
|
|
161
|
+
if (!exists) {
|
|
162
|
+
return { configPath, exists: false, configured: false };
|
|
163
|
+
}
|
|
164
|
+
const read = readJsonConfigSafe(configPath);
|
|
165
|
+
if (!read.ok) {
|
|
166
|
+
return {
|
|
167
|
+
configPath,
|
|
168
|
+
exists: true,
|
|
169
|
+
configured: false,
|
|
170
|
+
error: read.error,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const { configured, serverKey } = analyzeMcpServers(read.data, defaultName);
|
|
174
|
+
return {
|
|
175
|
+
configPath,
|
|
176
|
+
exists: true,
|
|
177
|
+
configured,
|
|
178
|
+
...(serverKey ? { serverKey } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} cwd
|
|
184
|
+
* @param {{ serverName?: string; clients?: string[] }} [options]
|
|
185
|
+
*/
|
|
186
|
+
function getOrderlySdkDocsMcpReport(cwd, options = {}) {
|
|
187
|
+
const resolvedCwd = path.resolve(cwd);
|
|
188
|
+
const defaultName = options.serverName || DEFAULT_ORDERLY_SDK_DOCS_MCP_NAME;
|
|
189
|
+
const paths = getClientPaths();
|
|
190
|
+
const want =
|
|
191
|
+
options.clients && options.clients.length > 0
|
|
192
|
+
? ALL_CLIENTS.filter((c) => options.clients.includes(c))
|
|
193
|
+
: ALL_CLIENTS;
|
|
194
|
+
|
|
195
|
+
/** @type {Record<string, { user: ReturnType<typeof inspectConfigFile>; project: ReturnType<typeof inspectConfigFile> }>} */
|
|
196
|
+
const clients = {};
|
|
197
|
+
|
|
198
|
+
for (const client of want) {
|
|
199
|
+
const p = paths[client];
|
|
200
|
+
clients[client] = {
|
|
201
|
+
user: inspectConfigFile(p.user, defaultName),
|
|
202
|
+
project: inspectConfigFile(
|
|
203
|
+
path.join(resolvedCwd, p.project),
|
|
204
|
+
defaultName,
|
|
205
|
+
),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
cwd: resolvedCwd,
|
|
211
|
+
defaultServerName: defaultName,
|
|
212
|
+
clients,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* True if any checked user or project config for this cwd shows Orderly SDK Docs MCP.
|
|
218
|
+
* @param {string} cwd
|
|
219
|
+
* @param {{ serverName?: string; clients?: string[] }} [options]
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
function isOrderlySdkDocsMcpConfiguredAnywhere(cwd, options = {}) {
|
|
223
|
+
const report = getOrderlySdkDocsMcpReport(cwd, options);
|
|
224
|
+
for (const row of Object.values(report.clients)) {
|
|
225
|
+
if (row.user.configured || row.project.configured) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* One-time style hint after scaffold/submit when MCP is missing (opt-out via env).
|
|
234
|
+
* @param {string} cwd
|
|
235
|
+
*/
|
|
236
|
+
function maybePrintOrderlyDevEnvironmentHints(cwd) {
|
|
237
|
+
if (process.env.ORDERLY_DEVKIT_NO_ENV_HINTS) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (isOrderlySdkDocsMcpConfiguredAnywhere(cwd)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
console.log();
|
|
244
|
+
dim("Tip: Orderly SDK Docs MCP was not detected in your agent config files.");
|
|
245
|
+
dim(" Install: orderly-devkit mcp install");
|
|
246
|
+
dim(" Verify: orderly-devkit mcp detect");
|
|
247
|
+
dim(" Skills: orderly-devkit skills install");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
ALL_CLIENTS,
|
|
252
|
+
getOrderlySdkDocsMcpReport,
|
|
253
|
+
isOrderlySdkDocsMcpConfiguredAnywhere,
|
|
254
|
+
maybePrintOrderlyDevEnvironmentHints,
|
|
255
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const degit = require("degit");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const Handlebars = require("handlebars");
|
|
5
|
+
const fg = require("fast-glob");
|
|
6
|
+
const { execSync } = require("child_process");
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Core API
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Download a template from a GitHub repository using degit.
|
|
14
|
+
* @param {string} repo - e.g. "OrderlyNetwork/orderly-plugin-template"
|
|
15
|
+
* @param {string} targetDir - local directory to clone into
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {boolean} options.force - overwrite existing files (default: true)
|
|
18
|
+
*/
|
|
19
|
+
async function downloadTemplate(repo, targetDir, options = {}) {
|
|
20
|
+
const { force = true } = options;
|
|
21
|
+
console.log(`\n Downloading template from https://github.com/${repo}...`);
|
|
22
|
+
const emitter = degit(repo, { cache: false, force });
|
|
23
|
+
await emitter.clone(targetDir);
|
|
24
|
+
console.log(" Template download completed");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Replace all Handlebars template variables in file contents.
|
|
29
|
+
* @param {string} dir - directory to scan
|
|
30
|
+
* @param {object} vars - key-value pairs for replacement
|
|
31
|
+
*/
|
|
32
|
+
async function replaceTemplateVars(dir, vars) {
|
|
33
|
+
const files = await fg(["**/*", "**/.*"], {
|
|
34
|
+
cwd: dir,
|
|
35
|
+
absolute: true,
|
|
36
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await Promise.all(
|
|
40
|
+
files.map(async (filePath) => {
|
|
41
|
+
if (isBinaryFile(filePath)) return;
|
|
42
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
43
|
+
const template = Handlebars.compile(content);
|
|
44
|
+
const result = template(vars);
|
|
45
|
+
await fs.writeFile(filePath, result);
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rename files and directories that contain __name__ to the actual name.
|
|
52
|
+
* @param {string} dir - directory to scan
|
|
53
|
+
* @param {string} nameVar - the name to replace __name__ with (PascalCase)
|
|
54
|
+
*/
|
|
55
|
+
async function renameTemplateFiles(dir, nameVar) {
|
|
56
|
+
// Get directories first (deepest first to rename children before parents)
|
|
57
|
+
const dirs = await fg(["**/*", "**/.*"], {
|
|
58
|
+
cwd: dir,
|
|
59
|
+
absolute: true,
|
|
60
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
61
|
+
onlyDirectories: true,
|
|
62
|
+
});
|
|
63
|
+
dirs.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
|
|
64
|
+
|
|
65
|
+
// Then get files
|
|
66
|
+
const files = await fg(["**/*", "**/.*"], {
|
|
67
|
+
cwd: dir,
|
|
68
|
+
absolute: true,
|
|
69
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
70
|
+
onlyDirectories: false,
|
|
71
|
+
onlyFiles: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const dirPath of dirs) {
|
|
75
|
+
await renamePath(dirPath, nameVar);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const filePath of files) {
|
|
79
|
+
await renamePath(filePath, nameVar);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run npm install in the target directory.
|
|
85
|
+
* @param {string} dir - directory to install deps
|
|
86
|
+
* @param {boolean} skipInstall - if true, skip installation
|
|
87
|
+
*/
|
|
88
|
+
function installDeps(dir, skipInstall = false) {
|
|
89
|
+
if (skipInstall) {
|
|
90
|
+
console.log(" Skipping dependency installation");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.log("\n Installing dependencies...");
|
|
94
|
+
execSync("npm install", { cwd: dir, stdio: "inherit" });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Full Generation Pipeline
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a directory exists and is non-empty.
|
|
103
|
+
* @param {string} dirPath
|
|
104
|
+
* @returns {Promise<boolean>}
|
|
105
|
+
*/
|
|
106
|
+
async function isNonEmptyDir(dirPath) {
|
|
107
|
+
if (!(await fs.pathExists(dirPath))) return false;
|
|
108
|
+
const stat = await fs.stat(dirPath);
|
|
109
|
+
if (!stat.isDirectory()) return false;
|
|
110
|
+
const entries = await fs.readdir(dirPath);
|
|
111
|
+
const visible = entries.filter((e) => !e.startsWith("."));
|
|
112
|
+
return visible.length > 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Verify a GitHub repo exists before attempting clone.
|
|
117
|
+
* Uses `git ls-remote` which fails fast if repo doesn't exist.
|
|
118
|
+
* @param {string} repo - e.g. "OrderlyNetwork/orderly-plugin-template"
|
|
119
|
+
* @returns {Promise<boolean>}
|
|
120
|
+
*/
|
|
121
|
+
async function verifyGitHubRepo(repo) {
|
|
122
|
+
console.log(` Checking repository https://github.com/${repo}...`);
|
|
123
|
+
try {
|
|
124
|
+
execSync(`git ls-remote https://github.com/${repo} HEAD`, {
|
|
125
|
+
cwd: process.cwd(),
|
|
126
|
+
stdio: "pipe",
|
|
127
|
+
timeout: 15000,
|
|
128
|
+
});
|
|
129
|
+
return true;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err.status === 128) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Repository "https://github.com/${repo}" not found or is not accessible. ` +
|
|
134
|
+
"Please verify the repository exists and you have access to it.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
err.status === null &&
|
|
139
|
+
err.message &&
|
|
140
|
+
err.message.includes("timed out")
|
|
141
|
+
) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Network timeout while connecting to https://github.com/${repo}. ` +
|
|
144
|
+
"Please check your internet connection.",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Failed to access repository https://github.com/${repo}: ${err.message}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run the full template generation pipeline.
|
|
155
|
+
* @param {object} config
|
|
156
|
+
* @param {string} config.repo - GitHub repo (e.g. "OrderlyNetwork/orderly-plugin-template")
|
|
157
|
+
* @param {string} config.targetDir - local output directory
|
|
158
|
+
* @param {object} config.vars - template variables
|
|
159
|
+
* @param {string} config.vars.name - the PascalCase name (e.g. "MyPlugin")
|
|
160
|
+
* @param {boolean} config.skipInstall
|
|
161
|
+
*/
|
|
162
|
+
async function generateFromTemplate(config) {
|
|
163
|
+
const { repo, targetDir, vars, skipInstall = false } = config;
|
|
164
|
+
|
|
165
|
+
// Ensure target dir is empty or does not exist
|
|
166
|
+
if (await isNonEmptyDir(targetDir)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Target directory "${targetDir}" is not empty. Please use an empty directory or a non-existent path.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Create the target directory if it doesn't exist
|
|
173
|
+
await fs.ensureDir(targetDir);
|
|
174
|
+
|
|
175
|
+
// Step 0: Verify repo exists
|
|
176
|
+
await verifyGitHubRepo(repo);
|
|
177
|
+
|
|
178
|
+
// Step 1: Download
|
|
179
|
+
await downloadTemplate(repo, targetDir);
|
|
180
|
+
|
|
181
|
+
// Step 2: Replace content
|
|
182
|
+
await replaceTemplateVars(targetDir, vars);
|
|
183
|
+
|
|
184
|
+
// Step 3: Rename files
|
|
185
|
+
await renameTemplateFiles(targetDir, vars.name);
|
|
186
|
+
|
|
187
|
+
// Step 4: Install deps
|
|
188
|
+
installDeps(targetDir, skipInstall);
|
|
189
|
+
|
|
190
|
+
console.log(`\n Plugin generated at: ${targetDir}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Helpers
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Determine if a file is binary (should skip template processing).
|
|
199
|
+
*/
|
|
200
|
+
function isBinaryFile(filePath) {
|
|
201
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
202
|
+
const binaryExts = [
|
|
203
|
+
".png",
|
|
204
|
+
".jpg",
|
|
205
|
+
".jpeg",
|
|
206
|
+
".gif",
|
|
207
|
+
".bmp",
|
|
208
|
+
".ico",
|
|
209
|
+
".woff",
|
|
210
|
+
".woff2",
|
|
211
|
+
".ttf",
|
|
212
|
+
".eot",
|
|
213
|
+
".otf",
|
|
214
|
+
".pdf",
|
|
215
|
+
".zip",
|
|
216
|
+
".tar",
|
|
217
|
+
".gz",
|
|
218
|
+
".rar",
|
|
219
|
+
];
|
|
220
|
+
return binaryExts.includes(ext);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Rename a file or directory if its name contains __name__.
|
|
225
|
+
*/
|
|
226
|
+
async function renamePath(filePath, nameVar) {
|
|
227
|
+
const dir = path.dirname(filePath);
|
|
228
|
+
const basename = path.basename(filePath);
|
|
229
|
+
|
|
230
|
+
if (basename.includes("__name__")) {
|
|
231
|
+
const newBasename = basename.replace(/__name__/g, nameVar);
|
|
232
|
+
const newPath = path.join(dir, newBasename);
|
|
233
|
+
await fs.move(filePath, newPath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Convert a string to PascalCase.
|
|
239
|
+
*/
|
|
240
|
+
function toPascalCase(str) {
|
|
241
|
+
return str
|
|
242
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
243
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Convert a string to kebab-case.
|
|
248
|
+
*/
|
|
249
|
+
function toKebabCase(str) {
|
|
250
|
+
return str
|
|
251
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
252
|
+
.replace(/[\s_]+/g, "-")
|
|
253
|
+
.toLowerCase();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Convert a string to camelCase.
|
|
258
|
+
*/
|
|
259
|
+
function toCamelCase(str) {
|
|
260
|
+
return str
|
|
261
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
262
|
+
.replace(/^(.)/, (c) => c.toLowerCase());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validate that a name is in PascalCase.
|
|
267
|
+
*/
|
|
268
|
+
function validateName(name) {
|
|
269
|
+
if (!name || typeof name !== "string") {
|
|
270
|
+
return { valid: false, error: "Name cannot be empty" };
|
|
271
|
+
}
|
|
272
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) {
|
|
273
|
+
return {
|
|
274
|
+
valid: false,
|
|
275
|
+
error: "Name must be in PascalCase (e.g. MyPlugin)",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return { valid: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
downloadTemplate,
|
|
283
|
+
replaceTemplateVars,
|
|
284
|
+
renameTemplateFiles,
|
|
285
|
+
installDeps,
|
|
286
|
+
generateFromTemplate,
|
|
287
|
+
isNonEmptyDir,
|
|
288
|
+
verifyGitHubRepo,
|
|
289
|
+
isBinaryFile,
|
|
290
|
+
toPascalCase,
|
|
291
|
+
toKebabCase,
|
|
292
|
+
toCamelCase,
|
|
293
|
+
validateName,
|
|
294
|
+
};
|
package/src/shared.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const { prompt } = require("enquirer");
|
|
3
|
+
|
|
4
|
+
// Logger utilities
|
|
5
|
+
function log(message) {
|
|
6
|
+
console.log(message);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function info(message) {
|
|
10
|
+
console.log(chalk.blue("ℹ"), message);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function success(message) {
|
|
14
|
+
console.log(chalk.green("✓"), message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function warn(message) {
|
|
18
|
+
console.log(chalk.yellow("⚠"), message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function error(message) {
|
|
22
|
+
console.error(chalk.red("✗"), message);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function heading(message) {
|
|
26
|
+
console.log(chalk.bold.cyan(message));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function dim(message) {
|
|
30
|
+
console.log(chalk.dim(message));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Interactive prompts
|
|
34
|
+
async function input(message, initial) {
|
|
35
|
+
const result = await prompt({
|
|
36
|
+
type: "input",
|
|
37
|
+
name: "value",
|
|
38
|
+
message,
|
|
39
|
+
initial: initial || "",
|
|
40
|
+
});
|
|
41
|
+
return result.value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function select(message, choices, initial) {
|
|
45
|
+
const result = await prompt({
|
|
46
|
+
type: "select",
|
|
47
|
+
name: "value",
|
|
48
|
+
message,
|
|
49
|
+
choices,
|
|
50
|
+
initial: initial || 0,
|
|
51
|
+
});
|
|
52
|
+
return result.value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function confirm(message) {
|
|
56
|
+
const result = await prompt({
|
|
57
|
+
type: "confirm",
|
|
58
|
+
name: "confirm",
|
|
59
|
+
message,
|
|
60
|
+
initial: true,
|
|
61
|
+
});
|
|
62
|
+
return result.confirm;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalize backend error payload into a human-readable message.
|
|
67
|
+
* @param {unknown} responseData
|
|
68
|
+
* @param {number} status
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function getErrorMessage(responseData, status) {
|
|
72
|
+
const candidates = [
|
|
73
|
+
responseData?.message,
|
|
74
|
+
responseData?.error,
|
|
75
|
+
responseData?.details,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(candidate) && candidate.length > 0) {
|
|
84
|
+
const firstString = candidate.find((item) => typeof item === "string");
|
|
85
|
+
if (firstString) {
|
|
86
|
+
return firstString;
|
|
87
|
+
}
|
|
88
|
+
return JSON.stringify(candidate);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (candidate && typeof candidate === "object") {
|
|
92
|
+
if (typeof candidate.message === "string" && candidate.message.trim()) {
|
|
93
|
+
return candidate.message;
|
|
94
|
+
}
|
|
95
|
+
return JSON.stringify(candidate);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return `HTTP ${status}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract a stable error code/message pair from backend payloads.
|
|
104
|
+
* Supports legacy `{ message }` and structured `{ error: { code, message } }`.
|
|
105
|
+
*
|
|
106
|
+
* @param {unknown} responseData
|
|
107
|
+
* @param {number} status
|
|
108
|
+
* @returns {{ code: string | null, message: string }}
|
|
109
|
+
*/
|
|
110
|
+
function getApiErrorInfo(responseData, status) {
|
|
111
|
+
const codeCandidate = responseData?.error?.code;
|
|
112
|
+
const normalizedCode =
|
|
113
|
+
typeof codeCandidate === "string" && codeCandidate.trim()
|
|
114
|
+
? codeCandidate.trim()
|
|
115
|
+
: null;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
code: normalizedCode,
|
|
119
|
+
message: getErrorMessage(responseData, status),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
log,
|
|
125
|
+
info,
|
|
126
|
+
success,
|
|
127
|
+
warn,
|
|
128
|
+
error,
|
|
129
|
+
heading,
|
|
130
|
+
dim,
|
|
131
|
+
input,
|
|
132
|
+
select,
|
|
133
|
+
confirm,
|
|
134
|
+
getErrorMessage,
|
|
135
|
+
getApiErrorInfo,
|
|
136
|
+
};
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
__ORDERLY_VERSION__?: {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
if (typeof window !== "undefined") {
|
|
9
|
+
window.__ORDERLY_VERSION__ = window.__ORDERLY_VERSION__ || {};
|
|
10
|
+
window.__ORDERLY_VERSION__["@orderly.network/devkit"] = "1.0.2";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default "1.0.2";
|