@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 ADDED
@@ -0,0 +1,160 @@
1
+ # @orderly.network/devkit
2
+
3
+ CLI toolkit for scaffolding plugins and modules, authenticating with Marketplace, submitting and managing plugins, and installing MCP / agent skill integrations.
4
+
5
+ ## Requirements
6
+
7
+ - **Node.js** v20.19.0 or newer.
8
+
9
+ ## Installation
10
+
11
+ ### From npm
12
+
13
+ ```bash
14
+ pnpm add -g @orderly.network/devkit
15
+ # or
16
+ npm install -g @orderly.network/devkit
17
+ ```
18
+
19
+ Then run:
20
+
21
+ ```bash
22
+ orderly-devkit --help
23
+ ```
24
+
25
+ ### One-off without global install
26
+
27
+ ```bash
28
+ pnpm dlx @orderly.network/devkit --help
29
+ # or
30
+ npx @orderly.network/devkit --help
31
+ ```
32
+
33
+ ## Authentication
34
+
35
+ Login uses GitHub OAuth in the browser and a short-lived local callback server.
36
+
37
+ - **Credentials file:** `~/.orderly/auth.json` (created automatically).
38
+ - The CLI opens the marketplace login page and completes OAuth callback automatically.
39
+
40
+ ```bash
41
+ orderly-devkit login # open browser, complete GitHub auth
42
+ orderly-devkit login --force # re-authenticate even if already logged in
43
+ orderly-devkit login --port 9877 # if default port is busy (default is 9876)
44
+ orderly-devkit whoami
45
+ orderly-devkit logout
46
+ ```
47
+
48
+ ## Commands overview
49
+
50
+ Run `orderly-devkit <command> --help` for options on any command.
51
+
52
+ ### `create`
53
+
54
+ Scaffold new artifacts (interactive).
55
+
56
+ | Subcommand | Description |
57
+ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | `create plugin` | Clone and render the plugin template (`OrderlyNetwork/orderly-plugin-template`), prompt for name/plugin id/interceptor target, and optionally generate `.orderly-manifest.json`. |
59
+ | `create module` | Guided flow for module type (`page`, `component`, `hook`, `utils`, `module`). **File generation is not implemented yet**—it only collects choices and prints a summary. |
60
+
61
+ ```bash
62
+ orderly-devkit create plugin
63
+ orderly-devkit create module
64
+ orderly-devkit create module --name my-module
65
+ ```
66
+
67
+ ### Marketplace: `submit`, `list`, `update`, `view`
68
+
69
+ These commands use the Marketplace API. You must be logged in (`orderly-devkit login`).
70
+
71
+ **`submit`** — register a new plugin from a local directory.
72
+
73
+ - Resolves metadata from `package.json` and/or `.orderly-manifest.json`.
74
+ - `repoUrl` should be a GitHub URL (`https://github.com/<owner>/<repo>`); it can be filled from `git remote` when missing.
75
+ - **Tags** must be from the allowed set: `UI`, `Indicator`, `Order Entry`, `Trading`, `Chart`, `Portfolio`, `Analytics`, `Tool`, `Widget` (max 5).
76
+
77
+ ```bash
78
+ orderly-devkit submit
79
+ orderly-devkit submit --path ./my-plugin
80
+ orderly-devkit submit -p ./my-plugin --tags UI,Trading --dry-run
81
+ orderly-devkit submit -p ./my-plugin --storybook-url https://example.com/storybook
82
+ ```
83
+
84
+ **`list`** — list plugins associated with your account.
85
+
86
+ ```bash
87
+ orderly-devkit list
88
+ orderly-devkit list --json
89
+ ```
90
+
91
+ **`update`** — PATCH plugin metadata for an existing listing (requires `pluginId` in `.orderly-manifest.json`).
92
+
93
+ Updatable fields include: `name`, `description`, `tags`, `coverImages`, `storybookUrl`, `storybookTooltip`, `usagePrompt`.
94
+
95
+ ```bash
96
+ orderly-devkit update --path ./my-plugin --dry-run
97
+ orderly-devkit update -p ./my-plugin
98
+ ```
99
+
100
+ **`view`** — fetch one plugin by ID as JSON.
101
+
102
+ ```bash
103
+ orderly-devkit view <plugin-id>
104
+ ```
105
+
106
+ ### `mcp install`
107
+
108
+ Install the **Orderly SDK Docs** MCP server entry for Claude, Codex, Cursor, OpenCode, etc.
109
+
110
+ ```bash
111
+ orderly-devkit mcp install
112
+ orderly-devkit mcp install --client cursor --scope project
113
+ orderly-devkit mcp install --client all --scope user
114
+ orderly-devkit mcp install --sdk-docs-version 0.1.0
115
+ orderly-devkit mcp install --dry-run
116
+ orderly-devkit mcp install --force
117
+ ```
118
+
119
+ | Option | Description |
120
+ | ---------- | ----------------------------------------------------------------------------------------- |
121
+ | `--client` | `claude`, `codex`, `cursor`, `opencode`, `all`, or comma-separated list (default: `all`). |
122
+ | `--scope` | `user` or `project` (default: `user`). |
123
+ | `--name` | MCP server id in config (default: `orderly-sdk-docs`). |
124
+
125
+ ### `skills install`
126
+
127
+ Install Orderly **agent skills** for plugin workflows (create, write, add, submit) with `npx -y skills add …`.
128
+
129
+ Default behavior installs four skills non-interactively: `orderly-plugin-create`, `orderly-plugin-write`, `orderly-plugin-add`, `orderly-plugin-submit`.
130
+
131
+ ```bash
132
+ orderly-devkit skills install
133
+ orderly-devkit skills install --list
134
+ orderly-devkit skills install --dry-run
135
+ orderly-devkit skills install other/repo --skill my-skill -y
136
+ orderly-devkit skills install -- --some-upstream-skills-flag
137
+ ```
138
+
139
+ | Option | Description |
140
+ | ----------------- | ----------------------------------------------------------------------- |
141
+ | `[source]` | GitHub `owner/repo`, URL, or local path (default source is built in). |
142
+ | `--list` | List skills in the source without installing. |
143
+ | `--skill` / `-s` | Repeatable; install only named skills (replaces default four when set). |
144
+ | `--all` | Forward `--all` to the skills CLI. |
145
+ | `--global` / `-g` | Global install for the skills CLI. |
146
+ | `--agent` / `-a` | Target agent(s), e.g. `-a cursor`. |
147
+ | `--copy` | Copy files instead of symlinks. |
148
+ | `--yes` / `-y` | With `--list` only: pass `-y` through. |
149
+ | `--` | Everything after `--` is forwarded to the upstream `skills` CLI. |
150
+
151
+ ## Troubleshooting
152
+
153
+ - **Marketplace API errors** — check your network connection; if the CLI reports a failed request, try again later or verify you are logged in (`orderly-devkit whoami`).
154
+ - **Login: port in use** — run `orderly-devkit login --port <free-port>`.
155
+ - **Ctrl+C during prompts** — the CLI handles enquirer cancellation and exits with a clear message when possible.
156
+ - **Invalid working directory** — if the shell’s cwd was deleted, the CLI may switch to `HOME` or the package directory and warn you.
157
+
158
+ ## License
159
+
160
+ See the repository root license for this package’s distribution terms.
package/bin/cli.js ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const yargs = require("yargs/yargs");
6
+ const { hideBin } = require("yargs/helpers");
7
+ const chalk = require("chalk");
8
+
9
+ // Resolve commands directory relative to this file's location
10
+ const commandsDir = path.resolve(__dirname, "../src/commands");
11
+
12
+ /**
13
+ * Detect known enquirer cancel crash on newer Node runtimes.
14
+ * This keeps Ctrl+C behavior user-friendly while preserving real errors.
15
+ * @param {unknown} err
16
+ * @returns {boolean}
17
+ */
18
+ function isEnquirerReadlineCancelError(err) {
19
+ if (!err || typeof err !== "object") {
20
+ return false;
21
+ }
22
+
23
+ const error =
24
+ /** @type {{ code?: string; stack?: string; message?: string }} */ (err);
25
+ const stack = typeof error.stack === "string" ? error.stack : "";
26
+ const message = typeof error.message === "string" ? error.message : "";
27
+
28
+ return (
29
+ error.code === "ERR_USE_AFTER_CLOSE" &&
30
+ message.includes("readline") &&
31
+ stack.includes("enquirer/lib/")
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Gracefully handle Ctrl+C prompt cancellation emitted by enquirer.
37
+ * Only swallows the known cancellation crash and rethrows everything else.
38
+ */
39
+ function setupGracefulPromptInterruptHandling() {
40
+ process.on("uncaughtException", (err) => {
41
+ if (!isEnquirerReadlineCancelError(err)) {
42
+ throw err;
43
+ }
44
+
45
+ console.log();
46
+ console.log(chalk.yellow("Operation cancelled."));
47
+ process.exit(130);
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Ensure process cwd is valid before yargs initialization.
53
+ * Yargs reads cwd eagerly; when users run CLI from a deleted directory,
54
+ * Node throws ENOENT before command parsing starts.
55
+ */
56
+ function ensureValidWorkingDirectory() {
57
+ const cwd = process.cwd();
58
+ try {
59
+ fs.accessSync(cwd);
60
+ return;
61
+ } catch {
62
+ const fallbackDir =
63
+ process.env.HOME ||
64
+ process.env.USERPROFILE ||
65
+ path.resolve(__dirname, "..");
66
+
67
+ try {
68
+ fs.accessSync(fallbackDir);
69
+ process.chdir(fallbackDir);
70
+ console.warn(
71
+ chalk.yellow(
72
+ `Current working directory is unavailable. Switched to: ${fallbackDir}`,
73
+ ),
74
+ );
75
+ } catch {
76
+ // Fallback also unavailable — let the error surface naturally.
77
+ throw new Error(
78
+ `Current working directory "${cwd}" is inaccessible. ` +
79
+ `Please change to a valid directory and try again.`,
80
+ );
81
+ }
82
+ }
83
+ }
84
+
85
+ setupGracefulPromptInterruptHandling();
86
+ ensureValidWorkingDirectory();
87
+
88
+ yargs(hideBin(process.argv))
89
+ .scriptName("orderly-devkit")
90
+ .usage(chalk.cyan("Usage:") + " $0 <command> [options]")
91
+ .alias("v", "version")
92
+ .alias("h", "help")
93
+ .showHelpOnFail(true)
94
+ .strictCommands()
95
+ .demandCommand(1, "Please specify a command.")
96
+ .command("create", "Create a new plugin or module", (yargs) => {
97
+ return yargs.commandDir(path.join(commandsDir, "create"));
98
+ })
99
+ .commandDir(commandsDir)
100
+ .recommendCommands()
101
+ .fail((message, error, parser) => {
102
+ // Always show usage/help for missing or invalid command input.
103
+ parser.showHelp();
104
+ if (message) {
105
+ console.error(chalk.red(`\n${message}`));
106
+ }
107
+ if (error && !message) {
108
+ console.error(chalk.red(`\n${error.message}`));
109
+ }
110
+ process.exit(1);
111
+ })
112
+ .parse();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@kodiak-finance/orderly-devkit",
3
+ "version": "1.0.2",
4
+ "description": "CLI toolkit for scaffolding and building DEXs, plugins, and modules with Orderly",
5
+ "main": "",
6
+ "bin": {
7
+ "kodiak-orderly-devkit": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20.19.0"
15
+ },
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "chalk": "^4.1.2",
19
+ "degit": "^2.8.4",
20
+ "enquirer": "^2.4.1",
21
+ "fast-glob": "^3.3.3",
22
+ "fs-extra": "^11.3.0",
23
+ "handlebars": "^4.7.9",
24
+ "yargs": "^17.7.2"
25
+ },
26
+ "devDependencies": {
27
+ "rimraf": "^5.0.0",
28
+ "tsconfig": "0.11.34-alpha.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "clean": "rimraf dist",
35
+ "link-global": "pnpm link --global"
36
+ }
37
+ }
@@ -0,0 +1,49 @@
1
+ const { input, select, heading, info, success } = require("../../shared");
2
+ const { MODULE_TYPES } = require("../../internal/constants");
3
+
4
+ // create module command - default export for yargs commandDir
5
+ module.exports = {
6
+ command: "module",
7
+ describe: "Create a new module",
8
+ builder: (yargs) => {
9
+ return yargs
10
+ .option("template", {
11
+ alias: "t",
12
+ type: "string",
13
+ describe:
14
+ "string; currently a placeholder and is not used by this command (module type is always prompted interactively)",
15
+ demandOption: false,
16
+ })
17
+ .positional("name", {
18
+ type: "string",
19
+ describe:
20
+ "string; module name used only for display in the guided flow (actual generation is implemented later)",
21
+ default: "my-module",
22
+ })
23
+ .example(
24
+ "orderly create module",
25
+ "Run guided module creation (prompt for module type)",
26
+ )
27
+ .example(
28
+ "orderly create module --name my-module",
29
+ "Provide a module name (still prompts for module type)",
30
+ );
31
+ },
32
+ handler: async (argv) => {
33
+ heading("Create a New Module");
34
+ info("This command will guide you through creating a new module.\n");
35
+
36
+ // Module type: use argv or prompt
37
+ const moduleType = await select("Select module type:", MODULE_TYPES, 0);
38
+
39
+ console.log("\n--- Module Creation Details ---");
40
+ console.log("Module name:", argv.name || "my-module");
41
+ console.log("Module type:", moduleType);
42
+ console.log("-----------------------------\n");
43
+
44
+ success("Module creation flow completed!");
45
+ success(`Module: ${argv.name || "my-module"}`);
46
+ success(`Type: ${moduleType}`);
47
+ console.log("\nNote: Actual file generation will be implemented later.");
48
+ },
49
+ };
@@ -0,0 +1,270 @@
1
+ const {
2
+ generateFromTemplate,
3
+ toPascalCase,
4
+ toKebabCase,
5
+ toCamelCase,
6
+ validateName,
7
+ } = require("../../internal/templateGenerator");
8
+ const {
9
+ input,
10
+ select,
11
+ confirm,
12
+ heading,
13
+ info,
14
+ success,
15
+ error,
16
+ warn,
17
+ } = require("../../shared");
18
+ const { INTERCEPTOR_TARGETS } = require("../../internal/constants");
19
+ const { generateManifest } = require("../../internal/manifest");
20
+ const {
21
+ maybePrintOrderlyDevEnvironmentHints,
22
+ } = require("../../internal/orderlySdkDocsMcpDetect");
23
+ const path = require("path");
24
+
25
+ const TEMPLATE_REPO = "OrderlyNetwork/orderly-plugin-template";
26
+
27
+ /**
28
+ * Validate npm package name for scoped and unscoped formats.
29
+ * Keep this check intentionally strict to prevent publishing failures.
30
+ * @param {string} name
31
+ * @returns {boolean}
32
+ */
33
+ function isValidNpmPackageName(name) {
34
+ if (!name || typeof name !== "string") {
35
+ return false;
36
+ }
37
+ const scoped = /^@[a-z0-9][a-z0-9-._]*\/[a-z0-9][a-z0-9-._]*$/;
38
+ const unscoped = /^[a-z0-9][a-z0-9-._]*$/;
39
+ return scoped.test(name) || unscoped.test(name);
40
+ }
41
+
42
+ /**
43
+ * Build a npm-compliant package name before template rendering.
44
+ * Prefer pluginId so users can control the final published package name.
45
+ * @param {string} pluginId
46
+ * @param {string} pluginName
47
+ * @returns {string}
48
+ */
49
+ function buildNpmName(pluginId, pluginName) {
50
+ const candidate = typeof pluginId === "string" ? pluginId.trim() : "";
51
+ if (isValidNpmPackageName(candidate)) {
52
+ return candidate;
53
+ }
54
+ return toKebabCase(pluginName);
55
+ }
56
+
57
+ // Build template variables from user answers
58
+ function buildVars(pluginName, pluginId, interceptorTarget, npmName) {
59
+ return {
60
+ name: pluginName,
61
+ pluginName,
62
+ pluginId,
63
+ npmName,
64
+ pluginIdCamel: toCamelCase(pluginId),
65
+ interceptorTarget,
66
+ pluginVersion: "1.0.0",
67
+ date: new Date().toISOString().split("T")[0],
68
+ };
69
+ }
70
+
71
+ // Interactive prompts
72
+ async function promptPluginName(existing) {
73
+ let name;
74
+ let validation;
75
+ do {
76
+ name = await input("Enter plugin name:", existing || "");
77
+ name = toPascalCase(name);
78
+ validation = validateName(name);
79
+ if (!validation.valid) {
80
+ console.log(` ${validation.error}`);
81
+ }
82
+ } while (!validation.valid);
83
+ return name;
84
+ }
85
+
86
+ async function promptPluginId(pluginName, existing) {
87
+ const defaultId = toKebabCase(pluginName);
88
+ return await input("Enter plugin ID:", existing || defaultId);
89
+ }
90
+
91
+ async function promptInterceptorTarget() {
92
+ return await select("Select interceptor target:", INTERCEPTOR_TARGETS, 0);
93
+ }
94
+
95
+ async function promptTargetDir(pluginName) {
96
+ return await input("Enter target directory:", `./${pluginName}`);
97
+ }
98
+
99
+ // Main command
100
+ module.exports = {
101
+ command: "plugin",
102
+ describe: "Create a new plugin from template",
103
+ builder: (yargs) => {
104
+ return yargs
105
+ .option("name", {
106
+ alias: "n",
107
+ type: "string",
108
+ describe:
109
+ "string; plugin name in PascalCase (input is normalized to PascalCase and validated). Required when `--no-interactive` is set",
110
+ demandOption: false,
111
+ })
112
+ .option("id", {
113
+ alias: "i",
114
+ type: "string",
115
+ describe:
116
+ "string; plugin ID in kebab-case. If omitted in `--no-interactive` mode, derived from `--name` via toKebabCase()",
117
+ demandOption: false,
118
+ })
119
+ .option("target", {
120
+ alias: "t",
121
+ type: "string",
122
+ describe:
123
+ "string; output directory. Must be empty or non-existent (template generator throws if non-empty). If omitted in `--no-interactive` mode, defaults to `./<PluginName>`",
124
+ demandOption: false,
125
+ })
126
+ .option("interceptor", {
127
+ type: "string",
128
+ choices: INTERCEPTOR_TARGETS,
129
+ describe:
130
+ "string; interceptor target (must be one of the listed values). Required when `--no-interactive` is set",
131
+ demandOption: false,
132
+ })
133
+ .option("skip-install", {
134
+ type: "boolean",
135
+ describe:
136
+ "boolean; if true, skip `npm install` in the generated plugin folder",
137
+ default: false,
138
+ })
139
+ .option("no-interactive", {
140
+ type: "boolean",
141
+ default: false,
142
+ describe:
143
+ "boolean; if true, do not prompt. Requires `--name` and `--interceptor`. If `--id`/`--target` are omitted, they default from `--name`. Prompts are also skipped when `--name --id --interceptor --target` are all provided",
144
+ })
145
+ .example(
146
+ "orderly create plugin --no-interactive --name MyPlugin --id my-plugin --interceptor Trading.Layout.Desktop --target ./my-plugin --skip-install",
147
+ "Create plugin without prompts (explicit id/target) and skip npm install",
148
+ )
149
+ .example(
150
+ "orderly create plugin",
151
+ "Interactive mode: prompt for name, id, interceptor, and target directory",
152
+ );
153
+ },
154
+ handler: async (argv) => {
155
+ heading("Create a New Plugin");
156
+
157
+ // Determine if all required params are provided (non-interactive mode)
158
+ const hasAllParams =
159
+ argv.name && argv.id && argv.interceptor && argv.target;
160
+ const nonInteractive = argv["no-interactive"] || hasAllParams;
161
+
162
+ if (!nonInteractive) {
163
+ info(
164
+ "This command will download a plugin template and customize it for you.\n",
165
+ );
166
+ }
167
+
168
+ // Gather inputs (from flags or interactive prompts)
169
+ let pluginName = argv.name ? toPascalCase(argv.name) : null;
170
+ if (!pluginName) {
171
+ if (nonInteractive) {
172
+ error("Missing required option: --name");
173
+ process.exitCode = 1;
174
+ return;
175
+ }
176
+ pluginName = await promptPluginName("");
177
+ }
178
+
179
+ let pluginId = argv.id || null;
180
+ if (!pluginId) {
181
+ if (nonInteractive) {
182
+ pluginId = toKebabCase(pluginName);
183
+ } else {
184
+ pluginId = await promptPluginId(pluginName, "");
185
+ }
186
+ }
187
+
188
+ let interceptorTarget = argv.interceptor || null;
189
+ if (!interceptorTarget) {
190
+ if (nonInteractive) {
191
+ error("Missing required option: --interceptor");
192
+ process.exitCode = 1;
193
+ return;
194
+ }
195
+ interceptorTarget = await promptInterceptorTarget();
196
+ }
197
+
198
+ let targetDir = argv.target || null;
199
+ if (!targetDir) {
200
+ if (nonInteractive) {
201
+ targetDir = `./${pluginName}`;
202
+ } else {
203
+ targetDir = await promptTargetDir(pluginName);
204
+ }
205
+ }
206
+
207
+ const skipInstall = argv["skip-install"];
208
+
209
+ // Summary
210
+ console.log("\n--- Plugin Creation Summary ---");
211
+ console.log(`Plugin Name: ${pluginName}`);
212
+ console.log(`Plugin ID: ${pluginId}`);
213
+ console.log(`Interceptor Target: ${interceptorTarget}`);
214
+ console.log(`Target Directory: ${targetDir}`);
215
+ console.log(`-----------------------------\n`);
216
+
217
+ if (!nonInteractive) {
218
+ const proceed = await confirm("Proceed with creation?");
219
+ if (!proceed) {
220
+ info("Cancelled.");
221
+ return;
222
+ }
223
+ }
224
+
225
+ // Build npm-safe package name up front and pass into template variables.
226
+ const npmName = buildNpmName(pluginId, pluginName);
227
+ if (npmName !== pluginId) {
228
+ warn(
229
+ `Plugin ID "${pluginId}" is not npm-compliant, using "${npmName}" as package name.`,
230
+ );
231
+ }
232
+
233
+ // Build vars and generate
234
+ const vars = buildVars(pluginName, pluginId, interceptorTarget, npmName);
235
+
236
+ try {
237
+ await generateFromTemplate({
238
+ repo: TEMPLATE_REPO,
239
+ targetDir: path.resolve(targetDir),
240
+ vars,
241
+ skipInstall,
242
+ });
243
+
244
+ // Generate orderly manifest file
245
+ const resolvedDir = path.resolve(targetDir);
246
+ try {
247
+ generateManifest(resolvedDir, {
248
+ pluginId: pluginId,
249
+ tags: [],
250
+ });
251
+ info(
252
+ `Manifest file generated at ${resolvedDir}/.orderly-manifest.json`,
253
+ );
254
+ } catch (manifestErr) {
255
+ warn(`Could not generate manifest: ${manifestErr.message}`);
256
+ }
257
+
258
+ success(`\nPlugin created successfully!`);
259
+ info(`Next steps:`);
260
+ info(` cd ${targetDir}`);
261
+ info(` npm run dev # or your setup command`);
262
+ maybePrintOrderlyDevEnvironmentHints(resolvedDir);
263
+ } catch (err) {
264
+ error(`Failed to create plugin: ${err.message}`);
265
+ if (err.stack) {
266
+ console.error(err.stack);
267
+ }
268
+ }
269
+ },
270
+ };