@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.
- package/dist/index.js +92 -9
- package/package.json +20 -9
- package/src/commands/add.test.ts +0 -147
- package/src/commands/add.ts +0 -335
- package/src/commands/init.ts +0 -114
- package/src/commands/list.ts +0 -79
- package/src/config/config-path.ts +0 -7
- package/src/config/has-config.ts +0 -9
- package/src/config/index.ts +0 -4
- package/src/config/read-config.ts +0 -20
- package/src/config/write-config.ts +0 -11
- package/src/config.test.ts +0 -64
- package/src/index.ts +0 -64
- package/src/installer.ts +0 -71
- package/src/registry/fetch-module-file.ts +0 -21
- package/src/registry/fetch-module-manifest.ts +0 -43
- package/src/registry/github-urls.ts +0 -13
- package/src/registry/index.ts +0 -5
- package/src/registry/list-available-modules.ts +0 -113
- package/src/registry/parse-module-id.ts +0 -30
- package/src/registry/registry.test.ts +0 -181
- package/src/schemas/impulse-config.ts +0 -21
- package/src/schemas/index.ts +0 -9
- package/src/schemas/module-dependency.ts +0 -3
- package/src/schemas/module-file.ts +0 -8
- package/src/schemas/module-manifest.ts +0 -23
- package/src/schemas/module-transform.ts +0 -15
- package/src/transforms/add-env.ts +0 -53
- package/src/transforms/add-nav-item.test.ts +0 -125
- package/src/transforms/add-nav-item.ts +0 -70
- package/src/transforms/append-export.test.ts +0 -50
- package/src/transforms/append-export.ts +0 -34
- package/src/transforms/index.ts +0 -32
- package/src/transforms/merge-schema.test.ts +0 -70
- package/src/transforms/merge-schema.ts +0 -35
- package/src/transforms/register-route.test.ts +0 -177
- package/src/transforms/register-route.ts +0 -47
- package/src/types.ts +0 -9
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|
package/src/commands/add.test.ts
DELETED
|
@@ -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
|
-
});
|