@yunsoft/yuncms 0.1.9 → 0.1.11
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 +24 -2
- package/package.json +3 -3
- package/src/project-backup.js +34 -8
package/README.md
CHANGED
|
@@ -2,12 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
Command-line setup, runtime launcher and guarded upgrade tooling for YunCMS.
|
|
4
4
|
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
You do **not** need to clone or fork the YunCMS repository. From an empty project directory, run the published package directly:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
mkdir my-yuncms
|
|
11
|
+
cd my-yuncms
|
|
12
|
+
npx --yes @yunsoft/yuncms init
|
|
13
|
+
npx --yes @yunsoft/yuncms start
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`init` creates the project `.env`, asks for MySQL connection details, applies the required database migrations and creates the first Administrator account.
|
|
17
|
+
|
|
18
|
+
The current directory is used as the YunCMS project directory, including local Files and local `extensions/`.
|
|
19
|
+
|
|
20
|
+
## Persistent project dependency
|
|
21
|
+
|
|
22
|
+
For a long-lived installation that should keep YunCMS in its own `package.json` and use managed updates, install the package once:
|
|
23
|
+
|
|
5
24
|
```bash
|
|
25
|
+
npm init -y
|
|
6
26
|
npm install @yunsoft/yuncms
|
|
7
27
|
npx yuncms init
|
|
8
28
|
npx yuncms start
|
|
9
29
|
```
|
|
10
30
|
|
|
31
|
+
Once installed locally, `npx yuncms ...` resolves the project's pinned package version.
|
|
32
|
+
|
|
11
33
|
Production maintenance commands:
|
|
12
34
|
|
|
13
35
|
```bash
|
|
@@ -21,10 +43,10 @@ npx yuncms restore /path/to/backup --yes
|
|
|
21
43
|
|
|
22
44
|
S3 objects are not copied by YunCMS backup; use provider-side versioning/snapshots and verify recovery before acknowledging an S3 update.
|
|
23
45
|
|
|
24
|
-
YunCMS requires Node.js 24 LTS and MySQL
|
|
46
|
+
YunCMS requires Node.js 24 LTS and MySQL. See the [project documentation](https://github.com/Yunsoft-Software/yuncms#documentation), [`docs/setup-cli.md`](https://github.com/Yunsoft-Software/yuncms/blob/main/docs/setup-cli.md) and [`docs/upgrades.md`](https://github.com/Yunsoft-Software/yuncms/blob/main/docs/upgrades.md) for setup, deployment and upgrade documentation.
|
|
25
47
|
|
|
26
48
|
## Project status
|
|
27
49
|
|
|
28
50
|
YunCMS is developed and maintained by [Yunsoft Software](https://yunsoft.com). It is under active development, so interfaces and behavior may change between releases. Test upgrades and keep verified backups before production use.
|
|
29
51
|
|
|
30
|
-
Use YunCMS at your own risk. This package is provided under the [MIT License](https://github.com/Yunsoft-Software/yuncms/blob/
|
|
52
|
+
Use YunCMS at your own risk. This package is provided under the [MIT License](https://github.com/Yunsoft-Software/yuncms/blob/main/LICENSE) without warranty.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yunsoft/yuncms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Command-line setup and runtime launcher for YunCMS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test": "node --test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@yunsoft/yuncms-api": "0.1.
|
|
34
|
-
"@yunsoft/yuncms-core": "0.1.
|
|
33
|
+
"@yunsoft/yuncms-api": "0.1.11",
|
|
34
|
+
"@yunsoft/yuncms-core": "0.1.11"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/project-backup.js
CHANGED
|
@@ -32,6 +32,7 @@ const LEGACY_BACKUP_FORMAT_VERSION = 1;
|
|
|
32
32
|
export const BACKUP_FORMAT_VERSION = 2;
|
|
33
33
|
const SUPPORTED_BACKUP_FORMATS = new Set([LEGACY_BACKUP_FORMAT_VERSION, BACKUP_FORMAT_VERSION]);
|
|
34
34
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/i;
|
|
35
|
+
const AI_SETTINGS_KEY_RELATIVE_PATH = join('.yuncms', 'ai-settings.key');
|
|
35
36
|
|
|
36
37
|
async function exists(path) {
|
|
37
38
|
try {
|
|
@@ -115,6 +116,9 @@ function validateBackupManifest(manifest, manifestPath) {
|
|
|
115
116
|
for (const key of ['env', 'packageJson', 'packageLock', 'extensions', 'localFiles']) {
|
|
116
117
|
assertManifestBoolean(manifest.project[key], `project.${key}`, manifestPath);
|
|
117
118
|
}
|
|
119
|
+
if (manifest.project.aiSettingsKey !== undefined) {
|
|
120
|
+
assertManifestBoolean(manifest.project.aiSettingsKey, 'project.aiSettingsKey', manifestPath);
|
|
121
|
+
}
|
|
118
122
|
assertManifestString(manifest.project.localFilesRoot, 'project.localFilesRoot', manifestPath);
|
|
119
123
|
|
|
120
124
|
if (!isObject(manifest.s3)) throw manifestError(manifestPath, 's3 metadata is required');
|
|
@@ -133,16 +137,21 @@ function validateBackupManifest(manifest, manifestPath) {
|
|
|
133
137
|
if (!isObject(manifest.integrity.project)) {
|
|
134
138
|
throw manifestError(manifestPath, 'integrity.project is required');
|
|
135
139
|
}
|
|
136
|
-
for (const [key, present] of [
|
|
137
|
-
['env', manifest.project.env],
|
|
138
|
-
['packageJson', manifest.project.packageJson],
|
|
139
|
-
['packageLock', manifest.project.packageLock],
|
|
140
|
-
['extensions', manifest.project.extensions],
|
|
141
|
-
['localFiles', manifest.project.localFiles],
|
|
140
|
+
for (const [key, present, optionalLegacy] of [
|
|
141
|
+
['env', manifest.project.env, false],
|
|
142
|
+
['packageJson', manifest.project.packageJson, false],
|
|
143
|
+
['packageLock', manifest.project.packageLock, false],
|
|
144
|
+
['extensions', manifest.project.extensions, false],
|
|
145
|
+
['localFiles', manifest.project.localFiles, false],
|
|
146
|
+
['aiSettingsKey', manifest.project.aiSettingsKey === true, manifest.project.aiSettingsKey === undefined],
|
|
142
147
|
]) {
|
|
143
148
|
const digest = manifest.integrity.project[key];
|
|
144
149
|
if (present) assertDigest(digest, `integrity.project.${key}`, manifestPath);
|
|
145
|
-
else if (digest !== null)
|
|
150
|
+
else if (!optionalLegacy && digest !== null) {
|
|
151
|
+
throw manifestError(manifestPath, `integrity.project.${key} must be null when the asset is absent`);
|
|
152
|
+
} else if (optionalLegacy && digest !== undefined && digest !== null) {
|
|
153
|
+
throw manifestError(manifestPath, `integrity.project.${key} must be null when the legacy manifest has no asset flag`);
|
|
154
|
+
}
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
|
|
@@ -208,6 +217,7 @@ async function assertExpectedBackupAssets(backupPath, manifest) {
|
|
|
208
217
|
[manifest.project.env, join(backupPath, 'project', '.env'), 'file'],
|
|
209
218
|
[manifest.project.packageJson, join(backupPath, 'project', 'package.json'), 'file'],
|
|
210
219
|
[manifest.project.packageLock, join(backupPath, 'project', 'package-lock.json'), 'file'],
|
|
220
|
+
[manifest.project.aiSettingsKey === true, join(backupPath, 'project', 'ai-settings.key'), 'file'],
|
|
211
221
|
[manifest.project.extensions, join(backupPath, 'extensions'), 'directory'],
|
|
212
222
|
[manifest.project.localFiles, join(backupPath, 'files'), 'directory'],
|
|
213
223
|
];
|
|
@@ -227,6 +237,7 @@ async function calculateBackupIntegrity(destination, presence) {
|
|
|
227
237
|
env: presence.env ? await hashFile(join(destination, 'project', '.env')) : null,
|
|
228
238
|
packageJson: presence.packageJson ? await hashFile(join(destination, 'project', 'package.json')) : null,
|
|
229
239
|
packageLock: presence.packageLock ? await hashFile(join(destination, 'project', 'package-lock.json')) : null,
|
|
240
|
+
aiSettingsKey: presence.aiSettingsKey ? await hashFile(join(destination, 'project', 'ai-settings.key')) : null,
|
|
230
241
|
extensions: presence.extensions ? await hashDirectory(join(destination, 'extensions')) : null,
|
|
231
242
|
localFiles: presence.localFiles ? await hashDirectory(join(destination, 'files')) : null,
|
|
232
243
|
},
|
|
@@ -240,6 +251,7 @@ async function verifyBackupIntegrity(backupPath, manifest) {
|
|
|
240
251
|
['env', ['project', '.env'], 'file'],
|
|
241
252
|
['packageJson', ['project', 'package.json'], 'file'],
|
|
242
253
|
['packageLock', ['project', 'package-lock.json'], 'file'],
|
|
254
|
+
['aiSettingsKey', ['project', 'ai-settings.key'], 'file'],
|
|
243
255
|
['extensions', ['extensions'], 'directory'],
|
|
244
256
|
['localFiles', ['files'], 'directory'],
|
|
245
257
|
]) {
|
|
@@ -308,6 +320,7 @@ async function assertRestoreTargets(cwd, localFilesPath, extensionsPath, allowDi
|
|
|
308
320
|
await assertRestoreTarget(extensionsPath, 'directory');
|
|
309
321
|
await assertRestoreTarget(resolve(cwd, 'package.json'), 'file');
|
|
310
322
|
await assertRestoreTarget(resolve(cwd, 'package-lock.json'), 'file');
|
|
323
|
+
await assertRestoreTarget(resolve(cwd, AI_SETTINGS_KEY_RELATIVE_PATH), 'file');
|
|
311
324
|
if (!allowDifferentDatabaseTarget) await assertRestoreTarget(resolve(cwd, '.env'), 'file');
|
|
312
325
|
}
|
|
313
326
|
|
|
@@ -393,6 +406,7 @@ export async function createProjectBackup({
|
|
|
393
406
|
env: await copyOptionalFile(resolve(cwd, '.env'), join(destination, 'project', '.env')),
|
|
394
407
|
packageJson: await copyOptionalFile(resolve(cwd, 'package.json'), join(destination, 'project', 'package.json')),
|
|
395
408
|
packageLock: await copyOptionalFile(resolve(cwd, 'package-lock.json'), join(destination, 'project', 'package-lock.json')),
|
|
409
|
+
aiSettingsKey: await copyOptionalFile(resolve(cwd, AI_SETTINGS_KEY_RELATIVE_PATH), join(destination, 'project', 'ai-settings.key')),
|
|
396
410
|
extensions: await copyOptionalDirectory(extensionsPath, join(destination, 'extensions')),
|
|
397
411
|
localFiles: await copyOptionalDirectory(localFilesPath, join(destination, 'files')),
|
|
398
412
|
};
|
|
@@ -462,7 +476,7 @@ async function restoreOptionalFile({ backupPath, existed, sourceName, target })
|
|
|
462
476
|
await rm(target, { force: true });
|
|
463
477
|
return;
|
|
464
478
|
}
|
|
465
|
-
await mkdir(dirname(target), { recursive: true });
|
|
479
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
466
480
|
await copyFile(source, target);
|
|
467
481
|
}
|
|
468
482
|
|
|
@@ -548,6 +562,16 @@ export async function restoreProjectBackup({
|
|
|
548
562
|
sourceName: 'package-lock.json',
|
|
549
563
|
target: resolve(cwd, 'package-lock.json'),
|
|
550
564
|
});
|
|
565
|
+
if (manifest.project.aiSettingsKey !== undefined) {
|
|
566
|
+
await restoreOptionalFile({
|
|
567
|
+
backupPath: resolvedBackupPath,
|
|
568
|
+
existed: manifest.project.aiSettingsKey,
|
|
569
|
+
sourceName: 'ai-settings.key',
|
|
570
|
+
target: resolve(cwd, AI_SETTINGS_KEY_RELATIVE_PATH),
|
|
571
|
+
});
|
|
572
|
+
} else {
|
|
573
|
+
output.warn?.('Backup predates AI settings key snapshots; preserving the current .yuncms/ai-settings.key if present.');
|
|
574
|
+
}
|
|
551
575
|
if (allowDifferentDatabaseTarget) {
|
|
552
576
|
output.warn?.('Preserving the current .env because restore targets a different database; the backup .env remains available inside the backup directory.');
|
|
553
577
|
} else {
|
|
@@ -562,3 +586,5 @@ export async function restoreProjectBackup({
|
|
|
562
586
|
output.log?.(`Restore completed: ${resolvedBackupPath}`);
|
|
563
587
|
return { backupPath: resolvedBackupPath, manifest };
|
|
564
588
|
}
|
|
589
|
+
|
|
590
|
+
export { AI_SETTINGS_KEY_RELATIVE_PATH, validateBackupManifest };
|