primo-cli 0.1.6 → 0.1.8
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/dev.js +70 -6
- package/dist/commands/new.js +0 -3
- package/dist/commands/pull.js +35 -10
- package/dist/commands/push.js +0 -2
- package/dist/index.js +6 -1
- package/dist/utils/server-config.d.ts +1 -0
- package/dist/utils/server-config.js +7 -1
- package/dist/utils/site-config.d.ts +0 -1
- package/package.json +1 -1
package/dist/commands/dev.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { createHash, randomInt } from 'crypto';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
-
import { spawn } from 'child_process';
|
|
6
|
+
import { spawn, execFileSync } from 'child_process';
|
|
7
7
|
import archiver from 'archiver';
|
|
8
8
|
import extract from 'extract-zip';
|
|
9
9
|
import { dump as dump_yaml, load as load_yaml } from 'js-yaml';
|
|
@@ -420,18 +420,51 @@ async function kill_port(port) {
|
|
|
420
420
|
resolve(false);
|
|
421
421
|
return;
|
|
422
422
|
}
|
|
423
|
+
// Don't kill ourselves or our ancestors — lsof returns every PID
|
|
424
|
+
// holding the port, which on macOS includes parent processes that
|
|
425
|
+
// inherited the fd. SIGKILL'ing them takes this CLI down too.
|
|
426
|
+
const self_ancestry = get_self_ancestry();
|
|
427
|
+
let killed_any = false;
|
|
423
428
|
for (const pid of pid_list) {
|
|
429
|
+
const pid_num = parseInt(pid, 10);
|
|
430
|
+
if (self_ancestry.has(pid_num))
|
|
431
|
+
continue;
|
|
424
432
|
try {
|
|
425
|
-
process.kill(
|
|
433
|
+
process.kill(pid_num, 'SIGKILL');
|
|
434
|
+
killed_any = true;
|
|
426
435
|
}
|
|
427
436
|
catch {
|
|
428
437
|
// Process may have already exited
|
|
429
438
|
}
|
|
430
439
|
}
|
|
431
|
-
resolve(
|
|
440
|
+
resolve(killed_any);
|
|
432
441
|
});
|
|
433
442
|
});
|
|
434
443
|
}
|
|
444
|
+
function get_self_ancestry() {
|
|
445
|
+
const ancestry = new Set();
|
|
446
|
+
let pid = process.pid;
|
|
447
|
+
while (pid && pid > 1) {
|
|
448
|
+
ancestry.add(pid);
|
|
449
|
+
pid = get_parent_pid(pid);
|
|
450
|
+
if (pid && ancestry.has(pid))
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
return ancestry;
|
|
454
|
+
}
|
|
455
|
+
function get_parent_pid(pid) {
|
|
456
|
+
try {
|
|
457
|
+
const result = execFileSync('ps', ['-o', 'ppid=', '-p', String(pid)], {
|
|
458
|
+
encoding: 'utf-8',
|
|
459
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
460
|
+
});
|
|
461
|
+
const parsed = parseInt(String(result).trim(), 10);
|
|
462
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
435
468
|
// Fetch with timeout helper
|
|
436
469
|
async function fetch_with_timeout(url, options = {}, timeout_ms = 10000) {
|
|
437
470
|
const controller = new AbortController();
|
|
@@ -970,8 +1003,12 @@ export async function dev_server(options) {
|
|
|
970
1003
|
console.log(chalk.dim(' Press Ctrl+C to stop'));
|
|
971
1004
|
// Handle cleanup
|
|
972
1005
|
const cleanup = async () => {
|
|
973
|
-
if (is_cleaning_up)
|
|
974
|
-
|
|
1006
|
+
if (is_cleaning_up) {
|
|
1007
|
+
// Second Ctrl-C while we're still cleaning up — bail immediately
|
|
1008
|
+
// so the user isn't stuck waiting on an in-flight push/sync.
|
|
1009
|
+
console.log(chalk.dim('\n Force exit'));
|
|
1010
|
+
process.exit(130);
|
|
1011
|
+
}
|
|
975
1012
|
is_cleaning_up = true;
|
|
976
1013
|
console.log(chalk.dim('\n Shutting down...'));
|
|
977
1014
|
for (const watcher of watchers) {
|
|
@@ -2066,7 +2103,18 @@ async function create_site_zip(dir, excluded_paths = new Set()) {
|
|
|
2066
2103
|
}
|
|
2067
2104
|
const site_json = path.join(dir, SITE_CONFIG_FILE);
|
|
2068
2105
|
if (!is_excluded_path(SITE_CONFIG_FILE, excluded_paths)) {
|
|
2069
|
-
|
|
2106
|
+
// Strip `host` if present — `site.yaml` no longer carries it,
|
|
2107
|
+
// but older dirs pulled by previous CLI versions still do,
|
|
2108
|
+
// and the local CMS's /import endpoint persists whatever
|
|
2109
|
+
// host the zipped yaml declares, overwriting bootstrap's
|
|
2110
|
+
// *.localhost value and breaking preview routing.
|
|
2111
|
+
const sanitized = await read_site_yaml_without_host(site_json);
|
|
2112
|
+
if (sanitized !== null) {
|
|
2113
|
+
archive.append(sanitized, { name: SITE_CONFIG_FILE });
|
|
2114
|
+
}
|
|
2115
|
+
else {
|
|
2116
|
+
archive.file(site_json, { name: SITE_CONFIG_FILE });
|
|
2117
|
+
}
|
|
2070
2118
|
}
|
|
2071
2119
|
await archive.finalize();
|
|
2072
2120
|
}
|
|
@@ -2076,6 +2124,22 @@ async function create_site_zip(dir, excluded_paths = new Set()) {
|
|
|
2076
2124
|
})();
|
|
2077
2125
|
});
|
|
2078
2126
|
}
|
|
2127
|
+
async function read_site_yaml_without_host(site_yaml_path) {
|
|
2128
|
+
try {
|
|
2129
|
+
const raw = await fs.readFile(site_yaml_path, 'utf-8');
|
|
2130
|
+
const parsed = load_yaml(raw);
|
|
2131
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
2132
|
+
return null;
|
|
2133
|
+
const data = parsed;
|
|
2134
|
+
if (!('host' in data))
|
|
2135
|
+
return null;
|
|
2136
|
+
const { host: _, ...rest } = data;
|
|
2137
|
+
return dump_yaml(rest, { lineWidth: -1, noRefs: true });
|
|
2138
|
+
}
|
|
2139
|
+
catch {
|
|
2140
|
+
return null;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2079
2143
|
async function sync_from_cms(site_dir, api_url, config, server_config, workspace_dir, sync_policy = { mode: 'both' }) {
|
|
2080
2144
|
const response = await fetch_with_timeout(`${api_url}/api/palacms/export/${config.site_id}`, {}, 15000);
|
|
2081
2145
|
if (!response.ok)
|
package/dist/commands/new.js
CHANGED
|
@@ -87,9 +87,6 @@ export async function new_site(options) {
|
|
|
87
87
|
const config = {
|
|
88
88
|
name: display_name,
|
|
89
89
|
site_id: generate_id(),
|
|
90
|
-
// Leave host empty for local dev - dev.ts will generate coffee-shop.localhost:3000
|
|
91
|
-
// Only set host if it looks like a real domain (has a dot)
|
|
92
|
-
host: site_name.includes('.') ? site_name : '',
|
|
93
90
|
group: 'default'
|
|
94
91
|
};
|
|
95
92
|
await write_site_config(site_dir, config);
|
package/dist/commands/pull.js
CHANGED
|
@@ -48,6 +48,15 @@ async function read_configured_server_config(dir) {
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
function is_remote_server(server) {
|
|
52
|
+
try {
|
|
53
|
+
const hostname = new URL(server).hostname;
|
|
54
|
+
return hostname !== 'localhost' && hostname !== '127.0.0.1' && hostname !== '::1';
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
51
60
|
export async function pull_site(options) {
|
|
52
61
|
const spinner = ora('Connecting...').start();
|
|
53
62
|
try {
|
|
@@ -79,21 +88,36 @@ export async function pull_site(options) {
|
|
|
79
88
|
}
|
|
80
89
|
// Decide root dir: explicit --output wins; if cwd already has a configured
|
|
81
90
|
// server.yaml, pull in place; otherwise nest under server hostname.
|
|
91
|
+
// Refuse to auto-create a `localhost/` folder — the user almost certainly
|
|
92
|
+
// didn't mean that. Require --output or --server explicitly in that case.
|
|
93
|
+
if (!options.output && !used_configured && !is_remote_server(server)) {
|
|
94
|
+
spinner.fail(`Resolved to ${server} but no output dir was given. ` +
|
|
95
|
+
`Pass --server <url> or --output <dir> to pull a local server.`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
82
98
|
const root_dir = options.output
|
|
83
99
|
? path.resolve(options.output)
|
|
84
100
|
: used_configured
|
|
85
101
|
? process.cwd()
|
|
86
102
|
: path.resolve(server_folder_name(server));
|
|
87
|
-
|
|
88
|
-
//
|
|
103
|
+
// Validate the server is reachable BEFORE creating the output dir, so a
|
|
104
|
+
// failed pull doesn't leave an empty folder behind.
|
|
89
105
|
spinner.text = 'Fetching sites...';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
let sites_response;
|
|
107
|
+
try {
|
|
108
|
+
sites_response = await fetch(`${server}/api/collections/sites/records?perPage=200`, {
|
|
109
|
+
headers
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
spinner.fail(`Could not reach ${server}: ${error instanceof Error ? error.message : error}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
93
116
|
if (!sites_response.ok) {
|
|
94
117
|
spinner.fail(`Failed to fetch sites (${sites_response.status})`);
|
|
95
118
|
process.exit(1);
|
|
96
119
|
}
|
|
120
|
+
await fs.mkdir(root_dir, { recursive: true });
|
|
97
121
|
const sites_data = await sites_response.json();
|
|
98
122
|
const sites = sites_data.items || [];
|
|
99
123
|
if (sites.length === 0) {
|
|
@@ -127,13 +151,15 @@ export async function pull_site(options) {
|
|
|
127
151
|
}
|
|
128
152
|
// Fetch site groups so server.yaml has them
|
|
129
153
|
const site_groups = await fetch_site_groups(server, headers);
|
|
130
|
-
// Preserve any existing server.yaml (port, format
|
|
131
|
-
//
|
|
154
|
+
// Preserve any existing server.yaml (port, format) and refresh
|
|
155
|
+
// site_groups from the source of truth. Also persist the server URL
|
|
156
|
+
// when it's a remote so subsequent bare `primo pull` runs in this dir
|
|
157
|
+
// don't fall back to localhost detection.
|
|
132
158
|
const existing = await read_configured_server_config(root_dir);
|
|
133
159
|
await write_server_config(root_dir, {
|
|
134
160
|
...existing,
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
site_groups: site_groups.length > 0 ? site_groups : existing?.site_groups,
|
|
162
|
+
server: existing?.server ?? (is_remote_server(server) ? server : undefined)
|
|
137
163
|
});
|
|
138
164
|
spinner.succeed(`Server pulled to ${chalk.cyan(root_dir)}`);
|
|
139
165
|
console.log('');
|
|
@@ -170,7 +196,6 @@ async function pull_one_site(server, headers, site, site_dir, spinner) {
|
|
|
170
196
|
await fs.unlink(temp_zip);
|
|
171
197
|
await write_site_config(site_dir, {
|
|
172
198
|
name: site.name || 'Imported Site',
|
|
173
|
-
host: site.host || '',
|
|
174
199
|
site_id: site.id,
|
|
175
200
|
server,
|
|
176
201
|
group: site.group
|
package/dist/commands/push.js
CHANGED
|
@@ -300,8 +300,6 @@ async function try_bootstrap_site(server, token, zip_buffer, config, site_id) {
|
|
|
300
300
|
form.append('site_id', site_id);
|
|
301
301
|
if (config?.name)
|
|
302
302
|
form.append('name', config.name);
|
|
303
|
-
if (config?.host)
|
|
304
|
-
form.append('host', config.host);
|
|
305
303
|
if (config?.group)
|
|
306
304
|
form.append('group', config.group);
|
|
307
305
|
form.append('file', new Blob([zip_buffer]), 'site.zip');
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
2
5
|
import { Command } from 'commander';
|
|
3
6
|
import chalk from 'chalk';
|
|
4
7
|
import { init_workspace } from './commands/init.js';
|
|
@@ -12,11 +15,13 @@ import { login } from './commands/login.js';
|
|
|
12
15
|
import { validate_site } from './commands/validate.js';
|
|
13
16
|
import { deploy } from './commands/deploy.js';
|
|
14
17
|
import { build_site } from './commands/build.js';
|
|
18
|
+
const pkg_path = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
19
|
+
const pkg_version = JSON.parse(fs.readFileSync(pkg_path, 'utf-8')).version;
|
|
15
20
|
const program = new Command();
|
|
16
21
|
program
|
|
17
22
|
.name('primo')
|
|
18
23
|
.description('Build sites visually, edit them anywhere')
|
|
19
|
-
.version(
|
|
24
|
+
.version(pkg_version);
|
|
20
25
|
// Top-level help: prepend a Deploy-vs-build-vs-push decision tree so first-time
|
|
21
26
|
// users can pick a command without reading every description. Use 'before' so
|
|
22
27
|
// it only renders for the root program, not subcommand help.
|
|
@@ -16,4 +16,5 @@ export declare function format_group_name(group_id: string): string;
|
|
|
16
16
|
export declare function normalize_server_config(config: ServerConfig): ServerConfig;
|
|
17
17
|
export declare function resolve_format_options(config: ServerConfig): FormatOptions;
|
|
18
18
|
export declare function read_server_config(base_dir: string): Promise<ServerConfig>;
|
|
19
|
+
export declare const DEFAULT_PORT = 3000;
|
|
19
20
|
export declare function write_server_config(base_dir: string, config: ServerConfig): Promise<void>;
|
|
@@ -44,6 +44,12 @@ export async function read_server_config(base_dir) {
|
|
|
44
44
|
const config_data = await fs.readFile(get_server_config_path(base_dir), 'utf-8');
|
|
45
45
|
return normalize_server_config(load_yaml(config_data));
|
|
46
46
|
}
|
|
47
|
+
export const DEFAULT_PORT = 3000;
|
|
47
48
|
export async function write_server_config(base_dir, config) {
|
|
48
|
-
|
|
49
|
+
const normalized = normalize_server_config(config);
|
|
50
|
+
// Drop port from the file when it's the default — it's noise otherwise.
|
|
51
|
+
const for_write = normalized.port === DEFAULT_PORT
|
|
52
|
+
? { ...normalized, port: undefined }
|
|
53
|
+
: normalized;
|
|
54
|
+
await fs.writeFile(get_server_config_path(base_dir), dump_yaml(for_write, { lineWidth: -1, noRefs: true }));
|
|
49
55
|
}
|