create-next-hydra 0.1.0 → 0.2.1

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/cli.js +0 -0
  4. package/dist/composition/add.d.ts +14 -0
  5. package/dist/composition/add.d.ts.map +1 -0
  6. package/dist/composition/add.js +453 -0
  7. package/dist/composition/catalog.d.ts +26 -0
  8. package/dist/composition/catalog.d.ts.map +1 -0
  9. package/dist/composition/catalog.js +313 -0
  10. package/dist/composition/errors.d.ts +5 -0
  11. package/dist/composition/errors.d.ts.map +1 -0
  12. package/dist/composition/errors.js +8 -0
  13. package/dist/composition/format.d.ts +6 -0
  14. package/dist/composition/format.d.ts.map +1 -0
  15. package/dist/composition/format.js +63 -0
  16. package/dist/composition/install.d.ts +16 -0
  17. package/dist/composition/install.d.ts.map +1 -0
  18. package/dist/composition/install.js +175 -0
  19. package/dist/composition/packages.d.ts +6 -0
  20. package/dist/composition/packages.d.ts.map +1 -0
  21. package/dist/composition/packages.js +54 -0
  22. package/dist/composition/paths.d.ts +4 -0
  23. package/dist/composition/paths.d.ts.map +1 -0
  24. package/dist/composition/paths.js +47 -0
  25. package/dist/composition/planner.d.ts +4 -0
  26. package/dist/composition/planner.d.ts.map +1 -0
  27. package/dist/composition/planner.js +320 -0
  28. package/dist/composition/schema.d.ts +235 -0
  29. package/dist/composition/schema.d.ts.map +1 -0
  30. package/dist/composition/schema.js +115 -0
  31. package/dist/composition/types.d.ts +87 -0
  32. package/dist/composition/types.d.ts.map +1 -0
  33. package/dist/composition/types.js +6 -0
  34. package/dist/composition/use.d.ts +23 -0
  35. package/dist/composition/use.d.ts.map +1 -0
  36. package/dist/composition/use.js +182 -0
  37. package/dist/composition/workspace.d.ts +10 -0
  38. package/dist/composition/workspace.d.ts.map +1 -0
  39. package/dist/composition/workspace.js +208 -0
  40. package/dist/constants.d.ts +4 -1
  41. package/dist/constants.d.ts.map +1 -1
  42. package/dist/constants.js +26 -1
  43. package/dist/fs-utils.js +3 -3
  44. package/dist/git.d.ts.map +1 -1
  45. package/dist/git.js +12 -12
  46. package/dist/index.d.ts +5 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +55 -6
  49. package/dist/instruction-renderer.d.ts +17 -0
  50. package/dist/instruction-renderer.d.ts.map +1 -0
  51. package/dist/instruction-renderer.js +132 -0
  52. package/dist/logger.d.ts +2 -1
  53. package/dist/logger.d.ts.map +1 -1
  54. package/dist/logger.js +7 -3
  55. package/dist/prompts.d.ts +2 -0
  56. package/dist/prompts.d.ts.map +1 -1
  57. package/dist/prompts.js +25 -2
  58. package/dist/sanitize.d.ts +2 -1
  59. package/dist/sanitize.d.ts.map +1 -1
  60. package/dist/sanitize.js +13 -4
  61. package/dist/scaffold.d.ts +5 -1
  62. package/dist/scaffold.d.ts.map +1 -1
  63. package/dist/scaffold.js +247 -71
  64. package/dist/types.d.ts +5 -0
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/version.d.ts +2 -0
  67. package/dist/version.d.ts.map +1 -0
  68. package/dist/version.js +2 -0
  69. package/package.json +35 -23
  70. package/schema/selection-definition.json +237 -0
  71. package/schema/source-registry.json +37 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JAKALA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # create-next-hydra
2
+
3
+ `create-next-hydra` composes a Next Hydra workspace from one Auth Provider, one CMS Provider, one Commerce Provider, and any compatible Add-ons. It uses [ShadCN registry items](https://ui.shadcn.com/docs/registry/registry-item-json) to copy code and adds Next Hydra's stack validation and package-specific manifest changes.
4
+
5
+ ## Create a project
6
+
7
+ Interactive scaffolding asks for every required Provider:
8
+
9
+ ```bash
10
+ pnpm dlx create-next-hydra@latest my-project
11
+ ```
12
+
13
+ For a non-interactive scaffold, provide all three Providers or a Preset:
14
+
15
+ ```bash
16
+ pnpm dlx create-next-hydra@latest my-project --yes \
17
+ --auth workos \
18
+ --cms drupal \
19
+ --commerce commercetools
20
+
21
+ pnpm dlx create-next-hydra@latest my-project --yes --preset standard
22
+ ```
23
+
24
+ Use `--add-on <selection>` more than once to include compatible Add-ons. A selection can be an official shorthand, a local registry-item JSON file, a URL, a public GitHub address such as `owner/repository/item#ref`, or a configured ShadCN registry name.
25
+
26
+ The destination must be missing or empty. If scaffolding fails, the CLI leaves the partial project in place and reports what completed and what was not attempted.
27
+
28
+ ## Change the maintainer workspace
29
+
30
+ The repository root is a Maintainer Workspace because it contains `next-hydra.json` and the source registry. Build the local CLI, then change a Provider or apply a Preset:
31
+
32
+ ```bash
33
+ pnpm --filter create-next-hydra build
34
+ node packages/create-next-hydra/dist/cli.js use --cms contentstack
35
+ node packages/create-next-hydra/dist/cli.js use --preset standard
36
+ node packages/create-next-hydra/dist/cli.js use --check
37
+ ```
38
+
39
+ `use` updates `next-hydra.json`, governed package entries, Provider-owned application files such as Next.js routes, and the lockfile. It does not remove canonical Provider or Add-on implementation source from the maintainer repository. If an operation fails, it leaves the Git diff in place for inspection or repair.
40
+
41
+ ## Add code to a customer workspace
42
+
43
+ Run `add` from an existing customer-owned workspace:
44
+
45
+ ```bash
46
+ pnpm dlx create-next-hydra@latest add owner/repository/drupal-dam
47
+ ```
48
+
49
+ The command walks the complete registry dependency graph before ShadCN flattens it, rejects duplicate file targets, and lists every prospective file, package entry, and other ShadCN-managed effect. It verifies known Provider requirements through exact aliases in `apps/web/package.json` and labels compatibility requirements that cannot be proven without a customer receipt. The inspected graph is prepared locally so the exact approved artifacts are also the installation input. To keep that preview truthful, customer `add` accepts only explicitly targeted, exact-copy `registry:file` and `registry:item` file entries; file types that ShadCN would transform are rejected. It creates missing files, skips files ShadCN considers identical after newline and surrounding-whitespace normalization, and asks before replacing a changed file or dependency. Like ShadCN, `--yes` skips confirmation prompts and `--overwrite` authorizes replacing changed targets. Combine them for a fully non-interactive installation after reviewing the disclosed plan. `--yes` alone still refuses changed customer code. When package-specific entries change, `add` runs the root `pnpm install` to settle the lockfile and workspace links. The command never deletes files and does not retain a receipt.
50
+
51
+ Customer `add` accepts ordinary registry items and Next Hydra Add-ons. It does not switch Providers or apply Presets. In v1, an Add-on that declares separate binary assets or pnpm patches must be selected in a Maintainer Workspace or during a new scaffold.
52
+
53
+ ## Author a Provider or Add-on
54
+
55
+ Keep source in its normal package or application directory and place a `registry.json` beside it. Add that registry file to the root `registry.json` `include` list. A registry containing Selection Definitions uses [`source-registry.json`](./schema/source-registry.json), which applies the complete [`selection-definition.json`](./schema/selection-definition.json) registry-item schema whenever an item contains `meta.nextHydra`. Ordinary registry items continue to use ShadCN's schema.
56
+
57
+ A Selection Definition declares:
58
+
59
+ - its stable ID and whether it is a Provider, Add-on, or Preset;
60
+ - a Provider Slot when it is a Provider;
61
+ - standard ShadCN `registryDependencies` for any other registry items it needs;
62
+ - required and conflicting selection IDs;
63
+ - exact package entries and aliases;
64
+ - exact pnpm patch entries and their patch-file assets; and
65
+ - setup instructions through the registry item's standard `docs` field.
66
+
67
+ Every copied registry file must have an explicit workspace-root target such as `~/packages/cms-drupal/src/index.ts`. The source `path` remains relative to the colocated registry, so maintainers edit and test canonical Provider code in its normal package or application.
68
+
69
+ When a Provider or Add-on needs to place a file outside its own source directory, keep that source under a colocated `registry/` directory using its final workspace path. For example, `packages/cms-drupal/registry/apps/web/app/api/draft/route.ts` is installed as `~/apps/web/app/api/draft/route.ts`. These files are ordinary ShadCN registry files: ShadCN installs them during scaffolding and customer `add`, while maintainer `use` may remove or replace their known targets when the selected stack changes.
70
+
71
+ Run these commands after adding, moving, or removing contribution files:
72
+
73
+ ```bash
74
+ pnpm registry:sync
75
+ pnpm registry:check
76
+ ```
77
+
78
+ `registry:sync` regenerates only each registry item's `files` list and its final workspace-root targets. Provider-owned metadata stays in the colocated registry file. Next Hydra first walks the intact `registryDependencies` graph to retain metadata and detect target conflicts, then prepares those exact artifacts and asks ShadCN to install them once from the workspace root.
79
+
80
+ Standard ShadCN `dependencies` and `devDependencies` apply to the workspace root. Use `meta.nextHydra.packages` only when a dependency must be added to a specific workspace package.
81
+
82
+ ShadCN reads registry source files as text. For a binary file that must survive byte-for-byte, or a root patch file referenced by `pnpmPatches`, an official or locally included source-registry selection may declare a typed `assets` source and target instead. Separately fetched external selections cannot contribute assets in v1.
83
+
84
+ ShadCN registry mappings, including private registry URLs and environment-backed headers, are read from the invoking workspace's `components.json` or `package.json`, used for both graph preflight and installation, and are not copied into a new scaffold. Secrets are never requested or generated by Next Hydra; registry `envVars` remain suitable only for safe placeholders or development defaults.
85
+
86
+ ## Current limits
87
+
88
+ - Auth, CMS, and Commerce each require exactly one Provider.
89
+ - A scaffolded Customer Workspace has no composition receipt and cannot be automatically upgraded or switched later.
90
+ - Composition installs local code and JavaScript dependencies. Remote service setup, real secrets, Composer changes, Drupal module enablement, and deployed extensions remain manual.
91
+ - Selection Definitions are declarative and cannot run arbitrary hooks.
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,14 @@
1
+ import { confirm } from "@clack/prompts";
2
+ export type AddOptions = {
3
+ cwd?: string;
4
+ overwrite?: boolean;
5
+ yes?: boolean;
6
+ };
7
+ type ConfirmPrompt = (options: Parameters<typeof confirm>[0]) => ReturnType<typeof confirm>;
8
+ export type AddDependencies = {
9
+ confirm?: ConfirmPrompt;
10
+ install?: (cwd: string) => Promise<void>;
11
+ };
12
+ export declare function addRegistryItem(reference: string, options?: AddOptions, dependencies?: AddDependencies): Promise<void>;
13
+ export {};
14
+ //# sourceMappingURL=add.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/composition/add.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAY,MAAM,gBAAgB,CAAC;AA4BnD,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEF,KAAK,aAAa,GAAG,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,KACnC,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAEhC,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAufF,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,UAAe,EACxB,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,IAAI,CAAC,CAuKf"}
@@ -0,0 +1,453 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { confirm, isCancel } from "@clack/prompts";
4
+ import { getRegistriesConfig, resolveRegistryItems } from "shadcn/registry";
5
+ import { pathExists } from "../fs-utils.js";
6
+ import { runCommand } from "../git.js";
7
+ import { info, success } from "../logger.js";
8
+ import { fetchRegistryItemGraph } from "./catalog.js";
9
+ import { CompositionValidationError } from "./errors.js";
10
+ import { addRegistryItemsQuietly, withPreparedRegistryArtifacts, } from "./install.js";
11
+ import { mergePackageRequirements, parsePackageJson, readPackageJson, } from "./packages.js";
12
+ import { resolveRegistryTarget } from "./paths.js";
13
+ import { formatZodError, NEXT_HYDRA_SELECTION_SCHEMA_URL, selectionDefinitionSchema, } from "./schema.js";
14
+ import { PROVIDER_ALIASES } from "./types.js";
15
+ import { applyPackageEntries } from "./workspace.js";
16
+ const PACKAGE_ALIAS_PROTOCOL = /^(?:workspace|npm):/;
17
+ const EXACT_COPY_FILE_TYPES = new Set(["registry:file", "registry:item"]);
18
+ const KNOWN_PROVIDERS = new Map([
19
+ [
20
+ "next-hydra/auth/workos",
21
+ { dependency: "@repo/auth", packageName: "@repo/auth-workos" },
22
+ ],
23
+ [
24
+ "next-hydra/cms/contentstack",
25
+ { dependency: "@repo/cms", packageName: "@repo/cms-contentstack" },
26
+ ],
27
+ [
28
+ "next-hydra/cms/drupal",
29
+ { dependency: "@repo/cms", packageName: "@repo/cms-drupal" },
30
+ ],
31
+ [
32
+ "next-hydra/commerce/commercetools",
33
+ {
34
+ dependency: "@repo/commerce-provider",
35
+ packageName: "@repo/commerce-commercetools",
36
+ },
37
+ ],
38
+ ]);
39
+ function parseDependency(request) {
40
+ const packageEnd = request.startsWith("@")
41
+ ? request.indexOf("@", request.indexOf("/") + 1)
42
+ : request.indexOf("@");
43
+ if (packageEnd < 1) {
44
+ return { name: request };
45
+ }
46
+ return {
47
+ name: request.slice(0, packageEnd),
48
+ specifier: request.slice(packageEnd + 1),
49
+ };
50
+ }
51
+ function packageStatus(actual, expected) {
52
+ if (actual === undefined) {
53
+ return "create";
54
+ }
55
+ return actual === expected ? "identical" : "changed";
56
+ }
57
+ function dependencyStatus(actual, requested) {
58
+ if (requested.specifier) {
59
+ return packageStatus(actual, requested.specifier);
60
+ }
61
+ return actual === undefined ? "create" : "identical";
62
+ }
63
+ function parseSelection(item) {
64
+ if (item.meta?.nextHydra === undefined) {
65
+ return;
66
+ }
67
+ if (item.$schema !== NEXT_HYDRA_SELECTION_SCHEMA_URL) {
68
+ throw new CompositionValidationError(`${item.name} does not use the Next Hydra Selection Definition schema.`, [`$schema must be ${NEXT_HYDRA_SELECTION_SCHEMA_URL}`]);
69
+ }
70
+ const result = selectionDefinitionSchema.safeParse(item.meta.nextHydra);
71
+ if (!result.success) {
72
+ throw new CompositionValidationError(`${item.name} contains invalid Next Hydra metadata. Run the latest create-next-hydra and try again.`, formatZodError(result.error));
73
+ }
74
+ return result.data;
75
+ }
76
+ async function changeStatus(absoluteTarget, expected) {
77
+ if (!(await pathExists(absoluteTarget))) {
78
+ return "create";
79
+ }
80
+ const normalize = (content) => content.replaceAll("\r\n", "\n").trim();
81
+ return normalize(await readFile(absoluteTarget, "utf-8")) ===
82
+ normalize(expected)
83
+ ? "identical"
84
+ : "changed";
85
+ }
86
+ function requireExactCopyFile(type, owner) {
87
+ if (!EXACT_COPY_FILE_TYPES.has(type)) {
88
+ throw new CompositionValidationError("Customer add supports only exact-copy registry files in v1.", [
89
+ `${owner} uses ${type}; use registry:file or registry:item with an explicit target so the reviewed content is exactly what ShadCN writes`,
90
+ ]);
91
+ }
92
+ }
93
+ async function inspectFiles(workspaceRoot, tree) {
94
+ const registryFiles = await Promise.all((tree.files ?? []).map(async (file) => {
95
+ requireExactCopyFile(file.type, file.path);
96
+ if (!(file.target && file.content !== undefined)) {
97
+ throw new CompositionValidationError("Next Hydra Add-ons require explicit workspace-root file targets.", [`${file.path} must include content and a target beginning with ~/`]);
98
+ }
99
+ const target = resolveRegistryTarget(file.target);
100
+ return {
101
+ kind: "registry file",
102
+ status: await changeStatus(path.join(workspaceRoot, target), file.content),
103
+ target,
104
+ };
105
+ }));
106
+ const claimed = new Map();
107
+ for (const change of registryFiles) {
108
+ const existing = claimed.get(change.target);
109
+ if (existing) {
110
+ throw new CompositionValidationError("The requested Add-on targets the same customer file more than once.", [`${change.target} is both a ${existing} and a ${change.kind}`]);
111
+ }
112
+ claimed.set(change.target, change.kind);
113
+ }
114
+ return registryFiles.sort((left, right) => left.target.localeCompare(right.target));
115
+ }
116
+ async function inspectPackages(workspaceRoot, requirements, tree) {
117
+ const prospectiveManifests = new Map();
118
+ for (const file of tree.files ?? []) {
119
+ if (!(file.target && file.content !== undefined)) {
120
+ continue;
121
+ }
122
+ const target = resolveRegistryTarget(file.target);
123
+ if (target.endsWith("/package.json") || target === "package.json") {
124
+ prospectiveManifests.set(target, file.content);
125
+ }
126
+ }
127
+ return await Promise.all(requirements.map(async (requirement) => {
128
+ const relativeManifest = path.posix.join(requirement.cwd, "package.json");
129
+ const prospective = prospectiveManifests.get(relativeManifest);
130
+ const manifest = prospective === undefined
131
+ ? await readPackageJson(path.join(workspaceRoot, relativeManifest), relativeManifest)
132
+ : parsePackageJson(prospective, relativeManifest);
133
+ const actual = manifest[requirement.section]?.[requirement.name];
134
+ return {
135
+ ...requirement,
136
+ actual,
137
+ status: packageStatus(actual, requirement.specifier),
138
+ };
139
+ }));
140
+ }
141
+ async function inspectRootDependencies(workspaceRoot, tree) {
142
+ const manifest = await readPackageJson(path.join(workspaceRoot, "package.json"), "package.json");
143
+ const requests = [
144
+ ...(tree.dependencies ?? []).map((request) => ({
145
+ ...parseDependency(request),
146
+ section: "dependencies",
147
+ })),
148
+ ...(tree.devDependencies ?? []).map((request) => ({
149
+ ...parseDependency(request),
150
+ section: "devDependencies",
151
+ })),
152
+ ];
153
+ return requests.map((request) => {
154
+ const actual = manifest[request.section]?.[request.name];
155
+ const specifier = request.specifier ?? actual ?? "registry default";
156
+ return {
157
+ actual,
158
+ cwd: ".",
159
+ name: request.name,
160
+ section: request.section,
161
+ specifier,
162
+ status: dependencyStatus(actual, request),
163
+ };
164
+ });
165
+ }
166
+ function usesProviderAlias(actual, packageName) {
167
+ if (!actual) {
168
+ return false;
169
+ }
170
+ return (actual === packageName ||
171
+ actual.startsWith(`workspace:${packageName}@`) ||
172
+ actual.startsWith(`npm:${packageName}@`));
173
+ }
174
+ function aliasedPackageName(specifier) {
175
+ const request = specifier.replace(PACKAGE_ALIAS_PROTOCOL, "");
176
+ const packageEnd = request.startsWith("@")
177
+ ? request.indexOf("@", request.indexOf("/") + 1)
178
+ : request.indexOf("@");
179
+ return packageEnd < 1 ? request || undefined : request.slice(0, packageEnd);
180
+ }
181
+ function providerExpectation(selectionId, selectionsById) {
182
+ const known = KNOWN_PROVIDERS.get(selectionId);
183
+ if (known) {
184
+ return known;
185
+ }
186
+ const selection = selectionsById.get(selectionId);
187
+ if (selection?.kind !== "provider" || !selection.slot) {
188
+ return;
189
+ }
190
+ const dependency = PROVIDER_ALIASES[selection.slot];
191
+ const requirement = selection.packages.find((candidate) => candidate.cwd === "apps/web" &&
192
+ candidate.section === "dependencies" &&
193
+ candidate.name === dependency);
194
+ const packageName = requirement
195
+ ? aliasedPackageName(requirement.specifier)
196
+ : undefined;
197
+ return packageName ? { dependency, packageName } : undefined;
198
+ }
199
+ async function validateKnownProviderCompatibility(workspaceRoot, selections) {
200
+ if (selections.length === 0) {
201
+ return [];
202
+ }
203
+ const manifest = await readPackageJson(path.join(workspaceRoot, "apps/web/package.json"), "apps/web/package.json");
204
+ const issues = [];
205
+ const assumptions = [];
206
+ const selectionsById = new Map(selections.map((item) => [item.id, item]));
207
+ for (const selection of selections) {
208
+ if (selection.kind !== "provider") {
209
+ continue;
210
+ }
211
+ const provider = providerExpectation(selection.id, selectionsById);
212
+ if (!provider) {
213
+ issues.push(`${selection.id} is a Provider in the requested graph but does not declare its apps/web stable alias`);
214
+ continue;
215
+ }
216
+ const actual = manifest.dependencies?.[provider.dependency];
217
+ if (!usesProviderAlias(actual, provider.packageName)) {
218
+ issues.push(`${selection.id} is a Provider in the requested graph, but the current provider alias ${provider.dependency} is ${actual ?? "missing"}`);
219
+ }
220
+ }
221
+ for (const selection of selections) {
222
+ for (const required of selection.compatibility.requires) {
223
+ const provider = providerExpectation(required, selectionsById);
224
+ const requiredSelection = selectionsById.get(required);
225
+ if (provider &&
226
+ !usesProviderAlias(manifest.dependencies?.[provider.dependency], provider.packageName)) {
227
+ issues.push(`${selection.id} requires ${required}`);
228
+ }
229
+ else if (!(provider || requiredSelection?.kind === "add-on")) {
230
+ assumptions.push(`${selection.id} requires ${required}; this customer workspace has no authoritative selection record, so Next Hydra cannot verify it`);
231
+ }
232
+ }
233
+ for (const conflict of selection.compatibility.conflicts) {
234
+ const provider = providerExpectation(conflict, selectionsById);
235
+ const conflictingSelection = selectionsById.get(conflict);
236
+ if ((provider &&
237
+ usesProviderAlias(manifest.dependencies?.[provider.dependency], provider.packageName)) ||
238
+ conflictingSelection?.kind === "add-on") {
239
+ issues.push(`${selection.id} conflicts with ${conflict}`);
240
+ }
241
+ else if (!provider) {
242
+ assumptions.push(`${selection.id} conflicts with ${conflict}; this customer workspace has no authoritative selection record, so Next Hydra cannot verify its absence`);
243
+ }
244
+ }
245
+ }
246
+ if (issues.length > 0) {
247
+ throw new CompositionValidationError("The Add-on is incompatible with the current provider aliases.", issues);
248
+ }
249
+ return [...new Set(assumptions)].sort((left, right) => left.localeCompare(right));
250
+ }
251
+ function validateRegistryTargetClaims(items) {
252
+ const claims = new Map();
253
+ const issues = [];
254
+ for (const item of items) {
255
+ for (const file of item.files ?? []) {
256
+ requireExactCopyFile(file.type, `${item.name}:${file.path}`);
257
+ if (!(file.target && file.content !== undefined)) {
258
+ throw new CompositionValidationError("Next Hydra Add-ons require explicit workspace-root file targets.", [
259
+ `${item.name}:${file.path} must include content and a target beginning with ~/`,
260
+ ]);
261
+ }
262
+ const target = resolveRegistryTarget(file.target);
263
+ const owner = `${item.name}:${file.path}`;
264
+ const existing = claims.get(target);
265
+ if (existing && existing !== owner) {
266
+ issues.push(`${target} is claimed by both ${existing} and ${owner}`);
267
+ }
268
+ else {
269
+ claims.set(target, owner);
270
+ }
271
+ }
272
+ }
273
+ if (issues.length > 0) {
274
+ throw new CompositionValidationError("The registry dependency graph targets the same customer file more than once.", issues);
275
+ }
276
+ }
277
+ function describeShadcnEffects(tree, items) {
278
+ const hasValues = (value) => {
279
+ if (Array.isArray(value)) {
280
+ return value.some(hasValues);
281
+ }
282
+ if (value !== null && typeof value === "object") {
283
+ return Object.values(value).some(hasValues);
284
+ }
285
+ return (value !== undefined && value !== null && value !== false && value !== "");
286
+ };
287
+ const effects = [];
288
+ const envKeys = Object.keys(tree.envVars ?? {}).sort();
289
+ if (envKeys.length > 0) {
290
+ effects.push(`environment placeholders may be merged by ShadCN: ${envKeys.join(", ")}`);
291
+ }
292
+ if (hasValues(tree.tailwind)) {
293
+ effects.push("Tailwind configuration will be merged by ShadCN");
294
+ }
295
+ if (hasValues(tree.cssVars)) {
296
+ effects.push("CSS variables will be merged by ShadCN");
297
+ }
298
+ if (hasValues(tree.css)) {
299
+ effects.push("CSS declarations will be merged by ShadCN");
300
+ }
301
+ if ((tree.fonts?.length ?? 0) > 0) {
302
+ effects.push(`${tree.fonts?.length ?? 0} font definition(s) will be applied by ShadCN`);
303
+ }
304
+ if ([...items].some((item) => "config" in item && item.config !== undefined)) {
305
+ effects.push("ShadCN project configuration will be updated");
306
+ }
307
+ return [...new Set(effects)];
308
+ }
309
+ async function confirmChanges(fileChanges, packageChanges, options, prompt) {
310
+ const overwrite = options.overwrite ?? false;
311
+ const yes = options.yes === true;
312
+ const conflicts = [
313
+ ...fileChanges
314
+ .filter((change) => change.status === "changed")
315
+ .map((change) => ({ kind: change.kind, label: change.target })),
316
+ ...packageChanges
317
+ .filter((change) => change.status === "changed")
318
+ .map((change) => ({
319
+ kind: "dependency",
320
+ label: `${change.cwd}/package.json ${change.name}: ${change.actual} -> ${change.specifier}`,
321
+ })),
322
+ ];
323
+ if (yes && !overwrite && conflicts.length > 0) {
324
+ throw new CompositionValidationError("Additive installation found customer-owned conflicts.", conflicts.map((conflict) => `${conflict.label} requires --overwrite; --yes only skips confirmation prompts for non-conflicting changes`));
325
+ }
326
+ if (!overwrite) {
327
+ for (const conflict of conflicts) {
328
+ // Without a global overwrite authorization, each customer-owned change
329
+ // receives its own explicit confirmation.
330
+ // oxlint-disable-next-line no-await-in-loop -- Sequential confirmation is the safety contract.
331
+ const approved = await prompt({
332
+ initialValue: false,
333
+ message: `Replace changed ${conflict.kind} ${conflict.label}?`,
334
+ });
335
+ if (isCancel(approved) || !approved) {
336
+ throw new Error("Installation cancelled. No files or package entries were changed.");
337
+ }
338
+ }
339
+ }
340
+ if (!yes) {
341
+ const approved = await prompt({
342
+ initialValue: true,
343
+ message: "Apply the listed Add-on changes?",
344
+ });
345
+ if (isCancel(approved) || !approved) {
346
+ throw new Error("Installation cancelled. No changes were made.");
347
+ }
348
+ }
349
+ }
350
+ export async function addRegistryItem(reference, options = {}, dependencies = {}) {
351
+ const cwd = path.resolve(options.cwd ?? process.cwd());
352
+ const localReference = path.resolve(cwd, reference);
353
+ const resolvedReference = !path.isAbsolute(reference) && (await pathExists(localReference))
354
+ ? localReference
355
+ : reference;
356
+ const config = await getRegistriesConfig(cwd);
357
+ const graph = await fetchRegistryItemGraph({
358
+ config,
359
+ cwd,
360
+ references: [resolvedReference],
361
+ });
362
+ const primaryName = graph.itemByReference.get(resolvedReference);
363
+ const primary = primaryName
364
+ ? graph.items.get(primaryName)
365
+ : undefined;
366
+ if (!primary) {
367
+ throw new Error(`The registry returned no item for ${reference}.`);
368
+ }
369
+ const selection = parseSelection(primary);
370
+ const graphSelections = [...graph.items.values()]
371
+ .map(parseSelection)
372
+ .filter((value) => Boolean(value));
373
+ if (selection) {
374
+ if (selection.kind === "preset") {
375
+ throw new Error("Presets compose a new workspace. Use `create-next-hydra <directory> --preset ...` instead of `add`.");
376
+ }
377
+ if (selection.kind === "provider") {
378
+ throw new Error("Providers must be selected while scaffolding or with `create-next-hydra use`; `add` does not switch a customer workspace provider.");
379
+ }
380
+ }
381
+ const nestedPresets = graphSelections.filter((candidate) => candidate.kind === "preset");
382
+ if (nestedPresets.length > 0) {
383
+ throw new CompositionValidationError("Presets cannot be installed into a Customer Workspace.", nestedPresets.map((candidate) => `${candidate.id} appears in the requested registry dependency graph`));
384
+ }
385
+ const assetSelections = graphSelections.filter((candidate) => candidate.assets.length > 0);
386
+ if (assetSelections.length > 0) {
387
+ throw new CompositionValidationError("Customer Add-ons cannot install separate binary assets in v1.", assetSelections.map((candidate) => `${candidate.id} must put text files in its registry item; binary asset transport requires a future extension`));
388
+ }
389
+ const patchSelections = graphSelections.filter((candidate) => candidate.pnpmPatches.length > 0);
390
+ if (patchSelections.length > 0) {
391
+ throw new CompositionValidationError("Customer Add-ons cannot change pnpm patches in v1.", patchSelections.map((candidate) => `${candidate.id} must avoid pnpmPatches or be selected during a new scaffold`));
392
+ }
393
+ const compatibilityAssumptions = await validateKnownProviderCompatibility(cwd, graphSelections);
394
+ validateRegistryTargetClaims(graph.items.values());
395
+ await withPreparedRegistryArtifacts({
396
+ artifacts: [...graph.items.values()],
397
+ entryItems: [primary.name],
398
+ itemByReference: graph.itemByReference,
399
+ run: async (entries) => {
400
+ const tree = await resolveRegistryItems(entries, { config });
401
+ if (!tree) {
402
+ throw new Error(`The registry dependency graph for ${reference} is empty.`);
403
+ }
404
+ const fileChanges = await inspectFiles(cwd, tree);
405
+ const packageChanges = await inspectPackages(cwd, mergePackageRequirements(graphSelections), tree);
406
+ const rootDependencyChanges = await inspectRootDependencies(cwd, tree);
407
+ const disclosedPackageChanges = [
408
+ ...packageChanges,
409
+ ...rootDependencyChanges,
410
+ ];
411
+ const shadcnEffects = describeShadcnEffects(tree, graph.items.values());
412
+ info([
413
+ `Registry item: ${primary.name}`,
414
+ "Files:",
415
+ ...fileChanges.map((change) => ` ${change.status}: ${change.target} (${change.kind})`),
416
+ "Package entries:",
417
+ ...disclosedPackageChanges.map((change) => ` ${change.status}: ${change.cwd} ${change.name} = ${change.specifier}`),
418
+ ...(compatibilityAssumptions.length > 0
419
+ ? [
420
+ "Compatibility assumptions:",
421
+ ...compatibilityAssumptions.map((assumption) => ` ${assumption}`),
422
+ ]
423
+ : []),
424
+ ...(shadcnEffects.length > 0
425
+ ? [
426
+ "Additional ShadCN effects:",
427
+ ...shadcnEffects.map((effect) => ` ${effect}`),
428
+ ]
429
+ : []),
430
+ ].join("\n"));
431
+ await confirmChanges(fileChanges, disclosedPackageChanges, options, dependencies.confirm ?? confirm);
432
+ await addRegistryItemsQuietly(entries, {
433
+ config,
434
+ cwd,
435
+ overwrite: options.overwrite === true ||
436
+ fileChanges.some((change) => change.kind === "registry file" && change.status === "changed"),
437
+ });
438
+ const changedPackageEntries = packageChanges
439
+ .filter((change) => change.status !== "identical")
440
+ .map(({ status: _status, actual: _actual, ...requirement }) => requirement);
441
+ await applyPackageEntries(cwd, changedPackageEntries);
442
+ if (changedPackageEntries.length > 0) {
443
+ if (dependencies.install) {
444
+ await dependencies.install(cwd);
445
+ }
446
+ else {
447
+ await runCommand("pnpm", ["install"], { cwd });
448
+ }
449
+ }
450
+ },
451
+ });
452
+ success(`Added ${primary.name}.`);
453
+ }
@@ -0,0 +1,26 @@
1
+ import { getRegistryItems } from "shadcn/registry";
2
+ import type { RegistryItem } from "shadcn/schema";
3
+ import type { CatalogSelection, RegistriesConfig, SourceRegistryCatalog } from "./types.js";
4
+ export declare function resolveRegistryItemGraph(catalog: SourceRegistryCatalog, entryItems: string[]): string[];
5
+ export type RegistryItemGraph = {
6
+ fetchedItemNames: Set<string>;
7
+ itemByReference: Map<string, string>;
8
+ items: Map<string, RegistryItem>;
9
+ };
10
+ export declare function fetchRegistryItemGraph(options: {
11
+ config: RegistriesConfig;
12
+ cwd: string;
13
+ fetchItems?: typeof getRegistryItems;
14
+ itemByReference?: Map<string, string>;
15
+ items?: Iterable<RegistryItem>;
16
+ references: Iterable<string>;
17
+ repository?: string;
18
+ }): Promise<RegistryItemGraph>;
19
+ export declare function loadSourceRegistryCatalog(cwd: string, registryFile?: string): Promise<SourceRegistryCatalog>;
20
+ export declare function loadGitHubSourceRegistryCatalog(repository: string, ref?: string, dependencies?: {
21
+ fetchItems?: (addresses: string[]) => RegistryItem[] | Promise<RegistryItem[]>;
22
+ loadRegistryConfig?: (cwd: string) => RegistriesConfig | Promise<RegistriesConfig>;
23
+ }): Promise<SourceRegistryCatalog>;
24
+ export declare function addCatalogReferences(catalog: SourceRegistryCatalog, references: Iterable<string>, cwd?: string): Promise<SourceRegistryCatalog>;
25
+ export declare function resolveCatalogSelection(catalog: SourceRegistryCatalog, reference: string): CatalogSelection;
26
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/composition/catalog.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,gBAAgB,EAEjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAclD,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AA2KpB,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,MAAM,EAAE,GACnB,MAAM,EAAE,CA+BV;AAaD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAClC,CAAC;AAEF,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,MAAM,EAAE,gBAAgB,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACrC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC/B,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0F7B;AAED,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,MAAM,EACX,YAAY,SAAkB,GAC7B,OAAO,CAAC,qBAAqB,CAAC,CAqChC;AAED,wBAAsB,+BAA+B,CACnD,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,MAAM,EACZ,YAAY,GAAE;IACZ,UAAU,CAAC,EAAE,CACX,SAAS,EAAE,MAAM,EAAE,KAChB,YAAY,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC9C,kBAAkB,CAAC,EAAE,CACnB,GAAG,EAAE,MAAM,KACR,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC9C,GACL,OAAO,CAAC,qBAAqB,CAAC,CA4BhC;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC5B,GAAG,SAA+B,GACjC,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,qBAAqB,EAC9B,SAAS,EAAE,MAAM,GAChB,gBAAgB,CASlB"}