@impulselab/cli 0.1.3 → 0.1.4

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 (2) hide show
  1. package/dist/index.js +110 -28
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -211,7 +211,11 @@ import { z as z5 } from "zod";
211
211
  import { z as z2 } from "zod";
212
212
  var ModuleFileSchema = z2.object({
213
213
  src: z2.string(),
214
- dest: z2.string()
214
+ dest: z2.string(),
215
+ /** If set, this file is only installed when the named module is already installed. */
216
+ when: z2.object({
217
+ moduleInstalled: z2.string()
218
+ }).optional()
215
219
  });
216
220
 
217
221
  // src/schemas/module-dependency.ts
@@ -243,11 +247,21 @@ var ModuleManifestSchema = z5.object({
243
247
  subModules: z5.array(z5.string()).default([]),
244
248
  dependencies: z5.array(ModuleDependencySchema).default([]),
245
249
  moduleDependencies: z5.array(z5.string()).default([]),
250
+ /** Optional module dependencies that enhance this module but are not required. */
251
+ optionalModuleDependencies: z5.array(z5.string()).default([]),
246
252
  files: z5.array(ModuleFileSchema).default([]),
247
253
  transforms: z5.array(ModuleTransformSchema).default([]),
248
254
  /** Documentation metadata listing env vars this module requires (displayed in install summary). */
249
255
  envVars: z5.array(z5.string()).default([]),
250
- postInstall: z5.array(z5.string()).optional()
256
+ postInstall: z5.array(z5.string()).optional(),
257
+ /** Logical category for grouping in `impulse list`. */
258
+ category: z5.enum(["core", "feature", "integration", "dx"]).optional(),
259
+ /** Module names that cannot be installed alongside this module. */
260
+ incompatibleWith: z5.array(z5.string()).default([]),
261
+ /** Capability tokens this module provides (for future dependency resolution). */
262
+ provides: z5.array(z5.string()).default([]),
263
+ /** Which package in a monorepo this module targets. */
264
+ targetPackage: z5.enum(["database", "server", "web", "root"]).default("root")
251
265
  });
252
266
 
253
267
  // src/registry/github-urls.ts
@@ -364,7 +378,8 @@ async function listAvailableModules(localPath) {
364
378
  result.push({
365
379
  name: parsed.data.name,
366
380
  description: parsed.data.description,
367
- ...subModules ? { subModules } : {}
381
+ ...subModules ? { subModules } : {},
382
+ ...parsed.data.category ? { category: parsed.data.category } : {}
368
383
  });
369
384
  } catch {
370
385
  }
@@ -432,9 +447,17 @@ import path5 from "path";
432
447
  import * as p2 from "@clack/prompts";
433
448
  var { outputFile, pathExists: pathExists6 } = fsExtra7;
434
449
  async function installFiles(options) {
435
- const { moduleName, files, cwd, dryRun, localPath } = options;
450
+ const { moduleName, files, cwd, dryRun, localPath, installedModules = /* @__PURE__ */ new Set() } = options;
436
451
  const results = [];
437
452
  for (const file of files) {
453
+ if (file.when?.moduleInstalled !== void 0 && !installedModules.has(file.when.moduleInstalled)) {
454
+ results.push({
455
+ dest: file.dest,
456
+ action: "conditional-skip",
457
+ reason: `requires ${file.when.moduleInstalled}`
458
+ });
459
+ continue;
460
+ }
438
461
  const destAbs = path5.join(cwd, file.dest);
439
462
  const exists = await pathExists6(destAbs);
440
463
  if (exists) {
@@ -737,17 +760,26 @@ function installNpmDeps(deps, cwd, dryRun) {
737
760
  p5.log.step(`Installing dependencies: ${deps.join(", ")}`);
738
761
  execFileSync(pm, args, { cwd, stdio: "inherit" });
739
762
  }
740
- async function installModule(moduleId, manifest, cwd, dryRun, localPath) {
763
+ async function installModule(moduleId, manifest, cwd, dryRun, installedModules, localPath) {
741
764
  p5.log.step(`Installing ${moduleId}@${manifest.version}...`);
765
+ const installedDests = [];
742
766
  if (manifest.files.length > 0) {
743
767
  const installed = await installFiles({
744
768
  moduleName: moduleId,
745
769
  files: manifest.files,
746
770
  cwd,
747
771
  dryRun,
748
- localPath
772
+ localPath,
773
+ installedModules
749
774
  });
750
775
  for (const f of installed) {
776
+ if (f.action === "conditional-skip") {
777
+ if (dryRun) {
778
+ p5.log.message(` \u25CB ${f.dest} [skipped: ${f.reason}]`);
779
+ }
780
+ continue;
781
+ }
782
+ installedDests.push(f.dest);
751
783
  const icon = f.action === "created" || f.action === "would-create" ? "+" : f.action === "overwritten" || f.action === "would-overwrite" ? "~" : "=";
752
784
  p5.log.message(` ${icon} ${f.dest}`);
753
785
  }
@@ -756,14 +788,15 @@ async function installModule(moduleId, manifest, cwd, dryRun, localPath) {
756
788
  p5.log.step(` transform: ${transform.type} \u2192 ${transform.target}`);
757
789
  await runTransform(transform, cwd, dryRun);
758
790
  }
791
+ return installedDests;
759
792
  }
760
- function recordModule(config, moduleId, manifest, now) {
793
+ function recordModule(config, moduleId, manifest, installedFiles, now) {
761
794
  const existing = config.installedModules.findIndex((m) => m.name === moduleId);
762
795
  const record = {
763
796
  name: moduleId,
764
797
  version: manifest.version,
765
798
  installedAt: now,
766
- files: manifest.files.map((f) => f.dest)
799
+ files: installedFiles
767
800
  };
768
801
  if (existing >= 0) {
769
802
  config.installedModules[existing] = record;
@@ -907,6 +940,19 @@ Available: ${parentManifest.subModules.join(", ")}`
907
940
  for (const id of orderedModules) {
908
941
  manifests.set(id, await fetchModuleManifest(id, localPath));
909
942
  }
943
+ for (const [id, manifest] of manifests) {
944
+ if (installedNames.has(id)) continue;
945
+ for (const incompatible of manifest.incompatibleWith) {
946
+ if (installedNames.has(incompatible)) {
947
+ p5.cancel(`Module "${id}" is incompatible with installed module "${incompatible}".`);
948
+ process.exit(1);
949
+ }
950
+ if (manifests.has(incompatible) && !installedNames.has(incompatible)) {
951
+ p5.cancel(`Module "${id}" is incompatible with module "${incompatible}" (also being installed).`);
952
+ process.exit(1);
953
+ }
954
+ }
955
+ }
910
956
  const allDeps = /* @__PURE__ */ new Set();
911
957
  for (const manifest of manifests.values()) {
912
958
  for (const dep of manifest.dependencies) {
@@ -951,13 +997,16 @@ Available: ${parentManifest.subModules.join(", ")}`
951
997
  const primaryTargetSet = new Set(allTargets);
952
998
  const depModules = orderedModules.filter((id) => !primaryTargetSet.has(id) && !installedNames.has(id));
953
999
  const targetModules = orderedModules.filter((id) => primaryTargetSet.has(id));
1000
+ const installedFilesMap = /* @__PURE__ */ new Map();
954
1001
  const depPostInstallHooks = [];
955
1002
  if (depModules.length > 0) {
956
1003
  p5.log.step(`Installing module dependencies: ${depModules.join(", ")}`);
957
1004
  for (const dep of depModules) {
958
1005
  const depManifest = manifests.get(dep);
959
1006
  if (!depManifest) continue;
960
- await installModule(dep, depManifest, cwd, dryRun, localPath);
1007
+ const dests = await installModule(dep, depManifest, cwd, dryRun, installedNames, localPath);
1008
+ installedFilesMap.set(dep, dests);
1009
+ installedNames.add(dep);
961
1010
  if (depManifest.postInstall && depManifest.postInstall.length > 0) {
962
1011
  depPostInstallHooks.push({ name: dep, hooks: depManifest.postInstall });
963
1012
  }
@@ -966,7 +1015,9 @@ Available: ${parentManifest.subModules.join(", ")}`
966
1015
  for (const targetId of targetModules) {
967
1016
  const targetManifest = manifests.get(targetId);
968
1017
  if (!targetManifest) continue;
969
- await installModule(targetId, targetManifest, cwd, dryRun, localPath);
1018
+ const dests = await installModule(targetId, targetManifest, cwd, dryRun, installedNames, localPath);
1019
+ installedFilesMap.set(targetId, dests);
1020
+ installedNames.add(targetId);
970
1021
  }
971
1022
  installNpmDeps([...allDeps], cwd, dryRun);
972
1023
  if (!dryRun) {
@@ -993,11 +1044,13 @@ Available: ${parentManifest.subModules.join(", ")}`
993
1044
  const now = (/* @__PURE__ */ new Date()).toISOString();
994
1045
  for (const dep of depModules) {
995
1046
  const depManifest = manifests.get(dep);
996
- if (depManifest) recordModule(config, dep, depManifest, now);
1047
+ const dests = installedFilesMap.get(dep) ?? [];
1048
+ if (depManifest) recordModule(config, dep, depManifest, dests, now);
997
1049
  }
998
1050
  for (const targetId of targetModules) {
999
1051
  const targetManifest = manifests.get(targetId);
1000
- if (targetManifest) recordModule(config, targetId, targetManifest, now);
1052
+ const dests = installedFilesMap.get(targetId) ?? [];
1053
+ if (targetManifest) recordModule(config, targetId, targetManifest, dests, now);
1001
1054
  }
1002
1055
  await writeConfig(config, cwd);
1003
1056
  }
@@ -1009,6 +1062,24 @@ Available: ${parentManifest.subModules.join(", ")}`
1009
1062
 
1010
1063
  // src/commands/list.ts
1011
1064
  import * as p6 from "@clack/prompts";
1065
+ function printModule(mod, installedNames, installedModules) {
1066
+ const installed = installedNames.has(mod.name);
1067
+ const installedInfo = installed ? installedModules?.find((m) => m.name === mod.name) : null;
1068
+ const status = installed ? `[installed v${installedInfo?.version ?? "?"}]` : "[available]";
1069
+ const desc = mod.description ? ` \u2014 ${mod.description}` : "";
1070
+ p6.log.message(` ${installed ? "\u2713" : "\u25CB"} ${mod.name} ${status}${desc}`);
1071
+ if (mod.subModules && mod.subModules.length > 0) {
1072
+ const last = mod.subModules.length - 1;
1073
+ mod.subModules.forEach((sub, i) => {
1074
+ const subId = `${mod.name}/${sub}`;
1075
+ const subInstalled = installedNames.has(subId);
1076
+ const subInfo = subInstalled ? installedModules?.find((m) => m.name === subId) : null;
1077
+ const subStatus = subInstalled ? `[installed v${subInfo?.version ?? "?"}]` : "[not installed]";
1078
+ const connector = i === last ? "\u2514\u2500" : "\u251C\u2500";
1079
+ p6.log.message(` ${connector} ${sub} ${subStatus}`);
1080
+ });
1081
+ }
1082
+ }
1012
1083
  async function runList(options) {
1013
1084
  const { cwd, localPath } = options;
1014
1085
  p6.intro("impulse list");
@@ -1034,22 +1105,33 @@ async function runList(options) {
1034
1105
  return;
1035
1106
  }
1036
1107
  p6.log.message("\nAvailable modules:\n");
1037
- for (const mod of available) {
1038
- const installed = installedNames.has(mod.name);
1039
- const installedInfo = installed ? config?.installedModules.find((m) => m.name === mod.name) : null;
1040
- const status = installed ? `[installed v${installedInfo?.version ?? "?"}]` : "[available]";
1041
- const desc = mod.description ? ` \u2014 ${mod.description}` : "";
1042
- p6.log.message(` ${installed ? "\u2713" : "\u25CB"} ${mod.name} ${status}${desc}`);
1043
- if (mod.subModules && mod.subModules.length > 0) {
1044
- const last = mod.subModules.length - 1;
1045
- mod.subModules.forEach((sub, i) => {
1046
- const subId = `${mod.name}/${sub}`;
1047
- const subInstalled = installedNames.has(subId);
1048
- const subInfo = subInstalled ? config?.installedModules.find((m) => m.name === subId) : null;
1049
- const subStatus = subInstalled ? `[installed v${subInfo?.version ?? "?"}]` : "[not installed]";
1050
- const connector = i === last ? "\u2514\u2500" : "\u251C\u2500";
1051
- p6.log.message(` ${connector} ${sub} ${subStatus}`);
1052
- });
1108
+ const hasCategories = available.some((m) => m.category !== void 0);
1109
+ if (hasCategories) {
1110
+ const groups = /* @__PURE__ */ new Map();
1111
+ for (const mod of available) {
1112
+ const key = mod.category ?? "other";
1113
+ const existing = groups.get(key);
1114
+ if (existing) {
1115
+ existing.push(mod);
1116
+ } else {
1117
+ groups.set(key, [mod]);
1118
+ }
1119
+ }
1120
+ const categoryOrder = ["core", "feature", "integration", "dx", "other"];
1121
+ const sortedKeys = [
1122
+ ...categoryOrder.filter((k) => groups.has(k)),
1123
+ ...[...groups.keys()].filter((k) => !categoryOrder.includes(k))
1124
+ ];
1125
+ for (const key of sortedKeys) {
1126
+ p6.log.message(`${key.toUpperCase()}`);
1127
+ for (const mod of groups.get(key)) {
1128
+ printModule(mod, installedNames, config?.installedModules);
1129
+ }
1130
+ p6.log.message("");
1131
+ }
1132
+ } else {
1133
+ for (const mod of available) {
1134
+ printModule(mod, installedNames, config?.installedModules);
1053
1135
  }
1054
1136
  }
1055
1137
  p6.log.message(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impulselab/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "ImpulseLab CLI — install and manage modules for your projects",
5
5
  "private": false,
6
6
  "type": "module",