primo-cli 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 +183 -0
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.js +379 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.js +261 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.js +516 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +163 -0
- package/dist/commands/import.d.ts +9 -0
- package/dist/commands/import.js +118 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +68 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/new.d.ts +7 -0
- package/dist/commands/new.js +507 -0
- package/dist/commands/publish.d.ts +6 -0
- package/dist/commands/publish.js +239 -0
- package/dist/commands/pull.d.ts +8 -0
- package/dist/commands/pull.js +243 -0
- package/dist/commands/push.d.ts +9 -0
- package/dist/commands/push.js +118 -0
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.js +514 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +70 -0
- package/dist/utils/auth.d.ts +2 -0
- package/dist/utils/auth.js +29 -0
- package/dist/utils/binary.d.ts +5 -0
- package/dist/utils/binary.js +129 -0
- package/package.json +53 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { execSync, spawn } from 'child_process';
|
|
7
|
+
export async function publish(options) {
|
|
8
|
+
const spinner = ora('Preparing deployment...').start();
|
|
9
|
+
try {
|
|
10
|
+
const site_dir = path.resolve(options.dir);
|
|
11
|
+
// Read primo.json
|
|
12
|
+
const config_path = path.join(site_dir, 'primo.json');
|
|
13
|
+
let config;
|
|
14
|
+
try {
|
|
15
|
+
const config_data = await fs.readFile(config_path, 'utf-8');
|
|
16
|
+
config = JSON.parse(config_data);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
spinner.fail('No primo.json found. Run `primo new` first.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
spinner.stop();
|
|
23
|
+
// Determine provider
|
|
24
|
+
let provider = options.provider;
|
|
25
|
+
if (!provider) {
|
|
26
|
+
const { selected_provider } = await inquirer.prompt([{
|
|
27
|
+
type: 'list',
|
|
28
|
+
name: 'selected_provider',
|
|
29
|
+
message: 'Where do you want to deploy?',
|
|
30
|
+
choices: [
|
|
31
|
+
{ name: 'Railway', value: 'railway' },
|
|
32
|
+
{ name: 'Fly.io', value: 'fly' }
|
|
33
|
+
]
|
|
34
|
+
}]);
|
|
35
|
+
provider = selected_provider;
|
|
36
|
+
}
|
|
37
|
+
// Check if provider CLI is installed
|
|
38
|
+
const cli_installed = await check_provider_cli(provider);
|
|
39
|
+
if (!cli_installed) {
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(chalk.yellow(`${provider} CLI not found. Install it first:`));
|
|
42
|
+
if (provider === 'railway') {
|
|
43
|
+
console.log(chalk.dim(' npm install -g @railway/cli'));
|
|
44
|
+
console.log(chalk.dim(' railway login'));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(chalk.dim(' curl -L https://fly.io/install.sh | sh'));
|
|
48
|
+
console.log(chalk.dim(' fly auth login'));
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
// Generate deployment files
|
|
53
|
+
spinner.start('Generating deployment files...');
|
|
54
|
+
await generate_dockerfile(site_dir, config);
|
|
55
|
+
if (provider === 'fly') {
|
|
56
|
+
await generate_fly_toml(site_dir, config);
|
|
57
|
+
}
|
|
58
|
+
spinner.succeed('Deployment files generated');
|
|
59
|
+
// Deploy
|
|
60
|
+
if (provider === 'railway') {
|
|
61
|
+
await deploy_to_railway(site_dir, config);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await deploy_to_fly(site_dir, config);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
spinner.fail(`Deployment failed: ${error instanceof Error ? error.message : error}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function check_provider_cli(provider) {
|
|
73
|
+
try {
|
|
74
|
+
if (provider === 'railway') {
|
|
75
|
+
execSync('railway --version', { stdio: 'ignore' });
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
execSync('fly version', { stdio: 'ignore' });
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function generate_dockerfile(site_dir, config) {
|
|
87
|
+
const dockerfile = `# Pala CMS Deployment
|
|
88
|
+
FROM golang:1.22-alpine AS builder
|
|
89
|
+
|
|
90
|
+
RUN apk add --no-cache git
|
|
91
|
+
|
|
92
|
+
WORKDIR /build
|
|
93
|
+
RUN git clone https://github.com/palacms/palacms.git . && \\
|
|
94
|
+
go build -o palacms .
|
|
95
|
+
|
|
96
|
+
FROM alpine:3.19
|
|
97
|
+
|
|
98
|
+
RUN apk add --no-cache ca-certificates
|
|
99
|
+
|
|
100
|
+
WORKDIR /app
|
|
101
|
+
|
|
102
|
+
COPY --from=builder /build/palacms /app/palacms
|
|
103
|
+
|
|
104
|
+
COPY blocks/ /app/pb_data/blocks/
|
|
105
|
+
COPY pages/ /app/pb_data/pages/
|
|
106
|
+
COPY page-types/ /app/pb_data/page-types/
|
|
107
|
+
COPY site/ /app/pb_data/site/
|
|
108
|
+
COPY uploads/ /app/pb_data/uploads/ 2>/dev/null || true
|
|
109
|
+
COPY primo.json /app/pb_data/
|
|
110
|
+
|
|
111
|
+
RUN chmod +x /app/palacms
|
|
112
|
+
|
|
113
|
+
ENV PB_DATA_DIR=/app/pb_data
|
|
114
|
+
|
|
115
|
+
EXPOSE 8080
|
|
116
|
+
|
|
117
|
+
CMD ["/app/palacms", "serve", "--http", "0.0.0.0:8080"]
|
|
118
|
+
`;
|
|
119
|
+
await fs.writeFile(path.join(site_dir, 'Dockerfile'), dockerfile);
|
|
120
|
+
const dockerignore = `node_modules/
|
|
121
|
+
.git/
|
|
122
|
+
.primo/
|
|
123
|
+
*.log
|
|
124
|
+
.DS_Store
|
|
125
|
+
`;
|
|
126
|
+
await fs.writeFile(path.join(site_dir, '.dockerignore'), dockerignore);
|
|
127
|
+
}
|
|
128
|
+
async function generate_fly_toml(site_dir, config) {
|
|
129
|
+
const app_name = config.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
130
|
+
const fly_toml = `app = "${app_name}"
|
|
131
|
+
primary_region = "sjc"
|
|
132
|
+
|
|
133
|
+
[build]
|
|
134
|
+
|
|
135
|
+
[env]
|
|
136
|
+
PB_DATA_DIR = "/app/pb_data"
|
|
137
|
+
|
|
138
|
+
[http_service]
|
|
139
|
+
internal_port = 8080
|
|
140
|
+
force_https = true
|
|
141
|
+
auto_stop_machines = true
|
|
142
|
+
auto_start_machines = true
|
|
143
|
+
min_machines_running = 0
|
|
144
|
+
|
|
145
|
+
[[vm]]
|
|
146
|
+
memory = "512mb"
|
|
147
|
+
cpu_kind = "shared"
|
|
148
|
+
cpus = 1
|
|
149
|
+
|
|
150
|
+
[mounts]
|
|
151
|
+
source = "pb_data"
|
|
152
|
+
destination = "/app/pb_data"
|
|
153
|
+
`;
|
|
154
|
+
await fs.writeFile(path.join(site_dir, 'fly.toml'), fly_toml);
|
|
155
|
+
}
|
|
156
|
+
async function deploy_to_railway(site_dir, config) {
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(chalk.cyan('Deploying to Railway...'));
|
|
159
|
+
const spinner = ora('Setting up Railway project...').start();
|
|
160
|
+
try {
|
|
161
|
+
try {
|
|
162
|
+
execSync('railway status', { cwd: site_dir, stdio: 'ignore' });
|
|
163
|
+
spinner.succeed('Linked to existing Railway project');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
spinner.text = 'Creating new Railway project...';
|
|
167
|
+
execSync(`railway init --name "${config.name}"`, { cwd: site_dir, stdio: 'inherit' });
|
|
168
|
+
spinner.succeed('Created new Railway project');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
spinner.fail('Failed to set up Railway project');
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(chalk.dim('Building and deploying...'));
|
|
177
|
+
console.log('');
|
|
178
|
+
const deploy_process = spawn('railway', ['up', '--detach'], {
|
|
179
|
+
cwd: site_dir,
|
|
180
|
+
stdio: 'inherit'
|
|
181
|
+
});
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
deploy_process.on('close', (code) => {
|
|
184
|
+
if (code === 0) {
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(chalk.green('✓ Deployment started'));
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(chalk.dim(' railway open - view deployment'));
|
|
189
|
+
console.log(chalk.dim(' railway logs - view logs'));
|
|
190
|
+
resolve();
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
reject(new Error(`Railway deployment failed with code ${code}`));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function deploy_to_fly(site_dir, config) {
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(chalk.cyan('Deploying to Fly.io...'));
|
|
201
|
+
const app_name = config.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
202
|
+
const spinner = ora('Checking Fly.io app...').start();
|
|
203
|
+
try {
|
|
204
|
+
execSync(`fly status --app ${app_name}`, { cwd: site_dir, stdio: 'ignore' });
|
|
205
|
+
spinner.succeed(`Found existing app: ${app_name}`);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
spinner.text = 'Creating Fly.io app...';
|
|
209
|
+
execSync(`fly apps create ${app_name}`, { cwd: site_dir, stdio: 'inherit' });
|
|
210
|
+
spinner.text = 'Creating persistent volume...';
|
|
211
|
+
execSync(`fly volumes create pb_data --size 1 --region sjc --app ${app_name}`, {
|
|
212
|
+
cwd: site_dir,
|
|
213
|
+
stdio: 'inherit'
|
|
214
|
+
});
|
|
215
|
+
spinner.succeed(`Created app: ${app_name}`);
|
|
216
|
+
}
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(chalk.dim('Building and deploying...'));
|
|
219
|
+
console.log('');
|
|
220
|
+
const deploy_process = spawn('fly', ['deploy'], {
|
|
221
|
+
cwd: site_dir,
|
|
222
|
+
stdio: 'inherit'
|
|
223
|
+
});
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
deploy_process.on('close', (code) => {
|
|
226
|
+
if (code === 0) {
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(chalk.green('✓ Deployed'));
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(chalk.cyan(` https://${app_name}.fly.dev`));
|
|
231
|
+
console.log(chalk.cyan(` https://${app_name}.fly.dev/_/ (admin)`));
|
|
232
|
+
resolve();
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
reject(new Error(`Fly deployment failed with code ${code}`));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import extract from 'extract-zip';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { get_auth_token } from '../utils/auth.js';
|
|
8
|
+
async function detect_server() {
|
|
9
|
+
// Check common local ports
|
|
10
|
+
const ports = [3000, 8080, 5173];
|
|
11
|
+
for (const port of ports) {
|
|
12
|
+
try {
|
|
13
|
+
const url = `http://127.0.0.1:${port}`;
|
|
14
|
+
const response = await fetch(`${url}/api/health`, {
|
|
15
|
+
signal: AbortSignal.timeout(500)
|
|
16
|
+
});
|
|
17
|
+
if (response.ok) {
|
|
18
|
+
return url;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Not running on this port
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export async function pull_site(options) {
|
|
28
|
+
const spinner = ora('Connecting...').start();
|
|
29
|
+
try {
|
|
30
|
+
// Detect or use provided server
|
|
31
|
+
let server;
|
|
32
|
+
if (options.server) {
|
|
33
|
+
server = options.server;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
spinner.text = 'Looking for local server...';
|
|
37
|
+
const detected = await detect_server();
|
|
38
|
+
// Default to localhost:3000 if no server detected
|
|
39
|
+
server = detected || 'http://localhost:3000';
|
|
40
|
+
spinner.text = `Using ${server}`;
|
|
41
|
+
}
|
|
42
|
+
// Get auth token (may not be needed for local)
|
|
43
|
+
const token = options.token || await get_auth_token(server);
|
|
44
|
+
// Local servers may not require auth
|
|
45
|
+
const headers = {};
|
|
46
|
+
if (token) {
|
|
47
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
48
|
+
}
|
|
49
|
+
let site_id = options.site;
|
|
50
|
+
let site_host;
|
|
51
|
+
// If no site specified, show interactive selection
|
|
52
|
+
if (!site_id) {
|
|
53
|
+
spinner.text = 'Fetching sites...';
|
|
54
|
+
const sites_response = await fetch(`${server}/api/collections/sites/records`, {
|
|
55
|
+
headers
|
|
56
|
+
});
|
|
57
|
+
if (!sites_response.ok) {
|
|
58
|
+
spinner.fail('Failed to fetch sites');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const sites_data = await sites_response.json();
|
|
62
|
+
const sites = sites_data.items || [];
|
|
63
|
+
if (sites.length === 0) {
|
|
64
|
+
spinner.fail('No sites found on this server');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
spinner.stop();
|
|
68
|
+
const { selected_site } = await inquirer.prompt([{
|
|
69
|
+
type: 'list',
|
|
70
|
+
name: 'selected_site',
|
|
71
|
+
message: 'Select a site to pull:',
|
|
72
|
+
choices: sites.map(site => ({
|
|
73
|
+
name: `${site.name} ${chalk.dim(`(${site.host})`)}`,
|
|
74
|
+
value: site
|
|
75
|
+
}))
|
|
76
|
+
}]);
|
|
77
|
+
site_id = selected_site.id;
|
|
78
|
+
site_host = selected_site.host;
|
|
79
|
+
spinner.start('Exporting site...');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Fetch site info to get hostname for folder name
|
|
83
|
+
spinner.text = 'Fetching site info...';
|
|
84
|
+
const site_response = await fetch(`${server}/api/collections/sites/records/${site_id}`, {
|
|
85
|
+
headers
|
|
86
|
+
});
|
|
87
|
+
if (site_response.ok) {
|
|
88
|
+
const site_data = await site_response.json();
|
|
89
|
+
site_host = site_data.host;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let output_dir = path.resolve(options.output);
|
|
93
|
+
// If output is default (.), use hostname as folder name
|
|
94
|
+
if (options.output === '.' && site_host) {
|
|
95
|
+
const hostname = site_host.split(':')[0];
|
|
96
|
+
if (hostname && hostname !== 'localhost') {
|
|
97
|
+
output_dir = path.resolve(hostname);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await fs.mkdir(output_dir, { recursive: true });
|
|
101
|
+
// Fetch the export
|
|
102
|
+
spinner.text = 'Exporting site...';
|
|
103
|
+
const response = await fetch(`${server}/api/palacms/export/${site_id}`, {
|
|
104
|
+
headers
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const error = await response.text();
|
|
108
|
+
spinner.fail(`Export failed: ${error}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
// Save ZIP temporarily
|
|
112
|
+
const zip_data = await response.arrayBuffer();
|
|
113
|
+
const temp_zip = path.join(output_dir, '.primo-export.zip');
|
|
114
|
+
await fs.writeFile(temp_zip, Buffer.from(zip_data));
|
|
115
|
+
// Extract ZIP
|
|
116
|
+
spinner.text = 'Extracting files...';
|
|
117
|
+
await extract(temp_zip, { dir: output_dir });
|
|
118
|
+
// Clean up temp ZIP
|
|
119
|
+
await fs.unlink(temp_zip);
|
|
120
|
+
// Copy JSON schemas
|
|
121
|
+
spinner.text = 'Adding JSON schemas...';
|
|
122
|
+
await copy_schemas(output_dir);
|
|
123
|
+
// Add $schema references
|
|
124
|
+
await add_schema_references(output_dir);
|
|
125
|
+
spinner.succeed(`Site exported to ${chalk.cyan(output_dir)}`);
|
|
126
|
+
// Show summary
|
|
127
|
+
const files = await count_files(output_dir);
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(chalk.dim(' Files exported:'));
|
|
130
|
+
console.log(chalk.dim(` blocks/ ${files.blocks} blocks`));
|
|
131
|
+
console.log(chalk.dim(` page-types/ ${files.page_types} page types`));
|
|
132
|
+
console.log(chalk.dim(` pages/ ${files.pages} pages`));
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(chalk.green(' Ready for local development!'));
|
|
135
|
+
console.log(chalk.dim(' Run `primo dev` to start the local server'));
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
spinner.fail(`Export failed: ${error instanceof Error ? error.message : error}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function count_files(dir) {
|
|
143
|
+
const counts = { blocks: 0, page_types: 0, pages: 0 };
|
|
144
|
+
try {
|
|
145
|
+
const blocks_dir = path.join(dir, 'blocks');
|
|
146
|
+
const entries = await fs.readdir(blocks_dir, { withFileTypes: true });
|
|
147
|
+
counts.blocks = entries.filter(e => e.isDirectory()).length;
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
try {
|
|
151
|
+
const pt_dir = path.join(dir, 'page-types');
|
|
152
|
+
const entries = await fs.readdir(pt_dir, { withFileTypes: true });
|
|
153
|
+
counts.page_types = entries.filter(e => e.isDirectory()).length;
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
try {
|
|
157
|
+
const pages_dir = path.join(dir, 'pages');
|
|
158
|
+
counts.pages = await count_json_files(pages_dir);
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
return counts;
|
|
162
|
+
}
|
|
163
|
+
async function count_json_files(dir) {
|
|
164
|
+
let count = 0;
|
|
165
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
count += await count_json_files(path.join(dir, entry.name));
|
|
169
|
+
}
|
|
170
|
+
else if (entry.name.endsWith('.json')) {
|
|
171
|
+
count++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
async function copy_schemas(output_dir) {
|
|
177
|
+
// Get path to schemas directory relative to compiled dist file
|
|
178
|
+
const current_file = new URL(import.meta.url).pathname;
|
|
179
|
+
const dist_dir = path.dirname(path.dirname(current_file)); // dist/
|
|
180
|
+
const project_root = path.dirname(dist_dir); // project root
|
|
181
|
+
const schemas_src = path.join(project_root, 'schemas');
|
|
182
|
+
const schemas_dest = path.join(output_dir, '.schemas');
|
|
183
|
+
await fs.mkdir(schemas_dest, { recursive: true });
|
|
184
|
+
const schema_files = await fs.readdir(schemas_src);
|
|
185
|
+
for (const file of schema_files) {
|
|
186
|
+
if (file.endsWith('.json')) {
|
|
187
|
+
await fs.copyFile(path.join(schemas_src, file), path.join(schemas_dest, file));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function add_schema_references(output_dir) {
|
|
192
|
+
// Add $schema to block fields.json
|
|
193
|
+
const blocks_dir = path.join(output_dir, 'blocks');
|
|
194
|
+
try {
|
|
195
|
+
const blocks = await fs.readdir(blocks_dir, { withFileTypes: true });
|
|
196
|
+
for (const block of blocks) {
|
|
197
|
+
if (block.isDirectory()) {
|
|
198
|
+
const fields_path = path.join(blocks_dir, block.name, 'fields.json');
|
|
199
|
+
try {
|
|
200
|
+
const fields = JSON.parse(await fs.readFile(fields_path, 'utf-8'));
|
|
201
|
+
// Create new object with $schema first
|
|
202
|
+
const with_schema = {
|
|
203
|
+
$schema: '../../.schemas/fields.schema.json',
|
|
204
|
+
...fields
|
|
205
|
+
};
|
|
206
|
+
await fs.writeFile(fields_path, JSON.stringify(with_schema, null, 2) + '\n');
|
|
207
|
+
}
|
|
208
|
+
catch { }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
// Add $schema to page-type config.json
|
|
214
|
+
const page_types_dir = path.join(output_dir, 'page-types');
|
|
215
|
+
try {
|
|
216
|
+
const page_types = await fs.readdir(page_types_dir, { withFileTypes: true });
|
|
217
|
+
for (const page_type of page_types) {
|
|
218
|
+
if (page_type.isDirectory()) {
|
|
219
|
+
const config_path = path.join(page_types_dir, page_type.name, 'config.json');
|
|
220
|
+
try {
|
|
221
|
+
const config = JSON.parse(await fs.readFile(config_path, 'utf-8'));
|
|
222
|
+
// Create new object with $schema first
|
|
223
|
+
const with_schema = {
|
|
224
|
+
$schema: '../../.schemas/page-type-config.schema.json',
|
|
225
|
+
...config
|
|
226
|
+
};
|
|
227
|
+
await fs.writeFile(config_path, JSON.stringify(with_schema, null, 2) + '\n');
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
// Add $schema to site fields.json
|
|
235
|
+
const site_fields_path = path.join(output_dir, 'site/fields.json');
|
|
236
|
+
try {
|
|
237
|
+
const site_fields = JSON.parse(await fs.readFile(site_fields_path, 'utf-8'));
|
|
238
|
+
// Site fields is an array, so we need to add $schema differently
|
|
239
|
+
// Since JSON Schema doesn't support $schema in arrays, we'll skip this for now
|
|
240
|
+
// IDEs can still use the schema if users manually add it via settings
|
|
241
|
+
}
|
|
242
|
+
catch { }
|
|
243
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import archiver from 'archiver';
|
|
6
|
+
import { get_auth_token } from '../utils/auth.js';
|
|
7
|
+
export async function push_site(options) {
|
|
8
|
+
const spinner = ora('Reading local files...').start();
|
|
9
|
+
try {
|
|
10
|
+
const site_dir = path.resolve(options.dir);
|
|
11
|
+
// Read primo.json for server/site info
|
|
12
|
+
const config_path = path.join(site_dir, 'primo.json');
|
|
13
|
+
let config = null;
|
|
14
|
+
try {
|
|
15
|
+
const config_data = await fs.readFile(config_path, 'utf-8');
|
|
16
|
+
config = JSON.parse(config_data);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// No config file, must provide options
|
|
20
|
+
}
|
|
21
|
+
const server = options.server || config?.server;
|
|
22
|
+
const site_id = options.site || config?.site_id;
|
|
23
|
+
if (!server) {
|
|
24
|
+
spinner.fail('Server URL required. Use --server or add server field to primo.json.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (!site_id) {
|
|
28
|
+
spinner.fail('Site ID required. Use --site or add site_id field to primo.json.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Get auth token
|
|
32
|
+
const token = options.token || await get_auth_token(server);
|
|
33
|
+
if (!token) {
|
|
34
|
+
spinner.fail('Authentication required. Use --token or run `primo login` first.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
// Create ZIP of the site directory
|
|
38
|
+
spinner.text = 'Packaging files...';
|
|
39
|
+
const zip_buffer = await create_zip(site_dir);
|
|
40
|
+
// Send to server
|
|
41
|
+
const endpoint = options.preview
|
|
42
|
+
? `${server}/api/palacms/import/${site_id}/preview`
|
|
43
|
+
: `${server}/api/palacms/import/${site_id}`;
|
|
44
|
+
spinner.text = options.preview ? 'Previewing changes...' : 'Pushing changes...';
|
|
45
|
+
const form_data = new FormData();
|
|
46
|
+
form_data.append('file', new Blob([zip_buffer]), 'site.zip');
|
|
47
|
+
const response = await fetch(endpoint, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Authorization': `Bearer ${token}`
|
|
51
|
+
},
|
|
52
|
+
body: form_data
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const error = await response.text();
|
|
56
|
+
spinner.fail(`Push failed: ${error}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const result = await response.json();
|
|
60
|
+
if (options.preview) {
|
|
61
|
+
spinner.succeed('Preview complete');
|
|
62
|
+
console.log('');
|
|
63
|
+
print_diff(result.diff);
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(chalk.dim(' Run without --preview to apply these changes'));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
spinner.succeed('Push complete');
|
|
69
|
+
console.log('');
|
|
70
|
+
print_diff(result.diff);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
spinner.fail(`Push failed: ${error instanceof Error ? error.message : error}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function create_zip(dir) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
81
|
+
const chunks = [];
|
|
82
|
+
archive.on('data', chunk => chunks.push(chunk));
|
|
83
|
+
archive.on('end', () => resolve(Buffer.concat(chunks)));
|
|
84
|
+
archive.on('error', reject);
|
|
85
|
+
// Add directories
|
|
86
|
+
const dirs_to_include = ['blocks', 'page-types', 'pages', 'site', 'uploads'];
|
|
87
|
+
for (const subdir of dirs_to_include) {
|
|
88
|
+
const full_path = path.join(dir, subdir);
|
|
89
|
+
archive.directory(full_path, subdir);
|
|
90
|
+
}
|
|
91
|
+
// Add primo.json
|
|
92
|
+
archive.file(path.join(dir, 'primo.json'), { name: 'primo.json' });
|
|
93
|
+
archive.finalize();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function print_diff(diff) {
|
|
97
|
+
let has_changes = false;
|
|
98
|
+
for (const [section, changes] of Object.entries(diff)) {
|
|
99
|
+
const { added, modified, deleted } = changes;
|
|
100
|
+
if (added.length === 0 && modified.length === 0 && deleted.length === 0) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
has_changes = true;
|
|
104
|
+
console.log(chalk.bold(` ${section}:`));
|
|
105
|
+
for (const item of added) {
|
|
106
|
+
console.log(chalk.green(` + ${item}`));
|
|
107
|
+
}
|
|
108
|
+
for (const item of modified) {
|
|
109
|
+
console.log(chalk.yellow(` ~ ${item}`));
|
|
110
|
+
}
|
|
111
|
+
for (const item of deleted) {
|
|
112
|
+
console.log(chalk.red(` - ${item}`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!has_changes) {
|
|
116
|
+
console.log(chalk.dim(' No changes detected'));
|
|
117
|
+
}
|
|
118
|
+
}
|