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.
- package/dist/commands/deploy.js +3 -3
- package/dist/commands/dev.js +229 -45
- package/dist/commands/new.js +0 -3
- package/dist/commands/pull.js +35 -10
- package/dist/commands/push.js +0 -2
- 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/deploy.js
CHANGED
|
@@ -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.
|
|
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
|
|
228
|
-
const PRIMO_SERVER_IMAGE = 'ghcr.io/
|
|
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
|
package/dist/commands/dev.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
1277
|
-
synced_files.set(file_path,
|
|
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
|
-
//
|
|
1835
|
-
//
|
|
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
|
|
2298
|
-
//
|
|
2299
|
-
synced_files
|
|
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
|
-
|
|
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
|
+
}
|
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');
|
|
@@ -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
|
}
|