@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 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 for V1. See the [project repository](https://github.com/Yunsoft-Software/yuncms), `docs/setup-cli.md` and `docs/upgrades.md` for setup, deployment and upgrade documentation.
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/22-08-2026/LICENSE) without warranty.
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.9",
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.9",
34
- "@yunsoft/yuncms-core": "0.1.9"
33
+ "@yunsoft/yuncms-api": "0.1.11",
34
+ "@yunsoft/yuncms-core": "0.1.11"
35
35
  }
36
36
  }
@@ -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) throw manifestError(manifestPath, `integrity.project.${key} must be null when the asset is absent`);
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 };