primo-cli 0.1.3 → 0.1.5
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 +111 -39
- package/dist/commands/build.js +488 -272
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +293 -141
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +2007 -150
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +65 -43
- package/dist/commands/login.d.ts +1 -2
- package/dist/commands/login.js +24 -6
- package/dist/commands/new.js +161 -274
- package/dist/commands/pull-library.d.ts +7 -0
- package/dist/commands/pull-library.js +92 -0
- package/dist/commands/pull.d.ts +0 -1
- package/dist/commands/pull.js +160 -165
- package/dist/commands/push-library.d.ts +7 -0
- package/dist/commands/push-library.js +88 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +358 -51
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +379 -161
- package/dist/index.js +110 -20
- package/dist/utils/binary.js +1 -1
- package/dist/utils/format.d.ts +12 -0
- package/dist/utils/format.js +98 -0
- package/dist/utils/head-svelte.d.ts +2 -0
- package/dist/utils/head-svelte.js +53 -0
- package/dist/utils/server-config.d.ts +19 -0
- package/dist/utils/server-config.js +49 -0
- package/dist/utils/site-config.d.ts +11 -0
- package/dist/utils/site-config.js +14 -0
- package/package.json +8 -4
- package/dist/commands/export.d.ts +0 -8
- package/dist/commands/export.js +0 -163
- package/dist/commands/import.d.ts +0 -9
- package/dist/commands/import.js +0 -118
- package/dist/commands/publish.d.ts +0 -6
- 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 {
|
|
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('
|
|
30
|
-
.description('Deploy
|
|
31
|
-
.option('-
|
|
32
|
-
.option('-
|
|
33
|
-
.
|
|
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('
|
|
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
|
-
.
|
|
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
|
|
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(
|
|
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
|
-
.command('login')
|
|
118
|
+
.command('login [server]')
|
|
53
119
|
.description('Login to hosted CMS')
|
|
54
|
-
.
|
|
120
|
+
.option('-s, --server <url>', 'Server URL (defaults to `server:` in server.yaml)')
|
|
55
121
|
.option('-e, --email <email>', 'Email')
|
|
56
|
-
.
|
|
57
|
-
.action((server, options) => login({ server, ...options }));
|
|
122
|
+
.action((server, options) => login({ ...options, server: server || options.server }));
|
|
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('
|
|
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', '
|
|
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();
|
package/dist/utils/binary.js
CHANGED
|
@@ -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 = '
|
|
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,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
|
+
"version": "0.1.5",
|
|
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/
|
|
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/
|
|
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",
|
package/dist/commands/export.js
DELETED
|
@@ -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
|
-
}
|