@life-and-dev/mdsite 0.5.3 → 0.6.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.
Files changed (34) hide show
  1. package/README.md +13 -20
  2. package/dist/commands/clean.d.ts +1 -0
  3. package/dist/commands/clean.js +52 -0
  4. package/dist/commands/clean.js.map +1 -0
  5. package/dist/commands/commands.test.js +109 -55
  6. package/dist/commands/commands.test.js.map +1 -1
  7. package/dist/commands/init.js +3 -62
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/prepare.js +0 -12
  10. package/dist/commands/prepare.js.map +1 -1
  11. package/dist/commands/prepare.test.js +17 -14
  12. package/dist/commands/prepare.test.js.map +1 -1
  13. package/dist/commands/workflows.test.js +17 -32
  14. package/dist/commands/workflows.test.js.map +1 -1
  15. package/dist/index.js +8 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.test.js +13 -0
  18. package/dist/index.test.js.map +1 -1
  19. package/dist/process/child-process.test.js +2 -2
  20. package/dist/process/child-process.test.js.map +1 -1
  21. package/dist/process/runtime-state.js +6 -2
  22. package/dist/process/runtime-state.js.map +1 -1
  23. package/dist/process/runtime-state.test.js +7 -5
  24. package/dist/process/runtime-state.test.js.map +1 -1
  25. package/dist/renderer/mdsite-nuxt.js +3 -15
  26. package/dist/renderer/mdsite-nuxt.js.map +1 -1
  27. package/dist/renderer/mdsite-nuxt.test.js +6 -27
  28. package/dist/renderer/mdsite-nuxt.test.js.map +1 -1
  29. package/mdsite-nuxt/nuxt.config.ts +1 -1
  30. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +0 -5
  31. package/mdsite-nuxt/scripts/renderer-hooks.ts +0 -2
  32. package/mdsite-nuxt/scripts/start.test.ts +0 -1
  33. package/mdsite-nuxt/scripts/start.ts +0 -1
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -22,32 +22,28 @@ You write Markdown. For example, your content directory with a few pages and a l
22
22
 
23
23
  ```yaml
24
24
  my-docs/
25
- └── content
26
- ├── index.md
27
- ├── about.md
28
- ├── blog/
29
- ├── 2026-01-hello.md
30
- └── 2026-03-release.md
31
- └── logo.png
25
+ ├── index.md
26
+ ├── about.md
27
+ ├── blog/
28
+ ├── 2026-01-hello.md
29
+ └── 2026-03-release.md
30
+ └── logo.png
32
31
  ```
33
32
 
34
33
  Run the `mdsite static` in your repo to generate the static pages:
35
34
 
36
35
  ```yaml
37
36
  my-docs/
38
- ├── content
39
- ├── index.md
40
- ├── about.md
41
- │ ├── blog/
42
- │ ├── 2026-01-hello.md
43
- │ │ └── 2026-03-release.md
44
- │ └── logo.png
37
+ ├── index.md
38
+ ├── about.md
39
+ ├── blog/
40
+ │ ├── 2026-01-hello.md
41
+ └── 2026-03-release.md
42
+ ├── logo.png
45
43
  ├── mdsite.yml # site configuration
46
- ├── package.json # package configuration
47
- ├── package-lock.json # package lock
48
44
  ├── .mdsite/ # renderer working dir (gitignored)
49
45
  │ ├── mdsite.log # detached webserver logs
50
- │ └── ... # Other Nuxt render files
46
+ │ └── ... # Other Node and Nuxt render files
51
47
  └── .output/ # deployable static site
52
48
  └── public/
53
49
  ├── index.html
@@ -63,9 +59,6 @@ my-docs/
63
59
  └── ...
64
60
  ```
65
61
 
66
- > [!NOTE]
67
- > You can technically mix your content and project files in the same directory, but it's easier to maintain content and generated files separately.
68
-
69
62
  ## Install
70
63
 
71
64
  Install the CLI globally from the npm registry on any machine with Node.js (>= 24.0.0) and npm:
@@ -0,0 +1 @@
1
+ export declare function runCleanCommand(contentDir: string): Promise<string>;
@@ -0,0 +1,52 @@
1
+ import { access, rm } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadMdsiteConfig } from '../config/mdsite-config.js';
4
+ import { isProcessRunning, readRuntimeState } from '../process/runtime-state.js';
5
+ async function pathExists(targetPath) {
6
+ try {
7
+ await access(targetPath);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export async function runCleanCommand(contentDir) {
15
+ const loaded = await loadMdsiteConfig(contentDir);
16
+ const { config, configDir } = loaded;
17
+ // Refuse to wipe state while a tracked process is still pointing at it.
18
+ // Tracked PIDs live inside <server.path>, so the clean would orphan them.
19
+ const trackedStates = await Promise.all([
20
+ readRuntimeState(configDir, config, 'start'),
21
+ readRuntimeState(configDir, config, 'preview')
22
+ ]);
23
+ for (const state of trackedStates) {
24
+ if (state && isProcessRunning(state.pid)) {
25
+ const alias = state.kind === 'start' ? 'live' : 'static';
26
+ throw new Error(`mdsite ${alias} is running with PID ${state.pid}. Run \`mdsite stop\` before \`mdsite clean\`.`);
27
+ }
28
+ }
29
+ const rendererPath = path.resolve(configDir, config.server.path);
30
+ const outputPath = path.resolve(configDir, config.server.output);
31
+ const [rendererExists, outputExists] = await Promise.all([
32
+ pathExists(rendererPath),
33
+ pathExists(outputPath)
34
+ ]);
35
+ // `force: true` lets us skip non-existent paths without a separate branch.
36
+ await Promise.all([
37
+ rm(rendererPath, { recursive: true, force: true }),
38
+ rm(outputPath, { recursive: true, force: true })
39
+ ]);
40
+ const removed = [];
41
+ if (rendererExists) {
42
+ removed.push(config.server.path);
43
+ }
44
+ if (outputExists) {
45
+ removed.push(config.server.output);
46
+ }
47
+ if (removed.length === 0) {
48
+ return `Nothing to clean in ${contentDir}.`;
49
+ }
50
+ return `Removed ${removed.join(' and ')} from ${contentDir}.`;
51
+ }
52
+ //# sourceMappingURL=clean.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clean.js","sourceRoot":"","sources":["../../src/commands/clean.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAC7D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAEhF,KAAK,UAAU,UAAU,CAAC,UAAkB;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,UAAU,CAAC,CAAA;QACxB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,UAAkB;IACtD,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAA;IACjD,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAA;IAEpC,wEAAwE;IACxE,0EAA0E;IAC1E,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACtC,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC;QAC5C,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC;KAC/C,CAAC,CAAA;IAEF,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,IAAI,KAAK,IAAI,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAA;YACxD,MAAM,IAAI,KAAK,CAAC,UAAU,KAAK,wBAAwB,KAAK,CAAC,GAAG,gDAAgD,CAAC,CAAA;QACnH,CAAC;IACH,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAChE,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAEhE,MAAM,CAAC,cAAc,EAAE,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACvD,UAAU,CAAC,YAAY,CAAC;QACxB,UAAU,CAAC,UAAU,CAAC;KACvB,CAAC,CAAA;IAEF,2EAA2E;IAC3E,MAAM,OAAO,CAAC,GAAG,CAAC;QAChB,EAAE,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAClD,EAAE,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;KACjD,CAAC,CAAA;IAEF,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAClC,CAAC;IACD,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACpC,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,uBAAuB,UAAU,GAAG,CAAA;IAC7C,CAAC;IAED,OAAO,WAAW,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,UAAU,GAAG,CAAA;AAC/D,CAAC"}
@@ -3,7 +3,6 @@ vi.mock('node:fs/promises', () => ({
3
3
  access: vi.fn(),
4
4
  copyFile: vi.fn(),
5
5
  cp: vi.fn(),
6
- mkdir: vi.fn(),
7
6
  readFile: vi.fn(),
8
7
  rm: vi.fn(),
9
8
  writeFile: vi.fn()
@@ -40,11 +39,12 @@ vi.mock('../renderer/mdsite-nuxt.js', () => ({
40
39
  startRendererInBackground: vi.fn()
41
40
  }));
42
41
  import path from 'node:path';
43
- import { access, copyFile, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
42
+ import { access, copyFile, cp, readFile, rm, writeFile } from 'node:fs/promises';
44
43
  import { buildDefaultMdsiteConfig, loadMdsiteConfig, resolveContentOutputPath, serializeMdsiteConfig } from '../config/mdsite-config.js';
45
44
  import { openUrlInBrowser, stopProcess, waitForTcpPort } from '../process/child-process.js';
46
45
  import { clearRuntimeState, getRuntimeLogPath, isProcessRunning, readRuntimeState, writeRuntimeState } from '../process/runtime-state.js';
47
46
  import { ensurePreviewArtifacts, ensureRendererDependencies, generateRenderer, getBundledRendererDir, getRendererGeneratedOutputPath, hasPreviewArtifacts, prepareRenderer, previewRendererForeground, previewRendererInBackground, startRendererForeground, startRendererInBackground } from '../renderer/mdsite-nuxt.js';
47
+ import { runCleanCommand } from './clean.js';
48
48
  import { runGenerateCommand } from './generate.js';
49
49
  import { runInitCommand } from './init.js';
50
50
  import { runPreviewCommand } from './preview.js';
@@ -55,7 +55,6 @@ const cpMock = vi.mocked(cp);
55
55
  const rmMock = vi.mocked(rm);
56
56
  const writeFileMock = vi.mocked(writeFile);
57
57
  const copyFileMock = vi.mocked(copyFile);
58
- const mkdirMock = vi.mocked(mkdir);
59
58
  const readFileMock = vi.mocked(readFile);
60
59
  const getBundledRendererDirMock = vi.mocked(getBundledRendererDir);
61
60
  const buildDefaultConfigMock = vi.mocked(buildDefaultMdsiteConfig);
@@ -102,7 +101,8 @@ describe('command helpers', () => {
102
101
  hasPreviewArtifactsMock.mockResolvedValue(true);
103
102
  prepareRendererMock.mockResolvedValue({ rendererDir: '/renderer', rendererEnv: { TEST: '1' } });
104
103
  getRuntimeLogPathMock.mockImplementation((configDir, config, kind) => {
105
- return `${configDir}/${config.server.path}/${kind}.log`;
104
+ const basename = kind === 'start' ? 'live' : 'static';
105
+ return `${configDir}/${config.server.path}/${basename}.log`;
106
106
  });
107
107
  resolveOutputMock.mockReturnValue('/content/.output/public');
108
108
  getRendererGeneratedOutputPathMock.mockReturnValue('/renderer/.output/public');
@@ -119,27 +119,16 @@ describe('command helpers', () => {
119
119
  });
120
120
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
121
121
  serializeConfigMock.mockReturnValue('serialized-config');
122
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', description: 'old', scripts: { dev: 'x' } }));
123
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'mdsite-nuxt-renderer', version: '0.1.0' } } }));
124
- await expect(runInitCommand('/content')).resolves.toBe('Created mdsite.yml, .nvmrc, .renderer/package.json, .renderer/package-lock.json in /content.');
122
+ await expect(runInitCommand('/content')).resolves.toBe('Created mdsite.yml, .nvmrc in /content.');
125
123
  expect(buildDefaultConfigMock).toHaveBeenCalledWith('/content');
126
124
  expect(loadConfigMock).not.toHaveBeenCalled();
127
125
  expect(writeFileMock).toHaveBeenCalledWith('/content/mdsite.yml', 'serialized-config', 'utf8');
128
126
  expect(writeFileMock).toHaveBeenCalledWith('/content/.nvmrc', '24\n', 'utf8');
129
- expect(mkdirMock).toHaveBeenCalledWith(path.join('/content', '.renderer'), { recursive: true });
130
- expect(readFileMock).toHaveBeenCalledWith(path.join('/home/gizbar/git/mdsite/mdsite-nuxt', 'package.json'), 'utf8');
131
- expect(readFileMock).toHaveBeenCalledWith(path.join('/home/gizbar/git/mdsite/mdsite-nuxt', 'package-lock.json'), 'utf8');
132
- const writtenPkg = writeFileMock.mock.calls.find(([p]) => p === path.join('/content', '.renderer', 'package.json'));
133
- expect(writtenPkg).toBeDefined();
134
- expect(writtenPkg[1]).toBe(`${JSON.stringify({ name: 'content', version: '0.1.0', description: 'Docs', scripts: { dev: 'x' } }, null, 2)}\n`);
135
- const writtenLock = writeFileMock.mock.calls.find(([p]) => p === path.join('/content', '.renderer', 'package-lock.json'));
136
- expect(writtenLock).toBeDefined();
137
- expect(writtenLock[1]).toBe(`${JSON.stringify({ name: 'content', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'content', version: '0.1.0' } } }, null, 2)}\n`);
138
127
  expect(copyFileMock).not.toHaveBeenCalled();
139
128
  expect(writeFileMock).toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('.renderer/*'), 'utf8');
140
- expect(writeFileMock).toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('!.renderer/package.json'), 'utf8');
141
- expect(writeFileMock).toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('!.renderer/package-lock.json'), 'utf8');
142
129
  expect(writeFileMock).toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('.output/'), 'utf8');
130
+ expect(writeFileMock).not.toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('!.renderer/package.json'), 'utf8');
131
+ expect(writeFileMock).not.toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('!.renderer/package-lock.json'), 'utf8');
143
132
  });
144
133
  it('runInitCommand preserves an existing .nvmrc and only creates the rest', async () => {
145
134
  const existing = new Set(['/content/.nvmrc']);
@@ -150,33 +139,23 @@ describe('command helpers', () => {
150
139
  });
151
140
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
152
141
  serializeConfigMock.mockReturnValue('serialized-config');
153
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', description: 'old', scripts: { dev: 'x' } }));
154
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'mdsite-nuxt-renderer', version: '0.1.0' } } }));
155
- await expect(runInitCommand('/content')).resolves.toBe('Created mdsite.yml, .renderer/package.json, .renderer/package-lock.json in /content.');
142
+ await expect(runInitCommand('/content')).resolves.toBe('Created mdsite.yml in /content.');
156
143
  expect(writeFileMock).not.toHaveBeenCalledWith('/content/.nvmrc', expect.anything(), expect.anything());
157
144
  expect(copyFileMock).not.toHaveBeenCalled();
158
145
  });
159
146
  it('runInitCommand loads an existing mdsite.yml to repair missing files without overwriting it', async () => {
160
147
  const existing = new Set([
161
- '/content/mdsite.yml',
162
- '/content/.nvmrc',
163
- path.join('/content', '.renderer', 'package.json')
148
+ '/content/mdsite.yml'
164
149
  ]);
165
150
  accessMock.mockImplementation(async (p) => {
166
151
  if (typeof p === 'string' && existing.has(p))
167
152
  return;
168
153
  throw new Error('missing');
169
154
  });
170
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'mdsite-nuxt-renderer', version: '0.1.0' } } }));
171
- await expect(runInitCommand('/content')).resolves.toBe('Created .renderer/package-lock.json in /content.');
155
+ await expect(runInitCommand('/content')).resolves.toBe('Created .nvmrc in /content.');
172
156
  expect(loadConfigMock).toHaveBeenCalledWith('/content');
173
157
  expect(buildDefaultConfigMock).not.toHaveBeenCalled();
174
158
  expect(writeFileMock).not.toHaveBeenCalledWith('/content/mdsite.yml', expect.anything(), expect.anything());
175
- const writeFileCallsForPkg = writeFileMock.mock.calls.filter(([p]) => typeof p === 'string' && p === path.join('/content', '.renderer', 'package.json'));
176
- expect(writeFileCallsForPkg).toHaveLength(0);
177
- const writtenLock = writeFileMock.mock.calls.find(([p]) => p === path.join('/content', '.renderer', 'package-lock.json'));
178
- expect(writtenLock).toBeDefined();
179
- expect(writtenLock[1]).toBe(`${JSON.stringify({ name: 'content', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'content', version: '0.1.0' } } }, null, 2)}\n`);
180
159
  expect(copyFileMock).not.toHaveBeenCalled();
181
160
  });
182
161
  it('runInitCommand reports nothing to create when every file already exists', async () => {
@@ -218,7 +197,7 @@ describe('command helpers', () => {
218
197
  readRuntimeStateMock.mockResolvedValueOnce({ kind: 'start', pid: 44 });
219
198
  isProcessRunningMock.mockReturnValueOnce(false);
220
199
  startRendererInBackgroundMock.mockResolvedValueOnce(777);
221
- await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 777). Log: /content/.renderer/start.log');
200
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 777). Log: /content/.renderer/live.log');
222
201
  expect(ensureRendererDependenciesMock).toHaveBeenCalledWith('/renderer');
223
202
  expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3000);
224
203
  expect(waitForTcpPortMock.mock.invocationCallOrder[0]).toBeGreaterThan(writeRuntimeStateMock.mock.invocationCallOrder[0] ?? 0);
@@ -239,7 +218,7 @@ describe('command helpers', () => {
239
218
  rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'start.local', NUXT_PORT: '4321' }
240
219
  });
241
220
  startRendererInBackgroundMock.mockResolvedValueOnce(778);
242
- await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 778). Log: /content/.renderer/start.log');
221
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 778). Log: /content/.renderer/live.log');
243
222
  expect(waitForTcpPortMock).toHaveBeenCalledWith('start.local', 4321);
244
223
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://start.local:4321');
245
224
  });
@@ -247,7 +226,7 @@ describe('command helpers', () => {
247
226
  readRuntimeStateMock.mockResolvedValueOnce(null);
248
227
  startRendererInBackgroundMock.mockResolvedValueOnce(779);
249
228
  waitForTcpPortMock.mockResolvedValueOnce(false);
250
- await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 779). Log: /content/.renderer/start.log');
229
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 779). Log: /content/.renderer/live.log');
251
230
  expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3000);
252
231
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
253
232
  });
@@ -255,7 +234,7 @@ describe('command helpers', () => {
255
234
  readRuntimeStateMock.mockResolvedValueOnce(null);
256
235
  startRendererInBackgroundMock.mockResolvedValueOnce(780);
257
236
  waitForTcpPortMock.mockRejectedValueOnce(new Error('connect failed'));
258
- await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 780). Log: /content/.renderer/start.log');
237
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 780). Log: /content/.renderer/live.log');
259
238
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
260
239
  });
261
240
  it('runStartCommand exposes the foreground renderer on the network when host is set', async () => {
@@ -270,12 +249,12 @@ describe('command helpers', () => {
270
249
  it('runStartCommand binds the detached renderer to a custom host when host is set', async () => {
271
250
  readRuntimeStateMock.mockResolvedValueOnce(null);
272
251
  startRendererInBackgroundMock.mockResolvedValueOnce(781);
273
- await expect(runStartCommand('/content', { detached: true, host: '0.0.0.0' })).resolves.toBe('mdsite live running in background (PID 781). Log: /content/.renderer/start.log');
252
+ await expect(runStartCommand('/content', { detached: true, host: '0.0.0.0' })).resolves.toBe('mdsite live running in background (PID 781). Log: /content/.renderer/live.log');
274
253
  expect(startRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
275
254
  NUXT_HOST: '0.0.0.0',
276
255
  HOST: '0.0.0.0',
277
256
  NITRO_HOST: '0.0.0.0'
278
- }), '/content/.renderer/start.log');
257
+ }), '/content/.renderer/live.log');
279
258
  expect(waitForTcpPortMock).toHaveBeenCalledWith('0.0.0.0', 3000);
280
259
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://0.0.0.0:3000');
281
260
  });
@@ -309,7 +288,7 @@ describe('command helpers', () => {
309
288
  vi.setSystemTime(new Date('2026-04-10T13:00:00.000Z'));
310
289
  readRuntimeStateMock.mockResolvedValueOnce(null);
311
290
  previewRendererInBackgroundMock.mockResolvedValueOnce(888);
312
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 888). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
291
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 888). URL: http://localhost:3000 Log: /content/.renderer/static.log');
313
292
  expect(ensurePreviewArtifactsMock).toHaveBeenCalledWith('/renderer');
314
293
  expect(ensurePreviewArtifactsMock.mock.invocationCallOrder[0]).toBeGreaterThan(ensureRendererDependenciesMock.mock.invocationCallOrder[0] ?? 0);
315
294
  expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
@@ -320,7 +299,7 @@ describe('command helpers', () => {
320
299
  PORT: '3000',
321
300
  NITRO_HOST: 'localhost',
322
301
  NITRO_PORT: '3000'
323
- }), '/content/.renderer/preview.log');
302
+ }), '/content/.renderer/static.log');
324
303
  expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3000);
325
304
  expect(waitForTcpPortMock.mock.invocationCallOrder[0]).toBeGreaterThan(writeRuntimeStateMock.mock.invocationCallOrder[0] ?? 0);
326
305
  expect(openUrlInBrowserMock.mock.invocationCallOrder[0]).toBeGreaterThan(waitForTcpPortMock.mock.invocationCallOrder[0] ?? 0);
@@ -339,7 +318,7 @@ describe('command helpers', () => {
339
318
  rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'preview.local', NUXT_PORT: '4321' }
340
319
  });
341
320
  previewRendererInBackgroundMock.mockResolvedValueOnce(999);
342
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 999). URL: http://preview.local:4321 Log: /content/.renderer/preview.log');
321
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 999). URL: http://preview.local:4321 Log: /content/.renderer/static.log');
343
322
  expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
344
323
  HOST: 'preview.local',
345
324
  PORT: '4321',
@@ -347,7 +326,7 @@ describe('command helpers', () => {
347
326
  NUXT_PORT: '4321',
348
327
  NITRO_HOST: 'preview.local',
349
328
  NITRO_PORT: '4321'
350
- }), '/content/.renderer/preview.log');
329
+ }), '/content/.renderer/static.log');
351
330
  expect(waitForTcpPortMock).toHaveBeenCalledWith('preview.local', 4321);
352
331
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://preview.local:4321');
353
332
  });
@@ -358,7 +337,7 @@ describe('command helpers', () => {
358
337
  rendererEnv: { HOST: '127.0.0.1', PORT: '4173' }
359
338
  });
360
339
  previewRendererInBackgroundMock.mockResolvedValueOnce(1000);
361
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1000). URL: http://127.0.0.1:4173 Log: /content/.renderer/preview.log');
340
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1000). URL: http://127.0.0.1:4173 Log: /content/.renderer/static.log');
362
341
  expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
363
342
  HOST: '127.0.0.1',
364
343
  PORT: '4173',
@@ -366,7 +345,7 @@ describe('command helpers', () => {
366
345
  NUXT_PORT: '4173',
367
346
  NITRO_HOST: '127.0.0.1',
368
347
  NITRO_PORT: '4173'
369
- }), '/content/.renderer/preview.log');
348
+ }), '/content/.renderer/static.log');
370
349
  expect(waitForTcpPortMock).toHaveBeenCalledWith('127.0.0.1', 4173);
371
350
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://127.0.0.1:4173');
372
351
  });
@@ -385,13 +364,13 @@ describe('command helpers', () => {
385
364
  rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'preview.local', NUXT_PORT: '4321' }
386
365
  });
387
366
  previewRendererInBackgroundMock.mockResolvedValueOnce(1003);
388
- await expect(runPreviewCommand('/content', { detached: true, host: '0.0.0.0' })).resolves.toBe('mdsite static running in background (PID 1003). URL: http://0.0.0.0:4321 Log: /content/.renderer/preview.log');
367
+ await expect(runPreviewCommand('/content', { detached: true, host: '0.0.0.0' })).resolves.toBe('mdsite static running in background (PID 1003). URL: http://0.0.0.0:4321 Log: /content/.renderer/static.log');
389
368
  expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
390
369
  NUXT_HOST: '0.0.0.0',
391
370
  HOST: '0.0.0.0',
392
371
  NITRO_HOST: '0.0.0.0',
393
372
  NUXT_PORT: '4321'
394
- }), '/content/.renderer/preview.log');
373
+ }), '/content/.renderer/static.log');
395
374
  expect(waitForTcpPortMock).toHaveBeenCalledWith('0.0.0.0', 4321);
396
375
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://0.0.0.0:4321');
397
376
  });
@@ -399,7 +378,7 @@ describe('command helpers', () => {
399
378
  readRuntimeStateMock.mockResolvedValueOnce(null);
400
379
  previewRendererInBackgroundMock.mockResolvedValueOnce(1001);
401
380
  waitForTcpPortMock.mockResolvedValueOnce(false);
402
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1001). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
381
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1001). URL: http://localhost:3000 Log: /content/.renderer/static.log');
403
382
  expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3000);
404
383
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
405
384
  });
@@ -407,7 +386,7 @@ describe('command helpers', () => {
407
386
  readRuntimeStateMock.mockResolvedValueOnce(null);
408
387
  previewRendererInBackgroundMock.mockResolvedValueOnce(1002);
409
388
  waitForTcpPortMock.mockRejectedValueOnce(new Error('connect failed'));
410
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1002). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
389
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1002). URL: http://localhost:3000 Log: /content/.renderer/static.log');
411
390
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
412
391
  });
413
392
  it('runStartCommand auto-runs mdsite init when mdsite.yml is missing', async () => {
@@ -416,7 +395,7 @@ describe('command helpers', () => {
416
395
  });
417
396
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
418
397
  serializeConfigMock.mockReturnValue('serialized-config');
419
- readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce('{}');
398
+ readFileMock.mockResolvedValueOnce('{}');
420
399
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
421
400
  await expect(runStartCommand('/content')).resolves.toBeUndefined();
422
401
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml found'));
@@ -431,7 +410,7 @@ describe('command helpers', () => {
431
410
  });
432
411
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
433
412
  serializeConfigMock.mockReturnValue('serialized-config');
434
- readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce('{}');
413
+ readFileMock.mockResolvedValueOnce('{}');
435
414
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
436
415
  await expect(runPreviewCommand('/content')).resolves.toBeUndefined();
437
416
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml found'));
@@ -447,13 +426,13 @@ describe('command helpers', () => {
447
426
  });
448
427
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
449
428
  serializeConfigMock.mockReturnValue('serialized-config');
450
- readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce('{}');
429
+ readFileMock.mockResolvedValueOnce('{}');
451
430
  previewRendererInBackgroundMock.mockResolvedValueOnce(5555);
452
431
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
453
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 5555). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
432
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 5555). URL: http://localhost:3000 Log: /content/.renderer/static.log');
454
433
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml found'));
455
434
  expect(writeFileMock).toHaveBeenCalledWith('/content/mdsite.yml', 'serialized-config', 'utf8');
456
- expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }), '/content/.renderer/preview.log');
435
+ expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }), '/content/.renderer/static.log');
457
436
  consoleSpy.mockRestore();
458
437
  });
459
438
  it('runGenerateCommand auto-runs mdsite init when mdsite.yml is missing', async () => {
@@ -462,7 +441,7 @@ describe('command helpers', () => {
462
441
  });
463
442
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
464
443
  serializeConfigMock.mockReturnValue('serialized-config');
465
- readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce('{}');
444
+ readFileMock.mockResolvedValueOnce('{}');
466
445
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
467
446
  await expect(runGenerateCommand('/content')).resolves.toBe('Generated site synced to /content/.output/public');
468
447
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml found'));
@@ -487,10 +466,10 @@ describe('command helpers', () => {
487
466
  hasPreviewArtifactsMock.mockResolvedValueOnce(false);
488
467
  previewRendererInBackgroundMock.mockResolvedValueOnce(1234);
489
468
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
490
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1234). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
469
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1234). URL: http://localhost:3000 Log: /content/.renderer/static.log');
491
470
  expect(hasPreviewArtifactsMock).toHaveBeenCalledWith('/renderer');
492
471
  expect(generateRendererMock).toHaveBeenCalledWith('/renderer', { TEST: '1' });
493
- expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }), '/content/.renderer/preview.log');
472
+ expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }), '/content/.renderer/static.log');
494
473
  consoleSpy.mockRestore();
495
474
  });
496
475
  it('runGenerateCommand syncs renderer output to the configured destination', async () => {
@@ -514,5 +493,80 @@ describe('command helpers', () => {
514
493
  expect(clearRuntimeStateMock).toHaveBeenNthCalledWith(1, '/content', loadedConfig.config, 'start');
515
494
  expect(clearRuntimeStateMock).toHaveBeenNthCalledWith(2, '/content', loadedConfig.config, 'preview');
516
495
  });
496
+ it('runCleanCommand removes both configured working dirs and reports both removals', async () => {
497
+ // `.renderer` and `.output` are reported as existing; everything else throws on access().
498
+ const existing = new Set(['/content/.renderer', '/content/.output']);
499
+ accessMock.mockImplementation(async (p) => {
500
+ if (typeof p === 'string' && existing.has(p))
501
+ return;
502
+ throw new Error('missing');
503
+ });
504
+ // No active tracked start or preview processes.
505
+ readRuntimeStateMock.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
506
+ await expect(runCleanCommand('/content')).resolves.toBe('Removed .renderer and .output from /content.');
507
+ expect(rmMock).toHaveBeenCalledWith('/content/.renderer', { recursive: true, force: true });
508
+ expect(rmMock).toHaveBeenCalledWith('/content/.output', { recursive: true, force: true });
509
+ });
510
+ it('runCleanCommand reports the single existing directory when only one of the two is present', async () => {
511
+ const existing = new Set(['/content/.output']);
512
+ accessMock.mockImplementation(async (p) => {
513
+ if (typeof p === 'string' && existing.has(p))
514
+ return;
515
+ throw new Error('missing');
516
+ });
517
+ readRuntimeStateMock.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
518
+ await expect(runCleanCommand('/content')).resolves.toBe('Removed .output from /content.');
519
+ expect(rmMock).toHaveBeenCalledTimes(2);
520
+ expect(rmMock).toHaveBeenCalledWith('/content/.renderer', { recursive: true, force: true });
521
+ expect(rmMock).toHaveBeenCalledWith('/content/.output', { recursive: true, force: true });
522
+ });
523
+ it('runCleanCommand is a no-op when neither configured working dir exists', async () => {
524
+ const existing = new Set();
525
+ accessMock.mockImplementation(async (p) => {
526
+ if (typeof p === 'string' && existing.has(p))
527
+ return;
528
+ throw new Error('missing');
529
+ });
530
+ readRuntimeStateMock.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
531
+ await expect(runCleanCommand('/content')).resolves.toBe('Nothing to clean in /content.');
532
+ // rm() is still called on both paths so force:true silently ignores missing dirs.
533
+ expect(rmMock).toHaveBeenCalledTimes(2);
534
+ });
535
+ it('runCleanCommand refuses to delete the working dirs while a tracked start process is alive', async () => {
536
+ readRuntimeStateMock
537
+ .mockResolvedValueOnce({ kind: 'start', pid: 44 })
538
+ .mockResolvedValueOnce(null);
539
+ isProcessRunningMock.mockReturnValueOnce(true);
540
+ await expect(runCleanCommand('/content')).rejects.toThrow('mdsite live is running with PID 44. Run `mdsite stop` before `mdsite clean`.');
541
+ expect(rmMock).not.toHaveBeenCalled();
542
+ });
543
+ it('runCleanCommand refuses to delete the working dirs while a tracked preview process is alive', async () => {
544
+ readRuntimeStateMock
545
+ .mockResolvedValueOnce(null)
546
+ .mockResolvedValueOnce({ kind: 'preview', pid: 55 });
547
+ isProcessRunningMock.mockReturnValueOnce(true);
548
+ await expect(runCleanCommand('/content')).rejects.toThrow('mdsite static is running with PID 55. Run `mdsite stop` before `mdsite clean`.');
549
+ expect(rmMock).not.toHaveBeenCalled();
550
+ });
551
+ it('runCleanCommand ignores stale tracked state whose PID is no longer alive', async () => {
552
+ const existing = new Set(['/content/.renderer', '/content/.output']);
553
+ accessMock.mockImplementation(async (p) => {
554
+ if (typeof p === 'string' && existing.has(p))
555
+ return;
556
+ throw new Error('missing');
557
+ });
558
+ readRuntimeStateMock
559
+ .mockResolvedValueOnce({ kind: 'start', pid: 44 })
560
+ .mockResolvedValueOnce({ kind: 'preview', pid: 55 });
561
+ isProcessRunningMock.mockReturnValue(false);
562
+ await expect(runCleanCommand('/content')).resolves.toBe('Removed .renderer and .output from /content.');
563
+ expect(rmMock).toHaveBeenCalledWith('/content/.renderer', { recursive: true, force: true });
564
+ expect(rmMock).toHaveBeenCalledWith('/content/.output', { recursive: true, force: true });
565
+ });
566
+ it('runCleanCommand surfaces the missing-config error from loadMdsiteConfig', async () => {
567
+ loadConfigMock.mockRejectedValueOnce(new Error('Missing mdsite.yml in /content. Run `mdsite init` first.'));
568
+ await expect(runCleanCommand('/content')).rejects.toThrow('Missing mdsite.yml in /content. Run `mdsite init` first.');
569
+ expect(rmMock).not.toHaveBeenCalled();
570
+ });
517
571
  });
518
572
  //# sourceMappingURL=commands.test.js.map