@impulselab/cli 0.1.5 → 0.2.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.js +549 -33
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -146,7 +146,7 @@ async function isUltimateTemplate(cwd) {
|
|
|
146
146
|
return foundDeps.size >= STRUCTURAL_MARKERS.workspaceDeps.length;
|
|
147
147
|
}
|
|
148
148
|
async function runInit(options) {
|
|
149
|
-
const { cwd, force } = options;
|
|
149
|
+
const { cwd, force, skipDetection = false } = options;
|
|
150
150
|
p.intro("impulse init");
|
|
151
151
|
if (!force && await hasConfig(cwd)) {
|
|
152
152
|
p.log.warn(
|
|
@@ -165,17 +165,21 @@ async function runInit(options) {
|
|
|
165
165
|
);
|
|
166
166
|
process.exit(1);
|
|
167
167
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
p.
|
|
177
|
-
|
|
168
|
+
if (!skipDetection) {
|
|
169
|
+
const isUT = await isUltimateTemplate(cwd);
|
|
170
|
+
s.stop(isUT ? "Ultimate Template project detected." : "Project detected.");
|
|
171
|
+
if (!isUT) {
|
|
172
|
+
const proceed = await p.confirm({
|
|
173
|
+
message: "This project does not appear to be an ImpulseLab Ultimate Template. Continue anyway?",
|
|
174
|
+
initialValue: false
|
|
175
|
+
});
|
|
176
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
177
|
+
p.cancel("Initialization cancelled.");
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
178
180
|
}
|
|
181
|
+
} else {
|
|
182
|
+
s.stop("Project ready.");
|
|
179
183
|
}
|
|
180
184
|
const projectName = await detectProjectName(cwd);
|
|
181
185
|
const srcExists = await pathExists3(path2.join(cwd, "src"));
|
|
@@ -197,6 +201,7 @@ async function runInit(options) {
|
|
|
197
201
|
// src/commands/add.ts
|
|
198
202
|
import { execSync as execSync2, execFileSync } from "child_process";
|
|
199
203
|
import { existsSync } from "fs";
|
|
204
|
+
import { readFile as readFile7, writeFile, unlink } from "fs/promises";
|
|
200
205
|
import path12 from "path";
|
|
201
206
|
import * as p5 from "@clack/prompts";
|
|
202
207
|
|
|
@@ -271,7 +276,8 @@ var registryConstants = {
|
|
|
271
276
|
githubBranch: "main",
|
|
272
277
|
modulesDir: "modules",
|
|
273
278
|
subModulesDirName: "sub-modules",
|
|
274
|
-
manifestFileName: "module.json"
|
|
279
|
+
manifestFileName: "module.json",
|
|
280
|
+
presetsDir: "presets"
|
|
275
281
|
};
|
|
276
282
|
|
|
277
283
|
// src/registry/github-urls.ts
|
|
@@ -442,10 +448,10 @@ async function listAvailableModules(localPath) {
|
|
|
442
448
|
// src/registry/fetch-module-file.ts
|
|
443
449
|
async function fetchModuleFile(moduleName, fileSrc, localPath) {
|
|
444
450
|
if (localPath) {
|
|
445
|
-
const { readFile:
|
|
451
|
+
const { readFile: readFile9 } = await import("fs/promises");
|
|
446
452
|
const { join } = await import("path");
|
|
447
453
|
const file = join(localPath, moduleName, fileSrc);
|
|
448
|
-
return
|
|
454
|
+
return readFile9(file, "utf-8");
|
|
449
455
|
}
|
|
450
456
|
const url = githubUrls.rawFile(moduleName, fileSrc);
|
|
451
457
|
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
@@ -782,6 +788,7 @@ function installNpmDeps(deps, cwd, dryRun) {
|
|
|
782
788
|
async function installModule(moduleId, manifest, cwd, dryRun, installedModules, localPath) {
|
|
783
789
|
p5.log.step(`Installing ${moduleId}@${manifest.version}...`);
|
|
784
790
|
const installedDests = [];
|
|
791
|
+
const createdDests = [];
|
|
785
792
|
if (manifest.files.length > 0) {
|
|
786
793
|
const installed = await installFiles({
|
|
787
794
|
moduleName: moduleId,
|
|
@@ -797,13 +804,34 @@ async function installModule(moduleId, manifest, cwd, dryRun, installedModules,
|
|
|
797
804
|
continue;
|
|
798
805
|
}
|
|
799
806
|
installedDests.push(f.dest);
|
|
807
|
+
if (f.action === "created" || f.action === "would-create") {
|
|
808
|
+
createdDests.push(f.dest);
|
|
809
|
+
}
|
|
800
810
|
const icon = f.action === "created" || f.action === "would-create" ? "+" : f.action === "overwritten" || f.action === "would-overwrite" ? "~" : "=";
|
|
801
811
|
p5.log.message(` ${icon} ${f.dest}`);
|
|
802
812
|
}
|
|
803
813
|
}
|
|
804
814
|
for (const transform of manifest.transforms) {
|
|
805
815
|
p5.log.step(` transform: ${transform.type} \u2192 ${transform.target}`);
|
|
806
|
-
|
|
816
|
+
try {
|
|
817
|
+
await runTransform(transform, cwd, dryRun);
|
|
818
|
+
} catch (err) {
|
|
819
|
+
p5.log.warn(
|
|
820
|
+
`Transform "${transform.type} \u2192 ${transform.target}" failed for module "${moduleId}": ${err instanceof Error ? err.message : String(err)}`
|
|
821
|
+
);
|
|
822
|
+
if (!dryRun && createdDests.length > 0) {
|
|
823
|
+
p5.log.warn(`Rolling back ${createdDests.length} file(s) created by "${moduleId}"...`);
|
|
824
|
+
for (const dest of createdDests) {
|
|
825
|
+
try {
|
|
826
|
+
await unlink(path12.join(cwd, dest));
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
throw new Error(
|
|
832
|
+
`Module "${moduleId}" install failed during transform "${transform.type} \u2192 ${transform.target}".`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
807
835
|
}
|
|
808
836
|
return installedDests;
|
|
809
837
|
}
|
|
@@ -829,7 +857,10 @@ async function pickModulesInteractively(localPath) {
|
|
|
829
857
|
modules = await listAvailableModules(localPath);
|
|
830
858
|
} catch (err) {
|
|
831
859
|
s.stop("Failed to load modules.");
|
|
832
|
-
|
|
860
|
+
p5.cancel(
|
|
861
|
+
`Could not load module list: ${err instanceof Error ? err.message : String(err)}`
|
|
862
|
+
);
|
|
863
|
+
process.exit(1);
|
|
833
864
|
}
|
|
834
865
|
s.stop("Modules loaded.");
|
|
835
866
|
if (modules.length === 0) {
|
|
@@ -847,7 +878,7 @@ async function pickModulesInteractively(localPath) {
|
|
|
847
878
|
options.push({
|
|
848
879
|
value: `${mod.name}/${sub}`,
|
|
849
880
|
label: ` \u21B3 ${sub}`,
|
|
850
|
-
hint: `
|
|
881
|
+
hint: `automatically includes ${mod.name}`
|
|
851
882
|
});
|
|
852
883
|
}
|
|
853
884
|
}
|
|
@@ -861,20 +892,25 @@ async function pickModulesInteractively(localPath) {
|
|
|
861
892
|
process.exit(0);
|
|
862
893
|
}
|
|
863
894
|
const result = new Set(selected);
|
|
895
|
+
const autoIncluded = [];
|
|
864
896
|
for (const id of selected) {
|
|
865
897
|
const { parent, child } = parseModuleId(id);
|
|
866
|
-
if (child !== null) {
|
|
898
|
+
if (child !== null && !selected.includes(parent)) {
|
|
867
899
|
result.add(parent);
|
|
900
|
+
autoIncluded.push(parent);
|
|
868
901
|
}
|
|
869
902
|
}
|
|
903
|
+
if (autoIncluded.length > 0) {
|
|
904
|
+
p5.log.info(`Auto-included parent modules: ${autoIncluded.join(", ")}`);
|
|
905
|
+
}
|
|
870
906
|
return [...result];
|
|
871
907
|
}
|
|
872
908
|
async function runAdd(options) {
|
|
873
|
-
let { moduleNames, cwd, dryRun, localPath, withSubModules = [], allowScripts = false } = options;
|
|
909
|
+
let { moduleNames, cwd, dryRun, localPath, withSubModules = [], allowScripts = false, skipIntro = false, skipAuth = false } = options;
|
|
874
910
|
let allTargets;
|
|
875
911
|
if (moduleNames.length === 0) {
|
|
876
|
-
p5.intro(`impulse add${dryRun ? " [dry-run]" : ""}`);
|
|
877
|
-
await requireAuth();
|
|
912
|
+
if (!skipIntro) p5.intro(`impulse add${dryRun ? " [dry-run]" : ""}`);
|
|
913
|
+
if (!skipAuth) await requireAuth();
|
|
878
914
|
if (withSubModules.length > 0) {
|
|
879
915
|
p5.log.warn(`--with is ignored in interactive mode. Select sub-modules from the picker.`);
|
|
880
916
|
withSubModules = [];
|
|
@@ -893,8 +929,8 @@ async function runAdd(options) {
|
|
|
893
929
|
}
|
|
894
930
|
const withIds = moduleNames.length === 1 && withSubModules.length > 0 ? withSubModules.map((sub) => `${moduleNames[0]}/${sub}`) : [];
|
|
895
931
|
allTargets = [...moduleNames, ...withIds];
|
|
896
|
-
p5.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
|
|
897
|
-
await requireAuth();
|
|
932
|
+
if (!skipIntro) p5.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
|
|
933
|
+
if (!skipAuth) await requireAuth();
|
|
898
934
|
}
|
|
899
935
|
const config = await readConfig(cwd);
|
|
900
936
|
if (!config) {
|
|
@@ -924,7 +960,14 @@ async function runAdd(options) {
|
|
|
924
960
|
}
|
|
925
961
|
}
|
|
926
962
|
if (moduleNames.length === 1 && withSubModules.length > 0) {
|
|
927
|
-
|
|
963
|
+
let parentManifest = null;
|
|
964
|
+
try {
|
|
965
|
+
parentManifest = await getManifest(moduleNames[0], localPath, manifests);
|
|
966
|
+
} catch (err) {
|
|
967
|
+
p5.log.warn(
|
|
968
|
+
`Could not fetch manifest for "${moduleNames[0]}" to validate --with sub-modules: ${err instanceof Error ? err.message : String(err)}. Skipping validation.`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
928
971
|
if (parentManifest) {
|
|
929
972
|
if (parentManifest.subModules.length === 0) {
|
|
930
973
|
p5.cancel(`"${moduleNames[0]}" has no declared sub-modules.`);
|
|
@@ -1090,13 +1133,26 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
1090
1133
|
runHooks = confirmed;
|
|
1091
1134
|
}
|
|
1092
1135
|
if (runHooks) {
|
|
1136
|
+
const hookFailures = [];
|
|
1093
1137
|
for (const { name, hooks } of allPostInstallHooks) {
|
|
1094
1138
|
p5.log.step(`Running post-install hooks for ${name}...`);
|
|
1095
1139
|
for (const hook of hooks) {
|
|
1096
1140
|
p5.log.message(` $ ${hook}`);
|
|
1097
|
-
|
|
1141
|
+
try {
|
|
1142
|
+
execSync2(hook, { cwd, stdio: "inherit" });
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
const msg = `Post-install hook failed for "${name}" ($ ${hook}): ${err instanceof Error ? err.message : String(err)}`;
|
|
1145
|
+
p5.log.warn(`${msg}. Continuing.`);
|
|
1146
|
+
hookFailures.push(msg);
|
|
1147
|
+
}
|
|
1098
1148
|
}
|
|
1099
1149
|
}
|
|
1150
|
+
if (hookFailures.length > 0) {
|
|
1151
|
+
p5.log.warn(
|
|
1152
|
+
`${hookFailures.length} post-install hook(s) failed. See warnings above.`
|
|
1153
|
+
);
|
|
1154
|
+
process.exitCode = 1;
|
|
1155
|
+
}
|
|
1100
1156
|
} else {
|
|
1101
1157
|
p5.log.warn("Post-install hooks skipped.");
|
|
1102
1158
|
}
|
|
@@ -1114,7 +1170,28 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
1114
1170
|
const dests = installedFilesMap.get(targetId) ?? [];
|
|
1115
1171
|
if (targetManifest) recordModule(config, targetId, targetManifest, dests, now);
|
|
1116
1172
|
}
|
|
1117
|
-
|
|
1173
|
+
const cfgPath = configPath(cwd);
|
|
1174
|
+
let backup = null;
|
|
1175
|
+
try {
|
|
1176
|
+
backup = await readFile7(cfgPath, "utf-8");
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
await writeConfig(config, cwd);
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
p5.log.warn(
|
|
1183
|
+
`Failed to write .impulse.json: ${err instanceof Error ? err.message : String(err)}`
|
|
1184
|
+
);
|
|
1185
|
+
if (backup !== null) {
|
|
1186
|
+
try {
|
|
1187
|
+
await writeFile(cfgPath, backup, "utf-8");
|
|
1188
|
+
p5.log.warn("Config restored to previous state.");
|
|
1189
|
+
} catch {
|
|
1190
|
+
p5.log.warn("Could not restore config backup \u2014 .impulse.json may be in a partial state.");
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
throw err;
|
|
1194
|
+
}
|
|
1118
1195
|
}
|
|
1119
1196
|
const label = allTargets.length === 1 ? `"${allTargets[0]}"` : allTargets.map((t) => `"${t}"`).join(", ");
|
|
1120
1197
|
p5.outro(
|
|
@@ -1214,11 +1291,20 @@ import { execFileSync as execFileSync2 } from "child_process";
|
|
|
1214
1291
|
var IMPULSE_BASE_URL = process.env.IMPULSE_BASE_URL ?? "https://impulselab.ai";
|
|
1215
1292
|
var CLIENT_ID = "impulse-cli";
|
|
1216
1293
|
async function requestDeviceCode() {
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1294
|
+
const url = `${IMPULSE_BASE_URL}/api/auth/device/code`;
|
|
1295
|
+
let res;
|
|
1296
|
+
try {
|
|
1297
|
+
res = await fetch(url, {
|
|
1298
|
+
method: "POST",
|
|
1299
|
+
headers: { "Content-Type": "application/json" },
|
|
1300
|
+
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
1301
|
+
});
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1304
|
+
throw new Error(
|
|
1305
|
+
`Network error calling ${url}: ${reason}. Check connection, VPN, proxy, and IMPULSE_BASE_URL (must match your deployed agency site).`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1222
1308
|
if (!res.ok) {
|
|
1223
1309
|
throw new Error(`Failed to initiate device flow: ${res.status} ${res.statusText}`);
|
|
1224
1310
|
}
|
|
@@ -1275,6 +1361,25 @@ function openBrowser(url) {
|
|
|
1275
1361
|
} catch {
|
|
1276
1362
|
}
|
|
1277
1363
|
}
|
|
1364
|
+
function copyToClipboard(text3) {
|
|
1365
|
+
try {
|
|
1366
|
+
const platform = process.platform;
|
|
1367
|
+
if (platform === "darwin") {
|
|
1368
|
+
execFileSync2("pbcopy", { input: text3 });
|
|
1369
|
+
} else if (platform === "win32") {
|
|
1370
|
+
execFileSync2("clip", { input: text3 });
|
|
1371
|
+
} else {
|
|
1372
|
+
try {
|
|
1373
|
+
execFileSync2("xclip", ["-selection", "clipboard"], { input: text3 });
|
|
1374
|
+
} catch {
|
|
1375
|
+
execFileSync2("xsel", ["--clipboard", "--input"], { input: text3 });
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return true;
|
|
1379
|
+
} catch {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1278
1383
|
function assertDeviceCodeResponse(data) {
|
|
1279
1384
|
if (typeof data !== "object" || data === null || typeof data.device_code !== "string" || typeof data.user_code !== "string" || typeof data.verification_uri !== "string") {
|
|
1280
1385
|
throw new Error("Invalid device code response from server");
|
|
@@ -1345,9 +1450,13 @@ async function runLogin() {
|
|
|
1345
1450
|
Visit the following URL to authenticate:
|
|
1346
1451
|
${browserUrl}
|
|
1347
1452
|
`);
|
|
1348
|
-
|
|
1349
|
-
|
|
1453
|
+
const rawCode = deviceCode.userCode;
|
|
1454
|
+
const mid = Math.ceil(rawCode.length / 2);
|
|
1455
|
+
const formattedCode = rawCode.length >= 6 ? `${rawCode.slice(0, mid)}-${rawCode.slice(mid)}` : rawCode;
|
|
1350
1456
|
openBrowser(browserUrl);
|
|
1457
|
+
const copied = copyToClipboard(rawCode);
|
|
1458
|
+
p7.log.message(`Your one-time code: ${formattedCode}${copied ? " (copied to clipboard)" : ""}
|
|
1459
|
+
`);
|
|
1351
1460
|
p7.log.info(`If the browser did not open, copy the URL above.
|
|
1352
1461
|
Base URL: ${IMPULSE_BASE_URL}`);
|
|
1353
1462
|
const pollInterval = Math.max(
|
|
@@ -1457,6 +1566,400 @@ async function runWhoami() {
|
|
|
1457
1566
|
p9.outro("Done.");
|
|
1458
1567
|
}
|
|
1459
1568
|
|
|
1569
|
+
// src/commands/create.ts
|
|
1570
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1571
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1572
|
+
import path15 from "path";
|
|
1573
|
+
import * as p10 from "@clack/prompts";
|
|
1574
|
+
import fsExtra18 from "fs-extra";
|
|
1575
|
+
|
|
1576
|
+
// src/registry/fetch-template.ts
|
|
1577
|
+
import fsExtra16 from "fs-extra";
|
|
1578
|
+
import path13 from "path";
|
|
1579
|
+
|
|
1580
|
+
// src/schemas/template-manifest.ts
|
|
1581
|
+
import { z as z7 } from "zod";
|
|
1582
|
+
var TemplateFileSchema = z7.object({
|
|
1583
|
+
src: z7.string(),
|
|
1584
|
+
dest: z7.string()
|
|
1585
|
+
});
|
|
1586
|
+
var TemplateManifestSchema = z7.object({
|
|
1587
|
+
name: z7.string(),
|
|
1588
|
+
description: z7.string(),
|
|
1589
|
+
/** Human-readable labels describing the tech stack (e.g. ["Next.js 15", "Tailwind CSS v4"]). */
|
|
1590
|
+
stack: z7.array(z7.string()).default([]),
|
|
1591
|
+
/** Module IDs pre-selected in the interactive picker (e.g. ["db", "auth"]). */
|
|
1592
|
+
defaultModules: z7.array(z7.string()).default([]),
|
|
1593
|
+
/** Files to copy from the template directory into the new project. */
|
|
1594
|
+
files: z7.array(TemplateFileSchema).default([])
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
// src/registry/fetch-template.ts
|
|
1598
|
+
var { readJson: readJson6, pathExists: pathExists14, readFile: readFile8 } = fsExtra16;
|
|
1599
|
+
var TEMPLATES_DIR = "templates";
|
|
1600
|
+
var TEMPLATE_MANIFEST_FILE = "template.json";
|
|
1601
|
+
function templateRawUrl(templateId, file) {
|
|
1602
|
+
return `https://raw.githubusercontent.com/${registryConstants.githubOrg}/${registryConstants.githubRepo}/${registryConstants.githubBranch}/${TEMPLATES_DIR}/${templateId}/${file}`;
|
|
1603
|
+
}
|
|
1604
|
+
async function fetchTemplateManifest(templateId, localPath) {
|
|
1605
|
+
if (localPath) {
|
|
1606
|
+
const file = path13.join(localPath, TEMPLATES_DIR, templateId, TEMPLATE_MANIFEST_FILE);
|
|
1607
|
+
if (!await pathExists14(file)) {
|
|
1608
|
+
throw new Error(`Local template not found: ${file}`);
|
|
1609
|
+
}
|
|
1610
|
+
const raw2 = await readJson6(file);
|
|
1611
|
+
const parsed2 = TemplateManifestSchema.safeParse(raw2);
|
|
1612
|
+
if (!parsed2.success) {
|
|
1613
|
+
throw new Error(`Invalid template.json for "${templateId}": ${parsed2.error.message}`);
|
|
1614
|
+
}
|
|
1615
|
+
return parsed2.data;
|
|
1616
|
+
}
|
|
1617
|
+
const url = templateRawUrl(templateId, TEMPLATE_MANIFEST_FILE);
|
|
1618
|
+
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
1619
|
+
if (!res.ok) {
|
|
1620
|
+
if (res.status === 404) {
|
|
1621
|
+
throw new Error(`Template not found in registry: ${templateId}`);
|
|
1622
|
+
}
|
|
1623
|
+
throw new Error(`Failed to fetch template manifest: ${res.status} ${res.statusText}`);
|
|
1624
|
+
}
|
|
1625
|
+
const raw = await res.json();
|
|
1626
|
+
const parsed = TemplateManifestSchema.safeParse(raw);
|
|
1627
|
+
if (!parsed.success) {
|
|
1628
|
+
throw new Error(`Invalid template.json for "${templateId}": ${parsed.error.message}`);
|
|
1629
|
+
}
|
|
1630
|
+
return parsed.data;
|
|
1631
|
+
}
|
|
1632
|
+
async function fetchTemplateFile(templateId, src, localPath) {
|
|
1633
|
+
if (localPath) {
|
|
1634
|
+
const file = path13.join(localPath, TEMPLATES_DIR, templateId, src);
|
|
1635
|
+
return readFile8(file, "utf-8");
|
|
1636
|
+
}
|
|
1637
|
+
const url = templateRawUrl(templateId, src);
|
|
1638
|
+
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
1639
|
+
if (!res.ok) {
|
|
1640
|
+
throw new Error(`Failed to fetch template file "${src}": ${res.status} ${res.statusText}`);
|
|
1641
|
+
}
|
|
1642
|
+
return res.text();
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/registry/fetch-preset.ts
|
|
1646
|
+
import fsExtra17 from "fs-extra";
|
|
1647
|
+
import path14 from "path";
|
|
1648
|
+
|
|
1649
|
+
// src/schemas/preset-manifest.ts
|
|
1650
|
+
import { z as z8 } from "zod";
|
|
1651
|
+
var PresetManifestSchema = z8.object({
|
|
1652
|
+
name: z8.string(),
|
|
1653
|
+
description: z8.string(),
|
|
1654
|
+
/** Template ID to scaffold from (references templates/<name>/). */
|
|
1655
|
+
template: z8.string(),
|
|
1656
|
+
/** Modules installed automatically — no user confirmation needed. */
|
|
1657
|
+
defaultModules: z8.array(z8.string()),
|
|
1658
|
+
/** Modules the user can optionally add during create. */
|
|
1659
|
+
optionalModules: z8.array(z8.string()).default([])
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// src/registry/fetch-preset.ts
|
|
1663
|
+
var { readJson: readJson7, pathExists: pathExists15 } = fsExtra17;
|
|
1664
|
+
var PRESETS_DIR = registryConstants.presetsDir;
|
|
1665
|
+
var PRESET_MANIFEST_FILE = "preset.json";
|
|
1666
|
+
function presetRawUrl(presetId, file) {
|
|
1667
|
+
return `https://raw.githubusercontent.com/${registryConstants.githubOrg}/${registryConstants.githubRepo}/${registryConstants.githubBranch}/${PRESETS_DIR}/${presetId}/${file}`;
|
|
1668
|
+
}
|
|
1669
|
+
async function fetchPresetManifest(presetId, localPath) {
|
|
1670
|
+
if (localPath) {
|
|
1671
|
+
const file = path14.join(localPath, PRESETS_DIR, presetId, PRESET_MANIFEST_FILE);
|
|
1672
|
+
if (!await pathExists15(file)) {
|
|
1673
|
+
throw new Error(`Local preset not found: ${file}`);
|
|
1674
|
+
}
|
|
1675
|
+
const raw2 = await readJson7(file);
|
|
1676
|
+
const parsed2 = PresetManifestSchema.safeParse(raw2);
|
|
1677
|
+
if (!parsed2.success) {
|
|
1678
|
+
throw new Error(`Invalid preset.json for "${presetId}": ${parsed2.error.message}`);
|
|
1679
|
+
}
|
|
1680
|
+
return parsed2.data;
|
|
1681
|
+
}
|
|
1682
|
+
const url = presetRawUrl(presetId, PRESET_MANIFEST_FILE);
|
|
1683
|
+
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
1684
|
+
if (!res.ok) {
|
|
1685
|
+
if (res.status === 404) {
|
|
1686
|
+
throw new Error(`Preset not found in registry: ${presetId}`);
|
|
1687
|
+
}
|
|
1688
|
+
throw new Error(`Failed to fetch preset manifest: ${res.status} ${res.statusText}`);
|
|
1689
|
+
}
|
|
1690
|
+
const raw = await res.json();
|
|
1691
|
+
const parsed = PresetManifestSchema.safeParse(raw);
|
|
1692
|
+
if (!parsed.success) {
|
|
1693
|
+
throw new Error(`Invalid preset.json for "${presetId}": ${parsed.error.message}`);
|
|
1694
|
+
}
|
|
1695
|
+
return parsed.data;
|
|
1696
|
+
}
|
|
1697
|
+
async function listAvailablePresets(localPath) {
|
|
1698
|
+
if (localPath) {
|
|
1699
|
+
const presetsDir = path14.join(localPath, PRESETS_DIR);
|
|
1700
|
+
if (!await pathExists15(presetsDir)) return [];
|
|
1701
|
+
const { readdir } = await import("fs/promises");
|
|
1702
|
+
const entries2 = await readdir(presetsDir, { withFileTypes: true });
|
|
1703
|
+
return entries2.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1704
|
+
}
|
|
1705
|
+
const url = `https://api.github.com/repos/${registryConstants.githubOrg}/${registryConstants.githubRepo}/contents/${PRESETS_DIR}`;
|
|
1706
|
+
const res = await fetch(url, { headers: getGitHubHeaders({ Accept: "application/vnd.github.v3+json" }) });
|
|
1707
|
+
if (!res.ok) return ["saas"];
|
|
1708
|
+
const entries = await res.json();
|
|
1709
|
+
if (!Array.isArray(entries)) return ["saas"];
|
|
1710
|
+
return entries.filter((e) => e.type === "dir").map((e) => e.name);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/commands/create.ts
|
|
1714
|
+
var { outputFile: outputFile7, pathExists: pathExists16 } = fsExtra18;
|
|
1715
|
+
function slugify(name) {
|
|
1716
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1717
|
+
}
|
|
1718
|
+
function isValidProjectName(name) {
|
|
1719
|
+
return /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name);
|
|
1720
|
+
}
|
|
1721
|
+
async function runCreate(options) {
|
|
1722
|
+
const { cwd, localPath } = options;
|
|
1723
|
+
p10.intro("impulse create");
|
|
1724
|
+
await requireAuth();
|
|
1725
|
+
let projectName = options.projectName;
|
|
1726
|
+
if (!projectName) {
|
|
1727
|
+
const answer = await p10.text({
|
|
1728
|
+
message: "Project name:",
|
|
1729
|
+
placeholder: "my-app",
|
|
1730
|
+
validate(value) {
|
|
1731
|
+
if (!value?.trim()) return "Project name is required.";
|
|
1732
|
+
const slug = slugify(value.trim());
|
|
1733
|
+
if (!isValidProjectName(slug)) {
|
|
1734
|
+
return "Use lowercase letters, numbers, and hyphens only (e.g. my-app).";
|
|
1735
|
+
}
|
|
1736
|
+
return void 0;
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
if (p10.isCancel(answer)) {
|
|
1740
|
+
p10.cancel("Cancelled.");
|
|
1741
|
+
process.exit(0);
|
|
1742
|
+
}
|
|
1743
|
+
projectName = slugify(answer.trim());
|
|
1744
|
+
} else {
|
|
1745
|
+
projectName = slugify(projectName.trim());
|
|
1746
|
+
}
|
|
1747
|
+
const targetDir = path15.resolve(cwd, projectName);
|
|
1748
|
+
if (existsSync2(targetDir)) {
|
|
1749
|
+
p10.cancel(`Directory "${projectName}" already exists.`);
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
}
|
|
1752
|
+
let templateId;
|
|
1753
|
+
let selectedModules = options.modules ?? [];
|
|
1754
|
+
if (options.template) {
|
|
1755
|
+
templateId = options.template;
|
|
1756
|
+
if (!options.modules) {
|
|
1757
|
+
const s = p10.spinner();
|
|
1758
|
+
s.start("Loading available modules...");
|
|
1759
|
+
let availableModules;
|
|
1760
|
+
try {
|
|
1761
|
+
availableModules = await listAvailableModules(localPath);
|
|
1762
|
+
} catch {
|
|
1763
|
+
availableModules = [];
|
|
1764
|
+
}
|
|
1765
|
+
s.stop("Modules loaded.");
|
|
1766
|
+
if (availableModules.length > 0) {
|
|
1767
|
+
const templateManifest = await fetchTemplateManifest(templateId, localPath).catch(() => null);
|
|
1768
|
+
const defaultValues = templateManifest ? templateManifest.defaultModules.filter((d) => availableModules.some((m) => m.name === d)) : [];
|
|
1769
|
+
const pickerOptions = availableModules.map((m) => ({
|
|
1770
|
+
value: m.name,
|
|
1771
|
+
label: m.name,
|
|
1772
|
+
...m.description !== void 0 ? { hint: m.description } : {}
|
|
1773
|
+
}));
|
|
1774
|
+
const picked = await p10.multiselect({
|
|
1775
|
+
message: "Select modules to install (space to toggle, enter to confirm):",
|
|
1776
|
+
options: pickerOptions,
|
|
1777
|
+
initialValues: defaultValues,
|
|
1778
|
+
required: false
|
|
1779
|
+
});
|
|
1780
|
+
if (p10.isCancel(picked)) {
|
|
1781
|
+
p10.cancel("Cancelled.");
|
|
1782
|
+
process.exit(0);
|
|
1783
|
+
}
|
|
1784
|
+
selectedModules = picked;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
} else {
|
|
1788
|
+
let presetManifest;
|
|
1789
|
+
if (options.preset) {
|
|
1790
|
+
const s = p10.spinner();
|
|
1791
|
+
s.start(`Fetching preset "${options.preset}"...`);
|
|
1792
|
+
try {
|
|
1793
|
+
presetManifest = await fetchPresetManifest(options.preset, localPath);
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
s.stop("Failed to fetch preset.");
|
|
1796
|
+
p10.cancel(err instanceof Error ? err.message : String(err));
|
|
1797
|
+
process.exit(1);
|
|
1798
|
+
}
|
|
1799
|
+
s.stop(`Preset "${options.preset}" ready.`);
|
|
1800
|
+
} else {
|
|
1801
|
+
const s = p10.spinner();
|
|
1802
|
+
s.start("Loading available presets...");
|
|
1803
|
+
let presetIds;
|
|
1804
|
+
try {
|
|
1805
|
+
presetIds = await listAvailablePresets(localPath);
|
|
1806
|
+
} catch {
|
|
1807
|
+
presetIds = ["saas"];
|
|
1808
|
+
}
|
|
1809
|
+
s.stop("Presets loaded.");
|
|
1810
|
+
let selectedPresetId;
|
|
1811
|
+
if (presetIds.length > 1) {
|
|
1812
|
+
const presetDetails = await Promise.all(
|
|
1813
|
+
presetIds.map(async (id) => {
|
|
1814
|
+
try {
|
|
1815
|
+
const m = await fetchPresetManifest(id, localPath);
|
|
1816
|
+
return { id, description: m.description };
|
|
1817
|
+
} catch {
|
|
1818
|
+
return { id, description: "" };
|
|
1819
|
+
}
|
|
1820
|
+
})
|
|
1821
|
+
);
|
|
1822
|
+
const selected = await p10.select({
|
|
1823
|
+
message: "Select a preset:",
|
|
1824
|
+
options: presetDetails.map(({ id, description }) => ({
|
|
1825
|
+
value: id,
|
|
1826
|
+
label: id,
|
|
1827
|
+
...description ? { hint: description } : {}
|
|
1828
|
+
})),
|
|
1829
|
+
initialValue: "saas"
|
|
1830
|
+
});
|
|
1831
|
+
if (p10.isCancel(selected)) {
|
|
1832
|
+
p10.cancel("Cancelled.");
|
|
1833
|
+
process.exit(0);
|
|
1834
|
+
}
|
|
1835
|
+
selectedPresetId = selected;
|
|
1836
|
+
} else {
|
|
1837
|
+
selectedPresetId = presetIds[0] ?? "saas";
|
|
1838
|
+
p10.log.info(`Using preset: ${selectedPresetId}`);
|
|
1839
|
+
}
|
|
1840
|
+
const s22 = p10.spinner();
|
|
1841
|
+
s22.start(`Fetching preset "${selectedPresetId}"...`);
|
|
1842
|
+
try {
|
|
1843
|
+
presetManifest = await fetchPresetManifest(selectedPresetId, localPath);
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
s22.stop("Failed to fetch preset.");
|
|
1846
|
+
p10.cancel(err instanceof Error ? err.message : String(err));
|
|
1847
|
+
process.exit(1);
|
|
1848
|
+
}
|
|
1849
|
+
s22.stop(`Preset "${selectedPresetId}" ready.`);
|
|
1850
|
+
if (!options.modules && presetManifest.optionalModules.length > 0) {
|
|
1851
|
+
const optionalSelected = await p10.multiselect({
|
|
1852
|
+
message: `Optional modules for "${selectedPresetId}" (space to toggle, enter to skip):`,
|
|
1853
|
+
options: presetManifest.optionalModules.map((id) => ({
|
|
1854
|
+
value: id,
|
|
1855
|
+
label: id
|
|
1856
|
+
})),
|
|
1857
|
+
required: false
|
|
1858
|
+
});
|
|
1859
|
+
if (p10.isCancel(optionalSelected)) {
|
|
1860
|
+
p10.cancel("Cancelled.");
|
|
1861
|
+
process.exit(0);
|
|
1862
|
+
}
|
|
1863
|
+
selectedModules = [
|
|
1864
|
+
...presetManifest.defaultModules,
|
|
1865
|
+
...optionalSelected
|
|
1866
|
+
];
|
|
1867
|
+
} else if (!options.modules) {
|
|
1868
|
+
selectedModules = presetManifest.defaultModules;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
templateId = presetManifest.template;
|
|
1872
|
+
if (options.preset && !options.modules) {
|
|
1873
|
+
selectedModules = presetManifest.defaultModules;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
const s2 = p10.spinner();
|
|
1877
|
+
s2.start(`Fetching template "${templateId}"...`);
|
|
1878
|
+
let manifest;
|
|
1879
|
+
try {
|
|
1880
|
+
manifest = await fetchTemplateManifest(templateId, localPath);
|
|
1881
|
+
} catch (err) {
|
|
1882
|
+
s2.stop("Failed to fetch template.");
|
|
1883
|
+
p10.cancel(err instanceof Error ? err.message : String(err));
|
|
1884
|
+
process.exit(1);
|
|
1885
|
+
}
|
|
1886
|
+
s2.stop(`Template "${templateId}" ready.`);
|
|
1887
|
+
p10.log.message(`
|
|
1888
|
+
Stack: ${manifest.stack.join(", ")}`);
|
|
1889
|
+
const s4 = p10.spinner();
|
|
1890
|
+
s4.start(`Scaffolding "${projectName}"...`);
|
|
1891
|
+
try {
|
|
1892
|
+
for (const file of manifest.files) {
|
|
1893
|
+
const content = await fetchTemplateFile(templateId, file.src, localPath);
|
|
1894
|
+
const destPath = path15.join(targetDir, file.dest);
|
|
1895
|
+
await outputFile7(destPath, content, "utf-8");
|
|
1896
|
+
}
|
|
1897
|
+
const pkgPath = path15.join(targetDir, "package.json");
|
|
1898
|
+
if (existsSync2(pkgPath)) {
|
|
1899
|
+
const { readJson: readJson8, writeJson: writeJson2 } = fsExtra18;
|
|
1900
|
+
const pkg = await readJson8(pkgPath);
|
|
1901
|
+
if (pkg && typeof pkg === "object") {
|
|
1902
|
+
pkg["name"] = projectName;
|
|
1903
|
+
await writeJson2(pkgPath, pkg, { spaces: 2 });
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
s4.stop("Scaffold failed.");
|
|
1908
|
+
p10.cancel(err instanceof Error ? err.message : String(err));
|
|
1909
|
+
process.exit(1);
|
|
1910
|
+
}
|
|
1911
|
+
s4.stop(`Project scaffolded at ./${projectName}`);
|
|
1912
|
+
p10.log.step("Initializing impulse...");
|
|
1913
|
+
await runInit({ cwd: targetDir, force: false, skipDetection: true });
|
|
1914
|
+
if (selectedModules.length > 0) {
|
|
1915
|
+
p10.log.step(`Installing modules: ${selectedModules.join(", ")}`);
|
|
1916
|
+
await runAdd({
|
|
1917
|
+
moduleNames: selectedModules,
|
|
1918
|
+
cwd: targetDir,
|
|
1919
|
+
dryRun: false,
|
|
1920
|
+
...localPath !== void 0 ? { localPath } : {},
|
|
1921
|
+
allowScripts: false,
|
|
1922
|
+
skipIntro: true,
|
|
1923
|
+
skipAuth: true
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
const hasPnpmLock = await pathExists16(path15.join(targetDir, "pnpm-lock.yaml"));
|
|
1927
|
+
const pm = hasPnpmLock ? "pnpm" : existsSync2(path15.join(targetDir, "yarn.lock")) ? "yarn" : "pnpm";
|
|
1928
|
+
const s5 = p10.spinner();
|
|
1929
|
+
s5.start(`Running ${pm} install...`);
|
|
1930
|
+
try {
|
|
1931
|
+
execFileSync3(pm, ["install"], { cwd: targetDir, stdio: "pipe" });
|
|
1932
|
+
s5.stop("Dependencies installed.");
|
|
1933
|
+
} catch {
|
|
1934
|
+
s5.stop(`${pm} install failed \u2014 run it manually.`);
|
|
1935
|
+
p10.log.warn(`Run \`cd ${projectName} && ${pm} install\` to install dependencies.`);
|
|
1936
|
+
}
|
|
1937
|
+
const s6 = p10.spinner();
|
|
1938
|
+
s6.start("Initializing git repository...");
|
|
1939
|
+
try {
|
|
1940
|
+
execFileSync3("git", ["init"], { cwd: targetDir, stdio: "pipe" });
|
|
1941
|
+
execFileSync3("git", ["add", "-A"], { cwd: targetDir, stdio: "pipe" });
|
|
1942
|
+
execFileSync3(
|
|
1943
|
+
"git",
|
|
1944
|
+
["commit", "-m", `feat: initial project from impulse create (${templateId} template)`],
|
|
1945
|
+
{ cwd: targetDir, stdio: "pipe" }
|
|
1946
|
+
);
|
|
1947
|
+
s6.stop("Git repository initialized.");
|
|
1948
|
+
} catch {
|
|
1949
|
+
s6.stop("Git init failed \u2014 skipping.");
|
|
1950
|
+
p10.log.warn(`Run \`git init && git add -A && git commit -m "init"\` manually.`);
|
|
1951
|
+
}
|
|
1952
|
+
p10.outro(
|
|
1953
|
+
`
|
|
1954
|
+
Project "${projectName}" created successfully!
|
|
1955
|
+
|
|
1956
|
+
cd ${projectName}
|
|
1957
|
+
${pm} dev
|
|
1958
|
+
|
|
1959
|
+
Add more modules anytime with \`impulse add\`.`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1460
1963
|
// src/cli-version.ts
|
|
1461
1964
|
import { createRequire } from "module";
|
|
1462
1965
|
var require2 = createRequire(import.meta.url);
|
|
@@ -1501,6 +2004,19 @@ program.command("list").description("List available and installed modules").opti
|
|
|
1501
2004
|
if (options.local !== void 0) listOpts.localPath = options.local;
|
|
1502
2005
|
await runList(listOpts);
|
|
1503
2006
|
});
|
|
2007
|
+
program.command("create [name]").description(
|
|
2008
|
+
"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`"
|
|
2009
|
+
).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) => {
|
|
2010
|
+
const moduleList = options.modules !== void 0 ? options.modules.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
2011
|
+
await runCreate({
|
|
2012
|
+
cwd: process.cwd(),
|
|
2013
|
+
...name !== void 0 ? { projectName: name } : {},
|
|
2014
|
+
...options.preset !== void 0 ? { preset: options.preset } : {},
|
|
2015
|
+
...options.template !== void 0 ? { template: options.template } : {},
|
|
2016
|
+
...moduleList !== void 0 ? { modules: moduleList } : {},
|
|
2017
|
+
...options.local !== void 0 ? { localPath: options.local } : {}
|
|
2018
|
+
});
|
|
2019
|
+
});
|
|
1504
2020
|
program.command("login").description("Authenticate with ImpulseLab (opens browser for device flow)").action(async () => {
|
|
1505
2021
|
await runLogin();
|
|
1506
2022
|
});
|