@convosite/cli 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,108 @@
1
- # Temporary Holding Version
1
+ # @convosite/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
+ Command-line tools for building and syncing Convosite projects.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 20 or newer
8
+ - A Convosite site and access token
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install --save-dev @convosite/cli
14
+ ```
15
+
16
+ You can then run commands with `npx convosite`, or install the package globally if you prefer a global `convosite` command.
17
+
18
+ ## Configure
19
+
20
+ Initialize Convosite in the root of your frontend project:
21
+
22
+ ```bash
23
+ npx convosite init
24
+ ```
25
+
26
+ This creates:
27
+
28
+ ```text
29
+ convosite.config.json
30
+ blocks/
31
+ ```
32
+
33
+ Add your access token to the project's `.env` file:
34
+
35
+ ```env
36
+ CONVOSITE_ACCESS_TOKEN=cvt_...
37
+ ```
38
+
39
+ The production API defaults to `https://app.convosite.io`. For local development you can override it:
40
+
41
+ ```env
42
+ CONVOSITE_API_URL=http://convosite-v5.local
43
+ ```
44
+
45
+ ## Create a block
46
+
47
+ ```bash
48
+ npx convosite make:block hero
49
+ ```
50
+
51
+ This creates `blocks/hero/block.json`:
52
+
53
+ ```json
54
+ {
55
+ "key": "hero",
56
+ "name": "Hero",
57
+ "description": "",
58
+ "props": []
59
+ }
60
+ ```
61
+
62
+ `key` is the block's stable machine identifier. Keep it unchanged when renaming a block. `name` is the human-readable display name.
63
+
64
+ A block with fields can look like:
65
+
66
+ ```json
67
+ {
68
+ "key": "hero",
69
+ "name": "Hero",
70
+ "description": "Primary page hero.",
71
+ "props": [
72
+ {
73
+ "key": "heading",
74
+ "type": "text",
75
+ "label": "Heading",
76
+ "required": true,
77
+ "rules": {
78
+ "max_chars": 120
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ ```
84
+
85
+ ## Validate
86
+
87
+ ```bash
88
+ npx convosite validate
89
+ ```
90
+
91
+ Validation is performed by Convosite, using the same server-side validation used by `push`. Validation does not modify the site's blocks.
92
+
93
+ ## Push
94
+
95
+ ```bash
96
+ npx convosite push
97
+ ```
98
+
99
+ A push synchronizes the local block definitions with Convosite. Local block definitions are authoritative: blocks missing from the push may be removed from the site.
100
+
101
+ ## Commands
102
+
103
+ ```text
104
+ convosite init
105
+ convosite make:block <name>
106
+ convosite validate
107
+ convosite push
108
+ ```
package/dist/api.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { BlockFile } from "./block.js";
2
+ export interface PushResponse {
3
+ site: string;
4
+ received: number;
5
+ created: number;
6
+ updated: number;
7
+ deleted: number;
8
+ }
9
+ export interface ValidateResponse {
10
+ valid: boolean;
11
+ received: number;
12
+ }
13
+ export declare class ApiError extends Error {
14
+ status: number;
15
+ errors: Record<string, string[]>;
16
+ constructor(status: number, message: string, errors?: Record<string, string[]>);
17
+ }
18
+ export declare function pushBlocks(token: string, blocks: BlockFile[]): Promise<PushResponse>;
19
+ export declare function validateBlocks(token: string, blocks: BlockFile[]): Promise<ValidateResponse>;
package/dist/api.js ADDED
@@ -0,0 +1,58 @@
1
+ const DEFAULT_API_URL = "https://app.convosite.io";
2
+ export class ApiError extends Error {
3
+ status;
4
+ errors;
5
+ constructor(status, message, errors = {}) {
6
+ super(message);
7
+ this.status = status;
8
+ this.errors = errors;
9
+ this.name = "ApiError";
10
+ }
11
+ }
12
+ export async function pushBlocks(token, blocks) {
13
+ return request("/api/cli/push", token, { blocks });
14
+ }
15
+ export async function validateBlocks(token, blocks) {
16
+ return request("/api/cli/validate", token, { blocks });
17
+ }
18
+ async function request(endpoint, token, body) {
19
+ const apiUrl = getApiUrl();
20
+ let response;
21
+ try {
22
+ response = await fetch(`${apiUrl}${endpoint}`, {
23
+ method: "POST",
24
+ headers: {
25
+ Authorization: `Bearer ${token}`,
26
+ Accept: "application/json",
27
+ "Content-Type": "application/json",
28
+ },
29
+ body: JSON.stringify(body),
30
+ });
31
+ }
32
+ catch {
33
+ throw new Error(`Could not connect to Convosite at ${apiUrl}`);
34
+ }
35
+ if (!response.ok) {
36
+ let error = {};
37
+ try {
38
+ error =
39
+ (await response.json());
40
+ }
41
+ catch {
42
+ // Response wasn't JSON.
43
+ }
44
+ throw new ApiError(response.status, error.message ??
45
+ `Convosite returned ${response.status}`, error.errors ?? {});
46
+ }
47
+ try {
48
+ return (await response.json());
49
+ }
50
+ catch {
51
+ throw new Error("Convosite returned an invalid response.");
52
+ }
53
+ }
54
+ function getApiUrl() {
55
+ const value = process.env.CONVOSITE_API_URL?.trim() ||
56
+ DEFAULT_API_URL;
57
+ return value.replace(/\/+$/, "");
58
+ }
@@ -0,0 +1,5 @@
1
+ export interface BlockFile {
2
+ path: string;
3
+ data: unknown;
4
+ }
5
+ export declare function findBlocks(blocksDirectory: string): Promise<BlockFile[]>;
package/dist/block.js ADDED
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function findBlocks(blocksDirectory) {
4
+ const root = path.resolve(process.cwd(), blocksDirectory);
5
+ const blocks = [];
6
+ async function walk(directory) {
7
+ const entries = await fs.readdir(directory, {
8
+ withFileTypes: true,
9
+ });
10
+ for (const entry of entries) {
11
+ const fullPath = path.join(directory, entry.name);
12
+ if (entry.isDirectory()) {
13
+ await walk(fullPath);
14
+ continue;
15
+ }
16
+ if (entry.name !== "block.json") {
17
+ continue;
18
+ }
19
+ const contents = await fs.readFile(fullPath, "utf8");
20
+ let data;
21
+ try {
22
+ data = JSON.parse(contents);
23
+ }
24
+ catch {
25
+ throw new Error(`Invalid JSON in ${path.relative(process.cwd(), fullPath)}`);
26
+ }
27
+ blocks.push({
28
+ path: path.relative(root, fullPath),
29
+ data,
30
+ });
31
+ }
32
+ }
33
+ try {
34
+ await walk(root);
35
+ }
36
+ catch (error) {
37
+ if (error instanceof Error &&
38
+ "code" in error &&
39
+ error.code === "ENOENT") {
40
+ throw new Error(`Blocks directory not found: ${blocksDirectory}`);
41
+ }
42
+ throw error;
43
+ }
44
+ return blocks;
45
+ }
@@ -0,0 +1 @@
1
+ export declare function runCommand(command: () => Promise<void>): Promise<void>;
@@ -0,0 +1,12 @@
1
+ export async function runCommand(command) {
2
+ try {
3
+ await command();
4
+ }
5
+ catch (error) {
6
+ const message = error instanceof Error
7
+ ? error.message
8
+ : "Unknown error";
9
+ console.error(`\nError: ${message}\n`);
10
+ process.exitCode = 1;
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const CONFIG_FILE = "convosite.config.json";
4
+ const BLOCKS_DIRECTORY = "blocks";
5
+ export async function init() {
6
+ console.log("Convosite\n");
7
+ const cwd = process.cwd();
8
+ const configPath = path.join(cwd, CONFIG_FILE);
9
+ const blocksPath = path.join(cwd, BLOCKS_DIRECTORY);
10
+ if (await exists(configPath)) {
11
+ throw new Error(`${CONFIG_FILE} already exists`);
12
+ }
13
+ const config = {
14
+ blocks: `./${BLOCKS_DIRECTORY}`,
15
+ };
16
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
17
+ await fs.mkdir(blocksPath, {
18
+ recursive: true,
19
+ });
20
+ console.log(`✓ Created ${CONFIG_FILE}`);
21
+ console.log(`✓ Created ${BLOCKS_DIRECTORY}/`);
22
+ console.log("");
23
+ console.log("Next:");
24
+ console.log("");
25
+ console.log(" Add your access token to .env:");
26
+ console.log("");
27
+ console.log(" CONVOSITE_ACCESS_TOKEN=cvt_...");
28
+ console.log("");
29
+ }
30
+ async function exists(filePath) {
31
+ try {
32
+ await fs.access(filePath);
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
@@ -0,0 +1 @@
1
+ export declare function makeBlock(name: string): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { loadConfig } from "../config.js";
4
+ export async function makeBlock(name) {
5
+ console.log("Convosite\n");
6
+ const config = await loadConfig();
7
+ const key = toKey(name);
8
+ if (!key) {
9
+ throw new Error("Block name must contain letters or numbers");
10
+ }
11
+ const displayName = toDisplayName(name);
12
+ const blockDirectory = path.resolve(process.cwd(), config.blocks, key);
13
+ const blockPath = path.join(blockDirectory, "block.json");
14
+ if (await exists(blockPath)) {
15
+ throw new Error(`Block "${key}" already exists`);
16
+ }
17
+ await fs.mkdir(blockDirectory, {
18
+ recursive: true,
19
+ });
20
+ const block = {
21
+ key,
22
+ name: displayName,
23
+ description: "",
24
+ props: [],
25
+ };
26
+ await fs.writeFile(blockPath, JSON.stringify(block, null, 2) + "\n", "utf8");
27
+ console.log(`✓ Created ${relativePath(blockPath)}`);
28
+ console.log("");
29
+ }
30
+ function toKey(value) {
31
+ return value
32
+ .trim()
33
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
34
+ .replace(/[^a-zA-Z0-9]+/g, "_")
35
+ .replace(/^_+|_+$/g, "")
36
+ .toLowerCase();
37
+ }
38
+ function toDisplayName(value) {
39
+ return value
40
+ .trim()
41
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
42
+ .replace(/[_-]+/g, " ")
43
+ .replace(/\s+/g, " ")
44
+ .replace(/\b\w/g, (character) => character.toUpperCase());
45
+ }
46
+ function relativePath(filePath) {
47
+ return path
48
+ .relative(process.cwd(), filePath)
49
+ .replaceAll("\\", "/");
50
+ }
51
+ async function exists(filePath) {
52
+ try {
53
+ await fs.access(filePath);
54
+ return true;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export declare function push(): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { loadConfig } from "../config.js";
2
+ import { printApiError } from "../errors.js";
3
+ import { findBlocks } from "../block.js";
4
+ import { ApiError, pushBlocks, } from "../api.js";
5
+ export async function push() {
6
+ console.log("Convosite\n");
7
+ const token = process.env.CONVOSITE_ACCESS_TOKEN;
8
+ if (!token) {
9
+ throw new Error("CONVOSITE_ACCESS_TOKEN is not set");
10
+ }
11
+ const config = await loadConfig();
12
+ console.log("Scanning blocks...");
13
+ const blocks = await findBlocks(config.blocks);
14
+ if (blocks.length === 0) {
15
+ throw new Error("No block.json files found");
16
+ }
17
+ console.log(`Found ${blocks.length} block${blocks.length === 1 ? "" : "s"}`);
18
+ console.log("Pushing to Convosite...");
19
+ try {
20
+ const result = await pushBlocks(token, blocks);
21
+ console.log("");
22
+ console.log("✓ Push complete");
23
+ console.log("");
24
+ console.log(` Created: ${result.created}`);
25
+ console.log(` Updated: ${result.updated}`);
26
+ console.log(` Deleted: ${result.deleted}`);
27
+ console.log("");
28
+ }
29
+ catch (error) {
30
+ if (error instanceof ApiError) {
31
+ printApiError(error, blocks);
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ throw error;
36
+ }
37
+ }
@@ -0,0 +1 @@
1
+ export declare function validate(): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { loadConfig } from "../config.js";
2
+ import { findBlocks } from "../block.js";
3
+ import { ApiError, validateBlocks, } from "../api.js";
4
+ import { printApiError } from "../errors.js";
5
+ export async function validate() {
6
+ console.log("Convosite\n");
7
+ const token = process.env.CONVOSITE_ACCESS_TOKEN;
8
+ if (!token) {
9
+ throw new Error("CONVOSITE_ACCESS_TOKEN is not set");
10
+ }
11
+ const config = await loadConfig();
12
+ console.log("Scanning blocks...");
13
+ const blocks = await findBlocks(config.blocks);
14
+ if (blocks.length === 0) {
15
+ throw new Error("No block.json files found");
16
+ }
17
+ console.log(`Found ${blocks.length} block${blocks.length === 1 ? "" : "s"}`);
18
+ console.log("Validating...");
19
+ try {
20
+ const result = await validateBlocks(token, blocks);
21
+ console.log("");
22
+ console.log(`✓ ${result.received} block${result.received === 1 ? "" : "s"} valid`);
23
+ console.log("");
24
+ }
25
+ catch (error) {
26
+ if (error instanceof ApiError) {
27
+ printApiError(error, blocks);
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ throw error;
32
+ }
33
+ }
@@ -0,0 +1,4 @@
1
+ export interface ConvositeConfig {
2
+ blocks: string;
3
+ }
4
+ export declare function loadConfig(): Promise<ConvositeConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function loadConfig() {
4
+ const configPath = path.resolve(process.cwd(), "convosite.config.json");
5
+ let contents;
6
+ try {
7
+ contents = await fs.readFile(configPath, "utf8");
8
+ }
9
+ catch {
10
+ throw new Error("Could not find convosite.config.json");
11
+ }
12
+ let config;
13
+ try {
14
+ config = JSON.parse(contents);
15
+ }
16
+ catch {
17
+ throw new Error("convosite.config.json contains invalid JSON");
18
+ }
19
+ if (typeof config !== "object" ||
20
+ config === null ||
21
+ !("blocks" in config) ||
22
+ typeof config.blocks !== "string") {
23
+ throw new Error('convosite.config.json must contain a "blocks" property');
24
+ }
25
+ return {
26
+ blocks: config.blocks,
27
+ };
28
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function loadEnv(): void;
package/dist/env.js ADDED
@@ -0,0 +1,8 @@
1
+ import path from "node:path";
2
+ import dotenv from "dotenv";
3
+ export function loadEnv() {
4
+ dotenv.config({
5
+ path: path.resolve(process.cwd(), ".env"),
6
+ quiet: true,
7
+ });
8
+ }
@@ -0,0 +1,3 @@
1
+ import type { BlockFile } from "./block.js";
2
+ import { ApiError } from "./api.js";
3
+ export declare function printApiError(error: ApiError, blocks: BlockFile[]): void;
package/dist/errors.js ADDED
@@ -0,0 +1,53 @@
1
+ export function printApiError(error, blocks) {
2
+ console.error("");
3
+ if (error.status === 401) {
4
+ console.error("Authentication failed.");
5
+ console.error("");
6
+ console.error(` ${error.message}`);
7
+ console.error("");
8
+ return;
9
+ }
10
+ if (error.status === 403) {
11
+ console.error("Access denied.");
12
+ console.error("");
13
+ console.error(` ${error.message}`);
14
+ console.error("");
15
+ return;
16
+ }
17
+ if (error.status === 422) {
18
+ console.error("Validation failed");
19
+ console.error("");
20
+ for (const [field, messages] of Object.entries(error.errors)) {
21
+ printValidationError(field, messages, blocks);
22
+ }
23
+ return;
24
+ }
25
+ console.error(`Convosite returned an error (${error.status}).`);
26
+ console.error("");
27
+ console.error(` ${error.message}`);
28
+ console.error("");
29
+ }
30
+ function printValidationError(field, messages, blocks) {
31
+ const match = field.match(/^blocks\.(\d+)\.data(?:\.(.+))?$/);
32
+ if (!match) {
33
+ console.error(` ${field}`);
34
+ for (const message of messages) {
35
+ console.error(` ${message}`);
36
+ }
37
+ console.error("");
38
+ return;
39
+ }
40
+ const index = Number(match[1]);
41
+ const property = match[2];
42
+ const block = blocks[index];
43
+ if (block) {
44
+ console.error(block.path);
45
+ }
46
+ if (property) {
47
+ console.error(` ${property}`);
48
+ }
49
+ for (const message of messages) {
50
+ console.error(` ${message}`);
51
+ }
52
+ console.error("");
53
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runCommand } from "./command.js";
4
+ import { init } from "./commands/init.js";
5
+ import { makeBlock } from "./commands/make-block.js";
6
+ import { push } from "./commands/push.js";
7
+ import { validate } from "./commands/validate.js";
8
+ import { loadEnv } from "./env.js";
9
+ loadEnv();
10
+ const program = new Command();
11
+ program
12
+ .name("convosite")
13
+ .description("CLI for Convosite")
14
+ .version("0.1.0");
15
+ program
16
+ .command("init")
17
+ .description("Initialize Convosite in your project")
18
+ .action(() => runCommand(init));
19
+ program
20
+ .command("make:block")
21
+ .description("Create a new Convosite block")
22
+ .argument("<name>", "Block name")
23
+ .action((name) => runCommand(() => makeBlock(name)));
24
+ program
25
+ .command("validate")
26
+ .description("Validate your Convosite project")
27
+ .action(() => runCommand(validate));
28
+ program
29
+ .command("push")
30
+ .description("Push your project structure to Convosite")
31
+ .action(() => runCommand(push));
32
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "@convosite/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Command-line tools for Convosite projects",
5
+ "type": "module",
6
+ "bin": {
7
+ "convosite": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
15
+ "build": "npm run clean && tsc",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "dependencies": {
22
+ "commander": "^14.0.0",
23
+ "dotenv": "^18.0.7"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^24.19.2",
27
+ "typescript": "^5.9.0"
28
+ }
29
+ }