@cmssy/cli 1.0.0 → 9.1.1

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 ADDED
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { createInterface } from "readline/promises";
5
+
6
+ // src/link.ts
7
+ import { existsSync, readFileSync, writeFileSync } from "fs";
8
+ import { join } from "path";
9
+ import {
10
+ buildEditorUrl,
11
+ checkDraftSecret,
12
+ checkWorkspaceReachable
13
+ } from "@cmssy/core/preflight";
14
+
15
+ // src/admin-client.ts
16
+ var DEFAULT_ADMIN_API_URL = "https://api.cmssy.io/graphql";
17
+ var BAD_TOKEN_HINT = {
18
+ message: "the cmssy API rejected the token",
19
+ fix: "create an API token in the cmssy dashboard (Settings \u2192 API Tokens) and pass it via --token or CMSSY_API_TOKEN"
20
+ };
21
+ var NEEDS_PAGES_EDIT_HINT = {
22
+ message: "the token's user cannot read this workspace's settings",
23
+ fix: "the token's user needs the PAGES_EDIT permission in the selected workspace"
24
+ };
25
+ var CliError = class extends Error {
26
+ fix;
27
+ constructor(message, fix) {
28
+ super(message);
29
+ this.name = "CliError";
30
+ this.fix = fix;
31
+ }
32
+ };
33
+ function isDenied(errors, status) {
34
+ return status === 401 || status === 403 || errors.some(
35
+ (error) => error.extensions?.code === "UNAUTHENTICATED" || error.extensions?.code === "FORBIDDEN" || /unauthenticated|unauthorized|not authorized|not authenticated|invalid token|permission|forbidden|not allowed|access denied/i.test(
36
+ error.message ?? ""
37
+ )
38
+ );
39
+ }
40
+ async function adminGraphql(query, variables, options) {
41
+ const doFetch = options.fetch ?? globalThis.fetch;
42
+ const url = options.apiUrl?.trim() || DEFAULT_ADMIN_API_URL;
43
+ let response;
44
+ try {
45
+ response = await doFetch(url, {
46
+ method: "POST",
47
+ headers: {
48
+ "content-type": "application/json",
49
+ authorization: `Bearer ${options.token}`,
50
+ ...options.workspaceId ? { "x-workspace-id": options.workspaceId } : {}
51
+ },
52
+ body: JSON.stringify({ query, variables })
53
+ });
54
+ } catch {
55
+ throw new CliError(
56
+ `cannot reach the cmssy API at ${url}`,
57
+ "check your network connection and CMSSY_API_URL (leave it unset for cmssy cloud)"
58
+ );
59
+ }
60
+ let envelope = null;
61
+ try {
62
+ envelope = await response.json();
63
+ } catch {
64
+ envelope = null;
65
+ }
66
+ const errors = Array.isArray(envelope?.errors) ? envelope.errors : [];
67
+ if (isDenied(errors, response.status)) {
68
+ const denied = options.denied ?? BAD_TOKEN_HINT;
69
+ throw new CliError(denied.message, denied.fix);
70
+ }
71
+ if (errors.length > 0) {
72
+ throw new CliError(
73
+ `the cmssy API rejected the request - ${errors.map((error) => error.message ?? "GraphQL error").join("; ")}`
74
+ );
75
+ }
76
+ if (!response.ok || envelope?.data === void 0) {
77
+ throw new CliError(`the cmssy API responded with HTTP ${response.status}`);
78
+ }
79
+ return envelope.data;
80
+ }
81
+ var WORKSPACES_MINE_QUERY = `query CliWorkspacesMine {
82
+ workspace {
83
+ mine {
84
+ id
85
+ slug
86
+ name
87
+ organizationSlug
88
+ }
89
+ }
90
+ }`;
91
+ var DRAFT_SECRET_QUERY = `query CliDraftSecret {
92
+ workspace {
93
+ draftSecret
94
+ }
95
+ }`;
96
+ var UPDATE_PREVIEW_URL_MUTATION = `mutation CliSetPreviewUrl($input: UpdateSiteConfigInput!) {
97
+ siteConfig {
98
+ update(input: $input) {
99
+ previewUrl
100
+ }
101
+ }
102
+ }`;
103
+ async function fetchMyWorkspaces(options) {
104
+ const data = await adminGraphql(WORKSPACES_MINE_QUERY, {}, options);
105
+ return data.workspace.mine;
106
+ }
107
+ async function fetchDraftSecret(options) {
108
+ const data = await adminGraphql(
109
+ DRAFT_SECRET_QUERY,
110
+ {},
111
+ { ...options, denied: NEEDS_PAGES_EDIT_HINT }
112
+ );
113
+ return data.workspace.draftSecret;
114
+ }
115
+ async function setPreviewUrl(previewUrl, options) {
116
+ await adminGraphql(
117
+ UPDATE_PREVIEW_URL_MUTATION,
118
+ { input: { previewUrl } },
119
+ { ...options, denied: NEEDS_PAGES_EDIT_HINT }
120
+ );
121
+ }
122
+
123
+ // src/env-file.ts
124
+ function parseEnvFile(content) {
125
+ const vars = {};
126
+ for (const rawLine of content.split(/\r?\n/)) {
127
+ const line = rawLine.trim();
128
+ if (!line || line.startsWith("#")) continue;
129
+ const withoutExport = line.replace(/^export\s+/, "");
130
+ const separator = withoutExport.indexOf("=");
131
+ if (separator <= 0) continue;
132
+ const key = withoutExport.slice(0, separator).trim();
133
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
134
+ let value = withoutExport.slice(separator + 1).trim();
135
+ const quoted = value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"));
136
+ if (quoted) {
137
+ value = value.slice(1, -1);
138
+ } else {
139
+ const comment = value.indexOf(" #");
140
+ if (comment !== -1) value = value.slice(0, comment).trim();
141
+ }
142
+ vars[key] = value;
143
+ }
144
+ return vars;
145
+ }
146
+ function applyEnv(vars, env) {
147
+ const applied = [];
148
+ for (const [key, value] of Object.entries(vars)) {
149
+ if (env[key] !== void 0) continue;
150
+ env[key] = value;
151
+ applied.push(key);
152
+ }
153
+ return applied;
154
+ }
155
+
156
+ // src/env-write.ts
157
+ function needsQuoting(value) {
158
+ return /[\s#"']/.test(value) || value === "";
159
+ }
160
+ function serialize(key, value) {
161
+ return needsQuoting(value) ? `${key}="${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : `${key}=${value}`;
162
+ }
163
+ function mergeEnvContent(existing, updates) {
164
+ const pending = new Map(Object.entries(updates));
165
+ const lines = existing ? existing.split(/\r?\n/) : [];
166
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
167
+ const merged = lines.map((line) => {
168
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
169
+ const key = match?.[1];
170
+ if (!key || !pending.has(key)) return line;
171
+ const value = pending.get(key);
172
+ pending.delete(key);
173
+ return serialize(key, value);
174
+ });
175
+ for (const [key, value] of pending) {
176
+ merged.push(serialize(key, value));
177
+ }
178
+ return `${merged.join("\n")}
179
+ `;
180
+ }
181
+
182
+ // src/format.ts
183
+ var GREEN = "\x1B[32m";
184
+ var RED = "\x1B[31m";
185
+ var YELLOW = "\x1B[33m";
186
+ var CYAN = "\x1B[36m";
187
+ var RESET = "\x1B[0m";
188
+ function useColor(env = process.env, isTty = Boolean(process.stdout.isTTY)) {
189
+ return isTty && env.NO_COLOR === void 0;
190
+ }
191
+ function paint(text, color, colored) {
192
+ return colored ? `${color}${text}${RESET}` : text;
193
+ }
194
+ function formatResult(result, colored = useColor()) {
195
+ if (result.status === "ok") {
196
+ return `${paint("\u2713", GREEN, colored)} ${result.message}`;
197
+ }
198
+ if (result.status === "unknown") {
199
+ return `${paint("?", YELLOW, colored)} ${result.message}`;
200
+ }
201
+ const fix = result.fix ? `
202
+ fix: ${result.fix}` : "";
203
+ return `${paint("\u2717", RED, colored)} ${result.message}${fix}`;
204
+ }
205
+ function formatEditorLink(url, colored = useColor()) {
206
+ return `
207
+ Edit this site visually:
208
+ ${paint(url, CYAN, colored)}
209
+ `;
210
+ }
211
+
212
+ // src/link.ts
213
+ var ENV_FILES = [".env.local", ".env"];
214
+ function loadEnvFiles(cwd, env) {
215
+ for (const file of ENV_FILES) {
216
+ const path = join(cwd, file);
217
+ if (!existsSync(path)) continue;
218
+ applyEnv(parseEnvFile(readFileSync(path, "utf8")), env);
219
+ }
220
+ }
221
+ function resolveToken(options, deps) {
222
+ const token = options.token?.trim() || deps.env.CMSSY_API_TOKEN?.trim();
223
+ if (!token) {
224
+ throw new CliError(
225
+ "no API token given",
226
+ "create an API token in the cmssy dashboard (Settings \u2192 API Tokens), then run cmssy link --token cs_... or set CMSSY_API_TOKEN"
227
+ );
228
+ }
229
+ return token;
230
+ }
231
+ function describeWorkspace(workspace) {
232
+ const org = workspace.organizationSlug ?? "?";
233
+ return `${workspace.name} (${org}/${workspace.slug})`;
234
+ }
235
+ async function selectWorkspace(workspaces, options, deps) {
236
+ if (workspaces.length === 0) {
237
+ throw new CliError(
238
+ "the token's user has no workspaces",
239
+ "create a workspace in the cmssy dashboard, or use a token from a user who is a member of one"
240
+ );
241
+ }
242
+ const slugs = workspaces.map((workspace) => `${workspace.organizationSlug}/${workspace.slug}`).join(", ");
243
+ if (options.workspace) {
244
+ const wanted = options.workspace.trim();
245
+ const match = workspaces.find(
246
+ (workspace) => workspace.slug === wanted || `${workspace.organizationSlug}/${workspace.slug}` === wanted
247
+ );
248
+ if (!match) {
249
+ throw new CliError(
250
+ `no workspace named "${wanted}"`,
251
+ `pass one of: ${slugs}`
252
+ );
253
+ }
254
+ return match;
255
+ }
256
+ if (workspaces.length === 1) {
257
+ return workspaces[0];
258
+ }
259
+ if (!deps.isTty) {
260
+ throw new CliError(
261
+ "several workspaces are available and there is no terminal to ask in",
262
+ `pass --workspace <slug> - one of: ${slugs}`
263
+ );
264
+ }
265
+ deps.log("Which workspace should this app be linked to?");
266
+ workspaces.forEach((workspace, index) => {
267
+ deps.log(` ${index + 1}. ${describeWorkspace(workspace)}`);
268
+ });
269
+ const answer = await deps.ask(`Workspace [1-${workspaces.length}]: `);
270
+ const choice = Number(answer.trim());
271
+ const selected = Number.isInteger(choice) && choice >= 1 && choice <= workspaces.length ? workspaces[choice - 1] : void 0;
272
+ if (!selected) {
273
+ throw new CliError(
274
+ `"${answer.trim()}" is not a number between 1 and ${workspaces.length}`,
275
+ "run cmssy link again, or pass --workspace <slug>"
276
+ );
277
+ }
278
+ return selected;
279
+ }
280
+ function resolvePreviewUrl(options) {
281
+ if (!options.previewUrl) return null;
282
+ let origin;
283
+ try {
284
+ origin = new URL(options.previewUrl).origin;
285
+ } catch {
286
+ throw new CliError(
287
+ `"${options.previewUrl}" is not a valid URL`,
288
+ "pass --preview-url with your DEPLOYED site origin, e.g. --preview-url https://example.com"
289
+ );
290
+ }
291
+ const { hostname } = new URL(origin);
292
+ if (hostname === "localhost" || hostname === "127.0.0.1") {
293
+ throw new CliError(
294
+ "the workspace preview URL is the DEPLOYED site every editor in the workspace previews - not your localhost",
295
+ "for local development, toggle dev mode in the cmssy editor and enter your local host there (per user, nothing shared)"
296
+ );
297
+ }
298
+ return origin;
299
+ }
300
+ function writeEnvLocal(cwd, updates) {
301
+ const path = join(cwd, ".env.local");
302
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
303
+ writeFileSync(path, mergeEnvContent(existing, updates));
304
+ }
305
+ async function runLink(options, deps) {
306
+ const { cwd, env, log } = deps;
307
+ try {
308
+ loadEnvFiles(cwd, env);
309
+ const token = resolveToken(options, deps);
310
+ const admin = {
311
+ token,
312
+ apiUrl: env.CMSSY_API_URL,
313
+ fetch: deps.fetch
314
+ };
315
+ const workspaces = await fetchMyWorkspaces(admin);
316
+ const workspace = await selectWorkspace(workspaces, options, deps);
317
+ const orgSlug = workspace.organizationSlug;
318
+ if (!orgSlug) {
319
+ throw new CliError(
320
+ `workspace ${workspace.slug} has no organization slug`,
321
+ "open the workspace in the cmssy dashboard once, then run cmssy link again"
322
+ );
323
+ }
324
+ log(
325
+ formatResult({
326
+ status: "ok",
327
+ message: `linking to ${describeWorkspace(workspace)}`
328
+ })
329
+ );
330
+ const draftSecret = await fetchDraftSecret({
331
+ ...admin,
332
+ workspaceId: workspace.id
333
+ });
334
+ log(formatResult({ status: "ok", message: "fetched the draft secret" }));
335
+ const previewUrl = resolvePreviewUrl(options);
336
+ if (previewUrl) {
337
+ await setPreviewUrl(previewUrl, { ...admin, workspaceId: workspace.id });
338
+ log(
339
+ formatResult({
340
+ status: "ok",
341
+ message: `set the workspace preview URL to ${previewUrl}`
342
+ })
343
+ );
344
+ } else {
345
+ log(
346
+ formatResult({
347
+ status: "unknown",
348
+ message: "preview URL left unchanged - pass --preview-url <deployed origin> to set it; for localhost use the editor dev-mode switch"
349
+ })
350
+ );
351
+ }
352
+ writeEnvLocal(cwd, {
353
+ CMSSY_ORG_SLUG: orgSlug,
354
+ CMSSY_WORKSPACE_SLUG: workspace.slug,
355
+ CMSSY_DRAFT_SECRET: draftSecret
356
+ });
357
+ log(
358
+ formatResult({
359
+ status: "ok",
360
+ message: "wrote CMSSY_ORG_SLUG, CMSSY_WORKSPACE_SLUG and CMSSY_DRAFT_SECRET to .env.local"
361
+ })
362
+ );
363
+ const preflight = {
364
+ apiUrl: env.CMSSY_API_URL,
365
+ org: orgSlug,
366
+ workspaceSlug: workspace.slug,
367
+ draftSecret,
368
+ fetch: deps.fetch
369
+ };
370
+ const reachable = await checkWorkspaceReachable(preflight);
371
+ log(formatResult(reachable));
372
+ const secretResult = await checkDraftSecret(preflight);
373
+ log(formatResult(secretResult));
374
+ log(formatEditorLink(buildEditorUrl(preflight)));
375
+ return reachable.status === "fail" || secretResult.status === "fail" ? 1 : 0;
376
+ } catch (error) {
377
+ if (error instanceof CliError) {
378
+ log(
379
+ formatResult({
380
+ status: "fail",
381
+ message: error.message,
382
+ fix: error.fix
383
+ })
384
+ );
385
+ return 1;
386
+ }
387
+ log(
388
+ formatResult({
389
+ status: "fail",
390
+ message: error instanceof Error ? error.message : String(error)
391
+ })
392
+ );
393
+ return 1;
394
+ }
395
+ }
396
+
397
+ // src/index.ts
398
+ var USAGE = "usage: cmssy link [--token <cs_...>] [--workspace <slug>] [--preview-url <url>]";
399
+ function flagValue(args, name) {
400
+ const index = args.findIndex((arg) => arg === name);
401
+ if (index !== -1) return args[index + 1];
402
+ return args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
403
+ }
404
+ async function main() {
405
+ const [command, ...args] = process.argv.slice(2);
406
+ if (command !== "link") {
407
+ console.error(USAGE);
408
+ process.exitCode = command === void 0 || command === "--help" ? 0 : 1;
409
+ return;
410
+ }
411
+ const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : null;
412
+ try {
413
+ process.exitCode = await runLink(
414
+ {
415
+ token: flagValue(args, "--token"),
416
+ workspace: flagValue(args, "--workspace"),
417
+ previewUrl: flagValue(args, "--preview-url")
418
+ },
419
+ {
420
+ cwd: process.cwd(),
421
+ env: process.env,
422
+ log: (line) => console.log(line),
423
+ fetch: globalThis.fetch,
424
+ isTty: rl !== null,
425
+ ask: (question) => rl ? rl.question(question) : Promise.resolve("")
426
+ }
427
+ );
428
+ } finally {
429
+ rl?.close();
430
+ }
431
+ }
432
+ main().catch((error) => {
433
+ console.error(
434
+ `cmssy: ${error instanceof Error ? error.message : String(error)}`
435
+ );
436
+ process.exitCode = 1;
437
+ });
package/package.json CHANGED
@@ -1,55 +1,41 @@
1
1
  {
2
2
  "name": "@cmssy/cli",
3
- "version": "1.0.0",
4
- "description": "CLI for wiring a Next.js app to a headless cmssy workspace",
5
- "type": "module",
6
- "bin": {
7
- "cmssy": "./dist/cli.js"
8
- },
9
- "files": [
10
- "dist",
11
- "templates",
12
- "README.md",
13
- "LICENSE"
14
- ],
3
+ "version": "9.1.1",
4
+ "description": "One command to connect an app to a cmssy workspace: `cmssy link` writes .env.local, sets the preview URL and verifies the editor wiring.",
15
5
  "keywords": [
16
6
  "cmssy",
17
7
  "cli",
18
- "headless",
19
- "cms",
20
- "nextjs"
8
+ "link"
21
9
  ],
22
- "author": "Cmssy Team",
23
- "license": "MIT",
10
+ "homepage": "https://cmssy.com",
24
11
  "repository": {
25
12
  "type": "git",
26
- "url": "https://github.com/cmssy-io/cmssy-cli.git"
27
- },
28
- "homepage": "https://github.com/cmssy-io/cmssy-cli#readme",
29
- "bugs": {
30
- "url": "https://github.com/cmssy-io/cmssy-cli/issues"
31
- },
32
- "engines": {
33
- "node": ">=18.18.0"
13
+ "url": "git+https://github.com/cmssy-io/cmssy-sdk.git",
14
+ "directory": "packages/cli"
34
15
  },
16
+ "type": "module",
17
+ "license": "MIT",
35
18
  "publishConfig": {
36
19
  "access": "public"
37
20
  },
21
+ "bin": {
22
+ "cmssy": "./dist/index.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
38
27
  "dependencies": {
39
- "@clack/prompts": "^1.6.0",
40
- "picocolors": "^1.1.1"
28
+ "@cmssy/core": "9.1.1"
41
29
  },
42
30
  "devDependencies": {
43
- "@types/node": "^26.0.1",
44
- "tsup": "^8.5.1",
45
- "typescript": "^6.0.3",
46
- "vitest": "^4.1.9"
31
+ "@types/node": "^20.0.0",
32
+ "tsup": "^8.3.0",
33
+ "typescript": "^5.6.0",
34
+ "vitest": "^2.1.0"
47
35
  },
48
36
  "scripts": {
49
37
  "build": "tsup",
50
- "dev": "tsup --watch",
51
- "typecheck": "tsc --noEmit",
52
38
  "test": "vitest run",
53
- "test:watch": "vitest"
39
+ "typecheck": "tsc --noEmit"
54
40
  }
55
41
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Cmssy Team
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/README.md DELETED
@@ -1,91 +0,0 @@
1
- # @cmssy/cli
2
-
3
- The `cmssy` CLI wires cmssy into your existing [Next.js](https://nextjs.org) (App Router) app
4
- and helps you grow it with blocks. It does not scaffold a Next app - bring your own
5
- (`npx create-next-app@latest`).
6
-
7
- ```bash
8
- npx create-next-app@latest my-site # your app, your way
9
- cd my-site
10
- npx @cmssy/cli init # add cmssy
11
- ```
12
-
13
- Requires **Node 18.18+** and an existing **Next.js App Router** project.
14
-
15
- ## Commands
16
-
17
- ### `cmssy init [dir]`
18
-
19
- Add the cmssy wiring to an existing Next.js App Router app, then link it to a workspace.
20
- Idempotent - it never overwrites a file you already have; skipped files are reported.
21
-
22
- It writes: `cmssy.config.ts`, a catch-all `app/[[...path]]/page.tsx`, the draft route,
23
- `proxy.ts` (edit-mode + CSP), the block registry, the lazy editor, and one self-styled example
24
- `hero` block (a CSS Module - cmssy does not impose a styling system). `next.config.mjs` is added
25
- only if you don't already have one. Then it links and installs dependencies.
26
-
27
- ```bash
28
- cmssy init # wire the current directory
29
- cmssy init --pm pnpm # choose the package manager (npm | pnpm | yarn | bun)
30
- cmssy init --skip-install # write files, install later
31
- cmssy init --no-link # skip the workspace prompt
32
- ```
33
-
34
- ### `cmssy link`
35
-
36
- Connect an already-initialized project to a workspace. Prompts for the workspace slug and draft
37
- secret (both from **cmssy dashboard → Settings → Headless**), validates the slug against the
38
- delivery API, and writes them to `.env` without touching your other variables.
39
-
40
- ```bash
41
- cmssy link
42
- cmssy link --slug my-workspace --secret <draft-secret> # non-interactive
43
- ```
44
-
45
- ### `cmssy add block <name>`
46
-
47
- Scaffold a new block and register it in `cmssy/blocks.ts`. The name is normalized: a folder in
48
- kebab-case, a PascalCase component, a camelCase registry export.
49
-
50
- ```bash
51
- cmssy add block "Feature Grid"
52
- # -> blocks/feature-grid/{block.ts, FeatureGrid.tsx, FeatureGrid.module.css}
53
- # registered as featureGridBlock
54
- ```
55
-
56
- ### `cmssy doctor`
57
-
58
- Diagnose a project's cmssy setup: required files, `@cmssy/*` install + version alignment, env
59
- vars, and that the block registry's imports resolve. Exits non-zero on a hard failure.
60
-
61
- ```bash
62
- cmssy doctor
63
- ```
64
-
65
- ## What "linked" means
66
-
67
- Only two values are required (cmssy cloud provides the rest):
68
-
69
- | Variable | Where to find it |
70
- | ---------------------- | ----------------------------------------------------- |
71
- | `CMSSY_WORKSPACE_SLUG` | cmssy dashboard → Settings → Headless |
72
- | `CMSSY_DRAFT_SECRET` | cmssy dashboard → Settings → Headless (per-workspace) |
73
-
74
- ## Docs
75
-
76
- - [cmssy docs](https://www.cmssy.com/docs)
77
- - [Installation](https://www.cmssy.com/docs/installation) · [Quickstart](https://www.cmssy.com/docs/quickstart)
78
- - Built on the cmssy SDK: [`@cmssy/next` + `@cmssy/react`](https://github.com/cmssy-io/cmssy-sdk)
79
-
80
- ## Contributing
81
-
82
- ```bash
83
- pnpm install
84
- pnpm run build # tsup -> dist/cli.js
85
- pnpm run typecheck
86
- pnpm test # vitest
87
- ```
88
-
89
- ## License
90
-
91
- MIT