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.
- package/dist/commands/agentSetup/agentSetup.js +85 -0
- package/dist/commands/agentSetup/agentsMd.js +59 -0
- package/dist/commands/agentSetup/buildMcpServerEntry.js +21 -0
- package/dist/commands/agentSetup/findProjectRoot.js +39 -0
- package/dist/commands/agentSetup/getDevCommand.js +50 -0
- package/dist/commands/agentSetup/skillMd.js +48 -0
- package/dist/commands/agentSetup/upsertAgentsMdSection.js +63 -0
- package/dist/commands/agentSetup/upsertClaudeSettings.js +56 -0
- package/dist/commands/agentSetup/upsertMcpServer.js +83 -0
- package/dist/commands/agentSetup/writeSkillFile.js +32 -0
- package/dist/commands/build/build.js +3 -3
- package/dist/commands/dev/dev.js +13 -8
- package/dist/commands/dev/resolveMockUser.js +37 -0
- package/dist/commands/dev/runDevServer.js +17 -11
- package/dist/commands/dockerOutput/dockerOutput.js +99 -0
- package/dist/commands/emails/emails.js +84 -0
- package/dist/commands/emails/generateEmailShims.js +94 -0
- package/dist/commands/emails/installReactEmail.js +53 -0
- package/dist/commands/emails/readNotificationArtifacts.js +37 -0
- package/dist/commands/emails/runEmailPreview.js +47 -0
- package/dist/commands/emails/warnMissingDataKeys.js +45 -0
- package/dist/commands/init-docker/Dockerfile +37 -19
- package/dist/commands/init-docker/dockerignore +8 -0
- package/dist/commands/init-docker/initDocker.js +3 -1
- package/dist/commands/init-vercel/README.md +94 -20
- package/dist/commands/init-vercel/initVercel.js +12 -2
- package/dist/commands/init-vercel/vercel.build.sh +8 -0
- package/dist/commands/init-vercel/vercel.install.sh +4 -2
- package/dist/commands/init-vercel/vercel.json +5 -0
- package/dist/commands/start/runStart.js +1 -2
- package/dist/commands/vercelOutput/apiHandler.js +83 -0
- package/dist/commands/vercelOutput/vercelOutput.js +138 -0
- package/dist/program.js +24 -2
- package/dist/utils/checkPortAvailable.js +9 -16
- package/dist/utils/copyTracedFiles.js +94 -0
- package/dist/utils/findTraceBase.js +45 -0
- package/dist/utils/getDirectories.js +2 -1
- package/dist/utils/getOptions.js +2 -0
- package/dist/utils/installServer.js +31 -23
- package/dist/utils/{runNextBuild.js → runClientBuild.js} +11 -14
- 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
|
|
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
|
|
58
|
+
if (context.options.clientBuild !== false && context.options.nextBuild !== false) {
|
|
59
|
+
await runClientBuild({
|
|
60
60
|
context,
|
|
61
61
|
directory
|
|
62
62
|
});
|
package/dist/commands/dev/dev.js
CHANGED
|
@@ -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
|
|
16
|
-
import
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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;
|