primo-cli 0.1.3 → 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 (38) hide show
  1. package/README.md +111 -39
  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 +109 -19
  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 +8 -4
  33. package/dist/commands/export.d.ts +0 -8
  34. package/dist/commands/export.js +0 -163
  35. package/dist/commands/import.d.ts +0 -9
  36. package/dist/commands/import.js +0 -118
  37. package/dist/commands/publish.d.ts +0 -6
  38. package/dist/commands/publish.js +0 -239
package/dist/index.js CHANGED
@@ -1,21 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { init_workspace } from './commands/init.js';
3
5
  import { new_site } from './commands/new.js';
4
6
  import { pull_site } from './commands/pull.js';
5
7
  import { push_site } from './commands/push.js';
8
+ import { pull_library } from './commands/pull-library.js';
9
+ import { push_library } from './commands/push-library.js';
6
10
  import { dev_server } from './commands/dev.js';
7
11
  import { login } from './commands/login.js';
8
12
  import { validate_site } from './commands/validate.js';
9
- import { publish } from './commands/publish.js';
13
+ import { deploy } from './commands/deploy.js';
10
14
  import { build_site } from './commands/build.js';
11
15
  const program = new Command();
12
16
  program
13
17
  .name('primo')
14
18
  .description('Build sites visually, edit them anywhere')
15
19
  .version('0.1.3');
20
+ // Top-level help: prepend a Deploy-vs-build-vs-push decision tree so first-time
21
+ // users can pick a command without reading every description. Use 'before' so
22
+ // it only renders for the root program, not subcommand help.
23
+ program.addHelpText('before', `
24
+ ${chalk.bold('Local development')}
25
+ ${chalk.cyan('primo dev')} Run the local CMS on this workspace
26
+
27
+ ${chalk.bold('Going live — pick one')}
28
+ Want others to edit content? .................. ${chalk.cyan('primo deploy')}
29
+ Just hosting a static blog? ................... ${chalk.cyan('primo build')}
30
+ Already have a hosted Primo server? ........... ${chalk.cyan('primo push')}
31
+ `);
32
+ program
33
+ .command('init [name]')
34
+ .description('Initialize a new Primo workspace (server) in a new folder or the current directory')
35
+ .action((name) => init_workspace({ name }));
16
36
  program
17
37
  .command('new [name]')
18
- .description('Create a new site and start local CMS')
38
+ .description('Create a new site in the current workspace and start local CMS')
19
39
  .option('-t, --template <template>', 'Starter template')
20
40
  .option('--skip-dev', 'Create files only, don\'t start CMS')
21
41
  .action((name, options) => new_site({ name, ...options }));
@@ -24,37 +44,82 @@ program
24
44
  .description('Start local CMS')
25
45
  .option('-d, --dir <dir>', 'Site directory', '.')
26
46
  .option('-p, --port <port>', 'Port', '3000')
47
+ .option('-f, --force', 'Kill existing processes on the port')
48
+ .option('--author <mode>', 'Who is authoring this session: "files" (push only; CMS UI is read-only — default), "cms" (CMS edits write to files; file edits revert), "both" (bidirectional; CMS edits often lost on conflict — beta)', 'files')
27
49
  .action(dev_server);
28
50
  program
29
- .command('publish')
30
- .description('Deploy site with CMS')
31
- .option('-d, --dir <dir>', 'Site directory', '.')
32
- .option('-p, --provider <provider>', 'Provider (railway, fly)')
33
- .action(publish);
51
+ .command('deploy')
52
+ .description('Deploy this workspace (all sites) with editable CMS (Railway, Fly)')
53
+ .option('-p, --provider <provider>', 'Provider: railway | fly')
54
+ .option('--dry-run', 'Show what would be deployed without doing anything')
55
+ .addHelpText('after', `
56
+ ${chalk.bold('Supported providers')}
57
+ railway Railway (railway.com) — requires the Railway CLI and a logged-in account
58
+ fly Fly.io — requires the flyctl CLI and a logged-in account
59
+
60
+ For other hosts (Netlify, Vercel, Cloudflare, GitHub Pages), use ${chalk.cyan('primo build')}
61
+ on a single site and deploy the output folder with that host's CLI.
62
+
63
+ ${chalk.bold('Workspace layout uploaded as one unit')}
64
+ server.yaml
65
+ library/ (if present)
66
+ sites/ (every site under this directory)
67
+
68
+ ${chalk.bold('See also')}
69
+ primo build Export a single site as static files
70
+ primo push Sync local changes to an existing hosted Primo server
71
+ `)
72
+ .action(deploy);
34
73
  program
35
- .command('push')
36
- .description('Push local files to hosted CMS')
74
+ .command('push [server]')
75
+ .description('Sync local changes to an existing hosted Primo server')
37
76
  .option('-s, --server <url>', 'Server URL')
38
77
  .option('--site <id>', 'Site ID')
78
+ .option('--only <slug>', 'Push only the named site folder under sites/ (skips library)')
39
79
  .option('-d, --dir <dir>', 'Directory', '.')
40
80
  .option('-t, --token <token>', 'Auth token')
41
81
  .option('--preview', 'Preview only')
42
- .action(push_site);
82
+ .option('--dry-run', 'Show what would be pushed without sending requests')
83
+ .addHelpText('after', `
84
+ ${chalk.bold('Requires an existing hosted Primo server.')}
85
+ Run ${chalk.cyan('primo deploy')} first to create one, then ${chalk.cyan('primo login -s <server-url>')}
86
+ to authenticate this machine before pushing.
87
+
88
+ ${chalk.bold('See also')}
89
+ primo deploy Stand up a new hosted Primo server
90
+ primo login Authenticate with a hosted Primo server
91
+ `)
92
+ .action((server, options) => push_site({ ...options, server: server || options.server }));
43
93
  program
44
- .command('pull')
45
- .description('Pull from hosted CMS to local files')
94
+ .command('pull [server]')
95
+ .description('Pull entire server (all sites + library) to local files')
96
+ .option('-s, --server <url>', 'Server URL (auto-detects local)')
97
+ .option('-o, --output <dir>', 'Output directory (defaults to ./<server-hostname>)', '.')
98
+ .option('-t, --token <token>', 'Auth token')
99
+ .action((server, options) => pull_site({ ...options, server: server || options.server }));
100
+ const library = program
101
+ .command('library')
102
+ .description('Manage shared block library');
103
+ library
104
+ .command('pull [server]')
105
+ .description('Pull shared library to local files')
46
106
  .option('-s, --server <url>', 'Server URL (auto-detects local)')
47
- .option('--site <id>', 'Site ID (interactive if not provided)')
48
107
  .option('-o, --output <dir>', 'Output directory', '.')
49
108
  .option('-t, --token <token>', 'Auth token')
50
- .action(pull_site);
109
+ .action((server, options) => pull_library({ ...options, server: server || options.server }));
110
+ library
111
+ .command('push [server]')
112
+ .description('Push local shared library to hosted CMS')
113
+ .option('-s, --server <url>', 'Server URL')
114
+ .option('-d, --dir <dir>', 'Workspace directory', '.')
115
+ .option('-t, --token <token>', 'Auth token')
116
+ .action((server, options) => push_library({ ...options, server: server || options.server }));
51
117
  program
52
118
  .command('login')
53
119
  .description('Login to hosted CMS')
54
- .argument('<server>', 'Server URL')
120
+ .option('-s, --server <url>', 'Server URL (defaults to `server:` in server.yaml)')
55
121
  .option('-e, --email <email>', 'Email')
56
- .option('-p, --password <password>', 'Password')
57
- .action((server, options) => login({ server, ...options }));
122
+ .action(login);
58
123
  program
59
124
  .command('validate')
60
125
  .description('Validate site structure')
@@ -63,8 +128,33 @@ program
63
128
  .action(validate_site);
64
129
  program
65
130
  .command('build')
66
- .description('Build static site')
131
+ .description('Export a single site as static files for any host (Netlify, Vercel, etc.)')
67
132
  .option('-d, --dir <dir>', 'Site directory', '.')
68
- .option('-o, --output <dir>', 'Output directory', 'dist')
133
+ .option('-o, --output <dir>', 'Output directory', '_site')
134
+ .addHelpText('after', `
135
+ ${chalk.bold('See also')}
136
+ primo deploy Want collaborators to edit content from a CMS UI? Use deploy instead —
137
+ it ships the workspace with an editable CMS to Railway or Fly.
138
+ `)
69
139
  .action(build_site);
140
+ // Custom unknown-command handler. commander's default suggestion engine is
141
+ // based on Levenshtein distance and won't reach across renames like
142
+ // publish→deploy, so handle the common renamed/unknown cases explicitly.
143
+ program.on('command:*', (operands) => {
144
+ const cmd = operands[0];
145
+ console.error('');
146
+ console.error(chalk.red(`Unknown command: ${cmd}`));
147
+ console.error('');
148
+ if (cmd === 'publish') {
149
+ console.error(` ${chalk.cyan('primo publish')} has been replaced by ${chalk.cyan('primo deploy')}.`);
150
+ console.error(` ${chalk.dim('It now deploys your whole workspace (all sites + library) as one unit.')}`);
151
+ console.error('');
152
+ console.error(` Run: ${chalk.cyan('primo deploy --help')}`);
153
+ }
154
+ else {
155
+ console.error(` Run ${chalk.cyan('primo --help')} to see available commands.`);
156
+ }
157
+ console.error('');
158
+ process.exit(1);
159
+ });
70
160
  program.parse();
@@ -10,7 +10,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const PRIMO_HOME = path.join(os.homedir(), '.primo');
11
11
  const BIN_DIR = path.join(PRIMO_HOME, 'bin');
12
12
  const DATA_DIR = path.join(PRIMO_HOME, 'data');
13
- const VERSION = '0.1.0'; // TODO: fetch latest from GitHub
13
+ const VERSION = '3.1.0'; // matches palacms releases
14
14
  // Path to locally built binary (for development)
15
15
  // The binary is at palacms/palacms (inside the palacms repo directory)
16
16
  const LOCAL_BINARY = path.resolve(__dirname, '..', '..', '..', 'palacms', 'palacms');
@@ -0,0 +1,12 @@
1
+ export interface FormatOptions {
2
+ enabled: boolean;
3
+ use_tabs: boolean;
4
+ tab_width: number;
5
+ print_width: number;
6
+ semi: boolean;
7
+ single_quote: boolean;
8
+ trailing_comma: 'none' | 'es5' | 'all';
9
+ }
10
+ export declare const DEFAULT_FORMAT_OPTIONS: FormatOptions;
11
+ export declare function should_format(file_path: string): boolean;
12
+ export declare function format_file_contents(file_path: string, contents: string, workspace_dir: string, options: FormatOptions): Promise<string>;
@@ -0,0 +1,98 @@
1
+ import path from 'path';
2
+ import { createRequire } from 'module';
3
+ import chalk from 'chalk';
4
+ export const DEFAULT_FORMAT_OPTIONS = {
5
+ enabled: true,
6
+ use_tabs: true,
7
+ tab_width: 2,
8
+ print_width: 100,
9
+ semi: false,
10
+ single_quote: true,
11
+ trailing_comma: 'none'
12
+ };
13
+ const FORMATTABLE_EXTENSIONS = new Set(['.yaml', '.yml', '.svelte', '.ts', '.js', '.json']);
14
+ let prettier_module = undefined;
15
+ let svelte_plugin_module = undefined;
16
+ let warned_no_prettier = false;
17
+ function resolve_prettier(workspace_dir) {
18
+ if (prettier_module !== undefined)
19
+ return prettier_module;
20
+ const require_from_workspace = createRequire(path.join(workspace_dir, 'package.json'));
21
+ try {
22
+ prettier_module = require_from_workspace('prettier');
23
+ return prettier_module;
24
+ }
25
+ catch {
26
+ // Not installed in workspace — try CLI's own deps as a fallback.
27
+ try {
28
+ prettier_module = createRequire(import.meta.url)('prettier');
29
+ return prettier_module;
30
+ }
31
+ catch {
32
+ prettier_module = null;
33
+ return null;
34
+ }
35
+ }
36
+ }
37
+ function resolve_svelte_plugin(workspace_dir) {
38
+ if (svelte_plugin_module !== undefined)
39
+ return svelte_plugin_module;
40
+ const require_from_workspace = createRequire(path.join(workspace_dir, 'package.json'));
41
+ try {
42
+ svelte_plugin_module = require_from_workspace('prettier-plugin-svelte');
43
+ return svelte_plugin_module;
44
+ }
45
+ catch {
46
+ try {
47
+ svelte_plugin_module = createRequire(import.meta.url)('prettier-plugin-svelte');
48
+ return svelte_plugin_module;
49
+ }
50
+ catch {
51
+ svelte_plugin_module = null;
52
+ return null;
53
+ }
54
+ }
55
+ }
56
+ export function should_format(file_path) {
57
+ return FORMATTABLE_EXTENSIONS.has(path.extname(file_path).toLowerCase());
58
+ }
59
+ export async function format_file_contents(file_path, contents, workspace_dir, options) {
60
+ if (!options.enabled)
61
+ return contents;
62
+ if (!should_format(file_path))
63
+ return contents;
64
+ const prettier = resolve_prettier(workspace_dir);
65
+ if (!prettier) {
66
+ if (!warned_no_prettier) {
67
+ warned_no_prettier = true;
68
+ console.log(chalk.dim(` formatter: prettier not found in workspace; install \`prettier\` (and \`prettier-plugin-svelte\` for .svelte) or set format.enabled=false in server.yaml to silence`));
69
+ }
70
+ return contents;
71
+ }
72
+ const ext = path.extname(file_path).toLowerCase();
73
+ const plugins = [];
74
+ if (ext === '.svelte') {
75
+ const svelte_plugin = resolve_svelte_plugin(workspace_dir);
76
+ if (svelte_plugin)
77
+ plugins.push(svelte_plugin);
78
+ }
79
+ const prettier_options = {
80
+ filepath: file_path,
81
+ useTabs: options.use_tabs,
82
+ tabWidth: options.tab_width,
83
+ printWidth: options.print_width,
84
+ semi: options.semi,
85
+ singleQuote: options.single_quote,
86
+ trailingComma: options.trailing_comma,
87
+ plugins
88
+ };
89
+ try {
90
+ const formatted = await prettier.format(contents, prettier_options);
91
+ return formatted;
92
+ }
93
+ catch (err) {
94
+ // Don't break the import flow on a formatter error — just skip and warn.
95
+ console.log(chalk.yellow(` formatter: skipping ${path.basename(file_path)} (${err.message})`));
96
+ return contents;
97
+ }
98
+ }
@@ -0,0 +1,2 @@
1
+ export declare function validate_head_svelte_content(source: string, file_path?: string): void;
2
+ export declare function get_head_svelte_validation_error(source: string, file_path?: string): string | null;
@@ -0,0 +1,53 @@
1
+ import { parse } from 'svelte/compiler';
2
+ export function validate_head_svelte_content(source, file_path = 'site/head.svelte') {
3
+ const error = get_head_svelte_validation_error(source, file_path);
4
+ if (error) {
5
+ throw new Error(error);
6
+ }
7
+ }
8
+ export function get_head_svelte_validation_error(source, file_path = 'site/head.svelte') {
9
+ const duplicate_head = find_duplicate_head(source);
10
+ if (duplicate_head) {
11
+ return format_violation(file_path, duplicate_head);
12
+ }
13
+ try {
14
+ parse(`<svelte:head>${source}</svelte:head>`, { modern: true });
15
+ }
16
+ catch (error) {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ return `${file_path} has invalid Svelte syntax: ${message}`;
19
+ }
20
+ return null;
21
+ }
22
+ function find_duplicate_head(source) {
23
+ const source_without_comments = source.replace(/<!--[\s\S]*?-->/g, (comment) => ' '.repeat(comment.length));
24
+ const match = /<\s*svelte:head\b/i.exec(source_without_comments);
25
+ if (!match) {
26
+ return null;
27
+ }
28
+ return {
29
+ tag: '<svelte:head>',
30
+ message: 'remove the wrapper.',
31
+ ...offset_to_line_column(source, match.index)
32
+ };
33
+ }
34
+ function format_violation(file_path, violation) {
35
+ const location = violation.line
36
+ ? `${file_path}:${violation.line}:${violation.column || 1}`
37
+ : file_path;
38
+ return `${location} contains ${violation.tag}. ${file_path} is injected into <svelte:head>; ${violation.message} Keep only head children such as <title>, <meta>, <link>, <script>, and <style>.`;
39
+ }
40
+ function offset_to_line_column(source, offset) {
41
+ let line = 1;
42
+ let column = 1;
43
+ for (let i = 0; i < offset; i++) {
44
+ if (source[i] === '\n') {
45
+ line += 1;
46
+ column = 1;
47
+ }
48
+ else {
49
+ column += 1;
50
+ }
51
+ }
52
+ return { line, column };
53
+ }
@@ -0,0 +1,19 @@
1
+ import { type FormatOptions } from './format.js';
2
+ export interface SiteGroupConfig {
3
+ id: string;
4
+ name: string;
5
+ index?: number;
6
+ }
7
+ export interface ServerConfig {
8
+ port?: number;
9
+ site_groups?: SiteGroupConfig[];
10
+ format?: Partial<FormatOptions>;
11
+ server?: string;
12
+ }
13
+ export declare const SERVER_CONFIG_FILE = "server.yaml";
14
+ export declare function get_server_config_path(base_dir: string): string;
15
+ export declare function format_group_name(group_id: string): string;
16
+ export declare function normalize_server_config(config: ServerConfig): ServerConfig;
17
+ export declare function resolve_format_options(config: ServerConfig): FormatOptions;
18
+ export declare function read_server_config(base_dir: string): Promise<ServerConfig>;
19
+ export declare function write_server_config(base_dir: string, config: ServerConfig): Promise<void>;
@@ -0,0 +1,49 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { dump as dump_yaml, load as load_yaml } from 'js-yaml';
4
+ import { DEFAULT_FORMAT_OPTIONS } from './format.js';
5
+ export const SERVER_CONFIG_FILE = 'server.yaml';
6
+ export function get_server_config_path(base_dir) {
7
+ return path.join(base_dir, SERVER_CONFIG_FILE);
8
+ }
9
+ export function format_group_name(group_id) {
10
+ if (!group_id.trim())
11
+ return 'Default';
12
+ return group_id
13
+ .replace(/[-_]+/g, ' ')
14
+ .replace(/\s+/g, ' ')
15
+ .trim()
16
+ .replace(/\b\w/g, (char) => char.toUpperCase());
17
+ }
18
+ export function normalize_server_config(config) {
19
+ const site_groups = Array.isArray(config.site_groups)
20
+ ? config.site_groups.reduce((groups, group, index) => {
21
+ if (!group || typeof group.id !== 'string' || !group.id.trim())
22
+ return groups;
23
+ groups.push({
24
+ id: group.id,
25
+ name: typeof group.name === 'string' && group.name.trim() ? group.name : format_group_name(group.id),
26
+ index: Number.isInteger(group.index) ? group.index : index
27
+ });
28
+ return groups;
29
+ }, [])
30
+ : undefined;
31
+ return {
32
+ port: config.port,
33
+ site_groups,
34
+ format: config.format,
35
+ server: typeof config.server === 'string' && config.server.trim()
36
+ ? config.server.trim().replace(/\/+$/, '')
37
+ : undefined
38
+ };
39
+ }
40
+ export function resolve_format_options(config) {
41
+ return { ...DEFAULT_FORMAT_OPTIONS, ...(config.format ?? {}) };
42
+ }
43
+ export async function read_server_config(base_dir) {
44
+ const config_data = await fs.readFile(get_server_config_path(base_dir), 'utf-8');
45
+ return normalize_server_config(load_yaml(config_data));
46
+ }
47
+ export async function write_server_config(base_dir, config) {
48
+ await fs.writeFile(get_server_config_path(base_dir), dump_yaml(normalize_server_config(config), { lineWidth: -1, noRefs: true }));
49
+ }
@@ -0,0 +1,11 @@
1
+ export interface SiteConfig {
2
+ name: string;
3
+ site_id: string;
4
+ host?: string;
5
+ server?: string;
6
+ group?: string;
7
+ }
8
+ export declare const SITE_CONFIG_FILE = "site.yaml";
9
+ export declare function get_site_config_path(site_dir: string): string;
10
+ export declare function read_site_config(site_dir: string): Promise<SiteConfig>;
11
+ export declare function write_site_config(site_dir: string, config: SiteConfig): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { dump as dump_yaml, load as load_yaml } from 'js-yaml';
4
+ export const SITE_CONFIG_FILE = 'site.yaml';
5
+ export function get_site_config_path(site_dir) {
6
+ return path.join(site_dir, SITE_CONFIG_FILE);
7
+ }
8
+ export async function read_site_config(site_dir) {
9
+ const config_data = await fs.readFile(get_site_config_path(site_dir), 'utf-8');
10
+ return load_yaml(config_data);
11
+ }
12
+ export async function write_site_config(site_dir, config) {
13
+ await fs.writeFile(get_site_config_path(site_dir), dump_yaml(config, { lineWidth: -1, noRefs: true }));
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primo-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Local development CLI for Primo",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,11 +14,11 @@
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/palacms/cli.git"
17
+ "url": "git+https://github.com/primocms/primo-cli.git"
18
18
  },
19
19
  "homepage": "https://primo.page",
20
20
  "bugs": {
21
- "url": "https://github.com/palacms/cli/issues"
21
+ "url": "https://github.com/primocms/primo-cli/issues"
22
22
  },
23
23
  "keywords": [
24
24
  "primo",
@@ -34,10 +34,14 @@
34
34
  "chalk": "^5.3.0",
35
35
  "chokidar": "^3.6.0",
36
36
  "commander": "^12.1.0",
37
+ "esbuild": "^0.27.4",
37
38
  "extract-zip": "^2.0.1",
38
39
  "inquirer": "^13.3.0",
39
40
  "js-yaml": "^4.1.0",
40
- "ora": "^8.0.1"
41
+ "ora": "^8.0.1",
42
+ "prettier": "^3.0.0",
43
+ "prettier-plugin-svelte": "^3.0.0",
44
+ "svelte": "^5.55.0"
41
45
  },
42
46
  "devDependencies": {
43
47
  "@types/archiver": "^6.0.2",
@@ -1,8 +0,0 @@
1
- interface ExportOptions {
2
- server: string;
3
- site: string;
4
- output: string;
5
- token?: string;
6
- }
7
- export declare function export_site(options: ExportOptions): Promise<void>;
8
- export {};
@@ -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
- }