@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.
- package/dist/index.js +86 -8
- package/package.json +18 -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/src/commands/init.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { readJson, pathExists } = fsExtra;
|
|
3
|
-
import path from "path";
|
|
4
|
-
import * as p from "@clack/prompts";
|
|
5
|
-
import { writeConfig, hasConfig } from "../config/index";
|
|
6
|
-
import type { ImpulseConfig } from "../types";
|
|
7
|
-
|
|
8
|
-
const ULTIMATE_TEMPLATE_MARKERS = [
|
|
9
|
-
"@impulselab/ultimate-template",
|
|
10
|
-
"ultimate-template",
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
async function detectProjectName(cwd: string): Promise<string> {
|
|
14
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
15
|
-
if (await pathExists(pkgPath)) {
|
|
16
|
-
const pkg: unknown = await readJson(pkgPath);
|
|
17
|
-
if (
|
|
18
|
-
pkg !== null &&
|
|
19
|
-
typeof pkg === "object" &&
|
|
20
|
-
"name" in pkg &&
|
|
21
|
-
typeof (pkg as Record<string, unknown>).name === "string"
|
|
22
|
-
) {
|
|
23
|
-
return (pkg as { name: string }).name;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return path.basename(cwd);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function isUltimateTemplate(cwd: string): Promise<boolean> {
|
|
30
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
31
|
-
if (!(await pathExists(pkgPath))) return false;
|
|
32
|
-
|
|
33
|
-
const raw: unknown = await readJson(pkgPath);
|
|
34
|
-
if (!raw || typeof raw !== "object") return false;
|
|
35
|
-
|
|
36
|
-
const pkg = raw as Record<string, unknown>;
|
|
37
|
-
const name = typeof pkg["name"] === "string" ? pkg["name"] : "";
|
|
38
|
-
const keywords = Array.isArray(pkg["keywords"]) ? pkg["keywords"] : [];
|
|
39
|
-
|
|
40
|
-
const nameMatches = ULTIMATE_TEMPLATE_MARKERS.some((m) => name.includes(m));
|
|
41
|
-
const keywordMatches = ULTIMATE_TEMPLATE_MARKERS.some((m) =>
|
|
42
|
-
keywords.includes(m)
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
return nameMatches || keywordMatches;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function runInit(options: {
|
|
49
|
-
cwd: string;
|
|
50
|
-
force: boolean;
|
|
51
|
-
}): Promise<void> {
|
|
52
|
-
const { cwd, force } = options;
|
|
53
|
-
|
|
54
|
-
p.intro("impulse init");
|
|
55
|
-
|
|
56
|
-
// Check if already initialized
|
|
57
|
-
if (!force && (await hasConfig(cwd))) {
|
|
58
|
-
p.log.warn(
|
|
59
|
-
".impulse.json already exists. Run with --force to reinitialize."
|
|
60
|
-
);
|
|
61
|
-
p.outro("Already initialized.");
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Validate project
|
|
66
|
-
const s = p.spinner();
|
|
67
|
-
s.start("Checking project...");
|
|
68
|
-
|
|
69
|
-
const pkgExists = await pathExists(path.join(cwd, "package.json"));
|
|
70
|
-
if (!pkgExists) {
|
|
71
|
-
s.stop("Not a Node.js project.");
|
|
72
|
-
p.cancel(
|
|
73
|
-
"No package.json found. Run impulse init from the root of your project."
|
|
74
|
-
);
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const isUT = await isUltimateTemplate(cwd);
|
|
79
|
-
s.stop(isUT ? "Ultimate Template project detected." : "Project detected.");
|
|
80
|
-
|
|
81
|
-
if (!isUT) {
|
|
82
|
-
const proceed = await p.confirm({
|
|
83
|
-
message:
|
|
84
|
-
"This project does not appear to be an ImpulseLab Ultimate Template. Continue anyway?",
|
|
85
|
-
initialValue: false,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
if (p.isCancel(proceed) || !proceed) {
|
|
89
|
-
p.cancel("Initialization cancelled.");
|
|
90
|
-
process.exit(0);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Detect project structure
|
|
95
|
-
const projectName = await detectProjectName(cwd);
|
|
96
|
-
|
|
97
|
-
const srcExists = await pathExists(path.join(cwd, "src"));
|
|
98
|
-
const srcPath = srcExists ? "src" : ".";
|
|
99
|
-
|
|
100
|
-
const config: ImpulseConfig = {
|
|
101
|
-
version: "1",
|
|
102
|
-
projectName,
|
|
103
|
-
srcPath,
|
|
104
|
-
dbPath: srcExists ? `${srcPath}/server/db` : "server/db",
|
|
105
|
-
routesPath: srcExists ? `${srcPath}/server/api` : "server/api",
|
|
106
|
-
installedModules: [],
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
await writeConfig(config, cwd);
|
|
110
|
-
|
|
111
|
-
p.outro(
|
|
112
|
-
`.impulse.json created for project "${projectName}". Run \`impulse list\` to see available modules.`
|
|
113
|
-
);
|
|
114
|
-
}
|
package/src/commands/list.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import * as p from "@clack/prompts";
|
|
2
|
-
import { readConfig } from "../config/index";
|
|
3
|
-
import { listAvailableModules } from "../registry/index";
|
|
4
|
-
|
|
5
|
-
export async function runList(options: {
|
|
6
|
-
cwd: string;
|
|
7
|
-
localPath?: string;
|
|
8
|
-
}): Promise<void> {
|
|
9
|
-
const { cwd, localPath } = options;
|
|
10
|
-
|
|
11
|
-
p.intro("impulse list");
|
|
12
|
-
|
|
13
|
-
const config = await readConfig(cwd);
|
|
14
|
-
const installedNames = new Set(
|
|
15
|
-
config?.installedModules.map((m) => m.name) ?? []
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const s = p.spinner();
|
|
19
|
-
s.start("Fetching available modules...");
|
|
20
|
-
|
|
21
|
-
let available: { name: string; description?: string; subModules?: string[] }[];
|
|
22
|
-
try {
|
|
23
|
-
available = await listAvailableModules(localPath);
|
|
24
|
-
} catch (err) {
|
|
25
|
-
s.stop("Failed to fetch module list.");
|
|
26
|
-
p.cancel(err instanceof Error ? err.message : String(err));
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
s.stop(`Found ${available.length} module(s).`);
|
|
31
|
-
|
|
32
|
-
if (available.length === 0) {
|
|
33
|
-
p.log.message("No modules available yet.");
|
|
34
|
-
p.outro("Done.");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
p.log.message("\nAvailable modules:\n");
|
|
39
|
-
|
|
40
|
-
for (const mod of available) {
|
|
41
|
-
const installed = installedNames.has(mod.name);
|
|
42
|
-
const installedInfo = installed
|
|
43
|
-
? config?.installedModules.find((m) => m.name === mod.name)
|
|
44
|
-
: null;
|
|
45
|
-
|
|
46
|
-
const status = installed
|
|
47
|
-
? `[installed v${installedInfo?.version ?? "?"}]`
|
|
48
|
-
: "[available]";
|
|
49
|
-
|
|
50
|
-
const desc = mod.description ? ` — ${mod.description}` : "";
|
|
51
|
-
p.log.message(` ${installed ? "✓" : "○"} ${mod.name} ${status}${desc}`);
|
|
52
|
-
|
|
53
|
-
// Show sub-modules nested under their parent
|
|
54
|
-
if (mod.subModules && mod.subModules.length > 0) {
|
|
55
|
-
const last = mod.subModules.length - 1;
|
|
56
|
-
mod.subModules.forEach((sub, i) => {
|
|
57
|
-
const subId = `${mod.name}/${sub}`;
|
|
58
|
-
const subInstalled = installedNames.has(subId);
|
|
59
|
-
const subInfo = subInstalled
|
|
60
|
-
? config?.installedModules.find((m) => m.name === subId)
|
|
61
|
-
: null;
|
|
62
|
-
const subStatus = subInstalled
|
|
63
|
-
? `[installed v${subInfo?.version ?? "?"}]`
|
|
64
|
-
: "[not installed]";
|
|
65
|
-
const connector = i === last ? "└─" : "├─";
|
|
66
|
-
p.log.message(` ${connector} ${sub} ${subStatus}`);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
p.log.message(
|
|
72
|
-
`\nRun \`impulse add <module>\` to install a module.`
|
|
73
|
-
);
|
|
74
|
-
p.log.message(
|
|
75
|
-
`Run \`impulse add <parent>/<sub>\` or \`impulse add <parent> --with <sub1>,<sub2>\` for sub-modules.`
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
p.outro("Done.");
|
|
79
|
-
}
|
package/src/config/has-config.ts
DELETED
package/src/config/index.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { readJson, pathExists } = fsExtra;
|
|
3
|
-
import { ImpulseConfigSchema } from "../schemas/impulse-config";
|
|
4
|
-
import type { ImpulseConfig } from "../schemas/impulse-config";
|
|
5
|
-
import { configPath } from "./config-path";
|
|
6
|
-
|
|
7
|
-
export async function readConfig(
|
|
8
|
-
cwd: string = process.cwd()
|
|
9
|
-
): Promise<ImpulseConfig | null> {
|
|
10
|
-
const file = configPath(cwd);
|
|
11
|
-
if (!(await pathExists(file))) return null;
|
|
12
|
-
const raw = await readJson(file);
|
|
13
|
-
const parsed = ImpulseConfigSchema.safeParse(raw);
|
|
14
|
-
if (!parsed.success) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
`Invalid .impulse.json: ${parsed.error.message}`
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
return parsed.data;
|
|
20
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { writeJson } = fsExtra;
|
|
3
|
-
import type { ImpulseConfig } from "../schemas/impulse-config";
|
|
4
|
-
import { configPath } from "./config-path";
|
|
5
|
-
|
|
6
|
-
export async function writeConfig(
|
|
7
|
-
config: ImpulseConfig,
|
|
8
|
-
cwd: string = process.cwd()
|
|
9
|
-
): Promise<void> {
|
|
10
|
-
await writeJson(configPath(cwd), config, { spaces: 2 });
|
|
11
|
-
}
|
package/src/config.test.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import os from "os";
|
|
5
|
-
import { readConfig, writeConfig, hasConfig } from "./config/index";
|
|
6
|
-
import type { ImpulseConfig } from "./types";
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
tmpDir = await mkdtemp(path.join(os.tmpdir(), "impulse-config-test-"));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const sampleConfig: ImpulseConfig = {
|
|
19
|
-
version: "1",
|
|
20
|
-
projectName: "my-project",
|
|
21
|
-
srcPath: "src",
|
|
22
|
-
dbPath: "src/server/db",
|
|
23
|
-
routesPath: "src/server/api",
|
|
24
|
-
installedModules: [],
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
describe("config", () => {
|
|
28
|
-
it("returns null if no config file", async () => {
|
|
29
|
-
expect(await readConfig(tmpDir)).toBeNull();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("writes and reads config", async () => {
|
|
33
|
-
await writeConfig(sampleConfig, tmpDir);
|
|
34
|
-
const config = await readConfig(tmpDir);
|
|
35
|
-
expect(config).toEqual(sampleConfig);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("hasConfig returns false before init", async () => {
|
|
39
|
-
expect(await hasConfig(tmpDir)).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("hasConfig returns true after write", async () => {
|
|
43
|
-
await writeConfig(sampleConfig, tmpDir);
|
|
44
|
-
expect(await hasConfig(tmpDir)).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("tracks installed modules", async () => {
|
|
48
|
-
const config: ImpulseConfig = {
|
|
49
|
-
...sampleConfig,
|
|
50
|
-
installedModules: [
|
|
51
|
-
{
|
|
52
|
-
name: "auth",
|
|
53
|
-
version: "1.0.0",
|
|
54
|
-
installedAt: "2026-01-01T00:00:00.000Z",
|
|
55
|
-
files: ["src/server/api/auth.ts"],
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
};
|
|
59
|
-
await writeConfig(config, tmpDir);
|
|
60
|
-
const read = await readConfig(tmpDir);
|
|
61
|
-
expect(read?.installedModules).toHaveLength(1);
|
|
62
|
-
expect(read?.installedModules[0]?.name).toBe("auth");
|
|
63
|
-
});
|
|
64
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { runInit } from "./commands/init";
|
|
4
|
-
import { runAdd } from "./commands/add";
|
|
5
|
-
import { runList } from "./commands/list";
|
|
6
|
-
|
|
7
|
-
const program = new Command();
|
|
8
|
-
|
|
9
|
-
program
|
|
10
|
-
.name("impulse")
|
|
11
|
-
.description("ImpulseLab CLI — install and manage modules for your projects")
|
|
12
|
-
.version("0.1.0");
|
|
13
|
-
|
|
14
|
-
program
|
|
15
|
-
.command("init")
|
|
16
|
-
.description("Initialize impulse in the current project")
|
|
17
|
-
.option("--force", "Reinitialize even if .impulse.json already exists", false)
|
|
18
|
-
.action(async (options: { force: boolean }) => {
|
|
19
|
-
await runInit({ cwd: process.cwd(), force: options.force });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
program
|
|
23
|
-
.command("add <module>")
|
|
24
|
-
.description(
|
|
25
|
-
"Add a module to the current project.\n" +
|
|
26
|
-
" Supports sub-module syntax: `add attio/quote-to-cash`\n" +
|
|
27
|
-
" Use --with to install multiple sub-modules at once: `add attio --with quote-to-cash,gocardless`"
|
|
28
|
-
)
|
|
29
|
-
.option("--dry-run", "Preview changes without writing files", false)
|
|
30
|
-
.option(
|
|
31
|
-
"--local <path>",
|
|
32
|
-
"Use a local modules directory (for development)"
|
|
33
|
-
)
|
|
34
|
-
.option(
|
|
35
|
-
"--with <submodules>",
|
|
36
|
-
"Comma-separated sub-modules to install alongside the parent (e.g. --with quote-to-cash,gocardless)"
|
|
37
|
-
)
|
|
38
|
-
.action(async (moduleName: string, options: { dryRun: boolean; local?: string; with?: string }) => {
|
|
39
|
-
const addOpts: Parameters<typeof runAdd>[0] = {
|
|
40
|
-
moduleName,
|
|
41
|
-
cwd: process.cwd(),
|
|
42
|
-
dryRun: options.dryRun,
|
|
43
|
-
};
|
|
44
|
-
if (options.local !== undefined) addOpts.localPath = options.local;
|
|
45
|
-
if (options.with !== undefined) {
|
|
46
|
-
addOpts.withSubModules = options.with.split(",").map((s) => s.trim()).filter(Boolean);
|
|
47
|
-
}
|
|
48
|
-
await runAdd(addOpts);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
program
|
|
52
|
-
.command("list")
|
|
53
|
-
.description("List available and installed modules")
|
|
54
|
-
.option(
|
|
55
|
-
"--local <path>",
|
|
56
|
-
"Use a local modules directory (for development)"
|
|
57
|
-
)
|
|
58
|
-
.action(async (options: { local?: string }) => {
|
|
59
|
-
const listOpts: Parameters<typeof runList>[0] = { cwd: process.cwd() };
|
|
60
|
-
if (options.local !== undefined) listOpts.localPath = options.local;
|
|
61
|
-
await runList(listOpts);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
program.parse(process.argv);
|
package/src/installer.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { outputFile, pathExists } = fsExtra;
|
|
3
|
-
import path from "path";
|
|
4
|
-
import * as p from "@clack/prompts";
|
|
5
|
-
import { fetchModuleFile } from "./registry/index";
|
|
6
|
-
import type { ModuleFile } from "./types";
|
|
7
|
-
|
|
8
|
-
export interface InstallFilesOptions {
|
|
9
|
-
moduleName: string;
|
|
10
|
-
files: ModuleFile[];
|
|
11
|
-
cwd: string;
|
|
12
|
-
dryRun: boolean;
|
|
13
|
-
localPath?: string | undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface InstalledFile {
|
|
17
|
-
dest: string;
|
|
18
|
-
action: "created" | "overwritten" | "skipped" | "would-create" | "would-overwrite";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function installFiles(
|
|
22
|
-
options: InstallFilesOptions
|
|
23
|
-
): Promise<InstalledFile[]> {
|
|
24
|
-
const { moduleName, files, cwd, dryRun, localPath } = options;
|
|
25
|
-
const results: InstalledFile[] = [];
|
|
26
|
-
|
|
27
|
-
for (const file of files) {
|
|
28
|
-
const destAbs = path.join(cwd, file.dest);
|
|
29
|
-
const exists = await pathExists(destAbs);
|
|
30
|
-
|
|
31
|
-
if (exists) {
|
|
32
|
-
const content = await fetchModuleFile(moduleName, file.src, localPath);
|
|
33
|
-
const existing = await import("fs/promises").then((fs) =>
|
|
34
|
-
fs.readFile(destAbs, "utf-8")
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
if (existing === content) {
|
|
38
|
-
results.push({ dest: file.dest, action: "skipped" });
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (dryRun) {
|
|
43
|
-
results.push({ dest: file.dest, action: "would-overwrite" });
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const answer = await p.confirm({
|
|
48
|
-
message: `File already exists: ${file.dest} — overwrite?`,
|
|
49
|
-
initialValue: false,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (p.isCancel(answer) || !answer) {
|
|
53
|
-
results.push({ dest: file.dest, action: "skipped" });
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
await outputFile(destAbs, content, "utf-8");
|
|
58
|
-
results.push({ dest: file.dest, action: "overwritten" });
|
|
59
|
-
} else {
|
|
60
|
-
if (dryRun) {
|
|
61
|
-
results.push({ dest: file.dest, action: "would-create" });
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const content = await fetchModuleFile(moduleName, file.src, localPath);
|
|
65
|
-
await outputFile(destAbs, content, "utf-8");
|
|
66
|
-
results.push({ dest: file.dest, action: "created" });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return results;
|
|
71
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { githubUrls } from "./github-urls";
|
|
2
|
-
|
|
3
|
-
export async function fetchModuleFile(
|
|
4
|
-
moduleName: string,
|
|
5
|
-
fileSrc: string,
|
|
6
|
-
localPath?: string
|
|
7
|
-
): Promise<string> {
|
|
8
|
-
if (localPath) {
|
|
9
|
-
const { readFile } = await import("fs/promises");
|
|
10
|
-
const { join } = await import("path");
|
|
11
|
-
const file = join(localPath, moduleName, fileSrc);
|
|
12
|
-
return readFile(file, "utf-8");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const url = githubUrls.rawFile(moduleName, fileSrc);
|
|
16
|
-
const res = await fetch(url);
|
|
17
|
-
if (!res.ok) {
|
|
18
|
-
throw new Error(`Failed to fetch file ${fileSrc} for module ${moduleName}: ${res.status}`);
|
|
19
|
-
}
|
|
20
|
-
return res.text();
|
|
21
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { readJson, pathExists } = fsExtra;
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { ModuleManifestSchema } from "../schemas/module-manifest";
|
|
5
|
-
import type { ModuleManifest } from "../schemas/module-manifest";
|
|
6
|
-
import { githubUrls } from "./github-urls";
|
|
7
|
-
import { moduleRegistryPath } from "./parse-module-id";
|
|
8
|
-
|
|
9
|
-
export async function fetchModuleManifest(
|
|
10
|
-
moduleId: string,
|
|
11
|
-
localPath?: string
|
|
12
|
-
): Promise<ModuleManifest> {
|
|
13
|
-
const registryPath = moduleRegistryPath(moduleId);
|
|
14
|
-
|
|
15
|
-
if (localPath) {
|
|
16
|
-
const file = path.join(localPath, registryPath, "module.json");
|
|
17
|
-
if (!(await pathExists(file))) {
|
|
18
|
-
throw new Error(`Local module not found: ${file}`);
|
|
19
|
-
}
|
|
20
|
-
const raw = await readJson(file);
|
|
21
|
-
const parsed = ModuleManifestSchema.safeParse(raw);
|
|
22
|
-
if (!parsed.success) {
|
|
23
|
-
throw new Error(`Invalid module.json for ${moduleId}: ${parsed.error.message}`);
|
|
24
|
-
}
|
|
25
|
-
return parsed.data;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const url = githubUrls.rawFile(registryPath, "module.json");
|
|
29
|
-
const res = await fetch(url);
|
|
30
|
-
if (!res.ok) {
|
|
31
|
-
if (res.status === 404) {
|
|
32
|
-
throw new Error(`Module not found in registry: ${moduleId}`);
|
|
33
|
-
}
|
|
34
|
-
throw new Error(`Failed to fetch module manifest: ${res.status} ${res.statusText}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const raw: unknown = await res.json();
|
|
38
|
-
const parsed = ModuleManifestSchema.safeParse(raw);
|
|
39
|
-
if (!parsed.success) {
|
|
40
|
-
throw new Error(`Invalid module.json for ${moduleId}: ${parsed.error.message}`);
|
|
41
|
-
}
|
|
42
|
-
return parsed.data;
|
|
43
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const GITHUB_ORG = "impulse-studio";
|
|
2
|
-
const GITHUB_REPO = "impulse-modules";
|
|
3
|
-
const GITHUB_BRANCH = "main";
|
|
4
|
-
const MODULES_DIR = "modules";
|
|
5
|
-
|
|
6
|
-
export const githubUrls = {
|
|
7
|
-
rawFile: (registryPath: string, file: string): string =>
|
|
8
|
-
`https://raw.githubusercontent.com/${GITHUB_ORG}/${GITHUB_REPO}/${GITHUB_BRANCH}/${MODULES_DIR}/${registryPath}/${file}`,
|
|
9
|
-
moduleList: (): string =>
|
|
10
|
-
`https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/contents/${MODULES_DIR}`,
|
|
11
|
-
subModulesList: (parentModule: string): string =>
|
|
12
|
-
`https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/contents/${MODULES_DIR}/${parentModule}/sub-modules`,
|
|
13
|
-
};
|
package/src/registry/index.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { fetchModuleManifest } from "./fetch-module-manifest";
|
|
2
|
-
export { listAvailableModules } from "./list-available-modules";
|
|
3
|
-
export type { RegistryModule } from "./list-available-modules";
|
|
4
|
-
export { fetchModuleFile } from "./fetch-module-file";
|
|
5
|
-
export { parseModuleId, isSubModuleId, moduleRegistryPath } from "./parse-module-id";
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import fsExtra from "fs-extra";
|
|
2
|
-
const { readJson, pathExists } = fsExtra;
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { ModuleManifestSchema } from "../schemas/module-manifest";
|
|
5
|
-
import { githubUrls } from "./github-urls";
|
|
6
|
-
|
|
7
|
-
export interface RegistryModule {
|
|
8
|
-
name: string;
|
|
9
|
-
description?: string;
|
|
10
|
-
subModules?: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let _cachedModuleList: RegistryModule[] | null = null;
|
|
14
|
-
|
|
15
|
-
export async function listAvailableModules(
|
|
16
|
-
localPath?: string
|
|
17
|
-
): Promise<RegistryModule[]> {
|
|
18
|
-
if (localPath) {
|
|
19
|
-
const { readdir } = await import("fs/promises");
|
|
20
|
-
const entries = await readdir(localPath, { withFileTypes: true });
|
|
21
|
-
const result: RegistryModule[] = [];
|
|
22
|
-
for (const entry of entries) {
|
|
23
|
-
if (!entry.isDirectory()) continue;
|
|
24
|
-
const manifestFile = path.join(localPath, entry.name, "module.json");
|
|
25
|
-
if (!(await pathExists(manifestFile))) continue;
|
|
26
|
-
try {
|
|
27
|
-
const raw = await readJson(manifestFile);
|
|
28
|
-
const parsed = ModuleManifestSchema.safeParse(raw);
|
|
29
|
-
if (!parsed.success) continue;
|
|
30
|
-
|
|
31
|
-
// Discover sub-modules from the sub-modules/ subdirectory
|
|
32
|
-
let subModules: string[] | undefined;
|
|
33
|
-
const subModulesDir = path.join(localPath, entry.name, "sub-modules");
|
|
34
|
-
if (await pathExists(subModulesDir)) {
|
|
35
|
-
const subEntries = await readdir(subModulesDir, { withFileTypes: true });
|
|
36
|
-
subModules = subEntries
|
|
37
|
-
.filter((e) => e.isDirectory())
|
|
38
|
-
.map((e) => e.name);
|
|
39
|
-
if (subModules.length === 0) subModules = undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
result.push({
|
|
43
|
-
name: parsed.data.name,
|
|
44
|
-
description: parsed.data.description,
|
|
45
|
-
...(subModules ? { subModules } : {}),
|
|
46
|
-
});
|
|
47
|
-
} catch {
|
|
48
|
-
// skip invalid modules
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (_cachedModuleList) return _cachedModuleList;
|
|
55
|
-
|
|
56
|
-
const res = await fetch(githubUrls.moduleList(), {
|
|
57
|
-
headers: { Accept: "application/vnd.github.v3+json" },
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (!res.ok) {
|
|
61
|
-
throw new Error(`Failed to fetch module list: ${res.status} ${res.statusText}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const entries: unknown = await res.json();
|
|
65
|
-
if (!Array.isArray(entries)) {
|
|
66
|
-
throw new Error("Unexpected response from GitHub API");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const baseModules: RegistryModule[] = [];
|
|
70
|
-
for (const entry of entries) {
|
|
71
|
-
if (
|
|
72
|
-
typeof entry === "object" &&
|
|
73
|
-
entry !== null &&
|
|
74
|
-
"type" in entry &&
|
|
75
|
-
entry.type === "dir" &&
|
|
76
|
-
"name" in entry &&
|
|
77
|
-
typeof entry.name === "string"
|
|
78
|
-
) {
|
|
79
|
-
baseModules.push({ name: entry.name });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Fetch sub-modules for each module in parallel (404 = no sub-modules dir, safe to ignore)
|
|
84
|
-
const modules = await Promise.all(
|
|
85
|
-
baseModules.map(async (mod) => {
|
|
86
|
-
try {
|
|
87
|
-
const subRes = await fetch(githubUrls.subModulesList(mod.name), {
|
|
88
|
-
headers: { Accept: "application/vnd.github.v3+json" },
|
|
89
|
-
});
|
|
90
|
-
if (!subRes.ok) return mod;
|
|
91
|
-
const subEntries: unknown = await subRes.json();
|
|
92
|
-
if (!Array.isArray(subEntries)) return mod;
|
|
93
|
-
const subModules = subEntries
|
|
94
|
-
.filter(
|
|
95
|
-
(e): e is { type: string; name: string } =>
|
|
96
|
-
typeof e === "object" &&
|
|
97
|
-
e !== null &&
|
|
98
|
-
"type" in e &&
|
|
99
|
-
(e as { type: unknown }).type === "dir" &&
|
|
100
|
-
"name" in e &&
|
|
101
|
-
typeof (e as { name: unknown }).name === "string",
|
|
102
|
-
)
|
|
103
|
-
.map((e) => e.name);
|
|
104
|
-
return subModules.length > 0 ? { ...mod, subModules } : mod;
|
|
105
|
-
} catch {
|
|
106
|
-
return mod;
|
|
107
|
-
}
|
|
108
|
-
}),
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
_cachedModuleList = modules;
|
|
112
|
-
return modules;
|
|
113
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parses a module identifier which may use slash notation for sub-modules.
|
|
3
|
-
* "attio" → { parent: "attio", child: null }
|
|
4
|
-
* "attio/quote-to-cash" → { parent: "attio", child: "quote-to-cash" }
|
|
5
|
-
*/
|
|
6
|
-
export function parseModuleId(moduleId: string): {
|
|
7
|
-
parent: string;
|
|
8
|
-
child: string | null;
|
|
9
|
-
} {
|
|
10
|
-
const slashIndex = moduleId.indexOf("/");
|
|
11
|
-
if (slashIndex === -1) return { parent: moduleId, child: null };
|
|
12
|
-
return {
|
|
13
|
-
parent: moduleId.slice(0, slashIndex),
|
|
14
|
-
child: moduleId.slice(slashIndex + 1),
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Returns true when moduleId uses the sub-module slash syntax.
|
|
20
|
-
*/
|
|
21
|
-
export function isSubModuleId(moduleId: string): boolean {
|
|
22
|
-
return moduleId.includes("/");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Resolve module identifier to the path prefix used inside the modules/ directory. */
|
|
26
|
-
export function moduleRegistryPath(moduleId: string): string {
|
|
27
|
-
const { parent, child } = parseModuleId(moduleId);
|
|
28
|
-
if (child === null) return parent;
|
|
29
|
-
return `${parent}/sub-modules/${child}`;
|
|
30
|
-
}
|