Add scripts to publish cli tools (#17914)

- moves workspace:* dependencies to dev-dependencies to avoid spreading
them in npm releases
- remove fix on rollup.external
- remove prepublishOnly and postpublish scripts
- set bundle packages to private
- add release-dump-version that update package.json version before
releasing to npm
- add release-verify-build that check no externalized twenty package
exists in `dist` before releasing to npm
- works with new release github action here ->
https://github.com/twentyhq/twenty-infra/pull/397
This commit is contained in:
martmull 2026-02-13 16:43:32 +01:00 committed by GitHub
parent e7b3f65c0c
commit 0befb021d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 149 additions and 163 deletions

View file

@ -20,6 +20,7 @@ jobs:
with:
files: |
packages/create-twenty-app/**
!packages/create-twenty-app/package.json
create-app-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'

View file

@ -28,11 +28,14 @@ jobs:
packages/twenty-ui/**
packages/twenty-shared/**
packages/twenty-sdk/**
!packages/twenty-sdk/package.json
changed-files-check-e2e:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
playwright.config.ts
.github/workflows/ci-front.yaml
front-sb-build:

View file

@ -18,6 +18,7 @@ jobs:
with:
files: |
packages/twenty-sdk/**
!packages/twenty-sdk/package.json
sdk-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'

View file

@ -35,29 +35,29 @@ cd my-twenty-app
corepack enable
yarn install
# Get help
yarn run help
# Get help and list all available commands
yarn twenty help
# Authenticate using your API key (you'll be prompted)
yarn auth:login
yarn twenty auth:login
# Add a new entity to your application (guided)
yarn entity:add
yarn twenty entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
yarn twenty app:generate
# Start dev mode: watches, builds, and syncs local changes to your workspace
yarn app:dev
yarn twenty app:dev
# Watch your application's function logs
yarn function:logs
yarn twenty function:logs
# Execute a function with a JSON payload
yarn function:execute -n my-function -p '{"key": "value"}'
yarn twenty function:execute -n my-function -p '{"key": "value"}'
# Uninstall the application from the current workspace
yarn app:uninstall
yarn twenty app:uninstall
```
## What gets scaffolded
@ -67,13 +67,14 @@ yarn app:uninstall
- `logic-functions/hello-world.ts` - Example logic function with HTTP trigger
- `front-components/hello-world.tsx` - Example front component
- TypeScript configuration
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
## Next steps
- Use `yarn auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn entity:add` (logic functions, front components, objects, roles).
- Use `yarn app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Keep your types uptodate using `yarn app:generate`.
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Keep your types uptodate using `yarn twenty app:generate`.
## Publish your application
@ -101,8 +102,8 @@ git push
Our team reviews contributions for quality, security, and reusability before merging.
## Troubleshooting
- Auth prompts not appearing: run `yarn auth:login` again and verify the API key permissions.
- Types not generated: ensure `yarn app:generate` runs without errors, then restart `yarn app:dev`.
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty app:generate` runs without errors, then restart `yarn twenty app:dev`.
## Contributing
- See our [GitHub](https://github.com/twentyhq/twenty)

View file

@ -10,9 +10,7 @@
"package.json"
],
"scripts": {
"build": "npx rimraf dist && npx vite build",
"prepublishOnly": "tsx ../twenty-utils/pack-scripts/pre-publish-only.ts",
"postpublish": "tsx ../twenty-utils/pack-scripts/post-publish.ts"
"build": "npx rimraf dist && npx vite build"
},
"keywords": [
"twenty",
@ -38,7 +36,6 @@
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"lodash.startcase": "^4.4.0",
"twenty-shared": "workspace:*",
"uuid": "^13.0.0"
},
"devDependencies": {
@ -48,6 +45,8 @@
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"twenty-sdk": "workspace:*",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",

View file

@ -5,34 +5,36 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn auth:login
yarn twenty auth:login
```
Then, start development mode to sync your app and watch for changes:
```bash
yarn app:dev
yarn twenty app:dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Available Commands
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Authentication
yarn auth:login # Authenticate with Twenty
yarn auth:logout # Remove credentials
yarn auth:status # Check auth status
yarn auth:switch # Switch default workspace
yarn auth:list # List all configured workspaces
yarn twenty auth:login # Authenticate with Twenty
yarn twenty auth:logout # Remove credentials
yarn twenty auth:status # Check auth status
yarn twenty auth:switch # Switch default workspace
yarn twenty auth:list # List all configured workspaces
# Application
yarn app:dev # Start dev mode (watch, build, and sync)
yarn entity:add # Add a new entity (function, front-component, object, role)
yarn app:generate # Generate typed Twenty client
yarn function:logs # Stream function logs
yarn function:execute # Execute a function with JSON payload
yarn app:uninstall # Uninstall app from workspace
yarn twenty app:dev # Start dev mode (watch, build, and sync)
yarn twenty entity:add # Add a new entity (function, front-component, object, role)
yarn twenty app:generate # Generate typed Twenty client
yarn twenty function:logs # Stream function logs
yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## Learn More

View file

@ -70,8 +70,8 @@ describe('copyBaseApplicationProject', () => {
const packageJson = await fs.readJson(packageJsonPath);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.version).toBe('0.1.0');
expect(packageJson.dependencies['twenty-sdk']).toBe('0.5.2');
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
expect(packageJson.dependencies['twenty-sdk']).toBe('latest');
expect(packageJson.scripts['twenty']).toBe('twenty');
});
it('should create .gitignore file', async () => {

View file

@ -261,23 +261,12 @@ const createPackageJson = async ({
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
twenty: 'twenty',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': '0.5.2',
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',

View file

@ -82,13 +82,11 @@ export default defineConfig(() => {
return true;
}
const deps = Object.entries(
const deps = Object.keys(
(packageJson as PackageJson).dependencies || {},
).filter(([_, version]) => !version?.startsWith('workspace:'));
return deps.some(
([dep, _]) => id === dep || id.startsWith(dep + '/'),
);
return deps.some((dep) => id === dep || id.startsWith(dep + '/'));
},
output: [
{

View file

@ -35,32 +35,32 @@ corepack enable
yarn install
# Authenticate using your API key (you'll be prompted)
yarn auth:login
yarn twenty auth:login
# Start dev mode: automatically syncs local changes to your workspace
yarn app:dev
yarn twenty app:dev
```
From here you can:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn entity:add
yarn twenty entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
yarn twenty app:generate
# Watch your application's function logs
yarn function:logs
yarn twenty function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Uninstall the application from the current workspace
yarn app:uninstall
yarn twenty app:uninstall
# Display commands' help
yarn help
yarn twenty help
```
See also: the CLI reference pages for [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) and [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@ -101,7 +101,7 @@ my-twenty-app/
At a high level:
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and authentication commands that delegate to the local `twenty` CLI.
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus a `twenty` script that delegates to the local `twenty` CLI. Run `yarn twenty help` to list all available commands.
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `dist/`, `build/`, coverage folders, log files, and `.env*` files.
- **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project.
- **.nvmrc**: Pins the Node.js version expected by the project.
@ -140,12 +140,12 @@ export default defineObject({
Later commands will add more files and folders:
- `yarn app:generate` will create a `generated/` folder (typed Twenty client + workspace types).
- `yarn entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
- `yarn twenty app:generate` will create a `generated/` folder (typed Twenty client + workspace types).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
## Authentication
The first time you run `yarn auth:login`, you'll be prompted for:
The first time you run `yarn twenty auth:login`, you'll be prompted for:
- API URL (defaults to http://localhost:3000 or your current workspace profile)
- API key
@ -156,25 +156,25 @@ Your credentials are stored per-user in `~/.twenty/config.json`. You can maintai
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
yarn twenty auth:status
```
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
Once you've switched workspaces with `yarn twenty auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
## Use the SDK resources (types & config)
@ -274,7 +274,7 @@ Key points:
- The `universalIdentifier` must be unique and stable across deployments.
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
- The `fields` array is optional — you can define objects without custom fields.
- You can scaffold new objects using `yarn entity:add`, which guides you through naming, fields, and relationships.
- You can scaffold new objects using `yarn twenty entity:add`, which guides you through naming, fields, and relationships.
<Note>
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields such as `name`, `createdAt`, `updatedAt`, `createdBy`, `position`, and `deletedAt`. You don't need to define these in your `fields` array — only add your custom fields.
@ -541,7 +541,7 @@ const handler = async (event: RoutePayload) => {
You can create new functions in two ways:
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
- **Manual**: Create a new `*.logic-function.ts` file and use `defineLogicFunction()`, following the same pattern.
### Front components
@ -573,16 +573,16 @@ Key points:
- Front components are React components that render in isolated contexts within Twenty.
- Use the `*.front-component.tsx` file suffix for automatic detection.
- The `component` field references your React component.
- Components are built and synced automatically during `yarn app:dev`.
- Components are built and synced automatically during `yarn twenty app:dev`.
You can create new front components in two ways:
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new front component.
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
### Generated typed client
Run yarn app:generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
Run `yarn twenty app:generate` to create a local typed client in `generated/` based on your workspace schema. Use it in your functions:
```typescript
import Twenty from '~/generated';
@ -591,7 +591,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
The client is re-generated by `yarn app:generate`. Re-run after changing your objects or when onboarding to a new workspace.
The client is re-generated by `yarn twenty app:generate`. Re-run after changing your objects or when onboarding to a new workspace.
#### Runtime credentials in logic functions
@ -612,40 +612,29 @@ Explore a minimal, end-to-end example that demonstrates objects, logic functions
## Manual setup (without the scaffolder)
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire scripts in your package.json:
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire a single script in your package.json:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Then add scripts like these:
Then add a `twenty` script:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:generate`, etc.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, etc.
## Troubleshooting
- Authentication errors: run `yarn auth:login` and ensure your API key has the required permissions.
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
- Types or client missing/outdated: run `yarn app:generate`.
- Dev mode not syncing: ensure `yarn app:dev` is running and that changes are not ignored by your environment.
- Types or client missing/outdated: run `yarn twenty app:generate`.
- Dev mode not syncing: ensure `yarn twenty app:dev` is running and that changes are not ignored by your environment.
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322

View file

@ -60,6 +60,8 @@ Commands:
help [command] display help for command
```
In a scaffolded project (via `create-twenty-app`), use `yarn twenty <command>` instead of calling `twenty` directly. For example: `yarn twenty help`, `yarn twenty app:dev`, etc.
## Global Options
- `--workspace <name>`: Use a specific workspace configuration profile. Defaults to `default`. See Configuration for details.

View file

@ -13,9 +13,7 @@
"package.json"
],
"scripts": {
"build": "npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts",
"prepublishOnly": "tsx ../twenty-utils/pack-scripts/pre-publish-only.ts",
"postpublish": "tsx ../twenty-utils/pack-scripts/post-publish.ts"
"build": "npx rimraf dist && npx vite build"
},
"keywords": [
"twenty",
@ -68,8 +66,6 @@
"lodash.camelcase": "^4.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"twenty-shared": "workspace:*",
"twenty-ui": "workspace:*",
"typescript": "^5.9.2",
"uuid": "^13.0.0",
"vite": "^7.0.0",
@ -92,6 +88,8 @@
"storybook": "^10.1.11",
"ts-morph": "^25.0.0",
"tsx": "^4.7.0",
"twenty-shared": "workspace:*",
"twenty-ui": "workspace:*",
"vite-plugin-dts": "^4.5.4",
"wait-on": "^7.2.0"
},

View file

@ -1,11 +1,10 @@
import path from 'path';
import { PackageJson } from 'type-fest';
import { type PackageJson } from 'type-fest';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import packageJson from './package.json';
const entries = ['src/ui/index.ts', 'src/front-component-renderer/index.ts'];
const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
@ -75,13 +74,11 @@ export default defineConfig(() => {
warn(warning);
},
external: (id: string) => {
const deps = Object.entries(
const deps = Object.keys(
(packageJson as PackageJson).dependencies || {},
).filter(([_, version]) => !version?.startsWith('workspace:'));
return deps.some(
([dep, _]) => id === dep || id.startsWith(dep + '/'),
);
return deps.some((dep) => id === dep || id.startsWith(dep + '/'));
},
output: [
{

View file

@ -48,13 +48,11 @@ export default defineConfig(() => {
return true;
}
const deps = Object.entries(
const deps = Object.keys(
(packageJson as PackageJson).dependencies || {},
).filter(([_, version]) => !version?.startsWith('workspace:'));
return deps.some(
([dep]) => id === dep || id.startsWith(dep + '/'),
);
return deps.some((dep) => id === dep || id.startsWith(dep + '/'));
},
output: [
{

View file

@ -1,5 +1,6 @@
{
"name": "twenty-shared",
"private": true,
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",

View file

@ -1,5 +1,6 @@
{
"name": "twenty-ui",
"private": true,
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"style": "./dist/style.css",

View file

@ -0,0 +1,28 @@
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
const verifyBuildPackagesBundleTwentyDependencies = () => {
const distPath = resolve('dist/cli.cjs');
if (!existsSync(distPath)) {
console.error(`Build check failed: ${distPath} does not exist`);
process.exit(1);
}
const content = readFileSync(distPath, 'utf8');
const filePath = resolve('package.json');
const pkg = JSON.parse(readFileSync(filePath, 'utf8'));
if (/require\("twenty/.test(content)) {
console.error(
`Build check failed: ${pkg.name}/dist/cli.cjs contains a require("twenty...) import. Workspace packages should be bundled, not externalized.`,
);
process.exit(1);
} else {
console.log(`${pkg.name}/dist/cli.cjs: OK`);
}
};
verifyBuildPackagesBundleTwentyDependencies();

View file

@ -0,0 +1,27 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
const packages = ['twenty-sdk', 'create-twenty-app'];
const verifyUniquePackageVersion = () => {
const packageVersions = packages.map((pkg) => {
const packageJsonPath = resolve('packages', pkg, 'package.json');
const packageJsonFile = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
return packageJsonFile.version as string;
});
if (new Set(packageVersions).size !== 1) {
console.error(
`Build check failed: "${packages.join('", "')}" should have the same package.json version. Got ${packageVersions.join(', ')}`,
);
process.exit(1);
return;
}
console.log(
`"${packages.join('", "')}" share the same version ${packageVersions[0]}: OK`,
);
};
verifyUniquePackageVersion();

View file

@ -1,7 +0,0 @@
import path from 'path';
export const PKG_PATH = path.resolve(process.cwd(), 'package.json');
export const BACKUP_PATH = path.resolve(
process.cwd(),
'package.__backup__.json',
);

View file

@ -1,16 +0,0 @@
import fs from 'fs-extra';
import { BACKUP_PATH, PKG_PATH } from './constants';
const main = async () => {
if (await fs.pathExists(BACKUP_PATH)) {
const original = await fs.readJson(BACKUP_PATH);
await fs.remove(PKG_PATH);
await fs.writeJson(PKG_PATH, original, { spaces: 2 });
await fs.remove(BACKUP_PATH);
}
};
main().catch((e) => {
console.error(e);
process.exit(1);
});

View file

@ -1,27 +0,0 @@
import * as fs from 'fs-extra';
import { BACKUP_PATH, PKG_PATH } from './constants';
import { type PackageJson } from 'type-fest';
const stripWorkspace = (deps: PackageJson['dependencies']) =>
Object.fromEntries(
Object.entries(deps).filter(
([_, value]) => !value.startsWith('workspace:'),
),
);
const main = async () => {
const pkg = await fs.readJson(PKG_PATH);
await fs.writeJson(BACKUP_PATH, pkg, { spaces: 2 });
const { devDependencies: _, ...builtPkg } = pkg;
builtPkg.dependencies = stripWorkspace(builtPkg.dependencies ?? {});
await fs.writeJson(PKG_PATH, builtPkg, { spaces: 2 });
};
main().catch((e) => {
console.error(e);
process.exit(1);
});

View file

@ -32179,6 +32179,7 @@ __metadata:
lodash.camelcase: "npm:^4.3.0"
lodash.kebabcase: "npm:^4.1.1"
lodash.startcase: "npm:^4.4.0"
twenty-sdk: "workspace:*"
twenty-shared: "workspace:*"
typescript: "npm:^5.9.2"
uuid: "npm:^13.0.0"