@convosite/cli 0.1.0 → 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 +17 -6
- package/dist/api.d.ts +9 -0
- package/dist/api.js +8 -3
- package/dist/commands/pull.d.ts +5 -0
- package/dist/commands/pull.js +151 -0
- package/dist/index.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,12 +36,6 @@ Add your access token to the project's `.env` file:
|
|
|
36
36
|
CONVOSITE_ACCESS_TOKEN=cvt_...
|
|
37
37
|
```
|
|
38
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
39
|
## Create a block
|
|
46
40
|
|
|
47
41
|
```bash
|
|
@@ -90,6 +84,22 @@ npx convosite validate
|
|
|
90
84
|
|
|
91
85
|
Validation is performed by Convosite, using the same server-side validation used by `push`. Validation does not modify the site's blocks.
|
|
92
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
|
+
|
|
93
103
|
## Push
|
|
94
104
|
|
|
95
105
|
```bash
|
|
@@ -104,5 +114,6 @@ A push synchronizes the local block definitions with Convosite. Local block defi
|
|
|
104
114
|
convosite init
|
|
105
115
|
convosite make:block <name>
|
|
106
116
|
convosite validate
|
|
117
|
+
convosite pull [--force]
|
|
107
118
|
convosite push
|
|
108
119
|
```
|
package/dist/api.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export interface ValidateResponse {
|
|
|
10
10
|
valid: boolean;
|
|
11
11
|
received: number;
|
|
12
12
|
}
|
|
13
|
+
export interface PullResponse {
|
|
14
|
+
blocks: Array<{
|
|
15
|
+
key: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string | null;
|
|
18
|
+
props?: unknown[];
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
13
21
|
export declare class ApiError extends Error {
|
|
14
22
|
status: number;
|
|
15
23
|
errors: Record<string, string[]>;
|
|
@@ -17,3 +25,4 @@ export declare class ApiError extends Error {
|
|
|
17
25
|
}
|
|
18
26
|
export declare function pushBlocks(token: string, blocks: BlockFile[]): Promise<PushResponse>;
|
|
19
27
|
export declare function validateBlocks(token: string, blocks: BlockFile[]): Promise<ValidateResponse>;
|
|
28
|
+
export declare function pullBlocks(token: string): Promise<PullResponse>;
|
package/dist/api.js
CHANGED
|
@@ -15,18 +15,23 @@ export async function pushBlocks(token, blocks) {
|
|
|
15
15
|
export async function validateBlocks(token, blocks) {
|
|
16
16
|
return request("/api/cli/validate", token, { blocks });
|
|
17
17
|
}
|
|
18
|
-
async function
|
|
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") {
|
|
19
22
|
const apiUrl = getApiUrl();
|
|
20
23
|
let response;
|
|
21
24
|
try {
|
|
22
25
|
response = await fetch(`${apiUrl}${endpoint}`, {
|
|
23
|
-
method
|
|
26
|
+
method,
|
|
24
27
|
headers: {
|
|
25
28
|
Authorization: `Bearer ${token}`,
|
|
26
29
|
Accept: "application/json",
|
|
27
30
|
"Content-Type": "application/json",
|
|
28
31
|
},
|
|
29
|
-
body
|
|
32
|
+
...(body === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { body: JSON.stringify(body) }),
|
|
30
35
|
});
|
|
31
36
|
}
|
|
32
37
|
catch {
|
|
@@ -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
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from "commander";
|
|
|
3
3
|
import { runCommand } from "./command.js";
|
|
4
4
|
import { init } from "./commands/init.js";
|
|
5
5
|
import { makeBlock } from "./commands/make-block.js";
|
|
6
|
+
import { pull } from "./commands/pull.js";
|
|
6
7
|
import { push } from "./commands/push.js";
|
|
7
8
|
import { validate } from "./commands/validate.js";
|
|
8
9
|
import { loadEnv } from "./env.js";
|
|
@@ -25,6 +26,11 @@ program
|
|
|
25
26
|
.command("validate")
|
|
26
27
|
.description("Validate your Convosite project")
|
|
27
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)));
|
|
28
34
|
program
|
|
29
35
|
.command("push")
|
|
30
36
|
.description("Push your project structure to Convosite")
|