primo-cli 0.1.0
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 +183 -0
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.js +379 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.js +261 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.js +516 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +163 -0
- package/dist/commands/import.d.ts +9 -0
- package/dist/commands/import.js +118 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +68 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/new.d.ts +7 -0
- package/dist/commands/new.js +507 -0
- package/dist/commands/publish.d.ts +6 -0
- package/dist/commands/publish.js +239 -0
- package/dist/commands/pull.d.ts +8 -0
- package/dist/commands/pull.js +243 -0
- package/dist/commands/push.d.ts +9 -0
- package/dist/commands/push.js +118 -0
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.js +514 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +70 -0
- package/dist/utils/auth.d.ts +2 -0
- package/dist/utils/auth.js +29 -0
- package/dist/utils/binary.d.ts +5 -0
- package/dist/utils/binary.js +129 -0
- package/package.json +53 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
// Auto-fix common issues without full validation output
|
|
5
|
+
export async function normalize_site(site_dir) {
|
|
6
|
+
const pages_dir = path.join(site_dir, 'pages');
|
|
7
|
+
const index_path = path.join(pages_dir, 'index.yaml');
|
|
8
|
+
try {
|
|
9
|
+
let content = await fs.readFile(index_path, 'utf-8');
|
|
10
|
+
const slug_index_pattern = /^slug:\s*index\s*$/m;
|
|
11
|
+
if (slug_index_pattern.test(content)) {
|
|
12
|
+
content = content.replace(slug_index_pattern, "slug: ''");
|
|
13
|
+
await fs.writeFile(index_path, content, 'utf-8');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// File doesn't exist, skip
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const VALID_FIELD_TYPES = [
|
|
21
|
+
'text',
|
|
22
|
+
'rich-text',
|
|
23
|
+
'markdown',
|
|
24
|
+
'image',
|
|
25
|
+
'link',
|
|
26
|
+
'url',
|
|
27
|
+
'icon',
|
|
28
|
+
'number',
|
|
29
|
+
'switch',
|
|
30
|
+
'select',
|
|
31
|
+
'repeater',
|
|
32
|
+
'group',
|
|
33
|
+
'page',
|
|
34
|
+
'page-list',
|
|
35
|
+
'page-field',
|
|
36
|
+
'site-field',
|
|
37
|
+
'slider',
|
|
38
|
+
'date',
|
|
39
|
+
'info'
|
|
40
|
+
];
|
|
41
|
+
export async function validate_site(options) {
|
|
42
|
+
const errors = [];
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const site_dir = path.resolve(options.dir);
|
|
45
|
+
console.log(chalk.bold('\nš Validating site structure...\n'));
|
|
46
|
+
try {
|
|
47
|
+
// Check if directory exists
|
|
48
|
+
await fs.access(site_dir);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
console.log(chalk.red(`ā Directory not found: ${site_dir}`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Validate blocks
|
|
55
|
+
const blocks_errors = await validate_blocks(site_dir);
|
|
56
|
+
errors.push(...blocks_errors.filter(e => e.severity === 'error'));
|
|
57
|
+
warnings.push(...blocks_errors.filter(e => e.severity === 'warning'));
|
|
58
|
+
// Validate page types
|
|
59
|
+
const page_types_errors = await validate_page_types(site_dir);
|
|
60
|
+
errors.push(...page_types_errors.filter(e => e.severity === 'error'));
|
|
61
|
+
warnings.push(...page_types_errors.filter(e => e.severity === 'warning'));
|
|
62
|
+
// Validate site fields
|
|
63
|
+
const site_errors = await validate_site_fields(site_dir);
|
|
64
|
+
errors.push(...site_errors.filter(e => e.severity === 'error'));
|
|
65
|
+
warnings.push(...site_errors.filter(e => e.severity === 'warning'));
|
|
66
|
+
// Validate pages
|
|
67
|
+
const pages_errors = await validate_pages(site_dir);
|
|
68
|
+
errors.push(...pages_errors.filter(e => e.severity === 'error'));
|
|
69
|
+
warnings.push(...pages_errors.filter(e => e.severity === 'warning'));
|
|
70
|
+
// Print results
|
|
71
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
72
|
+
console.log(chalk.green('ā All validations passed!\n'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (warnings.length > 0) {
|
|
76
|
+
console.log(chalk.yellow(`ā ${warnings.length} warning${warnings.length > 1 ? 's' : ''}:\n`));
|
|
77
|
+
for (const warning of warnings) {
|
|
78
|
+
print_error(warning);
|
|
79
|
+
}
|
|
80
|
+
console.log('');
|
|
81
|
+
}
|
|
82
|
+
if (errors.length > 0) {
|
|
83
|
+
console.log(chalk.red(`ā ${errors.length} error${errors.length > 1 ? 's' : ''}:\n`));
|
|
84
|
+
for (const error of errors) {
|
|
85
|
+
print_error(error);
|
|
86
|
+
}
|
|
87
|
+
console.log('');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function validate_blocks(site_dir) {
|
|
92
|
+
const errors = [];
|
|
93
|
+
const blocks_dir = path.join(site_dir, 'blocks');
|
|
94
|
+
try {
|
|
95
|
+
await fs.access(blocks_dir);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [{
|
|
99
|
+
file: 'blocks/',
|
|
100
|
+
message: 'Blocks directory not found',
|
|
101
|
+
severity: 'error'
|
|
102
|
+
}];
|
|
103
|
+
}
|
|
104
|
+
const block_names = await fs.readdir(blocks_dir);
|
|
105
|
+
for (const block_name of block_names) {
|
|
106
|
+
if (block_name.startsWith('.'))
|
|
107
|
+
continue;
|
|
108
|
+
const block_dir = path.join(blocks_dir, block_name);
|
|
109
|
+
const stat = await fs.stat(block_dir);
|
|
110
|
+
if (!stat.isDirectory())
|
|
111
|
+
continue;
|
|
112
|
+
// Check for required files - read actual directory listing to handle case-insensitive filesystems
|
|
113
|
+
const fields_path = path.join(block_dir, 'fields.json');
|
|
114
|
+
const block_files = await fs.readdir(block_dir);
|
|
115
|
+
// Check for component.svelte with correct casing
|
|
116
|
+
const component_file = block_files.find(f => f.toLowerCase() === 'component.svelte');
|
|
117
|
+
if (!component_file) {
|
|
118
|
+
errors.push({
|
|
119
|
+
file: `blocks/${block_name}/`,
|
|
120
|
+
message: 'Missing component.svelte file',
|
|
121
|
+
severity: 'error'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else if (component_file !== 'component.svelte') {
|
|
125
|
+
errors.push({
|
|
126
|
+
file: `blocks/${block_name}/${component_file}`,
|
|
127
|
+
message: `Filename must be lowercase: "component.svelte", not "${component_file}". The import will silently skip this block.`,
|
|
128
|
+
severity: 'error'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const fields_data = await fs.readFile(fields_path, 'utf-8');
|
|
133
|
+
let fields_json;
|
|
134
|
+
try {
|
|
135
|
+
fields_json = JSON.parse(fields_data);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
errors.push({
|
|
139
|
+
file: `blocks/${block_name}/fields.json`,
|
|
140
|
+
message: 'Invalid JSON syntax',
|
|
141
|
+
severity: 'error'
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// Validate fields structure
|
|
146
|
+
const field_errors = validate_fields(fields_json, `blocks/${block_name}/fields.json`);
|
|
147
|
+
errors.push(...field_errors);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
errors.push({
|
|
151
|
+
file: `blocks/${block_name}/`,
|
|
152
|
+
message: 'Missing fields.json file',
|
|
153
|
+
severity: 'error'
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return errors;
|
|
158
|
+
}
|
|
159
|
+
async function validate_page_types(site_dir) {
|
|
160
|
+
const errors = [];
|
|
161
|
+
const page_types_dir = path.join(site_dir, 'page-types');
|
|
162
|
+
try {
|
|
163
|
+
await fs.access(page_types_dir);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return [{
|
|
167
|
+
file: 'page-types/',
|
|
168
|
+
message: 'Page types directory not found',
|
|
169
|
+
severity: 'warning'
|
|
170
|
+
}];
|
|
171
|
+
}
|
|
172
|
+
const page_type_names = await fs.readdir(page_types_dir);
|
|
173
|
+
for (const page_type_name of page_type_names) {
|
|
174
|
+
if (page_type_name.startsWith('.'))
|
|
175
|
+
continue;
|
|
176
|
+
const page_type_dir = path.join(page_types_dir, page_type_name);
|
|
177
|
+
const stat = await fs.stat(page_type_dir);
|
|
178
|
+
if (!stat.isDirectory())
|
|
179
|
+
continue;
|
|
180
|
+
const config_path = path.join(page_type_dir, 'config.json');
|
|
181
|
+
try {
|
|
182
|
+
const config_data = await fs.readFile(config_path, 'utf-8');
|
|
183
|
+
let config;
|
|
184
|
+
try {
|
|
185
|
+
config = JSON.parse(config_data);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
errors.push({
|
|
189
|
+
file: `page-types/${page_type_name}/config.json`,
|
|
190
|
+
message: 'Invalid JSON syntax',
|
|
191
|
+
severity: 'error'
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// Validate config has required fields
|
|
196
|
+
if (!config.name) {
|
|
197
|
+
errors.push({
|
|
198
|
+
file: `page-types/${page_type_name}/config.json`,
|
|
199
|
+
message: 'Missing "name" field',
|
|
200
|
+
severity: 'error'
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// Validate page type fields if they exist
|
|
204
|
+
if (config.fields) {
|
|
205
|
+
const field_errors = validate_fields({ fields: config.fields }, `page-types/${page_type_name}/config.json`);
|
|
206
|
+
errors.push(...field_errors);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
errors.push({
|
|
211
|
+
file: `page-types/${page_type_name}/`,
|
|
212
|
+
message: 'Missing config.json file',
|
|
213
|
+
severity: 'error'
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return errors;
|
|
218
|
+
}
|
|
219
|
+
async function validate_site_fields(site_dir) {
|
|
220
|
+
const errors = [];
|
|
221
|
+
const site_fields_path = path.join(site_dir, 'site/fields.json');
|
|
222
|
+
try {
|
|
223
|
+
const fields_data = await fs.readFile(site_fields_path, 'utf-8');
|
|
224
|
+
let fields_json;
|
|
225
|
+
try {
|
|
226
|
+
fields_json = JSON.parse(fields_data);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
errors.push({
|
|
230
|
+
file: 'site/fields.json',
|
|
231
|
+
message: 'Invalid JSON syntax',
|
|
232
|
+
severity: 'error'
|
|
233
|
+
});
|
|
234
|
+
return errors;
|
|
235
|
+
}
|
|
236
|
+
// site/fields.json must be a plain array, not wrapped in an object
|
|
237
|
+
if (!Array.isArray(fields_json)) {
|
|
238
|
+
errors.push({
|
|
239
|
+
file: 'site/fields.json',
|
|
240
|
+
message: 'Must be a plain array (e.g., []) not wrapped in an object. The import will fail.',
|
|
241
|
+
severity: 'error'
|
|
242
|
+
});
|
|
243
|
+
return errors;
|
|
244
|
+
}
|
|
245
|
+
// Validate as array of fields
|
|
246
|
+
if (fields_json.length > 0) {
|
|
247
|
+
const field_errors = validate_fields({ fields: fields_json }, 'site/fields.json');
|
|
248
|
+
errors.push(...field_errors);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
errors.push({
|
|
253
|
+
file: 'site/',
|
|
254
|
+
message: 'Missing fields.json file',
|
|
255
|
+
severity: 'warning'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return errors;
|
|
259
|
+
}
|
|
260
|
+
function validate_fields(fields_json, file_path) {
|
|
261
|
+
const errors = [];
|
|
262
|
+
if (!fields_json.fields || !Array.isArray(fields_json.fields)) {
|
|
263
|
+
errors.push({
|
|
264
|
+
file: file_path,
|
|
265
|
+
message: 'Missing or invalid "fields" array',
|
|
266
|
+
severity: 'error'
|
|
267
|
+
});
|
|
268
|
+
return errors;
|
|
269
|
+
}
|
|
270
|
+
const field_ids = new Set();
|
|
271
|
+
const field_names = new Set();
|
|
272
|
+
const parent_names = new Set();
|
|
273
|
+
// Collect all field names for parent validation
|
|
274
|
+
for (const field of fields_json.fields) {
|
|
275
|
+
if (field.name) {
|
|
276
|
+
field_names.add(field.name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const field of fields_json.fields) {
|
|
280
|
+
const field_name = field.name || field.id || '(unnamed)';
|
|
281
|
+
// Check required properties
|
|
282
|
+
if (!field.id) {
|
|
283
|
+
errors.push({
|
|
284
|
+
file: file_path,
|
|
285
|
+
field: field_name,
|
|
286
|
+
message: 'Missing required "id" field',
|
|
287
|
+
severity: 'error'
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Check for duplicate IDs
|
|
292
|
+
if (field_ids.has(field.id)) {
|
|
293
|
+
errors.push({
|
|
294
|
+
file: file_path,
|
|
295
|
+
field: field_name,
|
|
296
|
+
message: `Duplicate field ID: "${field.id}"`,
|
|
297
|
+
severity: 'error'
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
field_ids.add(field.id);
|
|
301
|
+
}
|
|
302
|
+
if (!field.name) {
|
|
303
|
+
errors.push({
|
|
304
|
+
file: file_path,
|
|
305
|
+
field: field_name,
|
|
306
|
+
message: 'Missing required "name" field',
|
|
307
|
+
severity: 'error'
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (!field.label && field.type !== 'info') {
|
|
311
|
+
errors.push({
|
|
312
|
+
file: file_path,
|
|
313
|
+
field: field_name,
|
|
314
|
+
message: 'Missing "label" field',
|
|
315
|
+
severity: 'warning'
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (!field.type) {
|
|
319
|
+
errors.push({
|
|
320
|
+
file: file_path,
|
|
321
|
+
field: field_name,
|
|
322
|
+
message: 'Missing required "type" field',
|
|
323
|
+
severity: 'error'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// Check valid field type
|
|
328
|
+
if (!VALID_FIELD_TYPES.includes(field.type)) {
|
|
329
|
+
errors.push({
|
|
330
|
+
file: file_path,
|
|
331
|
+
field: field_name,
|
|
332
|
+
message: `Invalid field type: "${field.type}". Valid types: ${VALID_FIELD_TYPES.join(', ')}`,
|
|
333
|
+
severity: 'error'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Type-specific validation
|
|
337
|
+
if (field.type === 'select') {
|
|
338
|
+
const select_errors = validate_select_field(field, field_name, file_path);
|
|
339
|
+
errors.push(...select_errors);
|
|
340
|
+
}
|
|
341
|
+
if (field.type === 'repeater' || field.type === 'group') {
|
|
342
|
+
parent_names.add(field.name);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Validate parent reference
|
|
346
|
+
if (field.parent) {
|
|
347
|
+
if (!field_names.has(field.parent)) {
|
|
348
|
+
errors.push({
|
|
349
|
+
file: file_path,
|
|
350
|
+
field: field_name,
|
|
351
|
+
message: `Parent field "${field.parent}" not found`,
|
|
352
|
+
severity: 'error'
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return errors;
|
|
358
|
+
}
|
|
359
|
+
function validate_select_field(field, field_name, file_path) {
|
|
360
|
+
const errors = [];
|
|
361
|
+
if (!field.options || !field.options.options) {
|
|
362
|
+
errors.push({
|
|
363
|
+
file: file_path,
|
|
364
|
+
field: field_name,
|
|
365
|
+
message: 'Select field missing "options.options" array',
|
|
366
|
+
severity: 'error'
|
|
367
|
+
});
|
|
368
|
+
return errors;
|
|
369
|
+
}
|
|
370
|
+
if (!Array.isArray(field.options.options)) {
|
|
371
|
+
errors.push({
|
|
372
|
+
file: file_path,
|
|
373
|
+
field: field_name,
|
|
374
|
+
message: 'Select field "options.options" must be an array',
|
|
375
|
+
severity: 'error'
|
|
376
|
+
});
|
|
377
|
+
return errors;
|
|
378
|
+
}
|
|
379
|
+
for (let i = 0; i < field.options.options.length; i++) {
|
|
380
|
+
const option = field.options.options[i];
|
|
381
|
+
if (!option.label) {
|
|
382
|
+
errors.push({
|
|
383
|
+
file: file_path,
|
|
384
|
+
field: field_name,
|
|
385
|
+
message: `Option ${i + 1} missing "label" property`,
|
|
386
|
+
severity: 'error'
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (!option.value) {
|
|
390
|
+
errors.push({
|
|
391
|
+
file: file_path,
|
|
392
|
+
field: field_name,
|
|
393
|
+
message: `Option ${i + 1} missing "value" property`,
|
|
394
|
+
severity: 'error'
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (!('icon' in option)) {
|
|
398
|
+
errors.push({
|
|
399
|
+
file: file_path,
|
|
400
|
+
field: field_name,
|
|
401
|
+
message: `Option ${i + 1} missing "icon" property (can be empty string)`,
|
|
402
|
+
severity: 'error'
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return errors;
|
|
407
|
+
}
|
|
408
|
+
async function validate_pages(site_dir) {
|
|
409
|
+
const errors = [];
|
|
410
|
+
const pages_dir = path.join(site_dir, 'pages');
|
|
411
|
+
try {
|
|
412
|
+
await fs.access(pages_dir);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return [{
|
|
416
|
+
file: 'pages/',
|
|
417
|
+
message: 'Pages directory not found',
|
|
418
|
+
severity: 'warning'
|
|
419
|
+
}];
|
|
420
|
+
}
|
|
421
|
+
// Recursively find all YAML files in pages directory
|
|
422
|
+
const yaml_files = await find_yaml_files(pages_dir, 'pages');
|
|
423
|
+
for (const yaml_file of yaml_files) {
|
|
424
|
+
const file_path = path.join(site_dir, yaml_file);
|
|
425
|
+
try {
|
|
426
|
+
let content = await fs.readFile(file_path, 'utf-8');
|
|
427
|
+
// Auto-fix: normalize homepage slug from "index" to ""
|
|
428
|
+
if (yaml_file === 'pages/index.yaml' || yaml_file === 'pages\\index.yaml') {
|
|
429
|
+
const slug_index_pattern = /^slug:\s*index\s*$/m;
|
|
430
|
+
if (slug_index_pattern.test(content)) {
|
|
431
|
+
content = content.replace(slug_index_pattern, "slug: ''");
|
|
432
|
+
await fs.writeFile(file_path, content, 'utf-8');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Check for common mistakes
|
|
436
|
+
if (content.includes('\nblocks:') || content.match(/^blocks:/m)) {
|
|
437
|
+
errors.push({
|
|
438
|
+
file: yaml_file,
|
|
439
|
+
message: 'Page uses "blocks:" but should use "sections:" with nested "content" objects. The import will fail.',
|
|
440
|
+
severity: 'error'
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Check that sections have the correct structure
|
|
444
|
+
if (content.includes('\nsections:') || content.match(/^sections:/m)) {
|
|
445
|
+
// Basic check: sections should have "block:" and "content:" keys
|
|
446
|
+
const lines = content.split('\n');
|
|
447
|
+
let in_sections = false;
|
|
448
|
+
let found_block = false;
|
|
449
|
+
let found_content = false;
|
|
450
|
+
for (const line of lines) {
|
|
451
|
+
if (line.match(/^sections:/)) {
|
|
452
|
+
in_sections = true;
|
|
453
|
+
}
|
|
454
|
+
else if (in_sections && line.match(/^\s+- block:/)) {
|
|
455
|
+
found_block = true;
|
|
456
|
+
}
|
|
457
|
+
else if (in_sections && line.match(/^\s+content:/)) {
|
|
458
|
+
found_content = true;
|
|
459
|
+
}
|
|
460
|
+
else if (in_sections && line.match(/^[a-z]/)) {
|
|
461
|
+
// New top-level key, end of sections
|
|
462
|
+
in_sections = false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (found_block && !found_content) {
|
|
466
|
+
errors.push({
|
|
467
|
+
file: yaml_file,
|
|
468
|
+
message: 'Sections missing "content:" key. Each section should have "block:" and "content:" keys.',
|
|
469
|
+
severity: 'warning'
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
errors.push({
|
|
476
|
+
file: yaml_file,
|
|
477
|
+
message: `Failed to read file: ${err}`,
|
|
478
|
+
severity: 'error'
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return errors;
|
|
483
|
+
}
|
|
484
|
+
async function find_yaml_files(dir, relative_path) {
|
|
485
|
+
const files = [];
|
|
486
|
+
try {
|
|
487
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
488
|
+
for (const entry of entries) {
|
|
489
|
+
if (entry.name.startsWith('.'))
|
|
490
|
+
continue;
|
|
491
|
+
const full_path = path.join(dir, entry.name);
|
|
492
|
+
const rel_path = path.join(relative_path, entry.name);
|
|
493
|
+
if (entry.isDirectory()) {
|
|
494
|
+
const nested = await find_yaml_files(full_path, rel_path);
|
|
495
|
+
files.push(...nested);
|
|
496
|
+
}
|
|
497
|
+
else if (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml')) {
|
|
498
|
+
files.push(rel_path);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// Directory doesn't exist or not readable
|
|
504
|
+
}
|
|
505
|
+
return files;
|
|
506
|
+
}
|
|
507
|
+
function print_error(error) {
|
|
508
|
+
const icon = error.severity === 'error' ? chalk.red('ā') : chalk.yellow('ā ');
|
|
509
|
+
const location = error.field
|
|
510
|
+
? `${error.file} ā ${chalk.bold(error.field)}`
|
|
511
|
+
: error.file;
|
|
512
|
+
console.log(` ${icon} ${chalk.dim(location)}`);
|
|
513
|
+
console.log(` ${error.message}`);
|
|
514
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { new_site } from './commands/new.js';
|
|
4
|
+
import { pull_site } from './commands/pull.js';
|
|
5
|
+
import { push_site } from './commands/push.js';
|
|
6
|
+
import { dev_server } from './commands/dev.js';
|
|
7
|
+
import { login } from './commands/login.js';
|
|
8
|
+
import { validate_site } from './commands/validate.js';
|
|
9
|
+
import { publish } from './commands/publish.js';
|
|
10
|
+
import { build_site } from './commands/build.js';
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name('primo')
|
|
14
|
+
.description('Build sites visually, edit them anywhere')
|
|
15
|
+
.version('0.1.0');
|
|
16
|
+
program
|
|
17
|
+
.command('new [name]')
|
|
18
|
+
.description('Create a new site and start local CMS')
|
|
19
|
+
.option('-t, --template <template>', 'Starter template')
|
|
20
|
+
.option('--skip-dev', 'Create files only, don\'t start CMS')
|
|
21
|
+
.action((name, options) => new_site({ name, ...options }));
|
|
22
|
+
program
|
|
23
|
+
.command('dev')
|
|
24
|
+
.description('Start local CMS')
|
|
25
|
+
.option('-d, --dir <dir>', 'Site directory', '.')
|
|
26
|
+
.option('-p, --port <port>', 'Port', '3000')
|
|
27
|
+
.action(dev_server);
|
|
28
|
+
program
|
|
29
|
+
.command('publish')
|
|
30
|
+
.description('Deploy site with CMS')
|
|
31
|
+
.option('-d, --dir <dir>', 'Site directory', '.')
|
|
32
|
+
.option('-p, --provider <provider>', 'Provider (railway, fly)')
|
|
33
|
+
.action(publish);
|
|
34
|
+
program
|
|
35
|
+
.command('push')
|
|
36
|
+
.description('Push local files to hosted CMS')
|
|
37
|
+
.option('-s, --server <url>', 'Server URL')
|
|
38
|
+
.option('--site <id>', 'Site ID')
|
|
39
|
+
.option('-d, --dir <dir>', 'Directory', '.')
|
|
40
|
+
.option('-t, --token <token>', 'Auth token')
|
|
41
|
+
.option('--preview', 'Preview only')
|
|
42
|
+
.action(push_site);
|
|
43
|
+
program
|
|
44
|
+
.command('pull')
|
|
45
|
+
.description('Pull from hosted CMS to local files')
|
|
46
|
+
.option('-s, --server <url>', 'Server URL (auto-detects local)')
|
|
47
|
+
.option('--site <id>', 'Site ID (interactive if not provided)')
|
|
48
|
+
.option('-o, --output <dir>', 'Output directory', '.')
|
|
49
|
+
.option('-t, --token <token>', 'Auth token')
|
|
50
|
+
.action(pull_site);
|
|
51
|
+
program
|
|
52
|
+
.command('login')
|
|
53
|
+
.description('Login to hosted CMS')
|
|
54
|
+
.argument('<server>', 'Server URL')
|
|
55
|
+
.option('-e, --email <email>', 'Email')
|
|
56
|
+
.option('-p, --password <password>', 'Password')
|
|
57
|
+
.action((server, options) => login({ server, ...options }));
|
|
58
|
+
program
|
|
59
|
+
.command('validate')
|
|
60
|
+
.description('Validate site structure')
|
|
61
|
+
.option('-d, --dir <dir>', 'Directory', '.')
|
|
62
|
+
.option('--strict', 'Strict mode')
|
|
63
|
+
.action(validate_site);
|
|
64
|
+
program
|
|
65
|
+
.command('build')
|
|
66
|
+
.description('Build static site')
|
|
67
|
+
.option('-d, --dir <dir>', 'Site directory', '.')
|
|
68
|
+
.option('-o, --output <dir>', 'Output directory', 'dist')
|
|
69
|
+
.action(build_site);
|
|
70
|
+
program.parse();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), '.primo');
|
|
5
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, 'tokens.json');
|
|
6
|
+
export async function get_auth_token(server) {
|
|
7
|
+
try {
|
|
8
|
+
const data = await fs.readFile(TOKEN_FILE, 'utf-8');
|
|
9
|
+
const tokens = JSON.parse(data);
|
|
10
|
+
return tokens[normalize_server(server)] || null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function save_auth_token(server, token) {
|
|
17
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
18
|
+
let tokens = {};
|
|
19
|
+
try {
|
|
20
|
+
const data = await fs.readFile(TOKEN_FILE, 'utf-8');
|
|
21
|
+
tokens = JSON.parse(data);
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
tokens[normalize_server(server)] = token;
|
|
25
|
+
await fs.writeFile(TOKEN_FILE, JSON.stringify(tokens, null, 2));
|
|
26
|
+
}
|
|
27
|
+
function normalize_server(server) {
|
|
28
|
+
return server.replace(/\/+$/, '').toLowerCase();
|
|
29
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function get_binary_path(): Promise<string>;
|
|
2
|
+
export declare function ensure_data_dir(base_dir: string): Promise<string>;
|
|
3
|
+
export declare function is_binary_installed(): Promise<boolean>;
|
|
4
|
+
export declare function ensure_binary(): Promise<string>;
|
|
5
|
+
export declare function get_binary_version(): Promise<string | null>;
|