@schmadev/cli 0.0.0-stage → 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.
@@ -0,0 +1,10 @@
1
+ $ tsup
2
+ CLI Building entry: src/index.ts
3
+ CLI Using tsconfig: tsconfig.json
4
+ CLI tsup v8.5.1
5
+ CLI Using tsup config: /Users/balajim/Documents/Vibe-CMS/packages/cli/tsup.config.ts
6
+ CLI Target: es2022
7
+ CLI Cleaning output folder
8
+ ESM Build start
9
+ ESM dist/index.js 11.91 KB
10
+ ESM ⚡️ Build success in 32ms
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # @schmadev/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official command-line interface for [Schma](https://schma.dev).
4
+
5
+ Schma is the flexible headless CMS that lets you Bring Your Own Database (BYOD) and Bring Your Own AI Keys (BYOK). This CLI provides powerful tools to interact with your Schma workspaces directly from your terminal.
6
+
7
+ ## Installation
8
+
9
+ Normally, you run the CLI via `npx` or include it as a `devDependency` in your project.
10
+
11
+ ```bash
12
+ npm install -D @schmadev/cli
13
+ # or
14
+ pnpm add -D @schmadev/cli
15
+ ```
16
+
17
+ ## Commands
18
+
19
+ ### `generate`
20
+ Syncs your remote Schma content types down to your local codebase and generates strict TypeScript definitions. This ensures your SDK calls are completely type-safe.
21
+
22
+ ```bash
23
+ npx @schmadev/cli generate
24
+ ```
25
+
26
+ ### `login`
27
+ Authenticates your local development environment with your Schma account.
28
+
29
+ ```bash
30
+ npx @schmadev/cli login
31
+ ```
32
+
33
+ ### `link`
34
+ Links your current directory to a specific Schma workspace and generates the necessary configuration files.
35
+
36
+ ```bash
37
+ npx @schmadev/cli link
38
+ ```
39
+
40
+ ## Setup
41
+
42
+ When running commands, the CLI expects a valid `.env` file containing your `SCHMA_API_KEY`, or it will prompt you to log in.
43
+
44
+ If you haven't started a project yet, we recommend using the initialization wizard instead:
45
+ ```bash
46
+ npm create schma@latest
47
+ ```
48
+
49
+ ## Documentation
50
+
51
+ For the full CLI reference, visit [docs.schma.dev](https://docs.schma.dev).
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-api-production.up.railway.app";
174
+ const client = createClient({
175
+ baseUrl: 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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.2",
4
+ "description": "Schma Developer CLI",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "schma": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "vitest run",
12
+ "build": "tsup",
13
+ "dev": "tsup --watch",
14
+ "lint": "eslint \"src/**/*.ts*\"",
15
+ "clean": "rm -rf dist"
16
+ },
17
+ "dependencies": {
18
+ "@schmadev/client": "workspace:*",
19
+ "commander": "^12.1.0",
20
+ "dotenv": "^16.4.5",
21
+ "gradient-string": "^3.0.0",
22
+ "picocolors": "^1.1.0",
23
+ "prompts": "^2.4.2",
24
+ "zod": "^4.6.5"
25
+ },
26
+ "devDependencies": {
27
+ "@schmadev/typescript-config": "workspace:*",
28
+ "@types/gradient-string": "^1.1.6",
29
+ "@types/node": "^20.14.9",
30
+ "@types/prompts": "^2.4.9",
31
+ "tsup": "^8.3.0",
32
+ "typescript": "^5.6.2"
33
+ }
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-api-production.up.railway.app";
58
+
59
+ const client = createClient({
60
+ baseUrl: 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
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@schmadev/typescript-config/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext"
7
+ },
8
+ "include": ["src"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm"],
6
+ dts: false,
7
+ clean: true,
8
+ outDir: "dist",
9
+ });