@codedrifters/configulator 0.0.177 → 0.0.179

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
@@ -1255,44 +1255,42 @@ const project = new MonorepoProject({
1255
1255
  });
1256
1256
  ```
1257
1257
 
1258
- **Monorepo with per-package overrides:**
1258
+ **Monorepo with per-package opt-in:**
1259
+
1260
+ `AgentConfig` is **not** auto-inherited by sub-projects. Agent rule files are
1261
+ rendered only on projects that explicitly opt in, so the monorepo root's
1262
+ `AgentConfig` does not duplicate `.cursor/`, `.claude/`, or `CLAUDE.md` into
1263
+ each sub-project's `outdir`. Sub-projects that opt in do still resolve
1264
+ `ProjectMetadata` from the root, so template tokens like
1265
+ `{{repository.owner}}` work without redeclaring metadata.
1259
1266
 
1260
1267
  ```typescript
1261
1268
  const root = new MonorepoProject({
1262
1269
  name: "my-monorepo",
1263
1270
  agentConfig: {
1264
- // Root-level config applies to all sub-projects
1271
+ // Root-level config renders into the monorepo root only
1265
1272
  rules: [{ name: "monorepo-rule", description: "...", scope: AGENT_RULE_SCOPE.ALWAYS, content: "..." }],
1266
1273
  },
1267
1274
  });
1268
1275
 
1269
- // Sub-project inherits parent config by default
1276
+ // Sub-project with no agent config nothing rendered into packages/api/
1270
1277
  const api = new TypeScriptProject({
1271
1278
  parent: root,
1272
1279
  name: "@my-org/api",
1273
1280
  outdir: "packages/api",
1274
- // Inherits agentConfig from root
1275
1281
  });
1276
1282
 
1277
- // Sub-project with explicit overrides
1283
+ // Sub-project that explicitly opts into its own agent config
1278
1284
  const frontend = new TypeScriptProject({
1279
1285
  parent: root,
1280
1286
  name: "@my-org/frontend",
1281
1287
  outdir: "packages/frontend",
1282
1288
  agentConfig: {
1283
- // Override: only generate Claude config for this package
1289
+ // Only generate Claude config for this package
1284
1290
  platforms: [AGENT_PLATFORM.CLAUDE],
1285
1291
  excludeBundles: ["jest"], // This package uses Vitest, not Jest
1286
1292
  },
1287
1293
  });
1288
-
1289
- // Sub-project with agent config disabled
1290
- const scripts = new TypeScriptProject({
1291
- parent: root,
1292
- name: "@my-org/scripts",
1293
- outdir: "packages/scripts",
1294
- agentConfig: false, // No agent config for this package
1295
- });
1296
1294
  ```
1297
1295
 
1298
1296
  ## Troubleshooting
package/lib/index.d.mts CHANGED
@@ -388,6 +388,19 @@ interface AgentRuleBundle {
388
388
  * @example (project) => Vitest.of(project) !== undefined
389
389
  */
390
390
  readonly appliesWhen: (project: Project) => boolean;
391
+ /**
392
+ * Optional. When set, returns the list of projects (root and/or
393
+ * subprojects) that matched this bundle's detection predicate. Enables
394
+ * AgentConfig to narrow each rule's `filePatterns` to the outdirs of
395
+ * those projects instead of the bundle's original repo-wide patterns.
396
+ *
397
+ * Bundles that provide `FILE_PATTERN`-scoped rules about TypeScript or
398
+ * TypeScript-adjacent code (e.g. typescript, aws-cdk, vitest, jest) should
399
+ * implement this so their rules only activate for files in the packages
400
+ * that actually use them.
401
+ * @example (project) => findProjectsWithFile(project, 'tsconfig.json')
402
+ */
403
+ readonly findApplicableProjects?: (project: Project) => ReadonlyArray<Project>;
391
404
  /** Rules included in this bundle. */
392
405
  readonly rules: ReadonlyArray<AgentRule>;
393
406
  /** Skills included in this bundle (cross-platform where supported). */
@@ -836,6 +849,13 @@ declare class AgentConfig extends Component {
836
849
  preSynthesize(): void;
837
850
  private resolvePlatforms;
838
851
  private resolveRules;
852
+ /**
853
+ * Return a bundle's rules with `filePatterns` narrowed to the projects
854
+ * that actually matched the bundle's detection predicate (when the bundle
855
+ * provides `findApplicableProjects`). Rules with `ALWAYS` scope and rules
856
+ * on bundles that don't implement the hook are returned unchanged.
857
+ */
858
+ private bundleRulesFor;
839
859
  private resolveSkills;
840
860
  private resolveSubAgents;
841
861
  /**
@@ -2561,8 +2581,10 @@ declare class MonorepoProject extends TypeScriptAppProject {
2561
2581
  */
2562
2582
  declare class ProjectMetadata extends Component {
2563
2583
  /**
2564
- * Returns the ProjectMetadata instance for a project, or undefined
2565
- * if the component has not been added.
2584
+ * Returns the ProjectMetadata instance for a project. Walks up the parent
2585
+ * chain so sub-projects resolve the metadata declared on a root
2586
+ * `MonorepoProject` without needing a duplicate declaration of their own.
2587
+ * Returns `undefined` if no ancestor has a `ProjectMetadata` component.
2566
2588
  */
2567
2589
  static of(project: Project$1): ProjectMetadata | undefined;
2568
2590
  /** Resolved metadata with auto-detected values filled in. */
@@ -2718,8 +2740,10 @@ interface TypeScriptProjectOptions extends Omit<typescript.TypeScriptProjectOpti
2718
2740
  * - `AgentConfigOptions`: enable with explicit configuration
2719
2741
  * - `false` or `undefined`: disabled (default)
2720
2742
  *
2721
- * When the parent is a MonorepoProject with AgentConfig enabled and this
2722
- * option is not explicitly set, the sub-project inherits enablement.
2743
+ * Sub-projects do not inherit `AgentConfig` from their parent
2744
+ * `MonorepoProject`. Agent rule files are rendered only on projects that
2745
+ * explicitly opt in, so a monorepo's root `AgentConfig` does not duplicate
2746
+ * `.cursor/`, `.claude/`, or `CLAUDE.md` into each sub-project's `outdir`.
2723
2747
  *
2724
2748
  * @default false
2725
2749
  */
package/lib/index.d.ts CHANGED
@@ -437,6 +437,19 @@ interface AgentRuleBundle {
437
437
  * @example (project) => Vitest.of(project) !== undefined
438
438
  */
439
439
  readonly appliesWhen: (project: Project$1) => boolean;
440
+ /**
441
+ * Optional. When set, returns the list of projects (root and/or
442
+ * subprojects) that matched this bundle's detection predicate. Enables
443
+ * AgentConfig to narrow each rule's `filePatterns` to the outdirs of
444
+ * those projects instead of the bundle's original repo-wide patterns.
445
+ *
446
+ * Bundles that provide `FILE_PATTERN`-scoped rules about TypeScript or
447
+ * TypeScript-adjacent code (e.g. typescript, aws-cdk, vitest, jest) should
448
+ * implement this so their rules only activate for files in the packages
449
+ * that actually use them.
450
+ * @example (project) => findProjectsWithFile(project, 'tsconfig.json')
451
+ */
452
+ readonly findApplicableProjects?: (project: Project$1) => ReadonlyArray<Project$1>;
440
453
  /** Rules included in this bundle. */
441
454
  readonly rules: ReadonlyArray<AgentRule>;
442
455
  /** Skills included in this bundle (cross-platform where supported). */
@@ -885,6 +898,13 @@ declare class AgentConfig extends Component {
885
898
  preSynthesize(): void;
886
899
  private resolvePlatforms;
887
900
  private resolveRules;
901
+ /**
902
+ * Return a bundle's rules with `filePatterns` narrowed to the projects
903
+ * that actually matched the bundle's detection predicate (when the bundle
904
+ * provides `findApplicableProjects`). Rules with `ALWAYS` scope and rules
905
+ * on bundles that don't implement the hook are returned unchanged.
906
+ */
907
+ private bundleRulesFor;
888
908
  private resolveSkills;
889
909
  private resolveSubAgents;
890
910
  /**
@@ -2610,8 +2630,10 @@ declare class MonorepoProject extends TypeScriptAppProject {
2610
2630
  */
2611
2631
  declare class ProjectMetadata extends Component {
2612
2632
  /**
2613
- * Returns the ProjectMetadata instance for a project, or undefined
2614
- * if the component has not been added.
2633
+ * Returns the ProjectMetadata instance for a project. Walks up the parent
2634
+ * chain so sub-projects resolve the metadata declared on a root
2635
+ * `MonorepoProject` without needing a duplicate declaration of their own.
2636
+ * Returns `undefined` if no ancestor has a `ProjectMetadata` component.
2615
2637
  */
2616
2638
  static of(project: Project): ProjectMetadata | undefined;
2617
2639
  /** Resolved metadata with auto-detected values filled in. */
@@ -2767,8 +2789,10 @@ interface TypeScriptProjectOptions extends Omit<typescript.TypeScriptProjectOpti
2767
2789
  * - `AgentConfigOptions`: enable with explicit configuration
2768
2790
  * - `false` or `undefined`: disabled (default)
2769
2791
  *
2770
- * When the parent is a MonorepoProject with AgentConfig enabled and this
2771
- * option is not explicitly set, the sub-project inherits enablement.
2792
+ * Sub-projects do not inherit `AgentConfig` from their parent
2793
+ * `MonorepoProject`. Agent rule files are rendered only on projects that
2794
+ * explicitly opt in, so a monorepo's root `AgentConfig` does not duplicate
2795
+ * `.cursor/`, `.claude/`, or `CLAUDE.md` into each sub-project's `outdir`.
2772
2796
  *
2773
2797
  * @default false
2774
2798
  */
package/lib/index.js CHANGED
@@ -256,23 +256,50 @@ var MCP_TRANSPORT = {
256
256
  };
257
257
 
258
258
  // src/agent/bundles/utils.ts
259
+ function findProjectsWithComponent(project, ctor) {
260
+ const out = [];
261
+ if (project.components.some((c) => c instanceof ctor)) {
262
+ out.push(project);
263
+ }
264
+ for (const sub of project.subprojects) {
265
+ if (sub.components.some((c) => c instanceof ctor)) {
266
+ out.push(sub);
267
+ }
268
+ }
269
+ return out;
270
+ }
271
+ function findProjectsWithDep(project, name) {
272
+ const out = [];
273
+ if (project.deps.all.some((d) => d.name === name)) {
274
+ out.push(project);
275
+ }
276
+ for (const sub of project.subprojects) {
277
+ if (sub.deps.all.some((d) => d.name === name)) {
278
+ out.push(sub);
279
+ }
280
+ }
281
+ return out;
282
+ }
283
+ function findProjectsWithFile(project, filename) {
284
+ const out = [];
285
+ if (project.tryFindFile(filename) !== void 0) {
286
+ out.push(project);
287
+ }
288
+ for (const sub of project.subprojects) {
289
+ if (sub.tryFindFile(filename) !== void 0) {
290
+ out.push(sub);
291
+ }
292
+ }
293
+ return out;
294
+ }
259
295
  function hasComponent(project, ctor) {
260
- if (project.components.some((c) => c instanceof ctor)) return true;
261
- return project.subprojects.some(
262
- (sub) => sub.components.some((c) => c instanceof ctor)
263
- );
296
+ return findProjectsWithComponent(project, ctor).length > 0;
264
297
  }
265
298
  function hasDep(project, name) {
266
- if (project.deps.all.some((d) => d.name === name)) return true;
267
- return project.subprojects.some(
268
- (sub) => sub.deps.all.some((d) => d.name === name)
269
- );
299
+ return findProjectsWithDep(project, name).length > 0;
270
300
  }
271
301
  function hasFile(project, filename) {
272
- if (project.tryFindFile(filename) !== void 0) return true;
273
- return project.subprojects.some(
274
- (sub) => sub.tryFindFile(filename) !== void 0
275
- );
302
+ return findProjectsWithFile(project, filename).length > 0;
276
303
  }
277
304
 
278
305
  // src/agent/bundles/aws-cdk.ts
@@ -280,6 +307,7 @@ var awsCdkBundle = {
280
307
  name: "aws-cdk",
281
308
  description: "AWS CDK construct patterns, L2/L3 conventions, IAM best practices",
282
309
  appliesWhen: (project) => hasDep(project, "aws-cdk-lib"),
310
+ findApplicableProjects: (project) => findProjectsWithDep(project, "aws-cdk-lib"),
283
311
  rules: [
284
312
  {
285
313
  name: "aws-cdk-conventions",
@@ -911,6 +939,7 @@ var jestBundle = {
911
939
  name: "jest",
912
940
  description: "Jest testing conventions and patterns",
913
941
  appliesWhen: (project) => hasDep(project, "jest"),
942
+ findApplicableProjects: (project) => findProjectsWithDep(project, "jest"),
914
943
  rules: [
915
944
  {
916
945
  name: "jest-testing",
@@ -1697,6 +1726,7 @@ var typescriptBundle = {
1697
1726
  name: "typescript",
1698
1727
  description: "TypeScript conventions, type safety, naming, JSDoc, member ordering",
1699
1728
  appliesWhen: (project) => hasFile(project, "tsconfig.json"),
1729
+ findApplicableProjects: (project) => findProjectsWithFile(project, "tsconfig.json"),
1700
1730
  rules: [
1701
1731
  {
1702
1732
  name: "typescript-conventions",
@@ -1914,6 +1944,7 @@ var vitestBundle = {
1914
1944
  name: "vitest",
1915
1945
  description: "Vitest testing conventions, patterns, and file scoping",
1916
1946
  appliesWhen: (project) => hasComponent(project, Vitest),
1947
+ findApplicableProjects: (project) => findProjectsWithComponent(project, Vitest),
1917
1948
  rules: [
1918
1949
  {
1919
1950
  name: "vitest-testing",
@@ -1983,6 +2014,38 @@ var BUILT_IN_BUNDLES = [
1983
2014
  slackBundle
1984
2015
  ];
1985
2016
 
2017
+ // src/agent/bundles/scope.ts
2018
+ var path = __toESM(require("path"));
2019
+ function scopeFilePatternsToProjects(root, detected) {
2020
+ const patterns = /* @__PURE__ */ new Set();
2021
+ for (const project of detected) {
2022
+ const rel = path.relative(root.outdir, project.outdir);
2023
+ const include = readTsconfigInclude(project);
2024
+ if (include && include.length > 0) {
2025
+ for (const entry of include) {
2026
+ patterns.add(prefix(rel, entry));
2027
+ }
2028
+ } else {
2029
+ const srcdir = project.srcdir ?? "src";
2030
+ patterns.add(prefix(rel, path.posix.join(srcdir, "**/*.ts")));
2031
+ }
2032
+ }
2033
+ return [...patterns].sort();
2034
+ }
2035
+ function readTsconfigInclude(project) {
2036
+ const tsconfig = project.tsconfig;
2037
+ if (!tsconfig) {
2038
+ return void 0;
2039
+ }
2040
+ return tsconfig.include;
2041
+ }
2042
+ function prefix(rel, entry) {
2043
+ if (!rel) {
2044
+ return entry;
2045
+ }
2046
+ return path.posix.join(rel, entry);
2047
+ }
2048
+
1986
2049
  // src/projects/project-metadata.ts
1987
2050
  var import_projen5 = require("projen");
1988
2051
  var import_javascript2 = require("projen/lib/javascript");
@@ -2001,12 +2064,22 @@ function parseGitHubUrl(url) {
2001
2064
  }
2002
2065
  var ProjectMetadata = class _ProjectMetadata extends import_projen5.Component {
2003
2066
  /**
2004
- * Returns the ProjectMetadata instance for a project, or undefined
2005
- * if the component has not been added.
2067
+ * Returns the ProjectMetadata instance for a project. Walks up the parent
2068
+ * chain so sub-projects resolve the metadata declared on a root
2069
+ * `MonorepoProject` without needing a duplicate declaration of their own.
2070
+ * Returns `undefined` if no ancestor has a `ProjectMetadata` component.
2006
2071
  */
2007
2072
  static of(project) {
2008
2073
  const isProjectMetadata = (c) => c instanceof _ProjectMetadata;
2009
- return project.components.find(isProjectMetadata);
2074
+ let current = project;
2075
+ while (current) {
2076
+ const found = current.components.find(isProjectMetadata);
2077
+ if (found) {
2078
+ return found;
2079
+ }
2080
+ current = current.parent;
2081
+ }
2082
+ return void 0;
2010
2083
  }
2011
2084
  constructor(project, options = {}) {
2012
2085
  super(project);
@@ -2555,8 +2628,8 @@ var FALLBACKS = {
2555
2628
  docsPath: "<docs-path>"
2556
2629
  };
2557
2630
  var TEMPLATE_RE = /\{\{(\w+(?:\.\w+)*)\}\}/g;
2558
- function getNestedValue(obj, path) {
2559
- const parts = path.split(".");
2631
+ function getNestedValue(obj, path2) {
2632
+ const parts = path2.split(".");
2560
2633
  let current = obj;
2561
2634
  for (const part of parts) {
2562
2635
  if (current == null || typeof current !== "object") {
@@ -2797,7 +2870,7 @@ var AgentConfig = class _AgentConfig extends import_projen8.Component {
2797
2870
  continue;
2798
2871
  }
2799
2872
  if (bundle.appliesWhen(this.project)) {
2800
- for (const rule of bundle.rules) {
2873
+ for (const rule of this.bundleRulesFor(bundle)) {
2801
2874
  ruleMap.set(rule.name, rule);
2802
2875
  }
2803
2876
  }
@@ -2807,7 +2880,7 @@ var AgentConfig = class _AgentConfig extends import_projen8.Component {
2807
2880
  for (const bundleName of this.options.includeBundles) {
2808
2881
  const bundle = BUILT_IN_BUNDLES.find((b) => b.name === bundleName);
2809
2882
  if (bundle) {
2810
- for (const rule of bundle.rules) {
2883
+ for (const rule of this.bundleRulesFor(bundle)) {
2811
2884
  ruleMap.set(rule.name, rule);
2812
2885
  }
2813
2886
  }
@@ -2847,6 +2920,25 @@ ${extra}`
2847
2920
  return a.name.localeCompare(b.name);
2848
2921
  });
2849
2922
  }
2923
+ /**
2924
+ * Return a bundle's rules with `filePatterns` narrowed to the projects
2925
+ * that actually matched the bundle's detection predicate (when the bundle
2926
+ * provides `findApplicableProjects`). Rules with `ALWAYS` scope and rules
2927
+ * on bundles that don't implement the hook are returned unchanged.
2928
+ */
2929
+ bundleRulesFor(bundle) {
2930
+ if (!bundle.findApplicableProjects) {
2931
+ return bundle.rules;
2932
+ }
2933
+ const detected = bundle.findApplicableProjects(this.project);
2934
+ if (detected.length === 0) {
2935
+ return bundle.rules;
2936
+ }
2937
+ const scoped = scopeFilePatternsToProjects(this.project, detected);
2938
+ return bundle.rules.map(
2939
+ (rule) => rule.scope === AGENT_RULE_SCOPE.FILE_PATTERN ? { ...rule, filePatterns: scoped } : rule
2940
+ );
2941
+ }
2850
2942
  resolveSkills() {
2851
2943
  const skillMap = /* @__PURE__ */ new Map();
2852
2944
  if (this.options.autoDetectBundles !== false) {
@@ -3502,15 +3594,11 @@ var TypeScriptProject = class extends import_projen12.typescript.TypeScriptProje
3502
3594
  turbo.compileTask?.outputs.push("dist/**");
3503
3595
  turbo.compileTask?.outputs.push("lib/**");
3504
3596
  }
3505
- const parentHasAgentConfig = userOptions.parent instanceof MonorepoProject && AgentConfig.of(userOptions.parent) !== void 0;
3506
- if (options.agentConfig !== false) {
3507
- const shouldEnable = typeof options.agentConfig === "object" || options.agentConfig === true || parentHasAgentConfig;
3508
- if (shouldEnable) {
3509
- new AgentConfig(
3510
- this,
3511
- typeof options.agentConfig === "object" ? options.agentConfig : {}
3512
- );
3513
- }
3597
+ if (options.agentConfig === true || typeof options.agentConfig === "object") {
3598
+ new AgentConfig(
3599
+ this,
3600
+ typeof options.agentConfig === "object" ? options.agentConfig : {}
3601
+ );
3514
3602
  }
3515
3603
  if (options.resetTask !== false) {
3516
3604
  const defaultResetTaskOptions = {
@@ -3564,8 +3652,8 @@ var ResetTask = class _ResetTask extends import_projen13.Component {
3564
3652
  const resetTask = this.project.tasks.addTask(this.taskName, {
3565
3653
  description: "Delete build artifacts specified by pathsToRemove option, or artifactsDirectory if pathsToRemove is empty"
3566
3654
  });
3567
- this.pathsToRemove.forEach((path) => {
3568
- resetTask.exec(`[ -e "${path}" ] && rm -rf ${path} || true`);
3655
+ this.pathsToRemove.forEach((path2) => {
3656
+ resetTask.exec(`[ -e "${path2}" ] && rm -rf ${path2} || true`);
3569
3657
  });
3570
3658
  const rootHasTurbo = TurboRepo.of(this.project.root) !== void 0;
3571
3659
  const isSubproject = this.project !== this.project.root;
@@ -3880,7 +3968,11 @@ var MonorepoProject = class extends import_typescript3.TypeScriptAppProject {
3880
3968
  */
3881
3969
  disableTsconfigDev: true,
3882
3970
  /**
3883
- * Kill the srcdir in the root since we aren't using one.
3971
+ * Kill the srcdir in the root since we aren't using one. Projen's
3972
+ * default `tsconfig.include` still points at `src/**\/*.ts`, so
3973
+ * callers that want to treat the monorepo root as a pure shell
3974
+ * should see an empty include — consumers add their own entries via
3975
+ * `tsconfig.addInclude(...)` (e.g. `.projenrc.ts`, `projenrc/**\/*.ts`).
3884
3976
  */
3885
3977
  tsconfig: {
3886
3978
  compilerOptions: {
@@ -3954,6 +4046,7 @@ var MonorepoProject = class extends import_typescript3.TypeScriptAppProject {
3954
4046
  );
3955
4047
  super({ ...options });
3956
4048
  postInstallDependenciesMap.set(this, []);
4049
+ this.tsconfig?.removeInclude(`${this.srcdir}/**/*.ts`);
3957
4050
  this.pnpmVersion = options.pnpmVersion;
3958
4051
  this.configulatorRegistryConsumer = options.configulatorRegistryConsumer ?? true;
3959
4052
  new VSCodeConfig(this);
@@ -4163,9 +4256,9 @@ var AwsDeployWorkflow = class _AwsDeployWorkflow extends import_projen16.Compone
4163
4256
  for (const branch of branches) {
4164
4257
  const branchPattern = branch.branch;
4165
4258
  if (branchPattern.includes("*")) {
4166
- const prefix = branchPattern.replace(/\*.*$/, "");
4167
- conditions.push(`startsWith(github.ref, 'refs/heads/${prefix}')`);
4168
- conditions.push(`startsWith(github.head_ref, '${prefix}')`);
4259
+ const prefix2 = branchPattern.replace(/\*.*$/, "");
4260
+ conditions.push(`startsWith(github.ref, 'refs/heads/${prefix2}')`);
4261
+ conditions.push(`startsWith(github.head_ref, '${prefix2}')`);
4169
4262
  } else {
4170
4263
  conditions.push(`github.ref == 'refs/heads/${branchPattern}'`);
4171
4264
  }