(opts: RunAddArgs)
| 150 | } |
| 151 | |
| 152 | export async function runAdd(opts: RunAddArgs): Promise<RunAddResult> { |
| 153 | const projectDir = resolve(opts.projectDir); |
| 154 | |
| 155 | // 1. Load (or write default) project config. |
| 156 | let config = loadProjectConfig(projectDir); |
| 157 | const hasConfig = existsSync(projectConfigPath(projectDir)); |
| 158 | if (!hasConfig && existsSync(resolve(projectDir, "index.html"))) { |
| 159 | writeProjectConfig(projectDir, DEFAULT_PROJECT_CONFIG); |
| 160 | config = DEFAULT_PROJECT_CONFIG; |
| 161 | } |
| 162 | |
| 163 | // 2. Resolve the requested item and its transitive registryDependencies. |
| 164 | // The list comes back topologically sorted: dependencies first, the |
| 165 | // requested item last. |
| 166 | let resolved: RegistryItem[]; |
| 167 | try { |
| 168 | resolved = await resolveItemWithDependencies(opts.name, { baseUrl: config.registry }); |
| 169 | } catch (err) { |
| 170 | throw new AddError(err instanceof Error ? err.message : String(err), "unknown-item"); |
| 171 | } |
| 172 | // `resolveItemWithDependencies` always pushes the requested item last (or throws), |
| 173 | // so the final element is the item the user asked for. |
| 174 | const item = resolved[resolved.length - 1]!; |
| 175 | |
| 176 | if (item.type === "hyperframes:example") { |
| 177 | throw new AddError( |
| 178 | `"${item.name}" is an example — use \`hyperframes init <dir> --example ${item.name}\` instead.`, |
| 179 | "example-type", |
| 180 | ); |
| 181 | } |
| 182 | |
| 183 | // 3. Compatibility-gate every item we're about to install (dependencies |
| 184 | // included) before writing anything. |
| 185 | const warnings = assertCompatibleOrThrow(resolved, opts.cliVersion); |
| 186 | |
| 187 | // 4. Remap targets per project config — each item by its own type. |
| 188 | const installPlan: RegistryItem[] = resolved.map((resolvedItem) => ({ |
| 189 | ...resolvedItem, |
| 190 | files: resolvedItem.files.map((f) => ({ |
| 191 | ...f, |
| 192 | target: remapTarget(resolvedItem, f.target, config.paths), |
| 193 | })), |
| 194 | })); |
| 195 | |
| 196 | // 5. Install — dependencies first, requested item last. |
| 197 | const written = await installAll(installPlan, projectDir, config.registry); |
| 198 | |
| 199 | // 6. Build include snippet + clipboard copy for the requested item. |
| 200 | const itemForInstall = installPlan[installPlan.length - 1]!; |
| 201 | const primaryFile = |
| 202 | itemForInstall.files.find((f) => f.type === "hyperframes:snippet") ?? |
| 203 | itemForInstall.files.find((f) => f.type === "hyperframes:composition") ?? |
| 204 | itemForInstall.files[0]; |
| 205 | const snippetTargetRel = primaryFile?.target ?? ""; |
| 206 | const snippet = buildSnippet(item, snippetTargetRel); |
| 207 | const clipboardCopied = !opts.skipClipboard && snippet ? copyToClipboard(snippet) : false; |
| 208 | |
| 209 | return { |
no test coverage detected