UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

304 lines (249 loc) • 9.15 kB
import path from 'path' import { SpruceSchemas } from '@sprucelabs/spruce-core-schemas' import { diskUtil, namesUtil } from '@sprucelabs/spruce-skill-utils' import SpruceError from '../../../errors/SpruceError' import AbstractStore, { StoreOptions } from '../../../stores/AbstractStore' import { CurrentSkill, RegisteredSkill } from '../../../types/cli.types' export default class SkillStoreImpl extends AbstractStore implements SkillStore { public readonly name = 'skill' private static currentSkill?: CurrentSkill public constructor(options: StoreOptions<SkillStoreOptions>) { super(options) } public static clearCurrentSkill() { this.currentSkill = undefined } public async register( values: CreateSkill, options?: RegisterSkillOptions ): Promise<RegisteredSkill> { const isRegisteringCurrentSkill = options?.isRegisteringCurrentSkill !== false isRegisteringCurrentSkill && this.assertInSkill() const { name, slug, description, isPublished } = values const client = await this.connectToApi() const [{ skill }] = await client.emitAndFlattenResponses( 'register-skill::v2020_12_25', { payload: { name, slug, description, isPublished, }, } ) if (isRegisteringCurrentSkill) { await this.setCurrentSkillsNamespace(skill.slug) this.Service('auth').updateCurrentSkill(skill) } return skill } private assertInSkill() { const isInstalled = this.Service('settings').isMarkedAsInstalled('skill') if (!isInstalled) { throw new SpruceError({ code: 'DIRECTORY_NOT_SKILL' }) } } public async loadCurrentSkill(): Promise<CurrentSkill> { if (SkillStoreImpl.currentSkill) { return SkillStoreImpl.currentSkill } this.assertInSkill() const currentSkill = this.Service('auth').getCurrentSkill() if (currentSkill) { const client = await this.connectToApi({ shouldAuthAsCurrentSkill: true, }) const [{ skill }] = await client.emitAndFlattenResponses( 'get-skill::v2020_12_25', { target: { skillId: currentSkill.id, }, } ) SkillStoreImpl.currentSkill = { ...skill, namespacePascal: namesUtil.toPascal(skill.slug), isRegistered: true, apiKey: currentSkill.apiKey, } return SkillStoreImpl.currentSkill as CurrentSkill } return { name: this.getNamespaceFromPkg(), namespacePascal: this.getEventNamespaceForNotRegistered(), description: this.getSkillDescriptionFromPkg(), isRegistered: false, } } public async publish(options?: PublishOptions) { const { isInstallable = true } = options || {} const skill = await this.loadCurrentSkill() if (!skill.id) { throw new SpruceError({ code: 'NO_SKILLS_REGISTERED', friendlyMessage: 'You need to register your skill before you can publish it. Run `spruce register` to get started.', }) } const client = await this.connectToApi() await client.emitAndFlattenResponses('publish-skill::v2020_12_25', { target: { skillId: skill.id, }, payload: { canBeInstalled: isInstallable, }, }) delete SkillStoreImpl.currentSkill } public async isCurrentSkillRegistered() { const skill = await this.loadCurrentSkill() return skill.isRegistered } public getGoModuleName(options?: { shouldIncludePathFromCwd?: boolean }) { const { shouldIncludePathFromCwd = false } = options || {} let goModFile = diskUtil.resolvePath(this.cwd, 'go.mod') if (!diskUtil.doesFileExist(goModFile)) { goModFile = diskUtil.resolvePath(this.cwd, '../go.mod') } if (!diskUtil.doesFileExist(goModFile)) { throw new SpruceError({ code: 'DIRECTORY_NOT_GO_MODULE', friendlyMessage: `Couldn't find a go.mod file in the current directory or its parent. Are you in a Go module?`, cwd: this.cwd, }) } const goModContents = diskUtil.readFile(goModFile) const moduleLine = goModContents.match(/module\s+([^\s]+)/) const goModuleName = moduleLine?.[1] as string if (shouldIncludePathFromCwd) { const goModDir = path.dirname(goModFile) const relativePath = this.cwd.replace(goModDir, '') if (relativePath) { return `${goModuleName}${relativePath}` } } return goModuleName } private getNamespaceFromPkg() { if (this.isGoModule()) { const module = this.getGoModuleName() const moduleParts = module.split('/') ?? [] return moduleParts.pop() as string } const nameFromPackage = this.Service('pkg').getSkillNamespace() if (!nameFromPackage) { throw new Error( 'You need need to set skill.namespace in the package.json' ) } return nameFromPackage as string } private isGoModule() { return diskUtil.detectProjectLanguage(this.cwd) === 'go' } public async loadCurrentSkillsNamespace() { const fallback = this.getNamespaceFromPkg() if (!this.isGoModule() && this.Service('auth').getCurrentSkill()) { const current = await this.loadCurrentSkill() return namesUtil.toPascal(current.slug ?? fallback) } return namesUtil.toPascal(fallback) } public async setCurrentSkillsNamespace(namespace: string) { let isRegistered = false try { isRegistered = await this.isCurrentSkillRegistered() } catch {} if (isRegistered) { throw new SpruceError({ code: 'GENERIC', friendlyMessage: `You can't set the namespace of a skill that is registered.`, }) } this.Service('auth').updateCurrentSkillNamespace(namespace) } private getEventNamespaceForNotRegistered() { return namesUtil.toPascal(this.getNamespaceFromPkg()) } private getSkillDescriptionFromPkg() { const pkg = this.Service('pkg') return pkg.get('description') } public async unregisterSkill(skillId?: string) { const client = await this.connectToApi() let resolvedSkillId = skillId if (!skillId) { const currentSkill = this.Service('auth').getCurrentSkill() resolvedSkillId = currentSkill?.id } await client.emitAndFlattenResponses('unregister-skill::v2020_12_25', { target: { skillId: resolvedSkillId!, }, }) if (!skillId || SkillStoreImpl.currentSkill?.id === skillId) { SkillStoreImpl.currentSkill = undefined this.Service('auth').logoutCurrentSkill() } } public async fetchMySkills() { return this.fetchAllSkills({ shouldOnlyShowMine: true }) } public static setCurrentSkill(skill: CurrentSkill) { this.currentSkill = skill } public async fetchAllSkills(query?: { shouldOnlyShowMine?: boolean namespaces?: string[] }) { const client = await this.connectToApi() const [{ skills }] = await client.emitAndFlattenResponses( 'list-skills::v2020_12_25', { payload: { ...query, }, } ) return skills } } export interface CreateSkill { name: string slug?: string description?: string isPublished?: boolean } export interface RegisterSkillOptions { isRegisteringCurrentSkill?: boolean } export interface SkillStoreOptions {} export interface SkillStore { name: 'skill' register( values: CreateSkill, options?: RegisterSkillOptions ): Promise<RegisteredSkill> loadCurrentSkill(): Promise<CurrentSkill> isCurrentSkillRegistered(): Promise<boolean> setCurrentSkillsNamespace(namespace: string): Promise<void> fetchMySkills(): Promise<ListSkill[]> fetchAllSkills(query?: { shouldOnlyShowMine?: boolean namespaces?: string[] }): Promise<ListSkill[]> unregisterSkill(skillId?: string): Promise<void> publish(options?: { isInstallable?: boolean }): Promise<void> } type ListSkill = SpruceSchemas.Mercury.v2020_12_25.ListSkillsSkill export interface PublishOptions { isInstallable?: boolean }