@yunsoft/yuncms 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/README.md CHANGED
@@ -4,6 +4,10 @@ Command-line setup, runtime launcher and guarded upgrade tooling for YunCMS.
4
4
 
5
5
  YunCMS is developed and maintained by **[Yunsoft Software](https://yunsoft.com)**.
6
6
 
7
+ Need help building your project, integrating YunCMS or migrating an existing backend? [Talk to Yunsoft](https://yunsoft.com/contact?utm_source=npm&utm_medium=yuncms&utm_campaign=yuncms-services). Support the project by [starring it on GitHub](https://github.com/Yunsoft-Software/yuncms) or [sponsor YunCMS](https://github.com/sponsors/Yunsoft-Software).
8
+
9
+ Configure repeated summaries, classifications or translations through Studio/MCP with [AI Automations](https://github.com/Yunsoft-Software/yuncms/blob/main/docs/ai-automations.md): preview a record, select restricted input/output fields and inspect persistent run history.
10
+
7
11
  ## Quick start
8
12
 
9
13
  You do **not** need to clone or fork the YunCMS repository. From an empty project directory, run the published package directly:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunsoft/yuncms",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Command-line setup and runtime launcher for YunCMS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,11 @@
30
30
  "test": "node --test"
31
31
  },
32
32
  "dependencies": {
33
- "@yunsoft/yuncms-api": "0.1.23",
34
- "@yunsoft/yuncms-core": "0.1.23"
33
+ "@yunsoft/yuncms-api": "0.1.25",
34
+ "@yunsoft/yuncms-core": "0.1.25"
35
+ },
36
+ "funding": {
37
+ "type": "github",
38
+ "url": "https://github.com/sponsors/Yunsoft-Software"
35
39
  }
36
40
  }
package/src/cli.js CHANGED
@@ -4,6 +4,7 @@ import { runInitCommand } from './init-command.js';
4
4
  import { runRestoreCommand } from './restore-command.js';
5
5
  import { runStartCommand } from './start-command.js';
6
6
  import { runUpdateCommand } from './update-command.js';
7
+ import { printCommunityLinks } from './community-links.js';
7
8
 
8
9
  function assertSupportedNode(version = process.versions.node) {
9
10
  const major = Number(String(version).split('.')[0]);
@@ -41,7 +42,11 @@ export async function runCli(argv = process.argv.slice(2), {
41
42
  switch (command) {
42
43
  case 'init':
43
44
  assertNoArguments(command, rest);
44
- return runInitCommand({ env, cwd, output, ...(prompts ? { prompts } : {}) });
45
+ {
46
+ const result = await runInitCommand({ env, cwd, output, ...(prompts ? { prompts } : {}) });
47
+ printCommunityLinks({ env, output });
48
+ return result;
49
+ }
45
50
  case 'bootstrap':
46
51
  assertNoArguments(command, rest);
47
52
  return runBootstrapCommand({ env, output });
@@ -59,6 +64,7 @@ export async function runCli(argv = process.argv.slice(2), {
59
64
  case '-h':
60
65
  assertNoArguments(command, rest);
61
66
  printHelp(output);
67
+ printCommunityLinks({ env, output, help: true });
62
68
  return null;
63
69
  default: {
64
70
  const error = new Error(`Unknown YunCMS command: ${command}`);
@@ -0,0 +1,10 @@
1
+ export const COMMUNITY_LINKS = Object.freeze({
2
+ github: 'https://github.com/Yunsoft-Software/yuncms',
3
+ services: 'https://yunsoft.com/contact?utm_source=cli&utm_medium=yuncms&utm_campaign=yuncms-services',
4
+ sponsorship: 'https://github.com/sponsors/Yunsoft-Software',
5
+ });
6
+
7
+ export function printCommunityLinks({ output = console, env = process.env, isTTY = process.stdout.isTTY, help = false } = {}) {
8
+ if (!help && (!isTTY || env.CI)) return;
9
+ output.log?.(`\nEnjoy YunCMS? Star the project: ${COMMUNITY_LINKS.github}\nBuild with Yunsoft — setup, migration and AI workflows: ${COMMUNITY_LINKS.services}\nSponsor YunCMS: ${COMMUNITY_LINKS.sponsorship}`);
10
+ }
@@ -1,7 +1,10 @@
1
- import { spawn } from 'node:child_process';
1
+ import { execFileSync, spawn } from 'node:child_process';
2
2
  import { resolve } from 'node:path';
3
3
 
4
4
  import { MAINTENANCE_BYPASS_ENV } from '@yunsoft/yuncms-core';
5
+ import { ATTACHED_RUNTIME_ENV } from './start-command.js';
6
+
7
+ export const DEFAULT_PROBE_SHUTDOWN_GRACE_MS = 12_000;
5
8
 
6
9
  function delay(ms) {
7
10
  return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
@@ -9,8 +12,10 @@ function delay(ms) {
9
12
 
10
13
  function probeError(code, message, details = {}) {
11
14
  const error = new Error(message);
15
+ const { code: exitCode, ...otherDetails } = details;
16
+ Object.assign(error, otherDetails);
17
+ if (Object.hasOwn(details, 'code')) error.exitCode = exitCode;
12
18
  error.code = code;
13
- Object.assign(error, details);
14
19
  return error;
15
20
  }
16
21
 
@@ -22,7 +27,7 @@ export async function verifyInstalledRuntime({
22
27
  fetchFn = globalThis.fetch,
23
28
  spawnProcess = spawn,
24
29
  timeoutMs = 15_000,
25
- shutdownGraceMs = 3_000,
30
+ shutdownGraceMs = DEFAULT_PROBE_SHUTDOWN_GRACE_MS,
26
31
  } = {}) {
27
32
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
28
33
  throw probeError('UPDATE_PROBE_PORT_INVALID', `Invalid probe port: ${port}`);
@@ -48,6 +53,7 @@ export async function verifyInstalledRuntime({
48
53
  PORT: String(port),
49
54
  STUDIO_ORIGIN: origin,
50
55
  AUTH_PUBLIC_URL: origin,
56
+ [ATTACHED_RUNTIME_ENV]: '1',
51
57
  };
52
58
  if (maintenanceBypassToken !== null) childEnv[MAINTENANCE_BYPASS_ENV] = maintenanceBypassToken;
53
59
 
@@ -55,6 +61,7 @@ export async function verifyInstalledRuntime({
55
61
  cwd,
56
62
  env: childEnv,
57
63
  stdio: ['ignore', 'ignore', 'pipe'],
64
+ detached: process.platform !== 'win32',
58
65
  });
59
66
 
60
67
  let stderr = '';
@@ -65,6 +72,7 @@ export async function verifyInstalledRuntime({
65
72
 
66
73
  let settled = false;
67
74
  let shutdownAttempted = false;
75
+ let readyAndStopped = false;
68
76
  const exitPromise = new Promise((resolveExit, rejectExit) => {
69
77
  child.once('error', (error) => {
70
78
  if (settled) return;
@@ -81,9 +89,18 @@ export async function verifyInstalledRuntime({
81
89
 
82
90
  function requestKill(signal) {
83
91
  try {
92
+ if (signal === 'SIGKILL' && Number.isInteger(child.pid)) {
93
+ if (process.platform === 'win32') {
94
+ execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', timeout: 1000 });
95
+ } else {
96
+ process.kill(-child.pid, signal);
97
+ }
98
+ return;
99
+ }
84
100
  child.kill(signal);
85
101
  } catch {
86
- // The bounded shutdown path still escalates and reports a timeout.
102
+ // ESRCH is expected for an exited group; keep a bounded direct-child fallback.
103
+ try { child.kill(signal); } catch {}
87
104
  }
88
105
  }
89
106
 
@@ -145,6 +162,7 @@ export async function verifyInstalledRuntime({
145
162
  if (result.code !== 0 && result.signal !== 'SIGTERM') {
146
163
  throw probeError('UPDATE_PROBE_SHUTDOWN_FAILED', 'Updated runtime did not stop cleanly', result);
147
164
  }
165
+ readyAndStopped = true;
148
166
  return { ready: true, origin };
149
167
  }
150
168
  }
@@ -161,5 +179,6 @@ export async function verifyInstalledRuntime({
161
179
  if (!settled && !shutdownAttempted) {
162
180
  await stopProbe({ strict: false }).catch(() => null);
163
181
  }
182
+ if (settled && !readyAndStopped && Number.isInteger(child.pid)) requestKill('SIGKILL');
164
183
  }
165
184
  }
@@ -3,6 +3,9 @@ import { fileURLToPath } from 'node:url';
3
3
 
4
4
  import { assertMaintenanceStartupAllowed } from '@yunsoft/yuncms-core';
5
5
 
6
+ // Internal updater transport flag: the probe owns the entire process group.
7
+ export const ATTACHED_RUNTIME_ENV = 'YUNCMS_CLI_ATTACHED_RUNTIME';
8
+
6
9
  export async function runStartCommand({
7
10
  env = process.env,
8
11
  cwd = process.cwd(),
@@ -21,7 +24,7 @@ export async function runStartCommand({
21
24
  cwd,
22
25
  env: { ...env },
23
26
  stdio: 'inherit',
24
- detached: process.platform !== 'win32',
27
+ detached: process.platform !== 'win32' && env[ATTACHED_RUNTIME_ENV] !== '1',
25
28
  });
26
29
 
27
30
  return new Promise((resolve, reject) => {