@goodbones/cli 0.1.0-beta.16 → 0.1.0-beta.18

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.
Files changed (39) hide show
  1. package/build/dts/campaigns/host.d.ts +51 -0
  2. package/build/dts/campaigns/host.d.ts.map +1 -0
  3. package/build/dts/campaigns/live.d.ts +3 -0
  4. package/build/dts/campaigns/live.d.ts.map +1 -0
  5. package/build/dts/campaigns/none.d.ts +7 -0
  6. package/build/dts/campaigns/none.d.ts.map +1 -0
  7. package/build/dts/campaigns/verbs.d.ts +6 -0
  8. package/build/dts/campaigns/verbs.d.ts.map +1 -0
  9. package/build/dts/config-loader.d.ts +7 -5
  10. package/build/dts/config-loader.d.ts.map +1 -1
  11. package/build/dts/output.d.ts +9 -0
  12. package/build/dts/output.d.ts.map +1 -0
  13. package/build/dts/run.d.ts +3 -18
  14. package/build/dts/run.d.ts.map +1 -1
  15. package/build/esm/campaigns/host.js +2 -0
  16. package/build/esm/campaigns/host.js.map +1 -0
  17. package/build/esm/campaigns/live.js +106 -0
  18. package/build/esm/campaigns/live.js.map +1 -0
  19. package/build/esm/campaigns/none.js +34 -0
  20. package/build/esm/campaigns/none.js.map +1 -0
  21. package/build/esm/campaigns/verbs.js +489 -0
  22. package/build/esm/campaigns/verbs.js.map +1 -0
  23. package/build/esm/config-loader.js +40 -25
  24. package/build/esm/config-loader.js.map +1 -1
  25. package/build/esm/infer.js +3 -3
  26. package/build/esm/infer.js.map +1 -1
  27. package/build/esm/output.js +8 -0
  28. package/build/esm/output.js.map +1 -0
  29. package/build/esm/run.js +39 -555
  30. package/build/esm/run.js.map +1 -1
  31. package/package.json +18 -6
  32. package/src/campaigns/host.ts +120 -0
  33. package/src/campaigns/live.ts +136 -0
  34. package/src/campaigns/none.ts +47 -0
  35. package/src/campaigns/verbs.ts +620 -0
  36. package/src/config-loader.ts +41 -26
  37. package/src/infer.ts +3 -3
  38. package/src/output.ts +18 -0
  39. package/src/run.ts +56 -726
@@ -1,16 +1,10 @@
1
1
  import * as path from "node:path";
2
2
 
3
- import { astGrepMatcher } from "@goodbones/ast-grep";
4
- import {
5
- campaignsExtension,
6
- loadCampaignFunctions,
7
- makeReportSourceLive,
8
- } from "@goodbones/campaigns";
9
3
  import {
10
4
  type ConfigInvalid,
11
5
  findManifestFile,
6
+ importOptional,
12
7
  type Language,
13
- type LoadedPolicy,
14
8
  loadPolicy,
15
9
  makeFileSystemLive,
16
10
  type PatternInvalid,
@@ -19,15 +13,28 @@ import {
19
13
  import { typescriptLanguage } from "@goodbones/typescript";
20
14
  import * as Result from "effect/Result";
21
15
 
22
- export type { LoadedPolicy } from "@goodbones/core";
16
+ import type { CampaignsFamily, LoadedPolicy } from "./campaigns/host.js";
17
+ import { campaignsNotInstalled } from "./campaigns/none.js";
18
+
19
+ export type { LoadedPolicy } from "./campaigns/host.js";
23
20
 
24
21
  // The language packs this host is composed with. Constructed here and handed
25
22
  // down as the `Language` port, so nothing below this file names TypeScript —
26
- // or ast-grep, which the pack is composed with here for the campaigns
27
- // family's `syntax` term.
28
- export const hostLanguages = (): ReadonlyArray<Language> => [
29
- typescriptLanguage({ syntax: astGrepMatcher() }),
30
- ];
23
+ // or ast-grep, the syntax matcher for the campaigns family's `syntax` term.
24
+ // The matcher is an optional peer, with a native binary: composed into the
25
+ // pack when it is installed, and a campaign that needs it without it is
26
+ // refused at load, naming the package.
27
+ export const hostLanguages = async (): Promise<ReadonlyArray<Language>> => {
28
+ const astGrep = await importOptional(() => import("@goodbones/ast-grep"), "@goodbones/ast-grep");
29
+ return [typescriptLanguage(astGrep === null ? {} : { syntax: astGrep.astGrepMatcher() })];
30
+ };
31
+
32
+ // The campaigns family, when `@goodbones/campaigns` is installed beside this
33
+ // host, and `none` when it is not. Everything that names the package is
34
+ // under `campaigns/`, reached through this one import.
35
+ export const hostCampaigns = async (): Promise<CampaignsFamily> =>
36
+ (await importOptional(() => import("./campaigns/live.js"), "@goodbones/campaigns"))
37
+ ?.liveCampaigns ?? campaignsNotInstalled;
31
38
 
32
39
  // The clock the campaigns are judged by. `ARCHITECTURE_NOW` (an ISO date or
33
40
  // epoch milliseconds) pins it, for a CI that replays a day or a test that
@@ -53,43 +60,51 @@ export const manifestPathOf = (repoRoot: string, configFilename?: string): strin
53
60
  ? findManifestFile(repoRoot)
54
61
  : path.resolve(repoRoot, configFilename);
55
62
 
63
+ // `given` is the family to compose: found by `hostCampaigns` unless a
64
+ // test names one, which is how the host is run as if the package were absent.
56
65
  export const loadPolicyFromFile = async (
57
66
  repoRoot: string,
58
67
  configFilename?: string,
68
+ given?: CampaignsFamily,
59
69
  ): Promise<LoadedPolicy> => {
60
70
  const configPath = manifestPathOf(repoRoot, configFilename);
61
71
  const read = await readManifestFile(configPath);
62
- // The `fn` terms are imported here, before the policy loads: the core
63
- // receives the functions as a map and touches no module loader.
64
- const { functions, manifest } = await loadCampaignFunctions(configPath, read.manifest);
72
+ const campaigns = given ?? (await hostCampaigns());
73
+ const composed = await campaigns.compose(repoRoot, configPath, read.manifest);
65
74
  const loaded = loadPolicy({
66
75
  repoRoot,
67
76
  configPath,
68
- manifest,
77
+ manifest: composed.manifest,
69
78
  locate: read.locate,
70
- languages: hostLanguages(),
79
+ languages: await hostLanguages(),
71
80
  fileSystem: makeFileSystemLive(repoRoot),
72
- extensions: [campaignsExtension({ functions, reports: makeReportSourceLive(repoRoot) })],
81
+ extensions: composed.extensions,
82
+ uninstalled: composed.uninstalled,
73
83
  now: hostNow(),
74
84
  });
75
85
  if (Result.isFailure(loaded)) throw loaded.failure;
76
- return loaded.success;
86
+ return { ...loaded.success, campaigns: campaigns.host };
77
87
  };
78
88
 
79
89
  // The same composition, for a manifest the host already holds as a value
80
90
  // rather than a file: the bootstrap `infer` resolves through, and the
81
91
  // manifest it then proves loads. Failure is returned, not thrown — the caller
82
92
  // has a sentence to put around it.
83
- export const loadPolicyFromManifest = (
93
+ export const loadPolicyFromManifest = async (
84
94
  repoRoot: string,
85
95
  manifest: unknown,
86
- ): Result.Result<LoadedPolicy, ConfigInvalid | PatternInvalid> =>
87
- loadPolicy({
96
+ ): Promise<Result.Result<LoadedPolicy, ConfigInvalid | PatternInvalid>> => {
97
+ const campaigns = await hostCampaigns();
98
+ const composed = await campaigns.compose(repoRoot, null, manifest);
99
+ const loaded = loadPolicy({
88
100
  repoRoot,
89
101
  configPath: path.resolve(repoRoot, "architecture.yaml"),
90
- manifest,
91
- languages: hostLanguages(),
102
+ manifest: composed.manifest,
103
+ languages: await hostLanguages(),
92
104
  fileSystem: makeFileSystemLive(repoRoot),
93
- extensions: [campaignsExtension({ reports: makeReportSourceLive(repoRoot) })],
105
+ extensions: composed.extensions,
106
+ uninstalled: composed.uninstalled,
94
107
  now: hostNow(),
95
108
  });
109
+ return Result.map(loaded, (policy) => ({ ...policy, campaigns: campaigns.host }));
110
+ };
package/src/infer.ts CHANGED
@@ -237,13 +237,13 @@ const settingOf = async (
237
237
  const roots =
238
238
  flags.roots.length > 0
239
239
  ? flags.roots.map(normalizeRoot)
240
- : rootsOfRepository(repoRoot, hostLanguages());
240
+ : rootsOfRepository(repoRoot, await hostLanguages());
241
241
  if (roots.length === 0) {
242
242
  return Result.fail(
243
243
  "no source files found under any top-level folder. Name the folder to describe with --root <dir>.",
244
244
  );
245
245
  }
246
- const loaded = loadPolicyFromManifest(repoRoot, bootstrapManifest(roots, tsconfig));
246
+ const loaded = await loadPolicyFromManifest(repoRoot, bootstrapManifest(roots, tsconfig));
247
247
  if (Result.isFailure(loaded)) return Result.fail(String(loaded.failure));
248
248
  return Result.succeed({
249
249
  policy: loaded.success,
@@ -449,7 +449,7 @@ export const infer = (
449
449
 
450
450
  // The proof: what was written loads, every probe passes, and the floors
451
451
  // are the numbers it reaches today.
452
- const loaded = loadPolicyFromManifest(repoRoot, inferred.manifest);
452
+ const loaded = yield* Effect.promise(() => loadPolicyFromManifest(repoRoot, inferred.manifest));
453
453
  if (Result.isFailure(loaded)) {
454
454
  return yield* Effect.fail(
455
455
  fail(
package/src/output.ts ADDED
@@ -0,0 +1,18 @@
1
+ import * as Effect from "effect/Effect";
2
+
3
+ // What every command shares: how it fails, and how it writes. Its own module
4
+ // so the campaigns glue under `campaigns/` speaks the same way without
5
+ // reaching `run.ts`, which would close a cycle through the import that loads
6
+ // the glue.
7
+
8
+ export type CliFailure = { readonly _tag: "CliFailure"; readonly message: string };
9
+
10
+ export const fail = (message: string): CliFailure => ({ _tag: "CliFailure", message });
11
+
12
+ export const report = (lines: ReadonlyArray<string>): Effect.Effect<void> =>
13
+ Effect.sync(() => {
14
+ for (const line of lines) process.stdout.write(`${line}\n`);
15
+ });
16
+
17
+ export const count = (n: number, noun: string, plural = `${noun}s`): string =>
18
+ `${String(n)} ${n === 1 ? noun : plural}`;