Fix path retrieval for twenty apps in twenty-cli (#14818)

## Context

Now should not confuse package.json from non-twenty apps but check if
$schema correctly targets twenty manifest json
<img width="741" height="422" alt="Screenshot 2025-10-01 at 17 34 45"
src="https://github.com/user-attachments/assets/5b9e729e-8634-4eb8-a2a9-4991a63d49f0"
/>
This commit is contained in:
Weiko
2025-10-01 18:11:42 +02:00
committed by GitHub
parent bb0d37d79f
commit d715533a90
7 changed files with 61 additions and 43 deletions
@@ -1,11 +1,11 @@
import chalk from 'chalk';
import { resolveAppPath } from '../utils/app-path-resolver';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import { v4 } from 'uuid';
import path from 'path';
import { getSchemaUrls } from '../utils/schema-validator';
import { v4 } from 'uuid';
import { resolveAppPath } from '../utils/app-path-resolver';
import { writeJsoncFile } from '../utils/jsonc-parser';
import { getSchemaUrls } from '../utils/schema-validator';
type SyncableEntity = 'agent' | 'object';
@@ -22,11 +22,11 @@ const getFolderName = (entity: SyncableEntity) => {
export class AppAddCommand {
async execute(options: { path?: string }): Promise<void> {
const entity = await this.getEntity();
try {
const appPath = await resolveAppPath(options.path);
const entity = await this.getEntity();
const appExists = await fs.pathExists(appPath);
if (!appExists) {
@@ -49,7 +49,7 @@ export class AppAddCommand {
await writeJsoncFile(entityPath, entityData);
} catch (error) {
console.error(
chalk.red(`Add new ${entity} failed:`),
chalk.red(`Add new entity failed:`),
error instanceof Error ? error.message : error,
);
process.exit(1);
@@ -62,7 +62,6 @@ export class AppAddCommand {
type: 'select',
name: 'entity',
message: `What entity do you want to create?`,
default: '',
choices: ['agent', 'object'],
},
]);
@@ -0,0 +1,6 @@
const SCHEMA_BASE_URL =
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas';
export const APP_MANIFEST_SCHEMA_URL = `${SCHEMA_BASE_URL}/app-manifest.schema.json`;
export const AGENT_SCHEMA_URL = `${SCHEMA_BASE_URL}/agent.schema.json`;
export const OBJECT_SCHEMA_URL = `${SCHEMA_BASE_URL}/object.schema.json`;
@@ -3,6 +3,10 @@ import {
createBasePackageJson,
createReadmeContent,
} from '../app-template';
import {
AGENT_SCHEMA_URL,
APP_MANIFEST_SCHEMA_URL,
} from '../../constants/schemas';
// Mock crypto.randomUUID to make tests deterministic
jest.mock('crypto', () => ({
@@ -17,8 +21,7 @@ describe('app-template', () => {
const basePackageJson = createBasePackageJson(appName, description);
expect(basePackageJson).toEqual({
$schema:
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json',
$schema: APP_MANIFEST_SCHEMA_URL,
standardId: 'mocked-uuid-12345',
label: 'My Test App',
description: 'A Twenty application for my-test-app',
@@ -62,8 +65,7 @@ describe('app-template', () => {
const agent = createAgentManifest(appName);
expect(agent).toEqual({
$schema:
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json',
$schema: AGENT_SCHEMA_URL,
standardId: 'mocked-uuid-12345',
name: 'myTestAppAgent',
label: 'My Test App Agent',
+18 -8
View File
@@ -1,5 +1,6 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { APP_MANIFEST_SCHEMA_URL } from '../constants/schemas';
export const findProjectRoot = async (): Promise<string | null> => {
let currentDir = process.cwd();
@@ -53,13 +54,9 @@ export const findNearbyApps = async (startDir: string): Promise<string[]> => {
for (const item of items) {
if (item.isDirectory()) {
const packageJsonPath = path.join(
searchPath,
item.name,
'package.json',
);
if (await fs.pathExists(packageJsonPath)) {
apps.push(path.join(searchPath, item.name));
const itemPath = path.join(searchPath, item.name);
if (await isValidAppPath(itemPath)) {
apps.push(itemPath);
}
}
}
@@ -73,5 +70,18 @@ export const findNearbyApps = async (startDir: string): Promise<string[]> => {
};
export const isValidAppPath = async (appPath: string): Promise<boolean> => {
return fs.pathExists(path.join(appPath, 'package.json'));
const packageJsonPath = path.join(appPath, 'package.json');
if (!(await fs.pathExists(packageJsonPath))) {
return false;
}
try {
const packageJson = await fs.readJson(packageJsonPath);
// Check if this is a Twenty app by looking for the exact $schema URL
return packageJson.$schema === APP_MANIFEST_SCHEMA_URL;
} catch {
return false;
}
};
@@ -34,10 +34,10 @@ const resolveRelativePath = async (providedPath: string): Promise<string> => {
}
}
throw new Error(`Cannot find package.json at any of these locations:
throw new Error(`Cannot find Twenty app package.json at any of these locations:
- ${fromCwd}
- ${projectRoot ? path.resolve(projectRoot, providedPath) : 'N/A (no project root found)'}
Please check the path or run from the correct directory.`);
};
@@ -60,11 +60,11 @@ const autoDetectAppPath = async (): Promise<string> => {
const suggestions = await findNearbyApps(process.cwd());
let errorMessage =
'No package.json found in current directory or parent directories.';
'No Twenty app found in current directory or parent directories.';
if (suggestions.length > 0) {
errorMessage += '\n\nFound Twenty applications nearby:';
suggestions.forEach((suggestion, i) => {
suggestions.forEach((suggestion: string, i: number) => {
errorMessage += `\n ${i + 1}. ${suggestion}`;
});
errorMessage +=
@@ -77,24 +77,23 @@ const autoDetectAppPath = async (): Promise<string> => {
};
const validateAppPath = async (appPath: string): Promise<string> => {
const hasPackageJson = await fs.pathExists(
path.join(appPath, 'package.json'),
);
if (!(await fs.pathExists(appPath))) {
throw new Error(`Directory does not exist: ${appPath}`);
}
if (!hasPackageJson) {
let errorMessage = `package.json not found in: ${appPath}`;
if (!(await isValidAppPath(appPath))) {
let errorMessage = `Not a valid Twenty app: ${appPath}`;
if (await fs.pathExists(appPath)) {
try {
const files = await fs.readdir(appPath);
errorMessage += `\n\nFiles in directory: ${files.join(', ')}`;
} catch {
errorMessage += '\n\nCould not read directory contents.';
}
const packageJsonPath = path.join(appPath, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
errorMessage +=
'\n\npackage.json found but missing required $schema field for Twenty apps.';
} else {
errorMessage += '\n\nDirectory does not exist.';
errorMessage += '\n\npackage.json not found.';
}
errorMessage += '\n\nRun `twenty app init` to create a new application.';
throw new Error(errorMessage);
}
@@ -1,6 +1,11 @@
import Ajv from 'ajv';
import * as fs from 'fs-extra';
import * as path from 'path';
import {
AGENT_SCHEMA_URL,
APP_MANIFEST_SCHEMA_URL,
OBJECT_SCHEMA_URL,
} from '../constants/schemas';
export class SchemaValidationError extends Error {
constructor(
@@ -66,11 +71,8 @@ export const validateSchema = async (
export const getSchemaUrls = () => {
return {
agent:
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json',
object:
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/object.schema.json',
appManifest:
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json',
agent: AGENT_SCHEMA_URL,
object: OBJECT_SCHEMA_URL,
appManifest: APP_MANIFEST_SCHEMA_URL,
};
};
+1 -1
View File
@@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"module": "CommonJS",
"module": "commonjs",
"target": "ES2022",
"moduleResolution": "node",
"esModuleInterop": true,