superacli 1.1.7 → 1.1.9
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/CONTRIBUTING.md +270 -0
- package/README.md +64 -1
- package/__tests__/adapter-schema.test.js +251 -86
- package/__tests__/resend-plugin.test.js +109 -82
- package/cli/adapters/process.js +10 -5
- package/cli/plugins-manager.js +3 -3
- package/cli/skills-catalog.js +169 -10
- package/cli/skills.js +46 -3
- package/cli/supercli.js +214 -78
- package/docs/index.html +183 -123
- package/index.html +384 -0
- package/package.json +2 -2
- package/plugins/openhands/plugin.json +6 -6
- package/plugins/openhands/skills/quickstart/SKILL.md +23 -0
- package/plugins/plugins.json +11 -0
- package/docs/docs.html +0 -224
|
@@ -1,121 +1,286 @@
|
|
|
1
|
-
const { validateAdapterConfig } = require("../cli/adapter-schema")
|
|
1
|
+
const { validateAdapterConfig } = require("../cli/adapter-schema");
|
|
2
2
|
|
|
3
3
|
describe("adapter-schema", () => {
|
|
4
4
|
test("accepts valid process adapter", () => {
|
|
5
|
-
expect(() =>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
expect(() =>
|
|
6
|
+
validateAdapterConfig({
|
|
7
|
+
adapter: "process",
|
|
8
|
+
adapterConfig: { command: "br", baseArgs: ["list"], timeout_ms: 5000 },
|
|
9
|
+
}),
|
|
10
|
+
).not.toThrow();
|
|
11
|
+
});
|
|
10
12
|
|
|
11
13
|
test("ignores unknown adapters", () => {
|
|
12
|
-
expect(() => validateAdapterConfig({ adapter: "custom" })).not.toThrow()
|
|
13
|
-
})
|
|
14
|
+
expect(() => validateAdapterConfig({ adapter: "custom" })).not.toThrow();
|
|
15
|
+
});
|
|
14
16
|
|
|
15
17
|
test("rejects non-object adapterConfig", () => {
|
|
16
|
-
expect(() =>
|
|
17
|
-
|
|
18
|
+
expect(() =>
|
|
19
|
+
validateAdapterConfig({ adapter: "http", adapterConfig: "string" }),
|
|
20
|
+
).toThrow(/requires adapterConfig object/);
|
|
21
|
+
});
|
|
18
22
|
|
|
19
23
|
test("rejects invalid common types", () => {
|
|
20
|
-
expect(() =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
expect(() =>
|
|
25
|
+
validateAdapterConfig({
|
|
26
|
+
adapter: "process",
|
|
27
|
+
adapterConfig: { command: "a", parseJson: "yes" },
|
|
28
|
+
}),
|
|
29
|
+
).toThrow(/must be boolean/);
|
|
30
|
+
expect(() =>
|
|
31
|
+
validateAdapterConfig({
|
|
32
|
+
adapter: "process",
|
|
33
|
+
adapterConfig: { command: "a", cwd: 123 },
|
|
34
|
+
}),
|
|
35
|
+
).toThrow(/must be string/);
|
|
36
|
+
expect(() =>
|
|
37
|
+
validateAdapterConfig({
|
|
38
|
+
adapter: "process",
|
|
39
|
+
adapterConfig: { command: "a", safetyLevel: true },
|
|
40
|
+
}),
|
|
41
|
+
).toThrow(/must be string/);
|
|
42
|
+
});
|
|
24
43
|
|
|
25
44
|
test("timeout_ms validation", () => {
|
|
26
|
-
expect(() =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
expect(() =>
|
|
46
|
+
validateAdapterConfig({
|
|
47
|
+
adapter: "process",
|
|
48
|
+
adapterConfig: { command: "a", timeout_ms: "none" },
|
|
49
|
+
}),
|
|
50
|
+
).toThrow(/positive number/);
|
|
51
|
+
expect(() =>
|
|
52
|
+
validateAdapterConfig({
|
|
53
|
+
adapter: "process",
|
|
54
|
+
adapterConfig: { command: "a", timeout_ms: -1 },
|
|
55
|
+
}),
|
|
56
|
+
).toThrow(/positive number/);
|
|
57
|
+
expect(() =>
|
|
58
|
+
validateAdapterConfig({
|
|
59
|
+
adapter: "process",
|
|
60
|
+
adapterConfig: { command: "a", timeout_ms: 200000 },
|
|
61
|
+
}),
|
|
62
|
+
).toThrow(/cannot exceed 180000ms/);
|
|
63
|
+
});
|
|
30
64
|
|
|
31
65
|
test("http adapter validation", () => {
|
|
32
|
-
expect(() =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
expect(() =>
|
|
67
|
+
validateAdapterConfig({ adapter: "http", adapterConfig: { url: 123 } }),
|
|
68
|
+
).toThrow(/requires adapterConfig.url/);
|
|
69
|
+
expect(() =>
|
|
70
|
+
validateAdapterConfig({
|
|
71
|
+
adapter: "http",
|
|
72
|
+
adapterConfig: { url: "u", method: 1 },
|
|
73
|
+
}),
|
|
74
|
+
).toThrow(/method must be string/);
|
|
75
|
+
expect(() =>
|
|
76
|
+
validateAdapterConfig({ adapter: "http", adapterConfig: { url: "u" } }),
|
|
77
|
+
).not.toThrow();
|
|
78
|
+
});
|
|
36
79
|
|
|
37
80
|
test("openapi adapter validation", () => {
|
|
38
|
-
expect(() =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
81
|
+
expect(() =>
|
|
82
|
+
validateAdapterConfig({ adapter: "openapi", adapterConfig: { spec: 1 } }),
|
|
83
|
+
).toThrow(/requires adapterConfig.spec/);
|
|
84
|
+
expect(() =>
|
|
85
|
+
validateAdapterConfig({
|
|
86
|
+
adapter: "openapi",
|
|
87
|
+
adapterConfig: { spec: "s", operationId: 1 },
|
|
88
|
+
}),
|
|
89
|
+
).toThrow(/requires adapterConfig.operationId/);
|
|
90
|
+
expect(() =>
|
|
91
|
+
validateAdapterConfig({
|
|
92
|
+
adapter: "openapi",
|
|
93
|
+
adapterConfig: { spec: "s", operationId: "o" },
|
|
94
|
+
}),
|
|
95
|
+
).not.toThrow();
|
|
96
|
+
});
|
|
42
97
|
|
|
43
98
|
test("mcp adapter validation", () => {
|
|
44
|
-
expect(() =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
expect(() =>
|
|
48
|
-
|
|
49
|
-
|
|
99
|
+
expect(() =>
|
|
100
|
+
validateAdapterConfig({ adapter: "mcp", adapterConfig: { tool: 1 } }),
|
|
101
|
+
).toThrow(/requires adapterConfig.tool/);
|
|
102
|
+
expect(() =>
|
|
103
|
+
validateAdapterConfig({ adapter: "mcp", adapterConfig: { tool: "t" } }),
|
|
104
|
+
).toThrow(/requires one source/);
|
|
105
|
+
expect(() =>
|
|
106
|
+
validateAdapterConfig({
|
|
107
|
+
adapter: "mcp",
|
|
108
|
+
adapterConfig: { tool: "t", server: "s" },
|
|
109
|
+
}),
|
|
110
|
+
).not.toThrow();
|
|
111
|
+
expect(() =>
|
|
112
|
+
validateAdapterConfig({
|
|
113
|
+
adapter: "mcp",
|
|
114
|
+
adapterConfig: { tool: "t", server: "s", timeout_ms: 180000 },
|
|
115
|
+
}),
|
|
116
|
+
).not.toThrow();
|
|
117
|
+
expect(() =>
|
|
118
|
+
validateAdapterConfig({
|
|
119
|
+
adapter: "mcp",
|
|
120
|
+
adapterConfig: { tool: "t", server: "s", timeout_ms: 180001 },
|
|
121
|
+
}),
|
|
122
|
+
).toThrow(/cannot exceed 180000ms/);
|
|
123
|
+
});
|
|
50
124
|
|
|
51
125
|
test("process adapter validation", () => {
|
|
52
|
-
expect(() =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(() =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
126
|
+
expect(() =>
|
|
127
|
+
validateAdapterConfig({
|
|
128
|
+
adapter: "process",
|
|
129
|
+
adapterConfig: { command: 1 },
|
|
130
|
+
}),
|
|
131
|
+
).toThrow(/requires adapterConfig.command/);
|
|
132
|
+
expect(() =>
|
|
133
|
+
validateAdapterConfig({
|
|
134
|
+
adapter: "process",
|
|
135
|
+
adapterConfig: { command: "a", baseArgs: "none" },
|
|
136
|
+
}),
|
|
137
|
+
).toThrow(/must be an array/);
|
|
138
|
+
expect(() =>
|
|
139
|
+
validateAdapterConfig({
|
|
140
|
+
adapter: "process",
|
|
141
|
+
adapterConfig: { command: "a", positionalArgs: "none" },
|
|
142
|
+
}),
|
|
143
|
+
).toThrow(/must be an array/);
|
|
144
|
+
expect(() =>
|
|
145
|
+
validateAdapterConfig({
|
|
146
|
+
adapter: "process",
|
|
147
|
+
adapterConfig: { command: "a", passthrough: "yes" },
|
|
148
|
+
}),
|
|
149
|
+
).toThrow(/must be boolean/);
|
|
150
|
+
expect(() =>
|
|
151
|
+
validateAdapterConfig({
|
|
152
|
+
adapter: "process",
|
|
153
|
+
adapterConfig: { command: "a", flagsBeforePositionals: "yes" },
|
|
154
|
+
}),
|
|
155
|
+
).toThrow(/must be boolean/);
|
|
156
|
+
expect(() =>
|
|
157
|
+
validateAdapterConfig({
|
|
158
|
+
adapter: "process",
|
|
159
|
+
adapterConfig: { command: "a", missingDependencyHelp: 1 },
|
|
160
|
+
}),
|
|
161
|
+
).toThrow(/must be string/);
|
|
162
|
+
expect(() =>
|
|
163
|
+
validateAdapterConfig({
|
|
164
|
+
adapter: "process",
|
|
165
|
+
adapterConfig: { command: "a", env: [] },
|
|
166
|
+
}),
|
|
167
|
+
).toThrow(/must be object/);
|
|
168
|
+
expect(() =>
|
|
169
|
+
validateAdapterConfig({
|
|
170
|
+
adapter: "process",
|
|
171
|
+
adapterConfig: { command: "a", stream: true },
|
|
172
|
+
}),
|
|
173
|
+
).toThrow(/must be string/);
|
|
174
|
+
expect(() =>
|
|
175
|
+
validateAdapterConfig({
|
|
176
|
+
adapter: "process",
|
|
177
|
+
adapterConfig: { command: "a", stream: "sse" },
|
|
178
|
+
}),
|
|
179
|
+
).toThrow(/must be 'jsonl'/);
|
|
180
|
+
expect(() =>
|
|
181
|
+
validateAdapterConfig({
|
|
182
|
+
adapter: "process",
|
|
183
|
+
adapterConfig: { command: "a", stream: "jsonl" },
|
|
184
|
+
}),
|
|
185
|
+
).not.toThrow();
|
|
186
|
+
});
|
|
63
187
|
|
|
64
188
|
test("builtin adapter validation", () => {
|
|
65
|
-
expect(() =>
|
|
66
|
-
|
|
67
|
-
|
|
189
|
+
expect(() =>
|
|
190
|
+
validateAdapterConfig({
|
|
191
|
+
adapter: "builtin",
|
|
192
|
+
adapterConfig: { builtin: 1 },
|
|
193
|
+
}),
|
|
194
|
+
).toThrow(/requires adapterConfig.builtin/);
|
|
195
|
+
expect(() =>
|
|
196
|
+
validateAdapterConfig({
|
|
197
|
+
adapter: "builtin",
|
|
198
|
+
adapterConfig: { builtin: "b" },
|
|
199
|
+
}),
|
|
200
|
+
).not.toThrow();
|
|
201
|
+
});
|
|
68
202
|
|
|
69
203
|
test("shell adapter validation", () => {
|
|
70
|
-
expect(() =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
204
|
+
expect(() =>
|
|
205
|
+
validateAdapterConfig({
|
|
206
|
+
adapter: "shell",
|
|
207
|
+
adapterConfig: { script: 1, unsafe: true },
|
|
208
|
+
}),
|
|
209
|
+
).toThrow(/requires adapterConfig.script/);
|
|
210
|
+
expect(() =>
|
|
211
|
+
validateAdapterConfig({
|
|
212
|
+
adapter: "shell",
|
|
213
|
+
adapterConfig: { script: "s", unsafe: true, shell: 1 },
|
|
214
|
+
}),
|
|
215
|
+
).toThrow(/shell must be string/);
|
|
216
|
+
expect(() =>
|
|
217
|
+
validateAdapterConfig({
|
|
218
|
+
adapter: "shell",
|
|
219
|
+
adapterConfig: { script: "s", unsafe: true, env: "none" },
|
|
220
|
+
}),
|
|
221
|
+
).toThrow(/must be object/);
|
|
222
|
+
});
|
|
74
223
|
|
|
75
224
|
test("rejects interactive config", () => {
|
|
76
|
-
expect(() =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
225
|
+
expect(() =>
|
|
226
|
+
validateAdapterConfig({
|
|
227
|
+
adapter: "http",
|
|
228
|
+
adapterConfig: { url: "https://example.com", non_interactive: false },
|
|
229
|
+
}),
|
|
230
|
+
).toThrow(/Interactive command execution is not supported/);
|
|
231
|
+
});
|
|
81
232
|
|
|
82
233
|
test("requires unsafe true for shell adapter", () => {
|
|
83
|
-
expect(() =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
234
|
+
expect(() =>
|
|
235
|
+
validateAdapterConfig({
|
|
236
|
+
adapter: "shell",
|
|
237
|
+
adapterConfig: { script: "echo hi" },
|
|
238
|
+
}),
|
|
239
|
+
).toThrow(/unsafe=true/);
|
|
240
|
+
});
|
|
88
241
|
|
|
89
242
|
test("accepts shell adapter with unsafe true", () => {
|
|
90
|
-
expect(() =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
243
|
+
expect(() =>
|
|
244
|
+
validateAdapterConfig({
|
|
245
|
+
adapter: "shell",
|
|
246
|
+
adapterConfig: {
|
|
247
|
+
script: "echo '{\"ok\":true}'",
|
|
248
|
+
unsafe: true,
|
|
249
|
+
timeout_ms: 1000,
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
).not.toThrow();
|
|
253
|
+
});
|
|
95
254
|
|
|
96
255
|
test("accepts process safety metadata", () => {
|
|
97
|
-
expect(() =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
256
|
+
expect(() =>
|
|
257
|
+
validateAdapterConfig({
|
|
258
|
+
adapter: "process",
|
|
259
|
+
adapterConfig: {
|
|
260
|
+
command: "docker",
|
|
261
|
+
safetyLevel: "guarded",
|
|
262
|
+
interactiveFlags: ["-i", "--interactive"],
|
|
263
|
+
requiresInteractive: false,
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
).not.toThrow();
|
|
267
|
+
});
|
|
107
268
|
|
|
108
269
|
test("rejects non-array interactiveFlags", () => {
|
|
109
|
-
expect(() =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
270
|
+
expect(() =>
|
|
271
|
+
validateAdapterConfig({
|
|
272
|
+
adapter: "process",
|
|
273
|
+
adapterConfig: { command: "docker", interactiveFlags: "--tty" },
|
|
274
|
+
}),
|
|
275
|
+
).toThrow(/interactiveFlags must be an array/);
|
|
276
|
+
});
|
|
114
277
|
|
|
115
278
|
test("rejects non-string interactiveFlags values", () => {
|
|
116
|
-
expect(() =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
279
|
+
expect(() =>
|
|
280
|
+
validateAdapterConfig({
|
|
281
|
+
adapter: "process",
|
|
282
|
+
adapterConfig: { command: "docker", interactiveFlags: ["--tty", 42] },
|
|
283
|
+
}),
|
|
284
|
+
).toThrow(/interactiveFlags values must be strings/);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -1,122 +1,149 @@
|
|
|
1
|
-
const fs = require("fs")
|
|
2
|
-
const os = require("os")
|
|
3
|
-
const path = require("path")
|
|
4
|
-
const { execSync } = require("child_process")
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { execSync } = require("child_process");
|
|
5
5
|
|
|
6
|
-
const CLI = path.join(__dirname, "..", "cli", "supercli.js")
|
|
7
|
-
const nodeDir = path.dirname(process.execPath)
|
|
6
|
+
const CLI = path.join(__dirname, "..", "cli", "supercli.js");
|
|
7
|
+
const nodeDir = path.dirname(process.execPath);
|
|
8
8
|
|
|
9
9
|
function runNoServer(args, options = {}) {
|
|
10
10
|
try {
|
|
11
|
-
const env = { ...process.env }
|
|
12
|
-
delete env.SUPERCLI_SERVER
|
|
11
|
+
const env = { ...process.env };
|
|
12
|
+
delete env.SUPERCLI_SERVER;
|
|
13
13
|
const out = execSync(`node ${CLI} ${args}`, {
|
|
14
14
|
encoding: "utf-8",
|
|
15
15
|
timeout: 15000,
|
|
16
|
-
env: { ...env, ...(options.env || {}) }
|
|
17
|
-
})
|
|
18
|
-
return { ok: true, output: out.trim(), code: 0 }
|
|
16
|
+
env: { ...env, ...(options.env || {}) },
|
|
17
|
+
});
|
|
18
|
+
return { ok: true, output: out.trim(), code: 0 };
|
|
19
19
|
} catch (err) {
|
|
20
20
|
return {
|
|
21
21
|
ok: false,
|
|
22
22
|
output: (err.stdout || "").trim(),
|
|
23
23
|
stderr: (err.stderr || "").trim(),
|
|
24
|
-
code: err.status
|
|
25
|
-
}
|
|
24
|
+
code: err.status,
|
|
25
|
+
};
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function writeFakeCurlBinary(dir) {
|
|
30
|
-
const bin = path.join(dir, "curl")
|
|
31
|
-
fs.writeFileSync(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
const bin = path.join(dir, "curl");
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
bin,
|
|
33
|
+
[
|
|
34
|
+
"#!/usr/bin/env node",
|
|
35
|
+
"const args = process.argv.slice(2);",
|
|
36
|
+
"const url = args[args.length - 1] || '';",
|
|
37
|
+
"if (args.includes('--version')) { console.log('curl 8.0.0-test'); process.exit(0); }",
|
|
38
|
+
"if (url.endsWith('/README.md')) { console.log('# Resend CLI\\n\\nTest readme.'); process.exit(0); }",
|
|
39
|
+
"process.exit(22);",
|
|
40
|
+
].join("\n"),
|
|
41
|
+
"utf-8",
|
|
42
|
+
);
|
|
43
|
+
fs.chmodSync(bin, 0o755);
|
|
44
|
+
return bin;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
describe("resend hybrid plugin", () => {
|
|
44
|
-
const fakeBinDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-resend-bin-"))
|
|
45
|
-
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-resend-"))
|
|
46
|
-
const fakeUserHome = fs.mkdtempSync(
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const fakeBinDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-resend-bin-"));
|
|
49
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-resend-"));
|
|
50
|
+
const fakeUserHome = fs.mkdtempSync(
|
|
51
|
+
path.join(os.tmpdir(), "dcli-user-home-resend-"),
|
|
52
|
+
);
|
|
53
|
+
writeFakeCurlBinary(fakeBinDir);
|
|
54
|
+
|
|
49
55
|
const env = {
|
|
50
56
|
...process.env,
|
|
51
57
|
PATH: `${fakeBinDir}:${process.env.PATH || ""}`,
|
|
52
58
|
SUPERCLI_HOME: tempHome,
|
|
53
|
-
HOME: fakeUserHome
|
|
54
|
-
}
|
|
55
|
-
let removed = false
|
|
59
|
+
HOME: fakeUserHome,
|
|
60
|
+
};
|
|
61
|
+
let removed = false;
|
|
56
62
|
|
|
57
63
|
beforeAll(() => {
|
|
58
64
|
// Install the plugin
|
|
59
|
-
const install = runNoServer(
|
|
65
|
+
const install = runNoServer(
|
|
66
|
+
"plugins install ./plugins/resend --on-conflict replace --json",
|
|
67
|
+
{ env },
|
|
68
|
+
);
|
|
60
69
|
if (!install.ok) {
|
|
61
|
-
|
|
70
|
+
console.error("Install failed:", install.output, install.stderr);
|
|
62
71
|
}
|
|
63
|
-
expect(install.ok).toBe(true)
|
|
64
|
-
})
|
|
72
|
+
expect(install.ok).toBe(true);
|
|
73
|
+
});
|
|
65
74
|
|
|
66
75
|
afterAll(() => {
|
|
67
|
-
if (!removed) runNoServer("plugins remove resend --json", { env })
|
|
68
|
-
fs.rmSync(fakeBinDir, { recursive: true, force: true })
|
|
69
|
-
fs.rmSync(tempHome, { recursive: true, force: true })
|
|
70
|
-
fs.rmSync(fakeUserHome, { recursive: true, force: true })
|
|
71
|
-
})
|
|
76
|
+
if (!removed) runNoServer("plugins remove resend --json", { env });
|
|
77
|
+
fs.rmSync(fakeBinDir, { recursive: true, force: true });
|
|
78
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
79
|
+
fs.rmSync(fakeUserHome, { recursive: true, force: true });
|
|
80
|
+
});
|
|
72
81
|
|
|
73
82
|
test("indexes resend skills provider", () => {
|
|
74
|
-
const list = runNoServer("skills list --catalog --provider resend --json", {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
expect(
|
|
78
|
-
|
|
83
|
+
const list = runNoServer("skills list --catalog --provider resend --json", {
|
|
84
|
+
env,
|
|
85
|
+
});
|
|
86
|
+
expect(list.ok).toBe(true);
|
|
87
|
+
const listData = JSON.parse(list.output);
|
|
88
|
+
expect(
|
|
89
|
+
listData.skills.some((skill) => skill.id === "resend:root.readme"),
|
|
90
|
+
).toBe(true);
|
|
91
|
+
});
|
|
79
92
|
|
|
80
93
|
test("fetches indexed remote skill markdown", () => {
|
|
81
|
-
const skill = runNoServer("skills get resend:root.readme", { env })
|
|
82
|
-
expect(skill.ok).toBe(true)
|
|
83
|
-
expect(skill.output).toContain("Resend CLI")
|
|
84
|
-
})
|
|
94
|
+
const skill = runNoServer("skills get resend:root.readme", { env });
|
|
95
|
+
expect(skill.ok).toBe(true);
|
|
96
|
+
expect(skill.output).toContain("Resend CLI");
|
|
97
|
+
});
|
|
85
98
|
|
|
86
99
|
test("fails with helpful message when resend binary is missing", () => {
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
// Skip test if resend is installed in the same directory as node
|
|
101
|
+
const resendInNodeDir = fs.existsSync(path.join(nodeDir, "resend"));
|
|
102
|
+
if (resendInNodeDir) {
|
|
103
|
+
console.log("Skipping test: resend is installed in node directory");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create a minimal PATH with node but without resend
|
|
108
|
+
// Only include fakeBinDir (has curl) and nodeDir (has node)
|
|
109
|
+
const minimalPath = `${fakeBinDir}:${nodeDir}`;
|
|
110
|
+
|
|
111
|
+
const r = runNoServer("resend cli doctor --json", {
|
|
112
|
+
env: { ...env, PATH: minimalPath },
|
|
113
|
+
});
|
|
114
|
+
expect(r.ok).toBe(false);
|
|
115
|
+
const data = JSON.parse(r.output || r.stderr);
|
|
116
|
+
expect(data.error.message).toContain("Missing dependency 'resend'");
|
|
117
|
+
expect(data.error.message).toContain("Please run 'dcli resend cli setup'");
|
|
118
|
+
});
|
|
96
119
|
|
|
97
120
|
test("exposes expanded structured commands", () => {
|
|
98
|
-
const help = runNoServer("help resend --json", { env })
|
|
99
|
-
expect(help.ok).toBe(true)
|
|
100
|
-
const data = JSON.parse(help.output)
|
|
101
|
-
|
|
102
|
-
const resendNamespace = data.namespaces.find(n => n.name === "resend")
|
|
103
|
-
expect(resendNamespace).toBeDefined()
|
|
104
|
-
|
|
105
|
-
const resources = new Set(resendNamespace.resources.map(r => r.name))
|
|
106
|
-
|
|
107
|
-
expect(resources.has("domains")).toBe(true)
|
|
108
|
-
expect(resources.has("api-keys")).toBe(true)
|
|
109
|
-
expect(resources.has("webhooks")).toBe(true)
|
|
110
|
-
expect(resources.has("contacts")).toBe(true)
|
|
111
|
-
expect(resources.has("auth")).toBe(true)
|
|
112
|
-
})
|
|
121
|
+
const help = runNoServer("help resend --json", { env });
|
|
122
|
+
expect(help.ok).toBe(true);
|
|
123
|
+
const data = JSON.parse(help.output);
|
|
124
|
+
|
|
125
|
+
const resendNamespace = data.namespaces.find((n) => n.name === "resend");
|
|
126
|
+
expect(resendNamespace).toBeDefined();
|
|
127
|
+
|
|
128
|
+
const resources = new Set(resendNamespace.resources.map((r) => r.name));
|
|
129
|
+
|
|
130
|
+
expect(resources.has("domains")).toBe(true);
|
|
131
|
+
expect(resources.has("api-keys")).toBe(true);
|
|
132
|
+
expect(resources.has("webhooks")).toBe(true);
|
|
133
|
+
expect(resources.has("contacts")).toBe(true);
|
|
134
|
+
expect(resources.has("auth")).toBe(true);
|
|
135
|
+
});
|
|
113
136
|
|
|
114
137
|
test("doctor reports node and npm dependencies as healthy", () => {
|
|
115
|
-
const r = runNoServer("plugins doctor resend --json", { env })
|
|
116
|
-
expect(r.ok).toBe(true)
|
|
117
|
-
const data = JSON.parse(r.output)
|
|
118
|
-
expect(data.ok).toBe(true)
|
|
119
|
-
expect(data.checks.some(c => c.binary === "node" && c.ok === true)).toBe(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
const r = runNoServer("plugins doctor resend --json", { env });
|
|
139
|
+
expect(r.ok).toBe(true);
|
|
140
|
+
const data = JSON.parse(r.output);
|
|
141
|
+
expect(data.ok).toBe(true);
|
|
142
|
+
expect(data.checks.some((c) => c.binary === "node" && c.ok === true)).toBe(
|
|
143
|
+
true,
|
|
144
|
+
);
|
|
145
|
+
expect(data.checks.some((c) => c.binary === "npm" && c.ok === true)).toBe(
|
|
146
|
+
true,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
package/cli/adapters/process.js
CHANGED
|
@@ -19,11 +19,7 @@ function toCliFlags(flags) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function preflightBinary(binary) {
|
|
22
|
-
|
|
23
|
-
if (r.error) {
|
|
24
|
-
return { ok: false, reason: r.error.message }
|
|
25
|
-
}
|
|
26
|
-
return { ok: r.status === 0, reason: (r.stderr || "").trim() }
|
|
22
|
+
return { ok: true, reason: "" };
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
function buildSafetyViolation(details) {
|
|
@@ -175,16 +171,25 @@ async function execute(cmd, flags, context = {}) {
|
|
|
175
171
|
}
|
|
176
172
|
|
|
177
173
|
if (passthroughMode) {
|
|
174
|
+
// Passthrough mode: use __rawArgs from flags (collected from remaining positional args)
|
|
178
175
|
const passthroughArgs = Array.isArray(flags.__rawArgs) ? flags.__rawArgs : []
|
|
179
176
|
args.push(...passthroughArgs)
|
|
180
177
|
} else {
|
|
178
|
+
// Normal mode: collect positional values from flags OR from __positionalArgs
|
|
181
179
|
const positionalValues = []
|
|
180
|
+
|
|
181
|
+
// First, try to get positional values from flags (defined args)
|
|
182
182
|
for (const name of positionalNames) {
|
|
183
183
|
if (remainingFlags[name] !== undefined) {
|
|
184
184
|
positionalValues.push(String(remainingFlags[name]))
|
|
185
185
|
delete remainingFlags[name]
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
+
|
|
189
|
+
// Also support __positionalArgs array for commands that need raw positional values
|
|
190
|
+
if (Array.isArray(flags.__positionalArgs)) {
|
|
191
|
+
positionalValues.push(...flags.__positionalArgs)
|
|
192
|
+
}
|
|
188
193
|
|
|
189
194
|
const flagArgs = []
|
|
190
195
|
if (includeJsonFlag) flagArgs.push(includeJsonFlag)
|