primo-cli 0.1.23 → 0.1.25
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/add.js +55 -99
- package/dist/commands/push.js +224 -3
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
+
import net from 'net';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { randomInt } from 'crypto';
|
|
4
5
|
import chalk from 'chalk';
|
|
@@ -60,6 +61,34 @@ export async function add_site(target, options) {
|
|
|
60
61
|
process.exit(1);
|
|
61
62
|
}
|
|
62
63
|
let server_config = await read_server_config(base_dir);
|
|
64
|
+
const port = server_config.port ?? parseInt(options.port, 10);
|
|
65
|
+
// `primo add` and `primo dev`'s sites-root watcher both mint a site_id for a
|
|
66
|
+
// newly-appeared folder. If dev is up when we add, the two race: the watcher
|
|
67
|
+
// adopts the folder under its own id while add writes a different id to
|
|
68
|
+
// site.yaml, leaving disk pointing at an id the CMS doesn't have. Refuse
|
|
69
|
+
// while ANYTHING holds the port so there is exactly one minter — the headless
|
|
70
|
+
// import below. This check runs BEFORE ensure_site_config so we never stamp a
|
|
71
|
+
// site_id we'd then strand. `primo dev` picks the folder up on its next start
|
|
72
|
+
// (or its watcher, if it's already been given an id).
|
|
73
|
+
//
|
|
74
|
+
// The guard is TCP occupancy, not a healthy-Primo response: a server that's
|
|
75
|
+
// still starting, an unhealthy Primo, or any other listener all own the port
|
|
76
|
+
// and would make the headless CMS below fail to bind — after we'd already
|
|
77
|
+
// minted. So we only proceed on a confirmed connection refusal (port free);
|
|
78
|
+
// an accepted connection OR an inconclusive probe both count as occupied. A
|
|
79
|
+
// healthy Primo just gets a more specific hint.
|
|
80
|
+
if (await is_port_occupied(port)) {
|
|
81
|
+
if (await is_server_running(port)) {
|
|
82
|
+
console.log(chalk.red(`A Primo server is running on port ${port}.`));
|
|
83
|
+
console.log(chalk.dim(` Stop it (Ctrl+C in its terminal), then re-run \`primo add ${target}\`.`));
|
|
84
|
+
console.log(chalk.dim(' `primo dev` imports the folder itself once it\'s restarted.'));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log(chalk.red(`Port ${port} is in use, so \`primo add\` can't start a CMS to import into.`));
|
|
88
|
+
console.log(chalk.dim(` Free the port (stop whatever is on it), then re-run \`primo add ${target}\`.`));
|
|
89
|
+
}
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
63
92
|
const { config, created, minted } = await ensure_site_config(site_dir, folder_name);
|
|
64
93
|
if (created) {
|
|
65
94
|
console.log(chalk.dim(` created site.yaml (site_id ${config.site_id})`));
|
|
@@ -80,62 +109,11 @@ export async function add_site(target, options) {
|
|
|
80
109
|
await write_server_config(base_dir, server_config);
|
|
81
110
|
}
|
|
82
111
|
}
|
|
83
|
-
const port = server_config.port ?? parseInt(options.port, 10);
|
|
84
112
|
const api_url = `http://127.0.0.1:${port}`;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (reload.status === 'unreachable') {
|
|
90
|
-
// Older dev server or hot reload disabled (port+1 in use). Import
|
|
91
|
-
// directly against the running CMS — records land, but the dev
|
|
92
|
-
// server won't watch this site until it's restarted.
|
|
93
|
-
let timings = null;
|
|
94
|
-
try {
|
|
95
|
-
timings = await register_site(site_dir, api_url, config, port, server_config, base_dir);
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
-
console.log(chalk.red(` ✗ ${config.name} could not be imported: ${message}`));
|
|
100
|
-
console.log(chalk.dim(` Fix the problem, then re-run \`primo add ${target}\`.`));
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
report_import(config.name, timings);
|
|
104
|
-
console.log(chalk.yellow(' The running dev server couldn\'t be reloaded — restart `primo dev` to watch this site.'));
|
|
105
|
-
console.log('');
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
// The health check only proves *a* Primo server is on this port — it
|
|
109
|
-
// could belong to a different workspace. The reload response's `known`
|
|
110
|
-
// list is the only reliable way to tell: PocketBase 404s record reads
|
|
111
|
-
// pre-setup even when the record exists, so we can't just probe the
|
|
112
|
-
// site_id. (Older dev servers don't send `known` — trust the reload.)
|
|
113
|
-
const known = reload.known?.find(site => site.site_id === config.site_id);
|
|
114
|
-
if (reload.known && !known) {
|
|
115
|
-
console.log(chalk.red(` A Primo server is running on port ${port}, but it isn't serving this workspace.`));
|
|
116
|
-
console.log(chalk.dim(' Stop it (or start `primo dev` in this workspace) and re-run `primo add`.'));
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
if (known?.blocked || reload.quarantined.includes(config.name)) {
|
|
120
|
-
console.log(chalk.yellow(` ${config.name} was found, but the dev server couldn't import it (e.g. duplicate IDs or a missing pages/index.yaml).`));
|
|
121
|
-
console.log(chalk.dim(' Check the `primo dev` logs, fix the problem, then re-run `primo add`.'));
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
console.log('');
|
|
125
|
-
if (reload.loaded === 0 && reload.known) {
|
|
126
|
-
console.log(` ${chalk.cyan(config.name)} is already registered with the running dev server.`);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
console.log(chalk.green(` ✓ ${config.name} registered`));
|
|
130
|
-
}
|
|
131
|
-
const host = local_dev_host(config.name, port);
|
|
132
|
-
console.log(` ${chalk.dim('Edit:')} http://${host}/admin/site`);
|
|
133
|
-
console.log(` ${chalk.dim('Preview:')} http://${host}/`);
|
|
134
|
-
console.log('');
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// No dev server running: boot the CMS binary headlessly against the
|
|
138
|
-
// workspace database, import, and shut it back down.
|
|
113
|
+
// No server is running (guaranteed — we refused above if one held the port),
|
|
114
|
+
// so boot the CMS binary headlessly against the workspace database, import,
|
|
115
|
+
// and shut it back down. This is the single minter/import path, which is why
|
|
116
|
+
// `primo add` and the dev watcher can't double-mint the same folder.
|
|
139
117
|
const spinner = ora('Starting CMS for one-time import...').start();
|
|
140
118
|
const binary_path = await ensure_binary();
|
|
141
119
|
const data_dir = await ensure_data_dir(base_dir);
|
|
@@ -202,14 +180,6 @@ async function register_site(site_dir, api_url, config, port, server_config, bas
|
|
|
202
180
|
const use_bootstrap = !await site_exists(api_url, config.site_id);
|
|
203
181
|
return await import_site_files(site_dir, api_url, config, port, server_config, use_bootstrap, base_dir);
|
|
204
182
|
}
|
|
205
|
-
function report_import(site_name, timings) {
|
|
206
|
-
if (timings === null || !timings.ok) {
|
|
207
|
-
console.log(chalk.red(` ✗ ${site_name} could not be imported — see the errors above.`));
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
console.log(chalk.green(` ✓ ${site_name} registered (${timings.mode})`));
|
|
211
|
-
report_warnings(timings);
|
|
212
|
-
}
|
|
213
183
|
function report_warnings(timings) {
|
|
214
184
|
if (timings.warning_count > 0) {
|
|
215
185
|
console.log(chalk.yellow(` ⚠ imported with ${timings.warning_count} warning${timings.warning_count === 1 ? '' : 's'} (details above)`));
|
|
@@ -291,43 +261,29 @@ async function ensure_site_config(site_dir, folder_name) {
|
|
|
291
261
|
}
|
|
292
262
|
return { config, created, minted };
|
|
293
263
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
clearTimeout(timeout);
|
|
309
|
-
}
|
|
310
|
-
if (!res.ok)
|
|
311
|
-
return { status: 'unreachable' };
|
|
312
|
-
const result = await res.json().catch(() => null);
|
|
313
|
-
if (!result)
|
|
314
|
-
return { status: 'unreachable' };
|
|
315
|
-
return {
|
|
316
|
-
status: 'ok',
|
|
317
|
-
loaded: result.loaded ?? 0,
|
|
318
|
-
quarantined: result.quarantined ?? [],
|
|
319
|
-
known: Array.isArray(result.known) ? result.known : undefined
|
|
264
|
+
// True when something is listening on the port — the guard the headless import
|
|
265
|
+
// actually depends on (the CMS below can't bind an occupied port). Distinct from
|
|
266
|
+
// is_server_running, which only reports whether a *healthy Primo* answered: a
|
|
267
|
+
// starting/unhealthy server or a foreign listener returns false there but still
|
|
268
|
+
// owns the port. Fail safe — an accepted connection means occupied, and any
|
|
269
|
+
// inconclusive result (timeout, unexpected error) is treated as occupied too, so
|
|
270
|
+
// we never mint a site_id ahead of a bind we can't actually make. Only an
|
|
271
|
+
// explicit connection refusal (ECONNREFUSED) counts as free.
|
|
272
|
+
async function is_port_occupied(port) {
|
|
273
|
+
return new Promise(resolve => {
|
|
274
|
+
const socket = new net.Socket();
|
|
275
|
+
const done = (occupied) => {
|
|
276
|
+
socket.destroy();
|
|
277
|
+
resolve(occupied);
|
|
320
278
|
};
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'site';
|
|
330
|
-
return `${slug}.localhost:${port}`;
|
|
279
|
+
socket.setTimeout(1000);
|
|
280
|
+
socket.once('connect', () => done(true));
|
|
281
|
+
socket.once('timeout', () => done(true));
|
|
282
|
+
socket.once('error', (err) => {
|
|
283
|
+
done(err.code !== 'ECONNREFUSED');
|
|
284
|
+
});
|
|
285
|
+
socket.connect(port, '127.0.0.1');
|
|
286
|
+
});
|
|
331
287
|
}
|
|
332
288
|
async function is_server_running(port) {
|
|
333
289
|
try {
|
package/dist/commands/push.js
CHANGED
|
@@ -3,9 +3,11 @@ import path from 'path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import archiver from 'archiver';
|
|
6
|
+
import { dump as dump_yaml, load as load_yaml } from 'js-yaml';
|
|
6
7
|
import { get_auth_token } from '../utils/auth.js';
|
|
7
8
|
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, normalize_server_url } from '../utils/server-config.js';
|
|
9
|
+
import { get_server_config_path, read_server_config, resolve_format_options, normalize_server_url } from '../utils/server-config.js';
|
|
10
|
+
import { format_file_contents } from '../utils/format.js';
|
|
9
11
|
async function path_exists(p) {
|
|
10
12
|
try {
|
|
11
13
|
await fs.access(p);
|
|
@@ -39,6 +41,206 @@ async function resolve_group_name(site_dir, group_id) {
|
|
|
39
41
|
}
|
|
40
42
|
return undefined;
|
|
41
43
|
}
|
|
44
|
+
// Best-effort workspace root for a site dir, used to resolve the workspace
|
|
45
|
+
// prettier install when formatting rewritten yaml. Walks up looking for a
|
|
46
|
+
// server.yaml; falls back to the site dir if none is found (single-site checkout).
|
|
47
|
+
async function root_dir_for(site_dir) {
|
|
48
|
+
const candidates = [path.dirname(path.dirname(site_dir)), path.dirname(site_dir), site_dir];
|
|
49
|
+
for (const dir of candidates) {
|
|
50
|
+
if (await path_exists(get_server_config_path(dir)))
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
return site_dir;
|
|
54
|
+
}
|
|
55
|
+
// Converge the local site with the upload ids the server minted on push. The
|
|
56
|
+
// server creates its own site_uploads record per file (it can't honor a foreign
|
|
57
|
+
// CMS's id), renames files to a canonical suffixed name, and returns the
|
|
58
|
+
// symbolic->{id,canonical} map. `primo dev` applies this writeback via its
|
|
59
|
+
// watcher; `primo push` previously dropped it, so local refs stayed symbolic (or
|
|
60
|
+
// worse, kept a stale id from another CMS) and drifted from the server forever.
|
|
61
|
+
//
|
|
62
|
+
// This mirrors write_upload_writeback in dev.ts but without the chokidar
|
|
63
|
+
// bookkeeping (nothing is watching during a push). Best-effort throughout: a
|
|
64
|
+
// push has already succeeded by the time we get here, so a writeback hiccup
|
|
65
|
+
// should never fail the command.
|
|
66
|
+
async function apply_upload_writeback(site_dir, workspace_dir, created_ids) {
|
|
67
|
+
const manifest = created_ids?.['uploads/.manifest.json'];
|
|
68
|
+
const uploads_map = manifest && typeof manifest._uploads === 'object' && manifest._uploads !== null
|
|
69
|
+
? manifest._uploads
|
|
70
|
+
: null;
|
|
71
|
+
if (!uploads_map)
|
|
72
|
+
return;
|
|
73
|
+
// Build symbolic-filename -> record-id map and the rename list. Both
|
|
74
|
+
// symbolic and canonical are treated as untrusted (they come from the
|
|
75
|
+
// server response): reject anything that isn't a plain basename so a
|
|
76
|
+
// crafted `../` can't make a later path.join escape uploads/ (CWE-22).
|
|
77
|
+
const symbolic_to_id = new Map();
|
|
78
|
+
const rename_for = new Map();
|
|
79
|
+
for (const [symbolic, raw] of Object.entries(uploads_map)) {
|
|
80
|
+
if (!raw || typeof raw !== 'object')
|
|
81
|
+
continue;
|
|
82
|
+
if (!is_safe_basename(symbolic))
|
|
83
|
+
continue;
|
|
84
|
+
const entry = raw;
|
|
85
|
+
if (typeof entry.id !== 'string' || !entry.id)
|
|
86
|
+
continue;
|
|
87
|
+
symbolic_to_id.set(symbolic, entry.id);
|
|
88
|
+
const canonical = typeof entry.canonical === 'string' ? entry.canonical : '';
|
|
89
|
+
if (canonical && canonical !== symbolic && is_safe_basename(canonical)) {
|
|
90
|
+
rename_for.set(symbolic, canonical);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (symbolic_to_id.size === 0)
|
|
94
|
+
return;
|
|
95
|
+
// Rewrite refs FIRST, renames second — and only rename files whose ref
|
|
96
|
+
// rewrite actually succeeded. Renaming before rewriting risks leaving a file
|
|
97
|
+
// at its canonical name while the yaml still points at the old symbolic path
|
|
98
|
+
// (if the rewrite is skipped/unreadable/unwritable), which the next push
|
|
99
|
+
// would resend as a stale ref. So we rewrite yaml, track which symbolic keys
|
|
100
|
+
// were applied, and rename only those — keeping disk and yaml consistent even
|
|
101
|
+
// when some files can't be processed.
|
|
102
|
+
let format_options;
|
|
103
|
+
try {
|
|
104
|
+
format_options = resolve_format_options(await read_server_config(workspace_dir));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
format_options = resolve_format_options({});
|
|
108
|
+
}
|
|
109
|
+
// Track, per symbolic name, whether it was successfully rewritten in at least
|
|
110
|
+
// one file (`applied`) and whether ANY file that references it could not be
|
|
111
|
+
// persisted (`failed`). The same `upload: uploads/<name>` ref can appear in
|
|
112
|
+
// multiple files, so a name is only safe to rename once EVERY file bearing it
|
|
113
|
+
// is written — otherwise a skipped/failed file keeps the old symbolic ref
|
|
114
|
+
// while the file has already moved to its canonical name.
|
|
115
|
+
const applied = new Set();
|
|
116
|
+
const failed = new Set();
|
|
117
|
+
// Note which symbolic names a raw file mentions, so a file that fails before
|
|
118
|
+
// (or during) parse still marks its refs failed rather than silently passing.
|
|
119
|
+
const referenced_names = (raw) => [...symbolic_to_id.keys()].filter(name => raw.includes(`uploads/${name}`));
|
|
120
|
+
for (const subdir of UPLOAD_REF_DIRS) {
|
|
121
|
+
const files = await walk_yaml_files(path.join(site_dir, subdir));
|
|
122
|
+
for (const file_path of files) {
|
|
123
|
+
let content;
|
|
124
|
+
try {
|
|
125
|
+
content = await fs.readFile(file_path, 'utf-8');
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue; // unreadable file references nothing we can see; skip
|
|
129
|
+
}
|
|
130
|
+
if (!content.includes('uploads/'))
|
|
131
|
+
continue;
|
|
132
|
+
const names_here = referenced_names(content);
|
|
133
|
+
if (names_here.length === 0)
|
|
134
|
+
continue;
|
|
135
|
+
try {
|
|
136
|
+
const parsed = load_yaml(content);
|
|
137
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
138
|
+
names_here.forEach(n => failed.add(n));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const { value: rewritten, applied: file_applied } = rewrite_symbolic_upload_refs(parsed, symbolic_to_id);
|
|
142
|
+
if (file_applied.size === 0)
|
|
143
|
+
continue;
|
|
144
|
+
const raw = dump_yaml(rewritten, { lineWidth: -1 });
|
|
145
|
+
const formatted = await format_file_contents(file_path, raw, workspace_dir, format_options);
|
|
146
|
+
await fs.writeFile(file_path, formatted, 'utf-8');
|
|
147
|
+
for (const key of file_applied)
|
|
148
|
+
applied.add(key);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// parse/marshal/write failure — this file keeps its symbolic ref,
|
|
152
|
+
// so its name(s) must NOT be renamed even if another file succeeded.
|
|
153
|
+
names_here.forEach(n => failed.add(n));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Rename on-disk upload files to their canonical (server-suffixed) names,
|
|
158
|
+
// but only for names that were rewritten somewhere AND had no unpersisted
|
|
159
|
+
// reference anywhere. Missing source = already renamed or user-removed; skip.
|
|
160
|
+
const uploads_dir = path.join(site_dir, 'uploads');
|
|
161
|
+
for (const [symbolic, canonical] of rename_for) {
|
|
162
|
+
if (!applied.has(symbolic) || failed.has(symbolic))
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
await fs.rename(path.join(uploads_dir, symbolic), path.join(uploads_dir, canonical));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// missing source or permission issue — skip
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Guard against path traversal / absolute paths in server-supplied upload
|
|
173
|
+
// names. Only accept a plain filename (no separators, no `..`, no leading dot-dot).
|
|
174
|
+
function is_safe_basename(name) {
|
|
175
|
+
if (!name || name === '.' || name === '..')
|
|
176
|
+
return false;
|
|
177
|
+
if (name.includes('/') || name.includes('\\') || name.includes('\0'))
|
|
178
|
+
return false;
|
|
179
|
+
if (path.isAbsolute(name))
|
|
180
|
+
return false;
|
|
181
|
+
return path.basename(name) === name;
|
|
182
|
+
}
|
|
183
|
+
// Recursively collect .yaml files under a directory (dotfiles/dirs skipped).
|
|
184
|
+
async function walk_yaml_files(root) {
|
|
185
|
+
const out = [];
|
|
186
|
+
const stack = [root];
|
|
187
|
+
while (stack.length > 0) {
|
|
188
|
+
const dir = stack.pop();
|
|
189
|
+
let entries;
|
|
190
|
+
try {
|
|
191
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (entry.name.startsWith('.'))
|
|
198
|
+
continue;
|
|
199
|
+
const full = path.join(dir, entry.name);
|
|
200
|
+
if (entry.isDirectory())
|
|
201
|
+
stack.push(full);
|
|
202
|
+
else if (entry.isFile() && entry.name.endsWith('.yaml'))
|
|
203
|
+
out.push(full);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
// Rewrite `upload: "uploads/<file>"` values to the server record id. Mirrors the
|
|
209
|
+
// server's rewriteUploadRefs (and dev.ts's copy). Returns the set of symbolic
|
|
210
|
+
// filenames it actually remapped so the caller can rename exactly those files
|
|
211
|
+
// (and only after the yaml write succeeds). Note this only remaps symbolic
|
|
212
|
+
// paths — a bare id that matches no entry is left as-is (see PR notes on the
|
|
213
|
+
// separate stale-cross-CMS-id repair still needed for already-broken refs).
|
|
214
|
+
function rewrite_symbolic_upload_refs(value, map) {
|
|
215
|
+
const applied = new Set();
|
|
216
|
+
const walk = (v) => {
|
|
217
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
218
|
+
const obj = v;
|
|
219
|
+
const result = {};
|
|
220
|
+
for (const [k, val] of Object.entries(obj)) {
|
|
221
|
+
if (k === 'upload' && typeof val === 'string' && val.startsWith('uploads/')) {
|
|
222
|
+
const symbolic = val.substring('uploads/'.length);
|
|
223
|
+
const id = map.get(symbolic);
|
|
224
|
+
if (id) {
|
|
225
|
+
result[k] = id;
|
|
226
|
+
applied.add(symbolic);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
result[k] = walk(val);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(v))
|
|
235
|
+
return v.map(walk);
|
|
236
|
+
return v;
|
|
237
|
+
};
|
|
238
|
+
const rewritten = walk(value);
|
|
239
|
+
return { value: rewritten, applied };
|
|
240
|
+
}
|
|
241
|
+
// Subdirs whose yaml may carry `upload: uploads/...` refs. Mirrors the server's
|
|
242
|
+
// import scope (uploads/ itself holds binaries, not refs, so it's excluded).
|
|
243
|
+
const UPLOAD_REF_DIRS = ['blocks', 'page-types', 'pages', 'site'];
|
|
42
244
|
// Returns the labels (site slugs / 'library') that failed to push so callers
|
|
43
245
|
// like `primo deploy` can tell a clean run from a partial one. Empty = success.
|
|
44
246
|
export async function push_site(options) {
|
|
@@ -291,6 +493,9 @@ async function push_single_site(site_dir, options, spinner) {
|
|
|
291
493
|
console.log('');
|
|
292
494
|
console.log(chalk.dim(' Site created on server and content uploaded.'));
|
|
293
495
|
console.log(chalk.dim(' Run `primo login` and re-push to update content later.'));
|
|
496
|
+
// Converge local upload refs/filenames with the ids the server minted,
|
|
497
|
+
// same as the other push paths.
|
|
498
|
+
await apply_upload_writeback(site_dir, await root_dir_for(site_dir), bootstrap_result.created_ids);
|
|
294
499
|
return;
|
|
295
500
|
}
|
|
296
501
|
throw new Error(bootstrap_result.error);
|
|
@@ -321,6 +526,9 @@ async function push_single_site(site_dir, options, spinner) {
|
|
|
321
526
|
console.log('');
|
|
322
527
|
console.log(chalk.dim(' Site created on server and content uploaded.'));
|
|
323
528
|
console.log(chalk.dim(' Subsequent pushes will use the import endpoint.'));
|
|
529
|
+
// Converge local upload refs/filenames with the ids the server minted
|
|
530
|
+
// (see the import path below for why).
|
|
531
|
+
await apply_upload_writeback(site_dir, await root_dir_for(site_dir), bootstrap_result.created_ids);
|
|
324
532
|
return;
|
|
325
533
|
}
|
|
326
534
|
throw new Error(bootstrap_result.error);
|
|
@@ -341,6 +549,17 @@ async function push_single_site(site_dir, options, spinner) {
|
|
|
341
549
|
spinner.succeed(`Pushed ${label}`);
|
|
342
550
|
console.log('');
|
|
343
551
|
print_diff(result.diff);
|
|
552
|
+
// The server mints its own record id for each uploaded file and returns
|
|
553
|
+
// the symbolic->id map in created_ids. Converge the local copy so refs
|
|
554
|
+
// point at the server's id and files carry their canonical names — the
|
|
555
|
+
// same writeback `primo dev` does. Without this, local and server drift,
|
|
556
|
+
// and a symbolic ref baked to a *different* CMS's id dangles forever.
|
|
557
|
+
await apply_upload_writeback(site_dir, await root_dir_for(site_dir), result.created_ids);
|
|
558
|
+
// NOTE: push intentionally does NOT republish the served site. It syncs
|
|
559
|
+
// content into the CMS; regenerating the published output stays a
|
|
560
|
+
// separate, deliberate step (the editor's Publish action / the
|
|
561
|
+
// /api/primo/generate endpoint), so pushing content and choosing when it
|
|
562
|
+
// goes live remain decoupled.
|
|
344
563
|
}
|
|
345
564
|
}
|
|
346
565
|
async function try_bootstrap_site(server, token, zip_buffer, config, site_id, group_name) {
|
|
@@ -367,8 +586,10 @@ async function try_bootstrap_site(server, token, zip_buffer, config, site_id, gr
|
|
|
367
586
|
headers,
|
|
368
587
|
body: form
|
|
369
588
|
});
|
|
370
|
-
if (response.ok)
|
|
371
|
-
|
|
589
|
+
if (response.ok) {
|
|
590
|
+
const body = await response.json().catch(() => ({}));
|
|
591
|
+
return { ok: true, created_ids: body.created_ids };
|
|
592
|
+
}
|
|
372
593
|
if (response.status === 403) {
|
|
373
594
|
return {
|
|
374
595
|
ok: false,
|