primo-cli 0.1.2 → 0.1.4

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.
Files changed (39) hide show
  1. package/README.md +113 -41
  2. package/dist/commands/build.js +488 -272
  3. package/dist/commands/deploy.d.ts +1 -1
  4. package/dist/commands/deploy.js +293 -141
  5. package/dist/commands/dev.d.ts +2 -0
  6. package/dist/commands/dev.js +2007 -150
  7. package/dist/commands/init.d.ts +2 -2
  8. package/dist/commands/init.js +65 -43
  9. package/dist/commands/login.d.ts +1 -2
  10. package/dist/commands/login.js +24 -6
  11. package/dist/commands/new.js +161 -274
  12. package/dist/commands/pull-library.d.ts +7 -0
  13. package/dist/commands/pull-library.js +92 -0
  14. package/dist/commands/pull.d.ts +0 -1
  15. package/dist/commands/pull.js +160 -165
  16. package/dist/commands/push-library.d.ts +7 -0
  17. package/dist/commands/push-library.js +88 -0
  18. package/dist/commands/push.d.ts +2 -0
  19. package/dist/commands/push.js +358 -51
  20. package/dist/commands/validate.d.ts +1 -1
  21. package/dist/commands/validate.js +379 -161
  22. package/dist/index.js +110 -20
  23. package/dist/utils/binary.js +1 -1
  24. package/dist/utils/format.d.ts +12 -0
  25. package/dist/utils/format.js +98 -0
  26. package/dist/utils/head-svelte.d.ts +2 -0
  27. package/dist/utils/head-svelte.js +53 -0
  28. package/dist/utils/server-config.d.ts +19 -0
  29. package/dist/utils/server-config.js +49 -0
  30. package/dist/utils/site-config.d.ts +11 -0
  31. package/dist/utils/site-config.js +14 -0
  32. package/package.json +9 -5
  33. package/scripts/postinstall.js +1 -1
  34. package/dist/commands/export.d.ts +0 -8
  35. package/dist/commands/export.js +0 -163
  36. package/dist/commands/import.d.ts +0 -9
  37. package/dist/commands/import.js +0 -118
  38. package/dist/commands/publish.d.ts +0 -6
  39. package/dist/commands/publish.js +0 -239
@@ -1,163 +0,0 @@
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 { get_auth_token } from '../utils/auth.js';
7
- export async function export_site(options) {
8
- const spinner = ora('Connecting to server...').start();
9
- try {
10
- // Get auth token
11
- const token = options.token || await get_auth_token(options.server);
12
- if (!token) {
13
- spinner.fail('Authentication required. Use --token or run `pala login` first.');
14
- process.exit(1);
15
- }
16
- // Create output directory
17
- const output_dir = path.resolve(options.output);
18
- await fs.mkdir(output_dir, { recursive: true });
19
- // Fetch the export
20
- spinner.text = 'Exporting site...';
21
- const response = await fetch(`${options.server}/api/palacms/export/${options.site}`, {
22
- headers: {
23
- 'Authorization': `Bearer ${token}`
24
- }
25
- });
26
- if (!response.ok) {
27
- const error = await response.text();
28
- spinner.fail(`Export failed: ${error}`);
29
- process.exit(1);
30
- }
31
- // Save ZIP temporarily
32
- const zip_data = await response.arrayBuffer();
33
- const temp_zip = path.join(output_dir, '.pala-export.zip');
34
- await fs.writeFile(temp_zip, Buffer.from(zip_data));
35
- // Extract ZIP
36
- spinner.text = 'Extracting files...';
37
- await extract(temp_zip, { dir: output_dir });
38
- // Clean up temp ZIP
39
- await fs.unlink(temp_zip);
40
- // Copy JSON schemas
41
- spinner.text = 'Adding JSON schemas...';
42
- await copy_schemas(output_dir);
43
- // Add $schema references
44
- await add_schema_references(output_dir);
45
- spinner.succeed(`Site exported to ${chalk.cyan(output_dir)}`);
46
- // Show summary
47
- const files = await count_files(output_dir);
48
- console.log('');
49
- console.log(chalk.dim(' Files exported:'));
50
- console.log(chalk.dim(` blocks/ ${files.blocks} blocks`));
51
- console.log(chalk.dim(` page-types/ ${files.page_types} page types`));
52
- console.log(chalk.dim(` pages/ ${files.pages} pages`));
53
- console.log('');
54
- console.log(chalk.green(' Ready for local development!'));
55
- console.log(chalk.dim(' Run `pala dev` to start the local server'));
56
- }
57
- catch (error) {
58
- spinner.fail(`Export failed: ${error instanceof Error ? error.message : error}`);
59
- process.exit(1);
60
- }
61
- }
62
- async function count_files(dir) {
63
- const counts = { blocks: 0, page_types: 0, pages: 0 };
64
- try {
65
- const blocks_dir = path.join(dir, 'blocks');
66
- const entries = await fs.readdir(blocks_dir, { withFileTypes: true });
67
- counts.blocks = entries.filter(e => e.isDirectory()).length;
68
- }
69
- catch { }
70
- try {
71
- const pt_dir = path.join(dir, 'page-types');
72
- const entries = await fs.readdir(pt_dir, { withFileTypes: true });
73
- counts.page_types = entries.filter(e => e.isDirectory()).length;
74
- }
75
- catch { }
76
- try {
77
- const pages_dir = path.join(dir, 'pages');
78
- counts.pages = await count_json_files(pages_dir);
79
- }
80
- catch { }
81
- return counts;
82
- }
83
- async function count_json_files(dir) {
84
- let count = 0;
85
- const entries = await fs.readdir(dir, { withFileTypes: true });
86
- for (const entry of entries) {
87
- if (entry.isDirectory()) {
88
- count += await count_json_files(path.join(dir, entry.name));
89
- }
90
- else if (entry.name.endsWith('.json')) {
91
- count++;
92
- }
93
- }
94
- return count;
95
- }
96
- async function copy_schemas(output_dir) {
97
- // Get path to schemas directory relative to compiled dist file
98
- const current_file = new URL(import.meta.url).pathname;
99
- const dist_dir = path.dirname(path.dirname(current_file)); // dist/
100
- const project_root = path.dirname(dist_dir); // project root
101
- const schemas_src = path.join(project_root, 'schemas');
102
- const schemas_dest = path.join(output_dir, '.schemas');
103
- await fs.mkdir(schemas_dest, { recursive: true });
104
- const schema_files = await fs.readdir(schemas_src);
105
- for (const file of schema_files) {
106
- if (file.endsWith('.json')) {
107
- await fs.copyFile(path.join(schemas_src, file), path.join(schemas_dest, file));
108
- }
109
- }
110
- }
111
- async function add_schema_references(output_dir) {
112
- // Add $schema to block fields.json
113
- const blocks_dir = path.join(output_dir, 'blocks');
114
- try {
115
- const blocks = await fs.readdir(blocks_dir, { withFileTypes: true });
116
- for (const block of blocks) {
117
- if (block.isDirectory()) {
118
- const fields_path = path.join(blocks_dir, block.name, 'fields.json');
119
- try {
120
- const fields = JSON.parse(await fs.readFile(fields_path, 'utf-8'));
121
- // Create new object with $schema first
122
- const with_schema = {
123
- $schema: '../../.schemas/fields.schema.json',
124
- ...fields
125
- };
126
- await fs.writeFile(fields_path, JSON.stringify(with_schema, null, 2) + '\n');
127
- }
128
- catch { }
129
- }
130
- }
131
- }
132
- catch { }
133
- // Add $schema to page-type config.json
134
- const page_types_dir = path.join(output_dir, 'page-types');
135
- try {
136
- const page_types = await fs.readdir(page_types_dir, { withFileTypes: true });
137
- for (const page_type of page_types) {
138
- if (page_type.isDirectory()) {
139
- const config_path = path.join(page_types_dir, page_type.name, 'config.json');
140
- try {
141
- const config = JSON.parse(await fs.readFile(config_path, 'utf-8'));
142
- // Create new object with $schema first
143
- const with_schema = {
144
- $schema: '../../.schemas/page-type-config.schema.json',
145
- ...config
146
- };
147
- await fs.writeFile(config_path, JSON.stringify(with_schema, null, 2) + '\n');
148
- }
149
- catch { }
150
- }
151
- }
152
- }
153
- catch { }
154
- // Add $schema to site fields.json
155
- const site_fields_path = path.join(output_dir, 'site/fields.json');
156
- try {
157
- const site_fields = JSON.parse(await fs.readFile(site_fields_path, 'utf-8'));
158
- // Site fields is an array, so we need to add $schema differently
159
- // Since JSON Schema doesn't support $schema in arrays, we'll skip this for now
160
- // IDEs can still use the schema if users manually add it via settings
161
- }
162
- catch { }
163
- }
@@ -1,9 +0,0 @@
1
- interface ImportOptions {
2
- server?: string;
3
- site?: string;
4
- dir: string;
5
- token?: string;
6
- preview?: boolean;
7
- }
8
- export declare function import_site(options: ImportOptions): Promise<void>;
9
- export {};
@@ -1,118 +0,0 @@
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 import_site(options) {
8
- const spinner = ora('Reading local files...').start();
9
- try {
10
- const site_dir = path.resolve(options.dir);
11
- // Read pala.json for server/site info
12
- const config_path = path.join(site_dir, 'pala.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 ensure pala.json has server field.');
25
- process.exit(1);
26
- }
27
- if (!site_id) {
28
- spinner.fail('Site ID required. Use --site or ensure pala.json has site_id field.');
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 `pala 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...' : 'Importing 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(`Import 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('Import complete');
69
- console.log('');
70
- print_diff(result.diff);
71
- }
72
- }
73
- catch (error) {
74
- spinner.fail(`Import 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 pala.json
92
- archive.file(path.join(dir, 'pala.json'), { name: 'pala.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
- }
@@ -1,6 +0,0 @@
1
- interface PublishOptions {
2
- dir: string;
3
- provider?: string;
4
- }
5
- export declare function publish(options: PublishOptions): Promise<void>;
6
- export {};
@@ -1,239 +0,0 @@
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
- }