@theholocron/cli 2.0.0-alpha.18 → 2.0.0-alpha.19

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.
package/README.md CHANGED
@@ -10,11 +10,79 @@ npm i -g @theholocron/cli@alpha
10
10
  holocron --help
11
11
  ```
12
12
 
13
+ ## Config file
14
+
15
+ Holocron reads `holocron.config.{json,js,ts}` from the project root
16
+ (priority: json → js → ts).
17
+
18
+ **JSON** (simplest):
19
+ ```jsonc
20
+ // holocron.config.json
21
+ {
22
+ "project": { "name": "my-app" },
23
+ "providers": {
24
+ "vault": ["1password", { "vault": "my-app" }],
25
+ "source": "github"
26
+ }
27
+ }
28
+ ```
29
+
30
+ **JS/TS** — use `defineConfig` for autocomplete and type-checking:
31
+ ```ts
32
+ // holocron.config.ts
33
+ import { defineConfig } from '@theholocron/cli'
34
+
35
+ export default defineConfig({
36
+ project: { name: 'my-app' },
37
+ providers: {
38
+ vault: ['1password', { vault: 'my-app' }],
39
+ source: 'github',
40
+ },
41
+ })
42
+ ```
43
+
44
+ ### Shareable configs
45
+
46
+ **Level 1 — per-capability config packages.** Reference a published
47
+ package in any provider slot and Holocron resolves its bundled
48
+ `{ provider, options }` automatically. Per-project options merge on
49
+ top (project wins):
50
+
51
+ ```ts
52
+ providers: {
53
+ vault: '@acme/holocron-vault', // preset only
54
+ source: ['@acme/holocron-github', { repo: 'x' }], // preset + override
55
+ }
56
+ ```
57
+
58
+ A capability config package exports a `CapabilityConfigPackage` default:
59
+ ```ts
60
+ import type { CapabilityConfigPackage } from '@theholocron/cli'
61
+ export default {
62
+ provider: '1password',
63
+ options: { vault: 'acme-app' },
64
+ } satisfies CapabilityConfigPackage
65
+ ```
66
+
67
+ **Level 2 — whole-config presets.** Because the config file can be
68
+ JS/TS, a shared base is just an import:
69
+
70
+ ```ts
71
+ // holocron.config.ts
72
+ import { acmeConfig } from '@acme/holocron-config'
73
+ export default acmeConfig
74
+ ```
75
+
13
76
  ## What's in here
14
77
 
15
78
  - `src/capabilities/` — the 14 capability interfaces that providers
16
79
  implement
17
- - `src/config.ts` — `holocron.config.json` parser + plugin resolution
80
+ - `src/config.ts` — config schema, `defineConfig`, `resolveConfig`,
81
+ `CapabilityConfigPackage`
82
+ - `src/load-config.ts` — `loadConfig` — reads JSON/JS/TS config files
83
+ - `src/define-config.ts` — `defineConfig` typed pass-through
84
+ - `src/loader.ts` — `PluginLoader` — dynamic-imports plugins, resolves
85
+ capability config packages, builds the capability registry
18
86
  - `src/cli.ts` — yargs entry, dispatches subcommands
19
87
  - `src/commands/` — `setup`, `doctor`, `deploy`, `secret set`,
20
88
  `secrets sync`, `npm publish-initial`
package/dist/cli.mjs CHANGED
@@ -7,6 +7,7 @@ import path, { dirname, join } from "node:path";
7
7
  import { createHash } from "node:crypto";
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { readFile, stat } from "node:fs/promises";
10
+ import { pathToFileURL } from "node:url";
10
11
  //#region src/capabilities/index.ts
11
12
  const CARDINALITY = {
12
13
  source: "single",
@@ -446,17 +447,30 @@ var PluginLoader = class {
446
447
  }
447
448
  /** Internal — invoke a plugin's capability factory and return the impl. */
448
449
  async loadOne(key, tuple) {
449
- const module = await this.importer(tuple.packageName).catch((err) => {
450
+ const mod = await this.importer(tuple.packageName).catch((err) => {
450
451
  throw new LoaderError(`failed to import \`${tuple.packageName}\` for capability \`${key}\`: ${err instanceof Error ? err.message : String(err)}`);
451
452
  });
452
- if (typeof module.createPlugin !== "function") throw new LoaderError(`\`${tuple.packageName}\` does not export \`createPlugin(options)\``);
453
- const factory = module.createPlugin({
454
- ...this.projectDefaults(),
455
- ...this.context,
456
- ...tuple.options
457
- }).capabilities[key];
458
- if (typeof factory !== "function") throw new LoaderError(`\`${tuple.packageName}\` does not implement the \`${key}\` capability`);
459
- return factory();
453
+ if (isPluginModule(mod)) {
454
+ const factory = mod.createPlugin({
455
+ ...this.projectDefaults(),
456
+ ...this.context,
457
+ ...tuple.options
458
+ }).capabilities[key];
459
+ if (typeof factory !== "function") throw new LoaderError(`\`${tuple.packageName}\` does not implement the \`${key}\` capability`);
460
+ return factory();
461
+ }
462
+ if (isCapabilityConfigModule(mod)) {
463
+ const cap = mod.default;
464
+ return this.loadOne(key, {
465
+ provider: cap.provider,
466
+ packageName: resolvePluginPackage(cap.provider),
467
+ options: {
468
+ ...cap.options,
469
+ ...tuple.options
470
+ }
471
+ });
472
+ }
473
+ throw new LoaderError(`\`${tuple.packageName}\` does not export \`createPlugin(options)\` or a capability config ({ provider, options? })`);
460
474
  }
461
475
  /**
462
476
  * Project-level defaults that get merged into every plugin's options
@@ -471,8 +485,14 @@ var PluginLoader = class {
471
485
  };
472
486
  /** Default importer — native dynamic import. */
473
487
  const defaultImporter = async (pkg) => {
474
- return await import(pkg);
488
+ return import(pkg);
475
489
  };
490
+ function isPluginModule(mod) {
491
+ return typeof mod.createPlugin === "function";
492
+ }
493
+ function isCapabilityConfigModule(mod) {
494
+ return typeof mod.default?.provider === "string";
495
+ }
476
496
  //#endregion
477
497
  //#region src/commands/deploy.ts
478
498
  async function runDeploy(input) {
@@ -745,13 +765,13 @@ on: # yamllint disable-line rule:truthy
745
765
  workflow_call:
746
766
  inputs:
747
767
  build-script:
748
- description: Script to build before analyzing bundle size
768
+ description: Script to build and upload bundle stats to Codecov
749
769
  type: string
750
770
  required: false
751
771
  default: pnpm build
752
772
  secrets:
753
- BUNDLEWATCH_GITHUB_TOKEN:
754
- required: true
773
+ CODECOV_TOKEN:
774
+ required: false
755
775
 
756
776
  jobs:
757
777
  bundle-size:
@@ -764,7 +784,7 @@ jobs:
764
784
  group: audit-\${{ github.ref }}
765
785
  cancel-in-progress: true
766
786
  steps:
767
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
787
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
768
788
  name: Checkout repository
769
789
  with:
770
790
  fetch-depth: 0
@@ -772,11 +792,10 @@ jobs:
772
792
  - uses: theholocron/.github/.github/actions/setup@main
773
793
  name: Setup
774
794
 
775
- - uses: jackyef/bundlewatch-gh-action@01f51133d3580a6daa046ca83eb233d79735e1c1 # 0.3.0
776
- name: Analyze using BundleWatch
777
- with:
778
- build-script: \${{ inputs.build-script }}
779
- bundlewatch-github-token: \${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
795
+ - run: \${{ inputs.build-script }}
796
+ name: Build and upload bundle stats
797
+ env:
798
+ CODECOV_TOKEN: \${{ secrets.CODECOV_TOKEN }}
780
799
  `,
781
800
  "bookkeeping-pr": `\
782
801
  name: PR Bookkeeping
@@ -802,7 +821,7 @@ jobs:
802
821
  group: bookkeeping-pr-\${{ github.event.pull_request.number }}
803
822
  cancel-in-progress: true
804
823
  steps:
805
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
824
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
806
825
  with:
807
826
  sparse-checkout: \${{ inputs.configuration-path || '.github/labeler.yml' }}
808
827
  sparse-checkout-cone-mode: false
@@ -845,18 +864,18 @@ jobs:
845
864
  group: codeql-\${{ github.ref }}
846
865
  cancel-in-progress: false
847
866
  steps:
848
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
867
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
849
868
  name: Checkout repository
850
869
 
851
- - uses: github/codeql-action/init@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3
870
+ - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
852
871
  name: Initialize CodeQL
853
872
  with:
854
873
  languages: \${{ inputs.language }}
855
874
 
856
- - uses: github/codeql-action/autobuild@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3
875
+ - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
857
876
  name: Autobuild
858
877
 
859
- - uses: github/codeql-action/analyze@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3
878
+ - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
860
879
  name: Analyze
861
880
  with:
862
881
  category: /language:\${{ inputs.language }}
@@ -887,7 +906,7 @@ jobs:
887
906
  cancel-in-progress: true
888
907
  if: github.event.pull_request.user.login == 'dependabot[bot]'
889
908
  steps:
890
- - uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2
909
+ - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
891
910
  name: Fetch Dependabot metadata
892
911
  id: metadata
893
912
 
@@ -919,7 +938,7 @@ jobs:
919
938
  group: greetings-\${{ github.event.issue.number || github.event.pull_request.number }}
920
939
  cancel-in-progress: false
921
940
  steps:
922
- - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
941
+ - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
923
942
  name: Greet on first contribution
924
943
  with:
925
944
  script: |
@@ -995,7 +1014,7 @@ jobs:
995
1014
  env:
996
1015
  GPG_KEY_SET: \${{ secrets.SUPER_LINTER_GPG_PRIVATE_KEY != '' }}
997
1016
  steps:
998
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1017
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
999
1018
  name: Checkout repository
1000
1019
  with:
1001
1020
  fetch-depth: 0
@@ -1004,7 +1023,7 @@ jobs:
1004
1023
  - uses: theholocron/.github/.github/actions/setup@main
1005
1024
  name: Setup
1006
1025
 
1007
- - uses: super-linter/super-linter/slim@b92721f792f381cedc002ecdbb9847a15ece5bb8 # v7.1.0
1026
+ - uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0
1008
1027
  name: Run Super Linter
1009
1028
  env:
1010
1029
  GITHUB_TOKEN: \${{ github.token }}
@@ -1039,7 +1058,7 @@ jobs:
1039
1058
  VALIDATE_YAML: true
1040
1059
  YAML_CONFIG_FILE: \${{ inputs.yaml-config }}
1041
1060
 
1042
- - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
1061
+ - uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
1043
1062
  name: Import GPG Key
1044
1063
  # Conditions mirror auto-commit exactly — no point importing GPG if the
1045
1064
  # commit step will be skipped (fork PR, default branch, or secret unset).
@@ -1055,7 +1074,7 @@ jobs:
1055
1074
  GPG_PRIVATE_KEY: \${{ secrets.SUPER_LINTER_GPG_PRIVATE_KEY }}
1056
1075
  PASSPHRASE: \${{ secrets.SUPER_LINTER_GPG_PASSPHRASE }}
1057
1076
 
1058
- - uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5
1077
+ - uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
1059
1078
  name: Commit and push linting fixes
1060
1079
  if: >
1061
1080
  inputs.enable-auto-commit == true &&
@@ -1108,7 +1127,7 @@ jobs:
1108
1127
  group: release-\${{ github.ref }}
1109
1128
  cancel-in-progress: false
1110
1129
  steps:
1111
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1130
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
1112
1131
  name: Checkout repository
1113
1132
  with:
1114
1133
  fetch-depth: 0
@@ -1180,7 +1199,7 @@ jobs:
1180
1199
 
1181
1200
  steps:
1182
1201
  - name: Checkout repository
1183
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1202
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
1184
1203
 
1185
1204
  - name: Setup
1186
1205
  if: \${{ hashFiles('pnpm-lock.yaml') != '' }}
@@ -1283,7 +1302,7 @@ jobs:
1283
1302
 
1284
1303
  - name: dotenv-linter
1285
1304
  if: steps.detect.outputs.dotenv == 'true'
1286
- uses: dotenv-linter/action-dotenv-linter@21287e2624aaf2dc8da5dd8ccfe8e49c63501116 # v2
1305
+ uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0
1287
1306
  with:
1288
1307
  reporter: github-code-suggestions
1289
1308
 
@@ -1324,7 +1343,7 @@ jobs:
1324
1343
  runs-on: ubuntu-latest
1325
1344
  timeout-minutes: 10
1326
1345
  steps:
1327
- - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
1346
+ - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
1328
1347
  name: Run Stale
1329
1348
  with:
1330
1349
  close-issue-message: >
@@ -1361,7 +1380,7 @@ jobs:
1361
1380
  runs-on: ubuntu-latest
1362
1381
  timeout-minutes: 15
1363
1382
  steps:
1364
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1383
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
1365
1384
  name: Checkout repository
1366
1385
  with:
1367
1386
  fetch-depth: 0
@@ -1372,8 +1391,14 @@ jobs:
1372
1391
  - run: pnpm test -- --coverage
1373
1392
  name: Run tests with coverage
1374
1393
 
1375
- - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
1376
- name: Upload results to Codecov
1394
+ - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
1395
+ name: Upload coverage to Codecov
1396
+ with:
1397
+ token: \${{ secrets.CODECOV_TOKEN }}
1398
+
1399
+ - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1
1400
+ name: Upload test results to Codecov
1401
+ if: \${{ !cancelled() }}
1377
1402
  with:
1378
1403
  token: \${{ secrets.CODECOV_TOKEN }}
1379
1404
  `,
@@ -1424,7 +1449,7 @@ jobs:
1424
1449
  permissions:
1425
1450
  contents: read
1426
1451
  steps:
1427
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1452
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
1428
1453
  name: Checkout repository
1429
1454
 
1430
1455
  - uses: theholocron/.github/.github/actions/setup@main
@@ -1485,7 +1510,7 @@ jobs:
1485
1510
  runs-on: ubuntu-latest
1486
1511
  timeout-minutes: 10
1487
1512
  steps:
1488
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1513
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
1489
1514
  name: Checkout repository
1490
1515
 
1491
1516
  - uses: theholocron/.github/.github/actions/setup@main
@@ -3857,15 +3882,13 @@ function formatStep(step) {
3857
3882
  /**
3858
3883
  * `holocron.config.{json,js,ts}` file loader.
3859
3884
  *
3860
- * v2.0 only documents the JSON form, but the loader looks up all three
3861
- * extensions in priority order (json js ts) that's the schema
3862
- * commitment captured in [`.notes/tech-architecture.spec.md` Roadmap
3863
- * Shareable configs] so the JS/TS preset story (issue #75) can land
3864
- * later without a breaking change.
3885
+ * Search order: json js ts. JSON is parsed directly; JS is loaded
3886
+ * via native dynamic import; TS is loaded via `tsImport` from tsx (a
3887
+ * runtime dep) so operators can write typed configs with `defineConfig`
3888
+ * without needing a separate build step.
3865
3889
  *
3866
- * The JS/TS forms aren't actually parsed today; they trigger a clear
3867
- * error telling the operator to use JSON for v2.0. The contract is the
3868
- * lookup order, not the interpretation.
3890
+ * All three forms are validated through the same `resolveConfig` path.
3891
+ * Implements the lookup-order contract from issue #75 / #81.
3869
3892
  */
3870
3893
  const CANDIDATE_FILENAMES = [
3871
3894
  "holocron.config.json",
@@ -3878,7 +3901,7 @@ var ConfigFileError = class extends Error {
3878
3901
  /**
3879
3902
  * Read + parse + resolve `holocron.config.*` from the given directory.
3880
3903
  * Search order: json → js → ts. Throws `ConfigFileError` if nothing
3881
- * found, or `ConfigError` if the JSON is malformed / invalid.
3904
+ * found, or `ConfigError` if the config is malformed / invalid.
3882
3905
  */
3883
3906
  async function loadConfig(cwd) {
3884
3907
  for (const filename of CANDIDATE_FILENAMES) {
@@ -3888,7 +3911,14 @@ async function loadConfig(cwd) {
3888
3911
  resolved: await loadJson(fullPath),
3889
3912
  filepath: fullPath
3890
3913
  };
3891
- throw new ConfigFileError(`${filename} found, but v2.0 only supports the JSON form. Rename to holocron.config.json. The JS/TS form lands with the preset feature (see issue #75).`);
3914
+ if (filename.endsWith(".ts")) return {
3915
+ resolved: await loadTs(fullPath),
3916
+ filepath: fullPath
3917
+ };
3918
+ return {
3919
+ resolved: await loadJs(fullPath),
3920
+ filepath: fullPath
3921
+ };
3892
3922
  }
3893
3923
  }
3894
3924
  throw new ConfigFileError(`no holocron.config.{json,js,ts} found in ${cwd}. Create one — see the README for the schema.`);
@@ -3903,6 +3933,19 @@ async function loadJson(filepath) {
3903
3933
  }
3904
3934
  return resolveConfig(parsed);
3905
3935
  }
3936
+ async function loadJs(filepath) {
3937
+ return extractAndResolve(filepath, await import(pathToFileURL(filepath).href));
3938
+ }
3939
+ async function loadTs(filepath) {
3940
+ const { tsImport } = await import("tsx/esm/api");
3941
+ const outer = await tsImport(pathToFileURL(filepath).href, import.meta.url);
3942
+ return extractAndResolve(filepath, outer.default ?? outer);
3943
+ }
3944
+ function extractAndResolve(filepath, mod) {
3945
+ const raw = mod.default;
3946
+ if (raw === void 0 || raw === null) throw new ConfigFileError(`${filepath} must have a default export (use \`export default defineConfig({…})\`)`);
3947
+ return resolveConfig(raw);
3948
+ }
3906
3949
  async function fileExists(path) {
3907
3950
  try {
3908
3951
  return (await stat(path)).isFile();
package/dist/index.d.mts CHANGED
@@ -2,6 +2,23 @@ import { Analytics, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentit
2
2
 
3
3
  //#region src/config.d.ts
4
4
  type ProviderOptions = Record<string, unknown>;
5
+ /**
6
+ * The shape a capability config package's default export must satisfy.
7
+ * Config packages let teams share a pre-bundled provider + options across
8
+ * repos (Level 1 of the shareable-configs story, issue #75).
9
+ *
10
+ * @example
11
+ * // @acme/holocron-vault/index.ts
12
+ * import type { CapabilityConfigPackage } from '@theholocron/cli'
13
+ * export default {
14
+ * provider: '1password',
15
+ * options: { vault: 'acme-app' },
16
+ * } satisfies CapabilityConfigPackage
17
+ */
18
+ interface CapabilityConfigPackage {
19
+ provider: string;
20
+ options?: ProviderOptions;
21
+ }
5
22
  type SingleEntry = string | [provider: string, options: ProviderOptions];
6
23
  type MultiEntry = Array<string | [provider: string, options: ProviderOptions]>;
7
24
  type RawProviderEntry = SingleEntry | MultiEntry;
@@ -103,6 +120,9 @@ declare function resolvePluginPackage(provider: string): string;
103
120
  declare function resolveEntry(key: CapabilityKey, raw: RawProviderEntry): ResolvedProviderEntry;
104
121
  declare function resolveConfig(raw: HolocronConfig): ResolvedHolocronConfig;
105
122
  //#endregion
123
+ //#region src/define-config.d.ts
124
+ declare function defineConfig(config: HolocronConfig): HolocronConfig;
125
+ //#endregion
106
126
  //#region src/keyring.d.ts
107
127
  /**
108
128
  * Keyring-backed bootstrap credential store.
@@ -147,4 +167,20 @@ declare function deleteToken(provider: string): boolean;
147
167
  */
148
168
  declare function listStoredProviders(): string[];
149
169
  //#endregion
150
- export { Analytics, AppConfig, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConfigError, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, DoctorConfig, EnsureResult, Environment, EnvironmentReviewer, Environments, HolocronConfig, Issue, IssueSearchFilter, Issues, LifecycleResult, LifecycleSlot, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoPolicyConfig, RepoRef, RepoSettings, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, deleteToken, getToken, isMulti, listStoredProviders, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
170
+ //#region src/load-config.d.ts
171
+ declare class ConfigFileError extends Error {
172
+ name: string;
173
+ }
174
+ interface LoadedConfig {
175
+ resolved: ResolvedHolocronConfig;
176
+ /** Absolute path to the file the config was read from. */
177
+ filepath: string;
178
+ }
179
+ /**
180
+ * Read + parse + resolve `holocron.config.*` from the given directory.
181
+ * Search order: json → js → ts. Throws `ConfigFileError` if nothing
182
+ * found, or `ConfigError` if the config is malformed / invalid.
183
+ */
184
+ declare function loadConfig(cwd: string): Promise<LoadedConfig>;
185
+ //#endregion
186
+ export { Analytics, AppConfig, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityConfigPackage, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConfigError, ConfigFileError, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, DoctorConfig, EnsureResult, Environment, EnvironmentReviewer, Environments, HolocronConfig, Issue, IssueSearchFilter, Issues, LifecycleResult, LifecycleSlot, LoadedConfig, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoPolicyConfig, RepoRef, RepoSettings, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
package/dist/index.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  import { CARDINALITY, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, isMulti } from "./capabilities/index.mjs";
2
2
  import { Entry, findCredentials } from "@napi-rs/keyring";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
3
6
  //#region src/config.ts
4
7
  /**
5
8
  * `holocron.config.json` schema, parser, and provider resolution.
@@ -111,6 +114,11 @@ function resolveConfig(raw) {
111
114
  };
112
115
  }
113
116
  //#endregion
117
+ //#region src/define-config.ts
118
+ function defineConfig(config) {
119
+ return config;
120
+ }
121
+ //#endregion
114
122
  //#region src/keyring.ts
115
123
  /**
116
124
  * Keyring-backed bootstrap credential store.
@@ -181,4 +189,80 @@ function listStoredProviders() {
181
189
  }
182
190
  }
183
191
  //#endregion
184
- export { CARDINALITY, ConfigError, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, deleteToken, getToken, isMulti, listStoredProviders, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
192
+ //#region src/load-config.ts
193
+ /**
194
+ * `holocron.config.{json,js,ts}` file loader.
195
+ *
196
+ * Search order: json → js → ts. JSON is parsed directly; JS is loaded
197
+ * via native dynamic import; TS is loaded via `tsImport` from tsx (a
198
+ * runtime dep) so operators can write typed configs with `defineConfig`
199
+ * without needing a separate build step.
200
+ *
201
+ * All three forms are validated through the same `resolveConfig` path.
202
+ * Implements the lookup-order contract from issue #75 / #81.
203
+ */
204
+ const CANDIDATE_FILENAMES = [
205
+ "holocron.config.json",
206
+ "holocron.config.js",
207
+ "holocron.config.ts"
208
+ ];
209
+ var ConfigFileError = class extends Error {
210
+ name = "ConfigFileError";
211
+ };
212
+ /**
213
+ * Read + parse + resolve `holocron.config.*` from the given directory.
214
+ * Search order: json → js → ts. Throws `ConfigFileError` if nothing
215
+ * found, or `ConfigError` if the config is malformed / invalid.
216
+ */
217
+ async function loadConfig(cwd) {
218
+ for (const filename of CANDIDATE_FILENAMES) {
219
+ const fullPath = join(cwd, filename);
220
+ if (await fileExists(fullPath)) {
221
+ if (filename.endsWith(".json")) return {
222
+ resolved: await loadJson(fullPath),
223
+ filepath: fullPath
224
+ };
225
+ if (filename.endsWith(".ts")) return {
226
+ resolved: await loadTs(fullPath),
227
+ filepath: fullPath
228
+ };
229
+ return {
230
+ resolved: await loadJs(fullPath),
231
+ filepath: fullPath
232
+ };
233
+ }
234
+ }
235
+ throw new ConfigFileError(`no holocron.config.{json,js,ts} found in ${cwd}. Create one — see the README for the schema.`);
236
+ }
237
+ async function loadJson(filepath) {
238
+ const raw = await readFile(filepath, "utf8");
239
+ let parsed;
240
+ try {
241
+ parsed = JSON.parse(raw);
242
+ } catch (err) {
243
+ throw new ConfigError(`${filepath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
244
+ }
245
+ return resolveConfig(parsed);
246
+ }
247
+ async function loadJs(filepath) {
248
+ return extractAndResolve(filepath, await import(pathToFileURL(filepath).href));
249
+ }
250
+ async function loadTs(filepath) {
251
+ const { tsImport } = await import("tsx/esm/api");
252
+ const outer = await tsImport(pathToFileURL(filepath).href, import.meta.url);
253
+ return extractAndResolve(filepath, outer.default ?? outer);
254
+ }
255
+ function extractAndResolve(filepath, mod) {
256
+ const raw = mod.default;
257
+ if (raw === void 0 || raw === null) throw new ConfigFileError(`${filepath} must have a default export (use \`export default defineConfig({…})\`)`);
258
+ return resolveConfig(raw);
259
+ }
260
+ async function fileExists(path) {
261
+ try {
262
+ return (await stat(path)).isFile();
263
+ } catch {
264
+ return false;
265
+ }
266
+ }
267
+ //#endregion
268
+ export { CARDINALITY, ConfigError, ConfigFileError, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.18",
3
+ "version": "2.0.0-alpha.19",
4
4
  "description": "The Holocron CLI — a pluggable, capability-based orchestrator for spinning up and operating software projects.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/cli#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",
@@ -34,16 +34,19 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@napi-rs/keyring": "^1.3.0",
37
+ "tsx": "^4.22.4",
37
38
  "yargs": "^18.0.0"
38
39
  },
39
40
  "devDependencies": {
40
- "@theholocron/eslint-config": "^4.1.0",
41
+ "@theholocron/eslint-config": "^5.1.1",
41
42
  "@theholocron/tsconfig": "^4.1.0",
42
43
  "@tsconfig/node-lts": "^24.0.0",
43
44
  "@types/yargs": "^17.0.35",
44
45
  "@vitest/coverage-v8": "^3.2.6",
45
- "eslint": "^9.36.0",
46
- "globals": "^16.5.0",
46
+ "@vitest/eslint-plugin": "^1.6.23",
47
+ "eslint": "^10.7.0",
48
+ "eslint-plugin-n": "^18.2.2",
49
+ "globals": "^17.7.0",
47
50
  "tsdown": "^0.22.3",
48
51
  "typescript": "^5.9.3",
49
52
  "vitest": "^3.2.6",