Twenty sdk cli oauth (#18638)
<img width="1418" height="804" alt="image" src="https://github.com/user-attachments/assets/de6c8222-6496-4a71-bc21-7e5e1269d5cb" /> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
import { buildApplication } from '@/cli/utilities/build/common/build-application';
|
||||
import { synchronizeBuiltApplication } from '@/cli/utilities/build/common/synchronize-built-application';
|
||||
import { runTypecheck } from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
import { buildAndValidateManifest } from '@/cli/utilities/build/manifest/build-and-validate-manifest';
|
||||
import { ClientService } from '@/cli/utilities/client/client-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AppBuildOptions = {
|
||||
appPath: string;
|
||||
tarball?: boolean;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type AppBuildResult = {
|
||||
outputDir: string;
|
||||
fileCount: number;
|
||||
tarballPath?: string;
|
||||
};
|
||||
|
||||
const innerAppBuild = async (
|
||||
options: AppBuildOptions,
|
||||
): Promise<CommandResult<AppBuildResult>> => {
|
||||
const { appPath, onProgress } = options;
|
||||
|
||||
onProgress?.('Building manifest...');
|
||||
|
||||
const manifestResult = await buildAndValidateManifest(appPath);
|
||||
|
||||
if (!manifestResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_BUILD_FAILED,
|
||||
message: manifestResult.errors.join('\n'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { manifest, filePaths } = manifestResult;
|
||||
|
||||
for (const warning of manifestResult.warnings) {
|
||||
onProgress?.(`⚠ ${warning}`);
|
||||
}
|
||||
|
||||
onProgress?.('Building application files...');
|
||||
|
||||
const firstBuildResult = await buildApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
filePaths,
|
||||
});
|
||||
|
||||
onProgress?.('Syncing application schema...');
|
||||
|
||||
const firstSyncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos: firstBuildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
if (!firstSyncResult.success) {
|
||||
return firstSyncResult;
|
||||
}
|
||||
|
||||
onProgress?.('Generating API client...');
|
||||
|
||||
const clientService = new ClientService();
|
||||
|
||||
await clientService.generateCoreClient({ appPath });
|
||||
|
||||
onProgress?.('Running typecheck...');
|
||||
|
||||
const typecheckErrors = await runTypecheck(appPath);
|
||||
|
||||
if (typecheckErrors.length > 0) {
|
||||
const errorMessages = typecheckErrors.map(
|
||||
(error) =>
|
||||
`${error.file}(${error.line},${error.column + 1}): ${error.text}`,
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.TYPECHECK_FAILED,
|
||||
message: `Typecheck failed:\n${errorMessages.join('\n')}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onProgress?.('Rebuilding with generated client...');
|
||||
|
||||
const finalBuildResult = await buildApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
filePaths,
|
||||
});
|
||||
|
||||
onProgress?.('Syncing built files...');
|
||||
|
||||
const finalSyncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos: finalBuildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
if (!finalSyncResult.success) {
|
||||
return finalSyncResult;
|
||||
}
|
||||
|
||||
const outputDir = path.join(appPath, '.twenty', 'output');
|
||||
|
||||
const result: AppBuildResult = {
|
||||
outputDir,
|
||||
fileCount: finalBuildResult.builtFileInfos.size,
|
||||
};
|
||||
|
||||
if (options.tarball) {
|
||||
onProgress?.('Packing tarball...');
|
||||
|
||||
const packOutput = execSync('npm pack --pack-destination .', {
|
||||
cwd: outputDir,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
|
||||
const tarballName = packOutput.split('\n').pop()!;
|
||||
|
||||
result.tarballPath = path.join(outputDir, tarballName);
|
||||
}
|
||||
|
||||
return { success: true, data: result };
|
||||
};
|
||||
|
||||
export const appBuild = (
|
||||
options: AppBuildOptions,
|
||||
): Promise<CommandResult<AppBuildResult>> =>
|
||||
runSafe(() => innerAppBuild(options), APP_ERROR_CODES.BUILD_FAILED);
|
||||
@@ -0,0 +1,82 @@
|
||||
import fs from 'fs';
|
||||
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { appBuild } from './build';
|
||||
import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AppDeployOptions = {
|
||||
appPath: string;
|
||||
serverUrl: string;
|
||||
token?: string;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type AppDeployResult = {
|
||||
universalIdentifier: string;
|
||||
};
|
||||
|
||||
const innerAppDeploy = async (
|
||||
options: AppDeployOptions,
|
||||
): Promise<CommandResult<AppDeployResult>> => {
|
||||
const { appPath, serverUrl, token, onProgress } = options;
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath,
|
||||
tarball: true,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
return buildResult;
|
||||
}
|
||||
|
||||
onProgress?.(`Uploading ${buildResult.data.tarballPath}...`);
|
||||
|
||||
const tarballBuffer = fs.readFileSync(buildResult.data.tarballPath!);
|
||||
|
||||
const apiService = new ApiService({
|
||||
serverUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const uploadResult = await apiService.uploadAppTarball({ tarballBuffer });
|
||||
|
||||
if (!uploadResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.DEPLOY_FAILED,
|
||||
message: `Upload failed: ${uploadResult.error}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onProgress?.('Installing application...');
|
||||
|
||||
const installResult = await apiService.installTarballApp({
|
||||
universalIdentifier: uploadResult.data.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!installResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.DEPLOY_FAILED,
|
||||
message: `Install failed: ${installResult.error}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
universalIdentifier: uploadResult.data.universalIdentifier,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const appDeploy = (
|
||||
options: AppDeployOptions,
|
||||
): Promise<CommandResult<AppDeployResult>> =>
|
||||
runSafe(() => innerAppDeploy(options), APP_ERROR_CODES.DEPLOY_FAILED);
|
||||
@@ -0,0 +1,164 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import {
|
||||
APP_ERROR_CODES,
|
||||
FUNCTION_ERROR_CODES,
|
||||
type CommandResult,
|
||||
type FunctionExecutionResult,
|
||||
} from '@/cli/types';
|
||||
|
||||
export type FunctionExecuteOptions = {
|
||||
appPath: string;
|
||||
remote?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
} & (
|
||||
| { postInstall: true }
|
||||
| { functionUniversalIdentifier: string }
|
||||
| { functionName: string }
|
||||
);
|
||||
|
||||
type LogicFunction = {
|
||||
id: string;
|
||||
name: string;
|
||||
universalIdentifier: string;
|
||||
applicationId: string | null;
|
||||
};
|
||||
|
||||
const belongsToApplication = (
|
||||
logicFunction: LogicFunction,
|
||||
manifest: Manifest,
|
||||
): boolean => {
|
||||
return manifest.logicFunctions.some(
|
||||
(manifestFn) =>
|
||||
manifestFn.universalIdentifier === logicFunction.universalIdentifier,
|
||||
);
|
||||
};
|
||||
|
||||
const resolveIdentifier = (options: FunctionExecuteOptions): string => {
|
||||
if ('postInstall' in options) return 'post install';
|
||||
if ('functionUniversalIdentifier' in options)
|
||||
return options.functionUniversalIdentifier;
|
||||
if ('functionName' in options) return options.functionName;
|
||||
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
const innerFunctionExecute = async (
|
||||
options: FunctionExecuteOptions,
|
||||
): Promise<CommandResult<FunctionExecutionResult>> => {
|
||||
if (options.remote) {
|
||||
ConfigService.setActiveRemote(options.remote);
|
||||
}
|
||||
|
||||
const apiService = new ApiService();
|
||||
const manifest = await readManifestFromFile(options.appPath);
|
||||
|
||||
if (!manifest) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_NOT_FOUND,
|
||||
message: 'Manifest not found. Run `build` or `dev` first.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const functionsResult = await apiService.findLogicFunctions();
|
||||
|
||||
if (!functionsResult.success) {
|
||||
const errorMessage =
|
||||
functionsResult.error instanceof Error
|
||||
? functionsResult.error.message
|
||||
: String(functionsResult.error ?? 'Failed to fetch functions');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: FUNCTION_ERROR_CODES.FETCH_FUNCTIONS_FAILED,
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const appFunctions = functionsResult.data.filter(
|
||||
(logicFunction) =>
|
||||
logicFunction.universalIdentifier &&
|
||||
belongsToApplication(logicFunction, manifest),
|
||||
);
|
||||
|
||||
const targetFunction = appFunctions.find((logicFunction) => {
|
||||
if ('postInstall' in options && options.postInstall) {
|
||||
return (
|
||||
logicFunction.universalIdentifier ===
|
||||
manifest.application.postInstallLogicFunctionUniversalIdentifier
|
||||
);
|
||||
}
|
||||
if ('functionUniversalIdentifier' in options) {
|
||||
return (
|
||||
logicFunction.universalIdentifier ===
|
||||
options.functionUniversalIdentifier
|
||||
);
|
||||
}
|
||||
if ('functionName' in options) {
|
||||
return logicFunction.name === options.functionName;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!targetFunction) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: FUNCTION_ERROR_CODES.FUNCTION_NOT_FOUND,
|
||||
message: `Function "${resolveIdentifier(options)}" not found in application.`,
|
||||
details: {
|
||||
identifier: resolveIdentifier(options),
|
||||
availableFunctions: appFunctions.map((logicFunction) => ({
|
||||
name: logicFunction.name,
|
||||
universalIdentifier: logicFunction.universalIdentifier,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await apiService.executeLogicFunction({
|
||||
functionId: targetFunction.id,
|
||||
payload: options.payload ?? {},
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage =
|
||||
result.error instanceof Error
|
||||
? result.error.message
|
||||
: String(result.error ?? 'Execution failed');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: FUNCTION_ERROR_CODES.EXECUTION_FAILED,
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
functionName: targetFunction.name,
|
||||
...result.data!,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const functionExecute = (
|
||||
options: FunctionExecuteOptions,
|
||||
): Promise<CommandResult<FunctionExecutionResult>> =>
|
||||
runSafe(
|
||||
() => innerFunctionExecute(options),
|
||||
FUNCTION_ERROR_CODES.EXECUTION_FAILED,
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
// Auth
|
||||
export { authLogin } from './login';
|
||||
export type { AuthLoginOptions } from './login';
|
||||
export { authLoginOAuth } from './login-oauth';
|
||||
export type { AuthLoginOAuthOptions } from './login-oauth';
|
||||
export { authLogout } from './logout';
|
||||
export type { AuthLogoutOptions } from './logout';
|
||||
|
||||
// App
|
||||
export { appBuild } from './build';
|
||||
export type { AppBuildOptions, AppBuildResult } from './build';
|
||||
export { appDeploy } from './deploy';
|
||||
export type { AppDeployOptions, AppDeployResult } from './deploy';
|
||||
export { appPublish } from './publish';
|
||||
export type { AppPublishOptions, AppPublishResult } from './publish';
|
||||
export { appUninstall } from './uninstall';
|
||||
export type { AppUninstallOptions } from './uninstall';
|
||||
|
||||
// Functions
|
||||
export { functionExecute } from './execute';
|
||||
export type { FunctionExecuteOptions } from './execute';
|
||||
|
||||
// Shared types and error codes
|
||||
export {
|
||||
APP_ERROR_CODES,
|
||||
AUTH_ERROR_CODES,
|
||||
FUNCTION_ERROR_CODES,
|
||||
} from '@/cli/types';
|
||||
export type {
|
||||
AuthListRemote,
|
||||
AuthStatusResult,
|
||||
CommandError,
|
||||
CommandResult,
|
||||
FunctionExecutionResult,
|
||||
TypecheckResult,
|
||||
} from '@/cli/types';
|
||||
@@ -0,0 +1,143 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { startCallbackServer } from '@/cli/utilities/auth/callback-server';
|
||||
import { openBrowser } from '@/cli/utilities/auth/open-browser';
|
||||
import { generatePkceChallenge } from '@/cli/utilities/auth/pkce';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import axios from 'axios';
|
||||
|
||||
import { AUTH_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AuthLoginOAuthOptions = {
|
||||
apiUrl: string;
|
||||
remote?: string;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type OAuthDiscoveryResponse = {
|
||||
authorization_endpoint: string;
|
||||
token_endpoint: string;
|
||||
cli_client_id?: string;
|
||||
};
|
||||
|
||||
const innerAuthLoginOAuth = async (
|
||||
options: AuthLoginOAuthOptions,
|
||||
): Promise<CommandResult> => {
|
||||
const { apiUrl, remote, timeoutMs } = options;
|
||||
|
||||
if (remote) {
|
||||
ConfigService.setActiveRemote(remote);
|
||||
}
|
||||
|
||||
const configService = new ConfigService();
|
||||
|
||||
const discoveryUrl = `${apiUrl}/.well-known/oauth-authorization-server`;
|
||||
|
||||
let discovery: OAuthDiscoveryResponse;
|
||||
|
||||
try {
|
||||
const response = await axios.get(discoveryUrl);
|
||||
|
||||
discovery = response.data;
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: AUTH_ERROR_CODES.OAUTH_NOT_SUPPORTED,
|
||||
message: `Could not reach the OAuth discovery endpoint at ${discoveryUrl}. Ensure the server is running. Use --api-key instead.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!discovery.cli_client_id) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: AUTH_ERROR_CODES.OAUTH_NOT_SUPPORTED,
|
||||
message:
|
||||
'Server does not expose a CLI client ID. Ensure the server is up to date. Use --api-key instead.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const clientId = discovery.cli_client_id;
|
||||
|
||||
const { codeVerifier, codeChallenge } = generatePkceChallenge();
|
||||
|
||||
const callbackServer = await startCallbackServer({ timeoutMs });
|
||||
|
||||
try {
|
||||
const authUrl = new URL(discovery.authorization_endpoint);
|
||||
|
||||
authUrl.searchParams.set('clientId', clientId);
|
||||
authUrl.searchParams.set('codeChallenge', codeChallenge);
|
||||
authUrl.searchParams.set('redirectUrl', callbackServer.callbackUrl);
|
||||
|
||||
const browserOpened = await openBrowser(authUrl.toString());
|
||||
|
||||
if (!browserOpened) {
|
||||
console.log(
|
||||
`\nOpen this URL in your browser to authenticate:\n${authUrl.toString()}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const callbackResult = await callbackServer.waitForCallback();
|
||||
|
||||
if (!callbackResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: AUTH_ERROR_CODES.AUTH_FAILED,
|
||||
message: callbackResult.error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const tokenResponse = await axios.post(discovery.token_endpoint, {
|
||||
grant_type: 'authorization_code',
|
||||
code: callbackResult.code,
|
||||
code_verifier: codeVerifier,
|
||||
redirect_uri: callbackServer.callbackUrl,
|
||||
client_id: clientId,
|
||||
});
|
||||
|
||||
const { access_token: accessToken, refresh_token: refreshToken } =
|
||||
tokenResponse.data;
|
||||
|
||||
await configService.setConfig({
|
||||
apiUrl,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
oauthClientId: clientId,
|
||||
});
|
||||
|
||||
const apiService = new ApiService({
|
||||
serverUrl: apiUrl,
|
||||
token: accessToken,
|
||||
});
|
||||
|
||||
const validateAuth = await apiService.validateAuth();
|
||||
|
||||
if (!validateAuth.authValid) {
|
||||
await configService.clearConfig();
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: AUTH_ERROR_CODES.AUTH_FAILED,
|
||||
message:
|
||||
'OAuth tokens received but authentication validation failed.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
} finally {
|
||||
callbackServer.close();
|
||||
}
|
||||
};
|
||||
|
||||
export const authLoginOAuth = (
|
||||
options: AuthLoginOAuthOptions,
|
||||
): Promise<CommandResult> =>
|
||||
runSafe(() => innerAuthLoginOAuth(options), AUTH_ERROR_CODES.AUTH_FAILED);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { AUTH_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AuthLoginOptions = {
|
||||
apiKey: string;
|
||||
apiUrl: string;
|
||||
remote?: string;
|
||||
};
|
||||
|
||||
const innerAuthLogin = async (
|
||||
options: AuthLoginOptions,
|
||||
): Promise<CommandResult> => {
|
||||
const { apiKey, apiUrl, remote } = options;
|
||||
|
||||
if (remote) {
|
||||
ConfigService.setActiveRemote(remote);
|
||||
}
|
||||
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.setConfig({
|
||||
apiUrl,
|
||||
apiKey,
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
oauthClientId: undefined,
|
||||
});
|
||||
|
||||
const apiService = new ApiService();
|
||||
const validateAuth = await apiService.validateAuth();
|
||||
|
||||
if (!validateAuth.authValid) {
|
||||
await configService.clearConfig();
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: AUTH_ERROR_CODES.AUTH_FAILED,
|
||||
message: 'Authentication failed. Please check your credentials.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
|
||||
export const authLogin = (options: AuthLoginOptions): Promise<CommandResult> =>
|
||||
runSafe(() => innerAuthLogin(options), AUTH_ERROR_CODES.AUTH_FAILED);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { AUTH_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AuthLogoutOptions = {
|
||||
remote?: string;
|
||||
};
|
||||
|
||||
const innerAuthLogout = async (
|
||||
options?: AuthLogoutOptions,
|
||||
): Promise<CommandResult> => {
|
||||
if (options?.remote) {
|
||||
ConfigService.setActiveRemote(options.remote);
|
||||
}
|
||||
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.clearConfig();
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
|
||||
export const authLogout = (
|
||||
options?: AuthLogoutOptions,
|
||||
): Promise<CommandResult> =>
|
||||
runSafe(() => innerAuthLogout(options), AUTH_ERROR_CODES.AUTH_FAILED);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { appBuild } from './build';
|
||||
import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AppPublishOptions = {
|
||||
appPath: string;
|
||||
npmTag?: string;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type AppPublishResult = {
|
||||
target: 'npm';
|
||||
};
|
||||
|
||||
const innerAppPublish = async (
|
||||
options: AppPublishOptions,
|
||||
): Promise<CommandResult<AppPublishResult>> => {
|
||||
const { appPath, onProgress } = options;
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
return buildResult;
|
||||
}
|
||||
|
||||
onProgress?.('Publishing to npm...');
|
||||
|
||||
const tagArg = options.npmTag ? ` --tag ${options.npmTag}` : '';
|
||||
|
||||
try {
|
||||
execSync(`npm publish${tagArg}`, {
|
||||
cwd: buildResult.data.outputDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.PUBLISH_FAILED,
|
||||
message:
|
||||
'npm publish failed. Make sure you are logged in (`npm login`) and the package name is available.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: { target: 'npm' } };
|
||||
};
|
||||
|
||||
export const appPublish = (
|
||||
options: AppPublishOptions,
|
||||
): Promise<CommandResult<AppPublishResult>> =>
|
||||
runSafe(() => innerAppPublish(options), APP_ERROR_CODES.PUBLISH_FAILED);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AppUninstallOptions = {
|
||||
appPath: string;
|
||||
remote?: string;
|
||||
};
|
||||
|
||||
const innerAppUninstall = async (
|
||||
options: AppUninstallOptions,
|
||||
): Promise<CommandResult> => {
|
||||
if (options.remote) {
|
||||
ConfigService.setActiveRemote(options.remote);
|
||||
}
|
||||
|
||||
const apiService = new ApiService();
|
||||
const manifest = await readManifestFromFile(options.appPath);
|
||||
|
||||
if (!manifest) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_NOT_FOUND,
|
||||
message: 'Manifest not found. Run `build` or `dev` first.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await apiService.uninstallApplication(
|
||||
manifest.application.universalIdentifier,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage =
|
||||
result.error instanceof Error
|
||||
? result.error.message
|
||||
: String(result.error ?? 'Unknown error');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.UNINSTALL_FAILED,
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
|
||||
export const appUninstall = (
|
||||
options: AppUninstallOptions,
|
||||
): Promise<CommandResult> =>
|
||||
runSafe(() => innerAppUninstall(options), APP_ERROR_CODES.UNINSTALL_FAILED);
|
||||
Reference in New Issue
Block a user