@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.
@@ -475,7 +475,7 @@ var int = new type("tag:yaml.org,2002:int", {
475
475
  decimal: function(obj) {
476
476
  return obj.toString(10);
477
477
  },
478
-
478
+ /* eslint-disable max-len */
479
479
  hexadecimal: function(obj) {
480
480
  return obj >= 0 ? "0x" + obj.toString(16).toUpperCase() : "-0x" + obj.toString(16).toUpperCase().slice(1);
481
481
  }
@@ -2666,6 +2666,7 @@ var pluginSettingsSchema = T.Union(
2666
2666
  );
2667
2667
  var configSchema = T.Object(
2668
2668
  {
2669
+ imports: T.Optional(T.Array(T.String(), { default: [] })),
2669
2670
  plugins: T.Record(T.String(), pluginSettingsSchema, { default: {} })
2670
2671
  },
2671
2672
  {
@@ -2706,14 +2707,101 @@ var manifestSchema = T2.Object({
2706
2707
  var CONFIG_PROD_FULL_PATH = ".github/.ubiquity-os.config.yml";
2707
2708
  var CONFIG_DEV_FULL_PATH = ".github/.ubiquity-os.config.dev.yml";
2708
2709
  var CONFIG_ORG_REPO = ".ubiquity-os";
2710
+ var ENVIRONMENT_TO_CONFIG_SUFFIX = {
2711
+ development: "dev"
2712
+ };
2713
+ var VALID_CONFIG_SUFFIX = /^[a-z0-9][a-z0-9_-]*$/i;
2714
+ var MAX_IMPORT_DEPTH = 6;
2715
+ function normalizeEnvironmentName(environment) {
2716
+ return String(environment ?? "").trim().toLowerCase();
2717
+ }
2718
+ function getConfigPathCandidatesForEnvironment(environment) {
2719
+ const normalized = normalizeEnvironmentName(environment);
2720
+ if (!normalized) {
2721
+ return [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
2722
+ }
2723
+ if (normalized === "production" || normalized === "prod") {
2724
+ return [CONFIG_PROD_FULL_PATH];
2725
+ }
2726
+ const suffix = ENVIRONMENT_TO_CONFIG_SUFFIX[normalized] ?? normalized;
2727
+ if (suffix === "dev") {
2728
+ return [CONFIG_DEV_FULL_PATH];
2729
+ }
2730
+ if (!VALID_CONFIG_SUFFIX.test(suffix)) {
2731
+ return [CONFIG_DEV_FULL_PATH];
2732
+ }
2733
+ return [`.github/.ubiquity-os.config.${suffix}.yml`, CONFIG_PROD_FULL_PATH];
2734
+ }
2735
+ function normalizeImportKey(location) {
2736
+ return `${location.owner}`.trim().toLowerCase() + "/" + `${location.repo}`.trim().toLowerCase();
2737
+ }
2738
+ function parseImportSpec(value) {
2739
+ const trimmed = value.trim();
2740
+ if (!trimmed) return null;
2741
+ const parts = trimmed.split("/");
2742
+ if (parts.length !== 2) return null;
2743
+ const [owner, repo] = parts;
2744
+ if (!owner || !repo) return null;
2745
+ return { owner, repo };
2746
+ }
2747
+ function readImports(logger, value, source) {
2748
+ if (!value) return [];
2749
+ if (!Array.isArray(value)) {
2750
+ logger.warn("Invalid imports; expected a list of strings.", { source });
2751
+ return [];
2752
+ }
2753
+ const seen = /* @__PURE__ */ new Set();
2754
+ const imports = [];
2755
+ for (const entry of value) {
2756
+ if (typeof entry !== "string") {
2757
+ logger.warn("Ignoring invalid import entry; expected string.", { source, entry });
2758
+ continue;
2759
+ }
2760
+ const parsed = parseImportSpec(entry);
2761
+ if (!parsed) {
2762
+ logger.warn("Ignoring invalid import entry; expected owner/repo.", { source, entry });
2763
+ continue;
2764
+ }
2765
+ const key = normalizeImportKey(parsed);
2766
+ if (seen.has(key)) continue;
2767
+ seen.add(key);
2768
+ imports.push(parsed);
2769
+ }
2770
+ return imports;
2771
+ }
2772
+ function stripImports(config) {
2773
+ if (!config || typeof config !== "object") return config;
2774
+ const { imports: _imports, ...rest } = config;
2775
+ return rest;
2776
+ }
2777
+ function mergeImportedConfigs(imported, base) {
2778
+ if (!imported.length) {
2779
+ return base;
2780
+ }
2781
+ let merged = imported[0];
2782
+ for (let i = 1; i < imported.length; i++) {
2783
+ merged = {
2784
+ ...merged,
2785
+ ...imported[i],
2786
+ plugins: { ...merged.plugins, ...imported[i].plugins }
2787
+ };
2788
+ }
2789
+ return base ? {
2790
+ ...merged,
2791
+ ...base,
2792
+ plugins: { ...merged.plugins, ...base.plugins }
2793
+ } : merged;
2794
+ }
2709
2795
  var ConfigurationHandler = class {
2710
- constructor(_logger, _octokit, _environment = null) {
2796
+ constructor(_logger, _octokit, _environment = null, options) {
2711
2797
  this._logger = _logger;
2712
2798
  this._octokit = _octokit;
2713
2799
  this._environment = _environment;
2800
+ this._octokitFactory = options?.octokitFactory;
2714
2801
  }
2715
2802
  _manifestCache = {};
2716
2803
  _manifestPromiseCache = {};
2804
+ _octokitFactory;
2717
2805
  /**
2718
2806
  * Retrieves the configuration for the current plugin based on its manifest.
2719
2807
  * @param manifest - The plugin manifest containing the `short_name` identifier
@@ -2732,14 +2820,14 @@ var ConfigurationHandler = class {
2732
2820
  * @returns The merged plugin configuration with resolved plugin settings.
2733
2821
  */
2734
2822
  async getConfiguration(location) {
2735
- const defaultConfiguration = Value.Decode(configSchema, Value.Default(configSchema, {}));
2823
+ const defaultConfiguration = stripImports(Value.Decode(configSchema, Value.Default(configSchema, {})));
2736
2824
  if (!location) {
2737
- this._logger.debug("No location was provided, using the default configuration");
2825
+ this._logger.info("No location was provided, using the default configuration");
2738
2826
  return defaultConfiguration;
2739
2827
  }
2740
2828
  const { owner, repo } = location;
2741
2829
  let mergedConfiguration = defaultConfiguration;
2742
- this._logger.debug("Fetching configurations from the organization and repository", {
2830
+ this._logger.info("Fetching configurations from the organization and repository", {
2743
2831
  orgRepo: `${owner}/${CONFIG_ORG_REPO}`,
2744
2832
  repo: `${owner}/${repo}`
2745
2833
  });
@@ -2752,13 +2840,13 @@ var ConfigurationHandler = class {
2752
2840
  mergedConfiguration = this.mergeConfigurations(mergedConfiguration, repoConfig.config);
2753
2841
  }
2754
2842
  const resolvedPlugins = {};
2755
- this._logger.debug("Found plugins enabled", { repo: `${owner}/${repo}`, plugins: Object.keys(mergedConfiguration.plugins).length });
2843
+ this._logger.ok("Found plugins enabled", { repo: `${owner}/${repo}`, plugins: Object.keys(mergedConfiguration.plugins).length });
2756
2844
  for (const [pluginKey, pluginSettings] of Object.entries(mergedConfiguration.plugins)) {
2757
2845
  let pluginIdentifier;
2758
2846
  try {
2759
2847
  pluginIdentifier = parsePluginIdentifier(pluginKey);
2760
2848
  } catch (error) {
2761
- this._logger.error("Invalid plugin identifier; skipping", { plugin: pluginKey, err: error });
2849
+ this._logger.warn("Invalid plugin identifier; skipping", { plugin: pluginKey, err: error });
2762
2850
  continue;
2763
2851
  }
2764
2852
  const manifest = await this.getManifest(pluginIdentifier);
@@ -2793,87 +2881,189 @@ var ConfigurationHandler = class {
2793
2881
  * @param repository The repository name
2794
2882
  */
2795
2883
  async getConfigurationFromRepo(owner, repository) {
2884
+ const location = { owner, repo: repository };
2885
+ const state = this._createImportState();
2886
+ const octokit = await this._getOctokitForLocation(location, state);
2887
+ if (!octokit) {
2888
+ this._logger.warn("No Octokit available for configuration load", { owner, repository });
2889
+ return { config: null, errors: null, rawData: null };
2890
+ }
2891
+ const { config, imports, errors, rawData } = await this._loadConfigSource(location, octokit);
2892
+ if (!rawData) {
2893
+ return { config: null, errors: null, rawData: null };
2894
+ }
2895
+ if (errors && errors.length) {
2896
+ this._logger.warn("YAML could not be decoded", { owner, repository, errors });
2897
+ return { config: null, errors, rawData };
2898
+ }
2899
+ if (!config) {
2900
+ this._logger.warn("YAML could not be decoded", { owner, repository });
2901
+ return { config: null, errors, rawData };
2902
+ }
2903
+ const importedConfigs = [];
2904
+ for (const next of imports) {
2905
+ const resolved = await this._resolveImportedConfiguration(next, state, 1);
2906
+ if (resolved) importedConfigs.push(resolved);
2907
+ }
2908
+ const mergedConfig = mergeImportedConfigs(importedConfigs, config);
2909
+ if (!mergedConfig) {
2910
+ return { config: null, errors: null, rawData };
2911
+ }
2912
+ const decoded = this._decodeConfiguration(location, mergedConfig);
2913
+ return { config: decoded.config, errors: decoded.errors, rawData };
2914
+ }
2915
+ _createImportState() {
2916
+ return {
2917
+ cache: /* @__PURE__ */ new Map(),
2918
+ inFlight: /* @__PURE__ */ new Set(),
2919
+ octokitByLocation: /* @__PURE__ */ new Map()
2920
+ };
2921
+ }
2922
+ async _getOctokitForLocation(location, state) {
2923
+ const key = normalizeImportKey(location);
2924
+ if (state.octokitByLocation.has(key)) {
2925
+ return state.octokitByLocation.get(key) ?? null;
2926
+ }
2927
+ if (this._octokitFactory) {
2928
+ const resolved = await this._octokitFactory(location);
2929
+ if (resolved) {
2930
+ state.octokitByLocation.set(key, resolved);
2931
+ return resolved;
2932
+ }
2933
+ }
2934
+ state.octokitByLocation.set(key, this._octokit);
2935
+ return this._octokit;
2936
+ }
2937
+ async _loadConfigSource(location, octokit) {
2796
2938
  const rawData = await this._download({
2797
- repository,
2798
- owner
2939
+ repository: location.repo,
2940
+ owner: location.owner,
2941
+ octokit
2799
2942
  });
2800
- this._logger.debug("Downloaded configuration file", { owner, repository });
2943
+ this._logger.ok("Downloaded configuration file", { owner: location.owner, repository: location.repo });
2801
2944
  if (!rawData) {
2802
- this._logger.debug("No raw configuration data", { owner, repository });
2803
- return { config: null, errors: null, rawData: null };
2945
+ this._logger.warn("No raw configuration data", { owner: location.owner, repository: location.repo });
2946
+ return { config: null, imports: [], errors: null, rawData: null };
2804
2947
  }
2805
2948
  const { yaml, errors } = this._parseYaml(rawData);
2949
+ const imports = readImports(this._logger, yaml?.imports, location);
2950
+ if (yaml && typeof yaml === "object" && !Array.isArray(yaml) && "imports" in yaml) {
2951
+ delete yaml.imports;
2952
+ }
2806
2953
  const targetRepoConfiguration = yaml;
2807
- this._logger.debug("Decoding configuration", { owner, repository });
2808
- if (targetRepoConfiguration) {
2809
- try {
2810
- const configSchemaWithDefaults = Value.Default(configSchema, targetRepoConfiguration);
2811
- const errors2 = Value.Errors(configSchema, configSchemaWithDefaults);
2812
- if (errors2.First()) {
2813
- for (const error of errors2) {
2814
- this._logger.warn("Configuration validation error", { err: error });
2815
- }
2954
+ return { config: targetRepoConfiguration, imports, errors, rawData };
2955
+ }
2956
+ _decodeConfiguration(location, config) {
2957
+ this._logger.info("Decoding configuration", { owner: location.owner, repository: location.repo });
2958
+ try {
2959
+ const configSchemaWithDefaults = Value.Default(configSchema, config);
2960
+ const errors = Value.Errors(configSchema, configSchemaWithDefaults);
2961
+ if (errors.First()) {
2962
+ for (const error of errors) {
2963
+ this._logger.warn("Configuration validation error", { err: error });
2816
2964
  }
2817
- const decodedConfig = Value.Decode(configSchema, configSchemaWithDefaults);
2818
- return { config: decodedConfig, errors: errors2.First() ? errors2 : null, rawData };
2819
- } catch (error) {
2820
- this._logger.error("Error decoding configuration; Will ignore.", { err: error, owner, repository });
2821
- return { config: null, errors: [error instanceof TransformDecodeCheckError ? error.error : error], rawData };
2822
2965
  }
2966
+ const decodedConfig = Value.Decode(configSchema, configSchemaWithDefaults);
2967
+ return { config: stripImports(decodedConfig), errors: errors.First() ? errors : null };
2968
+ } catch (error) {
2969
+ this._logger.warn("Error decoding configuration; Will ignore.", { err: error, owner: location.owner, repository: location.repo });
2970
+ return { config: null, errors: [error instanceof TransformDecodeCheckError ? error.error : error] };
2823
2971
  }
2824
- this._logger.error("YAML could not be decoded", { owner, repository, errors });
2825
- return { config: null, errors, rawData };
2826
2972
  }
2827
- async _download({ repository, owner }) {
2828
- if (!repository || !owner) {
2829
- this._logger.error("Repo or owner is not defined, cannot download the requested file");
2973
+ async _resolveImportedConfiguration(location, state, depth) {
2974
+ const key = normalizeImportKey(location);
2975
+ if (state.cache.has(key)) {
2976
+ return state.cache.get(key) ?? null;
2977
+ }
2978
+ if (state.inFlight.has(key)) {
2979
+ this._logger.warn("Skipping import due to circular reference.", { location });
2830
2980
  return null;
2831
2981
  }
2832
- let pathList;
2833
- switch (this._environment) {
2834
- case "development":
2835
- pathList = [CONFIG_DEV_FULL_PATH];
2836
- break;
2837
- case "production":
2838
- pathList = [CONFIG_PROD_FULL_PATH];
2839
- break;
2840
- default:
2841
- pathList = [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
2982
+ if (depth > MAX_IMPORT_DEPTH) {
2983
+ this._logger.warn("Skipping import; maximum depth exceeded.", { location, depth });
2984
+ return null;
2985
+ }
2986
+ state.inFlight.add(key);
2987
+ let resolved = null;
2988
+ try {
2989
+ const octokit = await this._getOctokitForLocation(location, state);
2990
+ if (!octokit) {
2991
+ this._logger.warn("Skipping import; no authorized Octokit for owner.", { location });
2992
+ return null;
2993
+ }
2994
+ const { config, imports, errors } = await this._loadConfigSource(location, octokit);
2995
+ if (errors && errors.length) {
2996
+ this._logger.warn("Skipping import due to YAML parsing errors.", { location, errors });
2997
+ return null;
2998
+ }
2999
+ if (!config) {
3000
+ return null;
3001
+ }
3002
+ const importedConfigs = [];
3003
+ for (const next of imports) {
3004
+ const nested = await this._resolveImportedConfiguration(next, state, depth + 1);
3005
+ if (nested) importedConfigs.push(nested);
3006
+ }
3007
+ const mergedConfig = mergeImportedConfigs(importedConfigs, config);
3008
+ if (!mergedConfig) return null;
3009
+ const decoded = this._decodeConfiguration(location, mergedConfig);
3010
+ resolved = decoded.config;
3011
+ } finally {
3012
+ state.inFlight.delete(key);
3013
+ state.cache.set(key, resolved);
2842
3014
  }
3015
+ return resolved;
3016
+ }
3017
+ async _download({
3018
+ repository,
3019
+ owner,
3020
+ octokit
3021
+ }) {
3022
+ if (!repository || !owner) {
3023
+ this._logger.warn("Repo or owner is not defined, cannot download the requested file");
3024
+ return null;
3025
+ }
3026
+ const pathList = getConfigPathCandidatesForEnvironment(this._environment);
2843
3027
  for (const filePath of pathList) {
2844
3028
  try {
2845
- this._logger.debug("Attempting to fetch configuration", { owner, repository, filePath });
2846
- const { data, headers } = await this._octokit.rest.repos.getContent({
3029
+ this._logger.info("Attempting to fetch configuration", { owner, repository, filePath });
3030
+ const { data, headers } = await octokit.rest.repos.getContent({
2847
3031
  owner,
2848
3032
  repo: repository,
2849
3033
  path: filePath,
2850
3034
  mediaType: { format: "raw" }
2851
3035
  });
2852
- this._logger.debug("Configuration file found", { owner, repository, filePath, rateLimitRemaining: headers?.["x-ratelimit-remaining"], data });
3036
+ this._logger.ok("Configuration file found", { owner, repository, filePath, rateLimitRemaining: headers?.["x-ratelimit-remaining"], data });
2853
3037
  return data;
2854
3038
  } catch (err) {
2855
3039
  if (err && typeof err === "object" && "status" in err && err.status === 404) {
2856
3040
  this._logger.warn("No configuration file found", { owner, repository, filePath });
2857
3041
  } else {
2858
- this._logger.error("Failed to download the requested file", { err, owner, repository, filePath });
3042
+ const status = err && typeof err === "object" && "status" in err ? Number(err.status) : null;
3043
+ const metadata = { err, owner, repository, filePath, ...status ? { status } : {} };
3044
+ if (status && status >= 500) {
3045
+ this._logger.error("Failed to download the requested file", metadata);
3046
+ } else {
3047
+ this._logger.warn("Failed to download the requested file", metadata);
3048
+ }
2859
3049
  }
2860
3050
  }
2861
3051
  }
2862
3052
  return null;
2863
3053
  }
2864
3054
  _parseYaml(data) {
2865
- this._logger.debug("Will attempt to parse YAML data", { data });
3055
+ this._logger.info("Will attempt to parse YAML data", { data });
2866
3056
  try {
2867
3057
  if (data) {
2868
3058
  const parsedData = jsYaml.load(data);
2869
- this._logger.debug("Parsed yaml data", { parsedData });
3059
+ this._logger.ok("Parsed yaml data", { parsedData });
2870
3060
  return { yaml: parsedData ?? null, errors: null };
2871
3061
  }
2872
3062
  } catch (error) {
2873
- this._logger.error("Error parsing YAML", { error });
3063
+ this._logger.warn("Error parsing YAML", { error });
2874
3064
  return { errors: [error], yaml: null };
2875
3065
  }
2876
- this._logger.debug("Could not parse YAML");
3066
+ this._logger.warn("Could not parse YAML");
2877
3067
  return { yaml: null, errors: null };
2878
3068
  }
2879
3069
  mergeConfigurations(configuration1, configuration2) {
@@ -2914,7 +3104,7 @@ var ConfigurationHandler = class {
2914
3104
  return manifest;
2915
3105
  }
2916
3106
  } catch (e) {
2917
- this._logger.error("Could not find a valid manifest", { owner, repo, err: e });
3107
+ this._logger.warn("Could not find a valid manifest", { owner, repo, err: e });
2918
3108
  }
2919
3109
  return null;
2920
3110
  })();
@@ -2929,7 +3119,7 @@ var ConfigurationHandler = class {
2929
3119
  const errors = [...Value.Errors(manifestSchema, manifest)];
2930
3120
  if (errors.length) {
2931
3121
  for (const error of errors) {
2932
- this._logger.error("Manifest validation error", { error });
3122
+ this._logger.warn("Manifest validation error", { error });
2933
3123
  }
2934
3124
  throw new Error("Manifest is invalid.");
2935
3125
  }
@@ -45,6 +45,7 @@ declare class CommentHandler {
45
45
  _extractIssueContext(context: Context): IssueContext | null;
46
46
  _processMessage(context: Context, message: LogReturn | Error): {
47
47
  metadata: {
48
+ status?: number | undefined;
48
49
  message: string;
49
50
  name: string;
50
51
  stack: string | undefined;
@@ -45,6 +45,7 @@ declare class CommentHandler {
45
45
  _extractIssueContext(context: Context): IssueContext | null;
46
46
  _processMessage(context: Context, message: LogReturn | Error): {
47
47
  metadata: {
48
+ status?: number | undefined;
48
49
  message: string;
49
50
  name: string;
50
51
  stack: string | undefined;
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EmitterWebhookEventName } from '@octokit/webhooks';
2
- import { C as Context } from './context-sqbr2o6i.mjs';
3
- export { a as CommentHandler } from './context-sqbr2o6i.mjs';
2
+ import { C as Context } from './context-Ckj1HMjz.mjs';
3
+ export { a as CommentHandler } from './context-Ckj1HMjz.mjs';
4
4
  import { TSchema, TAnySchema } from '@sinclair/typebox';
5
5
  import { LogLevel } from '@ubiquity-os/ubiquity-os-logger';
6
6
  import * as hono_types from 'hono/types';
@@ -14,8 +14,8 @@ import '@octokit/plugin-paginate-graphql';
14
14
  import '@octokit/plugin-paginate-rest';
15
15
  import '@octokit/request-error';
16
16
  import '@octokit/core';
17
- import './signature.mjs';
18
17
  import 'openai/resources/chat/completions';
18
+ import './signature.mjs';
19
19
 
20
20
  type Return = Record<string, unknown> | undefined | void;
21
21
  type HandlerReturn = Promise<Return> | Return;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EmitterWebhookEventName } from '@octokit/webhooks';
2
- import { C as Context } from './context-BbEmsEct.js';
3
- export { a as CommentHandler } from './context-BbEmsEct.js';
2
+ import { C as Context } from './context-BE4WjJZf.js';
3
+ export { a as CommentHandler } from './context-BE4WjJZf.js';
4
4
  import { TSchema, TAnySchema } from '@sinclair/typebox';
5
5
  import { LogLevel } from '@ubiquity-os/ubiquity-os-logger';
6
6
  import * as hono_types from 'hono/types';
@@ -14,8 +14,8 @@ import '@octokit/plugin-paginate-graphql';
14
14
  import '@octokit/plugin-paginate-rest';
15
15
  import '@octokit/request-error';
16
16
  import '@octokit/core';
17
- import './signature.js';
18
17
  import 'openai/resources/chat/completions';
18
+ import './signature.js';
19
19
 
20
20
  type Return = Record<string, unknown> | undefined | void;
21
21
  type HandlerReturn = Promise<Return> | Return;