lowdefy 5.6.0 → 6.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.
Files changed (41) hide show
  1. package/dist/commands/agentSetup/agentSetup.js +85 -0
  2. package/dist/commands/agentSetup/agentsMd.js +59 -0
  3. package/dist/commands/agentSetup/buildMcpServerEntry.js +21 -0
  4. package/dist/commands/agentSetup/findProjectRoot.js +39 -0
  5. package/dist/commands/agentSetup/getDevCommand.js +50 -0
  6. package/dist/commands/agentSetup/skillMd.js +48 -0
  7. package/dist/commands/agentSetup/upsertAgentsMdSection.js +63 -0
  8. package/dist/commands/agentSetup/upsertClaudeSettings.js +56 -0
  9. package/dist/commands/agentSetup/upsertMcpServer.js +83 -0
  10. package/dist/commands/agentSetup/writeSkillFile.js +32 -0
  11. package/dist/commands/build/build.js +3 -3
  12. package/dist/commands/dev/dev.js +13 -8
  13. package/dist/commands/dev/resolveMockUser.js +37 -0
  14. package/dist/commands/dev/runDevServer.js +17 -11
  15. package/dist/commands/dockerOutput/dockerOutput.js +99 -0
  16. package/dist/commands/emails/emails.js +84 -0
  17. package/dist/commands/emails/generateEmailShims.js +94 -0
  18. package/dist/commands/emails/installReactEmail.js +53 -0
  19. package/dist/commands/emails/readNotificationArtifacts.js +37 -0
  20. package/dist/commands/emails/runEmailPreview.js +47 -0
  21. package/dist/commands/emails/warnMissingDataKeys.js +45 -0
  22. package/dist/commands/init-docker/Dockerfile +37 -19
  23. package/dist/commands/init-docker/dockerignore +8 -0
  24. package/dist/commands/init-docker/initDocker.js +3 -1
  25. package/dist/commands/init-vercel/README.md +94 -20
  26. package/dist/commands/init-vercel/initVercel.js +12 -2
  27. package/dist/commands/init-vercel/vercel.build.sh +8 -0
  28. package/dist/commands/init-vercel/vercel.install.sh +4 -2
  29. package/dist/commands/init-vercel/vercel.json +5 -0
  30. package/dist/commands/start/runStart.js +1 -2
  31. package/dist/commands/vercelOutput/apiHandler.js +83 -0
  32. package/dist/commands/vercelOutput/vercelOutput.js +138 -0
  33. package/dist/program.js +24 -2
  34. package/dist/utils/checkPortAvailable.js +9 -16
  35. package/dist/utils/copyTracedFiles.js +94 -0
  36. package/dist/utils/findTraceBase.js +45 -0
  37. package/dist/utils/getDirectories.js +2 -1
  38. package/dist/utils/getOptions.js +2 -0
  39. package/dist/utils/installServer.js +31 -23
  40. package/dist/utils/{runNextBuild.js → runClientBuild.js} +11 -14
  41. package/package.json +14 -13
@@ -0,0 +1,85 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import path from 'path';
16
+ import findProjectRoot from './findProjectRoot.js';
17
+ import getDevCommand from './getDevCommand.js';
18
+ import upsertAgentsMdSection from './upsertAgentsMdSection.js';
19
+ import upsertClaudeSettings from './upsertClaudeSettings.js';
20
+ import upsertMcpServer from './upsertMcpServer.js';
21
+ import writeSkillFile from './writeSkillFile.js';
22
+ // Resolves where agent files should be written (the project root an agent is
23
+ // launched from, not the app config directory) and the app's path relative
24
+ // to it, so generated instructions work from that root.
25
+ function resolveDirectories({ context }) {
26
+ const configDirectory = context.directories.config;
27
+ const projectDirectory = context.options.projectDirectory ? path.resolve(context.options.projectDirectory) : findProjectRoot({
28
+ configDirectory
29
+ });
30
+ const relative = path.relative(projectDirectory, configDirectory);
31
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
32
+ context.logger.warn(`Project directory '${projectDirectory}' does not contain the config directory '${configDirectory}' - writing agent files into the config directory instead.`);
33
+ return {
34
+ projectDirectory: configDirectory,
35
+ appPath: ''
36
+ };
37
+ }
38
+ return {
39
+ projectDirectory,
40
+ appPath: relative.split(path.sep).join('/')
41
+ };
42
+ }
43
+ async function agentSetup({ context }) {
44
+ context.logger.info('Setting up this project for AI coding agents.');
45
+ const port = context.options.port;
46
+ const { projectDirectory, appPath } = resolveDirectories({
47
+ context
48
+ });
49
+ if (appPath !== '') {
50
+ context.logger.info(`Project root detected at '${projectDirectory}' - writing agent files there. The Lowdefy config is in '${appPath}/'.`);
51
+ }
52
+ const devCommand = getDevCommand({
53
+ configDirectory: context.directories.config,
54
+ projectDirectory
55
+ });
56
+ const runCommand = appPath === '' ? devCommand : `cd ${appPath} && ${devCommand}`;
57
+ await upsertMcpServer({
58
+ context,
59
+ projectDirectory,
60
+ port
61
+ });
62
+ await upsertClaudeSettings({
63
+ context,
64
+ projectDirectory
65
+ });
66
+ await writeSkillFile({
67
+ context,
68
+ projectDirectory,
69
+ appPath,
70
+ port
71
+ });
72
+ await upsertAgentsMdSection({
73
+ context,
74
+ projectDirectory,
75
+ appPath,
76
+ port,
77
+ devCommand: runCommand
78
+ });
79
+ await context.sendTelemetry();
80
+ context.logger.info('Annotate the app: press Cmd/Ctrl+/ in the browser to point, draw, and copy feedback, then paste it into your agent session.');
81
+ context.logger.info({
82
+ spin: 'succeed'
83
+ }, 'Project set up for AI coding agents.');
84
+ }
85
+ export default agentSetup;
@@ -0,0 +1,59 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ function agentsMd({ port, devCommand, appPath }) {
16
+ const appDescription = appPath === '' ? 'This project is a [Lowdefy](https://lowdefy.com) app' : `This project contains a [Lowdefy](https://lowdefy.com) app in \`${appPath}/\``;
17
+ return `## Lowdefy
18
+
19
+ ${appDescription}: a web app defined in YAML config files
20
+ rather than hand-written code. Pages compose **blocks** (UI components), **operators** (logic
21
+ like \`_if\`, \`_get\`, \`_state\`), **actions** (event handlers), and **connections/requests**
22
+ (databases and APIs).
23
+
24
+ ### Running the app
25
+
26
+ Start the development server with:
27
+
28
+ \`\`\`bash
29
+ ${devCommand}
30
+ \`\`\`
31
+
32
+ This serves the app at \`http://localhost:${port}\` and hot-reloads on config changes.
33
+
34
+ ### Looking up types, schemas, and docs
35
+
36
+ While the dev server is running, it serves documentation for every block, operator, action,
37
+ connection, and request type installed in this project (including local plugins) at
38
+ \`http://localhost:${port}/lowdefy-docs\`, and as MCP tools via the \`lowdefy-docs\` MCP server
39
+ (see \`.mcp.json\`).
40
+
41
+ **Never guess type names or properties.** Before writing or editing Lowdefy config:
42
+
43
+ 1. List available types: call \`lowdefy_list_types\` (or \`GET /lowdefy-docs/{kind}\`, e.g.
44
+ \`/lowdefy-docs/blocks\`) — this includes this project's local plugins.
45
+ 2. Get the exact schema: call \`lowdefy_get_schema\` (or \`GET /lowdefy-docs/schema/{kind}/{type}\`).
46
+ 3. See real usage: call \`lowdefy_get_examples\` (or \`GET /lowdefy-docs/examples/{type}\`).
47
+ 4. Read concept docs or search: call \`lowdefy_get_doc\` or \`lowdefy_search_docs\` (or
48
+ \`GET /lowdefy-docs/content/{slug}\`, \`GET /lowdefy-docs/search?q=...\`).
49
+
50
+ ### Visual feedback
51
+
52
+ While the dev server is running, developers can press \`Cmd/Ctrl+/\` in the browser to point at,
53
+ draw on, and comment on the running app; the annotation helper copies an agent-readable feedback
54
+ block to their clipboard, which they paste into the agent session. Pasted blocks start with
55
+ "Feedback:" and include the config file and line each annotation refers to — treat them as
56
+ precise UI feedback.
57
+ `;
58
+ }
59
+ export default agentsMd;
@@ -0,0 +1,21 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ function buildMcpServerEntry({ port }) {
16
+ return {
17
+ type: 'http',
18
+ url: `http://localhost:${port}/lowdefy-docs/mcp`
19
+ };
20
+ }
21
+ export default buildMcpServerEntry;
@@ -0,0 +1,39 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ // Coding agents (Claude Code, Codex, Cursor) discover .mcp.json, .claude/
18
+ // skills, and instructions files from the directory they are launched in —
19
+ // in practice the git checkout root. In a monorepo where the Lowdefy app
20
+ // lives in a subdirectory, agent files written into the app directory are
21
+ // never found, so agent-setup targets the nearest ancestor containing .git
22
+ // instead. Falls back to the config directory when the app is not inside a
23
+ // git repository (single-directory projects keep today's behavior).
24
+ function findProjectRoot({ configDirectory }) {
25
+ let directory = configDirectory;
26
+ for(;;){
27
+ // .git is a directory in normal checkouts but a file in worktrees and
28
+ // submodules — existsSync covers both.
29
+ if (fs.existsSync(path.join(directory, '.git'))) {
30
+ return directory;
31
+ }
32
+ const parent = path.dirname(directory);
33
+ if (parent === directory) {
34
+ return configDirectory;
35
+ }
36
+ directory = parent;
37
+ }
38
+ }
39
+ export default findProjectRoot;
@@ -0,0 +1,50 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ import { type } from '@lowdefy/helpers';
18
+ const defaultCommand = 'npx lowdefy dev';
19
+ // Prefer a package.json script that already runs "lowdefy dev" over the npx fallback,
20
+ // using the package manager implied by the project's lockfile. Scripts come
21
+ // from the app's own package.json; the lockfile may live at the workspace
22
+ // root in a monorepo, so both directories are checked.
23
+ function getDevCommand({ configDirectory, projectDirectory = configDirectory }) {
24
+ const packageJsonPath = path.join(configDirectory, 'package.json');
25
+ if (!fs.existsSync(packageJsonPath)) {
26
+ return defaultCommand;
27
+ }
28
+ let packageJson;
29
+ try {
30
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
31
+ } catch {
32
+ return defaultCommand;
33
+ }
34
+ const scripts = packageJson.scripts ?? {};
35
+ const scriptName = Object.keys(scripts).find((name)=>scripts[name].includes('lowdefy dev'));
36
+ if (type.isNone(scriptName)) {
37
+ return defaultCommand;
38
+ }
39
+ function lockfileExists(lockfileName) {
40
+ return fs.existsSync(path.join(configDirectory, lockfileName)) || fs.existsSync(path.join(projectDirectory, lockfileName));
41
+ }
42
+ if (lockfileExists('pnpm-lock.yaml')) {
43
+ return `pnpm ${scriptName}`;
44
+ }
45
+ if (lockfileExists('yarn.lock')) {
46
+ return `yarn ${scriptName}`;
47
+ }
48
+ return `npm run ${scriptName}`;
49
+ }
50
+ export default getDevCommand;
@@ -0,0 +1,48 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ function skillMd({ port, appPath }) {
16
+ const appPathLine = appPath === '' ? '' : `\nThe Lowdefy app config (lowdefy.yaml, pages, requests) lives in \`${appPath}/\`.\n`;
17
+ return `---
18
+ name: lowdefy-config
19
+ description: Use when writing or editing Lowdefy YAML config — pages, blocks, operators, actions, connections, or requests. Looks up exact type names, schemas, and examples from the running dev server instead of guessing.
20
+ ---
21
+
22
+ # Writing Lowdefy config
23
+
24
+ The dev server serves docs for everything installed in this project at
25
+ \`http://localhost:${port}/lowdefy-docs\` (also as MCP tools via the \`lowdefy-docs\` server).
26
+ ${appPathLine}
27
+ Never guess type names or properties. Before writing config:
28
+
29
+ 1. Call \`lowdefy_list_types\` (or \`GET /lowdefy-docs/blocks\`, \`/lowdefy-docs/operators\`,
30
+ \`/lowdefy-docs/actions\`, \`/lowdefy-docs/connections\`, \`/lowdefy-docs/requests\`) to find the exact
31
+ type name — this includes this project's local plugins.
32
+ 2. Call \`lowdefy_get_schema\` (or \`GET /lowdefy-docs/schema/{kind}/{type}\`) for the
33
+ exact properties and events of that type.
34
+ 3. Call \`lowdefy_get_examples\` (or \`GET /lowdefy-docs/examples/{type}\`) to see real
35
+ usage YAML for blocks.
36
+ 4. For concepts (state, operators, events, requests), call \`lowdefy_get_doc\`
37
+ or \`lowdefy_search_docs\`.
38
+
39
+ ## Visual feedback
40
+
41
+ Developers can press \`Cmd/Ctrl+/\` in the running app to point at, draw on, and comment on
42
+ what's on screen; the annotation helper copies an agent-readable feedback block to their
43
+ clipboard, which they paste to you. Pasted blocks start with "Feedback:" and include the
44
+ config file and line each annotation refers to — treat them as precise UI feedback and use
45
+ \`lowdefy_inspect_state\` for the page's live state.
46
+ `;
47
+ }
48
+ export default skillMd;
@@ -0,0 +1,63 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import path from 'path';
16
+ import { readFile, writeFile } from '@lowdefy/node-utils';
17
+ import { type } from '@lowdefy/helpers';
18
+ import agentsMd from './agentsMd.js';
19
+ const lowdefyHeadingPattern = /^##\s+Lowdefy\b/m;
20
+ // Appends a "## Lowdefy" section to the project's existing agent instructions
21
+ // file instead of creating a competing one: an existing AGENTS.md wins, then
22
+ // an existing CLAUDE.md, and only when neither exists is an AGENTS.md
23
+ // created. Never overwrites — the file may already document the rest of the
24
+ // project.
25
+ async function upsertAgentsMdSection({ context, projectDirectory, appPath, port, devCommand }) {
26
+ const candidates = [
27
+ 'AGENTS.md',
28
+ 'CLAUDE.md'
29
+ ].map((fileName)=>({
30
+ fileName,
31
+ filePath: path.join(projectDirectory, fileName)
32
+ }));
33
+ const existingFiles = [];
34
+ for (const candidate of candidates){
35
+ const content = await readFile(candidate.filePath);
36
+ if (!type.isNone(content)) {
37
+ existingFiles.push({
38
+ ...candidate,
39
+ content
40
+ });
41
+ }
42
+ }
43
+ const withSection = existingFiles.find((file)=>lowdefyHeadingPattern.test(file.content));
44
+ if (withSection) {
45
+ context.logger.info(`'${withSection.fileName}' already has a 'Lowdefy' section - skipping.`);
46
+ return;
47
+ }
48
+ const section = agentsMd({
49
+ port,
50
+ devCommand,
51
+ appPath
52
+ });
53
+ if (existingFiles.length === 0) {
54
+ await writeFile(path.join(projectDirectory, 'AGENTS.md'), `${section}`);
55
+ context.logger.info("Created 'AGENTS.md'.");
56
+ return;
57
+ }
58
+ const target = existingFiles[0];
59
+ const updated = `${target.content.replace(/\s+$/, '')}\n\n${section}`;
60
+ await writeFile(target.filePath, updated);
61
+ context.logger.info(`Added a 'Lowdefy' section to '${target.fileName}'.`);
62
+ }
63
+ export default upsertAgentsMdSection;
@@ -0,0 +1,56 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import path from 'path';
16
+ import { readFile, writeFile } from '@lowdefy/node-utils';
17
+ import { type } from '@lowdefy/helpers';
18
+ const MCP_SERVER_NAME = 'lowdefy-docs';
19
+ // Pre-approves the lowdefy-docs MCP server for Claude Code so no developer is
20
+ // prompted to trust it. Written to the committed '.claude/settings.json'
21
+ // rather than the gitignored '.claude/settings.local.json' on purpose, so the
22
+ // whole team inherits the approval from version control.
23
+ async function upsertClaudeSettings({ context, projectDirectory }) {
24
+ const settingsRelativePath = path.join('.claude', 'settings.json');
25
+ const settingsPath = path.join(projectDirectory, settingsRelativePath);
26
+ const existing = await readFile(settingsPath);
27
+ if (type.isNone(existing)) {
28
+ const settings = {
29
+ enabledMcpjsonServers: [
30
+ MCP_SERVER_NAME
31
+ ]
32
+ };
33
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
34
+ context.logger.info(`Created '${settingsRelativePath}'.`);
35
+ return;
36
+ }
37
+ let settings;
38
+ try {
39
+ settings = JSON.parse(existing);
40
+ } catch {
41
+ context.logger.warn(`Could not parse existing '${settingsRelativePath}' as JSON - leaving it unchanged. Add '${MCP_SERVER_NAME}' to 'enabledMcpjsonServers' manually.`);
42
+ return;
43
+ }
44
+ const enabled = type.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
45
+ if (enabled.includes(MCP_SERVER_NAME)) {
46
+ context.logger.info(`'${settingsRelativePath}' already enables the '${MCP_SERVER_NAME}' MCP server - leaving it unchanged.`);
47
+ return;
48
+ }
49
+ settings.enabledMcpjsonServers = [
50
+ ...enabled,
51
+ MCP_SERVER_NAME
52
+ ];
53
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
54
+ context.logger.info(`Enabled the '${MCP_SERVER_NAME}' MCP server in '${settingsRelativePath}'.`);
55
+ }
56
+ export default upsertClaudeSettings;
@@ -0,0 +1,83 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ import { readFile, writeFile } from '@lowdefy/node-utils';
18
+ import { type } from '@lowdefy/helpers';
19
+ import buildMcpServerEntry from './buildMcpServerEntry.js';
20
+ // A .mcp.json left in the app subdirectory by a pre-monorepo-fix run of
21
+ // agent-setup is never discovered by agents launched from the project root.
22
+ function warnStaleConfigDirMcpJson({ context, projectDirectory }) {
23
+ const configDirectory = context.directories.config;
24
+ if (projectDirectory === configDirectory) {
25
+ return;
26
+ }
27
+ if (fs.existsSync(path.join(configDirectory, '.mcp.json'))) {
28
+ context.logger.warn(`Found a '.mcp.json' in '${configDirectory}' - agents launched from the project root will not discover it. If it only contains the 'lowdefy-docs' server, it can be removed.`);
29
+ }
30
+ }
31
+ // Merges the "lowdefy-docs" MCP server into an existing .mcp.json instead of overwriting it,
32
+ // since a project's .mcp.json may already declare other MCP servers.
33
+ async function upsertMcpServer({ context, projectDirectory, port }) {
34
+ const mcpJsonPath = path.join(projectDirectory, '.mcp.json');
35
+ const existing = await readFile(mcpJsonPath);
36
+ const mcpServerEntry = buildMcpServerEntry({
37
+ port
38
+ });
39
+ if (type.isNone(existing)) {
40
+ const mcpJson = {
41
+ mcpServers: {
42
+ 'lowdefy-docs': mcpServerEntry
43
+ }
44
+ };
45
+ await writeFile(mcpJsonPath, `${JSON.stringify(mcpJson, null, 2)}\n`);
46
+ context.logger.info("Created '.mcp.json'.");
47
+ warnStaleConfigDirMcpJson({
48
+ context,
49
+ projectDirectory
50
+ });
51
+ return;
52
+ }
53
+ let mcpJson;
54
+ try {
55
+ mcpJson = JSON.parse(existing);
56
+ } catch {
57
+ context.logger.warn("Could not parse existing '.mcp.json' as JSON - leaving it unchanged. Add the 'lowdefy-docs' MCP server manually:\n" + JSON.stringify({
58
+ mcpServers: {
59
+ 'lowdefy-docs': mcpServerEntry
60
+ }
61
+ }, null, 2));
62
+ return;
63
+ }
64
+ mcpJson.mcpServers = mcpJson.mcpServers ?? {};
65
+ const currentEntry = mcpJson.mcpServers['lowdefy-docs'];
66
+ if (!type.isNone(currentEntry)) {
67
+ if (currentEntry.url === mcpServerEntry.url) {
68
+ context.logger.info("'.mcp.json' already has a 'lowdefy-docs' MCP server - leaving it unchanged.");
69
+ } else {
70
+ // Likely a second Lowdefy app in the same project, or a stale port.
71
+ context.logger.warn(`'.mcp.json' already has a 'lowdefy-docs' MCP server pointing at '${currentEntry.url}', expected '${mcpServerEntry.url}' - leaving it unchanged. Rerun with --port to match, or add this app manually under a different server name.`);
72
+ }
73
+ return;
74
+ }
75
+ mcpJson.mcpServers['lowdefy-docs'] = mcpServerEntry;
76
+ await writeFile(mcpJsonPath, `${JSON.stringify(mcpJson, null, 2)}\n`);
77
+ context.logger.info("Added the 'lowdefy-docs' MCP server to '.mcp.json'.");
78
+ warnStaleConfigDirMcpJson({
79
+ context,
80
+ projectDirectory
81
+ });
82
+ }
83
+ export default upsertMcpServer;
@@ -0,0 +1,32 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ import { writeFile } from '@lowdefy/node-utils';
18
+ import skillMd from './skillMd.js';
19
+ const skillRelativePath = path.join('.claude', 'skills', 'lowdefy-config', 'SKILL.md');
20
+ async function writeSkillFile({ context, projectDirectory, appPath, port }) {
21
+ const skillPath = path.join(projectDirectory, skillRelativePath);
22
+ if (fs.existsSync(skillPath)) {
23
+ context.logger.info(`'${skillRelativePath}' already exists - skipping.`);
24
+ return;
25
+ }
26
+ await writeFile(skillPath, skillMd({
27
+ port,
28
+ appPath
29
+ }));
30
+ context.logger.info(`Created '${skillRelativePath}'.`);
31
+ }
32
+ export default writeSkillFile;
@@ -18,7 +18,7 @@ import getServer from '../../utils/getServer.js';
18
18
  import installServer from '../../utils/installServer.js';
19
19
  import resetServerPackageJson from '../../utils/resetServerPackageJson.js';
20
20
  import runLowdefyBuild from '../../utils/runLowdefyBuild.js';
21
- import runNextBuild from '../../utils/runNextBuild.js';
21
+ import runClientBuild from '../../utils/runClientBuild.js';
22
22
  const serverPackages = {
23
23
  e2e: '@lowdefy/server-e2e'
24
24
  };
@@ -55,8 +55,8 @@ async function build({ context }) {
55
55
  context,
56
56
  directory
57
57
  });
58
- if (context.options.nextBuild !== false) {
59
- await runNextBuild({
58
+ if (context.options.clientBuild !== false && context.options.nextBuild !== false) {
59
+ await runClientBuild({
60
60
  context,
61
61
  directory
62
62
  });
@@ -12,28 +12,33 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import addCustomPluginsAsDeps from '../../utils/addCustomPluginsAsDeps.js';
16
- import checkPortAvailable from '../../utils/checkPortAvailable.js';
15
+ */ import { findAvailablePort } from '@lowdefy/node-utils';
16
+ import addCustomPluginsAsDeps from '../../utils/addCustomPluginsAsDeps.js';
17
17
  import ensurePnpmWorkspaceYaml from '../../utils/ensurePnpmWorkspaceYaml.js';
18
18
  import installServer from '../../utils/installServer.js';
19
- import resetServerPackageJson from '../../utils/resetServerPackageJson.js';
20
19
  import runDevServer from './runDevServer.js';
21
20
  import getServer from '../../utils/getServer.js';
22
21
  async function dev({ context }) {
23
22
  const directory = context.directories.dev;
24
23
  context.logger.info('Starting development server.');
25
- await checkPortAvailable({
24
+ const port = await findAvailablePort({
26
25
  port: context.options.port
27
26
  });
27
+ if (port !== context.options.port) {
28
+ context.logger.warn(`Port ${context.options.port} is in use, using port ${port} instead.`);
29
+ context.options.port = port;
30
+ }
28
31
  await getServer({
29
32
  context,
30
33
  packageName: '@lowdefy/server-dev',
31
34
  directory
32
35
  });
33
- await resetServerPackageJson({
34
- context,
35
- directory
36
- });
36
+ // Dev keeps the plugin dependency set accumulated by previous sessions —
37
+ // resetting package.json to package.original.json here would uninstall
38
+ // JIT-added plugin packages, churn the lockfile (invalidating Vite's
39
+ // optimizer cache), and force reinstalls on first navigation. The full
40
+ // `lowdefy build` recomputes the complete set, and getServer replaces
41
+ // package.json when the lowdefy version changes.
37
42
  await addCustomPluginsAsDeps({
38
43
  context,
39
44
  directory
@@ -0,0 +1,37 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ const defaultMockUser = {
16
+ sub: 'lowdefy-dev',
17
+ id: 'lowdefy-dev',
18
+ name: 'Lowdefy Dev User',
19
+ roles: []
20
+ };
21
+ // Turns the `--mock-user [user]` CLI option into the JSON string the dev server
22
+ // reads from LOWDEFY_DEV_USER. Bare flag (commander yields `true`) → a default
23
+ // roleless user. A supplied value is validated here so an invalid `--mock-user`
24
+ // fails at the CLI with a clear message instead of deep in the server runtime.
25
+ function resolveMockUser(mockUser) {
26
+ if (mockUser === true) {
27
+ return JSON.stringify(defaultMockUser);
28
+ }
29
+ try {
30
+ return JSON.stringify(JSON.parse(mockUser));
31
+ } catch (error) {
32
+ throw new Error(`Invalid --mock-user value. Expected a JSON user object, received ${JSON.stringify(mockUser)}.`, {
33
+ cause: error
34
+ });
35
+ }
36
+ }
37
+ export default resolveMockUser;