@ubiquity-os/plugin-sdk 3.6.2 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -502,7 +502,7 @@ var int = new type("tag:yaml.org,2002:int", {
502
502
  decimal: function(obj) {
503
503
  return obj.toString(10);
504
504
  },
505
-
505
+ /* eslint-disable max-len */
506
506
  hexadecimal: function(obj) {
507
507
  return obj >= 0 ? "0x" + obj.toString(16).toUpperCase() : "-0x" + obj.toString(16).toUpperCase().slice(1);
508
508
  }
@@ -2693,6 +2693,7 @@ var pluginSettingsSchema = import_typebox.Type.Union(
2693
2693
  );
2694
2694
  var configSchema = import_typebox.Type.Object(
2695
2695
  {
2696
+ imports: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { default: [] })),
2696
2697
  plugins: import_typebox.Type.Record(import_typebox.Type.String(), pluginSettingsSchema, { default: {} })
2697
2698
  },
2698
2699
  {
@@ -2733,14 +2734,101 @@ var manifestSchema = import_typebox2.Type.Object({
2733
2734
  var CONFIG_PROD_FULL_PATH = ".github/.ubiquity-os.config.yml";
2734
2735
  var CONFIG_DEV_FULL_PATH = ".github/.ubiquity-os.config.dev.yml";
2735
2736
  var CONFIG_ORG_REPO = ".ubiquity-os";
2737
+ var ENVIRONMENT_TO_CONFIG_SUFFIX = {
2738
+ development: "dev"
2739
+ };
2740
+ var VALID_CONFIG_SUFFIX = /^[a-z0-9][a-z0-9_-]*$/i;
2741
+ var MAX_IMPORT_DEPTH = 6;
2742
+ function normalizeEnvironmentName(environment) {
2743
+ return String(environment ?? "").trim().toLowerCase();
2744
+ }
2745
+ function getConfigPathCandidatesForEnvironment(environment) {
2746
+ const normalized = normalizeEnvironmentName(environment);
2747
+ if (!normalized) {
2748
+ return [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
2749
+ }
2750
+ if (normalized === "production" || normalized === "prod") {
2751
+ return [CONFIG_PROD_FULL_PATH];
2752
+ }
2753
+ const suffix = ENVIRONMENT_TO_CONFIG_SUFFIX[normalized] ?? normalized;
2754
+ if (suffix === "dev") {
2755
+ return [CONFIG_DEV_FULL_PATH];
2756
+ }
2757
+ if (!VALID_CONFIG_SUFFIX.test(suffix)) {
2758
+ return [CONFIG_DEV_FULL_PATH];
2759
+ }
2760
+ return [`.github/.ubiquity-os.config.${suffix}.yml`, CONFIG_PROD_FULL_PATH];
2761
+ }
2762
+ function normalizeImportKey(location) {
2763
+ return `${location.owner}`.trim().toLowerCase() + "/" + `${location.repo}`.trim().toLowerCase();
2764
+ }
2765
+ function parseImportSpec(value) {
2766
+ const trimmed = value.trim();
2767
+ if (!trimmed) return null;
2768
+ const parts = trimmed.split("/");
2769
+ if (parts.length !== 2) return null;
2770
+ const [owner, repo] = parts;
2771
+ if (!owner || !repo) return null;
2772
+ return { owner, repo };
2773
+ }
2774
+ function readImports(logger, value, source) {
2775
+ if (!value) return [];
2776
+ if (!Array.isArray(value)) {
2777
+ logger.warn("Invalid imports; expected a list of strings.", { source });
2778
+ return [];
2779
+ }
2780
+ const seen = /* @__PURE__ */ new Set();
2781
+ const imports = [];
2782
+ for (const entry of value) {
2783
+ if (typeof entry !== "string") {
2784
+ logger.warn("Ignoring invalid import entry; expected string.", { source, entry });
2785
+ continue;
2786
+ }
2787
+ const parsed = parseImportSpec(entry);
2788
+ if (!parsed) {
2789
+ logger.warn("Ignoring invalid import entry; expected owner/repo.", { source, entry });
2790
+ continue;
2791
+ }
2792
+ const key = normalizeImportKey(parsed);
2793
+ if (seen.has(key)) continue;
2794
+ seen.add(key);
2795
+ imports.push(parsed);
2796
+ }
2797
+ return imports;
2798
+ }
2799
+ function stripImports(config) {
2800
+ if (!config || typeof config !== "object") return config;
2801
+ const { imports: _imports, ...rest } = config;
2802
+ return rest;
2803
+ }
2804
+ function mergeImportedConfigs(imported, base) {
2805
+ if (!imported.length) {
2806
+ return base;
2807
+ }
2808
+ let merged = imported[0];
2809
+ for (let i = 1; i < imported.length; i++) {
2810
+ merged = {
2811
+ ...merged,
2812
+ ...imported[i],
2813
+ plugins: { ...merged.plugins, ...imported[i].plugins }
2814
+ };
2815
+ }
2816
+ return base ? {
2817
+ ...merged,
2818
+ ...base,
2819
+ plugins: { ...merged.plugins, ...base.plugins }
2820
+ } : merged;
2821
+ }
2736
2822
  var ConfigurationHandler = class {
2737
- constructor(_logger, _octokit, _environment = null) {
2823
+ constructor(_logger, _octokit, _environment = null, options) {
2738
2824
  this._logger = _logger;
2739
2825
  this._octokit = _octokit;
2740
2826
  this._environment = _environment;
2827
+ this._octokitFactory = options?.octokitFactory;
2741
2828
  }
2742
2829
  _manifestCache = {};
2743
2830
  _manifestPromiseCache = {};
2831
+ _octokitFactory;
2744
2832
  /**
2745
2833
  * Retrieves the configuration for the current plugin based on its manifest.
2746
2834
  * @param manifest - The plugin manifest containing the `short_name` identifier
@@ -2759,14 +2847,14 @@ var ConfigurationHandler = class {
2759
2847
  * @returns The merged plugin configuration with resolved plugin settings.
2760
2848
  */
2761
2849
  async getConfiguration(location) {
2762
- const defaultConfiguration = import_value.Value.Decode(configSchema, import_value.Value.Default(configSchema, {}));
2850
+ const defaultConfiguration = stripImports(import_value.Value.Decode(configSchema, import_value.Value.Default(configSchema, {})));
2763
2851
  if (!location) {
2764
- this._logger.debug("No location was provided, using the default configuration");
2852
+ this._logger.info("No location was provided, using the default configuration");
2765
2853
  return defaultConfiguration;
2766
2854
  }
2767
2855
  const { owner, repo } = location;
2768
2856
  let mergedConfiguration = defaultConfiguration;
2769
- this._logger.debug("Fetching configurations from the organization and repository", {
2857
+ this._logger.info("Fetching configurations from the organization and repository", {
2770
2858
  orgRepo: `${owner}/${CONFIG_ORG_REPO}`,
2771
2859
  repo: `${owner}/${repo}`
2772
2860
  });
@@ -2779,13 +2867,13 @@ var ConfigurationHandler = class {
2779
2867
  mergedConfiguration = this.mergeConfigurations(mergedConfiguration, repoConfig.config);
2780
2868
  }
2781
2869
  const resolvedPlugins = {};
2782
- this._logger.debug("Found plugins enabled", { repo: `${owner}/${repo}`, plugins: Object.keys(mergedConfiguration.plugins).length });
2870
+ this._logger.ok("Found plugins enabled", { repo: `${owner}/${repo}`, plugins: Object.keys(mergedConfiguration.plugins).length });
2783
2871
  for (const [pluginKey, pluginSettings] of Object.entries(mergedConfiguration.plugins)) {
2784
2872
  let pluginIdentifier;
2785
2873
  try {
2786
2874
  pluginIdentifier = parsePluginIdentifier(pluginKey);
2787
2875
  } catch (error) {
2788
- this._logger.error("Invalid plugin identifier; skipping", { plugin: pluginKey, err: error });
2876
+ this._logger.warn("Invalid plugin identifier; skipping", { plugin: pluginKey, err: error });
2789
2877
  continue;
2790
2878
  }
2791
2879
  const manifest = await this.getManifest(pluginIdentifier);
@@ -2820,87 +2908,189 @@ var ConfigurationHandler = class {
2820
2908
  * @param repository The repository name
2821
2909
  */
2822
2910
  async getConfigurationFromRepo(owner, repository) {
2911
+ const location = { owner, repo: repository };
2912
+ const state = this._createImportState();
2913
+ const octokit = await this._getOctokitForLocation(location, state);
2914
+ if (!octokit) {
2915
+ this._logger.warn("No Octokit available for configuration load", { owner, repository });
2916
+ return { config: null, errors: null, rawData: null };
2917
+ }
2918
+ const { config, imports, errors, rawData } = await this._loadConfigSource(location, octokit);
2919
+ if (!rawData) {
2920
+ return { config: null, errors: null, rawData: null };
2921
+ }
2922
+ if (errors && errors.length) {
2923
+ this._logger.warn("YAML could not be decoded", { owner, repository, errors });
2924
+ return { config: null, errors, rawData };
2925
+ }
2926
+ if (!config) {
2927
+ this._logger.warn("YAML could not be decoded", { owner, repository });
2928
+ return { config: null, errors, rawData };
2929
+ }
2930
+ const importedConfigs = [];
2931
+ for (const next of imports) {
2932
+ const resolved = await this._resolveImportedConfiguration(next, state, 1);
2933
+ if (resolved) importedConfigs.push(resolved);
2934
+ }
2935
+ const mergedConfig = mergeImportedConfigs(importedConfigs, config);
2936
+ if (!mergedConfig) {
2937
+ return { config: null, errors: null, rawData };
2938
+ }
2939
+ const decoded = this._decodeConfiguration(location, mergedConfig);
2940
+ return { config: decoded.config, errors: decoded.errors, rawData };
2941
+ }
2942
+ _createImportState() {
2943
+ return {
2944
+ cache: /* @__PURE__ */ new Map(),
2945
+ inFlight: /* @__PURE__ */ new Set(),
2946
+ octokitByLocation: /* @__PURE__ */ new Map()
2947
+ };
2948
+ }
2949
+ async _getOctokitForLocation(location, state) {
2950
+ const key = normalizeImportKey(location);
2951
+ if (state.octokitByLocation.has(key)) {
2952
+ return state.octokitByLocation.get(key) ?? null;
2953
+ }
2954
+ if (this._octokitFactory) {
2955
+ const resolved = await this._octokitFactory(location);
2956
+ if (resolved) {
2957
+ state.octokitByLocation.set(key, resolved);
2958
+ return resolved;
2959
+ }
2960
+ }
2961
+ state.octokitByLocation.set(key, this._octokit);
2962
+ return this._octokit;
2963
+ }
2964
+ async _loadConfigSource(location, octokit) {
2823
2965
  const rawData = await this._download({
2824
- repository,
2825
- owner
2966
+ repository: location.repo,
2967
+ owner: location.owner,
2968
+ octokit
2826
2969
  });
2827
- this._logger.debug("Downloaded configuration file", { owner, repository });
2970
+ this._logger.ok("Downloaded configuration file", { owner: location.owner, repository: location.repo });
2828
2971
  if (!rawData) {
2829
- this._logger.debug("No raw configuration data", { owner, repository });
2830
- return { config: null, errors: null, rawData: null };
2972
+ this._logger.warn("No raw configuration data", { owner: location.owner, repository: location.repo });
2973
+ return { config: null, imports: [], errors: null, rawData: null };
2831
2974
  }
2832
2975
  const { yaml, errors } = this._parseYaml(rawData);
2976
+ const imports = readImports(this._logger, yaml?.imports, location);
2977
+ if (yaml && typeof yaml === "object" && !Array.isArray(yaml) && "imports" in yaml) {
2978
+ delete yaml.imports;
2979
+ }
2833
2980
  const targetRepoConfiguration = yaml;
2834
- this._logger.debug("Decoding configuration", { owner, repository });
2835
- if (targetRepoConfiguration) {
2836
- try {
2837
- const configSchemaWithDefaults = import_value.Value.Default(configSchema, targetRepoConfiguration);
2838
- const errors2 = import_value.Value.Errors(configSchema, configSchemaWithDefaults);
2839
- if (errors2.First()) {
2840
- for (const error of errors2) {
2841
- this._logger.warn("Configuration validation error", { err: error });
2842
- }
2981
+ return { config: targetRepoConfiguration, imports, errors, rawData };
2982
+ }
2983
+ _decodeConfiguration(location, config) {
2984
+ this._logger.info("Decoding configuration", { owner: location.owner, repository: location.repo });
2985
+ try {
2986
+ const configSchemaWithDefaults = import_value.Value.Default(configSchema, config);
2987
+ const errors = import_value.Value.Errors(configSchema, configSchemaWithDefaults);
2988
+ if (errors.First()) {
2989
+ for (const error of errors) {
2990
+ this._logger.warn("Configuration validation error", { err: error });
2843
2991
  }
2844
- const decodedConfig = import_value.Value.Decode(configSchema, configSchemaWithDefaults);
2845
- return { config: decodedConfig, errors: errors2.First() ? errors2 : null, rawData };
2846
- } catch (error) {
2847
- this._logger.error("Error decoding configuration; Will ignore.", { err: error, owner, repository });
2848
- return { config: null, errors: [error instanceof import_value.TransformDecodeCheckError ? error.error : error], rawData };
2849
2992
  }
2993
+ const decodedConfig = import_value.Value.Decode(configSchema, configSchemaWithDefaults);
2994
+ return { config: stripImports(decodedConfig), errors: errors.First() ? errors : null };
2995
+ } catch (error) {
2996
+ this._logger.warn("Error decoding configuration; Will ignore.", { err: error, owner: location.owner, repository: location.repo });
2997
+ return { config: null, errors: [error instanceof import_value.TransformDecodeCheckError ? error.error : error] };
2850
2998
  }
2851
- this._logger.error("YAML could not be decoded", { owner, repository, errors });
2852
- return { config: null, errors, rawData };
2853
2999
  }
2854
- async _download({ repository, owner }) {
2855
- if (!repository || !owner) {
2856
- this._logger.error("Repo or owner is not defined, cannot download the requested file");
3000
+ async _resolveImportedConfiguration(location, state, depth) {
3001
+ const key = normalizeImportKey(location);
3002
+ if (state.cache.has(key)) {
3003
+ return state.cache.get(key) ?? null;
3004
+ }
3005
+ if (state.inFlight.has(key)) {
3006
+ this._logger.warn("Skipping import due to circular reference.", { location });
2857
3007
  return null;
2858
3008
  }
2859
- let pathList;
2860
- switch (this._environment) {
2861
- case "development":
2862
- pathList = [CONFIG_DEV_FULL_PATH];
2863
- break;
2864
- case "production":
2865
- pathList = [CONFIG_PROD_FULL_PATH];
2866
- break;
2867
- default:
2868
- pathList = [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
3009
+ if (depth > MAX_IMPORT_DEPTH) {
3010
+ this._logger.warn("Skipping import; maximum depth exceeded.", { location, depth });
3011
+ return null;
3012
+ }
3013
+ state.inFlight.add(key);
3014
+ let resolved = null;
3015
+ try {
3016
+ const octokit = await this._getOctokitForLocation(location, state);
3017
+ if (!octokit) {
3018
+ this._logger.warn("Skipping import; no authorized Octokit for owner.", { location });
3019
+ return null;
3020
+ }
3021
+ const { config, imports, errors } = await this._loadConfigSource(location, octokit);
3022
+ if (errors && errors.length) {
3023
+ this._logger.warn("Skipping import due to YAML parsing errors.", { location, errors });
3024
+ return null;
3025
+ }
3026
+ if (!config) {
3027
+ return null;
3028
+ }
3029
+ const importedConfigs = [];
3030
+ for (const next of imports) {
3031
+ const nested = await this._resolveImportedConfiguration(next, state, depth + 1);
3032
+ if (nested) importedConfigs.push(nested);
3033
+ }
3034
+ const mergedConfig = mergeImportedConfigs(importedConfigs, config);
3035
+ if (!mergedConfig) return null;
3036
+ const decoded = this._decodeConfiguration(location, mergedConfig);
3037
+ resolved = decoded.config;
3038
+ } finally {
3039
+ state.inFlight.delete(key);
3040
+ state.cache.set(key, resolved);
2869
3041
  }
3042
+ return resolved;
3043
+ }
3044
+ async _download({
3045
+ repository,
3046
+ owner,
3047
+ octokit
3048
+ }) {
3049
+ if (!repository || !owner) {
3050
+ this._logger.warn("Repo or owner is not defined, cannot download the requested file");
3051
+ return null;
3052
+ }
3053
+ const pathList = getConfigPathCandidatesForEnvironment(this._environment);
2870
3054
  for (const filePath of pathList) {
2871
3055
  try {
2872
- this._logger.debug("Attempting to fetch configuration", { owner, repository, filePath });
2873
- const { data, headers } = await this._octokit.rest.repos.getContent({
3056
+ this._logger.info("Attempting to fetch configuration", { owner, repository, filePath });
3057
+ const { data, headers } = await octokit.rest.repos.getContent({
2874
3058
  owner,
2875
3059
  repo: repository,
2876
3060
  path: filePath,
2877
3061
  mediaType: { format: "raw" }
2878
3062
  });
2879
- this._logger.debug("Configuration file found", { owner, repository, filePath, rateLimitRemaining: headers?.["x-ratelimit-remaining"], data });
3063
+ this._logger.ok("Configuration file found", { owner, repository, filePath, rateLimitRemaining: headers?.["x-ratelimit-remaining"], data });
2880
3064
  return data;
2881
3065
  } catch (err) {
2882
3066
  if (err && typeof err === "object" && "status" in err && err.status === 404) {
2883
3067
  this._logger.warn("No configuration file found", { owner, repository, filePath });
2884
3068
  } else {
2885
- this._logger.error("Failed to download the requested file", { err, owner, repository, filePath });
3069
+ const status = err && typeof err === "object" && "status" in err ? Number(err.status) : null;
3070
+ const metadata = { err, owner, repository, filePath, ...status ? { status } : {} };
3071
+ if (status && status >= 500) {
3072
+ this._logger.error("Failed to download the requested file", metadata);
3073
+ } else {
3074
+ this._logger.warn("Failed to download the requested file", metadata);
3075
+ }
2886
3076
  }
2887
3077
  }
2888
3078
  }
2889
3079
  return null;
2890
3080
  }
2891
3081
  _parseYaml(data) {
2892
- this._logger.debug("Will attempt to parse YAML data", { data });
3082
+ this._logger.info("Will attempt to parse YAML data", { data });
2893
3083
  try {
2894
3084
  if (data) {
2895
3085
  const parsedData = jsYaml.load(data);
2896
- this._logger.debug("Parsed yaml data", { parsedData });
3086
+ this._logger.ok("Parsed yaml data", { parsedData });
2897
3087
  return { yaml: parsedData ?? null, errors: null };
2898
3088
  }
2899
3089
  } catch (error) {
2900
- this._logger.error("Error parsing YAML", { error });
3090
+ this._logger.warn("Error parsing YAML", { error });
2901
3091
  return { errors: [error], yaml: null };
2902
3092
  }
2903
- this._logger.debug("Could not parse YAML");
3093
+ this._logger.warn("Could not parse YAML");
2904
3094
  return { yaml: null, errors: null };
2905
3095
  }
2906
3096
  mergeConfigurations(configuration1, configuration2) {
@@ -2941,7 +3131,7 @@ var ConfigurationHandler = class {
2941
3131
  return manifest;
2942
3132
  }
2943
3133
  } catch (e) {
2944
- this._logger.error("Could not find a valid manifest", { owner, repo, err: e });
3134
+ this._logger.warn("Could not find a valid manifest", { owner, repo, err: e });
2945
3135
  }
2946
3136
  return null;
2947
3137
  })();
@@ -2956,7 +3146,7 @@ var ConfigurationHandler = class {
2956
3146
  const errors = [...import_value.Value.Errors(manifestSchema, manifest)];
2957
3147
  if (errors.length) {
2958
3148
  for (const error of errors) {
2959
- this._logger.error("Manifest validation error", { error });
3149
+ this._logger.warn("Manifest validation error", { error });
2960
3150
  }
2961
3151
  throw new Error("Manifest is invalid.");
2962
3152
  }