@sprucelabs/spruce-cli
Version:
Command line interface for building Spruce skills.
329 lines • 14.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FeatureInstallerImpl = void 0;
const schema_1 = require("@sprucelabs/schema");
const spruce_skill_utils_1 = require("@sprucelabs/spruce-skill-utils");
const lodash_1 = require("lodash");
const merge_1 = __importDefault(require("lodash/merge"));
const SpruceError_1 = __importDefault(require("../errors/SpruceError"));
class FeatureInstallerImpl {
cwd;
featureMap = {};
serviceFactory;
featuresMarkedAsSkippedThisRun = [];
static startInFlightIntertainmentHandler;
static stopInFlightIntertainmentHandler;
packagesToInstall = [];
devPackagesToInstall = [];
featuresToMarkAsInstalled = [];
afterPackageInstalls = [];
pendingFeatureInstalls = {};
constructor(cwd, serviceFactory) {
this.cwd = cwd;
this.serviceFactory = serviceFactory;
}
isInSpruceModule() {
return spruce_skill_utils_1.diskUtil.doesDirExist(spruce_skill_utils_1.diskUtil.resolvePath(this.cwd, spruce_skill_utils_1.HASH_SPRUCE_DIR));
}
async isInstalled(code) {
const feature = this.getFeature(code);
if (feature.isInstalled) {
return feature.isInstalled();
}
return this.Service('settings').isMarkedAsInstalled(code);
}
markAsSkippedThisRun(code) {
if (!this.isMarkedAsSkipped(code)) {
this.featuresMarkedAsSkippedThisRun.push(code);
}
}
markAsPermanentlySkipped(code) {
if (!this.isMarkedAsSkipped(code)) {
this.Service('settings').markAsPermanentlySkipped(code);
}
}
isMarkedAsSkipped(code) {
return (this.featuresMarkedAsSkippedThisRun.indexOf(code) > -1 ||
this.Service('settings').isMarkedAsPermanentlySkipped(code));
}
mapFeature(code, feature) {
this.featureMap[code] = feature;
}
getFeature(code) {
const feature = this.featureMap[code];
if (!feature) {
throw new SpruceError_1.default({
code: 'INVALID_FEATURE_CODE',
featureCode: code,
});
}
return feature;
}
async areInstalled(codes) {
const results = await Promise.all(codes.map((f) => {
return this.isInstalled(f);
}));
for (const result of results) {
if (!result) {
return false;
}
}
return true;
}
getFeatureDependencies(featureCode) {
if (this.isInGoProject()) {
return [];
}
let deps = this.getFeatureDependenciesIncludingSelf({ code: featureCode, isRequired: true }, []).filter((f) => f.code !== featureCode);
deps = this.sortFeatures(deps);
return deps;
}
isInGoProject() {
return spruce_skill_utils_1.diskUtil.detectProjectLanguage(this.cwd) === 'go';
}
getFeatureDependenciesIncludingSelf(featureDependency, trackedFeatures = []) {
const features = [];
if (!this.isDependencyInTracker(trackedFeatures, featureDependency)) {
features.push(featureDependency);
trackedFeatures.push(featureDependency);
}
const feature = this.getFeature(featureDependency.code);
const dependencies = feature.dependencies;
for (const dependency of dependencies) {
if (!this.isDependencyInTracker(trackedFeatures, dependency)) {
features.push(dependency);
trackedFeatures.push(dependency);
}
}
for (const dependency of dependencies) {
let dependencyDependencies = this.getFeatureDependenciesIncludingSelf(dependency, trackedFeatures);
if (!dependency.isRequired) {
dependencyDependencies = dependencyDependencies.map((f) => ({
...f,
isRequired: false,
}));
}
features.push(...dependencyDependencies);
}
return features;
}
isDependencyInTracker(trackedFeatures, dependency) {
return !!trackedFeatures.find((f) => f.code === dependency.code);
}
async install(options) {
let { features, installFeatureDependencies = true, didUpdateHandler, } = options;
const shouldAllowEntertainment = !!features.find((f) => f.code === 'skill');
if (FeatureInstallerImpl.startInFlightIntertainmentHandler &&
shouldAllowEntertainment) {
FeatureInstallerImpl.startInFlightIntertainmentHandler((handler) => {
didUpdateHandler = handler;
});
}
this.pendingFeatureInstalls = {};
let results = {};
let dependenciesToInstall = [];
for (const f of features) {
const code = f.code;
didUpdateHandler?.(`Checking if ${code} is installed...`);
const isInstalled = await this.isInstalled(code);
if (!isInstalled && installFeatureDependencies) {
didUpdateHandler?.(`It is not, checking dependencies...`);
dependenciesToInstall = dependenciesToInstall.concat(this.getFeatureDependenciesIncludingSelf({
code,
isRequired: true,
}));
}
else if (!isInstalled) {
dependenciesToInstall.push({ code, isRequired: true });
}
}
dependenciesToInstall = (0, lodash_1.uniq)(dependenciesToInstall);
dependenciesToInstall = this.sortFeatures(dependenciesToInstall);
for (const { code, isRequired } of dependenciesToInstall) {
const isInstalled = await this.isInstalledOrPendingInstall(code);
if (!isInstalled && isRequired) {
const installOptions = //@ts-ignore
options.features.find((f) => f.code === code)?.options;
const installFeature = {
code,
options: installOptions,
};
didUpdateHandler?.(`Installing the ${installFeature.code} feature....`);
const installResults = await this.installFeature(installFeature, didUpdateHandler);
results = (0, merge_1.default)(results, installResults);
}
}
const pendingResults = await this.installAllPending(didUpdateHandler);
results = (0, merge_1.default)(results, pendingResults);
if (FeatureInstallerImpl.stopInFlightIntertainmentHandler &&
shouldAllowEntertainment) {
FeatureInstallerImpl.stopInFlightIntertainmentHandler();
}
return results;
}
isInstalledOrPendingInstall(code) {
return (this.pendingFeatureInstalls[code] || this.isInstalled(code));
}
async installFeature(installFeature, didUpdateHandler) {
this.pendingFeatureInstalls[installFeature.code] = true;
const feature = this.getFeature(installFeature.code);
if (feature.optionsSchema) {
(0, schema_1.validateSchemaValues)(feature.optionsSchema,
//@ts-ignore
installFeature.options ?? {});
}
didUpdateHandler?.(`Running before package install hook...`);
const beforeInstallResults = await feature.beforePackageInstall(
//@ts-ignore
installFeature.options);
if (beforeInstallResults.cwd) {
this.cwd = beforeInstallResults.cwd;
}
didUpdateHandler?.(`Installing package dependencies...`);
const packagesInstalled = await this.queueInstallPackageDependenciesWithoutEntertainment(feature, didUpdateHandler);
didUpdateHandler?.(`Running after package install hook...`);
this.afterPackageInstalls.push(installFeature);
if (!feature.isInstalled) {
this.featuresToMarkAsInstalled.push(feature.code);
}
const files = [...(beforeInstallResults.files ?? [])];
return {
files: files ?? undefined,
packagesInstalled,
};
}
async installPackageDependencies(feature, didUpdateHandler) {
return this.installPackageDependenciesForFeatures([feature], didUpdateHandler);
}
async installPackageDependenciesForFeatures(features, didUpdateHandler) {
if (FeatureInstallerImpl.startInFlightIntertainmentHandler) {
FeatureInstallerImpl.startInFlightIntertainmentHandler((handler) => {
didUpdateHandler = handler;
});
}
for (const feature of features) {
await this.queueInstallPackageDependenciesWithoutEntertainment(feature, didUpdateHandler);
}
await this.installAllPending(didUpdateHandler);
if (FeatureInstallerImpl.stopInFlightIntertainmentHandler) {
FeatureInstallerImpl.stopInFlightIntertainmentHandler();
}
}
async installAllPending(didUpdateHandler) {
const pkgService = this.Service('pkg');
if (this.packagesToInstall.length > 0) {
didUpdateHandler?.(`Installing ${this.packagesToInstall.length} node module${this.packagesToInstall.length === 1 ? '' : 's'}. Please be patient.`);
await pkgService.install(this.packagesToInstall, {});
}
if (this.devPackagesToInstall.length > 0) {
didUpdateHandler?.(`Now installing ${this.devPackagesToInstall.length} DEV node module${this.devPackagesToInstall.length === 1 ? '' : 's'}. Please be patient.`);
await pkgService.install(this.devPackagesToInstall, {
isDev: true,
});
}
this.packagesToInstall = [];
this.devPackagesToInstall = [];
const settings = this.Service('settings');
for (const code of this.featuresToMarkAsInstalled) {
settings.markAsInstalled(code);
}
this.featuresToMarkAsInstalled = [];
let results = {};
for (const installFeature of this.afterPackageInstalls) {
const feature = this.getFeature(installFeature.code);
const afterInstallResults = await feature.afterPackageInstall(
//@ts-ignore
installFeature.options);
results = (0, merge_1.default)(results, afterInstallResults);
}
return results;
}
async queueInstallPackageDependenciesWithoutEntertainment(feature, didUpdateHandler) {
const packagesInstalled = [];
const pkgService = this.Service('pkg');
feature.packageDependencies?.forEach((pkg) => {
const packageName = pkgService.buildPackageName(pkg);
packagesInstalled.push(pkg);
didUpdateHandler?.(`Checking node dependency: ${pkg.name}`);
const goPkg = pkg;
const nodePkg = pkg;
const isGoPackage = goPkg.type === 'go';
const shouldConsider = (isGoPackage && this.isInGoProject()) ||
(!isGoPackage && !this.isInGoProject());
if (!shouldConsider) {
return;
}
if (isGoPackage) {
this.packagesToInstall.push(packageName);
}
else if (nodePkg.isDev &&
this.devPackagesToInstall.indexOf(packageName) === -1) {
this.devPackagesToInstall.push(packageName);
}
else if (!nodePkg.isDev &&
this.packagesToInstall.indexOf(packageName) === -1) {
this.packagesToInstall.push(packageName);
}
});
if (this.packagesToInstall.length > 0) {
didUpdateHandler?.(`Queueing install of ${this.packagesToInstall.length} node dependenc${this.packagesToInstall.length === 1
? 'y.'
: 'ies for ' +
this.getFeatureNameAndDesc(feature) +
'...'}.`);
}
if (this.devPackagesToInstall.length > 0) {
didUpdateHandler?.(`Queueing install of ${this.devPackagesToInstall.length} DEV node dependenc${this.devPackagesToInstall.length === 1
? 'y.'
: 'ies for ' +
this.getFeatureNameAndDesc(feature) +
'. 🤘'}.`);
}
return packagesInstalled;
}
getFeatureNameAndDesc(feature) {
return `${feature.nameReadable}${feature.description ? ' (' + feature.description + ')' : ''}`;
}
sortFeatures(featureDependencies) {
return [...featureDependencies].sort((a, b) => {
const aFeature = this.getFeature(a.code);
const bFeature = this.getFeature(b.code);
const aDependsOnB = aFeature.dependencies.find((d) => d.code === b.code);
const bDependsOnA = bFeature.dependencies.find((d) => d.code === a.code);
if (aDependsOnB ||
aFeature.installOrderWeight < bFeature.installOrderWeight) {
return 1;
}
else if (bDependsOnA ||
aFeature.installOrderWeight > bFeature.installOrderWeight) {
return -1;
}
return 0;
});
}
Service(type, cwd) {
return this.serviceFactory.Service(cwd ?? this.cwd, type);
}
getOptionsForFeature(code) {
return this.getFeature(code).optionsSchema;
}
getAllCodes() {
const codes = Object.keys(this.featureMap);
return this.sortFeatures(codes.map((code) => ({ code, isRequired: true }))).map((dep) => dep.code);
}
async getInstalledFeatures() {
const installed = await Promise.all(this.getAllCodes().map(async (code) => {
const isInstalled = await this.isInstalled(code);
if (isInstalled) {
return this.getFeature(code);
}
return false;
}));
return installed.filter((f) => !!f);
}
}
exports.FeatureInstallerImpl = FeatureInstallerImpl;
//# sourceMappingURL=FeatureInstaller.js.map