primo-cli 0.1.7 → 0.1.9

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.
@@ -221,11 +221,11 @@ async function check_provider_auth(provider) {
221
221
  return false;
222
222
  }
223
223
  }
224
- // Pinned to the upstream-published image. palacms's main.yml workflow
224
+ // Pinned to the upstream-published image. primocms's main.yml workflow
225
225
  // publishes branch tags for whitelisted prefixes (main, feature/**, rc/**),
226
226
  // with slashes slugified to dashes (feature/local-dev-cli → :feature-local-dev-cli).
227
- // Bump to a release tag (:v3.0.0) when palacms cuts a stable release.
228
- const PRIMO_SERVER_IMAGE = 'ghcr.io/palacms/palacms:feature-local-dev-cli';
227
+ // Bump to a release tag (:v3.0.0) when primocms cuts a stable release.
228
+ const PRIMO_SERVER_IMAGE = 'ghcr.io/primocms/primo:main';
229
229
  async function generate_dockerfile(inventory) {
230
230
  // One-line Dockerfile: pull the published palacms image and run it
231
231
  // unchanged. Workspace data (server.yaml, sites/, library/) is uploaded
@@ -36,8 +36,12 @@ let site_sync_baselines = new Map();
36
36
  // the same conflict block every pull cycle when palacms's serialization
37
37
  // keeps producing the same divergence (e.g. data-key mangling, key reorder).
38
38
  const last_logged_conflicts = new Map();
39
- // Track files written by sync to prevent watcher from re-pushing them
40
- // Map of filepath -> mtime (ms) when we wrote it
39
+ // Track files written by sync to prevent watcher from re-pushing them.
40
+ // Keyed by absolute path; value is the SHA-256 of the content we wrote.
41
+ // We compare against the file's *current* hash in the watcher, so a user
42
+ // edit that happens within the polling cycle is detected by content
43
+ // divergence rather than mtime±tolerance (which used to drop edits made
44
+ // within 3 seconds of a sync-write).
41
45
  const synced_files = new Map();
42
46
  const synced_deleted_paths = new Map();
43
47
  const warned_empty_schema_writebacks = new Set();
@@ -49,11 +53,25 @@ const MCP_CONFIG_FILE = '.mcp.json';
49
53
  const SITE_SYNC_DIRS = ['blocks', 'page-types', 'pages', 'site'];
50
54
  const LOCAL_PUSH_DEBOUNCE_MS = 150;
51
55
  const LOCAL_ZIP_COMPRESSION_LEVEL = 0;
52
- // Tolerance for matching a file mtime against the synced_files map to decide
53
- // whether an fs.watch event was caused by our own sync-write. Wider values
54
- // trade duplicate push work for safety against slow I/O and flaky watchers
55
- // (macOS fs.watch fires inconsistently for recursive writes).
56
- const SYNC_MTIME_TOLERANCE_MS = 3000;
56
+ function hash_content(content) {
57
+ return createHash('sha256').update(content).digest('hex');
58
+ }
59
+ // Returns true iff the file at `full_path` still has the exact content
60
+ // we last synced to it. Used by watcher event handlers to ignore their
61
+ // own writes without the old mtime-tolerance race that swallowed user
62
+ // edits made within seconds of a sync-write.
63
+ async function event_matches_synced_write(full_path) {
64
+ const expected_hash = synced_files.get(full_path);
65
+ if (!expected_hash)
66
+ return false;
67
+ try {
68
+ const current = await fs.readFile(full_path);
69
+ return hash_content(current) === expected_hash;
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
57
75
  // When the user writes a file locally, suppress CMS->local pulls for this
58
76
  // long to prevent a pull that was in-flight before the watcher fired from
59
77
  // stomping the just-written content on arrival.
@@ -636,8 +654,11 @@ export async function dev_server(options) {
636
654
  if (sites.length > 0 || !is_server_mode) {
637
655
  console.log('');
638
656
  }
639
- // Start watching for file changes
640
- const dirs_to_watch = ['blocks', 'page-types', 'pages', 'site'];
657
+ // Start watching for file changes. `uploads` is included so dropping
658
+ // an image into uploads/ triggers a push — the server-side reconcile
659
+ // creates a site_uploads record for it and the writeback renames
660
+ // the local file to the canonical (suffixed) name.
661
+ const dirs_to_watch = ['blocks', 'page-types', 'pages', 'site', 'uploads'];
641
662
  const known_sites = new Set(sites.map(s => s.dir));
642
663
  if (is_server_mode) {
643
664
  const library_path = path.join(base_dir, LIBRARY_DIR);
@@ -676,16 +697,18 @@ export async function dev_server(options) {
676
697
  const on_event = (full_path) => {
677
698
  if (should_skip_synced_delete(full_path))
678
699
  return;
679
- const synced_mtime = synced_files.get(full_path);
680
- if (synced_mtime) {
681
- // Our own sync-write — skip.
682
- fs.stat(full_path)
683
- .then(stat => {
684
- if (Math.abs(stat.mtimeMs - synced_mtime) < SYNC_MTIME_TOLERANCE_MS) {
685
- synced_files.delete(full_path);
686
- return;
687
- }
700
+ if (synced_files.has(full_path)) {
701
+ // We last wrote this file from a CMS pull. If the
702
+ // content on disk still matches our write, the
703
+ // chokidar event is just our own write echoing
704
+ // back — drop it. If it differs, the user edited
705
+ // the file (possibly very shortly after our pull
706
+ // landed) and we must push.
707
+ event_matches_synced_write(full_path)
708
+ .then(matches => {
688
709
  synced_files.delete(full_path);
710
+ if (matches)
711
+ return;
689
712
  push_or_ignore_library_change(full_path);
690
713
  })
691
714
  .catch(() => {
@@ -818,7 +841,16 @@ export async function dev_server(options) {
818
841
  }
819
842
  console.log(chalk.dim(` ${site.config.name}: normalize ${normalize_ms}ms, zip ${import_timings.zip_ms}ms, ${import_timings.mode} ${import_timings.request_ms}ms${reload_ms ? `, reload ${reload_ms}ms` : ''}`));
820
843
  console.log(chalk.green(` ✓ ${site.config.name} pushed`));
821
- await write_sync_status(site.dir, { ok: true });
844
+ if (import_timings.warning_count > 0) {
845
+ await write_sync_status(site.dir, {
846
+ ok: true,
847
+ warnings: import_timings.warning_count,
848
+ warned_at: new Date().toISOString()
849
+ });
850
+ }
851
+ else {
852
+ await write_sync_status(site.dir, { ok: true });
853
+ }
822
854
  }
823
855
  catch (err) {
824
856
  const message = err instanceof Error ? err.message : String(err);
@@ -880,15 +912,20 @@ export async function dev_server(options) {
880
912
  const on_event = (full_path) => {
881
913
  if (should_skip_synced_delete(full_path))
882
914
  return;
883
- const synced_mtime = synced_files.get(full_path);
884
- if (synced_mtime) {
885
- fs.stat(full_path)
886
- .then(stat => {
887
- if (Math.abs(stat.mtimeMs - synced_mtime) < SYNC_MTIME_TOLERANCE_MS) {
888
- synced_files.delete(full_path);
889
- return;
890
- }
915
+ if (synced_files.has(full_path)) {
916
+ // We last wrote this file from a CMS pull. Compare
917
+ // the file's *current* content against the hash we
918
+ // stored: if identical, this watcher event is the
919
+ // echo of our own write and must be ignored; if
920
+ // different, the user edited the file (possibly
921
+ // within ms of our pull) and the edit must push,
922
+ // which is exactly the case the old mtime-tolerance
923
+ // check used to swallow for site/head.svelte.
924
+ event_matches_synced_write(full_path)
925
+ .then(matches => {
891
926
  synced_files.delete(full_path);
927
+ if (matches)
928
+ return;
892
929
  push_or_ignore_file_change(full_path);
893
930
  })
894
931
  .catch(() => {
@@ -1273,15 +1310,8 @@ function track_duplicate(duplicates, category, id, occurrence) {
1273
1310
  existing.push(occurrence);
1274
1311
  by_id.set(id, existing);
1275
1312
  }
1276
- async function mark_written_file(file_path) {
1277
- synced_files.set(file_path, Date.now());
1278
- try {
1279
- const stat = await fs.stat(file_path);
1280
- synced_files.set(file_path, stat.mtimeMs);
1281
- }
1282
- catch {
1283
- // Ignore files that disappeared
1284
- }
1313
+ function mark_written_file(file_path, content) {
1314
+ synced_files.set(file_path, hash_content(content));
1285
1315
  }
1286
1316
  function mark_deleted_path(file_path) {
1287
1317
  synced_deleted_paths.set(file_path, Date.now());
@@ -1831,8 +1861,10 @@ async function import_site_files(site_dir, api_url, config, port, server_config,
1831
1861
  const zip_started = Date.now();
1832
1862
  const zip_buffer = await create_site_zip(site_dir, preparation.excluded_paths);
1833
1863
  const zip_ms = Date.now() - zip_started;
1834
- // Always use a localhost-style host in dev the production host stays in
1835
- // site.yaml for push, but local routing must hit *.localhost
1864
+ // Dev sites route via *.localhost. The host is computed from name+port
1865
+ // here and passed only to bootstrap (which seeds new sites with this
1866
+ // routing host); site.yaml does not carry host at all, and the regular
1867
+ // import path never sets host — so dashboard-managed routing is safe.
1836
1868
  const host = local_dev_host(config.name || path.basename(site_dir), port);
1837
1869
  if (!use_bootstrap) {
1838
1870
  const import_form = new FormData();
@@ -2294,13 +2326,12 @@ async function sync_directory(src, dest, relative_path = '', options = {}) {
2294
2326
  console.log(chalk.dim(` trash failed for ${file_relative}`));
2295
2327
  }
2296
2328
  }
2297
- // Track this file BEFORE writing to avoid race with watcher
2298
- // Use current time as estimate, watcher allows 1 second tolerance
2299
- synced_files.set(dest_path, Date.now());
2329
+ // Track this file's content hash BEFORE writing so the
2330
+ // chokidar event our own write produces can be matched
2331
+ // against `synced_files` and dropped, while a genuine user
2332
+ // edit (different content) still falls through and pushes.
2333
+ synced_files.set(dest_path, hash_content(src_content));
2300
2334
  await fs.writeFile(dest_path, src_content);
2301
- // Update with actual mtime after write
2302
- const stat = await fs.stat(dest_path);
2303
- synced_files.set(dest_path, stat.mtimeMs);
2304
2335
  // Surface shrinkage on the change line itself so a user
2305
2336
  // scanning the dev log notices when a YAML list silently
2306
2337
  // loses entries (the failure mode reported during the
@@ -2365,6 +2396,14 @@ async function has_library_content(library_dir) {
2365
2396
  }
2366
2397
  async function write_created_ids(site_dir, created_ids, server_config, workspace_dir) {
2367
2398
  const format_options = resolve_format_options(server_config);
2399
+ // Upload writeback is special: rename local files and rewrite any yaml
2400
+ // that still references symbolic paths. Surfaced under a non-standard
2401
+ // `_uploads` key in created_ids["uploads/.manifest.json"] so the rest of
2402
+ // this loop's `_id`/`sections` logic doesn't get confused by it.
2403
+ const uploads_payload = created_ids['uploads/.manifest.json'];
2404
+ if (uploads_payload && typeof uploads_payload._uploads === 'object' && uploads_payload._uploads !== null) {
2405
+ await write_upload_writeback(site_dir, uploads_payload._uploads, server_config, workspace_dir);
2406
+ }
2368
2407
  for (const [relative_path, id_data] of Object.entries(created_ids)) {
2369
2408
  if (!id_data._id && !Array.isArray(id_data.sections))
2370
2409
  continue;
@@ -2400,7 +2439,7 @@ async function write_created_ids(site_dir, created_ids, server_config, workspace
2400
2439
  const raw = dump_yaml(data, { lineWidth: -1 });
2401
2440
  const formatted = await format_file_contents(file_path, raw, workspace_dir, format_options);
2402
2441
  await fs.writeFile(file_path, formatted, 'utf-8');
2403
- await mark_written_file(file_path);
2442
+ mark_written_file(file_path, formatted);
2404
2443
  }
2405
2444
  }
2406
2445
  catch {
@@ -2408,3 +2447,148 @@ async function write_created_ids(site_dir, created_ids, server_config, workspace
2408
2447
  }
2409
2448
  }
2410
2449
  }
2450
+ // write_upload_writeback reconciles the local uploads/ folder and yaml refs
2451
+ // with what the server actually stored on the latest push.
2452
+ //
2453
+ // For each entry `symbolic -> {id, canonical}`:
2454
+ // - If canonical != symbolic, rename uploads/<symbolic> to uploads/<canonical>
2455
+ // on disk so subsequent pulls/pushes round-trip without churn.
2456
+ // - Rewrite any yaml file that still says `upload: "uploads/<symbolic>"` to
2457
+ // the record id, matching what the server stored. This converges the local
2458
+ // copy with the server's canonical content shape without needing a pull.
2459
+ //
2460
+ // The watcher is told about each rename and rewrite via mark_written_file /
2461
+ // mark_deleted_path so it doesn't echo our own writes back as user edits.
2462
+ async function write_upload_writeback(site_dir, uploads_map, server_config, workspace_dir) {
2463
+ // Build a symbolic -> record-id map for the yaml rewrite pass. Skip
2464
+ // malformed entries instead of failing the whole writeback so a partial
2465
+ // server response can still rename what it can.
2466
+ const symbolic_to_id = new Map();
2467
+ const renames = [];
2468
+ for (const [symbolic, raw_entry] of Object.entries(uploads_map)) {
2469
+ if (!raw_entry || typeof raw_entry !== 'object')
2470
+ continue;
2471
+ const entry = raw_entry;
2472
+ const id = typeof entry.id === 'string' ? entry.id : '';
2473
+ const canonical = typeof entry.canonical === 'string' ? entry.canonical : '';
2474
+ if (!id)
2475
+ continue;
2476
+ symbolic_to_id.set(symbolic, id);
2477
+ if (canonical && canonical !== symbolic) {
2478
+ renames.push({ symbolic, canonical });
2479
+ }
2480
+ }
2481
+ // Rename the on-disk files. Best-effort: if the symbolic file is missing
2482
+ // (already renamed by a prior push, or removed by the user) we silently
2483
+ // move on — the yaml rewrite still keeps content consistent.
2484
+ const uploads_dir = path.join(site_dir, 'uploads');
2485
+ for (const { symbolic, canonical } of renames) {
2486
+ const from = path.join(uploads_dir, symbolic);
2487
+ const to = path.join(uploads_dir, canonical);
2488
+ try {
2489
+ await fs.rename(from, to);
2490
+ mark_deleted_path(from);
2491
+ mark_written_file(to, await fs.readFile(to));
2492
+ }
2493
+ catch {
2494
+ // missing source or permission issue — skip
2495
+ }
2496
+ }
2497
+ // Rewrite symbolic upload references across all yaml under the site dir.
2498
+ // Walking the tree is bounded by site size and only re-marshals files
2499
+ // that mention `uploads/` literally, so the cost is small even on large
2500
+ // sites. Restricting to known sync subdirs avoids touching artifacts in
2501
+ // dot-directories or build output.
2502
+ if (symbolic_to_id.size === 0)
2503
+ return;
2504
+ const format_options = resolve_format_options(server_config);
2505
+ for (const subdir of SITE_SYNC_DIRS) {
2506
+ const root = path.join(site_dir, subdir);
2507
+ const files = await walk_yaml_files(root).catch(() => []);
2508
+ for (const file_path of files) {
2509
+ try {
2510
+ const content = await fs.readFile(file_path, 'utf-8');
2511
+ if (!content.includes('uploads/'))
2512
+ continue;
2513
+ const parsed = load_yaml(content);
2514
+ if (!parsed || typeof parsed !== 'object')
2515
+ continue;
2516
+ const { value: rewritten, changed } = rewrite_symbolic_upload_refs(parsed, symbolic_to_id);
2517
+ if (!changed)
2518
+ continue;
2519
+ const raw = dump_yaml(rewritten, { lineWidth: -1 });
2520
+ const formatted = await format_file_contents(file_path, raw, workspace_dir, format_options);
2521
+ await fs.writeFile(file_path, formatted, 'utf-8');
2522
+ mark_written_file(file_path, formatted);
2523
+ }
2524
+ catch {
2525
+ // skip unreadable / unparseable file
2526
+ }
2527
+ }
2528
+ }
2529
+ }
2530
+ async function walk_yaml_files(root) {
2531
+ const out = [];
2532
+ const stack = [root];
2533
+ while (stack.length > 0) {
2534
+ const dir = stack.pop();
2535
+ let entries;
2536
+ try {
2537
+ entries = await fs.readdir(dir, { withFileTypes: true });
2538
+ }
2539
+ catch {
2540
+ continue;
2541
+ }
2542
+ for (const entry of entries) {
2543
+ if (entry.name.startsWith('.'))
2544
+ continue;
2545
+ const full = path.join(dir, entry.name);
2546
+ if (entry.isDirectory()) {
2547
+ stack.push(full);
2548
+ }
2549
+ else if (entry.isFile() && entry.name.endsWith('.yaml')) {
2550
+ out.push(full);
2551
+ }
2552
+ }
2553
+ }
2554
+ return out;
2555
+ }
2556
+ // rewrite_symbolic_upload_refs is the CLI mirror of the server's rewriteUploadRefs.
2557
+ // Walking on the CLI side handles the case where the server's in-zip rewrite
2558
+ // updated the imported content but the source files on disk still reference
2559
+ // the symbolic path. Without this pass, the next push would resend the
2560
+ // symbolic ref and force the server to re-resolve every time.
2561
+ function rewrite_symbolic_upload_refs(value, map) {
2562
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
2563
+ const obj = value;
2564
+ let changed = false;
2565
+ const result = {};
2566
+ for (const [k, v] of Object.entries(obj)) {
2567
+ if (k === 'upload' && typeof v === 'string' && v.startsWith('uploads/')) {
2568
+ const filename = v.substring('uploads/'.length);
2569
+ const id = map.get(filename);
2570
+ if (id) {
2571
+ result[k] = id;
2572
+ changed = true;
2573
+ continue;
2574
+ }
2575
+ }
2576
+ const child = rewrite_symbolic_upload_refs(v, map);
2577
+ result[k] = child.value;
2578
+ if (child.changed)
2579
+ changed = true;
2580
+ }
2581
+ return { value: result, changed };
2582
+ }
2583
+ if (Array.isArray(value)) {
2584
+ let changed = false;
2585
+ const result = value.map(item => {
2586
+ const child = rewrite_symbolic_upload_refs(item, map);
2587
+ if (child.changed)
2588
+ changed = true;
2589
+ return child.value;
2590
+ });
2591
+ return { value: result, changed };
2592
+ }
2593
+ return { value, changed: false };
2594
+ }
@@ -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);
@@ -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
- await fs.mkdir(root_dir, { recursive: true });
88
- // List all sites
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
- const sites_response = await fetch(`${server}/api/collections/sites/records?perPage=200`, {
91
- headers
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, server URL) and just
131
- // refresh site_groups from the source of truth.
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
- port: existing?.port ?? 3000,
136
- site_groups: site_groups.length > 0 ? site_groups : existing?.site_groups
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
@@ -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');
@@ -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
- await fs.writeFile(get_server_config_path(base_dir), dump_yaml(normalize_server_config(config), { lineWidth: -1, noRefs: true }));
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
  }
@@ -1,7 +1,6 @@
1
1
  export interface SiteConfig {
2
2
  name: string;
3
3
  site_id: string;
4
- host?: string;
5
4
  server?: string;
6
5
  group?: string;
7
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primo-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Local development CLI for Primo",
5
5
  "type": "module",
6
6
  "bin": {