@convosite/cli 0.0.0-stage → 0.2.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 +118 -2
- package/dist/api.d.ts +28 -0
- package/dist/api.js +63 -0
- package/dist/block.d.ts +5 -0
- package/dist/block.js +45 -0
- package/dist/command.d.ts +1 -0
- package/dist/command.js +12 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +38 -0
- package/dist/commands/make-block.d.ts +1 -0
- package/dist/commands/make-block.js +59 -0
- package/dist/commands/pull.d.ts +5 -0
- package/dist/commands/pull.js +151 -0
- package/dist/commands/push.d.ts +1 -0
- package/dist/commands/push.js +37 -0
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.js +33 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +28 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +8 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @convosite/cli
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
## Create a block
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx convosite make:block hero
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This creates `blocks/hero/block.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"key": "hero",
|
|
50
|
+
"name": "Hero",
|
|
51
|
+
"description": "",
|
|
52
|
+
"props": []
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`key` is the block's stable machine identifier. Keep it unchanged when renaming a block. `name` is the human-readable display name.
|
|
57
|
+
|
|
58
|
+
A block with fields can look like:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"key": "hero",
|
|
63
|
+
"name": "Hero",
|
|
64
|
+
"description": "Primary page hero.",
|
|
65
|
+
"props": [
|
|
66
|
+
{
|
|
67
|
+
"key": "heading",
|
|
68
|
+
"type": "text",
|
|
69
|
+
"label": "Heading",
|
|
70
|
+
"required": true,
|
|
71
|
+
"rules": {
|
|
72
|
+
"max_chars": 120
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Validate
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx convosite validate
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Validation is performed by Convosite, using the same server-side validation used by `push`. Validation does not modify the site's blocks.
|
|
86
|
+
|
|
87
|
+
## Pull
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx convosite pull
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Pull downloads the latest block definitions from Convosite. New block definitions are created locally and unchanged definitions are left alone. If a local `block.json` differs from Convosite, the file is reported as a conflict and is not overwritten.
|
|
94
|
+
|
|
95
|
+
To make Convosite authoritative for the local block definitions:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx convosite pull --force
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`--force` overwrites conflicting `block.json` files and removes local `block.json` definitions that no longer exist in Convosite. It does **not** delete component/source files alongside a block; a block directory is only removed when it becomes empty.
|
|
102
|
+
|
|
103
|
+
## Push
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx convosite push
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
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.
|
|
110
|
+
|
|
111
|
+
## Commands
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
convosite init
|
|
115
|
+
convosite make:block <name>
|
|
116
|
+
convosite validate
|
|
117
|
+
convosite pull [--force]
|
|
118
|
+
convosite push
|
|
119
|
+
```
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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 interface PullResponse {
|
|
14
|
+
blocks: Array<{
|
|
15
|
+
key: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string | null;
|
|
18
|
+
props?: unknown[];
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare class ApiError extends Error {
|
|
22
|
+
status: number;
|
|
23
|
+
errors: Record<string, string[]>;
|
|
24
|
+
constructor(status: number, message: string, errors?: Record<string, string[]>);
|
|
25
|
+
}
|
|
26
|
+
export declare function pushBlocks(token: string, blocks: BlockFile[]): Promise<PushResponse>;
|
|
27
|
+
export declare function validateBlocks(token: string, blocks: BlockFile[]): Promise<ValidateResponse>;
|
|
28
|
+
export declare function pullBlocks(token: string): Promise<PullResponse>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
export async function pullBlocks(token) {
|
|
19
|
+
return request("/api/cli/pull", token, undefined, "GET");
|
|
20
|
+
}
|
|
21
|
+
async function request(endpoint, token, body, method = "POST") {
|
|
22
|
+
const apiUrl = getApiUrl();
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await fetch(`${apiUrl}${endpoint}`, {
|
|
26
|
+
method,
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${token}`,
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
},
|
|
32
|
+
...(body === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { body: JSON.stringify(body) }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Could not connect to Convosite at ${apiUrl}`);
|
|
39
|
+
}
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
let error = {};
|
|
42
|
+
try {
|
|
43
|
+
error =
|
|
44
|
+
(await response.json());
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Response wasn't JSON.
|
|
48
|
+
}
|
|
49
|
+
throw new ApiError(response.status, error.message ??
|
|
50
|
+
`Convosite returned ${response.status}`, error.errors ?? {});
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return (await response.json());
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error("Convosite returned an invalid response.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getApiUrl() {
|
|
60
|
+
const value = process.env.CONVOSITE_API_URL?.trim() ||
|
|
61
|
+
DEFAULT_API_URL;
|
|
62
|
+
return value.replace(/\/+$/, "");
|
|
63
|
+
}
|
package/dist/block.d.ts
ADDED
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>;
|
package/dist/command.js
ADDED
|
@@ -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,151 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ApiError, pullBlocks } from "../api.js";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
5
|
+
import { printApiError } from "../errors.js";
|
|
6
|
+
export async function pull(options = {}) {
|
|
7
|
+
console.log("Convosite\n");
|
|
8
|
+
const token = process.env.CONVOSITE_ACCESS_TOKEN;
|
|
9
|
+
if (!token) {
|
|
10
|
+
throw new Error("CONVOSITE_ACCESS_TOKEN is not set");
|
|
11
|
+
}
|
|
12
|
+
const config = await loadConfig();
|
|
13
|
+
const blocksRoot = path.resolve(process.cwd(), config.blocks);
|
|
14
|
+
console.log("Pulling from Convosite...");
|
|
15
|
+
try {
|
|
16
|
+
const result = await pullBlocks(token);
|
|
17
|
+
const remoteBlocks = result.blocks;
|
|
18
|
+
await fs.mkdir(blocksRoot, { recursive: true });
|
|
19
|
+
let created = 0;
|
|
20
|
+
let updated = 0;
|
|
21
|
+
let unchanged = 0;
|
|
22
|
+
let deleted = 0;
|
|
23
|
+
const conflicts = [];
|
|
24
|
+
const remoteKeys = new Set();
|
|
25
|
+
for (const block of remoteBlocks) {
|
|
26
|
+
remoteKeys.add(block.key);
|
|
27
|
+
const blockDirectory = path.join(blocksRoot, block.key);
|
|
28
|
+
const blockPath = path.join(blockDirectory, "block.json");
|
|
29
|
+
const remoteData = serialisableBlock(block);
|
|
30
|
+
const existing = await readJson(blockPath);
|
|
31
|
+
if (existing === null) {
|
|
32
|
+
await fs.mkdir(blockDirectory, { recursive: true });
|
|
33
|
+
await writeJson(blockPath, remoteData);
|
|
34
|
+
created++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (jsonEquals(existing, remoteData)) {
|
|
38
|
+
unchanged++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!options.force) {
|
|
42
|
+
conflicts.push(relativePath(blockPath));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
await writeJson(blockPath, remoteData);
|
|
46
|
+
updated++;
|
|
47
|
+
}
|
|
48
|
+
if (options.force) {
|
|
49
|
+
deleted = await removeMissingBlockDefinitions(blocksRoot, remoteKeys);
|
|
50
|
+
}
|
|
51
|
+
console.log("");
|
|
52
|
+
console.log("✓ Pull complete");
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(` Created: ${created}`);
|
|
55
|
+
console.log(` Updated: ${updated}`);
|
|
56
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
57
|
+
if (options.force) {
|
|
58
|
+
console.log(` Deleted: ${deleted}`);
|
|
59
|
+
}
|
|
60
|
+
if (conflicts.length > 0) {
|
|
61
|
+
console.log("");
|
|
62
|
+
console.error(`${conflicts.length} local file${conflicts.length === 1 ? " has" : "s have"} changes and ${conflicts.length === 1 ? "was" : "were"} not overwritten:`);
|
|
63
|
+
console.error("");
|
|
64
|
+
for (const conflict of conflicts) {
|
|
65
|
+
console.error(` ${conflict}`);
|
|
66
|
+
}
|
|
67
|
+
console.error("");
|
|
68
|
+
console.error("Run convosite pull --force to overwrite local block definitions.");
|
|
69
|
+
console.error("");
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof ApiError) {
|
|
77
|
+
printApiError(error, []);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function serialisableBlock(block) {
|
|
85
|
+
return {
|
|
86
|
+
key: block.key,
|
|
87
|
+
name: block.name,
|
|
88
|
+
description: block.description ?? "",
|
|
89
|
+
props: block.props ?? [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function readJson(filePath) {
|
|
93
|
+
try {
|
|
94
|
+
const contents = await fs.readFile(filePath, "utf8");
|
|
95
|
+
return JSON.parse(contents);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof Error &&
|
|
99
|
+
"code" in error &&
|
|
100
|
+
error.code === "ENOENT") {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (error instanceof SyntaxError) {
|
|
104
|
+
return Symbol("invalid-json");
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function writeJson(filePath, data) {
|
|
110
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
111
|
+
}
|
|
112
|
+
function jsonEquals(left, right) {
|
|
113
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
114
|
+
}
|
|
115
|
+
async function removeMissingBlockDefinitions(blocksRoot, remoteKeys) {
|
|
116
|
+
const entries = await fs.readdir(blocksRoot, {
|
|
117
|
+
withFileTypes: true,
|
|
118
|
+
});
|
|
119
|
+
let deleted = 0;
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (!entry.isDirectory() || remoteKeys.has(entry.name)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const blockPath = path.join(blocksRoot, entry.name, "block.json");
|
|
125
|
+
try {
|
|
126
|
+
await fs.unlink(blockPath);
|
|
127
|
+
deleted++;
|
|
128
|
+
await removeDirectoryIfEmpty(path.dirname(blockPath));
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error instanceof Error &&
|
|
132
|
+
"code" in error &&
|
|
133
|
+
error.code === "ENOENT") {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return deleted;
|
|
140
|
+
}
|
|
141
|
+
async function removeDirectoryIfEmpty(directory) {
|
|
142
|
+
const entries = await fs.readdir(directory);
|
|
143
|
+
if (entries.length === 0) {
|
|
144
|
+
await fs.rmdir(directory);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function relativePath(filePath) {
|
|
148
|
+
return path
|
|
149
|
+
.relative(process.cwd(), filePath)
|
|
150
|
+
.replaceAll("\\", "/");
|
|
151
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
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
package/dist/errors.d.ts
ADDED
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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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 { pull } from "./commands/pull.js";
|
|
7
|
+
import { push } from "./commands/push.js";
|
|
8
|
+
import { validate } from "./commands/validate.js";
|
|
9
|
+
import { loadEnv } from "./env.js";
|
|
10
|
+
loadEnv();
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("convosite")
|
|
14
|
+
.description("CLI for Convosite")
|
|
15
|
+
.version("0.1.0");
|
|
16
|
+
program
|
|
17
|
+
.command("init")
|
|
18
|
+
.description("Initialize Convosite in your project")
|
|
19
|
+
.action(() => runCommand(init));
|
|
20
|
+
program
|
|
21
|
+
.command("make:block")
|
|
22
|
+
.description("Create a new Convosite block")
|
|
23
|
+
.argument("<name>", "Block name")
|
|
24
|
+
.action((name) => runCommand(() => makeBlock(name)));
|
|
25
|
+
program
|
|
26
|
+
.command("validate")
|
|
27
|
+
.description("Validate your Convosite project")
|
|
28
|
+
.action(() => runCommand(validate));
|
|
29
|
+
program
|
|
30
|
+
.command("pull")
|
|
31
|
+
.description("Pull block definitions from Convosite")
|
|
32
|
+
.option("--force", "Overwrite local block definitions and remove missing definitions")
|
|
33
|
+
.action((options) => runCommand(() => pull(options)));
|
|
34
|
+
program
|
|
35
|
+
.command("push")
|
|
36
|
+
.description("Push your project structure to Convosite")
|
|
37
|
+
.action(() => runCommand(push));
|
|
38
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@convosite/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.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
|
+
}
|