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:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"module": "CommonJS",
|
||||
"module": "commonjs",
|
||||
"target": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
|
||||
Reference in New Issue
Block a user