@impulselab/cli 0.1.6 → 0.2.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 +616 -83
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,9 +15,6 @@ function configPath(cwd = process.cwd()) {
|
|
|
15
15
|
return path.join(cwd, CONFIG_FILE);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// src/config/read-config.ts
|
|
19
|
-
import fsExtra from "fs-extra";
|
|
20
|
-
|
|
21
18
|
// src/schemas/impulse-config.ts
|
|
22
19
|
import { z } from "zod";
|
|
23
20
|
var ImpulseConfigSchema = z.object({
|
|
@@ -36,20 +33,44 @@ var ImpulseConfigSchema = z.object({
|
|
|
36
33
|
).default([])
|
|
37
34
|
});
|
|
38
35
|
|
|
39
|
-
// src/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
// src/shared/json-file.ts
|
|
37
|
+
import fsExtra from "fs-extra";
|
|
38
|
+
import { chmod } from "fs/promises";
|
|
39
|
+
var { readJson, pathExists, outputJson } = fsExtra;
|
|
40
|
+
async function readJsonFile(filePath, schema, opts) {
|
|
41
|
+
if (!await pathExists(filePath)) return null;
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = await readJson(filePath);
|
|
45
|
+
} catch {
|
|
46
|
+
if (opts?.throwOnInvalid !== void 0) {
|
|
47
|
+
throw new Error(`${opts.throwOnInvalid}: failed to read file`);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const parsed = schema.safeParse(raw);
|
|
46
52
|
if (!parsed.success) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
if (opts?.throwOnInvalid !== void 0) {
|
|
54
|
+
throw new Error(`${opts.throwOnInvalid}: ${parsed.error.message}`);
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
50
57
|
}
|
|
51
58
|
return parsed.data;
|
|
52
59
|
}
|
|
60
|
+
async function writeJsonFile(filePath, data, opts) {
|
|
61
|
+
const { spaces = 2, mode } = opts ?? {};
|
|
62
|
+
await outputJson(filePath, data, { spaces, ...mode !== void 0 ? { mode } : {} });
|
|
63
|
+
if (mode !== void 0) {
|
|
64
|
+
await chmod(filePath, mode);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/config/read-config.ts
|
|
69
|
+
async function readConfig(cwd = process.cwd()) {
|
|
70
|
+
return readJsonFile(configPath(cwd), ImpulseConfigSchema, {
|
|
71
|
+
throwOnInvalid: "Invalid .impulse.json"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
53
74
|
|
|
54
75
|
// src/config/write-config.ts
|
|
55
76
|
import fsExtra2 from "fs-extra";
|
|
@@ -146,7 +167,7 @@ async function isUltimateTemplate(cwd) {
|
|
|
146
167
|
return foundDeps.size >= STRUCTURAL_MARKERS.workspaceDeps.length;
|
|
147
168
|
}
|
|
148
169
|
async function runInit(options) {
|
|
149
|
-
const { cwd, force } = options;
|
|
170
|
+
const { cwd, force, skipDetection = false } = options;
|
|
150
171
|
p.intro("impulse init");
|
|
151
172
|
if (!force && await hasConfig(cwd)) {
|
|
152
173
|
p.log.warn(
|
|
@@ -165,17 +186,21 @@ async function runInit(options) {
|
|
|
165
186
|
);
|
|
166
187
|
process.exit(1);
|
|
167
188
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
p.
|
|
177
|
-
|
|
189
|
+
if (!skipDetection) {
|
|
190
|
+
const isUT = await isUltimateTemplate(cwd);
|
|
191
|
+
s.stop(isUT ? "Ultimate Template project detected." : "Project detected.");
|
|
192
|
+
if (!isUT) {
|
|
193
|
+
const proceed = await p.confirm({
|
|
194
|
+
message: "This project does not appear to be an ImpulseLab Ultimate Template. Continue anyway?",
|
|
195
|
+
initialValue: false
|
|
196
|
+
});
|
|
197
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
198
|
+
p.cancel("Initialization cancelled.");
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
178
201
|
}
|
|
202
|
+
} else {
|
|
203
|
+
s.stop("Project ready.");
|
|
179
204
|
}
|
|
180
205
|
const projectName = await detectProjectName(cwd);
|
|
181
206
|
const srcExists = await pathExists3(path2.join(cwd, "src"));
|
|
@@ -197,6 +222,7 @@ async function runInit(options) {
|
|
|
197
222
|
// src/commands/add.ts
|
|
198
223
|
import { execSync as execSync2, execFileSync } from "child_process";
|
|
199
224
|
import { existsSync } from "fs";
|
|
225
|
+
import { readFile as readFile7, writeFile, unlink } from "fs/promises";
|
|
200
226
|
import path12 from "path";
|
|
201
227
|
import * as p5 from "@clack/prompts";
|
|
202
228
|
|
|
@@ -271,7 +297,8 @@ var registryConstants = {
|
|
|
271
297
|
githubBranch: "main",
|
|
272
298
|
modulesDir: "modules",
|
|
273
299
|
subModulesDirName: "sub-modules",
|
|
274
|
-
manifestFileName: "module.json"
|
|
300
|
+
manifestFileName: "module.json",
|
|
301
|
+
presetsDir: "presets"
|
|
275
302
|
};
|
|
276
303
|
|
|
277
304
|
// src/registry/github-urls.ts
|
|
@@ -325,7 +352,7 @@ function moduleRegistryPath(moduleId) {
|
|
|
325
352
|
}
|
|
326
353
|
|
|
327
354
|
// src/registry/fetch-module-manifest.ts
|
|
328
|
-
var {
|
|
355
|
+
var { pathExists: pathExists4 } = fsExtra5;
|
|
329
356
|
async function fetchModuleManifest(moduleId, localPath) {
|
|
330
357
|
const registryPath = moduleRegistryPath(moduleId);
|
|
331
358
|
if (localPath) {
|
|
@@ -333,12 +360,10 @@ async function fetchModuleManifest(moduleId, localPath) {
|
|
|
333
360
|
if (!await pathExists4(file)) {
|
|
334
361
|
throw new Error(`Local module not found: ${file}`);
|
|
335
362
|
}
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
return parsed2.data;
|
|
363
|
+
const manifest = await readJsonFile(file, ModuleManifestSchema, {
|
|
364
|
+
throwOnInvalid: `Invalid module.json for ${moduleId}`
|
|
365
|
+
});
|
|
366
|
+
return manifest;
|
|
342
367
|
}
|
|
343
368
|
const url = githubUrls.rawFile(registryPath, registryConstants.manifestFileName);
|
|
344
369
|
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
@@ -359,7 +384,7 @@ async function fetchModuleManifest(moduleId, localPath) {
|
|
|
359
384
|
// src/registry/list-available-modules.ts
|
|
360
385
|
import fsExtra6 from "fs-extra";
|
|
361
386
|
import path4 from "path";
|
|
362
|
-
var { readJson:
|
|
387
|
+
var { readJson: readJson3, pathExists: pathExists5 } = fsExtra6;
|
|
363
388
|
var _cachedModuleList = null;
|
|
364
389
|
async function listAvailableModules(localPath) {
|
|
365
390
|
if (localPath) {
|
|
@@ -375,7 +400,7 @@ async function listAvailableModules(localPath) {
|
|
|
375
400
|
);
|
|
376
401
|
if (!await pathExists5(manifestFile)) continue;
|
|
377
402
|
try {
|
|
378
|
-
const raw = await
|
|
403
|
+
const raw = await readJson3(manifestFile);
|
|
379
404
|
const parsed = ModuleManifestSchema.safeParse(raw);
|
|
380
405
|
if (!parsed.success) continue;
|
|
381
406
|
let subModules;
|
|
@@ -442,10 +467,10 @@ async function listAvailableModules(localPath) {
|
|
|
442
467
|
// src/registry/fetch-module-file.ts
|
|
443
468
|
async function fetchModuleFile(moduleName, fileSrc, localPath) {
|
|
444
469
|
if (localPath) {
|
|
445
|
-
const { readFile:
|
|
470
|
+
const { readFile: readFile9 } = await import("fs/promises");
|
|
446
471
|
const { join } = await import("path");
|
|
447
472
|
const file = join(localPath, moduleName, fileSrc);
|
|
448
|
-
return
|
|
473
|
+
return readFile9(file, "utf-8");
|
|
449
474
|
}
|
|
450
475
|
const url = githubUrls.rawFile(moduleName, fileSrc);
|
|
451
476
|
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
@@ -536,15 +561,32 @@ ${value}
|
|
|
536
561
|
// src/transforms/register-route.ts
|
|
537
562
|
import fsExtra9 from "fs-extra";
|
|
538
563
|
import path7 from "path";
|
|
564
|
+
|
|
565
|
+
// src/transforms/base-router-content.ts
|
|
566
|
+
var BASE_ROUTER_CONTENT = [
|
|
567
|
+
'import type { RouterInput } from "@orpc/server";',
|
|
568
|
+
'import { createRouter } from "@orpc/server";',
|
|
569
|
+
"",
|
|
570
|
+
"export const appRouter = createRouter({",
|
|
571
|
+
"} satisfies RouterInput);",
|
|
572
|
+
"",
|
|
573
|
+
"export type AppRouter = typeof appRouter;",
|
|
574
|
+
""
|
|
575
|
+
].join("\n");
|
|
576
|
+
|
|
577
|
+
// src/transforms/register-route.ts
|
|
539
578
|
var { readFile: readFile3, outputFile: outputFile3, pathExists: pathExists8 } = fsExtra9;
|
|
540
579
|
async function registerRoute(target, value, cwd, dryRun) {
|
|
541
580
|
const file = path7.join(cwd, target);
|
|
581
|
+
let content;
|
|
542
582
|
if (!await pathExists8(file)) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
583
|
+
content = BASE_ROUTER_CONTENT;
|
|
584
|
+
if (!dryRun) {
|
|
585
|
+
await outputFile3(file, content, "utf-8");
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
content = await readFile3(file, "utf-8");
|
|
546
589
|
}
|
|
547
|
-
const content = await readFile3(file, "utf-8");
|
|
548
590
|
if (content.includes(value)) return;
|
|
549
591
|
const routerPattern = /^([ \t]*\})[ \t]*(?:satisfies|as)\s/m;
|
|
550
592
|
const match = routerPattern.exec(content);
|
|
@@ -691,9 +733,6 @@ async function runTransform(transform, cwd, dryRun) {
|
|
|
691
733
|
// src/auth/require-auth.ts
|
|
692
734
|
import * as p4 from "@clack/prompts";
|
|
693
735
|
|
|
694
|
-
// src/auth/read-auth.ts
|
|
695
|
-
import fsExtra13 from "fs-extra";
|
|
696
|
-
|
|
697
736
|
// src/auth/auth-path.ts
|
|
698
737
|
import path11 from "path";
|
|
699
738
|
import os from "os";
|
|
@@ -710,19 +749,8 @@ var AuthCredentialsSchema = z6.object({
|
|
|
710
749
|
});
|
|
711
750
|
|
|
712
751
|
// src/auth/read-auth.ts
|
|
713
|
-
var { readJson: readJson5, pathExists: pathExists12 } = fsExtra13;
|
|
714
752
|
async function readAuth() {
|
|
715
|
-
|
|
716
|
-
if (!await pathExists12(file)) return null;
|
|
717
|
-
let raw;
|
|
718
|
-
try {
|
|
719
|
-
raw = await readJson5(file);
|
|
720
|
-
} catch {
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
const parsed = AuthCredentialsSchema.safeParse(raw);
|
|
724
|
-
if (!parsed.success) return null;
|
|
725
|
-
return parsed.data;
|
|
753
|
+
return readJsonFile(authPath(), AuthCredentialsSchema);
|
|
726
754
|
}
|
|
727
755
|
|
|
728
756
|
// src/auth/require-auth.ts
|
|
@@ -782,6 +810,7 @@ function installNpmDeps(deps, cwd, dryRun) {
|
|
|
782
810
|
async function installModule(moduleId, manifest, cwd, dryRun, installedModules, localPath) {
|
|
783
811
|
p5.log.step(`Installing ${moduleId}@${manifest.version}...`);
|
|
784
812
|
const installedDests = [];
|
|
813
|
+
const createdDests = [];
|
|
785
814
|
if (manifest.files.length > 0) {
|
|
786
815
|
const installed = await installFiles({
|
|
787
816
|
moduleName: moduleId,
|
|
@@ -797,13 +826,34 @@ async function installModule(moduleId, manifest, cwd, dryRun, installedModules,
|
|
|
797
826
|
continue;
|
|
798
827
|
}
|
|
799
828
|
installedDests.push(f.dest);
|
|
829
|
+
if (f.action === "created" || f.action === "would-create") {
|
|
830
|
+
createdDests.push(f.dest);
|
|
831
|
+
}
|
|
800
832
|
const icon = f.action === "created" || f.action === "would-create" ? "+" : f.action === "overwritten" || f.action === "would-overwrite" ? "~" : "=";
|
|
801
833
|
p5.log.message(` ${icon} ${f.dest}`);
|
|
802
834
|
}
|
|
803
835
|
}
|
|
804
836
|
for (const transform of manifest.transforms) {
|
|
805
837
|
p5.log.step(` transform: ${transform.type} \u2192 ${transform.target}`);
|
|
806
|
-
|
|
838
|
+
try {
|
|
839
|
+
await runTransform(transform, cwd, dryRun);
|
|
840
|
+
} catch (err) {
|
|
841
|
+
p5.log.warn(
|
|
842
|
+
`Transform "${transform.type} \u2192 ${transform.target}" failed for module "${moduleId}": ${err instanceof Error ? err.message : String(err)}`
|
|
843
|
+
);
|
|
844
|
+
if (!dryRun && createdDests.length > 0) {
|
|
845
|
+
p5.log.warn(`Rolling back ${createdDests.length} file(s) created by "${moduleId}"...`);
|
|
846
|
+
for (const dest of createdDests) {
|
|
847
|
+
try {
|
|
848
|
+
await unlink(path12.join(cwd, dest));
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
throw new Error(
|
|
854
|
+
`Module "${moduleId}" install failed during transform "${transform.type} \u2192 ${transform.target}".`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
807
857
|
}
|
|
808
858
|
return installedDests;
|
|
809
859
|
}
|
|
@@ -829,7 +879,10 @@ async function pickModulesInteractively(localPath) {
|
|
|
829
879
|
modules = await listAvailableModules(localPath);
|
|
830
880
|
} catch (err) {
|
|
831
881
|
s.stop("Failed to load modules.");
|
|
832
|
-
|
|
882
|
+
p5.cancel(
|
|
883
|
+
`Could not load module list: ${err instanceof Error ? err.message : String(err)}`
|
|
884
|
+
);
|
|
885
|
+
process.exit(1);
|
|
833
886
|
}
|
|
834
887
|
s.stop("Modules loaded.");
|
|
835
888
|
if (modules.length === 0) {
|
|
@@ -847,7 +900,7 @@ async function pickModulesInteractively(localPath) {
|
|
|
847
900
|
options.push({
|
|
848
901
|
value: `${mod.name}/${sub}`,
|
|
849
902
|
label: ` \u21B3 ${sub}`,
|
|
850
|
-
hint: `
|
|
903
|
+
hint: `automatically includes ${mod.name}`
|
|
851
904
|
});
|
|
852
905
|
}
|
|
853
906
|
}
|
|
@@ -861,20 +914,25 @@ async function pickModulesInteractively(localPath) {
|
|
|
861
914
|
process.exit(0);
|
|
862
915
|
}
|
|
863
916
|
const result = new Set(selected);
|
|
917
|
+
const autoIncluded = [];
|
|
864
918
|
for (const id of selected) {
|
|
865
919
|
const { parent, child } = parseModuleId(id);
|
|
866
|
-
if (child !== null) {
|
|
920
|
+
if (child !== null && !selected.includes(parent)) {
|
|
867
921
|
result.add(parent);
|
|
922
|
+
autoIncluded.push(parent);
|
|
868
923
|
}
|
|
869
924
|
}
|
|
925
|
+
if (autoIncluded.length > 0) {
|
|
926
|
+
p5.log.info(`Auto-included parent modules: ${autoIncluded.join(", ")}`);
|
|
927
|
+
}
|
|
870
928
|
return [...result];
|
|
871
929
|
}
|
|
872
930
|
async function runAdd(options) {
|
|
873
|
-
let { moduleNames, cwd, dryRun, localPath, withSubModules = [], allowScripts = false } = options;
|
|
931
|
+
let { moduleNames, cwd, dryRun, localPath, withSubModules = [], allowScripts = false, skipIntro = false, skipAuth = false } = options;
|
|
874
932
|
let allTargets;
|
|
875
933
|
if (moduleNames.length === 0) {
|
|
876
|
-
p5.intro(`impulse add${dryRun ? " [dry-run]" : ""}`);
|
|
877
|
-
await requireAuth();
|
|
934
|
+
if (!skipIntro) p5.intro(`impulse add${dryRun ? " [dry-run]" : ""}`);
|
|
935
|
+
if (!skipAuth) await requireAuth();
|
|
878
936
|
if (withSubModules.length > 0) {
|
|
879
937
|
p5.log.warn(`--with is ignored in interactive mode. Select sub-modules from the picker.`);
|
|
880
938
|
withSubModules = [];
|
|
@@ -893,8 +951,8 @@ async function runAdd(options) {
|
|
|
893
951
|
}
|
|
894
952
|
const withIds = moduleNames.length === 1 && withSubModules.length > 0 ? withSubModules.map((sub) => `${moduleNames[0]}/${sub}`) : [];
|
|
895
953
|
allTargets = [...moduleNames, ...withIds];
|
|
896
|
-
p5.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
|
|
897
|
-
await requireAuth();
|
|
954
|
+
if (!skipIntro) p5.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
|
|
955
|
+
if (!skipAuth) await requireAuth();
|
|
898
956
|
}
|
|
899
957
|
const config = await readConfig(cwd);
|
|
900
958
|
if (!config) {
|
|
@@ -924,7 +982,14 @@ async function runAdd(options) {
|
|
|
924
982
|
}
|
|
925
983
|
}
|
|
926
984
|
if (moduleNames.length === 1 && withSubModules.length > 0) {
|
|
927
|
-
|
|
985
|
+
let parentManifest = null;
|
|
986
|
+
try {
|
|
987
|
+
parentManifest = await getManifest(moduleNames[0], localPath, manifests);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
p5.log.warn(
|
|
990
|
+
`Could not fetch manifest for "${moduleNames[0]}" to validate --with sub-modules: ${err instanceof Error ? err.message : String(err)}. Skipping validation.`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
928
993
|
if (parentManifest) {
|
|
929
994
|
if (parentManifest.subModules.length === 0) {
|
|
930
995
|
p5.cancel(`"${moduleNames[0]}" has no declared sub-modules.`);
|
|
@@ -1090,13 +1155,26 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
1090
1155
|
runHooks = confirmed;
|
|
1091
1156
|
}
|
|
1092
1157
|
if (runHooks) {
|
|
1158
|
+
const hookFailures = [];
|
|
1093
1159
|
for (const { name, hooks } of allPostInstallHooks) {
|
|
1094
1160
|
p5.log.step(`Running post-install hooks for ${name}...`);
|
|
1095
1161
|
for (const hook of hooks) {
|
|
1096
1162
|
p5.log.message(` $ ${hook}`);
|
|
1097
|
-
|
|
1163
|
+
try {
|
|
1164
|
+
execSync2(hook, { cwd, stdio: "inherit" });
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
const msg = `Post-install hook failed for "${name}" ($ ${hook}): ${err instanceof Error ? err.message : String(err)}`;
|
|
1167
|
+
p5.log.warn(`${msg}. Continuing.`);
|
|
1168
|
+
hookFailures.push(msg);
|
|
1169
|
+
}
|
|
1098
1170
|
}
|
|
1099
1171
|
}
|
|
1172
|
+
if (hookFailures.length > 0) {
|
|
1173
|
+
p5.log.warn(
|
|
1174
|
+
`${hookFailures.length} post-install hook(s) failed. See warnings above.`
|
|
1175
|
+
);
|
|
1176
|
+
process.exitCode = 1;
|
|
1177
|
+
}
|
|
1100
1178
|
} else {
|
|
1101
1179
|
p5.log.warn("Post-install hooks skipped.");
|
|
1102
1180
|
}
|
|
@@ -1114,7 +1192,28 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
1114
1192
|
const dests = installedFilesMap.get(targetId) ?? [];
|
|
1115
1193
|
if (targetManifest) recordModule(config, targetId, targetManifest, dests, now);
|
|
1116
1194
|
}
|
|
1117
|
-
|
|
1195
|
+
const cfgPath = configPath(cwd);
|
|
1196
|
+
let backup = null;
|
|
1197
|
+
try {
|
|
1198
|
+
backup = await readFile7(cfgPath, "utf-8");
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
await writeConfig(config, cwd);
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
p5.log.warn(
|
|
1205
|
+
`Failed to write .impulse.json: ${err instanceof Error ? err.message : String(err)}`
|
|
1206
|
+
);
|
|
1207
|
+
if (backup !== null) {
|
|
1208
|
+
try {
|
|
1209
|
+
await writeFile(cfgPath, backup, "utf-8");
|
|
1210
|
+
p5.log.warn("Config restored to previous state.");
|
|
1211
|
+
} catch {
|
|
1212
|
+
p5.log.warn("Could not restore config backup \u2014 .impulse.json may be in a partial state.");
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
throw err;
|
|
1216
|
+
}
|
|
1118
1217
|
}
|
|
1119
1218
|
const label = allTargets.length === 1 ? `"${allTargets[0]}"` : allTargets.map((t) => `"${t}"`).join(", ");
|
|
1120
1219
|
p5.outro(
|
|
@@ -1214,11 +1313,20 @@ import { execFileSync as execFileSync2 } from "child_process";
|
|
|
1214
1313
|
var IMPULSE_BASE_URL = process.env.IMPULSE_BASE_URL ?? "https://impulselab.ai";
|
|
1215
1314
|
var CLIENT_ID = "impulse-cli";
|
|
1216
1315
|
async function requestDeviceCode() {
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1316
|
+
const url = `${IMPULSE_BASE_URL}/api/auth/device/code`;
|
|
1317
|
+
let res;
|
|
1318
|
+
try {
|
|
1319
|
+
res = await fetch(url, {
|
|
1320
|
+
method: "POST",
|
|
1321
|
+
headers: { "Content-Type": "application/json" },
|
|
1322
|
+
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
1323
|
+
});
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1326
|
+
throw new Error(
|
|
1327
|
+
`Network error calling ${url}: ${reason}. Check connection, VPN, proxy, and IMPULSE_BASE_URL (must match your deployed agency site).`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1222
1330
|
if (!res.ok) {
|
|
1223
1331
|
throw new Error(`Failed to initiate device flow: ${res.status} ${res.statusText}`);
|
|
1224
1332
|
}
|
|
@@ -1275,6 +1383,25 @@ function openBrowser(url) {
|
|
|
1275
1383
|
} catch {
|
|
1276
1384
|
}
|
|
1277
1385
|
}
|
|
1386
|
+
function copyToClipboard(text3) {
|
|
1387
|
+
try {
|
|
1388
|
+
const platform = process.platform;
|
|
1389
|
+
if (platform === "darwin") {
|
|
1390
|
+
execFileSync2("pbcopy", { input: text3 });
|
|
1391
|
+
} else if (platform === "win32") {
|
|
1392
|
+
execFileSync2("clip", { input: text3 });
|
|
1393
|
+
} else {
|
|
1394
|
+
try {
|
|
1395
|
+
execFileSync2("xclip", ["-selection", "clipboard"], { input: text3 });
|
|
1396
|
+
} catch {
|
|
1397
|
+
execFileSync2("xsel", ["--clipboard", "--input"], { input: text3 });
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return true;
|
|
1401
|
+
} catch {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1278
1405
|
function assertDeviceCodeResponse(data) {
|
|
1279
1406
|
if (typeof data !== "object" || data === null || typeof data.device_code !== "string" || typeof data.user_code !== "string" || typeof data.verification_uri !== "string") {
|
|
1280
1407
|
throw new Error("Invalid device code response from server");
|
|
@@ -1304,13 +1431,8 @@ function mapDeviceTokenResponse(raw) {
|
|
|
1304
1431
|
}
|
|
1305
1432
|
|
|
1306
1433
|
// src/auth/write-auth.ts
|
|
1307
|
-
import fsExtra14 from "fs-extra";
|
|
1308
|
-
import { chmod } from "fs/promises";
|
|
1309
|
-
var { outputJson } = fsExtra14;
|
|
1310
1434
|
async function writeAuth(credentials) {
|
|
1311
|
-
|
|
1312
|
-
await outputJson(file, credentials, { spaces: 2, mode: 384 });
|
|
1313
|
-
await chmod(file, 384);
|
|
1435
|
+
await writeJsonFile(authPath(), credentials, { mode: 384 });
|
|
1314
1436
|
}
|
|
1315
1437
|
|
|
1316
1438
|
// src/commands/login.ts
|
|
@@ -1345,9 +1467,13 @@ async function runLogin() {
|
|
|
1345
1467
|
Visit the following URL to authenticate:
|
|
1346
1468
|
${browserUrl}
|
|
1347
1469
|
`);
|
|
1348
|
-
|
|
1349
|
-
|
|
1470
|
+
const rawCode = deviceCode.userCode;
|
|
1471
|
+
const mid = Math.ceil(rawCode.length / 2);
|
|
1472
|
+
const formattedCode = rawCode.length >= 6 ? `${rawCode.slice(0, mid)}-${rawCode.slice(mid)}` : rawCode;
|
|
1350
1473
|
openBrowser(browserUrl);
|
|
1474
|
+
const copied = copyToClipboard(rawCode);
|
|
1475
|
+
p7.log.message(`Your one-time code: ${formattedCode}${copied ? " (copied to clipboard)" : ""}
|
|
1476
|
+
`);
|
|
1351
1477
|
p7.log.info(`If the browser did not open, copy the URL above.
|
|
1352
1478
|
Base URL: ${IMPULSE_BASE_URL}`);
|
|
1353
1479
|
const pollInterval = Math.max(
|
|
@@ -1406,11 +1532,11 @@ function sleep(ms) {
|
|
|
1406
1532
|
import * as p8 from "@clack/prompts";
|
|
1407
1533
|
|
|
1408
1534
|
// src/auth/clear-auth.ts
|
|
1409
|
-
import
|
|
1410
|
-
var { remove, pathExists:
|
|
1535
|
+
import fsExtra13 from "fs-extra";
|
|
1536
|
+
var { remove, pathExists: pathExists12 } = fsExtra13;
|
|
1411
1537
|
async function clearAuth() {
|
|
1412
1538
|
const file = authPath();
|
|
1413
|
-
if (!await
|
|
1539
|
+
if (!await pathExists12(file)) return false;
|
|
1414
1540
|
await remove(file);
|
|
1415
1541
|
return true;
|
|
1416
1542
|
}
|
|
@@ -1457,6 +1583,400 @@ async function runWhoami() {
|
|
|
1457
1583
|
p9.outro("Done.");
|
|
1458
1584
|
}
|
|
1459
1585
|
|
|
1586
|
+
// src/commands/create.ts
|
|
1587
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1588
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1589
|
+
import path15 from "path";
|
|
1590
|
+
import * as p10 from "@clack/prompts";
|
|
1591
|
+
import fsExtra16 from "fs-extra";
|
|
1592
|
+
|
|
1593
|
+
// src/registry/fetch-template.ts
|
|
1594
|
+
import fsExtra14 from "fs-extra";
|
|
1595
|
+
import path13 from "path";
|
|
1596
|
+
|
|
1597
|
+
// src/schemas/template-manifest.ts
|
|
1598
|
+
import { z as z7 } from "zod";
|
|
1599
|
+
var TemplateFileSchema = z7.object({
|
|
1600
|
+
src: z7.string(),
|
|
1601
|
+
dest: z7.string()
|
|
1602
|
+
});
|
|
1603
|
+
var TemplateManifestSchema = z7.object({
|
|
1604
|
+
name: z7.string(),
|
|
1605
|
+
description: z7.string(),
|
|
1606
|
+
/** Human-readable labels describing the tech stack (e.g. ["Next.js 15", "Tailwind CSS v4"]). */
|
|
1607
|
+
stack: z7.array(z7.string()).default([]),
|
|
1608
|
+
/** Module IDs pre-selected in the interactive picker (e.g. ["db", "auth"]). */
|
|
1609
|
+
defaultModules: z7.array(z7.string()).default([]),
|
|
1610
|
+
/** Files to copy from the template directory into the new project. */
|
|
1611
|
+
files: z7.array(TemplateFileSchema).default([])
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// src/registry/fetch-template.ts
|
|
1615
|
+
var { readJson: readJson4, pathExists: pathExists13, readFile: readFile8 } = fsExtra14;
|
|
1616
|
+
var TEMPLATES_DIR = "templates";
|
|
1617
|
+
var TEMPLATE_MANIFEST_FILE = "template.json";
|
|
1618
|
+
function templateRawUrl(templateId, file) {
|
|
1619
|
+
return `https://raw.githubusercontent.com/${registryConstants.githubOrg}/${registryConstants.githubRepo}/${registryConstants.githubBranch}/${TEMPLATES_DIR}/${templateId}/${file}`;
|
|
1620
|
+
}
|
|
1621
|
+
async function fetchTemplateManifest(templateId, localPath) {
|
|
1622
|
+
if (localPath) {
|
|
1623
|
+
const file = path13.join(localPath, TEMPLATES_DIR, templateId, TEMPLATE_MANIFEST_FILE);
|
|
1624
|
+
if (!await pathExists13(file)) {
|
|
1625
|
+
throw new Error(`Local template not found: ${file}`);
|
|
1626
|
+
}
|
|
1627
|
+
const raw2 = await readJson4(file);
|
|
1628
|
+
const parsed2 = TemplateManifestSchema.safeParse(raw2);
|
|
1629
|
+
if (!parsed2.success) {
|
|
1630
|
+
throw new Error(`Invalid template.json for "${templateId}": ${parsed2.error.message}`);
|
|
1631
|
+
}
|
|
1632
|
+
return parsed2.data;
|
|
1633
|
+
}
|
|
1634
|
+
const url = templateRawUrl(templateId, TEMPLATE_MANIFEST_FILE);
|
|
1635
|
+
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
1636
|
+
if (!res.ok) {
|
|
1637
|
+
if (res.status === 404) {
|
|
1638
|
+
throw new Error(`Template not found in registry: ${templateId}`);
|
|
1639
|
+
}
|
|
1640
|
+
throw new Error(`Failed to fetch template manifest: ${res.status} ${res.statusText}`);
|
|
1641
|
+
}
|
|
1642
|
+
const raw = await res.json();
|
|
1643
|
+
const parsed = TemplateManifestSchema.safeParse(raw);
|
|
1644
|
+
if (!parsed.success) {
|
|
1645
|
+
throw new Error(`Invalid template.json for "${templateId}": ${parsed.error.message}`);
|
|
1646
|
+
}
|
|
1647
|
+
return parsed.data;
|
|
1648
|
+
}
|
|
1649
|
+
async function fetchTemplateFile(templateId, src, localPath) {
|
|
1650
|
+
if (localPath) {
|
|
1651
|
+
const file = path13.join(localPath, TEMPLATES_DIR, templateId, src);
|
|
1652
|
+
return readFile8(file, "utf-8");
|
|
1653
|
+
}
|
|
1654
|
+
const url = templateRawUrl(templateId, src);
|
|
1655
|
+
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
1656
|
+
if (!res.ok) {
|
|
1657
|
+
throw new Error(`Failed to fetch template file "${src}": ${res.status} ${res.statusText}`);
|
|
1658
|
+
}
|
|
1659
|
+
return res.text();
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/registry/fetch-preset.ts
|
|
1663
|
+
import fsExtra15 from "fs-extra";
|
|
1664
|
+
import path14 from "path";
|
|
1665
|
+
|
|
1666
|
+
// src/schemas/preset-manifest.ts
|
|
1667
|
+
import { z as z8 } from "zod";
|
|
1668
|
+
var PresetManifestSchema = z8.object({
|
|
1669
|
+
name: z8.string(),
|
|
1670
|
+
description: z8.string(),
|
|
1671
|
+
/** Template ID to scaffold from (references templates/<name>/). */
|
|
1672
|
+
template: z8.string(),
|
|
1673
|
+
/** Modules installed automatically — no user confirmation needed. */
|
|
1674
|
+
defaultModules: z8.array(z8.string()),
|
|
1675
|
+
/** Modules the user can optionally add during create. */
|
|
1676
|
+
optionalModules: z8.array(z8.string()).default([])
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
// src/registry/fetch-preset.ts
|
|
1680
|
+
var { readJson: readJson5, pathExists: pathExists14 } = fsExtra15;
|
|
1681
|
+
var PRESETS_DIR = registryConstants.presetsDir;
|
|
1682
|
+
var PRESET_MANIFEST_FILE = "preset.json";
|
|
1683
|
+
function presetRawUrl(presetId, file) {
|
|
1684
|
+
return `https://raw.githubusercontent.com/${registryConstants.githubOrg}/${registryConstants.githubRepo}/${registryConstants.githubBranch}/${PRESETS_DIR}/${presetId}/${file}`;
|
|
1685
|
+
}
|
|
1686
|
+
async function fetchPresetManifest(presetId, localPath) {
|
|
1687
|
+
if (localPath) {
|
|
1688
|
+
const file = path14.join(localPath, PRESETS_DIR, presetId, PRESET_MANIFEST_FILE);
|
|
1689
|
+
if (!await pathExists14(file)) {
|
|
1690
|
+
throw new Error(`Local preset not found: ${file}`);
|
|
1691
|
+
}
|
|
1692
|
+
const raw2 = await readJson5(file);
|
|
1693
|
+
const parsed2 = PresetManifestSchema.safeParse(raw2);
|
|
1694
|
+
if (!parsed2.success) {
|
|
1695
|
+
throw new Error(`Invalid preset.json for "${presetId}": ${parsed2.error.message}`);
|
|
1696
|
+
}
|
|
1697
|
+
return parsed2.data;
|
|
1698
|
+
}
|
|
1699
|
+
const url = presetRawUrl(presetId, PRESET_MANIFEST_FILE);
|
|
1700
|
+
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
1701
|
+
if (!res.ok) {
|
|
1702
|
+
if (res.status === 404) {
|
|
1703
|
+
throw new Error(`Preset not found in registry: ${presetId}`);
|
|
1704
|
+
}
|
|
1705
|
+
throw new Error(`Failed to fetch preset manifest: ${res.status} ${res.statusText}`);
|
|
1706
|
+
}
|
|
1707
|
+
const raw = await res.json();
|
|
1708
|
+
const parsed = PresetManifestSchema.safeParse(raw);
|
|
1709
|
+
if (!parsed.success) {
|
|
1710
|
+
throw new Error(`Invalid preset.json for "${presetId}": ${parsed.error.message}`);
|
|
1711
|
+
}
|
|
1712
|
+
return parsed.data;
|
|
1713
|
+
}
|
|
1714
|
+
async function listAvailablePresets(localPath) {
|
|
1715
|
+
if (localPath) {
|
|
1716
|
+
const presetsDir = path14.join(localPath, PRESETS_DIR);
|
|
1717
|
+
if (!await pathExists14(presetsDir)) return [];
|
|
1718
|
+
const { readdir } = await import("fs/promises");
|
|
1719
|
+
const entries2 = await readdir(presetsDir, { withFileTypes: true });
|
|
1720
|
+
return entries2.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1721
|
+
}
|
|
1722
|
+
const url = `https://api.github.com/repos/${registryConstants.githubOrg}/${registryConstants.githubRepo}/contents/${PRESETS_DIR}`;
|
|
1723
|
+
const res = await fetch(url, { headers: getGitHubHeaders({ Accept: "application/vnd.github.v3+json" }) });
|
|
1724
|
+
if (!res.ok) return ["saas"];
|
|
1725
|
+
const entries = await res.json();
|
|
1726
|
+
if (!Array.isArray(entries)) return ["saas"];
|
|
1727
|
+
return entries.filter((e) => e.type === "dir").map((e) => e.name);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/commands/create.ts
|
|
1731
|
+
var { outputFile: outputFile7, pathExists: pathExists15 } = fsExtra16;
|
|
1732
|
+
function slugify(name) {
|
|
1733
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1734
|
+
}
|
|
1735
|
+
function isValidProjectName(name) {
|
|
1736
|
+
return /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name);
|
|
1737
|
+
}
|
|
1738
|
+
async function runCreate(options) {
|
|
1739
|
+
const { cwd, localPath } = options;
|
|
1740
|
+
p10.intro("impulse create");
|
|
1741
|
+
await requireAuth();
|
|
1742
|
+
let projectName = options.projectName;
|
|
1743
|
+
if (!projectName) {
|
|
1744
|
+
const answer = await p10.text({
|
|
1745
|
+
message: "Project name:",
|
|
1746
|
+
placeholder: "my-app",
|
|
1747
|
+
validate(value) {
|
|
1748
|
+
if (!value?.trim()) return "Project name is required.";
|
|
1749
|
+
const slug = slugify(value.trim());
|
|
1750
|
+
if (!isValidProjectName(slug)) {
|
|
1751
|
+
return "Use lowercase letters, numbers, and hyphens only (e.g. my-app).";
|
|
1752
|
+
}
|
|
1753
|
+
return void 0;
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
if (p10.isCancel(answer)) {
|
|
1757
|
+
p10.cancel("Cancelled.");
|
|
1758
|
+
process.exit(0);
|
|
1759
|
+
}
|
|
1760
|
+
projectName = slugify(answer.trim());
|
|
1761
|
+
} else {
|
|
1762
|
+
projectName = slugify(projectName.trim());
|
|
1763
|
+
}
|
|
1764
|
+
const targetDir = path15.resolve(cwd, projectName);
|
|
1765
|
+
if (existsSync2(targetDir)) {
|
|
1766
|
+
p10.cancel(`Directory "${projectName}" already exists.`);
|
|
1767
|
+
process.exit(1);
|
|
1768
|
+
}
|
|
1769
|
+
let templateId;
|
|
1770
|
+
let selectedModules = options.modules ?? [];
|
|
1771
|
+
if (options.template) {
|
|
1772
|
+
templateId = options.template;
|
|
1773
|
+
if (!options.modules) {
|
|
1774
|
+
const s = p10.spinner();
|
|
1775
|
+
s.start("Loading available modules...");
|
|
1776
|
+
let availableModules;
|
|
1777
|
+
try {
|
|
1778
|
+
availableModules = await listAvailableModules(localPath);
|
|
1779
|
+
} catch {
|
|
1780
|
+
availableModules = [];
|
|
1781
|
+
}
|
|
1782
|
+
s.stop("Modules loaded.");
|
|
1783
|
+
if (availableModules.length > 0) {
|
|
1784
|
+
const templateManifest = await fetchTemplateManifest(templateId, localPath).catch(() => null);
|
|
1785
|
+
const defaultValues = templateManifest ? templateManifest.defaultModules.filter((d) => availableModules.some((m) => m.name === d)) : [];
|
|
1786
|
+
const pickerOptions = availableModules.map((m) => ({
|
|
1787
|
+
value: m.name,
|
|
1788
|
+
label: m.name,
|
|
1789
|
+
...m.description !== void 0 ? { hint: m.description } : {}
|
|
1790
|
+
}));
|
|
1791
|
+
const picked = await p10.multiselect({
|
|
1792
|
+
message: "Select modules to install (space to toggle, enter to confirm):",
|
|
1793
|
+
options: pickerOptions,
|
|
1794
|
+
initialValues: defaultValues,
|
|
1795
|
+
required: false
|
|
1796
|
+
});
|
|
1797
|
+
if (p10.isCancel(picked)) {
|
|
1798
|
+
p10.cancel("Cancelled.");
|
|
1799
|
+
process.exit(0);
|
|
1800
|
+
}
|
|
1801
|
+
selectedModules = picked;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
} else {
|
|
1805
|
+
let presetManifest;
|
|
1806
|
+
if (options.preset) {
|
|
1807
|
+
const s = p10.spinner();
|
|
1808
|
+
s.start(`Fetching preset "${options.preset}"...`);
|
|
1809
|
+
try {
|
|
1810
|
+
presetManifest = await fetchPresetManifest(options.preset, localPath);
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
s.stop("Failed to fetch preset.");
|
|
1813
|
+
p10.cancel(err instanceof Error ? err.message : String(err));
|
|
1814
|
+
process.exit(1);
|
|
1815
|
+
}
|
|
1816
|
+
s.stop(`Preset "${options.preset}" ready.`);
|
|
1817
|
+
} else {
|
|
1818
|
+
const s = p10.spinner();
|
|
1819
|
+
s.start("Loading available presets...");
|
|
1820
|
+
let presetIds;
|
|
1821
|
+
try {
|
|
1822
|
+
presetIds = await listAvailablePresets(localPath);
|
|
1823
|
+
} catch {
|
|
1824
|
+
presetIds = ["saas"];
|
|
1825
|
+
}
|
|
1826
|
+
s.stop("Presets loaded.");
|
|
1827
|
+
let selectedPresetId;
|
|
1828
|
+
if (presetIds.length > 1) {
|
|
1829
|
+
const presetDetails = await Promise.all(
|
|
1830
|
+
presetIds.map(async (id) => {
|
|
1831
|
+
try {
|
|
1832
|
+
const m = await fetchPresetManifest(id, localPath);
|
|
1833
|
+
return { id, description: m.description };
|
|
1834
|
+
} catch {
|
|
1835
|
+
return { id, description: "" };
|
|
1836
|
+
}
|
|
1837
|
+
})
|
|
1838
|
+
);
|
|
1839
|
+
const selected = await p10.select({
|
|
1840
|
+
message: "Select a preset:",
|
|
1841
|
+
options: presetDetails.map(({ id, description }) => ({
|
|
1842
|
+
value: id,
|
|
1843
|
+
label: id,
|
|
1844
|
+
...description ? { hint: description } : {}
|
|
1845
|
+
})),
|
|
1846
|
+
initialValue: "saas"
|
|
1847
|
+
});
|
|
1848
|
+
if (p10.isCancel(selected)) {
|
|
1849
|
+
p10.cancel("Cancelled.");
|
|
1850
|
+
process.exit(0);
|
|
1851
|
+
}
|
|
1852
|
+
selectedPresetId = selected;
|
|
1853
|
+
} else {
|
|
1854
|
+
selectedPresetId = presetIds[0] ?? "saas";
|
|
1855
|
+
p10.log.info(`Using preset: ${selectedPresetId}`);
|
|
1856
|
+
}
|
|
1857
|
+
const s22 = p10.spinner();
|
|
1858
|
+
s22.start(`Fetching preset "${selectedPresetId}"...`);
|
|
1859
|
+
try {
|
|
1860
|
+
presetManifest = await fetchPresetManifest(selectedPresetId, localPath);
|
|
1861
|
+
} catch (err) {
|
|
1862
|
+
s22.stop("Failed to fetch preset.");
|
|
1863
|
+
p10.cancel(err instanceof Error ? err.message : String(err));
|
|
1864
|
+
process.exit(1);
|
|
1865
|
+
}
|
|
1866
|
+
s22.stop(`Preset "${selectedPresetId}" ready.`);
|
|
1867
|
+
if (!options.modules && presetManifest.optionalModules.length > 0) {
|
|
1868
|
+
const optionalSelected = await p10.multiselect({
|
|
1869
|
+
message: `Optional modules for "${selectedPresetId}" (space to toggle, enter to skip):`,
|
|
1870
|
+
options: presetManifest.optionalModules.map((id) => ({
|
|
1871
|
+
value: id,
|
|
1872
|
+
label: id
|
|
1873
|
+
})),
|
|
1874
|
+
required: false
|
|
1875
|
+
});
|
|
1876
|
+
if (p10.isCancel(optionalSelected)) {
|
|
1877
|
+
p10.cancel("Cancelled.");
|
|
1878
|
+
process.exit(0);
|
|
1879
|
+
}
|
|
1880
|
+
selectedModules = [
|
|
1881
|
+
...presetManifest.defaultModules,
|
|
1882
|
+
...optionalSelected
|
|
1883
|
+
];
|
|
1884
|
+
} else if (!options.modules) {
|
|
1885
|
+
selectedModules = presetManifest.defaultModules;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
templateId = presetManifest.template;
|
|
1889
|
+
if (options.preset && !options.modules) {
|
|
1890
|
+
selectedModules = presetManifest.defaultModules;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const s2 = p10.spinner();
|
|
1894
|
+
s2.start(`Fetching template "${templateId}"...`);
|
|
1895
|
+
let manifest;
|
|
1896
|
+
try {
|
|
1897
|
+
manifest = await fetchTemplateManifest(templateId, localPath);
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
s2.stop("Failed to fetch template.");
|
|
1900
|
+
p10.cancel(err instanceof Error ? err.message : String(err));
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
s2.stop(`Template "${templateId}" ready.`);
|
|
1904
|
+
p10.log.message(`
|
|
1905
|
+
Stack: ${manifest.stack.join(", ")}`);
|
|
1906
|
+
const s4 = p10.spinner();
|
|
1907
|
+
s4.start(`Scaffolding "${projectName}"...`);
|
|
1908
|
+
try {
|
|
1909
|
+
for (const file of manifest.files) {
|
|
1910
|
+
const content = await fetchTemplateFile(templateId, file.src, localPath);
|
|
1911
|
+
const destPath = path15.join(targetDir, file.dest);
|
|
1912
|
+
await outputFile7(destPath, content, "utf-8");
|
|
1913
|
+
}
|
|
1914
|
+
const pkgPath = path15.join(targetDir, "package.json");
|
|
1915
|
+
if (existsSync2(pkgPath)) {
|
|
1916
|
+
const { readJson: readJson6, writeJson: writeJson2 } = fsExtra16;
|
|
1917
|
+
const pkg = await readJson6(pkgPath);
|
|
1918
|
+
if (pkg && typeof pkg === "object") {
|
|
1919
|
+
pkg["name"] = projectName;
|
|
1920
|
+
await writeJson2(pkgPath, pkg, { spaces: 2 });
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
} catch (err) {
|
|
1924
|
+
s4.stop("Scaffold failed.");
|
|
1925
|
+
p10.cancel(err instanceof Error ? err.message : String(err));
|
|
1926
|
+
process.exit(1);
|
|
1927
|
+
}
|
|
1928
|
+
s4.stop(`Project scaffolded at ./${projectName}`);
|
|
1929
|
+
p10.log.step("Initializing impulse...");
|
|
1930
|
+
await runInit({ cwd: targetDir, force: false, skipDetection: true });
|
|
1931
|
+
if (selectedModules.length > 0) {
|
|
1932
|
+
p10.log.step(`Installing modules: ${selectedModules.join(", ")}`);
|
|
1933
|
+
await runAdd({
|
|
1934
|
+
moduleNames: selectedModules,
|
|
1935
|
+
cwd: targetDir,
|
|
1936
|
+
dryRun: false,
|
|
1937
|
+
...localPath !== void 0 ? { localPath } : {},
|
|
1938
|
+
allowScripts: false,
|
|
1939
|
+
skipIntro: true,
|
|
1940
|
+
skipAuth: true
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
const hasPnpmLock = await pathExists15(path15.join(targetDir, "pnpm-lock.yaml"));
|
|
1944
|
+
const pm = hasPnpmLock ? "pnpm" : existsSync2(path15.join(targetDir, "yarn.lock")) ? "yarn" : "pnpm";
|
|
1945
|
+
const s5 = p10.spinner();
|
|
1946
|
+
s5.start(`Running ${pm} install...`);
|
|
1947
|
+
try {
|
|
1948
|
+
execFileSync3(pm, ["install"], { cwd: targetDir, stdio: "pipe" });
|
|
1949
|
+
s5.stop("Dependencies installed.");
|
|
1950
|
+
} catch {
|
|
1951
|
+
s5.stop(`${pm} install failed \u2014 run it manually.`);
|
|
1952
|
+
p10.log.warn(`Run \`cd ${projectName} && ${pm} install\` to install dependencies.`);
|
|
1953
|
+
}
|
|
1954
|
+
const s6 = p10.spinner();
|
|
1955
|
+
s6.start("Initializing git repository...");
|
|
1956
|
+
try {
|
|
1957
|
+
execFileSync3("git", ["init"], { cwd: targetDir, stdio: "pipe" });
|
|
1958
|
+
execFileSync3("git", ["add", "-A"], { cwd: targetDir, stdio: "pipe" });
|
|
1959
|
+
execFileSync3(
|
|
1960
|
+
"git",
|
|
1961
|
+
["commit", "-m", `feat: initial project from impulse create (${templateId} template)`],
|
|
1962
|
+
{ cwd: targetDir, stdio: "pipe" }
|
|
1963
|
+
);
|
|
1964
|
+
s6.stop("Git repository initialized.");
|
|
1965
|
+
} catch {
|
|
1966
|
+
s6.stop("Git init failed \u2014 skipping.");
|
|
1967
|
+
p10.log.warn(`Run \`git init && git add -A && git commit -m "init"\` manually.`);
|
|
1968
|
+
}
|
|
1969
|
+
p10.outro(
|
|
1970
|
+
`
|
|
1971
|
+
Project "${projectName}" created successfully!
|
|
1972
|
+
|
|
1973
|
+
cd ${projectName}
|
|
1974
|
+
${pm} dev
|
|
1975
|
+
|
|
1976
|
+
Add more modules anytime with \`impulse add\`.`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1460
1980
|
// src/cli-version.ts
|
|
1461
1981
|
import { createRequire } from "module";
|
|
1462
1982
|
var require2 = createRequire(import.meta.url);
|
|
@@ -1501,6 +2021,19 @@ program.command("list").description("List available and installed modules").opti
|
|
|
1501
2021
|
if (options.local !== void 0) listOpts.localPath = options.local;
|
|
1502
2022
|
await runList(listOpts);
|
|
1503
2023
|
});
|
|
2024
|
+
program.command("create [name]").description(
|
|
2025
|
+
"Create a new project from a preset or template.\n No args: interactive prompts for name, preset, and optional modules\n With preset: `create my-app --preset saas`\n With template (advanced): `create my-app --template saas`\n With modules: `create my-app --preset minimal --modules auth,ui`"
|
|
2026
|
+
).option("--preset <preset>", "Preset to use (omit for interactive selection)").option("--template <template>", "Template override \u2014 bypasses preset selection (advanced)").option("--modules <modules>", "Comma-separated modules to install after scaffolding").option("--local <path>", "Use a local presets/templates/modules directory (for development)").action(async (name, options) => {
|
|
2027
|
+
const moduleList = options.modules !== void 0 ? options.modules.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
2028
|
+
await runCreate({
|
|
2029
|
+
cwd: process.cwd(),
|
|
2030
|
+
...name !== void 0 ? { projectName: name } : {},
|
|
2031
|
+
...options.preset !== void 0 ? { preset: options.preset } : {},
|
|
2032
|
+
...options.template !== void 0 ? { template: options.template } : {},
|
|
2033
|
+
...moduleList !== void 0 ? { modules: moduleList } : {},
|
|
2034
|
+
...options.local !== void 0 ? { localPath: options.local } : {}
|
|
2035
|
+
});
|
|
2036
|
+
});
|
|
1504
2037
|
program.command("login").description("Authenticate with ImpulseLab (opens browser for device flow)").action(async () => {
|
|
1505
2038
|
await runLogin();
|
|
1506
2039
|
});
|