Files
cal.diy/packages/app-store-cli/src/core.ts
T
4a6d608b19 refactor: simplify link-as-an-app template and migrate 24 existing apps (#27215)
* refactor: simplify link-as-an-app template to config.json only

- Remove api/, components/, index.ts, package.json from link-as-an-app template
- Add externalLink field to AppMetaSchema for external URL configuration
- Update API handler to dynamically create handlers for external link apps
- External link apps no longer create credentials (just redirect to URL)
- Update CLI to handle externalLinkUrl field for link-as-an-app template
- CLI now skips package.json update for apps without package.json

Co-Authored-By: peer@cal.com <peer@cal.com>

* fix: simplify external link handler to bypass credential creation

External link apps now directly return the redirect URL without going
through the AppDeclarativeHandler type, avoiding type conflicts with
the createCredential return type requirement.

Co-Authored-By: peer@cal.com <peer@cal.com>

* refactor: migrate 24 external link apps to simplified structure

Migrated the following apps to use config.json with externalLink field
instead of api/add.ts handlers:

- amie, autocheckin, baa-for-hipaa, bolna, chatbase, clic, cron, deel
- elevenlabs, fonio-ai, framer, granola, greetmate-ai, lindy, millis-ai
- monobot, n8n, pipedream, raycast, retell-ai, synthflow, telli, vimcal
- wordpress

Each app now only contains: config.json, DESCRIPTION.md, static/
Removed: api/, components/, index.ts, package.json

Co-Authored-By: peer@cal.com <peer@cal.com>

* nit

* Fix merge conflict issue

* refactor: generate REDIRECT_APPS dynamically during app-store build

- Add logic to build.ts to detect apps with externalLink field
- Generate redirect-apps.generated.ts with list of redirect app slugs
- Update redirectApps.ts to import from generated file
- Revert [...]args].ts to original state (Flow 1 is preferred)

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: cast REDIRECT_APPS to readonly string[] for includes check

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Fix error with React

* Take exteral link URL as input

* deleted bun.lockb

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2026-02-06 12:28:11 +00:00

158 lines
4.7 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { APP_STORE_PATH, IS_WINDOWS_PLATFORM, TEMPLATES_PATH } from "./constants";
import execSync from "./utils/execSync";
const slugify = (str: string) => {
// A valid dir name
// A valid URL path
// It is okay to not be a valid variable name. This is so that we can use hyphens which look better then underscores in URL and as directory name
return str.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
};
export function getSlugFromAppName(appName: string): string {
if (!appName) {
return appName;
}
return slugify(appName);
}
export function getAppDirPath(slug: string, isTemplate: boolean) {
if (!isTemplate) {
return path.join(APP_STORE_PATH, `${slug}`);
}
return path.join(TEMPLATES_PATH, `${slug}`);
}
const updatePackageJson = ({
slug,
appDescription,
appDirPath,
}: {
slug: string;
appDescription: string;
appDirPath: string;
}) => {
const packageJsonPath = `${appDirPath}/package.json`;
// Skip if package.json doesn't exist (e.g., for simplified link-as-an-app template)
if (!fs.existsSync(packageJsonPath)) {
return;
}
const packageJsonConfig = JSON.parse(fs.readFileSync(packageJsonPath).toString());
packageJsonConfig.name = `@calcom/${slug}`;
packageJsonConfig.description = appDescription;
// packageJsonConfig.description = `@calcom/${appName}`;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonConfig, null, 2));
};
const _workspaceDir = path.resolve(__dirname, "..", "..", "..");
export const BaseAppFork = {
create: async ({
category,
editMode = false,
description,
name,
slug,
publisher,
email,
template,
isTemplate,
oldSlug,
externalLinkUrl,
}: {
category: string;
editMode?: boolean;
description: string;
name: string;
slug: string;
publisher: string;
email: string;
template: string;
isTemplate: boolean;
oldSlug?: string;
externalLinkUrl?: string;
}) => {
const appDirPath = getAppDirPath(slug, isTemplate);
if (!editMode) {
await execSync(IS_WINDOWS_PLATFORM ? `mkdir ${appDirPath}` : `mkdir -p ${appDirPath}`);
await execSync(
IS_WINDOWS_PLATFORM
? `xcopy "${TEMPLATES_PATH}\\${template}\\*" "${appDirPath}" /e /i`
: `cp -r ${TEMPLATES_PATH}/${template}/* ${appDirPath}`
);
} else {
if (!oldSlug) {
throw new Error("oldSlug is required when editMode is true");
}
if (oldSlug !== slug) {
// We need to rename only if they are different
const oldAppDirPath = getAppDirPath(oldSlug, isTemplate);
await execSync(
IS_WINDOWS_PLATFORM ? `move ${oldAppDirPath} ${appDirPath}` : `mv ${oldAppDirPath} ${appDirPath}`
);
}
}
updatePackageJson({ slug, appDirPath, appDescription: description });
const categoryToVariantMap = {
video: "conferencing",
};
let config: Record<string, unknown> = {
name: name,
// Plan to remove it. DB already has it and name of dir is also the same.
slug: slug,
type: `${slug}_${category}`,
logo: `icon.svg`,
variant: categoryToVariantMap[category as keyof typeof categoryToVariantMap] || category,
categories: [category],
publisher: publisher,
email: email,
description: description,
// TODO: Use this to avoid edit and delete on the apps created outside of cli
__createdUsingCli: true,
isTemplate,
// Store the template used to create an app
__template: template,
};
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
config = {
...currentConfig,
...config,
};
// For link-as-an-app template, update the externalLink URL
if (template === "link-as-an-app" && externalLinkUrl) {
config.externalLink = {
url: externalLinkUrl,
newTab: true,
};
// Also update the legacy url field for backwards compatibility
config.url = externalLinkUrl;
}
fs.writeFileSync(`${appDirPath}/config.json`, JSON.stringify(config, null, 2));
fs.writeFileSync(
`${appDirPath}/DESCRIPTION.md`,
fs
.readFileSync(`${appDirPath}/DESCRIPTION.md`)
.toString()
.replace(/_DESCRIPTION_/g, description)
.replace(/_APP_DIR_/g, slug)
);
// New monorepo package has been added, so we need to run yarn again
await execSync("yarn");
},
delete: async ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) => {
const appDirPath = getAppDirPath(slug, isTemplate);
await execSync(IS_WINDOWS_PLATFORM ? `rd /s /q ${appDirPath}` : `rm -rf ${appDirPath}`);
},
};
export const generateAppFiles = async () => {
await execSync(`yarn ts-node --transpile-only src/build.ts`);
};