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.
@@ -0,0 +1,9 @@
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 {};
@@ -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,5 @@
1
+ interface InitOptions {
2
+ port?: string;
3
+ }
4
+ export declare function init_server(options: InitOptions): Promise<void>;
5
+ export {};
@@ -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,7 @@
1
+ interface LoginOptions {
2
+ server: string;
3
+ email?: string;
4
+ password?: string;
5
+ }
6
+ export declare function login(options: LoginOptions): Promise<void>;
7
+ export {};
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ interface NewOptions {
2
+ name?: string;
3
+ template?: string;
4
+ skipDev?: boolean;
5
+ }
6
+ export declare function new_site(options: NewOptions): Promise<void>;
7
+ export {};