@impulselab/cli 0.1.0 → 0.1.2

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 +92 -9
  2. package/package.json +20 -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
  }
@@ -854,9 +932,14 @@ Run \`impulse add <module>\` to install a module.`
854
932
  p5.outro("Done.");
855
933
  }
856
934
 
935
+ // src/cli-version.ts
936
+ import { createRequire } from "module";
937
+ var require2 = createRequire(import.meta.url);
938
+ var CLI_VERSION = require2("../package.json").version;
939
+
857
940
  // src/index.ts
858
941
  var program = new Command();
859
- program.name("impulse").description("ImpulseLab CLI \u2014 install and manage modules for your projects").version("0.1.0");
942
+ program.name("impulse").description("ImpulseLab CLI \u2014 install and manage modules for your projects").version(CLI_VERSION);
860
943
  program.command("init").description("Initialize impulse in the current project").option("--force", "Reinitialize even if .impulse.json already exists", false).action(async (options) => {
861
944
  await runInit({ cwd: process.cwd(), force: options.force });
862
945
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impulselab/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "ImpulseLab CLI — install and manage modules for your projects",
5
5
  "private": false,
6
6
  "type": "module",
@@ -8,15 +8,19 @@
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": {
23
+ "bumpp": "^10.3.2",
20
24
  "@types/fs-extra": "^11.0.4",
21
25
  "@types/node": "^20.0.0",
22
26
  "oxlint": "^0.13.0",
@@ -29,5 +33,12 @@
29
33
  "commander": "^14.0.3",
30
34
  "fs-extra": "^11.3.4",
31
35
  "zod": "^4.3.6"
36
+ },
37
+ "scripts": {
38
+ "build": "tsup src/index.ts --format esm --target es2022 --dts",
39
+ "dev": "tsup src/index.ts --format esm --target es2022 --watch",
40
+ "lint": "oxlint .",
41
+ "test": "vitest run",
42
+ "release": "bumpp"
32
43
  }
33
- }
44
+ }
@@ -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
- });