@impulselab/cli 0.1.0
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.d.ts +1 -0
- package/dist/index.js +891 -0
- package/package.json +33 -0
- package/src/commands/add.test.ts +147 -0
- package/src/commands/add.ts +335 -0
- package/src/commands/init.ts +114 -0
- package/src/commands/list.ts +79 -0
- package/src/config/config-path.ts +7 -0
- package/src/config/has-config.ts +9 -0
- package/src/config/index.ts +4 -0
- package/src/config/read-config.ts +20 -0
- package/src/config/write-config.ts +11 -0
- package/src/config.test.ts +64 -0
- package/src/index.ts +64 -0
- package/src/installer.ts +71 -0
- package/src/registry/fetch-module-file.ts +21 -0
- package/src/registry/fetch-module-manifest.ts +43 -0
- package/src/registry/github-urls.ts +13 -0
- package/src/registry/index.ts +5 -0
- package/src/registry/list-available-modules.ts +113 -0
- package/src/registry/parse-module-id.ts +30 -0
- package/src/registry/registry.test.ts +181 -0
- package/src/schemas/impulse-config.ts +21 -0
- package/src/schemas/index.ts +9 -0
- package/src/schemas/module-dependency.ts +3 -0
- package/src/schemas/module-file.ts +8 -0
- package/src/schemas/module-manifest.ts +23 -0
- package/src/schemas/module-transform.ts +15 -0
- package/src/transforms/add-env.ts +53 -0
- package/src/transforms/add-nav-item.test.ts +125 -0
- package/src/transforms/add-nav-item.ts +70 -0
- package/src/transforms/append-export.test.ts +50 -0
- package/src/transforms/append-export.ts +34 -0
- package/src/transforms/index.ts +32 -0
- package/src/transforms/merge-schema.test.ts +70 -0
- package/src/transforms/merge-schema.ts +35 -0
- package/src/transforms/register-route.test.ts +177 -0
- package/src/transforms/register-route.ts +47 -0
- package/src/types.ts +9 -0
- package/tsconfig.json +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import fsExtra4 from "fs-extra";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
import * as p from "@clack/prompts";
|
|
10
|
+
|
|
11
|
+
// src/config/config-path.ts
|
|
12
|
+
import path from "path";
|
|
13
|
+
var CONFIG_FILE = ".impulse.json";
|
|
14
|
+
function configPath(cwd = process.cwd()) {
|
|
15
|
+
return path.join(cwd, CONFIG_FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/config/read-config.ts
|
|
19
|
+
import fsExtra from "fs-extra";
|
|
20
|
+
|
|
21
|
+
// src/schemas/impulse-config.ts
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
var ImpulseConfigSchema = z.object({
|
|
24
|
+
version: z.string().default("1"),
|
|
25
|
+
projectName: z.string(),
|
|
26
|
+
srcPath: z.string().default("src"),
|
|
27
|
+
dbPath: z.string().default("src/server/db"),
|
|
28
|
+
routesPath: z.string().default("src/server/api"),
|
|
29
|
+
installedModules: z.array(
|
|
30
|
+
z.object({
|
|
31
|
+
name: z.string(),
|
|
32
|
+
version: z.string(),
|
|
33
|
+
installedAt: z.string(),
|
|
34
|
+
files: z.array(z.string())
|
|
35
|
+
})
|
|
36
|
+
).default([])
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// src/config/read-config.ts
|
|
40
|
+
var { readJson, pathExists } = fsExtra;
|
|
41
|
+
async function readConfig(cwd = process.cwd()) {
|
|
42
|
+
const file = configPath(cwd);
|
|
43
|
+
if (!await pathExists(file)) return null;
|
|
44
|
+
const raw = await readJson(file);
|
|
45
|
+
const parsed = ImpulseConfigSchema.safeParse(raw);
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Invalid .impulse.json: ${parsed.error.message}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return parsed.data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/config/write-config.ts
|
|
55
|
+
import fsExtra2 from "fs-extra";
|
|
56
|
+
var { writeJson } = fsExtra2;
|
|
57
|
+
async function writeConfig(config, cwd = process.cwd()) {
|
|
58
|
+
await writeJson(configPath(cwd), config, { spaces: 2 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/config/has-config.ts
|
|
62
|
+
import fsExtra3 from "fs-extra";
|
|
63
|
+
var { pathExists: pathExists2 } = fsExtra3;
|
|
64
|
+
async function hasConfig(cwd = process.cwd()) {
|
|
65
|
+
return pathExists2(configPath(cwd));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/commands/init.ts
|
|
69
|
+
var { readJson: readJson2, pathExists: pathExists3 } = fsExtra4;
|
|
70
|
+
var ULTIMATE_TEMPLATE_MARKERS = [
|
|
71
|
+
"@impulselab/ultimate-template",
|
|
72
|
+
"ultimate-template"
|
|
73
|
+
];
|
|
74
|
+
async function detectProjectName(cwd) {
|
|
75
|
+
const pkgPath = path2.join(cwd, "package.json");
|
|
76
|
+
if (await pathExists3(pkgPath)) {
|
|
77
|
+
const pkg = await readJson2(pkgPath);
|
|
78
|
+
if (pkg !== null && typeof pkg === "object" && "name" in pkg && typeof pkg.name === "string") {
|
|
79
|
+
return pkg.name;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return path2.basename(cwd);
|
|
83
|
+
}
|
|
84
|
+
async function isUltimateTemplate(cwd) {
|
|
85
|
+
const pkgPath = path2.join(cwd, "package.json");
|
|
86
|
+
if (!await pathExists3(pkgPath)) return false;
|
|
87
|
+
const raw = await readJson2(pkgPath);
|
|
88
|
+
if (!raw || typeof raw !== "object") return false;
|
|
89
|
+
const pkg = raw;
|
|
90
|
+
const name = typeof pkg["name"] === "string" ? pkg["name"] : "";
|
|
91
|
+
const keywords = Array.isArray(pkg["keywords"]) ? pkg["keywords"] : [];
|
|
92
|
+
const nameMatches = ULTIMATE_TEMPLATE_MARKERS.some((m) => name.includes(m));
|
|
93
|
+
const keywordMatches = ULTIMATE_TEMPLATE_MARKERS.some(
|
|
94
|
+
(m) => keywords.includes(m)
|
|
95
|
+
);
|
|
96
|
+
return nameMatches || keywordMatches;
|
|
97
|
+
}
|
|
98
|
+
async function runInit(options) {
|
|
99
|
+
const { cwd, force } = options;
|
|
100
|
+
p.intro("impulse init");
|
|
101
|
+
if (!force && await hasConfig(cwd)) {
|
|
102
|
+
p.log.warn(
|
|
103
|
+
".impulse.json already exists. Run with --force to reinitialize."
|
|
104
|
+
);
|
|
105
|
+
p.outro("Already initialized.");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const s = p.spinner();
|
|
109
|
+
s.start("Checking project...");
|
|
110
|
+
const pkgExists = await pathExists3(path2.join(cwd, "package.json"));
|
|
111
|
+
if (!pkgExists) {
|
|
112
|
+
s.stop("Not a Node.js project.");
|
|
113
|
+
p.cancel(
|
|
114
|
+
"No package.json found. Run impulse init from the root of your project."
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const isUT = await isUltimateTemplate(cwd);
|
|
119
|
+
s.stop(isUT ? "Ultimate Template project detected." : "Project detected.");
|
|
120
|
+
if (!isUT) {
|
|
121
|
+
const proceed = await p.confirm({
|
|
122
|
+
message: "This project does not appear to be an ImpulseLab Ultimate Template. Continue anyway?",
|
|
123
|
+
initialValue: false
|
|
124
|
+
});
|
|
125
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
126
|
+
p.cancel("Initialization cancelled.");
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const projectName = await detectProjectName(cwd);
|
|
131
|
+
const srcExists = await pathExists3(path2.join(cwd, "src"));
|
|
132
|
+
const srcPath = srcExists ? "src" : ".";
|
|
133
|
+
const config = {
|
|
134
|
+
version: "1",
|
|
135
|
+
projectName,
|
|
136
|
+
srcPath,
|
|
137
|
+
dbPath: srcExists ? `${srcPath}/server/db` : "server/db",
|
|
138
|
+
routesPath: srcExists ? `${srcPath}/server/api` : "server/api",
|
|
139
|
+
installedModules: []
|
|
140
|
+
};
|
|
141
|
+
await writeConfig(config, cwd);
|
|
142
|
+
p.outro(
|
|
143
|
+
`.impulse.json created for project "${projectName}". Run \`impulse list\` to see available modules.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/commands/add.ts
|
|
148
|
+
import { execSync, execFileSync } from "child_process";
|
|
149
|
+
import { existsSync } from "fs";
|
|
150
|
+
import path11 from "path";
|
|
151
|
+
import * as p4 from "@clack/prompts";
|
|
152
|
+
|
|
153
|
+
// src/registry/fetch-module-manifest.ts
|
|
154
|
+
import fsExtra5 from "fs-extra";
|
|
155
|
+
import path3 from "path";
|
|
156
|
+
|
|
157
|
+
// src/schemas/module-manifest.ts
|
|
158
|
+
import { z as z5 } from "zod";
|
|
159
|
+
|
|
160
|
+
// src/schemas/module-file.ts
|
|
161
|
+
import { z as z2 } from "zod";
|
|
162
|
+
var ModuleFileSchema = z2.object({
|
|
163
|
+
src: z2.string(),
|
|
164
|
+
dest: z2.string()
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// src/schemas/module-dependency.ts
|
|
168
|
+
import { z as z3 } from "zod";
|
|
169
|
+
var ModuleDependencySchema = z3.string();
|
|
170
|
+
|
|
171
|
+
// src/schemas/module-transform.ts
|
|
172
|
+
import { z as z4 } from "zod";
|
|
173
|
+
var ModuleTransformSchema = z4.object({
|
|
174
|
+
type: z4.enum([
|
|
175
|
+
"append-export",
|
|
176
|
+
"register-route",
|
|
177
|
+
"add-nav-item",
|
|
178
|
+
"merge-schema",
|
|
179
|
+
"add-env"
|
|
180
|
+
]),
|
|
181
|
+
target: z4.string(),
|
|
182
|
+
value: z4.string()
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// src/schemas/module-manifest.ts
|
|
186
|
+
var ModuleManifestSchema = z5.object({
|
|
187
|
+
name: z5.string(),
|
|
188
|
+
version: z5.string(),
|
|
189
|
+
description: z5.string(),
|
|
190
|
+
/** If set, this module is a sub-module of the named parent (e.g. "attio"). */
|
|
191
|
+
parentModule: z5.string().optional(),
|
|
192
|
+
/** Names of sub-modules available under this parent module (e.g. ["quote-to-cash", "gocardless"]). */
|
|
193
|
+
subModules: z5.array(z5.string()).default([]),
|
|
194
|
+
dependencies: z5.array(ModuleDependencySchema).default([]),
|
|
195
|
+
moduleDependencies: z5.array(z5.string()).default([]),
|
|
196
|
+
files: z5.array(ModuleFileSchema).default([]),
|
|
197
|
+
transforms: z5.array(ModuleTransformSchema).default([]),
|
|
198
|
+
/** Documentation metadata listing env vars this module requires (displayed in install summary). */
|
|
199
|
+
envVars: z5.array(z5.string()).default([]),
|
|
200
|
+
postInstall: z5.array(z5.string()).optional()
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// src/registry/github-urls.ts
|
|
204
|
+
var GITHUB_ORG = "impulse-studio";
|
|
205
|
+
var GITHUB_REPO = "impulse-modules";
|
|
206
|
+
var GITHUB_BRANCH = "main";
|
|
207
|
+
var MODULES_DIR = "modules";
|
|
208
|
+
var githubUrls = {
|
|
209
|
+
rawFile: (registryPath, file) => `https://raw.githubusercontent.com/${GITHUB_ORG}/${GITHUB_REPO}/${GITHUB_BRANCH}/${MODULES_DIR}/${registryPath}/${file}`,
|
|
210
|
+
moduleList: () => `https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/contents/${MODULES_DIR}`,
|
|
211
|
+
subModulesList: (parentModule) => `https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/contents/${MODULES_DIR}/${parentModule}/sub-modules`
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/registry/parse-module-id.ts
|
|
215
|
+
function parseModuleId(moduleId) {
|
|
216
|
+
const slashIndex = moduleId.indexOf("/");
|
|
217
|
+
if (slashIndex === -1) return { parent: moduleId, child: null };
|
|
218
|
+
return {
|
|
219
|
+
parent: moduleId.slice(0, slashIndex),
|
|
220
|
+
child: moduleId.slice(slashIndex + 1)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function moduleRegistryPath(moduleId) {
|
|
224
|
+
const { parent, child } = parseModuleId(moduleId);
|
|
225
|
+
if (child === null) return parent;
|
|
226
|
+
return `${parent}/sub-modules/${child}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/registry/fetch-module-manifest.ts
|
|
230
|
+
var { readJson: readJson3, pathExists: pathExists4 } = fsExtra5;
|
|
231
|
+
async function fetchModuleManifest(moduleId, localPath) {
|
|
232
|
+
const registryPath = moduleRegistryPath(moduleId);
|
|
233
|
+
if (localPath) {
|
|
234
|
+
const file = path3.join(localPath, registryPath, "module.json");
|
|
235
|
+
if (!await pathExists4(file)) {
|
|
236
|
+
throw new Error(`Local module not found: ${file}`);
|
|
237
|
+
}
|
|
238
|
+
const raw2 = await readJson3(file);
|
|
239
|
+
const parsed2 = ModuleManifestSchema.safeParse(raw2);
|
|
240
|
+
if (!parsed2.success) {
|
|
241
|
+
throw new Error(`Invalid module.json for ${moduleId}: ${parsed2.error.message}`);
|
|
242
|
+
}
|
|
243
|
+
return parsed2.data;
|
|
244
|
+
}
|
|
245
|
+
const url = githubUrls.rawFile(registryPath, "module.json");
|
|
246
|
+
const res = await fetch(url);
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
if (res.status === 404) {
|
|
249
|
+
throw new Error(`Module not found in registry: ${moduleId}`);
|
|
250
|
+
}
|
|
251
|
+
throw new Error(`Failed to fetch module manifest: ${res.status} ${res.statusText}`);
|
|
252
|
+
}
|
|
253
|
+
const raw = await res.json();
|
|
254
|
+
const parsed = ModuleManifestSchema.safeParse(raw);
|
|
255
|
+
if (!parsed.success) {
|
|
256
|
+
throw new Error(`Invalid module.json for ${moduleId}: ${parsed.error.message}`);
|
|
257
|
+
}
|
|
258
|
+
return parsed.data;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/registry/list-available-modules.ts
|
|
262
|
+
import fsExtra6 from "fs-extra";
|
|
263
|
+
import path4 from "path";
|
|
264
|
+
var { readJson: readJson4, pathExists: pathExists5 } = fsExtra6;
|
|
265
|
+
var _cachedModuleList = null;
|
|
266
|
+
async function listAvailableModules(localPath) {
|
|
267
|
+
if (localPath) {
|
|
268
|
+
const { readdir } = await import("fs/promises");
|
|
269
|
+
const entries2 = await readdir(localPath, { withFileTypes: true });
|
|
270
|
+
const result = [];
|
|
271
|
+
for (const entry of entries2) {
|
|
272
|
+
if (!entry.isDirectory()) continue;
|
|
273
|
+
const manifestFile = path4.join(localPath, entry.name, "module.json");
|
|
274
|
+
if (!await pathExists5(manifestFile)) continue;
|
|
275
|
+
try {
|
|
276
|
+
const raw = await readJson4(manifestFile);
|
|
277
|
+
const parsed = ModuleManifestSchema.safeParse(raw);
|
|
278
|
+
if (!parsed.success) continue;
|
|
279
|
+
let subModules;
|
|
280
|
+
const subModulesDir = path4.join(localPath, entry.name, "sub-modules");
|
|
281
|
+
if (await pathExists5(subModulesDir)) {
|
|
282
|
+
const subEntries = await readdir(subModulesDir, { withFileTypes: true });
|
|
283
|
+
subModules = subEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
284
|
+
if (subModules.length === 0) subModules = void 0;
|
|
285
|
+
}
|
|
286
|
+
result.push({
|
|
287
|
+
name: parsed.data.name,
|
|
288
|
+
description: parsed.data.description,
|
|
289
|
+
...subModules ? { subModules } : {}
|
|
290
|
+
});
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
if (_cachedModuleList) return _cachedModuleList;
|
|
297
|
+
const res = await fetch(githubUrls.moduleList(), {
|
|
298
|
+
headers: { Accept: "application/vnd.github.v3+json" }
|
|
299
|
+
});
|
|
300
|
+
if (!res.ok) {
|
|
301
|
+
throw new Error(`Failed to fetch module list: ${res.status} ${res.statusText}`);
|
|
302
|
+
}
|
|
303
|
+
const entries = await res.json();
|
|
304
|
+
if (!Array.isArray(entries)) {
|
|
305
|
+
throw new Error("Unexpected response from GitHub API");
|
|
306
|
+
}
|
|
307
|
+
const baseModules = [];
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (typeof entry === "object" && entry !== null && "type" in entry && entry.type === "dir" && "name" in entry && typeof entry.name === "string") {
|
|
310
|
+
baseModules.push({ name: entry.name });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const modules = await Promise.all(
|
|
314
|
+
baseModules.map(async (mod) => {
|
|
315
|
+
try {
|
|
316
|
+
const subRes = await fetch(githubUrls.subModulesList(mod.name), {
|
|
317
|
+
headers: { Accept: "application/vnd.github.v3+json" }
|
|
318
|
+
});
|
|
319
|
+
if (!subRes.ok) return mod;
|
|
320
|
+
const subEntries = await subRes.json();
|
|
321
|
+
if (!Array.isArray(subEntries)) return mod;
|
|
322
|
+
const subModules = subEntries.filter(
|
|
323
|
+
(e) => typeof e === "object" && e !== null && "type" in e && e.type === "dir" && "name" in e && typeof e.name === "string"
|
|
324
|
+
).map((e) => e.name);
|
|
325
|
+
return subModules.length > 0 ? { ...mod, subModules } : mod;
|
|
326
|
+
} catch {
|
|
327
|
+
return mod;
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
);
|
|
331
|
+
_cachedModuleList = modules;
|
|
332
|
+
return modules;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/registry/fetch-module-file.ts
|
|
336
|
+
async function fetchModuleFile(moduleName, fileSrc, localPath) {
|
|
337
|
+
if (localPath) {
|
|
338
|
+
const { readFile: readFile6 } = await import("fs/promises");
|
|
339
|
+
const { join } = await import("path");
|
|
340
|
+
const file = join(localPath, moduleName, fileSrc);
|
|
341
|
+
return readFile6(file, "utf-8");
|
|
342
|
+
}
|
|
343
|
+
const url = githubUrls.rawFile(moduleName, fileSrc);
|
|
344
|
+
const res = await fetch(url);
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
throw new Error(`Failed to fetch file ${fileSrc} for module ${moduleName}: ${res.status}`);
|
|
347
|
+
}
|
|
348
|
+
return res.text();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/installer.ts
|
|
352
|
+
import fsExtra7 from "fs-extra";
|
|
353
|
+
import path5 from "path";
|
|
354
|
+
import * as p2 from "@clack/prompts";
|
|
355
|
+
var { outputFile, pathExists: pathExists6 } = fsExtra7;
|
|
356
|
+
async function installFiles(options) {
|
|
357
|
+
const { moduleName, files, cwd, dryRun, localPath } = options;
|
|
358
|
+
const results = [];
|
|
359
|
+
for (const file of files) {
|
|
360
|
+
const destAbs = path5.join(cwd, file.dest);
|
|
361
|
+
const exists = await pathExists6(destAbs);
|
|
362
|
+
if (exists) {
|
|
363
|
+
const content = await fetchModuleFile(moduleName, file.src, localPath);
|
|
364
|
+
const existing = await import("fs/promises").then(
|
|
365
|
+
(fs) => fs.readFile(destAbs, "utf-8")
|
|
366
|
+
);
|
|
367
|
+
if (existing === content) {
|
|
368
|
+
results.push({ dest: file.dest, action: "skipped" });
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (dryRun) {
|
|
372
|
+
results.push({ dest: file.dest, action: "would-overwrite" });
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const answer = await p2.confirm({
|
|
376
|
+
message: `File already exists: ${file.dest} \u2014 overwrite?`,
|
|
377
|
+
initialValue: false
|
|
378
|
+
});
|
|
379
|
+
if (p2.isCancel(answer) || !answer) {
|
|
380
|
+
results.push({ dest: file.dest, action: "skipped" });
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
await outputFile(destAbs, content, "utf-8");
|
|
384
|
+
results.push({ dest: file.dest, action: "overwritten" });
|
|
385
|
+
} else {
|
|
386
|
+
if (dryRun) {
|
|
387
|
+
results.push({ dest: file.dest, action: "would-create" });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const content = await fetchModuleFile(moduleName, file.src, localPath);
|
|
391
|
+
await outputFile(destAbs, content, "utf-8");
|
|
392
|
+
results.push({ dest: file.dest, action: "created" });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return results;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/transforms/append-export.ts
|
|
399
|
+
import fsExtra8 from "fs-extra";
|
|
400
|
+
import path6 from "path";
|
|
401
|
+
var { readFile, outputFile: outputFile2, pathExists: pathExists7 } = fsExtra8;
|
|
402
|
+
async function appendExport(target, value, cwd, dryRun) {
|
|
403
|
+
const file = path6.join(cwd, target);
|
|
404
|
+
if (!await pathExists7(file)) {
|
|
405
|
+
if (!dryRun) {
|
|
406
|
+
await outputFile2(file, `${value}
|
|
407
|
+
`, "utf-8");
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const content = await readFile(file, "utf-8");
|
|
412
|
+
if (content.includes(value)) return;
|
|
413
|
+
if (!dryRun) {
|
|
414
|
+
const newContent = content.endsWith("\n") ? `${content}${value}
|
|
415
|
+
` : `${content}
|
|
416
|
+
${value}
|
|
417
|
+
`;
|
|
418
|
+
await outputFile2(file, newContent, "utf-8");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/transforms/register-route.ts
|
|
423
|
+
import fsExtra9 from "fs-extra";
|
|
424
|
+
import path7 from "path";
|
|
425
|
+
var { readFile: readFile2, outputFile: outputFile3, pathExists: pathExists8 } = fsExtra9;
|
|
426
|
+
async function registerRoute(target, value, cwd, dryRun) {
|
|
427
|
+
const file = path7.join(cwd, target);
|
|
428
|
+
if (!await pathExists8(file)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`register-route: target file not found: ${target}. Please ensure your router file exists.`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const content = await readFile2(file, "utf-8");
|
|
434
|
+
if (content.includes(value)) return;
|
|
435
|
+
const routerPattern = /^([ \t]*\})[ \t]*(?:satisfies|as)\s/m;
|
|
436
|
+
const match = routerPattern.exec(content);
|
|
437
|
+
if (!match) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`register-route: could not find router closing brace in ${target}. Add "${value}" manually.`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (!dryRun) {
|
|
443
|
+
const insertPos = match.index;
|
|
444
|
+
const newContent = content.slice(0, insertPos) + ` ${value},
|
|
445
|
+
` + content.slice(insertPos);
|
|
446
|
+
await outputFile3(file, newContent, "utf-8");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/transforms/add-nav-item.ts
|
|
451
|
+
import fsExtra10 from "fs-extra";
|
|
452
|
+
import path8 from "path";
|
|
453
|
+
var { readFile: readFile3, outputFile: outputFile4, pathExists: pathExists9 } = fsExtra10;
|
|
454
|
+
async function addNavItem(target, value, cwd, dryRun) {
|
|
455
|
+
const file = path8.join(cwd, target);
|
|
456
|
+
if (!await pathExists9(file)) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`add-nav-item: target file not found: ${target}`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const content = await readFile3(file, "utf-8");
|
|
462
|
+
if (content.includes(value)) return;
|
|
463
|
+
const openPattern = /(?:const\s+\w+(?:\s*:\s*[\w<>\[\], ]+)?\s*=\s*\[|items\s*:\s*\[)/;
|
|
464
|
+
const openMatch = openPattern.exec(content);
|
|
465
|
+
if (!openMatch) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`add-nav-item: could not locate nav array in ${target}. Add "${value}" manually.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
const bracketStart = openMatch.index + openMatch[0].length - 1;
|
|
471
|
+
let depth = 0;
|
|
472
|
+
let insertPos = -1;
|
|
473
|
+
for (let i = bracketStart; i < content.length; i++) {
|
|
474
|
+
if (content[i] === "[") depth++;
|
|
475
|
+
else if (content[i] === "]") {
|
|
476
|
+
depth--;
|
|
477
|
+
if (depth === 0) {
|
|
478
|
+
insertPos = i;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (insertPos === -1) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`add-nav-item: could not find closing bracket for nav array in ${target}. Add "${value}" manually.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
if (!dryRun) {
|
|
489
|
+
const newContent = content.slice(0, insertPos) + ` ${value},
|
|
490
|
+
` + content.slice(insertPos);
|
|
491
|
+
await outputFile4(file, newContent, "utf-8");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/transforms/merge-schema.ts
|
|
496
|
+
import fsExtra11 from "fs-extra";
|
|
497
|
+
import path9 from "path";
|
|
498
|
+
var { readFile: readFile4, outputFile: outputFile5, pathExists: pathExists10 } = fsExtra11;
|
|
499
|
+
async function mergeSchema(target, value, cwd, dryRun) {
|
|
500
|
+
const file = path9.join(cwd, target);
|
|
501
|
+
if (!await pathExists10(file)) {
|
|
502
|
+
if (!dryRun) {
|
|
503
|
+
await outputFile5(file, `${value}
|
|
504
|
+
`, "utf-8");
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const content = await readFile4(file, "utf-8");
|
|
509
|
+
if (content.includes(value)) return;
|
|
510
|
+
if (!dryRun) {
|
|
511
|
+
const newContent = content.endsWith("\n") ? `${content}${value}
|
|
512
|
+
` : `${content}
|
|
513
|
+
${value}
|
|
514
|
+
`;
|
|
515
|
+
await outputFile5(file, newContent, "utf-8");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/transforms/add-env.ts
|
|
520
|
+
import fsExtra12 from "fs-extra";
|
|
521
|
+
import path10 from "path";
|
|
522
|
+
import * as p3 from "@clack/prompts";
|
|
523
|
+
var { readFile: readFile5, outputFile: outputFile6, pathExists: pathExists11 } = fsExtra12;
|
|
524
|
+
async function addEnv(target, value, cwd, dryRun) {
|
|
525
|
+
const file = path10.join(cwd, target);
|
|
526
|
+
let content = "";
|
|
527
|
+
if (await pathExists11(file)) {
|
|
528
|
+
content = await readFile5(file, "utf-8");
|
|
529
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
530
|
+
const keyPattern = new RegExp(`^${escaped}=`, "m");
|
|
531
|
+
if (keyPattern.test(content)) return;
|
|
532
|
+
}
|
|
533
|
+
let envValue = "";
|
|
534
|
+
if (!dryRun) {
|
|
535
|
+
const answer = await p3.text({
|
|
536
|
+
message: `Enter value for ${value} (leave blank to skip):`,
|
|
537
|
+
placeholder: ""
|
|
538
|
+
});
|
|
539
|
+
if (p3.isCancel(answer)) {
|
|
540
|
+
p3.log.warn(`Skipped env var: ${value}`);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
envValue = String(answer);
|
|
544
|
+
}
|
|
545
|
+
const line = `${value}=${envValue}`;
|
|
546
|
+
if (!dryRun) {
|
|
547
|
+
const newContent = content ? content.endsWith("\n") ? `${content}${line}
|
|
548
|
+
` : `${content}
|
|
549
|
+
${line}
|
|
550
|
+
` : `${line}
|
|
551
|
+
`;
|
|
552
|
+
await outputFile6(file, newContent, "utf-8");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/transforms/index.ts
|
|
557
|
+
async function runTransform(transform, cwd, dryRun) {
|
|
558
|
+
switch (transform.type) {
|
|
559
|
+
case "append-export":
|
|
560
|
+
await appendExport(transform.target, transform.value, cwd, dryRun);
|
|
561
|
+
break;
|
|
562
|
+
case "register-route":
|
|
563
|
+
await registerRoute(transform.target, transform.value, cwd, dryRun);
|
|
564
|
+
break;
|
|
565
|
+
case "add-nav-item":
|
|
566
|
+
await addNavItem(transform.target, transform.value, cwd, dryRun);
|
|
567
|
+
break;
|
|
568
|
+
case "merge-schema":
|
|
569
|
+
await mergeSchema(transform.target, transform.value, cwd, dryRun);
|
|
570
|
+
break;
|
|
571
|
+
case "add-env":
|
|
572
|
+
await addEnv(transform.target, transform.value, cwd, dryRun);
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/commands/add.ts
|
|
578
|
+
async function resolveModuleDeps(moduleId, localPath, resolved, orderedModules) {
|
|
579
|
+
if (resolved.has(moduleId)) return;
|
|
580
|
+
resolved.add(moduleId);
|
|
581
|
+
const manifest = await fetchModuleManifest(moduleId, localPath);
|
|
582
|
+
for (const dep of manifest.moduleDependencies) {
|
|
583
|
+
await resolveModuleDeps(dep, localPath, resolved, orderedModules);
|
|
584
|
+
}
|
|
585
|
+
orderedModules.push(moduleId);
|
|
586
|
+
}
|
|
587
|
+
async function resolveWithParent(moduleId, localPath, installedNames, resolved, orderedModules) {
|
|
588
|
+
const { parent, child } = parseModuleId(moduleId);
|
|
589
|
+
if (child !== null && !installedNames.has(parent)) {
|
|
590
|
+
await resolveModuleDeps(parent, localPath, resolved, orderedModules);
|
|
591
|
+
}
|
|
592
|
+
await resolveModuleDeps(moduleId, localPath, resolved, orderedModules);
|
|
593
|
+
}
|
|
594
|
+
function detectPackageManager(cwd) {
|
|
595
|
+
if (existsSync(path11.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
596
|
+
if (existsSync(path11.join(cwd, "yarn.lock"))) return "yarn";
|
|
597
|
+
return "npm";
|
|
598
|
+
}
|
|
599
|
+
function installNpmDeps(deps, cwd, dryRun) {
|
|
600
|
+
if (deps.length === 0) return;
|
|
601
|
+
const pm = detectPackageManager(cwd);
|
|
602
|
+
const args = ["add", ...deps];
|
|
603
|
+
if (dryRun) {
|
|
604
|
+
p4.log.info(`[dry-run] Would run: ${pm} ${args.join(" ")}`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
p4.log.step(`Installing dependencies: ${deps.join(", ")}`);
|
|
608
|
+
execFileSync(pm, args, { cwd, stdio: "inherit" });
|
|
609
|
+
}
|
|
610
|
+
async function installModule(moduleId, manifest, cwd, dryRun, localPath) {
|
|
611
|
+
p4.log.step(`Installing ${moduleId}@${manifest.version}...`);
|
|
612
|
+
if (manifest.files.length > 0) {
|
|
613
|
+
const installed = await installFiles({
|
|
614
|
+
moduleName: moduleId,
|
|
615
|
+
files: manifest.files,
|
|
616
|
+
cwd,
|
|
617
|
+
dryRun,
|
|
618
|
+
localPath
|
|
619
|
+
});
|
|
620
|
+
for (const f of installed) {
|
|
621
|
+
const icon = f.action === "created" || f.action === "would-create" ? "+" : f.action === "overwritten" || f.action === "would-overwrite" ? "~" : "=";
|
|
622
|
+
p4.log.message(` ${icon} ${f.dest}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
for (const transform of manifest.transforms) {
|
|
626
|
+
p4.log.step(` transform: ${transform.type} \u2192 ${transform.target}`);
|
|
627
|
+
await runTransform(transform, cwd, dryRun);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function recordModule(config, moduleId, manifest, now) {
|
|
631
|
+
const existing = config.installedModules.findIndex((m) => m.name === moduleId);
|
|
632
|
+
const record = {
|
|
633
|
+
name: moduleId,
|
|
634
|
+
version: manifest.version,
|
|
635
|
+
installedAt: now,
|
|
636
|
+
files: manifest.files.map((f) => f.dest)
|
|
637
|
+
};
|
|
638
|
+
if (existing >= 0) {
|
|
639
|
+
config.installedModules[existing] = record;
|
|
640
|
+
} else {
|
|
641
|
+
config.installedModules.push(record);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async function runAdd(options) {
|
|
645
|
+
const { moduleName, cwd, dryRun, localPath, withSubModules = [] } = options;
|
|
646
|
+
const withIds = withSubModules.map((sub) => `${moduleName}/${sub}`);
|
|
647
|
+
const allTargets = [moduleName, ...withIds];
|
|
648
|
+
p4.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
|
|
649
|
+
const config = await readConfig(cwd);
|
|
650
|
+
if (!config) {
|
|
651
|
+
p4.cancel("No .impulse.json found. Run `impulse init` first.");
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
const installedNames = new Set(config.installedModules.map((m) => m.name));
|
|
655
|
+
const { child: mainChild } = parseModuleId(moduleName);
|
|
656
|
+
if (mainChild === null && withSubModules.length === 0 && installedNames.has(moduleName)) {
|
|
657
|
+
const existing = config.installedModules.find((m) => m.name === moduleName);
|
|
658
|
+
p4.log.warn(`Module "${moduleName}" is already installed (v${existing?.version ?? "?"}).`);
|
|
659
|
+
const reinstall = await p4.confirm({
|
|
660
|
+
message: "Reinstall?",
|
|
661
|
+
initialValue: false
|
|
662
|
+
});
|
|
663
|
+
if (p4.isCancel(reinstall) || !reinstall) {
|
|
664
|
+
p4.outro("Cancelled.");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (withSubModules.length > 0) {
|
|
669
|
+
const parentManifest = await fetchModuleManifest(moduleName, localPath).catch(() => null);
|
|
670
|
+
if (parentManifest) {
|
|
671
|
+
if (parentManifest.subModules.length === 0) {
|
|
672
|
+
p4.cancel(`"${moduleName}" has no declared sub-modules.`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
const invalid = withSubModules.filter((sub) => !parentManifest.subModules.includes(sub));
|
|
676
|
+
if (invalid.length > 0) {
|
|
677
|
+
p4.cancel(
|
|
678
|
+
`Unknown sub-module(s) for "${moduleName}": ${invalid.join(", ")}.
|
|
679
|
+
Available: ${parentManifest.subModules.join(", ")}`
|
|
680
|
+
);
|
|
681
|
+
process.exit(1);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const s = p4.spinner();
|
|
686
|
+
s.start("Resolving dependencies...");
|
|
687
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
688
|
+
const orderedModules = [];
|
|
689
|
+
try {
|
|
690
|
+
for (const target of allTargets) {
|
|
691
|
+
await resolveWithParent(target, localPath, installedNames, resolved, orderedModules);
|
|
692
|
+
}
|
|
693
|
+
} catch (err) {
|
|
694
|
+
s.stop("Dependency resolution failed.");
|
|
695
|
+
p4.cancel(err instanceof Error ? err.message : String(err));
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
s.stop(`Resolved: ${orderedModules.join(" \u2192 ")}`);
|
|
699
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
700
|
+
for (const id of orderedModules) {
|
|
701
|
+
manifests.set(id, await fetchModuleManifest(id, localPath));
|
|
702
|
+
}
|
|
703
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
704
|
+
for (const manifest of manifests.values()) {
|
|
705
|
+
for (const dep of manifest.dependencies) {
|
|
706
|
+
allDeps.add(dep);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
p4.log.message("\nSummary of changes:");
|
|
710
|
+
for (const [id, manifest] of manifests) {
|
|
711
|
+
p4.log.message(`
|
|
712
|
+
Module: ${id}@${manifest.version}`);
|
|
713
|
+
for (const file of manifest.files) {
|
|
714
|
+
p4.log.message(` + ${file.dest}`);
|
|
715
|
+
}
|
|
716
|
+
for (const transform of manifest.transforms) {
|
|
717
|
+
p4.log.message(` ~ ${transform.type} \u2192 ${transform.target}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const allEnvVars = /* @__PURE__ */ new Set();
|
|
721
|
+
for (const manifest of manifests.values()) {
|
|
722
|
+
for (const envVar of manifest.envVars) {
|
|
723
|
+
allEnvVars.add(envVar);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (allDeps.size > 0) {
|
|
727
|
+
p4.log.message(`
|
|
728
|
+
npm deps: ${[...allDeps].join(", ")}`);
|
|
729
|
+
}
|
|
730
|
+
if (allEnvVars.size > 0) {
|
|
731
|
+
p4.log.message(`
|
|
732
|
+
env vars required: ${[...allEnvVars].join(", ")}`);
|
|
733
|
+
}
|
|
734
|
+
if (!dryRun) {
|
|
735
|
+
const confirm4 = await p4.confirm({
|
|
736
|
+
message: "Proceed?",
|
|
737
|
+
initialValue: true
|
|
738
|
+
});
|
|
739
|
+
if (p4.isCancel(confirm4) || !confirm4) {
|
|
740
|
+
p4.outro("Cancelled.");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const primaryTargetSet = new Set(allTargets);
|
|
745
|
+
const depModules = orderedModules.filter((id) => !primaryTargetSet.has(id) && !installedNames.has(id));
|
|
746
|
+
const targetModules = orderedModules.filter((id) => primaryTargetSet.has(id));
|
|
747
|
+
const depPostInstallHooks = [];
|
|
748
|
+
if (depModules.length > 0) {
|
|
749
|
+
p4.log.step(`Installing module dependencies: ${depModules.join(", ")}`);
|
|
750
|
+
for (const dep of depModules) {
|
|
751
|
+
const depManifest = manifests.get(dep);
|
|
752
|
+
if (!depManifest) continue;
|
|
753
|
+
await installModule(dep, depManifest, cwd, dryRun, localPath);
|
|
754
|
+
if (depManifest.postInstall && depManifest.postInstall.length > 0) {
|
|
755
|
+
depPostInstallHooks.push({ name: dep, hooks: depManifest.postInstall });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
for (const targetId of targetModules) {
|
|
760
|
+
const targetManifest = manifests.get(targetId);
|
|
761
|
+
if (!targetManifest) continue;
|
|
762
|
+
await installModule(targetId, targetManifest, cwd, dryRun, localPath);
|
|
763
|
+
}
|
|
764
|
+
installNpmDeps([...allDeps], cwd, dryRun);
|
|
765
|
+
if (!dryRun) {
|
|
766
|
+
for (const { name, hooks } of depPostInstallHooks) {
|
|
767
|
+
p4.log.step(`Running post-install hooks for ${name}...`);
|
|
768
|
+
for (const hook of hooks) {
|
|
769
|
+
p4.log.message(` $ ${hook}`);
|
|
770
|
+
execSync(hook, { cwd, stdio: "inherit" });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (!dryRun) {
|
|
775
|
+
for (const targetId of targetModules) {
|
|
776
|
+
const targetManifest = manifests.get(targetId);
|
|
777
|
+
if (!targetManifest?.postInstall?.length) continue;
|
|
778
|
+
p4.log.step(`Running post-install hooks for ${targetId}...`);
|
|
779
|
+
for (const hook of targetManifest.postInstall) {
|
|
780
|
+
p4.log.message(` $ ${hook}`);
|
|
781
|
+
execSync(hook, { cwd, stdio: "inherit" });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (!dryRun) {
|
|
786
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
787
|
+
for (const dep of depModules) {
|
|
788
|
+
const depManifest = manifests.get(dep);
|
|
789
|
+
if (depManifest) recordModule(config, dep, depManifest, now);
|
|
790
|
+
}
|
|
791
|
+
for (const targetId of targetModules) {
|
|
792
|
+
const targetManifest = manifests.get(targetId);
|
|
793
|
+
if (targetManifest) recordModule(config, targetId, targetManifest, now);
|
|
794
|
+
}
|
|
795
|
+
await writeConfig(config, cwd);
|
|
796
|
+
}
|
|
797
|
+
const label = allTargets.length === 1 ? `"${allTargets[0]}"` : allTargets.map((t) => `"${t}"`).join(", ");
|
|
798
|
+
p4.outro(
|
|
799
|
+
dryRun ? "Dry run complete \u2014 no files were modified." : `Module(s) ${label} installed successfully!`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/commands/list.ts
|
|
804
|
+
import * as p5 from "@clack/prompts";
|
|
805
|
+
async function runList(options) {
|
|
806
|
+
const { cwd, localPath } = options;
|
|
807
|
+
p5.intro("impulse list");
|
|
808
|
+
const config = await readConfig(cwd);
|
|
809
|
+
const installedNames = new Set(
|
|
810
|
+
config?.installedModules.map((m) => m.name) ?? []
|
|
811
|
+
);
|
|
812
|
+
const s = p5.spinner();
|
|
813
|
+
s.start("Fetching available modules...");
|
|
814
|
+
let available;
|
|
815
|
+
try {
|
|
816
|
+
available = await listAvailableModules(localPath);
|
|
817
|
+
} catch (err) {
|
|
818
|
+
s.stop("Failed to fetch module list.");
|
|
819
|
+
p5.cancel(err instanceof Error ? err.message : String(err));
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
s.stop(`Found ${available.length} module(s).`);
|
|
823
|
+
if (available.length === 0) {
|
|
824
|
+
p5.log.message("No modules available yet.");
|
|
825
|
+
p5.outro("Done.");
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
p5.log.message("\nAvailable modules:\n");
|
|
829
|
+
for (const mod of available) {
|
|
830
|
+
const installed = installedNames.has(mod.name);
|
|
831
|
+
const installedInfo = installed ? config?.installedModules.find((m) => m.name === mod.name) : null;
|
|
832
|
+
const status = installed ? `[installed v${installedInfo?.version ?? "?"}]` : "[available]";
|
|
833
|
+
const desc = mod.description ? ` \u2014 ${mod.description}` : "";
|
|
834
|
+
p5.log.message(` ${installed ? "\u2713" : "\u25CB"} ${mod.name} ${status}${desc}`);
|
|
835
|
+
if (mod.subModules && mod.subModules.length > 0) {
|
|
836
|
+
const last = mod.subModules.length - 1;
|
|
837
|
+
mod.subModules.forEach((sub, i) => {
|
|
838
|
+
const subId = `${mod.name}/${sub}`;
|
|
839
|
+
const subInstalled = installedNames.has(subId);
|
|
840
|
+
const subInfo = subInstalled ? config?.installedModules.find((m) => m.name === subId) : null;
|
|
841
|
+
const subStatus = subInstalled ? `[installed v${subInfo?.version ?? "?"}]` : "[not installed]";
|
|
842
|
+
const connector = i === last ? "\u2514\u2500" : "\u251C\u2500";
|
|
843
|
+
p5.log.message(` ${connector} ${sub} ${subStatus}`);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
p5.log.message(
|
|
848
|
+
`
|
|
849
|
+
Run \`impulse add <module>\` to install a module.`
|
|
850
|
+
);
|
|
851
|
+
p5.log.message(
|
|
852
|
+
`Run \`impulse add <parent>/<sub>\` or \`impulse add <parent> --with <sub1>,<sub2>\` for sub-modules.`
|
|
853
|
+
);
|
|
854
|
+
p5.outro("Done.");
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/index.ts
|
|
858
|
+
var program = new Command();
|
|
859
|
+
program.name("impulse").description("ImpulseLab CLI \u2014 install and manage modules for your projects").version("0.1.0");
|
|
860
|
+
program.command("init").description("Initialize impulse in the current project").option("--force", "Reinitialize even if .impulse.json already exists", false).action(async (options) => {
|
|
861
|
+
await runInit({ cwd: process.cwd(), force: options.force });
|
|
862
|
+
});
|
|
863
|
+
program.command("add <module>").description(
|
|
864
|
+
"Add a module to the current project.\n Supports sub-module syntax: `add attio/quote-to-cash`\n Use --with to install multiple sub-modules at once: `add attio --with quote-to-cash,gocardless`"
|
|
865
|
+
).option("--dry-run", "Preview changes without writing files", false).option(
|
|
866
|
+
"--local <path>",
|
|
867
|
+
"Use a local modules directory (for development)"
|
|
868
|
+
).option(
|
|
869
|
+
"--with <submodules>",
|
|
870
|
+
"Comma-separated sub-modules to install alongside the parent (e.g. --with quote-to-cash,gocardless)"
|
|
871
|
+
).action(async (moduleName, options) => {
|
|
872
|
+
const addOpts = {
|
|
873
|
+
moduleName,
|
|
874
|
+
cwd: process.cwd(),
|
|
875
|
+
dryRun: options.dryRun
|
|
876
|
+
};
|
|
877
|
+
if (options.local !== void 0) addOpts.localPath = options.local;
|
|
878
|
+
if (options.with !== void 0) {
|
|
879
|
+
addOpts.withSubModules = options.with.split(",").map((s) => s.trim()).filter(Boolean);
|
|
880
|
+
}
|
|
881
|
+
await runAdd(addOpts);
|
|
882
|
+
});
|
|
883
|
+
program.command("list").description("List available and installed modules").option(
|
|
884
|
+
"--local <path>",
|
|
885
|
+
"Use a local modules directory (for development)"
|
|
886
|
+
).action(async (options) => {
|
|
887
|
+
const listOpts = { cwd: process.cwd() };
|
|
888
|
+
if (options.local !== void 0) listOpts.localPath = options.local;
|
|
889
|
+
await runList(listOpts);
|
|
890
|
+
});
|
|
891
|
+
program.parse(process.argv);
|