@impulselab/cli 0.1.0 → 0.1.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 (39) hide show
  1. package/dist/index.js +86 -8
  2. package/package.json +18 -9
  3. package/src/commands/add.test.ts +0 -147
  4. package/src/commands/add.ts +0 -335
  5. package/src/commands/init.ts +0 -114
  6. package/src/commands/list.ts +0 -79
  7. package/src/config/config-path.ts +0 -7
  8. package/src/config/has-config.ts +0 -9
  9. package/src/config/index.ts +0 -4
  10. package/src/config/read-config.ts +0 -20
  11. package/src/config/write-config.ts +0 -11
  12. package/src/config.test.ts +0 -64
  13. package/src/index.ts +0 -64
  14. package/src/installer.ts +0 -71
  15. package/src/registry/fetch-module-file.ts +0 -21
  16. package/src/registry/fetch-module-manifest.ts +0 -43
  17. package/src/registry/github-urls.ts +0 -13
  18. package/src/registry/index.ts +0 -5
  19. package/src/registry/list-available-modules.ts +0 -113
  20. package/src/registry/parse-module-id.ts +0 -30
  21. package/src/registry/registry.test.ts +0 -181
  22. package/src/schemas/impulse-config.ts +0 -21
  23. package/src/schemas/index.ts +0 -9
  24. package/src/schemas/module-dependency.ts +0 -3
  25. package/src/schemas/module-file.ts +0 -8
  26. package/src/schemas/module-manifest.ts +0 -23
  27. package/src/schemas/module-transform.ts +0 -15
  28. package/src/transforms/add-env.ts +0 -53
  29. package/src/transforms/add-nav-item.test.ts +0 -125
  30. package/src/transforms/add-nav-item.ts +0 -70
  31. package/src/transforms/append-export.test.ts +0 -50
  32. package/src/transforms/append-export.ts +0 -34
  33. package/src/transforms/index.ts +0 -32
  34. package/src/transforms/merge-schema.test.ts +0 -70
  35. package/src/transforms/merge-schema.ts +0 -35
  36. package/src/transforms/register-route.test.ts +0 -177
  37. package/src/transforms/register-route.ts +0 -47
  38. package/src/types.ts +0 -9
  39. package/tsconfig.json +0 -8
package/dist/index.js CHANGED
@@ -71,6 +71,11 @@ var ULTIMATE_TEMPLATE_MARKERS = [
71
71
  "@impulselab/ultimate-template",
72
72
  "ultimate-template"
73
73
  ];
74
+ var STRUCTURAL_MARKERS = {
75
+ requiredDirs: ["packages/server", "packages/database"],
76
+ requiredFiles: ["turbo.json", "pnpm-workspace.yaml"],
77
+ workspaceDeps: ["@orpc/server", "drizzle-orm", "better-auth"]
78
+ };
74
79
  async function detectProjectName(cwd) {
75
80
  const pkgPath = path2.join(cwd, "package.json");
76
81
  if (await pathExists3(pkgPath)) {
@@ -81,6 +86,18 @@ async function detectProjectName(cwd) {
81
86
  }
82
87
  return path2.basename(cwd);
83
88
  }
89
+ function getDeps(pkg) {
90
+ const deps = /* @__PURE__ */ new Set();
91
+ for (const key of ["dependencies", "devDependencies"]) {
92
+ const d = pkg[key];
93
+ if (d && typeof d === "object") {
94
+ for (const name of Object.keys(d)) {
95
+ deps.add(name);
96
+ }
97
+ }
98
+ }
99
+ return deps;
100
+ }
84
101
  async function isUltimateTemplate(cwd) {
85
102
  const pkgPath = path2.join(cwd, "package.json");
86
103
  if (!await pathExists3(pkgPath)) return false;
@@ -93,7 +110,40 @@ async function isUltimateTemplate(cwd) {
93
110
  const keywordMatches = ULTIMATE_TEMPLATE_MARKERS.some(
94
111
  (m) => keywords.includes(m)
95
112
  );
96
- return nameMatches || keywordMatches;
113
+ if (nameMatches || keywordMatches) return true;
114
+ if (pkg["impulseTemplate"] === true) return true;
115
+ const hasRequiredFiles = (await Promise.all(
116
+ STRUCTURAL_MARKERS.requiredFiles.map(
117
+ (f) => pathExists3(path2.join(cwd, f))
118
+ )
119
+ )).every(Boolean);
120
+ if (!hasRequiredFiles) return false;
121
+ const hasRequiredDirs = (await Promise.all(
122
+ STRUCTURAL_MARKERS.requiredDirs.map(
123
+ (d) => pathExists3(path2.join(cwd, d))
124
+ )
125
+ )).every(Boolean);
126
+ if (!hasRequiredDirs) return false;
127
+ const workspaceDirs = ["packages", "apps"];
128
+ const foundDeps = /* @__PURE__ */ new Set();
129
+ for (const dir of workspaceDirs) {
130
+ const dirPath = path2.join(cwd, dir);
131
+ if (!await pathExists3(dirPath)) continue;
132
+ const { readdir } = await import("fs/promises");
133
+ const entries = await readdir(dirPath, { withFileTypes: true });
134
+ for (const entry of entries) {
135
+ if (!entry.isDirectory()) continue;
136
+ const childPkg = path2.join(dirPath, entry.name, "package.json");
137
+ if (!await pathExists3(childPkg)) continue;
138
+ const childRaw = await readJson2(childPkg);
139
+ if (!childRaw || typeof childRaw !== "object") continue;
140
+ const deps = getDeps(childRaw);
141
+ for (const dep of STRUCTURAL_MARKERS.workspaceDeps) {
142
+ if (deps.has(dep)) foundDeps.add(dep);
143
+ }
144
+ }
145
+ }
146
+ return foundDeps.size >= STRUCTURAL_MARKERS.workspaceDeps.length;
97
147
  }
98
148
  async function runInit(options) {
99
149
  const { cwd, force } = options;
@@ -145,7 +195,7 @@ async function runInit(options) {
145
195
  }
146
196
 
147
197
  // src/commands/add.ts
148
- import { execSync, execFileSync } from "child_process";
198
+ import { execSync as execSync2, execFileSync } from "child_process";
149
199
  import { existsSync } from "fs";
150
200
  import path11 from "path";
151
201
  import * as p4 from "@clack/prompts";
@@ -201,6 +251,7 @@ var ModuleManifestSchema = z5.object({
201
251
  });
202
252
 
203
253
  // src/registry/github-urls.ts
254
+ import { execSync } from "child_process";
204
255
  var GITHUB_ORG = "impulse-studio";
205
256
  var GITHUB_REPO = "impulse-modules";
206
257
  var GITHUB_BRANCH = "main";
@@ -210,6 +261,33 @@ var githubUrls = {
210
261
  moduleList: () => `https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/contents/${MODULES_DIR}`,
211
262
  subModulesList: (parentModule) => `https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/contents/${MODULES_DIR}/${parentModule}/sub-modules`
212
263
  };
264
+ var _cachedToken;
265
+ function getGitHubToken() {
266
+ if (_cachedToken !== void 0) return _cachedToken;
267
+ const envToken = process.env.GITHUB_TOKEN;
268
+ if (envToken) {
269
+ _cachedToken = envToken;
270
+ return envToken;
271
+ }
272
+ try {
273
+ const token = execSync("gh auth token", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
274
+ if (token) {
275
+ _cachedToken = token;
276
+ return token;
277
+ }
278
+ } catch {
279
+ }
280
+ _cachedToken = null;
281
+ return null;
282
+ }
283
+ function getGitHubHeaders(extra) {
284
+ const headers = { ...extra };
285
+ const token = getGitHubToken();
286
+ if (token) {
287
+ headers["Authorization"] = `Bearer ${token}`;
288
+ }
289
+ return headers;
290
+ }
213
291
 
214
292
  // src/registry/parse-module-id.ts
215
293
  function parseModuleId(moduleId) {
@@ -243,7 +321,7 @@ async function fetchModuleManifest(moduleId, localPath) {
243
321
  return parsed2.data;
244
322
  }
245
323
  const url = githubUrls.rawFile(registryPath, "module.json");
246
- const res = await fetch(url);
324
+ const res = await fetch(url, { headers: getGitHubHeaders() });
247
325
  if (!res.ok) {
248
326
  if (res.status === 404) {
249
327
  throw new Error(`Module not found in registry: ${moduleId}`);
@@ -295,7 +373,7 @@ async function listAvailableModules(localPath) {
295
373
  }
296
374
  if (_cachedModuleList) return _cachedModuleList;
297
375
  const res = await fetch(githubUrls.moduleList(), {
298
- headers: { Accept: "application/vnd.github.v3+json" }
376
+ headers: getGitHubHeaders({ Accept: "application/vnd.github.v3+json" })
299
377
  });
300
378
  if (!res.ok) {
301
379
  throw new Error(`Failed to fetch module list: ${res.status} ${res.statusText}`);
@@ -314,7 +392,7 @@ async function listAvailableModules(localPath) {
314
392
  baseModules.map(async (mod) => {
315
393
  try {
316
394
  const subRes = await fetch(githubUrls.subModulesList(mod.name), {
317
- headers: { Accept: "application/vnd.github.v3+json" }
395
+ headers: getGitHubHeaders({ Accept: "application/vnd.github.v3+json" })
318
396
  });
319
397
  if (!subRes.ok) return mod;
320
398
  const subEntries = await subRes.json();
@@ -341,7 +419,7 @@ async function fetchModuleFile(moduleName, fileSrc, localPath) {
341
419
  return readFile6(file, "utf-8");
342
420
  }
343
421
  const url = githubUrls.rawFile(moduleName, fileSrc);
344
- const res = await fetch(url);
422
+ const res = await fetch(url, { headers: getGitHubHeaders() });
345
423
  if (!res.ok) {
346
424
  throw new Error(`Failed to fetch file ${fileSrc} for module ${moduleName}: ${res.status}`);
347
425
  }
@@ -767,7 +845,7 @@ Available: ${parentManifest.subModules.join(", ")}`
767
845
  p4.log.step(`Running post-install hooks for ${name}...`);
768
846
  for (const hook of hooks) {
769
847
  p4.log.message(` $ ${hook}`);
770
- execSync(hook, { cwd, stdio: "inherit" });
848
+ execSync2(hook, { cwd, stdio: "inherit" });
771
849
  }
772
850
  }
773
851
  }
@@ -778,7 +856,7 @@ Available: ${parentManifest.subModules.join(", ")}`
778
856
  p4.log.step(`Running post-install hooks for ${targetId}...`);
779
857
  for (const hook of targetManifest.postInstall) {
780
858
  p4.log.message(` $ ${hook}`);
781
- execSync(hook, { cwd, stdio: "inherit" });
859
+ execSync2(hook, { cwd, stdio: "inherit" });
782
860
  }
783
861
  }
784
862
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impulselab/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "ImpulseLab CLI — install and manage modules for your projects",
5
5
  "private": false,
6
6
  "type": "module",
@@ -8,13 +8,16 @@
8
8
  "bin": {
9
9
  "impulse": "dist/index.js"
10
10
  },
11
- "scripts": {
12
- "prepublishOnly": "pnpm run build",
13
- "publish:npm": "npm publish --access public",
14
- "build": "tsup src/index.ts --format esm --target es2022 --dts",
15
- "dev": "tsup src/index.ts --format esm --target es2022 --watch",
16
- "lint": "oxlint .",
17
- "test": "vitest run"
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/impulse-studio/impulse-modules",
20
+ "directory": "cli"
18
21
  },
19
22
  "devDependencies": {
20
23
  "@types/fs-extra": "^11.0.4",
@@ -29,5 +32,11 @@
29
32
  "commander": "^14.0.3",
30
33
  "fs-extra": "^11.3.4",
31
34
  "zod": "^4.3.6"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format esm --target es2022 --dts",
38
+ "dev": "tsup src/index.ts --format esm --target es2022 --watch",
39
+ "lint": "oxlint .",
40
+ "test": "vitest run"
32
41
  }
33
- }
42
+ }
@@ -1,147 +0,0 @@
1
- /**
2
- * Tests for sub-module resolution logic in the add command.
3
- * Uses a local modules fixture directory to avoid network calls.
4
- */
5
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
6
- import { mkdtemp, rm, mkdir } from "fs/promises";
7
- import { outputJson } from "fs-extra";
8
- import path from "path";
9
- import os from "os";
10
- import { parseModuleId } from "../registry/index";
11
-
12
- // Re-export parseModuleId through the test as a sanity check
13
- describe("parseModuleId integration", () => {
14
- it("correctly identifies sub-module ids", () => {
15
- const { parent, child } = parseModuleId("attio/gocardless");
16
- expect(parent).toBe("attio");
17
- expect(child).toBe("gocardless");
18
- });
19
-
20
- it("correctly identifies top-level ids", () => {
21
- const { parent, child } = parseModuleId("attio");
22
- expect(parent).toBe("attio");
23
- expect(child).toBeNull();
24
- });
25
- });
26
-
27
- // ---------------------------------------------------------------------------
28
- // resolveWithParent behaviour — validated via fetchModuleManifest + local fixtures
29
- // ---------------------------------------------------------------------------
30
-
31
- let tmpDir: string;
32
- let localModules: string;
33
-
34
- beforeEach(async () => {
35
- tmpDir = await mkdtemp(path.join(os.tmpdir(), "impulse-add-test-"));
36
- localModules = path.join(tmpDir, "modules");
37
- });
38
-
39
- afterEach(async () => {
40
- await rm(tmpDir, { recursive: true, force: true });
41
- });
42
-
43
- async function writeManifest(dir: string, manifest: Record<string, unknown>): Promise<void> {
44
- await mkdir(dir, { recursive: true });
45
- await outputJson(path.join(dir, "module.json"), manifest);
46
- }
47
-
48
- describe("sub-module dependency resolution (via fetchModuleManifest)", () => {
49
- it("resolves parent manifest when installing a sub-module", async () => {
50
- const { fetchModuleManifest } = await import("../registry/index");
51
-
52
- await writeManifest(path.join(localModules, "attio"), {
53
- name: "attio",
54
- version: "1.0.0",
55
- description: "Attio",
56
- subModules: ["quote-to-cash", "gocardless"],
57
- files: [],
58
- transforms: [],
59
- });
60
- await writeManifest(
61
- path.join(localModules, "attio", "sub-modules", "quote-to-cash"),
62
- {
63
- name: "attio/quote-to-cash",
64
- version: "1.0.0",
65
- description: "Quote-to-cash",
66
- parentModule: "attio",
67
- files: [],
68
- transforms: [],
69
- }
70
- );
71
-
72
- const parentManifest = await fetchModuleManifest("attio", localModules);
73
- expect(parentManifest.name).toBe("attio");
74
- expect(parentManifest.subModules).toContain("quote-to-cash");
75
-
76
- const subManifest = await fetchModuleManifest("attio/quote-to-cash", localModules);
77
- expect(subManifest.parentModule).toBe("attio");
78
- expect(subManifest.name).toBe("attio/quote-to-cash");
79
- });
80
-
81
- it("installs multiple --with sub-modules (validates all IDs resolvable)", async () => {
82
- const { fetchModuleManifest } = await import("../registry/index");
83
-
84
- await writeManifest(path.join(localModules, "attio"), {
85
- name: "attio",
86
- version: "1.0.0",
87
- description: "Attio",
88
- subModules: ["quote-to-cash", "gocardless", "pennylane"],
89
- files: [],
90
- transforms: [],
91
- });
92
-
93
- const subNames = ["quote-to-cash", "gocardless", "pennylane"];
94
- for (const sub of subNames) {
95
- await writeManifest(
96
- path.join(localModules, "attio", "sub-modules", sub),
97
- {
98
- name: `attio/${sub}`,
99
- version: "1.0.0",
100
- description: sub,
101
- parentModule: "attio",
102
- files: [],
103
- transforms: [],
104
- }
105
- );
106
- }
107
-
108
- // Verify each sub-module can be fetched via slash syntax
109
- for (const sub of subNames) {
110
- const manifest = await fetchModuleManifest(`attio/${sub}`, localModules);
111
- expect(manifest.parentModule).toBe("attio");
112
- }
113
- });
114
-
115
- it("auto-dependency: sub-module declares parent in moduleDependencies", async () => {
116
- const { fetchModuleManifest } = await import("../registry/index");
117
-
118
- await writeManifest(path.join(localModules, "attio"), {
119
- name: "attio",
120
- version: "1.0.0",
121
- description: "Attio",
122
- subModules: ["gocardless"],
123
- files: [],
124
- transforms: [],
125
- });
126
- await writeManifest(
127
- path.join(localModules, "attio", "sub-modules", "gocardless"),
128
- {
129
- name: "attio/gocardless",
130
- version: "1.0.0",
131
- description: "GoCardless",
132
- parentModule: "attio",
133
- moduleDependencies: [],
134
- files: [],
135
- transforms: [],
136
- }
137
- );
138
-
139
- // The parent manifest should be valid
140
- const parent = await fetchModuleManifest("attio", localModules);
141
- expect(parent.subModules).toContain("gocardless");
142
-
143
- // The sub-module should have parentModule set
144
- const sub = await fetchModuleManifest("attio/gocardless", localModules);
145
- expect(sub.parentModule).toBe("attio");
146
- });
147
- });
@@ -1,335 +0,0 @@
1
- import { execSync, execFileSync } from "child_process";
2
- import { existsSync } from "fs";
3
- import path from "path";
4
- import * as p from "@clack/prompts";
5
- import { readConfig, writeConfig } from "../config/index";
6
- import { fetchModuleManifest, parseModuleId } from "../registry/index";
7
- import { installFiles } from "../installer";
8
- import { runTransform } from "../transforms/index";
9
- import type { ModuleManifest } from "../types";
10
- import type { ImpulseConfig } from "../schemas/impulse-config";
11
-
12
- async function resolveModuleDeps(
13
- moduleId: string,
14
- localPath: string | undefined,
15
- resolved: Set<string>,
16
- orderedModules: string[]
17
- ): Promise<void> {
18
- if (resolved.has(moduleId)) return;
19
- resolved.add(moduleId);
20
-
21
- const manifest = await fetchModuleManifest(moduleId, localPath);
22
- for (const dep of manifest.moduleDependencies) {
23
- await resolveModuleDeps(dep, localPath, resolved, orderedModules);
24
- }
25
- orderedModules.push(moduleId);
26
- }
27
-
28
- /**
29
- * For a sub-module install (`parent/child`), ensure the parent module itself
30
- * is resolved first if it is not already installed.
31
- */
32
- async function resolveWithParent(
33
- moduleId: string,
34
- localPath: string | undefined,
35
- installedNames: Set<string>,
36
- resolved: Set<string>,
37
- orderedModules: string[]
38
- ): Promise<void> {
39
- const { parent, child } = parseModuleId(moduleId);
40
- if (child !== null && !installedNames.has(parent)) {
41
- await resolveModuleDeps(parent, localPath, resolved, orderedModules);
42
- }
43
- await resolveModuleDeps(moduleId, localPath, resolved, orderedModules);
44
- }
45
-
46
- function detectPackageManager(cwd: string): "pnpm" | "npm" | "yarn" {
47
- if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
48
- if (existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
49
- return "npm";
50
- }
51
-
52
- function installNpmDeps(
53
- deps: string[],
54
- cwd: string,
55
- dryRun: boolean
56
- ): void {
57
- if (deps.length === 0) return;
58
- const pm = detectPackageManager(cwd);
59
- const args = ["add", ...deps];
60
-
61
- if (dryRun) {
62
- p.log.info(`[dry-run] Would run: ${pm} ${args.join(" ")}`);
63
- return;
64
- }
65
-
66
- p.log.step(`Installing dependencies: ${deps.join(", ")}`);
67
- execFileSync(pm, args, { cwd, stdio: "inherit" });
68
- }
69
-
70
- async function installModule(
71
- moduleId: string,
72
- manifest: ModuleManifest,
73
- cwd: string,
74
- dryRun: boolean,
75
- localPath?: string
76
- ): Promise<void> {
77
- p.log.step(`Installing ${moduleId}@${manifest.version}...`);
78
-
79
- if (manifest.files.length > 0) {
80
- const installed = await installFiles({
81
- moduleName: moduleId,
82
- files: manifest.files,
83
- cwd,
84
- dryRun,
85
- localPath,
86
- });
87
-
88
- for (const f of installed) {
89
- const icon =
90
- f.action === "created" || f.action === "would-create"
91
- ? "+"
92
- : f.action === "overwritten" || f.action === "would-overwrite"
93
- ? "~"
94
- : "=";
95
- p.log.message(` ${icon} ${f.dest}`);
96
- }
97
- }
98
-
99
- for (const transform of manifest.transforms) {
100
- p.log.step(` transform: ${transform.type} → ${transform.target}`);
101
- await runTransform(transform, cwd, dryRun);
102
- }
103
- }
104
-
105
- function recordModule(
106
- config: ImpulseConfig,
107
- moduleId: string,
108
- manifest: ModuleManifest,
109
- now: string
110
- ): void {
111
- const existing = config.installedModules.findIndex((m) => m.name === moduleId);
112
- const record = {
113
- name: moduleId,
114
- version: manifest.version,
115
- installedAt: now,
116
- files: manifest.files.map((f) => f.dest),
117
- };
118
- if (existing >= 0) {
119
- config.installedModules[existing] = record;
120
- } else {
121
- config.installedModules.push(record);
122
- }
123
- }
124
-
125
- export async function runAdd(options: {
126
- moduleName: string;
127
- cwd: string;
128
- dryRun: boolean;
129
- localPath?: string;
130
- withSubModules?: string[];
131
- }): Promise<void> {
132
- const { moduleName, cwd, dryRun, localPath, withSubModules = [] } = options;
133
-
134
- // Build full list: main target + --with sub-modules
135
- const withIds = withSubModules.map((sub) => `${moduleName}/${sub}`);
136
- const allTargets = [moduleName, ...withIds];
137
-
138
- p.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
139
-
140
- const config = await readConfig(cwd);
141
- if (!config) {
142
- p.cancel("No .impulse.json found. Run `impulse init` first.");
143
- process.exit(1);
144
- }
145
-
146
- const installedNames = new Set(config.installedModules.map((m) => m.name));
147
-
148
- // Check already-installed only for a simple (non-sub-module, no --with) install
149
- const { child: mainChild } = parseModuleId(moduleName);
150
- if (mainChild === null && withSubModules.length === 0 && installedNames.has(moduleName)) {
151
- const existing = config.installedModules.find((m) => m.name === moduleName);
152
- p.log.warn(`Module "${moduleName}" is already installed (v${existing?.version ?? "?"}).`);
153
- const reinstall = await p.confirm({
154
- message: "Reinstall?",
155
- initialValue: false,
156
- });
157
- if (p.isCancel(reinstall) || !reinstall) {
158
- p.outro("Cancelled.");
159
- return;
160
- }
161
- }
162
-
163
- // Validate --with sub-module names against the parent manifest's declared sub-modules
164
- if (withSubModules.length > 0) {
165
- const parentManifest = await fetchModuleManifest(moduleName, localPath).catch(() => null);
166
- if (parentManifest) {
167
- if (parentManifest.subModules.length === 0) {
168
- p.cancel(`"${moduleName}" has no declared sub-modules.`);
169
- process.exit(1);
170
- }
171
- const invalid = withSubModules.filter((sub) => !parentManifest.subModules.includes(sub));
172
- if (invalid.length > 0) {
173
- p.cancel(
174
- `Unknown sub-module(s) for "${moduleName}": ${invalid.join(", ")}.\nAvailable: ${parentManifest.subModules.join(", ")}`
175
- );
176
- process.exit(1);
177
- }
178
- }
179
- }
180
-
181
- // Resolve dependency order for all targets
182
- const s = p.spinner();
183
- s.start("Resolving dependencies...");
184
-
185
- const resolved = new Set<string>();
186
- const orderedModules: string[] = [];
187
-
188
- try {
189
- for (const target of allTargets) {
190
- await resolveWithParent(target, localPath, installedNames, resolved, orderedModules);
191
- }
192
- } catch (err) {
193
- s.stop("Dependency resolution failed.");
194
- p.cancel(err instanceof Error ? err.message : String(err));
195
- process.exit(1);
196
- }
197
-
198
- s.stop(`Resolved: ${orderedModules.join(" → ")}`);
199
-
200
- // Fetch all manifests
201
- const manifests = new Map<string, ModuleManifest>();
202
- for (const id of orderedModules) {
203
- manifests.set(id, await fetchModuleManifest(id, localPath));
204
- }
205
-
206
- // Collect all npm deps
207
- const allDeps = new Set<string>();
208
- for (const manifest of manifests.values()) {
209
- for (const dep of manifest.dependencies) {
210
- allDeps.add(dep);
211
- }
212
- }
213
-
214
- // Show summary
215
- p.log.message("\nSummary of changes:");
216
- for (const [id, manifest] of manifests) {
217
- p.log.message(`\n Module: ${id}@${manifest.version}`);
218
- for (const file of manifest.files) {
219
- p.log.message(` + ${file.dest}`);
220
- }
221
- for (const transform of manifest.transforms) {
222
- p.log.message(` ~ ${transform.type} → ${transform.target}`);
223
- }
224
- }
225
-
226
- // Collect env vars documented across all modules (informational display)
227
- const allEnvVars = new Set<string>();
228
- for (const manifest of manifests.values()) {
229
- for (const envVar of manifest.envVars) {
230
- allEnvVars.add(envVar);
231
- }
232
- }
233
-
234
- if (allDeps.size > 0) {
235
- p.log.message(`\n npm deps: ${[...allDeps].join(", ")}`);
236
- }
237
-
238
- if (allEnvVars.size > 0) {
239
- p.log.message(`\n env vars required: ${[...allEnvVars].join(", ")}`);
240
- }
241
-
242
- if (!dryRun) {
243
- const confirm = await p.confirm({
244
- message: "Proceed?",
245
- initialValue: true,
246
- });
247
-
248
- if (p.isCancel(confirm) || !confirm) {
249
- p.outro("Cancelled.");
250
- return;
251
- }
252
- }
253
-
254
- // Split ordered modules into auto-dependencies vs primary targets
255
- const primaryTargetSet = new Set(allTargets);
256
- const depModules = orderedModules.filter((id) => !primaryTargetSet.has(id) && !installedNames.has(id));
257
- const targetModules = orderedModules.filter((id) => primaryTargetSet.has(id));
258
-
259
- // Collect dep postInstall hooks
260
- const depPostInstallHooks: Array<{ name: string; hooks: string[] }> = [];
261
-
262
- if (depModules.length > 0) {
263
- p.log.step(`Installing module dependencies: ${depModules.join(", ")}`);
264
- for (const dep of depModules) {
265
- const depManifest = manifests.get(dep);
266
- if (!depManifest) continue;
267
- await installModule(dep, depManifest, cwd, dryRun, localPath);
268
- if (depManifest.postInstall && depManifest.postInstall.length > 0) {
269
- depPostInstallHooks.push({ name: dep, hooks: depManifest.postInstall });
270
- }
271
- }
272
- }
273
-
274
- // Install primary targets in resolved order
275
- for (const targetId of targetModules) {
276
- const targetManifest = manifests.get(targetId);
277
- if (!targetManifest) continue;
278
- await installModule(targetId, targetManifest, cwd, dryRun, localPath);
279
- }
280
-
281
- // Install npm deps so all postInstall hooks have their packages available
282
- installNpmDeps([...allDeps], cwd, dryRun);
283
-
284
- // Run post-install hooks for dependency modules
285
- if (!dryRun) {
286
- for (const { name, hooks } of depPostInstallHooks) {
287
- p.log.step(`Running post-install hooks for ${name}...`);
288
- for (const hook of hooks) {
289
- p.log.message(` $ ${hook}`);
290
- execSync(hook, { cwd, stdio: "inherit" });
291
- }
292
- }
293
- }
294
-
295
- // Run post-install hooks for primary targets
296
- if (!dryRun) {
297
- for (const targetId of targetModules) {
298
- const targetManifest = manifests.get(targetId);
299
- if (!targetManifest?.postInstall?.length) continue;
300
- p.log.step(`Running post-install hooks for ${targetId}...`);
301
- for (const hook of targetManifest.postInstall) {
302
- p.log.message(` $ ${hook}`);
303
- execSync(hook, { cwd, stdio: "inherit" });
304
- }
305
- }
306
- }
307
-
308
- // Update .impulse.json
309
- if (!dryRun) {
310
- const now = new Date().toISOString();
311
-
312
- for (const dep of depModules) {
313
- const depManifest = manifests.get(dep);
314
- if (depManifest) recordModule(config, dep, depManifest, now);
315
- }
316
-
317
- for (const targetId of targetModules) {
318
- const targetManifest = manifests.get(targetId);
319
- if (targetManifest) recordModule(config, targetId, targetManifest, now);
320
- }
321
-
322
- await writeConfig(config, cwd);
323
- }
324
-
325
- const label =
326
- allTargets.length === 1
327
- ? `"${allTargets[0]}"`
328
- : allTargets.map((t) => `"${t}"`).join(", ");
329
-
330
- p.outro(
331
- dryRun
332
- ? "Dry run complete — no files were modified."
333
- : `Module(s) ${label} installed successfully!`
334
- );
335
- }