@schnebel-crm/cli 0.1.0
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/dist/cli.mjs +614 -0
- package/package.json +34 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { build, context } from "esbuild";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
//#region src/utils/log.ts
|
|
12
|
+
const log = {
|
|
13
|
+
success(msg) {
|
|
14
|
+
console.log(`${pc.green("✓")} ${msg}`);
|
|
15
|
+
},
|
|
16
|
+
error(msg) {
|
|
17
|
+
console.error(`${pc.red("✗")} ${msg}`);
|
|
18
|
+
},
|
|
19
|
+
info(msg) {
|
|
20
|
+
console.log(`${pc.blue("ℹ")} ${msg}`);
|
|
21
|
+
},
|
|
22
|
+
warn(msg) {
|
|
23
|
+
console.log(`${pc.yellow("⚠")} ${msg}`);
|
|
24
|
+
},
|
|
25
|
+
step(msg) {
|
|
26
|
+
console.log(`${pc.cyan("→")} ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/commands/init.ts
|
|
32
|
+
const initCommand = new Command("init").description("Create a new Schnebel CRM integration").argument("[name]", "Integration name").action(async (name) => {
|
|
33
|
+
const answers = await prompts([
|
|
34
|
+
{
|
|
35
|
+
type: name ? null : "text",
|
|
36
|
+
name: "name",
|
|
37
|
+
message: "Integration name:",
|
|
38
|
+
initial: name
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
name: "slug",
|
|
43
|
+
message: "Integration slug (kebab-case):",
|
|
44
|
+
initial: (prev) => (name ?? prev).toLowerCase().replace(/\s+/g, "-")
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
name: "description",
|
|
49
|
+
message: "Description:"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: "select",
|
|
53
|
+
name: "category",
|
|
54
|
+
message: "Category:",
|
|
55
|
+
choices: [
|
|
56
|
+
{
|
|
57
|
+
title: "Email",
|
|
58
|
+
value: "email"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: "Calendar",
|
|
62
|
+
value: "calendar"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: "Communication",
|
|
66
|
+
value: "communication"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: "Analytics",
|
|
70
|
+
value: "analytics"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: "Storage",
|
|
74
|
+
value: "storage"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
title: "Payment",
|
|
78
|
+
value: "payment"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: "Marketing",
|
|
82
|
+
value: "marketing"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
title: "Automation",
|
|
86
|
+
value: "automation"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "Other",
|
|
90
|
+
value: "other"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: "select",
|
|
96
|
+
name: "authType",
|
|
97
|
+
message: "Authentication type:",
|
|
98
|
+
choices: [
|
|
99
|
+
{
|
|
100
|
+
title: "API Key",
|
|
101
|
+
value: "api_key"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
title: "OAuth 2.0",
|
|
105
|
+
value: "oauth2"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
title: "Basic Auth",
|
|
109
|
+
value: "basic"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
title: "None",
|
|
113
|
+
value: "none"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: "Custom",
|
|
117
|
+
value: "custom"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
]);
|
|
122
|
+
if (!answers.slug) {
|
|
123
|
+
log.error("Aborted.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const integrationName = name ?? answers.name ?? answers.slug;
|
|
127
|
+
const dir = resolve(answers.slug);
|
|
128
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
129
|
+
await writeFile(join(dir, "package.json"), JSON.stringify({
|
|
130
|
+
name: answers.slug,
|
|
131
|
+
version: "1.0.0",
|
|
132
|
+
type: "module",
|
|
133
|
+
private: true,
|
|
134
|
+
scripts: {
|
|
135
|
+
build: "schnebel build",
|
|
136
|
+
dev: "schnebel dev",
|
|
137
|
+
deploy: "schnebel deploy"
|
|
138
|
+
},
|
|
139
|
+
dependencies: { "@schnebel-crm/integration-sdk": "^0.1.0" },
|
|
140
|
+
devDependencies: {
|
|
141
|
+
"@schnebel-crm/cli": "^0.1.0",
|
|
142
|
+
typescript: "^5"
|
|
143
|
+
}
|
|
144
|
+
}, null, 2));
|
|
145
|
+
await writeFile(join(dir, "tsconfig.json"), JSON.stringify({
|
|
146
|
+
compilerOptions: {
|
|
147
|
+
target: "ESNext",
|
|
148
|
+
module: "ESNext",
|
|
149
|
+
moduleResolution: "bundler",
|
|
150
|
+
strict: true,
|
|
151
|
+
skipLibCheck: true,
|
|
152
|
+
esModuleInterop: true,
|
|
153
|
+
verbatimModuleSyntax: true,
|
|
154
|
+
outDir: "./dist",
|
|
155
|
+
declaration: true
|
|
156
|
+
},
|
|
157
|
+
include: ["src"]
|
|
158
|
+
}, null, 2));
|
|
159
|
+
await writeFile(join(dir, "schnebel.config.ts"), `import { defineConfig } from "@schnebel-crm/integration-sdk/config";
|
|
160
|
+
|
|
161
|
+
export default defineConfig({
|
|
162
|
+
entry: "./src/index.ts",
|
|
163
|
+
// github: "https://github.com/your-org/${answers.slug}",
|
|
164
|
+
});
|
|
165
|
+
`);
|
|
166
|
+
await writeFile(join(dir, "src", "definition.ts"), `import { defineIntegration } from "@schnebel-crm/integration-sdk";
|
|
167
|
+
|
|
168
|
+
export const definition = defineIntegration({
|
|
169
|
+
id: "${answers.slug}",
|
|
170
|
+
name: "${integrationName}",
|
|
171
|
+
description: "${answers.description ?? ""}",
|
|
172
|
+
icon: "IconPlug",
|
|
173
|
+
category: "${answers.category}",
|
|
174
|
+
authType: "${answers.authType}",
|
|
175
|
+
version: "1.0.0",
|
|
176
|
+
webhookSupport: false,
|
|
177
|
+
capabilities: [],
|
|
178
|
+
|
|
179
|
+
configFields: [
|
|
180
|
+
// {
|
|
181
|
+
// key: "example",
|
|
182
|
+
// label: "Example Field",
|
|
183
|
+
// type: "text",
|
|
184
|
+
// required: true,
|
|
185
|
+
// },
|
|
186
|
+
],
|
|
187
|
+
|
|
188
|
+
credentialFields: [${answers.authType === "api_key" ? `
|
|
189
|
+
{
|
|
190
|
+
key: "api_key",
|
|
191
|
+
label: "API Key",
|
|
192
|
+
type: "secret",
|
|
193
|
+
required: true,
|
|
194
|
+
placeholder: "Your API key",
|
|
195
|
+
},` : ""}
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
`);
|
|
199
|
+
await writeFile(join(dir, "src", "handler.ts"), `import { defineHandler, createAction } from "@schnebel-crm/integration-sdk";
|
|
200
|
+
|
|
201
|
+
export const handler = defineHandler({
|
|
202
|
+
async testConnection(ctx) {
|
|
203
|
+
// TODO: Validate credentials and return connection status
|
|
204
|
+
try {
|
|
205
|
+
// Example: await fetch("https://api.example.com/health", {
|
|
206
|
+
// headers: { Authorization: \`Bearer \${ctx.credentials.api_key}\` },
|
|
207
|
+
// });
|
|
208
|
+
return { success: true };
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
error: err instanceof Error ? err.message : "Connection failed",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
actions: [
|
|
218
|
+
// createAction({
|
|
219
|
+
// id: "example_action",
|
|
220
|
+
// name: "Example Action",
|
|
221
|
+
// description: "An example action.",
|
|
222
|
+
// async execute(ctx, input) {
|
|
223
|
+
// return { result: "done" };
|
|
224
|
+
// },
|
|
225
|
+
// }),
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
`);
|
|
229
|
+
await writeFile(join(dir, "src", "index.ts"), `export { definition } from "./definition.js";
|
|
230
|
+
export { handler } from "./handler.js";
|
|
231
|
+
`);
|
|
232
|
+
await writeFile(join(dir, ".gitignore"), `node_modules/
|
|
233
|
+
dist/
|
|
234
|
+
.schnebel/
|
|
235
|
+
`);
|
|
236
|
+
console.log("");
|
|
237
|
+
log.success(`Created ${pc.bold(answers.slug)} integration`);
|
|
238
|
+
console.log("");
|
|
239
|
+
console.log(` ${pc.dim("$")} cd ${answers.slug}`);
|
|
240
|
+
console.log(` ${pc.dim("$")} npm install`);
|
|
241
|
+
console.log(` ${pc.dim("$")} npm run build`);
|
|
242
|
+
console.log("");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/utils/config.ts
|
|
247
|
+
const DEFAULTS = {
|
|
248
|
+
entry: "./src/index.ts",
|
|
249
|
+
outDir: "./dist",
|
|
250
|
+
github: ""
|
|
251
|
+
};
|
|
252
|
+
async function loadConfig(cwd) {
|
|
253
|
+
const configPath = resolve(cwd, "schnebel.config.ts");
|
|
254
|
+
try {
|
|
255
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
256
|
+
const userConfig = mod.default ?? mod;
|
|
257
|
+
return {
|
|
258
|
+
...DEFAULTS,
|
|
259
|
+
...userConfig
|
|
260
|
+
};
|
|
261
|
+
} catch {
|
|
262
|
+
return DEFAULTS;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/commands/build.ts
|
|
268
|
+
const buildCommand = new Command("build").description("Build the integration into a deployable bundle").action(async () => {
|
|
269
|
+
const cwd = process.cwd();
|
|
270
|
+
const config = await loadConfig(cwd);
|
|
271
|
+
const entryPoint = resolve(cwd, config.entry);
|
|
272
|
+
const outDir = resolve(cwd, config.outDir);
|
|
273
|
+
const outFile = join(outDir, "index.mjs");
|
|
274
|
+
log.step("Building integration...");
|
|
275
|
+
try {
|
|
276
|
+
await build({
|
|
277
|
+
entryPoints: [entryPoint],
|
|
278
|
+
bundle: true,
|
|
279
|
+
format: "esm",
|
|
280
|
+
platform: "node",
|
|
281
|
+
target: "node20",
|
|
282
|
+
outfile: outFile,
|
|
283
|
+
external: ["@schnebel-crm/integration-sdk"],
|
|
284
|
+
minify: false,
|
|
285
|
+
sourcemap: false
|
|
286
|
+
});
|
|
287
|
+
log.step("Generating manifest...");
|
|
288
|
+
const mod = await import(pathToFileURL(outFile).href);
|
|
289
|
+
if (!mod.definition) {
|
|
290
|
+
log.error("Bundle must export a 'definition' named export.");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
if (!mod.handler) {
|
|
294
|
+
log.error("Bundle must export a 'handler' named export.");
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
const def = mod.definition;
|
|
298
|
+
const manifest = {
|
|
299
|
+
id: def.id,
|
|
300
|
+
name: def.name,
|
|
301
|
+
version: def.version,
|
|
302
|
+
description: def.description,
|
|
303
|
+
icon: def.icon,
|
|
304
|
+
category: def.category,
|
|
305
|
+
authType: def.authType,
|
|
306
|
+
capabilities: def.capabilities,
|
|
307
|
+
webhookSupport: def.webhookSupport,
|
|
308
|
+
multiInstance: def.multiInstance ?? false,
|
|
309
|
+
configFields: def.configFields,
|
|
310
|
+
credentialFields: def.credentialFields,
|
|
311
|
+
documentationUrl: def.documentationUrl,
|
|
312
|
+
tags: def.tags,
|
|
313
|
+
sdkVersion: "0.1.0"
|
|
314
|
+
};
|
|
315
|
+
await writeFile(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
316
|
+
log.success(`Built ${def.name}@${def.version}`);
|
|
317
|
+
log.info(` Bundle: ${outFile}`);
|
|
318
|
+
log.info(` Manifest: ${join(outDir, "manifest.json")}`);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
log.error(`Build failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/commands/validate.ts
|
|
327
|
+
const validateCommand = new Command("validate").description("Validate the built integration bundle").action(async () => {
|
|
328
|
+
const cwd = process.cwd();
|
|
329
|
+
const outDir = resolve(cwd, (await loadConfig(cwd)).outDir);
|
|
330
|
+
const bundlePath = join(outDir, "index.mjs");
|
|
331
|
+
const manifestPath = join(outDir, "manifest.json");
|
|
332
|
+
let hasErrors = false;
|
|
333
|
+
try {
|
|
334
|
+
await access(bundlePath);
|
|
335
|
+
} catch {
|
|
336
|
+
log.error("Bundle not found. Run 'schnebel build' first.");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
await access(manifestPath);
|
|
341
|
+
} catch {
|
|
342
|
+
log.error("Manifest not found. Run 'schnebel build' first.");
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
log.step("Validating bundle exports...");
|
|
346
|
+
try {
|
|
347
|
+
const mod = await import(pathToFileURL(bundlePath).href);
|
|
348
|
+
if (!mod.definition) {
|
|
349
|
+
log.error("Missing 'definition' export.");
|
|
350
|
+
hasErrors = true;
|
|
351
|
+
} else {
|
|
352
|
+
const def = mod.definition;
|
|
353
|
+
if (!def.id) {
|
|
354
|
+
log.error("definition.id is required");
|
|
355
|
+
hasErrors = true;
|
|
356
|
+
}
|
|
357
|
+
if (!def.name) {
|
|
358
|
+
log.error("definition.name is required");
|
|
359
|
+
hasErrors = true;
|
|
360
|
+
}
|
|
361
|
+
if (!def.version) {
|
|
362
|
+
log.error("definition.version is required");
|
|
363
|
+
hasErrors = true;
|
|
364
|
+
}
|
|
365
|
+
if (!def.category) {
|
|
366
|
+
log.error("definition.category is required");
|
|
367
|
+
hasErrors = true;
|
|
368
|
+
}
|
|
369
|
+
if (!/^[a-z0-9-]+$/.test(def.id)) {
|
|
370
|
+
log.error("definition.id must be lowercase alphanumeric with hyphens only");
|
|
371
|
+
hasErrors = true;
|
|
372
|
+
}
|
|
373
|
+
if (!hasErrors) log.success(`Definition: ${def.name} (${def.id}@${def.version})`);
|
|
374
|
+
}
|
|
375
|
+
if (!mod.handler) {
|
|
376
|
+
log.error("Missing 'handler' export.");
|
|
377
|
+
hasErrors = true;
|
|
378
|
+
} else {
|
|
379
|
+
if (typeof mod.handler.testConnection !== "function") {
|
|
380
|
+
log.error("handler.testConnection must be a function");
|
|
381
|
+
hasErrors = true;
|
|
382
|
+
}
|
|
383
|
+
if (!Array.isArray(mod.handler.actions)) {
|
|
384
|
+
log.error("handler.actions must be an array");
|
|
385
|
+
hasErrors = true;
|
|
386
|
+
}
|
|
387
|
+
if (!hasErrors) log.success(`Handler: testConnection + ${mod.handler.actions.length} action(s)`);
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
log.error(`Failed to load bundle: ${err instanceof Error ? err.message : String(err)}`);
|
|
391
|
+
hasErrors = true;
|
|
392
|
+
}
|
|
393
|
+
if (hasErrors) {
|
|
394
|
+
log.error("Validation failed.");
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
log.success("Validation passed.");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/utils/auth.ts
|
|
402
|
+
const CONFIG_DIR = join(homedir(), ".schnebel");
|
|
403
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
404
|
+
async function saveAuth(config) {
|
|
405
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
406
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
407
|
+
}
|
|
408
|
+
async function loadAuth() {
|
|
409
|
+
try {
|
|
410
|
+
const data = await readFile(CONFIG_FILE, "utf-8");
|
|
411
|
+
return JSON.parse(data);
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/commands/deploy.ts
|
|
419
|
+
const deployCommand = new Command("deploy").description("Deploy the integration to your Schnebel CRM workspace").action(async () => {
|
|
420
|
+
const auth = await loadAuth();
|
|
421
|
+
if (!auth) {
|
|
422
|
+
log.error("Not authenticated. Run 'schnebel login' first.");
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const cwd = process.cwd();
|
|
426
|
+
const outDir = resolve(cwd, (await loadConfig(cwd)).outDir);
|
|
427
|
+
log.step("Reading build artifacts...");
|
|
428
|
+
let bundle;
|
|
429
|
+
let manifest;
|
|
430
|
+
try {
|
|
431
|
+
bundle = await readFile(join(outDir, "index.mjs"));
|
|
432
|
+
manifest = await readFile(join(outDir, "manifest.json"), "utf-8");
|
|
433
|
+
} catch {
|
|
434
|
+
log.error("Build artifacts not found. Run 'schnebel build' first.");
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
const manifestData = JSON.parse(manifest);
|
|
438
|
+
log.step(`Deploying ${manifestData.name}@${manifestData.version}...`);
|
|
439
|
+
try {
|
|
440
|
+
const formData = new FormData();
|
|
441
|
+
formData.append("bundle", new Blob([bundle]), "index.mjs");
|
|
442
|
+
formData.append("manifest", manifest);
|
|
443
|
+
const res = await fetch(`${auth.url}/api/integrations/custom/upload`, {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: { "x-api-key": auth.apiKey },
|
|
446
|
+
body: formData
|
|
447
|
+
});
|
|
448
|
+
if (!res.ok) {
|
|
449
|
+
const body = await res.text();
|
|
450
|
+
log.error(`Deploy failed (${res.status}): ${body}`);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
const result = await res.json();
|
|
454
|
+
log.success(`Deployed ${manifestData.name}@${manifestData.version} to your workspace`);
|
|
455
|
+
log.info(`Slug: ${result.slug}`);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
log.error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/commands/publish.ts
|
|
464
|
+
const publishCommand = new Command("publish").description("Submit the integration for App Store review").action(async () => {
|
|
465
|
+
const auth = await loadAuth();
|
|
466
|
+
if (!auth) {
|
|
467
|
+
log.error("Not authenticated. Run 'schnebel login' first.");
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
const cwd = process.cwd();
|
|
471
|
+
const config = await loadConfig(cwd);
|
|
472
|
+
if (!config.github) {
|
|
473
|
+
log.error("A GitHub repository URL is required for public integrations.");
|
|
474
|
+
log.info("Add 'github' to your schnebel.config.ts:");
|
|
475
|
+
log.info(" github: \"https://github.com/your-org/your-integration\"");
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const repoUrl = config.github.replace("https://github.com/", "https://api.github.com/repos/");
|
|
480
|
+
const res = await fetch(repoUrl);
|
|
481
|
+
if (!res.ok) {
|
|
482
|
+
log.error("GitHub repository not found or not accessible.");
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
if ((await res.json()).private) {
|
|
486
|
+
log.error("GitHub repository must be public for App Store integrations.");
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
} catch {
|
|
490
|
+
log.warn("Could not verify GitHub repository. Proceeding anyway.");
|
|
491
|
+
}
|
|
492
|
+
const outDir = resolve(cwd, config.outDir);
|
|
493
|
+
let manifest;
|
|
494
|
+
try {
|
|
495
|
+
manifest = await readFile(join(outDir, "manifest.json"), "utf-8");
|
|
496
|
+
} catch {
|
|
497
|
+
log.error("Manifest not found. Run 'schnebel build' first.");
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
const manifestData = JSON.parse(manifest);
|
|
501
|
+
log.step(`Submitting ${manifestData.name}@${manifestData.version} for review...`);
|
|
502
|
+
try {
|
|
503
|
+
const res = await fetch(`${auth.url}/api/integrations/custom/${manifestData.id}/submit`, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: {
|
|
506
|
+
"Content-Type": "application/json",
|
|
507
|
+
"x-api-key": auth.apiKey
|
|
508
|
+
},
|
|
509
|
+
body: JSON.stringify({ githubUrl: config.github })
|
|
510
|
+
});
|
|
511
|
+
if (!res.ok) {
|
|
512
|
+
const body = await res.text();
|
|
513
|
+
log.error(`Submit failed (${res.status}): ${body}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
log.success("Submitted for review.");
|
|
517
|
+
log.info("We'll notify you when the review is complete.");
|
|
518
|
+
} catch (err) {
|
|
519
|
+
log.error(`Submit failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/commands/login.ts
|
|
526
|
+
const loginCommand = new Command("login").description("Authenticate with your Schnebel CRM instance").action(async () => {
|
|
527
|
+
const answers = await prompts([{
|
|
528
|
+
type: "text",
|
|
529
|
+
name: "url",
|
|
530
|
+
message: "Schnebel CRM URL:",
|
|
531
|
+
initial: "https://api.schnebel-crm.de"
|
|
532
|
+
}, {
|
|
533
|
+
type: "password",
|
|
534
|
+
name: "apiKey",
|
|
535
|
+
message: "API Key:"
|
|
536
|
+
}]);
|
|
537
|
+
if (!answers.url || !answers.apiKey) {
|
|
538
|
+
log.error("Aborted.");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
log.step("Verifying connection...");
|
|
542
|
+
try {
|
|
543
|
+
const res = await fetch(`${answers.url}/v1/health`, { headers: { "x-api-key": answers.apiKey } });
|
|
544
|
+
if (!res.ok) {
|
|
545
|
+
log.error(`Connection failed (${res.status}). Check your URL and API key.`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
} catch (err) {
|
|
549
|
+
log.error(`Connection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
await saveAuth({
|
|
553
|
+
url: answers.url,
|
|
554
|
+
apiKey: answers.apiKey
|
|
555
|
+
});
|
|
556
|
+
log.success("Authenticated successfully.");
|
|
557
|
+
log.info(`Config saved to ~/.schnebel/config.json`);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/commands/whoami.ts
|
|
562
|
+
const whoamiCommand = new Command("whoami").description("Show current authentication status").action(async () => {
|
|
563
|
+
const auth = await loadAuth();
|
|
564
|
+
if (!auth) {
|
|
565
|
+
log.error("Not authenticated. Run 'schnebel login' first.");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
log.success("Authenticated");
|
|
569
|
+
log.info(`URL: ${auth.url}`);
|
|
570
|
+
log.info(`API Key: ${auth.apiKey.slice(0, 8)}...`);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/commands/dev.ts
|
|
575
|
+
const devCommand = new Command("dev").description("Watch and validate the integration in development mode").action(async () => {
|
|
576
|
+
const cwd = process.cwd();
|
|
577
|
+
const config = await loadConfig(cwd);
|
|
578
|
+
const entryPoint = resolve(cwd, config.entry);
|
|
579
|
+
const outDir = resolve(cwd, config.outDir);
|
|
580
|
+
log.info("Starting dev mode... (press Ctrl+C to stop)");
|
|
581
|
+
try {
|
|
582
|
+
await (await context({
|
|
583
|
+
entryPoints: [entryPoint],
|
|
584
|
+
bundle: true,
|
|
585
|
+
format: "esm",
|
|
586
|
+
platform: "node",
|
|
587
|
+
target: "node20",
|
|
588
|
+
outfile: `${outDir}/index.mjs`,
|
|
589
|
+
external: ["@schnebel-crm/integration-sdk"],
|
|
590
|
+
logLevel: "info"
|
|
591
|
+
})).watch();
|
|
592
|
+
log.success("Watching for changes...");
|
|
593
|
+
} catch (err) {
|
|
594
|
+
log.error(`Dev mode failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/cli.ts
|
|
601
|
+
const program = new Command();
|
|
602
|
+
program.name("schnebel").description("CLI for building and deploying Schnebel CRM integrations").version("0.1.0");
|
|
603
|
+
program.addCommand(initCommand);
|
|
604
|
+
program.addCommand(devCommand);
|
|
605
|
+
program.addCommand(buildCommand);
|
|
606
|
+
program.addCommand(validateCommand);
|
|
607
|
+
program.addCommand(deployCommand);
|
|
608
|
+
program.addCommand(publishCommand);
|
|
609
|
+
program.addCommand(loginCommand);
|
|
610
|
+
program.addCommand(whoamiCommand);
|
|
611
|
+
program.parse();
|
|
612
|
+
|
|
613
|
+
//#endregion
|
|
614
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@schnebel-crm/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI for building and deploying Schnebel CRM integrations",
|
|
6
|
+
"main": "./dist/cli.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"schnebel": "./dist/cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsdown src/cli.ts --format esm --clean",
|
|
13
|
+
"check-types": "tsc --noEmit",
|
|
14
|
+
"prepublishOnly": "pnpm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["schnebel", "crm", "integration", "cli"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/schnebel-it/schnebel-crm"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@schnebel-crm/integration-sdk": "workspace:^",
|
|
24
|
+
"commander": "^13.0.0",
|
|
25
|
+
"esbuild": "^0.25.0",
|
|
26
|
+
"picocolors": "^1.1.0",
|
|
27
|
+
"prompts": "^2.4.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/prompts": "^2.4.9",
|
|
31
|
+
"tsdown": "^0.16.5",
|
|
32
|
+
"typescript": "^5"
|
|
33
|
+
}
|
|
34
|
+
}
|