primo-cli 0.1.3 → 0.1.5
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/README.md +111 -39
- package/dist/commands/build.js +488 -272
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +293 -141
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +2007 -150
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +65 -43
- package/dist/commands/login.d.ts +1 -2
- package/dist/commands/login.js +24 -6
- package/dist/commands/new.js +161 -274
- package/dist/commands/pull-library.d.ts +7 -0
- package/dist/commands/pull-library.js +92 -0
- package/dist/commands/pull.d.ts +0 -1
- package/dist/commands/pull.js +160 -165
- package/dist/commands/push-library.d.ts +7 -0
- package/dist/commands/push-library.js +88 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +358 -51
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +379 -161
- package/dist/index.js +110 -20
- package/dist/utils/binary.js +1 -1
- package/dist/utils/format.d.ts +12 -0
- package/dist/utils/format.js +98 -0
- package/dist/utils/head-svelte.d.ts +2 -0
- package/dist/utils/head-svelte.js +53 -0
- package/dist/utils/server-config.d.ts +19 -0
- package/dist/utils/server-config.js +49 -0
- package/dist/utils/site-config.d.ts +11 -0
- package/dist/utils/site-config.js +14 -0
- package/package.json +8 -4
- package/dist/commands/export.d.ts +0 -8
- package/dist/commands/export.js +0 -163
- package/dist/commands/import.d.ts +0 -9
- package/dist/commands/import.js +0 -118
- package/dist/commands/publish.d.ts +0 -6
- package/dist/commands/publish.js +0 -239
package/dist/commands/new.js
CHANGED
|
@@ -4,15 +4,43 @@ import chalk from 'chalk';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { dev_server } from './dev.js';
|
|
7
|
+
import { SITE_CONFIG_FILE, write_site_config } from '../utils/site-config.js';
|
|
8
|
+
import { SERVER_CONFIG_FILE, read_server_config, write_server_config } from '../utils/server-config.js';
|
|
7
9
|
export async function new_site(options) {
|
|
8
10
|
const base_dir = process.cwd();
|
|
9
|
-
const server_config_path = path.join(base_dir,
|
|
10
|
-
|
|
11
|
+
const server_config_path = path.join(base_dir, SERVER_CONFIG_FILE);
|
|
12
|
+
const site_config_path = path.join(base_dir, SITE_CONFIG_FILE);
|
|
13
|
+
const sites_dir = path.join(base_dir, 'sites');
|
|
14
|
+
const library_dir = path.join(base_dir, 'library');
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(site_config_path);
|
|
17
|
+
console.log(chalk.red(`Found ${SITE_CONFIG_FILE} in the current directory.`));
|
|
18
|
+
console.log(chalk.red('Run `primo new` from a workspace root, not inside a site directory.'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Not inside a site directory, continue.
|
|
23
|
+
}
|
|
24
|
+
// Require an initialized workspace
|
|
11
25
|
try {
|
|
12
26
|
await fs.access(server_config_path);
|
|
13
27
|
}
|
|
14
28
|
catch {
|
|
15
|
-
|
|
29
|
+
console.log(chalk.red(`No ${SERVER_CONFIG_FILE} found in the current directory.`));
|
|
30
|
+
console.log(chalk.dim('Run `primo init [name]` first to create a workspace.'));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
await fs.mkdir(sites_dir, { recursive: true });
|
|
34
|
+
await fs.mkdir(library_dir, { recursive: true });
|
|
35
|
+
const server_config = await read_server_config(base_dir);
|
|
36
|
+
const site_groups = server_config.site_groups ?? [];
|
|
37
|
+
if (!site_groups.some((group) => group.id === 'default')) {
|
|
38
|
+
site_groups.push({
|
|
39
|
+
id: 'default',
|
|
40
|
+
name: 'Default',
|
|
41
|
+
index: site_groups.length
|
|
42
|
+
});
|
|
43
|
+
await write_server_config(base_dir, { ...server_config, site_groups });
|
|
16
44
|
}
|
|
17
45
|
let site_name = options.name;
|
|
18
46
|
// Prompt for name if not provided
|
|
@@ -32,8 +60,8 @@ export async function new_site(options) {
|
|
|
32
60
|
}]);
|
|
33
61
|
site_name = name;
|
|
34
62
|
}
|
|
35
|
-
// Always create site in
|
|
36
|
-
const site_dir = path.join(
|
|
63
|
+
// Always create site in sites/<name>
|
|
64
|
+
const site_dir = path.join(sites_dir, site_name);
|
|
37
65
|
// Check if directory exists
|
|
38
66
|
try {
|
|
39
67
|
await fs.access(site_dir);
|
|
@@ -51,8 +79,7 @@ export async function new_site(options) {
|
|
|
51
79
|
await fs.mkdir(path.join(site_dir, 'pages'), { recursive: true });
|
|
52
80
|
await fs.mkdir(path.join(site_dir, 'page-types', 'default'), { recursive: true });
|
|
53
81
|
await fs.mkdir(path.join(site_dir, 'site'), { recursive: true });
|
|
54
|
-
|
|
55
|
-
// Create primo.json
|
|
82
|
+
// Create site config
|
|
56
83
|
// If name has dots (hostname), use first part capitalized as display name
|
|
57
84
|
const display_name = site_name.includes('.')
|
|
58
85
|
? site_name.split('.')[0].charAt(0).toUpperCase() + site_name.split('.')[0].slice(1)
|
|
@@ -62,20 +89,36 @@ export async function new_site(options) {
|
|
|
62
89
|
site_id: generate_id(),
|
|
63
90
|
// Leave host empty for local dev - dev.ts will generate coffee-shop.localhost:3000
|
|
64
91
|
// Only set host if it looks like a real domain (has a dot)
|
|
65
|
-
host: site_name.includes('.') ? site_name : ''
|
|
92
|
+
host: site_name.includes('.') ? site_name : '',
|
|
93
|
+
group: 'default'
|
|
66
94
|
};
|
|
67
|
-
await
|
|
68
|
-
// Create default page type config
|
|
95
|
+
await write_site_config(site_dir, config);
|
|
96
|
+
// Create default page type config + sibling fields.yaml
|
|
69
97
|
const page_type_config = {
|
|
70
|
-
|
|
98
|
+
_id: generate_id(),
|
|
71
99
|
name: 'Default',
|
|
72
100
|
icon: 'mdi:file-document-outline',
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
color: '#2B407D',
|
|
102
|
+
allowed_blocks: ['hero']
|
|
75
103
|
};
|
|
76
|
-
await fs.writeFile(path.join(site_dir, 'page-types', 'default', 'config.
|
|
104
|
+
await fs.writeFile(path.join(site_dir, 'page-types', 'default', 'config.yaml'), `_id: ${page_type_config._id}
|
|
105
|
+
name: ${page_type_config.name}
|
|
106
|
+
icon: ${page_type_config.icon}
|
|
107
|
+
color: "${page_type_config.color}"
|
|
108
|
+
allowed_blocks:
|
|
109
|
+
- hero
|
|
110
|
+
`);
|
|
111
|
+
await fs.writeFile(path.join(site_dir, 'page-types', 'default', 'fields.yaml'), '[]\n');
|
|
112
|
+
await fs.writeFile(path.join(site_dir, 'page-types', 'default', 'layout.yaml'), `# Sections shared by every page of this type. Add blocks here to render
|
|
113
|
+
# the same header/footer across all pages of this type.
|
|
114
|
+
#
|
|
115
|
+
# header:
|
|
116
|
+
# - block: site-header
|
|
117
|
+
# footer:
|
|
118
|
+
# - block: site-footer
|
|
119
|
+
`);
|
|
77
120
|
// Create site fields (empty array)
|
|
78
|
-
await fs.writeFile(path.join(site_dir, 'site', 'fields.
|
|
121
|
+
await fs.writeFile(path.join(site_dir, 'site', 'fields.yaml'), '[]\n');
|
|
79
122
|
// Create site content (empty)
|
|
80
123
|
await fs.writeFile(path.join(site_dir, 'site', 'content.yaml'), '# Site-wide content\n');
|
|
81
124
|
// Create site head with CSS variables
|
|
@@ -94,18 +137,24 @@ export async function new_site(options) {
|
|
|
94
137
|
}
|
|
95
138
|
</style>
|
|
96
139
|
`);
|
|
97
|
-
// Create starter hero block
|
|
140
|
+
// Create starter hero block (4 files: config + fields + content + component)
|
|
98
141
|
await fs.mkdir(path.join(site_dir, 'blocks', 'hero'), { recursive: true });
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
await fs.writeFile(path.join(site_dir, 'blocks', 'hero', 'config.yaml'), `_id: ${generate_id()}
|
|
143
|
+
name: Hero
|
|
144
|
+
`);
|
|
145
|
+
await fs.writeFile(path.join(site_dir, 'blocks', 'hero', 'fields.yaml'), `- _id: ${generate_id()}
|
|
146
|
+
name: headline
|
|
147
|
+
label: Headline
|
|
148
|
+
type: text
|
|
149
|
+
- _id: ${generate_id()}
|
|
150
|
+
name: subheadline
|
|
151
|
+
label: Subheadline
|
|
152
|
+
type: text
|
|
153
|
+
- _id: ${generate_id()}
|
|
154
|
+
name: cta
|
|
155
|
+
label: Call to Action
|
|
156
|
+
type: link
|
|
157
|
+
`);
|
|
109
158
|
await fs.writeFile(path.join(site_dir, 'blocks', 'hero', 'component.svelte'), `<section class="hero">
|
|
110
159
|
<div class="container">
|
|
111
160
|
<h1>{headline}</h1>
|
|
@@ -174,13 +223,14 @@ cta:
|
|
|
174
223
|
`);
|
|
175
224
|
// Create index page
|
|
176
225
|
const page_id = generate_id();
|
|
177
|
-
|
|
226
|
+
const section_id = generate_id();
|
|
227
|
+
await fs.writeFile(path.join(site_dir, 'pages', 'index.yaml'), `_id: ${page_id}
|
|
178
228
|
name: Home
|
|
179
|
-
slug: index
|
|
180
229
|
page_type: default
|
|
181
230
|
fields: {}
|
|
182
231
|
sections:
|
|
183
|
-
-
|
|
232
|
+
- _id: ${section_id}
|
|
233
|
+
block: hero
|
|
184
234
|
content:
|
|
185
235
|
headline: Welcome to ${display_name}
|
|
186
236
|
subheadline: Edit this content in your local files or CMS
|
|
@@ -188,8 +238,16 @@ sections:
|
|
|
188
238
|
label: Get Started
|
|
189
239
|
url: "#"
|
|
190
240
|
`);
|
|
191
|
-
// Create
|
|
192
|
-
|
|
241
|
+
// Create workspace-level AGENTS.md if it doesn't already exist.
|
|
242
|
+
// Per-site AGENTS.md is generated by `export.go` for portable exports;
|
|
243
|
+
// in a workspace, one root-level file covers all sites.
|
|
244
|
+
const workspace_agents_path = path.join(base_dir, 'AGENTS.md');
|
|
245
|
+
try {
|
|
246
|
+
await fs.access(workspace_agents_path);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
await fs.writeFile(workspace_agents_path, generate_agent_md());
|
|
250
|
+
}
|
|
193
251
|
spinner.succeed(`Site created: ${chalk.cyan(site_dir)}`);
|
|
194
252
|
// Check if server is already running
|
|
195
253
|
const port = 3000;
|
|
@@ -245,263 +303,92 @@ async function is_server_running(port) {
|
|
|
245
303
|
return false;
|
|
246
304
|
}
|
|
247
305
|
}
|
|
248
|
-
function generate_agent_md(
|
|
249
|
-
return `#
|
|
250
|
-
|
|
251
|
-
Pala site for local development.
|
|
252
|
-
|
|
253
|
-
## Structure
|
|
254
|
-
|
|
255
|
-
\`\`\`
|
|
256
|
-
blocks/ # Svelte components with content fields
|
|
257
|
-
{name}/
|
|
258
|
-
component.svelte
|
|
259
|
-
fields.json
|
|
260
|
-
content.yaml # Default field values (optional)
|
|
261
|
-
page-types/ # Page templates
|
|
262
|
-
{name}/
|
|
263
|
-
config.json
|
|
264
|
-
pages/ # Page content (YAML)
|
|
265
|
-
index.yaml # Homepage
|
|
266
|
-
contact.yaml # Leaf page (/contact)
|
|
267
|
-
about/ # Section with children
|
|
268
|
-
index.yaml # /about
|
|
269
|
-
team.yaml # /about/team
|
|
270
|
-
site/ # Site-wide settings
|
|
271
|
-
fields.json
|
|
272
|
-
content.yaml
|
|
273
|
-
head.svelte # Injected into <head>
|
|
274
|
-
.primo/ # Internal metadata
|
|
275
|
-
\`\`\`
|
|
276
|
-
|
|
277
|
-
## Creating Blocks
|
|
278
|
-
|
|
279
|
-
Each block needs two files:
|
|
280
|
-
|
|
281
|
-
**component.svelte** - Svelte 5 component:
|
|
282
|
-
\`\`\`svelte
|
|
283
|
-
<h1>{headline}</h1>
|
|
284
|
-
{#if image?.url}
|
|
285
|
-
<img src={image.url} alt={image.alt} />
|
|
286
|
-
{/if}
|
|
306
|
+
function generate_agent_md() {
|
|
307
|
+
return `# Primo workspace
|
|
287
308
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
"fields": [
|
|
300
|
-
{ "name": "headline", "label": "Headline", "type": "text" },
|
|
301
|
-
{ "name": "image", "label": "Image", "type": "image" }
|
|
302
|
-
]
|
|
303
|
-
}
|
|
304
|
-
\`\`\`
|
|
305
|
-
|
|
306
|
-
## Field Types
|
|
307
|
-
|
|
308
|
-
### text
|
|
309
|
-
Single-line text input.
|
|
310
|
-
\`\`\`svelte
|
|
311
|
-
<h1>{headline}</h1>
|
|
312
|
-
\`\`\`
|
|
313
|
-
|
|
314
|
-
### rich-text
|
|
315
|
-
WYSIWYG editor. Outputs HTML.
|
|
316
|
-
\`\`\`svelte
|
|
317
|
-
{@html content}
|
|
318
|
-
\`\`\`
|
|
319
|
-
|
|
320
|
-
### markdown
|
|
321
|
-
Markdown editor. Outputs HTML.
|
|
322
|
-
\`\`\`svelte
|
|
323
|
-
{@html body}
|
|
324
|
-
\`\`\`
|
|
325
|
-
|
|
326
|
-
### image
|
|
327
|
-
Image upload. Returns \`{ url, alt, width, height }\`.
|
|
328
|
-
\`\`\`svelte
|
|
329
|
-
{#if image?.url}
|
|
330
|
-
<img src={image.url} alt={image.alt} />
|
|
331
|
-
{/if}
|
|
332
|
-
\`\`\`
|
|
333
|
-
|
|
334
|
-
### link
|
|
335
|
-
URL with label. Returns \`{ url, label }\`.
|
|
336
|
-
\`\`\`svelte
|
|
337
|
-
{#if cta?.url}
|
|
338
|
-
<a href={cta.url}>{cta.label}</a>
|
|
339
|
-
{/if}
|
|
340
|
-
\`\`\`
|
|
341
|
-
|
|
342
|
-
### url
|
|
343
|
-
Plain URL string.
|
|
344
|
-
\`\`\`svelte
|
|
345
|
-
<a href={website_url}>Visit</a>
|
|
346
|
-
\`\`\`
|
|
347
|
-
|
|
348
|
-
### icon
|
|
349
|
-
Icon picker. Returns SVG string.
|
|
350
|
-
\`\`\`svelte
|
|
351
|
-
{@html icon}
|
|
352
|
-
\`\`\`
|
|
353
|
-
|
|
354
|
-
### number
|
|
355
|
-
Numeric input.
|
|
356
|
-
\`\`\`json
|
|
357
|
-
{ "name": "columns", "type": "number", "options": { "min": 1, "max": 6 } }
|
|
358
|
-
\`\`\`
|
|
359
|
-
|
|
360
|
-
### switch
|
|
361
|
-
Boolean toggle.
|
|
362
|
-
\`\`\`svelte
|
|
363
|
-
{#if show_title}<h1>{title}</h1>{/if}
|
|
364
|
-
\`\`\`
|
|
365
|
-
|
|
366
|
-
### select
|
|
367
|
-
Dropdown selection.
|
|
368
|
-
\`\`\`json
|
|
369
|
-
{ "name": "align", "type": "select", "options": { "choices": ["left", "center", "right"] } }
|
|
370
|
-
\`\`\`
|
|
371
|
-
\`\`\`svelte
|
|
372
|
-
<div class="text-{align}">{content}</div>
|
|
373
|
-
\`\`\`
|
|
374
|
-
|
|
375
|
-
### repeater
|
|
376
|
-
List of items with nested fields.
|
|
377
|
-
\`\`\`json
|
|
378
|
-
{
|
|
379
|
-
"name": "features",
|
|
380
|
-
"type": "repeater",
|
|
381
|
-
"options": {
|
|
382
|
-
"fields": [
|
|
383
|
-
{ "name": "title", "type": "text" },
|
|
384
|
-
{ "name": "description", "type": "text" }
|
|
385
|
-
]
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
\`\`\`
|
|
389
|
-
\`\`\`svelte
|
|
390
|
-
{#each features as feature}
|
|
391
|
-
<div>
|
|
392
|
-
<h3>{feature.title}</h3>
|
|
393
|
-
<p>{feature.description}</p>
|
|
394
|
-
</div>
|
|
395
|
-
{/each}
|
|
396
|
-
\`\`\`
|
|
397
|
-
|
|
398
|
-
### group
|
|
399
|
-
Nested object of fields.
|
|
400
|
-
\`\`\`json
|
|
401
|
-
{
|
|
402
|
-
"name": "author",
|
|
403
|
-
"type": "group",
|
|
404
|
-
"options": {
|
|
405
|
-
"fields": [
|
|
406
|
-
{ "name": "name", "type": "text" },
|
|
407
|
-
{ "name": "avatar", "type": "image" }
|
|
408
|
-
]
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
\`\`\`
|
|
412
|
-
\`\`\`svelte
|
|
413
|
-
<div>{author.name}</div>
|
|
414
|
-
{#if author.avatar?.url}<img src={author.avatar.url} />{/if}
|
|
415
|
-
\`\`\`
|
|
416
|
-
|
|
417
|
-
### page
|
|
418
|
-
Reference to another page. Returns page data with \`_meta.url\`.
|
|
419
|
-
\`\`\`json
|
|
420
|
-
{ "name": "featured_post", "type": "page", "options": { "page_type": "blog-post" } }
|
|
421
|
-
\`\`\`
|
|
422
|
-
|
|
423
|
-
### page-list
|
|
424
|
-
All pages of a type.
|
|
425
|
-
\`\`\`json
|
|
426
|
-
{ "name": "posts", "type": "page-list", "options": { "page_type": "blog-post" } }
|
|
427
|
-
\`\`\`
|
|
428
|
-
|
|
429
|
-
### page-field
|
|
430
|
-
Reference a field from the current page type.
|
|
431
|
-
|
|
432
|
-
### site-field
|
|
433
|
-
Reference a site-wide field.
|
|
434
|
-
|
|
435
|
-
### slider
|
|
436
|
-
Range slider for numeric values.
|
|
437
|
-
\`\`\`json
|
|
438
|
-
{ "name": "opacity", "type": "slider", "options": { "min": 0, "max": 100, "step": 10 } }
|
|
439
|
-
\`\`\`
|
|
440
|
-
|
|
441
|
-
### date
|
|
442
|
-
Date picker.
|
|
443
|
-
|
|
444
|
-
### info
|
|
445
|
-
Display-only text for editors (not rendered in component).
|
|
446
|
-
|
|
447
|
-
## Svelte 5 Syntax
|
|
448
|
-
|
|
449
|
-
Components use Svelte 5:
|
|
450
|
-
- \`$state()\` for reactive variables
|
|
451
|
-
- \`$derived()\` for computed values
|
|
452
|
-
- \`$effect()\` for side effects
|
|
453
|
-
- \`onclick={handler}\` not \`on:click={handler}\`
|
|
454
|
-
|
|
455
|
-
## Editor Context
|
|
456
|
-
|
|
457
|
-
Check if component is running in the CMS editor:
|
|
458
|
-
\`\`\`svelte
|
|
459
|
-
let is_editor = $state(false)
|
|
460
|
-
|
|
461
|
-
if (typeof window !== 'undefined') {
|
|
462
|
-
is_editor = window.__PALA_CONTEXT__?.environment === 'editor'
|
|
463
|
-
}
|
|
464
|
-
\`\`\`
|
|
309
|
+
Primo workspace for local development. Each subdirectory under \`sites/\` is an independent Primo site.
|
|
310
|
+
|
|
311
|
+
The Primo MCP server (\`primo\`) is the source of truth, when present, for schema, validation, field types, and inline editing. Call \`list_docs\` first to see what's documented.
|
|
312
|
+
|
|
313
|
+
Without the MCP server, read \`sites/*/blocks/*/fields.yaml\` and \`sites/*/page-types/*/fields.yaml\` to infer schemas, and treat \`sites/*/pages/*.yaml\` page/section \`content:\` as the source of truth for rendered content (block \`content.yaml\` files are defaults only).
|
|
314
|
+
|
|
315
|
+
## Layout
|
|
316
|
+
|
|
317
|
+
- \`server.yaml\` — workspace config and site groups
|
|
318
|
+
- \`sites/<name>/\` — individual sites (\`site.yaml\`, \`site/\`, \`blocks/\`, \`page-types/\`, \`pages/\`, \`.primo/\`)
|
|
319
|
+
- \`library/\` — shared library content
|
|
465
320
|
|
|
466
|
-
|
|
467
|
-
- Disabling fixed/sticky positioning
|
|
468
|
-
- Skipping scroll/resize listeners
|
|
469
|
-
- Showing placeholder content
|
|
321
|
+
## Setup
|
|
470
322
|
|
|
471
|
-
|
|
323
|
+
- \`primo dev\` — start the local CMS and dev server. Run from the workspace root.
|
|
324
|
+
- \`primo new [name]\` — scaffold a new site under \`sites/\`.
|
|
325
|
+
- File edits sync automatically while \`primo dev\` is running. Structural changes (block schema, component) may trigger a browser reload.
|
|
472
326
|
|
|
473
|
-
|
|
327
|
+
## Source of truth
|
|
474
328
|
|
|
475
|
-
- \`
|
|
329
|
+
- Editable source lives in \`sites/<name>/site.yaml\`, \`site/\`, \`blocks/\`, \`page-types/\`, and \`pages/\`.
|
|
330
|
+
- \`.primo/\` is generated local state (SQLite + uploads). Do not edit, do not commit.
|
|
331
|
+
- Do not patch the SQLite DB to fix schema or content issues. Edit the source files; the dev server reimports.
|
|
332
|
+
- If local state seems wrong, delete \`.primo/\` and let the dev server reimport from files.
|
|
476
333
|
|
|
477
|
-
|
|
334
|
+
## Recovering overwritten files
|
|
478
335
|
|
|
479
|
-
|
|
336
|
+
When \`primo dev\` syncs CMS changes to disk, the prior file content is copied to \`.primo/trash/\` before the overwrite or delete. Entries are kept for 7 days.
|
|
480
337
|
|
|
481
|
-
|
|
338
|
+
If a file appears to have lost content after a sync (deleted entries, shrunken YAML lists, missing sections, removed files), check \`.primo/trash/\` for the most recent copy and restore with \`cp\`. The dev server also annotates the change log when a synced file shrinks or is deleted.
|
|
482
339
|
|
|
483
|
-
|
|
340
|
+
## IDs
|
|
484
341
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
{/if}
|
|
342
|
+
- Top-level entities use system-owned \`_id\` keys (pages, sections, blocks, page types, fields).
|
|
343
|
+
- For blocks, \`_id\` lives in \`blocks/<key>/config.yaml\`. For page types, in \`page-types/<key>/config.yaml\`. The folder name is the stable reference key — editing \`name\` in \`config.yaml\` only changes the editor display label.
|
|
344
|
+
- When creating a new entity, omit the ID. The dev server generates and writes it back on first sync.
|
|
345
|
+
- Do not invent or hand-author IDs. Keep existing IDs stable when editing.
|
|
346
|
+
- Duplicate IDs are treated as conflicts; affected files may be skipped.
|
|
491
347
|
|
|
492
|
-
|
|
493
|
-
<a href={cta?.url || '#'}>{cta?.label || 'Learn More'}</a>
|
|
348
|
+
## Routing
|
|
494
349
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
{/each}
|
|
499
|
-
\`\`\`
|
|
350
|
+
- Page slugs come from the file path under \`pages/\`, not a \`slug:\` key.
|
|
351
|
+
- \`pages/index.yaml\` → \`/\`, \`pages/about.yaml\` → \`/about\`, \`pages/about/team.yaml\` → \`/about/team\`.
|
|
352
|
+
- Do not add \`slug:\` to page files. It is ignored.
|
|
500
353
|
|
|
501
354
|
## Workflow
|
|
502
355
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
356
|
+
- When building out a new site, call \`get_docs('recommended-defaults')\` first for baseline fields and the wiring checklist.
|
|
357
|
+
- After editing a block file, call \`validate_block\`.
|
|
358
|
+
- After editing a page or page-type file, call \`validate_page\`.
|
|
359
|
+
- When creating a new block or page type, prefer \`scaffold_block\` / \`scaffold_page_type\`.
|
|
360
|
+
- When you create a reusable block, add its folder name to the relevant page type's \`allowed_blocks\` — otherwise it won't appear in the editor sidebar.
|
|
361
|
+
- For everything else, call \`get_docs\` with the relevant section.
|
|
362
|
+
- Block components are Svelte 5. If the Svelte MCP server is available, use \`mcp__svelte__svelte-autofixer\` after editing \`.svelte\` files.
|
|
363
|
+
|
|
364
|
+
## Permission prompts
|
|
365
|
+
|
|
366
|
+
Your MCP client may prompt before each Primo tool call. All Primo MCP tools are read-only or return scaffolds — they don't modify your files. To skip the prompts, allowlist the \`primo\` server in your client's MCP settings (mechanism varies by client).
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
# Project preferences
|
|
371
|
+
|
|
372
|
+
Fill in the sections below to give agents project-specific direction. Anything left blank is treated as "no preference."
|
|
373
|
+
|
|
374
|
+
## Design direction
|
|
375
|
+
|
|
376
|
+
<!-- e.g. warm earth tones, generous whitespace, system fonts, minimal animation -->
|
|
377
|
+
|
|
378
|
+
## Voice & tone
|
|
379
|
+
|
|
380
|
+
<!-- e.g. friendly but not casual, second-person, short sentences -->
|
|
381
|
+
|
|
382
|
+
## SEO preferences
|
|
383
|
+
|
|
384
|
+
<!-- e.g. always mention city in seo_description, target ~150 chars, prefer action verbs in titles -->
|
|
385
|
+
|
|
386
|
+
## Content guidelines
|
|
387
|
+
|
|
388
|
+
<!-- e.g. real customer names ok, no stock photos, prefer specific numbers over vague claims -->
|
|
389
|
+
|
|
390
|
+
## Other
|
|
391
|
+
|
|
392
|
+
<!-- e.g. accessibility targets, performance budgets, browser support, anything else agents should know -->
|
|
506
393
|
`;
|
|
507
394
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import extract from 'extract-zip';
|
|
6
|
+
import { get_auth_token } from '../utils/auth.js';
|
|
7
|
+
async function detect_server() {
|
|
8
|
+
const ports = [3000, 8080, 5173];
|
|
9
|
+
for (const port of ports) {
|
|
10
|
+
try {
|
|
11
|
+
const url = `http://127.0.0.1:${port}`;
|
|
12
|
+
const response = await fetch(`${url}/api/health`, {
|
|
13
|
+
signal: AbortSignal.timeout(500)
|
|
14
|
+
});
|
|
15
|
+
if (response.ok) {
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Not running on this port
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export async function pull_library(options) {
|
|
26
|
+
const spinner = ora('Connecting...').start();
|
|
27
|
+
try {
|
|
28
|
+
let server;
|
|
29
|
+
if (options.server) {
|
|
30
|
+
server = options.server.replace(/\/+$/, '');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
spinner.text = 'Looking for local server...';
|
|
34
|
+
const detected = await detect_server();
|
|
35
|
+
server = (detected || 'http://localhost:3000').replace(/\/+$/, '');
|
|
36
|
+
spinner.text = `Using ${server}`;
|
|
37
|
+
}
|
|
38
|
+
const token = options.token || await get_auth_token(server);
|
|
39
|
+
const headers = {};
|
|
40
|
+
if (token) {
|
|
41
|
+
headers.Authorization = `Bearer ${token}`;
|
|
42
|
+
}
|
|
43
|
+
const output_dir = path.resolve(options.output);
|
|
44
|
+
await fs.mkdir(output_dir, { recursive: true });
|
|
45
|
+
spinner.text = 'Exporting library...';
|
|
46
|
+
const response = await fetch(`${server}/api/palacms/export-library`, {
|
|
47
|
+
headers
|
|
48
|
+
});
|
|
49
|
+
if (response.status === 404) {
|
|
50
|
+
spinner.fail('Shared library sync is not supported by this palacms server. Update the server before using `primo library pull`.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const error = await response.text();
|
|
55
|
+
spinner.fail(`Export failed: ${error}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const zip_data = await response.arrayBuffer();
|
|
59
|
+
const temp_zip = path.join(output_dir, '.primo-library-export.zip');
|
|
60
|
+
await fs.writeFile(temp_zip, Buffer.from(zip_data));
|
|
61
|
+
spinner.text = 'Extracting library...';
|
|
62
|
+
await extract(temp_zip, { dir: output_dir });
|
|
63
|
+
await fs.unlink(temp_zip);
|
|
64
|
+
const summary = await count_library(path.join(output_dir, 'library'));
|
|
65
|
+
spinner.succeed(`Library exported to ${chalk.cyan(path.join(output_dir, 'library'))}`);
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(chalk.dim(' Library exported:'));
|
|
68
|
+
console.log(chalk.dim(` groups/ ${summary.groups}`));
|
|
69
|
+
console.log(chalk.dim(` blocks/ ${summary.blocks}`));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
spinner.fail(`Export failed: ${error instanceof Error ? error.message : error}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function count_library(library_dir) {
|
|
77
|
+
const counts = { groups: 0, blocks: 0 };
|
|
78
|
+
try {
|
|
79
|
+
const entries = await fs.readdir(library_dir, { withFileTypes: true });
|
|
80
|
+
const groups = entries.filter((entry) => entry.isDirectory());
|
|
81
|
+
counts.groups = groups.length;
|
|
82
|
+
for (const group of groups) {
|
|
83
|
+
const group_dir = path.join(library_dir, group.name);
|
|
84
|
+
const block_entries = await fs.readdir(group_dir, { withFileTypes: true });
|
|
85
|
+
counts.blocks += block_entries.filter((entry) => entry.isDirectory()).length;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// No library directory exported
|
|
90
|
+
}
|
|
91
|
+
return counts;
|
|
92
|
+
}
|