primo-cli 0.1.5 → 0.1.7

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.
@@ -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(parseInt(pid, 10), 'SIGKILL');
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(true);
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
- return;
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) {
@@ -1,6 +1,6 @@
1
1
  interface PullLibraryOptions {
2
2
  server?: string;
3
- output: string;
3
+ output?: string;
4
4
  token?: string;
5
5
  }
6
6
  export declare function pull_library(options: PullLibraryOptions): Promise<void>;
@@ -40,7 +40,7 @@ export async function pull_library(options) {
40
40
  if (token) {
41
41
  headers.Authorization = `Bearer ${token}`;
42
42
  }
43
- const output_dir = path.resolve(options.output);
43
+ const output_dir = path.resolve(options.output || '.');
44
44
  await fs.mkdir(output_dir, { recursive: true });
45
45
  spinner.text = 'Exporting library...';
46
46
  const response = await fetch(`${server}/api/palacms/export-library`, {
@@ -1,6 +1,6 @@
1
1
  interface PullOptions {
2
2
  server?: string;
3
- output: string;
3
+ output?: string;
4
4
  token?: string;
5
5
  }
6
6
  export declare function pull_site(options: PullOptions): Promise<void>;
@@ -6,7 +6,7 @@ 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
8
  import { write_site_config } from '../utils/site-config.js';
9
- import { write_server_config } from '../utils/server-config.js';
9
+ import { read_server_config, write_server_config } from '../utils/server-config.js';
10
10
  async function detect_server() {
11
11
  const ports = [3000, 8080, 5173];
12
12
  for (const port of ports) {
@@ -36,19 +36,40 @@ function server_folder_name(server) {
36
36
  return 'primo-server';
37
37
  }
38
38
  }
39
+ async function read_configured_server(dir) {
40
+ const config = await read_configured_server_config(dir);
41
+ return config?.server || null;
42
+ }
43
+ async function read_configured_server_config(dir) {
44
+ try {
45
+ return await read_server_config(dir);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
39
51
  export async function pull_site(options) {
40
52
  const spinner = ora('Connecting...').start();
41
53
  try {
42
- // Resolve server (flag > local detect)
54
+ // Resolve server (flag > server.yaml in cwd > local detect)
43
55
  let server;
56
+ let used_configured = false;
44
57
  if (options.server) {
45
58
  server = options.server.replace(/\/+$/, '');
46
59
  }
47
60
  else {
48
- spinner.text = 'Looking for local server...';
49
- const detected = await detect_server();
50
- server = (detected || 'http://localhost:3000').replace(/\/+$/, '');
51
- spinner.text = `Using ${server}`;
61
+ const configured = await read_configured_server(process.cwd());
62
+ if (configured) {
63
+ server = configured;
64
+ used_configured = true;
65
+ spinner.text = `Using ${server}`;
66
+ }
67
+ else {
68
+ spinner.text = 'Looking for local server...';
69
+ const detected = await detect_server();
70
+ server = (detected || 'http://localhost:3000').replace(/\/+$/, '');
71
+ spinner.text = `Using ${server}`;
72
+ }
52
73
  }
53
74
  // Auth (optional for local)
54
75
  const token = options.token || await get_auth_token(server);
@@ -56,11 +77,13 @@ export async function pull_site(options) {
56
77
  if (token) {
57
78
  headers['Authorization'] = `Bearer ${token}`;
58
79
  }
59
- // Decide root dir: if --output is default ('.'), nest under server hostname.
60
- // Otherwise use --output verbatim.
61
- const root_dir = options.output === '.'
62
- ? path.resolve(server_folder_name(server))
63
- : path.resolve(options.output);
80
+ // Decide root dir: explicit --output wins; if cwd already has a configured
81
+ // server.yaml, pull in place; otherwise nest under server hostname.
82
+ const root_dir = options.output
83
+ ? path.resolve(options.output)
84
+ : used_configured
85
+ ? process.cwd()
86
+ : path.resolve(server_folder_name(server));
64
87
  await fs.mkdir(root_dir, { recursive: true });
65
88
  // List all sites
66
89
  spinner.text = 'Fetching sites...';
@@ -104,10 +127,13 @@ export async function pull_site(options) {
104
127
  }
105
128
  // Fetch site groups so server.yaml has them
106
129
  const site_groups = await fetch_site_groups(server, headers);
107
- // Write minimal server.yaml so MCP registration + dev work at the root
130
+ // Preserve any existing server.yaml (port, format, server URL) and just
131
+ // refresh site_groups from the source of truth.
132
+ const existing = await read_configured_server_config(root_dir);
108
133
  await write_server_config(root_dir, {
109
- port: 3000,
110
- site_groups: site_groups.length > 0 ? site_groups : undefined
134
+ ...existing,
135
+ port: existing?.port ?? 3000,
136
+ site_groups: site_groups.length > 0 ? site_groups : existing?.site_groups
111
137
  });
112
138
  spinner.succeed(`Server pulled to ${chalk.cyan(root_dir)}`);
113
139
  console.log('');
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('0.1.3');
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.
@@ -91,22 +96,28 @@ ${chalk.bold('See also')}
91
96
  `)
92
97
  .action((server, options) => push_site({ ...options, server: server || options.server }));
93
98
  program
94
- .command('pull [server]')
95
- .description('Pull entire server (all sites + library) to local files')
99
+ .command('pull [server] [dir]')
100
+ .description('Pull entire server (all sites + library) to local files (defaults to ./<server-hostname>)')
96
101
  .option('-s, --server <url>', 'Server URL (auto-detects local)')
97
- .option('-o, --output <dir>', 'Output directory (defaults to ./<server-hostname>)', '.')
98
102
  .option('-t, --token <token>', 'Auth token')
99
- .action((server, options) => pull_site({ ...options, server: server || options.server }));
103
+ .action((server, dir, options) => pull_site({
104
+ ...options,
105
+ server: server || options.server,
106
+ output: dir
107
+ }));
100
108
  const library = program
101
109
  .command('library')
102
110
  .description('Manage shared block library');
103
111
  library
104
- .command('pull [server]')
112
+ .command('pull [server] [dir]')
105
113
  .description('Pull shared library to local files')
106
114
  .option('-s, --server <url>', 'Server URL (auto-detects local)')
107
- .option('-o, --output <dir>', 'Output directory', '.')
108
115
  .option('-t, --token <token>', 'Auth token')
109
- .action((server, options) => pull_library({ ...options, server: server || options.server }));
116
+ .action((server, dir, options) => pull_library({
117
+ ...options,
118
+ server: server || options.server,
119
+ output: dir
120
+ }));
110
121
  library
111
122
  .command('push [server]')
112
123
  .description('Push local shared library to hosted CMS')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primo-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Local development CLI for Primo",
5
5
  "type": "module",
6
6
  "bin": {