@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/src/commands/add.ts
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import { execSync, execFileSync } from "child_process";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import * as p from "@clack/prompts";
|
|
5
|
-
import { readConfig, writeConfig } from "../config/index";
|
|
6
|
-
import { fetchModuleManifest, parseModuleId } from "../registry/index";
|
|
7
|
-
import { installFiles } from "../installer";
|
|
8
|
-
import { runTransform } from "../transforms/index";
|
|
9
|
-
import type { ModuleManifest } from "../types";
|
|
10
|
-
import type { ImpulseConfig } from "../schemas/impulse-config";
|
|
11
|
-
|
|
12
|
-
async function resolveModuleDeps(
|
|
13
|
-
moduleId: string,
|
|
14
|
-
localPath: string | undefined,
|
|
15
|
-
resolved: Set<string>,
|
|
16
|
-
orderedModules: string[]
|
|
17
|
-
): Promise<void> {
|
|
18
|
-
if (resolved.has(moduleId)) return;
|
|
19
|
-
resolved.add(moduleId);
|
|
20
|
-
|
|
21
|
-
const manifest = await fetchModuleManifest(moduleId, localPath);
|
|
22
|
-
for (const dep of manifest.moduleDependencies) {
|
|
23
|
-
await resolveModuleDeps(dep, localPath, resolved, orderedModules);
|
|
24
|
-
}
|
|
25
|
-
orderedModules.push(moduleId);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* For a sub-module install (`parent/child`), ensure the parent module itself
|
|
30
|
-
* is resolved first if it is not already installed.
|
|
31
|
-
*/
|
|
32
|
-
async function resolveWithParent(
|
|
33
|
-
moduleId: string,
|
|
34
|
-
localPath: string | undefined,
|
|
35
|
-
installedNames: Set<string>,
|
|
36
|
-
resolved: Set<string>,
|
|
37
|
-
orderedModules: string[]
|
|
38
|
-
): Promise<void> {
|
|
39
|
-
const { parent, child } = parseModuleId(moduleId);
|
|
40
|
-
if (child !== null && !installedNames.has(parent)) {
|
|
41
|
-
await resolveModuleDeps(parent, localPath, resolved, orderedModules);
|
|
42
|
-
}
|
|
43
|
-
await resolveModuleDeps(moduleId, localPath, resolved, orderedModules);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function detectPackageManager(cwd: string): "pnpm" | "npm" | "yarn" {
|
|
47
|
-
if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
48
|
-
if (existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
49
|
-
return "npm";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function installNpmDeps(
|
|
53
|
-
deps: string[],
|
|
54
|
-
cwd: string,
|
|
55
|
-
dryRun: boolean
|
|
56
|
-
): void {
|
|
57
|
-
if (deps.length === 0) return;
|
|
58
|
-
const pm = detectPackageManager(cwd);
|
|
59
|
-
const args = ["add", ...deps];
|
|
60
|
-
|
|
61
|
-
if (dryRun) {
|
|
62
|
-
p.log.info(`[dry-run] Would run: ${pm} ${args.join(" ")}`);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
p.log.step(`Installing dependencies: ${deps.join(", ")}`);
|
|
67
|
-
execFileSync(pm, args, { cwd, stdio: "inherit" });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function installModule(
|
|
71
|
-
moduleId: string,
|
|
72
|
-
manifest: ModuleManifest,
|
|
73
|
-
cwd: string,
|
|
74
|
-
dryRun: boolean,
|
|
75
|
-
localPath?: string
|
|
76
|
-
): Promise<void> {
|
|
77
|
-
p.log.step(`Installing ${moduleId}@${manifest.version}...`);
|
|
78
|
-
|
|
79
|
-
if (manifest.files.length > 0) {
|
|
80
|
-
const installed = await installFiles({
|
|
81
|
-
moduleName: moduleId,
|
|
82
|
-
files: manifest.files,
|
|
83
|
-
cwd,
|
|
84
|
-
dryRun,
|
|
85
|
-
localPath,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
for (const f of installed) {
|
|
89
|
-
const icon =
|
|
90
|
-
f.action === "created" || f.action === "would-create"
|
|
91
|
-
? "+"
|
|
92
|
-
: f.action === "overwritten" || f.action === "would-overwrite"
|
|
93
|
-
? "~"
|
|
94
|
-
: "=";
|
|
95
|
-
p.log.message(` ${icon} ${f.dest}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
for (const transform of manifest.transforms) {
|
|
100
|
-
p.log.step(` transform: ${transform.type} → ${transform.target}`);
|
|
101
|
-
await runTransform(transform, cwd, dryRun);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function recordModule(
|
|
106
|
-
config: ImpulseConfig,
|
|
107
|
-
moduleId: string,
|
|
108
|
-
manifest: ModuleManifest,
|
|
109
|
-
now: string
|
|
110
|
-
): void {
|
|
111
|
-
const existing = config.installedModules.findIndex((m) => m.name === moduleId);
|
|
112
|
-
const record = {
|
|
113
|
-
name: moduleId,
|
|
114
|
-
version: manifest.version,
|
|
115
|
-
installedAt: now,
|
|
116
|
-
files: manifest.files.map((f) => f.dest),
|
|
117
|
-
};
|
|
118
|
-
if (existing >= 0) {
|
|
119
|
-
config.installedModules[existing] = record;
|
|
120
|
-
} else {
|
|
121
|
-
config.installedModules.push(record);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export async function runAdd(options: {
|
|
126
|
-
moduleName: string;
|
|
127
|
-
cwd: string;
|
|
128
|
-
dryRun: boolean;
|
|
129
|
-
localPath?: string;
|
|
130
|
-
withSubModules?: string[];
|
|
131
|
-
}): Promise<void> {
|
|
132
|
-
const { moduleName, cwd, dryRun, localPath, withSubModules = [] } = options;
|
|
133
|
-
|
|
134
|
-
// Build full list: main target + --with sub-modules
|
|
135
|
-
const withIds = withSubModules.map((sub) => `${moduleName}/${sub}`);
|
|
136
|
-
const allTargets = [moduleName, ...withIds];
|
|
137
|
-
|
|
138
|
-
p.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
|
|
139
|
-
|
|
140
|
-
const config = await readConfig(cwd);
|
|
141
|
-
if (!config) {
|
|
142
|
-
p.cancel("No .impulse.json found. Run `impulse init` first.");
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const installedNames = new Set(config.installedModules.map((m) => m.name));
|
|
147
|
-
|
|
148
|
-
// Check already-installed only for a simple (non-sub-module, no --with) install
|
|
149
|
-
const { child: mainChild } = parseModuleId(moduleName);
|
|
150
|
-
if (mainChild === null && withSubModules.length === 0 && installedNames.has(moduleName)) {
|
|
151
|
-
const existing = config.installedModules.find((m) => m.name === moduleName);
|
|
152
|
-
p.log.warn(`Module "${moduleName}" is already installed (v${existing?.version ?? "?"}).`);
|
|
153
|
-
const reinstall = await p.confirm({
|
|
154
|
-
message: "Reinstall?",
|
|
155
|
-
initialValue: false,
|
|
156
|
-
});
|
|
157
|
-
if (p.isCancel(reinstall) || !reinstall) {
|
|
158
|
-
p.outro("Cancelled.");
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Validate --with sub-module names against the parent manifest's declared sub-modules
|
|
164
|
-
if (withSubModules.length > 0) {
|
|
165
|
-
const parentManifest = await fetchModuleManifest(moduleName, localPath).catch(() => null);
|
|
166
|
-
if (parentManifest) {
|
|
167
|
-
if (parentManifest.subModules.length === 0) {
|
|
168
|
-
p.cancel(`"${moduleName}" has no declared sub-modules.`);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
const invalid = withSubModules.filter((sub) => !parentManifest.subModules.includes(sub));
|
|
172
|
-
if (invalid.length > 0) {
|
|
173
|
-
p.cancel(
|
|
174
|
-
`Unknown sub-module(s) for "${moduleName}": ${invalid.join(", ")}.\nAvailable: ${parentManifest.subModules.join(", ")}`
|
|
175
|
-
);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Resolve dependency order for all targets
|
|
182
|
-
const s = p.spinner();
|
|
183
|
-
s.start("Resolving dependencies...");
|
|
184
|
-
|
|
185
|
-
const resolved = new Set<string>();
|
|
186
|
-
const orderedModules: string[] = [];
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
for (const target of allTargets) {
|
|
190
|
-
await resolveWithParent(target, localPath, installedNames, resolved, orderedModules);
|
|
191
|
-
}
|
|
192
|
-
} catch (err) {
|
|
193
|
-
s.stop("Dependency resolution failed.");
|
|
194
|
-
p.cancel(err instanceof Error ? err.message : String(err));
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
s.stop(`Resolved: ${orderedModules.join(" → ")}`);
|
|
199
|
-
|
|
200
|
-
// Fetch all manifests
|
|
201
|
-
const manifests = new Map<string, ModuleManifest>();
|
|
202
|
-
for (const id of orderedModules) {
|
|
203
|
-
manifests.set(id, await fetchModuleManifest(id, localPath));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Collect all npm deps
|
|
207
|
-
const allDeps = new Set<string>();
|
|
208
|
-
for (const manifest of manifests.values()) {
|
|
209
|
-
for (const dep of manifest.dependencies) {
|
|
210
|
-
allDeps.add(dep);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Show summary
|
|
215
|
-
p.log.message("\nSummary of changes:");
|
|
216
|
-
for (const [id, manifest] of manifests) {
|
|
217
|
-
p.log.message(`\n Module: ${id}@${manifest.version}`);
|
|
218
|
-
for (const file of manifest.files) {
|
|
219
|
-
p.log.message(` + ${file.dest}`);
|
|
220
|
-
}
|
|
221
|
-
for (const transform of manifest.transforms) {
|
|
222
|
-
p.log.message(` ~ ${transform.type} → ${transform.target}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Collect env vars documented across all modules (informational display)
|
|
227
|
-
const allEnvVars = new Set<string>();
|
|
228
|
-
for (const manifest of manifests.values()) {
|
|
229
|
-
for (const envVar of manifest.envVars) {
|
|
230
|
-
allEnvVars.add(envVar);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (allDeps.size > 0) {
|
|
235
|
-
p.log.message(`\n npm deps: ${[...allDeps].join(", ")}`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (allEnvVars.size > 0) {
|
|
239
|
-
p.log.message(`\n env vars required: ${[...allEnvVars].join(", ")}`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (!dryRun) {
|
|
243
|
-
const confirm = await p.confirm({
|
|
244
|
-
message: "Proceed?",
|
|
245
|
-
initialValue: true,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
if (p.isCancel(confirm) || !confirm) {
|
|
249
|
-
p.outro("Cancelled.");
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Split ordered modules into auto-dependencies vs primary targets
|
|
255
|
-
const primaryTargetSet = new Set(allTargets);
|
|
256
|
-
const depModules = orderedModules.filter((id) => !primaryTargetSet.has(id) && !installedNames.has(id));
|
|
257
|
-
const targetModules = orderedModules.filter((id) => primaryTargetSet.has(id));
|
|
258
|
-
|
|
259
|
-
// Collect dep postInstall hooks
|
|
260
|
-
const depPostInstallHooks: Array<{ name: string; hooks: string[] }> = [];
|
|
261
|
-
|
|
262
|
-
if (depModules.length > 0) {
|
|
263
|
-
p.log.step(`Installing module dependencies: ${depModules.join(", ")}`);
|
|
264
|
-
for (const dep of depModules) {
|
|
265
|
-
const depManifest = manifests.get(dep);
|
|
266
|
-
if (!depManifest) continue;
|
|
267
|
-
await installModule(dep, depManifest, cwd, dryRun, localPath);
|
|
268
|
-
if (depManifest.postInstall && depManifest.postInstall.length > 0) {
|
|
269
|
-
depPostInstallHooks.push({ name: dep, hooks: depManifest.postInstall });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Install primary targets in resolved order
|
|
275
|
-
for (const targetId of targetModules) {
|
|
276
|
-
const targetManifest = manifests.get(targetId);
|
|
277
|
-
if (!targetManifest) continue;
|
|
278
|
-
await installModule(targetId, targetManifest, cwd, dryRun, localPath);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Install npm deps so all postInstall hooks have their packages available
|
|
282
|
-
installNpmDeps([...allDeps], cwd, dryRun);
|
|
283
|
-
|
|
284
|
-
// Run post-install hooks for dependency modules
|
|
285
|
-
if (!dryRun) {
|
|
286
|
-
for (const { name, hooks } of depPostInstallHooks) {
|
|
287
|
-
p.log.step(`Running post-install hooks for ${name}...`);
|
|
288
|
-
for (const hook of hooks) {
|
|
289
|
-
p.log.message(` $ ${hook}`);
|
|
290
|
-
execSync(hook, { cwd, stdio: "inherit" });
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Run post-install hooks for primary targets
|
|
296
|
-
if (!dryRun) {
|
|
297
|
-
for (const targetId of targetModules) {
|
|
298
|
-
const targetManifest = manifests.get(targetId);
|
|
299
|
-
if (!targetManifest?.postInstall?.length) continue;
|
|
300
|
-
p.log.step(`Running post-install hooks for ${targetId}...`);
|
|
301
|
-
for (const hook of targetManifest.postInstall) {
|
|
302
|
-
p.log.message(` $ ${hook}`);
|
|
303
|
-
execSync(hook, { cwd, stdio: "inherit" });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Update .impulse.json
|
|
309
|
-
if (!dryRun) {
|
|
310
|
-
const now = new Date().toISOString();
|
|
311
|
-
|
|
312
|
-
for (const dep of depModules) {
|
|
313
|
-
const depManifest = manifests.get(dep);
|
|
314
|
-
if (depManifest) recordModule(config, dep, depManifest, now);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
for (const targetId of targetModules) {
|
|
318
|
-
const targetManifest = manifests.get(targetId);
|
|
319
|
-
if (targetManifest) recordModule(config, targetId, targetManifest, now);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
await writeConfig(config, cwd);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const label =
|
|
326
|
-
allTargets.length === 1
|
|
327
|
-
? `"${allTargets[0]}"`
|
|
328
|
-
: allTargets.map((t) => `"${t}"`).join(", ");
|
|
329
|
-
|
|
330
|
-
p.outro(
|
|
331
|
-
dryRun
|
|
332
|
-
? "Dry run complete — no files were modified."
|
|
333
|
-
: `Module(s) ${label} installed successfully!`
|
|
334
|
-
);
|
|
335
|
-
}
|
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
|
-
});
|