primo-cli 0.1.16 → 0.1.17
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/dist/commands/build.js +68 -9
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +17 -15
- package/dist/commands/pull-library.js +2 -1
- package/dist/commands/pull.js +22 -5
- package/dist/commands/push-library.js +2 -1
- package/dist/commands/push.js +6 -4
- package/dist/utils/server-config.d.ts +1 -0
- package/dist/utils/server-config.js +11 -0
- package/package.json +1 -1
package/dist/commands/build.js
CHANGED
|
@@ -51,6 +51,11 @@ export async function build_site(options) {
|
|
|
51
51
|
// Find all pages
|
|
52
52
|
const pages_dir = path.join(site_dir, 'pages');
|
|
53
53
|
const page_files = await find_pages(pages_dir);
|
|
54
|
+
// Map each page's _id to its live URL so internal `page:` links resolve.
|
|
55
|
+
// The URL is derived from the page's file location (same convention the
|
|
56
|
+
// build uses to write output), not the slug, so a page-ref and a raw
|
|
57
|
+
// url pointing at the same page produce identical hrefs.
|
|
58
|
+
const page_url_map = await build_page_url_map(site_dir, page_files);
|
|
54
59
|
spinner.text = `Building ${page_files.length} page${page_files.length !== 1 ? 's' : ''}...`;
|
|
55
60
|
// Compile all blocks once and cache them
|
|
56
61
|
const block_cache = new Map();
|
|
@@ -77,7 +82,8 @@ export async function build_site(options) {
|
|
|
77
82
|
block_cache,
|
|
78
83
|
layout_cache,
|
|
79
84
|
page_type_head_cache,
|
|
80
|
-
site_data
|
|
85
|
+
site_data,
|
|
86
|
+
page_url_map
|
|
81
87
|
});
|
|
82
88
|
if (result.error) {
|
|
83
89
|
console.log(chalk.yellow(` Warning: ${page.name}: ${result.error}`));
|
|
@@ -140,7 +146,7 @@ async function load_page_type_head(site_dir, page_type, cache) {
|
|
|
140
146
|
return content;
|
|
141
147
|
}
|
|
142
148
|
async function build_page(options) {
|
|
143
|
-
const { page, page_path, site_dir, temp_dir, head_content, site_name, block_cache, layout_cache, page_type_head_cache, site_data } = options;
|
|
149
|
+
const { page, page_path, site_dir, temp_dir, head_content, site_name, block_cache, layout_cache, page_type_head_cache, site_data, page_url_map } = options;
|
|
144
150
|
try {
|
|
145
151
|
const page_build_id = safe_temp_id(page._id || page.id || page_path || page.name || 'page');
|
|
146
152
|
// Load layout for this page type
|
|
@@ -155,9 +161,9 @@ async function build_page(options) {
|
|
|
155
161
|
const page_type_head = await load_page_type_head(site_dir, page_type, page_type_head_cache);
|
|
156
162
|
const combined_head_content = page_type_head ? `${head_content}\n${page_type_head}` : head_content;
|
|
157
163
|
// Combine header + page sections + footer
|
|
158
|
-
const header_sections = await resolve_layout_sections(layout.header || [], site_dir, site_data);
|
|
159
|
-
const footer_sections = await resolve_layout_sections(layout.footer || [], site_dir, site_data);
|
|
160
|
-
const page_sections = await resolve_page_sections(page.sections || [], site_dir, site_data);
|
|
164
|
+
const header_sections = await resolve_layout_sections(layout.header || [], site_dir, site_data, page_url_map);
|
|
165
|
+
const footer_sections = await resolve_layout_sections(layout.footer || [], site_dir, site_data, page_url_map);
|
|
166
|
+
const page_sections = await resolve_page_sections(page.sections || [], site_dir, site_data, page_url_map);
|
|
161
167
|
const all_sections = [...header_sections, ...page_sections, ...footer_sections];
|
|
162
168
|
if (all_sections.length === 0) {
|
|
163
169
|
return { html: generate_empty_page(site_name, page.name, combined_head_content) };
|
|
@@ -448,7 +454,7 @@ async function load_layout(site_dir, page_type) {
|
|
|
448
454
|
return {};
|
|
449
455
|
}
|
|
450
456
|
}
|
|
451
|
-
async function resolve_layout_sections(sections, site_dir, site_data) {
|
|
457
|
+
async function resolve_layout_sections(sections, site_dir, site_data, page_url_map) {
|
|
452
458
|
// For layout sections without content, load from block's content.yaml
|
|
453
459
|
const resolved = [];
|
|
454
460
|
for (const section of sections) {
|
|
@@ -462,20 +468,73 @@ async function resolve_layout_sections(sections, site_dir, site_data) {
|
|
|
462
468
|
}
|
|
463
469
|
// Resolve any site-field references in the content
|
|
464
470
|
const resolved_content = await resolve_site_fields(site_dir, section.block, content, site_data);
|
|
465
|
-
|
|
471
|
+
// Resolve internal page: links to URLs (walks nested repeaters/groups too)
|
|
472
|
+
resolved.push({ ...section, content: resolve_links(resolved_content, page_url_map) });
|
|
466
473
|
}
|
|
467
474
|
return resolved;
|
|
468
475
|
}
|
|
469
|
-
async function resolve_page_sections(sections, site_dir, site_data) {
|
|
476
|
+
async function resolve_page_sections(sections, site_dir, site_data, page_url_map) {
|
|
470
477
|
// Resolve site-field references in page sections
|
|
471
478
|
const resolved = [];
|
|
472
479
|
for (const section of sections) {
|
|
473
480
|
const content = section.content || {};
|
|
474
481
|
const resolved_content = await resolve_site_fields(site_dir, section.block, content, site_data);
|
|
475
|
-
|
|
482
|
+
// Resolve internal page: links to URLs (walks nested repeaters/groups too)
|
|
483
|
+
resolved.push({ ...section, content: resolve_links(resolved_content, page_url_map) });
|
|
476
484
|
}
|
|
477
485
|
return resolved;
|
|
478
486
|
}
|
|
487
|
+
// Build a map of page _id -> live URL path. The URL is derived from the page
|
|
488
|
+
// file's location (the same convention used to write output HTML), so a
|
|
489
|
+
// `page:` reference resolves to exactly the path that page is deployed at.
|
|
490
|
+
async function build_page_url_map(site_dir, page_files) {
|
|
491
|
+
const map = new Map();
|
|
492
|
+
for (const page_file of page_files) {
|
|
493
|
+
try {
|
|
494
|
+
const page = load_yaml(await fs.readFile(page_file, 'utf-8'));
|
|
495
|
+
const id = page._id || page.id;
|
|
496
|
+
if (!id)
|
|
497
|
+
continue;
|
|
498
|
+
const page_path = get_page_path_from_file(site_dir, page_file);
|
|
499
|
+
map.set(id, page_path === '' ? '/' : `/${page_path}`);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// Skip unparseable page files; they'll surface elsewhere in the build.
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return map;
|
|
506
|
+
}
|
|
507
|
+
// Recursively resolve internal `page:` links to their URL. Walks arbitrarily
|
|
508
|
+
// nested content (repeaters, groups, page-lists), so links inside a
|
|
509
|
+
// site-field-referenced repeater resolve the same as top-level link fields.
|
|
510
|
+
//
|
|
511
|
+
// A link value is any object carrying a `page` id. We look the id up in the
|
|
512
|
+
// page URL map and populate `url`:
|
|
513
|
+
// - known page id -> the page's live URL
|
|
514
|
+
// - missing/deleted id -> '' (degrades to href="#" downstream, never crashes)
|
|
515
|
+
// Objects with only `url` (raw/external links) are left untouched, and every
|
|
516
|
+
// other key on the link object (label, etc.) is preserved.
|
|
517
|
+
function resolve_links(value, page_url_map) {
|
|
518
|
+
if (Array.isArray(value)) {
|
|
519
|
+
return value.map((item) => resolve_links(item, page_url_map));
|
|
520
|
+
}
|
|
521
|
+
if (value && typeof value === 'object') {
|
|
522
|
+
const obj = value;
|
|
523
|
+
// A link with a page reference: resolve it to a URL.
|
|
524
|
+
if (typeof obj.page === 'string' && obj.page) {
|
|
525
|
+
const url = page_url_map.get(obj.page) ?? '';
|
|
526
|
+
return { ...obj, url };
|
|
527
|
+
}
|
|
528
|
+
// Otherwise recurse into every value (covers repeater arrays, groups,
|
|
529
|
+
// and the `{ link: {...} }` wrapper repeaters produce).
|
|
530
|
+
const result = {};
|
|
531
|
+
for (const [key, child] of Object.entries(obj)) {
|
|
532
|
+
result[key] = resolve_links(child, page_url_map);
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
return value;
|
|
537
|
+
}
|
|
479
538
|
async function load_block_defaults(site_dir, block_name) {
|
|
480
539
|
const content_path = path.join(site_dir, 'blocks', block_name, 'content.yaml');
|
|
481
540
|
try {
|
package/dist/commands/login.d.ts
CHANGED
package/dist/commands/login.js
CHANGED
|
@@ -3,7 +3,7 @@ import ora from 'ora';
|
|
|
3
3
|
import readline from 'readline';
|
|
4
4
|
import fs from 'fs/promises';
|
|
5
5
|
import { save_auth_token } from '../utils/auth.js';
|
|
6
|
-
import { read_server_config, get_server_config_path } from '../utils/server-config.js';
|
|
6
|
+
import { read_server_config, get_server_config_path, normalize_server_url } from '../utils/server-config.js';
|
|
7
7
|
export async function login(options) {
|
|
8
8
|
let server_url = options.server;
|
|
9
9
|
if (!server_url) {
|
|
@@ -29,7 +29,19 @@ export async function login(options) {
|
|
|
29
29
|
console.log('');
|
|
30
30
|
console.log(chalk.bold(`Logging in to ${server}`));
|
|
31
31
|
console.log('');
|
|
32
|
-
|
|
32
|
+
const token = await authenticate_interactively(server, options.email);
|
|
33
|
+
if (!token) {
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(chalk.dim(' Token saved to ~/.primo/tokens.json'));
|
|
38
|
+
console.log(chalk.dim(' You can now use `primo pull` and `primo push` without --token'));
|
|
39
|
+
}
|
|
40
|
+
// Prompt for credentials (email if not supplied, then password), authenticate
|
|
41
|
+
// against the server, and persist the token on success. Returns the token, or
|
|
42
|
+
// null if authentication failed. Shared by `login` and the auto-login prompt
|
|
43
|
+
// that `pull`/`push` fall back to when no token is cached.
|
|
44
|
+
export async function authenticate_interactively(server, email) {
|
|
33
45
|
if (!email) {
|
|
34
46
|
email = await prompt('Email: ');
|
|
35
47
|
}
|
|
@@ -50,28 +62,18 @@ export async function login(options) {
|
|
|
50
62
|
if (!response.ok) {
|
|
51
63
|
const error = await response.json().catch(() => ({ message: 'Authentication failed' }));
|
|
52
64
|
spinner.fail(`Login failed: ${error.message || 'Invalid credentials'}`);
|
|
53
|
-
|
|
65
|
+
return null;
|
|
54
66
|
}
|
|
55
67
|
const data = await response.json();
|
|
56
68
|
// Save the token
|
|
57
69
|
await save_auth_token(server, data.token);
|
|
58
70
|
spinner.succeed(`Logged in as ${chalk.cyan(data.record.email)}`);
|
|
59
|
-
|
|
60
|
-
console.log(chalk.dim(' Token saved to ~/.primo/tokens.json'));
|
|
61
|
-
console.log(chalk.dim(' You can now use `primo pull` and `primo push` without --token'));
|
|
71
|
+
return data.token;
|
|
62
72
|
}
|
|
63
73
|
catch (error) {
|
|
64
74
|
spinner.fail(`Login failed: ${error instanceof Error ? error.message : error}`);
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function normalize_server_url(server) {
|
|
69
|
-
// Add https:// if no protocol specified
|
|
70
|
-
if (!server.startsWith('http://') && !server.startsWith('https://')) {
|
|
71
|
-
server = `https://${server}`;
|
|
75
|
+
return null;
|
|
72
76
|
}
|
|
73
|
-
// Remove trailing slash
|
|
74
|
-
return server.replace(/\/+$/, '');
|
|
75
77
|
}
|
|
76
78
|
function prompt(question) {
|
|
77
79
|
const rl = readline.createInterface({
|
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import extract from 'extract-zip';
|
|
6
6
|
import { get_auth_token } from '../utils/auth.js';
|
|
7
|
+
import { normalize_server_url } from '../utils/server-config.js';
|
|
7
8
|
async function detect_server() {
|
|
8
9
|
const ports = [3000, 8080, 5173];
|
|
9
10
|
for (const port of ports) {
|
|
@@ -27,7 +28,7 @@ export async function pull_library(options) {
|
|
|
27
28
|
try {
|
|
28
29
|
let server;
|
|
29
30
|
if (options.server) {
|
|
30
|
-
server = options.server
|
|
31
|
+
server = normalize_server_url(options.server);
|
|
31
32
|
}
|
|
32
33
|
else {
|
|
33
34
|
spinner.text = 'Looking for local server...';
|
package/dist/commands/pull.js
CHANGED
|
@@ -5,8 +5,9 @@ import ora from 'ora';
|
|
|
5
5
|
import extract from 'extract-zip';
|
|
6
6
|
import { dump as dump_yaml, load as load_yaml } from 'js-yaml';
|
|
7
7
|
import { get_auth_token } from '../utils/auth.js';
|
|
8
|
+
import { authenticate_interactively } from './login.js';
|
|
8
9
|
import { write_site_config } from '../utils/site-config.js';
|
|
9
|
-
import { read_server_config, write_server_config } from '../utils/server-config.js';
|
|
10
|
+
import { read_server_config, write_server_config, normalize_server_url } from '../utils/server-config.js';
|
|
10
11
|
async function detect_server() {
|
|
11
12
|
const ports = [3000, 8080, 5173];
|
|
12
13
|
for (const port of ports) {
|
|
@@ -64,12 +65,15 @@ export async function pull_site(options) {
|
|
|
64
65
|
let server;
|
|
65
66
|
let used_configured = false;
|
|
66
67
|
if (options.server) {
|
|
67
|
-
server = options.server
|
|
68
|
+
server = normalize_server_url(options.server);
|
|
68
69
|
}
|
|
69
70
|
else {
|
|
70
71
|
const configured = await read_configured_server(process.cwd());
|
|
71
72
|
if (configured) {
|
|
72
|
-
server
|
|
73
|
+
// A hand-edited server.yaml may hold a bare host; normalize it so
|
|
74
|
+
// is_remote_server() and the inline-login guard below behave the
|
|
75
|
+
// same as they do for a --server flag.
|
|
76
|
+
server = normalize_server_url(configured);
|
|
73
77
|
used_configured = true;
|
|
74
78
|
spinner.text = `Using ${server}`;
|
|
75
79
|
}
|
|
@@ -80,8 +84,21 @@ export async function pull_site(options) {
|
|
|
80
84
|
spinner.text = `Using ${server}`;
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
|
-
// Auth (optional for local)
|
|
84
|
-
|
|
87
|
+
// Auth (optional for local). For a remote server with no cached token,
|
|
88
|
+
// prompt for login inline instead of bailing out — the user almost
|
|
89
|
+
// always wants to authenticate and continue rather than re-run.
|
|
90
|
+
let token = options.token || await get_auth_token(server);
|
|
91
|
+
if (!token && is_remote_server(server)) {
|
|
92
|
+
spinner.stop();
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(chalk.dim(` Not logged in to ${server}. Log in to continue.`));
|
|
95
|
+
console.log('');
|
|
96
|
+
token = await authenticate_interactively(server);
|
|
97
|
+
if (!token) {
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
spinner.start('Fetching sites...');
|
|
101
|
+
}
|
|
85
102
|
const headers = {};
|
|
86
103
|
if (token) {
|
|
87
104
|
headers['Authorization'] = `Bearer ${token}`;
|
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import archiver from 'archiver';
|
|
6
6
|
import { get_auth_token } from '../utils/auth.js';
|
|
7
|
+
import { normalize_server_url } from '../utils/server-config.js';
|
|
7
8
|
function is_local_server(server) {
|
|
8
9
|
try {
|
|
9
10
|
const url = new URL(server);
|
|
@@ -28,7 +29,7 @@ export async function push_library(options) {
|
|
|
28
29
|
spinner.fail(`Library directory not found at ${chalk.cyan(library_dir)}.`);
|
|
29
30
|
process.exit(1);
|
|
30
31
|
}
|
|
31
|
-
const server = options.server
|
|
32
|
+
const server = options.server ? normalize_server_url(options.server) : undefined;
|
|
32
33
|
if (!server) {
|
|
33
34
|
spinner.fail('Server URL required. Pass it as the first argument or use --server.');
|
|
34
35
|
process.exit(1);
|
package/dist/commands/push.js
CHANGED
|
@@ -5,7 +5,7 @@ import ora from 'ora';
|
|
|
5
5
|
import archiver from 'archiver';
|
|
6
6
|
import { get_auth_token } from '../utils/auth.js';
|
|
7
7
|
import { read_site_config, get_site_config_path, SITE_CONFIG_FILE } from '../utils/site-config.js';
|
|
8
|
-
import { get_server_config_path, read_server_config } from '../utils/server-config.js';
|
|
8
|
+
import { get_server_config_path, read_server_config, normalize_server_url } from '../utils/server-config.js';
|
|
9
9
|
async function path_exists(p) {
|
|
10
10
|
try {
|
|
11
11
|
await fs.access(p);
|
|
@@ -133,7 +133,8 @@ async function print_push_dry_run(root_dir, has_site_yaml, has_server_yaml, opti
|
|
|
133
133
|
}
|
|
134
134
|
library_present = await path_exists(path.join(root_dir, 'library'));
|
|
135
135
|
}
|
|
136
|
-
const
|
|
136
|
+
const server_raw = options.server || inferred_server;
|
|
137
|
+
const server = server_raw ? normalize_server_url(server_raw) : undefined;
|
|
137
138
|
console.log(` Target server: ${chalk.cyan(server || '(not set — pass --server or set in site.yaml)')}`);
|
|
138
139
|
console.log('');
|
|
139
140
|
console.log(chalk.bold(' Will sync:'));
|
|
@@ -243,7 +244,8 @@ async function push_single_site(site_dir, options, spinner) {
|
|
|
243
244
|
catch {
|
|
244
245
|
// No config file, must provide options
|
|
245
246
|
}
|
|
246
|
-
const
|
|
247
|
+
const server_raw = options.server || config?.server;
|
|
248
|
+
const server = server_raw ? normalize_server_url(server_raw) : undefined;
|
|
247
249
|
const site_id = options.site || config?.site_id;
|
|
248
250
|
if (!server) {
|
|
249
251
|
throw new Error(`Server URL required. Use --server or add server field to ${SITE_CONFIG_FILE}.`);
|
|
@@ -362,7 +364,7 @@ async function try_bootstrap_site(server, token, zip_buffer, config, site_id, gr
|
|
|
362
364
|
}
|
|
363
365
|
async function push_library_dir(root_dir, options, spinner) {
|
|
364
366
|
// Resolve server: --server > any site.yaml's server (they all point at the same server)
|
|
365
|
-
let server = options.server
|
|
367
|
+
let server = options.server ? normalize_server_url(options.server) : undefined;
|
|
366
368
|
if (!server) {
|
|
367
369
|
const sites_root = path.join(root_dir, 'sites');
|
|
368
370
|
if (await path_exists(sites_root)) {
|
|
@@ -12,6 +12,7 @@ export interface ServerConfig {
|
|
|
12
12
|
}
|
|
13
13
|
export declare const SERVER_CONFIG_FILE = "server.yaml";
|
|
14
14
|
export declare function get_server_config_path(base_dir: string): string;
|
|
15
|
+
export declare function normalize_server_url(server: string): string;
|
|
15
16
|
export declare function format_group_name(group_id: string): string;
|
|
16
17
|
export declare function normalize_server_config(config: ServerConfig): ServerConfig;
|
|
17
18
|
export declare function resolve_format_options(config: ServerConfig): FormatOptions;
|
|
@@ -6,6 +6,17 @@ export const SERVER_CONFIG_FILE = 'server.yaml';
|
|
|
6
6
|
export function get_server_config_path(base_dir) {
|
|
7
7
|
return path.join(base_dir, SERVER_CONFIG_FILE);
|
|
8
8
|
}
|
|
9
|
+
// Normalize a user-supplied server value: prepend a protocol when missing and
|
|
10
|
+
// strip trailing slashes. Bare localhost/127.0.0.1 stay on http:// (local dev);
|
|
11
|
+
// everything else gets https://.
|
|
12
|
+
export function normalize_server_url(server) {
|
|
13
|
+
let normalized = server.trim();
|
|
14
|
+
if (!/^https?:\/\//.test(normalized)) {
|
|
15
|
+
const is_local = /^(localhost|127\.0\.0\.1|\[::1\]|::1)(:\d+)?(\/|$)/.test(normalized);
|
|
16
|
+
normalized = `${is_local ? 'http' : 'https'}://${normalized}`;
|
|
17
|
+
}
|
|
18
|
+
return normalized.replace(/\/+$/, '');
|
|
19
|
+
}
|
|
9
20
|
export function format_group_name(group_id) {
|
|
10
21
|
if (!group_id.trim())
|
|
11
22
|
return 'Default';
|