@schmadev/cli 0.0.0-stage → 1.0.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/index.js +322 -0
- package/package.json +31 -3
- package/src/commands/generate.ts +131 -0
- package/src/commands/link.ts +63 -0
- package/src/commands/login.ts +63 -0
- package/src/index.ts +37 -0
- package/src/utils/auth.ts +44 -0
- package/src/utils/project.ts +48 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +9 -0
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/login.ts
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/utils/auth.ts
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import os from "os";
|
|
14
|
+
var CONFIG_DIR = path.join(os.homedir(), ".schma");
|
|
15
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
16
|
+
function getConfig() {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
19
|
+
const data = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
20
|
+
return JSON.parse(data);
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
function saveConfig(config) {
|
|
27
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
28
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
31
|
+
}
|
|
32
|
+
function getApiKey() {
|
|
33
|
+
if (process.env.SCHMA_API_KEY) {
|
|
34
|
+
return process.env.SCHMA_API_KEY;
|
|
35
|
+
}
|
|
36
|
+
const config = getConfig();
|
|
37
|
+
if (config.apiKey) {
|
|
38
|
+
return config.apiKey;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/commands/login.ts
|
|
44
|
+
import gradient from "gradient-string";
|
|
45
|
+
var SCHMA_LOGO = `
|
|
46
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557
|
|
47
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
48
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
|
|
49
|
+
\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551
|
|
50
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
51
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
|
|
52
|
+
`;
|
|
53
|
+
var prism = gradient(["#C0C0C0", "#FFFFFF", "#FFD700", "#FF8C00", "#8A2BE2", "#00FFFF"]);
|
|
54
|
+
async function loginCommand() {
|
|
55
|
+
console.log(prism.multiline(SCHMA_LOGO));
|
|
56
|
+
console.log(pc.bold(prism("Welcome to the Schma CLI!")));
|
|
57
|
+
console.log(pc.gray("Authenticate your local machine to interact with the Headless CMS."));
|
|
58
|
+
console.log("");
|
|
59
|
+
const response = await prompts({
|
|
60
|
+
type: "password",
|
|
61
|
+
name: "apiKey",
|
|
62
|
+
message: "Enter your Schma API Key (Delivery or Management):",
|
|
63
|
+
validate: (val) => val.length > 0 || "API Key is required"
|
|
64
|
+
});
|
|
65
|
+
const apiKey = response.apiKey;
|
|
66
|
+
if (!apiKey) {
|
|
67
|
+
console.error(pc.red("Login cancelled."));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
console.log(pc.cyan("Verifying API Key..."));
|
|
71
|
+
try {
|
|
72
|
+
if (!apiKey.startsWith("schma_")) {
|
|
73
|
+
console.log(pc.yellow("Warning: The API key does not look like a standard Schma API Key."));
|
|
74
|
+
}
|
|
75
|
+
const config = getConfig();
|
|
76
|
+
config.apiKey = apiKey;
|
|
77
|
+
saveConfig(config);
|
|
78
|
+
console.log(pc.green("\u2714 Successfully authenticated!"));
|
|
79
|
+
console.log(pc.gray("Your API key is securely stored in ~/.schma/config.json"));
|
|
80
|
+
console.log("");
|
|
81
|
+
console.log(pc.gray("Next step: run ") + pc.cyan("schma generate --tenant <your_tenant_slug>") + pc.gray(" to pull your content types."));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(pc.red("Failed to authenticate."));
|
|
84
|
+
console.error(error);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/commands/generate.ts
|
|
90
|
+
import pc2 from "picocolors";
|
|
91
|
+
import fs3 from "fs";
|
|
92
|
+
import path3 from "path";
|
|
93
|
+
import { createClient } from "@schmadev/client";
|
|
94
|
+
|
|
95
|
+
// src/utils/project.ts
|
|
96
|
+
import fs2 from "fs";
|
|
97
|
+
import path2 from "path";
|
|
98
|
+
var LOCAL_CONFIG_DIR = ".schma";
|
|
99
|
+
var LOCAL_CONFIG_FILE = "config.json";
|
|
100
|
+
function getProjectConfigDir() {
|
|
101
|
+
return path2.join(process.cwd(), LOCAL_CONFIG_DIR);
|
|
102
|
+
}
|
|
103
|
+
function getProjectConfigPath() {
|
|
104
|
+
return path2.join(getProjectConfigDir(), LOCAL_CONFIG_FILE);
|
|
105
|
+
}
|
|
106
|
+
function getProjectConfig() {
|
|
107
|
+
try {
|
|
108
|
+
const configPath = getProjectConfigPath();
|
|
109
|
+
if (fs2.existsSync(configPath)) {
|
|
110
|
+
const data = fs2.readFileSync(configPath, "utf-8");
|
|
111
|
+
return JSON.parse(data);
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
}
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
function saveProjectConfig(config) {
|
|
118
|
+
const configDir = getProjectConfigDir();
|
|
119
|
+
const configPath = getProjectConfigPath();
|
|
120
|
+
if (!fs2.existsSync(configDir)) {
|
|
121
|
+
fs2.mkdirSync(configDir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
const gitignorePath = path2.join(configDir, ".gitignore");
|
|
124
|
+
if (!fs2.existsSync(gitignorePath)) {
|
|
125
|
+
fs2.writeFileSync(gitignorePath, "# Schma local config can be committed to source control\n# Do NOT add your API keys here\n");
|
|
126
|
+
}
|
|
127
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/commands/generate.ts
|
|
131
|
+
function mapSchemaTypeToTypescript(type, field) {
|
|
132
|
+
switch (type) {
|
|
133
|
+
case "text":
|
|
134
|
+
case "markdown":
|
|
135
|
+
case "string":
|
|
136
|
+
case "media":
|
|
137
|
+
case "richText":
|
|
138
|
+
return "string";
|
|
139
|
+
case "boolean":
|
|
140
|
+
return "boolean";
|
|
141
|
+
case "date":
|
|
142
|
+
return "string | Date";
|
|
143
|
+
case "number":
|
|
144
|
+
return "number";
|
|
145
|
+
case "json":
|
|
146
|
+
case "image":
|
|
147
|
+
return "Record<string, unknown>";
|
|
148
|
+
case "reference":
|
|
149
|
+
if (field?.targetType) {
|
|
150
|
+
const refName = field.targetType.charAt(0).toUpperCase() + field.targetType.slice(1).replace(/-./g, (x) => x[1].toUpperCase());
|
|
151
|
+
return `${refName} | string`;
|
|
152
|
+
}
|
|
153
|
+
return "Record<string, unknown> | string";
|
|
154
|
+
default:
|
|
155
|
+
return "any";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function generateCommand(options) {
|
|
159
|
+
const apiKey = getApiKey();
|
|
160
|
+
if (!apiKey) {
|
|
161
|
+
console.error(pc2.red("Error: Unauthenticated."));
|
|
162
|
+
console.error(`Run ${pc2.cyan("schma login")} first or set the ${pc2.cyan("SCHMA_API_KEY")} environment variable.`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const projectConfig = getProjectConfig();
|
|
166
|
+
const tenantSlug = options.tenant || process.env.SCHMA_TENANT_SLUG || projectConfig.tenantSlug;
|
|
167
|
+
if (!tenantSlug) {
|
|
168
|
+
console.error(pc2.red("Error: Tenant Slug not provided."));
|
|
169
|
+
console.error(`Run ${pc2.cyan("schma link")}, pass ${pc2.cyan("--tenant <slug>")}, or set the ${pc2.cyan("SCHMA_TENANT_SLUG")} environment variable.`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
console.log(pc2.cyan(`Fetching schemas for tenant '${tenantSlug}'...`));
|
|
173
|
+
const apiUrl = process.env.SCHMA_API_URL || projectConfig.apiUrl || "https://schma-cms-production.up.railway.app";
|
|
174
|
+
const client = createClient({
|
|
175
|
+
apiUrl,
|
|
176
|
+
tenantSlug,
|
|
177
|
+
apiKey
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
const res = await client.contentTypes.list();
|
|
181
|
+
const contentTypes = res.data || [];
|
|
182
|
+
if (contentTypes.length === 0) {
|
|
183
|
+
console.log(pc2.yellow("No content types found for this tenant."));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log(pc2.gray(`Found ${contentTypes.length} content types. Generating types...`));
|
|
187
|
+
let dtsContent = `// Automatically generated by Schma CLI
|
|
188
|
+
// Do not edit this file manually
|
|
189
|
+
|
|
190
|
+
`;
|
|
191
|
+
dtsContent += `import type { SchmaClient } from '@schmadev/client';
|
|
192
|
+
|
|
193
|
+
`;
|
|
194
|
+
const modelNames = [];
|
|
195
|
+
for (const ct of contentTypes) {
|
|
196
|
+
const interfaceName = ct.slug.charAt(0).toUpperCase() + ct.slug.slice(1).replace(/-./g, (x) => x[1].toUpperCase());
|
|
197
|
+
modelNames.push(`'${ct.slug}': ${interfaceName}`);
|
|
198
|
+
dtsContent += `/**
|
|
199
|
+
* Content Model: ${ct.name}
|
|
200
|
+
* Description: ${ct.description || "No description"}
|
|
201
|
+
*/
|
|
202
|
+
`;
|
|
203
|
+
dtsContent += `export interface ${interfaceName} {
|
|
204
|
+
`;
|
|
205
|
+
dtsContent += ` id: string;
|
|
206
|
+
`;
|
|
207
|
+
dtsContent += ` createdAt: string;
|
|
208
|
+
`;
|
|
209
|
+
dtsContent += ` updatedAt: string;
|
|
210
|
+
`;
|
|
211
|
+
dtsContent += ` publishedAt?: string;
|
|
212
|
+
`;
|
|
213
|
+
dtsContent += ` locale: string;
|
|
214
|
+
`;
|
|
215
|
+
dtsContent += ` status: 'draft' | 'published' | 'archived';
|
|
216
|
+
`;
|
|
217
|
+
const fields = ct.schemaJson.fields || [];
|
|
218
|
+
for (const field of fields) {
|
|
219
|
+
const tsType = mapSchemaTypeToTypescript(field.type, field);
|
|
220
|
+
const optional = field.required ? "" : "?";
|
|
221
|
+
dtsContent += ` /** ${field.label || field.name} (${field.type}) */
|
|
222
|
+
`;
|
|
223
|
+
dtsContent += ` ${field.name}${optional}: ${tsType};
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
dtsContent += `}
|
|
227
|
+
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
dtsContent += `export interface SchmaModels {
|
|
231
|
+
${modelNames.join(";\n ")}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
`;
|
|
235
|
+
dtsContent += `export type TypedSchmaClient = Omit<SchmaClient, 'delivery'> & {
|
|
236
|
+
delivery: {
|
|
237
|
+
getEntry<T extends keyof SchmaModels>(options: { id: string }): Promise<SchmaModels[T]>;
|
|
238
|
+
listEntries<T extends keyof SchmaModels>(options?: { type?: T; limit?: number; offset?: number; locale?: string; preview?: string }): Promise<{ data: SchmaModels[T][], total: number }>;
|
|
239
|
+
searchSimilar<T extends keyof SchmaModels>(options: { text: string; limit?: number; preview?: string }): Promise<{ data: SchmaModels[T][], total: number }>;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
`;
|
|
243
|
+
const outPath = path3.resolve(process.cwd(), options.out);
|
|
244
|
+
const outDir = path3.dirname(outPath);
|
|
245
|
+
if (!fs3.existsSync(outDir)) {
|
|
246
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
247
|
+
}
|
|
248
|
+
fs3.writeFileSync(outPath, dtsContent, "utf-8");
|
|
249
|
+
console.log(pc2.green(`\u2714 Successfully generated types to ${options.out}`));
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(pc2.red("Failed to fetch schemas."));
|
|
252
|
+
if (error.message) {
|
|
253
|
+
console.error(pc2.red(error.message));
|
|
254
|
+
}
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/index.ts
|
|
260
|
+
import dotenv from "dotenv";
|
|
261
|
+
|
|
262
|
+
// src/commands/link.ts
|
|
263
|
+
import prompts2 from "prompts";
|
|
264
|
+
import pc3 from "picocolors";
|
|
265
|
+
import { createClient as createClient2 } from "@schmadev/client";
|
|
266
|
+
async function linkCommand() {
|
|
267
|
+
console.log(pc3.bold("Link Project to Schma Tenant"));
|
|
268
|
+
console.log(pc3.gray("This will create a local .schma/config.json file for this project."));
|
|
269
|
+
console.log("");
|
|
270
|
+
const apiKey = getApiKey();
|
|
271
|
+
if (!apiKey) {
|
|
272
|
+
console.error(pc3.red("Error: Unauthenticated."));
|
|
273
|
+
console.error(`Run ${pc3.cyan("schma login")} first or set the ${pc3.cyan("SCHMA_API_KEY")} environment variable.`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
const response = await prompts2({
|
|
277
|
+
type: "text",
|
|
278
|
+
name: "tenantSlug",
|
|
279
|
+
message: "Enter your Schma Tenant Slug (e.g. my-store):",
|
|
280
|
+
validate: (val) => val.length > 0 || "Tenant Slug is required"
|
|
281
|
+
});
|
|
282
|
+
const tenantSlug = response.tenantSlug;
|
|
283
|
+
if (!tenantSlug) {
|
|
284
|
+
console.error(pc3.red("Link cancelled."));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
const apiUrl = process.env.SCHMA_API_URL || "https://schma-cms-production.up.railway.app";
|
|
288
|
+
console.log(pc3.cyan(`
|
|
289
|
+
Verifying connection to '${tenantSlug}'...`));
|
|
290
|
+
const client = createClient2({
|
|
291
|
+
apiUrl,
|
|
292
|
+
tenantSlug,
|
|
293
|
+
apiKey
|
|
294
|
+
});
|
|
295
|
+
try {
|
|
296
|
+
await client.contentTypes.list();
|
|
297
|
+
saveProjectConfig({
|
|
298
|
+
tenantSlug,
|
|
299
|
+
apiUrl
|
|
300
|
+
});
|
|
301
|
+
console.log(pc3.green("\u2714 Successfully linked project!"));
|
|
302
|
+
console.log(pc3.gray(`Local configuration saved to .schma/config.json`));
|
|
303
|
+
console.log("");
|
|
304
|
+
console.log(pc3.gray("You can now run ") + pc3.cyan("schma generate") + pc3.gray(" without specifying a tenant."));
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error(pc3.red(`Failed to link to tenant '${tenantSlug}'.`));
|
|
307
|
+
console.error(pc3.red("Check your tenant slug and API key."));
|
|
308
|
+
if (error.message) {
|
|
309
|
+
console.error(pc3.gray(error.message));
|
|
310
|
+
}
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/index.ts
|
|
316
|
+
dotenv.config();
|
|
317
|
+
var program = new Command();
|
|
318
|
+
program.name("schma").description("Schma Developer CLI for interacting with Headless CMS").version("1.0.0");
|
|
319
|
+
program.command("login").description("Authenticate and store your API key globally").action(loginCommand);
|
|
320
|
+
program.command("link").description("Link the current directory to a Schma project (tenant)").action(linkCommand);
|
|
321
|
+
program.command("generate").description("Pull schemas and generate TypeScript types").option("-t, --tenant <tenantSlug>", "The tenant slug to generate types for (or reads from SCHMA_TENANT_SLUG env var / local config)").option("-o, --out <path>", "Output file path", "schma-types.d.ts").action(generateCommand);
|
|
322
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmadev/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Schma Developer CLI",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"schma": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@schmadev/client": "1.0.0",
|
|
12
|
+
"commander": "^12.1.0",
|
|
13
|
+
"dotenv": "^16.4.5",
|
|
14
|
+
"gradient-string": "^3.0.0",
|
|
15
|
+
"picocolors": "^1.1.0",
|
|
16
|
+
"prompts": "^2.4.2",
|
|
17
|
+
"zod": "^4.6.5"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@schmadev/typescript-config": "0.0.0",
|
|
21
|
+
"@types/gradient-string": "^1.1.6",
|
|
22
|
+
"@types/node": "^20.14.9",
|
|
23
|
+
"@types/prompts": "^2.4.9",
|
|
24
|
+
"tsup": "^8.3.0",
|
|
25
|
+
"typescript": "^5.6.2"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsup --watch",
|
|
31
|
+
"lint": "eslint \"src/**/*.ts*\"",
|
|
32
|
+
"clean": "rm -rf dist"
|
|
33
|
+
}
|
|
6
34
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getApiKey } from '../utils/auth.js';
|
|
5
|
+
import { createClient } from '@schmadev/client';
|
|
6
|
+
|
|
7
|
+
import { getProjectConfig } from '../utils/project.js';
|
|
8
|
+
|
|
9
|
+
function mapSchemaTypeToTypescript(type: string, field?: any): string {
|
|
10
|
+
switch (type) {
|
|
11
|
+
case 'text':
|
|
12
|
+
case 'markdown':
|
|
13
|
+
case 'string':
|
|
14
|
+
case 'media':
|
|
15
|
+
case 'richText':
|
|
16
|
+
return 'string';
|
|
17
|
+
case 'boolean':
|
|
18
|
+
return 'boolean';
|
|
19
|
+
case 'date':
|
|
20
|
+
return 'string | Date';
|
|
21
|
+
case 'number':
|
|
22
|
+
return 'number';
|
|
23
|
+
case 'json':
|
|
24
|
+
case 'image':
|
|
25
|
+
return 'Record<string, unknown>';
|
|
26
|
+
case 'reference':
|
|
27
|
+
if (field?.targetType) {
|
|
28
|
+
const refName = field.targetType.charAt(0).toUpperCase() +
|
|
29
|
+
field.targetType.slice(1).replace(/-./g, (x: string) => x[1].toUpperCase());
|
|
30
|
+
return `${refName} | string`;
|
|
31
|
+
}
|
|
32
|
+
return 'Record<string, unknown> | string';
|
|
33
|
+
default:
|
|
34
|
+
return 'any';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function generateCommand(options: { tenant?: string, out: string }) {
|
|
39
|
+
const apiKey = getApiKey();
|
|
40
|
+
if (!apiKey) {
|
|
41
|
+
console.error(pc.red('Error: Unauthenticated.'));
|
|
42
|
+
console.error(`Run ${pc.cyan('schma login')} first or set the ${pc.cyan('SCHMA_API_KEY')} environment variable.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const projectConfig = getProjectConfig();
|
|
47
|
+
const tenantSlug = options.tenant || process.env.SCHMA_TENANT_SLUG || projectConfig.tenantSlug;
|
|
48
|
+
|
|
49
|
+
if (!tenantSlug) {
|
|
50
|
+
console.error(pc.red('Error: Tenant Slug not provided.'));
|
|
51
|
+
console.error(`Run ${pc.cyan('schma link')}, pass ${pc.cyan('--tenant <slug>')}, or set the ${pc.cyan('SCHMA_TENANT_SLUG')} environment variable.`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(pc.cyan(`Fetching schemas for tenant '${tenantSlug}'...`));
|
|
56
|
+
|
|
57
|
+
const apiUrl = process.env.SCHMA_API_URL || projectConfig.apiUrl || "https://schma-cms-production.up.railway.app";
|
|
58
|
+
|
|
59
|
+
const client = createClient({
|
|
60
|
+
apiUrl,
|
|
61
|
+
tenantSlug,
|
|
62
|
+
apiKey,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const res = await client.contentTypes.list();
|
|
67
|
+
const contentTypes = res.data || [];
|
|
68
|
+
|
|
69
|
+
if (contentTypes.length === 0) {
|
|
70
|
+
console.log(pc.yellow('No content types found for this tenant.'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(pc.gray(`Found ${contentTypes.length} content types. Generating types...`));
|
|
75
|
+
|
|
76
|
+
let dtsContent = `// Automatically generated by Schma CLI\n// Do not edit this file manually\n\n`;
|
|
77
|
+
dtsContent += `import type { SchmaClient } from '@schmadev/client';\n\n`;
|
|
78
|
+
|
|
79
|
+
const modelNames: string[] = [];
|
|
80
|
+
|
|
81
|
+
for (const ct of contentTypes) {
|
|
82
|
+
const interfaceName = ct.slug.charAt(0).toUpperCase() + ct.slug.slice(1).replace(/-./g, (x: string) => x[1].toUpperCase());
|
|
83
|
+
modelNames.push(`'${ct.slug}': ${interfaceName}`);
|
|
84
|
+
|
|
85
|
+
dtsContent += `/**\n * Content Model: ${ct.name}\n * Description: ${ct.description || 'No description'}\n */\n`;
|
|
86
|
+
dtsContent += `export interface ${interfaceName} {\n`;
|
|
87
|
+
dtsContent += ` id: string;\n`;
|
|
88
|
+
dtsContent += ` createdAt: string;\n`;
|
|
89
|
+
dtsContent += ` updatedAt: string;\n`;
|
|
90
|
+
dtsContent += ` publishedAt?: string;\n`;
|
|
91
|
+
dtsContent += ` locale: string;\n`;
|
|
92
|
+
dtsContent += ` status: 'draft' | 'published' | 'archived';\n`;
|
|
93
|
+
|
|
94
|
+
const fields = (ct.schemaJson as any).fields || [];
|
|
95
|
+
for (const field of fields) {
|
|
96
|
+
const tsType = mapSchemaTypeToTypescript(field.type, field);
|
|
97
|
+
const optional = field.required ? '' : '?';
|
|
98
|
+
dtsContent += ` /** ${field.label || field.name} (${field.type}) */\n`;
|
|
99
|
+
dtsContent += ` ${field.name}${optional}: ${tsType};\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dtsContent += `}\n\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
dtsContent += `export interface SchmaModels {\n ${modelNames.join(';\n ')}\n}\n\n`;
|
|
106
|
+
|
|
107
|
+
dtsContent += `export type TypedSchmaClient = Omit<SchmaClient, 'delivery'> & {
|
|
108
|
+
delivery: {
|
|
109
|
+
getEntry<T extends keyof SchmaModels>(options: { id: string }): Promise<SchmaModels[T]>;
|
|
110
|
+
listEntries<T extends keyof SchmaModels>(options?: { type?: T; limit?: number; offset?: number; locale?: string; preview?: string }): Promise<{ data: SchmaModels[T][], total: number }>;
|
|
111
|
+
searchSimilar<T extends keyof SchmaModels>(options: { text: string; limit?: number; preview?: string }): Promise<{ data: SchmaModels[T][], total: number }>;
|
|
112
|
+
}
|
|
113
|
+
};\n`;
|
|
114
|
+
|
|
115
|
+
const outPath = path.resolve(process.cwd(), options.out);
|
|
116
|
+
// ensure dir exists
|
|
117
|
+
const outDir = path.dirname(outPath);
|
|
118
|
+
if (!fs.existsSync(outDir)) {
|
|
119
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
fs.writeFileSync(outPath, dtsContent, 'utf-8');
|
|
122
|
+
|
|
123
|
+
console.log(pc.green(`✔ Successfully generated types to ${options.out}`));
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
console.error(pc.red('Failed to fetch schemas.'));
|
|
126
|
+
if (error.message) {
|
|
127
|
+
console.error(pc.red(error.message));
|
|
128
|
+
}
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getApiKey } from '../utils/auth.js';
|
|
4
|
+
import { saveProjectConfig } from '../utils/project.js';
|
|
5
|
+
import { createClient } from '@schmadev/client';
|
|
6
|
+
|
|
7
|
+
export async function linkCommand() {
|
|
8
|
+
console.log(pc.bold('Link Project to Schma Tenant'));
|
|
9
|
+
console.log(pc.gray('This will create a local .schma/config.json file for this project.'));
|
|
10
|
+
console.log('');
|
|
11
|
+
|
|
12
|
+
const apiKey = getApiKey();
|
|
13
|
+
if (!apiKey) {
|
|
14
|
+
console.error(pc.red('Error: Unauthenticated.'));
|
|
15
|
+
console.error(`Run ${pc.cyan('schma login')} first or set the ${pc.cyan('SCHMA_API_KEY')} environment variable.`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const response = await prompts({
|
|
20
|
+
type: 'text',
|
|
21
|
+
name: 'tenantSlug',
|
|
22
|
+
message: 'Enter your Schma Tenant Slug (e.g. my-store):',
|
|
23
|
+
validate: (val: string) => val.length > 0 || 'Tenant Slug is required'
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const tenantSlug = response.tenantSlug;
|
|
27
|
+
if (!tenantSlug) {
|
|
28
|
+
console.error(pc.red('Link cancelled.'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const apiUrl = process.env.SCHMA_API_URL || "https://schma-cms-production.up.railway.app";
|
|
33
|
+
|
|
34
|
+
console.log(pc.cyan(`\nVerifying connection to '${tenantSlug}'...`));
|
|
35
|
+
|
|
36
|
+
const client = createClient({
|
|
37
|
+
apiUrl,
|
|
38
|
+
tenantSlug,
|
|
39
|
+
apiKey,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Attempt to fetch content types to verify access
|
|
44
|
+
await client.contentTypes.list();
|
|
45
|
+
|
|
46
|
+
saveProjectConfig({
|
|
47
|
+
tenantSlug,
|
|
48
|
+
apiUrl,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(pc.green('✔ Successfully linked project!'));
|
|
52
|
+
console.log(pc.gray(`Local configuration saved to .schma/config.json`));
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(pc.gray('You can now run ') + pc.cyan('schma generate') + pc.gray(' without specifying a tenant.'));
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
console.error(pc.red(`Failed to link to tenant '${tenantSlug}'.`));
|
|
57
|
+
console.error(pc.red('Check your tenant slug and API key.'));
|
|
58
|
+
if (error.message) {
|
|
59
|
+
console.error(pc.gray(error.message));
|
|
60
|
+
}
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { saveConfig, getConfig } from '../utils/auth.js';
|
|
4
|
+
import { createClient } from '@schmadev/client';
|
|
5
|
+
import gradient from 'gradient-string';
|
|
6
|
+
|
|
7
|
+
const SCHMA_LOGO = `
|
|
8
|
+
██████╗ ██████╗██╗ ██╗███╗ ███╗ █████╗
|
|
9
|
+
██╔════╝ ██╔════╝██║ ██║████╗ ████║██╔══██╗
|
|
10
|
+
███████╗ ██║ ███████║██╔████╔██║███████║
|
|
11
|
+
╚════██║ ██║ ██╔══██║██║╚██╔╝██║██╔══██║
|
|
12
|
+
███████║ ╚██████╗██║ ██║██║ ╚═╝ ██║██║ ██║
|
|
13
|
+
╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const prism = gradient(['#C0C0C0', '#FFFFFF', '#FFD700', '#FF8C00', '#8A2BE2', '#00FFFF']);
|
|
17
|
+
|
|
18
|
+
export async function loginCommand() {
|
|
19
|
+
console.log(prism.multiline(SCHMA_LOGO));
|
|
20
|
+
console.log(pc.bold(prism('Welcome to the Schma CLI!')));
|
|
21
|
+
console.log(pc.gray('Authenticate your local machine to interact with the Headless CMS.'));
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
const response = await prompts({
|
|
25
|
+
type: 'password',
|
|
26
|
+
name: 'apiKey',
|
|
27
|
+
message: 'Enter your Schma API Key (Delivery or Management):',
|
|
28
|
+
validate: (val: string) => val.length > 0 || 'API Key is required'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const apiKey = response.apiKey;
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
console.error(pc.red('Login cancelled.'));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(pc.cyan('Verifying API Key...'));
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Attempt a lightweight test by configuring a client without a tenant
|
|
41
|
+
// Since api keys are tenant-bound, we don't necessarily know the tenant yet.
|
|
42
|
+
// However, the /content-types endpoint requires a tenant.
|
|
43
|
+
// In our backend, API keys are globally unique strings starting with `schma_`.
|
|
44
|
+
// Let's just save it. The actual validation happens during `generate`.
|
|
45
|
+
|
|
46
|
+
if (!apiKey.startsWith('schma_')) {
|
|
47
|
+
console.log(pc.yellow('Warning: The API key does not look like a standard Schma API Key.'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const config = getConfig();
|
|
51
|
+
config.apiKey = apiKey;
|
|
52
|
+
saveConfig(config);
|
|
53
|
+
|
|
54
|
+
console.log(pc.green('✔ Successfully authenticated!'));
|
|
55
|
+
console.log(pc.gray('Your API key is securely stored in ~/.schma/config.json'));
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(pc.gray('Next step: run ') + pc.cyan('schma generate --tenant <your_tenant_slug>') + pc.gray(' to pull your content types.'));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(pc.red('Failed to authenticate.'));
|
|
60
|
+
console.error(error);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { loginCommand } from './commands/login.js';
|
|
5
|
+
import { generateCommand } from './commands/generate.js';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
|
|
8
|
+
import { linkCommand } from './commands/link.js';
|
|
9
|
+
|
|
10
|
+
// Load .env files if present (for SCHMA_API_KEY override)
|
|
11
|
+
dotenv.config();
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('schma')
|
|
17
|
+
.description('Schma Developer CLI for interacting with Headless CMS')
|
|
18
|
+
.version('1.0.0');
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('login')
|
|
22
|
+
.description('Authenticate and store your API key globally')
|
|
23
|
+
.action(loginCommand);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('link')
|
|
27
|
+
.description('Link the current directory to a Schma project (tenant)')
|
|
28
|
+
.action(linkCommand);
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('generate')
|
|
32
|
+
.description('Pull schemas and generate TypeScript types')
|
|
33
|
+
.option('-t, --tenant <tenantSlug>', 'The tenant slug to generate types for (or reads from SCHMA_TENANT_SLUG env var / local config)')
|
|
34
|
+
.option('-o, --out <path>', 'Output file path', 'schma-types.d.ts')
|
|
35
|
+
.action(generateCommand);
|
|
36
|
+
|
|
37
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.schma');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
|
|
8
|
+
export interface SchmaConfig {
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getConfig(): SchmaConfig {
|
|
13
|
+
try {
|
|
14
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
15
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
16
|
+
return JSON.parse(data);
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
// Ignore read errors
|
|
20
|
+
}
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function saveConfig(config: SchmaConfig) {
|
|
25
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
26
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getApiKey(): string | null {
|
|
32
|
+
// 1. Environment variable override (CI/CD)
|
|
33
|
+
if (process.env.SCHMA_API_KEY) {
|
|
34
|
+
return process.env.SCHMA_API_KEY;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Global config
|
|
38
|
+
const config = getConfig();
|
|
39
|
+
if (config.apiKey) {
|
|
40
|
+
return config.apiKey;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const LOCAL_CONFIG_DIR = '.schma';
|
|
5
|
+
const LOCAL_CONFIG_FILE = 'config.json';
|
|
6
|
+
|
|
7
|
+
export interface SchmaProjectConfig {
|
|
8
|
+
tenantSlug?: string;
|
|
9
|
+
apiUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getProjectConfigDir(): string {
|
|
13
|
+
return path.join(process.cwd(), LOCAL_CONFIG_DIR);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getProjectConfigPath(): string {
|
|
17
|
+
return path.join(getProjectConfigDir(), LOCAL_CONFIG_FILE);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getProjectConfig(): SchmaProjectConfig {
|
|
21
|
+
try {
|
|
22
|
+
const configPath = getProjectConfigPath();
|
|
23
|
+
if (fs.existsSync(configPath)) {
|
|
24
|
+
const data = fs.readFileSync(configPath, 'utf-8');
|
|
25
|
+
return JSON.parse(data);
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// Ignore read errors
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveProjectConfig(config: SchmaProjectConfig) {
|
|
34
|
+
const configDir = getProjectConfigDir();
|
|
35
|
+
const configPath = getProjectConfigPath();
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(configDir)) {
|
|
38
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add warning to .gitignore
|
|
42
|
+
const gitignorePath = path.join(configDir, '.gitignore');
|
|
43
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
44
|
+
fs.writeFileSync(gitignorePath, '# Schma local config can be committed to source control\n# Do NOT add your API keys here\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
48
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
package/README.md
DELETED