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,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 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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
export async function init_server(options) {
|
|
6
|
+
const spinner = ora('Initializing Pala server...').start();
|
|
7
|
+
try {
|
|
8
|
+
const base_dir = process.cwd();
|
|
9
|
+
const server_config_path = path.join(base_dir, 'server.json');
|
|
10
|
+
// Check if server.json already exists
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(server_config_path);
|
|
13
|
+
spinner.fail('server.json already exists');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Good, doesn't exist
|
|
18
|
+
}
|
|
19
|
+
// Check if pala.json exists (single-site mode)
|
|
20
|
+
const pala_config_path = path.join(base_dir, 'pala.json');
|
|
21
|
+
try {
|
|
22
|
+
await fs.access(pala_config_path);
|
|
23
|
+
spinner.fail('This directory contains a site (pala.json). Run pala init in a parent directory.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Good, not a site directory
|
|
28
|
+
}
|
|
29
|
+
const port = options.port ? parseInt(options.port, 10) : 3000;
|
|
30
|
+
const server_config = {
|
|
31
|
+
port
|
|
32
|
+
};
|
|
33
|
+
await fs.writeFile(server_config_path, JSON.stringify(server_config, null, 2) + '\n');
|
|
34
|
+
// Discover existing sites
|
|
35
|
+
const entries = await fs.readdir(base_dir, { withFileTypes: true });
|
|
36
|
+
const sites = [];
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
39
|
+
const site_config = path.join(base_dir, entry.name, 'pala.json');
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(site_config);
|
|
42
|
+
sites.push(entry.name);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Not a site
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
spinner.succeed('Server initialized');
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(` ${chalk.cyan('server.json')} created`);
|
|
52
|
+
console.log(` ${chalk.dim('Port:')} ${port}`);
|
|
53
|
+
if (sites.length > 0) {
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(` ${chalk.dim('Discovered sites:')}`);
|
|
56
|
+
for (const site of sites) {
|
|
57
|
+
console.log(` ${chalk.dim('•')} ${site}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(chalk.green(' Run `pala new <hostname>` to create a site'));
|
|
62
|
+
console.log(chalk.green(' Run `pala dev` to start the server'));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
spinner.fail(`Failed to initialize: ${error instanceof Error ? error.message : error}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
import { save_auth_token } from '../utils/auth.js';
|
|
5
|
+
export async function login(options) {
|
|
6
|
+
const server = normalize_server_url(options.server);
|
|
7
|
+
console.log('');
|
|
8
|
+
console.log(chalk.bold(`Logging in to ${server}`));
|
|
9
|
+
console.log('');
|
|
10
|
+
// Get email and password
|
|
11
|
+
let email = options.email;
|
|
12
|
+
let password = options.password;
|
|
13
|
+
if (!email) {
|
|
14
|
+
email = await prompt('Email: ');
|
|
15
|
+
}
|
|
16
|
+
if (!password) {
|
|
17
|
+
password = await prompt_password('Password: ');
|
|
18
|
+
}
|
|
19
|
+
const spinner = ora('Authenticating...').start();
|
|
20
|
+
try {
|
|
21
|
+
// Authenticate with PocketBase
|
|
22
|
+
const response = await fetch(`${server}/api/collections/users/auth-with-password`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json'
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
identity: email,
|
|
29
|
+
password: password
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const error = await response.json().catch(() => ({ message: 'Authentication failed' }));
|
|
34
|
+
spinner.fail(`Login failed: ${error.message || 'Invalid credentials'}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
// Save the token
|
|
39
|
+
await save_auth_token(server, data.token);
|
|
40
|
+
spinner.succeed(`Logged in as ${chalk.cyan(data.record.email)}`);
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(chalk.dim(' Token saved to ~/.primo/tokens.json'));
|
|
43
|
+
console.log(chalk.dim(' You can now use `primo pull` and `primo push` without --token'));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
spinner.fail(`Login failed: ${error instanceof Error ? error.message : error}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function normalize_server_url(server) {
|
|
51
|
+
// Add https:// if no protocol specified
|
|
52
|
+
if (!server.startsWith('http://') && !server.startsWith('https://')) {
|
|
53
|
+
server = `https://${server}`;
|
|
54
|
+
}
|
|
55
|
+
// Remove trailing slash
|
|
56
|
+
return server.replace(/\/+$/, '');
|
|
57
|
+
}
|
|
58
|
+
function prompt(question) {
|
|
59
|
+
const rl = readline.createInterface({
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout
|
|
62
|
+
});
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
rl.question(question, (answer) => {
|
|
65
|
+
rl.close();
|
|
66
|
+
resolve(answer);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function prompt_password(question) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const rl = readline.createInterface({
|
|
73
|
+
input: process.stdin,
|
|
74
|
+
output: process.stdout
|
|
75
|
+
});
|
|
76
|
+
// Disable echoing for password input
|
|
77
|
+
if (process.stdin.isTTY) {
|
|
78
|
+
process.stdout.write(question);
|
|
79
|
+
const stdin = process.stdin;
|
|
80
|
+
stdin.setRawMode(true);
|
|
81
|
+
stdin.resume();
|
|
82
|
+
let password = '';
|
|
83
|
+
stdin.on('data', function handler(char) {
|
|
84
|
+
const c = char.toString('utf8');
|
|
85
|
+
switch (c) {
|
|
86
|
+
case '\n':
|
|
87
|
+
case '\r':
|
|
88
|
+
case '\u0004': // Ctrl+D
|
|
89
|
+
stdin.setRawMode(false);
|
|
90
|
+
stdin.pause();
|
|
91
|
+
stdin.removeListener('data', handler);
|
|
92
|
+
process.stdout.write('\n');
|
|
93
|
+
rl.close();
|
|
94
|
+
resolve(password);
|
|
95
|
+
break;
|
|
96
|
+
case '\u0003': // Ctrl+C
|
|
97
|
+
process.exit();
|
|
98
|
+
break;
|
|
99
|
+
case '\u007F': // Backspace
|
|
100
|
+
if (password.length > 0) {
|
|
101
|
+
password = password.slice(0, -1);
|
|
102
|
+
process.stdout.clearLine(0);
|
|
103
|
+
process.stdout.cursorTo(0);
|
|
104
|
+
process.stdout.write(question + '*'.repeat(password.length));
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
password += c;
|
|
109
|
+
process.stdout.clearLine(0);
|
|
110
|
+
process.stdout.cursorTo(0);
|
|
111
|
+
process.stdout.write(question + '*'.repeat(password.length));
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Non-TTY fallback
|
|
118
|
+
rl.question(question, (answer) => {
|
|
119
|
+
rl.close();
|
|
120
|
+
resolve(answer);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|